Commit Graph
2795 Commits
Author SHA1 Message Date
Sameh Khamis c2fa15b205 Merge branch 'main' into motion/vessels-frame-cadence 2026-11-14 23:37:10 -08:00
Sameh Khamis 52695daf5c fix(vessels): world-clock motion moves only drawn vessels, once per world time
Vessels out of view move on every 250 ms so one coming into view is current;
vessels at the end of their fixes ask for no frames; nothing moves while the
layer is off; 2D views judge the half-pixel step across the view's width; a
re-floor redraws vessels where they are on the next pass.
2026-11-14 23:30:38 -08:00
Sameh Khamis 57bacd98a8 Move vessels every frame and ask for frames as they move
Vessels between two fixes, and live vessels a host placement places, were
redrawn at most every 250 ms, and a frame was asked for only once a second.
With nothing else asking for frames the scene drew vessels at about 1 to
2 fps live, and none at all while the world clock played at 1x.

They are now redrawn every frame the scene draws, moved in place without
new allocations. After each pass the layer works out how far the fastest
drawn vessel moved on screen and asks for the next frame for when it will
have moved another half pixel, at least once a second while any drawn
vessel moves. A vessel crossing the view close up therefore glides, and
one far away lets the scene idle. A frame already asked for later is
brought forward when a vessel speeds up.
2026-11-14 23:18:57 -08:00
Sameh Khamis e31de29ccc Move satellites every frame between SGP4 samples
Satellite points were propagated once a second and held still in between,
so while the world clock plays faster than real time they jumped: at
1 min/s, about 45 px once a second at globe scale.

Each SGP4 call now keeps a sample of the satellite's inertial position,
velocity, gravity and its rate of change (src/layers/satellites/motion.js).
Every frame each shown point is reckoned from its sample to the world time
and turned into the Earth-fixed frame by the sidereal angle, so points
glide at any play speed. Core satellites still take a fresh sample every
second, and at once when the world time is 30 s from it (fast play, a
seek), which keeps every point within about 10 m of SGP4. Starlink extras
take their first sample as they load and are resampled round-robin, more
per frame the faster time plays (up to 2,000 a frame); a point more than
two minutes from its sample waits for the next one. Hidden points are
moved when shown again, and the tracked satellite keeps its per-frame path.

Measured at globe scale: at 1 min/s points move on every frame, 0.4 px a
frame and at most 1.4 px (before: once a second, 45 px). With 11,880
satellites (Starlink on) the scene holds 60 fps, frames 16.7 ms median and
19.7 ms p95; reckoning costs 0.02 ms a frame for 1,342 points.
2026-11-14 23:18:28 -08:00
Sameh Khamis ac04812db2 A row plays only with a timeline to play; the speed shown follows the world clock; mark labels merge whole and cut by character
A weather row's play started the world clock even with no time to show,
where there is no timeline; it now does nothing then, as the timeline's
own Play cannot be reached. A weather layer made without a weather clock
does nothing once destroyed, so enabling it after builds no clock.

The timeline's speed follows the world clock's rate whoever set it, and
its Play runs at that rate, so it and a row's play never run at different
rates; a rate not offered shows as the nearest offered speed. Choosing a
speed sets the clock's rate first, also while a driver steps.

Marks merge on their whole label and are cut for showing after, so two
whose labels only differ past the cut stay two, and the cut counts
graphemes (code points where the runtime cannot tell them), so an emoji is
never split.

Tests that compared a DOM element with null built an assertion message
from the element when they failed, which hung the run; they compare plain
values now, so they fail at once. The timeline tests put the clock back to
real time after each.
2026-11-14 21:12:43 -08:00
Sameh Khamis 620a337ce8 Weather rows play at the timeline's speed; the frame time shows under reduced motion; tidy the timeline's mark label
A weather row's play ran the world clock at whatever rate it had, and
going live set that back to 1, so the speed chosen on the timeline was
lost. The world clock now keeps its rate while paused and live, and the
timeline's speed sets it whenever it is chosen, so any play runs at it.
A row's play starts from the start of the covered time when live or at
its end, as the timeline's does, and does nothing while every product the
row shows is suspended, as its reduced-motion note says.

Live, the Weather panel's frame time is the newest of each product's
frames, suspended or not, so it no longer goes away under reduced motion,
on a hidden page or without an imagery host.

A weather layer made without a weather clock builds its own when first
enabled rather than when made, so a layer never turned on leaves the
world clock alone, and its own clock (createWeatherClock({ shared: false }))
leaves the world's time where it is when destroyed; only the last shared
weather clock sends the world live.

On the timeline, a zoom or a pan clears the hovered tick's label, a label
is never wider than the track and ends in an ellipsis, and a mark's label
is cut to 60 characters (WORLD_CLOCK_MARK_LABEL_LENGTH).

The changelog entry says what a row's step does, names the earthquake
layer's new exports and says it registers with the world clock.
2026-11-14 20:52:45 -08:00
Sameh Khamis 558e4a919a Timeline marks carry labels; a tap by a tick goes to its mark
A layer's world clock coverage may now give each mark as an object,
{ time, label?, id?, kind? }, as well as a bare epoch ms number, and the
two may be mixed. worldClock.coverage() keeps marks as the sorted list of
distinct times, so existing readers are unchanged, and adds markDetails:
the merged marks in time order (then kind, label and id), each frozen and
holding only the fields given. Marks given alike by several sources appear
once, a bare time is dropped where a labelled mark shares it, and a mark
with no finite time is ignored. The contract is documented in the
worldClock JSDoc and docs/APPLICATION.md.

Launches label their marks with the mission's name (the launch's where it
has none), cut to 27 characters in capitals as the mission index shows
names. Earthquakes of magnitude 6 and up label theirs with the magnitude
as the map shows it and the feed's place cut to 40 characters, such as
"M6.4 · 113 km SSE of Lata, Solomon Islands"; the layer now tells the clock
when the labels change as well as the times.

On the timeline, a pointer within 6 px of a tick shows its label over the
track (nothing for an unlabelled mark), and a tap there goes exactly to
the mark's time instead of the point tapped. A tap elsewhere and a drag
work as before, and ticks stay ticks. The arrow keys still step from mark
to mark; with the handle on a mark, its labels follow the time in the
handle's aria-valuetext, and show over the track while it has focus.
2026-11-14 20:30:01 -08:00
Sameh Khamis 7cb854c923 The timeline marks launches and strong earthquakes, not every weather frame
The timeline drew a tick for every registered mark, and the weather clock
registered every radar, satellite and lightning frame, which come every few
minutes, so the track was a wall of dashes and the arrow keys crept from
frame to frame.

Weather products now register only the span of their frames (start and
end, as before) with no marks. Holds, the gate and the next-frame wait
while the world plays are unchanged. A weather row's step param goes to
the previous or next weather frame (every shown product's frames, and the
row's own while it is suspended) rather than to the timeline's marks.

The Earthquakes layer registers with the world clock once, at init, and
unregisters on destroy. While on, its quakes of magnitude 6 and up
(EARTHQUAKE_MARK_MAGNITUDE) are marks, and its coverage is their own span,
or none without any, so it never stretches the timeline beyond the feed.
Off, it has none. It tells the clock when the set of marks changes. The
map still draws every M2.5+ quake.

Launches stay marks. With no mark ahead or behind, the arrow keys step
along the track as shown.
2026-11-14 20:28:56 -08:00
Sameh Khamis 326cab4cf2 A weather layer without a shared clock keeps its own on the world clock
Built without a weather clock, the weather layer still ran a frame replay
of its own: params.play stepped its frames every two seconds and went round
from the last to the first, params.step and params.latest picked frames
locally, and the layer tracked whether it followed the newest frame. That
was a second way to move time.

The layer now builds a weather clock on the page's world clock when none
is passed, and destroys it with the layer, so every layer follows the world
clock and its params move the world clock as the timeline does. The local
replay, its timer, the follow-latest flag, the wrap to the first frame, the
timerActive diagnostic and the reduced-motion "manual history available"
message are removed.
2026-11-14 20:26:26 -08:00
Sameh Khamis 3cfd414e70 The main timeline is the only time control; the Weather panel shows its frame time
The Weather panel kept a second time transport of its own: a slider, step
buttons, Latest and Play, backed by a replay in the weather clock that
stepped the observed frames every two seconds and went round from the last
to the first. The weather clock also attached itself to the world clock as
a driver, so the timeline's Play became that replay whenever weather was on.

The panel now shows the frame time only: the newest frame and its age while
live, the timeline time alone at a past time. The weather clock no longer
has setTarget, step, latest, play, pause or togglePlay, its state no longer
has playing, and it no longer drives the world clock. It still follows the
world time, registers each product's frames as marks the timeline's arrow
keys step to, holds its products, and gates the world's continuous play at
each next frame until it loads. weatherClock.world names the world clock it
follows.

A weather row's play, latest and step params move the world clock as the
timeline does: play toggles continuous play (from the start of the covered
time when live), latest goes live, and step goes to the previous or next
mark. The rail timeline component, which only the panel used, is removed,
along with its unused messages in every locale.
2026-11-14 20:25:31 -08:00
Sameh Khamis 1cba89d860 Merge branch 'main' into weather/source-names 2026-11-14 19:58:48 -08:00
Manjunath PaparajuandSameh Khamis 1dc5a67518 Bundle Wikidata cities for offline place lookups
A pack of the world's settlements of 15,000 people or more (31,827 places,
1.6 MB, CC0) built from the Wikidata Query Service by
scripts/build-wikidata-cities.mjs. src/data/wikidataCities.js reads it on the
first lookup and answers the nearest city to a point, the cities around it,
a city by name ("Paris, France", or the nearest namesake in view) and a
country's name in the page language. Names match without accents or case.

The build leaves out groupings of settlements (metropolitan areas,
agglomerations) and places that no longer stand, keeps a city that is also a
narrower grouping kind (Istanbul) or a unit dissolved where nothing else
names the spot (the Vietnamese provincial cities of 2025), fills a country
from the area a place lies in, splits a population band the service cannot
answer in time, and fails if well-known cities are missing or a known
grouping gets in. The app never calls Wikidata itself.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 19:27:46 -08:00
Sameh Khamis 7f1c35c5ed A weather source can name its provider for each product
The Rain radar, Lightning density and Satellite clouds rows named NOAA
nowCOAST until a manifest named its own source, whatever the source read.
A weather source may now supply sourceName(kind), kind being radar,
lightning or satellite; the row shows that name until a manifest names one.
The name is read once when the layer is built and held to the same rule as
a manifest's display.source. A source without names, or one whose name is
refused or fails, still shows NOAA nowCOAST.
2026-11-14 19:20:02 -08:00
Sameh Khamis 35d3a1a8fe Merge branch 'main' into sameh/scene-file-format 2026-11-14 18:04:21 -08:00
Sameh Khamis c19d82d906 feat(i18n): an application uses Core's translator and words Core's messages its own way
The ./i18n entry exports t, tn, msg, translateKnown and the number and date
formatters. setMessageOverrides({ en: {...}, de: {...} }) shows the
application's text for any catalog key: in the active locale, else its
English, else Core's translation.
2026-11-14 17:51:03 -08:00
Sameh KhamisandManjunath Paparaju 4c4f24355f Give the voice test's stub row a classList.contains
focusDataLayerRow now asks whether the finder hides the row.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 16:27:54 -08:00
Sameh KhamisandManjunath Paparaju a375ace8eb Test the Data Layers finder and section index
Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 16:25:32 -08:00
Sameh KhamisandManjunath Paparaju f940484498 Read the section index only when the groups change, and keep its label on screen
Live counts repaint the rows many times a second; the index now reads its
headings at most once a frame, and only when a heading changes. The label
goes before the tick when there is no room after it, as in a right-to-left
language, and the ticks sit at the panel's inline end.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 16:25:32 -08:00
Sameh KhamisandManjunath Paparaju 919ea8d4c0 Make the Data Layers finder keep Escape, its category and keyboard users
- Escape in the field closes the search alone, focus back on its button,
  without folding the panel or leaving the cockpit.
- The category is the group's key (LayerRows' data-group), so a language
  change keeps it; a group that goes away clears it.
- On and Off follow whether the layer is enabled, not its feed's state.
- The filter is a disclosure of two groups of toggle buttons: opening it
  focuses the chosen option, the arrows, Home and End move, Escape closes
  it, and it stays inside the panel.
- No layers match is translated, the search matches names only, and the
  rows are filtered once per frame at most.
- A voice request to show a layer the finder hides clears the finder.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 16:25:32 -08:00
Sameh KhamisandManjunath Paparaju dcabbb4141 Translate the Data Layers finder and section index
Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 16:16:24 -08:00
Sameh KhamisandManjunath Paparaju 8da6fb7931 Find layers by the name on screen, and share the finder's state in its module
The finder matched only the English name, so a search in another language
found nothing. Its header buttons and its bar share state through the
component's module rather than a global.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 16:16:24 -08:00
Manjunath PaparajuandSameh Khamis 0550f7c2d2 Place the Data Layers group label outside the panel and soften the finder's focus
The group label is drawn on the page just outside the panel's edge instead
of over the rows; the panel clips what leaves it, so the label is teleported
to the body and placed from the tick. The finder shows focus as a soft ring
on the field's frame in place of the hard outline round the bare input.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 16:12:59 -08:00
Manjunath PaparajuandSameh Khamis 584e77dc4f Add a finder and a section index to Data Layers
The Data Layers panel gains a search field, an All / On / Off and category
filter, and a tick index down its side for jumping between sections. Both
sit beside the Data Library browse control.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 16:12:59 -08:00
Sameh KhamisandManjunath Paparaju 394bfe71c4 Leave the map credits' place in the cockpit to the credits change
Restyling them here moved the credits where the attribution layout model
cannot follow.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:54:32 -08:00
Sameh KhamisandManjunath Paparaju 95a0be13b6 Open the cockpit panels from the rail in one step and give focus back to it
The rail's buttons are disclosures (aria-expanded, aria-controls) and go
through one toggleCockpitUtility action, which already keeps one panel open.
Collapsing returns focus to the rail when the launcher's toggle is hidden.
The edge switch is gone: edge mode does not apply in the cockpit.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:51:13 -08:00
Sameh KhamisandManjunath Paparaju 0c295ea770 Give only the desktop cockpit the rail look, placed by the layout pass
Cyber and the phone keep their own cockpit layouts. Display and Radio take
their top from the layout pass, whose floor now matches the left rail, so the
panel's height still stops short of the briefing. The rail keeps its physical
side in a right-to-left language, the Live Signals rows use logical sides,
and rules for the launchers' old strips, which the rail hides, are gone.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:51:13 -08:00
Sameh KhamisandManjunath Paparaju d157198ecb Load the cockpit panel styles with the shell, formatted
Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:51:13 -08:00
Sameh KhamisandManjunath Paparaju af7f659f0d Keep the cockpit briefing's Regional News page
The restyle had dropped the page and its tab while the news section, its
fetch and the briefing tests still use it.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:51:13 -08:00
Manjunath PaparajuandSameh Khamis 52b256ce94 Hide Visual from the rail in cockpit
In cockpit the left rail now shows Data Layers only: Visual's rail item
and panel are hidden there, as Authoring and Utilities are, and the
cockpit's own Display panel stands in for it. The rules that trimmed
Visual's groups in cockpit are removed as no longer reachable. If Visual
was open on entering cockpit, the rail's sliding highlight no longer
stays where its hidden icon was.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:51:13 -08:00
Manjunath PaparajuandSameh Khamis 50086f56b6 Hide Clean UI from the Visual panel in cockpit
Cockpit's Visual panel now also leaves out the Clean UI toggle, as it
does the celestial ring.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:51:13 -08:00
Manjunath PaparajuandSameh Khamis 5b3be915c8 Keep only what cockpit Display lacks in the Visual panel
In cockpit, the Visual panel hides the HUD, detection, parameters and
3D-model groups, which the cockpit's Display panel already offers, and
the celestial ring toggle. It keeps Look, Map source, Scope, Bloom and
Sharpen.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:51:13 -08:00
Manjunath PaparajuandSameh Khamis fd1f516192 Tighten the cockpit briefing's rows and weather tiles
Live Signals rows drop from 43.5 px to 36 px with less padding. The local
weather tiles lose the lit grid that showed between them, stand 5 px apart
as cards and shrink from 60 px to 53 px, so all four fit the shorter
window.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:51:13 -08:00
Manjunath PaparajuandSameh Khamis 839409a3fc Fit the side rails and panels to cockpit view
- The left rail shows Data Layers and Visual in cockpit view; Authoring and
  Utilities are hidden along with their slots and panels, so no empty
  pieces are left behind.
- With a panel open there, each icon left is its own island; a hidden
  panel still open underneath leaves the rail in one piece.
- The cockpit's Display and Radio rail uses the main rails' 12 px spacing
  and draws each icon as its own island while a panel is open.
- Contact and Live Signals stand shorter (min(27vh, 260px)); Contact's
  buttons share the kicker's row and its subject runs full width on at
  most two lines.
- Edge scrolling skips rail items that are not shown.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:51:13 -08:00
Manjunath PaparajuandSameh Khamis 733b227c01 Restyle the cockpit side panels
The cockpit's Display and Radio panels get the compact dark styling of the
rest of the shell, a header with the edge switch and collapse, and a rail.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:51:13 -08:00
Sameh KhamisandManjunath Paparaju 1500ffeb17 Keep Cyber's skin on the Visual panel and give its pills to aria-pressed
The card skin stands aside under the Cyber HUD; the pills are decoration
for screen readers, the buttons carry aria-pressed, and Clean UI, an action,
has no pill. A collapsed Visual hides its new parts too.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 2fd8d32a5d Keep Scope opacity at half or more, and ignore a value that is not a number
A fainter mask reads as Scope off while labels still fade outside the
keyhole and the HUD gets no backplates.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju a1e7b2a394 Make a reset inside Contacts the detection that leaving puts back
Contacts lends detection for the session and puts back the earlier state
on leaving; a share link carries that earlier state. A reset in Visual is
the user's choice, so it becomes both.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 63dc5756d5 Leave the presets out of Visual in the phone's More sheet
The More sheet already shows the dock's Visual Presets tray right above
Visual, so the panel's own copy is hidden on the phone shell.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 81a18d327a Expect the Visual panel's name and its new order in the markup tests
The panel is headed VISUAL, and Clean UI now sits with the overlay
controls, above the Effects group.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 6c1744e55b Test scope opacity, the Visual reset and the panel's presets
The share codec carries field 30 and still encodes a link made before it
to the same code; the long form writes sco only below full opacity; the
mask fades without repainting; the panel shows and sets the opacity, and
the reset and the presets call their actions.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 471151910d Read scope opacity for share links from the mask, as feather is
The share state took the opacity the slider last showed, which a
show-only update can leave apart from the mask; it now reads the mask
itself, as the feather does. The test reset also restores full opacity.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 5383b470ee Translate the Visual panel and keep its name apart from the satellite class
The panel's new headings, the scope opacity slider and the reset are
translated in every language, and the presets' names and descriptions use
the dock's translations. The cards' on/off pills take their words from the
locale instead of fixed English. VISUAL, the panel's name on the rail and in
its header, gets its own translation context: several languages had it as
"visible", from the satellite class of the same English word.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 493e76401e Own the Visual panel's new files in the package boundaries and format scope
Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:33 -08:00
Sameh KhamisandManjunath Paparaju 1dcc025832 Format the scope opacity changes
Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 15:31:25 -08:00
Manjunath PaparajuandSameh Khamis 8379a0ffd3 Wire scope opacity and the Visual reset into the shell
The application shell passes the scope opacity setter and the global post
defaults through to the Display bindings, the stylesheet imports the Visual
panel and Scope-off styles, and the icon subset gains the reset icon.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:31:25 -08:00
Manjunath PaparajuandSameh Khamis d145c30967 Add scope opacity and a reset to the Visual panel
The Display panel is regrouped as Visual (Look, Overlay, Effects) with style
presets, a scope opacity slider and a reset to the default look. Scope
opacity is carried in share links as field 30 and as the legacy sco
parameter; 100 is the default and is left out of a link.

Co-authored-by: Sameh Khamis <[email protected]>
2026-11-14 15:31:25 -08:00
Sameh Khamis 6f830a2e70 Let a host application place live contacts
createApplicationCatalog takes an optional placement provider. Its
layer(kind) hands Flights, Military Flights, AIS Vessels, Transit and Rail
a place(id, wallMs), and optionally observe(id, fix) and forget(id). While
live, each layer tells it every fix it accepts and draws a contact where it
places it: position, height (the layer's own when none is given), course
and speed. A null answer, a provider that throws, a kind it leaves out, or
no provider at all keeps the layer's own motion unchanged, and following
the world clock the layers never ask it. A transit vehicle or train it
holds still lets the render loop idle and is asked again each second;
vessels ask for frames only while it places one, and stop when the layer
turns off.

Tests show the layers draw and reconcile exactly as before without a
provider and with one that places nothing, draw where a provider places
them, and idle while it holds them still.
2026-11-14 15:07:25 -08:00
Sameh Khamis 5d733eb07b Fix scrubs to live on long tracks, fire ages in a past window, and gaps between hourly cloud frames
- World timeline: a scrub counts as at now within a share of the track
  (0.5%), now held to at most two minutes. A layer whose source covers
  years made the window weeks wide, so most of the last month snapped to
  live. Short tracks keep their window.
- Fires: card and detail ages count back from the end of the 24 hours the
  layer shows when its snapshot ends at a set time, and from now only when
  it runs to now. A window that ended months ago read "new 105d" on fires
  three hours old at the time shown.
- Weather: a product's max gap and the delay after which its latest frame
  reads late are at least one and a half of its feed's median frame
  spacing (over its latest frames). A regional cloud feed with frames an
  hour apart drew nothing for the second half of every hour while
  scrubbing and read late after twenty minutes; feeds with frames minutes
  apart keep their gaps.
2026-11-14 14:52:48 -08:00
Sameh KhamisandManjunath Paparaju 7578fcc9a1 Own the rail modules in their package boundaries and format scope
Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 14:01:04 -08:00
Sameh KhamisandManjunath Paparaju c54ead9432 Test the rails' new layout and translate the edge switch
CCTV and Context take the full right column when open, so the hysteresis
tests use a content-sized panel, and a test covers the full-height rule.

Co-authored-by: Manjunath Paparaju <[email protected]>
2026-11-14 13:42:06 -08:00