From 715661a4d24e59425c9d685a3ec30e883c94c46a Mon Sep 17 00:00:00 2001 From: MangoPig Date: Mon, 6 Jul 2026 16:59:50 +0100 Subject: [PATCH] Feat: add route-backed settings pages --- Frontend/package.json | 1 + Frontend/pnpm-lock.yaml | 12 + Frontend/src/app.tsx | 11 +- .../app-shell/AppShell/AppShell.module.scss | 10 + .../app-shell/AppShell/AppShell.tsx | 24 +- .../app-shell/ServerDock/ServerDock.tsx | 19 +- .../components/app-shell/data/shell.types.ts | 4 +- .../app-shell/data/workspace-routes.ts | 79 ++++++ .../SettingsDetailPage.module.scss | 86 +++++++ .../SettingsDetailPage/SettingsDetailPage.tsx | 32 +++ .../SettingsWorkspace.module.scss | 229 ++++++++++++++++++ .../SettingsWorkspace/SettingsWorkspace.tsx | 95 ++++++++ .../components/top-bar/TopBar/ProfileMenu.tsx | 21 +- .../MobileWorkspaceBrowser.parts.tsx | 30 ++- .../MobileWorkspaceBrowser.tsx | 14 +- .../WorkspaceSidebar.parts.tsx | 14 +- Frontend/src/routes/index.tsx | 10 + Frontend/src/routes/settings.tsx | 10 + Frontend/src/routes/settings/account.tsx | 16 ++ Frontend/src/routes/settings/index.tsx | 10 + Frontend/src/routes/settings/members.tsx | 16 ++ Frontend/src/routes/settings/security.tsx | 16 ++ Frontend/src/routes/settings/server.tsx | 16 ++ Frontend/src/routes/settings/theme.tsx | 16 ++ Frontend/src/routes/settings/workspace.tsx | 16 ++ Frontend/src/routes/workspace.tsx | 10 + Frontend/src/routes/workspace/home.tsx | 10 + Frontend/src/routes/workspace/index.tsx | 10 + Frontend/src/routes/workspace/settings.tsx | 9 + .../src/routes/workspace/settings/account.tsx | 10 + .../src/routes/workspace/settings/index.tsx | 10 + .../src/routes/workspace/settings/members.tsx | 10 + .../routes/workspace/settings/security.tsx | 10 + .../src/routes/workspace/settings/server.tsx | 10 + .../src/routes/workspace/settings/theme.tsx | 10 + .../routes/workspace/settings/workspace.tsx | 10 + 36 files changed, 881 insertions(+), 35 deletions(-) create mode 100644 Frontend/src/components/app-shell/data/workspace-routes.ts create mode 100644 Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.module.scss create mode 100644 Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.tsx create mode 100644 Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.module.scss create mode 100644 Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.tsx create mode 100644 Frontend/src/routes/index.tsx create mode 100644 Frontend/src/routes/settings.tsx create mode 100644 Frontend/src/routes/settings/account.tsx create mode 100644 Frontend/src/routes/settings/index.tsx create mode 100644 Frontend/src/routes/settings/members.tsx create mode 100644 Frontend/src/routes/settings/security.tsx create mode 100644 Frontend/src/routes/settings/server.tsx create mode 100644 Frontend/src/routes/settings/theme.tsx create mode 100644 Frontend/src/routes/settings/workspace.tsx create mode 100644 Frontend/src/routes/workspace.tsx create mode 100644 Frontend/src/routes/workspace/home.tsx create mode 100644 Frontend/src/routes/workspace/index.tsx create mode 100644 Frontend/src/routes/workspace/settings.tsx create mode 100644 Frontend/src/routes/workspace/settings/account.tsx create mode 100644 Frontend/src/routes/workspace/settings/index.tsx create mode 100644 Frontend/src/routes/workspace/settings/members.tsx create mode 100644 Frontend/src/routes/workspace/settings/security.tsx create mode 100644 Frontend/src/routes/workspace/settings/server.tsx create mode 100644 Frontend/src/routes/workspace/settings/theme.tsx create mode 100644 Frontend/src/routes/workspace/settings/workspace.tsx diff --git a/Frontend/package.json b/Frontend/package.json index 05df22c..b5234a3 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -17,6 +17,7 @@ "preview": "vite preview" }, "dependencies": { + "@solidjs/router": "^0.16.1", "@solidjs/start": "2.0.0-alpha.2", "@solidjs/vite-plugin-nitro-2": "^0.1.0", "lucide-solid": "^0.542.0", diff --git a/Frontend/pnpm-lock.yaml b/Frontend/pnpm-lock.yaml index 6da1c4c..078ef36 100644 --- a/Frontend/pnpm-lock.yaml +++ b/Frontend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@solidjs/router': + specifier: ^0.16.1 + version: 0.16.1(solid-js@1.9.11) '@solidjs/start': specifier: 2.0.0-alpha.2 version: 2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)) @@ -1210,6 +1213,11 @@ packages: peerDependencies: solid-js: '>=1.8.4' + '@solidjs/router@0.16.1': + resolution: {integrity: sha512-IhyjedgC6LRpw/8CPGGI89FrV+r0xTHzOl2c4CRyzYQ1bLepJxbVI1LLKvsavMWY5TRBRacV7hAeOhuTXkjiqg==} + peerDependencies: + solid-js: ^1.8.6 + '@solidjs/start@2.0.0-alpha.2': resolution: {integrity: sha512-z56ATi3P07q8F5Io2I+RQrwjyWZtFZzpXN/J+8scf/gqrAW83LtgRkZFZjJaGH7i9WrHP+ep9F+ZiJ2gDHVBcw==} engines: {node: '>=22'} @@ -4485,6 +4493,10 @@ snapshots: dependencies: solid-js: 1.9.11 + '@solidjs/router@0.16.1(solid-js@1.9.11)': + dependencies: + solid-js: 1.9.11 + '@solidjs/start@2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))': dependencies: '@babel/core': 7.29.0 diff --git a/Frontend/src/app.tsx b/Frontend/src/app.tsx index 6a0d34c..a0a3468 100644 --- a/Frontend/src/app.tsx +++ b/Frontend/src/app.tsx @@ -1,12 +1,17 @@ // Path: Frontend/src/app.tsx -import type { JSX } from "solid-js"; -import { AppShell } from "./components/app-shell/AppShell/AppShell"; +import { Suspense, type JSX } from "solid-js"; +import { Router } from "@solidjs/router"; +import { FileRoutes } from "@solidjs/start/router"; import "./styles/main.scss"; import "./styles/user-overrides.scss"; const App = (): JSX.Element => { - return ; + return ( + {props.children}}> + + + ); }; export default App; diff --git a/Frontend/src/components/app-shell/AppShell/AppShell.module.scss b/Frontend/src/components/app-shell/AppShell/AppShell.module.scss index e17c88f..b1ab270 100644 --- a/Frontend/src/components/app-shell/AppShell/AppShell.module.scss +++ b/Frontend/src/components/app-shell/AppShell/AppShell.module.scss @@ -95,6 +95,8 @@ min-width: 0; min-height: 0; position: relative; + display: grid; + grid-template-rows: auto minmax(0, 1fr); overflow: hidden; z-index: 1; border-top: 1px solid var(--shell-frame-border); @@ -103,6 +105,14 @@ border-top-right-radius: 0; } +.workspaceContent { + min-width: 0; + min-height: 0; + height: 100%; + overflow: auto; + scrollbar-gutter: stable; +} + .mobileWorkspaceView { min-width: 0; min-height: 0; diff --git a/Frontend/src/components/app-shell/AppShell/AppShell.tsx b/Frontend/src/components/app-shell/AppShell/AppShell.tsx index 2e5baa0..99a871d 100644 --- a/Frontend/src/components/app-shell/AppShell/AppShell.tsx +++ b/Frontend/src/components/app-shell/AppShell/AppShell.tsx @@ -1,10 +1,11 @@ // Path: Frontend/src/components/app-shell/AppShell/AppShell.tsx +import { useLocation } from "@solidjs/router"; import { createSignal, onCleanup, onMount, Show, type JSX } from "solid-js"; import { getDocumentTheme, setTheme, type Theme } from "../../../helper/themeRuntime"; import { BootstrapWizard } from "../../bootstrap/BootstrapWizard/BootstrapWizard"; -import { WorkspaceHome } from "../../workspace-home/WorkspaceHome"; import { AppShellDataProvider, useAppShellData } from "../data/app-shell.context"; +import { getWorkspaceBreadcrumbSegments } from "../data/workspace-routes"; import { LeftRail } from "../../workspace-navigation/LeftRail/LeftRail"; import { MobileBottomNav } from "../MobileBottomNav/MobileBottomNav"; import { MobileWorkspaceBrowser } from "../../workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser"; @@ -19,7 +20,7 @@ import styles from "./AppShell.module.scss"; type MobileWorkspaceView = "notifications" | "profile" | null; const MOBILE_VIEWPORT_QUERY = "(max-width: 48rem)"; -const AppShellContent = (): JSX.Element => { +const AppShellContent = (props: { children: JSX.Element }): JSX.Element => { const [themeState, setThemeState] = createSignal("light"); const [isRailCollapsed, setIsRailCollapsed] = createSignal(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = createSignal(false); @@ -27,6 +28,7 @@ const AppShellContent = (): JSX.Element => { const [isMobileWorkspaceBrowserOpen, setIsMobileWorkspaceBrowserOpen] = createSignal(false); const [activeMobileWorkspaceView, setActiveMobileWorkspaceView] = createSignal(null); const appShellData = useAppShellData(); + const location = useLocation(); onMount((): void => { setThemeState(getDocumentTheme()); @@ -82,7 +84,15 @@ const AppShellContent = (): JSX.Element => { setActiveMobileWorkspaceView(null); }; - const workspaceBreadcrumb = (): string => `${appShellData.activeServer().name} / ${appShellData.activeProject().name} / Home`; + const workspaceBreadcrumb = (): string => + [ + appShellData.activeServer().name, + ...getWorkspaceBreadcrumbSegments(location.pathname, { + activeProjectName: appShellData.activeProject().name, + activeDepartmentName: appShellData.activeDepartment().name, + activeTeamName: appShellData.activeDepartment().teamName, + }), + ].join(" / "); return (
@@ -136,7 +146,9 @@ const AppShellContent = (): JSX.Element => { setIsSidebarCollapsed((collapsed) => !collapsed); }} /> - +
+ {props.children} +
} > @@ -175,10 +187,10 @@ const AppShellContent = (): JSX.Element => { ); }; -export const AppShell = (): JSX.Element => { +export const AppShell = (props: { children: JSX.Element }): JSX.Element => { return ( - + {props.children} ); }; diff --git a/Frontend/src/components/app-shell/ServerDock/ServerDock.tsx b/Frontend/src/components/app-shell/ServerDock/ServerDock.tsx index 2cd568e..92bb2e9 100644 --- a/Frontend/src/components/app-shell/ServerDock/ServerDock.tsx +++ b/Frontend/src/components/app-shell/ServerDock/ServerDock.tsx @@ -1,13 +1,22 @@ // Path: Frontend/src/components/app-shell/ServerDock/ServerDock.tsx import { For, Show, type JSX } from "solid-js"; +import { useNavigate } from "@solidjs/router"; import { useAppShellData } from "../data/app-shell.context"; +import { getWorkspaceSurfaceRoute } from "../data/workspace-routes"; import styles from "./ServerDock.module.scss"; export const ServerDock = (): JSX.Element => { const appShellData = useAppShellData(); + const navigate = useNavigate(); const activeServer = () => appShellData.activeServer(); + const handleAction = (actionId: string): void => { + if (actionId === "settings" || actionId === "server") { + navigate(getWorkspaceSurfaceRoute("settings")); + } + }; + return (
@@ -35,7 +44,15 @@ export const ServerDock = (): JSX.Element => { const Icon = item.icon; return ( - diff --git a/Frontend/src/components/app-shell/data/shell.types.ts b/Frontend/src/components/app-shell/data/shell.types.ts index 2fc993e..ebbf319 100644 --- a/Frontend/src/components/app-shell/data/shell.types.ts +++ b/Frontend/src/components/app-shell/data/shell.types.ts @@ -103,10 +103,12 @@ export type SidebarItem = { export type WorkspaceStaticKind = "workspace" | "home" | "settings"; +export type WorkspaceStaticSurfaceKind = Exclude; + export type WorkspaceItemTypeId = string; export type WorkspaceStaticItem = SidebarItem & { - contextKind: WorkspaceStaticKind; + contextKind: WorkspaceStaticSurfaceKind; }; export type WorkspaceFolderNode = { diff --git a/Frontend/src/components/app-shell/data/workspace-routes.ts b/Frontend/src/components/app-shell/data/workspace-routes.ts new file mode 100644 index 0000000..d54b834 --- /dev/null +++ b/Frontend/src/components/app-shell/data/workspace-routes.ts @@ -0,0 +1,79 @@ +// Path: Frontend/src/components/app-shell/data/workspace-routes.ts + +import type { WorkspaceStaticKind } from "./shell.types"; + +export type WorkspaceSurfaceKind = Exclude; +export type SettingsSectionKind = "account" | "workspace" | "server" | "theme" | "security" | "members"; + +export type WorkspaceBreadcrumbContext = { + activeProjectName: string; + activeDepartmentName: string; + activeTeamName?: string; +}; + +const workspaceSurfaceRoutes: Record = { + home: "/workspace/home", + settings: "/settings", +}; + +export const getWorkspaceSurfaceRoute = (surface: WorkspaceSurfaceKind): string => workspaceSurfaceRoutes[surface]; + +const settingsSectionRoutes: Record = { + account: "/settings/account", + workspace: "/settings/workspace", + server: "/settings/server", + theme: "/settings/theme", + security: "/settings/security", + members: "/settings/members", +}; + +export const getSettingsSectionRoute = (section: SettingsSectionKind): string => settingsSectionRoutes[section]; + +export const getWorkspaceSurfaceFromPathname = (pathname: string): WorkspaceSurfaceKind => { + if (pathname.startsWith(workspaceSurfaceRoutes.settings) || pathname.startsWith("/workspace/settings")) { + return "settings"; + } + + return "home"; +}; + +export const getWorkspaceSurfaceBreadcrumbLabel = (pathname: string): string => { + const surface = getWorkspaceSurfaceFromPathname(pathname); + return surface === "settings" ? "Settings" : "Home"; +}; + +const getSettingsSectionFromPathname = (pathname: string): SettingsSectionKind | null => { + const matchedEntry = (Object.entries(settingsSectionRoutes) as Array<[SettingsSectionKind, string]>).find(([, route]) => pathname.startsWith(route)); + + return matchedEntry?.[0] ?? null; +}; + +export const getWorkspaceBreadcrumbSegments = ( + pathname: string, + context: WorkspaceBreadcrumbContext, +): string[] => { + const settingsSection = getSettingsSectionFromPathname(pathname); + + if (pathname === workspaceSurfaceRoutes.settings || pathname === "/workspace/settings") { + return ["Settings"]; + } + + if (settingsSection) { + switch (settingsSection) { + case "account": + return ["Users", "Settings"]; + case "workspace": + return [context.activeProjectName, "Settings"]; + case "server": + return ["Settings"]; + case "theme": + return ["Personal", "Settings"]; + case "security": + return ["Users", "Settings"]; + case "members": + return [context.activeTeamName || context.activeDepartmentName || "Members", "Users"]; + } + } + + return [context.activeProjectName, getWorkspaceSurfaceBreadcrumbLabel(pathname)]; +}; diff --git a/Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.module.scss b/Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.module.scss new file mode 100644 index 0000000..6e1bf24 --- /dev/null +++ b/Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.module.scss @@ -0,0 +1,86 @@ +// Path: Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.module.scss + +.viewport { + width: 100%; + max-width: min(72rem, 100%); + min-height: 100%; + padding: var(--space-5) var(--space-6) calc(var(--space-7) + env(safe-area-inset-bottom)); + box-sizing: border-box; + display: grid; + gap: var(--space-6); + align-content: start; +} + +.hero, +.panel { + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-xl); + background: var(--color-surface-raised); + box-shadow: var(--shadow-sm); + padding: var(--space-6); + display: grid; + gap: var(--space-2); +} + +.hero { + border-color: color-mix(in srgb, var(--color-accent) 18%, var(--color-border-subtle)); + box-shadow: + 0 10px 24px color-mix(in srgb, var(--color-accent) 8%, transparent), + var(--shadow-sm); +} + +.backLink { + width: fit-content; + text-decoration: none; + color: color-mix(in srgb, var(--color-accent) 34%, var(--color-text-primary)); + @include text-label; +} + +.backLink:hover { + text-decoration: underline; +} + +.title, +.panelTitle { + margin: 0; + color: var(--color-text-primary); +} + +.title { + @include text-display; + line-height: 1.02; + letter-spacing: -0.02em; +} + +.panelTitle { + @include text-title; + line-height: 1.12; +} + +.description, +.focusList { + margin: 0; + color: var(--color-text-secondary); + @include text-body; +} + +.focusList { + padding-left: 1.1rem; + display: grid; + gap: 0.35rem; +} + +.focusList li::marker { + color: color-mix(in srgb, var(--color-accent) 44%, var(--color-text-secondary)); +} + +@media (max-width: 48rem) { + .viewport { + padding: var(--space-4) var(--space-4) calc(var(--space-8) + env(safe-area-inset-bottom)); + } + + .hero, + .panel { + padding: var(--space-4); + } +} diff --git a/Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.tsx b/Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.tsx new file mode 100644 index 0000000..8def71c --- /dev/null +++ b/Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.tsx @@ -0,0 +1,32 @@ +// Path: Frontend/src/components/settings/SettingsDetailPage/SettingsDetailPage.tsx + +import { A } from "@solidjs/router"; +import { For, type JSX } from "solid-js"; +import styles from "./SettingsDetailPage.module.scss"; + +type SettingsDetailPageProps = { + title: string; + description: string; + focus: readonly string[]; +}; + +export const SettingsDetailPage = (props: SettingsDetailPageProps): JSX.Element => { + return ( +
+
+ + Back to Settings + +

{props.title}

+

{props.description}

+
+ +
+

Focus areas

+
    + {(item) =>
  • {item}
  • }
    +
+
+
+ ); +}; diff --git a/Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.module.scss b/Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.module.scss new file mode 100644 index 0000000..eb94590 --- /dev/null +++ b/Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.module.scss @@ -0,0 +1,229 @@ +// Path: Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.module.scss + +.viewport { + --settings-page-max-width: min(88rem, 100%); + + min-height: 100%; + box-sizing: border-box; + display: grid; + gap: var(--space-6); + padding-top: var(--space-5); + padding-right: var(--space-6); + padding-bottom: calc(var(--space-7) + env(safe-area-inset-bottom)); + padding-left: var(--space-6); + align-content: start; +} + +.hero { + display: grid; + gap: var(--space-4); + width: 100%; + max-width: var(--settings-page-max-width); + padding-inline: var(--space-6); + box-sizing: border-box; +} + +.heroCopy, +.card { + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-sm); +} + +.heroCopy { + padding: var(--space-6); + display: grid; + gap: var(--space-2); + position: relative; + background: var(--color-surface-raised); + border-color: color-mix(in srgb, var(--color-accent) 32%, var(--color-border-subtle)); + box-shadow: + 0 12px 28px color-mix(in srgb, var(--color-accent) 8%, transparent), + var(--shadow-sm); +} + + .eyebrow, + .sectionEyebrow { + margin: 0; + color: color-mix(in srgb, var(--color-accent) 42%, var(--color-text-tertiary)); + @include text-label; +} + +.title, +.sectionTitle, +.cardTitle { + margin: 0; + color: var(--color-text-primary); +} + +.title { + @include text-display; + max-width: 13ch; + text-wrap: balance; + letter-spacing: -0.025em; + line-height: 1.02; +} + + .description, + .sectionDescription, + .cardDescription, + .focusList { + margin: 0; + color: var(--color-text-secondary); + @include text-body; +} + +.focusList { + padding-left: 1.1rem; + display: grid; + gap: 0.45rem; +} + +.sectionStack { + width: 100%; + max-width: var(--settings-page-max-width); + display: grid; + gap: var(--space-5); + padding-inline: var(--space-6); + box-sizing: border-box; +} + +.section { + display: grid; + gap: var(--space-4); +} + +.sectionHeader { + display: grid; + gap: var(--space-2); +} + +.sectionTitle { + @include text-title; +} + +.cardGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-4); +} + +.card { + padding: var(--space-5); + display: grid; + gap: var(--space-3); + align-items: start; + text-decoration: none; + color: inherit; + position: relative; + background: var(--color-surface-raised); + transition: + transform 120ms ease, + border-color 120ms ease, + box-shadow 120ms ease; +} + +.card::before { + content: none; +} + +.card::after { + content: none; +} + +.card:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--color-accent) 34%, var(--color-border-strong)); + box-shadow: + 0 10px 22px color-mix(in srgb, var(--color-accent) 8%, transparent), + var(--shadow-md); +} + +.card:focus-visible { + outline: 2px solid color-mix(in srgb, var(--color-accent) 54%, transparent); + outline-offset: 2px; + border-color: color-mix(in srgb, var(--color-accent) 26%, var(--color-border-strong)); + box-shadow: var(--shadow-md); +} + +.cardHeader { + display: flex; + align-items: center; + gap: var(--space-3); + position: relative; + z-index: 1; +} + +.cardIconWrap { + width: 2.75rem; + height: 2.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-lg); + background: linear-gradient(180deg, color-mix(in srgb, var(--color-accent) 14%, transparent), color-mix(in srgb, var(--color-accent-soft) 22%, transparent)); + color: var(--color-text-primary); + border: 1px solid color-mix(in srgb, var(--color-accent) 16%, var(--color-border-subtle)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, white 8%, transparent), + 0 6px 14px color-mix(in srgb, var(--color-accent) 7%, transparent); +} + +.cardBody { + display: grid; + gap: var(--space-2); + position: relative; + z-index: 1; +} + +.cardTitle { + @include text-title; + line-height: 1.1; +} + +.focusList li::marker { + color: color-mix(in srgb, var(--color-accent) 44%, var(--color-text-secondary)); +} + +.card:nth-child(2n) .cardIconWrap { + background: linear-gradient(180deg, color-mix(in srgb, #8b5cf6 14%, transparent), color-mix(in srgb, var(--color-accent-soft) 22%, transparent)); + border-color: color-mix(in srgb, #8b5cf6 16%, var(--color-border-subtle)); +} + +.card:nth-child(3n) .cardIconWrap { + background: linear-gradient(180deg, color-mix(in srgb, #06b6d4 12%, transparent), color-mix(in srgb, var(--color-accent) 18%, transparent)); + border-color: color-mix(in srgb, #06b6d4 14%, var(--color-border-subtle)); +} + +@media (max-width: 64rem) { + .cardGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 48rem) { + .viewport { + padding-top: var(--space-4); + padding-right: var(--space-4); + padding-bottom: calc(var(--space-8) + env(safe-area-inset-bottom)); + padding-left: var(--space-4); + } + + .hero, + .sectionStack { + padding-inline: var(--space-4); + } + + .heroCopy, + .card { + padding: var(--space-4); + } + + .cardHeader { + align-items: flex-start; + } + + .title { + max-width: none; + } +} diff --git a/Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.tsx b/Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.tsx new file mode 100644 index 0000000..8bf1c1a --- /dev/null +++ b/Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.tsx @@ -0,0 +1,95 @@ +// Path: Frontend/src/components/settings/SettingsWorkspace/SettingsWorkspace.tsx + +import { A } from "@solidjs/router"; +import { For, type JSX } from "solid-js"; +import { Bell, Home, Moon, Shield, Settings, User } from "../../../lib/icons"; +import { getSettingsSectionRoute } from "../../app-shell/data/workspace-routes"; +import styles from "./SettingsWorkspace.module.scss"; + +const sections = [ + { + title: "Account Settings", + description: "Profile, identity, and account preferences for the active user.", + icon: User, + href: getSettingsSectionRoute("account"), + focus: ["Profile", "Notifications", "Personal defaults"], + }, + { + title: "Workspace Settings", + description: "Core workspace behavior mapped to the project-level `settings.cbor` surface.", + icon: Home, + href: getSettingsSectionRoute("workspace"), + focus: ["Navigation defaults", "Workspace behavior", "Content rules"], + }, + { + title: "Server Settings", + description: "Organization and server-wide controls that should live above individual workspaces.", + icon: Settings, + href: getSettingsSectionRoute("server"), + focus: ["Server identity", "Policies", "Operational defaults"], + }, + { + title: "Theme Settings", + description: "Theme preferences and presentation settings that should eventually map cleanly to POSIX-backed configuration.", + icon: Moon, + href: getSettingsSectionRoute("theme"), + focus: ["Theme preset", "Density", "Visual preferences"], + }, + { + title: "Security Settings", + description: "Authentication, sessions, and access hardening for users and organizations.", + icon: Shield, + href: getSettingsSectionRoute("security"), + focus: ["Sessions", "Access controls", "Recovery"], + }, + { + title: "Members & Roles", + description: "People, invitations, and organization-level permission management.", + icon: Bell, + href: getSettingsSectionRoute("members"), + focus: ["Members", "Roles", "Invitations"], + }, +] as const; + +export const SettingsWorkspace = (): JSX.Element => { + return ( +
+
+
+

Settings

+
+
+ +
+
+
+ + {(section) => { + const Icon = section.icon; + + return ( + +
+
+ +
+
+ +
+

{section.title}

+

{section.description}

+ +
    + {(focus) =>
  • {focus}
  • }
    +
+
+
+ ); + }} +
+
+
+
+
+ ); +}; diff --git a/Frontend/src/components/top-bar/TopBar/ProfileMenu.tsx b/Frontend/src/components/top-bar/TopBar/ProfileMenu.tsx index 3689082..7e3accd 100644 --- a/Frontend/src/components/top-bar/TopBar/ProfileMenu.tsx +++ b/Frontend/src/components/top-bar/TopBar/ProfileMenu.tsx @@ -1,6 +1,8 @@ +import { useNavigate } from "@solidjs/router"; import { For, type JSX } from "solid-js"; import { User } from "../../../lib/icons"; import { useAppShellData } from "../../app-shell/data/app-shell.context"; +import { getWorkspaceSurfaceRoute } from "../../app-shell/data/workspace-routes"; import { profileMenuSections } from "../../app-shell/data/shell.data"; import styles from "./ProfileMenu.module.scss"; @@ -14,8 +16,17 @@ type ProfileMenuProps = { export const ProfileMenu = (props: ProfileMenuProps): JSX.Element => { const variant = props.variant ?? "popover"; const appShellData = useAppShellData(); + const navigate = useNavigate(); const activeUserProfile = () => appShellData.activeUserProfile(); + const handleSelect = (actionId: string): void => { + if (actionId === "account-settings" || actionId === "security" || actionId === "theme-preferences") { + navigate(getWorkspaceSurfaceRoute("settings")); + } + + props.onSelect(); + }; + return (
{ [styles.item]: true, [styles.itemDanger]: item.tone === "danger", }} - data-slot="profile-action" - data-action-id={item.id} - data-tone={item.tone ?? "default"} - onClick={props.onSelect} - > + data-slot="profile-action" + data-action-id={item.id} + data-tone={item.tone ?? "default"} + onClick={() => handleSelect(item.id)} + > diff --git a/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.parts.tsx b/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.parts.tsx index 15b4e2d..9309709 100644 --- a/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.parts.tsx +++ b/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.parts.tsx @@ -1,8 +1,10 @@ // Path: Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.parts.tsx +import { useLocation, useNavigate } from "@solidjs/router"; import { For, Show, type JSX } from "solid-js"; import { ChevronRight } from "../../../lib/icons"; import { createLongPressGesture } from "../../../helper/createLongPressGesture"; +import { getWorkspaceSurfaceRoute } from "../../app-shell/data/workspace-routes"; import { createWorkspaceStaticTarget, createWorkspaceTreeTarget, @@ -72,8 +74,12 @@ export const StaticRow = (props: { item: SidebarItem }): JSX.Element => { export const WorkspaceStaticRow = (props: { item: WorkspaceStaticItem; onOpenActionSheet: (target: WorkspaceContextMenuTarget) => void; + onSelect: VoidFunction; }): JSX.Element => { + const navigate = useNavigate(); + const location = useLocation(); const target = createWorkspaceStaticTarget(props.item); + const isActive = (): boolean => location.pathname === getWorkspaceSurfaceRoute(props.item.contextKind); const longPress = createLongPressGesture({ onLongPress: () => { props.onOpenActionSheet(target); @@ -84,16 +90,20 @@ export const WorkspaceStaticRow = (props: {
  • { - event.preventDefault(); - props.onOpenActionSheet(target); - }} - {...longPress} - > - -
  • - ); + data-target-kind={target.kind} + onContextMenu={(event): void => { + event.preventDefault(); + props.onOpenActionSheet(target); + }} + onClick={(): void => { + navigate(getWorkspaceSurfaceRoute(props.item.contextKind)); + props.onSelect(); + }} + {...longPress} + > + + + ); }; export const WorkspaceTreeBranch = (props: { diff --git a/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx b/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx index 780fc50..c2fa8f0 100644 --- a/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx +++ b/Frontend/src/components/workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx @@ -72,12 +72,14 @@ export const MobileWorkspaceBrowser = (props: MobileWorkspaceBrowserProps): JSX.
    Workspace -
      - - {(item): JSX.Element => } - -
    -
    +
      + + {(item): JSX.Element => ( + + )} + +
    +
    Items diff --git a/Frontend/src/components/workspace-navigation/WorkspaceSidebar/WorkspaceSidebar.parts.tsx b/Frontend/src/components/workspace-navigation/WorkspaceSidebar/WorkspaceSidebar.parts.tsx index 461763e..db701ae 100644 --- a/Frontend/src/components/workspace-navigation/WorkspaceSidebar/WorkspaceSidebar.parts.tsx +++ b/Frontend/src/components/workspace-navigation/WorkspaceSidebar/WorkspaceSidebar.parts.tsx @@ -1,7 +1,9 @@ // Path: Frontend/src/components/workspace-navigation/WorkspaceSidebar/WorkspaceSidebar.parts.tsx +import { useLocation, useNavigate } from "@solidjs/router"; import { For, Show, type JSX } from "solid-js"; import { ChevronRight, Folder } from "../../../lib/icons"; +import { getWorkspaceSurfaceRoute } from "../../app-shell/data/workspace-routes"; import { createWorkspaceStaticTarget, createWorkspaceTreeTarget, @@ -66,8 +68,11 @@ export const WorkspaceHomeEntry = (props: { onOpenContextMenu: (event: MouseEvent, target: WorkspaceContextMenuTarget) => void; onOpenContextMenuFromKeyboard: (element: HTMLElement, target: WorkspaceContextMenuTarget) => void; }): JSX.Element => { + const navigate = useNavigate(); + const location = useLocation(); const Icon = props.item.icon; const target = createWorkspaceStaticTarget(props.item); + const isActive = (): boolean => location.pathname === getWorkspaceSurfaceRoute(props.item.contextKind); return (
  • @@ -75,14 +80,17 @@ export const WorkspaceHomeEntry = (props: { type="button" classList={{ [styles.navItem]: true, - [styles.navItemActive]: !!props.item.active, + [styles.navItemActive]: isActive(), }} - aria-current={props.item.active ? "page" : undefined} + aria-current={isActive() ? "page" : undefined} aria-label={props.item.label} title={props.item.label} data-slot="workspace-static-item" data-target-kind={target.kind} - data-active={props.item.active ? "true" : "false"} + data-active={isActive() ? "true" : "false"} + onClick={(): void => { + navigate(getWorkspaceSurfaceRoute(props.item.contextKind)); + }} onContextMenu={(event): void => { event.stopPropagation(); props.onOpenContextMenu(event, target); diff --git a/Frontend/src/routes/index.tsx b/Frontend/src/routes/index.tsx new file mode 100644 index 0000000..f505f9e --- /dev/null +++ b/Frontend/src/routes/index.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/index.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const IndexRoute = (): JSX.Element => { + return ; +}; + +export default IndexRoute; diff --git a/Frontend/src/routes/settings.tsx b/Frontend/src/routes/settings.tsx new file mode 100644 index 0000000..c1c36ed --- /dev/null +++ b/Frontend/src/routes/settings.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/settings.tsx + +import type { RouteSectionProps } from "@solidjs/router"; +import { AppShell } from "../components/app-shell/AppShell/AppShell"; + +const SettingsLayoutRoute = (props: RouteSectionProps) => { + return {props.children}; +}; + +export default SettingsLayoutRoute; diff --git a/Frontend/src/routes/settings/account.tsx b/Frontend/src/routes/settings/account.tsx new file mode 100644 index 0000000..f560830 --- /dev/null +++ b/Frontend/src/routes/settings/account.tsx @@ -0,0 +1,16 @@ +// Path: Frontend/src/routes/settings/account.tsx + +import { type JSX } from "solid-js"; +import { SettingsDetailPage } from "../../components/settings/SettingsDetailPage/SettingsDetailPage"; + +const SettingsAccountRoute = (): JSX.Element => { + return ( + + ); +}; + +export default SettingsAccountRoute; diff --git a/Frontend/src/routes/settings/index.tsx b/Frontend/src/routes/settings/index.tsx new file mode 100644 index 0000000..a8a8b20 --- /dev/null +++ b/Frontend/src/routes/settings/index.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/settings/index.tsx + +import { type JSX } from "solid-js"; +import { SettingsWorkspace } from "../../components/settings/SettingsWorkspace/SettingsWorkspace"; + +const SettingsIndexRoute = (): JSX.Element => { + return ; +}; + +export default SettingsIndexRoute; diff --git a/Frontend/src/routes/settings/members.tsx b/Frontend/src/routes/settings/members.tsx new file mode 100644 index 0000000..01d048e --- /dev/null +++ b/Frontend/src/routes/settings/members.tsx @@ -0,0 +1,16 @@ +// Path: Frontend/src/routes/settings/members.tsx + +import { type JSX } from "solid-js"; +import { SettingsDetailPage } from "../../components/settings/SettingsDetailPage/SettingsDetailPage"; + +const SettingsMembersRoute = (): JSX.Element => { + return ( + + ); +}; + +export default SettingsMembersRoute; diff --git a/Frontend/src/routes/settings/security.tsx b/Frontend/src/routes/settings/security.tsx new file mode 100644 index 0000000..8083ca9 --- /dev/null +++ b/Frontend/src/routes/settings/security.tsx @@ -0,0 +1,16 @@ +// Path: Frontend/src/routes/settings/security.tsx + +import { type JSX } from "solid-js"; +import { SettingsDetailPage } from "../../components/settings/SettingsDetailPage/SettingsDetailPage"; + +const SettingsSecurityRoute = (): JSX.Element => { + return ( + + ); +}; + +export default SettingsSecurityRoute; diff --git a/Frontend/src/routes/settings/server.tsx b/Frontend/src/routes/settings/server.tsx new file mode 100644 index 0000000..4d7d846 --- /dev/null +++ b/Frontend/src/routes/settings/server.tsx @@ -0,0 +1,16 @@ +// Path: Frontend/src/routes/settings/server.tsx + +import { type JSX } from "solid-js"; +import { SettingsDetailPage } from "../../components/settings/SettingsDetailPage/SettingsDetailPage"; + +const SettingsServerRoute = (): JSX.Element => { + return ( + + ); +}; + +export default SettingsServerRoute; diff --git a/Frontend/src/routes/settings/theme.tsx b/Frontend/src/routes/settings/theme.tsx new file mode 100644 index 0000000..7500612 --- /dev/null +++ b/Frontend/src/routes/settings/theme.tsx @@ -0,0 +1,16 @@ +// Path: Frontend/src/routes/settings/theme.tsx + +import { type JSX } from "solid-js"; +import { SettingsDetailPage } from "../../components/settings/SettingsDetailPage/SettingsDetailPage"; + +const SettingsThemeRoute = (): JSX.Element => { + return ( + + ); +}; + +export default SettingsThemeRoute; diff --git a/Frontend/src/routes/settings/workspace.tsx b/Frontend/src/routes/settings/workspace.tsx new file mode 100644 index 0000000..d9b9f7b --- /dev/null +++ b/Frontend/src/routes/settings/workspace.tsx @@ -0,0 +1,16 @@ +// Path: Frontend/src/routes/settings/workspace.tsx + +import { type JSX } from "solid-js"; +import { SettingsDetailPage } from "../../components/settings/SettingsDetailPage/SettingsDetailPage"; + +const SettingsWorkspaceRoute = (): JSX.Element => { + return ( + + ); +}; + +export default SettingsWorkspaceRoute; diff --git a/Frontend/src/routes/workspace.tsx b/Frontend/src/routes/workspace.tsx new file mode 100644 index 0000000..35e5a01 --- /dev/null +++ b/Frontend/src/routes/workspace.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace.tsx + +import type { RouteSectionProps } from "@solidjs/router"; +import { AppShell } from "../components/app-shell/AppShell/AppShell"; + +const WorkspaceLayout = (props: RouteSectionProps) => { + return {props.children}; +}; + +export default WorkspaceLayout; diff --git a/Frontend/src/routes/workspace/home.tsx b/Frontend/src/routes/workspace/home.tsx new file mode 100644 index 0000000..209d315 --- /dev/null +++ b/Frontend/src/routes/workspace/home.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/home.tsx + +import { type JSX } from "solid-js"; +import { WorkspaceHome } from "../../components/workspace-home/WorkspaceHome"; + +const WorkspaceHomeRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceHomeRoute; diff --git a/Frontend/src/routes/workspace/index.tsx b/Frontend/src/routes/workspace/index.tsx new file mode 100644 index 0000000..407b31c --- /dev/null +++ b/Frontend/src/routes/workspace/index.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/index.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceIndexRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceIndexRoute; diff --git a/Frontend/src/routes/workspace/settings.tsx b/Frontend/src/routes/workspace/settings.tsx new file mode 100644 index 0000000..370d1a2 --- /dev/null +++ b/Frontend/src/routes/workspace/settings.tsx @@ -0,0 +1,9 @@ +// Path: Frontend/src/routes/workspace/settings.tsx + +import type { RouteSectionProps } from "@solidjs/router"; + +const WorkspaceSettingsLayoutRoute = (props: RouteSectionProps) => { + return props.children; +}; + +export default WorkspaceSettingsLayoutRoute; diff --git a/Frontend/src/routes/workspace/settings/account.tsx b/Frontend/src/routes/workspace/settings/account.tsx new file mode 100644 index 0000000..dfc0f65 --- /dev/null +++ b/Frontend/src/routes/workspace/settings/account.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/settings/account.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceSettingsAccountRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceSettingsAccountRoute; diff --git a/Frontend/src/routes/workspace/settings/index.tsx b/Frontend/src/routes/workspace/settings/index.tsx new file mode 100644 index 0000000..9e1dc07 --- /dev/null +++ b/Frontend/src/routes/workspace/settings/index.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/settings/index.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceSettingsIndexRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceSettingsIndexRoute; diff --git a/Frontend/src/routes/workspace/settings/members.tsx b/Frontend/src/routes/workspace/settings/members.tsx new file mode 100644 index 0000000..422a7b9 --- /dev/null +++ b/Frontend/src/routes/workspace/settings/members.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/settings/members.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceSettingsMembersRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceSettingsMembersRoute; diff --git a/Frontend/src/routes/workspace/settings/security.tsx b/Frontend/src/routes/workspace/settings/security.tsx new file mode 100644 index 0000000..20fdb43 --- /dev/null +++ b/Frontend/src/routes/workspace/settings/security.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/settings/security.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceSettingsSecurityRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceSettingsSecurityRoute; diff --git a/Frontend/src/routes/workspace/settings/server.tsx b/Frontend/src/routes/workspace/settings/server.tsx new file mode 100644 index 0000000..fd7bfbc --- /dev/null +++ b/Frontend/src/routes/workspace/settings/server.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/settings/server.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceSettingsServerRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceSettingsServerRoute; diff --git a/Frontend/src/routes/workspace/settings/theme.tsx b/Frontend/src/routes/workspace/settings/theme.tsx new file mode 100644 index 0000000..d998cef --- /dev/null +++ b/Frontend/src/routes/workspace/settings/theme.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/settings/theme.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceSettingsThemeRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceSettingsThemeRoute; diff --git a/Frontend/src/routes/workspace/settings/workspace.tsx b/Frontend/src/routes/workspace/settings/workspace.tsx new file mode 100644 index 0000000..230d5c0 --- /dev/null +++ b/Frontend/src/routes/workspace/settings/workspace.tsx @@ -0,0 +1,10 @@ +// Path: Frontend/src/routes/workspace/settings/workspace.tsx + +import { Navigate } from "@solidjs/router"; +import { type JSX } from "solid-js"; + +const WorkspaceSettingsWorkspaceRoute = (): JSX.Element => { + return ; +}; + +export default WorkspaceSettingsWorkspaceRoute;