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

@@ -1,14 +1,17 @@
.rail {
--rail-workspace-size: var(--control-size-lg);
--rail-action-size: var(--control-size-md);
--rail-dock-clearance: 8rem;
position: relative;
z-index: 3;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-2);
overflow: hidden;
padding: var(--space-3) var(--space-2) calc(var(--space-3) + var(--rail-dock-clearance));
overflow: visible;
}
.topCluster,
@@ -20,6 +23,14 @@
gap: var(--space-2);
}
.bottomCluster {
margin-top: auto;
}
.topCluster {
gap: var(--space-3);
}
.items {
width: 100%;
min-height: 0;
@@ -28,22 +39,101 @@
flex-direction: column;
align-items: center;
gap: var(--space-2);
overflow-y: auto;
overscroll-behavior: contain;
overflow: visible;
padding-block: var(--space-1);
}
.logo {
width: var(--rail-workspace-size);
height: var(--rail-workspace-size);
.itemSlot {
position: relative;
width: 100%;
display: flex;
justify-content: center;
overflow: visible;
}
.itemSlot:hover,
.itemSlot:focus-within,
.itemSlotActive {
z-index: 12;
}
.activeIndicator {
position: absolute;
left: calc(50% - (var(--rail-workspace-size) / 2) - var(--space-2));
top: 50%;
width: 0.26rem;
height: 0.55rem;
border-radius: var(--radius-pill);
background: hsl(0 0% 100% / 0.94);
transform: translateY(-50%) scaleY(0.72);
transform-origin: center;
opacity: 0;
z-index: 2;
transition:
opacity 140ms var(--easing-standard),
height 180ms var(--easing-standard),
transform 180ms var(--easing-standard);
}
.itemSlot:hover .activeIndicator {
opacity: 1;
height: 1.1rem;
transform: translateY(-50%) scaleY(1);
}
.itemSlotActive .activeIndicator {
opacity: 1;
height: 2.1rem;
transform: translateY(-50%) scaleY(1);
}
.hoverLabel {
position: absolute;
left: calc(100% + var(--space-3));
top: 50%;
z-index: 8;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
background: var(--color-accent);
color: var(--color-accent-contrast);
font-weight: 700;
letter-spacing: -0.02em;
min-height: 2rem;
padding: 0 var(--space-3);
border: 1px solid color-mix(in srgb, var(--color-border-strong) 52%, transparent);
border-radius: var(--radius-md);
background: var(--color-surface-muted);
color: var(--color-text);
white-space: nowrap;
box-shadow: 0 12px 28px color-mix(in srgb, black 16%, transparent);
@include text-label;
pointer-events: none;
opacity: 0;
transform: translateY(-50%) translateX(calc(var(--space-2) * -1));
transition:
opacity 140ms var(--easing-standard),
transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hoverLabel::before {
content: "";
position: absolute;
top: 50%;
left: calc(var(--space-2) * -1);
width: 0.7rem;
height: 0.7rem;
border-left: 1px solid color-mix(in srgb, var(--color-border-strong) 52%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--color-border-strong) 52%, transparent);
background: var(--color-surface-muted);
transform: translateY(-50%) rotate(45deg);
}
.sectionDivider {
width: calc(var(--rail-workspace-size) - var(--space-2));
height: 1px;
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--color-border-strong) 58%, transparent);
}
.itemSlot:hover .hoverLabel {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
.workspaceButton {
@@ -55,13 +145,33 @@
@include interactive-frame(var(--color-surface-muted), var(--color-border), var(--color-text-muted), var(--radius-lg));
@include text-label;
@include interactive-frame-hover();
transition:
border-radius 180ms var(--easing-standard),
background 180ms var(--easing-standard),
color 180ms var(--easing-standard),
transform 180ms var(--easing-standard);
}
.personalButton {
background: var(--color-accent);
border-color: transparent;
color: var(--color-accent-contrast);
font-weight: 700;
letter-spacing: -0.02em;
}
.itemSlot:hover .workspaceButton,
.itemSlot:focus-within .workspaceButton {
border-radius: var(--radius-md);
transform: translateY(-1px);
}
.workspaceButtonActive {
background: var(--color-accent);
border-color: transparent;
color: var(--color-accent-contrast);
box-shadow: var(--shadow-soft);
border-radius: var(--radius-md);
box-shadow: none;
}
.addButton {

View File

@@ -2,38 +2,66 @@
import { For, type JSX } from "solid-js";
import { Plus } from "../../../lib/icons";
import { railItems } from "../data/shell.data";
import { railItems, type RailItem } from "../data/shell.data";
import styles from "./LeftRail.module.scss";
export const LeftRail = (): JSX.Element => {
type RailEntryProps = {
item: RailItem;
label: string;
abbreviation: string;
personal?: boolean;
};
const RailEntry = (props: RailEntryProps): JSX.Element => {
return (
<aside class={styles.rail} aria-label="Workspace rail">
<div
classList={{
[styles.itemSlot]: true,
[styles.itemSlotActive]: !!props.item.active,
}}
>
<span class={styles.activeIndicator} aria-hidden="true" />
<button
type="button"
classList={{
[styles.workspaceButton]: true,
[styles.workspaceButtonActive]: !!props.item.active,
[styles.personalButton]: !!props.personal,
}}
aria-label={props.label}
title={props.label}
>
{props.abbreviation}
</button>
<span class={styles.hoverLabel} role="presentation">
{props.label}
</span>
</div>
);
};
export const LeftRail = (): JSX.Element => {
const personalItem = railItems.find((item) => item.kind === "personal");
const organizationItems = railItems.filter((item) => item.kind === "organization");
return (
<aside class={styles.rail} aria-label="Server rail">
<div class={styles.topCluster}>
<div class={styles.logo} aria-hidden="true">
M
</div>
{personalItem ? <RailEntry item={personalItem} label={personalItem.label} abbreviation="M" personal /> : null}
<div class={styles.sectionDivider} aria-hidden="true" />
</div>
<div class={styles.items}>
<For each={railItems}>
{(item): JSX.Element => (
<button
type="button"
classList={{
[styles.workspaceButton]: true,
[styles.workspaceButtonActive]: !!item.active,
}}
title={item.label}
aria-label={item.label}
>
{item.abbreviation}
</button>
)}
<For each={organizationItems}>
{(item): JSX.Element => <RailEntry item={item} label={item.label} abbreviation={item.abbreviation} />}
</For>
</div>
<div class={styles.bottomCluster}>
<button type="button" class={styles.addButton} aria-label="Create workspace" title="Create workspace">
<button type="button" class={styles.addButton} aria-label="Create server" title="Create server">
<Plus size={16} strokeWidth={2} />
</button>
</div>