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:
parent
c7cb739a6d
commit
d0de376f4a
5 changed files with 65 additions and 30 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue