Feat: Build out server shell
This commit is contained in:
68
Frontend/src/components/shell/TopBar/ThemeToggle.module.scss
Normal file
68
Frontend/src/components/shell/TopBar/ThemeToggle.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
32
Frontend/src/components/shell/TopBar/ThemeToggle.tsx
Normal file
32
Frontend/src/components/shell/TopBar/ThemeToggle.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
100
Frontend/src/components/shell/TopBar/UserNavButton.module.scss
Normal file
100
Frontend/src/components/shell/TopBar/UserNavButton.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
19
Frontend/src/components/shell/TopBar/UserNavButton.tsx
Normal file
19
Frontend/src/components/shell/TopBar/UserNavButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user