The rail tabs show one Material Symbols glyph each (Display tune, CCTV
videocam, Street Level streetview, Context travel_explore, Weather cloud,
Recent Imagery satellite_alt) in a fixed 34px square, the size of the Radio
launcher under them. The panel's translated title is the tab's aria-label and
tooltip. The sideways labels, the equal share of a short strip, the end fade
and the column's lane cap are gone: every tab stays whole, and on the shortest
windows the strip runs past the lane instead of clipping. The glyphs join the
icon font subset, with a test that keeps them there. The screenshot is
refreshed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A Radio pick reopens a closed Context around an expanded Radio section in
every HUD layout, not only Cyber.
- The Radio reveal's scrollend landing gives way to a user scroll, key or
pointer, and stops after two seconds.
- The compact player's Details button scrolls the Radio section and its
Enable into view inside Context.
- A camera layer switched on by a share link or a saved session no longer
opens the CCTV panel over the restored tab; a user switch-on still does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
rightRailTabs.js exports only createRightRailTabs, nextRailTabIndex and railJoinPath and returns element, sync and destroy; the id constants, railTabPanels, the argument guard and the createElementNS fallback existed for the tests alone. The sub-720px density tier and the slot's focus ring go; the five rail cards carry data-rail-card so the stylesheet lists them once.
src/ui/mobileShellContract.js holds the phone shell's media query and class, dependency-free so the rail layout and panel packages can read them; rightPanelRail.js, panelPositionControls.js and radioPanelPlacement.js use it instead of their own literals. src/testSupport/fakeDom.mjs replaces the two hand-made DOM fakes of the rail and strip tests; the rail tests now expect null for a missing attribute, as in the DOM. The two Radio gate checks of the removed several-panels rail policy are gone.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Comments that still spoke of the strip scrolling or of a lifted max-height say what the code does now; the tab and column comments are shorter; the changelog bullet is wrapped. panelDisclosure.js is back to its reviewed shape: railTabFor had one caller left after the header click handoff was reverted.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The fitTabs pass and its lane observer go: the column stands the lane's full height, which makes the strip's size definite, and the tabs take flex 1 1 0% with max-height max-content, so flexbox hands the strip's height out in equal parts, a tab's own length caps its part, and what a short label leaves goes to the longer ones. The basis is 0%, not 0, so the strip's content size, which sizes the column, still comes from the labels. Only the strip and the tool slot take the pointer; the measuring pass sets the column back to height auto so the rail's climb sees the natural column. Same heights as the JS pass at every viewport checked.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Proportional shrinking clipped every label by the same fraction, so CCTV lost half its letters while STREET LEVEL kept most of them. The strip now divides the lane less the tool slot equally (fitTabs in rightRailTabs.js): a tab shorter than its share keeps its height and the rest is shared again, so the long labels end up the same height and clip alike; tabs that fit keep their natural height. A style observer on the rail re-fits on a new lane, and the measuring pass lifts the shared heights so the rail's climb still sees the natural column.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Tactical with CCTV open, Tactical with Context open, Cyber with Street Level open, headless at 1440x900: no header collapse arrows, the compact Radio launcher under the tabs, no strip scrollbar.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A lane too short for the tab strip, even after the rail has climbed toward the HUD corner, shrank the strip into a scrollbar. The tabs now give way in proportion to their labels (flex 0 1 auto, min-height 0) and clip them at the end with a short fade; the join keeps corners no larger than half of a shrunken tab. The strip's scroll handling in placeJoin, the scroll listener, railJoinPath's tab-less path and the scroller marker are gone with the scrolling.
The expanded Radio Enable's reveal of the directory and Play lands its smooth scroll on scrollend when a rail layout pass, measuring the panels mid-scroll, puts the Context scroller's offset back and cancels it; the Radio browser gate stopped there since the strip-fit commit.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Review fixes P1.1-P2.7: a panel hidden at mount gets its tab when revealed; any live opening of a rail panel makes it the owner, restores do not; a short lane scrolls the strip with a thin scrollbar and brings a newly opened tab into view once; the join and the panel cover stay inside the lane and the fillet shrinks to the room left; a share link carries an owner-collapsed peer as collapsed so the recipient opens the sender's panel; border-box on phones; the strip's column folds into the panel width at 721-760px; the focus ring draws inside the tab.
The compact Radio launcher moves from Context's header into a tool slot under the tabs on desktop (back into the header for the phone sheet) and always toggles the compact player, also while Context is open; the player's Details button leads to the Radio section. The jump-to-section role, its two labels and the #804 closings of the player on Context expansion and rail layout passes are gone with it.
The rail panels' own collapse arrows are hidden on desktop: the tab opens and closes a panel, Escape inside closes it and returns focus to the tab.
The tabs are denser and the rail climbs toward the HUD corner above its left-rail baseline when the strip's column is taller than the lane, so the strip no longer scrolls on windows down to about 680px tall; the measuring pass keeps the strip's scroll offset. scripts/qa-radio.mjs expectations follow the launcher's new role.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
shouldHideCollapsedRightPanels had become a constant: collapsed right-rail panels always hide behind the tab strip on desktop, so the function, its re-exports, the hud option of layoutRightPanelRail and the test of the constant go. The rail takes the theme's width as its content box instead of restating it per theme. Remove the unread --rail-tab-top property, dead CSS (an unset clip-path variable, an invisible Cyber highlight, redundant radius rules), an unused strip class, per-tab resize observation and guards that only served the test double, which now carries a style object.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Drop change-history wording, shorten the module, stylesheet and rail comments, and document the owner rule's stored-over-default step in CURRENT-STATE. No runtime change.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
With every panel closed nothing shows that the sideways labels are launchers, so Display starts expanded for a first-time visitor on a desktop viewport; phones keep it collapsed and a share link decides for itself. A stored choice still wins. When the rail picks its one owner, a panel the visitor or a share link opened outranks one open only by default, so a stored CCTV choice is not displaced by the Display default.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Cyber dresses every .panel-collapsible wrapper as a surface with an inset ring, so a panel other than Display showed that ring as a line along its edge beside the open tab and a slightly different tone from the tab. Under the tabs the join is the only surface.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The strip traces the joined silhouette of the open panel and its tab as an SVG path (rounded corners, concave fillets; Cyber's chamfered frame and a square tab) and keeps a join element under the panels: one layer blurs and tints the backdrop inside the path, an SVG strokes the outline along it and another casts the drop shadow. The panel card and the tab draw no surface of their own, so the junction has no seam, doubled edge or second blur region. Cyber's generic selected-button treatment is kept off the tab.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The open tab and its panel share one glass surface and one outline, like a folder tab: the panel card draws no border of its own, the rail's ::before draws the outline with a notch at the tab, the tab closes the shape with its own borders and two concave fillets round the inner corners. The strip publishes the open tab's extent and the panel's height on the rail; a short panel grows to reach its tab. Cyber joins square below its chamfer. Unselected tabs are plain labels.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Drop the four alternative strip presentations (cards-up, spine, icons,
minimal) and the icon glyphs they needed; the strip marks the rail with
data-rail-tabs so a rail without it keeps its legacy geometry. Describe
the tab strip and the desktop one-owner rule in docs/CURRENT-STATE.md.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Replace the right rail's collapsed launcher cards with a vertical tab
strip: one sideways tab per panel, the open panel beside it, at most one
panel open in every HUD layout. The panels' collapsed classes stay the
source of truth; the strip mirrors them and routes every change through
setPanelCollapsed, so persistence and share links are unchanged. Phones
keep the sheet shell.
Five presentations, picked with data-rail-tabs on the rail: cards-down
(default), cards-up, spine, icons and minimal.
The one-owner rule, previously Cyber-only, now applies to every desktop
layout, so the right rail's Tactical auto-collapse retry path is dead and
removed. Escape inside a panel returns focus to the panel's tab.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The style shaders and visualPresets imported msg() from src/i18n/index.js,
which loads every locale catalog. The share-view codec reads the styles'
defaults, so a server bundling the codec carried about 0.9 MB of
translations it never uses. They now import the catalog-free marker, as
other portable modules do; the codec's bundle drops from about 1 MB to
130 KB.
A location search that found nothing or failed now keeps its query, the open field and focus (text selected), and shows a persistent role=alert message under the search row instead of a toast. Typing, a new search or closing the emptied field clears it. HAL-727.
* feat(share): export the share-view codec for host servers
Add `@gods-eye-view/core/share/view-codec` (src/share/viewCodec.js) as a
package export with its own boundary group. The codec is pure (no DOM, no
Cesium, no external dependencies), so a host's server can decode a
/share/<code> link, for example to describe the view in link previews.
* feat(share): export the share-link address helpers
Add `@gods-eye-view/core/share/link-path` (src/share/shareLinkPath.js), so a
host's server reads /share/<code> addresses the way the app does.
* feat(share): add compact versioned share-view codec
Add a standalone codec for `#s=<base64url>` share links: a version byte
followed by protobuf-style tag-length-value fields. Settings equal to
their default are omitted and refilled from the same table on decode;
unknown fields are skipped by wire type, so new fields never break
older apps. Views are a place (camera) or a thing (an aircraft,
military aircraft, vessel or satellite to follow, with camera offset
and last-known fix); an unknown kind falls back to the camera.
The decoder bounds its input and rejects malformed payloads with a
reason instead of throwing. A converter maps the long form's parsed
state to and from the codec state; the long form is unchanged.
* fix(share): tighten codec decoding after review
Unknown detection mode and allocation names now fall back to their
defaults instead of reaching the lenient long-form migration. Future
panel values and style-parameter tokens are skipped rather than applied
or rejected, and layer options are accepted only under their owner
layer. Wrap longitudes before scaling, normalize negative zero, reuse
the layer-state tracking ID grammar, and align the format doc with the
decoder.
* feat(share): let a host share the link its own way
ShareLinkManager.buildLink() returns the current share link without
copying it, and copyLink() now copies exactly that. The share button
passes the link to an optional services.shareLink(url), which resolves
to 'shared', 'copied', 'cancelled' or false; 'cancelled' shows no toast
and 'shared' shows the new "Link shared" (es, ru, zh-CN). Without the
service the button copies as before.
* feat(share): open and make short /share/<code> links
The share button now makes `<app>/share/<code>` links with the compact
share-view code, and the app opens them at start-up through the same
restore pipeline as long links. Once the app next writes the address, the
share path is replaced (no new history entry) by the app's own address and
the live long-form hash, so a reload follows the live view. A host can pass
the code as `initialShareCode` instead. A code that fails to decode restores
nothing and shows "This share link can't be opened".
The code gains a trailing CRC-8 check byte, so any single mistyped
character is rejected instead of opening a different view. Thing views no
longer store a camera: it is derived from the entity's last-known fix and
the follow offset on a sphere, the entity kind is implied by the id's
field, and the followed entity's layer bit is implied; an aircraft followed
from the app's own camera is a 42-character code. The `#s=` hash form is
gone.
While the camera follows an aircraft, military aircraft or satellite, the
link is a thing view with the camera's offset and the entity's position.
Opening one turns the layer on and follows the entity through the existing
shared-follow restore, then swings the camera round to the shared offset; a
vessel view selects the vessel once the vessel feed has it. If the entity
is not reporting within its window, the view stays at its last-known fix
and says so. The panel and style-parameter tables now come from the codec,
and a host share hook that returns a plain value or throws still gets a
toast.
* fix(share): encode style settings against the style's own look
Settings a style applies when selected (bloom, sharpen, HUD, detection,
celestial ring and the style's shader parameters) now default, per field,
to what that style applies in a fresh session, read from the tables the
app applies (the first-load baseline, the style presets and the shader
defaults). The encoder compares against the view's style and the decoder
fills omitted settings from the decoded style, so a view that keeps its
style's look costs only its camera and style. The previous defaults did not
match a fresh session (sharpen, HUD and detection were off), so every link
paid for them, and a style's own shader parameters were always written.
A test pins the per-style defaults as frozen v1 meanings. A followed
aircraft in Noir is now 44 characters and the real long-link example 39.
* docs: changelog for #1418
* fix(layers): make Clear All clear the saved and shared layer selection
After Clear All every layer control was off, but the share URL could keep
listing layers (for example l=e.f) and a reload switched them back on.
The URL and local preferences are written from the committed layer
state, which only explicit visibility changes update. Clear All issued an
explicit OFF only for layers it found on at the start of the batch and
whose turn was not superseded. A layer already off for a passive reason
(isolated by Contacts or another context mode, or released by Contacts'
own teardown while the batch ran) never received one, so it stayed in the
committed state. Clear All also discards the context snapshot that would
have restored those layers, leaving the screen and the URL out of step.
Clear All now publishes the set of layers still on once its batch
settles, and the layer-state coordinator commits that selection: every
saved layer not in it is dropped and its passive restore is cancelled,
while a layer a newer explicit request turned back on is kept. A context
restore interrupted by Clear All no longer brings its snapshot back after
Contacts settles.
* docs: changelog for #1417
* fix(share): restore a share link opened by hash navigation
Opening a different share link in an already-open tab (pasting it into
the address bar, following a link, or Back/Forward) changed the URL but
left the camera, visual style, map, panels and layers on the old view
until a reload.
The share manager now listens for hashchange and popstate. A new hash
that parses as a share and differs from the last hash the app read or
wrote runs the start-up restore pipeline: lane authority is reserved, URL
writes are held (including a pending debounced write of the old view),
the camera restore takes a new navigation generation, and the layer
coordinator replaces any in-flight layer restore with the new share's
layers through the passive share origin. A newer navigation supersedes an
older restore end to end; only the newest one releases URL writes. The
app's own replaceState writes never restore. Listeners are removed on
destroy.
Returning to the Normal style and resetting an already-active preset now
also apply when a share is restored after start-up, so the result matches
a fresh load of the same link.
* docs: changelog for #1416
Add the shared Voice Preview and Settings panel with browser-local preferences, inactivity controls and visible cost warnings.
Contribution by @manjunath22466.
* chore: remove four modules nothing in the app uses
src/app/sourceSlot.js re-exported src/sources/sourceSlot.js, which every
caller imports directly. src/data/flowTiles.js and src/data/issPass.js were
reached only by their own tests. src/data/localGeojson.js wrapped
localGeojsonCore.js with the app's services, which the catalog already
passes itself. The local GeoJSON layer tests now build layers the way the
catalog does, so they keep covering localGeojsonCore.js; only the test of
the wrapper's own default host goes. Boundary lists, format scope and docs
drop the removed paths.
* docs: changelog for #1403