studio: finish Hanzo rebrand — sidebar mark, black+purple theme, progress favicon

- Sidebar logo: replace the inline ComfyLogo Vue component (top-left mark,
  compiled into the JS bundle, missed by the file-asset pass) with the Hanzo
  mark via branding/patch_sidebar_logo.py (keyed off the stable __name:`ComfyLogo`).
- Theme: branding/hanzo-theme.css forces a pure-black shell + Hanzo-purple
  accents (AA contrast), injected as the last stylesheet; patch_theme.py also
  rewrites the dark palette's canvas clear color to #000000 (CSS can't reach the
  litegraph <canvas>).
- Favicon: make_progress_favicons.py renders the 10 progress frames with the
  Hanzo mark + purple ring, fixing the tab reverting to the Comfy mark during
  generations. Committed PNGs like favicon.ico (Docker needs no rasterizer).
- Tab title: rebrand the runtime ' - ComfyUI' title suffix the string pass missed.
- apply-branding.sh replays all of the above on frontend-package upgrades.
- Rename fork-owned update_comfyui{,_stable}.bat -> update{,_stable}.bat.
This commit is contained in:
hanzo-dev
2026-07-03 02:20:33 -07:00
parent 1ff6280992
commit ca9a72f2a7
18 changed files with 342 additions and 8 deletions
+29 -8
View File
@@ -11,7 +11,7 @@ echo "=== Hanzo Studio Branding ==="
echo "Target: $STATIC_DIR"
# --- 1. Replace in-app logos ---
echo "[1/5] Replacing logos..."
echo "[1/7] Replacing logos..."
for logo_file in comfy-logo-single.svg comfy-logo-mono.svg comfy-cloud-logo.svg; do
target="$STATIC_DIR/assets/images/$logo_file"
if [ -f "$target" ]; then
@@ -25,16 +25,25 @@ done
# --- 2. Install favicon (Hanzo mark) ---
# The frontend sets the runtime favicon to /assets/favicon.ico via useFavicon();
# the web root copies cover the pre-boot page load and the default /favicon.ico
# request. favicon.ico is a committed artifact rendered from favicon.svg by
# make_favicon.py (the Docker image has no rasterizer).
echo "[2/5] Installing favicon..."
# request. During a generation it instead cycles the 10 progress frames under
# assets/images/favicon_progress_16x16/, so those must be Hanzo-branded too or the
# tab reverts to the Comfy mark while a job runs. All PNG/ICO are committed
# artifacts (rendered by make_favicon.py / make_progress_favicons.py) so the Docker
# image needs no rasterizer.
echo "[2/7] Installing favicon..."
cp "$BRANDING_DIR/favicon.ico" "$STATIC_DIR/assets/favicon.ico"
cp "$BRANDING_DIR/favicon.ico" "$STATIC_DIR/favicon.ico"
cp "$BRANDING_DIR/favicon.svg" "$STATIC_DIR/favicon.svg"
echo " Installed favicon.ico + favicon.svg"
progress_dst="$STATIC_DIR/assets/images/favicon_progress_16x16"
if [ -d "$progress_dst" ]; then
cp "$BRANDING_DIR"/favicon_progress_16x16/frame_*.png "$progress_dst/"
echo " Installed favicon.ico + favicon.svg + progress frames"
else
echo " Installed favicon.ico + favicon.svg (progress dir absent, skipped)"
fi
# --- 3. Patch index.html (title + icon link + loading text) ---
echo "[3/5] Patching index.html..."
echo "[3/7] Patching index.html..."
if [ -f "$STATIC_DIR/index.html" ]; then
# Rename the title and inject icon links (the upstream head has none).
sed -i 's#<title>ComfyUI</title>#<title>Hanzo Studio</title><link rel="icon" type="image/svg+xml" href="favicon.svg"/><link rel="alternate icon" href="favicon.ico"/>#g' "$STATIC_DIR/index.html"
@@ -44,13 +53,25 @@ if [ -f "$STATIC_DIR/index.html" ]; then
fi
# --- 4. Patch manifest JSON files ---
echo "[4/5] Patching manifests..."
echo "[4/7] Patching manifests..."
find "$STATIC_DIR" -name "manifest*.json" -exec sed -i 's/"ComfyUI"/"Hanzo Studio"/g' {} \;
find "$STATIC_DIR" -name "manifest*.json" -exec sed -i 's/studio\.org/hanzo.ai/g' {} \;
echo " Done"
# --- 5. Smart JS/CSS/JSON display-string patching ---
echo "[5/5] Smart JS patching..."
echo "[5/7] Smart JS patching..."
python3 "$BRANDING_DIR/patch_frontend.py" "$STATIC_DIR"
# --- 6. Replace the inline sidebar logo (ComfyLogo Vue component) ---
# This is the top-left in-app mark, an inline SVG compiled into a JS chunk — a
# different asset from the file-based logos handled in step 1.
echo "[6/7] Replacing in-app sidebar logo..."
python3 "$BRANDING_DIR/patch_sidebar_logo.py" "$STATIC_DIR"
# --- 7. Apply the black + Hanzo-purple theme ---
# Installs hanzo-theme.css, injects it as the last stylesheet in index.html, and
# rewrites the "dark" palette's canvas clear color to pure black.
echo "[7/7] Applying black + purple theme..."
python3 "$BRANDING_DIR/patch_theme.py" "$STATIC_DIR" "$BRANDING_DIR"
echo "=== Hanzo Studio branding complete ==="
Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

+64
View File
@@ -0,0 +1,64 @@
/* Hanzo Studio — black surface + Hanzo-purple accent theme.
* Minimal override: only background/surface/accent tokens are touched so the
* rest of the frontend's dark palette (text, node colors, borders) is intact.
* Loaded LAST in index.html so it beats PrimeVue's runtime-injected tokens;
* every declaration is !important because those tokens are injected after this
* sheet at boot. Colors chosen for WCAG AA contrast on black:
* accent #8B5CF6 on #000 -> 6.2:1 (AA normal text)
* button #ffffff on #7C3AED -> 5.25:1 (AA normal text)
*/
:root {
/* --- App shell (ComfyUI-native vars from the "dark" palette) --- */
--bg-color: #000000 !important;
--fg-color: #ffffff !important;
--comfy-menu-bg: #0a0a0a !important;
--comfy-menu-secondary-bg: #141414 !important;
--comfy-input-bg: #0a0a0a !important;
--content-bg: #0a0a0a !important;
--content-hover-bg: #171717 !important;
--border-color: #1f1f1f !important;
--tr-even-bg-color: #0a0a0a !important;
--tr-odd-bg-color: #141414 !important;
/* --- PrimeVue surface ramp: darken the panel/sidebar/topbar shades only.
* 500..900 are the backgrounds; 100/300 stay light (used for text/borders). */
--p-surface-900: #000000 !important;
--p-surface-800: #0a0a0a !important;
--p-surface-700: #141414 !important;
--p-surface-600: #1c1c1c !important;
--p-surface-500: #242424 !important;
/* --- PrimeVue semantic surfaces --- */
--p-content-background: #000000 !important;
--p-content-hover-background: #171717 !important;
--p-overlay-modal-background: #0a0a0a !important;
--p-overlay-popover-background: #0a0a0a !important;
--p-mask-background: rgba(0, 0, 0, 0.72) !important;
/* --- Hanzo purple accent --- */
--p-primary-color: #8b5cf6 !important; /* icons/links/active — AA on black */
--p-primary-contrast-color: #ffffff !important;
--p-primary-400: #a78bfa !important;
--p-primary-500: #8b5cf6 !important;
--p-primary-600: #7c3aed !important;
--p-focus-ring-color: #8b5cf6 !important;
--p-highlight-background: rgba(124, 58, 237, 0.24) !important;
--p-highlight-focus-background: rgba(124, 58, 237, 0.32) !important;
--p-highlight-color: #ede9fe !important;
/* --- Primary buttons: white on deeper violet keeps AA (5.25:1) --- */
--p-button-primary-background: #7c3aed !important;
--p-button-primary-hover-background: #6d28d9 !important;
--p-button-primary-active-background: #5b21b6 !important;
--p-button-primary-border-color: #7c3aed !important;
--p-button-primary-color: #ffffff !important;
}
/* Pure-black canvas gutter behind the litegraph <canvas>. */
html,
body,
body.litegraph,
#vue-app {
background-color: #000000 !important;
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Render the 10 animated progress-favicon frames with the Hanzo mark.
During a generation the frontend cycles
`/assets/images/favicon_progress_16x16/frame_{0..9}.png` as the browser-tab
favicon. Upstream ships ComfyUI-branded frames, so the tab reverts to the Comfy
mark whenever a job runs. These frames are the Hanzo mark (white on a black tile,
matching favicon.svg) with a Hanzo-purple progress ring that fills 0 -> 100%.
Like favicon.ico, the PNGs are committed build artifacts so apply-branding.sh can
install them without a rasterizer in the Docker image. Re-run only if the mark or
palette changes: python3 branding/make_progress_favicons.py
"""
import os
from PIL import Image, ImageDraw
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "favicon_progress_16x16")
SS = 128 # supersampled canvas, downsampled to 16x16
FRAMES = 10
MARK_SCALE = 0.92
RING_BBOX = (12, 12, SS - 12, SS - 12)
RING_WIDTH = 10
BG = (0, 0, 0, 255) # black tile, like favicon.svg
MARK = (255, 255, 255, 255) # white Hanzo mark
RING = (139, 92, 246, 255) # Hanzo purple #8B5CF6
# Hanzo mark: five subpaths from branding/hanzo-logo.svg, as polygons (0..67 space).
POLYS = [
[(22.21, 67), (22.21, 44.6369), (0, 44.6369), (0, 67)],
[(66.7038, 22.3184), (22.2534, 22.3184), (0.0878906, 44.6367), (44.4634, 44.6367)],
[(22.21, 0), (0, 0), (0, 22.3184), (22.21, 22.3184)],
[(66.7198, 0), (44.5098, 0), (44.5098, 22.3184), (66.7198, 22.3184)],
[(66.7198, 67), (66.7198, 44.6369), (44.5098, 44.6369), (44.5098, 67)],
]
MARK_W = 66.7198
MARK_H = 67.0
OFF_X = (SS - MARK_W * MARK_SCALE) / 2
OFF_Y = (SS - MARK_H * MARK_SCALE) / 2
def draw_frame(frac: float) -> Image.Image:
img = Image.new("RGBA", (SS, SS), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
d.rounded_rectangle((0, 0, SS - 1, SS - 1), radius=20, fill=BG)
for poly in POLYS:
pts = [(x * MARK_SCALE + OFF_X, y * MARK_SCALE + OFF_Y) for x, y in poly]
d.polygon(pts, fill=MARK)
if frac > 0:
d.arc(RING_BBOX, start=-90, end=-90 + frac * 360, fill=RING, width=RING_WIDTH)
return img.resize((16, 16), Image.LANCZOS)
def main() -> None:
os.makedirs(OUT, exist_ok=True)
for i in range(FRAMES):
frac = (i + 1) / FRAMES
draw_frame(frac).save(os.path.join(OUT, f"frame_{i}.png"))
print(f"Wrote {FRAMES} progress frames to {OUT}")
if __name__ == "__main__":
main()
+4
View File
@@ -41,6 +41,10 @@ DISPLAY_REPLACEMENTS = [
# Exact display strings — multi-word phrases are safe to replace globally
# because they only appear as display text, not in code identifiers.
# Order matters: longer matches first to avoid partial replacements.
# Browser-tab title suffix: the frontend builds document.title as
# `${workflowName} - ComfyUI`. The " - ComfyUI" constant has a space (not a
# quote) before ComfyUI, so the quoted-string patterns below miss it.
(' - ComfyUI', ' - Hanzo Studio'),
('ComfyUI Issues', 'Hanzo Studio Issues'),
('ComfyUI Docs', 'Hanzo Studio Docs'),
('ComfyUI Forum', 'Hanzo Studio Forum'),
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
r"""Replace the in-app top-left sidebar logo (the ComfyUI mark) with the Hanzo mark.
The sidebar logo is NOT a file asset — it is an inline single-path SVG compiled
into the `ComfyLogo` Vue component inside a hashed JS chunk (e.g. GraphView-*.js).
`branding/apply-branding.sh` already swaps the file-based logos in assets/images/;
this handles the inline one that pass missed.
We key off the stable Vue component name `__name:\`ComfyLogo\`` so the patch replays
across frontend-package upgrades even as chunk hashes and minified identifiers change.
Within that component we rewrite the viewBox and the single path `d` to the Hanzo
mark (five filled rectangles forming the H glyph, in a 0..67 coordinate space).
The component renders with mode="fill" and color=currentColor(white), so the mark
fills white exactly like the original.
"""
import os
import re
import sys
STATIC_DIR = sys.argv[1]
HANZO_VIEWBOX = "0 0 67 67"
# Five subpaths in one `d`, matching branding/hanzo-logo.svg (0..67 space).
HANZO_PATH = (
"M22.21 67V44.6369H0V67H22.21Z"
"M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"
"M22.21 0H0V22.3184H22.21V0Z"
"M66.7198 0H44.5098V22.3184H66.7198V0Z"
"M66.7198 67V44.6369H44.5098V67H66.7198Z"
)
ANCHOR = "__name:`ComfyLogo`"
SEGMENT_LEN = 3000
def patch_content(content: str):
idx = content.find(ANCHOR)
if idx == -1:
return content, False
head, seg, tail = content[:idx], content[idx:idx + SEGMENT_LEN], content[idx + SEGMENT_LEN:]
orig = seg
seg = re.sub(r'viewBox:`0 0 [0-9.]+ [0-9.]+`', f'viewBox:`{HANZO_VIEWBOX}`', seg, count=1)
seg = re.sub(r'd:`M[0-9][^`]*`', f'd:`{HANZO_PATH}`', seg, count=1)
if seg == orig:
return content, False
return head + seg + tail, True
def main() -> int:
found = 0
for root, _dirs, files in os.walk(STATIC_DIR):
for fname in files:
if not fname.endswith(".js"):
continue
path = os.path.join(root, fname)
try:
with open(path, encoding="utf-8", errors="ignore") as f:
content = f.read()
except OSError:
continue
if ANCHOR not in content:
continue
found += 1
new_content, changed = patch_content(content)
if changed:
with open(path, "w", encoding="utf-8") as f:
f.write(new_content)
print(f" Replaced ComfyLogo sidebar mark in {fname}")
elif HANZO_PATH in content:
print(f" ComfyLogo already Hanzo mark in {fname}")
else:
print(f" WARNING: ComfyLogo in {fname} but path not matched")
return 1
if found == 0:
print(" WARNING: ComfyLogo component not found — sidebar logo unchanged")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Apply the Hanzo Studio black + purple theme to the prebuilt frontend.
Three orthogonal levers, each in its own lane:
1. CSS shell skin -> copy branding/hanzo-theme.css into static/ and inject a
<link> as the LAST stylesheet in index.html (idempotent).
2. Canvas pixels -> CSS cannot reach the litegraph <canvas>; rewrite the
"dark" palette's CLEAR_BACKGROUND_COLOR to pure black in
the JS bundle. Keyed off the stable palette id+name so it
replays across upgrades even if the value changes.
Usage: patch_theme.py <static_dir> <branding_dir>
"""
import os
import re
import shutil
import sys
STATIC_DIR = sys.argv[1]
BRANDING_DIR = sys.argv[2]
THEME_CSS = "hanzo-theme.css"
LINK_TAG = f'<link rel="stylesheet" href="{THEME_CSS}"/>'
PALETTE_ANCHOR = "id:`dark`,name:`Dark (Default)`"
PALETTE_SEGMENT_LEN = 1600
def install_css() -> None:
src = os.path.join(BRANDING_DIR, THEME_CSS)
dst = os.path.join(STATIC_DIR, THEME_CSS)
shutil.copyfile(src, dst)
print(f" Installed {THEME_CSS}")
def inject_link() -> None:
index = os.path.join(STATIC_DIR, "index.html")
if not os.path.isfile(index):
print(" WARNING: index.html not found — theme link not injected")
return
with open(index, encoding="utf-8") as f:
html = f.read()
if LINK_TAG in html:
print(" Theme link already present in index.html")
return
if "</head>" not in html:
print(" WARNING: no </head> in index.html — theme link not injected")
return
html = html.replace("</head>", LINK_TAG + "</head>", 1)
with open(index, "w", encoding="utf-8") as f:
f.write(html)
print(" Injected hanzo-theme.css link into index.html")
def blacken_canvas() -> None:
patched = 0
for root, _dirs, files in os.walk(STATIC_DIR):
for fname in files:
if not fname.endswith(".js"):
continue
path = os.path.join(root, fname)
try:
with open(path, encoding="utf-8", errors="ignore") as f:
content = f.read()
except OSError:
continue
if PALETTE_ANCHOR not in content:
continue
new_content = content
for m in re.finditer(re.escape(PALETTE_ANCHOR), content):
start = m.start()
seg = new_content[start:start + PALETTE_SEGMENT_LEN]
seg2 = re.sub(
r'CLEAR_BACKGROUND_COLOR:`#[0-9a-fA-F]{3,8}`',
"CLEAR_BACKGROUND_COLOR:`#000000`",
seg,
count=1,
)
if seg2 != seg:
new_content = new_content[:start] + seg2 + new_content[start + PALETTE_SEGMENT_LEN:]
if new_content != content:
with open(path, "w", encoding="utf-8") as f:
f.write(new_content)
print(f" Blackened canvas clear color in {fname}")
patched += 1
elif "CLEAR_BACKGROUND_COLOR:`#000000`" in content:
print(f" Canvas clear color already black in {fname}")
patched += 1
if patched == 0:
print(" WARNING: 'dark' palette not found — canvas color unchanged")
def main() -> None:
install_css()
inject_link()
blacken_canvas()
if __name__ == "__main__":
main()