๐Ÿก index : quine-global/hyper.quineglobal.com.git

package html

import (
	"fmt"
	"strings"
	"time"

	. "maragu.dev/gomponents"
	. "maragu.dev/gomponents/html"
)

// Release is a single GitHub release with per-platform download URLs.
type Release struct {
	TagName     string    // Full GitHub tag, e.g. "v4.0.0-q-canary.12"
	Version     string    // Base semver, e.g. "4.0.0"
	CanaryBuild int       // >0 for canary, e.g. 12 from "v4.0.0-q-canary.12"
	Date        string    // Human date for UI, e.g. "May 2026"
	PublishedAt time.Time // For update feed pub_date
	IsCanary    bool
	Assets      map[string]map[string]string // os -> arch -> URL
}

// DownloadURL returns the asset URL for the given OS and arch, or "" if unavailable.
func (r Release) DownloadURL(os, arch string) string {
	if m := r.Assets[os]; m != nil {
		return m[arch]
	}
	return ""
}

// DisplayVersion returns a human-readable version string.
// Canary builds render as "4.0.0 Canary #12".
func (r Release) DisplayVersion() string {
	if r.CanaryBuild > 0 {
		return fmt.Sprintf("%s Canary #%d", r.Version, r.CanaryBuild)
	}
	return r.Version
}

func buildFallback(version, date string, published time.Time, canary bool) Release {
	assets := make(map[string]map[string]string)
	for _, os := range []string{"mac", "windows", "linux"} {
		assets[os] = make(map[string]string)
		for _, arch := range []string{"arm64", "x64"} {
			assets[os][arch] = fallbackURL(version, os, arch)
		}
	}
	tag := "v" + version
	if canary {
		tag = "v" + version + "-q-canary.1" // placeholder build
	}
	return Release{
		TagName:     tag,
		Version:     version,
		Date:        date,
		PublishedAt: published,
		IsCanary:    canary,
		Assets:      assets,
	}
}

func fallbackURL(version, os, arch string) string {
	switch os {
	case "windows":
		return fmt.Sprintf("https://github.com/quine-global/hyper/releases/download/v%s/QuineHyper.Setup.%s.exe", version, version)
	case "linux":
		if arch == "arm64" {
			return fmt.Sprintf("https://github.com/quine-global/hyper/releases/download/v%s/QuineHyper-%s-arm64.AppImage", version, version)
		}
		return fmt.Sprintf("https://github.com/quine-global/hyper/releases/download/v%s/QuineHyper-%s.AppImage", version, version)
	default: // mac
		if arch == "arm64" {
			return fmt.Sprintf("https://github.com/quine-global/hyper/releases/download/v%s/QuineHyper-%s-arm64.dmg", version, version)
		}
		return fmt.Sprintf("https://github.com/quine-global/hyper/releases/download/v%s/QuineHyper-%s.dmg", version, version)
	}
}

// DownloadProps carries platform selection and release list for the download page.
type DownloadProps struct {
	DetectedOS   string
	DetectedArch string
	SelectedOS   string
	SelectedArch string
	ArchExplicit bool // true if arch came from an explicit ?arch= query param rather than auto-detection
	Releases     []Release
}

type platformOption struct {
	OS    string
	Arch  string
	Label string
}

var platforms = []platformOption{
	{"mac", "arm64", "macOS ยท Apple Silicon"},
	{"mac", "x64", "macOS ยท Intel"},
	{"windows", "x64", "Windows ยท x64"},
	{"linux", "x64", "Linux ยท x64"},
	{"linux", "arm64", "Linux ยท ARM64"},
}

func fileExt(os string) string {
	switch os {
	case "windows":
		return ".exe"
	case "linux":
		return ".AppImage"
	case "linux-deb":
		return ".deb"
	case "linux-rpm":
		return ".rpm"
	case "linux-snap":
		return ".snap"
	case "linux-pacman":
		return ".pacman"
	case "linux-flatpak":
		return ".flatpak"
	default:
		return ".dmg"
	}
}

func platformLabel(os, arch string) string {
	effectiveOS := os
	if os == "linux-rpm" {
		effectiveOS = "linux"
	}
	for _, p := range platforms {
		if p.OS == effectiveOS && p.Arch == arch {
			return p.Label
		}
	}
	return effectiveOS + " ยท " + arch
}

// DownloadPage renders the download page with OS/arch selection and version list.
func DownloadPage(props PageProps, dl DownloadProps) Node {
	props.Title = "Download โ€” Quine Hyper"
	props.Description = "Download Quine Hyper for macOS, Windows, or Linux."

	// Split into stable and canary.
	var stable, canary []Release
	for _, r := range dl.Releases {
		if r.IsCanary {
			canary = append(canary, r)
		} else {
			stable = append(stable, r)
		}
	}

	content := []Node{
		If(dl.SelectedOS == "mac" && !dl.ArchExplicit, macArchDetectScript()),
		H1(Class("font-mono text-3xl font-bold text-white"), Text("Download Quine Hyper")),
		Div(Class("mt-8"),
			P(Class("font-mono text-xs uppercase tracking-widest text-zinc-500 mb-3"), Text("Platform")),
			Div(Class("flex flex-wrap gap-2"),
				Group(platformButtons(dl)),
			),
			If(strings.HasPrefix(dl.SelectedOS, "linux"),
				linuxFormatToggle(dl),
			),
		),
	}
	if len(stable) > 0 {
		content = append(content, stableSection(stable, dl))
	}
	if len(canary) > 0 {
		content = append(content, canarySection(canary, dl))
	}
	content = append(content,
		Div(Class("mt-6 text-center"),
			A(
				Href("https://github.com/quine-global/hyper/releases"),
				Class("font-mono text-xs text-zinc-500 hover:text-white transition-colors underline underline-offset-4"),
				Text("All releases on GitHub โ†—"),
			),
		),
	)

	return page(props,
		Div(Class("mx-auto max-w-xl py-16"),
			Group(content),
		),
	)
}

func stableSection(stable []Release, dl DownloadProps) Node {
	latest := stable[0]
	older := stable[1:]
	url := latest.DownloadURL(dl.SelectedOS, dl.SelectedArch)

	return Group{
		Div(Class("mt-10"),
			Div(Class("mb-4 flex items-center gap-3"),
				P(Class("font-mono text-xs uppercase tracking-widest text-seafoam-400"), Text("Stable releases")),
			),

			// Primary download card
			Div(Class("min-h-[200px] flex items-center rounded-xl border border-seafoam-400/30 bg-zinc-900 p-6"),
				Div(Class("flex w-full items-center justify-between gap-4 flex-wrap"),
					Div(
						Span(Class("font-mono text-xs font-semibold uppercase tracking-widest text-seafoam-400"),
							Text("Latest Stable"),
						),
						H2(Class("mt-1 font-mono text-xl font-bold text-white"),
							Text("Quine Hyper v"+latest.DisplayVersion()),
						),
						P(Class("mt-1 font-mono text-xs text-zinc-500"),
							Text(latest.Date+" ยท "+platformLabel(dl.SelectedOS, dl.SelectedArch)+fileExt(dl.SelectedOS)),
						),
					),
					Div(Class("flex-1")),
					downloadButton(url, "bg-seafoam-400 hover:bg-seafoam-500 text-black"),
				),
			),
		),

		// Older stable releases
		If(len(older) > 0,
			Div(Class("mt-8"),
				P(Class("font-mono text-xs uppercase tracking-widest text-zinc-500 mb-4"), Text("Older releases")),
				Div(Class("divide-y divide-zinc-800 rounded-xl border border-zinc-800"),
					Group(releaseRows(older, dl, "text-seafoam-400 hover:text-seafoam-500")),
				),
			),
		),
	}
}

func canarySection(canary []Release, dl DownloadProps) Node {
	latest := canary[0]
	older := canary[1:]
	url := latest.DownloadURL(dl.SelectedOS, dl.SelectedArch)

	return Div(Class("mt-10"),
		Div(Class("mb-4 flex items-center gap-3"),
			P(Class("font-mono text-xs uppercase tracking-widest text-canary-500"), Text("Canary releases")),
			Span(Class("font-mono text-xs text-zinc-600"), Text("โ€” unstable, may contain bugs")),
		),

		// Latest canary card
		Div(Class("rounded-xl border border-canary-500/30 bg-zinc-900 p-6"),
			Div(Class("flex items-center justify-between gap-4 flex-wrap"),
				Div(
					Span(Class("font-mono text-xs font-semibold uppercase tracking-widest text-canary-400"),
						Text("Latest Canary"),
					),
					H2(Class("mt-1 font-mono text-base font-bold text-white"),
						Text("Quine Hyper v"+latest.DisplayVersion()),
					),
					P(Class("mt-1 font-mono text-xs text-zinc-500"),
						Text(latest.Date+" ยท "+platformLabel(dl.SelectedOS, dl.SelectedArch)+fileExt(dl.SelectedOS)),
					),
				),
				downloadButton(url, "bg-canary-500 hover:bg-canary-400 text-black", "py-2 text-xs"),
			),
		),

		// Older canary releases
		If(len(older) > 0,
			Div(Class("mt-4 divide-y divide-zinc-800 rounded-xl border border-zinc-800"),
				Group(releaseRows(older, dl, "text-canary-400 hover:text-canary-300")),
			),
		),
	)
}

func downloadButton(url, colorCls string, extraCls ...string) Node {
	base := "inline-flex items-center justify-center gap-2 rounded-lg w-[135px] font-mono font-semibold transition-colors shrink-0 "

	sizeCls := "px-6 py-3 text-sm"
	if len(extraCls) > 0 && extraCls[0] != "" {
		sizeCls = extraCls[0]
	}

	btnCls := base + sizeCls + " " + colorCls
	unavailableCls := base + sizeCls + " border border-zinc-800 text-zinc-600"

	if url == "" {
		return Span(Class(unavailableCls), Text("Not available"))
	}
	return A(
		Href(url),
		Class(btnCls),
		Text("โ†“ Download"),
	)
}

func releaseRows(releases []Release, dl DownloadProps, linkCls string) []Node {
	nodes := make([]Node, len(releases))
	for i, r := range releases {
		url := r.DownloadURL(dl.SelectedOS, dl.SelectedArch)
		var link Node
		if url != "" {
			link = A(
				Href(url),
				Class("font-mono text-xs transition-colors "+linkCls),
				Text("โ†“ "+platformLabel(dl.SelectedOS, dl.SelectedArch)+fileExt(dl.SelectedOS)),
			)
		} else {
			link = Span(
				Class("font-mono text-xs text-zinc-600"),
				Text("Not available"),
			)
		}
		nodes[i] = Div(Class("flex items-center justify-between px-5 py-4"),
			Div(Class("flex items-center gap-4"),
				Span(Class("font-mono text-sm font-semibold text-white"), Text("v"+r.DisplayVersion())),
				Span(Class("font-mono text-xs text-zinc-500"), Text(r.Date)),
			),
			link,
		)
	}
	return nodes
}

func platformButtons(dl DownloadProps) []Node {
	nodes := make([]Node, len(platforms))
	// Treat linux-rpm as linux for platform button highlight/detection purposes.
	effectiveSelectedOS := dl.SelectedOS
	if strings.HasPrefix(dl.SelectedOS, "linux") {
		effectiveSelectedOS = "linux"
	}
	for i, p := range platforms {
		isSelected := p.OS == effectiveSelectedOS && p.Arch == dl.SelectedArch
		isDetected := p.OS == dl.DetectedOS && p.Arch == dl.DetectedArch

		// Preserve the linux format when switching arch; snap is x64-only so don't carry it to arm64.
		targetOS := p.OS
		if p.OS == "linux" && strings.HasPrefix(dl.SelectedOS, "linux-") {
			if dl.SelectedOS != "linux-snap" || p.Arch == "x64" {
				targetOS = dl.SelectedOS
			}
		}

		var cls string
		if isSelected {
			cls = "inline-flex items-center gap-1.5 rounded-md bg-primary-500 px-3 py-1.5 font-mono text-xs font-semibold text-black"
		} else {
			cls = "inline-flex items-center gap-1.5 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 font-mono text-xs text-zinc-300 hover:border-zinc-500 hover:text-white transition-colors"
		}

		children := []Node{Text(p.Label)}
		if isDetected && !isSelected {
			children = append(children,
				Span(Class("rounded bg-zinc-700 px-1 py-0.5 text-zinc-400 text-[10px]"), Text("auto")),
			)
		} else if isDetected && isSelected {
			children = append(children,
				Span(Class("rounded bg-black/20 px-1 py-0.5 text-[10px]"), Text("auto")),
			)
		}

		nodes[i] = A(
			Href(fmt.Sprintf("/download?os=%s&arch=%s", targetOS, p.Arch)),
			Class(cls),
			Group(children),
		)
	}
	return nodes
}

// macArchDetectScript corrects the arm64 default guess on real Intel Macs.
// Browser User-Agent strings report "Intel Mac OS X" even on Apple Silicon, so
// the server can't tell arm64 from x64 from headers alone. The WebGL renderer
// string is the one client-side signal that actually differs: Apple Silicon
// reports something containing "Apple" (e.g. "Apple M2"); real Intel/AMD/Nvidia
// GPUs never do. If it doesn't look like Apple Silicon, reload with ?arch=x64.
func macArchDetectScript() Node {
	return Script(Raw(`(function(){
  try {
    var c = document.createElement('canvas');
    var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
    if (!gl) return;
    var ext = gl.getExtension('WEBGL_debug_renderer_info');
    if (!ext) return;
    var renderer = String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) || '');
    if (!renderer || /Apple/i.test(renderer)) return;
    var url = new URL(window.location.href);
    url.searchParams.set('arch', 'x64');
    window.location.replace(url.toString());
  } catch (e) {}
})();`))
}

func linuxFormatToggle(dl DownloadProps) Node {
	selectedCls := "inline-flex items-center rounded-md bg-primary-500 px-3 py-1.5 font-mono text-xs font-semibold text-black"
	unselectedCls := "inline-flex items-center rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 font-mono text-xs text-zinc-300 hover:border-zinc-500 hover:text-white transition-colors"
	cls := func(targetOS string) string {
		if dl.SelectedOS == targetOS {
			return selectedCls
		}
		return unselectedCls
	}
	href := func(targetOS string) string {
		return fmt.Sprintf("/download?os=%s&arch=%s", targetOS, dl.SelectedArch)
	}

	buttons := []Node{
		A(Href(href("linux")), Class(cls("linux")), Text("AppImage")),
		A(Href(href("linux-deb")), Class(cls("linux-deb")), Text("DEB")),
		A(Href(href("linux-rpm")), Class(cls("linux-rpm")), Text("RPM")),
		A(Href(href("linux-pacman")), Class(cls("linux-pacman")), Text("Pacman")),
		A(Href(href("linux-flatpak")), Class(cls("linux-flatpak")), Text("Flatpak")),
	}
	if dl.SelectedArch == "x64" {
		buttons = append(buttons, A(Href(href("linux-snap")), Class(cls("linux-snap")), Text("Snap")))
	}

	return Div(Class("mt-4"),
		P(Class("font-mono text-xs uppercase tracking-widest text-zinc-500 mb-3"), Text("Format")),
		Div(Class("flex items-center gap-2"),
			Group(buttons),
		),
	)
}