Persistent sidebar, nav & flat theme overhaul

Introduce a persistent left sidebar, update navigation to support regions, and convert the UI to a flat/no-blur visual style.

Key changes:
- index.html: add a persistent sidebar markup with nav items, user area and main app layout.
- src/core/nav.js: improve candidate scoring to use element bounding centers and overlap penalty; include extra focus roots; add region metadata handling, helpers for resolving default content/sidebar indices, and special-case movements between sidebar and content; respect contract.useNebulaNavigation flag.
- src/main.js: manage sidebar visibility for full-screen flows (lock/onboarding), update sidebar active state on view changes, include sidebar as an extraFocusRoot when appropriate, and handle sidebar item activation on accept to navigate views.
- src/styles/*: major visual/theme updates to remove blur/glow effects (Zero-Blur policy), add flat sidebar styles, update components to solid borders and simpler focus styling, adjust theme variables (palette, spacing, durations), and redesign the home view layout and controls.

Rationale: provide a persistent, keyboard/controller-friendly sidebar for quick navigation while simplifying visuals to a flat, performance-friendly theme and refining focus/navigation behavior across sidebar and content regions.
This commit is contained in:
2026-05-15 23:13:31 +12:00
parent 5261a82115
commit 38be2f43f1
8 changed files with 1302 additions and 454 deletions
+100 -9
View File
@@ -9,17 +9,30 @@ const getRect = (element) => {
};
const scoreCandidate = (source, target, direction) => {
const horizontal = target.col - source.col;
const vertical = target.row - source.row;
const sourceRect = getRect(source.element);
const targetRect = getRect(target.element);
const sourceCenterX = sourceRect.x + sourceRect.width / 2;
const sourceCenterY = sourceRect.y + sourceRect.height / 2;
const targetCenterX = targetRect.x + targetRect.width / 2;
const targetCenterY = targetRect.y + targetRect.height / 2;
const horizontal = targetCenterX - sourceCenterX;
const vertical = targetCenterY - sourceCenterY;
if (direction === "up" && vertical >= 0) return Number.POSITIVE_INFINITY;
if (direction === "down" && vertical <= 0) return Number.POSITIVE_INFINITY;
if (direction === "left" && horizontal >= 0) return Number.POSITIVE_INFINITY;
if (direction === "right" && horizontal <= 0) return Number.POSITIVE_INFINITY;
if (direction === "up" && vertical >= -1) return Number.POSITIVE_INFINITY;
if (direction === "down" && vertical <= 1) return Number.POSITIVE_INFINITY;
if (direction === "left" && horizontal >= -1) return Number.POSITIVE_INFINITY;
if (direction === "right" && horizontal <= 1) return Number.POSITIVE_INFINITY;
const primary = direction === "up" || direction === "down" ? Math.abs(vertical) : Math.abs(horizontal);
const secondary = direction === "up" || direction === "down" ? Math.abs(horizontal) : Math.abs(vertical);
return primary * 100 + secondary;
const sourceStart = direction === "up" || direction === "down" ? sourceRect.x : sourceRect.y;
const sourceEnd = sourceStart + (direction === "up" || direction === "down" ? sourceRect.width : sourceRect.height);
const targetStart = direction === "up" || direction === "down" ? targetRect.x : targetRect.y;
const targetEnd = targetStart + (direction === "up" || direction === "down" ? targetRect.width : targetRect.height);
const overlap = Math.max(0, Math.min(sourceEnd, targetEnd) - Math.max(sourceStart, targetStart));
const overlapPenalty = overlap > 0 ? -40 : 0;
return primary * 10 + secondary + overlapPenalty;
};
export const createNavigationManager = () => {
@@ -75,7 +88,10 @@ export const createNavigationManager = () => {
return;
}
const nodes = Array.from(contract.focusRoot.querySelectorAll("[data-focusable='true']"));
const roots = [contract.focusRoot, ...(contract.extraFocusRoots ?? [])].filter(Boolean);
const nodes = Array.from(
new Set(roots.flatMap((root) => Array.from(root.querySelectorAll("[data-focusable='true']")))),
);
focusables = nodes
.map((element, index) => {
@@ -86,6 +102,7 @@ export const createNavigationManager = () => {
row: Number(element.dataset.row ?? 0),
col: Number(element.dataset.col ?? 0),
key: element.dataset.focusKey ?? String(index),
region: element.dataset.navRegion ?? "content",
};
})
.sort((left, right) => {
@@ -111,7 +128,56 @@ export const createNavigationManager = () => {
return 0;
};
const resolveDefaultContentFocus = () => {
if (!focusables.length) {
return -1;
}
if (contract?.defaultFocus) {
const defaultIndex = focusables.findIndex((focusable) => focusable.element === contract.defaultFocus);
if (defaultIndex >= 0 && focusables[defaultIndex]?.region !== "sidebar") {
return defaultIndex;
}
}
return focusables.findIndex((focusable) => focusable.region !== "sidebar");
};
const findSidebarIndex = () => {
const activeIndex = focusables.findIndex(
(focusable) => focusable.region === "sidebar" && focusable.element.classList.contains("is-active"),
);
if (activeIndex >= 0) {
return activeIndex;
}
return focusables.findIndex((focusable) => focusable.region === "sidebar");
};
const findNextSidebarIndex = (direction) => {
const source = focusables[focusedIndex];
if (!source || source.region !== "sidebar") {
return null;
}
const sidebarItems = focusables
.map((item, index) => ({ item, index }))
.filter(({ item }) => item.region === "sidebar")
.sort((left, right) => left.item.row - right.item.row);
const currentSidebarIndex = sidebarItems.findIndex(({ index }) => index === focusedIndex);
if (currentSidebarIndex < 0) {
return null;
}
const nextSidebarIndex = direction === "up" ? currentSidebarIndex - 1 : currentSidebarIndex + 1;
return sidebarItems[nextSidebarIndex]?.index ?? null;
};
const moveWithNebula = (direction) => {
if (contract?.useNebulaNavigation === false) {
return null;
}
const picker = contract?.nebulaNavigation?.pickBestCandidate;
if (typeof picker !== "function") {
return null;
@@ -149,13 +215,38 @@ export const createNavigationManager = () => {
return;
}
const source = focusables[focusedIndex];
if (direction === "left" && source?.region !== "sidebar") {
const sidebarIndex = findSidebarIndex();
if (sidebarIndex >= 0) {
applyFocus(sidebarIndex);
}
return;
}
if (direction === "right" && source?.region === "sidebar") {
const contentIndex = resolveDefaultContentFocus();
if (contentIndex >= 0) {
applyFocus(contentIndex);
}
return;
}
if ((direction === "up" || direction === "down") && source?.region === "sidebar") {
const nextSidebarIndex = findNextSidebarIndex(direction);
if (nextSidebarIndex !== null) {
applyFocus(nextSidebarIndex);
}
return;
}
const nebulaIndex = moveWithNebula(direction);
if (nebulaIndex !== null) {
applyFocus(nebulaIndex);
return;
}
const source = focusables[focusedIndex];
let bestIndex = focusedIndex;
let bestScore = Number.POSITIVE_INFINITY;
+45 -4
View File
@@ -24,13 +24,54 @@
<div class="nebula-layer fog"></div>
<div class="nebula-layer vignette"></div>
</div>
<div class="shell-chrome" aria-hidden="true">
<div class="shell-depth-blur"></div>
<div class="app-layout">
<!-- Persistent left sidebar — hidden for lock/onboarding via JS class -->
<nav class="sidebar" id="sidebar" aria-label="Main navigation">
<div class="sidebar-logo">
<span class="sidebar-logo-icon" aria-hidden="true"></span>
<span class="sidebar-logo-text">Nebula OS</span>
</div>
<ul class="sidebar-nav" role="list">
<li class="sidebar-nav-item is-active focusable" data-sidebar-nav="home" data-target="home" data-nav-region="sidebar" data-focusable="true" data-row="0" data-col="-1" data-focus-key="sidebar-home" role="listitem" aria-current="page">
<span class="sidebar-nav-icon" aria-hidden="true"></span>
<span class="sidebar-nav-label">Home</span>
</li>
<li class="sidebar-nav-item focusable" data-sidebar-nav="library" data-target="library" data-nav-region="sidebar" data-focusable="true" data-row="1" data-col="-1" data-focus-key="sidebar-library" role="listitem">
<span class="sidebar-nav-icon" aria-hidden="true"></span>
<span class="sidebar-nav-label">Library</span>
</li>
<li class="sidebar-nav-item focusable" data-sidebar-nav="media" data-nav-region="sidebar" data-focusable="true" data-row="2" data-col="-1" data-focus-key="sidebar-media" data-disabled="true" role="listitem" aria-disabled="true">
<span class="sidebar-nav-icon" aria-hidden="true"></span>
<span class="sidebar-nav-label">Media</span>
</li>
<li class="sidebar-nav-item focusable" data-sidebar-nav="store" data-nav-region="sidebar" data-focusable="true" data-row="3" data-col="-1" data-focus-key="sidebar-store" data-disabled="true" role="listitem" aria-disabled="true">
<span class="sidebar-nav-icon" aria-hidden="true"></span>
<span class="sidebar-nav-label">Store</span>
</li>
<li class="sidebar-nav-item focusable" data-sidebar-nav="settings" data-target="settings" data-nav-region="sidebar" data-focusable="true" data-row="4" data-col="-1" data-focus-key="sidebar-settings" role="listitem">
<span class="sidebar-nav-icon" aria-hidden="true"></span>
<span class="sidebar-nav-label">Settings</span>
</li>
</ul>
<div class="sidebar-user">
<div class="sidebar-user-avatar" aria-hidden="true"></div>
</div>
</nav>
<!-- Main content area -->
<div class="app-main-area">
<main id="app" class="app-shell"></main>
<footer class="app-footer" id="app-footer"></footer>
</div>
</div>
<main id="app" class="app-shell"></main>
<div id="overlay-root"></div>
<div id="keyboard-root"></div>
<footer class="app-footer" id="app-footer"></footer>
<template id="global-hints-template">
<div class="hint-row">
+53 -3
View File
@@ -15,6 +15,12 @@ const overlayRoot = document.querySelector("#overlay-root");
const keyboardRoot = document.querySelector("#keyboard-root");
const footer = document.querySelector("#app-footer");
// Views that should hide the sidebar (full-screen flows)
const SIDEBAR_HIDDEN_VIEWS = new Set(["lock", "user-setup"]);
// Views that can be opened from the persistent sidebar.
const SIDEBAR_NAV_VIEWS = new Set(["home", "library", "settings"]);
const state = createAppState();
const nav = createNavigationManager();
const router = createRouter(appRoot);
@@ -64,14 +70,49 @@ const updateClockLabels = () => {
});
};
/**
* Update the persistent left sidebar to reflect the active view.
* Hides the sidebar entirely for lock/onboarding screens.
*/
const updateSidebar = (viewId) => {
const body = document.body;
const sidebar = document.querySelector("#sidebar");
if (SIDEBAR_HIDDEN_VIEWS.has(viewId)) {
body.classList.add("body-no-sidebar");
return;
}
body.classList.remove("body-no-sidebar");
if (!sidebar) return;
sidebar.querySelectorAll("[data-sidebar-nav]").forEach((item) => {
const matches = item.dataset.sidebarNav === viewId;
item.classList.toggle("is-active", matches);
if (matches) {
item.setAttribute("aria-current", "page");
} else {
item.removeAttribute("aria-current");
}
});
};
const renderView = (viewId) => {
const contract = router.navigate(viewId);
currentViewContract = contract;
if (!contract) {
return;
}
nav.mount(contract);
setFooterHints(contract.hintsTemplate ?? "#global-hints-template", state.glyphs);
state.activeView = viewId;
updateSidebar(viewId);
currentViewContract = {
...contract,
extraFocusRoots: SIDEBAR_HIDDEN_VIEWS.has(viewId) ? [] : [document.querySelector("#sidebar")],
};
nav.mount(currentViewContract);
setFooterHints(currentViewContract.hintsTemplate ?? "#global-hints-template", state.glyphs);
updateClockLabels();
};
@@ -136,6 +177,15 @@ const handleAction = (action) => {
focused?.classList.add("is-pressed");
window.setTimeout(() => focused?.classList.remove("is-pressed"), 180);
emitUiHook("accept", { focusKey: focused?.dataset.focusKey ?? null });
if (focused?.dataset.navRegion === "sidebar") {
const target = focused.dataset.target;
if (target && SIDEBAR_NAV_VIEWS.has(target)) {
renderView(target);
}
return;
}
currentViewContract.onAccept?.(focused);
return;
}
+222 -160
View File
@@ -7,104 +7,211 @@ body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--nebula-color-bg);
color: var(--nebula-color-text);
font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}
body {
display: grid;
grid-template-rows: 1fr auto;
overflow: hidden;
position: relative;
isolation: isolate;
}
/* ─── Flat background: no blur layers ─── */
#nebula-background {
position: fixed;
inset: 0;
z-index: -2;
overflow: hidden;
transform: translate3d(var(--bg-parallax-x, 0px), 0, 0);
transition: transform var(--nebula-duration-slow) var(--nebula-ease-standard);
}
.nebula-layer {
position: absolute;
inset: -6%;
will-change: transform, opacity, filter;
}
.nebula-layer.gradient {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 16% 12%, rgba(82, 116, 218, 0.26), transparent 48%),
radial-gradient(circle at 82% 76%, rgba(147, 79, 188, 0.22), transparent 46%),
radial-gradient(circle at 48% 88%, rgba(79, 216, 255, 0.14), transparent 38%),
linear-gradient(135deg, #050a17 0%, #090f28 24%, #0d1435 48%, #1a1542 78%, #23173c 100%);
animation: nebulaGradientDrift 28s var(--nebula-ease-standard) infinite alternate;
radial-gradient(circle at 15% 10%, rgba(79, 216, 255, 0.06), transparent 38%),
radial-gradient(circle at 85% 80%, rgba(157, 79, 224, 0.07), transparent 38%),
linear-gradient(165deg, #050810 0%, #070a14 40%, #0a0d1c 70%, #0d0a1e 100%);
}
.nebula-layer.starfield {
background-image:
radial-gradient(circle, rgba(255, 255, 255, 0.9) 0.8px, transparent 1.6px),
radial-gradient(circle, rgba(79, 216, 255, 0.6) 0.6px, transparent 1.4px),
radial-gradient(circle, rgba(147, 79, 188, 0.5) 1px, transparent 1.8px);
background-size: 180px 180px, 260px 260px, 320px 320px;
background-position: 0 0, 140px 110px, 60px 180px;
opacity: 0.24;
animation: starfieldShift 45s linear infinite;
}
.nebula-layer.fog {
background:
radial-gradient(ellipse at 28% 72%, rgba(82, 182, 255, 0.22), transparent 52%),
radial-gradient(ellipse at 72% 28%, rgba(147, 77, 203, 0.22), transparent 48%),
radial-gradient(ellipse at 45% 50%, rgba(79, 216, 255, 0.16), transparent 46%);
filter: blur(52px);
opacity: 0.76;
animation: fogDrift 34s var(--nebula-ease-standard) infinite alternate;
}
.nebula-layer.vignette {
background: radial-gradient(circle at center, transparent 45%, rgba(2, 6, 20, 0.55) 100%);
inset: 0;
}
.shell-chrome {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
.shell-depth-blur {
position: absolute;
inset: 80px 40px 120px;
backdrop-filter: blur(calc(8px + var(--nebula-focus-strength, 0) * 6px));
opacity: calc(0.22 + var(--nebula-focus-strength, 0) * 0.35);
border-radius: 32px;
transition:
opacity var(--nebula-duration-nav) var(--nebula-ease-console),
backdrop-filter var(--nebula-duration-nav) var(--nebula-ease-console);
inset: 0;
background-image:
radial-gradient(circle, rgba(255, 255, 255, 0.75) 0.6px, transparent 1.2px),
radial-gradient(circle, rgba(79, 216, 255, 0.4) 0.5px, transparent 1px);
background-size: 200px 200px, 300px 300px;
background-position: 0 0, 140px 110px;
opacity: 0.18;
}
/* fog and vignette layers: hidden for Zero-Blur Policy */
.nebula-layer.fog,
.nebula-layer.vignette {
display: none;
}
/* ─── No shell depth blur ─── */
.shell-chrome,
.shell-depth-blur {
display: none;
}
/* ─── Root layout: sidebar + main ─── */
.app-layout {
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
z-index: 0;
}
/* ─── Left sidebar ─── */
.sidebar {
width: 82px;
flex-shrink: 0;
height: 100%;
background: var(--nebula-color-sidebar);
border-right: 1px solid rgba(79, 216, 255, 0.1);
display: flex;
flex-direction: column;
align-items: center;
padding: 18px 0 16px;
z-index: 10;
}
.body-no-sidebar .sidebar {
display: none;
}
.sidebar-logo {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
margin-bottom: 20px;
}
.sidebar-logo-icon {
width: 46px;
height: 46px;
border-radius: 50%;
background: radial-gradient(circle at 38% 35%, rgba(79, 216, 255, 0.7) 0%, rgba(157, 79, 224, 0.85) 60%, rgba(40, 20, 80, 1) 100%);
border: 1.5px solid rgba(79, 216, 255, 0.35);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
line-height: 1;
}
.sidebar-logo-text {
font-size: 8px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--nebula-color-muted);
text-align: center;
}
.sidebar-nav {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 2px;
flex: 1;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
}
.sidebar-nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 11px 0;
cursor: pointer;
color: var(--nebula-color-muted);
border-left: 3px solid transparent;
border-right: 3px solid transparent;
transition:
color var(--nebula-duration-fast) var(--nebula-ease-standard),
background var(--nebula-duration-fast) var(--nebula-ease-standard),
border-left-color var(--nebula-duration-fast) var(--nebula-ease-standard);
}
.sidebar-nav-item.is-active {
color: var(--nebula-color-text);
background: rgba(79, 216, 255, 0.07);
border-left-color: var(--nebula-color-accent);
}
.sidebar-nav-item.is-focused {
color: var(--nebula-color-text);
background: rgba(79, 216, 255, 0.12);
border-left-color: var(--nebula-color-accent);
border-right-color: var(--nebula-color-purple);
transform: none;
}
.sidebar-nav-item[data-disabled="true"] {
opacity: 0.55;
}
.sidebar-nav-icon {
font-size: 19px;
line-height: 1;
}
.sidebar-nav-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.04em;
}
.sidebar-user {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 14px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
width: 100%;
}
.sidebar-user-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: radial-gradient(circle at 34% 30%, rgba(255, 255, 255, 0.85), rgba(108, 180, 255, 0.5));
border: 2px solid rgba(79, 216, 255, 0.25);
}
/* ─── Main content area ─── */
.app-main-area {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
min-width: 0;
}
.app-shell {
flex: 1;
min-height: 0;
overflow: hidden;
position: relative;
z-index: 2;
width: 100%;
height: 100%;
padding: 24px var(--nebula-spacing-xl) 0;
overflow: hidden;
}
/* ─── View transitions ─── */
.view {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: var(--nebula-spacing-lg);
opacity: 0;
transform: translateX(32px);
transform: translateX(24px);
}
.view.view-entered {
@@ -115,18 +222,37 @@ body {
opacity var(--nebula-duration-nav) var(--nebula-ease-standard);
}
.view-header {
display: flex;
align-items: center;
justify-content: space-between;
}
/* ─── Shared shell elements ─── */
.shell-topbar {
display: flex;
flex-direction: column;
min-height: 64px;
padding-bottom: var(--nebula-spacing-sm);
position: relative;
padding: 14px var(--nebula-spacing-xl);
border-bottom: 1px solid var(--nebula-color-border);
flex-shrink: 0;
}
.shell-brand {
margin: 0;
letter-spacing: 0.12em;
text-transform: uppercase;
font-size: 14px;
font-weight: 700;
color: var(--nebula-color-text);
}
.shell-time {
letter-spacing: 0.05em;
font-size: 16px;
font-weight: 600;
color: var(--nebula-color-muted);
}
.shell-avatar {
width: 30px;
height: 30px;
border-radius: var(--nebula-radius-pill);
border: 2px solid rgba(79, 216, 255, 0.3);
background: radial-gradient(circle at 34% 30%, rgba(255, 255, 255, 0.8), rgba(75, 81, 155, 0.7));
}
.shell-topbar-content {
@@ -136,14 +262,8 @@ body {
width: 100%;
}
.shell-brand {
margin: 0;
letter-spacing: 0.12em;
text-transform: uppercase;
font-size: 15px;
font-weight: 680;
color: color-mix(in srgb, var(--nebula-color-text) 92%, var(--nebula-color-accent));
text-shadow: 0 0 24px rgba(79, 216, 255, 0.38), 0 0 6px rgba(79, 216, 255, 0.2);
.shell-accent-line {
display: none;
}
.shell-status {
@@ -152,56 +272,10 @@ body {
gap: var(--nebula-spacing-md);
}
.shell-time {
letter-spacing: 0.05em;
font-size: 16px;
font-weight: 560;
color: color-mix(in srgb, var(--nebula-color-text) 90%, var(--nebula-color-muted));
}
.shell-avatar {
width: 32px;
height: 32px;
border-radius: var(--nebula-radius-pill);
border: 2px solid rgba(79, 216, 255, 0.4);
background:
radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.85), transparent 46%),
linear-gradient(145deg, rgba(108, 180, 255, 0.7), rgba(75, 81, 155, 0.6));
box-shadow:
0 0 12px rgba(79, 216, 255, 0.25),
0 2px 8px rgba(0, 0, 0, 0.3);
}
.shell-accent-line {
position: absolute;
inset: auto 0 0;
height: 3px;
overflow: hidden;
background: linear-gradient(90deg,
transparent,
rgba(79, 216, 255, 0.12) 25%,
rgba(79, 216, 255, 0.08) 75%,
transparent
);
}
.shell-accent-line::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: clamp(120px, 14vw, 200px);
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(79, 216, 255, 0.6) 30%,
var(--nebula-color-accent) 50%,
rgba(79, 216, 255, 0.6) 70%,
transparent
);
box-shadow: 0 0 16px rgba(79, 216, 255, 0.5);
transform: translateX(var(--nebula-accent-line-x, 0px));
transition: transform var(--nebula-duration-nav) var(--nebula-ease-console);
.view-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.view-title {
@@ -215,9 +289,14 @@ body {
color: var(--nebula-color-muted);
}
/* ─── Footer hints bar ─── */
.app-footer {
min-height: 56px;
padding: 0 var(--nebula-spacing-xl) var(--nebula-spacing-md);
flex-shrink: 0;
min-height: 44px;
padding: 0 var(--nebula-spacing-xl) var(--nebula-spacing-sm);
border-top: 1px solid var(--nebula-color-border);
display: flex;
align-items: center;
}
.hint-row {
@@ -229,34 +308,17 @@ body {
.hint {
display: inline-flex;
gap: var(--nebula-spacing-xs);
gap: 6px;
align-items: center;
font-size: 15px;
}
@keyframes nebulaGradientDrift {
0% {
transform: translate3d(-2%, -1.5%, 0) scale(1.03) rotate(0deg);
}
100% {
transform: translate3d(1.5%, 2%, 0) scale(1.05) rotate(0.5deg);
}
font-size: 14px;
}
/* ─── Background animations ─── */
@keyframes starfieldShift {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-140px, -110px, 0);
}
0% { transform: translate3d(0, 0, 0); }
100% { transform: translate3d(-140px, -110px, 0); }
}
@keyframes fogDrift {
0% {
transform: translate3d(-2%, 1.2%, 0) scale(1.02);
}
100% {
transform: translate3d(2.2%, -1.4%, 0) scale(1.04);
}
.nebula-layer.starfield {
animation: starfieldShift 60s linear infinite;
}
+82 -89
View File
@@ -1,164 +1,157 @@
/* ─── Panel (flat, no blur) ─── */
.panel {
background: var(--nebula-color-panel);
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--nebula-color-border);
border-radius: var(--nebula-radius-md);
padding: var(--nebula-spacing-lg);
box-shadow: var(--nebula-depth-shadow);
backdrop-filter: blur(12px);
}
/* ─── Focusable — flat neon border, no glow/blur ─── */
.focusable {
border: 1px solid transparent;
border: 2px solid transparent;
border-radius: var(--nebula-radius-md);
outline: none;
position: relative;
overflow: hidden;
will-change: transform, box-shadow, border-color;
cursor: pointer;
will-change: transform, border-color;
transform: translateZ(0);
transition:
transform var(--nebula-duration-nav) var(--nebula-ease-console),
border-color var(--nebula-duration-nav) var(--nebula-ease-console),
box-shadow var(--nebula-duration-nav) var(--nebula-ease-console),
background-color var(--nebula-duration-nav) var(--nebula-ease-standard);
}
.focusable.is-focused {
border-color: rgba(79, 216, 255, 0.5);
box-shadow:
0 0 0 2px color-mix(in srgb, var(--nebula-color-focus) 45%, transparent),
0 0 28px color-mix(in srgb, var(--nebula-color-focus) 35%, transparent),
0 4px 16px rgba(2, 6, 18, 0.3),
var(--nebula-depth-shadow-focus);
border-color: var(--nebula-color-accent);
transform: scale(1.03) translateZ(0);
}
.focusable::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(
circle at var(--ripple-x, 50%) var(--ripple-y, 50%),
rgba(79, 216, 255, 0.28),
rgba(79, 216, 255, 0.12) 40%,
transparent 65%
);
opacity: 0;
transform: scale(0.75);
transition:
opacity var(--nebula-duration-nav) var(--nebula-ease-console),
transform var(--nebula-duration-slow) var(--nebula-ease-console);
pointer-events: none;
}
.focusable.is-focused::before {
opacity: 1;
transform: scale(1.12);
.sidebar-nav-item.focusable.is-focused {
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: var(--nebula-color-accent);
border-right-color: var(--nebula-color-purple);
transform: none;
}
.focusable.is-pressed {
animation: uiPressPulse var(--nebula-duration-fast) var(--nebula-ease-snap);
}
/* ─── Tile ─── */
.tile {
min-height: 188px;
min-width: 320px;
min-height: 160px;
min-width: 280px;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: var(--nebula-spacing-xs);
background:
linear-gradient(160deg, rgba(65, 108, 189, 0.32), rgba(24, 36, 72, 0.90)),
radial-gradient(circle at 75% 25%, rgba(79, 216, 255, 0.12), transparent 48%),
var(--nebula-color-panelAlt);
background: var(--nebula-color-panel-alt);
color: var(--nebula-color-text);
border-radius: var(--nebula-radius-md);
padding: var(--nebula-spacing-lg);
transform-origin: center;
box-shadow:
0 8px 24px rgba(2, 6, 18, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
position: relative;
overflow: hidden;
border: 2px solid var(--nebula-color-border);
}
.tile::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(79, 216, 255, 0.08), transparent 60%);
opacity: 0;
transition: opacity var(--nebula-duration-nav) var(--nebula-ease-console);
pointer-events: none;
}
.tile.is-focused::after {
opacity: 1;
.tile.is-focused {
border-color: var(--nebula-color-accent);
transform: scale(1.04) translateZ(0);
}
.tile-icon {
font-size: 42px;
font-size: 38px;
line-height: 1;
opacity: 0.94;
filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.32));
opacity: 0.9;
transition: transform var(--nebula-duration-nav) var(--nebula-ease-console);
}
.tile.is-focused .tile-icon {
transform: scale(1.08) translateY(-2px);
transform: scale(1.06) translateY(-2px);
}
.tile-label {
margin: 0;
font-size: clamp(22px, 2vw, 28px);
font-weight: 720;
font-size: clamp(20px, 2vw, 26px);
font-weight: 700;
letter-spacing: -0.01em;
z-index: 1;
transition: transform var(--nebula-duration-fast) var(--nebula-ease-console);
}
.tile.is-focused .tile-label {
transform: translateX(3px);
}
.tile-meta {
margin: 0;
font-size: 15px;
font-size: 14px;
color: var(--nebula-color-muted);
z-index: 1;
opacity: 0.88;
transition:
transform var(--nebula-duration-fast) var(--nebula-ease-console),
opacity var(--nebula-duration-fast) var(--nebula-ease-console);
}
.tile.is-focused .tile-meta {
transform: translateX(3px);
.tile-accent-bar {
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 3px;
background: var(--nebula-color-accent);
opacity: 0;
transform: scaleX(0);
transform-origin: left center;
transition:
opacity var(--nebula-duration-nav) var(--nebula-ease-console),
transform var(--nebula-duration-nav) var(--nebula-ease-console);
}
.dashboard-tile.is-focused .tile-accent-bar {
opacity: 1;
transform: scaleX(1);
}
.tile.is-focused {
transform: scale(1.06) translateZ(0);
box-shadow:
0 16px 40px rgba(2, 6, 18, 0.5),
0 0 0 2px rgba(79, 216, 255, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.12);
/* ─── Controller button prompts ─── */
.btn-prompt {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
font-size: 11px;
font-weight: 800;
flex-shrink: 0;
line-height: 1;
}
.btn-a { background: var(--btn-a); color: #fff; }
.btn-b { background: var(--btn-b); color: #fff; }
.btn-x { background: var(--btn-x); color: #fff; }
.btn-y { background: var(--btn-y); color: #fff; }
.btn-glyph {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 6px;
height: 18px;
border-radius: var(--nebula-radius-sm);
font-size: 11px;
font-weight: 700;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.18);
color: var(--nebula-color-muted);
}
/* ─── Button-like ─── */
.button-like {
background: var(--nebula-color-panelAlt);
background: var(--nebula-color-panel-alt);
color: var(--nebula-color-text);
padding: var(--nebula-spacing-md) var(--nebula-spacing-lg);
border-radius: var(--nebula-radius-sm);
}
/* ─── Animations ─── */
@keyframes uiPressPulse {
0% {
transform: scale(1);
}
40% {
transform: scale(0.96);
}
100% {
transform: scale(1);
}
0% { transform: scale(1); }
40% { transform: scale(0.97); }
100% { transform: scale(1); }
}
+47 -30
View File
@@ -1,41 +1,58 @@
:root {
--nebula-color-bg: #050a17;
--nebula-color-bg-deep: #0a1028;
--nebula-color-bg-purple: #1a1342;
--nebula-color-panel: rgba(18, 30, 58, 0.75);
--nebula-color-panelAlt: rgba(26, 43, 82, 0.88);
--nebula-color-text: #f2f7ff;
--nebula-color-muted: #a8bdd8;
--nebula-color-accent: #4fd8ff;
--nebula-color-accent-soft: rgba(79, 216, 255, 0.4);
--nebula-color-danger: #ff6b88;
--nebula-color-success: #7dff9e;
--nebula-color-focus: #4fd8ff;
--nebula-color-overlay: rgba(5, 8, 20, 0.82);
/* Core palette — solid, no heavy transparency */
--nebula-color-bg: #070a14;
--nebula-color-bg-deep: #050810;
--nebula-color-sidebar: #0a0c18;
--nebula-color-panel: #0d1020;
--nebula-color-panel-alt: #111425;
--nebula-color-border: rgba(255, 255, 255, 0.08);
--nebula-color-border-mid: rgba(255, 255, 255, 0.14);
--nebula-spacing-xs: 6px;
--nebula-spacing-sm: 10px;
--nebula-spacing-md: 16px;
--nebula-spacing-lg: 24px;
--nebula-spacing-xl: 36px;
--nebula-color-text: #f2f7ff;
--nebula-color-muted: #7a8fa8;
--nebula-radius-sm: 10px;
--nebula-radius-md: 14px;
--nebula-radius-lg: 20px;
/* Neon accents */
--nebula-color-accent: #4fd8ff;
--nebula-color-purple: #9d4fe0;
--nebula-color-danger: #ff4f6b;
--nebula-color-success: #4fff88;
--nebula-color-focus: #4fd8ff;
/* Controller button colours (flat solid) */
--btn-a: #4caf50;
--btn-b: #f44336;
--btn-x: #2196f3;
--btn-y: #ff9800;
--nebula-spacing-xs: 6px;
--nebula-spacing-sm: 10px;
--nebula-spacing-md: 16px;
--nebula-spacing-lg: 24px;
--nebula-spacing-xl: 36px;
--nebula-radius-sm: 6px;
--nebula-radius-md: 10px;
--nebula-radius-lg: 16px;
--nebula-radius-pill: 999px;
--nebula-type-body: 18px;
--nebula-type-title: 24px;
--nebula-type-body: 18px;
--nebula-type-title: 24px;
--nebula-type-display: 34px;
--nebula-ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1);
--nebula-ease-console: cubic-bezier(0.19, 0.82, 0.18, 1);
--nebula-ease-snap: cubic-bezier(0.32, 0.94, 0.18, 1);
--nebula-ease-console: cubic-bezier(0.19, 0.82, 0.18, 1);
--nebula-ease-snap: cubic-bezier(0.32, 0.94, 0.18, 1);
--nebula-duration-fast: 120ms;
--nebula-duration-nav: 180ms;
--nebula-duration-slow: 340ms;
--nebula-duration-fast: 100ms;
--nebula-duration-nav: 160ms;
--nebula-duration-slow: 300ms;
--nebula-depth-shadow: 0 12px 32px rgba(2, 6, 20, 0.48);
--nebula-depth-shadow-focus: 0 20px 48px rgba(2, 12, 38, 0.62), 0 8px 16px rgba(2, 6, 20, 0.3);
/* No glow/shadow — flat */
--nebula-depth-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
/* Overlay backdrop (power menu, etc.) — kept solid/dark */
--nebula-color-overlay: rgba(5, 7, 18, 0.88);
/* Backward-compat aliases */
--nebula-color-panelAlt: #111425;
}
+512 -70
View File
@@ -1,90 +1,532 @@
/* ════════════════════════════════════════════════════════
HOME VIEW — Sci-fi dashboard layout
════════════════════════════════════════════════════════ */
.home-view {
justify-content: flex-start;
gap: var(--nebula-spacing-xl);
display: flex;
flex-direction: column;
height: 100%;
padding: 0;
gap: 0;
overflow: hidden;
}
/* ── Top status bar ──────────────────────────────────── */
.home-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px 12px;
flex-shrink: 0;
border-bottom: 1px solid var(--nebula-color-border);
}
.home-time {
font-size: clamp(36px, 4vw, 52px);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--nebula-color-text);
line-height: 1;
}
.home-status-icons {
display: flex;
align-items: center;
gap: 14px;
color: var(--nebula-color-muted);
}
.home-status-icon {
display: flex;
align-items: center;
justify-content: center;
}
/* ── Body: two columns ────────────────────────────────── */
.home-body {
flex: 1;
display: grid;
grid-template-columns: 1fr 310px;
gap: 16px;
padding: 14px 20px 0;
min-height: 0;
overflow: hidden;
}
/* ── Center column ───────────────────────────────────── */
.home-center {
display: flex;
flex-direction: column;
gap: 12px;
min-height: 0;
overflow: hidden;
}
/* ── Category tabs ───────────────────────────────────── */
.home-tabs {
display: flex;
align-items: center;
gap: 20px;
flex-shrink: 0;
border-bottom: 1px solid var(--nebula-color-border);
padding-bottom: 10px;
}
.home-tab {
background: none;
color: var(--nebula-color-muted);
font-size: 16px;
font-weight: 700;
padding: 0 2px 8px;
border: none;
border-bottom: 2px solid transparent;
border-radius: 0;
cursor: pointer;
transition:
color var(--nebula-duration-fast) var(--nebula-ease-standard),
border-bottom-color var(--nebula-duration-fast) var(--nebula-ease-standard);
transform: none;
}
.home-tab.is-active {
color: var(--nebula-color-text);
border-bottom-color: var(--nebula-color-accent);
}
.home-tab.is-focused {
color: var(--nebula-color-accent);
border-bottom-color: var(--nebula-color-accent);
transform: none;
border-radius: 0;
}
.tab-hint {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: var(--nebula-color-muted);
}
/* ── Hero card ───────────────────────────────────────── */
.hero-card {
flex: 1;
position: relative;
border-radius: var(--nebula-radius-lg);
overflow: hidden;
min-height: 0;
border: 2px solid var(--nebula-color-border);
transition: border-color var(--nebula-duration-nav) var(--nebula-ease-console);
transform: none;
}
.hero-card.is-focused {
border-color: var(--nebula-color-accent);
transform: none;
}
/* Game art layers */
.hero-art {
position: absolute;
inset: 0;
overflow: hidden;
}
.hero-art-bg {
position: absolute;
inset: 0;
background:
linear-gradient(165deg,
#0d0e00 0%,
#1a1600 20%,
#2a2200 40%,
#3a3000 60%,
rgba(180, 150, 0, 0.25) 80%,
rgba(220, 180, 0, 0.15) 100%
);
}
.hero-art-mid {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 65% 50%, rgba(255, 200, 0, 0.18) 0%, transparent 55%),
radial-gradient(ellipse at 30% 80%, rgba(0, 80, 180, 0.2) 0%, transparent 45%);
}
/* Stylised "character" area */
.hero-art-character {
position: absolute;
bottom: 0;
right: 10%;
width: 42%;
height: 90%;
background:
linear-gradient(180deg,
transparent 0%,
rgba(60, 50, 0, 0.4) 30%,
rgba(100, 90, 0, 0.6) 60%,
rgba(30, 25, 0, 0.9) 100%
);
clip-path: polygon(15% 0%, 85% 0%, 100% 100%, 0% 100%);
}
.hero-title-watermark {
position: absolute;
bottom: 110px;
right: 5%;
font-size: clamp(32px, 5vw, 64px);
font-weight: 900;
color: rgba(255, 220, 0, 0.85);
letter-spacing: -0.03em;
line-height: 0.95;
text-align: right;
text-transform: uppercase;
pointer-events: none;
font-style: italic;
}
/* Controller overlay */
.hero-ctrl-overlay {
position: absolute;
top: 20px;
left: 20px;
display: flex;
gap: 14px;
align-items: center;
opacity: 0.55;
}
.ctrl-glyph {
display: block;
}
.hero-l-badge {
width: 22px;
height: 22px;
border-radius: var(--nebula-radius-sm);
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.25);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 800;
color: rgba(255, 255, 255, 0.7);
}
/* Gradient overlay + info */
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(7, 10, 20, 0.98) 0%,
rgba(7, 10, 20, 0.7) 36%,
transparent 65%
);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 20px 24px;
}
.hero-info {
display: flex;
flex-direction: column;
gap: 14px;
}
.hero-game-title {
margin: 0;
font-size: clamp(26px, 3.2vw, 42px);
font-weight: 800;
letter-spacing: -0.02em;
color: #fff;
line-height: 1.1;
}
.hero-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.hero-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 18px;
border-radius: var(--nebula-radius-sm);
font-size: 14px;
font-weight: 700;
cursor: pointer;
background: rgba(255, 255, 255, 0.08);
border: 1.5px solid rgba(255, 255, 255, 0.2);
color: #fff;
transition:
background var(--nebula-duration-fast) var(--nebula-ease-standard),
border-color var(--nebula-duration-fast) var(--nebula-ease-standard),
transform var(--nebula-duration-fast) var(--nebula-ease-console);
transform: none;
}
.hero-btn-primary {
background: var(--nebula-color-accent);
border-color: transparent;
color: #070a14;
}
.hero-btn.is-focused {
border-color: var(--nebula-color-accent);
transform: translateY(-1px);
}
.hero-btn-primary.is-focused {
border-color: #fff;
transform: translateY(-1px);
}
.hero-dots {
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
}
.hero-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.28);
transition: all var(--nebula-duration-fast) var(--nebula-ease-standard);
}
.hero-dot.is-active {
width: 22px;
border-radius: 3px;
background: var(--nebula-color-accent);
}
/* ── Featured strip ──────────────────────────────────── */
.featured-strip {
flex-shrink: 0;
padding-bottom: 12px;
}
.section-label {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--nebula-color-muted);
margin: 0 0 10px;
}
.featured-row {
display: flex;
gap: 10px;
}
.featured-thumb {
flex: 1;
height: 68px;
border-radius: var(--nebula-radius-md);
background: linear-gradient(135deg, var(--thumb-a, #1a1a2e), var(--thumb-b, #2a1050));
border: 2px solid var(--nebula-color-border);
cursor: pointer;
transition: border-color var(--nebula-duration-fast);
overflow: hidden;
padding: 0;
}
.featured-thumb.is-focused {
border-color: var(--nebula-color-accent);
transform: translateY(-2px);
}
/* ── Right panel ─────────────────────────────────────── */
.home-right-panel {
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
min-height: 0;
padding-bottom: 12px;
}
.panel-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.panel-heading {
font-size: 15px;
font-weight: 700;
color: var(--nebula-color-text);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.panel-heading-sm {
font-size: 13px;
font-weight: 700;
color: var(--nebula-color-text);
margin: 0 0 8px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* ── Quick launch grid ───────────────────────────────── */
.quick-launch {
flex-shrink: 0;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.quick-tile {
position: relative;
border-radius: var(--nebula-radius-md);
overflow: hidden;
background: var(--nebula-color-panel);
border: 2px solid var(--nebula-color-border);
cursor: pointer;
padding: 0;
text-align: left;
color: var(--nebula-color-text);
transition:
border-color var(--nebula-duration-fast) var(--nebula-ease-console),
transform var(--nebula-duration-fast) var(--nebula-ease-console);
transform: translateZ(0);
}
.quick-tile.is-focused {
border-color: var(--nebula-color-accent);
transform: scale(1.04) translateZ(0);
}
.quick-tile-art {
height: 70px;
background: linear-gradient(135deg, var(--ta, #1a1a2e), var(--tb, #2a2050));
position: relative;
overflow: hidden;
}
.tile-badge {
position: absolute;
bottom: 4px;
right: 4px;
font-size: 12px;
line-height: 1;
}
.quick-tile-footer {
padding: 6px 8px 7px;
}
.quick-tile-name {
margin: 0;
font-size: 11px;
font-weight: 700;
color: var(--nebula-color-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
}
.quick-tile-meta {
margin: 3px 0 0;
font-size: 10px;
color: var(--nebula-color-muted);
line-height: 1.3;
}
/* Solid progress bar */
.quick-tile-bar {
position: relative;
padding-left: 0;
}
.home-hero {
.quick-tile-bar::before {
content: "";
display: block;
height: 2px;
background: var(--nebula-color-border);
border-radius: 1px;
margin-bottom: 3px;
}
.quick-tile-bar::after {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
width: var(--pct, 0%);
height: 2px;
background: var(--nebula-color-accent);
border-radius: 1px;
}
/* ── Bottom side panels ──────────────────────────────── */
.side-bottom-panels {
display: grid;
gap: var(--nebula-spacing-xs);
padding-left: var(--nebula-spacing-xl);
grid-template-columns: 1fr 1fr;
gap: 10px;
flex-shrink: 0;
}
.home-hero .muted {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 14px;
font-weight: 600;
opacity: 0.72;
.friends-panel,
.activity-panel {
background: var(--nebula-color-panel);
border: 1px solid var(--nebula-color-border);
border-radius: var(--nebula-radius-md);
padding: 12px;
}
.home-hero .view-title {
font-size: clamp(32px, 3.2vw, 44px);
font-weight: 700;
letter-spacing: -0.02em;
}
.tile-rail {
.friends-avatars {
display: flex;
gap: var(--nebula-spacing-lg);
overflow-x: auto;
overflow-y: hidden;
padding: 10px var(--nebula-spacing-xl) 18px;
scroll-behavior: smooth;
scrollbar-width: none;
transform: translate3d(var(--home-parallax-x, 0px), 0, 0);
transition: transform var(--nebula-duration-slow) var(--nebula-ease-console);
gap: 6px;
flex-wrap: wrap;
}
.tile-rail::-webkit-scrollbar {
display: none;
.friend-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--fc, var(--nebula-color-accent));
opacity: 0.85;
border: 1.5px solid rgba(255, 255, 255, 0.12);
}
.dashboard-tile {
flex: 0 0 clamp(280px, 22vw, 340px);
min-height: clamp(200px, 18vh, 240px);
position: relative;
overflow: visible;
}
.dashboard-tile.tile-large {
flex: 0 0 clamp(360px, 28vw, 440px);
min-height: clamp(240px, 22vh, 300px);
}
.tile-content {
.activity-row {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 100%;
gap: var(--nebula-spacing-md);
position: relative;
z-index: 2;
margin-bottom: 4px;
}
.tile-text {
display: flex;
flex-direction: column;
.activity-label {
font-size: 17px;
font-weight: 800;
color: var(--nebula-color-text);
line-height: 1;
}
.activity-hint {
margin: 0;
font-size: 11px;
color: var(--nebula-color-muted);
display: inline-flex;
align-items: center;
gap: 4px;
}
.tile-accent-bar {
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--nebula-color-accent), transparent);
opacity: 0;
transform: scaleX(0);
transform-origin: left center;
transition:
opacity var(--nebula-duration-nav) var(--nebula-ease-console),
transform var(--nebula-duration-nav) var(--nebula-ease-console);
}
.dashboard-tile.is-focused .tile-accent-bar {
opacity: 1;
transform: scaleX(1);
}
+241 -89
View File
@@ -1,63 +1,227 @@
const HOME_TEMPLATE = `
<section class="view home-view" data-view="home">
<header class="shell-topbar">
<div class="shell-topbar-content">
<p class="shell-brand">Nebula OS</p>
<div class="shell-status">
<span class="shell-avatar" aria-hidden="true"></span>
<p class="shell-time" data-clock>--:--</p>
</div>
<!-- ── Top status bar ─────────────────────────────── -->
<header class="home-topbar">
<span class="home-time" data-clock>--:--</span>
<div class="home-status-icons" aria-label="System status">
<span class="home-status-icon" title="Wi-Fi">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/>
</svg>
</span>
<span class="home-status-icon" title="Controller">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="2" y="6" width="20" height="12" rx="6"/><path d="M6 12h4m-2-2v4"/><circle cx="17" cy="11" r="1" fill="currentColor"/><circle cx="15" cy="13" r="1" fill="currentColor"/>
</svg>
</span>
<span class="home-status-icon" title="Battery">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="1" y="6" width="18" height="12" rx="2"/><path d="M23 11v2" stroke-width="2.5" stroke-linecap="round"/><rect x="3" y="8" width="12" height="8" rx="1" fill="currentColor" stroke="none"/>
</svg>
</span>
</div>
<div class="shell-accent-line"></div>
</header>
<section class="home-hero">
<p class="muted">Dashboard</p>
<h1 class="view-title">Jump back in</h1>
</section>
<!-- ── Main body: center + right panel ───────────── -->
<div class="home-body">
<section class="tile-rail" data-focus-root data-home-rail>
<button class="focusable tile dashboard-tile tile-large" data-focusable="true" data-row="0" data-col="0" data-target="library" data-focus-key="library">
<div class="tile-content">
<span class="tile-icon" aria-hidden="true">📚</span>
<div class="tile-text">
<p class="tile-label">Library</p>
<p class="tile-meta">Your games & apps</p>
<!-- ── Center column ──────────────────────────── -->
<div class="home-center">
<!-- Category tabs -->
<nav class="home-tabs" aria-label="Content categories">
<button
class="home-tab is-active focusable"
data-focusable="true" data-row="0" data-col="0"
data-focus-key="tab-now-playing"
aria-selected="true"
>Now Playing</button>
<button
class="home-tab focusable"
data-focusable="true" data-row="0" data-col="1"
data-focus-key="tab-featured"
aria-selected="false"
>Featured</button>
<span class="tab-hint" aria-hidden="true">
<span class="btn-glyph">LT</span> / <span class="btn-glyph">RT</span>
to cycle categories
</span>
</nav>
<!-- Hero game card -->
<article class="hero-card" aria-label="Now playing: Cyberpunk 2077">
<!-- Game art (placeholder — would be a real cover image) -->
<div class="hero-art" aria-hidden="true">
<div class="hero-art-bg"></div>
<div class="hero-art-mid"></div>
<div class="hero-art-character"></div>
<div class="hero-title-watermark" aria-hidden="true">Cyberpunk<br>2077</div>
<!-- Controller overlay icons -->
<div class="hero-ctrl-overlay" aria-hidden="true">
<svg class="ctrl-glyph ctrl-analog" width="48" height="48" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="22" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
<circle cx="24" cy="24" r="10" stroke="rgba(255,255,255,0.25)" stroke-width="1.5"/>
<circle cx="24" cy="24" r="4" fill="rgba(255,255,255,0.2)"/>
</svg>
<svg class="ctrl-glyph ctrl-dpad" width="40" height="40" viewBox="0 0 40 40" fill="none">
<rect x="13" y="4" width="14" height="32" rx="3" fill="rgba(255,255,255,0.18)"/>
<rect x="4" y="13" width="32" height="14" rx="3" fill="rgba(255,255,255,0.18)"/>
<rect x="15" y="15" width="10" height="10" rx="2" fill="rgba(255,255,255,0.1)"/>
</svg>
<span class="hero-l-badge">L</span>
</div>
</div>
</div>
<div class="tile-accent-bar"></div>
</button>
<button class="focusable tile dashboard-tile" data-focusable="true" data-row="0" data-col="1" data-target="browser" data-focus-key="browser">
<div class="tile-content">
<span class="tile-icon" aria-hidden="true">🌐</span>
<div class="tile-text">
<p class="tile-label">Browser</p>
<p class="tile-meta">Explore the web</p>
<!-- Gradient overlay + info -->
<div class="hero-overlay">
<div class="hero-info">
<h1 class="hero-game-title">Cyberpunk 2077</h1>
<div class="hero-actions">
<button
class="hero-btn hero-btn-primary focusable"
data-focusable="true" data-row="1" data-col="0"
data-focus-key="btn-continue"
>
<span class="btn-prompt btn-a" aria-label="A button">A</span>
Continue Game
</button>
<button
class="hero-btn focusable"
data-focusable="true" data-row="1" data-col="1"
data-focus-key="btn-progress"
>
<span class="btn-prompt btn-x" aria-label="X button">X</span>
View Progress (68%)
</button>
<button
class="hero-btn focusable"
data-focusable="true" data-row="1" data-col="2"
data-focus-key="btn-community"
>
<span class="btn-prompt btn-y" aria-label="Y button">Y</span>
Community Hub
</button>
</div>
</div>
<div class="hero-dots" aria-hidden="true">
<span class="hero-dot is-active"></span>
<span class="hero-dot"></span>
<span class="hero-dot"></span>
<span class="hero-dot"></span>
<span class="hero-dot"></span>
</div>
</div>
</div>
<div class="tile-accent-bar"></div>
</button>
<button class="focusable tile dashboard-tile" data-focusable="true" data-row="0" data-col="2" data-target="settings" data-focus-key="settings">
<div class="tile-content">
<span class="tile-icon" aria-hidden="true">⚙️</span>
<div class="tile-text">
<p class="tile-label">Settings</p>
<p class="tile-meta">System configuration</p>
</article>
<!-- Featured strip -->
<section class="featured-strip" aria-label="Featured games">
<h2 class="section-label">Featured</h2>
<div class="featured-row">
<button class="featured-thumb focusable" data-focusable="true" data-row="3" data-col="0" data-focus-key="feat-0" style="--thumb-a:#1a0a2e;--thumb-b:#2e1050;" aria-label="Featured game 1"></button>
<button class="featured-thumb focusable" data-focusable="true" data-row="3" data-col="1" data-focus-key="feat-1" style="--thumb-a:#0a1a0a;--thumb-b:#0a3a18;" aria-label="Featured game 2"></button>
<button class="featured-thumb focusable" data-focusable="true" data-row="3" data-col="2" data-focus-key="feat-2" style="--thumb-a:#2e1500;--thumb-b:#4a2800;" aria-label="Featured game 3"></button>
<button class="featured-thumb focusable" data-focusable="true" data-row="3" data-col="3" data-focus-key="feat-3" style="--thumb-a:#001020;--thumb-b:#002040;" aria-label="Featured game 4"></button>
</div>
</div>
<div class="tile-accent-bar"></div>
</button>
<button class="focusable tile dashboard-tile" data-focusable="true" data-row="0" data-col="3" data-target="power" data-focus-key="power">
<div class="tile-content">
<span class="tile-icon" aria-hidden="true">⏻</span>
<div class="tile-text">
<p class="tile-label">Power</p>
<p class="tile-meta">Sleep, restart, shut down</p>
</section>
</div>
<!-- ── Right panel ─────────────────────────────── -->
<aside class="home-right-panel" aria-label="Quick launch and activity">
<!-- Quick Launch grid -->
<section class="quick-launch" aria-label="Quick launch">
<div class="panel-header-row">
<h2 class="panel-heading">Quick Launch</h2>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" rx="1" fill="rgba(79,216,255,0.5)"/>
<rect x="9" y="1" width="6" height="6" rx="1" fill="rgba(79,216,255,0.5)"/>
<rect x="1" y="9" width="6" height="6" rx="1" fill="rgba(79,216,255,0.5)"/>
<rect x="9" y="9" width="6" height="6" rx="1" fill="rgba(79,216,255,0.5)"/>
</svg>
</div>
<div class="quick-grid">
<button class="quick-tile focusable" data-focusable="true" data-row="1" data-col="3" data-focus-key="ql-0" aria-label="Elden Ring">
<div class="quick-tile-art" style="--ta:#3d1a00;--tb:#6b2e00;" aria-hidden="true"></div>
<div class="quick-tile-footer">
<p class="quick-tile-name">Elden Ring</p>
<p class="quick-tile-meta">Progress</p>
</div>
</button>
<button class="quick-tile focusable" data-focusable="true" data-row="1" data-col="4" data-focus-key="ql-1" aria-label="Forza Horizon 5">
<div class="quick-tile-art" style="--ta:#0a2200;--tb:#1a4a00;" aria-hidden="true">
<span class="tile-badge" aria-label="Trophy">🏆</span>
</div>
<div class="quick-tile-footer">
<p class="quick-tile-name">Forza Horizon 5</p>
<p class="quick-tile-meta">Progress <span aria-label="Trophy">🏆</span></p>
</div>
</button>
<button class="quick-tile focusable" data-focusable="true" data-row="1" data-col="5" data-focus-key="ql-2" aria-label="Hades II">
<div class="quick-tile-art" style="--ta:#1a0a30;--tb:#3a0a60;" aria-hidden="true">
<span class="tile-badge" aria-label="Trophy">🏆</span>
</div>
<div class="quick-tile-footer">
<p class="quick-tile-name">Hades II</p>
<p class="quick-tile-meta">35 Achievements</p>
</div>
</button>
<button class="quick-tile focusable" data-focusable="true" data-row="2" data-col="3" data-focus-key="ql-3" aria-label="Forza Horizon 4">
<div class="quick-tile-art" style="--ta:#001830;--tb:#002850;" aria-hidden="true"></div>
<div class="quick-tile-footer">
<p class="quick-tile-name">Forza Horizon 4</p>
<p class="quick-tile-meta quick-tile-bar" style="--pct:20%">20% Progress</p>
</div>
</button>
<button class="quick-tile focusable" data-focusable="true" data-row="2" data-col="4" data-focus-key="ql-4" aria-label="Forza Horizon 5 alt">
<div class="quick-tile-art" style="--ta:#200800;--tb:#401000;" aria-hidden="true">
<span class="tile-badge" aria-label="Trophy">🏆</span>
</div>
<div class="quick-tile-footer">
<p class="quick-tile-name">Forza Horizon 5</p>
<p class="quick-tile-meta quick-tile-bar" style="--pct:30%">30% Progress</p>
</div>
</button>
<button class="quick-tile focusable" data-focusable="true" data-row="2" data-col="5" data-focus-key="ql-5" aria-label="Cuitfroots">
<div class="quick-tile-art" style="--ta:#101e00;--tb:#203e00;" aria-hidden="true">
<span class="tile-badge" aria-label="Trophy">🏆</span>
</div>
<div class="quick-tile-footer">
<p class="quick-tile-name">Cuitfroots</p>
<p class="quick-tile-meta quick-tile-bar" style="--pct:50%">50% Achievements</p>
</div>
</button>
</div>
</section>
<!-- Friends + System Activity -->
<div class="side-bottom-panels">
<section class="friends-panel" aria-label="Friends online">
<div class="panel-header-row">
<h3 class="panel-heading-sm">Friends Online</h3>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<div class="friends-avatars" aria-label="Online friends">
<div class="friend-avatar" style="--fc:#4fd8ff;" aria-hidden="true"></div>
<div class="friend-avatar" style="--fc:#4fff88;" aria-hidden="true"></div>
<div class="friend-avatar" style="--fc:#ff9800;" aria-hidden="true"></div>
</div>
</section>
<section class="activity-panel" aria-label="System activity">
<h3 class="panel-heading-sm">System Activity</h3>
<div class="activity-row">
<span class="activity-label">Home</span>
<span class="btn-prompt btn-y" aria-label="Y button">Y</span>
</div>
<p class="activity-hint">Press <span class="btn-prompt btn-a" aria-label="A button">A</span> to select</p>
</section>
</div>
<div class="tile-accent-bar"></div>
</button>
</section>
</aside>
</div>
</section>
`;
@@ -65,61 +229,49 @@ export const createHomeView = ({ state, renderView, openPowerMenu }) => ({
id: "home",
render: () => HOME_TEMPLATE,
mount: () => {
const rail = document.querySelector("[data-home-rail]");
const background = document.querySelector("#nebula-background");
if (!rail) {
return;
}
const view = document.querySelector("[data-view='home']");
if (!view) return;
const focusTile = (tile) => {
if (!tile) {
return;
}
const targetScroll = tile.offsetLeft - rail.clientWidth * 0.22;
rail.scrollTo({
left: Math.max(0, targetScroll),
behavior: "smooth",
// Tab switching behaviour
view.querySelectorAll(".home-tab").forEach((tab) => {
tab.addEventListener("click", () => {
view.querySelectorAll(".home-tab").forEach((t) => {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
});
const focusColumn = Number(tile.dataset.col ?? 0);
document.documentElement.style.setProperty("--nebula-focus-strength", "1");
document.documentElement.style.setProperty("--nebula-accent-line-x", `${20 + focusColumn * 112}px`);
rail.style.setProperty("--home-parallax-x", `${focusColumn * -10}px`);
background?.style.setProperty("--bg-parallax-x", `${focusColumn * -6}px`);
};
rail.addEventListener("focusin", (event) => {
const tile = event.target.closest("[data-focusable='true']");
focusTile(tile);
if (tile) {
window.dispatchEvent(
new CustomEvent("nebula-ui-hook", {
detail: { type: "focus", target: tile.dataset.target },
}),
);
}
});
},
getNavigationContract: () => {
const root = document.querySelector("[data-home-rail]");
const root = document.querySelector("[data-view='home']");
return {
focusRoot: root,
defaultFocus: root?.querySelector("[data-target='library']") ?? null,
layout: { type: "grid", cols: 4, rows: 1 },
defaultFocus: root?.querySelector("[data-focus-key='btn-continue']") ?? null,
layout: { type: "grid", cols: 6, rows: 4 },
hintsTemplate: "#global-hints-template",
nebulaNavigation: state.nebula.navigation,
useNebulaNavigation: false,
onAccept: (element) => {
if (!element) {
return;
}
if (!element) return;
const target = element.dataset.target;
const focusKey = element.dataset.focusKey;
if (target === "power") {
openPowerMenu();
return;
}
state.activeView = target;
renderView(target);
if (target) {
state.activeView = target;
renderView(target);
return;
}
// Hero action buttons
if (focusKey === "tab-now-playing" || focusKey === "tab-featured") {
element.click();
}
},
onBack: () => {
state.locked = true;