diff --git a/web/package-lock.json b/web/package-lock.json index 9b24bdb..f039c4c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@fontsource/instrument-serif": "^5.2.8", + "@fontsource/inter": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8" }, "devDependencies": { @@ -447,6 +448,14 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@fontsource/jetbrains-mono": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", @@ -2300,6 +2309,11 @@ "resolved": "https://registry.npmjs.org/@fontsource/instrument-serif/-/instrument-serif-5.2.8.tgz", "integrity": "sha512-s+bkz+syj2rO00Rmq9g0P+PwuLig33DR1xDR8pTWmovH1pUjwnncrFk++q9mmOex8fUQ7oW80gPpPDaw7V1MMw==" }, + "@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==" + }, "@fontsource/jetbrains-mono": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", diff --git a/web/package.json b/web/package.json index 9c0d5aa..26a9431 100644 --- a/web/package.json +++ b/web/package.json @@ -22,6 +22,7 @@ "type": "module", "dependencies": { "@fontsource/instrument-serif": "^5.2.8", + "@fontsource/inter": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8" } } diff --git a/web/src/app.css b/web/src/app.css index c42057b..6e4df99 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,7 +1,9 @@ @import 'tailwindcss'; @import '$lib/styles/tokens.css'; -@import '@fontsource/instrument-serif/400.css'; -@import '@fontsource/instrument-serif/400-italic.css'; +@import '@fontsource/inter/400.css'; +@import '@fontsource/inter/500.css'; +@import '@fontsource/inter/600.css'; +@import '@fontsource/inter/700.css'; @import '@fontsource/jetbrains-mono/400.css'; @import '@fontsource/jetbrains-mono/500.css'; @import '@fontsource/jetbrains-mono/700.css'; @@ -69,3 +71,416 @@ input[type="number"] { ::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); } + +/* ═══════════════════════════════════════════════════════════════════ + FORGE DESIGN SYSTEM — global utilities + ═══════════════════════════════════════════════════════════════════ */ + +:root { + /* Display font now uses the app's standard sans stack. + --forge-serif is kept as an alias so all existing usages switch with one edit. */ + --forge-serif: var(--font-family-sans); + --forge-display: var(--font-family-sans); + --forge-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Code', monospace; + --forge-accent: var(--color-brand-600); + --forge-accent-soft: color-mix(in srgb, var(--color-brand-500) 14%, transparent); + --forge-glow: color-mix(in srgb, var(--color-brand-500) 32%, transparent); +} + +/* ── Dot-grid backdrop ─────────────────────────────────────────── */ +.forge-backdrop { + position: relative; + isolation: isolate; +} +.forge-backdrop::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; height: 480px; + background-image: radial-gradient(var(--border-primary) 1px, transparent 1px); + background-size: 22px 22px; + -webkit-mask-image: radial-gradient(ellipse at 20% 0%, #000 0%, transparent 70%); + mask-image: radial-gradient(ellipse at 20% 0%, #000 0%, transparent 70%); + pointer-events: none; + z-index: -1; + opacity: 0.8; +} + +/* ── Eyebrow label (mono caps) ─────────────────────────────────── */ +.forge-eyebrow { + display: inline-flex; + align-items: center; + gap: 0.55rem; + font-family: var(--forge-mono); + font-size: 0.7rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--text-tertiary); +} +.forge-eyebrow .sep { opacity: 0.5; } + +/* ── Ember (breathing accent dot) ──────────────────────────────── */ +.forge-ember { + width: 8px; height: 8px; + border-radius: 50%; + background: var(--forge-accent); + box-shadow: 0 0 0 3px var(--forge-accent-soft); + animation: forge-breathe 2.4s ease-in-out infinite; +} +@keyframes forge-breathe { + 0%, 100% { box-shadow: 0 0 0 3px var(--forge-accent-soft); } + 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-brand-500) 20%, transparent); } +} +@keyframes forge-blink { + 0%, 60%, 100% { opacity: 1; } + 70%, 90% { opacity: 0.3; } +} + +/* ── Display heading ──────────────────────────────────────────── */ +.forge-display { + font-family: var(--forge-display); + font-weight: 700; + line-height: 1.05; + letter-spacing: -0.02em; + margin: 0; + color: var(--text-primary); +} +.forge-display .accent { color: var(--forge-accent); font-weight: 700; } +.forge-display em { color: var(--forge-accent); font-style: normal; font-weight: 700; } + +/* ── Lede paragraph ───────────────────────────────────────────── */ +.forge-lede { + font-family: var(--font-family-sans); + color: var(--text-secondary); + font-size: 1rem; + line-height: 1.55; + max-width: 62ch; + margin: 0.75rem 0 0; +} +.forge-lede em { color: var(--forge-accent); font-style: normal; font-weight: 500; } +.forge-lede code { + font-family: var(--forge-mono); + font-size: 0.8em; + padding: 0.1rem 0.4rem; + background: var(--surface-card-hover); + border-radius: var(--radius-sm); + color: var(--text-primary); +} + +/* ── Status pill with pulse ────────────────────────────────────── */ +.forge-pill { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.2rem 0.55rem; + border-radius: var(--radius-full); + background: var(--surface-card-hover); + font-family: var(--forge-mono); + font-size: 0.62rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-secondary); +} +.forge-pill .pulse { + width: 6px; height: 6px; border-radius: 50%; + background: var(--text-tertiary); +} +.forge-pill.is-running { background: var(--color-success-light); color: var(--color-success-dark); } +.forge-pill.is-running .pulse { background: var(--color-success); animation: forge-blink 1.8s infinite; } +.forge-pill.is-info { background: var(--color-info-light); color: var(--color-info-dark); } +.forge-pill.is-info .pulse { background: var(--color-info); animation: forge-blink 0.8s infinite; } +.forge-pill.is-warn { background: var(--color-warning-light); color: var(--color-warning-dark); } +.forge-pill.is-warn .pulse { background: var(--color-warning); animation: forge-blink 0.9s infinite; } +.forge-pill.is-fail { background: var(--color-danger-light); color: var(--color-danger-dark); } +.forge-pill.is-fail .pulse { background: var(--color-danger); animation: forge-blink 0.5s infinite; } + +[data-theme='dark'] .forge-pill.is-running { background: color-mix(in srgb, var(--color-success) 16%, transparent); color: #86efac; } +[data-theme='dark'] .forge-pill.is-info { background: color-mix(in srgb, var(--color-info) 16%, transparent); color: #93c5fd; } +[data-theme='dark'] .forge-pill.is-warn { background: color-mix(in srgb, var(--color-warning) 16%, transparent); color: #fcd34d; } +[data-theme='dark'] .forge-pill.is-fail { background: color-mix(in srgb, var(--color-danger) 16%, transparent); color: #fca5a5; } + +/* ── Buttons ───────────────────────────────────────────────────── */ +.forge-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1rem; + background: var(--text-primary); + color: var(--surface-card); + border: 0; + border-radius: var(--radius-lg); + font-family: var(--forge-mono); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + text-decoration: none; + cursor: pointer; + transition: transform 150ms ease, box-shadow 150ms ease; + box-shadow: 0 0 0 0 var(--forge-glow); +} +.forge-btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 0 0 4px var(--forge-glow); +} +.forge-btn:disabled { opacity: 0.5; cursor: not-allowed; } + +.forge-btn-ghost { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.5rem 0.85rem; + background: var(--surface-card); + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + color: var(--text-secondary); + font-family: var(--forge-mono); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + text-decoration: none; + cursor: pointer; + transition: all 150ms ease; +} +.forge-btn-ghost:hover:not(:disabled) { + background: var(--surface-card-hover); + color: var(--text-primary); + border-color: var(--color-brand-300); +} +.forge-btn-ghost:disabled { opacity: 0.5; cursor: not-allowed; } + +.forge-btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + background: var(--surface-card); + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + color: var(--text-secondary); + cursor: pointer; + transition: all 150ms ease; +} +.forge-btn-icon:hover:not(:disabled) { + background: var(--surface-card-hover); + color: var(--text-primary); + border-color: var(--color-brand-300); +} +.forge-btn-icon:disabled { opacity: 0.5; cursor: not-allowed; } + +.forge-btn-danger { color: var(--color-danger); border-color: var(--border-primary); } +.forge-btn-danger:hover:not(:disabled) { + background: var(--color-danger-light); + border-color: var(--color-danger); + color: var(--color-danger-dark); +} +[data-theme='dark'] .forge-btn-danger:hover:not(:disabled) { + background: color-mix(in srgb, var(--color-danger) 14%, transparent); + color: #fca5a5; +} + +/* ── Panel / Card with registration marks ──────────────────────── */ +.forge-panel { + background: var(--surface-card); + border: 1px solid var(--border-primary); + border-radius: var(--radius-xl); +} +.forge-panel-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 1rem 1.35rem 0.85rem; + border-bottom: 1px solid var(--border-secondary); +} +.forge-panel-title { + font-family: var(--forge-display); + font-size: 1.35rem; + margin: 0; + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.01em; + color: var(--text-primary); +} +.forge-panel-title .accent { color: var(--forge-accent); font-weight: 600; } +.forge-panel-body { padding: 1.15rem 1.35rem 1.35rem; } + +/* ── Stat tile ─────────────────────────────────────────────────── */ +.forge-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + border: 1px solid var(--border-primary); + border-radius: var(--radius-xl); + overflow: hidden; + background: var(--surface-card); +} +.forge-stat { + padding: 1rem 1.15rem; + border-right: 1px solid var(--border-secondary); + display: flex; + flex-direction: column; + gap: 0.2rem; +} +.forge-stat:last-child { border-right: 0; } +.forge-stat-label { + font-family: var(--forge-mono); + font-size: 0.62rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--text-tertiary); +} +.forge-stat-value { + font-family: var(--forge-display); + font-size: 2.25rem; + line-height: 1.1; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-primary); + font-variant-numeric: tabular-nums; +} +.forge-stat-value.accent { color: var(--forge-accent); } +.forge-stat-value.warn { color: var(--color-warning-dark); } +.forge-stat-value.fail { color: var(--color-danger); } +.forge-stat-sub { + font-family: var(--forge-mono); + font-size: 0.66rem; + color: var(--text-tertiary); +} + +/* ── Registration corner marks (reveal on hover) ───────────────── */ +.forge-regs { + position: relative; +} +.forge-regs::before, +.forge-regs::after, +.forge-regs > .reg-bl, +.forge-regs > .reg-br { + content: ''; + position: absolute; + width: 8px; height: 8px; + border-color: var(--color-brand-500); + border-style: solid; + border-width: 0; + opacity: 0; + transition: opacity 180ms ease; + pointer-events: none; +} +.forge-regs::before { top: -1px; left: -1px; border-top-width: 2px; border-left-width: 2px; } +.forge-regs::after { top: -1px; right: -1px; border-top-width: 2px; border-right-width: 2px; } +.forge-regs:hover::before, +.forge-regs:hover::after { opacity: 1; } + +/* ── Alert ─────────────────────────────────────────────────────── */ +.forge-alert { + display: flex; + gap: 0.7rem; + align-items: center; + padding: 0.7rem 0.9rem; + background: var(--color-danger-light); + color: var(--color-danger-dark); + border: 1px solid var(--color-danger); + border-left-width: 4px; + border-radius: var(--radius-lg); + font-size: 0.875rem; +} +.forge-alert-tag { + font-family: var(--forge-mono); + font-weight: 700; + font-size: 0.65rem; + letter-spacing: 0.16em; + padding: 0.15rem 0.4rem; + background: var(--color-danger); + color: #fff; + border-radius: var(--radius-sm); +} +[data-theme='dark'] .forge-alert { + background: color-mix(in srgb, var(--color-danger) 14%, transparent); + color: #fca5a5; +} + +/* ── Terminal frame ────────────────────────────────────────────── */ +.forge-terminal { + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + background: #0b1020; + overflow: hidden; +} +[data-theme='dark'] .forge-terminal { background: #05070f; } +.forge-terminal-head { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.5rem 0.9rem; + background: #141a2e; + border-bottom: 1px solid #0a0e1c; +} +[data-theme='dark'] .forge-terminal-head { background: #0a0e1c; } +.forge-terminal-head .dot { + width: 9px; height: 9px; border-radius: 50%; + background: rgba(255,255,255,0.12); +} +.forge-terminal-head .dot:nth-child(1) { background: color-mix(in srgb, var(--color-danger) 70%, black); } +.forge-terminal-head .dot:nth-child(2) { background: color-mix(in srgb, var(--color-warning) 70%, black); } +.forge-terminal-head .dot:nth-child(3) { background: color-mix(in srgb, var(--color-success) 70%, black); } +.forge-terminal-head .title { + margin-left: 0.6rem; + font-family: var(--forge-mono); + font-size: 0.7rem; + color: rgba(255,255,255,0.45); +} +.forge-terminal-body { + margin: 0; + padding: 1rem 1.1rem; + font-family: var(--forge-mono); + font-size: 0.76rem; + line-height: 1.55; + color: #c7d0e0; + white-space: pre-wrap; + word-break: break-all; + max-height: 480px; + overflow: auto; +} + +/* ── Native heading tune-up ────────────────────────────────────── */ +h1.forge, h2.forge, h3.forge { + font-family: var(--forge-display); + font-weight: 700; + letter-spacing: -0.01em; +} + +/* ── Table refresh ─────────────────────────────────────────────── */ +.forge-table { + width: 100%; + border-collapse: collapse; +} +.forge-table thead th { + font-family: var(--forge-mono); + font-size: 0.62rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--text-tertiary); + font-weight: 600; + text-align: left; + padding: 0.7rem 1rem; + border-bottom: 1px solid var(--border-primary); + background: var(--surface-card-hover); +} +.forge-table tbody td { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-secondary); + font-size: 0.88rem; + color: var(--text-primary); +} +.forge-table tbody tr:last-child td { border-bottom: 0; } +.forge-table tbody tr:hover { background: var(--surface-card-hover); } + +/* ── Spin & fade ───────────────────────────────────────────────── */ +@keyframes forge-spin { to { transform: rotate(360deg); } } +.forge-spinner { + width: 12px; height: 12px; + border: 2px solid var(--text-tertiary); + border-right-color: transparent; + border-radius: 50%; + animation: forge-spin 0.9s linear infinite; +} diff --git a/web/src/lib/components/ConfirmDialog.svelte b/web/src/lib/components/ConfirmDialog.svelte index 9819fe7..36fd872 100644 --- a/web/src/lib/components/ConfirmDialog.svelte +++ b/web/src/lib/components/ConfirmDialog.svelte @@ -25,12 +25,6 @@ oncancel }: Props = $props(); - const confirmClass = $derived( - confirmVariant === 'danger' - ? 'bg-[var(--color-danger)] hover:bg-[var(--color-danger-dark)] focus-visible:outline-[var(--color-danger)]' - : 'bg-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] focus-visible:outline-[var(--color-brand-600)]' - ); - const iconBgClass = $derived( confirmVariant === 'danger' ? 'bg-[var(--color-danger-light)]' @@ -50,28 +44,29 @@
{message}
+{message}
{description}
+{description}
{/if} {#if actionLabel} {#if actionHref} - -{@render lede_html()}
+ {:else if lede} +{lede}
+ {/if} +docker-compose.yml. All services in the blueprint deploy as a single atomic unit.",
+ "back": "Stacks",
+ "name": "Name",
+ "namePlaceholder": "my-app-stack",
+ "nameHint": "Lowercase, hyphenated. Used as the compose project name.",
+ "description": "Description",
+ "descriptionPlaceholder": "What does this stack do?",
+ "composeYaml": "Compose YAML",
+ "required": "required",
+ "optional": "optional",
+ "loadSample": "Load sample",
+ "uploadFile": "Upload file",
+ "dropHere": "Drop a docker-compose.yml here",
+ "dropSub": "or click to browse · or use Load sample above",
+ "lines": "{n} lines",
+ "bytes": "{n} bytes",
+ "clear": "Clear",
+ "deployImmediate": "Deploy immediately",
+ "deployHint": "Strike while the iron's hot. If unchecked, the stack is saved cold.",
+ "cancel": "Cancel",
+ "forging": "Forging…",
+ "forgeAndDeploy": "Forge & deploy",
+ "saveBlueprint": "Save blueprint",
+ "errorRequired": "Name and compose YAML are required.",
+ "errorCreate": "Failed to create stack"
+ },
+ "detail": {
+ "manifest": "MANIFEST",
+ "loading": "Loading blueprint…",
+ "composeProject": "COMPOSE PROJECT",
+ "noDescription": "No description",
+ "refresh": "Refresh",
+ "start": "Start",
+ "stop": "Stop",
+ "delete": "Delete",
+ "fault": "FAULT",
+ "err": "ERR",
+ "stats": {
+ "services": "Services",
+ "servicesSub": "in blueprint",
+ "running": "Running",
+ "runningSub": "active containers",
+ "revisions": "Revisions",
+ "revisionsSub": "in history",
+ "current": "Current",
+ "currentSub": "deployed"
+ },
+ "services": {
+ "title": "Services",
+ "count": "{n} on the floor",
+ "empty": "— no containers running —"
+ },
+ "tabs": {
+ "blueprint": "Blueprint",
+ "revisions": "Revisions",
+ "logs": "Logs"
+ },
+ "yaml": {
+ "currentRevision": "Current revision",
+ "edit": "Edit & redeploy",
+ "cancel": "Cancel",
+ "forging": "Forging…",
+ "deployNew": "Deploy new revision"
+ },
+ "revisions": {
+ "current": "CURRENT",
+ "by": "by",
+ "rollback": "← Rollback to this revision",
+ "rollbackTitle": "Rollback to revision?",
+ "rollbackMessage": "Create a new revision from rev {n} and redeploy the stack.",
+ "rollbackConfirm": "Rollback"
+ },
+ "logs": {
+ "service": "Service:",
+ "allServices": "All services",
+ "fetching": "Fetching…",
+ "fetch": "Fetch logs",
+ "empty": "— no logs loaded. tap fetch. —"
+ },
+ "delete": {
+ "title": "Delete stack?",
+ "messageBase": "This runs 'docker compose down' and removes \"{name}\".",
+ "messageVolumes": " Named volumes will also be removed.",
+ "confirm": "Delete"
+ },
+ "errors": {
+ "load": "Failed to load stack",
+ "stop": "Stop failed",
+ "start": "Start failed",
+ "update": "Update failed",
+ "rollback": "Rollback failed",
+ "delete": "Delete failed",
+ "fetchLogs": "Failed to load logs"
+ }
+ }
}
}
diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json
index 09fe1a3..cdd035b 100644
--- a/web/src/lib/i18n/ru.json
+++ b/web/src/lib/i18n/ru.json
@@ -890,5 +890,127 @@
"language": {
"en": "Английский",
"ru": "Русский"
+ },
+ "stacks": {
+ "eyebrow": "КУЗНИЦА",
+ "title": "Стеки",
+ "lede": "Compose-чертежи, выкованные как атомарные единицы. Запускайте сервисы, меняйте ревизии и откатывайтесь без нервов.",
+ "newStack": "Новый стек",
+ "refresh": "Обновить",
+ "total": "Всего",
+ "running": "Работают",
+ "deploying": "Куются",
+ "failed": "Сбой",
+ "stopped": "Холодные",
+ "empty": {
+ "title": "Наковальня остыла.",
+ "desc": "Загрузите docker-compose.yml, чтобы выковать первый стек."
+ },
+ "card": {
+ "noDescription": "Без описания",
+ "updated": "Обновлён",
+ "start": "Запустить",
+ "stop": "Остановить",
+ "delete": "Удалить",
+ "open": "Открыть"
+ },
+ "new": {
+ "eyebrow": "НОВЫЙ ЧЕРТЁЖ",
+ "title": "Выковать новый стек.",
+ "lede": "Загрузите или вставьте docker-compose.yml. Все сервисы чертежа разворачиваются как одна атомарная единица.",
+ "back": "Стеки",
+ "name": "Имя",
+ "namePlaceholder": "мой-стек",
+ "nameHint": "Строчные буквы, через дефис. Используется как имя compose-проекта.",
+ "description": "Описание",
+ "descriptionPlaceholder": "Что делает этот стек?",
+ "composeYaml": "Compose YAML",
+ "required": "обязательно",
+ "optional": "необязательно",
+ "loadSample": "Загрузить пример",
+ "uploadFile": "Загрузить файл",
+ "dropHere": "Перетащите сюда docker-compose.yml",
+ "dropSub": "или нажмите для выбора · или используйте Загрузить пример выше",
+ "lines": "{n} строк",
+ "bytes": "{n} байт",
+ "clear": "Очистить",
+ "deployImmediate": "Развернуть сразу",
+ "deployHint": "Куй железо, пока горячо. Без галочки стек сохраняется холодным.",
+ "cancel": "Отмена",
+ "forging": "Куём…",
+ "forgeAndDeploy": "Выковать и развернуть",
+ "saveBlueprint": "Сохранить чертёж",
+ "errorRequired": "Имя и compose YAML обязательны.",
+ "errorCreate": "Не удалось создать стек"
+ },
+ "detail": {
+ "manifest": "МАНИФЕСТ",
+ "loading": "Загрузка чертежа…",
+ "composeProject": "COMPOSE-ПРОЕКТ",
+ "noDescription": "Без описания",
+ "refresh": "Обновить",
+ "start": "Запустить",
+ "stop": "Остановить",
+ "delete": "Удалить",
+ "fault": "СБОЙ",
+ "err": "ОШБ",
+ "stats": {
+ "services": "Сервисы",
+ "servicesSub": "в чертеже",
+ "running": "Работают",
+ "runningSub": "активных контейнеров",
+ "revisions": "Ревизии",
+ "revisionsSub": "в истории",
+ "current": "Текущая",
+ "currentSub": "развёрнута"
+ },
+ "services": {
+ "title": "Сервисы",
+ "count": "{n} в работе",
+ "empty": "— нет запущенных контейнеров —"
+ },
+ "tabs": {
+ "blueprint": "Чертёж",
+ "revisions": "Ревизии",
+ "logs": "Логи"
+ },
+ "yaml": {
+ "currentRevision": "Текущая ревизия",
+ "edit": "Править и развернуть",
+ "cancel": "Отмена",
+ "forging": "Куём…",
+ "deployNew": "Развернуть новую ревизию"
+ },
+ "revisions": {
+ "current": "ТЕКУЩАЯ",
+ "by": "автор",
+ "rollback": "← Откатиться к этой ревизии",
+ "rollbackTitle": "Откатить ревизию?",
+ "rollbackMessage": "Создать новую ревизию из rev {n} и развернуть стек заново.",
+ "rollbackConfirm": "Откатить"
+ },
+ "logs": {
+ "service": "Сервис:",
+ "allServices": "Все сервисы",
+ "fetching": "Загрузка…",
+ "fetch": "Получить логи",
+ "empty": "— логи не загружены. нажмите получить. —"
+ },
+ "delete": {
+ "title": "Удалить стек?",
+ "messageBase": "Будет выполнен 'docker compose down' и удалён \"{name}\".",
+ "messageVolumes": " Именованные тома также будут удалены.",
+ "confirm": "Удалить"
+ },
+ "errors": {
+ "load": "Не удалось загрузить стек",
+ "stop": "Остановка не удалась",
+ "start": "Запуск не удался",
+ "update": "Обновление не удалось",
+ "rollback": "Откат не удался",
+ "delete": "Удаление не удалось",
+ "fetchLogs": "Не удалось загрузить логи"
+ }
+ }
}
}
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index d57af58..25f6aba 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -129,13 +129,9 @@
{sidebarOpen ? 'translate-x-0' : '-translate-x-full'}"
>
- {$t('dashboard.totalProjects')}
-{totalProjects}
-{$t('dashboard.runningInstances')}
-{totalRunning}
-{$t('dashboard.failedInstances')}
-{totalFailed}
-{$t('dashboard.staleContainers')}
-{totalStale}
-{$t('dashboard.totalSites')}
-- {totalSites} - {#if deployedSites > 0} - {deployedSites} {$t('dashboard.deployedSites')} - {/if} - {#if failedSitesCount > 0} - {failedSitesCount} {$t('dashboard.failedSites')} - {/if} -
-- Compose blueprints, forged as atomic units. - Spin up services, iterate on revisions, roll back without breaking a sweat. -
+{@html $t('stacks.lede')}
Upload a docker-compose.yml to forge your first stack.
{$t('stacks.empty.desc')}
-{s.description}
{:else} -No description
+{$t('stacks.card.noDescription')}
{/if} {#if s.error} @@ -141,24 +139,24 @@ {/if} {/each} @@ -168,9 +166,9 @@