From 094647ebee6f502bae2eea365b06578e7353c58b Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 22:05:03 +0000 Subject: [PATCH] C11: one legend collapse caret, and it says whether the legend is open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three copies of the same control: SemanticTreeLegend's .stl-collapse-btn, and two in LegendDesktop — the data-view caret and the detail-mode button, whose own comment says it "Matches SemanticTreeLegend's .stl-collapse-btn look". It matched the look. A control is not its look, and the copies lost both of the parts that do not render: · aria-expanded. A disclosure control that never reports its state leaves a screen-reader user with a button called "toggle legend" and no way to learn whether the legend is open. This is the same defect I fixed in LegendMobile last week, in a component sitting in the same folder as the fix. · the RTL arrow. STL carries .stl-collapse-btn:dir(rtl) svg{rotate(0deg)} because a panel that slides off the other edge needs its caret to point the other way. LegendDesktop has no :dir(rtl) rule at all, so in Arabic its caret points AT the panel it would open. The label also now names the ACTION rather than the control: collapseLegend / expandLegend instead of a fixed "toggle legend". All three keys were already translated in all eleven locales, so this adds no translation debt. THE ARROW AND THE ARIA COME FROM ONE PROP. They drifted apart because they were two independent pieces of markup that happened to agree; `open` now drives both, so a future copy cannot get one right and the other wrong. STATE STAYS WITH THE CALLER. Two consumers drive collapse from uiStore's tri-state cascade (fullyOpen -> open -> collapsed) and one from a local boolean. A shared control that quietly adopted either would have broken the other, so this one owns no state and no persistence — it renders what it is told and emits the intent. TWO THINGS DELIBERATELY UNCHANGED. The detail-mode control was an INVISIBLE 28x28 hit target — transparent fill, no border, transparent colour — over the panel's corner; making it suddenly visible is a look change nobody asked for, so the slot keeps the position, the size and the invisibility, and only the semantics improve. And LegendDesktop's four light/dark theme rules stay in LegendDesktop: they are its themes, not the control's. They outrank the shared base on specificity (0,3,0 and 0,2,0 against 0,1,0), so which sheet is injected first does not decide the colour. Caught while writing it: the first draft styled the caret with diff --git a/library/components/legends/LegendDesktop.vue b/library/components/legends/LegendDesktop.vue index 1c8ee13f1..3db1899fb 100644 --- a/library/components/legends/LegendDesktop.vue +++ b/library/components/legends/LegendDesktop.vue @@ -13,6 +13,7 @@ import { retryableAsyncComponent } from '../../composables/retryableAsyncCompone import LegendRows from './LegendRows.vue' import PeopleGroupDetail from '../PeopleGroupDetail.vue' import ApproxPinNote from './ApproxPinNote.vue' +import LegendCollapseCaret from './LegendCollapseCaret.vue' import { buildColocationIndex, colocatedWith } from '../../utils/colocation.js' // Async-import the heavier branches so the boot bundle stays lean. const LegendMobile = retryableAsyncComponent(() => import('./LegendMobile.vue')) @@ -68,11 +69,6 @@ useShadowStyles(` LegendRows. Matches SemanticTreeLegend's .stl-collapse-btn look: 22x20px, rounded-rect, subtle dark bg + border, right-chevron SVG that rotates 180deg when expanded. */ -.legend-collapse-caret{background:rgba(110,118,129,0.15);border:1px solid #30363d;border-radius:5px;color:#8b949e;cursor:pointer;width:22px;height:20px;flex-shrink:0;display:flex;align-items:center;justify-content:center;transition:color 0.1s,background 0.1s,transform 0.18s;outline:none;-webkit-tap-highlight-color:transparent;padding:0;} -.legend-collapse-caret:focus{outline:none;} -.legend-collapse-caret:hover{color:#c9d1d9;background:rgba(59,70,61,0.18);border-color:#73A17F;} -.legend-collapse-caret svg{transition:transform 0.18s;transform:rotate(180deg);} -.legend-collapse-caret.rotated svg{transform:rotate(0deg);} /* Light mode caret — mirrors STL's [data-theme="light"] .stl-collapse-btn */ .legend-container:not(.legend-dark) .legend-collapse-caret{background:rgba(208,215,222,0.4);border-color:#d0d7de;color:#57606a;} .legend-container:not(.legend-dark) .legend-collapse-caret:hover{background:rgba(59,70,61,0.12);border-color:#3b463d;color:#3b463d;} @@ -86,8 +82,14 @@ useShadowStyles(` above the PeopleGroupDetail sticky header (.detail-header z-index:10) so it is always tappable even when the detail panel scrolls. The button is only rendered in the detail-mode template branch (see