merge: workspace sidebar navigation
This commit is contained in:
commit
613f412659
|
|
@ -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>
|
||||||
157
web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
Normal file
157
web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ListResponse, Worker } from './types';
|
||||||
|
|
||||||
|
const MAX_VISIBLE_WORKERS = 6;
|
||||||
|
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let workers = $state<Worker[]>([]);
|
||||||
|
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()) as ListResponse<Worker>;
|
||||||
|
workers = Array.isArray(payload.items) ? payload.items.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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.worker_id)}
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="item-title">{worker.label}</span>
|
||||||
|
<span class="item-meta">
|
||||||
|
{worker.state} · {worker.status}{worker.role ? ` · ${worker.role}` : ''}
|
||||||
|
</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>
|
||||||
82
web/workspace/src/lib/workspace-sidebar/types.ts
Normal file
82
web/workspace/src/lib/workspace-sidebar/types.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
export type ExtensionPoint = {
|
||||||
|
status: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
display_name: string;
|
||||||
|
record_authority: string;
|
||||||
|
extension_points: {
|
||||||
|
event_stream: ExtensionPoint;
|
||||||
|
host_worker_bridge: ExtensionPoint;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Diagnostic = {
|
||||||
|
code: string;
|
||||||
|
severity: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Host = {
|
||||||
|
host_id: string;
|
||||||
|
label: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
observed_at: string;
|
||||||
|
last_seen_at: string;
|
||||||
|
capabilities: {
|
||||||
|
local_pod_inspection: string;
|
||||||
|
workspace_root: string;
|
||||||
|
os: string;
|
||||||
|
arch: string;
|
||||||
|
max_workers: number;
|
||||||
|
};
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Worker = {
|
||||||
|
worker_id: string;
|
||||||
|
host_id: string;
|
||||||
|
label: string;
|
||||||
|
pod_name: string;
|
||||||
|
role?: string;
|
||||||
|
profile?: string;
|
||||||
|
workspace_root?: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
last_seen_at?: string;
|
||||||
|
implementation: { kind: string; pod_name: string };
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListResponse<T> = {
|
||||||
|
workspace_id: string;
|
||||||
|
limit: number;
|
||||||
|
items: T[];
|
||||||
|
source: string;
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
@ -1,59 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type WorkspaceResponse = {
|
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||||
workspace_id: string;
|
import type { Diagnostic, Host, ListResponse, Worker, WorkspaceResponse } from '$lib/workspace-sidebar/types';
|
||||||
display_name: string;
|
|
||||||
record_authority: string;
|
|
||||||
extension_points: {
|
|
||||||
event_stream: { status: string; note: string };
|
|
||||||
host_worker_bridge: { status: string; note: string };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Diagnostic = {
|
|
||||||
code: string;
|
|
||||||
severity: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Host = {
|
|
||||||
host_id: string;
|
|
||||||
label: string;
|
|
||||||
kind: string;
|
|
||||||
status: string;
|
|
||||||
observed_at: string;
|
|
||||||
last_seen_at: string;
|
|
||||||
capabilities: {
|
|
||||||
local_pod_inspection: string;
|
|
||||||
workspace_root: string;
|
|
||||||
os: string;
|
|
||||||
arch: string;
|
|
||||||
max_workers: number;
|
|
||||||
};
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Worker = {
|
|
||||||
worker_id: string;
|
|
||||||
host_id: string;
|
|
||||||
label: string;
|
|
||||||
pod_name: string;
|
|
||||||
role?: string;
|
|
||||||
profile?: string;
|
|
||||||
workspace_root?: string;
|
|
||||||
state: string;
|
|
||||||
status: string;
|
|
||||||
last_seen_at?: string;
|
|
||||||
implementation: { kind: string; pod_name: string };
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ListResponse<T> = {
|
|
||||||
workspace_id: string;
|
|
||||||
limit: number;
|
|
||||||
items: T[];
|
|
||||||
source: string;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoints = [
|
const endpoints = [
|
||||||
{ label: 'Workspace', path: '/api/workspace' },
|
{ label: 'Workspace', path: '/api/workspace' },
|
||||||
|
|
@ -67,7 +14,9 @@
|
||||||
let workspace = $state<WorkspaceResponse | null>(null);
|
let workspace = $state<WorkspaceResponse | null>(null);
|
||||||
let hosts = $state<ListResponse<Host> | null>(null);
|
let hosts = $state<ListResponse<Host> | null>(null);
|
||||||
let workers = $state<ListResponse<Worker> | null>(null);
|
let workers = $state<ListResponse<Worker> | null>(null);
|
||||||
let loadError = $state<string | null>(null);
|
let workspaceError = $state<string | null>(null);
|
||||||
|
let hostsError = $state<string | null>(null);
|
||||||
|
let workersError = $state<string | null>(null);
|
||||||
|
|
||||||
async function getJson<T>(path: string): Promise<T> {
|
async function getJson<T>(path: string): Promise<T> {
|
||||||
const response = await fetch(path);
|
const response = await fetch(path);
|
||||||
|
|
@ -78,18 +27,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkspace() {
|
async function loadWorkspace() {
|
||||||
|
workspaceError = null;
|
||||||
try {
|
try {
|
||||||
const [workspaceResponse, hostResponse, workerResponse] = await Promise.all([
|
workspace = await getJson<WorkspaceResponse>('/api/workspace');
|
||||||
getJson<WorkspaceResponse>('/api/workspace'),
|
|
||||||
getJson<ListResponse<Host>>('/api/hosts'),
|
|
||||||
getJson<ListResponse<Worker>>('/api/workers')
|
|
||||||
]);
|
|
||||||
workspace = workspaceResponse;
|
|
||||||
hosts = hostResponse;
|
|
||||||
workers = workerResponse;
|
|
||||||
loadError = null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loadError = error instanceof Error ? error.message : String(error);
|
workspaceError = error instanceof Error ? error.message : String(error);
|
||||||
|
workspace = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHosts() {
|
||||||
|
hostsError = null;
|
||||||
|
try {
|
||||||
|
hosts = await getJson<ListResponse<Host>>('/api/hosts');
|
||||||
|
} catch (error) {
|
||||||
|
hostsError = error instanceof Error ? error.message : String(error);
|
||||||
|
hosts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkers() {
|
||||||
|
workersError = null;
|
||||||
|
try {
|
||||||
|
workers = await getJson<ListResponse<Worker>>('/api/workers');
|
||||||
|
} catch (error) {
|
||||||
|
workersError = error instanceof Error ? error.message : String(error);
|
||||||
|
workers = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +62,8 @@
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
void loadWorkspace();
|
void loadWorkspace();
|
||||||
|
void loadHosts();
|
||||||
|
void loadWorkers();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -110,174 +75,182 @@
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="shell">
|
<div class="workspace-layout">
|
||||||
<section class="hero">
|
<WorkspaceSidebar {workspace} {workspaceError} />
|
||||||
<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
|
|
||||||
and the local Host / Worker execution view through bounded backend APIs.
|
|
||||||
Ticket and Objective lifecycle authority stays in the existing local record
|
|
||||||
workflow.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card">
|
<main class="shell">
|
||||||
<h2>Workspace</h2>
|
<section class="hero">
|
||||||
{#if workspace}
|
<p class="eyebrow">Local / single-workspace bootstrap</p>
|
||||||
<dl>
|
<h1>Yoi Workspace Control Plane</h1>
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<dt>Host / Worker bridge</dt>
|
|
||||||
<dd>{workspace.extension_points.host_worker_bridge.status}</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>
|
<p>
|
||||||
Event streams remain represented as extension-point state in the backend
|
Static SPA shell for reading canonical <code>.yoi</code> project records
|
||||||
response. Hosts and Workers are read-only local observations; no
|
and the local Host / Worker execution view through bounded backend APIs.
|
||||||
scheduler, lifecycle control, or hosted multi-tenant behavior is
|
Ticket and Objective lifecycle authority stays in the existing local record
|
||||||
implemented in this slice.
|
workflow.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="grid runtime">
|
<section class="card">
|
||||||
<div class="card">
|
<h2>Workspace</h2>
|
||||||
<h2>Hosts</h2>
|
{#if workspace}
|
||||||
{#if hosts}
|
<dl>
|
||||||
{#if hosts.items.length === 0}
|
<div>
|
||||||
<p>No local Hosts are visible.</p>
|
<dt>ID</dt>
|
||||||
{:else}
|
<dd>{workspace.workspace_id}</dd>
|
||||||
<div class="stack">
|
|
||||||
{#each hosts.items as host}
|
|
||||||
<article class="runtime-card">
|
|
||||||
<div class="runtime-heading">
|
|
||||||
<strong>{host.label}</strong>
|
|
||||||
<span class:warn={host.status !== 'available'}>{host.status}</span>
|
|
||||||
</div>
|
|
||||||
<dl>
|
|
||||||
<div>
|
|
||||||
<dt>ID</dt>
|
|
||||||
<dd><code>{host.host_id}</code></dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Kind</dt>
|
|
||||||
<dd>{host.kind}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Local Pod inspection</dt>
|
|
||||||
<dd>{host.capabilities.local_pod_inspection}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Platform</dt>
|
|
||||||
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</article>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div>
|
||||||
{:else if loadError}
|
<dt>Name</dt>
|
||||||
<p class="error">{loadError}</p>
|
<dd>{workspace.display_name}</dd>
|
||||||
{:else}
|
|
||||||
<p>Waiting for <code>/api/hosts</code>…</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>Workers</h2>
|
|
||||||
{#if workers}
|
|
||||||
{#if workers.items.length === 0}
|
|
||||||
<p>No local Workers are visible.</p>
|
|
||||||
{:else}
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Worker</th>
|
|
||||||
<th>Host</th>
|
|
||||||
<th>State</th>
|
|
||||||
<th>Workspace</th>
|
|
||||||
<th>Implementation</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each workers.items as worker}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong>{worker.label}</strong>
|
|
||||||
{#if worker.role || worker.profile}
|
|
||||||
<small>{worker.role ?? 'role unknown'} / {worker.profile ?? 'profile unknown'}</small>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
<td><code>{worker.host_id}</code></td>
|
|
||||||
<td>{worker.state} · {worker.status}</td>
|
|
||||||
<td>{worker.workspace_root ?? 'unknown'}</td>
|
|
||||||
<td>{worker.implementation.kind}: {worker.implementation.pod_name}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div>
|
||||||
{:else if loadError}
|
<dt>Record authority</dt>
|
||||||
<p class="error">{loadError}</p>
|
<dd>{workspace.record_authority}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Host / Worker bridge</dt>
|
||||||
|
<dd>{workspace.extension_points.host_worker_bridge.status}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
{:else if workspaceError}
|
||||||
|
<p class="error">{workspaceError}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Waiting for <code>/api/workers</code>…</p>
|
<p>Waiting for <code>/api/workspace</code>…</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if hosts || workers}
|
<section class="grid">
|
||||||
{@const diagnostics = diagnosticsFor(hosts?.diagnostics, workers?.diagnostics)}
|
<div class="card">
|
||||||
{#if diagnostics.length > 0}
|
<h2>Read API surface</h2>
|
||||||
<section class="card diagnostics">
|
|
||||||
<h2>Diagnostics</h2>
|
|
||||||
<ul>
|
<ul>
|
||||||
{#each diagnostics as diagnostic}
|
{#each endpoints as endpoint}
|
||||||
<li>
|
<li><code>{endpoint.path}</code> — {endpoint.label}</li>
|
||||||
<strong>{diagnostic.severity}</strong>
|
|
||||||
<code>{diagnostic.code}</code>
|
|
||||||
<span>{diagnostic.message}</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Reserved seams</h2>
|
||||||
|
<p>
|
||||||
|
Event streams remain represented as extension-point state in the backend
|
||||||
|
response. Hosts and Workers are read-only local observations; no
|
||||||
|
scheduler, lifecycle control, or hosted multi-tenant behavior is
|
||||||
|
implemented in this slice.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid runtime">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Hosts</h2>
|
||||||
|
{#if hosts}
|
||||||
|
{#if hosts.items.length === 0}
|
||||||
|
<p>No local Hosts are visible.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="stack">
|
||||||
|
{#each hosts.items as host}
|
||||||
|
<article class="runtime-card">
|
||||||
|
<div class="runtime-heading">
|
||||||
|
<strong>{host.label}</strong>
|
||||||
|
<span class:warn={host.status !== 'available'}>{host.status}</span>
|
||||||
|
</div>
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>ID</dt>
|
||||||
|
<dd><code>{host.host_id}</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Kind</dt>
|
||||||
|
<dd>{host.kind}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Local inspection</dt>
|
||||||
|
<dd>{host.capabilities.local_pod_inspection}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Platform</dt>
|
||||||
|
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if hostsError}
|
||||||
|
<p class="error">{hostsError}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for <code>/api/hosts</code>…</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Workers</h2>
|
||||||
|
{#if workers}
|
||||||
|
{#if workers.items.length === 0}
|
||||||
|
<p>No local Workers are visible.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Worker</th>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Workspace</th>
|
||||||
|
<th>Implementation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each workers.items as worker}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{worker.label}</strong>
|
||||||
|
{#if worker.role || worker.profile}
|
||||||
|
<small>{worker.role ?? 'role unknown'} / {worker.profile ?? 'profile unknown'}</small>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td><code>{worker.host_id}</code></td>
|
||||||
|
<td>{worker.state} · {worker.status}</td>
|
||||||
|
<td>{worker.workspace_root ?? 'unknown'}</td>
|
||||||
|
<td>{worker.implementation.kind}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if workersError}
|
||||||
|
<p class="error">{workersError}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for <code>/api/workers</code>…</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if hosts || workers}
|
||||||
|
{@const diagnostics = diagnosticsFor(hosts?.diagnostics, workers?.diagnostics)}
|
||||||
|
{#if diagnostics.length > 0}
|
||||||
|
<section class="card diagnostics">
|
||||||
|
<h2>Diagnostics</h2>
|
||||||
|
<ul>
|
||||||
|
{#each diagnostics as diagnostic}
|
||||||
|
<li>
|
||||||
|
<strong>{diagnostic.severity}</strong>
|
||||||
|
<code>{diagnostic.code}</code>
|
||||||
|
<span>{diagnostic.message}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</main>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:global(*) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
:global(body) {
|
:global(body) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
|
|
@ -286,14 +259,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(1120px, calc(100vw - 32px));
|
display: grid;
|
||||||
|
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
width: min(1240px, 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 {
|
||||||
|
|
@ -307,12 +294,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;
|
||||||
}
|
}
|
||||||
|
|
@ -320,12 +314,12 @@
|
||||||
.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));
|
||||||
margin-top: 16px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runtime {
|
.runtime {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -334,6 +328,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack {
|
.stack {
|
||||||
|
|
@ -421,4 +416,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