mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-18 05:23:57 +08:00
GalaxySelect refactor, update tests, rename components to be more accurate, Discord Support button
This commit is contained in:
@@ -544,7 +544,7 @@ ul { list-style: none; margin: 0; padding: 0; }
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.liquid-glass-nav {
|
||||
.blur-nav {
|
||||
position: fixed;
|
||||
bottom: calc(18px + env(safe-area-inset-bottom, 0px));
|
||||
left: 50%;
|
||||
@@ -571,11 +571,11 @@ ul { list-style: none; margin: 0; padding: 0; }
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
[data-theme="light"] .liquid-glass-nav {
|
||||
[data-theme="light"] .blur-nav {
|
||||
border: 1.5px solid var(--primary);
|
||||
}
|
||||
|
||||
.liquid-glass-nav .nav-item {
|
||||
.blur-nav .nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -592,28 +592,28 @@ ul { list-style: none; margin: 0; padding: 0; }
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.liquid-glass-nav .nav-item i {
|
||||
.blur-nav .nav-item i {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 2px;
|
||||
transition: transform var(--motion-fast);
|
||||
}
|
||||
|
||||
.liquid-glass-nav .nav-item:hover {
|
||||
.blur-nav .nav-item:hover {
|
||||
color: var(--on-surface);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.liquid-glass-nav .nav-item:active i {
|
||||
.blur-nav .nav-item:active i {
|
||||
transform: scale(0.88);
|
||||
}
|
||||
|
||||
.liquid-glass-nav .nav-item.active {
|
||||
.blur-nav .nav-item.active {
|
||||
background: var(--glass-active-bg);
|
||||
color: var(--primary);
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
|
||||
.liquid-glass-nav .nav-item.active i {
|
||||
.blur-nav .nav-item.active i {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@@ -791,7 +791,7 @@ body.is-scrolling .gx-tile {
|
||||
.gx-row__desc {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--fs-sm);
|
||||
margin-top: 2px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -807,14 +807,6 @@ body.is-scrolling .gx-tile {
|
||||
.gx-row--favorites { align-items: stretch; flex-direction: column; }
|
||||
.gx-row--favorites .gx-row__info { flex: none; }
|
||||
.gx-row--stack { align-items: stretch; flex-direction: column; }
|
||||
/* Keep the native keyboard/touch picker, but wrap its verified label instead
|
||||
of clipping Conditional Experimental at enlarged phone UI scales. */
|
||||
.gx-mode-select { position: relative; width: 100%; }
|
||||
.gx-mode-select__label { display: flex; align-items: center; justify-content: space-between; gap: 12px; white-space: normal; }
|
||||
.gx-mode-select__label span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.gx-mode-select__label i { flex-shrink: 0; }
|
||||
.gx-mode-select select { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
|
||||
.gx-mode-select:focus-within { outline: 2px solid var(--primary); outline-offset: 3px; border-radius: 16px; }
|
||||
.gx-row--stack .gx-row__info { flex: none; }
|
||||
.gx-row--stack .gx-field,
|
||||
.gx-row--stack .gx-slider-row { width: 100%; }
|
||||
@@ -981,7 +973,6 @@ input[type="range"].gx-slider:active::-webkit-slider-thumb {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
select.gx-field { background: var(--surface-container-high); }
|
||||
textarea.gx-field { padding: var(--sp-3); border-radius: var(--radius-md); }
|
||||
|
||||
input[type="color"].gx-color {
|
||||
@@ -1285,7 +1276,7 @@ button.gx-chip:hover {
|
||||
|
||||
.gx-drawer.open { transform: translateX(0); }
|
||||
|
||||
.gx-drawer.open ~ .liquid-glass-nav {
|
||||
.gx-drawer.open ~ .blur-nav {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(12px) translateZ(0);
|
||||
@@ -1935,75 +1926,6 @@ button.gx-chip:hover {
|
||||
flex: 0 1 240px;
|
||||
}
|
||||
|
||||
/* Custom dropdown standing in for a native <select>: Android renders a native
|
||||
select as a full-screen wall of options with no way to restyle it, so segment
|
||||
picking gets its own small themed menu instead, consistent on every platform. */
|
||||
.gx-segment-picker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gx-segment-picker__trigger {
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-sizing: border-box;
|
||||
color: var(--on-surface);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--fs-base);
|
||||
justify-content: space-between;
|
||||
min-height: 46px;
|
||||
padding: 0 var(--sp-3);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gx-segment-picker__trigger:hover { background: rgba(255, 255, 255, 0.1); }
|
||||
|
||||
.gx-segment-picker__menu {
|
||||
background: var(--surface-container-high);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
bottom: calc(100% + 4px);
|
||||
box-shadow: var(--elev-3);
|
||||
left: 0;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
padding: var(--sp-1);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[data-theme="light"] .gx-segment-picker__menu {
|
||||
border: 1.5px solid var(--primary);
|
||||
}
|
||||
|
||||
.gx-segment-picker__item {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--on-surface);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--fs-sm);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gx-segment-picker__item:hover { background: var(--glass-bg-hover); }
|
||||
|
||||
.gx-segment-picker__item.active {
|
||||
background: var(--primary-container);
|
||||
color: var(--on-primary-container);
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
|
||||
.gx-route-logs-section {
|
||||
scroll-margin-top: calc(var(--appbar-height) + var(--sp-3));
|
||||
}
|
||||
@@ -2317,7 +2239,7 @@ button.gx-chip:hover {
|
||||
.gx-install__manual { display: grid; gap: var(--sp-2); }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.liquid-glass-nav { display: flex; }
|
||||
.blur-nav { display: flex; }
|
||||
.gx-drawer { padding-bottom: calc(var(--sp-5) + env(safe-area-inset-bottom, 0px)); }
|
||||
.gx-back-btn { display: inline-flex; }
|
||||
.gx-appbar__search { flex: 1; min-width: 0; }
|
||||
@@ -2326,7 +2248,7 @@ button.gx-chip:hover {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.liquid-glass-nav { display: none; }
|
||||
.blur-nav { display: none; }
|
||||
.gx-back-btn { display: none; }
|
||||
.gx-content { padding-bottom: var(--sp-6); }
|
||||
}
|
||||
@@ -2353,7 +2275,7 @@ button.gx-chip:hover {
|
||||
.gx-monitor tbody tr:hover { background:var(--surface); }
|
||||
@media(max-width:600px) { .gx-monitor__summary { grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; } .gx-monitor__metric { padding:12px; } .gx-monitor__metric strong { font-size:22px; } }
|
||||
|
||||
/* Version and branch selector menus. */
|
||||
/* GalaxySelect: the themed dropdown used for every select in the app. */
|
||||
.gx-select { display: inline-flex; position: relative; vertical-align: middle; min-width: 0; max-width: 100%; }
|
||||
.gx-select.gx-field { padding: 0; background: var(--surface-container-high); }
|
||||
.gx-select.gx-field--full { display: flex; width: 100%; }
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Cameras } from "./views/Cameras.js"
|
||||
import { store, initRouter, navigate } from "./store.js"
|
||||
import { showSnackbar } from "./api.js"
|
||||
import { installDomTranslator } from "./i18n.js"
|
||||
import { GalaxySelect } from "./components/GalaxySelect.js"
|
||||
|
||||
window.__galaxyVue = { createApp, h }
|
||||
|
||||
@@ -87,6 +88,8 @@ const app = createApp({
|
||||
},
|
||||
})
|
||||
|
||||
app.component("GalaxySelect", GalaxySelect)
|
||||
|
||||
app.mount("#galaxy-app")
|
||||
installDomTranslator(document.getElementById("galaxy-app"))
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export const AppShell = {
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<nav class="liquid-glass-nav">
|
||||
<nav class="blur-nav">
|
||||
<button v-for="item in BOTTOM_NAV" :key="item.link" type="button"
|
||||
class="nav-item" :class="{ active: isActive(item.link) }"
|
||||
@click="bottomNavTo(item)">
|
||||
|
||||
@@ -311,11 +311,11 @@ export const GalaxyToggleCard = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select v-else-if="isSelect" class="gx-field" :disabled="locked || updating" :value="String(value ?? '')" @change="onSelect">
|
||||
<GalaxySelect v-else-if="isSelect" class="gx-field" :disabled="locked || updating" :value="String(value ?? '')" @change="onSelect">
|
||||
<option v-if="optionsLoading" value="">{{ tr("Loading...") }}</option>
|
||||
<option v-else-if="!selectOptions.length" value="">{{ tr("No options available") }}</option>
|
||||
<option v-for="opt in selectOptions" :key="String(opt.value)" :value="String(opt.value)">{{ tr(opt.label, opt.label) }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
|
||||
<input v-else-if="isText" class="gx-field" :type="param.input_type || 'text'" :value="value ?? ''"
|
||||
:placeholder="param.placeholder || ''" :disabled="locked || updating" @change="onText" />
|
||||
|
||||
@@ -57,9 +57,9 @@ export const LanguageSelector = {
|
||||
<span class="gx-row__label">{{ tr("Select language") }}</span>
|
||||
<span class="gx-row__desc">{{ tr("Galaxy uses English when no language is selected.") }}</span>
|
||||
</div>
|
||||
<select class="gx-field" :value="selected" :disabled="saving" @change="change">
|
||||
<GalaxySelect class="gx-field" :value="selected" :disabled="saving" @change="change">
|
||||
<option v-for="option in languages" :key="option.value" :value="option.value">{{ tr(option.label, option.label) }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
<GxNotice v-if="error" tone="danger" :text="error" style="margin: 0 var(--sp-4) var(--sp-4);" />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ export const LongitudinalMode = {
|
||||
data() { return { snapshot: null, pending: false, reading: false, expanded: {}, open: false, error: "", generation: 0, timer: null, disposed: false, modes: LONGITUDINAL_MODES } },
|
||||
computed: {
|
||||
mode() { return this.snapshot?.mode || "" },
|
||||
label() { return this.modes.find(m => m.value === this.mode)?.label || "Unavailable" },
|
||||
reason() { return this.snapshot?.locked ? this.snapshot.reason : this.snapshot ? "" : "Speed control state unavailable." },
|
||||
locked() { return this.pending || !this.snapshot || this.snapshot.locked },
|
||||
conditional() { return this.mode === "conditional_experimental" || this.mode === "conditional_chill" },
|
||||
@@ -66,13 +65,10 @@ export const LongitudinalMode = {
|
||||
<span v-if="reason" class="gx-row__desc" role="status">{{ reason }}</span>
|
||||
<span v-if="error" class="gx-row__desc" role="alert">{{ error }}</span>
|
||||
</div>
|
||||
<div class="gx-mode-select">
|
||||
<div class="gx-field gx-mode-select__label" aria-hidden="true"><span>{{ label }}</span><i class="bi bi-chevron-down"></i></div>
|
||||
<select id="gx-longitudinal-mode" :value="mode" :disabled="locked" aria-describedby="gx-longitudinal-description" @change="select">
|
||||
<option v-if="!snapshot" value="">Unavailable</option>
|
||||
<option v-for="m in modes" :key="m.value" :value="m.value">{{ m.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<GalaxySelect id="gx-longitudinal-mode" class="gx-field gx-field--full" :value="mode" :disabled="locked" aria-describedby="gx-longitudinal-description" @change="select">
|
||||
<option v-if="!snapshot" value="">Unavailable</option>
|
||||
<option v-for="m in modes" :key="m.value" :value="m.value">{{ m.label }}</option>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
<button v-if="conditional" type="button" class="gx-manage-btn" :aria-expanded="open" aria-controls="gx-longitudinal-children" @click="open = !open">
|
||||
{{ open ? 'Close' : 'Manage' }}<i class="bi" aria-hidden="true" :class="open ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
||||
|
||||
@@ -397,9 +397,9 @@ export const MapsPanel = {
|
||||
</div>
|
||||
<div class="gx-row" style="border-top:none; flex-wrap:wrap;">
|
||||
<span class="gx-row__label">Auto Update</span>
|
||||
<select class="gx-field" style="flex:1; min-width:160px;" :value="scheduleDraft" @change="scheduleDraft = $event.target.value">
|
||||
<GalaxySelect class="gx-field" style="flex:1; min-width:160px;" :value="scheduleDraft" @change="scheduleDraft = $event.target.value">
|
||||
<option v-for="opt in scheduleOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
<button type="button" class="gx-btn gx-btn--tonal" :disabled="savingSchedule || !scheduleDirty" @click="saveSchedule">
|
||||
{{ savingSchedule ? 'Applying...' : 'Apply' }}
|
||||
</button>
|
||||
|
||||
@@ -164,7 +164,7 @@ export const SystemMonitor = {
|
||||
<section class="gx-card gx-monitor__metric"><span>eGPU VRAM</span><strong>{{ vramUsed == null ? '—' : number(vramUsed / 1073741824) + ' GiB' }}</strong><small v-if="vramTotal != null">{{ number(vramTotal / 1073741824) }} GiB total · {{ number(100 * vramUsed / vramTotal, '%') }}</small><progress v-if="vramTotal > 0 && vramUsed != null" :value="vramUsed" :max="vramTotal" aria-label="eGPU VRAM usage"></progress></section>
|
||||
</div>
|
||||
<details class="gx-card gx-monitor__cores"><summary>CPU cores</summary><div><span v-for="core in snapshot.cores" :key="core.name">{{ core.name.toUpperCase() }} <b>{{ number(core.percent, '%') }}</b><progress :value="core.percent || 0" max="100" :aria-label="core.name + ' usage'"></progress></span></div></details>
|
||||
<div class="gx-monitor__filters"><input class="gx-field" type="search" v-model="query" aria-label="Search processes" placeholder="Search feature, process, PID or user…"/><select class="gx-field" v-model="scope" aria-label="Process group"><option value="comma">Comma processes</option><option value="users">Apps and services</option><option value="all">All processes</option></select></div>
|
||||
<div class="gx-monitor__filters"><input class="gx-field" type="search" v-model="query" aria-label="Search processes" placeholder="Search feature, process, PID or user…"/><GalaxySelect class="gx-field" v-model="scope" aria-label="Process group"><option value="comma">Comma processes</option><option value="users">Apps and services</option><option value="all">All processes</option></GalaxySelect></div>
|
||||
<p class="gx-note">{{ rows.length }} processes shown. {{ snapshot.cpuPercent == null ? 'Collecting the first CPU sample…' : '' }}</p>
|
||||
<section class="gx-card gx-monitor__table" tabindex="0" aria-label="Process table; scroll horizontally for more columns">
|
||||
<table><thead><tr><th v-for="column in [['name','Process'],['pid','PID'],['cpu','CPU'],['memoryMiB','Memory'],['user','User'],['state','Status']]" :key="column[0]" :aria-sort="ariaSort(column[0])"><button type="button" @click="sortBy(column[0])">{{ column[1] }}{{ arrow(column[0]) }}</button></th></tr></thead>
|
||||
|
||||
@@ -11,6 +11,7 @@ const TABS = {
|
||||
errors: "Error Logs",
|
||||
tmux: "Tmux Live Log",
|
||||
monitor: "System Monitor",
|
||||
discord: "Get Help via Discord",
|
||||
}
|
||||
|
||||
function parseLogDate(filename) {
|
||||
@@ -38,7 +39,7 @@ export const Logs = {
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return useTabRouting("/logs", { troubleshoot: "troubleshoot", errors: "errors", tmux: "tmux", monitor: "monitor" })
|
||||
return useTabRouting("/logs", { troubleshoot: "troubleshoot", errors: "errors", tmux: "tmux", monitor: "monitor", discord: "discord" })
|
||||
},
|
||||
created() {
|
||||
this.stream = useLogStream({ endpoint: "/api/tmux_log/live", snapshotFn: () => api.tmuxSnapshot(), interval: 2000 })
|
||||
@@ -228,6 +229,21 @@ export const Logs = {
|
||||
<SystemMonitor />
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab === 'discord'">
|
||||
<section class="gx-card">
|
||||
<div class="gx-section__header">
|
||||
<i class="bi bi-discord"></i>
|
||||
<span class="gx-section__title">{{ TABS.discord }}</span>
|
||||
</div>
|
||||
<div style="padding: var(--sp-4); display:grid; gap:12px;">
|
||||
<p class="gx-note" style="margin:0;">StarPilot has a vibrant, welcoming community discord. Stop by to chat or ask questions!</p>
|
||||
<a class="gx-btn" href="https://firestar.link/discord" target="_blank" rel="noopener" style="justify-self:start;">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Get Help via Discord
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<TroubleshootPanel />
|
||||
</template>
|
||||
|
||||
@@ -237,18 +237,18 @@ export const ModelLaboratory = {
|
||||
<label style="display:grid; gap:4px;">
|
||||
<strong style="font-size:var(--fs-sm);">Lateral model</strong>
|
||||
<small style="color:var(--text-muted); font-size:var(--fs-xs);">Path shape, curvature, lane geometry, and driving desire</small>
|
||||
<select class="gx-field" :value="configuration.lateralModel" @change="configuration.lateralModel = $event.target.value; onLateralChange()">
|
||||
<GalaxySelect class="gx-field" :value="configuration.lateralModel" @change="configuration.lateralModel = $event.target.value; onLateralChange()">
|
||||
<option value="">Choose a model</option>
|
||||
<option v-for="m in readyModels" :key="m.value" :value="m.value">{{ m.label }} · {{ m.version }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</label>
|
||||
<label style="display:grid; gap:4px;">
|
||||
<strong style="font-size:var(--fs-sm);">Longitudinal model</strong>
|
||||
<small style="color:var(--text-muted); font-size:var(--fs-xs);">Speed, acceleration, stopping, leads, and scene confidence</small>
|
||||
<select class="gx-field" :value="configuration.longitudinalModel" @change="configuration.longitudinalModel = $event.target.value; dirty = true">
|
||||
<GalaxySelect class="gx-field" :value="configuration.longitudinalModel" @change="configuration.longitudinalModel = $event.target.value; dirty = true">
|
||||
<option value="">Choose a model</option>
|
||||
<option v-for="m in candidates" :key="m.value" :value="m.value">{{ m.label }} · {{ m.version }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</label>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:6px; align-items:center;">
|
||||
<strong>{{ modelLabel(configuration.lateralModel) }}</strong><span class="gx-row__desc" style="margin:0;">steers</span>
|
||||
|
||||
@@ -281,47 +281,47 @@ export const ModelManager = {
|
||||
|
||||
<div class="gx-row" style="border-top:none;">
|
||||
<span class="gx-row__label">Active Small</span>
|
||||
<select class="gx-field" style="flex:1;" :value="activeSmallModel" :disabled="!!busy || selectionUncertain || status.isOnroad" @change="runAction('select-small', installedSmallModels.find(m => m.value === $event.target.value))">
|
||||
<GalaxySelect class="gx-field" style="flex:1;" :value="activeSmallModel" :disabled="!!busy || selectionUncertain || status.isOnroad" @change="runAction('select-small', installedSmallModels.find(m => m.value === $event.target.value))">
|
||||
<option v-for="m in installedSmallModels" :key="m.value" :value="m.value">{{ m.label || m.value }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
|
||||
<div class="gx-row" style="border-top:none;">
|
||||
<span class="gx-row__label">Active Big</span>
|
||||
<select class="gx-field" style="flex:1;" :value="activeBigModel" :disabled="!!busy || selectionUncertain || status.isOnroad" @change="$event.target.value ? runAction('select-big', installedBigModels.find(m => m.value === $event.target.value)) : runAction('select-big')">
|
||||
<GalaxySelect class="gx-field" style="flex:1;" :value="activeBigModel" :disabled="!!busy || selectionUncertain || status.isOnroad" @change="$event.target.value ? runAction('select-big', installedBigModels.find(m => m.value === $event.target.value)) : runAction('select-big')">
|
||||
<option value="">None — always use Active Small</option>
|
||||
<option v-for="m in installedBigModels" :key="m.value" :value="m.value">{{ m.label || m.value }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
|
||||
<div class="gx-row" style="border-top:none;">
|
||||
<span class="gx-row__label">Sort</span>
|
||||
<select class="gx-field" style="flex:1;" :value="sortMode" @change="sortMode = $event.target.value">
|
||||
<GalaxySelect class="gx-field" style="flex:1;" :value="sortMode" @change="sortMode = $event.target.value">
|
||||
<option value="release_date">Release Date</option>
|
||||
<option value="alphabetical">Alphabetical</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
|
||||
<div class="gx-row" style="border-top:none;">
|
||||
<label class="gx-row__label" for="gx-model-hardware">Model hardware</label>
|
||||
<select id="gx-model-hardware" class="gx-field" style="flex:1; min-width:0;" :value="hardwareFilter" @change="setHardwareFilter($event.target.value)">
|
||||
<GalaxySelect id="gx-model-hardware" class="gx-field" style="flex:1; min-width:0;" :value="hardwareFilter" @change="setHardwareFilter($event.target.value)">
|
||||
<option value="both">Both</option>
|
||||
<option value="gpu">GPU models only</option>
|
||||
<option value="comma">Comma models only</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<select class="gx-field" style="flex:1; min-width:140px;" :value="userFilter" @change="userFilter = $event.target.value">
|
||||
<GalaxySelect class="gx-field" style="flex:1; min-width:140px;" :value="userFilter" @change="userFilter = $event.target.value">
|
||||
<option value="all">Your Favorite: All</option>
|
||||
<option value="yes">Your Favorite: Yes</option>
|
||||
<option value="no">Your Favorite: No</option>
|
||||
</select>
|
||||
<select class="gx-field" style="flex:1; min-width:140px;" :value="communityFilter" @change="communityFilter = $event.target.value">
|
||||
</GalaxySelect>
|
||||
<GalaxySelect class="gx-field" style="flex:1; min-width:140px;" :value="communityFilter" @change="communityFilter = $event.target.value">
|
||||
<option value="all">Community: All</option>
|
||||
<option value="yes">Community: Yes</option>
|
||||
<option value="no">Community: No</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,6 @@ export const Recordings = {
|
||||
playerError: "",
|
||||
segments: [],
|
||||
current: 0,
|
||||
segmentMenuOpen: false,
|
||||
cameras: [],
|
||||
selectedCamera: "",
|
||||
logsRoute: null,
|
||||
@@ -213,7 +212,6 @@ export const Recordings = {
|
||||
if (!cameras.length) throw new Error("No camera video for this route.")
|
||||
this.segments = segments
|
||||
this.current = 0
|
||||
this.segmentMenuOpen = false
|
||||
this.cameras = cameras
|
||||
this.selectedCamera = cameras.includes("forward") ? "forward" : cameras[0]
|
||||
this.$nextTick(() => this.playSegment())
|
||||
@@ -246,14 +244,7 @@ export const Recordings = {
|
||||
video.load()
|
||||
video.play().catch(() => {})
|
||||
},
|
||||
toggleSegmentMenu() {
|
||||
this.segmentMenuOpen = !this.segmentMenuOpen
|
||||
if (this.segmentMenuOpen) {
|
||||
this.$nextTick(() => this.$el.querySelector(".gx-segment-picker__item.active")?.scrollIntoView({ block: "nearest" }))
|
||||
}
|
||||
},
|
||||
selectSegment(i) {
|
||||
this.segmentMenuOpen = false
|
||||
if (i === this.current) return
|
||||
this.current = i
|
||||
this.playSegment()
|
||||
@@ -272,7 +263,6 @@ export const Recordings = {
|
||||
this.playerLoading = false
|
||||
this.playerError = ""
|
||||
this.segments = []
|
||||
this.segmentMenuOpen = false
|
||||
this.cameras = []
|
||||
},
|
||||
async openLogs(route) {
|
||||
@@ -368,12 +358,12 @@ export const Recordings = {
|
||||
</div>
|
||||
<div style="padding: var(--sp-3); display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<input class="gx-field" style="flex:1; min-width:160px;" type="search" placeholder="Search routes, dates, or IDs..." v-model="searchQuery" />
|
||||
<select class="gx-field" v-model="sortOrder">
|
||||
<GalaxySelect class="gx-field" v-model="sortOrder">
|
||||
<option value="newest">Newest first</option>
|
||||
<option value="oldest">Oldest first</option>
|
||||
<option value="longest">Longest duration</option>
|
||||
<option value="shortest">Shortest duration</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
<div style="padding: 0 var(--sp-3) var(--sp-3);">
|
||||
<GalaxyTabs :items="{ all: 'All', preserved: 'Preserved' }" :active="showPreservedOnly ? 'preserved' : 'all'" @select="setPreservedFilter" />
|
||||
@@ -469,7 +459,7 @@ export const Recordings = {
|
||||
<Teleport to="body">
|
||||
<transition name="gx-fade">
|
||||
<div v-if="sub === 'routes' && playerRoute" class="gx-scrim gx-scrim--bottomsheet" @click.self="closePlayer">
|
||||
<div class="gx-sheet" role="dialog" aria-label="Route video player" @click="segmentMenuOpen = false">
|
||||
<div class="gx-sheet" role="dialog" aria-label="Route video player">
|
||||
<div class="gx-section__header gx-video-player-header" style="cursor:default;">
|
||||
<i class="bi bi-camera-video"></i>
|
||||
<span class="gx-section__title">{{ playerRoute.displayName }}</span>
|
||||
@@ -483,15 +473,9 @@ export const Recordings = {
|
||||
<div style="display:flex; flex-direction:column; gap:8px; padding: var(--sp-3) 0 0;">
|
||||
<div class="gx-video-segment-controls">
|
||||
<button type="button" class="gx-btn gx-btn--tonal gx-btn--icon" aria-label="Previous segment" :disabled="current<=0" @click="current--; playSegment()"><i class="bi bi-chevron-left"></i></button>
|
||||
<div class="gx-segment-picker gx-video-segment-select">
|
||||
<button type="button" class="gx-segment-picker__trigger" :aria-expanded="segmentMenuOpen ? 'true' : 'false'" @click.stop="toggleSegmentMenu">
|
||||
<span>Segment {{ current + 1 }} of {{ segments.length }}</span>
|
||||
<i class="bi" :class="segmentMenuOpen ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
||||
</button>
|
||||
<div v-if="segmentMenuOpen" class="gx-segment-picker__menu" role="listbox" @click.stop>
|
||||
<button v-for="(s,i) in segments" :key="i" type="button" class="gx-segment-picker__item" :class="{ active: i === current }" role="option" :aria-selected="i === current ? 'true' : 'false'" @click="selectSegment(i)">Segment {{ i + 1 }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<GalaxySelect class="gx-field gx-video-segment-select" aria-label="Video segment" :value="String(current)" :disabled="!segments.length" @change="selectSegment(Number($event.target.value))">
|
||||
<option v-for="(s, i) in segments" :key="i" :value="String(i)">Segment {{ i + 1 }} of {{ segments.length }}</option>
|
||||
</GalaxySelect>
|
||||
<button type="button" class="gx-btn gx-btn--tonal gx-btn--icon" aria-label="Next segment" :disabled="current>=segments.length-1" @click="current++; playSegment()"><i class="bi bi-chevron-right"></i></button>
|
||||
</div>
|
||||
<div class="gx-video-actions" style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||||
|
||||
@@ -2,7 +2,6 @@ import { api, showSnackbar } from "../api.js"
|
||||
import { usePolling } from "../composables.js"
|
||||
import { GalaxyConfirm } from "../components/GalaxyModal.js"
|
||||
import { GalaxySection } from "../components/GalaxySection.js"
|
||||
import { GalaxySelect } from "../components/GalaxySelect.js"
|
||||
import { VersionHistoryPicker, versionTitle, releaseVersions } from "../components/VersionHistoryPicker.js"
|
||||
import { GxNotice } from "../components/GxNotice.js"
|
||||
|
||||
@@ -59,7 +58,7 @@ async function resolveDeviceScope() {
|
||||
|
||||
export const SystemTools = {
|
||||
name: "SystemTools",
|
||||
components: { GalaxySection, GxNotice, GalaxySelect, VersionHistoryPicker },
|
||||
components: { GalaxySection, GxNotice, VersionHistoryPicker },
|
||||
data() {
|
||||
return {
|
||||
branches: [],
|
||||
|
||||
@@ -214,9 +214,9 @@ export const TestingGround = {
|
||||
|
||||
<div class="gx-row" style="border-top:none;">
|
||||
<span class="gx-row__label">View Slot</span>
|
||||
<select class="gx-field" :value="selectedSlot" :disabled="busy" @change="onSelectSlot">
|
||||
<GalaxySelect class="gx-field" :value="selectedSlot" :disabled="busy" @change="onSelectSlot">
|
||||
<option v-for="slot in selectableSlots" :key="slotId(slot)" :value="slotId(slot)">{{ slotId(slot) }}. {{ slot.name }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
</div>
|
||||
<p class="gx-note">
|
||||
Only one Testing Ground can be active at a time. Switching slots only changes what you're viewing; the active test stays enabled until you explicitly choose another mode.
|
||||
|
||||
@@ -90,10 +90,10 @@ export const Tsk = {
|
||||
<div v-if="loading" class="gx-loading">Loading keys...</div>
|
||||
<template v-else>
|
||||
<label class="gx-row__label" style="font-size:var(--fs-xs);">Select Key</label>
|
||||
<select class="gx-field gx-field--full" :value="selectedKeyName" @change="selectKey($event.target.value)">
|
||||
<GalaxySelect class="gx-field gx-field--full" :value="selectedKeyName" @change="selectKey($event.target.value)">
|
||||
<option value="">-- Select a saved key --</option>
|
||||
<option v-for="k in keys" :key="k.name" :value="k.name">{{ k.name }}</option>
|
||||
</select>
|
||||
</GalaxySelect>
|
||||
|
||||
<label class="gx-row__label" style="font-size:var(--fs-xs);">Key Name</label>
|
||||
<input class="gx-field gx-field--full" v-model="keyName" @input="selectedKeyName = ''" placeholder="Enter key name..." autocomplete="off" />
|
||||
|
||||
@@ -56,14 +56,17 @@ window.app=createApp(Settings); window.vm=window.app.mount('#app');
|
||||
})
|
||||
await page.goto('http://offline.invalid/')
|
||||
await page.evaluate(scale=>{document.documentElement.style.zoom=String(scale)},cfg.scale)
|
||||
const select=page.locator('#gx-longitudinal-mode'), manage=page.locator('[aria-controls="gx-longitudinal-children"]')
|
||||
const waitMode=mode=>page.waitForFunction(mode=>{const e=document.querySelector('#gx-longitudinal-mode');return e?.value===mode&&!e.disabled},mode)
|
||||
const trigger=page.locator('#gx-longitudinal-mode'), manage=page.locator('[aria-controls="gx-longitudinal-children"]')
|
||||
const modeSelect=trigger.locator('xpath=..')
|
||||
const nativeValue=()=>page.evaluate(()=>document.querySelector('#gx-longitudinal-mode')?.closest('.gx-select')?.querySelector('select')?.value)
|
||||
const waitMode=mode=>page.waitForFunction(mode=>{const b=document.querySelector('#gx-longitudinal-mode');const s=b?.closest('.gx-select')?.querySelector('select');return s?.value===mode&&!b.disabled},mode)
|
||||
const choose=async value=>{await (cfg.touch?trigger.tap():trigger.click());await page.locator(`.gx-select-menu[open] [role="option"][data-value="${value}"]`).click()}
|
||||
await waitMode('conditional_experimental')
|
||||
assert.equal(await page.locator('.gx-mode-select__label span').innerText(),'Conditional Experimental')
|
||||
assert.equal(await page.locator('.gx-mode-select__label span').evaluate(e=>e.scrollWidth<=e.clientWidth+1),true)
|
||||
await select.focus()
|
||||
assert.equal(await page.locator('.gx-mode-select').evaluate(e=>getComputedStyle(e).outlineStyle),'solid')
|
||||
assert.deepEqual(await select.locator('option').allTextContents(),['Chill','Experimental','Conditional Experimental','Conditional Chill'])
|
||||
assert.equal(await page.locator('#gx-longitudinal-mode > span').innerText(),'Conditional Experimental')
|
||||
assert.equal(await page.locator('#gx-longitudinal-mode > span').evaluate(e=>e.scrollWidth<=e.clientWidth+1),true)
|
||||
await trigger.focus()
|
||||
assert.equal(await modeSelect.evaluate(e=>getComputedStyle(e).outlineStyle),'solid')
|
||||
assert.deepEqual(await modeSelect.locator('select option').allTextContents(),['Chill','Experimental','Conditional Experimental','Conditional Chill'])
|
||||
assert.equal(await page.evaluate(()=>writes.length),0)
|
||||
assert.equal(await manage.getAttribute('aria-expanded'),'false')
|
||||
assert.equal(await page.locator('#gx-longitudinal-children').count(),0)
|
||||
@@ -75,7 +78,7 @@ window.app=createApp(Settings); window.vm=window.app.mount('#app');
|
||||
// Every direct classic child appears with identical description and native control.
|
||||
for(const owner of ['ConditionalExperimental','ConditionalChill']) {
|
||||
const target=owner==='ConditionalExperimental'?'conditional_experimental':'conditional_chill'
|
||||
if(target!=='conditional_experimental') {await select.selectOption(target);await waitMode(target)}
|
||||
if(target!=='conditional_experimental') {await choose(target);await waitMode(target)}
|
||||
await page.screenshot({path:path.join(out,`${cfg.width}-${cfg.scale}-${target}.png`),fullPage:true})
|
||||
for(const p of layout.flatMap(s=>s.params||[]).filter(p=>p.parent_key===owner)) {
|
||||
assert.ok((await children.innerText()).includes(p.label),p.key)
|
||||
@@ -99,23 +102,23 @@ window.app=createApp(Settings); window.vm=window.app.mount('#app');
|
||||
await slider.focus();await slider.press('ArrowRight');await slider.press('Tab')
|
||||
assert.ok(await page.evaluate(()=>paramWrites.length>0))
|
||||
for(const target of ['chill','experimental']) {
|
||||
await select.selectOption(target);await waitMode(target);assert.equal(await manage.count(),0);assert.equal(await children.count(),0)
|
||||
await choose(target);await waitMode(target);assert.equal(await manage.count(),0);assert.equal(await children.count(),0)
|
||||
await page.screenshot({path:path.join(out,`${cfg.width}-${cfg.scale}-${target}.png`),fullPage:true})
|
||||
}
|
||||
assert.equal(await page.evaluate(()=>writes.at(-1).acknowledged),true)
|
||||
await page.evaluate(()=>{holdWrite=true});await select.selectOption('conditional_chill')
|
||||
await page.evaluate(()=>{holdWrite=true});await choose('conditional_chill')
|
||||
await page.waitForFunction(()=>!!window.releaseWrite)
|
||||
assert.equal(await select.inputValue(),'experimental');assert.equal(await select.isDisabled(),true)
|
||||
assert.equal(await nativeValue(),'experimental');assert.equal(await trigger.isDisabled(),true)
|
||||
await page.evaluate(()=>{holdWrite=false;releaseWrite()});await waitMode('conditional_chill')
|
||||
await page.evaluate(()=>{failWrite=true});await select.selectOption('chill');await waitMode('conditional_chill')
|
||||
await page.evaluate(()=>{failWrite=true});await choose('chill');await waitMode('conditional_chill')
|
||||
assert.ok((await page.locator('[role=alert]').innerText()).includes('Injected'))
|
||||
await page.evaluate(()=>{failWrite=false;externalMode('conditional_experimental')});await waitMode('conditional_experimental')
|
||||
// Routine polling never dims an available control; stale pre-write GET is ignored.
|
||||
await page.evaluate(()=>{holdRead=true});await page.waitForFunction(()=>!!window.releaseRead)
|
||||
assert.equal(await select.isEnabled(),true)
|
||||
await select.selectOption('conditional_chill');await waitMode('conditional_chill')
|
||||
assert.equal(await trigger.isEnabled(),true)
|
||||
await choose('conditional_chill');await waitMode('conditional_chill')
|
||||
await page.evaluate(()=>{holdRead=false;releaseRead()});await page.waitForTimeout(100)
|
||||
assert.equal(await select.inputValue(),'conditional_chill')
|
||||
assert.equal(await nativeValue(),'conditional_chill')
|
||||
for(const reason of ['Locked by Safe Mode.','openpilot longitudinal unavailable.']) {
|
||||
await page.evaluate(reason=>{state={...state,locked:true,reason}},reason)
|
||||
await page.waitForFunction(()=>document.querySelector('#gx-longitudinal-mode').disabled)
|
||||
@@ -130,13 +133,13 @@ window.app=createApp(Settings); window.vm=window.app.mount('#app');
|
||||
await page.screenshot({path:path.join(out,`${cfg.width}-${cfg.scale}-dark.png`),fullPage:true})
|
||||
await page.evaluate(()=>document.documentElement.setAttribute('data-theme','light'))
|
||||
await page.screenshot({path:path.join(out,`${cfg.width}-${cfg.scale}-light.png`),fullPage:true})
|
||||
await page.evaluate(()=>{failRead=true});await page.waitForFunction(()=>document.querySelector('#gx-longitudinal-mode').value==='')
|
||||
assert.equal(await select.isDisabled(),true)
|
||||
await page.evaluate(()=>{failRead=true});await page.waitForFunction(()=>document.querySelector('#gx-longitudinal-mode').closest('.gx-select').querySelector('select').value==='')
|
||||
assert.equal(await trigger.isDisabled(),true)
|
||||
await page.evaluate(()=>{failRead=false});await waitMode('conditional_chill')
|
||||
// Search must use the same guarded selector, never generic Params for virtual key.
|
||||
await page.evaluate(()=>{store.search='Longitudinal control mode'})
|
||||
await page.locator('.gx-section__header').last().click()
|
||||
await waitMode('conditional_chill');await select.selectOption('chill');await waitMode('chill')
|
||||
await waitMode('conditional_chill');await choose('chill');await waitMode('chill')
|
||||
assert.equal(await page.evaluate(()=>paramWrites.some(p=>p.key==='LongitudinalControlMode')),false)
|
||||
assert.deepEqual(errors,[])
|
||||
reports.push({...cfg,passed:true,writes:await page.evaluate(()=>writes.length)})
|
||||
|
||||
@@ -282,6 +282,8 @@ test('Latest explains normal OS handling and local-edit behavior; historical con
|
||||
assert.match(confirmations[0].message, /required OS update/)
|
||||
assert.match(confirmations[0].message, /Local code changes may be overwritten/)
|
||||
assert.doesNotMatch(confirmations[0].message, /code changes are backed up/)
|
||||
instance.rebootPending = false
|
||||
instance.rebootStartedAt = 0
|
||||
await instance.onVersionModeSelect({target:{value:'earlier'}})
|
||||
instance.selectedCommit=SHA
|
||||
await instance.installSelectedVersion()
|
||||
|
||||
@@ -54,13 +54,15 @@ for (const surface of ['classic','mobile']) for (const scenario of ['stale-poll'
|
||||
new MutationObserver(records=>{for(const r of records) if(r.target.id==='snackbar_wrapper') for(const n of r.addedNodes) window.notices.push(n.textContent);}).observe(document,{childList:true,subtree:true});
|
||||
});
|
||||
const select=mobile?page.locator('.gx-row').filter({has:page.getByText('Active Big',{exact:true})}).locator('select'):page.locator('#mm-active-big-model-select');
|
||||
const trigger=mobile?page.locator('.gx-row').filter({has:page.getByText('Active Big',{exact:true})}).locator('.gx-select__button'):null;
|
||||
const choose=async value=>{if(mobile){await trigger.click();await page.locator(`.gx-select-menu[open] [role="option"][data-value="${value}"]`).click();}else{await select.selectOption(value);}};
|
||||
const until=async(fn,msg)=>{for(let i=0;i<80;i++){if(await fn())return;await tick();}throw new Error(msg);};
|
||||
let failure=null;
|
||||
try {
|
||||
await page.goto('http://galaxy.invalid/manage_models');
|
||||
await until(async()=>await select.count() && await select.inputValue()==='gpu-a','initial selection');
|
||||
if(scenario==='stale-poll') {holdNext=true;await page.evaluate(()=>{window.poll()});await until(()=>heldGet,'held prewrite poll');}
|
||||
await select.selectOption('');
|
||||
await choose('');
|
||||
await until(()=>writes.length===1,'selection PUT');
|
||||
assert.deepEqual(writes,[{method:'PUT',path:'/api/models/active',body:{profile:'big',model:''}}]);
|
||||
if(scenario==='stale-poll') {
|
||||
|
||||
@@ -45,13 +45,15 @@ try {
|
||||
const rows=page.locator(rowSelector);
|
||||
await rows.first().waitFor();
|
||||
const select=mobile?page.locator('.gx-row').filter({has:page.getByText('Active Big',{exact:true})}).locator('select'):page.locator('#mm-active-big-model-select');
|
||||
const trigger=mobile?page.locator('.gx-row').filter({has:page.getByText('Active Big',{exact:true})}).locator('.gx-select__button'):null;
|
||||
const choose=async value=>{if(mobile){await trigger.click();await page.locator(`.gx-select-menu[open] [role="option"][data-value="${value}"]`).click();}else{await select.selectOption(value);}};
|
||||
assert.equal(await select.inputValue(),'fixture-0');
|
||||
await select.selectOption('');
|
||||
await choose('');
|
||||
await new Promise(r=>setTimeout(r,350));
|
||||
assert.deepEqual(writes,[{path:'/api/models/active',body:{profile:'big',model:''}}],`${surface}: None must disable Active Big through API`);
|
||||
assert.equal(await select.inputValue(),process.env.MODEL_FAULT === 'reject' ? 'fixture-0' : '', 'Selection must match authoritative status after uncertain response');
|
||||
if (process.env.MODEL_FAULT) assert.ok(polls >= 2, 'Uncertain write requires immediate authoritative readback');
|
||||
await select.selectOption('fixture-2');
|
||||
await choose('fixture-2');
|
||||
await new Promise(r=>setTimeout(r,350));
|
||||
assert.equal(writes.length,2);
|
||||
assert.deepEqual(writes[1],{path:'/api/models/active',body:{profile:'big',model:'fixture-2'}});
|
||||
|
||||
@@ -36,9 +36,9 @@ vm=await reset();const pending=deferred();write=()=>pending.promise;const action
|
||||
vm=await reset();const confirmation=deferred();prompt=()=>confirmation.promise;const downloading=vm.runAction('download',vm.models[0]);await vm.runAction('downloadAll');assert.deepEqual(requests,[]);component.beforeUnmount.call(vm);confirmation.resolve(true);await downloading;assert.deepEqual(requests,[]);
|
||||
// Two concurrent polls resolve newest-first.
|
||||
vm=await reset();const a=deferred(),b=deferred();status=()=>a.promise;const first=vm.refresh();status=()=>b.promise;const second=vm.refresh();b.resolve(payload({activeBigModel:'new'}));await second;a.resolve(payload({activeBigModel:'old'}));await first;assert.equal(vm.activeBigModel,'new');
|
||||
assert.ok(!source.includes('GalaxySelect'));assert.ok(!source.includes('model_metrics'));assert.ok(!source.includes('model_stats'));assert.ok(source.includes('selectionUncertain || status.isOnroad'));
|
||||
assert.ok(source.includes('<GalaxySelect'));assert.ok(!source.includes('model_metrics'));assert.ok(!source.includes('model_stats'));assert.ok(source.includes('selectionUncertain || status.isOnroad'));
|
||||
console.log('PASS: 15 hardware/download/selection/lifecycle scenarios against shipped methods');
|
||||
|
||||
const {compile} = await import('../assets/vendor/vue/vue.esm-browser.js');
|
||||
assert.equal(typeof compile(component.template, {onError(error) {throw error;}}), 'function');
|
||||
console.log('PASS: shipped Vue compiler accepts the native-select Model Manager template');
|
||||
console.log('PASS: shipped Vue compiler accepts the GalaxySelect Model Manager template');
|
||||
|
||||
@@ -78,7 +78,7 @@ def test_ui_device_picker_uses_gateway_directory_and_preserves_local_galaxy():
|
||||
assert shell.index('<DevicePicker />') > shell.index('v-for="(links, section) in NAV"')
|
||||
assert '"GalaxyDeviceName", {PERSISTENT | DONT_LOG, STRING' in param_keys
|
||||
assert "localStorage" not in picker
|
||||
assert ".gx-drawer.open ~ .liquid-glass-nav" in css
|
||||
assert ".gx-drawer.open ~ .blur-nav" in css
|
||||
assert "Local Galaxy instances do not have the gateway directory endpoint." in picker
|
||||
|
||||
|
||||
@@ -331,11 +331,9 @@ def test_ui_developer_mode_banner_offers_unlock():
|
||||
|
||||
def test_ui_has_bottom_navigation_and_drawer():
|
||||
shell = _read("js/components/AppShell.js")
|
||||
# Exactly one navigation affordance: liquid-glass bottom nav (mobile) OR the
|
||||
# drawer hamburger (desktop). Mobile shows a back button instead.
|
||||
assert "liquid-glass-nav" in shell
|
||||
assert "blur-nav" in shell
|
||||
assert "nav-item" in shell
|
||||
assert "gx-menu-btn" in shell
|
||||
assert "gx-appbar__menu" in shell
|
||||
assert "gx-back-btn" in shell
|
||||
assert "goBack" in shell or "back()" in shell
|
||||
assert "gx-drawer" in shell
|
||||
@@ -351,23 +349,23 @@ def test_ui_search_visible_on_mobile_and_content_full_width():
|
||||
# Search must NOT be hidden on mobile (regression: it was display:none <600px).
|
||||
assert ".gx-appbar__search" in css
|
||||
# A single breakpoint picks mobile (bottom nav + back) vs desktop (drawer).
|
||||
assert ".liquid-glass-nav { display: flex; }" in css
|
||||
assert ".gx-menu-btn { display: none; }" in css
|
||||
assert ".blur-nav { display: flex; }" in css
|
||||
assert ".gx-appbar__menu { display: inline-flex; }" in css
|
||||
assert ".gx-back-btn { display: inline-flex; }" in css
|
||||
# Content + embedded tools fill the available width (no 760px cap).
|
||||
assert "max-width: none" in css
|
||||
# Liquid Glass styling is present.
|
||||
# Blur nav styling is present.
|
||||
assert "--glass-bg" in css
|
||||
assert "backdrop-filter" in css
|
||||
|
||||
|
||||
def test_ui_glass_nav_single_breakpoint_no_dual_nav():
|
||||
def test_ui_blur_nav_single_breakpoint_no_dual_nav():
|
||||
css = _read("css/material.css")
|
||||
assert ".liquid-glass-nav" in css
|
||||
assert ".blur-nav" in css
|
||||
assert "@media (min-width: 768px)" in css
|
||||
assert ".liquid-glass-nav { display: none; }" in css
|
||||
assert ".blur-nav { display: none; }" in css
|
||||
assert ".gx-back-btn { display: none; }" in css
|
||||
assert ".gx-menu-btn { display: inline-flex; }" in css
|
||||
assert ".gx-appbar__menu { display: inline-flex; }" in css
|
||||
|
||||
|
||||
def test_ui_settings_deep_links_and_dev_mode_updates():
|
||||
|
||||
Reference in New Issue
Block a user