v0.319.0: the app page in panels, the login in one place, a password the box never had is said in words
gates / gates (push) Successful in 52s
gates / gates (push) Successful in 52s
- style A (operator 2026-10-16): hero, tab bar and every section are panels on --bg-1, one width, 16px apart; the install page's form groups use the same rule; the settings tab's 700px inner wrapper is gone - „Belépés az alkalmazásba": name + password side by side, fetched on demand (R-254, R-709); pairing in app_login.go - the empty-value rule: no stored value -> a sentence, never an eye; page and reveal endpoint share storedValuePresent - „Cím és kulcsok" is rows (no Domain input, no disabled storage select, no install form); hint under the title - tests red-proved: app_page_tuning_test.go (4); 36 app-page parity fixtures re-captured, 4 new; install fixtures unchanged Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
{{if .FlashError}}<div class="monitoring-banner monitoring-banner-red">{{.FlashError}}</div>{{end}}
|
||||
|
||||
{{if .DataPathCards}}
|
||||
<div class="app-section" style="margin-top:1rem">
|
||||
<div class="app-section">
|
||||
<h3>{{T "app_info.hova_tegyem_a_fajlokat"}}</h3>
|
||||
<p class="form-hint">{{T "app_info.ezeket_a_mappakat_a_fajlkezeloben"}}</p>
|
||||
<div class="datapath-list">
|
||||
@@ -36,7 +36,7 @@
|
||||
{{end}}
|
||||
|
||||
{{if and .Stack.Deployed .MigrateTargets}}
|
||||
<div class="app-section" style="margin-top:1rem">
|
||||
<div class="app-section">
|
||||
<h3>{{T "app_info.athelyezes_masik_tarhelyre"}}</h3>
|
||||
<p class="form-hint">{{T "app_info.ennek_az_alkalmazasnak_az_adatait"}}</p>
|
||||
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.5rem">
|
||||
@@ -87,7 +87,7 @@ function appMigrate(btn,app,label){
|
||||
|
||||
{{/* The nightly copy, stated as a fact. It was a card on the settings page; the files it is about
|
||||
live here. */}}
|
||||
<div class="app-section deploy-cross-drive">
|
||||
<div class="app-section">
|
||||
<h3>{{T "deploy.biztonsagi_mentes"}}</h3>
|
||||
|
||||
<div class="cross-drive-nightly">
|
||||
@@ -255,13 +255,13 @@ function appMigrate(btn,app,label){
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="progressCard" class="card" style="max-width:700px;display:none">
|
||||
<div id="progressCard" class="app-section" style="display:none">
|
||||
<h3>{{T "app_export.folyamat"}}</h3>
|
||||
<div id="progressSteps"></div>
|
||||
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
|
||||
</div>
|
||||
|
||||
<div id="doneCard" class="card" style="max-width:700px;display:none">
|
||||
<div id="doneCard" class="app-section" style="display:none">
|
||||
<h3 style="color:var(--blue)">{{T "app_export.kesz"}}</h3>
|
||||
<div style="margin-bottom:1rem">
|
||||
<span id="doneFile" style="font-weight:600"></span>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{{/* THE APP'S CONFIGURATION FIELDS — shared by the install wizard and the settings tab.
|
||||
{{/* THE APP'S CONFIGURATION FIELDS — the install wizard's form (deploy.html).
|
||||
|
||||
ONE copy of this markup, rendered by two surfaces, because they are the same fields: the
|
||||
install page submits them, the settings tab shows what was submitted. `.AlreadyDeployed` is
|
||||
the single flag every branch below reads, exactly as it did when both surfaces were one page —
|
||||
that is what keeps the install form and the settings view from drifting apart.
|
||||
v0.319.0: the settings tab no longer renders this — it shows the install answers as rows and
|
||||
the login as one panel (app_settings.html, app_login.go). The `.AlreadyDeployed` branches below
|
||||
are NOT dead: `/apps/{slug}/install` still answers for an app that is installed, and renders
|
||||
this form read-only. Their guards are what keep an installed app's secrets out of that page
|
||||
(TestDeployPage_DeployedApp_DoesNotCarryTheSecret, TestR709_AnInstalledAppsPasswordIsNotInThePage)
|
||||
and the parity case `deploy_installed` renders them.
|
||||
|
||||
The secret rules are unchanged and load-bearing:
|
||||
R-254 — an INSTALLED app's `type: secret` value is not in the page; the eye fetches it.
|
||||
|
||||
@@ -14,21 +14,33 @@
|
||||
Everything here is PLAIN TEXT through html/template. No markdown, no raw HTML: a step that
|
||||
contains a tag is shown as the characters the author typed. */}}
|
||||
|
||||
<!-- Screenshots (graceful — hidden if assets don't exist) -->
|
||||
<script>
|
||||
// Defined BEFORE the images: an error event can fire as soon as an <img> is parsed.
|
||||
function appShotGone(img) {
|
||||
img.style.display = 'none';
|
||||
var box = img.parentNode, left = false;
|
||||
box.querySelectorAll('img').forEach(function (i) { if (i.style.display !== 'none') { left = true; } });
|
||||
if (!left) { box.closest('.app-section').style.display = 'none'; }
|
||||
}
|
||||
</script>
|
||||
<!-- Screenshots (graceful — each hidden if its asset does not exist, and the section with the last
|
||||
one, so a panel is never drawn around nothing) -->
|
||||
<div class="app-section" data-screenshots-section="1">
|
||||
<div class="app-screenshots" id="screenshots">
|
||||
<img src="{{screenshotURL .Meta.Slug 1}}" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
onerror="appShotGone(this)">
|
||||
<img src="{{screenshotURL .Meta.Slug 2}}" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
onerror="appShotGone(this)">
|
||||
<img src="{{screenshotURL .Meta.Slug 3}}" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
onerror="appShotGone(this)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if not .Deployed}}
|
||||
{{/* The install button lives on this tab, because this tab IS the page for a not-installed app.
|
||||
The server refuses a non-installable template regardless; hiding the button keeps the page
|
||||
from offering an action that would be rejected. */}}
|
||||
<div class="app-section" style="margin-top:1rem">
|
||||
<div class="app-section">
|
||||
{{if canInstall .Meta}}
|
||||
<a href="/apps/{{.Meta.Slug}}/install" class="btn btn-primary" data-install-cta="1"
|
||||
onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">{{T "app_info.telepites"}}</a>
|
||||
@@ -71,7 +83,7 @@
|
||||
{{if .AppInfo.Guide.Rendered}}
|
||||
<div class="app-guide" data-guide="1" data-guide-checked="{{.AppInfo.Guide.CheckedVersion}}">
|
||||
{{- range .AppInfo.Guide.Sections}}
|
||||
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
|
||||
<div class="app-section" data-guide-section="1">
|
||||
<h3>{{.Title}}</h3>
|
||||
{{if .Intro}}<p>{{.Intro}}</p>{{end}}
|
||||
{{if .Steps}}
|
||||
@@ -82,7 +94,7 @@
|
||||
</div>
|
||||
{{- end}}
|
||||
{{- if .AppInfo.Guide.FAQ}}
|
||||
<div class="app-section" style="margin-top:1rem" data-guide-faq="1">
|
||||
<div class="app-section" data-guide-faq="1">
|
||||
<h3>{{T "app_page.faq"}}</h3>
|
||||
<dl class="app-faq">
|
||||
{{- range .AppInfo.Guide.FAQ}}
|
||||
@@ -94,7 +106,7 @@
|
||||
{{- end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">{{T "app_page.guide_missing"}}</p>
|
||||
<div class="app-section" data-guide-missing="1"><p>{{T "app_page.guide_missing"}}</p></div>
|
||||
{{end}}
|
||||
|
||||
<div class="app-sections">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
the reason the tabs are four URLs and not four hidden panels: a poller on the settings tab
|
||||
would ask the box for 200 log lines every three seconds for nothing. */}}
|
||||
|
||||
<div data-tab-panel="logs">
|
||||
<div class="app-section" data-tab-panel="logs">
|
||||
<div class="logs-container" id="logs-container">
|
||||
<pre class="logs-output" id="logs-output">{{.Logs}}</pre>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
„Adatok es mentes" holds everything about the FILES, including the backup card that used to
|
||||
sit on this page. */}}
|
||||
|
||||
<div class="deploy-container">
|
||||
<div class="app-settings-tab">
|
||||
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
|
||||
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
|
||||
|
||||
@@ -62,13 +62,13 @@
|
||||
|
||||
{{/* 3. THE REST OF WHAT THE APP WANTS FROM THE HOUSEHOLD. */}}
|
||||
{{- if .InstallHoldClosed}}
|
||||
<div class="app-section" style="margin-top:1rem" id="install-hold-card">
|
||||
<div class="app-section" id="install-hold-card">
|
||||
<h3>{{T "app_info.install_hold_title"}}</h3>
|
||||
<p>{{T "app_info.install_hold_closed"}}</p>
|
||||
</div>
|
||||
{{- end}}
|
||||
{{- if .SetupGateClosed}}
|
||||
<div class="app-section" style="margin-top:1rem" id="setup-gate-card">
|
||||
<div class="app-section" id="setup-gate-card">
|
||||
<h3>{{T "app_info.setup_gate_title"}}</h3>
|
||||
<p>{{T "app_info.setup_gate_closed"}}</p>{{- if .SignupNative}}
|
||||
<p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}}
|
||||
@@ -84,7 +84,7 @@
|
||||
{{- end}}
|
||||
|
||||
{{- if .CloseSignupOffered}}
|
||||
<div class="app-section" style="margin-top:1rem" id="close-signup-card">
|
||||
<div class="app-section" id="close-signup-card">
|
||||
<h3>{{T "app_info.signup_title"}}</h3>
|
||||
<p>{{T "app_info.close_signup_text"}}</p>{{- if .SignupNative}}
|
||||
<p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}}
|
||||
@@ -93,7 +93,7 @@
|
||||
</div>
|
||||
{{- end}}
|
||||
{{- if or .SignupClosed .SignupOpenUntil}}
|
||||
<div class="app-section" style="margin-top:1rem" id="signup-card">
|
||||
<div class="app-section" id="signup-card">
|
||||
<h3>{{T "app_info.signup_title"}}</h3>
|
||||
{{- if .SignupOpenUntil}}
|
||||
<p>{{T "app_info.signup_open_until"}} {{.SignupOpenUntil}}</p>
|
||||
@@ -121,6 +121,35 @@
|
||||
|
||||
{{/* The logins, each already its own guarded card on the details page. */}}
|
||||
<div class="app-sections">
|
||||
{{/* v0.319.0 — „BELEPES AZ ALKALMAZASBA": the name and the password of the app's first login,
|
||||
side by side. The values are fetched on demand (R-254, R-709); the page learns only whether
|
||||
the box holds one (app_login.go), and a field it holds none for is a sentence, not an eye. */}}
|
||||
{{if .LoginFields}}
|
||||
<div class="app-section" id="app-login-panel" data-login-panel="1">
|
||||
<h3>{{T "app_page.login_title"}}</h3>
|
||||
<p>{{T "app_page.login_desc"}}</p>
|
||||
<div class="app-login-pair">
|
||||
{{- range .LoginFields}}
|
||||
<div class="app-login-field" data-login-field="{{.EnvVar}}">
|
||||
<label class="app-login-label" for="field-{{.EnvVar}}">{{.Label}}</label>
|
||||
{{- if .Restored}}
|
||||
<p class="form-hint" data-login-restored="{{.EnvVar}}">{{T "deploy.login_from_backup"}}</p>
|
||||
{{- else if not .Stored}}
|
||||
<p class="form-hint" data-login-not-stored="{{.EnvVar}}">{{if .IsName}}{{T "app_page.login_no_name"}}{{else}}{{T "app_page.login_no_password"}}{{end}}</p>
|
||||
{{- else if .OnDemand}}
|
||||
<div class="input-with-button">
|
||||
<input type="password" id="field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" readonly>
|
||||
<button type="button" class="btn btn-sm btn-outline" data-login-reveal="{{.EnvVar}}" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this, 'field-{{.EnvVar}}')">{{T "deploy.megjelenites"}}</button>
|
||||
</div>
|
||||
{{- else}}
|
||||
<input type="text" id="field-{{.EnvVar}}" class="form-control" value="{{.Value}}" readonly>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .InitialCreds}}
|
||||
<div class="app-section">
|
||||
<h3>{{T "app_info.kezdeti_belepesi_adatok"}}</h3>
|
||||
@@ -173,8 +202,8 @@
|
||||
household reached by pressing „Beallitasok" on an installed app. The hint lives with the
|
||||
fields it is about, at the bottom of this tab. */}}
|
||||
{{if .StorageInfo}}
|
||||
<div class="deploy-storage-info">
|
||||
<h4>{{T "deploy.adattarolas"}}</h4>
|
||||
<div class="app-section">
|
||||
<h3>{{T "deploy.adattarolas"}}</h3>
|
||||
<div class="settings-grid">
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">{{T "deploy.tarhely"}}</span>
|
||||
@@ -201,8 +230,8 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .StaleData}}
|
||||
<div class="deploy-stale-data">
|
||||
<h4>{{T "deploy.korabbi_adatok"}}</h4>
|
||||
<div class="app-section deploy-stale-data">
|
||||
<h3>{{T "deploy.korabbi_adatok"}}</h3>
|
||||
<p class="form-hint" style="margin-bottom:1rem">
|
||||
{{T "deploy.az_alkalmazas_adatainak_masolata_megtala"}}
|
||||
</p>
|
||||
@@ -230,7 +259,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .HasIntegrations}}
|
||||
<div class="app-optional-config">
|
||||
<div class="app-section">
|
||||
<h3>{{T "deploy.integraciok"}}</h3>
|
||||
<p class="config-group-desc">
|
||||
{{T "deploy.mas_telepitett_alkalmazasokkal_valo_ossz"}}
|
||||
@@ -290,7 +319,7 @@
|
||||
{{end}}
|
||||
|
||||
{{if .GeoGlobalEnabled}}
|
||||
<div class="app-optional-config">
|
||||
<div class="app-section">
|
||||
<h3>{{T "deploy.foldrajzi_korlatozas"}}</h3>
|
||||
<p class="config-group-desc">
|
||||
{{T "deploy.az_alkalmazas_egyeni_orszagkorlatozas_ne"}}
|
||||
@@ -425,7 +454,7 @@
|
||||
{{end}}
|
||||
|
||||
{{if .HasOptionalConfig}}
|
||||
<div class="app-optional-config">
|
||||
<div class="app-section">
|
||||
<h3>{{T "deploy.opcionalis_beallitasok"}}</h3>
|
||||
{{range .OptionalConfig}}
|
||||
<div class="config-group">
|
||||
@@ -519,7 +548,7 @@
|
||||
{{end}}
|
||||
|
||||
{{if .AppEmailSupported}}
|
||||
<div class="app-optional-config">
|
||||
<div class="app-section">
|
||||
<h3>{{T "deploy.email_kuldes"}}</h3>
|
||||
<p class="config-group-desc">
|
||||
{{T "deploy.ez_az_alkalmazas_tud_emailt"}}
|
||||
@@ -542,18 +571,38 @@
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="app-section">
|
||||
{{/* „CIM ES KULCSOK" (v0.319.0) — what was set at install, as rows. The hint that says they
|
||||
cannot be changed sits under the title it is about, not at the bottom of the tab. The domain,
|
||||
the subdomain and the data folder are the „Cim" and „Tarhely" rows; the login is the panel
|
||||
above; everything else is a row here (app_login.go appSettingRows). */}}
|
||||
<div class="app-section" data-address-keys="1">
|
||||
<h3>{{T "app_page.address_and_keys"}}</h3>
|
||||
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p>
|
||||
{{if .EffectiveSubdomain}}
|
||||
<div class="app-field-row">
|
||||
<div class="app-field-label">{{T "app_page.address"}}</div>
|
||||
<div class="app-field-value" data-app-address-row="1">{{.EffectiveSubdomain}}.{{.Domain}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="deploy-form" class="deploy-form">
|
||||
{{template "app_deploy_fields" .}}
|
||||
</form>
|
||||
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p>
|
||||
{{if .CurrentHDDPath}}
|
||||
<div class="app-field-row">
|
||||
<div class="app-field-label">{{T "deploy.tarhely"}}</div>
|
||||
<div class="app-field-value" data-app-storage-row="1">{{if .CurrentHDDPathMissing}}{{T "deploy.nem_elerheto"}}{{else if .CurrentHDDLabel}}{{.CurrentHDDLabel}}{{else}}<span class="mono">{{.CurrentHDDPath}}</span>{{end}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- range .SettingsRows}}
|
||||
<div class="app-field-row" data-setting-row="{{.EnvVar}}">
|
||||
<div class="app-field-label">{{.Label}}</div>
|
||||
{{- if .OnDemand}}
|
||||
<div class="app-field-value input-with-button">
|
||||
<input type="password" id="auto-field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" readonly aria-label="{{.Label}}">
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this)">{{T "deploy.megjelenites"}}</button>
|
||||
</div>
|
||||
{{- else}}
|
||||
<div class="app-field-value">{{.Value}}</div>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -612,46 +661,9 @@ function icCopyPw(btn) {
|
||||
|
||||
{{template "app_press_script" .}}
|
||||
<script>
|
||||
function checkStorageSpace(sel) {
|
||||
var opt = sel.options[sel.selectedIndex];
|
||||
var warn = document.getElementById('storage-space-warn');
|
||||
if (!warn) return;
|
||||
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
|
||||
warn.style.display = freePct < 20 ? 'block' : 'none';
|
||||
}
|
||||
// Check on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
|
||||
if (sel) checkStorageSpace(sel);
|
||||
});
|
||||
|
||||
// R-254 site two: on an already-deployed app the value is NOT in this page — it is fetched on
|
||||
// demand, and the server records the act. Nothing caches it between presses.
|
||||
// R-709 (v0.280.0): an installed app's `type: password` value, fetched on demand like a secret.
|
||||
function revealPasswordField(stackName, envVar, btn) {
|
||||
var el = document.getElementById('field-' + envVar);
|
||||
if (!el) return;
|
||||
if (btn.dataset.shown === '1') {
|
||||
el.value = '';
|
||||
el.type = 'password';
|
||||
btn.dataset.shown = '';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
|
||||
credentials: 'same-origin',
|
||||
body: 'env_var=' + encodeURIComponent(envVar)
|
||||
}).then(function (r) { return r.json(); }).then(function (j) {
|
||||
btn.disabled = false;
|
||||
if (!j.ok) { showAlert(j.error || '{{T "deploy.a_lekeres_nem_sikerult"}}'); return; }
|
||||
el.value = j.data.value;
|
||||
el.type = 'text';
|
||||
el.dataset.revealed = '1';
|
||||
btn.dataset.shown = '1';
|
||||
}).catch(function () { btn.disabled = false; showAlert('{{T "deploy.a_lekeres_nem_sikerult"}}'); });
|
||||
}
|
||||
// R-254 site two: on an already-deployed app no secret is in this page — it is fetched on demand,
|
||||
// and the server records the act. Nothing caches it between presses. Since v0.319.0 the login
|
||||
// panel's name and password use this too (R-709: a `type: password` value, fetched like a secret).
|
||||
function revealAutoField(stackName, envVar, btn, elId) {
|
||||
var el = document.getElementById(elId || ('auto-field-' + envVar));
|
||||
if (!el) return;
|
||||
|
||||
@@ -1300,13 +1300,17 @@ a.stat-card:hover {
|
||||
.login-footer a:hover { color: var(--blue); }
|
||||
|
||||
/* --- App Info Page --- */
|
||||
/* ── THE APP PAGE (v0.316.0) — one column, hairlines, boxes only where they mean something ───────
|
||||
Operator-approved mockup, 2026-10-15. The rules, and why each one:
|
||||
ONE COLUMN of ONE WIDTH. Boxes of different widths read as a page assembled by accident.
|
||||
SECTIONS ARE SEPARATED BY HAIRLINES, not drawn as cards. A card says "this is a thing you act
|
||||
on"; most of an app page is a thing you READ, so a card there is a promise it cannot keep.
|
||||
A BOX MEANS SOMETHING. Exactly two exist: blue when the app is waiting for the household, amber
|
||||
when something is risky. The exception-colour principle, applied to shape as well as colour.
|
||||
/* ── THE APP PAGE (v0.319.0) — one column, the header, the tab bar and every section a panel ─────
|
||||
Operator-approved mockup „Felhom app oldal — egy oszlop", 2026-10-16, section style A. The rules:
|
||||
ONE COLUMN of ONE WIDTH. Every panel is the column's full width; panels of different widths
|
||||
read as a page assembled by accident.
|
||||
PANELS ON --bg-1. v0.316.0's hairline sections sat on the page background with the header and
|
||||
the tab bar, and the operator found the page flat: nothing told one section from the next.
|
||||
Now the hero, the tab bar and each section are their own panel, 16px apart.
|
||||
ONE SECTION STYLE ON EVERY SURFACE — the four tabs and the install page. Never mixed. Pinned by
|
||||
TestAppPage_OneSectionStyleEverywhere.
|
||||
COLOUR STILL MEANS SOMETHING. A panel is neutral; blue is the app waiting for the household,
|
||||
amber is a risk. The callouts keep their colours inside or as a panel.
|
||||
Tokens only — no raw hexes, 2px radius, no shadows. */
|
||||
|
||||
.app-page {
|
||||
@@ -1330,18 +1334,23 @@ a.stat-card:hover {
|
||||
}
|
||||
.app-back:hover { color: var(--text-1); }
|
||||
|
||||
/* HERO — no box, no border. The app's identity is a heading, not a card. */
|
||||
/* HERO — its own panel. */
|
||||
.app-hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.app-hero-logo {
|
||||
width: 64px; height: 64px;
|
||||
min-width: 64px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg-1);
|
||||
background: var(--bg-2);
|
||||
border-radius: var(--radius);
|
||||
object-fit: contain;
|
||||
padding: 8px;
|
||||
@@ -1364,40 +1373,62 @@ a.stat-card:hover {
|
||||
worked. A success that needed no decision is not news; it is a date. */
|
||||
.app-meta { font-size: 12px; color: var(--text-3); }
|
||||
|
||||
/* TABS — links with a line under the open one. No pills, no fills. */
|
||||
/* TABS — a panel; the open tab is filled and underlined. */
|
||||
.app-tabs {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
margin: 28px 0 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 4px;
|
||||
margin: 16px 0 20px;
|
||||
}
|
||||
.app-tab {
|
||||
font-size: 15px; font-weight: 600;
|
||||
padding: 12px 0;
|
||||
padding: 10px 18px;
|
||||
color: var(--text-2);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius);
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.app-tab:hover { color: var(--text-1); }
|
||||
.app-tab-active { color: var(--text-1); border-bottom-color: var(--blue-bright); }
|
||||
.app-tab-active { color: var(--text-1); background: var(--bg-2); border-bottom-color: var(--blue-bright); }
|
||||
|
||||
/* SECTIONS — a hairline between, nothing around. */
|
||||
.app-section {
|
||||
padding: 28px 0;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
background: none;
|
||||
border-left: 0; border-right: 0; border-top: 0;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
/* SECTIONS — style A: each its own panel, the column's width, 16px apart, the title 18px from the
|
||||
top edge with a soft rule under it. The install page's form groups (.form-section inside the
|
||||
wizard's form) and its progress panel are the same panel: one style on every surface. */
|
||||
.app-section,
|
||||
.app-page .deploy-form .form-section,
|
||||
.app-page .deploy-progress {
|
||||
padding: 18px 24px 22px;
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.app-section:last-child { border-bottom: 0; }
|
||||
.app-section > h3, .app-section > h4 {
|
||||
font-size: 17px; font-weight: 700; color: var(--text-1); margin: 0 0 6px;
|
||||
.app-section > h3:first-child, .app-section > h4:first-child,
|
||||
.app-page .deploy-form .form-section > h4:first-child,
|
||||
.app-page .deploy-progress > h3:first-child {
|
||||
font-size: 17px; font-weight: 700; color: var(--text-1);
|
||||
margin: 0 0 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
/* A heading further down a panel is a sub-heading, not a second title. */
|
||||
.app-section > h3:not(:first-child), .app-section > h4:not(:first-child) {
|
||||
font-size: 14px; font-weight: 600; color: var(--text-1); margin: 16px 0 6px;
|
||||
}
|
||||
.app-section > p { font-size: 14px; color: var(--text-2); line-height: 1.55; margin: 0 0 16px; }
|
||||
.app-section > :last-child { margin-bottom: 0; }
|
||||
/* The install wizard's form was one box around all its groups; the groups are the panels now. */
|
||||
.app-page .deploy-form { background: none; border: 0; padding: 0; }
|
||||
|
||||
/* THE LOGIN PAIR — the name and the password side by side, stacked on a phone. */
|
||||
.app-login-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
||||
.app-login-field { min-width: 0; }
|
||||
.app-login-label { display: block; font-size: 13px; color: var(--text-2); margin-bottom: 6px; }
|
||||
|
||||
/* ROWS — label and value, one grid so every value on the page starts at the same x. */
|
||||
.app-field-row {
|
||||
@@ -1418,17 +1449,13 @@ a.stat-card:hover {
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.app-callout > h3, .app-callout > h4 { font-size: 15px; font-weight: 700; margin: 0 0 6px; color: var(--text-1); }
|
||||
.app-callout > h3, .app-callout > h4 { font-size: 15px; font-weight: 700; margin: 0 0 6px; color: var(--text-1); padding-bottom: 0; border-bottom: 0; }
|
||||
/* „The app is waiting for you" — a request, never a fault, so blue and never amber. */
|
||||
.app-callout-ask { border-color: var(--blue); background: var(--blue-dim); }
|
||||
/* „Something here is risky" — the exception colour, and only on a deviation. */
|
||||
.app-callout-warn { border-color: var(--warn); background: var(--warn-dim); }
|
||||
|
||||
/* The settings tab reuses the install wizard's field markup, which carries that page's boxes: a
|
||||
card around the whole form and one around every generated value. On a TAB those are sections, so
|
||||
the boxes come off — the input itself is still a field, which is what tells the household it is a
|
||||
value. Scoped to .app-section, so the install page (same column, its own form) is untouched. */
|
||||
.app-section .deploy-form,
|
||||
/* A box inside a panel would be a narrower inner card; the panel is the box. */
|
||||
.app-section .form-group-auto,
|
||||
.app-section .app-optional-config,
|
||||
.app-section .deploy-storage-info,
|
||||
@@ -1450,8 +1477,9 @@ a.stat-card:hover {
|
||||
.app-page { padding: 0 0 24px; }
|
||||
.app-field-row { grid-template-columns: 1fr; gap: 4px; }
|
||||
.app-hero-actions { margin-left: 0; flex-basis: 100%; }
|
||||
.app-tabs { gap: 18px; }
|
||||
.app-tab { font-size: 14px; }
|
||||
.app-tab { font-size: 14px; padding: 8px 12px; }
|
||||
.app-hero, .app-section, .app-page .deploy-form .form-section, .app-page .deploy-progress { padding-left: 16px; padding-right: 16px; }
|
||||
.app-login-pair { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.app-info-hero {
|
||||
|
||||
Reference in New Issue
Block a user