mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 03:06:57 +00:00
feat(ui): add Classic, Smart, and Compact desktop navigation styles (#1642)
* feat(ui): add Classic, Smart, and Compact desktop navigation styles Introduce a shared app-nav registry and reachable model so TopBar, the command palette, and mobile menus share one destination source. Smart bar is the default; Appearance gains a Navigation subsection with quick links. * fix(e2e): stop clearing top-nav prefs on every reload The desktop-navigation suite used addInitScript to wipe mode storage, which re-ran on reload and undid the classic/compact values under test. * fix(ui): harden nav PR docs and Compact quick-link coverage Drop the partial docs-refresh import that left missing image assets and a stale Display screenshot, keep navigation-scoped operator docs against main, and add an E2E path that proves Compact quick-link add, persist, and render after reload. * fix(ui): polish Compact quick links and menu mastheads Align Smart/Compact menus with Theme chrome, keep pin labels always visible, and drive add capacity from persisted pins (max five) with a trailing + picker and per-pin remove. * test(e2e): exact-match Compact Networking pin locator Avoid Playwright strict-mode clash with Actions for Networking. * fix(ui): simplify Compact quick link removal and fix + button trailing Remove the (...) dropdown per quick link in Compact mode. Right-click context menu remains as the sole on-bar removal affordance. Fix the + add-button to trail quick links rather than pinning to the far right by removing flex-1 from the quick-link rail.
This commit is contained in:
@@ -9,6 +9,11 @@ import type { Density } from '@/hooks/use-density';
|
||||
import { useLogChipColorMode, type LogChipColorMode } from '@/hooks/use-log-chip-color-mode';
|
||||
import { useTopNavLabels } from '@/hooks/use-top-nav-labels';
|
||||
import { useTopNavAlign, type TopNavAlign } from '@/hooks/use-top-nav-align';
|
||||
import { useTopNavMode, type TopNavMode } from '@/hooks/use-top-nav-mode';
|
||||
import { useTopNavQuickLinks, MAX_QUICK_LINKS } from '@/hooks/use-top-nav-quick-links';
|
||||
import { getAppNavItem } from '@/lib/navigation/appNavRegistry';
|
||||
import type { NavDestination } from '@/lib/navigation/appNavRegistry';
|
||||
import type { ActiveView } from '@/lib/router/routeTypes';
|
||||
import {
|
||||
useTheme, activeVisualStyle, THEME_MODE_OPTIONS, ACCENTS, CONTRAST, BORDER_BOOST, GLOW, TYPE_SCALE,
|
||||
type VisualStyle, type HeadingStyle, type ChartStyle,
|
||||
@@ -37,6 +42,12 @@ const TOP_NAV_ALIGN_OPTIONS: { value: TopNavAlign; label: string }[] = [
|
||||
{ value: 'center', label: 'Center' },
|
||||
];
|
||||
|
||||
const TOP_NAV_MODE_OPTIONS: { value: TopNavMode; label: string }[] = [
|
||||
{ value: 'classic', label: 'Classic bar' },
|
||||
{ value: 'smart', label: 'Smart bar' },
|
||||
{ value: 'compact', label: 'Compact launcher' },
|
||||
];
|
||||
|
||||
const CHART_STYLE_OPTIONS: { value: ChartStyle; label: string }[] = [
|
||||
{ value: 'muted', label: 'Muted' },
|
||||
{ value: 'heat', label: 'Heat' },
|
||||
@@ -134,11 +145,33 @@ function VisualCard({
|
||||
);
|
||||
}
|
||||
|
||||
export function AppearanceSection() {
|
||||
export function AppearanceSection({
|
||||
quickLinkCandidates = [],
|
||||
}: {
|
||||
quickLinkCandidates?: NavDestination[];
|
||||
}) {
|
||||
const [density, setDensity] = useDensity();
|
||||
const [chipColorMode, setChipColorMode] = useLogChipColorMode();
|
||||
const [topNavLabels, setTopNavLabels] = useTopNavLabels();
|
||||
const [topNavAlign, setTopNavAlign] = useTopNavAlign();
|
||||
const [topNavMode, setTopNavMode] = useTopNavMode();
|
||||
const {
|
||||
persistedIds: quickLinkIds,
|
||||
addQuickLink,
|
||||
removeQuickLink,
|
||||
resetQuickLinks,
|
||||
} = useTopNavQuickLinks();
|
||||
const persistedSet = new Set(quickLinkIds);
|
||||
const unpinnedCandidates = quickLinkCandidates.filter((item) => !persistedSet.has(item.value));
|
||||
const atCapacity = quickLinkIds.length >= MAX_QUICK_LINKS;
|
||||
const addEnabled = !atCapacity && unpinnedCandidates.length > 0;
|
||||
const addDisabledReason = atCapacity
|
||||
? 'Remove a pin or reset to free a slot'
|
||||
: 'No more destinations available on this node';
|
||||
const addOptions = unpinnedCandidates.map((item) => ({
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
}));
|
||||
const {
|
||||
theme, accent, borderBoost, glow, contrast, uiFont, monoFont, typeScale,
|
||||
headingStyle, chartStyle, reducedEffects, reducedMotion, readability,
|
||||
@@ -413,13 +446,41 @@ export function AppearanceSection() {
|
||||
</SettingsField>
|
||||
|
||||
<SettingsField
|
||||
label="Top navigation labels"
|
||||
helper="Show text labels beside top navigation icons. Turn off for a more compact navigation bar."
|
||||
label="Log chip color"
|
||||
helper="Unified uses the accent color for all service chips. Per-service assigns each service a stable label color for faster visual scanning."
|
||||
>
|
||||
<TogglePill checked={topNavLabels} onChange={setTopNavLabels} />
|
||||
<SegmentedControl
|
||||
value={chipColorMode}
|
||||
options={CHIP_COLOR_OPTIONS}
|
||||
onChange={setChipColorMode}
|
||||
ariaLabel="Log chip color mode"
|
||||
/>
|
||||
</SettingsField>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Navigation" kicker="this browser">
|
||||
<SettingsField
|
||||
label="Navigation style"
|
||||
helper="Smart bar is the recommended default: primary destinations stay visible, and the rest live under More. Classic keeps the full horizontal strip. Compact launcher puts destinations in a menu with optional quick links."
|
||||
>
|
||||
<SegmentedControl
|
||||
value={topNavMode}
|
||||
options={TOP_NAV_MODE_OPTIONS}
|
||||
onChange={setTopNavMode}
|
||||
ariaLabel="Navigation style"
|
||||
/>
|
||||
</SettingsField>
|
||||
|
||||
{!topNavLabels && (
|
||||
{(topNavMode === 'classic' || topNavMode === 'smart') && (
|
||||
<SettingsField
|
||||
label="Top navigation labels"
|
||||
helper="Show text labels beside top navigation icons. Turn off for a more compact navigation bar."
|
||||
>
|
||||
<TogglePill checked={topNavLabels} onChange={setTopNavLabels} />
|
||||
</SettingsField>
|
||||
)}
|
||||
|
||||
{(topNavMode === 'classic' || topNavMode === 'smart') && !topNavLabels && (
|
||||
<SettingsField
|
||||
label="Top navigation alignment"
|
||||
helper="Place the icon-only navigation against the left edge or centered in the bar."
|
||||
@@ -433,17 +494,64 @@ export function AppearanceSection() {
|
||||
</SettingsField>
|
||||
)}
|
||||
|
||||
<SettingsField
|
||||
label="Log chip color"
|
||||
helper="Unified uses the accent color for all service chips. Per-service assigns each service a stable label color for faster visual scanning."
|
||||
>
|
||||
<SegmentedControl
|
||||
value={chipColorMode}
|
||||
options={CHIP_COLOR_OPTIONS}
|
||||
onChange={setChipColorMode}
|
||||
ariaLabel="Log chip color mode"
|
||||
/>
|
||||
</SettingsField>
|
||||
{topNavMode === 'compact' && (
|
||||
<SettingsField
|
||||
label="Quick links"
|
||||
helper="Up to five pinned destinations on the top bar. Defaults are a starting set; add reachable destinations here or with the trailing + on the Compact bar."
|
||||
>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{quickLinkIds.length === 0 ? (
|
||||
<p className="font-mono text-[11px] text-stat-subtitle">No quick links pinned.</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-1">
|
||||
{quickLinkIds.map((id) => {
|
||||
const item = getAppNavItem(id);
|
||||
const label = item?.label ?? id;
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="flex items-center justify-between gap-2 rounded-md border border-glass-border px-2 py-1.5"
|
||||
>
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.14em]">
|
||||
{label}
|
||||
</span>
|
||||
<SettingsSecondaryButton
|
||||
type="button"
|
||||
aria-label={`Remove ${label}`}
|
||||
onClick={() => removeQuickLink(id)}
|
||||
>
|
||||
Remove
|
||||
</SettingsSecondaryButton>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">
|
||||
Add quick link
|
||||
</span>
|
||||
{addEnabled ? (
|
||||
<Combobox
|
||||
options={addOptions}
|
||||
value=""
|
||||
onValueChange={(value) => {
|
||||
if (value) addQuickLink(value as ActiveView);
|
||||
}}
|
||||
placeholder="Choose a destination"
|
||||
/>
|
||||
) : (
|
||||
<p className="font-mono text-[11px] text-stat-subtitle">{addDisabledReason}</p>
|
||||
)}
|
||||
</div>
|
||||
<SettingsActions>
|
||||
<SettingsSecondaryButton type="button" onClick={resetQuickLinks}>
|
||||
Reset to defaults
|
||||
</SettingsSecondaryButton>
|
||||
</SettingsActions>
|
||||
</div>
|
||||
</SettingsField>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<p className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle/70">
|
||||
|
||||
Reference in New Issue
Block a user