Phones: one top bar on a website too; maps fill a sized host, so the legend is never cut off
- WEB MODE GETS THE TOP BAR BACK. The corner ☰ with no header was rejected on a real phone. Both shell modes now show the one top bar on a phone, the ☰ at its start and the language at its end; web mode's bar takes no camera inset (the shell is not at the top of the screen in a website). The corner circle and the drawer-foot language are gone. check:shell-header renders both modes (13 planted defects). - THE FLOOR NEVER EXCEEDS A SIZED HOST. ProfileLoader's 100svh floor is for a host sized by its content (the scroll-page leaf). Inside a host with a definite height (DX Life's map slot, overflow hidden) it made the map taller than the host, which clipped the map's bottom and its legend footer, and tied the map's height to the window's. ProfileLoader now MEASURES the host (the floor at 0 and at a huge value: a sized host keeps its height) and drops the floor inside a sized one, re-measuring when the host resizes (a ResizeObserver on the host, not a window resize). - LegendDesktop caps itself at the map's height (100% - 80px), not the window's (100vh - 80px).
This commit is contained in:
parent
443d2efdaa
commit
d533431997
4 changed files with 72 additions and 64 deletions
|
|
@ -58,9 +58,9 @@ async function audit({ vtl = VTL, menu = MENU, langs = LANGS } = {}) {
|
|||
// locale detection reads <html lang>; SSR has no document, so the render gets a one-field stand-in.
|
||||
if (typeof globalThis.document === 'undefined') globalThis.document = { documentElement: { lang: 'en' } }
|
||||
const Layout = (await server.ssrLoadModule('/' + vtl)).default
|
||||
// shellMode (resolved into data.mode at mount): 'app' owns the screen and gets the bar; 'web' sits under a
|
||||
// host's own site header and gets NO bar, a corner ☰ (with its --map-fab-* footprint) and the language at
|
||||
// the drawer's foot. The drawer is shut (navOpen false), as mounted() leaves it on a phone.
|
||||
// shellMode (resolved into data.mode at mount): BOTH modes get the one top bar on a phone (☰ at the start, the
|
||||
// language at the end); web mode's bar takes no camera inset (.is-web). No corner circle in either mode
|
||||
// (a corner ☰ with no header was rejected on a real phone). The drawer is shut, as on a phone at load.
|
||||
for (const mode of ['app', 'web']) for (const narrow of [true, false]) for (const header of [true, false]) {
|
||||
// A copy of the real layout whose data says `narrow` and `mode` (in a browser mounted() reads them from
|
||||
// matchMedia and the host, which SSR never runs). A copy, not `extends`: the render function is not inherited.
|
||||
|
|
@ -70,17 +70,16 @@ async function audit({ vtl = VTL, menu = MENU, langs = LANGS } = {}) {
|
|||
'rail-end': () => h('i', { class: 'probe-rail-end' }) }) })
|
||||
const html = (await renderToString(app)).replace(/<!--[\s\S]*?-->/g, '') // template comments are not markup
|
||||
const tag = `mode=${mode} narrow=${narrow} header=${header}`
|
||||
// The bar's class list, in whatever order Vue renders it (a bound class can come first).
|
||||
const pbar = (html.match(/<div class="([^"]*\bvtl-pbar\b[^"]*)"/) || [])[1] || null
|
||||
const phoneNoHeader = narrow && !header
|
||||
const wantBar = phoneNoHeader && mode === 'app', wantFab = phoneNoHeader && mode === 'web', wantRail = !narrow && !header
|
||||
if (/class="vtl-pbar"/.test(html) !== wantBar) fail(`the phone bar ${wantBar ? 'is missing' : 'renders'} at ${tag}`)
|
||||
if (/probe-phone-end/.test(html) !== phoneNoHeader) fail(`#phone-end ${phoneNoHeader ? 'is not filled' : 'renders'} at ${tag}`)
|
||||
if (/vtl-drawer-end/.test(html) !== wantFab) fail(`the language at the drawer's foot ${wantFab ? 'is missing' : 'renders'} at ${tag}`)
|
||||
// The foot is margin-top:auto in a FLEX column (.has-rail-end); in a block nav it sits under the last row.
|
||||
if (wantFab && !/<nav class="[^"]*\bhas-rail-end\b[^"]*"/.test(html)) fail(`the drawer is not a flex column, so its language sits under the last row, not at the foot, at ${tag}`)
|
||||
const wantBar = phoneNoHeader, wantRail = !narrow && !header
|
||||
if (!!pbar !== wantBar) fail(`the phone bar ${wantBar ? 'is missing' : 'renders'} at ${tag}`)
|
||||
if (/probe-phone-end/.test(html) !== wantBar) fail(`#phone-end ${wantBar ? 'is not filled' : 'renders'} at ${tag}`)
|
||||
if (wantBar && /\bis-web\b/.test(pbar || '') !== (mode === 'web')) fail(`the bar ${mode === 'web' ? 'keeps the camera inset in web mode (.is-web missing)' : 'drops the camera inset in app mode'} at ${tag}`)
|
||||
if (/class="vtl-rail-end"/.test(html) !== wantRail || /probe-rail-end/.test(html) !== wantRail) fail(`#rail-end ${wantRail ? 'is missing' : 'renders'} at ${tag}`)
|
||||
if (wantBar && !/class="vtl-phone-menu"[^>]*aria-expanded=/.test(html)) fail(`the bar's ☰ has no aria-expanded at ${tag}`)
|
||||
if (/class="vtl-fab"/.test(html) !== wantFab) fail(`the corner ☰ ${wantFab ? 'is missing' : 'renders'} at ${tag}`)
|
||||
if (/--map-fab-/.test(html) !== wantFab) fail(`the corner ☰'s --map-fab-* footprint ${wantFab ? 'is not published' : 'is published'} at ${tag}`)
|
||||
if (/vtl-fab|--map-fab-|vtl-drawer-end/.test(html)) fail(`a corner ☰ (or its --map-fab-* footprint, or the drawer-foot language) renders at ${tag}`)
|
||||
if (wantBar !== /has-phone-bar/.test(html)) fail(`the phone bar's grid row (.has-phone-bar) ${wantBar ? 'is missing' : 'is set'} at ${tag}`)
|
||||
}
|
||||
|
||||
|
|
@ -142,12 +141,11 @@ function plant(rel, suffix, edit, redirect = []) {
|
|||
}
|
||||
const PLANTED = [] // every temporary copy, removed in finally whatever happens
|
||||
const POISONS = [
|
||||
{ name: 'the bar also shows with a header', files: () => ({ vtl: plant(VTL, 'bar-header', (s) => s.replace("phoneBarShown() { return this.narrow && !this.header && this.mode === 'app' }", "phoneBarShown() { return this.narrow && this.mode === 'app' }")) }) },
|
||||
{ name: 'the bar also shows with a header', files: () => ({ vtl: plant(VTL, 'bar-header', (s) => s.replace('phoneBarShown() { return this.narrow && !this.header }', 'phoneBarShown() { return this.narrow }')) }) },
|
||||
{ name: 'the rail-end also shows on a phone', files: () => ({ vtl: plant(VTL, 'rail-phone', (s) => s.replace('railEndShown() { return !this.narrow && !this.header }', 'railEndShown() { return !this.header }')) }) },
|
||||
{ name: 'web mode stacks the phone bar under the host header', files: () => ({ vtl: plant(VTL, 'webbar', (s) => s.replace("phoneBarShown() { return this.narrow && !this.header && this.mode === 'app' }", 'phoneBarShown() { return this.narrow && !this.header }')) }) },
|
||||
{ name: 'web mode leaves the drawer a block (the language under the last row)', files: () => ({ vtl: plant(VTL, 'webfoot', (s) => s.replace(" || (drawerEndShown && !!$slots['phone-end'])", '')) }) },
|
||||
{ name: 'web mode drops the language', files: () => ({ vtl: plant(VTL, 'weblang', (s) => s.replace("drawerEndShown() { return this.narrow && !this.header && this.mode === 'web' },", 'drawerEndShown() { return false },')) }) },
|
||||
{ name: 'the corner circle in app mode (under the bar)', files: () => ({ vtl: plant(VTL, 'fab', (s) => s.replace('<div v-if="phoneBarShown" class="vtl-pbar">', '<button v-if="phoneBarShown" class="vtl-fab">☰</button><div v-if="phoneBarShown" class="vtl-pbar">')) }) },
|
||||
{ name: 'web mode drops the header (no bar)', files: () => ({ vtl: plant(VTL, 'webnobar', (s) => s.replace('phoneBarShown() { return this.narrow && !this.header }', "phoneBarShown() { return this.narrow && !this.header && this.mode === 'app' }")) }) },
|
||||
{ name: 'web mode keeps the camera inset', files: () => ({ vtl: plant(VTL, 'webinset', (s) => s.replace(" :class=\"{ 'is-web': mode === 'web' }\"", '')) }) },
|
||||
{ name: 'the corner circle is back', files: () => ({ vtl: plant(VTL, 'fab', (s) => s.replace('<div v-if="phoneBarShown" class="vtl-pbar"', '<button v-if="phoneBarShown" class="vtl-fab">☰</button><div v-if="phoneBarShown" class="vtl-pbar"')) }) },
|
||||
{ name: 'the event is not cancelable', files: () => { const r = plant(REQ, 'nocancel', (s) => s.replace('bubbles: true, composed: true, cancelable: true', 'bubbles: true, composed: true')); return { menu: plant(MENU, 'm-nocancel', (s) => s + ' ', [['../utils/languageRequest.js', '../utils/' + r.split('/').pop()]]) } } },
|
||||
{ name: 'the event does not leave the shadow root', files: () => { const r = plant(REQ, 'nocomposed', (s) => s.replace('bubbles: true, composed: true, cancelable: true', 'bubbles: true, cancelable: true')); return { menu: plant(MENU, 'm-nocomposed', (s) => s + ' ', [['../utils/languageRequest.js', '../utils/' + r.split('/').pop()]]) } } },
|
||||
{ name: 'the detail is Vue-shaped (an array)', files: () => { const r = plant(REQ, 'arr', (s) => s.replace('detail: { language: code }', 'detail: [{ language: code }]')); return { menu: plant(MENU, 'm-arr', (s) => s + ' ', [['../utils/languageRequest.js', '../utils/' + r.split('/').pop()]]) } } },
|
||||
|
|
@ -175,7 +173,7 @@ try {
|
|||
} else {
|
||||
const bad = await audit()
|
||||
if (bad.length) { exit = 1; console.log(`SHELL-HEADER: FAIL — ${bad.length} problem(s)`); for (const b of bad) console.log(' ✗ ' + b) }
|
||||
else console.log('SHELL-HEADER: PASS — app mode: the phone bar only on a narrow header-off layout; web mode: no bar, the corner ☰ + its footprint, the language at the drawer foot; #rail-end only on a wide one; one cancelable composed doxa-language-request per pick; cancelled → no switch; ?lang= only on tree-owned pages; the switcher has catalogues and the current language is always listed.')
|
||||
else console.log('SHELL-HEADER: PASS — one top bar on a narrow header-off layout in both modes (web: no camera inset), never a corner circle; #rail-end only on a wide one; one cancelable composed doxa-language-request per pick; cancelled → no switch; ?lang= only on tree-owned pages; the switcher has catalogues and the current language is always listed.')
|
||||
}
|
||||
} finally {
|
||||
while (PLANTED.length) rmSync(join(ROOT, PLANTED.pop()), { force: true })
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* never interfere with each other.
|
||||
*/
|
||||
|
||||
import { computed, provide, inject, watchEffect, watch, ref } from 'vue'
|
||||
import { computed, provide, inject, watchEffect, watch, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useSharedMapToken, publishMapToken, fetchMapToken } from './composables/useSharedMapToken.js'
|
||||
import { retryableAsyncComponent } from './composables/retryableAsyncComponent.js'
|
||||
import { SUPPORTED_LOCALES, RTL_LOCALES, setRuntimeLocale, localeFromUrl, loadLocale, localeChromeReady } from './i18n/index.js'
|
||||
|
|
@ -344,10 +344,45 @@ try {
|
|||
provide('uiStore', uiStore)
|
||||
provide('mapStore', mapStore)
|
||||
provide('dataStore', dataStore)
|
||||
|
||||
// ★ THE FLOOR NEVER EXCEEDS A SIZED HOST (measured 2026-10-02 on a phone, DX Life's Home/Pray/Research). The
|
||||
// viewport floor below (min-height: 100svh, see the style block) exists for a host whose height comes from its
|
||||
// CONTENT (the scroll-page leaf, BUG-UPG100). A host that is already SIZED (DX Life's slot: a definite height,
|
||||
// overflow hidden; a tab pane) must be filled exactly: there the floor made the map taller than its host, the
|
||||
// host clipped its bottom, and the legend footer was cut off. It also tied the map's height to the window's,
|
||||
// which is what moved the map as a phone's URL bar came and went.
|
||||
// Which kind of host this is cannot be read from CSS, so it is MEASURED: with the floor at 0 and then at a huge
|
||||
// value, a sized host keeps its height and a content-sized one grows. Sized → the floor is switched off
|
||||
// (height:100% fills the host); content-sized → the floor stays. Both probes are undone before the frame paints.
|
||||
// Re-measured whenever the host's size changes (a ResizeObserver on the HOST, never a window resize).
|
||||
const loaderEl = ref(null)
|
||||
let hostRO = null
|
||||
function fitFloorToHost() {
|
||||
const el = loaderEl.value
|
||||
const host = el && el.getRootNode && el.getRootNode().host
|
||||
if (!el || !host) return
|
||||
const keep = el.style.minHeight
|
||||
el.style.minHeight = '0px'
|
||||
const h0 = host.getBoundingClientRect().height
|
||||
el.style.minHeight = '100000px'
|
||||
const h1 = host.getBoundingClientRect().height
|
||||
const sized = h0 > 0 && Math.abs(h1 - h0) < 1
|
||||
el.style.minHeight = sized ? '0px' : (keep === '0px' ? '' : keep)
|
||||
el.toggleAttribute('data-host-sized', sized)
|
||||
}
|
||||
onMounted(() => {
|
||||
fitFloorToHost()
|
||||
const host = loaderEl.value && loaderEl.value.getRootNode && loaderEl.value.getRootNode().host
|
||||
if (host && typeof ResizeObserver !== 'undefined') {
|
||||
hostRO = new ResizeObserver(() => fitFloorToHost())
|
||||
hostRO.observe(host)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => { if (hostRO) { hostRO.disconnect(); hostRO = null } })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="profile-loader" :class="{ 'has-screen-notice': adviseOnThisScreen }" :dir="dirAttr" :data-locale-pending="localePending ? '' : null">
|
||||
<div ref="loaderEl" class="profile-loader" :class="{ 'has-screen-notice': adviseOnThisScreen }" :dir="dirAttr" :data-locale-pending="localePending ? '' : null">
|
||||
<!-- Error: no profile-config prop -->
|
||||
<div v-if="!config" class="profile-error">
|
||||
<p>⚠️ <strong>doxa-map</strong>: missing or invalid <code>profile-config</code> prop.</p>
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ useShadowStyles(`
|
|||
left:8px, width:380px) so the LegendDesktop card and the SemanticTreeLegend
|
||||
sit at exactly the same position+size — no visible jump when the user
|
||||
clicks a pin and the detail panel swaps in over the language-family legend
|
||||
The cap is the MAP's height (100%), never the window's (100vh): a window-height cap followed a phone's URL bar.
|
||||
*/
|
||||
.legend-container{position:absolute;top:60px;inset-inline-start:8px;width:var(--map-legend-width,380px);max-height:calc(100vh - 80px);background:#fff;border-radius:var(--map-radius,16px);box-shadow:0 2px 8px rgba(0,0,0,0.12),0 1px 3px rgba(0,0,0,0.08);z-index:1000;overflow:hidden;display:flex;flex-direction:column;transition:width 200ms cubic-bezier(0.2,0,0,1),max-height 200ms cubic-bezier(0.2,0,0,1),min-width 200ms cubic-bezier(0.2,0,0,1),border-radius 200ms linear,box-shadow 200ms linear;will-change:width,max-height;}
|
||||
.legend-container{position:absolute;top:60px;inset-inline-start:8px;width:var(--map-legend-width,380px);max-height:calc(100% - 80px);background:#fff;border-radius:var(--map-radius,16px);box-shadow:0 2px 8px rgba(0,0,0,0.12),0 1px 3px rgba(0,0,0,0.08);z-index:1000;overflow:hidden;display:flex;flex-direction:column;transition:width 200ms cubic-bezier(0.2,0,0,1),max-height 200ms cubic-bezier(0.2,0,0,1),min-width 200ms cubic-bezier(0.2,0,0,1),border-radius 200ms linear,box-shadow 200ms linear;will-change:width,max-height;}
|
||||
/* ── DESKTOP TYPE SCALE for the LegendRows-based card (2026-09-16: larger, more
|
||||
readable desktop legends). The semantic-tree panel's scale lives in SemanticTreeLegend.vue
|
||||
(it renders without this card on some maps). Size only, never layout. ── */
|
||||
|
|
|
|||
|
|
@ -91,12 +91,10 @@
|
|||
after a resize back to desktop — the previous broken-restore bug.
|
||||
-->
|
||||
<template>
|
||||
<!-- ★ THE CORNER CIRCLE IS WEB MODE ONLY. In app mode the phone bar below carries the ☰ ABOVE the pane, so nothing
|
||||
<!-- ★ NO CORNER CIRCLE. On a phone the ☰ lives in the phone bar ABOVE the pane, in both shell modes, so nothing
|
||||
covers plugin content and --map-fab-clearance / --map-fab-beside are not set: every reader of them
|
||||
(BackToTopButton, PinHint, LegendMobile, the grid) takes its own fallback. In web mode there is no bar, the
|
||||
☰ is a circle over the pane's bottom-start corner, and the layout publishes both while the circle shows. -->
|
||||
<div class="vtl" :class="{ 'nav-open': navOpen, 'rail-collapsed': railCollapsed, 'no-header': !header, 'has-phone-bar': phoneBarShown }"
|
||||
:style="fabShown ? { '--map-fab-clearance': 'calc(56px + env(safe-area-inset-bottom))', '--map-fab-beside': 'calc(56px + env(safe-area-inset-left))' } : null">
|
||||
(BackToTopButton, PinHint, LegendMobile, the grid) takes its own fallback. -->
|
||||
<div class="vtl" :class="{ 'nav-open': navOpen, 'rail-collapsed': railCollapsed, 'no-header': !header, 'has-phone-bar': phoneBarShown }">
|
||||
<header class="vtl-top" v-if="header">
|
||||
<button class="vtl-menu" type="button" :aria-expanded="String(navOpen)" @click="navOpen = !navOpen"
|
||||
:aria-label="label('toggleList')">☰</button>
|
||||
|
|
@ -116,26 +114,19 @@
|
|||
every header-on consumer (plugin-studio) render exactly as before. It replaced the floating
|
||||
corner circle that did this job before (found 2026-09-22: header-off on a phone, every tab
|
||||
but the first was unreachable), and check:nav-reachable still presses it. -->
|
||||
<div v-if="phoneBarShown" class="vtl-pbar">
|
||||
<div v-if="phoneBarShown" class="vtl-pbar" :class="{ 'is-web': mode === 'web' }">
|
||||
<button class="vtl-phone-menu" type="button" :aria-expanded="String(navOpen)"
|
||||
@click="navOpen = !navOpen" :aria-label="label('toggleList')">☰</button>
|
||||
<span class="vtl-pbar-end"><slot name="phone-end" /></span>
|
||||
</div>
|
||||
|
||||
<!-- ★ WEB MODE (shellMode, below): ON A WEBSITE THE HOST'S HEADER IS THE ONLY HEADER. Inside a host page that
|
||||
has its own site header (DX Life), the phone bar above stacked a second white bar under it. So a phone in
|
||||
web mode gets no bar: the rail opens from this corner circle (40px, 8px from the bottom-start corner,
|
||||
over the content, never a row), and the language menu (#phone-end) moves into the drawer's foot.
|
||||
The circle publishes its footprint (--map-fab-clearance / --map-fab-beside on .vtl, above), which the
|
||||
legend sheet, the pin hint and back-to-top already read, so they step aside while it shows.
|
||||
It hides while the drawer is open: a tab tap closes the drawer, and a circle over an open drawer would
|
||||
only cover a row. -->
|
||||
<button v-if="fabShown"
|
||||
class="vtl-fab" type="button" :aria-expanded="String(navOpen)"
|
||||
@click="navOpen = true" :aria-label="label('openViews')">☰</button>
|
||||
<!-- ★ WEB MODE (shellMode, below) IS THE SAME BAR, minus the camera inset. Inside a website (DX Life) the shell
|
||||
is not at the top of the screen, so the bar takes no env(safe-area-inset-top) padding (.is-web); in an
|
||||
app it may sit under the notch, so it keeps it. One header either way, with the ☰ at its start and the
|
||||
language at its end. (A corner ☰ with no header was tried for web mode and rejected on a real phone.) -->
|
||||
|
||||
<!-- Driving the shell's own scroller. It reads --map-fab-beside, which only web mode's circle sets; with no circle
|
||||
it sits at its plain inset. -->
|
||||
<!-- Driving the shell's own scroller. It reads --map-fab-beside, which nothing sets now that there is no corner
|
||||
circle, so it sits at its plain inset. -->
|
||||
<BackToTopButton v-if="narrow" :target="activeHostEl"
|
||||
:labels="{ backToTop: T('actions.backToTop', 'Back to top') }" />
|
||||
|
||||
|
|
@ -146,7 +137,7 @@
|
|||
an off-canvas drawer parked at translateX(-100%) and slid in by the `nav-open` class. Because it
|
||||
is never display:none, a mobile-closed drawer can no longer strand the rail after a resize back
|
||||
to desktop (the old cross-breakpoint bug). `aria-hidden` mirrors real visibility for a11y. -->
|
||||
<nav class="vtl-nav" :class="{ 'has-rail-end': (railEndShown && !!$slots['rail-end']) || (drawerEndShown && !!$slots['phone-end']) }" :aria-label="navLabel" :aria-hidden="String(!railShown)"
|
||||
<nav class="vtl-nav" :class="{ 'has-rail-end': railEndShown && !!$slots['rail-end'] }" :aria-label="navLabel" :aria-hidden="String(!railShown)"
|
||||
:role="$slots.rail ? null : 'tablist'" :aria-orientation="$slots.rail ? null : 'vertical'">
|
||||
<!-- THE HOST'S WAY HOME (optional). A host page that embeds this layout full-page (DX Life's
|
||||
/deep-research) has no header of its own there, so it hands in its homepage URL and label
|
||||
|
|
@ -250,10 +241,6 @@
|
|||
<div v-if="railEndShown && $slots['rail-end']" class="vtl-rail-end">
|
||||
<slot name="rail-end" :collapsed="railCollapsed" />
|
||||
</div>
|
||||
<!-- Web mode on a phone: no bar, so the bar's #phone-end (the language menu) sits at the drawer's foot. -->
|
||||
<div v-if="drawerEndShown && $slots['phone-end']" class="vtl-rail-end vtl-drawer-end">
|
||||
<slot name="phone-end" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="vtl-main">
|
||||
|
|
@ -367,12 +354,8 @@ const VTL_SHADOW_CSS = `
|
|||
.vtl-phone-menu { flex:0 0 auto; width:44px; height:44px; display:inline-flex; align-items:center; justify-content:center; padding:0; margin:0; font:20px/1 system-ui; color:#3b463d; background:transparent; border:0; border-radius:8px; cursor:pointer; }
|
||||
.vtl-phone-menu:hover { background:var(--vtl-accented); }
|
||||
.vtl-phone-menu:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
|
||||
/* WEB MODE's corner circle (fabShown = narrow AND header-off AND web AND the drawer shut). Fixed: 40px, 8px from the
|
||||
bottom-start corner plus the safe area; above the content, below the drawer (.vtl-nav, z-index 20), so the drawer
|
||||
covers it. It never rides a legend sheet: the sheet pads its start edge by --map-fab-beside instead. */
|
||||
.vtl-fab { position:absolute; z-index:15; left:calc(8px + env(safe-area-inset-left)); bottom:calc(8px + env(safe-area-inset-bottom)); width:40px; height:40px; display:flex; align-items:center; justify-content:center; padding:0; font:18px/1 system-ui; color:#3b463d; background:#ffffff; border:1px solid var(--vtl-border); border-radius:50%; box-shadow:0 2px 10px rgba(0,0,0,.18); cursor:pointer; }
|
||||
.vtl-fab:hover { background:#d1d4cd; }
|
||||
.vtl-fab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:2px; }
|
||||
/* WEB MODE: the same bar, without the camera inset (the shell is not at the top of the screen in a website). */
|
||||
.vtl-pbar.is-web { height:44px; padding-top:0; }
|
||||
.vtl-pbar-end { margin-inline-start:auto; display:inline-flex; align-items:center; min-width:0; }
|
||||
.vtl-menu:hover { background:#d1d4cd; }
|
||||
.vtl-brand { display:inline-flex; align-items:center; }
|
||||
|
|
@ -598,7 +581,6 @@ const VTL_SHADOW_CSS = `
|
|||
shadow boundary, so either a consumer setting dir="rtl" on the layout or an RTL host page flips it.
|
||||
translateX has no logical form, which is why this is a mirror block rather than logical properties. */
|
||||
.vtl:dir(rtl) .vtl-nav { left:auto; right:0; border-right:0; border-left:1px solid var(--vtl-border); }
|
||||
.vtl:dir(rtl) .vtl-fab { left:auto; right:calc(8px + env(safe-area-inset-right)); }
|
||||
.vtl:dir(rtl) .vtl-main { left:0; right:256px; }
|
||||
.vtl:dir(rtl) .vtl-tab, .vtl:dir(rtl) .vtl-tier-btn { text-align:right; }
|
||||
.vtl:dir(rtl) .vtl-meta, .vtl:dir(rtl) .vtl-td, .vtl:dir(rtl) .vtl-chev { margin-left:0; margin-right:auto; }
|
||||
|
|
@ -623,7 +605,7 @@ const VTL_SHADOW_CSS = `
|
|||
/* ★ 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). Additive only. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vtl-nav, .vtl-collapse-ic, .vtl-tab, .vtl-tier, .vtl-pbar, .vtl-phone-menu, .vtl-fab, .vtl-rail-end, .vtl-scrim { transition: none !important; animation: none !important; }
|
||||
.vtl-nav, .vtl-collapse-ic, .vtl-tab, .vtl-tier, .vtl-pbar, .vtl-phone-menu, .vtl-rail-end, .vtl-scrim { transition: none !important; animation: none !important; }
|
||||
}
|
||||
`
|
||||
|
||||
|
|
@ -705,11 +687,8 @@ export default defineComponent({
|
|||
* on a desktop, and neither when a header is on (its #toolbar carries the menu). The template,
|
||||
* the grid row (.has-phone-bar) and check:shell-header all read these two, never a copy.
|
||||
*/
|
||||
phoneBarShown() { return this.narrow && !this.header && this.mode === 'app' },
|
||||
phoneBarShown() { return this.narrow && !this.header },
|
||||
railEndShown() { return !this.narrow && !this.header },
|
||||
// Web mode on a phone: the corner circle (while the drawer is shut) and the language at the drawer's foot.
|
||||
fabShown() { return this.narrow && !this.header && this.mode === 'web' && !this.navOpen },
|
||||
drawerEndShown() { return this.narrow && !this.header && this.mode === 'web' },
|
||||
/**
|
||||
* The element the back-to-top arrow scrolls. .vtl-host is the shell's scroller
|
||||
* (overflow-y:auto) and there is one PER TAB, so this follows `active` rather than binding a
|
||||
|
|
@ -1014,12 +993,8 @@ export default defineComponent({
|
|||
.vtl-phone-menu { flex:0 0 auto; width:44px; height:44px; display:inline-flex; align-items:center; justify-content:center; padding:0; margin:0; font:20px/1 system-ui; color:#3b463d; background:transparent; border:0; border-radius:8px; cursor:pointer; }
|
||||
.vtl-phone-menu:hover { background:var(--vtl-accented); }
|
||||
.vtl-phone-menu:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
|
||||
/* WEB MODE's corner circle (fabShown = narrow AND header-off AND web AND the drawer shut). Fixed: 40px, 8px from the
|
||||
bottom-start corner plus the safe area; above the content, below the drawer (.vtl-nav, z-index 20), so the drawer
|
||||
covers it. It never rides a legend sheet: the sheet pads its start edge by --map-fab-beside instead. */
|
||||
.vtl-fab { position:absolute; z-index:15; left:calc(8px + env(safe-area-inset-left)); bottom:calc(8px + env(safe-area-inset-bottom)); width:40px; height:40px; display:flex; align-items:center; justify-content:center; padding:0; font:18px/1 system-ui; color:#3b463d; background:#ffffff; border:1px solid var(--vtl-border); border-radius:50%; box-shadow:0 2px 10px rgba(0,0,0,.18); cursor:pointer; }
|
||||
.vtl-fab:hover { background:#d1d4cd; }
|
||||
.vtl-fab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:2px; }
|
||||
/* WEB MODE: the same bar, without the camera inset (the shell is not at the top of the screen in a website). */
|
||||
.vtl-pbar.is-web { height:44px; padding-top:0; }
|
||||
.vtl-pbar-end { margin-inline-start:auto; display:inline-flex; align-items:center; min-width:0; }
|
||||
.vtl-menu:hover { background:#d1d4cd; }
|
||||
.vtl-brand { display:inline-flex; align-items:center; }
|
||||
|
|
@ -1245,7 +1220,6 @@ export default defineComponent({
|
|||
shadow boundary, so either a consumer setting dir="rtl" on the layout or an RTL host page flips it.
|
||||
translateX has no logical form, which is why this is a mirror block rather than logical properties. */
|
||||
.vtl:dir(rtl) .vtl-nav { left:auto; right:0; border-right:0; border-left:1px solid var(--vtl-border); }
|
||||
.vtl:dir(rtl) .vtl-fab { left:auto; right:calc(8px + env(safe-area-inset-right)); }
|
||||
.vtl:dir(rtl) .vtl-main { left:0; right:256px; }
|
||||
.vtl:dir(rtl) .vtl-tab, .vtl:dir(rtl) .vtl-tier-btn { text-align:right; }
|
||||
.vtl:dir(rtl) .vtl-meta, .vtl:dir(rtl) .vtl-td, .vtl:dir(rtl) .vtl-chev { margin-left:0; margin-right:auto; }
|
||||
|
|
@ -1270,6 +1244,6 @@ export default defineComponent({
|
|||
/* ★ 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). Additive only. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vtl-nav, .vtl-collapse-ic, .vtl-tab, .vtl-tier, .vtl-pbar, .vtl-phone-menu, .vtl-fab, .vtl-rail-end, .vtl-scrim { transition: none !important; animation: none !important; }
|
||||
.vtl-nav, .vtl-collapse-ic, .vtl-tab, .vtl-tier, .vtl-pbar, .vtl-phone-menu, .vtl-rail-end, .vtl-scrim { transition: none !important; animation: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue