-
Logs
-
- Runtime events for {project.name} · {environment}
-
+
+
+
+
+
Logs
+
+ Deployment events for {project.name} ·{" "}
+ {environment}
+
+
-
-
- Filter logs by resource
- setResourceFilter(event.target.value)}
+
-
- {visibleDeployments.length > 0 ? (
- visibleDeployments.map(({ service, deployment }) => (
-
-
{service.name}
-
- {!live && (
-
- Live tail paused; showing the last saved deployment snapshot.
+
+ {live ? "Live" : "Paused"}
+
+
+
+ {visibleDeployments.length > 0 ? (
+ visibleDeployments.map(({ service, deployment }) => (
+
+
{service.name}
+
+ {!live && (
+
+ Live tail paused; showing the last saved deployment
+ snapshot.
+
+ )}
+
+ ))
+ ) : (
+
+
+
- )}
-
- ))
- ) : (
-
-
-
-
-
No logs yet
-
- Logs will appear here once{" "}
- {resourceFilter === "all" ? "a resource" : "this resource"} starts
- handling traffic.
-
- {(search || !live) && (
-
- {search ? `Filtering for “${search}” · ` : ""}
- {live ? "Live tail enabled" : "Live tail paused"}
-
+
No logs yet
+
+ Logs will appear here once{" "}
+ {resourceFilter === "all" ? "a resource" : "this resource"}{" "}
+ starts handling traffic.
+
+ {(search || !live) && (
+
+ {search ? `Filtering for “${search}” · ` : ""}
+ {live ? "Live tail enabled" : "Live tail paused"}
+
+ )}
+
)}
- )}
-
-
+
+
+
)
}
function ProjectAddOption({
label,
- mark,
+ description,
+ icon,
disabled = false,
onClick,
}: {
label: string
- mark: string
+ description: string
+ icon: ReactNode
disabled?: boolean
onClick: () => void
}) {
@@ -1739,27 +1739,21 @@ function ProjectAddOption({
- {mark}
+ {icon}
+
+
+ {label}
+ {description}
- {label}
)
}
-function NodeIcon({ nodeId }: { nodeId: TopologyNodeId }) {
- if (nodeId === "postgres") {
- return
- }
- if (nodeId === "redis") {
- return
- }
- return
-}
-
function appServiceNodeId(serviceId: string) {
return `app:${serviceId}`
}
@@ -1830,15 +1824,3 @@ function isTopologyNodeId(value: unknown): value is TopologyNodeId {
(typeof value === "string" && value.startsWith("app:") && value.length > 4)
)
}
-
-function getInitial(value: string) {
- return value.trim().charAt(0).toUpperCase() || "K"
-}
-
-function readStoredTheme(key: string): Theme {
- try {
- return window.localStorage.getItem(key) === "dark" ? "dark" : "light"
- } catch {
- return "light"
- }
-}
diff --git a/apps/web/src/pages/project-home.css b/apps/web/src/pages/project-home.css
index b92a4cf..fdaa3fa 100644
--- a/apps/web/src/pages/project-home.css
+++ b/apps/web/src/pages/project-home.css
@@ -1,2182 +1,1167 @@
-.project-home {
- --project-bg: #ffffff;
- --project-fg: #111111;
- --project-accent: #1677ff;
- --project-surface: #f7f8fa;
- --project-muted: #6b7280;
- --project-border: #d9dee7;
- --project-ok: #16803c;
- --project-ok-soft: #e9f7ee;
- --project-warn: #b45309;
- --project-danger: #c2410c;
- --project-accent-soft: color-mix(
- in oklch,
- var(--project-accent) 10%,
- var(--project-bg)
- );
+/*
+ * Project dashboard. Layout chrome comes from the app shell (index.css); this
+ * file owns the project views and the topology canvas.
+ *
+ * The --project-* aliases keep the resource workspace and older component
+ * styles on the shared design tokens in both themes.
+ */
+:root {
+ --project-bg: var(--background);
+ --project-fg: var(--foreground);
+ --project-accent: var(--brand);
+ --project-surface: var(--surface);
+ --project-muted: var(--muted-foreground);
+ --project-border: var(--border);
+ --project-ok: var(--ok);
+ --project-ok-soft: color-mix(in oklch, var(--ok) 10%, var(--background));
+ --project-warn: var(--warn);
+ --project-danger: var(--danger);
+ --project-accent-soft: var(--brand-soft);
--project-accent-soft-strong: color-mix(
in oklch,
- var(--project-accent) 16%,
- var(--project-bg)
+ var(--brand) 16%,
+ var(--background)
);
- --project-shadow: 0 14px 32px
- color-mix(in oklch, var(--project-fg) 10%, transparent);
- --project-shadow-soft: 0 5px 16px
- color-mix(in oklch, var(--project-fg) 7%, transparent);
- --project-radius: 8px;
- --project-rail-width: 224px;
- --project-topbar-height: 64px;
- --project-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
- position: relative;
- min-height: 100svh;
- height: 100svh;
- overflow: hidden;
- background: var(--project-bg);
- color: var(--project-fg);
- font-family: var(--font-sans);
- font-size: 14px;
- line-height: 1.45;
- -webkit-font-smoothing: antialiased;
-}
-
-.project-home[data-theme="dark"],
-.project-topology-shell[data-canvas-theme="dark"] {
- --project-bg: #0f1216;
- --project-fg: #f4f6f8;
- --project-accent: #3b8bff;
- --project-surface: #171c22;
- --project-muted: #9aa3af;
- --project-border: #2a323c;
- --project-ok: #3dd68c;
- --project-ok-soft: #163024;
- --project-warn: #f0b429;
- --project-danger: #f07167;
-}
-
-.project-home *,
-.project-home *::before,
-.project-home *::after {
- box-sizing: border-box;
-}
-
-.project-home button {
- color: inherit;
- font: inherit;
+ --project-shadow: var(--shadow-lg);
+ --project-shadow-soft: var(--shadow-md);
+ --project-radius: 0.5rem;
+ --project-ease: var(--ease);
}
-.project-home button:focus-visible,
-.project-home a:focus-visible,
-.project-home [tabindex]:focus-visible {
- outline: 2px solid var(--project-accent);
- outline-offset: 3px;
+.project-dashboard-main {
+ display: flex;
+ min-height: 0;
+ flex: 1;
+ flex-direction: column;
}
.project-home [hidden] {
display: none !important;
}
-.project-topbar {
- position: fixed;
- z-index: 20;
- inset: 0 0 auto;
- display: flex;
- height: var(--project-topbar-height);
- align-items: center;
- justify-content: space-between;
- gap: 24px;
- padding: 0 20px;
- border-bottom: 1px solid var(--project-border);
- background: color-mix(in oklch, var(--project-bg) 96%, transparent);
- backdrop-filter: blur(12px);
-}
-
-.project-topbar-left,
-.project-topbar-right,
-.project-workspace-switcher,
-.project-workspace-name,
-.project-topbar-actions,
-.project-billing-badge {
+/* ───────────── Loading / unavailable ───────────── */
+
+.project-load-screen,
+.project-load-error {
display: flex;
+ min-height: 100svh;
+ flex-direction: column;
align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ padding: 1.5rem;
+ background: var(--canvas);
+ color: var(--muted-foreground);
+ font-size: 0.8125rem;
}
-.project-topbar-left {
- min-width: 0;
- gap: 20px;
+.project-load-screen {
+ flex-direction: row;
+ gap: 0.625rem;
}
-.project-topbar-right {
- flex: 0 0 auto;
- gap: 14px;
-}
-
-.project-brand-button,
-.project-icon-button,
-.project-context-trigger,
-.project-rail-button,
-.project-zoom-button,
-.project-layers-button,
-.project-agent-button {
- appearance: none;
- border: 0;
- background: transparent;
- cursor: pointer;
+.project-load-error {
+ color: var(--foreground);
}
-.project-brand-button {
- display: grid;
- height: 36px;
- width: 36px;
- flex: 0 0 auto;
- place-items: center;
- padding: 0;
-}
+/* ───────────── Add resource menu ───────────── */
-.project-brand-mark {
+.project-add-wrap {
position: relative;
- display: grid;
- height: 30px;
- width: 30px;
- place-items: center;
- border: 1px solid var(--project-fg);
- border-radius: 50%;
- background: var(--project-fg);
-}
-
-.project-brand-mark::before,
-.project-brand-mark::after,
-.project-brand-mark > span {
- position: absolute;
- display: block;
- height: 1px;
- width: 14px;
- background: var(--project-bg);
- content: "";
}
-.project-brand-mark::before {
- transform: translateY(-5px);
+.project-add-menu {
+ width: 18.5rem;
}
-.project-brand-mark::after {
- transform: translateY(5px);
+.project-menu-heading {
+ padding: 0.5rem 0.625rem 0.375rem;
+ color: var(--faint);
+ font-size: 0.75rem;
+ font-weight: 500;
}
-.project-topbar-divider {
- height: 24px;
- width: 1px;
- background: var(--project-border);
+.project-menu-option {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 0.625rem;
+ border-radius: 0.375rem;
+ text-align: left;
+ transition: background 120ms var(--ease);
}
-.project-workspace-switcher {
- min-width: 0;
- gap: 12px;
+.project-menu-option:hover:not(:disabled),
+.project-menu-option:focus-visible {
+ background: var(--surface-2);
+ outline: none;
}
-.project-workspace-name {
- min-width: 0;
- gap: 8px;
+.project-menu-option:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
}
-.project-avatar-dot {
+.project-menu-icon {
display: grid;
- height: 20px;
- width: 20px;
+ height: 2rem;
+ width: 2rem;
flex: 0 0 auto;
place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 50%;
- background: var(--project-surface);
- color: var(--project-fg);
- font-size: 10px;
- font-weight: 700;
- letter-spacing: -0.02em;
+ border: 1px solid var(--border);
+ border-radius: 0.4375rem;
+ background: var(--background);
+ color: var(--foreground);
}
-.project-context-trigger {
- display: inline-flex;
- min-height: 40px;
- align-items: center;
- gap: 9px;
- padding: 0 8px;
- border-radius: var(--project-radius);
- font-weight: 700;
- white-space: nowrap;
- transition: background 160ms var(--project-ease);
+.project-menu-icon svg {
+ height: 0.9375rem;
+ width: 0.9375rem;
+ stroke-width: 1.75;
}
-.project-context-trigger:hover {
- background: var(--project-surface);
+.project-menu-text {
+ display: grid;
+ min-width: 0;
+ gap: 0.0625rem;
}
-.project-workspace-name .project-context-trigger {
- padding-left: 0;
+.project-menu-text strong {
+ font-size: 0.8125rem;
+ font-weight: 500;
}
-.project-context-label {
- max-width: min(22rem, 34vw);
- overflow: hidden;
- text-overflow: ellipsis;
+.project-menu-text small {
+ color: var(--muted-foreground);
+ font-size: 0.75rem;
}
-.project-environment-trigger {
- font-size: 16px;
+/* ───────────── Status badge ───────────── */
+
+.status-badge {
+ display: inline-flex;
+ height: 1.375rem;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ background: var(--background);
+ color: var(--foreground);
+ font-size: 0.75rem;
+ font-weight: 500;
+ white-space: nowrap;
}
-.project-chevron {
- height: 14px;
- width: 14px;
+.status-badge-dot {
+ height: 0.4375rem;
+ width: 0.4375rem;
flex: 0 0 auto;
- color: var(--project-muted);
- transition: transform 160ms var(--project-ease);
+ border-radius: 999px;
+ background: var(--faint);
}
-.project-context-trigger[aria-expanded="true"] .project-chevron {
- transform: rotate(180deg);
+.status-badge.status-online .status-badge-dot,
+.status-badge.status-ready .status-badge-dot {
+ background: var(--ok);
}
-.project-project-context {
- max-width: min(24rem, 34vw);
- overflow: hidden;
- font-size: 14px;
- font-weight: 700;
- text-overflow: ellipsis;
- white-space: nowrap;
+.status-badge.status-creating .status-badge-dot {
+ background: var(--warn);
+ animation: status-pulse 1.4s ease-in-out infinite;
}
-.project-environment-context {
- padding: 5px 9px;
- border: 1px solid var(--project-border);
- border-radius: 6px;
- color: var(--project-muted);
- font-size: 12px;
- font-weight: 600;
- text-transform: capitalize;
- white-space: nowrap;
+.status-badge.status-needs-attention {
+ border-color: color-mix(in oklch, var(--danger) 30%, var(--border));
+ background: color-mix(in oklch, var(--danger) 6%, var(--background));
+ color: var(--danger);
+}
+
+.status-badge.status-needs-attention .status-badge-dot {
+ background: var(--danger);
}
-.project-topbar-actions {
- gap: 6px;
+@keyframes status-pulse {
+ 50% {
+ opacity: 0.35;
+ }
}
-.project-icon-button {
+/* ───────────── Resources view ───────────── */
+
+.project-stats {
display: grid;
- height: 36px;
- width: 36px;
- place-items: center;
- border-radius: var(--project-radius);
- color: var(--project-muted);
- transition:
- background 160ms var(--project-ease),
- color 160ms var(--project-ease);
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin: 0 0 1.5rem;
+ border: 1px solid var(--border);
+ border-radius: 0.625rem;
+ background: var(--background);
}
-.project-icon-button:hover {
- background: var(--project-surface);
- color: var(--project-fg);
+.project-stats > div {
+ display: grid;
+ gap: 0.375rem;
+ padding: 0.875rem 1.125rem;
}
-.project-icon-button svg {
- height: 18px;
- width: 18px;
- stroke-width: 1.6;
+.project-stats > div + div {
+ border-left: 1px solid var(--border);
}
-.project-billing-badge {
- min-height: 32px;
- padding: 0 10px;
- overflow: hidden;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- color: var(--project-muted);
- white-space: nowrap;
+.project-stats dt {
+ display: flex;
+ align-items: center;
+ gap: 0.4375rem;
+ color: var(--muted-foreground);
+ font-size: 0.75rem;
+ font-weight: 500;
}
-.project-billing-badge strong {
- color: var(--project-fg);
- font-weight: 700;
+.project-stats dd {
+ margin: 0;
+ font-size: 1.375rem;
+ font-variant-numeric: tabular-nums;
+ font-weight: 600;
+ letter-spacing: -0.03em;
+ line-height: 1;
}
-.project-agent-button {
- display: inline-flex;
- min-height: 40px;
- align-items: center;
- gap: 8px;
- padding: 0 2px 0 12px;
- border-top: 0;
- border-right: 0;
- border-bottom: 0;
- border-left: 1px solid var(--project-border);
- color: var(--project-muted);
- transition: color 160ms var(--project-ease);
+.project-stats .is-online dt::before,
+.project-stats .is-creating dt::before,
+.project-stats .is-attention dt::before {
+ height: 0.4375rem;
+ width: 0.4375rem;
+ border-radius: 999px;
+ content: "";
}
-.project-agent-button:hover {
- color: var(--project-fg);
+.project-stats .is-online dt::before {
+ background: var(--ok);
}
-.project-sign-out-label {
- font-size: 13px;
+.project-stats .is-creating dt::before {
+ background: var(--warn);
}
-.project-agent-avatar {
- display: grid;
- height: 26px;
- width: 26px;
- place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 50%;
- background: var(--project-surface);
- color: var(--project-fg);
- font-size: 11px;
- font-weight: 700;
+.project-stats .is-attention dt::before {
+ background: var(--danger);
}
-.project-side-rail {
- position: fixed;
- z-index: 15;
- inset: var(--project-topbar-height) auto 0 0;
- display: flex;
- width: var(--project-rail-width);
- flex-direction: column;
- align-items: stretch;
- justify-content: flex-start;
- padding: 22px 12px 16px;
- border-right: 1px solid var(--project-border);
- background: var(--project-bg);
+.project-stats .is-attention dd {
+ color: var(--danger);
}
-.project-rail-project {
+.project-resource-toolbar {
display: flex;
- min-width: 0;
- flex-direction: column;
- gap: 3px;
- margin: 0 0 18px;
- padding: 0 12px 16px;
- border-bottom: 1px solid var(--project-border);
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ margin-bottom: 0.75rem;
}
-.project-rail-project strong,
-.project-rail-project span {
- overflow: hidden;
- text-overflow: ellipsis;
+.segmented {
+ display: inline-flex;
+ padding: 0.1875rem;
+ border-radius: 0.5rem;
+ background: var(--surface-2);
+}
+
+.segmented button {
+ height: 1.625rem;
+ padding: 0 0.75rem;
+ border-radius: 0.3125rem;
+ color: var(--muted-foreground);
+ font-size: 0.8125rem;
+ font-weight: 500;
white-space: nowrap;
+ transition:
+ background 120ms var(--ease),
+ color 120ms var(--ease);
}
-.project-rail-project strong {
- font-size: 15px;
- font-weight: 700;
+.segmented button:hover {
+ color: var(--foreground);
}
-.project-rail-project span {
- color: var(--project-muted);
- font-size: 12px;
+.segmented button[aria-pressed="true"] {
+ background: var(--background);
+ box-shadow:
+ 0 0 0 1px var(--border),
+ var(--shadow-sm);
+ color: var(--foreground);
}
-.project-rail-nav {
- display: grid;
- width: 100%;
- gap: 4px;
+.data-table-wrap {
+ overflow-x: auto;
+ border: 1px solid var(--border);
+ border-radius: 0.625rem;
+ background: var(--background);
}
-.project-rail-button {
- display: flex;
+.data-table {
width: 100%;
- min-height: 44px;
- align-items: center;
- gap: 12px;
- padding: 0 12px;
- border-radius: var(--project-radius);
- color: var(--project-muted);
- font-size: 13px;
- font-weight: 600;
+ border-collapse: collapse;
+ font-size: 0.8125rem;
text-align: left;
- transition:
- background 160ms var(--project-ease),
- color 160ms var(--project-ease),
- transform 160ms var(--project-ease);
}
-.project-rail-button:hover {
- background: var(--project-surface);
- color: var(--project-fg);
+.data-table thead th {
+ height: 2.25rem;
+ padding: 0 1rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--muted-foreground);
+ font-size: 0.75rem;
+ font-weight: 500;
+ white-space: nowrap;
}
-.project-rail-button[aria-current="page"] {
- color: var(--project-fg);
- background: var(--project-accent-soft);
- box-shadow: inset 0 0 0 1px
- color-mix(in oklch, var(--project-accent) 20%, var(--project-border));
+.data-table tbody th,
+.data-table tbody td {
+ height: 3.5rem;
+ padding: 0 1rem;
+ border-bottom: 1px solid var(--border);
+ font-weight: 400;
+ vertical-align: middle;
}
-.project-rail-button:active {
- transform: scale(0.96);
+.data-table tbody tr:last-child th,
+.data-table tbody tr:last-child td {
+ border-bottom: 0;
}
-.project-rail-button svg {
- height: 18px;
- width: 18px;
- stroke-width: 1.6;
+.project-resource-table tbody tr {
+ cursor: pointer;
+ transition: background 100ms var(--ease);
}
-.project-rail-icon {
- display: grid;
- width: 20px;
- flex: 0 0 auto;
- place-items: center;
+.project-resource-table tbody tr:hover {
+ background: var(--surface);
}
-.project-rail-label {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.project-resource-name-cell {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 0.75rem;
}
-.project-account-button {
+.project-resource-icon {
display: grid;
- height: 34px;
- width: 34px;
+ height: 2rem;
+ width: 2rem;
+ flex: 0 0 auto;
place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 50%;
- background: var(--project-surface);
- color: var(--project-fg);
- cursor: pointer;
- font-size: 11px;
- font-weight: 700;
- margin: auto 0 0 12px;
- transition:
- border-color 160ms var(--project-ease),
- background 160ms var(--project-ease);
-}
-
-.project-account-button:hover {
- border-color: var(--project-fg);
- background: var(--project-bg);
+ border: 1px solid var(--border);
+ border-radius: 0.4375rem;
+ background: var(--background);
+ color: var(--foreground);
}
-.project-dashboard-main {
- position: relative;
- height: 100%;
- overflow-y: auto;
- padding: calc(var(--project-topbar-height) + 32px) 32px 32px
- calc(var(--project-rail-width) + 32px);
+.project-resource-icon.is-data {
+ background: var(--surface);
}
-.project-topology-shell {
- position: relative;
- height: 100%;
- overflow: hidden;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background-color: var(--project-bg);
- background-image: radial-gradient(
- circle,
- color-mix(in oklch, var(--project-border) 82%, transparent) 1px,
- transparent 1px
- );
- background-position: 12px 12px;
- background-size: 24px 24px;
+.project-resource-icon svg {
+ height: 0.9375rem;
+ width: 0.9375rem;
+ stroke-width: 1.75;
}
-.project-canvas-toolbar {
- position: absolute;
- z-index: 8;
- top: 20px;
- right: 20px;
- display: flex;
- align-items: center;
- gap: 10px;
+.project-resource-name {
+ display: grid;
+ min-width: 0;
+ gap: 0.125rem;
}
-.project-canvas-theme-toggle {
- display: grid;
- height: 44px;
- width: 44px;
- place-items: center;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
- color: var(--project-muted);
- cursor: pointer;
- box-shadow: var(--project-shadow-soft);
- transition:
- background 160ms var(--project-ease),
- color 160ms var(--project-ease);
+.project-resource-name strong {
+ overflow: hidden;
+ font-weight: 500;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.project-canvas-theme-toggle:hover {
- background: var(--project-surface);
- color: var(--project-fg);
+.project-resource-subtitle {
+ max-width: 28rem;
+ overflow: hidden;
+ color: var(--muted-foreground);
+ font-family: var(--font-mono);
+ font-size: 0.71875rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.project-canvas-theme-toggle svg {
- height: 18px;
- width: 18px;
- stroke-width: 1.5;
+.project-resource-type-cell {
+ color: var(--muted-foreground);
+ white-space: nowrap;
}
-.project-add-wrap {
- position: relative;
+.project-resource-action-cell {
+ width: 1%;
+ text-align: right;
}
-.project-primary-button {
+.project-resource-open {
display: inline-flex;
- min-height: 44px;
+ height: 1.75rem;
align-items: center;
- justify-content: center;
- gap: 9px;
- padding: 0 16px;
- border: 1px solid var(--project-accent);
- border-radius: var(--project-radius);
- background: var(--project-accent);
- color: var(--project-bg);
- cursor: pointer;
- font-size: 14px;
- font-weight: 700;
- box-shadow: var(--project-shadow-soft);
+ gap: 0.25rem;
+ padding: 0 0.375rem 0 0.625rem;
+ border-radius: 0.375rem;
+ color: var(--muted-foreground);
+ font-size: 0.8125rem;
+ font-weight: 500;
transition:
- background 160ms var(--project-ease),
- border-color 160ms var(--project-ease),
- transform 160ms var(--project-ease);
+ background 120ms var(--ease),
+ color 120ms var(--ease);
}
-.project-primary-button:hover {
- border-color: color-mix(
- in oklch,
- var(--project-accent) 88%,
- var(--project-fg)
- );
- background: color-mix(in oklch, var(--project-accent) 88%, var(--project-fg));
+.project-resource-open svg {
+ height: 0.875rem;
+ width: 0.875rem;
}
-.project-primary-button:active {
- transform: translateY(1px);
+.project-resource-table tbody tr:hover .project-resource-open,
+.project-resource-open:focus-visible {
+ background: var(--background);
+ box-shadow: 0 0 0 1px var(--border);
+ color: var(--foreground);
}
-.project-plus {
- font-size: 20px;
- font-weight: 400;
- line-height: 0.8;
+.project-view-error {
+ display: grid;
+ gap: 0.25rem;
+ margin-bottom: 1rem;
+ padding: 0.75rem 0.875rem;
+ border: 1px solid color-mix(in oklch, var(--danger) 30%, var(--border));
+ border-radius: 0.5rem;
+ background: color-mix(in oklch, var(--danger) 5%, var(--background));
+ color: var(--danger);
+ font-size: 0.8125rem;
}
-.project-add-menu {
- position: absolute;
- z-index: 12;
- top: calc(100% + 8px);
- right: 0;
- width: 220px;
- padding: 8px;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
- box-shadow: var(--project-shadow);
+.project-view-error span {
+ color: var(--foreground);
}
-.project-menu-heading {
- padding: 8px 10px 6px;
- color: var(--project-muted);
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.08em;
- text-transform: uppercase;
+.project-empty-compact {
+ min-height: 10rem;
}
-.project-menu-option,
-.project-context-menu-item {
- display: flex;
- width: 100%;
- min-height: 42px;
- align-items: center;
- gap: 10px;
- padding: 0 10px;
- border: 0;
- border-radius: 6px;
- background: transparent;
+.project-quick-add {
+ display: grid;
+ width: min(100%, 40rem);
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+}
+
+.project-quick-add-tile {
+ display: grid;
+ align-content: start;
+ justify-items: start;
+ gap: 0.25rem;
+ padding: 0.875rem;
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ background: var(--background);
text-align: left;
- cursor: pointer;
- transition: background 160ms var(--project-ease);
+ transition:
+ border-color 120ms var(--ease),
+ box-shadow 120ms var(--ease);
}
-.project-menu-option:hover,
-.project-context-menu-item:hover {
- background: var(--project-surface);
+.project-quick-add-tile:hover {
+ border-color: var(--border-strong);
+ box-shadow: var(--shadow-md);
}
-.project-menu-option:disabled {
- cursor: not-allowed;
- opacity: 0.5;
+.project-quick-add-tile > span {
+ display: grid;
+ height: 1.75rem;
+ width: 1.75rem;
+ place-items: center;
+ margin-bottom: 0.5rem;
+ border-radius: 0.375rem;
+ background: var(--surface-2);
}
-.project-menu-option > span:last-child {
- font-weight: 700;
+.project-quick-add-tile svg {
+ height: 0.875rem;
+ width: 0.875rem;
}
-.project-menu-icon {
- display: grid;
- height: 24px;
- width: 24px;
- place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 6px;
- color: var(--project-muted);
- font-size: 12px;
- font-weight: 700;
+.project-quick-add-tile strong {
+ font-size: 0.8125rem;
+ font-weight: 500;
}
-.project-canvas-world,
-.project-connector-layer {
- position: absolute;
- inset: 0;
+.project-quick-add-tile small {
+ color: var(--muted-foreground);
+ font-size: 0.75rem;
}
-.project-canvas-world {
- z-index: 2;
- transform-origin: 50% 50%;
- transition: transform 280ms var(--project-ease);
+/* ───────────── Metrics view ───────────── */
+
+.project-metric-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
+ gap: 0.75rem;
}
-.project-canvas-message,
-.project-canvas-empty {
- position: absolute;
- z-index: 4;
- left: 50%;
+.project-metric-card {
display: flex;
- transform: translateX(-50%);
+ min-height: 10rem;
+ width: 100%;
flex-direction: column;
- align-items: center;
- gap: 10px;
- color: var(--project-muted);
- text-align: center;
+ gap: 1rem;
+ padding: 1rem;
+ border: 1px solid var(--border);
+ border-radius: 0.625rem;
+ background: var(--background);
+ text-align: left;
+ transition:
+ border-color 140ms var(--ease),
+ box-shadow 140ms var(--ease);
}
-.project-canvas-message {
- top: 50%;
- flex-direction: row;
- transform: translate(-50%, -50%);
+.project-metric-card:hover {
+ border-color: var(--border-strong);
+ box-shadow: var(--shadow-md);
}
-.project-canvas-message-error {
- max-width: min(28rem, calc(100% - 3rem));
- color: var(--project-fg);
+.project-metric-card-head,
+.project-metric-card-foot {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
}
-.project-canvas-message-error span {
- color: var(--project-muted);
- font-size: 13px;
+.project-metric-card-body {
+ display: grid;
+ gap: 0.125rem;
}
-.project-canvas-empty {
- top: 66%;
- max-width: 300px;
+.project-metric-card-body strong {
+ font-size: 0.9375rem;
+ font-weight: 600;
+ letter-spacing: -0.01em;
}
-.project-canvas-empty strong {
- color: var(--project-fg);
- font-size: 15px;
+.project-metric-card-body span {
+ color: var(--muted-foreground);
+ font-size: 0.8125rem;
}
-.project-canvas-empty > span:not(.project-canvas-empty-icon) {
- font-size: 13px;
- line-height: 1.5;
+.project-metric-card-foot {
+ margin-top: auto;
+ padding-top: 0.75rem;
+ border-top: 1px solid var(--border);
+ color: var(--faint);
+ font-size: 0.75rem;
}
-.project-canvas-empty-icon {
- display: grid;
- height: 40px;
- width: 40px;
- place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 50%;
- background: var(--project-surface);
- color: var(--project-muted);
+.project-metric-card-cta {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ color: var(--foreground);
+ font-weight: 500;
+ white-space: nowrap;
}
-.project-canvas-empty-icon svg {
- height: 19px;
- width: 19px;
- stroke-width: 1.5;
+.project-metric-card-cta svg {
+ height: 0.875rem;
+ width: 0.875rem;
}
-.project-connector-layer {
- z-index: 1;
- height: 100%;
- width: 100%;
- pointer-events: none;
- transform-origin: 50% 50%;
- transition: transform 280ms var(--project-ease);
-}
-
-.project-connector {
- fill: none;
- stroke: var(--project-muted);
- stroke-width: 0.12;
- stroke-dasharray: 0.52 0.72;
- vector-effect: non-scaling-stroke;
- opacity: 0.72;
- transition:
- stroke 160ms var(--project-ease),
- opacity 160ms var(--project-ease);
-}
-
-.project-topology-shell.has-selection .project-connector {
- opacity: 0.3;
-}
-
-.project-topology-shell.has-selection .project-connector.is-highlighted {
- stroke: var(--project-accent);
- opacity: 1;
-}
+/* ───────────── Logs view ───────────── */
-.project-node-card {
- touch-action: none;
- position: absolute;
- display: flex;
- width: 360px;
- min-height: 212px;
- transform: translate(-50%, 0);
- flex-direction: column;
+.project-logs-panel {
overflow: hidden;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
- box-shadow: var(--project-shadow-soft);
- cursor: pointer;
- transition:
- border-color 160ms var(--project-ease),
- box-shadow 160ms var(--project-ease),
- transform 160ms var(--project-ease);
+ border: 1px solid var(--border);
+ border-radius: 0.625rem;
+ background: var(--background);
}
-.project-app-node {
- width: min(360px, 29vw);
+.project-logs-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.625rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface);
}
-.project-node-card:hover {
- border-color: color-mix(
- in oklch,
- var(--project-fg) 32%,
- var(--project-border)
- );
- box-shadow: var(--project-shadow);
+.project-logs-toolbar .search-field {
+ flex: 1;
+ width: auto;
+ max-width: 24rem;
}
-.project-node-card.is-selected {
- border-color: var(--project-accent);
- box-shadow:
- 0 0 0 3px var(--project-accent-soft-strong),
- var(--project-shadow);
+.project-select select {
+ height: 2rem;
+ padding: 0 1.75rem 0 0.625rem;
+ border: 1px solid var(--border);
+ border-radius: 0.375rem;
+ background: var(--background);
+ color: var(--foreground);
+ font: inherit;
+ font-size: 0.8125rem;
}
-.project-node-main {
- display: flex;
- flex: 1;
- min-height: 151px;
- flex-direction: column;
- justify-content: space-between;
- gap: 36px;
- padding: 28px 32px 24px;
+.project-logs-live {
+ display: inline-flex;
+ height: 2rem;
+ align-items: center;
+ gap: 0.5rem;
+ margin-left: auto;
+ padding: 0 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: 0.375rem;
+ background: var(--background);
+ font-size: 0.8125rem;
+ font-weight: 500;
}
-.project-node-title {
- display: flex;
- min-width: 0;
- align-items: center;
- gap: 14px;
+.project-logs-live span {
+ height: 0.4375rem;
+ width: 0.4375rem;
+ border-radius: 999px;
+ background: var(--faint);
}
-.project-node-title > div {
- min-width: 0;
- flex: 1 1 auto;
- overflow: hidden;
+.project-logs-live[aria-pressed="true"] span {
+ background: var(--ok);
+ animation: status-pulse 1.6s ease-in-out infinite;
}
-.project-node-logo {
+.project-logs-stream {
display: grid;
- height: 28px;
- width: 28px;
- flex: 0 0 auto;
- place-items: center;
- color: var(--project-fg);
+ min-height: 26rem;
+ align-content: start;
+ gap: 1.5rem;
+ padding: 1rem;
}
-.project-node-logo svg {
- height: 28px;
- width: 28px;
- stroke-width: 1.4;
+.project-logs-deployment {
+ display: grid;
+ gap: 0.5rem;
}
-.project-node-heading {
- margin: 0;
- overflow: hidden;
- font-size: 18px;
- font-weight: 700;
- letter-spacing: -0.02em;
- text-overflow: ellipsis;
- white-space: nowrap;
+.project-logs-deployment > strong {
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ font-weight: 500;
}
-.project-node-subtitle {
- margin: 2px 0 0;
- overflow: hidden;
- color: var(--project-muted);
- font-size: 14px;
- text-overflow: ellipsis;
- white-space: nowrap;
+.project-logs-filter-note {
+ color: var(--faint);
+ font-size: 0.75rem;
}
-.project-node-status {
+.project-logs-empty {
display: flex;
- min-width: 0;
+ min-height: 24rem;
+ flex-direction: column;
align-items: center;
- gap: 12px;
- color: var(--project-fg);
- font-size: 16px;
+ justify-content: center;
+ gap: 0.5rem;
+ text-align: center;
}
-.project-node-status > span:last-child,
-.project-node-footer > span {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.project-logs-empty h2 {
+ margin-top: 0.5rem;
+ font-size: 0.9375rem;
+ font-weight: 600;
}
-.project-status-dot {
- position: relative;
- display: block;
- height: 12px;
- width: 12px;
- flex: 0 0 auto;
- border: 3px solid var(--project-accent-soft-strong);
- border-radius: 50%;
- background: var(--project-accent);
+.project-logs-empty p {
+ max-width: 24rem;
+ color: var(--muted-foreground);
+ font-size: 0.8125rem;
+ line-height: 1.5;
}
-.project-status-dot-creating {
- border-color: color-mix(in oklch, #d97706 24%, transparent);
- background: #d97706;
-}
+/* ───────────── Topology canvas ───────────── */
-.project-status-dot-needs {
- border-color: color-mix(in oklch, var(--destructive) 24%, transparent);
- background: var(--destructive);
+.project-topology-shell {
+ position: relative;
+ min-height: 0;
+ flex: 1;
+ overflow: hidden;
+ background-color: var(--canvas);
+ background-image: radial-gradient(
+ color-mix(in oklch, var(--foreground) 13%, transparent) 1px,
+ transparent 1px
+ );
+ background-size: 22px 22px;
}
-.project-node-footer {
+.project-canvas-legend {
+ position: absolute;
+ z-index: 3;
+ top: 0.875rem;
+ left: 0.875rem;
display: flex;
- min-height: 60px;
- align-items: center;
- gap: 14px;
- padding: 0 32px;
- border-top: 1px solid var(--project-border);
- background: var(--project-surface);
- color: var(--project-muted);
- font-size: 16px;
+ gap: 1rem;
+ padding: 0.375rem 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ background: var(--background);
+ color: var(--muted-foreground);
+ font-size: 0.75rem;
}
-.project-storage-icon {
- height: 22px;
- width: 22px;
- flex: 0 0 auto;
- stroke-width: 1.35;
+.project-canvas-legend span {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.375rem;
}
-.project-selection-hint {
- position: absolute;
- z-index: 5;
- bottom: 20px;
- left: 24px;
- display: none;
- min-height: 32px;
- align-items: center;
- gap: 8px;
- padding: 0 10px;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: color-mix(in oklch, var(--project-bg) 92%, transparent);
- color: var(--project-muted);
- font-size: 12px;
- pointer-events: none;
-}
-
-.project-selection-hint kbd {
- padding: 1px 5px;
- border: 1px solid var(--project-border);
- border-radius: 4px;
- background: var(--project-surface);
- color: var(--project-fg);
- font-family: inherit;
- font-size: 11px;
+.project-canvas-legend i {
+ display: inline-block;
+ height: 0.5rem;
+ width: 0.5rem;
+ border-radius: 0.125rem;
}
-.project-zoom-dock {
- position: absolute;
- z-index: 8;
- bottom: 20px;
- left: 20px;
- display: grid;
- gap: 8px;
+.project-canvas-legend .is-service {
+ border: 1.5px solid var(--foreground);
}
-.project-zoom-group,
-.project-history-group {
- display: grid;
- overflow: hidden;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
- box-shadow: var(--project-shadow-soft);
+.project-canvas-legend .is-data {
+ background: var(--brand);
}
-.project-zoom-button,
-.project-layers-button {
- display: grid;
- height: 44px;
- width: 44px;
- place-items: center;
- color: var(--project-muted);
- transition:
- background 160ms var(--project-ease),
- color 160ms var(--project-ease);
+.project-canvas-legend .is-link {
+ height: 0;
+ width: 0.875rem;
+ border-top: 1.5px dashed var(--muted-foreground);
+ border-radius: 0;
}
-.project-zoom-button:hover,
-.project-layers-button:hover {
- background: var(--project-surface);
- color: var(--project-fg);
+.project-connector-layer,
+.project-canvas-world {
+ position: absolute;
+ inset: 4.5rem 2rem 4.5rem;
+ transform-origin: center;
+ transition: transform 180ms var(--ease);
}
-.project-zoom-button + .project-zoom-button,
-.project-history-group .project-zoom-button + .project-zoom-button {
- border-top: 1px solid var(--project-border);
+.project-connector-layer {
+ height: calc(100% - 9rem);
+ width: calc(100% - 4rem);
+ overflow: visible;
}
-.project-zoom-button svg,
-.project-layers-button svg {
- height: 18px;
- width: 18px;
+.project-connector {
+ fill: none;
+ stroke: var(--border-strong);
+ stroke-dasharray: 5 5;
stroke-width: 1.5;
+ vector-effect: non-scaling-stroke;
+ transition: stroke 160ms var(--ease);
}
-.project-history-group {
- margin-top: 4px;
+.project-connector.is-highlighted {
+ stroke: var(--brand);
+ stroke-dasharray: none;
}
-.project-layers-button[aria-pressed="true"] {
- color: var(--project-fg);
- background: var(--project-accent-soft);
+.project-node-card {
+ position: absolute;
+ z-index: 1;
+ display: flex;
+ width: 15.5rem;
+ flex-direction: column;
+ border: 1px solid var(--border);
+ border-radius: 0.625rem;
+ background: var(--background);
+ box-shadow: var(--shadow-md);
+ cursor: grab;
+ touch-action: none;
+ transform: translate(-50%, 0);
+ user-select: none;
+ transition:
+ border-color 140ms var(--ease),
+ box-shadow 140ms var(--ease);
}
-.project-zoom-readout {
- position: absolute;
- top: 50%;
- left: 54px;
- min-width: 48px;
- transform: translateY(-50%);
- color: var(--project-muted);
- font-size: 12px;
- white-space: nowrap;
+.project-node-card:active {
+ cursor: grabbing;
}
-.project-inspect-panel {
- position: absolute;
- z-index: 10;
- top: 20px;
- right: 20px;
- width: 360px;
- padding: 20px;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
- box-shadow: var(--project-shadow);
- animation: project-panel-in 240ms var(--project-ease);
-}
-
-@keyframes project-panel-in {
- from {
- opacity: 0;
- transform: translateY(-6px);
- }
+.project-node-card:hover {
+ border-color: var(--border-strong);
+ box-shadow: var(--shadow-lg);
+}
- to {
- opacity: 1;
- transform: translateY(0);
- }
+.project-node-card.is-selected {
+ border-color: var(--brand);
+ box-shadow:
+ 0 0 0 3px color-mix(in oklch, var(--brand) 18%, transparent),
+ var(--shadow-lg);
}
-.project-inspect-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
+.project-node-card:focus-visible {
+ outline: 2px solid var(--brand);
+ outline-offset: 2px;
}
-.project-inspect-kicker {
- margin: 0 0 4px;
- color: var(--project-muted);
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.08em;
- text-transform: uppercase;
+.project-node-card.is-data::before {
+ position: absolute;
+ top: 0.875rem;
+ bottom: 0.875rem;
+ left: -1px;
+ width: 2px;
+ border-radius: 0 2px 2px 0;
+ background: var(--brand);
+ content: "";
}
-.project-inspect-title {
- margin: 0;
- font-size: 18px;
- letter-spacing: -0.02em;
+.project-node-main {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 0.625rem;
+ padding: 0.875rem 0.875rem 0;
}
-.project-close-button {
+.project-node-logo {
display: grid;
- height: 32px;
- width: 32px;
+ height: 2rem;
+ width: 2rem;
flex: 0 0 auto;
place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 6px;
- background: var(--project-bg);
- color: var(--project-muted);
- cursor: pointer;
- transition:
- background 160ms var(--project-ease),
- color 160ms var(--project-ease);
-}
-
-.project-close-button:hover {
- background: var(--project-surface);
- color: var(--project-fg);
+ border: 1px solid var(--border);
+ border-radius: 0.4375rem;
+ background: var(--surface);
+ color: var(--foreground);
}
-.project-close-button svg {
- height: 16px;
- width: 16px;
- stroke-width: 1.5;
+.project-node-logo svg {
+ height: 0.9375rem;
+ width: 0.9375rem;
+ stroke-width: 1.75;
}
-.project-inspect-list {
- margin: 20px 0 0;
- border-top: 1px solid var(--project-border);
+.project-node-text {
+ min-width: 0;
}
-.project-inspect-row {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- gap: 16px;
- padding: 12px 0;
- border-bottom: 1px solid var(--project-border);
+.project-node-heading {
+ overflow: hidden;
+ font-size: 0.875rem;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.project-inspect-row dt {
- color: var(--project-muted);
+.project-node-type {
+ color: var(--muted-foreground);
+ font-size: 0.75rem;
}
-.project-inspect-row dd {
- max-width: 220px;
- margin: 0;
+.project-node-subtitle {
+ margin: 0.625rem 0.875rem 0;
overflow: hidden;
- font-weight: 700;
- text-align: right;
+ color: var(--muted-foreground);
+ font-family: var(--font-mono);
+ font-size: 0.6875rem;
text-overflow: ellipsis;
white-space: nowrap;
}
-.project-connection-block {
- display: flex;
- margin-top: 20px;
- flex-direction: column;
- gap: 10px;
-}
-
-.project-connection-heading {
+.project-node-footer {
display: flex;
align-items: center;
justify-content: space-between;
- gap: 12px;
- color: var(--project-muted);
- font-size: 12px;
- font-weight: 700;
-}
-
-.project-connection-value {
- display: block;
- max-height: 96px;
- overflow: auto;
- padding: 10px;
- border: 1px solid var(--project-border);
- border-radius: 6px;
- background: var(--project-surface);
- color: var(--project-fg);
- font-family:
- ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
- "Courier New", monospace;
- font-size: 11px;
- line-height: 1.5;
- overflow-wrap: anywhere;
- white-space: pre-wrap;
-}
-
-.project-connection-warning {
- margin: 0;
- color: var(--project-muted);
- font-size: 11px;
- line-height: 1.5;
+ gap: 0.5rem;
+ margin-top: 0.875rem;
+ padding: 0.5rem 0.875rem;
+ border-top: 1px solid var(--border);
+ background: var(--surface);
+ border-radius: 0 0 0.625rem 0.625rem;
}
-.project-context-menu {
- position: fixed;
- z-index: 30;
- top: 56px;
- width: 220px;
- padding: 8px;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
- box-shadow: var(--project-shadow);
+.project-node-volume {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 0.3125rem;
+ color: var(--faint);
+ font-family: var(--font-mono);
+ font-size: 0.6875rem;
}
-.project-workspace-menu {
- left: 84px;
+.project-node-volume svg {
+ height: 0.75rem;
+ width: 0.75rem;
+ flex: 0 0 auto;
}
-.project-environment-menu {
- left: 304px;
+.project-node-volume span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.project-context-menu-item {
- min-height: 40px;
+.project-canvas-message,
+.project-canvas-empty {
+ position: absolute;
+ z-index: 4;
+ top: 50%;
+ left: 50%;
+ display: flex;
+ max-width: 22rem;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 1.25rem 1.5rem;
+ border: 1px solid var(--border);
+ border-radius: 0.625rem;
+ background: var(--background);
+ box-shadow: var(--shadow-md);
+ color: var(--muted-foreground);
+ font-size: 0.8125rem;
+ text-align: center;
+ transform: translate(-50%, -50%);
}
-.project-context-menu-item.is-current {
- color: var(--project-accent);
- font-weight: 700;
+.project-canvas-message {
+ flex-direction: row;
+ gap: 0.625rem;
}
-.project-toast {
- position: fixed;
- z-index: 40;
- right: 24px;
- bottom: 24px;
- display: flex;
- min-height: 44px;
- align-items: center;
- padding: 0 14px;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-fg);
- color: var(--project-bg);
- box-shadow: var(--project-shadow);
- animation: project-toast-in 240ms var(--project-ease);
+.project-canvas-message-error {
+ flex-direction: column;
+ color: var(--danger);
}
-@keyframes project-toast-in {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
+.project-canvas-message-error span {
+ color: var(--foreground);
}
-.project-logs-shell {
- display: flex;
- height: 100%;
- min-height: 0;
- flex-direction: column;
- overflow: hidden;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
+.project-canvas-empty {
+ top: auto;
+ bottom: 4.5rem;
+ transform: translateX(-50%);
}
-.project-logs-toolbar {
- display: flex;
- align-items: flex-end;
- justify-content: space-between;
- gap: 24px;
- padding: 24px 28px 20px;
- border-bottom: 1px solid var(--project-border);
+.project-canvas-empty strong {
+ color: var(--foreground);
+ font-weight: 600;
}
-.project-logs-heading h1 {
- margin: 0;
- font-size: 24px;
- letter-spacing: -0.03em;
+.project-canvas-empty button {
+ margin-top: 0.5rem;
}
-.project-logs-heading p {
- margin: 4px 0 0;
- color: var(--project-muted);
- font-size: 13px;
+.project-selection-hint {
+ position: absolute;
+ z-index: 3;
+ bottom: 0.875rem;
+ left: 0.875rem;
+ color: var(--faint);
+ font-size: 0.75rem;
}
-.project-logs-filters {
+.project-zoom-dock {
+ position: absolute;
+ z-index: 3;
+ right: 0.875rem;
+ bottom: 0.875rem;
display: flex;
align-items: center;
- gap: 8px;
+ padding: 0.1875rem;
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ background: var(--background);
+ box-shadow: var(--shadow-sm);
}
-.project-logs-filters select,
-.project-logs-search input {
- min-height: 36px;
- border: 1px solid var(--project-border);
- border-radius: 8px;
- background: var(--project-bg);
- color: var(--project-fg);
- font: inherit;
+.project-zoom-button {
+ display: grid;
+ height: 1.75rem;
+ width: 1.75rem;
+ place-items: center;
+ border-radius: 0.3125rem;
+ color: var(--muted-foreground);
+ transition:
+ background 120ms var(--ease),
+ color 120ms var(--ease);
}
-.project-logs-filters select {
- padding: 0 10px;
+.project-zoom-button:hover {
+ background: var(--surface-2);
+ color: var(--foreground);
}
-.project-logs-search {
- position: relative;
- display: block;
+.project-zoom-button svg {
+ height: 0.875rem;
+ width: 0.875rem;
}
-.project-logs-search svg {
- position: absolute;
- z-index: 1;
- top: 50%;
- left: 10px;
- height: 14px;
- width: 14px;
- color: var(--project-muted);
- pointer-events: none;
- transform: translateY(-50%);
+.project-zoom-readout {
+ min-width: 2.75rem;
+ color: var(--foreground);
+ font-family: var(--font-mono);
+ font-size: 0.71875rem;
+ text-align: center;
}
-.project-logs-search input {
- width: 220px;
- padding: 0 10px 0 32px;
+.project-zoom-divider {
+ height: 1rem;
+ width: 1px;
+ margin: 0 0.25rem;
+ background: var(--border);
}
-.project-logs-filters select:focus-visible,
-.project-logs-search input:focus-visible,
-.project-logs-live:focus-visible {
- outline: 2px solid var(--project-accent);
- outline-offset: 3px;
-}
+/* ───────────── Toast ───────────── */
-.project-logs-live {
+.project-toast {
+ position: fixed;
+ z-index: 80;
+ bottom: 1.25rem;
+ left: 50%;
display: inline-flex;
- min-height: 36px;
align-items: center;
- gap: 8px;
- padding: 0 12px;
- border: 1px solid var(--project-border);
- border-radius: 8px;
- background: var(--project-bg);
- color: var(--project-muted);
- cursor: pointer;
- font: inherit;
- font-weight: 600;
+ gap: 0.5rem;
+ padding: 0.5rem 0.875rem;
+ border-radius: 0.5rem;
+ background: var(--foreground);
+ box-shadow: var(--shadow-lg);
+ color: var(--background);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ transform: translateX(-50%);
+ animation: project-toast-in 180ms var(--ease);
}
-.project-logs-live:hover,
-.project-logs-live[aria-pressed="true"] {
- color: var(--project-fg);
- background: var(--project-surface);
+.project-toast svg {
+ height: 0.875rem;
+ width: 0.875rem;
+ color: var(--ok);
}
-.project-logs-live span {
- height: 8px;
- width: 8px;
- border-radius: 50%;
- background: var(--project-muted);
+@keyframes project-toast-in {
+ from {
+ opacity: 0;
+ transform: translate(-50%, 6px);
+ }
}
-.project-logs-live[aria-pressed="true"] span {
- background: var(--project-ok);
-}
-
-.project-logs-stream {
- min-height: 0;
- flex: 1;
- overflow: auto;
- padding: 20px 28px;
-}
-
-.project-logs-empty {
- display: grid;
- min-height: 100%;
- place-items: center;
- align-content: center;
- padding: 32px;
- color: var(--project-muted);
- text-align: center;
-}
-
-.project-logs-empty-icon {
- display: grid;
- height: 42px;
- width: 42px;
- place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 50%;
- background: var(--project-surface);
-}
-
-.project-logs-empty-icon svg {
- height: 20px;
- width: 20px;
-}
-
-.project-logs-empty h2 {
- margin: 14px 0 6px;
- color: var(--project-fg);
- font-size: 16px;
-}
-
-.project-logs-empty p {
- max-width: 28rem;
- margin: 0;
- line-height: 1.6;
-}
-
-.project-logs-filter-note {
- margin-top: 12px;
- color: var(--project-muted);
- font-size: 12px;
-}
-
-.project-load-screen,
-.project-load-error {
- display: flex;
- min-height: 100svh;
- align-items: center;
- justify-content: center;
- gap: 0.75rem;
- background: var(--background);
- color: var(--muted-foreground);
-}
-
-.project-load-error {
- flex-direction: column;
- padding: 2rem;
-}
-
-.project-load-error-mark {
- display: grid;
- height: 3.25rem;
- width: 3.25rem;
- place-items: center;
- border-radius: 50%;
- background: var(--destructive);
- color: white;
-}
-
-.project-load-error-mark svg {
- height: 1.5rem;
- width: 1.5rem;
-}
-
-.project-content-view {
- width: min(100%, 1240px);
- margin: 0 auto;
-}
-
-.project-view-heading {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 20px;
- margin: 0 0 26px;
-}
-
-.project-view-heading h1 {
- margin: 0;
- font-size: clamp(28px, 3vw, 36px);
- font-weight: 700;
- letter-spacing: -0.045em;
- line-height: 1.12;
-}
-
-.project-view-heading p {
- margin: 9px 0 0;
- color: var(--project-muted);
- font-size: 15px;
- line-height: 1.55;
-}
-
-.project-resource-toolbar {
- display: flex;
- align-items: center;
- gap: 14px;
- margin-bottom: 22px;
-}
-
-.project-resource-search {
- display: flex;
- min-width: 220px;
- min-height: 46px;
- flex: 1 1 auto;
- align-items: center;
- gap: 10px;
- padding: 0 13px;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
- color: var(--project-muted);
- transition:
- border-color 160ms var(--project-ease),
- box-shadow 160ms var(--project-ease);
-}
-
-.project-resource-search:focus-within {
- border-color: var(--project-accent);
- box-shadow: 0 0 0 3px var(--project-accent-soft);
-}
-
-.project-resource-search svg {
- height: 18px;
- width: 18px;
- flex: 0 0 auto;
-}
-
-.project-resource-search input {
- width: 100%;
- min-width: 0;
- border: 0;
- outline: 0;
- background: transparent;
- color: var(--project-fg);
- font: inherit;
-}
-
-.project-resource-search input::placeholder {
- color: var(--project-muted);
- opacity: 1;
-}
-
-.project-resource-search input:focus-visible {
- outline: 0;
-}
-
-.project-resource-filters {
- display: inline-flex;
- min-height: 46px;
- flex: 0 0 auto;
- align-items: stretch;
- padding: 3px;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
-}
-
-.project-resource-filters button {
- min-height: 38px;
- padding: 0 15px;
- border: 1px solid transparent;
- border-radius: 6px;
- background: transparent;
- color: var(--project-muted);
- cursor: pointer;
- font-size: 13px;
- font-weight: 600;
- white-space: nowrap;
- transition:
- color 160ms var(--project-ease),
- background 160ms var(--project-ease),
- border-color 160ms var(--project-ease);
-}
-
-.project-resource-filters button:hover {
- color: var(--project-fg);
-}
-
-.project-resource-filters button.is-active {
- border-color: var(--project-accent);
- background: var(--project-accent-soft);
- color: var(--project-accent);
-}
-
-.project-resource-table-wrap {
- overflow: hidden;
- border: 1px solid var(--project-border);
- border-radius: var(--project-radius);
- background: var(--project-bg);
-}
-
-.project-resource-table {
- width: 100%;
- border-collapse: collapse;
- text-align: left;
-}
-
-.project-resource-table thead {
- background: var(--project-surface);
-}
-
-.project-resource-table th,
-.project-resource-table td {
- padding: 14px 20px;
- border-bottom: 1px solid var(--project-border);
- vertical-align: middle;
-}
-
-.project-resource-table thead th {
- color: var(--project-muted);
- font-size: 12px;
- font-weight: 700;
-}
-
-.project-resource-table tbody tr:last-child > * {
- border-bottom: 0;
-}
-
-.project-resource-table tbody tr {
- transition: background 140ms var(--project-ease);
-}
-
-.project-resource-table tbody tr:hover {
- background: color-mix(in oklch, var(--project-surface) 68%, transparent);
-}
-
-.project-resource-table tbody th {
- width: 40%;
- color: var(--project-fg);
- font-size: 14px;
- font-weight: 600;
-}
-
-.project-resource-name-cell {
- display: flex;
- align-items: center;
- gap: 14px;
-}
-
-.project-resource-icon {
- display: grid;
- height: 28px;
- width: 28px;
- flex: 0 0 auto;
- place-items: center;
- color: var(--project-fg);
-}
-
-.project-resource-icon svg {
- height: 21px;
- width: 21px;
- stroke-width: 1.6;
-}
-
-.project-resource-name {
- display: flex;
- min-width: 0;
- flex-direction: column;
- gap: 3px;
-}
-
-.project-resource-name strong {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.project-resource-type-cell {
- color: var(--project-muted);
- font-size: 13px;
-}
-
-.project-resource-mobile-type {
- display: none;
- color: var(--project-muted);
- font-size: 12px;
- font-weight: 400;
-}
-
-.project-resource-status {
- display: inline-flex;
- align-items: center;
- gap: 9px;
- color: var(--project-fg);
- font-size: 13px;
- white-space: nowrap;
-}
-
-.project-resource-status-dot {
- height: 9px;
- width: 9px;
- flex: 0 0 auto;
- border-radius: 50%;
- background: var(--project-muted);
-}
-
-.project-resource-status-dot.status-ready {
- background: var(--project-ok);
-}
-
-.project-resource-status-dot.status-provisioning {
- background: var(--project-warn);
-}
-
-.project-resource-status-dot.status-error {
- background: var(--project-danger);
-}
-
-.project-resource-action-cell {
- width: 140px;
- text-align: right;
-}
-
-.project-resource-open {
- display: inline-flex;
- min-height: 38px;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 0 11px;
- border: 1px solid var(--project-border);
- border-radius: 6px;
- background: var(--project-bg);
- color: var(--project-fg);
- cursor: pointer;
- font-size: 12px;
- font-weight: 600;
- white-space: nowrap;
- transition:
- border-color 160ms var(--project-ease),
- background 160ms var(--project-ease),
- color 160ms var(--project-ease);
-}
-
-.project-resource-open:hover {
- border-color: var(--project-accent);
- background: var(--project-accent-soft);
- color: var(--project-accent);
-}
-
-.project-resource-open svg {
- height: 15px;
- width: 15px;
-}
+/* ───────────── Responsive ───────────── */
-.project-view-loading,
-.project-view-error,
-.project-view-empty {
- display: flex;
- width: min(100%, 640px);
- margin: 72px auto 0;
- flex-direction: column;
- align-items: center;
- gap: 12px;
- color: var(--project-muted);
- text-align: center;
-}
-
-.project-view-loading {
- flex-direction: row;
- justify-content: center;
- font-size: 13px;
-}
-
-.project-view-error {
- margin-top: 0;
- align-items: flex-start;
- padding: 14px 16px;
- border: 1px solid
- color-mix(in oklch, var(--project-danger) 30%, var(--project-border));
- border-radius: var(--project-radius);
- color: var(--project-fg);
- text-align: left;
-}
-
-.project-view-error span {
- color: var(--project-muted);
-}
-
-.project-view-empty {
- margin-top: 92px;
-}
-
-.project-view-empty-compact {
- margin-top: 52px;
-}
-
-.project-view-empty-icon {
- display: grid;
- height: 48px;
- width: 48px;
- place-items: center;
- border: 1px solid var(--project-border);
- border-radius: 50%;
- background: var(--project-surface);
- color: var(--project-muted);
-}
-
-.project-view-empty-icon svg {
- height: 21px;
- width: 21px;
-}
-
-.project-view-empty h2 {
- margin: 2px 0 0;
- color: var(--project-fg);
- font-size: 19px;
- font-weight: 700;
- letter-spacing: -0.025em;
-}
-
-.project-view-empty p {
- max-width: 36rem;
- margin: 0 0 4px;
- font-size: 13px;
- line-height: 1.6;
-}
-
-@media (max-width: 1080px) {
- .project-topbar {
- gap: 12px;
- padding: 0 16px;
- }
-
- .project-topbar-right {
- gap: 10px;
- }
-
- .project-billing-badge {
- display: none;
+@media (max-width: 1023px) {
+ .project-stats {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
}
- .project-resource-toolbar {
- align-items: stretch;
- flex-direction: column;
- }
-
- .project-node-card {
- width: 320px;
+ .project-stats > div:nth-child(3) {
+ border-left: 0;
}
- .project-node-main {
- padding: 24px;
+ .project-stats > div:nth-child(n + 3) {
+ border-top: 1px solid var(--border);
}
- .project-node-footer {
- padding: 0 24px;
+ .project-quick-add {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
-@media (max-width: 760px) {
- .project-home {
- height: auto;
- min-height: 100svh;
- overflow: visible;
- }
-
- .project-topbar {
- height: 60px;
- gap: 7px;
- padding: 0 10px;
- }
-
- .project-topbar-left {
- flex: 1 1 auto;
- gap: 7px;
- overflow: hidden;
- }
-
- .project-workspace-switcher {
- flex: 1 1 0;
- gap: 6px;
- overflow: hidden;
- }
-
- .project-brand-button {
- height: 32px;
- width: 32px;
- }
-
- .project-brand-mark {
- height: 26px;
- width: 26px;
- }
-
- .project-context-trigger {
- min-width: 0;
- gap: 4px;
- padding: 0 3px;
- font-size: 12px;
- }
-
- .project-topbar-divider {
- display: none;
- }
-
- .project-project-context,
- .project-environment-context {
- display: none;
- }
-
- .project-context-label {
- max-width: 27vw;
- }
-
- .project-topbar-right {
- gap: 5px;
- }
-
- .project-primary-button {
- min-height: 38px;
- width: 38px;
- padding: 0;
- }
-
+@media (max-width: 767px) {
.project-add-label {
display: none;
}
- .project-plus {
- font-size: 22px;
- }
-
- .project-agent-button {
- min-height: 36px;
- padding: 0;
- border: 0;
- }
-
- .project-sign-out-label {
- display: none;
- }
-
- .project-side-rail {
- z-index: 25;
- inset: auto 0 0;
- display: flex;
- height: calc(68px + env(safe-area-inset-bottom));
- width: 100%;
- flex-direction: row;
- align-items: center;
- padding: 3px 4px calc(3px + env(safe-area-inset-bottom));
- border-top: 1px solid var(--project-border);
- border-right: 0;
- }
-
- .project-rail-project {
- display: none;
- }
-
- .project-rail-nav {
- display: grid;
- width: 100%;
- grid-template-columns: repeat(5, minmax(0, 1fr));
- align-items: center;
- gap: 0;
- }
-
- .project-rail-button {
- display: flex;
- width: 100%;
- min-width: 0;
- min-height: 56px;
- flex-direction: column;
- justify-content: center;
- gap: 3px;
- padding: 4px 1px;
- font-size: 10px;
- }
-
- .project-rail-icon {
- height: 19px;
- width: 22px;
- }
-
- .project-rail-button svg {
- height: 18px;
- width: 18px;
- }
-
- .project-rail-label {
- max-width: 100%;
- font-size: 9px;
- line-height: 1.15;
- text-align: center;
- }
-
- .project-account-button {
- display: none;
- }
-
- .project-dashboard-main {
- height: auto;
- min-height: calc(100svh - 60px);
- overflow: visible;
- padding: 84px 16px calc(88px + env(safe-area-inset-bottom));
- }
-
- .project-content-view {
- width: 100%;
- }
-
- .project-view-heading {
- margin-bottom: 22px;
- }
-
- .project-view-heading h1 {
- font-size: 30px;
- }
-
- .project-view-heading p {
- max-width: 31rem;
- font-size: 13px;
+ .project-add-menu {
+ position: fixed;
+ top: 3.5rem;
+ right: 0.5rem;
+ left: 0.5rem;
+ width: auto;
}
.project-resource-toolbar {
align-items: stretch;
- flex-direction: column;
- gap: 10px;
- margin-bottom: 16px;
+ flex-direction: column-reverse;
}
- .project-resource-filters {
+ .project-resource-toolbar .search-field {
width: 100%;
- overflow-x: auto;
- }
-
- .project-resource-filters button {
- min-width: 0;
- flex: 1 0 auto;
- padding: 0 10px;
- font-size: 11px;
- }
-
- .project-resource-table-wrap {
- overflow: visible;
- border-radius: 0;
- border-width: 1px 0 0;
- }
-
- .project-resource-table thead {
- display: none;
- }
-
- .project-resource-table tbody tr {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto auto;
- align-items: center;
- gap: 2px 8px;
- padding: 11px 0;
- border-bottom: 1px solid var(--project-border);
}
- .project-resource-table tbody tr:last-child > * {
- border-bottom: 0;
- }
-
- .project-resource-table th,
- .project-resource-table td {
- min-width: 0;
- padding: 3px 0;
- border: 0;
- }
-
- .project-resource-table tbody th {
- width: auto;
- font-size: 13px;
- }
-
- .project-resource-name-cell {
- grid-column: 1;
- grid-row: 1;
- gap: 8px;
- }
-
- .project-resource-icon {
- height: 24px;
- width: 24px;
- }
-
- .project-resource-icon svg {
- height: 19px;
- width: 19px;
- }
-
- .project-resource-name strong {
- font-size: 13px;
+ .segmented {
+ overflow-x: auto;
}
- .project-resource-mobile-type {
- display: block;
+ .segmented button {
+ flex: 1;
}
.project-resource-type-cell {
display: none;
}
- .project-resource-status {
- grid-column: 2;
- grid-row: 1;
- gap: 5px;
- font-size: 10px;
- }
-
- .project-resource-status-dot {
- height: 7px;
- width: 7px;
- }
-
- .project-resource-action-cell {
- grid-column: 3;
- grid-row: 1;
- width: auto;
- }
-
- .project-resource-open {
- height: 34px;
- min-height: 34px;
- width: 34px;
- padding: 0;
+ .project-resource-subtitle {
+ max-width: 11rem;
}
.project-resource-open span {
display: none;
}
- .project-resource-open svg {
- height: 15px;
- width: 15px;
+ .project-logs-toolbar {
+ flex-wrap: wrap;
}
- .project-view-empty {
- margin-top: 68px;
+ .project-logs-toolbar .search-field {
+ max-width: none;
+ flex-basis: 100%;
+ order: 3;
}
+ /* Absolute positioning is meaningless on a phone: stack the nodes. */
.project-topology-shell {
- height: auto;
- min-height: calc(100svh - 144px);
- overflow: visible;
- background-size: 22px 22px;
+ overflow-y: auto;
+ padding-bottom: 4rem;
}
- .project-canvas-toolbar {
- top: 16px;
- right: 16px;
+ .project-canvas-legend,
+ .project-connector-layer,
+ .project-selection-hint,
+ .project-zoom-dock {
+ display: none;
}
.project-canvas-world {
- position: relative;
- inset: auto;
+ position: static;
display: grid;
- gap: 16px;
- padding: 84px 16px 104px;
+ gap: 0.75rem;
+ padding: 1rem;
transform: none !important;
}
- .project-canvas-empty {
- position: relative;
- top: auto;
- left: auto;
- margin: 84px 24px 0;
- transform: none;
- }
-
- .project-connector-layer {
- display: none;
- }
-
- .project-node-card,
- .project-node-card[style] {
- position: relative;
- inset: auto;
- left: auto !important;
- top: auto !important;
+ .project-node-card {
+ position: static;
width: 100%;
- max-width: 440px;
- min-height: 196px;
transform: none;
- justify-self: center;
- }
-
- .project-node-main {
- gap: 28px;
- }
-
- .project-selection-hint {
- display: none;
- }
-
- .project-zoom-dock {
- bottom: 16px;
- left: 16px;
- }
-
- .project-inspect-panel {
- position: fixed;
- top: auto;
- right: 16px;
- bottom: 76px;
- left: 16px;
- width: auto;
}
- .project-workspace-menu {
- top: 54px;
- left: 16px;
- }
-
- .project-environment-menu {
- top: 54px;
- right: 16px;
- left: auto;
+ .project-canvas-empty {
+ position: static;
+ max-width: none;
+ margin: 1rem 1rem 0;
+ transform: none;
}
.project-toast {
- right: 16px;
- bottom: 76px;
- left: 16px;
- justify-content: center;
- text-align: center;
- }
-
- .project-logs-shell {
- min-height: calc(100svh - 144px);
- overflow: visible;
- }
-
- .project-logs-toolbar {
- align-items: stretch;
- flex-direction: column;
- gap: 16px;
- padding: 20px 16px;
- }
-
- .project-logs-filters {
- align-items: stretch;
- flex-wrap: wrap;
- }
-
- .project-logs-filters label:first-child,
- .project-logs-search {
- flex: 1 1 100%;
- }
-
- .project-logs-filters select,
- .project-logs-search input {
- width: 100%;
- }
-
- .project-logs-live {
- justify-content: center;
- }
-
- .project-logs-stream {
- min-height: 420px;
- padding: 16px;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .project-home *,
- .project-home *::before,
- .project-home *::after {
- animation-duration: 0.01ms !important;
- scroll-behavior: auto !important;
- transition-duration: 0.01ms !important;
+ bottom: calc(4.5rem + env(safe-area-inset-bottom));
}
}
diff --git a/apps/web/src/pages/workspace-page.tsx b/apps/web/src/pages/workspace-page.tsx
index 3b0a13c..cc3f0e9 100644
--- a/apps/web/src/pages/workspace-page.tsx
+++ b/apps/web/src/pages/workspace-page.tsx
@@ -3,13 +3,18 @@ import {
ArrowUpRightIcon,
CheckIcon,
FolderKanbanIcon,
- LogOutIcon,
+ PlugIcon,
PlusIcon,
- Settings2Icon,
+ SearchIcon,
} from "lucide-react"
import { Link, Navigate, useNavigate, useParams } from "react-router-dom"
import { useAuth } from "@/auth/auth-context"
+import {
+ AppShell,
+ BreadcrumbSeparator,
+ SidebarNavItem,
+} from "@/components/app-shell"
import { BrandMark } from "@/components/brand-mark"
import { ProjectCreateDialog } from "@/components/project-create-dialog"
import { Alert, AlertDescription } from "@/components/ui/alert"
@@ -23,6 +28,7 @@ import {
import { Input } from "@/components/ui/input"
import { Spinner } from "@/components/ui/spinner"
import { ApiError } from "@/lib/api"
+import { initials } from "@/lib/initials"
import { listProjects } from "@/lib/projects"
import type { Project, Workspace } from "@/lib/types"
@@ -81,15 +87,18 @@ export function NewWorkspacePage() {
return (
-
-
-
+
-
-
+
+
Create your first workspace
-
+
A workspace is where your ideas come together.
@@ -125,14 +134,14 @@ export function NewWorkspacePage() {
type="submit"
size="lg"
disabled={isSubmitting}
- className="h-11 w-full text-sm"
+ className="h-9 w-full text-sm"
>
{isSubmitting && }
Create workspace
-
+
Projects and services can be added any time.
@@ -141,13 +150,14 @@ export function NewWorkspacePage() {
}
export function WorkspacePage() {
- const { session, signOut } = useAuth()
+ const { session } = useAuth()
const { workspaceId: routeWorkspaceId } = useParams()
const navigate = useNavigate()
const workspace = session?.workspace
const [projects, setProjects] = useState
(null)
const [projectsError, setProjectsError] = useState(null)
const [isCreateOpen, setIsCreateOpen] = useState(false)
+ const [search, setSearch] = useState("")
const workspaceId = workspace?.id
useEffect(() => {
@@ -179,19 +189,17 @@ export function WorkspacePage() {
}
}, [workspaceId])
- async function handleSignOut() {
- try {
- await signOut()
- } finally {
- navigate("/login", { replace: true })
- }
- }
-
if (!session || !workspace || workspace.id !== routeWorkspaceId) {
return null
}
const currentWorkspaceId = workspace.id
+ const normalizedSearch = search.trim().toLowerCase()
+ const visibleProjects = (projects ?? []).filter(
+ (project) =>
+ !normalizedSearch ||
+ `${project.name} ${project.slug}`.toLowerCase().includes(normalizedSearch)
+ )
function handleProjectCreated(project: Project) {
setProjects((currentProjects) =>
@@ -202,138 +210,152 @@ export function WorkspacePage() {
}
return (
-
-
-
-
-
-
- {session.user.email}
-
-
setIsCreateOpen(true)}
- >
-
- New project
-
-
navigate("/settings/integrations")}
- >
-
- Settings
-
-
-
- Sign out
-
-
-
-
-
-
-
-
Workspace
-
- Welcome to {workspace.name}
-
-
- Create a project to map its services, data, and infrastructure in
- one place.
-
+
}
+ active
+ to={`/workspace/${workspace.id}`}
+ />
+ }
+ footerNav={
+
}
+ to="/settings/integrations"
+ />
+ }
+ breadcrumbs={
+ <>
+
{workspace.name}
+
+
Projects
+ >
+ }
+ actions={
+
setIsCreateOpen(true)}>
+
+ New project
+
+ }
+ >
+
+
+
+
+
Workspace
+
Welcome to {workspace.name}
+
+ Each project groups its services, databases and private network.
+
+
-
setIsCreateOpen(true)}>
-
- New project
-
-
- {projectsError && (
-
- {projectsError}
-
- )}
+ {projectsError && (
+
+ {projectsError}
+
+ )}
- {projects === null ? (
-
-
- Loading projects
-
- ) : projects.length === 0 ? (
-
-
-
-
-
-
- Create your first project
-
-
- Projects give your workspace a focused home for topology,
- resources, and future deployments.
-
+ {projects === null ? (
+
+
+ Loading projects
-
setIsCreateOpen(true)}>
-
- Create project
-
-
- ) : (
-
-
+ ) : projects.length === 0 ? (
+
+
+
+
-
- Projects
-
-
- Choose a project to open its home.
+
Create your first project
+
+ Projects give your workspace a focused home for topology,
+ resources, and deployments.
-
- {projects.length}{" "}
- {projects.length === 1 ? "project" : "projects"}
-
-
-
- {projects.map((project) => (
-
-
-
-
-
-
- {project.name}
+ setIsCreateOpen(true)}>
+
+ Create project
+
+
+
+ 01 Create a project
+
+
+ 02 Add services and data
+
+
+ 03 Deploy and observe
+
+
+
+ ) : (
+
+
+
+
Projects
+ {projects.length}
+
+
+
+ Search projects
+ setSearch(event.target.value)}
+ />
+
+
+
+ {visibleProjects.map((project) => (
+
+
+
+ {initials(project.name)}
+
+
-
- /{project.slug}
+
+ {project.name}
+ /{project.slug}
-
-
-
- ))}
-
-
- )}
-
+
+ ))}
+ {!normalizedSearch && (
+ setIsCreateOpen(true)}
+ >
+
+ New project
+
+ )}
+
+ {visibleProjects.length === 0 && (
+
+ No projects match “{search.trim()}”.
+
+ )}
+
+ )}
+
+
-
+
)
}
function WorkspaceFrame({ children }: { children: ReactNode }) {
return (
-
-
- {children}
-
+
+ {children}
)
}
@@ -369,15 +391,15 @@ function WorkspaceCreated({
-
-
+
+
Workspace created
-
+
{workspace.name} is ready for your ideas.
-
+
Continue to workspace
diff --git a/docs/frontend-system-design.md b/docs/frontend-system-design.md
index b363d4a..d7e5d3e 100644
--- a/docs/frontend-system-design.md
+++ b/docs/frontend-system-design.md
@@ -2,7 +2,7 @@
> **Status:** Implemented MVP, living design contract
> **Scope:** `apps/web`
-> **Last updated:** 2026-09-13
+> **Last updated:** 2026-10-04
> **Frontend:** Vite + React 19 + React Router + shadcn/Base UI + Tailwind CSS v4
> **Production:** `https://cloud.knotree.com`
> **API:** `https://cloudapi.knotree.com/api/v1`
@@ -48,7 +48,8 @@ session should feel quick, calm, and trustworthy:
Metrics, Console, and Settings. The Database section reads and mutates the
selected project's dedicated PostgreSQL instance through typed API calls;
Metrics reads live per-project runtime data through the same boundary.
-- Light and dark dashboard themes persisted as local view preferences.
+- One app-wide light/dark theme, persisted as a local view preference and
+ applied before first paint.
- Cloudflare Workers static-asset deployment with SPA fallback.
### Not in the current slice
@@ -133,11 +134,13 @@ Navigation is intentionally small at the workspace boundary:
- Brand mark identifies the product on auth/onboarding screens.
- Auth screens link only to the alternate auth mode.
-- Workspace project index exposes the signed-in email, project list, `New
-project`, and `Sign out`.
-- Project home provides Resources, Topology, Metrics, Logs, and Integrations
- navigation, workspace/environment context, and a clear route back to the
- workspace project index.
+- Every signed-in screen uses the shared `AppShell`: a sidebar on the canvas
+ (workspace switcher, scope navigation, Integrations pinned to the bottom)
+ and an inset panel with a breadcrumb header. The header always carries the
+ page action, the theme toggle, and the account menu (identity, Integrations,
+ `Sign out`).
+- Workspace scope navigation: Projects. Project scope navigation: Resources,
+ Topology, Metrics, Logs; the sidebar also shows the open project.
- The workspace success state provides the first forward transition into the
workspace.
@@ -242,40 +245,23 @@ creating a second database.
### 5.1 Authentication shell
-Component: `AuthShell`
+Component: `AuthShell`
Used by: `/login`, `/register`
-#### Desktop composition
-
-- Full viewport white canvas with `1rem` outer padding; `2rem` from the
- `md` breakpoint upward.
-- Centered frame with `max-width: 1200px`.
-- Two-column grid at `lg`:
- - brand panel: `0.82fr`;
- - form panel: `1.18fr`.
-- Frame has a subtle border, `1rem` radius, and a low-contrast shadow.
-- Brand panel has a minimum viewport-height and contains:
- - `Knotree Cloud` brand mark;
- - headline: “A calmer way to build together”;
- - supporting copy: “Knotree Cloud gives teams a flexible workspace to turn
- ideas into structure.”;
- - decorative line-art motif;
- - footer phrase: “Ideas. Structure. Progress together.”
-- Form panel centers a content column no wider than `25rem`.
-
-#### Mobile composition
-
-- The frame collapses to one column.
-- The brand panel becomes a compact top section with a minimum height of
- `18rem`; the form follows below.
-- Form padding is `1.5rem` on small screens and increases at `md`/`lg`.
-- Decorative imagery is clipped and never becomes interactive or required to
- understand the form.
+- Two columns from `lg`: a form column (brand, alternate-mode link, centered
+ form no wider than `22.5rem`, quiet footer) and an inset showcase panel on a
+ dotted canvas.
+- The showcase is a static schematic of one service linked to its data stores
+ over the private network, with the caption “Services, databases and the
+ network between them, in one place.” It is `aria-hidden` and never presents
+ itself as live data.
+- Below `lg` the showcase is hidden; below `480px` the alternate-mode prompt
+ keeps only its link.
### 5.2 Login
Heading: `Welcome back`
-Supporting text: `Sign in to your workspace`
+Supporting text: `Sign in with your Knotree account to open your workspace.`
Fields:
@@ -312,66 +298,43 @@ On success, the account is already authenticated. In development,
### 5.4 Create first workspace
-Component: `NewWorkspacePage`
+Component: `NewWorkspacePage`
Route: `/new/workspace`
-Layout:
-
-- Full viewport white canvas.
-- Centered content frame no wider than `38rem`.
-- Brand mark at the top.
-- Two-segment stepper with accessible label `Step 1 of 2`.
-- Content column no wider than `25rem`.
+- Canvas background with the compact brand mark top-left.
+- A single bordered card no wider than `26rem` holding a `Step 1 of 2`
+ indicator, heading `Create your first workspace`, supporting text `A
+ workspace is where your ideas come together.`, the workspace name field
+ (required, 1–80 characters), and the `Create workspace` CTA.
+- Helper text: `Projects and services can be added any time.`
-Heading: `Create your first workspace`
-Supporting text: `A workspace is where your ideas come together.`
-
-Fields:
-
-| Field | Behavior |
-| -------------- | -------------------------- |
-| Workspace name | Required, 1–80 characters. |
-
-CTA: `Create workspace`
-Helper text: `Projects and services can be added any time.`
-
-The API generates a UUID for workspace identity. The UUID is returned by
-workspace creation and session endpoints and forms the workspace route; users
-do not choose a workspace URL slug.
+The API generates a UUID for workspace identity; users do not choose a
+workspace URL slug.
### 5.5 Creation confirmation
-Displayed inline after a successful `POST /workspaces`:
-
-- Centered checkmark in the primary color.
-- Heading: `Workspace created`.
-- Supporting copy: `
is ready for your ideas.`
-- Full-width CTA: `Continue to workspace`.
-
-The confirmation is intentionally separate from the API response transition so
-the user can recognize that the workspace was created before entering it.
+The same card shows a check mark on the brand-soft surface, `Workspace
+created`, ` is ready for your ideas.`, and a full-width
+`Continue to workspace` CTA.
### 5.6 Workspace project index
Route: `/workspace/:workspaceId`
-This is the authenticated project index and the entry point for project work.
-
-- Header contains compact brand mark on the left.
-- Signed-in email is visible on desktop and hidden on narrow screens to keep
- the header compact.
-- Header provides `New project` and `Sign out` actions.
-- Empty state uses the heading `Create your first project`, explains that a
- project owns topology/resources, and keeps `Create project` as the dominant
- action.
-- Populated state lists projects with name, URL slug, and links to their homes.
-- Loading uses a centered spinner; project-list failures use a page-level
- alert while leaving the create action available.
+- Rendered inside `AppShell` with the breadcrumb ` / Projects` and
+ `New project` as the header action.
+- Page header: eyebrow `Workspace`, heading `Welcome to `.
+- Populated state: a `Projects` heading with the count, a project search, and a
+ card grid. Each card shows the project initials tile, name, and mono `/slug`;
+ a dashed `New project` card closes the grid.
+- Empty state uses the heading `Create your first project`, a `Create project`
+ CTA, and a three-step explainer (create, add services and data, deploy and
+ observe).
+- Loading uses an inline spinner row; list failures use a page-level alert
+ while leaving the create action available.
The workspace route must only render the workspace returned for the current
-session. A mismatching UUID is corrected by the route guard. Legacy workspace
-slug URLs redirect to the current workspace UUID; legacy project URLs retain
-the project slug during that redirect.
+session. A mismatching UUID is corrected by the route guard.
### 5.7 Project dashboard
@@ -385,62 +348,36 @@ page-local surface tokens.
#### Desktop composition
-- Topbar: Knotree mark, workspace and project context, environment, `Add
- resource`, theme toggle, and sign-out.
-- Side rail: Resources (active by default), Topology, Metrics, Logs, and
- Integrations. Integrations opens the GitHub integration settings.
-- The `Resources` view has search plus `All resources`, `App services`, and
- `Data stores` filters. Its table lists app services, hosted HTML pages,
- PostgreSQL, and Redis with name, type, status, and an action to open the
- selected resource. Empty and no-match states explain the next action.
-- `Add resource` opens Postgres, Redis, and App service / HTML page creation.
- App service creation caps a project at six services; resource creation and
- status come from the real API responses.
-- The `Topology` view has the project service card, optional real PostgreSQL
- and Redis cards, dashed connectors, and zoom/theme controls. Dragged positions
- and view preferences are persisted per project.
-- The `Metrics` view lists resources that expose runtime metrics. Selecting one
- opens its Metrics workspace tab, which renders real API samples and explicit
+- `AppShell` chrome: sidebar with the workspace switcher, the open project
+ (initials, name, `/slug`), Resources / Topology / Metrics / Logs, and
+ Integrations at the bottom. The header breadcrumb is ` /
+ ` plus an environment badge; `Add resource` is the primary action.
+- `Add resource` opens a menu of Postgres, Redis, Images, and App service /
+ HTML page, each with a one-line description. App service creation is capped
+ at six services and the option says so when the cap is reached.
+- The `Resources` view opens by default: a health strip (Resources, Healthy,
+ Deploying, Needs attention — counted from real statuses), a segmented filter
+ (`All resources`, `App services`, `Data stores`), search, and a table of
+ name + mono detail, type, status badge, and `Open`. Rows are clickable. The
+ empty state offers quick-create tiles for each resource type.
+- The `Topology` view is a full-bleed dotted canvas with a legend, draggable
+ resource cards (icon, name, type, mono detail, status, volume), dashed
+ private-network connectors from the database to assigned services, and a
+ zoom dock (zoom out, readout, zoom in, fit). Data stores carry a brand-colour
+ edge.
+- The `Metrics` view lists resources with runtime metrics as cards; each opens
+ the resource Metrics tab, which renders real API samples with explicit
loading, error, or unsupported-provider states.
-- The `Logs` view shows deployment logs when available and a truthful empty
- state until runtime log ingestion exists.
-- Resource cards expose name, status, and database name. Clicking a card opens
- the `ResourceWorkspace` sheet. The sheet has `Deployments`, `Database`,
- `Backups`, `Variables`, `Metrics`, `Console`, and `Settings` tabs.
-- The `Deployments` tab communicates resource status and lifecycle history.
- The `Database` tab has `Data`, `Stats`, and `Config` sub-tabs, search,
- refresh, live table rows, a table builder, bounded SQL execution, and a
- truthful `No tables yet` state when the dedicated instance has no schema
- data. `Connect` copies the real connection string without displaying
- credentials in the UI.
-- The `Metrics` tab calls the selected resource's metrics endpoint, keeps live
- polling at the server-provided sample interval, and renders CPU, memory,
- volume, network RX/TX, and disk read/write series. It displays current
- values, the last update time, loading/error states, and a Live/Paused control;
- its range selector loads 1-hour, 6-hour, 24-hour, 7-day, or 30-day history,
- and hovering any chart shows the nearest sample's timestamp and exact values.
- It never uses placeholder chart coordinates.
-- Add → `Postgres` opens the creation dialog. The API generates the
- database identifier, login role, and strong password, provisions a dedicated
- per-project instance, and returns a connection string after a successful
- connectivity check.
-- Add → `App service` opens a Docker deployment dialog with a display name,
- image reference, image access mode, and container port. Public mode pulls
- without credentials. Private mode is restricted to `ghcr.io` and exposes a
- GitHub OAuth connection action; the browser never receives the GitHub token.
-- The App service Settings menu assigns or removes a ready Postgres resource.
- The Deployments tab then shows the internal `postgres:5432` endpoint and the
- number of assigned variables; the Variables tab shows `DATABASE_URL` and
- `PGHOST/PGPORT/PGDATABASE/PGUSER/PGPASSWORD`, with secrets masked. Creating
- either resource does not change the other until the user chooses the
- connection.
-- The project node remains the application entry point. Once deployed it
- displays the real image/status and opens a service workspace with deployment
- metadata, URL, variables, and settings. A failed deployment is shown as an
- error state and can be retried through Add.
-- `Backups`, write-oriented `Settings` controls, and unsupported resource
- actions use explicit empty or unavailable states. The App service Console
- tab does not expose a shell; container logs remain a follow-up capability.
+- The `Logs` view shows deployment logs in a toolbar + stream panel and a
+ truthful empty state until runtime log ingestion exists.
+- Selecting a resource opens `ResourceWorkspace` as a right-hand drawer (at
+ most `66rem` wide) with `Deployments`, `Database`, `Backups`, `Variables`,
+ `Metrics`, `Console`, and `Settings` tabs (HTML pages add `Analytics` and
+ drop `Database`/`Backups`). The drawer header shows the type, environment,
+ and status.
+- Database, Variables, Metrics, Settings, and Console behaviour is unchanged:
+ live tables and bounded SQL, masked variables, ranged metrics with hover
+ values, assign/remove a Postgres connection, and no shell access.
#### Context and transient state
@@ -449,8 +386,9 @@ page-local surface tokens.
the current page.
- Zoom is clamped from 80% to 125%; fit returns to 100%. Zoom and selected
resource are persisted per project in local storage as view preferences.
-- The global dashboard theme and canvas theme toggle between `light` and
- `dark`, and persist in local storage. Theme controls have explicit labels.
+- One global theme toggles between `light` and `dark` from the header, persists
+ in local storage, and is applied by an inline script before first paint. The
+ first visit follows `prefers-color-scheme`.
- The Logs rail item opens a project-scoped logs shell with resource filtering,
search, and Live/Paused controls. It remains empty until log ingestion exists.
- Metrics samples are scoped to the selected dedicated PostgreSQL resource. In
@@ -472,15 +410,13 @@ workspace` action.
#### Mobile composition
-- The desktop rail becomes a fixed bottom navigation bar.
-- Topbar keeps a truncated workspace label, environment, activity, and
- notifications, and the theme action; Agent and the billing badge hide to
- preserve space.
-- The dotted canvas stacks resource cards in a single column; connectors are
- hidden because relationship lines are not useful in the narrow layout.
-- `ResourceWorkspace` becomes a full-viewport sheet with a horizontally
- scrollable tab bar; the body remains vertically scrollable and the fixed
- bottom navigation is not visible while the modal is open.
+- Below `768px` the sidebar navigation docks to a fixed bottom bar; the header
+ keeps a compact brand link, the current breadcrumb, the action (icon-only
+ `Add resource`), theme toggle, and account menu.
+- The health strip becomes two columns, the table hides the Type column, and
+ the topology cards stack in a single column without connectors.
+- `ResourceWorkspace` fills the viewport with a horizontally scrollable tab
+ bar.
- The layout must remain within the viewport width at 320px and 390px.
The project card is a logical project/App service node. PostgreSQL and App
@@ -511,102 +447,73 @@ until the user understands how to recover.
### 6.1 Visual language
-The visual direction is “quiet structure”: an airy white foundation, deep pine
-for action, indigo for links, and muted slate for supporting information.
-Avoid gradients, heavy illustrations, glassmorphism, and dense dashboard
-chrome in this product area.
+“Instrument panel”: warm near-neutral greys, hairline borders, small radii,
+and almost no shadow outside overlays. Ink (the foreground colour) is the
+action colour; pine is the single brand accent for focus, selection, links,
+and live state. Status colours (ok, warn, danger) are reserved for status and
+always paired with text. Technical values — images, hosts, slugs, database
+names, timestamps — are set in Geist Mono. Avoid gradients, glows, decorative
+illustration, and oversized display type.
### 6.2 Color tokens
-Tokens are defined in `apps/web/src/index.css` using OKLCH so components use
-semantic roles instead of hard-coded colors.
-
-| Token | Current value | Role |
-| -------------------- | ------------------------ | ----------------------------------- |
-| `background` | `oklch(1 0 0)` | Page and input background |
-| `foreground` | `oklch(0.19 0.025 255)` | Primary text |
-| `primary` | `oklch(0.31 0.075 165)` | Main CTA, success mark, active step |
-| `primary-foreground` | `oklch(0.985 0.01 165)` | Text/icon on primary |
-| `secondary` | `oklch(0.965 0.012 255)` | Secondary surfaces |
-| `muted` | `oklch(0.972 0.008 255)` | Hover and quiet surfaces |
-| `muted-foreground` | `oklch(0.52 0.025 255)` | Supporting text |
-| `accent` | `oklch(0.965 0.02 260)` | Accent surface |
-| `link` | `oklch(0.42 0.13 275)` | Text links |
-| `destructive` | `oklch(0.58 0.19 26)` | Validation and API error |
-| `border` | `oklch(0.90 0.018 255)` | Dividers and frames |
-| `input` | `oklch(0.86 0.025 255)` | Input border |
-| `ring` | `oklch(0.45 0.09 165)` | Keyboard focus ring |
-
-Rules:
-
-- Prefer semantic tokens (`bg-primary`, `text-muted-foreground`,
- `border-border`) over raw colors.
-- Do not use color as the only error or status signal; pair it with text,
- structure, or an icon.
-- Preserve the contrast of primary text, field errors, and focus rings against
- white surfaces.
+Tokens live in `apps/web/src/index.css` as OKLCH custom properties on `:root`
+and are redefined under `.dark`. Components use semantic roles, never raw
+colours.
+
+| Token | Role |
+| ------------------ | ------------------------------------------------------- |
+| `canvas` | App background behind the shell and topology canvas |
+| `background` | Panels, cards, inputs |
+| `surface` | Table headers, quiet fills, hover |
+| `surface-2` | Active nav, segmented control track, pressed states |
+| `border` / `border-strong` | Hairlines / hover and input borders |
+| `foreground` | Primary text and primary button fill |
+| `muted-foreground` | Secondary text |
+| `faint` | Tertiary text, placeholders, idle dots |
+| `brand` | Focus ring, selection, active nav icon, links |
+| `ok` / `warn` / `danger` / `info` | Status only |
+| `chart-*` | Metric series (blue, violet, green, orange; ink = fg) |
+| `code-bg` | Log and SQL surfaces |
+
+The shadcn roles (`primary`, `secondary`, `muted`, `accent`, `destructive`,
+`input`, `ring`, …) are mapped onto these tokens, so primitives follow the
+theme automatically. The `--project-*` aliases in `project-home.css` exist
+only so older component styles resolve to the same tokens.
### 6.3 Typography
-- Font family: Geist Variable, loaded from `@fontsource-variable/geist`.
-- `font-heading` and `font-sans` resolve to Geist Variable.
-- Headings use semibold weight and tight tracking, usually `text-3xl` to
- `text-5xl` depending on viewport.
-- Body copy uses regular weight, `text-base`, and approximately `1.75` line
- height for supporting paragraphs.
-- Eyebrows use small text, medium weight, uppercase, and increased tracking.
-- Error text is at least `text-sm` and remains readable when zoomed.
+- Geist Variable for UI text, Geist Mono Variable for technical values
+ (`@fontsource-variable/geist`, `@fontsource-variable/geist-mono`).
+- Base size 14px. Page titles 22px semibold with tight tracking; section
+ titles 15px; table and control text 13px; meta text 12px.
+- Numbers in stats and charts use tabular figures.
### 6.4 Spacing, shape, and elevation
-- Base radius token: `0.75rem`.
-- Small/medium/large radii derive from the base token.
-- Inputs and buttons are compact but touchable; primary form buttons use
- `h-11` in the auth/onboarding forms.
-- Form groups use about `1.25rem` between fields and `1.5rem` around the
- submit region.
-- The auth frame uses a single subtle shadow rather than layered elevation.
-- Success marks are circular and `3.25rem` in the workspace surfaces.
+- Base radius `0.5rem`; cards and tables `0.625rem`; the shell panel and
+ overlays `0.75rem`; controls `0.375rem`.
+- Controls are 32px high (`h-8`); primary auth/onboarding CTAs are 36–40px.
+- Elevation: `shadow-sm` for the shell panel and active nav, `shadow-md` for
+ hover and canvas nodes, `shadow-lg` for menus, dialogs, drawers, and toasts.
### 6.5 Iconography and imagery
-- Use `lucide-react` icons for controls and status affordances.
+- Use `lucide-react` icons at 14–16px with a 1.75 stroke for controls,
+ resource types, and navigation.
+- The Knotree glyph (`KnotreeGlyph`) is an inline SVG of three linked nodes;
+ `BrandMark` pairs it with the product name.
- Current icons: eye/eye-off for password visibility, check for success, and
log-out for sign out.
-- Decorative assets:
- - `apps/web/public/brand/knotree-mark.png` — brand mark;
- - `apps/web/public/brand/knotree-lines.png` — non-semantic auth motif.
- Decorative images use an empty `alt` attribute. Product-critical information
must remain in text.
-### 6.6 Topology dashboard tokens
-
-The topology dashboard is a denser product surface than auth/onboarding, but
-it keeps the same restraint. Its page-local tokens live in
-`apps/web/src/pages/project-home.css`:
-
-| Token | Value | Role |
-| ------------------- | --------- | ----------------------------------- |
-| `--project-bg` | `#ffffff` | Canvas and card background |
-| `--project-fg` | `#111111` | Node titles and primary controls |
-| `--project-accent` | `#1677ff` | Add CTA, selected node, active rail |
-| `--project-surface` | `#f7f8fa` | Card footers, hover, quiet surfaces |
-| `--project-muted` | `#6b7280` | Supporting text and connector lines |
-| `--project-border` | `#d9dee7` | Shell, card, and control borders |
-| `--project-ok` | `#16803c` | Ready state text and status accents |
-| `--project-warn` | `#b45309` | Pending and caution states |
-
-The default light values above are overridden by the `[data-theme="dark"]`
-dashboard scope (`#0f1216` background, `#f4f6f8` foreground, `#171c22`
-surface, and `#2a323c` border). Topology layout constants are a 64px topbar,
-a 64px desktop rail, 8px shell inset, 8px card radius, and 360px desktop
-node cards. The canvas uses a 24px dot grid, low-elevation card shadows, and
-dashed connector paths. The resource workspace is at most 1180x860px on
-desktop. On mobile the rail is 60px high, the content is stacked, and the
-resource workspace fills the viewport.
-
-Use the local topology tokens only inside the topology page. Shared auth and
-workspace surfaces continue to use the semantic OKLCH tokens above.
+### 6.6 Topology canvas
+
+The canvas uses the `canvas` token with a 22px dot grid. Nodes are 248px wide,
+positioned by percentage (centre x, top edge) inside an inset world that zooms
+from 80% to 125%. Connectors are dashed `border-strong` paths that turn solid
+`brand` when an endpoint is selected; selection adds a brand ring to the node.
## 7. Component architecture
@@ -637,7 +544,9 @@ StrictMode
| Component | Responsibility | Reuse rule |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `AuthProvider` | Bootstrap session, expose auth/workspace commands | Keep auth mutations here; pages should not own session synchronization |
-| `AuthShell` | Shared auth composition and brand panel | Use for public auth entry screens only |
+| `AuthShell` | Shared auth composition and showcase panel | Use for public auth entry screens only |
+| `AppShell`, `SidebarNavItem` | Signed-in chrome: sidebar, breadcrumb header, theme toggle, account menu | Every signed-in screen; pass scope navigation rather than re-implementing it |
+| `ThemeProvider` | App-wide light/dark theme and persistence | Read it with `useTheme`; never store a second, page-local theme |
| `BrandMark` | Product identity, compact or full size | Keep `alt=""` because adjacent text carries the name |
| `PasswordField` | Password input plus visibility toggle | Always provide correct `autocomplete` and field error |
| `LoadingScreen` | Auth bootstrap loading state | Use before protected/public route decisions are known |
@@ -749,7 +658,7 @@ topology dashboard
| `project`, load error, selected node, menus, toast | `ProjectHomePage` / `TopologyDashboard` | Project data is API-backed; view preferences persist locally |
| `postgresResource`, resource loading/error, create dialog, copy state | `TopologyDashboard` | Database metadata and credentials are API-backed; connection string is held in component state only |
| Zoom and selected resource | `TopologyDashboard` | `localStorage` keyed by project id |
-| Global theme and canvas theme | `TopologyDashboard` | `localStorage` keys `project-topology-dashboard-theme` and `project-topology-canvas-theme` |
+| Global theme | `ThemeProvider` | `localStorage` key `knotree-theme` (the legacy `project-topology-dashboard-theme` is read once) |
| Resource tab, table search, variables search, logs filters | `ResourceWorkspace` / `LogsWorkspace` | Memory only; reset when the surface unmounts |
The frontend does not store the session token, password, or workspace
diff --git a/docs/intakes/IN-048.md b/docs/intakes/IN-048.md
index fcd5a6f..e478a56 100644
--- a/docs/intakes/IN-048.md
+++ b/docs/intakes/IN-048.md
@@ -9,14 +9,14 @@ links:
- US-049
- US-050
notes: null
-status: pending
+status: completed
stories:
- US-049
- US-050
story: null
summary: "Knotree Accounts is the only sign-in for Cloud; Cloud reaches Registry for the signed-in account through Registry's TokenReview-authenticated internal API, so users never connect, consent or reconnect Registry"
type: intake
-updated_at: "2026-10-03T15:39:47.727225600+00:00"
+updated_at: "2026-10-03T19:20:38.998445800+00:00"
---
# Intake IN-048
diff --git a/docs/intakes/IN-049.md b/docs/intakes/IN-049.md
new file mode 100644
index 0000000..59c02fe
--- /dev/null
+++ b/docs/intakes/IN-049.md
@@ -0,0 +1,20 @@
+---
+created_at: "2026-10-03T17:30:38.053129300+00:00"
+docs: null
+flags: null
+id: IN-049
+input_type: change_request
+lane: normal
+links: []
+notes: null
+status: pending
+stories: []
+story: null
+summary: "Redesign the entire web frontend into a new professional dashboard: unified design tokens with light/dark theme, sidebar app shell, redesigned auth, workspace, project resources/topology/metrics/logs, resource drawer and integrations pages"
+type: intake
+updated_at: "2026-10-03T17:30:38.053133100+00:00"
+---
+
+# Intake IN-049
+
+Redesign the entire web frontend into a new professional dashboard: unified design tokens with light/dark theme, sidebar app shell, redesigned auth, workspace, project resources/topology/metrics/logs, resource drawer and integrations pages
diff --git a/docs/stories/US-051.md b/docs/stories/US-051.md
new file mode 100644
index 0000000..7cf8a41
--- /dev/null
+++ b/docs/stories/US-051.md
@@ -0,0 +1,19 @@
+---
+contract: null
+created_at: "2026-10-03T17:30:42.622890400+00:00"
+e2e: 0
+evidence: null
+id: US-051
+integration: 1
+lane: normal
+notes: null
+platform: 0
+status: implemented
+title: "Redesign the web dashboard: design tokens, sidebar shell, and restyled pages"
+type: story
+unit: 1
+updated_at: "2026-10-03T19:20:38.959951700+00:00"
+verify: null
+---
+
+# Redesign the web dashboard: design tokens, sidebar shell, and restyled pages
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0f965af..8207e5a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -117,6 +117,9 @@ importers:
'@fontsource-variable/geist':
specifier: ^5.3.0
version: 5.3.0
+ '@fontsource-variable/geist-mono':
+ specifier: ^5.3.0
+ version: 5.3.0
'@tailwindcss/vite':
specifier: ^4
version: 4.3.3(vite@8.3.0(@types/node@24.13.4)(esbuild@0.28.2)(jiti@2.7.0))
@@ -834,6 +837,9 @@ packages:
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+ '@fontsource-variable/geist-mono@5.3.0':
+ resolution: {integrity: sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==}
+
'@fontsource-variable/geist@5.3.0':
resolution: {integrity: sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==}
@@ -4162,6 +4168,8 @@ snapshots:
'@floating-ui/utils@0.2.12': {}
+ '@fontsource-variable/geist-mono@5.3.0': {}
+
'@fontsource-variable/geist@5.3.0': {}
'@hono/node-server@2.1.1(hono@4.13.7)':