Launcher polish: monogram reveal-on-failure + app placeholder on every icon surface (v0.163.1)
(A) The launcher monogram bled through every transparent white logo — it rendered unconditionally under the img. Now hidden by default (.launch-mono display:none), revealed only when the img chain fails (onerror adds .launch-tile--noimg). (B) The /static/app-placeholder.svg default reached only app_list_row. The four other sanctioned app-logo onerror chains now match the canonical SVG->PNG->placeholder grammar: backups_apps (aligned row), stacks (infra->infra-logo else placeholder), app_info hero (screenshots still vanish), deploy (keeps .LogoURL/.LogoPNGURL). Template/CSS only; no handler/funcmap change. 5 tests + 2 red-proofs.
This commit is contained in:
@@ -1,5 +1,25 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.163.1 — Launcher polish: monogram reveal-on-failure + placeholder on every icon surface (2026-07-24)
|
||||||
|
|
||||||
|
No agent coupling; MinAgent unchanged. Two live findings from the v0.163.0 operator browser pass on 9201.
|
||||||
|
|
||||||
|
- **Monogram bled through every tile.** The launcher rendered `.launch-mono` unconditionally UNDER
|
||||||
|
the logo `<img>`; app logos are white monochrome SVGs with transparent backgrounds, so the big
|
||||||
|
white letter showed through the glyph gaps on EVERY tile. The monogram is now hidden by default
|
||||||
|
(`.launch-mono { display: none }`) and revealed ONLY when the img chain fails — the final `onerror`
|
||||||
|
step adds `.launch-tile--noimg` to the tile, which flips the monogram back on. Applies to both the
|
||||||
|
operational `<a>` and the stopped `<div>` branch.
|
||||||
|
- **Placeholder reached only the canonical row.** The `/static/app-placeholder.svg` default landed in
|
||||||
|
`app_list_row` only; four more sanctioned app-logo `onerror` chains still dead-ended in
|
||||||
|
hidden/none for logo-less apps (observed: Docmost with no icon on Biztonsági mentés →
|
||||||
|
Alkalmazások). Every app-logo surface now follows one grammar — **SVG → PNG → placeholder** (infra
|
||||||
|
rows → infra icon): `backups_apps.html` (the allowlisted aligned row), `stacks.html` (the
|
||||||
|
`data-fallback` is now always present: infra → `infra-logo.svg`, else `app-placeholder.svg`),
|
||||||
|
`app_info.html` (hero logo only — **screenshots deliberately still vanish on error**),
|
||||||
|
`deploy.html` (keeps its `.LogoURL`/`.LogoPNGURL` data source). No handler/funcmap changes.
|
||||||
|
|
||||||
|
|
||||||
### v0.163.0 — Indítópult (app launcher page) + universal app placeholder icon (2026-07-24)
|
### v0.163.0 — Indítópult (app launcher page) + universal app placeholder icon (2026-07-24)
|
||||||
|
|
||||||
No agent coupling; MinAgent unchanged. Adds a customer-facing **Indítópult** launcher grid and a
|
No agent coupling; MinAgent unchanged. Adds a customer-facing **Indítópult** launcher grid and a
|
||||||
|
|||||||
+10
-1
@@ -7,7 +7,16 @@
|
|||||||
>
|
>
|
||||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||||
|
|
||||||
Last updated: 2026-07-24 (v0.163.0 — Indítópult launcher + universal app placeholder)
|
Last updated: 2026-07-24 (v0.163.1 — launcher polish: monogram reveal-on-failure + placeholder everywhere)
|
||||||
|
|
||||||
|
> **2026-07-24 — v0.163.1 (launcher polish).** Two v0.163.0 live findings fixed. RULE recorded:
|
||||||
|
> **every app-logo surface ends in a visible placeholder** (`SVG → PNG → /static/app-placeholder.svg`,
|
||||||
|
> infra rows → `infra-logo.svg`) — the four sibling `onerror` chains (`backups_apps`, `stacks`,
|
||||||
|
> `app_info` hero, `deploy`) now match `app_row.html`; `app_info` screenshots deliberately still
|
||||||
|
> vanish on error. And the **launcher monogram is launcher-only AND failure-only**: hidden by default,
|
||||||
|
> revealed when the tile's img chain fails (`onerror` adds `.launch-tile--noimg`) — it was bleeding
|
||||||
|
> through every transparent white glyph. Template/CSS only; no handler/funcmap change. 5 tests + 2
|
||||||
|
> red-proofs. [[launcher-v0163-2026-07-24]]
|
||||||
|
|
||||||
> **2026-07-24 — v0.163.0 (Indítópult app launcher + universal placeholder icon).** New
|
> **2026-07-24 — v0.163.0 (Indítópult app launcher + universal placeholder icon).** New
|
||||||
> customer-facing `/launcher` page: the FIRST sidebar item (above Vezérlőpult), a grid of large
|
> customer-facing `/launcher` page: the FIRST sidebar item (above Vezérlőpult), a grid of large
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
// v0.163.1 launcher polish. Two live findings: (A) the launcher monogram bled through every
|
||||||
|
// transparent white logo because it rendered unconditionally UNDER the img; it must be revealed
|
||||||
|
// ONLY when the img chain fails. (B) the /static/app-placeholder.svg default landed only in the
|
||||||
|
// canonical app_list_row — four sibling icon chains still dead-ended in hidden for logo-less apps.
|
||||||
|
|
||||||
|
// canonical3Step is the SVG→PNG→data-fallback→hidden onerror grammar every app-logo surface must use.
|
||||||
|
const canonical3Step = `else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}`
|
||||||
|
|
||||||
|
// Group A — the launcher monogram is failure-only.
|
||||||
|
// COMPANION red-proof (REPORT): drop `classList.add('launch-tile--noimg')` from the else branch →
|
||||||
|
// the onerror-content assertion FAILS.
|
||||||
|
func TestLauncher_MonogramRevealOnFailure(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Page": "launcher", "Title": "Indítópult", "Domain": "demo-felhom.eu",
|
||||||
|
"Apps": []LauncherApp{
|
||||||
|
{Name: "mealie", DisplayName: "Mealie", Slug: "mealie", State: stacks.StateRunning, Subdomain: "recept"},
|
||||||
|
{Name: "jellyfin", DisplayName: "Jellyfin", Slug: "jellyfin", State: stacks.StateStopped, Subdomain: "media"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
html := renderBackupPage(t, "launcher", data)
|
||||||
|
|
||||||
|
// The reveal class must never sit in a class attribute in the initial markup — it is added by
|
||||||
|
// onerror at runtime only. Its ONLY occurrences are the two onerror `classList.add(...)` calls;
|
||||||
|
// a count above 2 would mean it leaked into a class="" attribute.
|
||||||
|
if got := strings.Count(html, "launch-tile--noimg"); got != 2 {
|
||||||
|
t.Errorf("launch-tile--noimg must appear only in the two onerror hooks (2), got %d — it must not be a static class", got)
|
||||||
|
}
|
||||||
|
// Both branches (operational <a> + stopped <div>) keep the monogram span AND the reveal hook.
|
||||||
|
if got := strings.Count(html, "launch-mono"); got != 2 {
|
||||||
|
t.Errorf("expected the monogram span in both tile branches (2), got %d", got)
|
||||||
|
}
|
||||||
|
if got := strings.Count(html, "classList.add('launch-tile--noimg')"); got != 2 {
|
||||||
|
t.Errorf("both tile onerror chains must reveal the monogram on final failure, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group B — every app-logo surface ends in the placeholder (or infra glyph), never hidden.
|
||||||
|
|
||||||
|
// TestBackupsApps_IconPlaceholder — the allowlisted aligned row now matches the canonical chain.
|
||||||
|
// COMPANION red-proof (REPORT): revert backups_apps.html to the 2-step chain → this FAILS.
|
||||||
|
func TestBackupsApps_IconPlaceholder(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Page": "backups-apps", "Title": "Biztonsági mentés — Alkalmazások",
|
||||||
|
"Backup": &backup.FullBackupStatus{AppDataInfo: []backup.AppBackupInfo{{StackName: "docmost", DisplayName: "Docmost"}}},
|
||||||
|
"AppBackupRows": []AppBackupRow{{StackName: "docmost", DisplayName: "Docmost", Slug: "docmost", Status: "green", Tier3State: "off"}},
|
||||||
|
"DBSectionState": "dumps",
|
||||||
|
}
|
||||||
|
html := renderBackupPage(t, "backups_apps", data)
|
||||||
|
if !strings.Contains(html, `data-fallback="/static/app-placeholder.svg"`) {
|
||||||
|
t.Error("backups_apps icon must default to the app placeholder")
|
||||||
|
}
|
||||||
|
if !strings.Contains(html, canonical3Step) {
|
||||||
|
t.Error("backups_apps icon must use the canonical 3-step onerror chain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStacks_IconPlaceholderAndInfra — regular apps default to the placeholder; infra keeps the
|
||||||
|
// infra glyph.
|
||||||
|
func TestStacks_IconPlaceholderAndInfra(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Page": "stacks", "Title": "Alkalmazások",
|
||||||
|
"Stacks": []stacks.Stack{
|
||||||
|
{Name: "docmost", Deployed: true, State: stacks.StateRunning,
|
||||||
|
Meta: stacks.Metadata{Slug: "docmost", DisplayName: "Docmost"}},
|
||||||
|
{Name: "filebrowser", Protected: true, State: stacks.StateRunning,
|
||||||
|
Meta: stacks.Metadata{Slug: "filebrowser", DisplayName: "Filebrowser"}},
|
||||||
|
},
|
||||||
|
"MissingStorage": map[string]string{},
|
||||||
|
"NetworkWarnings": map[string]string{},
|
||||||
|
"NetworkStubs": map[string]string{},
|
||||||
|
"StorageLabels": map[string]string{},
|
||||||
|
"Subdomains": map[string]string{"filebrowser": "files"},
|
||||||
|
"RunningCount": 2, "StoppedCount": 0, "TotalCount": 2,
|
||||||
|
"SystemInfo": system.SystemInfo{},
|
||||||
|
"BackupEnabled": false,
|
||||||
|
"Domain": "demo-felhom.eu",
|
||||||
|
}
|
||||||
|
html := renderBackupPage(t, "stacks", data)
|
||||||
|
if !strings.Contains(html, `data-fallback="/static/app-placeholder.svg"`) {
|
||||||
|
t.Error("a regular app card must default to the app placeholder")
|
||||||
|
}
|
||||||
|
if !strings.Contains(html, `data-fallback="/static/infra-logo.svg"`) {
|
||||||
|
t.Error("an infra app card must keep the infra glyph fallback")
|
||||||
|
}
|
||||||
|
if !strings.Contains(html, canonical3Step) {
|
||||||
|
t.Error("stacks card icon must use the canonical 3-step onerror chain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAppInfo_HeroPlaceholder_ScreenshotsUntouched — hero logo aligned; screenshots still vanish.
|
||||||
|
func TestAppInfo_HeroPlaceholder_ScreenshotsUntouched(t *testing.T) {
|
||||||
|
st := stacks.Stack{Name: "docmost", Deployed: true, State: stacks.StateRunning,
|
||||||
|
Meta: stacks.Metadata{Slug: "docmost", DisplayName: "Docmost"}}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Page": "stacks", "Title": st.Meta.DisplayName,
|
||||||
|
"Stack": &st, "Meta": st.Meta, "AppInfo": st.Meta.AppInfo,
|
||||||
|
"HasAppInfo": st.Meta.HasAppInfo(), "EffectiveSubdomain": st.Meta.Subdomain,
|
||||||
|
"Domain": "demo-felhom.eu",
|
||||||
|
}
|
||||||
|
html := renderBackupPage(t, "app_info", data)
|
||||||
|
if !strings.Contains(html, `data-fallback="/static/app-placeholder.svg"`) {
|
||||||
|
t.Error("app_info hero logo must default to the app placeholder")
|
||||||
|
}
|
||||||
|
if !strings.Contains(html, canonical3Step) {
|
||||||
|
t.Error("app_info hero logo must use the canonical 3-step chain")
|
||||||
|
}
|
||||||
|
// Screenshots must be byte-identical to before — they still vanish on error, no placeholder.
|
||||||
|
if !strings.Contains(html, `class="app-screenshot"`) || !strings.Contains(html, `onerror="this.style.display='none'"`) {
|
||||||
|
t.Error("app_info screenshot imgs must keep their original vanish-on-error onerror")
|
||||||
|
}
|
||||||
|
if strings.Contains(html, `class="app-screenshot" `) && strings.Contains(html, `app-screenshot" data-fallback`) {
|
||||||
|
t.Error("app_info screenshots must NOT gain a placeholder fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeploy_LogoPlaceholder — deploy logo aligned, keeping its .LogoURL/.LogoPNGURL data source.
|
||||||
|
func TestDeploy_LogoPlaceholder(t *testing.T) {
|
||||||
|
st := stacks.Stack{Name: "docmost", State: stacks.StateNotDeployed,
|
||||||
|
Meta: stacks.Metadata{Slug: "docmost", DisplayName: "Docmost"}}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Page": "deploy", "Title": "Docmost — Telepítés",
|
||||||
|
"Stack": &st, "Meta": st.Meta, "AlreadyDeployed": false,
|
||||||
|
"LogoURL": "/static/assets/docmost-logo.svg", "LogoPNGURL": "/static/assets/docmost-logo.png",
|
||||||
|
"Domain": "demo-felhom.eu",
|
||||||
|
}
|
||||||
|
html := renderBackupPage(t, "deploy", data)
|
||||||
|
if !strings.Contains(html, `data-fallback="/static/app-placeholder.svg"`) {
|
||||||
|
t.Error("deploy logo must default to the app placeholder")
|
||||||
|
}
|
||||||
|
if !strings.Contains(html, canonical3Step) {
|
||||||
|
t.Error("deploy logo must use the canonical 3-step chain")
|
||||||
|
}
|
||||||
|
// Data source preserved: the deploy page keeps .LogoPNGURL (not a funcmap call). The URL is
|
||||||
|
// JS-escaped inside the onerror string (\/static\/…), so assert the slash-free filename.
|
||||||
|
if !strings.Contains(html, "docmost-logo.png") {
|
||||||
|
t.Error("deploy logo must keep its .LogoPNGURL data source in the chain")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
<!-- Hero section -->
|
<!-- Hero section -->
|
||||||
<div class="app-info-hero">
|
<div class="app-info-hero">
|
||||||
<img class="app-info-logo" src="{{logoURL .Meta.Slug}}"
|
<img class="app-info-logo" src="{{logoURL .Meta.Slug}}"
|
||||||
alt="{{.Meta.DisplayName}}"
|
alt="{{.Meta.DisplayName}}" data-fallback="/static/app-placeholder.svg"
|
||||||
onerror="this.onerror=function(){this.style.display='none'};this.src='{{logoPNGURL .Meta.Slug}}'">
|
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Meta.Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||||||
<div class="app-info-hero-text">
|
<div class="app-info-hero-text">
|
||||||
{{if .AppInfo.Tagline}}
|
{{if .AppInfo.Tagline}}
|
||||||
<p class="app-info-tagline">{{.AppInfo.Tagline}}</p>
|
<p class="app-info-tagline">{{.AppInfo.Tagline}}</p>
|
||||||
|
|||||||
@@ -139,10 +139,12 @@
|
|||||||
<div class="app-backup-row" data-status="{{.Status}}">
|
<div class="app-backup-row" data-status="{{.Status}}">
|
||||||
<!-- Aligned to the shared app-row grammar (icon + name left, status + chevron right);
|
<!-- Aligned to the shared app-row grammar (icon + name left, status + chevron right);
|
||||||
the expander behavior is untouched — this header is the one allowlisted aligned
|
the expander behavior is untouched — this header is the one allowlisted aligned
|
||||||
copy in scripts/app_row_dedup_gate.py, NOT a partial render (it owns the toggle). -->
|
copy in scripts/app_row_dedup_gate.py, NOT a partial render (it owns the toggle).
|
||||||
|
The icon chain matches app_row.html: SVG -> PNG -> /static/app-placeholder.svg, so a
|
||||||
|
logo-less app shows the placeholder here too instead of a hidden dead-end (v0.163.1). -->
|
||||||
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
|
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
|
||||||
<img class="app-row-icon" src="{{logoURL .Slug}}" alt=""
|
<img class="app-row-icon" src="{{logoURL .Slug}}" alt="" data-fallback="/static/app-placeholder.svg"
|
||||||
onerror="this.onerror=function(){this.style.visibility='hidden'};this.src='{{logoPNGURL .Slug}}'">
|
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||||||
<span class="app-backup-row-name">{{.DisplayName}}</span>
|
<span class="app-backup-row-name">{{.DisplayName}}</span>
|
||||||
<div class="app-backup-row-meta">
|
<div class="app-backup-row-meta">
|
||||||
{{if .DriveDisconnected}}
|
{{if .DriveDisconnected}}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
|
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
|
||||||
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
|
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
|
||||||
<div class="deploy-info">
|
<div class="deploy-info">
|
||||||
<img class="deploy-logo" src="{{.LogoURL}}" alt="" onerror="this.onerror=function(){this.style.display='none'};this.src='{{.LogoPNGURL}}'">
|
<img class="deploy-logo" src="{{.LogoURL}}" alt="" data-fallback="/static/app-placeholder.svg"
|
||||||
|
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{.LogoPNGURL}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||||||
<div>
|
<div>
|
||||||
<h3>{{.Meta.DisplayName}}</h3>
|
<h3>{{.Meta.DisplayName}}</h3>
|
||||||
{{if .Meta.Description}}<p>{{.Meta.Description}}</p>{{end}}
|
{{if .Meta.Description}}<p>{{.Meta.Description}}</p>{{end}}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<span class="launch-tile" style="background: {{tileColor .Slug .BrandColor}}">
|
<span class="launch-tile" style="background: {{tileColor .Slug .BrandColor}}">
|
||||||
<span class="launch-mono">{{initial .DisplayName}}</span>
|
<span class="launch-mono">{{initial .DisplayName}}</span>
|
||||||
<img class="launch-logo" src="{{logoURL .Slug}}" alt=""
|
<img class="launch-logo" src="{{logoURL .Slug}}" alt=""
|
||||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else{this.onerror=null;this.style.display='none';}">
|
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||||
</span>
|
</span>
|
||||||
<span class="launch-name">{{.DisplayName}}</span>
|
<span class="launch-name">{{.DisplayName}}</span>
|
||||||
{{if ne (stateStr .State) "running"}}<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>{{end}}
|
{{if ne (stateStr .State) "running"}}<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>{{end}}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<span class="launch-tile launch-tile--off" style="background: {{tileColor .Slug .BrandColor}}">
|
<span class="launch-tile launch-tile--off" style="background: {{tileColor .Slug .BrandColor}}">
|
||||||
<span class="launch-mono">{{initial .DisplayName}}</span>
|
<span class="launch-mono">{{initial .DisplayName}}</span>
|
||||||
<img class="launch-logo" src="{{logoURL .Slug}}" alt=""
|
<img class="launch-logo" src="{{logoURL .Slug}}" alt=""
|
||||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else{this.onerror=null;this.style.display='none';}">
|
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||||
</span>
|
</span>
|
||||||
<span class="launch-name">{{.DisplayName}}</span>
|
<span class="launch-name">{{.DisplayName}}</span>
|
||||||
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
|
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="stack-detail-card stack-state-{{stateColor .State}}" data-filter-state="{{filterCategory .State .Deployed}}"{{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
<div class="stack-detail-card stack-state-{{stateColor .State}}" data-filter-state="{{filterCategory .State .Deployed}}"{{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
||||||
<div class="stack-detail-header">
|
<div class="stack-detail-header">
|
||||||
<div class="stack-title-row">
|
<div class="stack-title-row">
|
||||||
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}" alt=""{{if $im}} data-fallback="/static/infra-logo.svg"{{end}}
|
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}" alt="" data-fallback="{{if $im}}/static/infra-logo.svg{{else}}/static/app-placeholder.svg{{end}}"
|
||||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Meta.Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Meta.Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||||||
<div>
|
<div>
|
||||||
<h3>{{if $im}}{{$im.DisplayName}}{{else}}{{.Meta.DisplayName}}{{end}}</h3>
|
<h3>{{if $im}}{{$im.DisplayName}}{{else}}{{.Meta.DisplayName}}{{end}}</h3>
|
||||||
|
|||||||
@@ -1146,8 +1146,15 @@ a.stat-card:hover {
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
border-color: var(--blue);
|
border-color: var(--blue);
|
||||||
}
|
}
|
||||||
|
/* The monogram is the logo-less fallback ONLY: hidden by default, revealed when the tile's <img>
|
||||||
|
chain fails (onerror adds .launch-tile--noimg). Otherwise a transparent white glyph would show
|
||||||
|
the letter bleeding through its gaps on every tile (v0.163.1 live finding). */
|
||||||
.launch-mono {
|
.launch-mono {
|
||||||
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
@@ -1155,6 +1162,7 @@ a.stat-card:hover {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.launch-tile--noimg .launch-mono { display: flex; }
|
||||||
.launch-logo {
|
.launch-logo {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 58%;
|
width: 58%;
|
||||||
|
|||||||
Reference in New Issue
Block a user