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.
This commit is contained in:
Sameh Khamis committed 2026-11-01 08:33:18 -08:00
1 parent a3fc475cbe
commit cd231faae9
30 files changed
+427 -197

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
<script setup>
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']);
</script>
<template>
<component
:is="props.tag"
:id="props.id"
v-class-toggle="{
'ui-panel--collapsed': props.collapsible && props.collapsed,
}"
class="ui-panel panel-collapsible"
>
<div v-if="props.glow" class="panel-glow"></div>
<div
:class="[`${props.id}-inner`, { 'ui-scroll': props.scroll }]"
:data-rail-scroller="props.railScroller ? '' : undefined"
>
<div class="ui-panel__head panel-header">
<slot name="title">
<span class="ui-panel__title panel-title" :title="props.hint">{{
props.title
}}</span>
</slot>
<slot name="badge" />
<span class="panel-divider"></span>
<slot name="actions" />
<UiPanelToggle
v-if="props.collapsible"
:target="props.id"
:name="props.title"
:collapsed="props.collapsed"
:sideways="props.sideways"
:words="props.toggleWords"
@update:collapsed="emit('update:collapsed', $event)"
/>
</div>
<slot />
</div>
</component>
</template>
+54
View File
@@ -0,0 +1,54 @@
<script setup>
import { computed } from 'vue';
import { msg, t } from '../../i18n/index.js';
import UiButton from './UiButton.vue';
// A panel's collapse button, internal to the kit: UiPanel draws it, and so
// does PanelCollapseButton for the panels not built on UiPanel. Its glyph and
// words follow `collapsed`; a click asks for the other state.
const props = defineProps({
/** The panel's id, which PanelChrome finds the button by. */
target: { type: String, required: true },
/** The panel's name in "Expand {name}", already translated. */
name: { type: String, default: () => t('panel') },
collapsed: { type: Boolean, required: true },
/** A right-rail panel opens and closes sideways: ◀ / ▶, not + / −. */
sideways: { type: Boolean, default: false },
/** Words that replace "Expand/Collapse {name}" (Radio's own wording). */
words: { type: Object, default: null },
});
const emit = defineEmits(['update:collapsed']);
const glyph = computed(() => {
if (props.sideways) return props.collapsed ? '◀' : '▶';
return props.collapsed ? '+' : '−';
});
const label = computed(() => {
if (props.words)
return props.collapsed
? props.words.expandLabel
: props.words.collapseLabel;
const key = props.collapsed ? msg('Expand {name}') : msg('Collapse {name}');
return t(key, { name: props.name });
});
const title = computed(() => {
if (props.words)
return props.collapsed
? props.words.expandTitle
: props.words.collapseTitle;
return label.value;
});
</script>
<template>
<UiButton
variant="icon"
class="panel-collapse-btn"
:data-collapse-target="target"
:label="label"
:title="title"
:aria-expanded="String(!collapsed)"
@click="emit('update:collapsed', !collapsed)"
>{{ glyph }}</UiButton
>
</template>