From 5f42c17caa17b20954f567bd8ef301b8ff88bd0f Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 24 Aug 2026 17:35:51 +0700 Subject: [PATCH] feat(frontend): tema monokrom + sidebar animasi game-menu + gate ringan mobile --- .../plans/2026-08-24_mono-gamemenu-mobile.md | 39 +++++ .../src/app/(dashboard)/dashboard/view.tsx | 2 +- services/frontend/src/app/globals.css | 150 ++++++++++++++++-- .../src/components/ambient/ambient-canvas.tsx | 13 ++ .../components/ambient/ambient-context.tsx | 11 +- .../src/components/shell/mobile-nav.tsx | 19 ++- .../src/components/shell/nav-rail.tsx | 24 ++- 7 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 .hermes/plans/2026-08-24_mono-gamemenu-mobile.md diff --git a/.hermes/plans/2026-08-24_mono-gamemenu-mobile.md b/.hermes/plans/2026-08-24_mono-gamemenu-mobile.md new file mode 100644 index 00000000..a0dc088b --- /dev/null +++ b/.hermes/plans/2026-08-24_mono-gamemenu-mobile.md @@ -0,0 +1,39 @@ +# GMW FE — Monokrom Hitam-Putih + Sidebar Ala Menu Game + Ringan di Mobile + +Tanggal: 2026-08-24 · Basis: `eda5c75` (shell usable hasil revert) + +## Tujuan +1. Tema **monokrom murni** (hitam-putih, tanpa warna) di dark & light. +2. Sidebar (desktop NavRail + mobile dock) beranimasi **ala menu game** — corner + brackets, sweep, stagger masuk, marker segitiga. +3. **Ringan di mobile**: matikan WebGL ambient di layar kecil, kurangi biaya + blur/backdrop, animasi transform/opacity saja. + +## Non-goals +- Tidak menyentuh backend, endpoint, hooks/data-flow, struktur route. +- Tidak menambah dependensi baru (CSS murni untuk semua animasi). + +## File yang disentuh +| File | Perubahan | +|---|---| +| `src/app/globals.css` | Token mono (dark+light): signal/amber/vermilion → skala putih-abu; `.glass` blur adaptif; kelas baru `.game-nav-item` (bracket ::before/::after, sweep, stagger via `--i`), `.game-frame` (panel sudut terpotong + garis tergambar), keyframes `sweep-x`, `draw-line`, `nav-in`; media query ` {/* Hero */} - +
diff --git a/services/frontend/src/app/globals.css b/services/frontend/src/app/globals.css index e37ca79e..7a138c5b 100644 --- a/services/frontend/src/app/globals.css +++ b/services/frontend/src/app/globals.css @@ -29,13 +29,13 @@ --color-hairline: oklch(1 0 0 / 0.1); --hairline-w: 1px; - /* ── Semantic signals ── */ - --color-signal: oklch(0.86 0.19 128); - --color-signal-ink: oklch(0.18 0.03 70); - --color-signal-glow: oklch(0.86 0.19 128 / 0.4); - --color-amber: oklch(0.85 0.15 72); - --color-vermilion: oklch(0.68 0.21 25); - --color-vermilion-glow: oklch(0.68 0.21 25 / 0.4); + /* ── Semantic signals (monochrome: tone = luminance steps) ── */ + --color-signal: oklch(0.97 0 0); + --color-signal-ink: oklch(0.1 0 0); + --color-signal-glow: oklch(1 0 0 / 0.35); + --color-amber: oklch(0.72 0 0); + --color-vermilion: oklch(0.85 0 0); + --color-vermilion-glow: oklch(1 0 0 / 0.45); --color-ring: var(--color-signal); @@ -74,8 +74,8 @@ background: oklch(0.22 0.02 70 / 0.34); } .light ::selection { - background: var(--color-signal-glow); - color: var(--color-ink); + background: oklch(0.2 0 0 / 0.9); + color: oklch(1 0 0); } @layer base { @@ -300,6 +300,138 @@ } } +/* ═══ Game-menu nav (monochrome) ═══ + Corner brackets + one-shot light sweep + staggered entrance. + Pure transform/opacity — compositor only. */ + +.game-nav-item { + --i: 0; + position: relative; + isolation: isolate; + animation: nav-in 0.45s cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(var(--i) * 45ms); +} +/* corner brackets on hover/active */ +.game-nav-item::before, +.game-nav-item::after { + content: ""; + position: absolute; + inset: 0; + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; +} +.game-nav-item::before { + border-left: 1.5px solid currentColor; + border-top: 1.5px solid currentColor; + border-top-left-radius: 13px; + clip-path: polygon(0 0, 55% 0, 55% 28%, 28% 28%, 28% 100%, 0 100%); +} +.game-nav-item::after { + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + border-bottom-right-radius: 13px; + clip-path: polygon(100% 100%, 45% 100%, 45% 72%, 72% 72%, 72% 0, 100% 0); +} +.game-nav-item:hover::before, +.game-nav-item:hover::after, +.game-nav-item.is-active::before, +.game-nav-item.is-active::after { + opacity: 0.9; +} +/* one-shot sweep */ +.game-nav-item .game-sweep { + position: absolute; + inset: 0; + overflow: hidden; + border-radius: inherit; + pointer-events: none; +} +.game-nav-item .game-sweep > i { + position: absolute; + top: -20%; + bottom: -20%; + width: 34%; + left: -60%; + background: linear-gradient( + 105deg, + transparent, + oklch(1 0 0 / 0.16), + transparent + ); + transform: skewX(-14deg); +} +.game-nav-item:hover .game-sweep > i, +.game-nav-item.is-active .game-sweep > i { + animation: sweep-x 0.55s ease-out both; +} +/* active triangle marker (game cursor) */ +.game-marker { + position: absolute; + top: 50%; + width: 0; + height: 0; + border-block: 5px solid transparent; + border-left: 7px solid currentColor; + transform: translateY(-50%); + filter: drop-shadow(0 0 4px var(--color-signal-glow)); +} + +@keyframes sweep-x { + from { left: -60%; } + to { left: 130%; } +} +@keyframes nav-in { + from { opacity: 0; transform: translateX(-10px); } + to { opacity: 1; transform: translateX(0); } +} + +/* ── Game frame: panel with cut corners + drawn-in edge lines ── */ +.game-frame { + position: relative; +} +.game-frame::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: + linear-gradient(var(--color-hairline), var(--color-hairline)) top left / + 26px 1.5px, + linear-gradient(var(--color-hairline), var(--color-hairline)) top left / 1.5px + 26px, + linear-gradient(var(--color-hairline), var(--color-hairline)) top right / + 26px 1.5px, + linear-gradient(var(--color-hairline), var(--color-hairline)) top right / 1.5px + 26px, + linear-gradient(var(--color-hairline), var(--color-hairline)) bottom left / + 26px 1.5px, + linear-gradient(var(--color-hairline), var(--color-hairline)) bottom left / + 1.5px 26px, + linear-gradient(var(--color-hairline), var(--color-hairline)) bottom right / + 26px 1.5px, + linear-gradient(var(--color-hairline), var(--color-hairline)) bottom right / + 1.5px 26px; + background-repeat: no-repeat; +} + +/* ── Mobile lightness ( = { - signal: [0.42, 1.0, 0.52], - amber: [1.0, 0.76, 0.28], - vermilion: [1.0, 0.34, 0.28], + signal: [0.92, 0.94, 0.96], // nominal — near-white + amber: [0.62, 0.64, 0.68], // warn — mid grey + vermilion: [0.78, 0.8, 0.84], // danger — bright flash grey }; export interface AmbientState { diff --git a/services/frontend/src/components/shell/mobile-nav.tsx b/services/frontend/src/components/shell/mobile-nav.tsx index 04ba5b4e..70a5b66b 100644 --- a/services/frontend/src/components/shell/mobile-nav.tsx +++ b/services/frontend/src/components/shell/mobile-nav.tsx @@ -24,16 +24,25 @@ export function MobileNav() { aria-label={item.label} aria-current={active ? "page" : undefined} className={cn( - "relative flex flex-1 flex-col items-center justify-center rounded-[12px] rounded-t-[20px] px-2 pb-[calc(0.4rem+env(safe-area-inset-bottom))] pt-2 md:hidden", + "game-nav-item relative flex min-h-[44px] flex-1 flex-col items-center justify-center rounded-t-[20px] px-2 pt-2 pb-[calc(0.4rem+env(safe-area-inset-bottom))] md:hidden", active - ? "bg-signal/15 text-signal" - : "text-ink-faint hover:bg-white/5 hover:text-ink-soft", + ? "is-active text-white" + : "text-ink-faint hover:text-ink-soft", )} > {active && ( - +