Some checks are pending
build-gate / build (push) Waiting to run
The first tab a new user meets is now the poster/resources pane, not the map. That pane already explains what a UPG-100 list is at the top and shows the posters, so it needs no explanation; a choropleth of zones does. Maps need not be primary — a familiar list or grid is the on-ramp people use before they transition into the map. Renames the tab from "Prayer Lists" to "Prayer Resources". Pane order lives in at[0], not in array order: buildPaneColumn sorts by at[0] while ViewSelect renders the array verbatim, so both are swapped together or the tab strip desyncs from the scroll column. The active view is now derived from that same sort instead of a hardcoded 'map'. In the wrapper shell the map pane is resolved by PROFILE rather than by "pane 0 is the map": under the positional rule, reordering mounted the map element into the poster pane, and "Show on map" scrolled to the wall you clicked from. The shell's tab strip also resets to a profile's first view on switch, since a stale 'map' id is valid in nearly every profile. ViewSelect keeps each tab on one line (nowrap + ellipsis), ported from the shell's own mobile rule that the original extraction left behind. Measured at 390px: "Prayer Resources" wrapped inside its button and grew the strip 45 -> 60px; it now holds 45px at every width from 1280 down to 390.
101 lines
5 KiB
Vue
101 lines
5 KiB
Vue
<!--
|
|
ViewSelect.vue — the "SELECT VIEW" switcher: a fieldset + legend wrapping a row of
|
|
underline-indicator tabs.
|
|
|
|
THE CANONICAL SOURCE is the app shell, layouts/app-wrapper-material/index.html
|
|
(`.viewsel` / `.seg`, a PrimeVue-style Fieldset). This component is that control
|
|
extracted VERBATIM — same structure (semantic <fieldset>/<legend>), same metrics
|
|
(8px radius, 10px/700/.7px-tracking uppercase legend, 13px/500 buttons with
|
|
6px 12px 7px padding), same 2px accent underline on the pressed tab.
|
|
|
|
WHY IT LIVES IN THE LIBRARY. It had two independent implementations before this
|
|
file existed — the shell's vanilla one and a hand-copied Vue port inside
|
|
my-upg-100-list/upg-100-mfe.vue — and a third consumer (regions-dashboard) was
|
|
about to copy it again. A control with three consumers and no map-specific
|
|
assumptions is what this library is for; a fourth divergent copy is not.
|
|
|
|
PARAMETERIZED, per library/components/WARNING.md — it takes props, not assumptions:
|
|
it knows nothing about maps, panes, bundles or scrolling. It renders the views it
|
|
is handed and reports which one was clicked. Positioning is the CALLER's job:
|
|
this component ships no `position`/`top`/`left`, so a header that wants it
|
|
absolutely centred styles `.viewsel` itself (as both the shell and the MFE do).
|
|
|
|
Props:
|
|
views — [{ id, label }, …] the views to offer, in order
|
|
current — id of the active view
|
|
legend — the fieldset's legend text (default "Select View")
|
|
label — accessible name for the tablist (default "Views")
|
|
Emits:
|
|
select(id) — a view's tab was clicked
|
|
-->
|
|
<script setup>
|
|
import { useShadowStyles } from '../composables/useShadowStyles.js'
|
|
|
|
defineProps({
|
|
views: { type: Array, default: () => [] },
|
|
current: { type: String, default: '' },
|
|
legend: { type: String, default: 'Select View' },
|
|
label: { type: String, default: 'Views' },
|
|
})
|
|
|
|
defineEmits(['select'])
|
|
|
|
// Child components' <style> blocks are dropped by defineCustomElement, so the CSS
|
|
// goes through useShadowStyles like every other library component.
|
|
//
|
|
// NO BACKTICKS ANYWHERE IN THIS STRING, including in comments: one stray backtick
|
|
// terminates the JS template literal and the build stops compiling. Hence string
|
|
// concatenation rather than one template literal (house style, AGENT-BRIEF.md).
|
|
useShadowStyles(
|
|
// Metrics copied from the shell's .viewsel; colours come from the ui-* custom
|
|
// properties the host element already declares, with the material-light palette
|
|
// as the fallback so this renders correctly even outside a themed host.
|
|
'.viewsel{ margin:0; padding:0 10px 2px;' +
|
|
' border:1px solid var(--ui-border,#d1d4cd); border-radius:8px;' +
|
|
' background:var(--ui-bg,#ffffff); }' +
|
|
// Legend default alignment is inline-start; the shell centres it.
|
|
'.viewsel > legend{ margin:0 auto; padding:0 6px; font-size:10px;' +
|
|
' text-transform:uppercase; letter-spacing:.7px;' +
|
|
' color:var(--ui-text-muted,#6d766e); font-weight:700; }' +
|
|
// Slim UNDERLINE indicator, not a filled button.
|
|
'.seg{ display:flex; gap:2px; }' +
|
|
'.seg button{ position:relative; display:flex; align-items:center; gap:6px;' +
|
|
' border:0; background:none; color:var(--ui-text-muted,#6d766e);' +
|
|
' padding:6px 12px 7px; font-size:13px; font-weight:500; cursor:pointer;' +
|
|
' transition:color .15s; font-family:inherit;' +
|
|
// ONE LINE PER TAB. Ported from the shell's own mobile rule (app-wrapper-material
|
|
// index.html, max-width:960px: white-space:nowrap + ellipsis) — the extraction that
|
|
// created this component copied the DESKTOP metrics verbatim but left that hardening
|
|
// behind, so the leaf and the shell disagreed on narrow viewports.
|
|
// MEASURED at 390px with the label "Prayer Resources": the fieldset is width-capped
|
|
// by the header, the TEXT wrapped inside the button (28 -> 43px) and the strip grew
|
|
// 45 -> 60px, while the shell held 45px. The row of tabs never wrapped; only the
|
|
// words inside one did. Ellipsis is the better failure than a taller header.
|
|
' white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }' +
|
|
'.seg button::after{ content:""; position:absolute; left:8px; right:8px; bottom:0;' +
|
|
' height:2px; border-radius:2px 2px 0 0; background:transparent;' +
|
|
' transition:background .15s; }' +
|
|
'.seg button:hover{ color:var(--ui-text,#3b463d); }' +
|
|
'.seg button:hover::after{ background:var(--ui-border,#d1d4cd); }' +
|
|
'.seg button[aria-pressed="true"]{ color:var(--ui-primary,#92b195); font-weight:600; }' +
|
|
'.seg button[aria-pressed="true"]::after{ background:var(--ui-primary,#92b195); }',
|
|
'view-select'
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<fieldset class="viewsel">
|
|
<legend>{{ legend }}</legend>
|
|
<div class="seg" role="tablist" :aria-label="label">
|
|
<button
|
|
v-for="v in views"
|
|
:key="v.id"
|
|
type="button"
|
|
role="tab"
|
|
:aria-pressed="current === v.id ? 'true' : 'false'"
|
|
:aria-selected="current === v.id ? 'true' : 'false'"
|
|
@click="$emit('select', v.id)"
|
|
>{{ v.label }}</button>
|
|
</div>
|
|
</fieldset>
|
|
</template>
|