Feat: Build out server shell

This commit is contained in:
MangoPig
2026-06-16 13:11:14 +01:00
parent 35586729ba
commit 829d7b3d8f
28 changed files with 1990 additions and 149 deletions

View File

@@ -0,0 +1,68 @@
.toggleButton {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2.75rem;
height: 2.75rem;
aspect-ratio: 1;
padding: 0;
border: 0;
border-radius: 999px;
flex-shrink: 0;
cursor: pointer;
background: transparent;
color: var(--color-text-muted);
transition:
background-color 500ms ease,
color 220ms var(--easing-standard),
transform 180ms var(--easing-standard);
}
.toggleButton:hover {
background: color-mix(in srgb, var(--color-text) 8%, transparent);
color: var(--color-text);
}
.toggleButton:focus-visible {
outline: none;
background: color-mix(in srgb, var(--color-accent-strong) 12%, transparent);
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--color-accent-strong) 28%, transparent);
color: var(--color-text);
}
.iconContainer {
position: relative;
width: 1.375rem;
height: 1.375rem;
}
.iconLayer {
position: absolute;
inset: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition:
transform 1000ms ease,
opacity 500ms ease;
}
.moonLayer {
transform: rotate(90deg);
opacity: 0;
:global([data-theme="dark"]) & {
transform: rotate(0deg);
opacity: 1;
}
}
.sunLayer {
transform: rotate(0deg);
opacity: 1;
:global([data-theme="dark"]) & {
transform: rotate(-90deg);
opacity: 0;
}
}

View File

@@ -0,0 +1,32 @@
// Path: Frontend/src/components/shell/TopBar/ThemeToggle.tsx
import type { JSX } from "solid-js";
import type { Theme } from "../../../theme/runtime";
import { Moon, Sun } from "../../../lib/icons";
import styles from "./ThemeToggle.module.scss";
type ThemeToggleProps = {
theme: Theme;
onToggle: VoidFunction;
};
export const ThemeToggle = (props: ThemeToggleProps): JSX.Element => {
return (
<button
class={styles.toggleButton}
type="button"
onClick={props.onToggle}
aria-label={`Switch to ${props.theme === "dark" ? "light" : "dark"} theme`}
title={`Switch to ${props.theme === "dark" ? "light" : "dark"} theme`}
>
<span class={styles.iconContainer} aria-hidden="true">
<span class={`${styles.iconLayer} ${styles.moonLayer}`}>
<Moon size={18} strokeWidth={2} />
</span>
<span class={`${styles.iconLayer} ${styles.sunLayer}`}>
<Sun size={18} strokeWidth={2} />
</span>
</span>
</button>
);
};

View File

@@ -1,8 +1,8 @@
.topBar {
--topbar-control-size: var(--control-size-md);
--topbar-control-size: 2.5rem;
min-height: 4rem;
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4) var(--space-3);
@@ -12,6 +12,7 @@
.identity {
min-width: 0;
display: grid;
justify-items: start;
gap: 0;
}
@@ -22,55 +23,50 @@
letter-spacing: 0.08em;
}
.title {
@include text-title;
display: flex;
align-items: center;
gap: var(--space-2);
strong {
font: inherit;
font-weight: var(--font-weight-title);
}
}
.context {
color: var(--color-text-muted);
}
.controls,
.actions {
display: flex;
align-items: center;
}
.controls {
gap: var(--space-1);
}
.actionButton,
.themeButton {
height: var(--topbar-control-size);
display: inline-flex;
align-items: center;
justify-content: center;
@include interactive-frame();
.actions {
gap: var(--space-1);
}
.actionButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--topbar-control-size);
height: var(--topbar-control-size);
aspect-ratio: 1;
padding: 0;
border: 0;
border-radius: 999px;
flex-shrink: 0;
cursor: pointer;
background: transparent;
color: var(--color-text-muted);
transition:
background-color 220ms var(--easing-standard),
color 220ms var(--easing-standard),
transform 180ms var(--easing-standard);
}
.themeButton {
width: auto;
padding-inline: var(--space-2);
gap: var(--space-1);
.actionButton:hover {
background: color-mix(in srgb, var(--color-text) 8%, transparent);
color: var(--color-text);
}
.actionButton,
.themeButton {
@include interactive-frame-hover();
}
.themeLabel {
@include text-label;
.actionButton:focus-visible {
outline: none;
background: color-mix(in srgb, var(--color-accent-strong) 12%, transparent);
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--color-accent-strong) 28%, transparent);
color: var(--color-text);
}
@include respond-down(mobile) {
@@ -82,4 +78,8 @@
.actions {
display: none;
}
.controls {
gap: 0;
}
}

View File

@@ -1,9 +1,11 @@
// Path: Frontend/src/components/shell/TopBar/TopBar.tsx
import { For, type JSX } from "solid-js";
import { ChevronDown } from "../../../lib/icons";
import type { Theme } from "../../../theme/runtime";
import { topBarActions } from "../data/shell.data";
import { DepartmentSelector } from "../DepartmentSelector/DepartmentSelector";
import { ThemeToggle } from "./ThemeToggle";
import { UserNavButton } from "./UserNavButton";
import styles from "./TopBar.module.scss";
type TopBarProps = {
@@ -16,29 +18,26 @@ export const TopBar = (props: TopBarProps): JSX.Element => {
<header class={styles.topBar}>
<div class={styles.identity}>
<span class={styles.eyebrow}>Moku Work</span>
<div class={styles.title}>
<strong>Workspace Shell</strong>
<span class={styles.context}>Moku / Product</span>
<ChevronDown size={16} strokeWidth={2} />
</div>
<DepartmentSelector />
</div>
<button class={styles.themeButton} type="button" onClick={props.onToggleTheme}>
<span class={styles.themeLabel}>{props.theme === "dark" ? "Dark" : "Light"}</span>
</button>
<div class={styles.controls}>
<div class={styles.actions}>
<For each={topBarActions}>
{(item): JSX.Element => {
const Icon = item.icon;
<div class={styles.actions}>
<For each={topBarActions}>
{(item): JSX.Element => {
const Icon = item.icon;
return (
<button class={styles.actionButton} type="button" aria-label={item.label} title={item.label}>
<Icon size={18} strokeWidth={2} />
</button>
);
}}
</For>
</div>
return (
<button class={styles.actionButton} type="button" aria-label={item.label} title={item.label}>
<Icon size={18} strokeWidth={2} />
</button>
);
}}
</For>
<ThemeToggle theme={props.theme} onToggle={props.onToggleTheme} />
<UserNavButton />
</div>
</header>
);

View File

@@ -0,0 +1,100 @@
.userButton {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
width: 2.75rem;
height: 2.75rem;
margin-left: var(--space-1);
padding: 0;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
overflow: hidden;
transition:
transform 180ms var(--easing-standard),
color 220ms var(--easing-standard);
}
.userButton:hover {
transform: scale(1.05);
color: var(--color-text);
}
.userButton:hover .spinContainer {
animation-play-state: running;
opacity: 1;
}
.userButton:focus-visible {
outline: none;
color: var(--color-text);
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--color-accent-strong) 28%, transparent);
}
.spinContainer {
position: absolute;
inset: 0;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.72;
transition: opacity 220ms var(--easing-standard);
animation: spin-reverse 1.5s ease-in-out infinite reverse;
animation-play-state: paused;
pointer-events: none;
}
.spinRing {
width: 100%;
height: 100%;
border-radius: 999px;
background:
conic-gradient(
from 0deg,
transparent 0deg 28deg,
var(--color-primary-1) 28deg 118deg,
transparent 118deg 148deg,
var(--color-primary-2) 148deg 238deg,
transparent 238deg 268deg,
var(--color-primary-3) 268deg 358deg,
transparent 358deg 360deg
);
mask: radial-gradient(circle, transparent 63%, black 66%);
-webkit-mask: radial-gradient(circle, transparent 63%, black 66%);
animation: spin-forward 14s linear infinite;
}
.userCore {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 78%;
height: 78%;
border-radius: 999px;
background: var(--color-surface-muted);
}
@keyframes spin-forward {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes spin-reverse {
from {
transform: rotate(0deg);
}
to {
transform: rotate(-360deg);
}
}

View File

@@ -0,0 +1,19 @@
// Path: Frontend/src/components/shell/TopBar/UserNavButton.tsx
import type { JSX } from "solid-js";
import { User } from "../../../lib/icons";
import styles from "./UserNavButton.module.scss";
export const UserNavButton = (): JSX.Element => {
return (
<button class={styles.userButton} type="button" aria-label="Open profile" title="Open profile">
<span class={styles.spinContainer} aria-hidden="true">
<span class={styles.spinRing} />
</span>
<span class={styles.userCore} aria-hidden="true">
<User size={16} strokeWidth={2.2} />
</span>
</button>
);
};