shelley: change favicon to Cool S

Philip Zeyliger and Shelley created

I couldn't find a good shell SVG anywhere with licensing that
I could be sure of, so I drew a cool S in Excalidraw, and
the 90's are back.

Prompt: Let's try to fix
https://github.com/boldsoftware/shelley/issues/18 by changing the
favicon. I want to preserve the fact that the favicons have random
colors and therefore keep it in svg. Let's do the "Cool S"
(https://en.wikipedia.org/wiki/Cool_S) as our image for now; that should
be easy to draw in svg.

Replace the scallop shell design with the iconic "Cool S" (also known
as the Stussy S or Super S), using the hand-drawn Excalidraw SVG
directly to preserve the sketchy aesthetic.

The color varies by hostname hash (hue 0-360°) while keeping
saturation and lightness constant for consistent appearance.

Fixes https://github.com/boldsoftware/shelley/issues/18

Co-authored-by: Shelley <shelley@exe.dev>

Change summary

server/handlers.go         | 72 +++++++++++++++------------------------
ui/src/services/favicon.ts |  5 +-
2 files changed, 31 insertions(+), 46 deletions(-)

Detailed changes

server/handlers.go 🔗

@@ -305,54 +305,38 @@ func hashString(s string) uint32 {
 	return hash
 }
 
-// generateFaviconSVG creates a seashell favicon with color based on hostname hash
+// generateFaviconSVG creates a Cool S favicon with color based on hostname hash
+// Big colored circle background with the Cool S inscribed in white
 func generateFaviconSVG(hostname string) string {
 	hash := hashString(hostname)
 	h := hash % 360
-	s := 55
-	l := 65
-	lightL := l + 15
-	if lightL > 90 {
-		lightL = 90
-	}
-	darkL := l - 15
-	if darkL < 40 {
-		darkL = 40
-	}
-	strokeL := darkL - 15
-	if strokeL < 25 {
-		strokeL = 25
-	}
-
-	return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
-  <defs>
-    <linearGradient id="shellGrad" x1="0%%" y1="0%%" x2="100%%" y2="100%%">
-      <stop offset="0%%" style="stop-color:hsl(%d, %d%%, %d%%)"/>
-      <stop offset="50%%" style="stop-color:hsl(%d, %d%%, %d%%)"/>
-      <stop offset="100%%" style="stop-color:hsl(%d, %d%%, %d%%)"/>
-    </linearGradient>
-  </defs>
-  <path d="M16 4 C8 4 3 12 3 20 C3 24 6 28 16 28 C26 28 29 24 29 20 C29 12 24 4 16 4"
-        fill="url(#shellGrad)" stroke="hsl(%d, %d%%, %d%%)" stroke-width="1"/>
-  <path d="M16 6 L16 26" stroke="hsl(%d, %d%%, %d%%)" stroke-width="1" fill="none"/>
-  <path d="M16 6 L8 25" stroke="hsl(%d, %d%%, %d%%)" stroke-width="1" fill="none"/>
-  <path d="M16 6 L24 25" stroke="hsl(%d, %d%%, %d%%)" stroke-width="1" fill="none"/>
-  <path d="M16 6 L5 22" stroke="hsl(%d, %d%%, %d%%)" stroke-width="1" fill="none"/>
-  <path d="M16 6 L27 22" stroke="hsl(%d, %d%%, %d%%)" stroke-width="1" fill="none"/>
-  <path d="M16 6 L11 26" stroke="hsl(%d, %d%%, %d%%)" stroke-width="0.8" fill="none"/>
-  <path d="M16 6 L21 26" stroke="hsl(%d, %d%%, %d%%)" stroke-width="0.8" fill="none"/>
+	bgColor := fmt.Sprintf("hsl(%d, 70%%, 55%%)", h)
+	// White S on colored background - good contrast on any saturated hue
+	strokeColor := "#ffffff"
+
+	// Original Cool S viewBox: 0 0 171 393 (tall rectangle)
+	// Square viewBox 0 0 400 400 with circle, S scaled and centered inside
+	// S dimensions: 171x393, scale 0.97 gives 166x381, centered in 400x400
+	return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
+<circle cx="200" cy="200" r="200" fill="%s"/>
+<g transform="translate(117 10) scale(0.97)">
+<g stroke-linecap="round"><g transform="translate(13.3 97.5) rotate(0 1.4 42.2)"><path d="M1.28 0.48C1.15 14.67,-0.96 71.95,-1.42 86.14M-1.47-1.73C-0.61 11.51,4.65 66.62,4.21 81.75" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(87.6 97.2) rotate(0 1.2 42.4)"><path d="M-1.42 1.14C-1.89 15.33,-1.41 71.93,-1.52 85.6M3-0.71C3.35 12.53,3.95 66.59,4.06 80.91" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(156.3 91) rotate(0 0.7 42.1)"><path d="M-1.52 0.6C-1.62 14.26,-1.97 68.6,-2.04 83.12M2.86-1.55C3.77 12.32,3.09 71.53,3.26 85.73" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(157.7 230.3) rotate(0 0.6 42.9)"><path d="M-2.04-1.88C-2.11 12.64,-2.52 72.91,-1.93 87.72M2.05 3.27C3.01 17.02,3.68 70.97,3.43 84.18" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(12.6 226.7) rotate(0 0.2 44.3)"><path d="M-1.93 2.72C-1.33 17.52,1.37 73.57,1.54 86.96M2.23 1.72C2.77 15.92,1.05 69.12,0.14 83.02" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(82.8 226.6) rotate(0 -1.1 43.1)"><path d="M1.54 1.96C1.7 15.35,-0.76 69.37,-0.93 83.06M-1.07 0.56C-1.19 15.45,-3.69 71.28,-3.67 85.64" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(152.7 311.8) rotate(0 -32.3 34.6)"><path d="M-0.93-1.94C-12.26 9.08,-55.27 56.42,-66.46 68.08M3.76 3.18C-8.04 14.42,-56.04 59.98,-68.41 71.22" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(14.7 308.2) rotate(0 34.1 33.6)"><path d="M0.54-0.92C12.51 10.75,58.76 55.93,70.91 68.03M-2.62-3.88C8.97 8.35,55.58 59.22,68.08 71.13" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(11.3 178.5) rotate(0 35.7 23.4)"><path d="M-1.09-0.97C10.89 7.63,60.55 42.51,72.41 50.67M3.51-3.96C15.2 4,60.24 37.93,70.94 47.11" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(11.3 223.5) rotate(0 13.4 -10.2)"><path d="M1.41 2.67C6.27-1,23.83-19.1,28.07-23M-1.26 1.66C3.24-1.45,19.69-14.92,25.32-19.37" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(13.3 94.5) rotate(0 34.6 -42.2)"><path d="M-0.93 0C9.64-13.89,53.62-66.83,64.85-80.71M3.76-2.46C15.07-15.91,59.99-71.5,70.08-84.48" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(81.3 12.5) rotate(0 36.1 39.1)"><path d="M-2.15 2.29C10.41 14.58,61.78 62.2,74.43 73.73M1.88 1.07C14.1 13.81,60.32 65.18,71.89 77.21" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(88.3 177.5) rotate(0 31.2 22.9)"><path d="M-0.57-0.27C10.92 7.09,55.6 38.04,66.75 46.48M-4.32-2.89C6.87 4.52,51.07 40.67,63.83 48.74" stroke="%s" stroke-width="14" fill="none"/></g></g>
+<g stroke-linecap="round"><g transform="translate(155.3 174.5) rotate(0 -10.7 13.4)"><path d="M-1.25-2.52C-5.27 2.41,-21.09 24.62,-24.67 29.33M3.26 2.28C0.21 6.4,-14.57 20.81,-19.18 25.04" stroke="%s" stroke-width="14" fill="none"/></g></g>
+</g>
 </svg>`,
-		h, s, lightL,
-		h, s, l,
-		h, s, darkL,
-		h, s-10, strokeL,
-		h, s-20, darkL,
-		h, s-20, darkL,
-		h, s-20, darkL,
-		h, s-20, darkL,
-		h, s-20, darkL,
-		h, s-20, darkL,
-		h, s-20, darkL,
+		bgColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor, strokeColor,
 	)
 }
 

ui/src/services/favicon.ts 🔗

@@ -34,10 +34,11 @@ function addStatusDot(svg: string, status: FaviconStatus): string {
   const svgWithoutClose = svg.substring(0, closingTagIndex);
 
   // Add the status dot in the bottom-right corner
+  // New viewBox is 400x400, so position dot near bottom-right at ~350,350
   const dotColor = status === "working" ? "#f59e0b" : "#22c55e";
   const statusDot = `
-  <circle cx="26" cy="26" r="6" fill="white"/>
-  <circle cx="26" cy="26" r="5" fill="${dotColor}"/>
+  <circle cx="340" cy="340" r="50" fill="white"/>
+  <circle cx="340" cy="340" r="40" fill="${dotColor}"/>
 `;
 
   return svgWithoutClose + statusDot + "</svg>";