feat: add workspace sidebar skeleton
This commit is contained in:
parent
2c7ef24a29
commit
d3b8bdfddc
|
|
@ -0,0 +1,157 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ObjectiveListResponse, ObjectiveSummary } from './types';
|
||||||
|
|
||||||
|
const MAX_VISIBLE_OBJECTIVES = 6;
|
||||||
|
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let objectives = $state<ObjectiveSummary[]>([]);
|
||||||
|
let invalidRecordCount = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
void loadObjectives(controller.signal);
|
||||||
|
return () => controller.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadObjectives(signal: AbortSignal) {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/objectives', { signal });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`objectives request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
const payload = (await response.json()) as ObjectiveListResponse;
|
||||||
|
objectives = Array.isArray(payload.items) ? payload.items.slice(0, MAX_VISIBLE_OBJECTIVES) : [];
|
||||||
|
invalidRecordCount = Array.isArray(payload.invalid_records) ? payload.invalid_records.length : 0;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error = err instanceof Error ? err.message : 'objectives request failed';
|
||||||
|
objectives = [];
|
||||||
|
invalidRecordCount = 0;
|
||||||
|
} finally {
|
||||||
|
if (!signal.aborted) {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="nav-section" aria-labelledby="objectives-heading">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 id="objectives-heading">objectives</h2>
|
||||||
|
{#if !loading && !error}
|
||||||
|
<span class="section-count">{objectives.length}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<p class="section-state">Loading objectives…</p>
|
||||||
|
{:else if error}
|
||||||
|
<p class="section-state error">{error}</p>
|
||||||
|
{:else if objectives.length === 0}
|
||||||
|
<p class="section-state">No objectives found.</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="nav-list" aria-label="Objectives">
|
||||||
|
{#each objectives as objective (objective.id)}
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="item-title">{objective.title}</span>
|
||||||
|
<span class="item-meta">{objective.state}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if invalidRecordCount > 0}
|
||||||
|
<p class="section-note">{invalidRecordCount} invalid objective record(s) hidden.</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.64);
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title,
|
||||||
|
.item-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta,
|
||||||
|
.section-state,
|
||||||
|
.section-note {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state,
|
||||||
|
.section-note {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state {
|
||||||
|
border: 1px dashed rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state.error {
|
||||||
|
border-color: rgba(248, 113, 113, 0.36);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { WorkspaceResponse } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspace: WorkspaceResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { workspace }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="nav-section" aria-labelledby="repositories-heading">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 id="repositories-heading">repositories</h2>
|
||||||
|
<span class="section-count">1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav-list" aria-label="Repositories">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<span class="item-title">{workspace?.display_name ?? 'local workspace'}</span>
|
||||||
|
<span class="item-meta">local project records</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="section-note">
|
||||||
|
Repository API is not wired yet; this placeholder keeps the navigation seam
|
||||||
|
ready without adding repository authority.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.64);
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
border-color: rgba(56, 189, 248, 0.36);
|
||||||
|
background: rgba(14, 165, 233, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title,
|
||||||
|
.item-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta,
|
||||||
|
.section-note {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-note {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
198
web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
Normal file
198
web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { WorkerSummary } from './types';
|
||||||
|
|
||||||
|
const MAX_VISIBLE_WORKERS = 6;
|
||||||
|
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let workers = $state<WorkerSummary[]>([]);
|
||||||
|
let placeholder = $state<string | null>(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
void loadWorkers(controller.signal);
|
||||||
|
return () => controller.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadWorkers(signal: AbortSignal) {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
placeholder = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/workers', { signal });
|
||||||
|
if (response.status === 404) {
|
||||||
|
workers = [];
|
||||||
|
placeholder = 'Worker API is not integrated in this build yet.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`workers request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
const payload = await response.json();
|
||||||
|
workers = normalizeWorkers(payload).slice(0, MAX_VISIBLE_WORKERS);
|
||||||
|
if (workers.length === 0) {
|
||||||
|
placeholder = 'No workers reported by the current API.';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error = err instanceof Error ? err.message : 'workers request failed';
|
||||||
|
workers = [];
|
||||||
|
} finally {
|
||||||
|
if (!signal.aborted) {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorkers(payload: unknown): WorkerSummary[] {
|
||||||
|
const items = Array.isArray(payload)
|
||||||
|
? payload
|
||||||
|
: isRecord(payload) && Array.isArray(payload.items)
|
||||||
|
? payload.items
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return items.map((item, index) => normalizeWorker(item, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorker(item: unknown, index: number): WorkerSummary {
|
||||||
|
if (!isRecord(item)) {
|
||||||
|
return {
|
||||||
|
id: `worker-${index + 1}`,
|
||||||
|
label: `worker ${index + 1}`,
|
||||||
|
status: 'unknown'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = readText(item, ['id', 'worker_id', 'name']) ?? `worker-${index + 1}`;
|
||||||
|
const label = readText(item, ['display_name', 'label', 'name', 'worker_id', 'id']) ?? id;
|
||||||
|
const status = readText(item, ['status', 'state', 'lifecycle']) ?? 'unknown';
|
||||||
|
const detail = readText(item, ['role', 'profile', 'note']);
|
||||||
|
|
||||||
|
return { id, label, status, detail };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readText(record: Record<string, unknown>, keys: string[]): string | null {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="nav-section" aria-labelledby="workers-heading">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 id="workers-heading">workers</h2>
|
||||||
|
{#if !loading && !error && workers.length > 0}
|
||||||
|
<span class="section-count">{workers.length}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<p class="section-state">Checking workers…</p>
|
||||||
|
{:else if error}
|
||||||
|
<p class="section-state error">{error}</p>
|
||||||
|
{:else if workers.length === 0}
|
||||||
|
<p class="section-state">{placeholder ?? 'Workers will appear here when an API is connected.'}</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="nav-list" aria-label="Workers">
|
||||||
|
{#each workers as worker (worker.id)}
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="item-title">{worker.label}</span>
|
||||||
|
<span class="item-meta">
|
||||||
|
{worker.status}{worker.detail ? ` · ${worker.detail}` : ''}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.64);
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title,
|
||||||
|
.item-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta,
|
||||||
|
.section-state {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px dashed rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state.error {
|
||||||
|
border-color: rgba(248, 113, 113, 0.36);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
Normal file
124
web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||||
|
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
||||||
|
import WorkersNavSection from './WorkersNavSection.svelte';
|
||||||
|
import type { WorkspaceResponse } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspace: WorkspaceResponse | null;
|
||||||
|
workspaceError?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { workspace, workspaceError = null }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside class="workspace-sidebar" aria-label="Workspace navigation">
|
||||||
|
<header class="sidebar-header">
|
||||||
|
<div class="workspace-label">
|
||||||
|
<span class="eyebrow">workspace</span>
|
||||||
|
<h1>{workspace?.display_name ?? 'Yoi workspace'}</h1>
|
||||||
|
{#if workspaceError}
|
||||||
|
<p class="workspace-status error">Workspace summary unavailable.</p>
|
||||||
|
{:else if workspace}
|
||||||
|
<p class="workspace-status">{workspace.workspace_id}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="workspace-status">Loading workspace…</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="settings-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Workspace settings"
|
||||||
|
title="Workspace settings placeholder"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
⚙
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||||
|
<RepositoriesNavSection {workspace} />
|
||||||
|
<ObjectivesNavSection />
|
||||||
|
<WorkersNavSection />
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.workspace-sidebar {
|
||||||
|
align-self: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 26px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.82)),
|
||||||
|
rgba(15, 23, 42, 0.88);
|
||||||
|
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.28);
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-label {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: #38bdf8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
.workspace-status {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-status {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-status.error {
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(15, 23, 42, 0.7);
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sections {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
web/workspace/src/lib/workspace-sidebar/types.ts
Normal file
38
web/workspace/src/lib/workspace-sidebar/types.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
export type WorkspaceResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
display_name: string;
|
||||||
|
record_authority: string;
|
||||||
|
extension_points: {
|
||||||
|
event_stream: { status: string; note: string };
|
||||||
|
runner_connection: { status: string; note: string };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ObjectiveSummary = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
updated_at?: string | null;
|
||||||
|
linked_tickets?: string[];
|
||||||
|
record_source?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InvalidProjectRecord = {
|
||||||
|
label: string;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ObjectiveListResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
limit: number;
|
||||||
|
items: ObjectiveSummary[];
|
||||||
|
invalid_records: InvalidProjectRecord[];
|
||||||
|
record_authority: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkerSummary = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
detail?: string | null;
|
||||||
|
};
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type WorkspaceResponse = {
|
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||||
workspace_id: string;
|
import type { WorkspaceResponse } from '$lib/workspace-sidebar/types';
|
||||||
display_name: string;
|
|
||||||
record_authority: string;
|
|
||||||
extension_points: {
|
|
||||||
event_stream: { status: string; note: string };
|
|
||||||
runner_connection: { status: string; note: string };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoints = [
|
const endpoints = [
|
||||||
{ label: 'Workspace', path: '/api/workspace' },
|
{ label: 'Workspace', path: '/api/workspace' },
|
||||||
|
|
@ -21,6 +14,7 @@
|
||||||
let loadError = $state<string | null>(null);
|
let loadError = $state<string | null>(null);
|
||||||
|
|
||||||
async function loadWorkspace() {
|
async function loadWorkspace() {
|
||||||
|
loadError = null;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/workspace');
|
const response = await fetch('/api/workspace');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -45,7 +39,10 @@
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="shell">
|
<div class="workspace-layout">
|
||||||
|
<WorkspaceSidebar {workspace} workspaceError={loadError} />
|
||||||
|
|
||||||
|
<main class="shell">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<p class="eyebrow">Local / single-workspace bootstrap</p>
|
<p class="eyebrow">Local / single-workspace bootstrap</p>
|
||||||
<h1>Yoi Workspace Control Plane</h1>
|
<h1>Yoi Workspace Control Plane</h1>
|
||||||
|
|
@ -99,9 +96,14 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:global(*) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
:global(body) {
|
:global(body) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
|
|
@ -110,14 +112,28 @@
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.workspace-layout {
|
||||||
width: min(980px, calc(100vw - 32px));
|
display: grid;
|
||||||
|
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
width: min(1180px, calc(100vw - 32px));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 48px 0;
|
padding: 32px 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
margin-bottom: 24px;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
max-width: 68ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
|
|
@ -131,12 +147,19 @@
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
font-size: clamp(2.5rem, 8vw, 5rem);
|
font-size: clamp(2.5rem, 8vw, 5rem);
|
||||||
line-height: 0.95;
|
line-height: 0.95;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
li,
|
||||||
|
dd {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
color: #bae6fd;
|
color: #bae6fd;
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +167,8 @@
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -153,6 +177,7 @@
|
||||||
background: rgba(15, 23, 42, 0.75);
|
background: rgba(15, 23, 42, 0.75);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
|
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
|
|
@ -173,4 +198,17 @@
|
||||||
.error {
|
.error {
|
||||||
color: #fca5a5;
|
color: #fca5a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.workspace-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: min(100vw - 24px, 620px);
|
||||||
|
gap: 18px;
|
||||||
|
padding: 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user