@webpieces/nx-webpieces-rules 0.4.947 → 0.4.949

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +20 -11
  2. package/package.json +7 -7
  3. package/src/index.d.ts +1 -1
  4. package/src/index.js +3 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/graph-color-modes.d.ts +18 -12
  7. package/src/lib/graph-color-modes.js +18 -20
  8. package/src/lib/graph-color-modes.js.map +1 -1
  9. package/src/lib/graph-legend.d.ts +11 -3
  10. package/src/lib/graph-legend.js +25 -9
  11. package/src/lib/graph-legend.js.map +1 -1
  12. package/src/lib/graph-mode-state.client.d.ts +2 -1
  13. package/src/lib/graph-mode-state.client.js +3 -1
  14. package/src/lib/graph-mode-state.client.js.map +1 -1
  15. package/src/lib/graph-page-shell.js +2 -2
  16. package/src/lib/graph-page-shell.js.map +1 -1
  17. package/src/lib/graph-page-styles.js +4 -1
  18. package/src/lib/graph-page-styles.js.map +1 -1
  19. package/src/lib/graph-product-paint.client.d.ts +92 -0
  20. package/src/lib/graph-product-paint.client.js +196 -0
  21. package/src/lib/graph-product-paint.client.js.map +1 -0
  22. package/src/lib/graph-products.d.ts +54 -6
  23. package/src/lib/graph-products.js +62 -9
  24. package/src/lib/graph-products.js.map +1 -1
  25. package/src/lib/graph-render-model.d.ts +12 -12
  26. package/src/lib/graph-render-model.js +12 -14
  27. package/src/lib/graph-render-model.js.map +1 -1
  28. package/src/lib/graph-visualizer.client.d.ts +13 -1
  29. package/src/lib/graph-visualizer.client.js +27 -4
  30. package/src/lib/graph-visualizer.client.js.map +1 -1
  31. package/src/lib/graph-visualizer.d.ts +3 -3
  32. package/src/lib/graph-visualizer.js +13 -12
  33. package/src/lib/graph-visualizer.js.map +1 -1
  34. package/src/lib/runtime-goal-comparison.js +1 -1
  35. package/src/lib/runtime-goal-comparison.js.map +1 -1
  36. package/src/lib/runtime-html-page.js +3 -2
  37. package/src/lib/runtime-html-page.js.map +1 -1
  38. package/src/lib/runtime-layout.js +1 -1
  39. package/src/lib/runtime-layout.js.map +1 -1
  40. package/src/lib/runtime-viewer-adapter.d.ts +4 -1
  41. package/src/lib/runtime-viewer-adapter.js +6 -13
  42. package/src/lib/runtime-viewer-adapter.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"graph-page-styles.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-styles.ts"],"names":[],"mappings":";;;AAAA,2DAAoD;AAEpD;;;;GAIG;AACH,MAAa,eAAe;IACxB,GAAG;QACC,OAAO,CACH,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,MAAM,EAAE;YACb,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,aAAa,EAAE;YACpB,IAAI,CAAC,KAAK,EAAE;YACZ,IAAI,CAAC,KAAK,EAAE,CACf,CAAC;IACN,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;;;;;;;;;;uFAuBwE,CAAC;IACpF,CAAC;IAEO,MAAM;QACV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mJAqCoI,CAAC;IAChJ,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;;;;;;;;;;mGAsBoF,CAAC;IAChG,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;uDAcwC,CAAC;IACpD,CAAC;IAED,iFAAiF;IACzE,aAAa;QACjB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;kEA4BmD,CAAC;IAC/D,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;uFAawE,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,KAAK;QACT,MAAM,KAAK,GAAG,6BAA6B,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,KAAK,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,KAAK,QAAQ,KAAK,GAAG,CAAC;QACvC,MAAM,IAAI,GAAG,mGAAmG,CAAC;QACjH,OAAO;;;6CAG8B,KAAK;qCACb,iCAAa;qCACb,iCAAa,mCAAmC,IAAI;uCAClD,iCAAa,OAAO,KAAK;kCAC9B,iCAAa,wEAAwE,CAAC;IACpH,CAAC;IAEO,KAAK;QACT,OAAO;;;;;;;;;;sCAUuB,CAAC;IACnC,CAAC;CACJ;AA1ND,0CA0NC","sourcesContent":["import { STRIPED_CLASS } from './graph-color-modes';\n\n/**\n * The architecture page's stylesheet: the dark drawer, the graph filling the viewport, the floating\n * popovers and the responsibilities panel. Emitted AFTER the shared menu/filter/navigation styles so\n * its `#graph` rules (fill the viewport, no 75vh cap) win.\n */\nexport class GraphPageStyles {\n css(): string {\n return (\n this.base() +\n this.drawer() +\n this.controls() +\n this.main() +\n this.overlays() +\n this.filterPopover() +\n this.graph() +\n this.cards()\n );\n }\n\n private base(): string {\n return `\n :root {\n --wp-side-bg: #15161b; --wp-side-control: #1e1f25; --wp-side-fg: #e9eaf0; --wp-side-muted: #8b8fa0;\n --wp-side-hover: #26272f; --wp-side-line: #3a3c46; --wp-accent: #8b3cf0; --wp-accent-hover: #a46bf7;\n --wp-fg: #1b1e27; --wp-muted: #5d6475; --wp-line: #d9dce5; --wp-surface: #ffffff; --wp-canvas: #faf9f5;\n }\n html, body { height: 100%; }\n /* A class that sets display (the legend lists are grids) must not un-hide a [hidden] element. */\n [hidden] { display: none !important; }\n body { margin: 0; padding: 0; overflow: hidden; background: var(--wp-canvas); color: var(--wp-fg);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Arial, sans-serif; font-size: 14px; }\n kbd { font-family: ui-monospace, Menlo, monospace; font-size: 12px; border: 1px solid var(--wp-line);\n border-radius: 4px; padding: 0 5px; background: #f3f4f7; }\n .wp-shell { display: grid; grid-template-columns: 272px minmax(0, 1fr); height: 100vh; }\n .wp-shell.wp-collapsed { grid-template-columns: 0 minmax(0, 1fr); }\n .wp-shell.wp-collapsed .wp-side { visibility: hidden; }\n @media (max-width: 760px) {\n .wp-shell { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, auto) minmax(0, 1fr); }\n .wp-side { max-height: 42vh; }\n .wp-shell.wp-collapsed { grid-template-columns: minmax(0, 1fr); }\n .wp-shell.wp-collapsed .wp-side { display: none; }\n .wp-main { height: auto; min-height: 0; }\n }\n @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }`;\n }\n\n private drawer(): string {\n return `\n .wp-side { background: var(--wp-side-bg); color: var(--wp-side-fg); overflow-y: auto; overflow-x: hidden;\n display: flex; flex-direction: column; padding: 12px 0 10px; min-width: 0; font-size: 13px; }\n .wp-brand { display: flex; align-items: center; gap: 10px; padding: 0 16px 10px; font-weight: 600; font-size: 14px; }\n .wp-brand svg { flex: none; }\n .wp-group { padding: 8px 14px; border-top: 1px solid var(--wp-side-line); display: grid; gap: 6px; }\n .wp-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--wp-side-muted);\n font-weight: 600; padding-left: 4px; }\n .wp-label-row { display: flex; justify-content: space-between; align-items: center; }\n .wp-pulldown { position: relative; }\n .wp-pill-trigger { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; gap: 10px;\n width: 100%; padding: 7px 16px; border-radius: 999px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); }\n .wp-pill-trigger:hover, .wp-pill-trigger[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-pill-text { flex: 1 1 auto; min-width: 0; }\n .wp-caret { color: var(--wp-side-muted); }\n .wp-menu { position: absolute; z-index: 35; left: 0; right: 0; top: calc(100% + 6px); display: grid; gap: 2px; padding: 6px;\n background: var(--wp-side-control); border: 1px solid var(--wp-side-line); border-radius: 12px; box-shadow: 0 10px 28px #0008; }\n .wp-mode { all: unset; box-sizing: border-box; cursor: pointer; display: block; padding: 6px 10px; border-radius: 8px; }\n .wp-mode:hover { background: var(--wp-side-hover); }\n .wp-mode[aria-checked=\"true\"] { background: #2a2333; box-shadow: inset 3px 0 0 var(--wp-accent); }\n .wp-mode:disabled { cursor: not-allowed; opacity: 0.45; }\n .wp-mode-name { display: block; font-size: 14px; font-weight: 600; line-height: 1.3; }\n .wp-mode-sub { display: block; font-size: 12px; color: var(--wp-side-muted); line-height: 1.3; }\n .wp-filter-btn { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;\n align-items: center; padding: 7px 12px; border-radius: 8px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); font-size: 13px; font-weight: 600; }\n .wp-filter-btn:hover, .wp-filter-btn[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-badge { min-width: 18px; padding: 1px 6px; border-radius: 999px; background: var(--wp-accent); color: #fff;\n font-size: 11.5px; text-align: center; box-sizing: border-box; }\n .wp-mode-reason { margin: 2px 4px 0; font-size: 11.5px; line-height: 1.35; color: var(--wp-side-muted); }\n .wp-legend-list { display: grid; gap: 5px; padding-left: 4px; }\n .wp-legend-row { display: flex; align-items: center; gap: 10px; line-height: 1.25; }\n .wp-legend-row svg { flex: none; }\n .wp-legend-row small { display: block; color: var(--wp-side-muted); font-size: 11.5px; }\n .wp-impact-note { margin: 2px 0 0; font-size: 11.5px; color: var(--wp-side-muted); }\n .wp-impact-note:empty { display: none; }\n .wp-foot { margin-top: auto; padding: 10px 18px 0; border-top: 1px solid var(--wp-side-line); display: flex; gap: 14px; flex-wrap: wrap; }`;\n }\n\n private controls(): string {\n return `\n .wp-side input[type=\"search\"] { width: 100%; box-sizing: border-box; background: var(--wp-side-control); color: var(--wp-side-fg);\n border: 1px solid var(--wp-side-line); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }\n .wp-toggle { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;\n align-items: center; padding: 6px 10px; border-radius: 8px; font-size: 13px; }\n .wp-toggle:hover { background: var(--wp-side-hover); }\n .wp-toggle:disabled { cursor: not-allowed; opacity: 0.45; }\n .wp-sw { width: 30px; height: 18px; border-radius: 9px; background: #3a3d4a; position: relative; transition: background 0.15s; }\n .wp-sw::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%;\n background: #fff; transition: transform 0.15s; }\n .wp-toggle[aria-pressed=\"true\"] .wp-sw { background: var(--wp-accent); }\n .wp-toggle[aria-pressed=\"true\"] .wp-sw::after { transform: translateX(12px); }\n .wp-link { all: unset; cursor: pointer; color: var(--wp-side-muted); font-size: 12px; text-decoration: underline; }\n .wp-link:hover { color: var(--wp-side-fg); }\n .wp-icon { all: unset; box-sizing: border-box; cursor: pointer; display: inline-grid; place-items: center; flex: none;\n width: 34px; height: 34px; border-radius: 8px; background: var(--wp-surface); border: 1px solid var(--wp-line);\n color: var(--wp-fg); font-size: 15px; }\n .wp-icon:hover { border-color: var(--wp-accent); }\n .wp-icon-mini { width: 24px; height: 24px; font-size: 12px; }\n .wp-icon-dark { background: transparent; border-color: var(--wp-side-line); color: var(--wp-side-fg); }\n .wp-mode:focus-visible, .wp-pill-trigger:focus-visible, .wp-filter-btn:focus-visible, .wp-chip:focus-visible,\n .wp-primary:focus-visible, .wp-pill button:focus-visible, .wp-toggle:focus-visible, .wp-side input:focus-visible, .wp-icon:focus-visible,\n .wp-link:focus-visible { outline: 2px solid var(--wp-accent-hover); outline-offset: 2px; }`;\n }\n\n private main(): string {\n return `\n .wp-main { position: relative; min-width: 0; height: 100vh; display: flex; flex-direction: column; }\n .wp-topbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 7px 12px;\n border-bottom: 1px solid var(--wp-line); background: var(--wp-surface); }\n .wp-crumb { flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--wp-muted); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n .wp-crumb b { color: var(--wp-fg); }\n .wp-topbar #wp-filter-status, .wp-topbar #wp-graph-error { margin: 0; font-size: 13px; }\n .wp-pills { display: flex; gap: 6px; flex-wrap: wrap; }\n .wp-pills:empty { display: none; }\n .wp-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 10px; border-radius: 999px;\n background: #f1e8fe; color: #4a1d8a; border: 1px solid #d6bcfa; font-size: 12.5px; }\n .wp-pill button { all: unset; cursor: pointer; width: 18px; height: 18px; display: inline-grid; place-items: center;\n border-radius: 50%; color: #4a1d8a; }\n .wp-pill button:hover { background: #e2cffc; }`;\n }\n\n /** The Filter popover: three groups, chips, and the \"Show N projects\" footer. */\n private filterPopover(): string {\n return `\n .wp-filter-pop { left: 12px; top: 56px; width: min(380px, calc(100% - 24px)); max-height: calc(100% - 80px); overflow: auto; }\n .wp-fgroup { border: 0; margin: 0 0 12px; padding: 0; display: grid; gap: 4px; }\n .wp-fgroup legend { padding: 0; margin-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;\n color: var(--wp-muted); font-weight: 600; }\n .wp-fgroup legend small { text-transform: none; letter-spacing: 0; font-weight: 400; }\n .wp-fgroup:disabled .wp-scope { opacity: 0.45; cursor: not-allowed; }\n .wp-scope { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 6px; cursor: pointer; }\n .wp-scope:hover { background: #f4f1fb; }\n .wp-scope input { accent-color: var(--wp-accent); margin: 0; }\n .wp-scope-text { flex: 1 1 auto; display: grid; }\n .wp-scope-sub { font-size: 11.5px; color: var(--wp-muted); line-height: 1.3; }\n .wp-chip-swatch { vertical-align: -1px; margin-right: 5px; }\n .wp-count { color: var(--wp-muted); font-size: 12px; font-variant-numeric: tabular-nums; }\n .wp-scope-reason { margin: 2px 6px 0; font-size: 12px; color: var(--wp-muted); }\n .wp-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n .wp-chip { all: unset; box-sizing: border-box; cursor: pointer; padding: 3px 11px; border-radius: 999px;\n border: 1px solid var(--wp-line); background: var(--wp-surface); font-size: 12.5px; }\n .wp-chip:hover { border-color: var(--wp-accent-hover); }\n .wp-chip[aria-pressed=\"true\"] { background: var(--wp-accent); border-color: var(--wp-accent); color: #fff; }\n .wp-chip:disabled { opacity: 0.45; cursor: default; }\n .wp-impact-kinds { margin: 6px 0 4px; }\n .wp-side .wp-chip:not([aria-pressed=\"true\"]) { background: var(--wp-side-control); border-color: var(--wp-side-line); }\n .wp-filter-pop footer { display: flex; justify-content: space-between; align-items: center; gap: 8px;\n padding-top: 10px; border-top: 1px solid var(--wp-line); }\n .wp-text-btn { all: unset; cursor: pointer; color: var(--wp-muted); font-size: 12.5px; text-decoration: underline; }\n .wp-primary { all: unset; box-sizing: border-box; cursor: pointer; padding: 7px 14px; border-radius: 8px;\n background: var(--wp-accent); color: #fff; font-weight: 600; font-size: 13px; }\n .wp-primary:hover { background: var(--wp-accent-hover); }`;\n }\n\n private overlays(): string {\n return `\n .wp-pop { position: absolute; z-index: 30; background: var(--wp-surface); border: 1px solid var(--wp-line);\n border-radius: 10px; box-shadow: 0 8px 24px #0002; padding: 12px 14px; font-size: 13px; box-sizing: border-box; }\n .wp-pop header { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 8px; }\n .wp-legend-popout { left: 12px; bottom: 12px; width: 270px; max-height: 60vh; overflow: auto; }\n .wp-legend-popout .wp-legend-row small, .wp-legend-popout .wp-impact-note { color: var(--wp-muted); }\n .wp-help { right: 12px; top: 56px; width: min(380px, calc(100% - 24px)); line-height: 1.5; }\n .wp-help p { margin: 0 0 8px; }\n .wp-snapshot { left: 12px; bottom: 12px; width: min(440px, calc(100% - 24px)); }\n .wp-snapshot .hint { margin: 0; }\n .wp-panel { position: absolute; top: 0; right: 0; bottom: 0; z-index: 40; width: min(540px, 100%); box-sizing: border-box;\n overflow-y: auto; padding: 0 16px 16px; background: #f5f6f9; border-left: 1px solid var(--wp-line); box-shadow: -8px 0 24px #0002; }\n .wp-panel > header { position: sticky; top: 0; z-index: 1; display: flex; justify-content: space-between; align-items: center;\n padding: 12px 0; background: #f5f6f9; font-weight: 600; font-size: 15px; }`;\n }\n\n /**\n * The graph fills what the topbar leaves.\n *\n * The hover glow and the lock outline belong to the OUTER box only. A nested Runtime box (one\n * runtime plus a specialization) is an HTML-like table, so after the node's own outline Graphviz\n * emits a `<polygon>` for the base-colored cell and a `<path>` for the inner `STYLE=\"rounded\"`\n * table — matching only `polygon` here is what let the glow stroke the inner box (#1158). So the\n * glow is suppressed on EVERY shape after the node's first, whatever its element type.\n *\n * A striped (multi-runtime) box is the one exception to \"the first shape is the outline\":\n * Graphviz draws one filled polygon per stripe first and the outline LAST, as an unfilled polygon.\n * graph-color-modes.ts stamps those boxes `wp-striped`, and for them the outline is that last,\n * `fill=\"none\"` polygon; the stripes keep their thin dividers.\n */\n private graph(): string {\n const shape = ':is(polygon, ellipse, path)';\n const later = `${shape} ~ ${shape}`;\n const first = `${shape}:not(${later})`;\n const glow = 'stroke: var(--wp-accent); stroke-width: 5; filter: drop-shadow(0 0 6px rgba(139, 60, 240, 0.85));';\n return `\n #graph { flex: 1 1 auto; min-height: 0; max-height: none; overflow: auto; padding: 16px 16px 64px;\n box-sizing: border-box; background: var(--wp-canvas); }\n #graph g.wp-node-clickable:hover > ${later} { stroke: none; stroke-width: 0; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon:not([fill=\"none\"]) { stroke: #000000; stroke-width: 0.5; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon[fill=\"none\"] { ${glow} }\n #graph g.node.wp-locked:not(.${STRIPED_CLASS}) > ${first},\n #graph g.node.wp-locked.${STRIPED_CLASS} > polygon[fill=\"none\"] { stroke: var(--wp-accent); stroke-width: 3; }`;\n }\n\n private cards(): string {\n return `\n #wp-responsibilities { margin: 0; }\n #wp-responsibilities > h2 { display: none; }\n #wp-responsibilities > .hint { color: var(--wp-muted); margin: 0 0 8px; }\n .wp-resp-card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 10px 0; padding: 10px 15px; }\n .wp-resp-card > summary { cursor: pointer; color: #333; }\n .wp-resp-level { display: inline-block; min-width: 26px; padding: 1px 6px; margin-right: 6px; border-radius: 4px;\n background: #eef; font-size: 12px; font-weight: bold; text-align: center; }\n .wp-resp-body { margin-top: 8px; color: #444; }\n .wp-resp-body code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n .wp-hidden { display: none; }`;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-page-styles.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-styles.ts"],"names":[],"mappings":";;;AAAA,2DAAoD;AAEpD;;;;GAIG;AACH,MAAa,eAAe;IACxB,GAAG;QACC,OAAO,CACH,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,MAAM,EAAE;YACb,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,aAAa,EAAE;YACpB,IAAI,CAAC,KAAK,EAAE;YACZ,IAAI,CAAC,KAAK,EAAE,CACf,CAAC;IACN,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;;;;;;;;;;uFAuBwE,CAAC;IACpF,CAAC;IAEO,MAAM;QACV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mJAwCoI,CAAC;IAChJ,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;;;;;;;;;;mGAsBoF,CAAC;IAChG,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;uDAcwC,CAAC;IACpD,CAAC;IAED,iFAAiF;IACzE,aAAa;QACjB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;kEA4BmD,CAAC;IAC/D,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;uFAawE,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,KAAK;QACT,MAAM,KAAK,GAAG,6BAA6B,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,KAAK,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,KAAK,QAAQ,KAAK,GAAG,CAAC;QACvC,MAAM,IAAI,GAAG,mGAAmG,CAAC;QACjH,OAAO;;;6CAG8B,KAAK;qCACb,iCAAa;qCACb,iCAAa,mCAAmC,IAAI;uCAClD,iCAAa,OAAO,KAAK;kCAC9B,iCAAa,wEAAwE,CAAC;IACpH,CAAC;IAEO,KAAK;QACT,OAAO;;;;;;;;;;sCAUuB,CAAC;IACnC,CAAC;CACJ;AA7ND,0CA6NC","sourcesContent":["import { STRIPED_CLASS } from './graph-color-modes';\n\n/**\n * The architecture page's stylesheet: the dark drawer, the graph filling the viewport, the floating\n * popovers and the responsibilities panel. Emitted AFTER the shared menu/filter/navigation styles so\n * its `#graph` rules (fill the viewport, no 75vh cap) win.\n */\nexport class GraphPageStyles {\n css(): string {\n return (\n this.base() +\n this.drawer() +\n this.controls() +\n this.main() +\n this.overlays() +\n this.filterPopover() +\n this.graph() +\n this.cards()\n );\n }\n\n private base(): string {\n return `\n :root {\n --wp-side-bg: #15161b; --wp-side-control: #1e1f25; --wp-side-fg: #e9eaf0; --wp-side-muted: #8b8fa0;\n --wp-side-hover: #26272f; --wp-side-line: #3a3c46; --wp-accent: #8b3cf0; --wp-accent-hover: #a46bf7;\n --wp-fg: #1b1e27; --wp-muted: #5d6475; --wp-line: #d9dce5; --wp-surface: #ffffff; --wp-canvas: #faf9f5;\n }\n html, body { height: 100%; }\n /* A class that sets display (the legend lists are grids) must not un-hide a [hidden] element. */\n [hidden] { display: none !important; }\n body { margin: 0; padding: 0; overflow: hidden; background: var(--wp-canvas); color: var(--wp-fg);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Arial, sans-serif; font-size: 14px; }\n kbd { font-family: ui-monospace, Menlo, monospace; font-size: 12px; border: 1px solid var(--wp-line);\n border-radius: 4px; padding: 0 5px; background: #f3f4f7; }\n .wp-shell { display: grid; grid-template-columns: 272px minmax(0, 1fr); height: 100vh; }\n .wp-shell.wp-collapsed { grid-template-columns: 0 minmax(0, 1fr); }\n .wp-shell.wp-collapsed .wp-side { visibility: hidden; }\n @media (max-width: 760px) {\n .wp-shell { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, auto) minmax(0, 1fr); }\n .wp-side { max-height: 42vh; }\n .wp-shell.wp-collapsed { grid-template-columns: minmax(0, 1fr); }\n .wp-shell.wp-collapsed .wp-side { display: none; }\n .wp-main { height: auto; min-height: 0; }\n }\n @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }`;\n }\n\n private drawer(): string {\n return `\n .wp-side { background: var(--wp-side-bg); color: var(--wp-side-fg); overflow-y: auto; overflow-x: hidden;\n display: flex; flex-direction: column; padding: 12px 0 10px; min-width: 0; font-size: 13px; }\n .wp-brand { display: flex; align-items: center; gap: 10px; padding: 0 16px 10px; font-weight: 600; font-size: 14px; }\n .wp-brand svg { flex: none; }\n .wp-group { padding: 8px 14px; border-top: 1px solid var(--wp-side-line); display: grid; gap: 6px; }\n .wp-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--wp-side-muted);\n font-weight: 600; padding-left: 4px; }\n .wp-label-row { display: flex; justify-content: space-between; align-items: center; }\n .wp-pulldown { position: relative; }\n .wp-pill-trigger { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; gap: 10px;\n width: 100%; padding: 7px 16px; border-radius: 999px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); }\n .wp-pill-trigger:hover, .wp-pill-trigger[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-pill-text { flex: 1 1 auto; min-width: 0; }\n .wp-caret { color: var(--wp-side-muted); }\n .wp-menu { position: absolute; z-index: 35; left: 0; right: 0; top: calc(100% + 6px); display: grid; gap: 2px; padding: 6px;\n background: var(--wp-side-control); border: 1px solid var(--wp-side-line); border-radius: 12px; box-shadow: 0 10px 28px #0008; }\n .wp-mode { all: unset; box-sizing: border-box; cursor: pointer; display: block; padding: 6px 10px; border-radius: 8px; }\n .wp-mode:hover { background: var(--wp-side-hover); }\n .wp-mode[aria-checked=\"true\"] { background: #2a2333; box-shadow: inset 3px 0 0 var(--wp-accent); }\n .wp-mode:disabled { cursor: not-allowed; opacity: 0.45; }\n .wp-mode-name { display: block; font-size: 14px; font-weight: 600; line-height: 1.3; }\n .wp-mode-sub { display: block; font-size: 12px; color: var(--wp-side-muted); line-height: 1.3; }\n .wp-filter-btn { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;\n align-items: center; padding: 7px 12px; border-radius: 8px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); font-size: 13px; font-weight: 600; }\n .wp-filter-btn:hover, .wp-filter-btn[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-badge { min-width: 18px; padding: 1px 6px; border-radius: 999px; background: var(--wp-accent); color: #fff;\n font-size: 11.5px; text-align: center; box-sizing: border-box; }\n .wp-mode-reason { margin: 2px 4px 0; font-size: 11.5px; line-height: 1.35; color: var(--wp-side-muted); }\n .wp-legend-list { display: grid; gap: 5px; padding-left: 4px; }\n .wp-legend-row { display: flex; align-items: center; gap: 10px; line-height: 1.25; }\n .wp-legend-row svg { flex: none; }\n .wp-legend-row small { display: block; color: var(--wp-side-muted); font-size: 11.5px; }\n .wp-product-toggle { cursor: pointer; }\n .wp-product-toggle input { flex: none; margin: 0; accent-color: var(--wp-accent); cursor: pointer; }\n .wp-product-note { margin: 2px 0 0; font-size: 11.5px; color: var(--wp-side-muted); }\n .wp-impact-note { margin: 2px 0 0; font-size: 11.5px; color: var(--wp-side-muted); }\n .wp-impact-note:empty { display: none; }\n .wp-foot { margin-top: auto; padding: 10px 18px 0; border-top: 1px solid var(--wp-side-line); display: flex; gap: 14px; flex-wrap: wrap; }`;\n }\n\n private controls(): string {\n return `\n .wp-side input[type=\"search\"] { width: 100%; box-sizing: border-box; background: var(--wp-side-control); color: var(--wp-side-fg);\n border: 1px solid var(--wp-side-line); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }\n .wp-toggle { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;\n align-items: center; padding: 6px 10px; border-radius: 8px; font-size: 13px; }\n .wp-toggle:hover { background: var(--wp-side-hover); }\n .wp-toggle:disabled { cursor: not-allowed; opacity: 0.45; }\n .wp-sw { width: 30px; height: 18px; border-radius: 9px; background: #3a3d4a; position: relative; transition: background 0.15s; }\n .wp-sw::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%;\n background: #fff; transition: transform 0.15s; }\n .wp-toggle[aria-pressed=\"true\"] .wp-sw { background: var(--wp-accent); }\n .wp-toggle[aria-pressed=\"true\"] .wp-sw::after { transform: translateX(12px); }\n .wp-link { all: unset; cursor: pointer; color: var(--wp-side-muted); font-size: 12px; text-decoration: underline; }\n .wp-link:hover { color: var(--wp-side-fg); }\n .wp-icon { all: unset; box-sizing: border-box; cursor: pointer; display: inline-grid; place-items: center; flex: none;\n width: 34px; height: 34px; border-radius: 8px; background: var(--wp-surface); border: 1px solid var(--wp-line);\n color: var(--wp-fg); font-size: 15px; }\n .wp-icon:hover { border-color: var(--wp-accent); }\n .wp-icon-mini { width: 24px; height: 24px; font-size: 12px; }\n .wp-icon-dark { background: transparent; border-color: var(--wp-side-line); color: var(--wp-side-fg); }\n .wp-mode:focus-visible, .wp-pill-trigger:focus-visible, .wp-filter-btn:focus-visible, .wp-chip:focus-visible,\n .wp-primary:focus-visible, .wp-pill button:focus-visible, .wp-toggle:focus-visible, .wp-side input:focus-visible, .wp-icon:focus-visible,\n .wp-link:focus-visible { outline: 2px solid var(--wp-accent-hover); outline-offset: 2px; }`;\n }\n\n private main(): string {\n return `\n .wp-main { position: relative; min-width: 0; height: 100vh; display: flex; flex-direction: column; }\n .wp-topbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 7px 12px;\n border-bottom: 1px solid var(--wp-line); background: var(--wp-surface); }\n .wp-crumb { flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--wp-muted); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n .wp-crumb b { color: var(--wp-fg); }\n .wp-topbar #wp-filter-status, .wp-topbar #wp-graph-error { margin: 0; font-size: 13px; }\n .wp-pills { display: flex; gap: 6px; flex-wrap: wrap; }\n .wp-pills:empty { display: none; }\n .wp-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 10px; border-radius: 999px;\n background: #f1e8fe; color: #4a1d8a; border: 1px solid #d6bcfa; font-size: 12.5px; }\n .wp-pill button { all: unset; cursor: pointer; width: 18px; height: 18px; display: inline-grid; place-items: center;\n border-radius: 50%; color: #4a1d8a; }\n .wp-pill button:hover { background: #e2cffc; }`;\n }\n\n /** The Filter popover: three groups, chips, and the \"Show N projects\" footer. */\n private filterPopover(): string {\n return `\n .wp-filter-pop { left: 12px; top: 56px; width: min(380px, calc(100% - 24px)); max-height: calc(100% - 80px); overflow: auto; }\n .wp-fgroup { border: 0; margin: 0 0 12px; padding: 0; display: grid; gap: 4px; }\n .wp-fgroup legend { padding: 0; margin-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;\n color: var(--wp-muted); font-weight: 600; }\n .wp-fgroup legend small { text-transform: none; letter-spacing: 0; font-weight: 400; }\n .wp-fgroup:disabled .wp-scope { opacity: 0.45; cursor: not-allowed; }\n .wp-scope { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 6px; cursor: pointer; }\n .wp-scope:hover { background: #f4f1fb; }\n .wp-scope input { accent-color: var(--wp-accent); margin: 0; }\n .wp-scope-text { flex: 1 1 auto; display: grid; }\n .wp-scope-sub { font-size: 11.5px; color: var(--wp-muted); line-height: 1.3; }\n .wp-chip-swatch { vertical-align: -1px; margin-right: 5px; }\n .wp-count { color: var(--wp-muted); font-size: 12px; font-variant-numeric: tabular-nums; }\n .wp-scope-reason { margin: 2px 6px 0; font-size: 12px; color: var(--wp-muted); }\n .wp-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n .wp-chip { all: unset; box-sizing: border-box; cursor: pointer; padding: 3px 11px; border-radius: 999px;\n border: 1px solid var(--wp-line); background: var(--wp-surface); font-size: 12.5px; }\n .wp-chip:hover { border-color: var(--wp-accent-hover); }\n .wp-chip[aria-pressed=\"true\"] { background: var(--wp-accent); border-color: var(--wp-accent); color: #fff; }\n .wp-chip:disabled { opacity: 0.45; cursor: default; }\n .wp-impact-kinds { margin: 6px 0 4px; }\n .wp-side .wp-chip:not([aria-pressed=\"true\"]) { background: var(--wp-side-control); border-color: var(--wp-side-line); }\n .wp-filter-pop footer { display: flex; justify-content: space-between; align-items: center; gap: 8px;\n padding-top: 10px; border-top: 1px solid var(--wp-line); }\n .wp-text-btn { all: unset; cursor: pointer; color: var(--wp-muted); font-size: 12.5px; text-decoration: underline; }\n .wp-primary { all: unset; box-sizing: border-box; cursor: pointer; padding: 7px 14px; border-radius: 8px;\n background: var(--wp-accent); color: #fff; font-weight: 600; font-size: 13px; }\n .wp-primary:hover { background: var(--wp-accent-hover); }`;\n }\n\n private overlays(): string {\n return `\n .wp-pop { position: absolute; z-index: 30; background: var(--wp-surface); border: 1px solid var(--wp-line);\n border-radius: 10px; box-shadow: 0 8px 24px #0002; padding: 12px 14px; font-size: 13px; box-sizing: border-box; }\n .wp-pop header { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 8px; }\n .wp-legend-popout { left: 12px; bottom: 12px; width: 270px; max-height: 60vh; overflow: auto; }\n .wp-legend-popout .wp-legend-row small, .wp-legend-popout .wp-impact-note, .wp-legend-popout .wp-product-note { color: var(--wp-muted); }\n .wp-help { right: 12px; top: 56px; width: min(380px, calc(100% - 24px)); line-height: 1.5; }\n .wp-help p { margin: 0 0 8px; }\n .wp-snapshot { left: 12px; bottom: 12px; width: min(440px, calc(100% - 24px)); }\n .wp-snapshot .hint { margin: 0; }\n .wp-panel { position: absolute; top: 0; right: 0; bottom: 0; z-index: 40; width: min(540px, 100%); box-sizing: border-box;\n overflow-y: auto; padding: 0 16px 16px; background: #f5f6f9; border-left: 1px solid var(--wp-line); box-shadow: -8px 0 24px #0002; }\n .wp-panel > header { position: sticky; top: 0; z-index: 1; display: flex; justify-content: space-between; align-items: center;\n padding: 12px 0; background: #f5f6f9; font-weight: 600; font-size: 15px; }`;\n }\n\n /**\n * The graph fills what the topbar leaves.\n *\n * The hover glow and the lock outline belong to the OUTER box only. A nested Runtime box (one\n * runtime plus a specialization) is an HTML-like table, so after the node's own outline Graphviz\n * emits a `<polygon>` for the base-colored cell and a `<path>` for the inner `STYLE=\"rounded\"`\n * table — matching only `polygon` here is what let the glow stroke the inner box (#1158). So the\n * glow is suppressed on EVERY shape after the node's first, whatever its element type.\n *\n * A striped (multi-runtime) box is the one exception to \"the first shape is the outline\":\n * Graphviz draws one filled polygon per stripe first and the outline LAST, as an unfilled polygon.\n * graph-color-modes.ts stamps those boxes `wp-striped`, and for them the outline is that last,\n * `fill=\"none\"` polygon; the stripes keep their thin dividers.\n */\n private graph(): string {\n const shape = ':is(polygon, ellipse, path)';\n const later = `${shape} ~ ${shape}`;\n const first = `${shape}:not(${later})`;\n const glow = 'stroke: var(--wp-accent); stroke-width: 5; filter: drop-shadow(0 0 6px rgba(139, 60, 240, 0.85));';\n return `\n #graph { flex: 1 1 auto; min-height: 0; max-height: none; overflow: auto; padding: 16px 16px 64px;\n box-sizing: border-box; background: var(--wp-canvas); }\n #graph g.wp-node-clickable:hover > ${later} { stroke: none; stroke-width: 0; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon:not([fill=\"none\"]) { stroke: #000000; stroke-width: 0.5; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon[fill=\"none\"] { ${glow} }\n #graph g.node.wp-locked:not(.${STRIPED_CLASS}) > ${first},\n #graph g.node.wp-locked.${STRIPED_CLASS} > polygon[fill=\"none\"] { stroke: var(--wp-accent); stroke-width: 3; }`;\n }\n\n private cards(): string {\n return `\n #wp-responsibilities { margin: 0; }\n #wp-responsibilities > h2 { display: none; }\n #wp-responsibilities > .hint { color: var(--wp-muted); margin: 0 0 8px; }\n .wp-resp-card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 10px 0; padding: 10px 15px; }\n .wp-resp-card > summary { cursor: pointer; color: #333; }\n .wp-resp-level { display: inline-block; min-width: 26px; padding: 1px 6px; margin-right: 6px; border-radius: 4px;\n background: #eef; font-size: 12px; font-weight: bold; text-align: center; }\n .wp-resp-body { margin-top: 8px; color: #444; }\n .wp-resp-body code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n .wp-hidden { display: none; }`;\n }\n}\n"]}
@@ -0,0 +1,92 @@
1
+ /** Which of the five looks a Product-mode node gets for one ON set. */
2
+ declare enum WpProductPaintKind {
3
+ /** The node belongs to no product: white, dashed border. */
4
+ NONE = "none",
5
+ /** Every product it carries is OFF: light grey, still visible. */
6
+ OFF = "off",
7
+ /** Exactly one of its products is ON: solid in that product's fixed color. */
8
+ SOLID = "solid",
9
+ /** Two or more of its products are ON: one stripe per ON product. */
10
+ STRIPES = "stripes",
11
+ /** Every product is ON and the node carries every one: the neutral shared fill. */
12
+ SHARED = "shared"
13
+ }
14
+ /** How one node is painted in Product mode. Data-only. */
15
+ declare class WpProductPaint {
16
+ readonly kind: WpProductPaintKind;
17
+ /** SOLID: its one color; STRIPES: one per ON product, in the node's product order; else empty. */
18
+ readonly colors: string[];
19
+ /** The text color that reads on the fill. */
20
+ readonly fontColor: string;
21
+ constructor(kind: WpProductPaintKind,
22
+ /** SOLID: its one color; STRIPES: one per ON product, in the node's product order; else empty. */
23
+ colors: string[],
24
+ /** The text color that reads on the fill. */
25
+ fontColor: string);
26
+ }
27
+ /** Which products are ON when a page loads (#1319): the same answer on every load, never persisted. */
28
+ declare class WpProductSelection {
29
+ /** The first two products in legend order. */
30
+ initialOn(order: string[]): Set<string>;
31
+ }
32
+ /** Decides and writes every Product-mode statement for one ON set. Immutable: a toggle builds a new one. */
33
+ declare class WpProductPainter {
34
+ private readonly legend;
35
+ private readonly on;
36
+ /** graph-products.ts PRODUCT_SHARED_FILL. */
37
+ static readonly SHARED_FILL = "#c9ced8";
38
+ /** graph-products.ts PRODUCT_NONE_FILL / PRODUCT_NONE_BORDER. */
39
+ static readonly NONE_FILL = "#ffffff";
40
+ static readonly NONE_BORDER = "#8a90a0";
41
+ /** graph-products.ts PRODUCT_OFF_FILL / PRODUCT_OFF_BORDER. */
42
+ static readonly OFF_FILL = "#eef0f4";
43
+ static readonly OFF_BORDER = "#b4b9c4";
44
+ /** graph-color-modes.ts DARK_TEXT / LIGHT_TEXT / STRIPED_CLASS. */
45
+ static readonly DARK_TEXT = "#1a1c22";
46
+ static readonly LIGHT_TEXT = "#ffffff";
47
+ static readonly STRIPED_CLASS = "wp-striped";
48
+ /** graph-products.ts PRODUCTS_ON_AT_LOAD. */
49
+ static readonly ON_AT_LOAD = 2;
50
+ private readonly colors;
51
+ /**
52
+ * @param legend the products the legend lists, in its order, each with its FIXED palette color —
53
+ * a color is never reassigned to the ON set, so it always means the same product
54
+ * @param on the products switched ON
55
+ */
56
+ constructor(legend: ProductColorJson[], on: ReadonlySet<string>);
57
+ /** The paint for a node carrying `products`. */
58
+ paint(products: string[]): WpProductPaint;
59
+ /**
60
+ * The color a cross-product dependency is dashed in (#1204): the first product it crosses into
61
+ * that is ON; null draws it plain.
62
+ */
63
+ edgeColor(crossesInto: string[]): string | null;
64
+ /** A node's Product-mode statement. */
65
+ nodeDot(template: ProductTemplateJson, products: string[]): string;
66
+ /** An edge's Product-mode statement: dashed in its ON target product's color, else plain. */
67
+ edgeDot(edge: RenderEdgeJson): string;
68
+ private isOn;
69
+ /** Every product is ON — of a legend listing two or more (a lone product is its own color). */
70
+ private allOn;
71
+ private attrs;
72
+ /** Equal vertical stripes, the last taking the remainder — the multi-runtime box's renderer. */
73
+ private stripes;
74
+ /**
75
+ * A queue, clock, datastore or vendor keeps its kind statement; only its fill changes. Graphviz
76
+ * cylinders cannot stripe, so several ON products are a gradient; no product is dashed.
77
+ */
78
+ private context;
79
+ private contextFill;
80
+ }
81
+ /** The Product legend's checkboxes (#1319): one per product, in the drawer AND the popped-out legend. */
82
+ declare class WpProductToggles {
83
+ /** The products the legend lists, in its order, once each. */
84
+ products(): string[];
85
+ /** Each checkbox switches its product ON or OFF. */
86
+ wire(onChange: (product: string, on: boolean) => void): void;
87
+ /** Every copy of every checkbox shows the ON set. */
88
+ sync(on: ReadonlySet<string>): void;
89
+ /** A painter over the legend's products with their FIXED palette colors, for a snapshot of `on`. */
90
+ painter(palette: ProductColorJson[], on: ReadonlySet<string>): WpProductPainter;
91
+ private inputs;
92
+ }
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ /*
3
+ * The Product color mode's paint, decided IN THE BROWSER from the products switched ON (#1319).
4
+ *
5
+ * Each product's legend row is a toggle. Painting every declared product at once gave a workspace with
6
+ * eight products eight saturated colors plus stripes, which hid exactly what the mode is for: one
7
+ * product's footprint, or where two overlap. So the page keeps an ON set — the first two products in
8
+ * legend (sorted-name) order at EVERY load, never persisted — and repaints from it without
9
+ * regenerating anything: the page already carries each node's products and the ONE palette (#1244).
10
+ *
11
+ * Prepended to graph-visualizer.client.js like graph-mode-state.client.ts. The painter is pure (no
12
+ * DOM), so a spec runs it in a bare VM; WpProductToggles is the only class here that touches the page. The colors it writes are graph-products.ts's (a spec pins that they agree);
13
+ * they are spelled here because this script is not a module and cannot import them.
14
+ */
15
+ /** Which of the five looks a Product-mode node gets for one ON set. */
16
+ var WpProductPaintKind;
17
+ (function (WpProductPaintKind) {
18
+ /** The node belongs to no product: white, dashed border. */
19
+ WpProductPaintKind["NONE"] = "none";
20
+ /** Every product it carries is OFF: light grey, still visible. */
21
+ WpProductPaintKind["OFF"] = "off";
22
+ /** Exactly one of its products is ON: solid in that product's fixed color. */
23
+ WpProductPaintKind["SOLID"] = "solid";
24
+ /** Two or more of its products are ON: one stripe per ON product. */
25
+ WpProductPaintKind["STRIPES"] = "stripes";
26
+ /** Every product is ON and the node carries every one: the neutral shared fill. */
27
+ WpProductPaintKind["SHARED"] = "shared";
28
+ })(WpProductPaintKind || (WpProductPaintKind = {}));
29
+ /** How one node is painted in Product mode. Data-only. */
30
+ class WpProductPaint {
31
+ kind;
32
+ colors;
33
+ fontColor;
34
+ constructor(kind,
35
+ /** SOLID: its one color; STRIPES: one per ON product, in the node's product order; else empty. */
36
+ colors,
37
+ /** The text color that reads on the fill. */
38
+ fontColor) {
39
+ this.kind = kind;
40
+ this.colors = colors;
41
+ this.fontColor = fontColor;
42
+ }
43
+ }
44
+ /** Which products are ON when a page loads (#1319): the same answer on every load, never persisted. */
45
+ class WpProductSelection {
46
+ /** The first two products in legend order. */
47
+ initialOn(order) {
48
+ return new Set(order.slice(0, WpProductPainter.ON_AT_LOAD));
49
+ }
50
+ }
51
+ /** Decides and writes every Product-mode statement for one ON set. Immutable: a toggle builds a new one. */
52
+ class WpProductPainter {
53
+ legend;
54
+ on;
55
+ /** graph-products.ts PRODUCT_SHARED_FILL. */
56
+ static SHARED_FILL = '#c9ced8';
57
+ /** graph-products.ts PRODUCT_NONE_FILL / PRODUCT_NONE_BORDER. */
58
+ static NONE_FILL = '#ffffff';
59
+ static NONE_BORDER = '#8a90a0';
60
+ /** graph-products.ts PRODUCT_OFF_FILL / PRODUCT_OFF_BORDER. */
61
+ static OFF_FILL = '#eef0f4';
62
+ static OFF_BORDER = '#b4b9c4';
63
+ /** graph-color-modes.ts DARK_TEXT / LIGHT_TEXT / STRIPED_CLASS. */
64
+ static DARK_TEXT = '#1a1c22';
65
+ static LIGHT_TEXT = '#ffffff';
66
+ static STRIPED_CLASS = 'wp-striped';
67
+ /** graph-products.ts PRODUCTS_ON_AT_LOAD. */
68
+ static ON_AT_LOAD = 2;
69
+ colors = new Map();
70
+ /**
71
+ * @param legend the products the legend lists, in its order, each with its FIXED palette color —
72
+ * a color is never reassigned to the ON set, so it always means the same product
73
+ * @param on the products switched ON
74
+ */
75
+ constructor(legend, on) {
76
+ this.legend = legend;
77
+ this.on = on;
78
+ for (const color of legend)
79
+ this.colors.set(color.product, color);
80
+ }
81
+ /** The paint for a node carrying `products`. */
82
+ paint(products) {
83
+ if (products.length === 0)
84
+ return new WpProductPaint(WpProductPaintKind.NONE, [], WpProductPainter.DARK_TEXT);
85
+ const on = products.filter((product) => this.isOn(product));
86
+ if (on.length === 0)
87
+ return new WpProductPaint(WpProductPaintKind.OFF, [], WpProductPainter.DARK_TEXT);
88
+ if (this.allOn() && this.legend.every((color) => products.includes(color.product)))
89
+ return new WpProductPaint(WpProductPaintKind.SHARED, [], WpProductPainter.DARK_TEXT);
90
+ const colors = on.map((product) => this.colors.get(product));
91
+ if (colors.length === 1)
92
+ return new WpProductPaint(WpProductPaintKind.SOLID, [colors[0].color], colors[0].fontColor);
93
+ return new WpProductPaint(WpProductPaintKind.STRIPES, colors.map((color) => color.color), WpProductPainter.DARK_TEXT);
94
+ }
95
+ /**
96
+ * The color a cross-product dependency is dashed in (#1204): the first product it crosses into
97
+ * that is ON; null draws it plain.
98
+ */
99
+ edgeColor(crossesInto) {
100
+ const into = crossesInto.find((product) => this.isOn(product));
101
+ return into === undefined ? null : this.colors.get(into).color;
102
+ }
103
+ /** A node's Product-mode statement. */
104
+ nodeDot(template, products) {
105
+ const paint = this.paint(products);
106
+ if (template.kind === 'context')
107
+ return this.context(template.head, paint);
108
+ const label = paint.fontColor === WpProductPainter.LIGHT_TEXT ? template.lightLabel : template.darkLabel;
109
+ return `${template.head}${this.attrs(paint)}, label=<${label}>];\n`;
110
+ }
111
+ /** An edge's Product-mode statement: dashed in its ON target product's color, else plain. */
112
+ edgeDot(edge) {
113
+ const color = this.edgeColor(edge.crossesInto);
114
+ if (color === null)
115
+ return edge.dot;
116
+ return `${edge.dot.slice(0, -2)} [style=dashed, color="${color}", penwidth=2];\n`;
117
+ }
118
+ isOn(product) {
119
+ return this.on.has(product) && this.colors.has(product);
120
+ }
121
+ /** Every product is ON — of a legend listing two or more (a lone product is its own color). */
122
+ allOn() {
123
+ return this.legend.length > 1 && this.legend.every((color) => this.on.has(color.product));
124
+ }
125
+ attrs(paint) {
126
+ const dark = WpProductPainter.DARK_TEXT;
127
+ if (paint.kind === WpProductPaintKind.NONE)
128
+ return `style="filled,dashed", fillcolor="${WpProductPainter.NONE_FILL}", fontcolor="${dark}", color="${WpProductPainter.NONE_BORDER}", penwidth=1.5`;
129
+ if (paint.kind === WpProductPaintKind.OFF)
130
+ return `style="filled", fillcolor="${WpProductPainter.OFF_FILL}", fontcolor="${dark}", color="${WpProductPainter.OFF_BORDER}"`;
131
+ if (paint.kind === WpProductPaintKind.SHARED)
132
+ return `style="filled", fillcolor="${WpProductPainter.SHARED_FILL}", fontcolor="${dark}"`;
133
+ if (paint.kind === WpProductPaintKind.SOLID)
134
+ return `style="filled", fillcolor="${paint.colors[0]}", fontcolor="${paint.fontColor}"`;
135
+ return `${this.stripes(paint.colors)}, fontcolor="${dark}"`;
136
+ }
137
+ /** Equal vertical stripes, the last taking the remainder — the multi-runtime box's renderer. */
138
+ stripes(colors) {
139
+ const share = (1 / colors.length).toFixed(3);
140
+ const weighted = colors.map((color, index) => index === colors.length - 1 ? color : `${color};${share}`);
141
+ return `style="striped", fillcolor="${weighted.join(':')}", class="${WpProductPainter.STRIPED_CLASS}"`;
142
+ }
143
+ /**
144
+ * A queue, clock, datastore or vendor keeps its kind statement; only its fill changes. Graphviz
145
+ * cylinders cannot stripe, so several ON products are a gradient; no product is dashed.
146
+ */
147
+ context(dot, paint) {
148
+ const recolored = dot.replace(/fillcolor="[^"]*"/, `fillcolor="${this.contextFill(paint)}"`);
149
+ return paint.kind === WpProductPaintKind.NONE ? recolored.replace(/style="[^"]*"/, 'style="filled,dashed"') : recolored;
150
+ }
151
+ contextFill(paint) {
152
+ if (paint.kind === WpProductPaintKind.NONE)
153
+ return WpProductPainter.NONE_FILL;
154
+ if (paint.kind === WpProductPaintKind.OFF)
155
+ return WpProductPainter.OFF_FILL;
156
+ if (paint.kind === WpProductPaintKind.SHARED)
157
+ return WpProductPainter.SHARED_FILL;
158
+ return paint.colors.join(':');
159
+ }
160
+ }
161
+ /** The Product legend's checkboxes (#1319): one per product, in the drawer AND the popped-out legend. */
162
+ class WpProductToggles {
163
+ /** The products the legend lists, in its order, once each. */
164
+ products() {
165
+ const order = [];
166
+ this.inputs().forEach((input) => {
167
+ const product = input.dataset['wpProductToggle'] ?? '';
168
+ if (product !== '' && !order.includes(product))
169
+ order.push(product);
170
+ });
171
+ return order;
172
+ }
173
+ /** Each checkbox switches its product ON or OFF. */
174
+ wire(onChange) {
175
+ this.inputs().forEach((input) => {
176
+ input.addEventListener('change', () => onChange(input.dataset['wpProductToggle'] ?? '', input.checked));
177
+ });
178
+ }
179
+ /** Every copy of every checkbox shows the ON set. */
180
+ sync(on) {
181
+ this.inputs().forEach((input) => {
182
+ input.checked = on.has(input.dataset['wpProductToggle'] ?? '');
183
+ });
184
+ }
185
+ /** A painter over the legend's products with their FIXED palette colors, for a snapshot of `on`. */
186
+ painter(palette, on) {
187
+ const legend = this.products()
188
+ .map((product) => palette.find((color) => color.product === product))
189
+ .filter((color) => color !== undefined);
190
+ return new WpProductPainter(legend, new Set(on));
191
+ }
192
+ inputs() {
193
+ return document.querySelectorAll('[data-wp-product-toggle]');
194
+ }
195
+ }
196
+ //# sourceMappingURL=graph-product-paint.client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-product-paint.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-product-paint.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The Product color mode's paint, decided IN THE BROWSER from the products switched ON (#1319).\n *\n * Each product's legend row is a toggle. Painting every declared product at once gave a workspace with\n * eight products eight saturated colors plus stripes, which hid exactly what the mode is for: one\n * product's footprint, or where two overlap. So the page keeps an ON set — the first two products in\n * legend (sorted-name) order at EVERY load, never persisted — and repaints from it without\n * regenerating anything: the page already carries each node's products and the ONE palette (#1244).\n *\n * Prepended to graph-visualizer.client.js like graph-mode-state.client.ts. The painter is pure (no\n * DOM), so a spec runs it in a bare VM; WpProductToggles is the only class here that touches the page. The colors it writes are graph-products.ts's (a spec pins that they agree);\n * they are spelled here because this script is not a module and cannot import them.\n */\n\n/** Which of the five looks a Product-mode node gets for one ON set. */\nenum WpProductPaintKind {\n /** The node belongs to no product: white, dashed border. */\n NONE = 'none',\n /** Every product it carries is OFF: light grey, still visible. */\n OFF = 'off',\n /** Exactly one of its products is ON: solid in that product's fixed color. */\n SOLID = 'solid',\n /** Two or more of its products are ON: one stripe per ON product. */\n STRIPES = 'stripes',\n /** Every product is ON and the node carries every one: the neutral shared fill. */\n SHARED = 'shared',\n}\n\n/** How one node is painted in Product mode. Data-only. */\nclass WpProductPaint {\n constructor(\n readonly kind: WpProductPaintKind,\n /** SOLID: its one color; STRIPES: one per ON product, in the node's product order; else empty. */\n readonly colors: string[],\n /** The text color that reads on the fill. */\n readonly fontColor: string,\n ) {}\n}\n\n/** Which products are ON when a page loads (#1319): the same answer on every load, never persisted. */\nclass WpProductSelection {\n /** The first two products in legend order. */\n initialOn(order: string[]): Set<string> {\n return new Set(order.slice(0, WpProductPainter.ON_AT_LOAD));\n }\n}\n\n/** Decides and writes every Product-mode statement for one ON set. Immutable: a toggle builds a new one. */\nclass WpProductPainter {\n /** graph-products.ts PRODUCT_SHARED_FILL. */\n static readonly SHARED_FILL = '#c9ced8';\n /** graph-products.ts PRODUCT_NONE_FILL / PRODUCT_NONE_BORDER. */\n static readonly NONE_FILL = '#ffffff';\n static readonly NONE_BORDER = '#8a90a0';\n /** graph-products.ts PRODUCT_OFF_FILL / PRODUCT_OFF_BORDER. */\n static readonly OFF_FILL = '#eef0f4';\n static readonly OFF_BORDER = '#b4b9c4';\n /** graph-color-modes.ts DARK_TEXT / LIGHT_TEXT / STRIPED_CLASS. */\n static readonly DARK_TEXT = '#1a1c22';\n static readonly LIGHT_TEXT = '#ffffff';\n static readonly STRIPED_CLASS = 'wp-striped';\n /** graph-products.ts PRODUCTS_ON_AT_LOAD. */\n static readonly ON_AT_LOAD = 2;\n\n private readonly colors = new Map<string, ProductColorJson>();\n\n /**\n * @param legend the products the legend lists, in its order, each with its FIXED palette color —\n * a color is never reassigned to the ON set, so it always means the same product\n * @param on the products switched ON\n */\n constructor(\n private readonly legend: ProductColorJson[],\n private readonly on: ReadonlySet<string>,\n ) {\n for (const color of legend) this.colors.set(color.product, color);\n }\n\n /** The paint for a node carrying `products`. */\n paint(products: string[]): WpProductPaint {\n if (products.length === 0) return new WpProductPaint(WpProductPaintKind.NONE, [], WpProductPainter.DARK_TEXT);\n const on = products.filter((product: string): boolean => this.isOn(product));\n if (on.length === 0) return new WpProductPaint(WpProductPaintKind.OFF, [], WpProductPainter.DARK_TEXT);\n if (this.allOn() && this.legend.every((color: ProductColorJson): boolean => products.includes(color.product)))\n return new WpProductPaint(WpProductPaintKind.SHARED, [], WpProductPainter.DARK_TEXT);\n const colors = on.map((product: string): ProductColorJson => this.colors.get(product) as ProductColorJson);\n if (colors.length === 1) return new WpProductPaint(WpProductPaintKind.SOLID, [colors[0].color], colors[0].fontColor);\n return new WpProductPaint(\n WpProductPaintKind.STRIPES,\n colors.map((color: ProductColorJson): string => color.color),\n WpProductPainter.DARK_TEXT,\n );\n }\n\n /**\n * The color a cross-product dependency is dashed in (#1204): the first product it crosses into\n * that is ON; null draws it plain.\n */\n edgeColor(crossesInto: string[]): string | null {\n const into = crossesInto.find((product: string): boolean => this.isOn(product));\n return into === undefined ? null : (this.colors.get(into) as ProductColorJson).color;\n }\n\n /** A node's Product-mode statement. */\n nodeDot(template: ProductTemplateJson, products: string[]): string {\n const paint = this.paint(products);\n if (template.kind === 'context') return this.context(template.head, paint);\n const label = paint.fontColor === WpProductPainter.LIGHT_TEXT ? template.lightLabel : template.darkLabel;\n return `${template.head}${this.attrs(paint)}, label=<${label}>];\\n`;\n }\n\n /** An edge's Product-mode statement: dashed in its ON target product's color, else plain. */\n edgeDot(edge: RenderEdgeJson): string {\n const color = this.edgeColor(edge.crossesInto);\n if (color === null) return edge.dot;\n return `${edge.dot.slice(0, -2)} [style=dashed, color=\"${color}\", penwidth=2];\\n`;\n }\n\n private isOn(product: string): boolean {\n return this.on.has(product) && this.colors.has(product);\n }\n\n /** Every product is ON — of a legend listing two or more (a lone product is its own color). */\n private allOn(): boolean {\n return this.legend.length > 1 && this.legend.every((color: ProductColorJson): boolean => this.on.has(color.product));\n }\n\n private attrs(paint: WpProductPaint): string {\n const dark = WpProductPainter.DARK_TEXT;\n if (paint.kind === WpProductPaintKind.NONE)\n return `style=\"filled,dashed\", fillcolor=\"${WpProductPainter.NONE_FILL}\", fontcolor=\"${dark}\", color=\"${WpProductPainter.NONE_BORDER}\", penwidth=1.5`;\n if (paint.kind === WpProductPaintKind.OFF)\n return `style=\"filled\", fillcolor=\"${WpProductPainter.OFF_FILL}\", fontcolor=\"${dark}\", color=\"${WpProductPainter.OFF_BORDER}\"`;\n if (paint.kind === WpProductPaintKind.SHARED) return `style=\"filled\", fillcolor=\"${WpProductPainter.SHARED_FILL}\", fontcolor=\"${dark}\"`;\n if (paint.kind === WpProductPaintKind.SOLID)\n return `style=\"filled\", fillcolor=\"${paint.colors[0]}\", fontcolor=\"${paint.fontColor}\"`;\n return `${this.stripes(paint.colors)}, fontcolor=\"${dark}\"`;\n }\n\n /** Equal vertical stripes, the last taking the remainder — the multi-runtime box's renderer. */\n private stripes(colors: string[]): string {\n const share = (1 / colors.length).toFixed(3);\n const weighted = colors.map((color: string, index: number): string =>\n index === colors.length - 1 ? color : `${color};${share}`,\n );\n return `style=\"striped\", fillcolor=\"${weighted.join(':')}\", class=\"${WpProductPainter.STRIPED_CLASS}\"`;\n }\n\n /**\n * A queue, clock, datastore or vendor keeps its kind statement; only its fill changes. Graphviz\n * cylinders cannot stripe, so several ON products are a gradient; no product is dashed.\n */\n private context(dot: string, paint: WpProductPaint): string {\n const recolored = dot.replace(/fillcolor=\"[^\"]*\"/, `fillcolor=\"${this.contextFill(paint)}\"`);\n return paint.kind === WpProductPaintKind.NONE ? recolored.replace(/style=\"[^\"]*\"/, 'style=\"filled,dashed\"') : recolored;\n }\n\n private contextFill(paint: WpProductPaint): string {\n if (paint.kind === WpProductPaintKind.NONE) return WpProductPainter.NONE_FILL;\n if (paint.kind === WpProductPaintKind.OFF) return WpProductPainter.OFF_FILL;\n if (paint.kind === WpProductPaintKind.SHARED) return WpProductPainter.SHARED_FILL;\n return paint.colors.join(':');\n }\n}\n\n/** The Product legend's checkboxes (#1319): one per product, in the drawer AND the popped-out legend. */\nclass WpProductToggles {\n /** The products the legend lists, in its order, once each. */\n products(): string[] {\n const order: string[] = [];\n this.inputs().forEach((input: HTMLInputElement) => {\n const product = input.dataset['wpProductToggle'] ?? '';\n if (product !== '' && !order.includes(product)) order.push(product);\n });\n return order;\n }\n\n /** Each checkbox switches its product ON or OFF. */\n wire(onChange: (product: string, on: boolean) => void): void {\n this.inputs().forEach((input: HTMLInputElement) => {\n input.addEventListener('change', () => onChange(input.dataset['wpProductToggle'] ?? '', input.checked));\n });\n }\n\n /** Every copy of every checkbox shows the ON set. */\n sync(on: ReadonlySet<string>): void {\n this.inputs().forEach((input: HTMLInputElement) => {\n input.checked = on.has(input.dataset['wpProductToggle'] ?? '');\n });\n }\n\n /** A painter over the legend's products with their FIXED palette colors, for a snapshot of `on`. */\n painter(palette: ProductColorJson[], on: ReadonlySet<string>): WpProductPainter {\n const legend = this.products()\n .map((product: string): ProductColorJson | undefined => palette.find((color: ProductColorJson): boolean => color.product === product))\n .filter((color: ProductColorJson | undefined): color is ProductColorJson => color !== undefined);\n return new WpProductPainter(legend, new Set(on));\n }\n\n private inputs(): NodeListOf<HTMLInputElement> {\n return document.querySelectorAll<HTMLInputElement>('[data-wp-product-toggle]');\n }\n}\n"]}
@@ -17,10 +17,14 @@
17
17
  * between them. A service whose project carries no tag is in no product.
18
18
  * - ProductPalette — the Product color mode's colors (#1244): eight fixed saturated colors, each
19
19
  * product hashed (SHA-256 of its name) to a preferred one, with collisions resolved in sorted-name
20
- * order. Both viewers build it over the FULL declared product set, so they agree. Neutral means
21
- * shared by EVERY product; a dashed white box belongs to none.
20
+ * order. Both viewers build it over the FULL declared product set, so they agree, and a color is
21
+ * never reassigned when a product is toggled OFF.
22
+ * - ProductTemplate — what a page needs to paint one box in the Product color mode (#1319). Each
23
+ * product's legend row is a toggle and the page repaints in the browser from the ON set
24
+ * (WpProductPainter, graph-product-paint.client.ts), so the box's product statement is computed
25
+ * there, not here.
22
26
  * - ProductBoundary — which dependency edges cross from one product into another (#1204), drawn
23
- * dashed in the target product's color in the Product color mode.
27
+ * dashed in the target product's color in the Product color mode while that product is ON.
24
28
  *
25
29
  * Membership is stable per checkout (it depends on tags only, never on a diff), so it is written into
26
30
  * the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.
@@ -42,11 +46,50 @@ export declare class ProductPaletteSlot {
42
46
  }
43
47
  /** The eight product colors, indexed by slot. Distinct from every runtime and role color. */
44
48
  export declare const PRODUCT_PALETTE_SLOTS: readonly ProductPaletteSlot[];
45
- /** A box shared by EVERY product. */
49
+ /** A box shared by EVERY product, drawn neutral only while every product is ON (#1319). */
46
50
  export declare const PRODUCT_SHARED_FILL = "#c9ced8";
47
51
  /** A box in no product: white, with a dashed border. */
48
52
  export declare const PRODUCT_NONE_FILL = "#ffffff";
49
53
  export declare const PRODUCT_NONE_BORDER = "#8a90a0";
54
+ /**
55
+ * A box carrying only products switched OFF (#1319): light grey with a grey border, so it stays
56
+ * visible — hiding boxes is the Filter's job — and never reads as the neutral shared fill.
57
+ */
58
+ export declare const PRODUCT_OFF_FILL = "#eef0f4";
59
+ export declare const PRODUCT_OFF_BORDER = "#b4b9c4";
60
+ /**
61
+ * How many products are ON when a page loads (#1319): the first ones in legend (sorted-name) order.
62
+ * Always this many, on every load — the selection is never persisted.
63
+ */
64
+ export declare const PRODUCTS_ON_AT_LOAD = 2;
65
+ /** Whether a ProductTemplate paints a styled box or restyles a contextual node's kind statement. */
66
+ export declare enum ProductTemplateKind {
67
+ /** A project or service box: the page writes its whole statement. */
68
+ BOX = "box",
69
+ /** A runtime queue, clock, datastore or vendor: the page recolors its kind statement's fill. */
70
+ CONTEXT = "context"
71
+ }
72
+ /**
73
+ * What the page needs to write one node's Product-mode statement for any ON set (#1319). Data-only.
74
+ * A BOX statement is `head` + the paint's attributes + the label for its text color; a CONTEXT
75
+ * statement is `head` (the kind statement) with its fill replaced.
76
+ */
77
+ export declare class ProductTemplate {
78
+ readonly kind: ProductTemplateKind;
79
+ /** BOX: ` "id" [`. CONTEXT: the node's whole kind statement. */
80
+ readonly head: string;
81
+ /** BOX: the HTML label written in dark text; '' for CONTEXT. */
82
+ readonly darkLabel: string;
83
+ /** BOX: the HTML label written in light text; '' for CONTEXT. */
84
+ readonly lightLabel: string;
85
+ constructor(kind: ProductTemplateKind,
86
+ /** BOX: ` "id" [`. CONTEXT: the node's whole kind statement. */
87
+ head: string,
88
+ /** BOX: the HTML label written in dark text; '' for CONTEXT. */
89
+ darkLabel: string,
90
+ /** BOX: the HTML label written in light text; '' for CONTEXT. */
91
+ lightLabel: string);
92
+ }
50
93
  export declare class ProductPalette {
51
94
  /** Every declared product, sorted, each with its color. */
52
95
  readonly colors: ProductColor[];
@@ -72,13 +115,18 @@ export declare class ProductPalette {
72
115
  * Where a dependency edge crosses a product boundary (#1204), for the Product color mode: an edge
73
116
  * A -> B crosses when B does not carry every product of A, and it is drawn dashed in the color of the
74
117
  * product it crosses INTO, so the boundaries read at a glance while same-product edges stay quiet.
118
+ * Since #1319 that is the first of those products that is ON; with none ON the edge draws plain.
75
119
  * Whether a crossing is ALLOWED is the product dependency matrix's call (product-dependencies.ts):
76
120
  * with `product-tags` ON, generation already refused every crossing the matrix does not allow, so
77
121
  * every crossing drawn is an allowed one.
78
122
  */
79
123
  export declare class ProductBoundary {
80
- /** The product the edge crosses into, or null when the target carries every product of the source. */
81
- crossedInto(from: readonly string[], to: readonly string[]): string | null;
124
+ /**
125
+ * The products the edge crosses into, in the target's order: the target's products its source
126
+ * lacks, else (a target narrower than its source) every product of the target. Empty when the
127
+ * target carries every product of the source. The page draws the edge in the first one that is ON.
128
+ */
129
+ crossedInto(from: readonly string[], to: readonly string[]): string[];
82
130
  }
83
131
  /**
84
132
  * The architecture graph's product membership: exactly each project's declared `product:` tags (#1216).
@@ -18,16 +18,20 @@
18
18
  * between them. A service whose project carries no tag is in no product.
19
19
  * - ProductPalette — the Product color mode's colors (#1244): eight fixed saturated colors, each
20
20
  * product hashed (SHA-256 of its name) to a preferred one, with collisions resolved in sorted-name
21
- * order. Both viewers build it over the FULL declared product set, so they agree. Neutral means
22
- * shared by EVERY product; a dashed white box belongs to none.
21
+ * order. Both viewers build it over the FULL declared product set, so they agree, and a color is
22
+ * never reassigned when a product is toggled OFF.
23
+ * - ProductTemplate — what a page needs to paint one box in the Product color mode (#1319). Each
24
+ * product's legend row is a toggle and the page repaints in the browser from the ON set
25
+ * (WpProductPainter, graph-product-paint.client.ts), so the box's product statement is computed
26
+ * there, not here.
23
27
  * - ProductBoundary — which dependency edges cross from one product into another (#1204), drawn
24
- * dashed in the target product's color in the Product color mode.
28
+ * dashed in the target product's color in the Product color mode while that product is ON.
25
29
  *
26
30
  * Membership is stable per checkout (it depends on tags only, never on a diff), so it is written into
27
31
  * the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.
28
32
  */
29
33
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.RuntimeProductMembership = exports.ProductMembership = exports.ProductBoundary = exports.ProductPalette = exports.PRODUCT_NONE_BORDER = exports.PRODUCT_NONE_FILL = exports.PRODUCT_SHARED_FILL = exports.PRODUCT_PALETTE_SLOTS = exports.ProductPaletteSlot = exports.ProductColor = void 0;
34
+ exports.RuntimeProductMembership = exports.ProductMembership = exports.ProductBoundary = exports.ProductPalette = exports.ProductTemplate = exports.ProductTemplateKind = exports.PRODUCTS_ON_AT_LOAD = exports.PRODUCT_OFF_BORDER = exports.PRODUCT_OFF_FILL = exports.PRODUCT_NONE_BORDER = exports.PRODUCT_NONE_FILL = exports.PRODUCT_SHARED_FILL = exports.PRODUCT_PALETTE_SLOTS = exports.ProductPaletteSlot = exports.ProductColor = void 0;
31
35
  const node_crypto_1 = require("node:crypto");
32
36
  // The same two text colors graph-color-modes.ts writes on every fill. Spelled here rather than
33
37
  // imported because graph-color-modes.ts imports THIS file (for the palette), and a file-import cycle
@@ -67,11 +71,54 @@ exports.PRODUCT_PALETTE_SLOTS = [
67
71
  new ProductPaletteSlot('#8e5ea2', LIGHT_TEXT),
68
72
  new ProductPaletteSlot('#ff9da7', DARK_TEXT),
69
73
  ];
70
- /** A box shared by EVERY product. */
74
+ /** A box shared by EVERY product, drawn neutral only while every product is ON (#1319). */
71
75
  exports.PRODUCT_SHARED_FILL = '#c9ced8';
72
76
  /** A box in no product: white, with a dashed border. */
73
77
  exports.PRODUCT_NONE_FILL = '#ffffff';
74
78
  exports.PRODUCT_NONE_BORDER = '#8a90a0';
79
+ /**
80
+ * A box carrying only products switched OFF (#1319): light grey with a grey border, so it stays
81
+ * visible — hiding boxes is the Filter's job — and never reads as the neutral shared fill.
82
+ */
83
+ exports.PRODUCT_OFF_FILL = '#eef0f4';
84
+ exports.PRODUCT_OFF_BORDER = '#b4b9c4';
85
+ /**
86
+ * How many products are ON when a page loads (#1319): the first ones in legend (sorted-name) order.
87
+ * Always this many, on every load — the selection is never persisted.
88
+ */
89
+ exports.PRODUCTS_ON_AT_LOAD = 2;
90
+ /** Whether a ProductTemplate paints a styled box or restyles a contextual node's kind statement. */
91
+ var ProductTemplateKind;
92
+ (function (ProductTemplateKind) {
93
+ /** A project or service box: the page writes its whole statement. */
94
+ ProductTemplateKind["BOX"] = "box";
95
+ /** A runtime queue, clock, datastore or vendor: the page recolors its kind statement's fill. */
96
+ ProductTemplateKind["CONTEXT"] = "context";
97
+ })(ProductTemplateKind || (exports.ProductTemplateKind = ProductTemplateKind = {}));
98
+ /**
99
+ * What the page needs to write one node's Product-mode statement for any ON set (#1319). Data-only.
100
+ * A BOX statement is `head` + the paint's attributes + the label for its text color; a CONTEXT
101
+ * statement is `head` (the kind statement) with its fill replaced.
102
+ */
103
+ class ProductTemplate {
104
+ kind;
105
+ head;
106
+ darkLabel;
107
+ lightLabel;
108
+ constructor(kind,
109
+ /** BOX: ` "id" [`. CONTEXT: the node's whole kind statement. */
110
+ head,
111
+ /** BOX: the HTML label written in dark text; '' for CONTEXT. */
112
+ darkLabel,
113
+ /** BOX: the HTML label written in light text; '' for CONTEXT. */
114
+ lightLabel) {
115
+ this.kind = kind;
116
+ this.head = head;
117
+ this.darkLabel = darkLabel;
118
+ this.lightLabel = lightLabel;
119
+ }
120
+ }
121
+ exports.ProductTemplate = ProductTemplate;
75
122
  class ProductPalette {
76
123
  /** Every declared product, sorted, each with its color. */
77
124
  colors;
@@ -136,18 +183,24 @@ exports.ProductPalette = ProductPalette;
136
183
  * Where a dependency edge crosses a product boundary (#1204), for the Product color mode: an edge
137
184
  * A -> B crosses when B does not carry every product of A, and it is drawn dashed in the color of the
138
185
  * product it crosses INTO, so the boundaries read at a glance while same-product edges stay quiet.
186
+ * Since #1319 that is the first of those products that is ON; with none ON the edge draws plain.
139
187
  * Whether a crossing is ALLOWED is the product dependency matrix's call (product-dependencies.ts):
140
188
  * with `product-tags` ON, generation already refused every crossing the matrix does not allow, so
141
189
  * every crossing drawn is an allowed one.
142
190
  */
143
191
  class ProductBoundary {
144
- /** The product the edge crosses into, or null when the target carries every product of the source. */
192
+ /**
193
+ * The products the edge crosses into, in the target's order: the target's products its source
194
+ * lacks, else (a target narrower than its source) every product of the target. Empty when the
195
+ * target carries every product of the source. The page draws the edge in the first one that is ON.
196
+ */
145
197
  crossedInto(from, to) {
146
198
  if (from.length === 0 || to.length === 0)
147
- return null;
199
+ return [];
148
200
  if (from.every((product) => to.includes(product)))
149
- return null;
150
- return to.find((product) => !from.includes(product)) ?? to[0];
201
+ return [];
202
+ const added = to.filter((product) => !from.includes(product));
203
+ return added.length > 0 ? added : [...to];
151
204
  }
152
205
  }
153
206
  exports.ProductBoundary = ProductBoundary;