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:
+100
-9
@@ -9,17 +9,30 @@ const getRect = (element) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const scoreCandidate = (source, target, direction) => {
|
const scoreCandidate = (source, target, direction) => {
|
||||||
const horizontal = target.col - source.col;
|
const sourceRect = getRect(source.element);
|
||||||
const vertical = target.row - source.row;
|
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 === "up" && vertical >= -1) return Number.POSITIVE_INFINITY;
|
||||||
if (direction === "down" && vertical <= 0) return Number.POSITIVE_INFINITY;
|
if (direction === "down" && vertical <= 1) return Number.POSITIVE_INFINITY;
|
||||||
if (direction === "left" && horizontal >= 0) return Number.POSITIVE_INFINITY;
|
if (direction === "left" && horizontal >= -1) return Number.POSITIVE_INFINITY;
|
||||||
if (direction === "right" && horizontal <= 0) 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 primary = direction === "up" || direction === "down" ? Math.abs(vertical) : Math.abs(horizontal);
|
||||||
const secondary = direction === "up" || direction === "down" ? Math.abs(horizontal) : Math.abs(vertical);
|
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 = () => {
|
export const createNavigationManager = () => {
|
||||||
@@ -75,7 +88,10 @@ export const createNavigationManager = () => {
|
|||||||
return;
|
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
|
focusables = nodes
|
||||||
.map((element, index) => {
|
.map((element, index) => {
|
||||||
@@ -86,6 +102,7 @@ export const createNavigationManager = () => {
|
|||||||
row: Number(element.dataset.row ?? 0),
|
row: Number(element.dataset.row ?? 0),
|
||||||
col: Number(element.dataset.col ?? 0),
|
col: Number(element.dataset.col ?? 0),
|
||||||
key: element.dataset.focusKey ?? String(index),
|
key: element.dataset.focusKey ?? String(index),
|
||||||
|
region: element.dataset.navRegion ?? "content",
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
@@ -111,7 +128,56 @@ export const createNavigationManager = () => {
|
|||||||
return 0;
|
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) => {
|
const moveWithNebula = (direction) => {
|
||||||
|
if (contract?.useNebulaNavigation === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const picker = contract?.nebulaNavigation?.pickBestCandidate;
|
const picker = contract?.nebulaNavigation?.pickBestCandidate;
|
||||||
if (typeof picker !== "function") {
|
if (typeof picker !== "function") {
|
||||||
return null;
|
return null;
|
||||||
@@ -149,13 +215,38 @@ export const createNavigationManager = () => {
|
|||||||
return;
|
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);
|
const nebulaIndex = moveWithNebula(direction);
|
||||||
if (nebulaIndex !== null) {
|
if (nebulaIndex !== null) {
|
||||||
applyFocus(nebulaIndex);
|
applyFocus(nebulaIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = focusables[focusedIndex];
|
|
||||||
let bestIndex = focusedIndex;
|
let bestIndex = focusedIndex;
|
||||||
let bestScore = Number.POSITIVE_INFINITY;
|
let bestScore = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
|||||||
+45
-4
@@ -24,13 +24,54 @@
|
|||||||
<div class="nebula-layer fog"></div>
|
<div class="nebula-layer fog"></div>
|
||||||
<div class="nebula-layer vignette"></div>
|
<div class="nebula-layer vignette"></div>
|
||||||
</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>
|
</div>
|
||||||
<main id="app" class="app-shell"></main>
|
|
||||||
<div id="overlay-root"></div>
|
<div id="overlay-root"></div>
|
||||||
<div id="keyboard-root"></div>
|
<div id="keyboard-root"></div>
|
||||||
<footer class="app-footer" id="app-footer"></footer>
|
|
||||||
|
|
||||||
<template id="global-hints-template">
|
<template id="global-hints-template">
|
||||||
<div class="hint-row">
|
<div class="hint-row">
|
||||||
|
|||||||
+53
-3
@@ -15,6 +15,12 @@ const overlayRoot = document.querySelector("#overlay-root");
|
|||||||
const keyboardRoot = document.querySelector("#keyboard-root");
|
const keyboardRoot = document.querySelector("#keyboard-root");
|
||||||
const footer = document.querySelector("#app-footer");
|
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 state = createAppState();
|
||||||
const nav = createNavigationManager();
|
const nav = createNavigationManager();
|
||||||
const router = createRouter(appRoot);
|
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 renderView = (viewId) => {
|
||||||
const contract = router.navigate(viewId);
|
const contract = router.navigate(viewId);
|
||||||
currentViewContract = contract;
|
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nav.mount(contract);
|
state.activeView = viewId;
|
||||||
setFooterHints(contract.hintsTemplate ?? "#global-hints-template", state.glyphs);
|
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();
|
updateClockLabels();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,6 +177,15 @@ const handleAction = (action) => {
|
|||||||
focused?.classList.add("is-pressed");
|
focused?.classList.add("is-pressed");
|
||||||
window.setTimeout(() => focused?.classList.remove("is-pressed"), 180);
|
window.setTimeout(() => focused?.classList.remove("is-pressed"), 180);
|
||||||
emitUiHook("accept", { focusKey: focused?.dataset.focusKey ?? null });
|
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);
|
currentViewContract.onAccept?.(focused);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+221
-159
@@ -7,104 +7,211 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
background: var(--nebula-color-bg);
|
background: var(--nebula-color-bg);
|
||||||
color: var(--nebula-color-text);
|
color: var(--nebula-color-text);
|
||||||
font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
/* ─── Flat background: no blur layers ─── */
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr auto;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nebula-background {
|
#nebula-background {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -2;
|
z-index: -2;
|
||||||
overflow: hidden;
|
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 {
|
.nebula-layer.gradient {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 16% 12%, rgba(82, 116, 218, 0.26), transparent 48%),
|
radial-gradient(circle at 15% 10%, rgba(79, 216, 255, 0.06), transparent 38%),
|
||||||
radial-gradient(circle at 82% 76%, rgba(147, 79, 188, 0.22), transparent 46%),
|
radial-gradient(circle at 85% 80%, rgba(157, 79, 224, 0.07), transparent 38%),
|
||||||
radial-gradient(circle at 48% 88%, rgba(79, 216, 255, 0.14), transparent 38%),
|
linear-gradient(165deg, #050810 0%, #070a14 40%, #0a0d1c 70%, #0d0a1e 100%);
|
||||||
linear-gradient(135deg, #050a17 0%, #090f28 24%, #0d1435 48%, #1a1542 78%, #23173c 100%);
|
|
||||||
animation: nebulaGradientDrift 28s var(--nebula-ease-standard) infinite alternate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nebula-layer.starfield {
|
.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;
|
position: absolute;
|
||||||
inset: 80px 40px 120px;
|
inset: 0;
|
||||||
backdrop-filter: blur(calc(8px + var(--nebula-focus-strength, 0) * 6px));
|
background-image:
|
||||||
opacity: calc(0.22 + var(--nebula-focus-strength, 0) * 0.35);
|
radial-gradient(circle, rgba(255, 255, 255, 0.75) 0.6px, transparent 1.2px),
|
||||||
border-radius: 32px;
|
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:
|
transition:
|
||||||
opacity var(--nebula-duration-nav) var(--nebula-ease-console),
|
color var(--nebula-duration-fast) var(--nebula-ease-standard),
|
||||||
backdrop-filter var(--nebula-duration-nav) var(--nebula-ease-console);
|
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 {
|
.app-shell {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 24px var(--nebula-spacing-xl) 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── View transitions ─── */
|
||||||
.view {
|
.view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--nebula-spacing-lg);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(32px);
|
transform: translateX(24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view.view-entered {
|
.view.view-entered {
|
||||||
@@ -115,18 +222,37 @@ body {
|
|||||||
opacity var(--nebula-duration-nav) var(--nebula-ease-standard);
|
opacity var(--nebula-duration-nav) var(--nebula-ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-header {
|
/* ─── Shared shell elements ─── */
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell-topbar {
|
.shell-topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 64px;
|
padding: 14px var(--nebula-spacing-xl);
|
||||||
padding-bottom: var(--nebula-spacing-sm);
|
border-bottom: 1px solid var(--nebula-color-border);
|
||||||
position: relative;
|
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 {
|
.shell-topbar-content {
|
||||||
@@ -136,14 +262,8 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-brand {
|
.shell-accent-line {
|
||||||
margin: 0;
|
display: none;
|
||||||
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-status {
|
.shell-status {
|
||||||
@@ -152,56 +272,10 @@ body {
|
|||||||
gap: var(--nebula-spacing-md);
|
gap: var(--nebula-spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-time {
|
.view-header {
|
||||||
letter-spacing: 0.05em;
|
display: flex;
|
||||||
font-size: 16px;
|
align-items: center;
|
||||||
font-weight: 560;
|
justify-content: space-between;
|
||||||
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-title {
|
.view-title {
|
||||||
@@ -215,9 +289,14 @@ body {
|
|||||||
color: var(--nebula-color-muted);
|
color: var(--nebula-color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Footer hints bar ─── */
|
||||||
.app-footer {
|
.app-footer {
|
||||||
min-height: 56px;
|
flex-shrink: 0;
|
||||||
padding: 0 var(--nebula-spacing-xl) var(--nebula-spacing-md);
|
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 {
|
.hint-row {
|
||||||
@@ -229,34 +308,17 @@ body {
|
|||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: var(--nebula-spacing-xs);
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Background animations ─── */
|
||||||
@keyframes starfieldShift {
|
@keyframes starfieldShift {
|
||||||
0% {
|
0% { transform: translate3d(0, 0, 0); }
|
||||||
transform: translate3d(0, 0, 0);
|
100% { transform: translate3d(-140px, -110px, 0); }
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate3d(-140px, -110px, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fogDrift {
|
.nebula-layer.starfield {
|
||||||
0% {
|
animation: starfieldShift 60s linear infinite;
|
||||||
transform: translate3d(-2%, 1.2%, 0) scale(1.02);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate3d(2.2%, -1.4%, 0) scale(1.04);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-88
@@ -1,164 +1,157 @@
|
|||||||
|
/* ─── Panel (flat, no blur) ─── */
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--nebula-color-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);
|
border-radius: var(--nebula-radius-md);
|
||||||
padding: var(--nebula-spacing-lg);
|
padding: var(--nebula-spacing-lg);
|
||||||
box-shadow: var(--nebula-depth-shadow);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Focusable — flat neon border, no glow/blur ─── */
|
||||||
.focusable {
|
.focusable {
|
||||||
border: 1px solid transparent;
|
border: 2px solid transparent;
|
||||||
border-radius: var(--nebula-radius-md);
|
border-radius: var(--nebula-radius-md);
|
||||||
outline: none;
|
outline: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
will-change: transform, box-shadow, border-color;
|
cursor: pointer;
|
||||||
|
will-change: transform, border-color;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
transition:
|
transition:
|
||||||
transform var(--nebula-duration-nav) var(--nebula-ease-console),
|
transform var(--nebula-duration-nav) var(--nebula-ease-console),
|
||||||
border-color 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);
|
background-color var(--nebula-duration-nav) var(--nebula-ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusable.is-focused {
|
.focusable.is-focused {
|
||||||
border-color: rgba(79, 216, 255, 0.5);
|
border-color: var(--nebula-color-accent);
|
||||||
box-shadow:
|
transform: scale(1.03) translateZ(0);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusable::before {
|
.sidebar-nav-item.focusable.is-focused {
|
||||||
content: "";
|
border-top-color: transparent;
|
||||||
position: absolute;
|
border-bottom-color: transparent;
|
||||||
inset: 0;
|
border-left-color: var(--nebula-color-accent);
|
||||||
border-radius: inherit;
|
border-right-color: var(--nebula-color-purple);
|
||||||
background: radial-gradient(
|
transform: none;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusable.is-pressed {
|
.focusable.is-pressed {
|
||||||
animation: uiPressPulse var(--nebula-duration-fast) var(--nebula-ease-snap);
|
animation: uiPressPulse var(--nebula-duration-fast) var(--nebula-ease-snap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Tile ─── */
|
||||||
.tile {
|
.tile {
|
||||||
min-height: 188px;
|
min-height: 160px;
|
||||||
min-width: 320px;
|
min-width: 280px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--nebula-spacing-xs);
|
gap: var(--nebula-spacing-xs);
|
||||||
background:
|
background: var(--nebula-color-panel-alt);
|
||||||
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);
|
|
||||||
color: var(--nebula-color-text);
|
color: var(--nebula-color-text);
|
||||||
border-radius: var(--nebula-radius-md);
|
border-radius: var(--nebula-radius-md);
|
||||||
padding: var(--nebula-spacing-lg);
|
padding: var(--nebula-spacing-lg);
|
||||||
transform-origin: center;
|
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;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border: 2px solid var(--nebula-color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile::after {
|
.tile.is-focused {
|
||||||
content: "";
|
border-color: var(--nebula-color-accent);
|
||||||
position: absolute;
|
transform: scale(1.04) translateZ(0);
|
||||||
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-icon {
|
.tile-icon {
|
||||||
font-size: 42px;
|
font-size: 38px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
opacity: 0.94;
|
opacity: 0.9;
|
||||||
filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.32));
|
|
||||||
transition: transform var(--nebula-duration-nav) var(--nebula-ease-console);
|
transition: transform var(--nebula-duration-nav) var(--nebula-ease-console);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.is-focused .tile-icon {
|
.tile.is-focused .tile-icon {
|
||||||
transform: scale(1.08) translateY(-2px);
|
transform: scale(1.06) translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-label {
|
.tile-label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(22px, 2vw, 28px);
|
font-size: clamp(20px, 2vw, 26px);
|
||||||
font-weight: 720;
|
font-weight: 700;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: transform var(--nebula-duration-fast) var(--nebula-ease-console);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile.is-focused .tile-label {
|
|
||||||
transform: translateX(3px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-meta {
|
.tile-meta {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
color: var(--nebula-color-muted);
|
color: var(--nebula-color-muted);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
opacity: 0.88;
|
}
|
||||||
|
|
||||||
|
.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:
|
transition:
|
||||||
transform var(--nebula-duration-fast) var(--nebula-ease-console),
|
opacity var(--nebula-duration-nav) var(--nebula-ease-console),
|
||||||
opacity var(--nebula-duration-fast) var(--nebula-ease-console);
|
transform var(--nebula-duration-nav) var(--nebula-ease-console);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.is-focused .tile-meta {
|
.dashboard-tile.is-focused .tile-accent-bar {
|
||||||
transform: translateX(3px);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: scaleX(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.is-focused {
|
/* ─── Controller button prompts ─── */
|
||||||
transform: scale(1.06) translateZ(0);
|
.btn-prompt {
|
||||||
box-shadow:
|
display: inline-flex;
|
||||||
0 16px 40px rgba(2, 6, 18, 0.5),
|
align-items: center;
|
||||||
0 0 0 2px rgba(79, 216, 255, 0.15),
|
justify-content: center;
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
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 {
|
.button-like {
|
||||||
background: var(--nebula-color-panelAlt);
|
background: var(--nebula-color-panel-alt);
|
||||||
color: var(--nebula-color-text);
|
color: var(--nebula-color-text);
|
||||||
padding: var(--nebula-spacing-md) var(--nebula-spacing-lg);
|
padding: var(--nebula-spacing-md) var(--nebula-spacing-lg);
|
||||||
border-radius: var(--nebula-radius-sm);
|
border-radius: var(--nebula-radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Animations ─── */
|
||||||
@keyframes uiPressPulse {
|
@keyframes uiPressPulse {
|
||||||
0% {
|
0% { transform: scale(1); }
|
||||||
transform: scale(1);
|
40% { transform: scale(0.97); }
|
||||||
}
|
100% { transform: scale(1); }
|
||||||
40% {
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-30
@@ -1,41 +1,58 @@
|
|||||||
:root {
|
:root {
|
||||||
--nebula-color-bg: #050a17;
|
/* Core palette — solid, no heavy transparency */
|
||||||
--nebula-color-bg-deep: #0a1028;
|
--nebula-color-bg: #070a14;
|
||||||
--nebula-color-bg-purple: #1a1342;
|
--nebula-color-bg-deep: #050810;
|
||||||
--nebula-color-panel: rgba(18, 30, 58, 0.75);
|
--nebula-color-sidebar: #0a0c18;
|
||||||
--nebula-color-panelAlt: rgba(26, 43, 82, 0.88);
|
--nebula-color-panel: #0d1020;
|
||||||
--nebula-color-text: #f2f7ff;
|
--nebula-color-panel-alt: #111425;
|
||||||
--nebula-color-muted: #a8bdd8;
|
--nebula-color-border: rgba(255, 255, 255, 0.08);
|
||||||
--nebula-color-accent: #4fd8ff;
|
--nebula-color-border-mid: rgba(255, 255, 255, 0.14);
|
||||||
--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);
|
|
||||||
|
|
||||||
--nebula-spacing-xs: 6px;
|
--nebula-color-text: #f2f7ff;
|
||||||
--nebula-spacing-sm: 10px;
|
--nebula-color-muted: #7a8fa8;
|
||||||
--nebula-spacing-md: 16px;
|
|
||||||
--nebula-spacing-lg: 24px;
|
|
||||||
--nebula-spacing-xl: 36px;
|
|
||||||
|
|
||||||
--nebula-radius-sm: 10px;
|
/* Neon accents */
|
||||||
--nebula-radius-md: 14px;
|
--nebula-color-accent: #4fd8ff;
|
||||||
--nebula-radius-lg: 20px;
|
--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-radius-pill: 999px;
|
||||||
|
|
||||||
--nebula-type-body: 18px;
|
--nebula-type-body: 18px;
|
||||||
--nebula-type-title: 24px;
|
--nebula-type-title: 24px;
|
||||||
--nebula-type-display: 34px;
|
--nebula-type-display: 34px;
|
||||||
|
|
||||||
--nebula-ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1);
|
--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-console: cubic-bezier(0.19, 0.82, 0.18, 1);
|
||||||
--nebula-ease-snap: cubic-bezier(0.32, 0.94, 0.18, 1);
|
--nebula-ease-snap: cubic-bezier(0.32, 0.94, 0.18, 1);
|
||||||
|
|
||||||
--nebula-duration-fast: 120ms;
|
--nebula-duration-fast: 100ms;
|
||||||
--nebula-duration-nav: 180ms;
|
--nebula-duration-nav: 160ms;
|
||||||
--nebula-duration-slow: 340ms;
|
--nebula-duration-slow: 300ms;
|
||||||
|
|
||||||
--nebula-depth-shadow: 0 12px 32px rgba(2, 6, 20, 0.48);
|
/* No glow/shadow — flat */
|
||||||
--nebula-depth-shadow-focus: 0 20px 48px rgba(2, 12, 38, 0.62), 0 8px 16px rgba(2, 6, 20, 0.3);
|
--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
@@ -1,90 +1,532 @@
|
|||||||
|
/* ════════════════════════════════════════════════════════
|
||||||
|
HOME VIEW — Sci-fi dashboard layout
|
||||||
|
════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.home-view {
|
.home-view {
|
||||||
justify-content: flex-start;
|
display: flex;
|
||||||
gap: var(--nebula-spacing-xl);
|
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;
|
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;
|
display: grid;
|
||||||
gap: var(--nebula-spacing-xs);
|
grid-template-columns: 1fr 1fr;
|
||||||
padding-left: var(--nebula-spacing-xl);
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-hero .muted {
|
.friends-panel,
|
||||||
margin: 0;
|
.activity-panel {
|
||||||
text-transform: uppercase;
|
background: var(--nebula-color-panel);
|
||||||
letter-spacing: 0.14em;
|
border: 1px solid var(--nebula-color-border);
|
||||||
font-size: 14px;
|
border-radius: var(--nebula-radius-md);
|
||||||
font-weight: 600;
|
padding: 12px;
|
||||||
opacity: 0.72;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-hero .view-title {
|
.friends-avatars {
|
||||||
font-size: clamp(32px, 3.2vw, 44px);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-rail {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--nebula-spacing-lg);
|
gap: 6px;
|
||||||
overflow-x: auto;
|
flex-wrap: wrap;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-rail::-webkit-scrollbar {
|
.friend-avatar {
|
||||||
display: none;
|
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 {
|
.activity-row {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
margin-bottom: 4px;
|
||||||
gap: var(--nebula-spacing-md);
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-text {
|
.activity-label {
|
||||||
display: flex;
|
font-size: 17px;
|
||||||
flex-direction: column;
|
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;
|
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
@@ -1,63 +1,227 @@
|
|||||||
const HOME_TEMPLATE = `
|
const HOME_TEMPLATE = `
|
||||||
<section class="view home-view" data-view="home">
|
<section class="view home-view" data-view="home">
|
||||||
<header class="shell-topbar">
|
|
||||||
<div class="shell-topbar-content">
|
<!-- ── Top status bar ─────────────────────────────── -->
|
||||||
<p class="shell-brand">Nebula OS</p>
|
<header class="home-topbar">
|
||||||
<div class="shell-status">
|
<span class="home-time" data-clock>--:--</span>
|
||||||
<span class="shell-avatar" aria-hidden="true"></span>
|
<div class="home-status-icons" aria-label="System status">
|
||||||
<p class="shell-time" data-clock>--:--</p>
|
<span class="home-status-icon" title="Wi-Fi">
|
||||||
</div>
|
<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>
|
||||||
<div class="shell-accent-line"></div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="home-hero">
|
<!-- ── Main body: center + right panel ───────────── -->
|
||||||
<p class="muted">Dashboard</p>
|
<div class="home-body">
|
||||||
<h1 class="view-title">Jump back in</h1>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="tile-rail" data-focus-root data-home-rail>
|
<!-- ── Center column ──────────────────────────── -->
|
||||||
<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="home-center">
|
||||||
<div class="tile-content">
|
|
||||||
<span class="tile-icon" aria-hidden="true">📚</span>
|
<!-- Category tabs -->
|
||||||
<div class="tile-text">
|
<nav class="home-tabs" aria-label="Content categories">
|
||||||
<p class="tile-label">Library</p>
|
<button
|
||||||
<p class="tile-meta">Your games & apps</p>
|
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>
|
|
||||||
<div class="tile-accent-bar"></div>
|
<!-- Gradient overlay + info -->
|
||||||
</button>
|
<div class="hero-overlay">
|
||||||
<button class="focusable tile dashboard-tile" data-focusable="true" data-row="0" data-col="1" data-target="browser" data-focus-key="browser">
|
<div class="hero-info">
|
||||||
<div class="tile-content">
|
<h1 class="hero-game-title">Cyberpunk 2077</h1>
|
||||||
<span class="tile-icon" aria-hidden="true">🌐</span>
|
<div class="hero-actions">
|
||||||
<div class="tile-text">
|
<button
|
||||||
<p class="tile-label">Browser</p>
|
class="hero-btn hero-btn-primary focusable"
|
||||||
<p class="tile-meta">Explore the web</p>
|
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>
|
</article>
|
||||||
<div class="tile-accent-bar"></div>
|
|
||||||
</button>
|
<!-- Featured strip -->
|
||||||
<button class="focusable tile dashboard-tile" data-focusable="true" data-row="0" data-col="2" data-target="settings" data-focus-key="settings">
|
<section class="featured-strip" aria-label="Featured games">
|
||||||
<div class="tile-content">
|
<h2 class="section-label">Featured</h2>
|
||||||
<span class="tile-icon" aria-hidden="true">⚙️</span>
|
<div class="featured-row">
|
||||||
<div class="tile-text">
|
<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>
|
||||||
<p class="tile-label">Settings</p>
|
<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>
|
||||||
<p class="tile-meta">System configuration</p>
|
<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>
|
</section>
|
||||||
<div class="tile-accent-bar"></div>
|
</div>
|
||||||
</button>
|
|
||||||
<button class="focusable tile dashboard-tile" data-focusable="true" data-row="0" data-col="3" data-target="power" data-focus-key="power">
|
<!-- ── Right panel ─────────────────────────────── -->
|
||||||
<div class="tile-content">
|
<aside class="home-right-panel" aria-label="Quick launch and activity">
|
||||||
<span class="tile-icon" aria-hidden="true">⏻</span>
|
|
||||||
<div class="tile-text">
|
<!-- Quick Launch grid -->
|
||||||
<p class="tile-label">Power</p>
|
<section class="quick-launch" aria-label="Quick launch">
|
||||||
<p class="tile-meta">Sleep, restart, shut down</p>
|
<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>
|
||||||
|
<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>
|
||||||
<div class="tile-accent-bar"></div>
|
|
||||||
</button>
|
</aside>
|
||||||
</section>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -65,61 +229,49 @@ export const createHomeView = ({ state, renderView, openPowerMenu }) => ({
|
|||||||
id: "home",
|
id: "home",
|
||||||
render: () => HOME_TEMPLATE,
|
render: () => HOME_TEMPLATE,
|
||||||
mount: () => {
|
mount: () => {
|
||||||
const rail = document.querySelector("[data-home-rail]");
|
const view = document.querySelector("[data-view='home']");
|
||||||
const background = document.querySelector("#nebula-background");
|
if (!view) return;
|
||||||
if (!rail) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusTile = (tile) => {
|
// Tab switching behaviour
|
||||||
if (!tile) {
|
view.querySelectorAll(".home-tab").forEach((tab) => {
|
||||||
return;
|
tab.addEventListener("click", () => {
|
||||||
}
|
view.querySelectorAll(".home-tab").forEach((t) => {
|
||||||
|
t.classList.remove("is-active");
|
||||||
const targetScroll = tile.offsetLeft - rail.clientWidth * 0.22;
|
t.setAttribute("aria-selected", "false");
|
||||||
rail.scrollTo({
|
});
|
||||||
left: Math.max(0, targetScroll),
|
tab.classList.add("is-active");
|
||||||
behavior: "smooth",
|
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: () => {
|
getNavigationContract: () => {
|
||||||
const root = document.querySelector("[data-home-rail]");
|
const root = document.querySelector("[data-view='home']");
|
||||||
return {
|
return {
|
||||||
focusRoot: root,
|
focusRoot: root,
|
||||||
defaultFocus: root?.querySelector("[data-target='library']") ?? null,
|
defaultFocus: root?.querySelector("[data-focus-key='btn-continue']") ?? null,
|
||||||
layout: { type: "grid", cols: 4, rows: 1 },
|
layout: { type: "grid", cols: 6, rows: 4 },
|
||||||
hintsTemplate: "#global-hints-template",
|
hintsTemplate: "#global-hints-template",
|
||||||
nebulaNavigation: state.nebula.navigation,
|
nebulaNavigation: state.nebula.navigation,
|
||||||
|
useNebulaNavigation: false,
|
||||||
onAccept: (element) => {
|
onAccept: (element) => {
|
||||||
if (!element) {
|
if (!element) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = element.dataset.target;
|
const target = element.dataset.target;
|
||||||
|
const focusKey = element.dataset.focusKey;
|
||||||
|
|
||||||
if (target === "power") {
|
if (target === "power") {
|
||||||
openPowerMenu();
|
openPowerMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.activeView = target;
|
if (target) {
|
||||||
renderView(target);
|
state.activeView = target;
|
||||||
|
renderView(target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hero action buttons
|
||||||
|
if (focusKey === "tab-now-playing" || focusKey === "tab-featured") {
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onBack: () => {
|
onBack: () => {
|
||||||
state.locked = true;
|
state.locked = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user