Big Picture header: rounded-square ⓘ left of a rounded-square Share

The dashboard header gains an info button ("What can this map do for you?") to
the left of Share. Both are rounded squares at the vertical tabs' radius
(--vtl-radius), matching the app's own roundedness; map controls stay round.

Built as a reusable, parameterized header-button form so other dashboards adopt
it by prop, not by copy:
- MapControlButton: new shape="rounded-square" (border-radius: var(--vtl-radius,8px)).
- ShareButton / MapInfoButton: a shape prop forwarded to MapControlButton;
  ShareButton also squares its rotating ring so a round ring never wraps a square.
  Defaults stay 'round' — every existing consumer is unchanged in behaviour.
- research-dashboard DashboardTopBar mounts the ⓘ and Share as one header-button
  group, sized to the bar with the 44px tap target kept for both. DOM order
  [ⓘ, Share] puts ⓘ at the logical start: left in English, right of Share in
  Arabic (the flex row reverses under rtl).
- doxa-maps-spa dashboard-plus-grid tab -> trigger:'control', so the tab's
  MapInfo is driven by the header ⓘ and the gallery's floating ⓘ is suppressed:
  exactly one ⓘ, fail-open. No new strings (reuses mapInfo.open, 11 locales).
This commit is contained in:
Doxa Maps 2026-09-30 04:25:04 +00:00
parent c7cb739a6d
commit d0de376f4a
5 changed files with 65 additions and 30 deletions

View file

@ -68,6 +68,11 @@ useShadowStyles(`
/* shape="square" on a STANDALONE button: their 4px corners, our fill and shadow kept.
A button inside .map-toolbar--square is overridden back to 0 by the toolbar's rules. */
.mcb--square{ border-radius:4px; }
/* shape="rounded-square" — the HEADER-BAR button form (design rule, 2026-09-30): map controls stay ROUND,
but a button in a dashboard HEADER is a rounded square at the SAME radius as the gallery's vertical
tabs. --vtl-radius is that ONE shared variable (VerticalTabsLayout .vtl-tab), so the header button
and the tabs can never drift; 8px is only the fallback for a context that never set it. */
.mcb--rounded-square{ border-radius:var(--vtl-radius,8px); }
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
@ -93,7 +98,7 @@ defineProps({
* strip it back off -- the two rule sets compose rather than compete, and the toolbar wins by
* being the more specific selector.
*/
shape: { type: String, default: 'round' }
shape: { type: String, default: 'round', validator: (v) => ['round', 'square', 'rounded-square'].includes(v) }
})
defineEmits(['click'])
@ -123,7 +128,7 @@ defineEmits(['click'])
-->
<button
class="mcb"
:class="{ dark: isDark, active, 'mcb--square': shape === 'square' }"
:class="{ dark: isDark, active, 'mcb--square': shape === 'square', 'mcb--rounded-square': shape === 'rounded-square' }"
:title="title"
:aria-label="title || undefined"
:disabled="disabled"

View file

@ -29,6 +29,9 @@ const { t } = useI18n()
defineProps({
isDark: { type: Boolean, default: false },
// 'round' (default) is the map-control form. 'rounded-square' is the dashboard HEADER form (the ⓘ
// sits in a header bar beside Share, not in a map's control column) — forwarded to MapControlButton.
shape: { type: String, default: 'round' },
})
// ★ AN UNANSWERED CONTROL ADDS NO CHILD TO THE COLUMN. The button renders only once a host has answered
@ -68,7 +71,7 @@ onBeforeUnmount(() => { window.removeEventListener('dxmaps:mapinfo-host', ask) }
</script>
<template>
<MapControlButton v-if="hosted" ref="btn" data-control="map-info" :is-dark="isDark" :title="t('mapInfo.open')"
<MapControlButton v-if="hosted" ref="btn" data-control="map-info" :is-dark="isDark" :shape="shape" :title="t('mapInfo.open')"
aria-haspopup="dialog" aria-expanded="false" @click="openInfo">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" />

View file

@ -45,6 +45,9 @@ useShadowStyles(`
-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;
opacity:0.6;animation:share-ring-turn 5s linear infinite;}
.share-wrap:hover::before{opacity:0.95;}
/* ROUNDED-SQUARE header form (design rule, 2026-09-30): the wrap and its rotating ring take the vertical
tabs' radius, so a circular ring can never wrap a square button. --vtl-radius is the one shared var. */
.share-wrap--rounded-square,.share-wrap--rounded-square::before{border-radius:var(--vtl-radius,8px);}
/* The ring itself rotates, so the colours travel around the button in every browser. */
@keyframes share-ring-turn{to{transform:rotate(360deg);}}
@media(prefers-reduced-motion:reduce){.share-wrap::before{animation:none;}}
@ -333,7 +336,11 @@ const { t, tm, rt } = useI18n()
const props = defineProps({
isDark: { type: Boolean, default: false },
embedPath: { type: String, default: '' }
embedPath: { type: String, default: '' },
// 'round' (default) is the map-control form. 'rounded-square' is the dashboard HEADER form: the
// trigger button AND its rainbow ring take --vtl-radius (the vertical tabs' radius) instead of 50%,
// so a header Share button matches the header ⓘ and the tabs. Every existing consumer keeps 'round'.
shape: { type: String, default: 'round' }
})
/* ---- state ---- */
@ -512,12 +519,13 @@ function bindAnchor(el) {
<template>
<!-- :ref is a FUNCTION, deliberately — it must run during render, before MapControlMenu's
mounted hook latches its area. See bindAnchor() in <script> for why onMounted is too late. -->
<div :ref="bindAnchor" class="share-wrap" data-share-root>
<div :ref="bindAnchor" class="share-wrap" :class="{ 'share-wrap--rounded-square': shape === 'rounded-square' }" data-share-root>
<MapControlButton
ref="btnEl"
data-control="share"
:is-dark="isDark"
:active="isOpen"
:shape="shape"
:title="t('buttons.share')"
@click="toggle"
>

View file

@ -67,6 +67,13 @@ export const TAB_INFO = {
},
'dashboard-plus-grid': {
storageKey: 'dashboard-plus-grid',
// ⓘ IN THE HEADER, NOT FLOATING (design rule, 2026-09-30). Big Picture draws its own ⓘ as a rounded-square
// header button beside Share (DashboardTopBar), announcing itself through MapInfoButton's control
// bridge. trigger:'control' hands this MapInfo to that header button and SUPPRESSES the floating ⓘ
// whenever it is on screen (fail-open: the grace period keeps the floating one off while the composite
// loads, so there is no flash, and it still shows if the header control never appears). The pos below
// stays as the FALLBACK placement for that floating button (a leaf without the header control).
trigger: 'control',
// top 46px is the empty END of Big Picture's filter area at every width (1 row at 1440, 2 at 1024,
// 3 at 800), but that bar is a positioned layer (z-index 55) painted above the button, which left it
// unclickable (rega, 1440). raise lifts the BUTTON over the bar; the pointer stays under it.

View file

@ -95,6 +95,10 @@ import SearchBar from '@map/components/search/SearchBar.vue'
// DashboardBreadcrumb is NOT imported here — it renders bus.filterSet, i.e. selection state, which
// this header must not show (see the ★ rule at the top). It remains exported from ./index.js.
import ShareButton from '@map/components/map-controls/ShareButton.vue'
// The header ⓘ ("What can this map do?"). It reuses the map-control MapInfoButton in its HEADER form
// (shape="rounded-square"): the same control bridge announces to the tab's MapInfo (trigger:'control'
// in doxa-maps-spa/src/tabInfo.js), which suppresses the gallery's floating ⓘ — exactly one ⓘ, fail-open.
import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
import { lbl } from '@map/composables/useFacetAggregation.js'
const dash = inject('dashboardData', null)
@ -216,24 +220,26 @@ useShadowStyles(`
.rd-topbar .rd-ttl{ font-weight:800; font-size:var(--fs-lg,13px); letter-spacing:.2px; color:var(--rd-header-ink,#f3f3f1); }
.rd-topbar .rd-ttl .dot{ color:var(--rd-accent,#e67e22); }
.rd-topbar .rd-spacer{ flex:1; }
/* SHARE in this bar (reported 2026-09-28: "too large"). ShareButton is the map toolbar's 40px round button
(36px under 768px) + a 2px rainbow ring, so it measured 44px in a 38px bar: taller than the bar itself.
Here it takes the bar's own control scale instead: 28px + the ring = 32px, the search box's height, with the
icon at the search icon's 15px. Scoped to .rd-topbar, so the map toolbars keep their 40px button. */
.rd-topbar .share-wrap .mcb{ width:28px; height:28px; position:relative; }
.rd-topbar .share-wrap .mcb svg{ width:15px; height:15px; }
/* ★ THE BUTTON GOT SMALLER TO LOOK RIGHT; THE THING A FINGER HAS TO HIT DID NOT HAVE TO. Shrinking the
control to 28px (32 with the ring) also shrank the TAP TARGET to 32px, and this repo states its own
floor in two places — DataTable.vue:294 "44px is the accessible tap target; anything smaller is a miss
on a phone", and LegendDesktop.vue:124 sizing rows to keep it. A transparent box centred on the button
restores the target without moving a pixel of the drawing: it is absolutely positioned, so it changes
no layout, and it is part of the button, so it takes the same clicks.
★ 44 WIDE, 38 TALL, AND THE SHORTFALL IS DELIBERATE RATHER THAN OVERLOOKED. The bar is 38px
(--rd-topbar-h), so 44 vertical cannot fit inside it: the extra 3px each side would hang over the
filters row below and start eating taps meant for a filter — trading one miss for another. 44x38 is
the largest target that costs nothing. Closing the last 6px needs a taller bar, which is a look
decision already made in the other direction, so it is asked rather than assumed. */
.rd-topbar .share-wrap .mcb::after{
/* HEADER BUTTONS — the ⓘ + Share, rounded squares at the vertical tabs' radius (design rule, 2026-09-30).
The rounded-square SHAPE is not set here: it comes from each button's shape="rounded-square" prop
(library MapControlButton → border-radius:var(--vtl-radius)), so the header button and the gallery's
tabs share ONE radius variable and can never drift. This bar only sizes them to its own control scale
and restores the tap target — the same treatment the Share button already had, now covering both so
the ⓘ matches it exactly. Scoped to .rd-topbar, so the map toolbars keep their 40px round buttons.
Reported 2026-09-28 ("too large"): ShareButton is a 40px round map control + a 2px rainbow ring, 44px
in a 38px bar. Here both take 28px (+ Share's ring = 32px, the search box's height), icon 16px. */
.rd-topbar .rd-header-btns{ display:inline-flex; align-items:center; gap:8px; flex:0 0 auto; }
.rd-topbar .rd-header-btns .mcb{ --map-btn-size:28px; position:relative; }
.rd-topbar .rd-header-btns .mcb svg{ width:16px; height:16px; }
/* ★ THE BUTTONS ARE 28px TO LOOK RIGHT; THE THING A FINGER HAS TO HIT IS NOT. This repo states its own
44px floor twice — DataTable.vue:294 ("44px is the accessible tap target; anything smaller is a miss
on a phone") and LegendDesktop.vue:124. A transparent box centred on each button restores the target
without moving a pixel of the drawing: absolutely positioned, so it changes no layout, and part of the
button, so it takes the same clicks.
★ 44 WIDE, 38 TALL, AND THE SHORTFALL IS DELIBERATE. The bar is 38px (--rd-topbar-h), so 44 vertical
cannot fit inside it: the extra 3px each side would hang over the filters row below and start eating
taps meant for a filter — trading one miss for another. 44x38 is the largest target that costs nothing. */
.rd-topbar .rd-header-btns .mcb::after{
content:''; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
width:44px; height:var(--rd-topbar-h,38px);
}
@ -340,12 +346,18 @@ useShadowStyles(`
<!-- CSV export MOVED to the people-groups table header (DashboardDataTable), below the map —
card BUILD-CSV-EXPORT-MOVE-TO-TABLE-HEADER-ALL-FIELDS. It exports THAT table, so it now
lives on that table's header bar, which stays docked even when the table is collapsed. -->
<!-- SHARE the WHOLE dashboard (decided 2026-09-20): "if we could actually embed this on a
partner site from one share button... they can embed this map on all their partner
websites." Reuses the SAME ShareButton the upg-100 MFE ships (no hand-rolled variant); the
embed path is THIS profile's tree-relative leaf, which ShareButton joins to the served tree
root — so the snippet it hands out works from whatever host the tree was dropped on. It
sits where the CSV button used to be, the top-right of the dashboard. -->
<ShareButton :embed-path="SHARE_EMBED_PATH" />
<!-- HEADER BUTTONS (design rule, 2026-09-30): a rounded-square ⓘ to the LEFT of a rounded-square Share.
DOM order [ⓘ, Share] gives ⓘ at the logical START — left in English, and RIGHT of Share in
Arabic, because this flex row reverses its main axis under dir=rtl. Both are the SAME reusable
header-button form, shape="rounded-square" (radius = the vertical tabs' --vtl-radius).
SHARE the WHOLE dashboard (decided 2026-09-20): "if we could actually embed this on a partner
site from one share button." Reuses the SAME ShareButton the upg-100 MFE ships (no hand-rolled
variant); the embed path is THIS profile's tree-relative leaf, which ShareButton joins to the
served tree root. The ⓘ reuses MapInfoButton — it renders ONLY once the tab's MapInfo answers
(fail-open: no host, no ⓘ, same as before), so a standalone leaf with no gallery is unchanged. -->
<div class="rd-header-btns" data-header-btns>
<MapInfoButton shape="rounded-square" />
<ShareButton :embed-path="SHARE_EMBED_PATH" shape="rounded-square" />
</div>
</header>
</template>