@cfasim-ui/docs 0.7.0 → 0.7.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.
@@ -482,6 +482,12 @@ shows that feature's tooltip. Users can keep exploring around the focused
482
482
  area; the built-in **reset** button clears focus and snaps back to the full
483
483
  extent.
484
484
 
485
+ Each focus target's outline can be styled by passing a `FocusItem` object:
486
+ `style` picks the dash pattern (`"solid" | "dashed" | "dotted"`), `stroke`
487
+ the color, and `strokeWidth` the width in CSS px. In-place highlights
488
+ default to pure black/white following the theme (`light-dark(#000, #fff)`);
489
+ cross-geoType overlays default to white.
490
+
485
491
  Set `:focus-zoom="false"` to highlight (and draw cross-geoType overlays)
486
492
  **without** panning or zooming — useful for a click-to-select interaction
487
493
  where the map should stay put while a side panel shows the details.
@@ -90,10 +90,14 @@ export interface FocusItem {
90
90
  * `"dashed"` uses long dashes; `"dotted"` uses small round dots —
91
91
  * useful when stacking multiple outlines of different geoTypes. */
92
92
  style?: FocusStyle;
93
- /** Stroke color for the outline. Applies to cross-geoType overlay
94
- * paths only (base-geoType highlights stay at the default focus
95
- * color). Default: `"#fff"`. */
93
+ /** Stroke color for the outline. In-place highlights (items in the
94
+ * base geoType) default to pure black/white following the theme
95
+ * (`light-dark(#000, #fff)`); cross-geoType overlay paths default to
96
+ * `"#fff"`. */
96
97
  stroke?: string;
98
+ /** Outline width in CSS px. Defaults to the map's stroke width + 1
99
+ * for in-place highlights and + 1.5 for cross-geoType overlays. */
100
+ strokeWidth?: number;
97
101
  }
98
102
 
99
103
  export type FocusValue = string | FocusItem | Array<string | FocusItem> | null;
@@ -381,14 +385,18 @@ let hoveredEl: SVGPathElement | null = null;
381
385
  // Paths currently styled as focused. Tracked separately from hover so the
382
386
  // two states compose: hovering a focused path keeps the highlight on
383
387
  // un-hover, and clearing focus while still hovering keeps the hover style.
384
- // Maps each focused base-geoType path to the style it was given so a
385
- // repeat focus with a different style can re-apply without diffing the
388
+ // Maps each focused base-geoType path to the FocusItem that styled it so
389
+ // a repeat focus with different styling can re-apply without diffing the
386
390
  // attribute set manually.
387
- const focusedPathStyles = new Map<SVGPathElement, FocusStyle>();
391
+ const focusedPathStyles = new Map<SVGPathElement, FocusItem>();
388
392
  // Cross-geoType focus items render as standalone outline paths layered on
389
393
  // top of the base map. Keyed by `${geoType}:${id}` so we can diff add /
390
- // remove / restyle on each applyFocus.
391
- const overlayPathEls = new Map<string, SVGPathElement>();
394
+ // remove / restyle on each applyFocus. `strokeWidth` (visual CSS px) is
395
+ // kept alongside so applyStrokeScale can re-compensate custom widths.
396
+ const overlayPathEls = new Map<
397
+ string,
398
+ { el: SVGPathElement; strokeWidth?: number }
399
+ >();
392
400
  let isZooming = false;
393
401
  let tooltipObserver: ResizeObserver | null = null;
394
402
  const lastTooltipSize = { width: 0, height: 0 };
@@ -665,22 +673,27 @@ function applyFocus() {
665
673
 
666
674
  // Diff base-geoType highlights, keyed by path element so we can
667
675
  // restyle on style change without churning unrelated paths.
668
- const nextBaseStyles = new Map<SVGPathElement, FocusStyle>();
676
+ const nextBaseStyles = new Map<SVGPathElement, FocusItem>();
669
677
  for (const r of baseResolved) {
670
678
  const p = pathsByFeatureId.get(String(r.feature.id));
671
- if (p) nextBaseStyles.set(p, r.item.style ?? "solid");
679
+ if (p) nextBaseStyles.set(p, r.item);
672
680
  }
673
681
  for (const [p] of focusedPathStyles) {
674
682
  if (nextBaseStyles.has(p) || p === hoveredEl) continue;
675
683
  restoreDefaultStroke(p);
676
684
  }
677
- for (const [p, style] of nextBaseStyles) {
685
+ for (const [p, item] of nextBaseStyles) {
678
686
  const prev = focusedPathStyles.get(p);
679
- if (prev === style && p !== hoveredEl) continue; // already styled
680
- if (p !== hoveredEl) applyHighlightStroke(p, style);
687
+ const unchanged =
688
+ prev != null &&
689
+ prev.style === item.style &&
690
+ prev.stroke === item.stroke &&
691
+ prev.strokeWidth === item.strokeWidth;
692
+ if (unchanged && p !== hoveredEl) continue; // already styled
693
+ if (p !== hoveredEl) applyHighlightStroke(p, item);
681
694
  }
682
695
  focusedPathStyles.clear();
683
- for (const [p, style] of nextBaseStyles) focusedPathStyles.set(p, style);
696
+ for (const [p, item] of nextBaseStyles) focusedPathStyles.set(p, item);
684
697
 
685
698
  // Cross-geoType outlines render as non-interactive paths on top of
686
699
  // the base layer.
@@ -766,9 +779,9 @@ function syncOverlayPaths(items: ResolvedFocus[]) {
766
779
  if (!g) return;
767
780
 
768
781
  const nextKeys = new Set(items.map((i) => i.key));
769
- for (const [key, el] of overlayPathEls) {
782
+ for (const [key, entry] of overlayPathEls) {
770
783
  if (!nextKeys.has(key)) {
771
- el.remove();
784
+ entry.el.remove();
772
785
  overlayPathEls.delete(key);
773
786
  }
774
787
  }
@@ -777,25 +790,29 @@ function syncOverlayPaths(items: ResolvedFocus[]) {
777
790
  // Overlay strokes need extra weight: they paint on top of the base map
778
791
  // and the (optional) state-borders mesh, so a stroke that matches the
779
792
  // in-place focused base path's weight would visually merge with the
780
- // layers underneath. The width itself lives on the overlay *group*
781
- // (compensated by applyStrokeScale); the paths only carry color/dash.
793
+ // layers underneath. The default width lives on the overlay *group*;
794
+ // per-item `strokeWidth` overrides are written per path — both
795
+ // compensated by applyStrokeScale.
782
796
  for (const { item, feature: f, key } of items) {
783
- let el = overlayPathEls.get(key);
784
- if (!el) {
785
- el = document.createElementNS(SVG_NS, "path") as SVGPathElement;
797
+ let entry = overlayPathEls.get(key);
798
+ if (!entry) {
799
+ const el = document.createElementNS(SVG_NS, "path") as SVGPathElement;
786
800
  el.setAttribute("d", generator(f) ?? "");
787
801
  el.setAttribute("fill", "none");
788
802
  el.setAttribute("pointer-events", "none");
789
803
  el.setAttribute("stroke-linejoin", "round");
790
804
  el.setAttribute("class", "focus-overlay");
791
805
  g.appendChild(el);
792
- overlayPathEls.set(key, el);
806
+ entry = { el };
807
+ overlayPathEls.set(key, entry);
793
808
  }
809
+ entry.strokeWidth = item.strokeWidth;
794
810
  // White contrasts cleanly against the (typically dark) data-colored
795
811
  // fill; callers can override per-item via `FocusItem.stroke`.
796
- el.setAttribute("stroke", item.stroke ?? "#fff");
797
- applyDasharray(el, item.style);
812
+ entry.el.setAttribute("stroke", item.stroke ?? "#fff");
813
+ applyDasharray(entry.el, item.style);
798
814
  }
815
+ applyStrokeScale();
799
816
  }
800
817
 
801
818
  function resetZoom() {
@@ -1550,37 +1567,56 @@ function strokeDivisor(): number {
1550
1567
  return scaleK.value * viewScale.value || 1;
1551
1568
  }
1552
1569
 
1570
+ // Highlight (hover + focus) outline color: pure black on light, pure
1571
+ // white on dark, following the theme's color-scheme (the same
1572
+ // light-dark() mechanism the theme tokens use). Applied via inline style
1573
+ // — SVG presentation attributes don't parse CSS functions.
1574
+ const HIGHLIGHT_STROKE = "light-dark(#000, #fff)";
1575
+
1576
+ /** Visual outline width for an in-place highlight (hover or focus). */
1577
+ function highlightWidthFor(item?: FocusItem): number {
1578
+ return item?.strokeWidth ?? effectiveStrokeWidth.value + 1;
1579
+ }
1580
+
1553
1581
  function applyStrokeScale() {
1554
1582
  const d = strokeDivisor();
1555
1583
  const eff = effectiveStrokeWidth.value;
1556
1584
  baseGroupRef.value?.setAttribute("stroke-width", String(eff / d));
1557
1585
  bordersPathEl?.setAttribute("stroke-width", String(1 / d));
1558
1586
  overlayGroupRef.value?.setAttribute("stroke-width", String((eff + 1.5) / d));
1559
- const hw = String((eff + 1) / d);
1560
- for (const [p] of focusedPathStyles) p.setAttribute("stroke-width", hw);
1561
- if (hoveredEl) hoveredEl.setAttribute("stroke-width", hw);
1587
+ for (const { el, strokeWidth } of overlayPathEls.values()) {
1588
+ if (strokeWidth != null) {
1589
+ el.setAttribute("stroke-width", String(strokeWidth / d));
1590
+ } else {
1591
+ el.removeAttribute("stroke-width");
1592
+ }
1593
+ }
1594
+ for (const [p, item] of focusedPathStyles) {
1595
+ p.setAttribute("stroke-width", String(highlightWidthFor(item) / d));
1596
+ }
1597
+ if (hoveredEl && !focusedPathStyles.has(hoveredEl)) {
1598
+ hoveredEl.setAttribute("stroke-width", String(highlightWidthFor() / d));
1599
+ }
1562
1600
  }
1563
1601
 
1564
- function applyHighlightStroke(
1565
- pathEl: SVGPathElement,
1566
- style: FocusStyle = "solid",
1567
- ) {
1602
+ function applyHighlightStroke(pathEl: SVGPathElement, item?: FocusItem) {
1568
1603
  // Bring path to top so its thicker border isn't clipped by neighbors.
1569
1604
  // Skip for overlay paths (they live above everything and own their
1570
1605
  // own z-order via syncOverlayPaths).
1571
1606
  pathEl.parentNode?.appendChild(pathEl);
1572
1607
  pathEl.setAttribute(
1573
1608
  "stroke-width",
1574
- String((effectiveStrokeWidth.value + 1) / strokeDivisor()),
1609
+ String(highlightWidthFor(item) / strokeDivisor()),
1575
1610
  );
1576
- pathEl.setAttribute("stroke", "#555");
1577
- applyDasharray(pathEl, style);
1611
+ pathEl.style.stroke = item?.stroke ?? HIGHLIGHT_STROKE;
1612
+ applyDasharray(pathEl, item?.style);
1578
1613
  }
1579
1614
 
1580
1615
  function restoreDefaultStroke(pathEl: SVGPathElement) {
1581
1616
  // Dropping the attribute lets the path inherit the compensated width
1582
1617
  // from the base group again.
1583
1618
  pathEl.removeAttribute("stroke-width");
1619
+ pathEl.style.stroke = "";
1584
1620
  pathEl.setAttribute("stroke", props.strokeColor);
1585
1621
  pathEl.removeAttribute("stroke-dasharray");
1586
1622
  pathEl.removeAttribute("stroke-linecap");
@@ -1594,18 +1630,20 @@ function setHover(pathEl: SVGPathElement) {
1594
1630
  restoreDefaultStroke(hoveredEl);
1595
1631
  }
1596
1632
  hoveredEl = pathEl;
1597
- // Hover style follows whatever focus style is in effect (or solid).
1598
- applyHighlightStroke(pathEl, focusedPathStyles.get(pathEl) ?? "solid");
1633
+ // Hover styling follows whatever focus styling is in effect (or the
1634
+ // defaults).
1635
+ applyHighlightStroke(pathEl, focusedPathStyles.get(pathEl));
1599
1636
  }
1600
1637
 
1601
1638
  function clearHover() {
1602
1639
  if (hoveredEl) {
1603
- const focusStyle = focusedPathStyles.get(hoveredEl);
1604
- if (focusStyle == null) {
1640
+ const focusItem = focusedPathStyles.get(hoveredEl);
1641
+ if (focusItem == null) {
1605
1642
  restoreDefaultStroke(hoveredEl);
1606
1643
  } else {
1607
- // Restore the focused style (in case hover overwrote a dashed item).
1608
- applyHighlightStroke(hoveredEl, focusStyle);
1644
+ // Restore the focused styling (in case hover overwrote a dashed
1645
+ // or custom-styled item).
1646
+ applyHighlightStroke(hoveredEl, focusItem);
1609
1647
  }
1610
1648
  hoveredEl = null;
1611
1649
  emit("stateHover", null);
package/index.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.0",
2
+ "version": "0.7.1",
3
3
  "package": "@cfasim-ui/docs",
4
4
  "content": {
5
5
  "components": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfasim-ui/docs",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "LLM-friendly component and chart documentation for cfasim-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {