Merge feature/e1-header-chrome: E1 header/account/appearance chrome (nav overlay, role tooltip, logout, opt-in glass tuner) into dev for v0.43.0
This commit is contained in:
@@ -369,8 +369,9 @@ export default function App() {
|
|||||||
// Collapse state for the two full-height panels (left nav + filter sidebar). Persisted to the
|
// Collapse state for the two full-height panels (left nav + filter sidebar). Persisted to the
|
||||||
// user's preferences so it follows the account across devices; a localStorage cache seeds the
|
// user's preferences so it follows the account across devices; a localStorage cache seeds the
|
||||||
// initial render synchronously so nothing flashes open before the server prefs arrive.
|
// initial render synchronously so nothing flashes open before the server prefs arrive.
|
||||||
|
// Default unpinned (slim + hover-expand overlay); accounts that explicitly pin keep their choice.
|
||||||
const [navCollapsed, setNavCollapsedState] = useState<boolean>(() =>
|
const [navCollapsed, setNavCollapsedState] = useState<boolean>(() =>
|
||||||
Boolean(readAccount(LS.navCollapsed, false))
|
Boolean(readAccount(LS.navCollapsed, true))
|
||||||
);
|
);
|
||||||
const [filterCollapsed, setFilterCollapsedState] = useState<boolean>(() =>
|
const [filterCollapsed, setFilterCollapsedState] = useState<boolean>(() =>
|
||||||
Boolean(readAccount(LS.filterCollapsed, false))
|
Boolean(readAccount(LS.filterCollapsed, false))
|
||||||
@@ -984,8 +985,8 @@ export default function App() {
|
|||||||
<ErrorDialog />
|
<ErrorDialog />
|
||||||
{/* Re-binds to the active page's scroll container on navigation (page or channel change). */}
|
{/* Re-binds to the active page's scroll container on navigation (page or channel change). */}
|
||||||
<BackToTop dep={channelView ? `chan:${channelView.id}` : page} />
|
<BackToTop dep={channelView ? `chan:${channelView.id}` : page} />
|
||||||
{/* Local-only live appearance tuner (renders only on localhost; null everywhere else). */}
|
{/* Opt-in live appearance tuner (Settings → Appearance; off by default). */}
|
||||||
<GlassTuner />
|
<GlassTuner enabled={theme.showTuner} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local-only live appearance tuner. Mounted at the app root so it's reachable on every page; it
|
* A floating live appearance tuner. Mounted at the app root so it's reachable on every page; it
|
||||||
* writes the glass CSS custom properties (index.css `:root`) inline on the document element for
|
* writes the glass CSS custom properties (index.css `:root`) inline on the document element for
|
||||||
* instant, build-free feedback, and persists to localStorage.
|
* instant, build-free feedback, and persists to localStorage.
|
||||||
*
|
*
|
||||||
* Only active on localhost, so it never appears in a hosted build. A scratch tool for dialling in
|
* Opt-in via Settings → Appearance (`theme.showTuner`, off by default) — a secondary "geek toy"
|
||||||
* the surface values — "Copy CSS" exports them to paste into index.css.
|
* for dialling in the surface values. "Copy CSS" exports the values.
|
||||||
*/
|
*/
|
||||||
const DEV_TUNER =
|
|
||||||
typeof window !== "undefined" && /^(localhost|127\.0\.0\.1)$/.test(window.location.hostname);
|
|
||||||
|
|
||||||
const LS_KEY = "siftlode.devGlassTuner";
|
const LS_KEY = "siftlode.devGlassTuner";
|
||||||
|
|
||||||
type Slider = {
|
type Slider = {
|
||||||
@@ -93,7 +90,7 @@ function loadSaved(): Saved {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GlassTuner() {
|
export default function GlassTuner({ enabled }: { enabled: boolean }) {
|
||||||
const initial = useMemo(loadSaved, []);
|
const initial = useMemo(loadSaved, []);
|
||||||
const [vars, setVars] = useState<Record<string, number>>(() => {
|
const [vars, setVars] = useState<Record<string, number>>(() => {
|
||||||
const base: Record<string, number> = {};
|
const base: Record<string, number> = {};
|
||||||
@@ -216,7 +213,7 @@ export default function GlassTuner() {
|
|||||||
persist({ open: v });
|
persist({ open: v });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DEV_TUNER) return null;
|
if (!enabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed right-0 top-24 z-[9999] flex items-start" style={{ pointerEvents: "none" }}>
|
<div className="fixed right-0 top-24 z-[9999] flex items-start" style={{ pointerEvents: "none" }}>
|
||||||
@@ -225,7 +222,7 @@ export default function GlassTuner() {
|
|||||||
onClick={() => persistOpen(true)}
|
onClick={() => persistOpen(true)}
|
||||||
style={{ pointerEvents: "auto" }}
|
style={{ pointerEvents: "auto" }}
|
||||||
className="glass-menu glass-hover rounded-l-xl rounded-r-none px-2 py-3 text-xs font-semibold tracking-wide text-fg"
|
className="glass-menu glass-hover rounded-l-xl rounded-r-none px-2 py-3 text-xs font-semibold tracking-wide text-fg"
|
||||||
title="Open the glass tuner (dev only)"
|
title="Open the glass tuner"
|
||||||
>
|
>
|
||||||
<span style={{ writingMode: "vertical-rl" }}>◐ GLASS</span>
|
<span style={{ writingMode: "vertical-rl" }}>◐ GLASS</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -237,7 +234,7 @@ export default function GlassTuner() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60">
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60">
|
||||||
<span className="text-sm font-semibold text-fg">◐ Glass Tuner</span>
|
<span className="text-sm font-semibold text-fg">◐ Glass Tuner</span>
|
||||||
<span className="text-[10px] uppercase tracking-wider text-muted">dev</span>
|
<span className="text-[10px] uppercase tracking-wider text-muted">lab</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => persistOpen(false)}
|
onClick={() => persistOpen(false)}
|
||||||
className="ml-auto text-muted hover:text-fg text-lg leading-none px-1"
|
className="ml-auto text-muted hover:text-fg text-lg leading-none px-1"
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ export default function Header({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Fixed left = the left zone at its widest: nav card (12 margin + 208 + 12 margin = 232) +
|
// Absolutely positioned within the (relative) content column, so its left edge tracks the
|
||||||
// filter sidebar (256) + 16 gap = 504. Constant, so it never shifts on collapse/module change.
|
// actual left zone: it sits just inside the content, reclaiming the space when the nav rail is
|
||||||
<div className="pointer-events-none fixed top-3 left-4 right-[10%] z-30 flex items-center gap-3 md:left-[504px]">
|
// slim and/or no filter sidebar is present — no hard-coded offset.
|
||||||
|
<div className="pointer-events-none absolute top-3 left-3 right-[10%] z-30 flex items-center gap-3">
|
||||||
<ModuleName
|
<ModuleName
|
||||||
label={t(moduleLabelKey[page])}
|
label={t(moduleLabelKey[page])}
|
||||||
labels={moduleLabels}
|
labels={moduleLabels}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Bell,
|
Bell,
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Clapperboard,
|
Clapperboard,
|
||||||
Download,
|
Download,
|
||||||
Home,
|
Home,
|
||||||
@@ -15,6 +13,8 @@ import {
|
|||||||
ListVideo,
|
ListVideo,
|
||||||
LogOut,
|
LogOut,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
|
Pin,
|
||||||
|
PinOff,
|
||||||
ScrollText,
|
ScrollText,
|
||||||
Settings,
|
Settings,
|
||||||
Shield,
|
Shield,
|
||||||
@@ -62,6 +62,14 @@ export default function NavSidebar({
|
|||||||
onGoToFullHistory: () => void;
|
onGoToFullHistory: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
// Hover/focus expand: when unpinned, the rail sits slim (icons only) and expands to a labelled
|
||||||
|
// overlay on pointer hover or keyboard focus WITHOUT pushing content. Pinned keeps it expanded
|
||||||
|
// in-flow like before. `collapsed` (persisted per-account) now means "unpinned".
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
const pinned = !collapsed;
|
||||||
|
const expanded = pinned || hovered || focused;
|
||||||
|
const slim = !expanded; // visual: icon-only rail
|
||||||
const [acctOpen, setAcctOpen] = useState(false);
|
const [acctOpen, setAcctOpen] = useState(false);
|
||||||
const acctBtnRef = useRef<HTMLButtonElement | null>(null);
|
const acctBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const acctPanelRef = useRef<HTMLDivElement | null>(null);
|
const acctPanelRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -213,21 +221,14 @@ export default function NavSidebar({
|
|||||||
const rowBase =
|
const rowBase =
|
||||||
"w-full flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition";
|
"w-full flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition";
|
||||||
const name = me.display_name ?? me.email.split("@")[0];
|
const name = me.display_name ?? me.email.split("@")[0];
|
||||||
// Role shown next to the name (expanded) / as an avatar dot (collapsed). Demo isn't a real
|
// Role is surfaced as a plain title tooltip on the account row (no visual badge). Demo isn't a
|
||||||
// role (it's a flag on a `user` row), so surface it first.
|
// real role (it's a flag on a `user` row), so surface it first.
|
||||||
const roleKey: "admin" | "user" | "demo" = me.is_demo
|
const roleKey: "admin" | "user" | "demo" = me.is_demo
|
||||||
? "demo"
|
? "demo"
|
||||||
: me.role === "admin"
|
: me.role === "admin"
|
||||||
? "admin"
|
? "admin"
|
||||||
: "user";
|
: "user";
|
||||||
const roleChipCls =
|
const roleLabel = t(`nav.role.${roleKey}`);
|
||||||
roleKey === "admin"
|
|
||||||
? "bg-accent/15 text-accent"
|
|
||||||
: roleKey === "demo"
|
|
||||||
? "bg-amber-500/20 text-amber-500"
|
|
||||||
: "bg-muted/15 text-muted";
|
|
||||||
const roleDotCls =
|
|
||||||
roleKey === "admin" ? "bg-accent" : roleKey === "demo" ? "bg-amber-500" : "bg-muted";
|
|
||||||
|
|
||||||
const renderItem = ({ page: p, icon: Icon, label, badge }: NavItem) => {
|
const renderItem = ({ page: p, icon: Icon, label, badge }: NavItem) => {
|
||||||
const active = page === p;
|
const active = page === p;
|
||||||
@@ -239,9 +240,9 @@ export default function NavSidebar({
|
|||||||
<button
|
<button
|
||||||
key={p}
|
key={p}
|
||||||
onClick={() => setPage(p)}
|
onClick={() => setPage(p)}
|
||||||
title={collapsed ? label : undefined}
|
title={slim ? label : undefined}
|
||||||
aria-current={active ? "page" : undefined}
|
aria-current={active ? "page" : undefined}
|
||||||
className={`${rowBase} ${collapsed ? "justify-center px-0" : ""} relative ${
|
className={`${rowBase} ${slim ? "justify-center px-0" : ""} relative ${
|
||||||
active ? "bg-accent text-accent-fg" : "text-muted hover:text-fg hover:bg-card"
|
active ? "bg-accent text-accent-fg" : "text-muted hover:text-fg hover:bg-card"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -249,7 +250,7 @@ export default function NavSidebar({
|
|||||||
<Icon className="w-[18px] h-[18px]" />
|
<Icon className="w-[18px] h-[18px]" />
|
||||||
{/* Collapsed rail: a numeric badge centred on a circle at the icon corner; the ring
|
{/* Collapsed rail: a numeric badge centred on a circle at the icon corner; the ring
|
||||||
matches the row background so it reads cleanly whether the row is active or not. */}
|
matches the row background so it reads cleanly whether the row is active or not. */}
|
||||||
{show && collapsed && (
|
{show && slim && (
|
||||||
<span
|
<span
|
||||||
className={`absolute -top-2 -right-2 min-w-[16px] h-4 px-1 rounded-full text-[10px] font-bold leading-none grid place-items-center ring-2 ${badgeColor} ${
|
className={`absolute -top-2 -right-2 min-w-[16px] h-4 px-1 rounded-full text-[10px] font-bold leading-none grid place-items-center ring-2 ${badgeColor} ${
|
||||||
active ? "ring-accent" : "ring-bg"
|
active ? "ring-accent" : "ring-bg"
|
||||||
@@ -259,8 +260,8 @@ export default function NavSidebar({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{!collapsed && <span className="truncate">{label}</span>}
|
{!slim && <span className="truncate">{label}</span>}
|
||||||
{show && !collapsed && (
|
{show && !slim && (
|
||||||
<span
|
<span
|
||||||
className={`ml-auto min-w-[18px] h-[18px] px-1.5 rounded-full text-[11px] font-semibold leading-none inline-flex items-center justify-center tabular-nums ${badgeColor}`}
|
className={`ml-auto min-w-[18px] h-[18px] px-1.5 rounded-full text-[11px] font-semibold leading-none inline-flex items-center justify-center tabular-nums ${badgeColor}`}
|
||||||
>
|
>
|
||||||
@@ -272,14 +273,25 @@ export default function NavSidebar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
// Layout slot: reserves only the slim footprint when unpinned, so the expanded rail (below)
|
||||||
className={`glass relative shrink-0 rounded-2xl flex flex-col py-3 m-3 transition-[width] ${
|
// overlays content instead of pushing it. Pinned reserves the full width (in-flow).
|
||||||
collapsed ? "w-[56px] px-2" : "w-52 px-3"
|
<div
|
||||||
}`}
|
className={`relative shrink-0 transition-[width] ${pinned ? "w-[232px]" : "w-20"}`}
|
||||||
aria-label={t("nav.primary")}
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
onFocus={() => setFocused(true)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) setFocused(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center mb-3 ${collapsed ? "justify-center" : "justify-between"}`}>
|
<nav
|
||||||
{!collapsed && (
|
className={`glass absolute top-0 bottom-0 left-0 z-40 rounded-2xl flex flex-col py-3 m-3 transition-[width] ${
|
||||||
|
slim ? "w-[56px] px-2" : "w-52 px-3"
|
||||||
|
}`}
|
||||||
|
aria-label={t("nav.primary")}
|
||||||
|
>
|
||||||
|
<div className={`flex items-center mb-3 ${slim ? "justify-center" : "justify-between"}`}>
|
||||||
|
{!slim && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage("feed")}
|
onClick={() => setPage("feed")}
|
||||||
className="text-lg font-bold tracking-tight select-none cursor-pointer rounded-md -mx-1 px-1 hover:bg-card hover:opacity-90 transition"
|
className="text-lg font-bold tracking-tight select-none cursor-pointer rounded-md -mx-1 px-1 hover:bg-card hover:opacity-90 transition"
|
||||||
@@ -290,15 +302,11 @@ export default function NavSidebar({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onToggleCollapse}
|
onClick={onToggleCollapse}
|
||||||
title={collapsed ? t("nav.expand") : t("nav.collapse")}
|
title={pinned ? t("nav.unpin") : t("nav.pin")}
|
||||||
aria-label={collapsed ? t("nav.expand") : t("nav.collapse")}
|
aria-label={pinned ? t("nav.unpin") : t("nav.pin")}
|
||||||
className="p-1 rounded-md text-muted hover:text-fg hover:bg-card transition"
|
className="p-1 rounded-md text-muted hover:text-fg hover:bg-card transition"
|
||||||
>
|
>
|
||||||
{collapsed ? (
|
{pinned ? <PinOff className="w-4 h-4" /> : <Pin className="w-4 h-4" />}
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -308,7 +316,7 @@ export default function NavSidebar({
|
|||||||
isAdmin={me.role === "admin"}
|
isAdmin={me.role === "admin"}
|
||||||
onGoToFullHistory={onGoToFullHistory}
|
onGoToFullHistory={onGoToFullHistory}
|
||||||
variant="rail"
|
variant="rail"
|
||||||
collapsed={collapsed}
|
collapsed={slim}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -319,7 +327,7 @@ export default function NavSidebar({
|
|||||||
>
|
>
|
||||||
{userItems.map(renderItem)}
|
{userItems.map(renderItem)}
|
||||||
{systemItems.length > 0 &&
|
{systemItems.length > 0 &&
|
||||||
(collapsed ? (
|
(slim ? (
|
||||||
// Collapsed rail: a short, thicker centred rule is the clearest "new section" cue
|
// Collapsed rail: a short, thicker centred rule is the clearest "new section" cue
|
||||||
// when there's no room for a label.
|
// when there's no room for a label.
|
||||||
<div className="my-2 mx-auto w-7 border-t-2 border-border" />
|
<div className="my-2 mx-auto w-7 border-t-2 border-border" />
|
||||||
@@ -337,7 +345,7 @@ export default function NavSidebar({
|
|||||||
<div className="mt-2 pt-2 border-t border-border flex flex-col gap-1">
|
<div className="mt-2 pt-2 border-t border-border flex flex-col gap-1">
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center gap-2 pb-2 mb-1 border-b border-border ${
|
className={`flex items-center justify-center gap-2 pb-2 mb-1 border-b border-border ${
|
||||||
collapsed ? "flex-col" : "flex-row"
|
slim ? "flex-col" : "flex-row"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<LanguageSwitcher variant="rail" value={language} onChange={onChangeLanguage} />
|
<LanguageSwitcher variant="rail" value={language} onChange={onChangeLanguage} />
|
||||||
@@ -349,27 +357,35 @@ export default function NavSidebar({
|
|||||||
>
|
>
|
||||||
<Info className="w-5 h-5" />
|
<Info className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
title={t("header.account.signOut")}
|
||||||
|
aria-label={t("header.account.signOut")}
|
||||||
|
className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage("settings")}
|
onClick={() => setPage("settings")}
|
||||||
title={collapsed ? t("header.account.settings") : undefined}
|
title={slim ? t("header.account.settings") : undefined}
|
||||||
aria-current={page === "settings" ? "page" : undefined}
|
aria-current={page === "settings" ? "page" : undefined}
|
||||||
className={`${rowBase} ${collapsed ? "justify-center px-0" : ""} ${
|
className={`${rowBase} ${slim ? "justify-center px-0" : ""} ${
|
||||||
page === "settings"
|
page === "settings"
|
||||||
? "bg-accent text-accent-fg"
|
? "bg-accent text-accent-fg"
|
||||||
: "text-muted hover:text-fg hover:bg-card"
|
: "text-muted hover:text-fg hover:bg-card"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Settings className="w-[18px] h-[18px] shrink-0" />
|
<Settings className="w-[18px] h-[18px] shrink-0" />
|
||||||
{!collapsed && <span className="truncate">{t("header.account.settings")}</span>}
|
{!slim && <span className="truncate">{t("header.account.settings")}</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
ref={acctBtnRef}
|
ref={acctBtnRef}
|
||||||
onClick={toggleAccount}
|
onClick={toggleAccount}
|
||||||
title={collapsed ? name : undefined}
|
title={slim ? `${name} · ${roleLabel}` : undefined}
|
||||||
className={`${rowBase} ${collapsed ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`}
|
className={`${rowBase} ${slim ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`}
|
||||||
>
|
>
|
||||||
<span className="relative shrink-0">
|
<span className="relative shrink-0">
|
||||||
<AvatarImg
|
<AvatarImg
|
||||||
@@ -377,19 +393,10 @@ export default function NavSidebar({
|
|||||||
fallback={name}
|
fallback={name}
|
||||||
className="w-[22px] h-[22px] rounded-full text-[10px]"
|
className="w-[22px] h-[22px] rounded-full text-[10px]"
|
||||||
/>
|
/>
|
||||||
{collapsed && (
|
|
||||||
<span
|
|
||||||
className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full ring-2 ring-bg ${roleDotCls}`}
|
|
||||||
title={t(`nav.role.${roleKey}`)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
{!collapsed && <span className="truncate flex-1 text-left">{name}</span>}
|
{!slim && (
|
||||||
{!collapsed && (
|
<span title={roleLabel} className="truncate flex-1 text-left">
|
||||||
<span
|
{name}
|
||||||
className={`shrink-0 text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${roleChipCls}`}
|
|
||||||
>
|
|
||||||
{t(`nav.role.${roleKey}`)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -469,6 +476,7 @@ export default function NavSidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,16 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
|
|||||||
>
|
>
|
||||||
<Switch label={t("settings.appearance.showHints")} checked={hints} onChange={setHints} />
|
<Switch label={t("settings.appearance.showHints")} checked={hints} onChange={setHints} />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
label={t("settings.appearance.showTuner")}
|
||||||
|
hint={t("settings.appearance.showTunerHint")}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
label={t("settings.appearance.showTuner")}
|
||||||
|
checked={theme.showTuner}
|
||||||
|
onChange={(v) => setTheme({ ...theme, showTuner: v })}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={t("settings.appearance.textSize")}>
|
<Section title={t("settings.appearance.textSize")}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"primary": "Primary navigation",
|
"primary": "Primary navigation",
|
||||||
"collapse": "Collapse sidebar",
|
"pin": "Pin sidebar open",
|
||||||
"expand": "Expand sidebar",
|
"unpin": "Unpin sidebar",
|
||||||
"adminSection": "Admin",
|
"adminSection": "Admin",
|
||||||
"role": {
|
"role": {
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
"performanceModeHint": "Turns off the translucent glass blur and soft shadows for snappier rendering on slower machines.",
|
"performanceModeHint": "Turns off the translucent glass blur and soft shadows for snappier rendering on slower machines.",
|
||||||
"showHints": "Show hints",
|
"showHints": "Show hints",
|
||||||
"showHintsHint": "These little hover explanations. Turn them off once you know your way around.",
|
"showHintsHint": "These little hover explanations. Turn them off once you know your way around.",
|
||||||
|
"showTuner": "Show the glass tuner",
|
||||||
|
"showTunerHint": "A floating panel for live-tweaking the frosted-glass look. A geek toy — off by default.",
|
||||||
"textSize": "Text size"
|
"textSize": "Text size"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"primary": "Fő navigáció",
|
"primary": "Fő navigáció",
|
||||||
"collapse": "Oldalsáv összecsukása",
|
"pin": "Oldalsáv rögzítése nyitva",
|
||||||
"expand": "Oldalsáv kinyitása",
|
"unpin": "Oldalsáv rögzítésének feloldása",
|
||||||
"adminSection": "Admin",
|
"adminSection": "Admin",
|
||||||
"role": {
|
"role": {
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
"performanceModeHint": "Kikapcsolja az áttetsző üvegelmosást és a lágy árnyékokat a gyorsabb megjelenítésért lassabb gépeken.",
|
"performanceModeHint": "Kikapcsolja az áttetsző üvegelmosást és a lágy árnyékokat a gyorsabb megjelenítésért lassabb gépeken.",
|
||||||
"showHints": "Tippek megjelenítése",
|
"showHints": "Tippek megjelenítése",
|
||||||
"showHintsHint": "Ezek a kis lebegő magyarázatok. Kapcsold ki őket, ha már kiismerted magad.",
|
"showHintsHint": "Ezek a kis lebegő magyarázatok. Kapcsold ki őket, ha már kiismerted magad.",
|
||||||
|
"showTuner": "Glass tuner megjelenítése",
|
||||||
|
"showTunerHint": "Lebegő panel a mattüveg-hatás élő hangolásához. Geek-játékszer — alapból kikapcsolva.",
|
||||||
"textSize": "Betűméret"
|
"textSize": "Betűméret"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ export interface ReleaseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||||
|
{
|
||||||
|
version: "0.43.0",
|
||||||
|
date: "2026-07-14",
|
||||||
|
summary:
|
||||||
|
"A slimmer, smarter navigation sidebar, a couple of account tweaks, and an optional live appearance tuner.",
|
||||||
|
features: [
|
||||||
|
"The navigation sidebar now stays slim by default and slides open over the page when you hover it or tab into it — so it barely takes any room until you need it. Prefer it always open? Pin it with the button at the top of the rail.",
|
||||||
|
"The top bar reclaims the empty space on the left: it now hugs your content instead of leaving a fixed gap when the sidebar is slim.",
|
||||||
|
"A logout button now sits right next to the language and about buttons, and your role shows as a tooltip on your name (the separate badge is gone).",
|
||||||
|
"New optional glass tuner: a floating panel for live-tweaking the frosted-glass look. It's off by default — switch it on under Settings → Appearance if you like to fiddle.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "0.42.0",
|
version: "0.42.0",
|
||||||
date: "2026-07-13",
|
date: "2026-07-13",
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface ThemePrefs {
|
|||||||
bgImage: boolean;
|
bgImage: boolean;
|
||||||
/** How strongly the background image is faded out, 0–100% (higher = fainter). */
|
/** How strongly the background image is faded out, 0–100% (higher = fainter). */
|
||||||
bgFade: number;
|
bgFade: number;
|
||||||
|
/** Show the floating glass/theme tuner (a secondary "geek toy"); off by default. */
|
||||||
|
showTuner: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_THEME: ThemePrefs = {
|
export const DEFAULT_THEME: ThemePrefs = {
|
||||||
@@ -28,6 +30,7 @@ export const DEFAULT_THEME: ThemePrefs = {
|
|||||||
fontScale: 1.06,
|
fontScale: 1.06,
|
||||||
bgImage: true,
|
bgImage: true,
|
||||||
bgFade: 60,
|
bgFade: 60,
|
||||||
|
showTuner: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function applyTheme(t: ThemePrefs): void {
|
export function applyTheme(t: ThemePrefs): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user