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,
- /
/,
+ /
/,
);
assert.match(
html,
@@ -26,7 +26,7 @@ test('Street Level is a collapsible right-rail panel that starts collapsed', ()
);
assert.match(
html,
- /STREET LEVEL<\/span>/,
+ /STREET LEVEL<\/span>/,
);
// Provider-neutral header: no vendor mark; the inner restores its scroll.
assert.doesNotMatch(html, /sl-mark|mly-mark/);
@@ -49,7 +49,7 @@ test('the panel is registered with panel chrome, cockpit entry and the right rai
// A right-rail panel's collapse button opens sideways (◀ / ▶).
assert.match(
read('src/ui/views/panels/StreetLevelPanel.vue'),
- /]*target="street-level-panel"[^>]*\bsideways\b/,
+ /]*\bsideways\b/,
);
assert.match(
layoutController,
diff --git a/src/tooling/panelStorageDocs.test.mjs b/src/tooling/panelStorageDocs.test.mjs
index d1179a397..5a83f9fb7 100644
--- a/src/tooling/panelStorageDocs.test.mjs
+++ b/src/tooling/panelStorageDocs.test.mjs
@@ -84,7 +84,7 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
);
assert.match(
html,
- /
]*class="panel-collapsible collapsed"/,
+ /]*\bstart-collapsed\b/,
'the CCTV panel starts collapsed in the markup',
);
diff --git a/src/ui/contextPanel.test.mjs b/src/ui/contextPanel.test.mjs
index 670c84f70..8de52cbb2 100644
--- a/src/ui/contextPanel.test.mjs
+++ b/src/ui/contextPanel.test.mjs
@@ -53,7 +53,10 @@ function fixture(t) {
test('before ContextControls renders, the switch shows its initial markup', (t) => {
const { $ } = fixture(t);
const panel = $('global-context-panel');
- assert.equal(panel.className, 'panel-collapsible collapsed');
+ assert.equal(
+ panel.className,
+ 'ui-panel panel-collapsible collapsed ui-panel--collapsed',
+ );
assert.equal(panel.hasAttribute('data-context-mode'), false);
for (const id of [
'global-context-flights-btn',
@@ -118,14 +121,21 @@ test('the panel carries the mode beside the classes its chrome writes', async (t
panel.classList.remove('collapsed');
controls._contextMode = 'flights';
await sync();
- assert.equal(panel.className, 'panel-collapsible context-enabled');
+ // Without an owner the button, and the kit class, keep the start state.
+ assert.equal(
+ panel.className,
+ 'ui-panel panel-collapsible ui-panel--collapsed context-enabled',
+ );
assert.equal(panel.getAttribute('data-context-mode'), 'flights');
assert.equal($('context-flights-view').hidden, false);
panel.classList.add('collapsed');
controls._contextMode = null;
await sync();
- assert.equal(panel.className, 'panel-collapsible collapsed');
+ assert.equal(
+ panel.className,
+ 'ui-panel panel-collapsible ui-panel--collapsed collapsed',
+ );
assert.equal(panel.getAttribute('data-context-mode'), 'none');
assert.equal($('context-mode-standby').hidden, false);
assert.equal($('context-flights-view').hidden, true);
diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs
index 832b81e33..adde0637a 100644
--- a/src/ui/cyberTheme.test.mjs
+++ b/src/ui/cyberTheme.test.mjs
@@ -28,7 +28,9 @@ test('CCTV and Context keep their headers outside Cyber-only scroll bodies', ()
/class="cyber-panel-body ui-scroll" data-rail-scroller/,
);
assert.ok(
- markup.indexOf('class="panel-header"') <
+ // The header is RailPanel's; the body is its first content.
+ markup.indexOf(' -1 &&
+ markup.indexOf('
+import { vClassToggle } from '../rail/classToggle.js';
+import UiPanelToggle from './UiPanelToggle.vue';
+
+// Core's panel, with @halfpixel/ui-vue UiPanel's name, title, slots and
+// v-model:collapsed so the two kits can merge. It draws Core's panel markup
+// (glow, inner, a header with the title, badges, divider, actions and the
+// collapse button, then the body) and keeps Core's classes beside the kit's.
+// Unlike ui-vue's, `collapsed` does not hide the body: the root's classes
+// stay as rendered, and whatever owns the panel's state writes its
+// `collapsed` class at once (PanelChrome, through vPanelState), because the
+// rails measure a panel in the task that opens it. `collapsed` drives the
+// button and the kit's `ui-panel--collapsed`, toggled without a re-render.
+// There is no Close: Core's panels collapse.
+const props = defineProps({
+ /** The root's id, its inner's class (`{id}-inner`) and the button's target. */
+ id: { type: String, required: true },
+ /** The heading, already translated; also the button's "Expand {name}". */
+ title: { type: String, required: true },
+ /** The heading's tip. */
+ hint: { type: String, default: undefined },
+ tag: { type: String, default: 'div' },
+ collapsible: { type: Boolean, default: false },
+ collapsed: { type: Boolean, default: false },
+ /** A right-rail panel opens and closes sideways: ◀ / ▶, not + / −. */
+ sideways: { type: Boolean, default: false },
+ /** Words that replace "Expand/Collapse {title}" on the button. */
+ toggleWords: { type: Object, default: null },
+ glow: { type: Boolean, default: true },
+ /** The inner scrolls with Core's scroll bar. */
+ scroll: { type: Boolean, default: false },
+ /** The inner is a scroller the right rail keeps in view. */
+ railScroller: { type: Boolean, default: false },
+});
+const emit = defineEmits(['update:collapsed']);
+
+
+
+
+
+