-
-
+
+
{preview.dockerfile}
-
-
+
+
{preview.compose}
diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css
index 3d52af8..91ad228 100644
--- a/apps/web/src/styles/global.css
+++ b/apps/web/src/styles/global.css
@@ -428,21 +428,71 @@
margin: 0 0 1rem;
}
-.form-field {
+.form-field,
+.form-group {
display: flex;
flex-direction: column;
- gap: 0.35rem;
+ gap: var(--space-2);
+ margin-bottom: var(--space-4);
+}
+
+.form-field:last-child,
+.form-group:last-child {
+ margin-bottom: 0;
}
.form-field input,
.form-field textarea,
-.form-field select {
- padding: 0.55rem 0.7rem;
+.form-field select,
+.form-group input,
+.form-group textarea,
+.form-group select {
+ padding: var(--space-2) var(--space-3);
border: 1px solid var(--border);
- border-radius: 10px;
+ border-radius: var(--radius-md);
font: inherit;
background: var(--panel);
color: var(--ink);
+ width: 100%;
+}
+
+.form-row {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-3);
+}
+
+.form-row .form-field,
+.form-row .form-group {
+ flex: 1;
+ margin-bottom: 0;
+}
+
+.form-section {
+ padding: var(--space-4);
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+}
+
+.form-section > h4:first-child,
+.form-section > h3:first-child {
+ margin-top: 0;
+ margin-bottom: var(--space-3);
+}
+
+.form-help {
+ display: block;
+ font-size: var(--font-size-sm);
+ color: var(--muted);
+ margin-top: var(--space-1);
+ line-height: var(--line-height-normal);
+}
+
+.text-error {
+ color: var(--danger);
+ font-size: var(--font-size-sm);
+ line-height: var(--line-height-normal);
}
.dialog-actions {
@@ -494,19 +544,6 @@
flex: 1;
}
-.form-group {
- display: flex;
- flex-direction: column;
- gap: 0.35rem;
-}
-
-.form-group input {
- padding: 0.55rem 0.7rem;
- border: 1px solid var(--border);
- border-radius: 10px;
- font: inherit;
-}
-
.error {
color: #b91c1c;
padding: 0.75rem;
@@ -1205,7 +1242,11 @@ a .icon:last-child {
}
}
/* ─── Primitive Buttons ─── */
-.btn {
+.btn,
+.primary-button,
+.secondary-button,
+.ghost-button,
+.button-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -1224,29 +1265,41 @@ a .icon:last-child {
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
-.btn:focus-visible {
+.btn:focus-visible,
+.primary-button:focus-visible,
+.secondary-button:focus-visible,
+.ghost-button:focus-visible,
+.button-secondary:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
-.btn-primary {
+.btn-primary,
+.primary-button {
background: var(--brand);
color: var(--primary-fg);
border-color: var(--brand);
}
-.btn-primary:hover {
+.btn-primary:hover,
+.primary-button:hover {
background: var(--brand-strong);
border-color: var(--brand-strong);
}
-.btn-secondary {
+.btn-secondary,
+.secondary-button,
+.ghost-button,
+.button-secondary {
background: var(--panel);
color: var(--ink);
border-color: var(--border);
}
-.btn-secondary:hover {
+.btn-secondary:hover,
+.secondary-button:hover,
+.ghost-button:hover,
+.button-secondary:hover {
background: var(--bg);
}
@@ -1411,6 +1464,12 @@ a .icon:last-child {
color: var(--info);
}
+.alert-success {
+ background: var(--success-light);
+ border-color: color-mix(in srgb, var(--success) 30%, transparent);
+ color: var(--success);
+}
+
/* ─── Loading State ─── */
.loading-state {
display: flex;
@@ -1454,6 +1513,104 @@ a .icon:last-child {
color: var(--muted);
}
+/* ─── Status Badges ─── */
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
+ padding: var(--space-1) var(--space-2);
+ border-radius: var(--radius-full);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ text-transform: capitalize;
+ line-height: var(--line-height-tight);
+ background: var(--bg);
+ color: var(--muted);
+ border: 1px solid var(--border);
+}
+
+.status-badge::before {
+ content: "";
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.status-badge--running,
+.status-badge--success {
+ background: var(--success-light);
+ color: var(--success);
+ border-color: color-mix(in srgb, var(--success) 30%, transparent);
+}
+
+.status-badge--error,
+.status-badge--failed {
+ background: var(--danger-light);
+ color: var(--danger);
+ border-color: color-mix(in srgb, var(--danger) 30%, transparent);
+}
+
+.status-badge--warning {
+ background: var(--warning-light);
+ color: var(--warning);
+ border-color: color-mix(in srgb, var(--warning) 30%, transparent);
+}
+
+.status-badge--info,
+.status-badge--pending,
+.status-badge--idle {
+ background: var(--info-light);
+ color: var(--info);
+ border-color: color-mix(in srgb, var(--info) 30%, transparent);
+}
+
+/* Legacy instance-chip alias */
+.instance-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
+ padding: var(--space-1) var(--space-2);
+ border-radius: var(--radius-full);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ text-transform: capitalize;
+ line-height: var(--line-height-tight);
+ background: var(--bg);
+ color: var(--muted);
+ border: 1px solid var(--border);
+}
+
+.instance-chip::before {
+ content: "";
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.instance-chip.running {
+ background: var(--success-light);
+ color: var(--success);
+ border-color: color-mix(in srgb, var(--success) 30%, transparent);
+}
+
+.instance-chip.starting,
+.instance-chip.building,
+.instance-chip.probing {
+ background: var(--warning-light);
+ color: var(--warning);
+ border-color: color-mix(in srgb, var(--warning) 30%, transparent);
+}
+
+.instance-chip.error,
+.instance-chip.unhealthy,
+.instance-chip.stopped {
+ background: var(--danger-light);
+ color: var(--danger);
+ border-color: color-mix(in srgb, var(--danger) 30%, transparent);
+}
+
/* ─── Focus States ─── */
.nav-item:focus-visible,
.mobile-nav-item:focus-visible,
diff --git a/apps/web/src/styles/utilities.css b/apps/web/src/styles/utilities.css
index 0e3713e..2e8aebe 100644
--- a/apps/web/src/styles/utilities.css
+++ b/apps/web/src/styles/utilities.css
@@ -13,7 +13,7 @@
.stack {
display: flex;
flex-direction: column;
- gap: var(--space-4);
+ gap: var(--space-3);
}
.stack-sm {
@@ -21,20 +21,103 @@
}
.stack-md {
- gap: var(--space-4);
+ gap: var(--space-3);
}
.stack-lg {
- gap: var(--space-6);
+ gap: var(--space-4);
+}
+
+.stack-xl {
+ gap: var(--space-5);
}
.row {
display: flex;
flex-wrap: wrap;
- gap: var(--space-4);
+ gap: var(--space-3);
align-items: center;
}
+.row-sm {
+ gap: var(--space-2);
+}
+
+.row-md {
+ gap: var(--space-3);
+}
+
+.row-lg {
+ gap: var(--space-4);
+}
+
+.row.items-start,
+.items-start {
+ align-items: flex-start;
+}
+
+.row.items-center,
+.items-center {
+ align-items: center;
+}
+
+.row.items-end,
+.items-end {
+ align-items: flex-end;
+}
+
+.justify-between {
+ justify-content: space-between;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.justify-end {
+ justify-content: flex-end;
+}
+
+.flex-1 {
+ flex: 1;
+}
+
+.flex-2 {
+ flex: 2;
+}
+
+.flex-none {
+ flex: none;
+}
+
+.flex-wrap {
+ flex-wrap: wrap;
+}
+
+.min-h-0 {
+ min-height: 0;
+}
+
+.w-full {
+ width: 100%;
+}
+
+.w-fit {
+ width: fit-content;
+}
+
+.min-w-0 {
+ min-width: 0;
+}
+
+.max-w-prose {
+ max-width: 65ch;
+}
+
+.max-w-form {
+ max-width: 800px;
+}
+
.grid {
display: grid;
gap: var(--space-4);
@@ -48,6 +131,286 @@
white-space: nowrap;
}
+.text-xs {
+ font-size: var(--font-size-xs);
+}
+
+.text-sm {
+ font-size: var(--font-size-sm);
+}
+
+.text-base {
+ font-size: var(--font-size-base);
+}
+
+.text-lg {
+ font-size: var(--font-size-lg);
+}
+
+.text-xl {
+ font-size: var(--font-size-xl);
+}
+
+.font-mono {
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+}
+
+.font-medium {
+ font-weight: 500;
+}
+
+.font-semibold {
+ font-weight: 600;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-left {
+ text-align: left;
+}
+
+.text-right {
+ text-align: right;
+}
+
+/* Visual Utilities */
+.cursor-pointer {
+ cursor: pointer;
+}
+
+.overflow-auto {
+ overflow: auto;
+}
+
+.overflow-hidden {
+ overflow: hidden;
+}
+
+.rounded-sm {
+ border-radius: var(--radius-sm);
+}
+
+.rounded-md {
+ border-radius: var(--radius-md);
+}
+
+.rounded-lg {
+ border-radius: var(--radius-lg);
+}
+
+.rounded-xl {
+ border-radius: var(--radius-xl);
+}
+
+.rounded-full {
+ border-radius: var(--radius-full);
+}
+
+/* Spacing Utilities */
+.m-0 {
+ margin: 0;
+}
+
+.mt-1 {
+ margin-top: var(--space-1);
+}
+
+.mt-2 {
+ margin-top: var(--space-2);
+}
+
+.mt-3 {
+ margin-top: var(--space-3);
+}
+
+.mt-4 {
+ margin-top: var(--space-4);
+}
+
+.mt-5 {
+ margin-top: var(--space-5);
+}
+
+.mt-6 {
+ margin-top: var(--space-6);
+}
+
+.mb-0 {
+ margin-bottom: 0;
+}
+
+.mb-1 {
+ margin-bottom: var(--space-1);
+}
+
+.mb-2 {
+ margin-bottom: var(--space-2);
+}
+
+.mb-3 {
+ margin-bottom: var(--space-3);
+}
+
+.mb-4 {
+ margin-bottom: var(--space-4);
+}
+
+.mb-5 {
+ margin-bottom: var(--space-5);
+}
+
+.mb-6 {
+ margin-bottom: var(--space-6);
+}
+
+.ml-auto {
+ margin-left: auto;
+}
+
+.mr-auto {
+ margin-right: auto;
+}
+
+.ml-1 {
+ margin-left: var(--space-1);
+}
+
+.ml-2 {
+ margin-left: var(--space-2);
+}
+
+.ml-3 {
+ margin-left: var(--space-3);
+}
+
+.mr-1 {
+ margin-right: var(--space-1);
+}
+
+.mr-2 {
+ margin-right: var(--space-2);
+}
+
+.mr-3 {
+ margin-right: var(--space-3);
+}
+
+.p-1 {
+ padding: var(--space-1);
+}
+
+.p-2 {
+ padding: var(--space-2);
+}
+
+.p-3 {
+ padding: var(--space-3);
+}
+
+.p-4 {
+ padding: var(--space-4);
+}
+
+.p-5 {
+ padding: var(--space-5);
+}
+
+.px-2 {
+ padding-left: var(--space-2);
+ padding-right: var(--space-2);
+}
+
+.px-3 {
+ padding-left: var(--space-3);
+ padding-right: var(--space-3);
+}
+
+.px-4 {
+ padding-left: var(--space-4);
+ padding-right: var(--space-4);
+}
+
+.py-1 {
+ padding-top: var(--space-1);
+ padding-bottom: var(--space-1);
+}
+
+.py-2 {
+ padding-top: var(--space-2);
+ padding-bottom: var(--space-2);
+}
+
+.py-3 {
+ padding-top: var(--space-3);
+ padding-bottom: var(--space-3);
+}
+
+.py-4 {
+ padding-top: var(--space-4);
+ padding-bottom: var(--space-4);
+}
+
+.pl-2 {
+ padding-left: var(--space-2);
+}
+
+.pl-3 {
+ padding-left: var(--space-3);
+}
+
+.pl-4 {
+ padding-left: var(--space-4);
+}
+
+.pr-2 {
+ padding-right: var(--space-2);
+}
+
+.pr-3 {
+ padding-right: var(--space-3);
+}
+
+.pr-4 {
+ padding-right: var(--space-4);
+}
+
+/* Border Utilities */
+.border {
+ border: 1px solid var(--border);
+}
+
+.border-l {
+ border-left: 1px solid var(--border);
+}
+
+.border-l-2 {
+ border-left: 2px solid var(--border);
+}
+
+/* Card Utilities */
+.card-sm {
+ padding: var(--space-3);
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+}
+
+.card-md {
+ padding: var(--space-4);
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+}
+
+.card-lg {
+ padding: var(--space-5);
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+}
+
.truncate-multiline {
display: -webkit-box;
-webkit-line-clamp: 3;
@@ -3777,6 +4140,209 @@ a:active,
gap: var(--space-3);
}
+/* ─── Sticky Footer ─── */
+.sticky-footer {
+ position: sticky;
+ bottom: var(--space-4);
+ z-index: 10;
+ background: var(--panel);
+ padding: var(--space-4);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-md);
+}
+
+/* ─── Drag Items ─── */
+.drag-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-2) var(--space-3);
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ margin-bottom: var(--space-1);
+ cursor: grab;
+ transition: background 0.15s;
+}
+
+.drag-item:hover {
+ background: var(--bg);
+}
+
+.drag-item-active {
+ background: var(--brand-light, color-mix(in srgb, var(--brand) 10%, transparent));
+ border-color: var(--brand);
+}
+
+.drag-handle {
+ cursor: grab;
+ color: var(--muted);
+}
+
+/* ─── Code Block ─── */
+.code-block {
+ background: var(--code-bg, #1e1e1e);
+ color: var(--code-fg, #d4d4d4);
+ padding: var(--space-4);
+ border-radius: var(--radius-md);
+ overflow: auto;
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: var(--font-size-sm);
+ line-height: var(--line-height-normal);
+}
+
+/* ─── Empty State ─── */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: var(--space-10);
+ color: var(--muted);
+ text-align: center;
+}
+
+.empty-state-icon {
+ opacity: 0.3;
+ margin-bottom: var(--space-4);
+}
+
+.empty-state h3 {
+ margin: 0 0 var(--space-2);
+ font-weight: 500;
+}
+
+.empty-state p {
+ margin: 0;
+}
+
+/* ─── Sidebar ─── */
+.sidebar {
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid var(--border);
+ background: var(--panel);
+}
+
+.sidebar-header {
+ padding: var(--space-4);
+ border-bottom: 1px solid var(--border);
+}
+
+.sidebar-header h2 {
+ margin: 0;
+ font-size: var(--font-size-lg);
+}
+
+.sidebar-header p {
+ margin: var(--space-1) 0 0;
+ font-size: var(--font-size-sm);
+}
+
+.sidebar-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-2);
+}
+
+.sidebar-footer {
+ padding: var(--space-4);
+ border-top: 1px solid var(--border);
+}
+
+.sidebar-item {
+ width: 100%;
+ text-align: left;
+ padding: var(--space-3) var(--space-4);
+ margin-bottom: var(--space-1);
+ border-radius: var(--radius-md);
+ border: none;
+ background: transparent;
+ color: var(--ink);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ transition: background 0.15s;
+}
+
+.sidebar-item:hover {
+ background: var(--bg);
+}
+
+.sidebar-item-active,
+.sidebar-item-active:hover {
+ background: var(--brand);
+ color: white;
+}
+
+.sidebar-item-active .delete-btn {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.sidebar-item-active .delete-btn:hover {
+ background: rgba(255, 255, 255, 0.15);
+ color: white;
+}
+
+.sidebar-item-active .badge {
+ background: rgba(255, 255, 255, 0.2);
+ border-color: rgba(255, 255, 255, 0.3);
+ color: white;
+}
+
+.sidebar-item-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.sidebar-item-title {
+ font-weight: 600;
+ font-size: var(--font-size-base);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.sidebar-item-subtitle {
+ font-size: var(--font-size-sm);
+ opacity: 0.8;
+ margin-top: var(--space-1);
+}
+
+.sidebar-item .delete-btn {
+ opacity: 0;
+}
+
+.sidebar-item:hover .delete-btn,
+.sidebar-item:focus-within .delete-btn {
+ opacity: 1;
+}
+
+.sidebar-create-button {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ padding: var(--space-3);
+ border-radius: var(--radius-md);
+ border: 2px dashed var(--border);
+ background: transparent;
+ color: var(--muted);
+ cursor: pointer;
+ font-weight: 600;
+ transition: border-color 0.15s, color 0.15s;
+}
+
+.sidebar-create-button:hover {
+ border-color: var(--brand);
+ color: var(--brand);
+}
+
/* ─── Mobile FAB ─── */
.mobile-fab {
position: fixed;