feat(ui): app icons on the house canvas; detail page gets the tile treatment
Demo images / Build & push demo images (push) Successful in 3m33s
Demo images / Build & push demo images (push) Successful in 3m33s
- alby-hub + phoenixd icons re-set with the standard 12% inner margin (they shipped edge-to-edge; every other icon carries whitespace). - scripts/normalize-app-icon.py: wraps any third-party SVG mark onto the house canvas — the system applies the tile plate (archy-app-icon) automatically but deliberately no runtime inset, so the margin must be baked; the guide now says exactly that. - MarketplaceAppDetails: the icon now carries archy-app-icon like the store tiles — the treatment no longer stops at the detail page. - v1.8.2 changelog: third curated bullet (the ceremony gate requires 3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4b4e1ab1a3
commit
7c34df36cd
+2
-1
@@ -3,7 +3,8 @@
|
||||
## v1.8.2-alpha (2026-08-13)
|
||||
|
||||
- **An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.
|
||||
- **The screensaver and login emblem are back to their clean black finish.** A glossy paint experiment on the circle behind the logo shipped by mistake in the previous update; it's removed everywhere.
|
||||
- **The logo emblem got its glossy black paint finish — properly this time.** The circle behind the A on the screensaver, intro, and login now wears a deep wet-paint look: warm light blooming from the top edge, fine grain so the dark tones stay smooth instead of banding, and no more ring border. (An earlier rougher version of this experiment briefly shipped by accident and then vanished depending on which screen you were on — this is the finished, deliberate one, everywhere.)
|
||||
- **New app icons now match the store's look, on every screen.** Alby Hub and phoenixd arrived with edge-to-edge logos that ignored the breathing room every other app icon has, and the app detail page skipped the icon backdrop entirely. Both icons are re-set on the standard canvas, the detail page now applies the same icon treatment as the store tiles, and app developers get a one-command tool that puts any logo onto the house canvas automatically.
|
||||
|
||||
## v1.8.1-alpha (2026-08-13)
|
||||
|
||||
|
||||
@@ -531,6 +531,20 @@ The broader app update policy for `1.8-alpha` is still being finalized. Until th
|
||||
|
||||
## App Icon
|
||||
|
||||
- **The tile plate (dark rounded background, border, sheen) is applied by
|
||||
the system** — every surface renders your icon with the house
|
||||
`archy-app-icon` treatment automatically. What the system deliberately
|
||||
does NOT add is an inner margin (icons that already carry whitespace
|
||||
would double-margin), so your file needs it baked in: **square canvas,
|
||||
~12% margin per side**. Edge-to-edge marks look wrong next to every
|
||||
other tile. Don't hand-tune it: run your mark through the normalizer
|
||||
and commit its output —
|
||||
```bash
|
||||
scripts/normalize-app-icon.py your-mark.svg apps-icon-output.svg
|
||||
```
|
||||
It wraps any SVG (full-bleed upstream logos included) onto the house
|
||||
canvas with the standard margin, preserving the original artwork
|
||||
untouched inside.
|
||||
- Provide a URL to your app icon (PNG, WebP, or SVG)
|
||||
- Recommended size: 256x256 pixels
|
||||
- Square aspect ratio
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<!-- normalized by scripts/normalize-app-icon.py: margin=0.12 per side -->
|
||||
<svg x="12.000" y="12.000" width="76.000" height="76.000" viewBox="0 0 8000 8000" preserveAspectRatio="xMidYMid meet">
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8000 8000">
|
||||
<path d="M1404.94034,2404.75143c.19948.39893.416.79745.63896,1.19934-.21965-.3998-.4293-.7996-.63896-1.19934ZM1398.36987,2392.55825c.01.02001.01999.03996.01999.04996.01999.02001.03998.04996.04998.06997-.01999-.03996-.04998-.07997-.06997-.11993ZM7957.22848,6407.80899c-75.3022-453.82005-341.31246-1051.68618-631.76608-1536.83905-217.58172,280.41299-443.70125,543.12568-695.66263,796.78364,151.96083,272.22676,381.52012,831.33939,371.78586,1131.07073-3.34955,103.13742-84.59871,186.38404-187.48323,193.03587-323.69068,20.92769-761.15088-169.80941-1126.96417-352.01125-274.08918,250.36976-539.73074,478.97192-815.02811,690.20321,570.85548,338.56749,1305.54247,669.74202,1904.46573,668.08293,872.77622,46.26671,1378.68446-776.20339,1180.65263-1590.32607ZM5249.15884,5683.56479c1101.91285-1101.01171,2353.03599-2527.31198,2708.11113-4090.5697,253.93721-1273.80049-760.34667-1869.98142-1895.16006-1469.66567,211.65045,278.23428,364.22738,589.33979,457.73054,914.98744,86.80988-20.20216,196.93112-34.80914,289.99476-31.50156,103.33929,3.67279,186.93541,86.38156,191.60732,189.77353,13.96581,309.07109-235.73659,902.8025-402.1382,1193.8406,0,.01-.01.02001-.01.03001l-.04998.07991c-624.44753,1143.09071-1591.89389,2208.42396-2600.29743,3077.06633l-.22964-.19984c-263.64443,226.46298-526.16056,433.98687-804.2439,626.56893.39938.34966.79882.69956,1.19826,1.04946-509.31622,353.04862-1419.46673,925.33606-2012.13405,893.44541-103.82534-5.58671-186.23795-90.1864-188.67838-194.22674-6.97909-297.53161,215.6413-843.71786,363.83869-1111.11806-237.05325-259.94423-464.71032-529.69327-686.31643-814.38364-1228.33988,1900.32534-772.83666,3911.20697,1772.96127,2814.86279,1004.95316-454.63593,1968.00538-1208.05697,2803.81609-2000.0392ZM2528.2177,4000.38756c-468.17105-551.20508-806.89093-1020.88409-1125.16461-1599.13417h-.01c-.00197-.02709-.02111-.04517-.02999-.07997h-.01c-.00225-.03154-.04191-.05163-.03986-.08992,0,0-.01,0-.01-.01-1.46642-2.7001-3.08021-5.72568-4.5634-8.46529.01999.02001.03998.04996.04998.06997-165.01013-287.23702-415.64692-882.63022-403.53149-1193.46936,4.07951-104.66591,88.1281-188.31126,192.71288-192.04153,92.1143-3.28547,200.69379,10.46969,286.62767,30.61985,93.58312-325.69743,246.26999-636.85321,458.06014-915.09749C797.66386-275.84563-214.1867,319.25478,39.32792,1592.99819c208.75964,1126.24494,1140.55524,2354.14531,1865.30175,3206.20191-1.24817,1.76903,1.23818-1.76903,0,0,177.91975,202.67638,363.17881,406.52227,558.17368,604.08153,267.29912-191.50259,530.82373-396.10806,795.10723-617.7438-259.10108-257.46588-503.73334-520.6384-729.69288-785.15026ZM1420.59738,2433.86513c.84886,1.56913,1.71746,3.17821,2.5963,4.82725-.85885-1.58908-1.72745-3.19816-2.5963-4.82725ZM1457.40366,2502.26682c.92857,1.699,1.83716,3.39806,2.73599,5.07711-.89883-1.64904-1.80742-3.3481-2.73599-5.07711ZM1403.01311,2401.17342c-.01-.01-.01987-.02995-.02986-.03996.01.01.01987.02995.01987.03996.01999.03001.02999.04996.03998.06997-.01-.02001-.01999-.03996-.02999-.06997ZM1404.94034,2404.75143c.19948.39893.416.79745.63896,1.19934-.21965-.3998-.4293-.7996-.63896-1.19934Z" fill="#ffe480"/>
|
||||
<path d="M2762.56314,2987.43935c-683.41752-683.42045-683.41752-1791.46134,0-2474.87781,683.41752-683.41755,1791.45841-683.41755,2474.87885,0,683.41068,683.41648,683.41068,1791.45737,0,2474.87781l-1237.44089,1237.44089-1237.43796-1237.44089Z" fill="#fff"/>
|
||||
</svg>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -1,7 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<!-- normalized by scripts/normalize-app-icon.py: margin=0.12 per side -->
|
||||
<svg x="12.000" y="12.000" width="76.000" height="76.000" viewBox="0 0 94 94" preserveAspectRatio="xMidYMid meet">
|
||||
<svg viewBox="0 0 94 94" id="vector" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="group" transform="matrix(1, 0, 0, 1, -6.999915, -7)">
|
||||
<path id="path" d="M 30 81 C 23 80 21.63 71.49 29.16 45.87 C 35 26 46.18 26 58 26 C 73.17 26 81.61 29.18 83.79 35.73 C 84.243 36.883 84.39 38.135 84.214 39.362 C 84.039 40.589 83.548 41.75 82.79 42.73 C 82.613 42.96 82.419 43.178 82.21 43.38 C 85.1 45.31 86.88 48.02 86.52 51.9 C 86.13 56.26 83.16 58.55 78.52 59.66 C 78.849 60.396 79.013 61.194 79 62 C 78.988 63.199 78.695 64.378 78.147 65.444 C 77.598 66.51 76.808 67.433 75.84 68.14 C 70.09 72.76 60 75 46 73 C 43 72.57 41 75 39 77 C 36.62 79.38 34.24 81.61 30 81 Z" fill="#25A5FF"/>
|
||||
<path id="path_1" d="M 58 30 C 66 30 78 31 80 37 C 82.24 43.71 69 43 59 42 C 68 43 83.18 44 82.5 51.5 C 82 57 72 57 55 56 C 63 57 75 58 75 62 C 75 68.28 58.74 71 47.67 69.31 C 39 68 38.06 76.77 31 75 C 27 74 28 63 33 47 C 38.29 30.09 46.49 30 58 30 Z" fill="#ffffff" stroke="#25A5FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -23,7 +23,7 @@
|
||||
v-if="app.icon"
|
||||
:src="app.icon"
|
||||
:alt="app.title"
|
||||
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0"
|
||||
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0 archy-app-icon"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center flex-shrink-0">
|
||||
@@ -99,7 +99,7 @@
|
||||
v-if="app.icon"
|
||||
:src="app.icon"
|
||||
:alt="app.title"
|
||||
class="w-20 h-20 rounded-xl shadow-xl"
|
||||
class="w-20 h-20 rounded-xl shadow-xl archy-app-icon"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center">
|
||||
|
||||
@@ -370,7 +370,8 @@ init()
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>**An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.</p>
|
||||
<p>**The screensaver and login emblem are back to their clean black finish.** A glossy paint experiment on the circle behind the logo shipped by mistake in the previous update; it's removed everywhere.</p>
|
||||
<p>**The logo emblem got its glossy black paint finish — properly this time.** The circle behind the A on the screensaver, intro, and login now wears a deep wet-paint look: warm light blooming from the top edge, fine grain so the dark tones stay smooth instead of banding, and no more ring border. (An earlier rougher version of this experiment briefly shipped by accident and then vanished depending on which screen you were on — this is the finished, deliberate one, everywhere.)</p>
|
||||
<p>**New app icons now match the store's look, on every screen.** Alby Hub and phoenixd arrived with edge-to-edge logos that ignored the breathing room every other app icon has, and the app detail page skipped the icon backdrop entirely. Both icons are re-set on the standard canvas, the detail page now applies the same icon treatment as the store tiles, and app developers get a one-command tool that puts any logo onto the house canvas automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.8.1-alpha -->
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Normalize a third-party SVG mark onto the Archipelago app-icon canvas.
|
||||
|
||||
Store tiles render icons as-is — there is deliberately no runtime inset
|
||||
(it would double-margin every icon that already ships whitespace), so the
|
||||
margin must be baked into the file. House icons carry it naturally;
|
||||
third-party marks are usually full-bleed (Alby Hub's tile icon was the
|
||||
first to land edge-to-edge in the store, 2026-08-14). This wraps any SVG
|
||||
in a square canvas with a uniform inner margin, preserving the original
|
||||
untouched as a nested <svg>.
|
||||
|
||||
Usage:
|
||||
scripts/normalize-app-icon.py in.svg out.svg [--margin 0.12]
|
||||
|
||||
Margin is a fraction of the canvas per side (default 12%, the house look).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("src")
|
||||
ap.add_argument("dst")
|
||||
ap.add_argument("--margin", type=float, default=0.12,
|
||||
help="inner margin per side, fraction of canvas (default 0.12)")
|
||||
args = ap.parse_args()
|
||||
|
||||
svg = open(args.src, encoding="utf-8").read()
|
||||
|
||||
m = re.search(r"<svg\b[^>]*>", svg, re.S)
|
||||
if not m:
|
||||
print("error: no <svg> root element found", file=sys.stderr)
|
||||
return 1
|
||||
root = m.group(0)
|
||||
|
||||
vb = re.search(r'viewBox\s*=\s*["\']([\d.eE+\s,-]+)["\']', root)
|
||||
if vb:
|
||||
nums = [float(x) for x in re.split(r"[\s,]+", vb.group(1).strip())]
|
||||
if len(nums) != 4:
|
||||
print("error: unparseable viewBox", file=sys.stderr)
|
||||
return 1
|
||||
_, _, w, h = nums
|
||||
inner_viewbox = vb.group(1).strip()
|
||||
else:
|
||||
wm = re.search(r'width\s*=\s*["\']([\d.]+)', root)
|
||||
hm = re.search(r'height\s*=\s*["\']([\d.]+)', root)
|
||||
if not (wm and hm):
|
||||
print("error: no viewBox and no width/height to derive one", file=sys.stderr)
|
||||
return 1
|
||||
w, h = float(wm.group(1)), float(hm.group(1))
|
||||
inner_viewbox = f"0 0 {w} {h}"
|
||||
|
||||
# Square canvas fitting the larger dimension; the mark centers in it.
|
||||
side = max(w, h)
|
||||
canvas = 100.0
|
||||
margin = canvas * args.margin
|
||||
avail = canvas - 2 * margin
|
||||
scale_w = avail * (w / side)
|
||||
scale_h = avail * (h / side)
|
||||
x = (canvas - scale_w) / 2
|
||||
y = (canvas - scale_h) / 2
|
||||
|
||||
# Strip any XML prolog from the original; it nests inside the wrapper.
|
||||
inner = re.sub(r"^\s*<\?xml[^>]*\?>\s*", "", svg)
|
||||
|
||||
out = (
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {canvas:g} {canvas:g}">\n'
|
||||
f' <!-- normalized by scripts/normalize-app-icon.py: margin={args.margin:g} per side -->\n'
|
||||
f' <svg x="{x:.3f}" y="{y:.3f}" width="{scale_w:.3f}" height="{scale_h:.3f}" '
|
||||
f'viewBox="{inner_viewbox}" preserveAspectRatio="xMidYMid meet">\n'
|
||||
f'{inner}\n'
|
||||
f' </svg>\n'
|
||||
f'</svg>\n'
|
||||
)
|
||||
open(args.dst, "w", encoding="utf-8").write(out)
|
||||
print(f"wrote {args.dst}: {canvas:g}x{canvas:g} canvas, mark {scale_w:.1f}x{scale_h:.1f} at {args.margin:.0%} margin")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user