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">
|
||||
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 };
|
||||
};
|
||||
};
|
||||
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||
import type { WorkspaceResponse } from '$lib/workspace-sidebar/types';
|
||||
|
||||
const endpoints = [
|
||||
{ label: 'Workspace', path: '/api/workspace' },
|
||||
|
|
@ -21,6 +14,7 @@
|
|||
let loadError = $state<string | null>(null);
|
||||
|
||||
async function loadWorkspace() {
|
||||
loadError = null;
|
||||
try {
|
||||
const response = await fetch('/api/workspace');
|
||||
if (!response.ok) {
|
||||
|
|
@ -45,63 +39,71 @@
|
|||
/>
|
||||
</svelte:head>
|
||||
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<p class="eyebrow">Local / single-workspace bootstrap</p>
|
||||
<h1>Yoi Workspace Control Plane</h1>
|
||||
<p>
|
||||
Static SPA shell for reading canonical <code>.yoi</code> project records
|
||||
through bounded backend APIs. Ticket and Objective lifecycle authority stays
|
||||
in the existing local record workflow.
|
||||
</p>
|
||||
</section>
|
||||
<div class="workspace-layout">
|
||||
<WorkspaceSidebar {workspace} workspaceError={loadError} />
|
||||
|
||||
<section class="card">
|
||||
<h2>Workspace</h2>
|
||||
{#if workspace}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd>{workspace.workspace_id}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Name</dt>
|
||||
<dd>{workspace.display_name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Record authority</dt>
|
||||
<dd>{workspace.record_authority}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{:else if loadError}
|
||||
<p class="error">{loadError}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/workspace</code>…</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<div class="card">
|
||||
<h2>Read API surface</h2>
|
||||
<ul>
|
||||
{#each endpoints as endpoint}
|
||||
<li><code>{endpoint.path}</code> — {endpoint.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Reserved seams</h2>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<p class="eyebrow">Local / single-workspace bootstrap</p>
|
||||
<h1>Yoi Workspace Control Plane</h1>
|
||||
<p>
|
||||
Event streams and runner connections are represented as extension-point
|
||||
state in the backend response, but no scheduler, write API, or hosted
|
||||
multi-tenant behavior is implemented in this slice.
|
||||
Static SPA shell for reading canonical <code>.yoi</code> project records
|
||||
through bounded backend APIs. Ticket and Objective lifecycle authority stays
|
||||
in the existing local record workflow.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Workspace</h2>
|
||||
{#if workspace}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd>{workspace.workspace_id}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Name</dt>
|
||||
<dd>{workspace.display_name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Record authority</dt>
|
||||
<dd>{workspace.record_authority}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{:else if loadError}
|
||||
<p class="error">{loadError}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/workspace</code>…</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<div class="card">
|
||||
<h2>Read API surface</h2>
|
||||
<ul>
|
||||
{#each endpoints as endpoint}
|
||||
<li><code>{endpoint.path}</code> — {endpoint.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Reserved seams</h2>
|
||||
<p>
|
||||
Event streams and runner connections are represented as extension-point
|
||||
state in the backend response, but no scheduler, write API, or hosted
|
||||
multi-tenant behavior is implemented in this slice.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(*) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
background: #0f172a;
|
||||
|
|
@ -110,14 +112,28 @@
|
|||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(980px, calc(100vw - 32px));
|
||||
.workspace-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
width: min(1180px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 48px 0;
|
||||
padding: 32px 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
max-width: 68ch;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
|
|
@ -131,12 +147,19 @@
|
|||
margin: 0 0 16px;
|
||||
font-size: clamp(2.5rem, 8vw, 5rem);
|
||||
line-height: 0.95;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p,
|
||||
li,
|
||||
dd {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #bae6fd;
|
||||
}
|
||||
|
|
@ -144,7 +167,8 @@
|
|||
.grid {
|
||||
display: grid;
|
||||
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 {
|
||||
|
|
@ -153,6 +177,7 @@
|
|||
background: rgba(15, 23, 42, 0.75);
|
||||
padding: 24px;
|
||||
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
dl {
|
||||
|
|
@ -173,4 +198,17 @@
|
|||
.error {
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user