From cd231faae9028d029efb94a704c8e39cf9c1e7ca Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 08:33:18 -0800 Subject: [PATCH] feat(panels): build the rail panels on one UiPanel The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level and Radio panels each wrote the same frame by hand. They are now one kit component, UiPanel, with a title, badge/actions/default slots and v-model:collapsed, drawing Core's panel markup and classes beside the kit's; props cover the differences (sideways, glow, scroll, railScroller, tag, toggleWords). RailPanel binds it to the panel chrome store: v-panel-state writes its state classes at once, and its button's clicks reach PanelChrome. UiPanelToggle is the collapse button UiPanel and PanelCollapseButton share. UiPanel's collapsed drives the button and ui-panel--collapsed only; the owner hides the body by writing the root's collapsed class, since the rails measure a panel in the task that opens it. PanelCollapseButton takes startCollapsed instead of initial and initialTitle, so a button shows its state's glyph and words before startup presents the panel. Titles render through t() instead of data-i18n, and the unused "Expand panel" string is pruned. v-panel-state redraws the presented state after a re-render, in case a root's class binding ever changes. --- CHANGELOG.md | 8 ++ docs/VUE-MIGRATION.md | 21 ++- docs/panel-surfaces.md | 14 +- src/contextTabKeyboard.test.mjs | 6 +- src/i18n/locales/de.json | 1 - src/i18n/locales/es.json | 1 - src/i18n/locales/fr.json | 1 - src/i18n/locales/it.json | 1 - src/i18n/locales/ja.json | 1 - src/i18n/locales/pt.json | 1 - src/i18n/locales/ru.json | 1 - src/i18n/locales/uk.json | 1 - src/i18n/locales/zh-CN.json | 1 - src/i18n/locales/zh-Hant.json | 1 - src/streetLevelMarkup.test.mjs | 6 +- src/tooling/panelStorageDocs.test.mjs | 2 +- src/ui/contextPanel.test.mjs | 16 ++- src/ui/cyberTheme.test.mjs | 4 +- src/ui/kit/UiPanel.vue | 74 ++++++++++ src/ui/kit/UiPanelToggle.vue | 54 ++++++++ src/ui/panelChromeButtons.test.mjs | 166 ++++++++++++++++++++--- src/ui/rail/PanelCollapseButton.vue | 66 +++------ src/ui/rail/RailPanel.vue | 41 ++++++ src/ui/rail/panelState.js | 7 + src/ui/views/ContextView.vue | 47 ++----- src/ui/views/LayerPanelsView.vue | 29 +--- src/ui/views/panels/CctvPanel.vue | 15 +- src/ui/views/panels/DisplayPanel.vue | 4 +- src/ui/views/panels/RadioPanel.vue | 17 +-- src/ui/views/panels/StreetLevelPanel.vue | 17 +-- 30 files changed, 427 insertions(+), 197 deletions(-) create mode 100644 src/ui/kit/UiPanel.vue create mode 100644 src/ui/kit/UiPanelToggle.vue create mode 100644 src/ui/rail/RailPanel.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bbcc175..3db16bbf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ the bottom tab bar. The rail is the first view built with Vue and Pinia, which now ship with the app; see docs/VUE-MIGRATION.md (samehkhamis). +- The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level + and Radio panels are one component, `UiPanel` (`src/ui/kit/`), with the + same markup plus the kit's `ui-panel` classes, their titles translated by + `t()` rather than `data-i18n`; `RailPanel` binds it to the panel chrome + store. `PanelCollapseButton` drops its `initial` and `initialTitle` props + and takes `startCollapsed`, so before startup presents a panel its button + already shows that state's glyph, words and `aria-expanded` (samehkhamis). + - The page's panels, dock, cockpit and dialogs are Vue views with the same markup as before. Hosts that do not initialize Core's locale pass `localePicker: false` to `createApplicationControls` to drop the language diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 402010caa..1cb7fdd52 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -36,14 +36,20 @@ one-panel rule too. Share links continue to record which panels are open. ## Component kit -`src/ui/kit/` holds the generic components: `UiRail`, `UiTooltip`, `UiIcon` -and `UiButton` so far, with more (`UiPanel`, `UiSegmented`, `UiSelect` and -so on) joining as the controls they serve are converted. Names, props, +`src/ui/kit/` holds the generic components: `UiRail`, `UiTooltip`, `UiIcon`, +`UiButton` and `UiPanel` so far, with more (`UiSegmented`, `UiSelect` and +so on) joining as the controls they serve are converted. One component +serves every use of a control, with props for the differences (`UiPanel`'s +`sideways`, `glow`, `scroll`), rather than a copy per panel. Names, props, events and class names follow Halfpixel's `ui-vue` kit so the two can merge later, but the components are written for this repository and styled by its existing stylesheet and custom properties; a kit class such as `ui-button--icon` carries no Core style yet, and a converted control keeps its own classes beside it. They contain no application state. +`UiPanel` takes ui-vue's `title`, slots and `v-model:collapsed`, but its +`collapsed` drives only the button and `ui-panel--collapsed`: the panel's +owner hides the body by writing the root's `collapsed` class at once, and +there is no `closable`, since Core's panels collapse rather than close. Controls share one skin, so every panel reads the same. Every scrolling surface carries the kit's `.ui-scroll` (ui-vue's name), which draws the thin @@ -64,8 +70,10 @@ a feature supplying only its accent. when a view needs one, mirroring state an owner already publishes, so there is never a second owner of the same state. 2. **Rail.** `PanelChrome` enforces one panel per side; the panel store - mirrors each side's panels and the rail renders from it. Each panel's - collapse button (`PanelCollapseButton`), the dock tray toggles and the + mirrors each side's panels and the rail renders from it. The rail panels + are `RailPanel`s: the kit's `UiPanel` drawing Core's panel markup, bound to the `gev-panel-chrome` + store, with their titles rendered by `t()` rather than `data-i18n`. Each panel's + collapse button, the dock tray toggles and the dock pins render the open and pinned state PanelChrome presents to the `gev-panel-chrome` store, and their clicks call it there. Each panel's root draws its own state from there too (`v-panel-state`: `collapsed`, @@ -75,7 +83,8 @@ a feature supplying only its accent. watcher, not on Vue's next render, because the rails measure a panel in the same task that opens or closes it. Done. 3. **Panels.** Each template and its wiring become a component, keeping ids, - classes, `data-i18n` text and CSS. The page places each view with a + classes and CSS; text a template renders with `t()` drops its `data-i18n` + marker. The page places each view with a `.gev-view` host (`display: contents`), and `mountApplicationViews()` (`src/ui/views/index.js`) renders the view into it before the page is translated and wired. `createApplication()` calls it too, removing the diff --git a/docs/panel-surfaces.md b/docs/panel-surfaces.md index e2fa75728..30f1be88c 100644 --- a/docs/panel-surfaces.md +++ b/docs/panel-surfaces.md @@ -25,7 +25,6 @@ surface. Keep the header and any outward popup outside the scrolling body: class="panel-surface-control" target="example-panel" :name="t('Example')" - initial="−" />
@@ -90,10 +89,15 @@ Do not copy the rail example's `.panel-collapse-btn` class into the dock: existing dock rules intentionally hide that class. The generic surface skin does not replace the dock's disclosure policy. -`PanelCollapseButton` (`src/ui/rail/`) renders a panel's collapse button -(`.panel-collapse-btn`) from the open state `PanelChrome` presents to the -`gev-panel-chrome` store, and its click asks `PanelChrome` to toggle the -panel through `setPanelCollapsed`. `PanelChrome` collects the buttons' +A panel with Core's standard header (glow, title, badges, divider, actions +and collapse button) is a `RailPanel` (`src/ui/rail/`): the kit's `UiPanel` +bound to the `gev-panel-chrome` store, with its state classes drawn by +`v-panel-state` and its button's click asking `PanelChrome` to toggle it. +A panel with its own header uses `PanelCollapseButton`, which renders the +collapse button (`.panel-collapse-btn`) from the open state `PanelChrome` +presents to that store; until then, both show their `startCollapsed` state, +which should match the markup's `collapsed` class. +Either way, the click reaches `PanelChrome`'s `setPanelCollapsed`. `PanelChrome` collects the buttons' targets at initialization and binds each panel's Escape. For additions present at startup, keep those hooks and the existing chrome lifecycle. A dynamically mounted panel must own its disclosure: its own state, button and diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index 71c1b8aa6..e3b5add68 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -97,11 +97,11 @@ test('Context async action buttons remain focused while busy', () => { test('Context header holds only its title and collapse control', () => { const panel = html.match(/
([\s\S]*?)<\/div>\s*
([\s\S]*?)<\/div>\s*
- `${tag}.${cls.split(' ').filter((name) => !/^ui-(button|focus)/.test(name)).join(' ')}`); + `${tag}.${cls.split(' ').filter((name) => !/^ui-/.test(name)).join(' ')}`); assert.deepEqual(tags, ['span.panel-title', 'span.panel-divider', 'button.panel-collapse-btn']); }); diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index fa56aaf84..3c6d8a2d8 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -746,7 +746,6 @@ "Search any location...": "Ort suchen...", "WEATHER": "WETTER", "CONTEXT": "KONTEXT", - "Expand panel": "Bereich ausklappen", "Context mode": "Kontextmodus", "CONTACTS": "KONTAKTE", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Wechselt zwischen den nächstgelegenen Kontakten des gewählten Typs: Flugzeuge, Schiffe, Anlagen. Satelliten werden unabhängig verfolgt.", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index ebabfe1b2..3693b127f 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -176,7 +176,6 @@ "AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR": "SOLO DATOS DISPONIBLES · NO IMPLICA TODO DESPEJADO", "Enable cockpit weather effects": "Activar los efectos climáticos de la cabina", "CONTEXT": "CONTEXTO", - "Expand panel": "Expandir panel", "Context mode": "Modo de contexto", "CONTACTS": "CONTACTOS", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Recorre los contactos más cercanos del tipo que elijas — aeronaves, buques, instalaciones. Los satélites se rastrean por separado.", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index e768b6a89..a3e967758 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -2791,7 +2791,6 @@ "RECENT IMAGERY": "IMAGERIE RÉCENTE", "Imagery days in the box": "Jours d'imagerie dans la zone", "CONTEXT": "CONTEXTE", - "Expand panel": "Développer le panneau", "Context mode": "Mode de contexte", "CONTACTS": "CONTACTS", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Parcourt les contacts les plus proches du type sélectionné — avions, navires, installations. Les satellites sont suivis séparément.", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index f1bf08e73..73cfdb2c4 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -2791,7 +2791,6 @@ "RECENT IMAGERY": "IMMAGINI RECENTI", "Imagery days in the box": "Giorni con immagini nel riquadro", "CONTEXT": "CONTESTO", - "Expand panel": "Espandi il pannello", "Context mode": "Modalità contesto", "CONTACTS": "CONTATTI", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Scorre i contatti più vicini del tipo selezionato: aerei, navi, installazioni. I satelliti vengono tracciati in modo indipendente.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 130308457..2968f050f 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -2649,7 +2649,6 @@ "RECENT IMAGERY": "最近の衛星画像", "Imagery days in the box": "範囲内の画像のある日", "CONTEXT": "コンテキスト", - "Expand panel": "パネルを展開", "Context mode": "コンテキストモード", "CONTACTS": "コンタクト", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "選択した種類(航空機、船舶、施設)の最寄りのコンタクトを順に切り替えます。衛星は独立して追跡されます。", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 6ba74601f..82f306093 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -2793,7 +2793,6 @@ "RECENT IMAGERY": "IMAGENS RECENTES", "Imagery days in the box": "Dias com imagens na área", "CONTEXT": "CONTEXTO", - "Expand panel": "Expandir painel", "Context mode": "Modo de contexto", "CONTACTS": "CONTATOS", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Percorre os contatos mais próximos do tipo que você selecionar — aviões, embarcações, instalações. Os satélites são rastreados de forma independente.", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 7cc27b20c..746288a94 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -707,7 +707,6 @@ "FLIR": "FLIR", "Style": "Стиль", "STATION SITE": "САЙТ СТАНЦИИ", - "Expand panel": "Развернуть панель", "CONTACTS": "КОНТАКТЫ", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Перебирает ближайшие контакты выбранного типа — самолёты, суда, объекты. Спутники отслеживаются отдельно.", "SEARCH NEARBY SITES": "ПОИСК ОБЪЕКТОВ РЯДОМ", diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index 238c94fef..c27ea6332 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -2861,7 +2861,6 @@ "RECENT IMAGERY": "СВІЖІ ЗНІМКИ", "Imagery days in the box": "Дні зі знімками в області", "CONTEXT": "КОНТЕКСТ", - "Expand panel": "Розгорнути панель", "Context mode": "Режим контексту", "CONTACTS": "КОНТАКТИ", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Гортає найближчі контакти вибраного вами типу — літаки, судна, обʼєкти. Супутники відстежуються окремо.", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index d6b46cda6..dbffef805 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -2545,7 +2545,6 @@ "RECENT IMAGERY": "近期影像", "Imagery days in the box": "视野内的影像天数", "CONTEXT": "态势", - "Expand panel": "展开面板", "Context mode": "态势模式", "CONTACTS": "目标", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "循环切换你所选类型中最近的目标,包括飞机、船舶、设施。卫星独立跟踪。", diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index caa1f5948..f3f89690e 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -2533,7 +2533,6 @@ "RECENT IMAGERY": "近期影像", "Imagery days in the box": "視野內的影像天數", "CONTEXT": "態勢", - "Expand panel": "展開面板", "Context mode": "態勢模式", "CONTACTS": "目標", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "循環切換你所選類型中最近的目標,包括飛機、船舶、設施。衛星獨立跟蹤。", diff --git a/src/streetLevelMarkup.test.mjs b/src/streetLevelMarkup.test.mjs index 5cf9ed0a5..7e2e32314 100644 --- a/src/streetLevelMarkup.test.mjs +++ b/src/streetLevelMarkup.test.mjs @@ -18,7 +18,7 @@ const layoutController = read('src/ui/panelLayoutController.js'); test('Street Level is a collapsible right-rail panel that starts collapsed', () => { assert.match( html, - /