@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.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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
|
|
385
|
-
// repeat focus with
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
685
|
+
for (const [p, item] of nextBaseStyles) {
|
|
678
686
|
const prev = focusedPathStyles.get(p);
|
|
679
|
-
|
|
680
|
-
|
|
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,
|
|
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,
|
|
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
|
|
781
|
-
//
|
|
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
|
|
784
|
-
if (!
|
|
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
|
-
|
|
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
|
|
1560
|
-
|
|
1561
|
-
|
|
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((
|
|
1609
|
+
String(highlightWidthFor(item) / strokeDivisor()),
|
|
1575
1610
|
);
|
|
1576
|
-
pathEl.
|
|
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
|
|
1598
|
-
|
|
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
|
|
1604
|
-
if (
|
|
1640
|
+
const focusItem = focusedPathStyles.get(hoveredEl);
|
|
1641
|
+
if (focusItem == null) {
|
|
1605
1642
|
restoreDefaultStroke(hoveredEl);
|
|
1606
1643
|
} else {
|
|
1607
|
-
// Restore the focused
|
|
1608
|
-
|
|
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