diff --git a/Documentation/STYLING-THEME-SAMPLE.json b/Documentation/STYLING-THEME-SAMPLE.json new file mode 100644 index 0000000..194b895 --- /dev/null +++ b/Documentation/STYLING-THEME-SAMPLE.json @@ -0,0 +1,177 @@ +{ + "schemaVersion": "1.0.0", + "id": "moku-styling-sample", + "name": "Moku Styling Sample", + "description": "Sample theme showing the background, surface, border, accent, and primary tokens documented in Documentation/STYLING.md.", + "author": "Moku Work", + "tokens": { + "shared": { + "palette": { + "gray": { + "0": "hsl(210 20% 99%)", + "50": "hsl(220 20% 97%)", + "100": "hsl(220 16% 93%)", + "200": "hsl(220 13% 87%)", + "300": "hsl(220 11% 75%)", + "400": "hsl(220 9% 58%)", + "500": "hsl(220 10% 45%)", + "600": "hsl(220 14% 34%)", + "700": "hsl(220 18% 24%)", + "800": "hsl(220 22% 16%)", + "900": "hsl(220 28% 10%)" + }, + "blue": { + "400": "hsl(218 88% 61%)", + "500": "hsl(221 83% 53%)", + "600": "hsl(224 76% 48%)" + }, + "green": { + "500": "hsl(154 60% 40%)" + }, + "red": { + "500": "hsl(0 72% 54%)" + }, + "amber": { + "500": "hsl(36 100% 50%)" + } + }, + "space": { + "1": "0.25rem", + "2": "0.5rem", + "3": "0.75rem", + "4": "1rem", + "5": "1.25rem", + "6": "1.5rem", + "8": "2rem", + "10": "2.5rem", + "12": "3rem" + }, + "radius": { + "sm": "0.375rem", + "md": "0.625rem", + "lg": "0.875rem", + "xl": "1.25rem", + "pill": "999px" + }, + "size": { + "controlMd": "2.25rem", + "controlLg": "2.5rem", + "contentWidthWide": "72rem", + "blurOverlay": "18px" + }, + "shadow": { + "soft": "0 12px 32px hsl(220 30% 10% / 0.08)", + "strong": "0 20px 48px hsl(220 30% 10% / 0.16)" + }, + "zIndex": { + "base": "1", + "dropdown": "100", + "sticky": "200", + "overlay": "400", + "modal": "500", + "toast": "600" + }, + "motion": { + "durationFast": "140ms", + "durationBase": "220ms", + "durationSlow": "320ms", + "easeStandard": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "typography": { + "fontFamily": { + "sans": "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif", + "heading": "\"Avenir Next\", \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif", + "display": "\"Avenir Next\", \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif", + "serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "mono": "ui-monospace, \"SF Mono\", \"SFMono-Regular\", Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace" + }, + "fontSize": { + "caption": "0.75rem", + "label": "0.875rem", + "body": "1rem", + "title": "clamp(1.125rem, 1.05rem + 0.3vw, 1.25rem)", + "heading": "clamp(1.5rem, 1.2rem + 1vw, 2.125rem)", + "display": "clamp(2.25rem, 1.7rem + 2.2vw, 3.75rem)" + }, + "lineHeight": { + "caption": "1.4", + "label": "1.35", + "body": "1.55", + "title": "1.3", + "heading": "1.15", + "display": "1.05" + }, + "fontWeight": { + "caption": "500", + "label": "600", + "body": "400", + "title": "600", + "heading": "600", + "display": "700" + }, + "letterSpacing": { + "caption": "0.01em", + "label": "0.005em", + "body": "0", + "title": "-0.01em", + "heading": "-0.02em", + "display": "-0.03em" + } + } + }, + "modes": { + "light": { + "colorScheme": "light", + "colors": { + "canvas": "var(--gray-50)", + "surface": "hsl(0 0% 100% / 0.9)", + "surfaceMuted": "var(--gray-0)", + "surfaceHover": "var(--gray-100)", + "border": "hsl(220 15% 85% / 0.9)", + "borderStrong": "hsl(220 12% 70% / 0.9)", + "text": "var(--gray-800)", + "textMuted": "var(--gray-500)", + "accent": "var(--blue-500)", + "accentStrong": "var(--blue-600)", + "accentSoft": "hsl(218 88% 61% / 0.12)", + "accentContrast": "hsl(0 0% 100%)", + "primaryOne": "var(--blue-500)", + "primaryTwo": "hsl(271 72% 60%)", + "primaryThree": "hsl(192 76% 48%)", + "success": "var(--green-500)", + "danger": "var(--red-500)", + "warning": "var(--amber-500)", + "focusRing": "hsl(221 83% 53% / 0.55)" + } + }, + "dark": { + "colorScheme": "dark", + "colors": { + "canvas": "var(--gray-900)", + "surface": "hsl(220 23% 14% / 0.92)", + "surfaceMuted": "hsl(220 22% 12% / 0.96)", + "surfaceHover": "hsl(220 18% 20% / 0.96)", + "border": "hsl(220 12% 26% / 0.9)", + "borderStrong": "hsl(220 12% 38% / 0.9)", + "text": "hsl(210 20% 96%)", + "textMuted": "hsl(220 12% 70%)", + "accent": "hsl(217 91% 67%)", + "accentStrong": "hsl(218 88% 61%)", + "accentSoft": "hsl(217 91% 67% / 0.18)", + "accentContrast": "hsl(220 28% 10%)", + "primaryOne": "hsl(217 91% 67%)", + "primaryTwo": "hsl(272 80% 70%)", + "primaryThree": "hsl(190 84% 62%)", + "success": "hsl(154 55% 48%)", + "danger": "hsl(0 72% 62%)", + "warning": "hsl(36 100% 60%)", + "focusRing": "hsl(217 91% 67% / 0.65)" + }, + "shadow": { + "soft": "0 16px 40px hsl(220 40% 3% / 0.45)", + "strong": "0 24px 60px hsl(220 40% 3% / 0.55)" + } + } + } + } +} diff --git a/Documentation/STYLING.md b/Documentation/STYLING.md new file mode 100644 index 0000000..a4c7a51 --- /dev/null +++ b/Documentation/STYLING.md @@ -0,0 +1,327 @@ +# Styling Reference + +This document explains which theme tokens control the main backgrounds, surfaces, +borders, and shell gradients in Moku Work. + +It is focused on the current frontend shell scaffold so future visual tuning can +be done intentionally instead of by guesswork. + +## Source Of Truth + +There are two places to look when changing styling tokens: + +- Runtime theme payload: + - `Frontend/public/themes/moku-default.json` +- SCSS fallback defaults: + - `Frontend/src/styles/themes/_light.scss` + - `Frontend/src/styles/themes/_dark.scss` +- Full sample theme file: + - `Documentation/STYLING-THEME-SAMPLE.json` + +If you want to change the actual themed values used by the app, update the theme +JSON first. The SCSS files act as fallback/default variables. + +--- + +## Core Surface Tokens + +These are the main tokens currently driving shell backgrounds and card surfaces: + +- `--color-canvas` + - outer page background +- `--color-surface` + - standard panel/card/topbar surface +- `--color-surface-muted` + - quieter secondary panels and dropdown surfaces +- `--color-surface-hover` + - hover state for solid surface rows +- `--color-border` + - standard card/panel border +- `--color-border-strong` + - stronger shell edges, separators, and emphasized borders +- `--color-text` + - primary text color +- `--color-text-muted` + - secondary/meta text color +- `--color-accent` + - primary accent lane +- `--color-accent-strong` + - stronger accent emphasis +- `--color-accent-soft` + - soft accent wash for subtle fills + +There are also ring-only multi-primary tokens currently used for the top-right +profile ring: + +- `--color-primary-1` +- `--color-primary-2` +- `--color-primary-3` + +--- + +## Light Mode Values + +Current light-mode surface values: + +```text +--color-canvas: var(--gray-50) +--color-surface: hsl(0 0% 100% / 0.9) +--color-surface-muted: var(--gray-0) +--color-surface-hover: var(--gray-100) +--color-border: hsl(220 15% 85% / 0.9) +--color-border-strong: hsl(220 12% 70% / 0.9) +--color-text: var(--gray-800) +--color-text-muted: var(--gray-500) +--color-accent: var(--blue-500) +--color-accent-strong: var(--blue-600) +--color-accent-soft: hsl(218 88% 61% / 0.12) +--color-primary-1: var(--blue-500) +--color-primary-2: hsl(271 72% 60%) +--color-primary-3: hsl(192 76% 48%) +``` + +## Dark Mode Values + +Current dark-mode surface values: + +```text +--color-canvas: var(--gray-900) +--color-surface: hsl(220 23% 14% / 0.92) +--color-surface-muted: hsl(220 22% 12% / 0.96) +--color-surface-hover: hsl(220 18% 20% / 0.96) +--color-border: hsl(220 12% 26% / 0.9) +--color-border-strong: hsl(220 12% 38% / 0.9) +--color-text: hsl(210 20% 96%) +--color-text-muted: hsl(220 12% 70%) +--color-accent: hsl(217 91% 67%) +--color-accent-strong: hsl(218 88% 61%) +--color-accent-soft: hsl(217 91% 67% / 0.18) +--color-primary-1: hsl(217 91% 67%) +--color-primary-2: hsl(272 80% 70%) +--color-primary-3: hsl(190 84% 62%) +``` + +--- + +## What Controls What + +### 1. App Background + +File: + +- `Frontend/src/components/shell/AppShell/AppShell.module.scss` + +The outer app frame uses: + +```scss +background: var(--color-canvas); +color: var(--color-text); +``` + +So if you want to change the overall page backdrop, change `--color-canvas`. + +### 2. Main Shell Split Background + +File: + +- `Frontend/src/components/shell/AppShell/AppShell.module.scss` + +The shell derives two important internal surface blends: + +```text +--sidebar-panel-surface: color-mix(in srgb, var(--color-surface-muted) 92%, transparent) +--workspace-panel-surface: color-mix(in srgb, var(--color-canvas) 94%, var(--color-surface)) +``` + +It also derives frame/separator borders: + +```text +--shell-frame-border: color-mix(in srgb, var(--color-border-strong) 44%, transparent) +--shell-divider-border: color-mix(in srgb, var(--color-border-strong) 34%, transparent) +``` + +Surface usage inside the shell: + +- `.body` → `var(--color-surface)` +- `.railColumn` → `var(--color-surface)` +- `.sidebarColumn` → `var(--sidebar-panel-surface)` +- `.workspaceMain` → `var(--workspace-panel-surface)` + +The major left/right shell background is drawn by `workspaceRegion::before` using a +horizontal gradient from sidebar surface to workspace surface. + +### 3. Standard Content Cards + +File: + +- `Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.module.scss` + +Cards currently use: + +```text +background: var(--color-surface) +border: 1px solid var(--color-border) +box-shadow: var(--shadow-soft) +``` + +If cards feel too flat or too strong, start by adjusting: + +- `--color-surface` +- `--color-border` + +### 4. Top Bar + +File: + +- `Frontend/src/components/shell/TopBar/TopBar.module.scss` + +The top bar itself uses: + +```text +background: var(--color-surface) +``` + +Hover/focus states for top-right controls use transparent mixes based on: + +- `--color-text` +- `--color-accent-strong` + +So the bar is mostly controlled by `--color-surface`, while the interactive polish +comes from text/accent tokens. + +### 5. Server Dock + +File: + +- `Frontend/src/components/shell/ServerDock/ServerDock.module.scss` + +The dock uses two derived tokens: + +```text +--server-dock-border: color-mix(in srgb, var(--color-border-strong) 75%, transparent) +--server-dock-surface: color-mix(in srgb, var(--color-surface) 94%, transparent) +``` + +That means the dock is visually tied most strongly to: + +- `--color-surface` +- `--color-border-strong` + +The server glyph fill uses a soft accent wash derived from: + +- `--color-accent-soft` + +### 6. Project Drawer + +File: + +- `Frontend/src/components/shell/ProjectSelector/ProjectSelector.module.scss` + +Important layers: + +- scrim: + - `color-mix(in srgb, black 8%, transparent)` +- drawer panel surface: + - defined in `.drawer::before` + - vertical gradient from `--color-surface` to `--color-surface-muted` +- current-project summary block: + - `color-mix(in srgb, var(--color-surface) 72%, transparent)` +- menu row hover: + - based on `--color-surface-hover` +- menu row active: + - based on `--color-surface` + +So the drawer’s look is mainly shaped by: + +- `--color-surface` +- `--color-surface-muted` +- `--color-surface-hover` +- `--color-border-strong` + +### 7. Department Selector Dropdown + +File: + +- `Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss` + +The department dropdown is intentionally solid, not blurred. + +It uses: + +```text +.menu background: var(--color-surface-muted) +.menu border: 1px solid var(--color-border-strong) +.menuItem background: var(--color-surface-muted) +.submenuItem background: var(--color-surface-muted) +hover/active rows: var(--color-surface) +``` + +If the department menu feels too heavy or too subtle, start by adjusting: + +- `--color-surface-muted` +- `--color-surface` +- `--color-border-strong` + +--- + +## Quick Tuning Guide + +If you want to change the overall visual mood quickly, these are the highest-leverage tokens: + +### Make the app feel lighter / airier + +Adjust: + +- `--color-canvas` +- `--color-surface` +- `--color-surface-muted` + +### Make shells/cards feel more separated + +Adjust: + +- `--color-border` +- `--color-border-strong` +- `--color-surface-muted` + +### Make accent washes more or less noticeable + +Adjust: + +- `--color-accent-soft` + +### Change the visual personality of the profile ring + +Adjust: + +- `--color-primary-1` +- `--color-primary-2` +- `--color-primary-3` + +--- + +## Practical Rule Of Thumb + +Use this mental model: + +```text +canvas = app/page background +surface = primary panel or card +surface-muted = quieter secondary panel +surface-hover = solid hover state +border = normal edge +border-strong = stronger shell edge or divider +accent-soft = subtle tinted wash +primary-1/2/3 = decorative multi-color accents +``` + +If you are unsure where to start, tune these in this order: + +1. `--color-canvas` +2. `--color-surface` +3. `--color-surface-muted` +4. `--color-border` +5. `--color-border-strong` +6. `--color-accent-soft` + +That usually gives the biggest visual shift with the fewest unintended side effects. diff --git a/Frontend/public/themes/moku-default.json b/Frontend/public/themes/moku-default.json index 3a21476..a0aea32 100644 --- a/Frontend/public/themes/moku-default.json +++ b/Frontend/public/themes/moku-default.json @@ -134,6 +134,9 @@ "accentStrong": "var(--blue-600)", "accentSoft": "hsl(218 88% 61% / 0.12)", "accentContrast": "hsl(0 0% 100%)", + "primaryOne": "var(--blue-500)", + "primaryTwo": "hsl(271 72% 60%)", + "primaryThree": "hsl(192 76% 48%)", "success": "var(--green-500)", "danger": "var(--red-500)", "warning": "var(--amber-500)", @@ -155,6 +158,9 @@ "accentStrong": "hsl(218 88% 61%)", "accentSoft": "hsl(217 91% 67% / 0.18)", "accentContrast": "hsl(220 28% 10%)", + "primaryOne": "hsl(217 91% 67%)", + "primaryTwo": "hsl(272 80% 70%)", + "primaryThree": "hsl(190 84% 62%)", "success": "hsl(154 55% 48%)", "danger": "hsl(0 72% 62%)", "warning": "hsl(36 100% 60%)", diff --git a/Frontend/public/themes/moku-midnight.json b/Frontend/public/themes/moku-midnight.json new file mode 100644 index 0000000..8bd2561 --- /dev/null +++ b/Frontend/public/themes/moku-midnight.json @@ -0,0 +1,177 @@ +{ + "schemaVersion": "1.0.0", + "id": "moku-midnight", + "name": "Moku Midnight", + "description": "A warm, low-light Moku theme inspired by the mood and palette direction of refact0r's Midnight Discord theme, adapted to Moku's token schema.", + "author": "Moku Work", + "tokens": { + "shared": { + "palette": { + "gray": { + "0": "#f9f5d7", + "50": "#fbf1c7", + "100": "#ebdbb2", + "200": "#d5c4a1", + "300": "#bdae93", + "400": "#a89984", + "500": "#928374", + "600": "#7c6f64", + "700": "#665c54", + "800": "#3c3836", + "900": "#282828" + }, + "blue": { + "400": "hsl(167 24% 68%)", + "500": "#7caea3", + "600": "hsl(167 24% 48%)" + }, + "green": { + "500": "#a8b665" + }, + "red": { + "500": "#ea6962" + }, + "amber": { + "500": "#d8a656" + } + }, + "space": { + "1": "0.25rem", + "2": "0.5rem", + "3": "0.75rem", + "4": "1rem", + "5": "1.25rem", + "6": "1.5rem", + "8": "2rem", + "10": "2.5rem", + "12": "3rem" + }, + "radius": { + "sm": "0.375rem", + "md": "0.625rem", + "lg": "0.875rem", + "xl": "1.25rem", + "pill": "999px" + }, + "size": { + "controlMd": "2.25rem", + "controlLg": "2.5rem", + "contentWidthWide": "72rem", + "blurOverlay": "18px" + }, + "shadow": { + "soft": "0 12px 28px hsl(28 16% 12% / 0.08)", + "strong": "0 18px 40px hsl(28 18% 10% / 0.14)" + }, + "zIndex": { + "base": "1", + "dropdown": "100", + "sticky": "200", + "overlay": "400", + "modal": "500", + "toast": "600" + }, + "motion": { + "durationFast": "140ms", + "durationBase": "220ms", + "durationSlow": "320ms", + "easeStandard": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "typography": { + "fontFamily": { + "sans": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif", + "heading": "Inter, \"Avenir Next\", \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif", + "display": "Inter, \"Avenir Next\", \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif", + "serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "mono": "ui-monospace, \"SF Mono\", \"SFMono-Regular\", Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace" + }, + "fontSize": { + "caption": "0.75rem", + "label": "0.875rem", + "body": "1rem", + "title": "clamp(1.125rem, 1.05rem + 0.3vw, 1.25rem)", + "heading": "clamp(1.5rem, 1.2rem + 1vw, 2.125rem)", + "display": "clamp(2.25rem, 1.7rem + 2.2vw, 3.75rem)" + }, + "lineHeight": { + "caption": "1.4", + "label": "1.35", + "body": "1.55", + "title": "1.3", + "heading": "1.15", + "display": "1.05" + }, + "fontWeight": { + "caption": "500", + "label": "600", + "body": "400", + "title": "600", + "heading": "600", + "display": "700" + }, + "letterSpacing": { + "caption": "0.01em", + "label": "0.005em", + "body": "0", + "title": "-0.01em", + "heading": "-0.02em", + "display": "-0.03em" + } + } + }, + "modes": { + "light": { + "colorScheme": "light", + "colors": { + "canvas": "hsl(38 24% 97%)", + "surface": "hsl(36 22% 99% / 0.94)", + "surfaceMuted": "hsl(36 20% 96%)", + "surfaceHover": "hsl(34 18% 93%)", + "border": "hsl(30 14% 76% / 0.72)", + "borderStrong": "hsl(28 16% 60% / 0.82)", + "text": "hsl(22 16% 22%)", + "textMuted": "hsl(28 10% 42%)", + "accent": "#d3869b", + "accentStrong": "hsl(344 47% 56%)", + "accentSoft": "hsl(344 47% 70% / 0.12)", + "accentContrast": "var(--gray-0)", + "primaryOne": "#7caea3", + "primaryTwo": "#d3869b", + "primaryThree": "#d8a656", + "success": "#a8b665", + "danger": "#ea6962", + "warning": "#d8a656", + "focusRing": "hsl(344 47% 56% / 0.28)" + } + }, + "dark": { + "colorScheme": "dark", + "colors": { + "canvas": "var(--gray-900)", + "surface": "hsl(20 8% 16% / 0.94)", + "surfaceMuted": "var(--gray-800)", + "surfaceHover": "hsl(22 9% 24% / 0.96)", + "border": "hsl(20 10% 30% / 0.72)", + "borderStrong": "hsl(30 14% 55% / 0.62)", + "text": "#d4be98", + "textMuted": "#a79a83", + "accent": "#d3869b", + "accentStrong": "hsl(344 47% 63%)", + "accentSoft": "hsl(344 47% 63% / 0.18)", + "accentContrast": "var(--gray-900)", + "primaryOne": "#7caea3", + "primaryTwo": "#d3869b", + "primaryThree": "#d8a656", + "success": "#a8b665", + "danger": "#ea6962", + "warning": "#d8a656", + "focusRing": "hsl(344 47% 63% / 0.45)" + }, + "shadow": { + "soft": "0 14px 32px hsl(20 16% 3% / 0.28)", + "strong": "0 20px 48px hsl(20 16% 2% / 0.38)" + } + } + } + } +} diff --git a/Frontend/src/components/shell/AppShell/AppShell.module.scss b/Frontend/src/components/shell/AppShell/AppShell.module.scss index 4c1ac03..54c29c1 100644 --- a/Frontend/src/components/shell/AppShell/AppShell.module.scss +++ b/Frontend/src/components/shell/AppShell/AppShell.module.scss @@ -15,6 +15,7 @@ --shell-divider-border: color-mix(in srgb, var(--color-border-strong) 34%, transparent); --sidebar-panel-surface: color-mix(in srgb, var(--color-surface-muted) 92%, transparent); --workspace-panel-surface: color-mix(in srgb, var(--color-canvas) 94%, var(--color-surface)); + position: relative; min-height: 0; display: grid; grid-template-columns: var(--rail-width) minmax(0, 1fr); @@ -26,7 +27,9 @@ min-height: 0; display: flex; position: relative; - z-index: 1; + z-index: 6; + isolation: isolate; + overflow: visible; background: var(--color-surface); } @@ -92,9 +95,10 @@ .sidebarDock { position: absolute; - right: var(--space-1); bottom: var(--space-3); - left: calc(var(--space-1) - (var(--rail-width) * 0.9)); + left: calc(var(--space-1) + (var(--rail-width) * 0.1)); + width: calc(var(--sidebar-width) + (var(--rail-width) * 0.9) - var(--space-2)); + right: auto; z-index: calc(var(--z-modal) + 1); pointer-events: none; diff --git a/Frontend/src/components/shell/AppShell/AppShell.tsx b/Frontend/src/components/shell/AppShell/AppShell.tsx index e57fb8f..cd17542 100644 --- a/Frontend/src/components/shell/AppShell/AppShell.tsx +++ b/Frontend/src/components/shell/AppShell/AppShell.tsx @@ -26,24 +26,28 @@ export const AppShell = (): JSX.Element => { return (
+
+ {/* Left server rail */}
+ {/* Sidebar + main workspace frame */}
- -
- -
+ + {/* Floating server dock overlay */} +
+ +
); diff --git a/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss new file mode 100644 index 0000000..36927a9 --- /dev/null +++ b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss @@ -0,0 +1,185 @@ +.root { + position: relative; + min-width: 0; +} + +.selector { + min-width: 0; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: var(--space-2); + border: 0; + background: transparent; + color: var(--color-text); + text-align: left; + transition: + background-color 180ms var(--easing-standard), + color 180ms var(--easing-standard); +} + +.selectorOpen { + .meta, + .icon { + color: var(--color-text-subtle); + } +} + +.selector:hover { + .value { + color: var(--color-text); + } + + .meta, + .icon { + color: var(--color-text-subtle); + } +} + +.selector:focus-visible { + outline: none; + border-radius: var(--radius-sm); + box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--color-accent-soft) 14%, transparent); +} + +.value { + @include text-title; + color: var(--color-text); + font-weight: var(--font-weight-title); +} + +.meta { + color: var(--color-text-muted); +} + +.icon { + flex: 0 0 auto; + color: var(--color-text-muted); + transition: transform 180ms var(--easing-standard), color 180ms var(--easing-standard); +} + +.iconOpen { + transform: rotate(180deg); +} + +.menu { + position: absolute; + top: calc(100% + var(--space-2)); + left: 0; + min-width: min(18rem, calc(100vw - (var(--space-4) * 2))); + display: grid; + gap: var(--space-2); + padding: var(--space-2); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + background: var(--color-surface-muted); + box-shadow: 0 16px 32px color-mix(in srgb, black 18%, transparent); + z-index: 20; +} + +.menuSection { + display: grid; + gap: 0.15rem; +} + +.menuSectionLabel { + @include text-caption; + display: block; + color: var(--color-text-muted); + letter-spacing: 0.05em; + text-transform: uppercase; + padding-inline: var(--space-1); + margin-bottom: var(--space-2); +} + +.menuDivider { + height: 1px; + background: var(--color-border); +} + +.menuItem { + min-width: 0; + min-height: 2.75rem; + display: flex; + align-items: center; + width: 100%; + padding: var(--space-2) var(--space-2); + border: 1px solid transparent; + border-radius: var(--radius-sm); + background: var(--color-surface-muted); + color: var(--color-text); + text-align: left; + transition: + background-color 160ms var(--easing-standard), + border-color 160ms var(--easing-standard); +} + +.menuItem:hover { + border-color: var(--color-border); + background: var(--color-surface); +} + +.menuItemActive { + border-color: var(--color-accent-soft); + background: var(--color-surface); +} + +.menuItemCopy { + min-width: 0; + display: grid; + gap: 0; +} + +.menuItemValue { + @include text-label; + color: var(--color-text); + font-weight: var(--font-weight-semibold); +} + +.menuItemMeta { + @include text-caption; + color: var(--color-text-muted); +} + +.submenuItem { + min-width: 0; + min-height: 2.5rem; + display: flex; + align-items: center; + width: 100%; + gap: var(--space-2); + padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3); + border: 1px solid transparent; + border-radius: var(--radius-sm); + background: var(--color-surface-muted); + color: var(--color-text); + text-align: left; + transition: + background-color 160ms var(--easing-standard), + border-color 160ms var(--easing-standard); +} + +.submenuItem:hover { + border-color: var(--color-border); + background: var(--color-surface); +} + +.submenuItemActive { + border-color: var(--color-accent-soft); + background: var(--color-surface); +} + +.submenuIndicator { + width: 0.35rem; + height: 0.35rem; + flex: 0 0 auto; + border-radius: 999px; + background: var(--color-accent-soft); +} + +@include respond-down(mobile) { + .menu { + min-width: min(16rem, calc(100vw - (var(--space-4) * 2))); + } +} diff --git a/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.tsx b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.tsx new file mode 100644 index 0000000..d695c00 --- /dev/null +++ b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.tsx @@ -0,0 +1,111 @@ +import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js"; +import { ChevronDown } from "../../../lib/icons"; +import { activeDepartment, departmentItems, type DepartmentItem } from "../data/shell.data"; +import styles from "./DepartmentSelector.module.scss"; + +const defaultDepartment = departmentItems.find((item) => item.id === activeDepartment.id) ?? departmentItems[0]; +const defaultTeamName = departmentItems + .find((item) => item.id === activeDepartment.id) + ?.teams.find((teamName) => teamName === activeDepartment.teamName) + ?? defaultDepartment?.teams[0] + ?? ""; + +export const DepartmentSelector = (): JSX.Element => { + const [isOpen, setIsOpen] = createSignal(false); + const [selectedDepartment, setSelectedDepartment] = createSignal(defaultDepartment); + const [selectedTeamName, setSelectedTeamName] = createSignal(defaultTeamName); + + let rootRef: HTMLDivElement | undefined; + + onMount(() => { + const handlePointerDown = (event: PointerEvent): void => { + if (!isOpen()) return; + if (!rootRef) return; + + const target = event.target; + if (target instanceof Node && !rootRef.contains(target)) { + setIsOpen(false); + } + }; + + document.addEventListener("pointerdown", handlePointerDown); + onCleanup(() => document.removeEventListener("pointerdown", handlePointerDown)); + }); + + const selectDepartment = (item: DepartmentItem): void => { + setSelectedDepartment(item); + setSelectedTeamName(item.teams[0] ?? ""); + }; + + const selectTeam = (teamName: string): void => { + setSelectedTeamName(teamName); + setIsOpen(false); + }; + + return ( +
+ + + {isOpen() ? ( + + ); +}; diff --git a/Frontend/src/components/shell/LeftRail/LeftRail.module.scss b/Frontend/src/components/shell/LeftRail/LeftRail.module.scss index 04fbd9c..e63ba89 100644 --- a/Frontend/src/components/shell/LeftRail/LeftRail.module.scss +++ b/Frontend/src/components/shell/LeftRail/LeftRail.module.scss @@ -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 { diff --git a/Frontend/src/components/shell/LeftRail/LeftRail.tsx b/Frontend/src/components/shell/LeftRail/LeftRail.tsx index 32034a9..5445cbf 100644 --- a/Frontend/src/components/shell/LeftRail/LeftRail.tsx +++ b/Frontend/src/components/shell/LeftRail/LeftRail.tsx @@ -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 ( -
+ ); +}; diff --git a/Frontend/src/components/shell/TopBar/ThemeToggle.module.scss b/Frontend/src/components/shell/TopBar/ThemeToggle.module.scss new file mode 100644 index 0000000..6139f8a --- /dev/null +++ b/Frontend/src/components/shell/TopBar/ThemeToggle.module.scss @@ -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; + } +} diff --git a/Frontend/src/components/shell/TopBar/ThemeToggle.tsx b/Frontend/src/components/shell/TopBar/ThemeToggle.tsx new file mode 100644 index 0000000..86403c9 --- /dev/null +++ b/Frontend/src/components/shell/TopBar/ThemeToggle.tsx @@ -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 ( + + ); +}; diff --git a/Frontend/src/components/shell/TopBar/TopBar.module.scss b/Frontend/src/components/shell/TopBar/TopBar.module.scss index 0436b5f..efa7ef3 100644 --- a/Frontend/src/components/shell/TopBar/TopBar.module.scss +++ b/Frontend/src/components/shell/TopBar/TopBar.module.scss @@ -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; + } } diff --git a/Frontend/src/components/shell/TopBar/TopBar.tsx b/Frontend/src/components/shell/TopBar/TopBar.tsx index a94fa0c..4bb49a2 100644 --- a/Frontend/src/components/shell/TopBar/TopBar.tsx +++ b/Frontend/src/components/shell/TopBar/TopBar.tsx @@ -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 => {
Moku Work -
- Workspace Shell - Moku / Product - -
+
- +
+
+ + {(item): JSX.Element => { + const Icon = item.icon; -
- - {(item): JSX.Element => { - const Icon = item.icon; + return ( + + ); + }} + +
- return ( - - ); - }} -
+ +
); diff --git a/Frontend/src/components/shell/TopBar/UserNavButton.module.scss b/Frontend/src/components/shell/TopBar/UserNavButton.module.scss new file mode 100644 index 0000000..d33f387 --- /dev/null +++ b/Frontend/src/components/shell/TopBar/UserNavButton.module.scss @@ -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); + } +} diff --git a/Frontend/src/components/shell/TopBar/UserNavButton.tsx b/Frontend/src/components/shell/TopBar/UserNavButton.tsx new file mode 100644 index 0000000..06b3955 --- /dev/null +++ b/Frontend/src/components/shell/TopBar/UserNavButton.tsx @@ -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 ( + + ); +}; diff --git a/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.module.scss b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.module.scss index 789e8da..2538491 100644 --- a/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.module.scss +++ b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.module.scss @@ -1,6 +1,7 @@ .sidebar { --sidebar-nav-item-min-height: var(--control-size-lg); --sidebar-dock-clearance: 8rem; + position: relative; min-width: 0; min-height: 0; display: grid; @@ -8,28 +9,17 @@ gap: var(--space-4); padding: var(--space-4); overflow: hidden; + border-top-left-radius: inherit; + isolation: isolate; } .header { display: grid; - gap: 0.2rem; + gap: var(--space-2); } -.eyebrow { - @include text-caption; - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.title { - @include text-title; -} - -.meta { - @include text-caption; - color: var(--color-text-muted); - max-width: 28ch; +.headerDrawerOpen { + z-index: 4; } .section { @@ -37,6 +27,17 @@ grid-template-rows: auto minmax(0, 1fr); gap: var(--space-2); min-height: 0; + position: relative; + z-index: 1; + transition: + opacity 180ms var(--easing-standard), + transform 220ms var(--easing-standard); +} + +.sectionHidden { + opacity: 0; + pointer-events: none; + transform: translateX(var(--space-3)); } .navScroller { @@ -78,7 +79,7 @@ border-color: var(--color-border); background: var(--color-surface); color: var(--color-text); - box-shadow: var(--shadow-soft); + box-shadow: inset 0 1px 0 color-mix(in srgb, white 4%, transparent); } .icon { diff --git a/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx index 579bee5..7d0329a 100644 --- a/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -1,23 +1,42 @@ // Path: Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx -import { For, Show, type JSX } from "solid-js"; -import { workspaceSidebarItems } from "../data/shell.data"; +import { For, Show, createSignal, type JSX } from "solid-js"; +import { ProjectSelector } from "../ProjectSelector/ProjectSelector"; +import { serverSidebarItems } from "../data/shell.data"; import styles from "./WorkspaceSidebar.module.scss"; export const WorkspaceSidebar = (): JSX.Element => { + const [isProjectDrawerOpen, setIsProjectDrawerOpen] = createSignal(false); + return ( -