init logo
@@ -1,6 +1,10 @@
|
|||||||
# Arboretum
|
<p align="center">
|
||||||
|
<img src="brand/arboretum-logo-on-dark.png" alt="Arboretum" width="300">
|
||||||
|
</p>
|
||||||
|
|
||||||
> A self-hosted web dashboard for your git worktrees and the Claude Code sessions running on them — from any device.
|
<p align="center">
|
||||||
|
A self-hosted web dashboard for your git worktrees and the Claude Code sessions running on them — from any device.
|
||||||
|
</p>
|
||||||
|
|
||||||
**Status: MVP.** The worktree-first dashboard, session discovery & resume, multi-repo worktree lifecycle, live session states, the web terminal, and mobile supervision (installable PWA, Web Push when a session needs you, approve/deny without opening a terminal) are implemented and tested.
|
**Status: MVP.** The worktree-first dashboard, session discovery & resume, multi-repo worktree lifecycle, live session states, the web terminal, and mobile supervision (installable PWA, Web Push when a session needs you, approve/deny without opening a terminal) are implemented and tested.
|
||||||
|
|
||||||
|
|||||||
42
brand/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Brand assets
|
||||||
|
|
||||||
|
Logo and icon assets for Arboretum. Source artwork is a neon circuit-tree (green
|
||||||
|
branches, cyan session nodes, a `>_` prompt at the base) on a dark background.
|
||||||
|
|
||||||
|
| File | Use |
|
||||||
|
| --- | --- |
|
||||||
|
| `arboretum-logo-source.png` | Master artwork (opaque dark background). Keep; everything else derives from it. |
|
||||||
|
| `arboretum-logo.png` | Full logo, **transparent**. Best on dark surfaces (the wordmark is light). |
|
||||||
|
| `arboretum-logo-on-dark.png` | Full logo on the app background `#09090b`. Safe on any theme — used in the README. |
|
||||||
|
| `arboretum-mark.png` | Square, **transparent**, tree only (no wordmark). Ideal **Gitea repo avatar** — reads on both light and dark. |
|
||||||
|
|
||||||
|
The transparent versions are extracted by luminance (alpha ∝ brightness), the clean
|
||||||
|
way to lift glow-on-black artwork: the dark background becomes fully transparent, the
|
||||||
|
bright strokes stay opaque, and the glow halos stay semi-transparent so the logo sits
|
||||||
|
correctly on any dark surface.
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
|
||||||
|
Upload `arboretum-mark.png` as the repository avatar (Settings → uploads a square image;
|
||||||
|
the tree-only mark stays legible at small sizes and works on Gitea's light and dark themes).
|
||||||
|
Use `arboretum-logo.png` (transparent) on dark pages, or `arboretum-logo-on-dark.png`
|
||||||
|
when the surrounding background might be light.
|
||||||
|
|
||||||
|
## App / favicon assets
|
||||||
|
|
||||||
|
The web-facing assets live in `packages/web/public/` and are wired into the SPA:
|
||||||
|
|
||||||
|
- `icon.svg` — scalable favicon, redrawn to match the brand (vector, glow, `>_`).
|
||||||
|
- `icon-192.png` / `icon-512.png` — maskable PWA icons (tree on `#09090b`, content in the safe zone).
|
||||||
|
- `apple-touch-icon.png` — iOS home-screen icon (180×180).
|
||||||
|
- `favicon.ico` — multi-size favicon (16/32/48), transparent.
|
||||||
|
- `logo.png` — transparent full logo for in-app use.
|
||||||
|
|
||||||
|
## Regenerate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 brand/build-assets.py # uses brand/arboretum-logo-source.png
|
||||||
|
python3 brand/build-assets.py other.png # or pass another source
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python with Pillow + numpy. Writes both `brand/` and `packages/web/public/`.
|
||||||
BIN
brand/arboretum-logo-on-dark.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
brand/arboretum-logo-source.png
Normal file
|
After Width: | Height: | Size: 959 KiB |
BIN
brand/arboretum-logo.png
Normal file
|
After Width: | Height: | Size: 561 KiB |
BIN
brand/arboretum-mark.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
100
brand/build-assets.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Génère le jeu complet d'assets de marque Arboretum depuis le logo source.
|
||||||
|
|
||||||
|
Source : un PNG « néon sur fond sombre » (arbre-circuit + texte « Arboretum »).
|
||||||
|
On extrait l'alpha par luminance (méthode propre pour ce type d'artwork glow-on-black) :
|
||||||
|
chaque pixel reçoit une transparence proportionnelle à sa luminosité, ce qui rend le
|
||||||
|
fond sombre totalement transparent, garde le cœur des traits opaque et conserve le halo
|
||||||
|
en semi-transparent — donc lisible sur n'importe quel fond.
|
||||||
|
|
||||||
|
Sorties :
|
||||||
|
brand/arboretum-logo.png logo complet transparent (haute déf) — Gitea / README
|
||||||
|
brand/arboretum-mark.png marque carrée transparente (arbre seul) — avatar Gitea
|
||||||
|
brand/arboretum-logo-on-dark.png logo complet sur fond #09090b (fallback fond clair)
|
||||||
|
packages/web/public/logo.png logo complet transparent, optimisé pour l'UI
|
||||||
|
packages/web/public/icon-192.png icône PWA maskable (arbre, fond #09090b)
|
||||||
|
packages/web/public/icon-512.png icône PWA maskable (arbre, fond #09090b)
|
||||||
|
packages/web/public/apple-touch-icon.png icône iOS 180 (arbre, fond #09090b)
|
||||||
|
packages/web/public/favicon.ico favicon transparent (arbre, 16/32/48)
|
||||||
|
|
||||||
|
Usage : python3 brand/build-assets.py <source.png>
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
SRC = sys.argv[1] if len(sys.argv) > 1 else "brand/arboretum-logo-source.png"
|
||||||
|
BG = (9, 9, 11) # #09090b — couleur de fond du dashboard (manifest background_color/theme_color)
|
||||||
|
|
||||||
|
# Découpe verticale arbre / texte (mesurée sur la source)
|
||||||
|
TREE_Y = (130, 930) # arbre + curseur >_
|
||||||
|
FULL_Y = (130, 1060) # arbre + texte
|
||||||
|
|
||||||
|
|
||||||
|
def extract_rgba(path):
|
||||||
|
"""Charge la source et calcule l'alpha par luminance (floor + knee)."""
|
||||||
|
rgb = np.asarray(Image.open(path).convert("RGB"), dtype=np.float32)
|
||||||
|
maxc = rgb.max(axis=2)
|
||||||
|
# floor > canal max du fond (~14) pour annuler totalement le fond ; knee = seuil d'opacité.
|
||||||
|
floor, knee = 20.0, 185.0 # < floor : transparent ; >= knee : opaque
|
||||||
|
alpha = np.clip((maxc - floor) / (knee - floor), 0.0, 1.0) * 255.0
|
||||||
|
alpha[alpha < 8] = 0.0 # gate anti-bruit : un vrai fond transparent et une bbox serrée
|
||||||
|
out = np.dstack([rgb, alpha]).astype(np.uint8)
|
||||||
|
return Image.fromarray(out, "RGBA")
|
||||||
|
|
||||||
|
|
||||||
|
def crop_band(rgba, y0, y1):
|
||||||
|
"""Recadre une bande verticale puis serre sur le contenu non transparent."""
|
||||||
|
band = rgba.crop((0, y0, rgba.width, y1))
|
||||||
|
bbox = band.getbbox() # bbox sur l'alpha
|
||||||
|
return band.crop(bbox)
|
||||||
|
|
||||||
|
|
||||||
|
def square(content, pad_frac, bg=None):
|
||||||
|
"""Centre `content` dans un carré ; bg=None → transparent, sinon fond plein."""
|
||||||
|
side = round(max(content.size) / (1 - 2 * pad_frac))
|
||||||
|
fill = (0, 0, 0, 0) if bg is None else (*bg, 255)
|
||||||
|
canvas = Image.new("RGBA", (side, side), fill)
|
||||||
|
canvas.paste(content, ((side - content.width) // 2, (side - content.height) // 2), content)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
|
def save(img, path, size=None):
|
||||||
|
if size:
|
||||||
|
img = img.resize((size, size), Image.LANCZOS)
|
||||||
|
img.save(path)
|
||||||
|
print(f" {path} {img.size[0]}x{img.size[1]}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rgba = extract_rgba(SRC)
|
||||||
|
full = crop_band(rgba, *FULL_Y) # logo complet transparent serré
|
||||||
|
tree = crop_band(rgba, *TREE_Y) # arbre seul transparent serré
|
||||||
|
|
||||||
|
print("brand/")
|
||||||
|
save(full, "brand/arboretum-logo.png")
|
||||||
|
save(square(tree, 0.08), "brand/arboretum-mark.png")
|
||||||
|
# logo complet sur fond sombre (pour surfaces claires où la transparence gêne)
|
||||||
|
on_dark = Image.new("RGBA", (full.width + 160, full.height + 160), (*BG, 255))
|
||||||
|
on_dark.paste(full, (80, 80), full)
|
||||||
|
save(on_dark, "brand/arboretum-logo-on-dark.png")
|
||||||
|
|
||||||
|
print("packages/web/public/")
|
||||||
|
# logo UI : largeur max 600 px, transparent
|
||||||
|
ui = full.copy()
|
||||||
|
ui.thumbnail((600, 600), Image.LANCZOS)
|
||||||
|
ui.save("packages/web/public/logo.png")
|
||||||
|
print(f" packages/web/public/logo.png {ui.size[0]}x{ui.size[1]}")
|
||||||
|
# icônes maskable : arbre dans la zone sûre (contenu ~80 %), fond #09090b
|
||||||
|
mask = square(tree, 0.12, bg=BG)
|
||||||
|
save(mask, "packages/web/public/icon-192.png", 192)
|
||||||
|
save(mask, "packages/web/public/icon-512.png", 512)
|
||||||
|
save(mask, "packages/web/public/apple-touch-icon.png", 180)
|
||||||
|
# favicon : arbre transparent, multi-tailles
|
||||||
|
fav = square(tree, 0.04)
|
||||||
|
fav.save("packages/web/public/favicon.ico", sizes=[(16, 16), (32, 32), (48, 48)])
|
||||||
|
print(" packages/web/public/favicon.ico 16/32/48")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -7,11 +7,12 @@
|
|||||||
<meta name="theme-color" content="#09090b" />
|
<meta name="theme-color" content="#09090b" />
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<!-- iOS : le Web Push n'est disponible qu'en PWA installée (« Ajouter à l'écran d'accueil ») -->
|
<!-- iOS : le Web Push n'est disponible qu'en PWA installée (« Ajouter à l'écran d'accueil ») -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Arboretum" />
|
<meta name="apple-mobile-web-app-title" content="Arboretum" />
|
||||||
<link rel="apple-touch-icon" href="/icon.svg" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<title>Arboretum</title>
|
<title>Arboretum</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
packages/web/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
packages/web/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
packages/web/public/icon-192.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
packages/web/public/icon-512.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
@@ -1,18 +1,46 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="Arboretum">
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="Arboretum">
|
||||||
|
<defs>
|
||||||
|
<!-- glow néon : flou doux derrière les traits et les nœuds -->
|
||||||
|
<filter id="glow" x="-30%" y="-30%" width="160%" height="160%">
|
||||||
|
<feGaussianBlur stdDeviation="6" result="b"/>
|
||||||
|
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="branch" x1="256" y1="430" x2="256" y2="90" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#10b981"/>
|
||||||
|
<stop offset="1" stop-color="#34d399"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
<rect width="512" height="512" fill="#09090b"/>
|
<rect width="512" height="512" fill="#09090b"/>
|
||||||
<!-- arbre / branches (worktrees) — tracé dans la zone sûre maskable (~80% central) -->
|
|
||||||
<g fill="none" stroke="#34d399" stroke-width="22" stroke-linecap="round" stroke-linejoin="round">
|
<!-- branches / circuit (tronc + fourches symétriques), tracées dans la zone sûre maskable -->
|
||||||
<!-- tronc -->
|
<g fill="none" stroke="url(#branch)" stroke-width="17" stroke-linecap="round" stroke-linejoin="round" filter="url(#glow)">
|
||||||
<path d="M256 420 V232"/>
|
<!-- tronc : du nœud sommet jusqu'à la base avec le curseur >_ -->
|
||||||
<!-- branche gauche -->
|
<path d="M256 96 V392"/>
|
||||||
<path d="M256 300 L168 212"/>
|
<!-- paire haute -->
|
||||||
<!-- branche droite -->
|
<path d="M256 232 L150 232 V150"/>
|
||||||
<path d="M256 268 L344 180"/>
|
<path d="M256 232 L362 232 V150"/>
|
||||||
|
<!-- paire médiane -->
|
||||||
|
<path d="M256 300 L104 300 V214"/>
|
||||||
|
<path d="M256 300 L408 300 V214"/>
|
||||||
|
<!-- paire basse -->
|
||||||
|
<path d="M256 356 L150 356 V300"/>
|
||||||
|
<path d="M256 356 L362 356 V300"/>
|
||||||
|
<!-- base : invite de commande >_ -->
|
||||||
|
<path d="M232 404 L246 418 L232 432" stroke-width="14"/>
|
||||||
|
<path d="M258 434 H286" stroke-width="14"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- nœuds (sessions) : anneaux cyan lumineux, centre sombre -->
|
||||||
|
<g filter="url(#glow)">
|
||||||
|
<g fill="#09090b" stroke="#22d3ee" stroke-width="9">
|
||||||
|
<circle cx="256" cy="96" r="20"/>
|
||||||
|
<circle cx="150" cy="150" r="16"/>
|
||||||
|
<circle cx="362" cy="150" r="16"/>
|
||||||
|
<circle cx="104" cy="214" r="16"/>
|
||||||
|
<circle cx="408" cy="214" r="16"/>
|
||||||
|
<circle cx="150" cy="300" r="16"/>
|
||||||
|
<circle cx="362" cy="300" r="16"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- nœuds (sessions) -->
|
|
||||||
<g fill="#34d399">
|
|
||||||
<circle cx="256" cy="156" r="34"/>
|
|
||||||
<circle cx="160" cy="196" r="26"/>
|
|
||||||
<circle cx="352" cy="164" r="26"/>
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 727 B After Width: | Height: | Size: 1.9 KiB |
BIN
packages/web/public/logo.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
@@ -8,6 +8,8 @@
|
|||||||
"background_color": "#09090b",
|
"background_color": "#09090b",
|
||||||
"theme_color": "#09090b",
|
"theme_color": "#09090b",
|
||||||
"icons": [
|
"icons": [
|
||||||
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
|
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" },
|
||||||
|
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
|
||||||
|
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
@submit.prevent="onSubmit"
|
@submit.prevent="onSubmit"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<img src="/icon.svg" alt="" class="h-9 w-9" />
|
||||||
<h1 class="text-lg font-semibold text-zinc-100">{{ t('common.appName') }}</h1>
|
<h1 class="text-lg font-semibold text-zinc-100">{{ t('common.appName') }}</h1>
|
||||||
|
</div>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-zinc-400">{{ t('login.title') }}</p>
|
<p class="text-sm text-zinc-400">{{ t('login.title') }}</p>
|
||||||
|
|||||||