feat(plugins): give plugins the surface, insets and mobile palette they need (#1886)

* Tell a plugin where it is mounted

A plugin sits in one of six host surfaces and cannot see which: a trip tab and
the plugin page fill their container and scroll themselves, a dashboard widget
and the settings frame report their height, a detail slot is a narrow column in
someone else's panel. The contracts differ, and on a filling surface the height
report is discarded, so a plugin cannot even discover the difference by trying.

trek:context now carries viewport: the surface by name, phone or desktop, the
fill flag, and the insets the host already keeps clear. The insets describe
space that is held free, not padding to add — the mobile trip tab is the only
surface with chrome floating over it, and the numbers come from the same
variables the native tabs use so the two cannot drift.

* Seat the plugin tab like a native one

The mobile plugin tab kept clearance at the bottom and none at the top, so the
plugin's first row sat under the status bar and behind the floating back button.
The bottom value was a copied 84 that ignored the safe area on the devices that
have one, and the padding sat on the container rather than in the scroller.

Both clearances now come from the variables TabScroller uses, so the tab cannot
drift away from the native ones. The frame also states its surface, and pins
color-scheme for the reason both settings mounts already pin it: Chromium paints
a white canvas behind a transparent frame otherwise.

The tab also had no name anywhere on screen. It is not in the dock, so nothing
lit up there either, and the header had a branch for every other tab. It gets
the same treatment, built from the tab entry the planner already assembles.

* Keep the widget card intact on the phone

The card is built from the dashboard's glass vocabulary, which is scoped to
.trek-dash. The phone dashboard is a different route and mounts these widgets
outside that subtree, so background, border, shadow and blur all resolved empty
and the widget stood on the gradient as a bare title over an iframe. Each value
now falls back to the mobile token the surrounding cards are made of.

* Give the design kit a mobile layer

The kit was written for the desktop and stayed there: a filled card with a drop
shadow where the mobile design is translucent and border-led, inputs at 13px
which make iOS Safari zoom on focus and never zoom back, feedback on :hover
which never fires on a touchscreen, and one media query in the whole stylesheet.

It reads the new viewport field now and sets data-form-factor, data-surface and
data-fill on the document, plus the insets as variables. Everything mobile hangs
off data-form-factor="phone", so nothing changes on the desktop: the card loses
its shadow and takes the mobile surface tokens, targets grow to 44px, inputs to
16px, and pressing something scales it instead of waiting for a hover that will
not come. A filling surface anchors the body and scrolls an inner element,
because its height report is discarded by the host.

Plus the pieces a v4 screen is actually made of, so an author does not have to
reverse-engineer them: eyebrow, count pill, section header, status dot, round
icon button, field, segmented control. Each falls back to a global token where
the mobile family is absent, so they work on both.

* Document what a plugin needs on a phone

The guide had nothing on the subject: no mention of the second token family, of
who scrolls, of what sizes to expect, or that the kit brings almost nothing
responsive. It also promised more than it delivered, claiming the palette was
complete and that applying it matches the host "exactly" — fonts are named, not
shipped, so that was never true.

The new section covers the six surfaces and which of them fill versus report a
height, what insets mean (space already held free, not padding to add), the
mobile palette with the branch-on-absence pattern, and the four rules that
decide whether it feels native. It also says plainly that the dev preview cannot
show a phone width yet.
This commit is contained in:
Maurice
2026-08-11 16:51:02 +02:00
committed by GitHub
parent d1e3d8a75a
commit 6fe89588c2
17 changed files with 433 additions and 30 deletions
+131
View File
@@ -279,6 +279,125 @@ body.trek-ui {
@media (max-width: 639px) {
.trek-modal-enter { animation: trek-drawer-enter 320ms cubic-bezier(0.32, 0.72, 0, 1); }
}
/* Mobile layer ------------------------------------------------------------- *
* TREK 4 gives the phone its own design: a translucent, border-led surface over
* a gradient, no drop shadows, and touch feedback rather than hover. The host
* hands the mobile palette (--m-*) down with the rest of the tokens whenever a
* frame is mounted inside the mobile shell, so these rules only restate the
* SHAPE — the colours come from the same variables the native screens use.
*
* Everything here is keyed on [data-form-factor="phone"], which the host sets
* from the viewport it reports. On desktop not a single rule below applies. */
[data-form-factor="phone"] .trek-ui,
[data-form-factor="phone"] body.trek-ui { font-size: 15px; }
/* The card is the workhorse. On the phone it is a translucent tile with a hairline
border and no shadow — a drop shadow over the gradient reads as a sticker. */
[data-form-factor="phone"] .trek-card {
background: var(--m-card, var(--bg-card));
border: 1px solid var(--m-cbr, var(--border-primary));
border-radius: 16px;
box-shadow: none;
padding: 14px;
-webkit-backdrop-filter: blur(30px) saturate(1.8);
backdrop-filter: blur(30px) saturate(1.8);
}
[data-form-factor="phone"] .trek-glass {
background: var(--m-glass, var(--glass-bg));
border-color: var(--m-gbr, var(--glass-border));
border-radius: 20px;
box-shadow: none;
padding: 16px;
}
/* Touch has no hover. The lift on hover never fires on a phone and the pressed
state never existed, so a tap gave no feedback at all. */
[data-form-factor="phone"] .trek-card.trek-interactive:hover,
[data-form-factor="phone"] .trek-glass.trek-interactive:hover { transform: none; box-shadow: none; }
[data-form-factor="phone"] .trek-interactive:active,
[data-form-factor="phone"] .trek-btn:active,
[data-form-factor="phone"] .trek-row:active { transform: scale(.985); opacity: .9; }
[data-form-factor="phone"] .trek-btn:hover { filter: none; }
/* 44px is the smallest target that reliably hits on a phone. */
[data-form-factor="phone"] .trek-btn { min-height: 44px; border-radius: 12px; }
[data-form-factor="phone"] .trek-row { min-height: 48px; }
/* An input below 16px makes iOS Safari zoom the page on focus, and the zoom does
not come back. This is the single most common mobile bug in embedded UI. */
[data-form-factor="phone"] .trek-input,
[data-form-factor="phone"] .trek-select,
[data-form-factor="phone"] .trek-textarea { font-size: 16px; min-height: 44px; border-radius: 12px; }
/* Ink follows the mobile palette where it exists. */
[data-form-factor="phone"] .trek-title { color: var(--m-ink, var(--text-primary)); }
[data-form-factor="phone"] .trek-sub,
[data-form-factor="phone"] .trek-muted { color: var(--m-muted, var(--text-muted)); }
/* A filling surface owns its own scrolling: the host will not scroll for it, and
a height report is discarded. Anchor the body and scroll the content. */
[data-fill] .trek-ui, [data-fill] body.trek-ui { height: 100%; overflow: hidden; }
[data-fill] .trek-scroll {
height: 100%; overflow-y: auto; -webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
/* Building blocks the native mobile screens are made of, so a plugin does not
have to reverse-engineer them from screenshots. */
.trek-eyebrow {
font-size: 10px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
color: var(--m-faint, var(--text-faint));
}
.trek-countpill {
display: inline-flex; align-items: center; justify-content: center;
min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
background: var(--m-ic, var(--bg-tertiary)); color: var(--m-muted, var(--text-muted));
font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.trek-sectionhead {
display: flex; align-items: center; gap: 7px; width: 100%;
margin: 15px 0 2px; padding: 0 2px; background: none; border: 0;
font: inherit; text-align: left; cursor: pointer;
}
.trek-statusdot { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--m-st-neutral, var(--text-faint)); }
.trek-statusdot[data-status="confirmed"] { background: var(--m-st-confirmed, var(--success)); }
.trek-statusdot[data-status="pending"] { background: var(--m-st-pending, var(--warning)); }
.trek-statusdot[data-status="info"] { background: var(--m-st-info, var(--info)); }
.trek-statusdot[data-status="danger"] { background: var(--m-st-danger, var(--danger)); }
.trek-iconbtn {
display: inline-grid; place-items: center; flex: none;
width: 38px; height: 38px; border-radius: 999px; cursor: pointer;
background: var(--m-ic, var(--bg-tertiary));
border: 1px solid var(--m-gbr, var(--border-primary));
color: var(--m-ink, var(--text-primary));
}
.trek-iconbtn:active { transform: scale(.94); }
.trek-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.trek-field > .trek-field-label {
font-size: 9px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
color: var(--m-faint, var(--text-faint));
}
.trek-field > .trek-field-value {
font-size: 13px; font-weight: 600; color: var(--m-ink, var(--text-primary));
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trek-segmented {
display: inline-flex; align-items: center; gap: 2px; padding: 3px; border-radius: 999px;
background: var(--m-glass, var(--bg-tertiary));
border: 1px solid var(--m-gbr, var(--border-primary));
}
.trek-segmented > button {
border: 0; background: none; cursor: pointer; font: inherit;
padding: 8px 16px; border-radius: 999px; white-space: nowrap;
font-size: 13px; font-weight: 500; color: var(--m-ink, var(--text-primary));
}
.trek-segmented > button[aria-pressed="true"],
.trek-segmented > button.on {
background: var(--m-act, var(--accent)); color: var(--m-actfg, var(--accent-text)); font-weight: 600;
}
.trek-backdrop-enter { animation: trek-backdrop-enter 180ms var(--trek-ease-quint); }
.trek-toast-enter { animation: trek-toast-enter 260ms var(--trek-ease-quint); will-change: transform, opacity; }
.trek-pie-reveal {
@@ -371,6 +490,18 @@ export const TREK_THEME_JS = `(function () {
if (m.theme) { docEl.setAttribute('data-theme', m.theme); }
if (m.locale) { docEl.setAttribute('lang', m.locale); }
docEl.setAttribute('dir', m.dir === 'rtl' ? 'rtl' : 'ltr');
// Where this frame sits, and in what shape. data-form-factor drives the
// mobile layer of this stylesheet; data-surface lets a plugin tell a
// full-height tab from a widget that reports its height. The insets are the
// space the HOST already keeps clear — exposed so a plugin can align its own
// sticky chrome with the host's, not so it adds padding on top.
var vp = m.viewport || {};
if (vp.formFactor) { docEl.setAttribute('data-form-factor', vp.formFactor); }
if (vp.surface) { docEl.setAttribute('data-surface', vp.surface); }
setFlag('data-fill', vp.fill);
var ins = vp.insets || {};
docEl.style.setProperty('--trek-inset-top', (ins.top || 0) + 'px');
docEl.style.setProperty('--trek-inset-bottom', (ins.bottom || 0) + 'px');
var t = m.tokens || {};
for (var k in t) {
if (Object.prototype.hasOwnProperty.call(t, k) && t[k]) { docEl.style.setProperty(k, t[k]); }