web: move sidebar fold into frame
This commit is contained in:
parent
60460b23dc
commit
e975c648c2
|
|
@ -393,6 +393,12 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
||||||
const globalSidebar = await Deno.readTextFile(
|
const globalSidebar = await Deno.readTextFile(
|
||||||
new URL("../sidebar/GlobalSidebar.svelte", import.meta.url),
|
new URL("../sidebar/GlobalSidebar.svelte", import.meta.url),
|
||||||
);
|
);
|
||||||
|
const sidebarFrame = await Deno.readTextFile(
|
||||||
|
new URL("../sidebar/SidebarFrame.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
const sidebarCss = await Deno.readTextFile(
|
||||||
|
new URL("../sidebar/sidebar.css", import.meta.url),
|
||||||
|
);
|
||||||
const workspaceLayout = await Deno.readTextFile(
|
const workspaceLayout = await Deno.readTextFile(
|
||||||
new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url),
|
new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url),
|
||||||
);
|
);
|
||||||
|
|
@ -432,6 +438,7 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
||||||
assert(
|
assert(
|
||||||
rootLayout.includes("SIDEBAR_CONTEXT") &&
|
rootLayout.includes("SIDEBAR_CONTEXT") &&
|
||||||
rootLayout.includes("GlobalSidebar") &&
|
rootLayout.includes("GlobalSidebar") &&
|
||||||
|
rootLayout.includes("SidebarFrame") &&
|
||||||
rootLayout.includes("{@render sidebar()}") &&
|
rootLayout.includes("{@render sidebar()}") &&
|
||||||
!rootLayout.includes("WorkspaceSidebar") &&
|
!rootLayout.includes("WorkspaceSidebar") &&
|
||||||
rootLayout.includes("workspace-topbar") &&
|
rootLayout.includes("workspace-topbar") &&
|
||||||
|
|
@ -459,14 +466,23 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
||||||
"Workspace layout should load workspace data and register a WorkspaceSidebar snippet",
|
"Workspace layout should load workspace data and register a WorkspaceSidebar snippet",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
workspaceLayout.includes("sidebarFolded") &&
|
sidebarFrame.includes("let folded = $state(false)") &&
|
||||||
workspaceLayout.includes("onToggleFold={toggleSidebarFold}") &&
|
sidebarFrame.includes("sidebar-fold-button") &&
|
||||||
sidebar.includes("folded?: boolean") &&
|
sidebarFrame.includes("Fold sidebar") &&
|
||||||
sidebar.includes("onToggleFold?: () => void") &&
|
sidebarFrame.includes("Unfold sidebar") &&
|
||||||
sidebar.includes("sidebar-fold-button") &&
|
!workspaceLayout.includes("sidebarFolded") &&
|
||||||
sidebar.includes("Fold sidebar") &&
|
!workspaceLayout.includes("onToggleFold") &&
|
||||||
sidebar.includes("Unfold sidebar"),
|
!sidebar.includes("folded?: boolean") &&
|
||||||
"Workspace sidebar should keep a visible fold button with fold/folded naming",
|
!sidebar.includes("onToggleFold?: () => void") &&
|
||||||
|
!sidebar.includes("sidebar-fold-button"),
|
||||||
|
"Sidebar fold control should belong to SidebarFrame, not WorkspaceSidebar",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
sidebarCss.startsWith("@layer reset, tokens, base, layout, components;") &&
|
||||||
|
sidebarCss.includes(".sidebar-frame") &&
|
||||||
|
sidebarCss.includes(".sidebar-link") &&
|
||||||
|
sidebarCss.includes("text-decoration: none"),
|
||||||
|
"Sidebar styles should define their layer order before component rules so base link styles do not win by import order",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
sidebarOverride.includes("controller.setSidebar(sidebar)") &&
|
sidebarOverride.includes("controller.setSidebar(sidebar)") &&
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="global-sidebar" aria-label="Global navigation">
|
<div class="global-sidebar" aria-label="Global navigation">
|
||||||
<div class="global-sidebar-section">
|
<div class="global-sidebar-section">
|
||||||
<p class="sidebar-section-label">Global</p>
|
<p class="sidebar-section-label">Global</p>
|
||||||
<nav class="sidebar-list" aria-label="Global pages">
|
<nav class="sidebar-list" aria-label="Global pages">
|
||||||
|
|
@ -29,4 +29,4 @@
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</div>
|
||||||
|
|
|
||||||
46
web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte
Normal file
46
web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import './sidebar.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: Snippet<[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { children }: Props = $props();
|
||||||
|
let folded = $state(false);
|
||||||
|
|
||||||
|
function toggleFold() {
|
||||||
|
folded = !folded;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside class="sidebar-frame" class:folded aria-label="Sidebar">
|
||||||
|
<div class="sidebar-control-row">
|
||||||
|
<button
|
||||||
|
class="sidebar-fold-button"
|
||||||
|
type="button"
|
||||||
|
aria-label={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||||
|
aria-expanded={!folded}
|
||||||
|
title={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||||
|
onclick={toggleFold}
|
||||||
|
>
|
||||||
|
{#if folded}
|
||||||
|
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||||
|
<path d="m6 17 5-5-5-5" />
|
||||||
|
<path d="m13 17 5-5-5-5" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||||
|
<path d="m11 17-5-5 5-5" />
|
||||||
|
<path d="m18 17-5-5 5-5" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !folded}
|
||||||
|
<div class="sidebar-frame-content">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</aside>
|
||||||
|
|
@ -13,8 +13,6 @@
|
||||||
repositories?: RepositoryListResponse | null;
|
repositories?: RepositoryListResponse | null;
|
||||||
repositoriesError?: string | null;
|
repositoriesError?: string | null;
|
||||||
currentPath?: string;
|
currentPath?: string;
|
||||||
folded?: boolean;
|
|
||||||
onToggleFold?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -22,9 +20,7 @@
|
||||||
workspaceError = null,
|
workspaceError = null,
|
||||||
repositories = null,
|
repositories = null,
|
||||||
repositoriesError = null,
|
repositoriesError = null,
|
||||||
currentPath = '/',
|
currentPath = '/'
|
||||||
folded = false,
|
|
||||||
onToggleFold
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let workspaceId = $derived(workspace?.workspace_id ?? '');
|
let workspaceId = $derived(workspace?.workspace_id ?? '');
|
||||||
|
|
@ -32,35 +28,8 @@
|
||||||
let settingsHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/settings') : '/settings');
|
let settingsHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/settings') : '/settings');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<div class="workspace-sidebar">
|
||||||
class:folded
|
|
||||||
class="workspace-sidebar"
|
|
||||||
aria-label="Workspace navigation"
|
|
||||||
>
|
|
||||||
<header class="sidebar-header">
|
<header class="sidebar-header">
|
||||||
<div class="sidebar-control-row">
|
|
||||||
<button
|
|
||||||
class="sidebar-fold-button"
|
|
||||||
type="button"
|
|
||||||
aria-label={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
|
||||||
aria-expanded={!folded}
|
|
||||||
title={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
|
||||||
onclick={onToggleFold}
|
|
||||||
>
|
|
||||||
{#if folded}
|
|
||||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
|
||||||
<path d="m6 17 5-5-5-5" />
|
|
||||||
<path d="m13 17 5-5-5-5" />
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
|
||||||
<path d="m11 17-5-5 5-5" />
|
|
||||||
<path d="m18 17-5-5 5-5" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-title-row">
|
<div class="sidebar-title-row">
|
||||||
<div class="workspace-label">
|
<div class="workspace-label">
|
||||||
{#if workspace}
|
{#if workspace}
|
||||||
|
|
@ -106,12 +75,10 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if !folded}
|
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||||
<nav class="sidebar-sections" aria-label="Workspace sections">
|
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} {workspaceId} />
|
||||||
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} {workspaceId} />
|
<ObjectivesNavSection {currentPath} {workspaceId} />
|
||||||
<ObjectivesNavSection {currentPath} {workspaceId} />
|
<TicketsNavSection {currentPath} {workspaceId} />
|
||||||
<TicketsNavSection {currentPath} {workspaceId} />
|
<WorkersNavSection {currentPath} {workspaceId} />
|
||||||
<WorkersNavSection {currentPath} {workspaceId} />
|
</nav>
|
||||||
</nav>
|
</div>
|
||||||
{/if}
|
|
||||||
</aside>
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
@layer reset, tokens, base, layout, components;
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.global-sidebar {
|
.sidebar-frame {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
width: clamp(220px, 20vw, 280px);
|
width: clamp(220px, 20vw, 280px);
|
||||||
|
|
@ -9,24 +11,22 @@
|
||||||
padding: var(--space-4) var(--space-3);
|
padding: var(--space-4) var(--space-3);
|
||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.global-sidebar-section {
|
.sidebar-frame.folded {
|
||||||
|
width: max-content;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-inline: var(--space-2);
|
||||||
|
}
|
||||||
|
.sidebar-frame-content,
|
||||||
|
.global-sidebar,
|
||||||
|
.workspace-sidebar {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.global-sidebar,
|
||||||
|
.global-sidebar-section,
|
||||||
|
.workspace-sidebar {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
.workspace-sidebar {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1 / 3;
|
|
||||||
width: clamp(220px, 20vw, 280px);
|
|
||||||
align-self: stretch;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: var(--space-4) var(--space-3);
|
|
||||||
border-right: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
.workspace-sidebar.folded {
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
|
|
@ -39,6 +39,13 @@
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
}
|
}
|
||||||
|
.sidebar-control-row {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
.sidebar-frame.folded .sidebar-control-row {
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
.sidebar-title-row {
|
.sidebar-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -84,13 +91,13 @@
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.sidebar-fold-button {
|
.sidebar-fold-button {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.sidebar-icon {
|
.sidebar-icon {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|
@ -107,25 +114,6 @@
|
||||||
background: var(--interactive-hover);
|
background: var(--interactive-hover);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
.workspace-sidebar.folded {
|
|
||||||
overflow: hidden;
|
|
||||||
padding-inline: var(--space-2);
|
|
||||||
}
|
|
||||||
.workspace-sidebar.folded .sidebar-header {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.workspace-sidebar.folded .sidebar-control-row,
|
|
||||||
.workspace-sidebar.folded .sidebar-title-row,
|
|
||||||
.workspace-sidebar.folded .sidebar-actions-row {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.workspace-sidebar.folded .sidebar-actions-row {
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.workspace-sidebar.folded .workspace-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.sidebar-sections {
|
.sidebar-sections {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-5);
|
gap: var(--space-5);
|
||||||
|
|
@ -249,8 +237,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.global-sidebar,
|
.sidebar-frame,
|
||||||
.workspace-sidebar {
|
.sidebar-frame.folded {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
|
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
|
||||||
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
|
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
|
||||||
|
import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte';
|
||||||
import { SIDEBAR_CONTEXT, type SidebarSnippet } from '$lib/workspace/sidebar/context';
|
import { SIDEBAR_CONTEXT, type SidebarSnippet } from '$lib/workspace/sidebar/context';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import type { LayoutProps } from './$types';
|
import type { LayoutProps } from './$types';
|
||||||
|
|
@ -23,11 +24,13 @@
|
||||||
<WorkspaceAlerts />
|
<WorkspaceAlerts />
|
||||||
|
|
||||||
<div class="workspace-layout">
|
<div class="workspace-layout">
|
||||||
{#if sidebar}
|
<SidebarFrame>
|
||||||
{@render sidebar()}
|
{#if sidebar}
|
||||||
{:else}
|
{@render sidebar()}
|
||||||
<GlobalSidebar currentPath={page.url.pathname} />
|
{:else}
|
||||||
{/if}
|
<GlobalSidebar currentPath={page.url.pathname} />
|
||||||
|
{/if}
|
||||||
|
</SidebarFrame>
|
||||||
<header class="workspace-topbar">
|
<header class="workspace-topbar">
|
||||||
<nav class="workspace-topbar-actions" aria-label="Global navigation">
|
<nav class="workspace-topbar-actions" aria-label="Global navigation">
|
||||||
<a class="topbar-icon-button" href="/account" aria-label="Open Account" title="Account">
|
<a class="topbar-icon-button" href="/account" aria-label="Open Account" title="Account">
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,6 @@
|
||||||
import type { LayoutProps } from './$types';
|
import type { LayoutProps } from './$types';
|
||||||
|
|
||||||
let { data, children }: LayoutProps = $props();
|
let { data, children }: LayoutProps = $props();
|
||||||
let sidebarFolded = $state(false);
|
|
||||||
|
|
||||||
function toggleSidebarFold() {
|
|
||||||
sidebarFolded = !sidebarFolded;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet workspaceSidebar()}
|
{#snippet workspaceSidebar()}
|
||||||
|
|
@ -19,8 +14,6 @@
|
||||||
repositories={data.repositories ?? null}
|
repositories={data.repositories ?? null}
|
||||||
repositoriesError={data.repositoriesError ?? null}
|
repositoriesError={data.repositoriesError ?? null}
|
||||||
currentPath={page.url.pathname}
|
currentPath={page.url.pathname}
|
||||||
folded={sidebarFolded}
|
|
||||||
onToggleFold={toggleSidebarFold}
|
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user