diff --git a/cmd/appview/main.go b/cmd/appview/main.go index c12345e..8d07f03 100644 --- a/cmd/appview/main.go +++ b/cmd/appview/main.go @@ -13,6 +13,9 @@ import ( // Register our custom middleware _ "atcr.io/pkg/appview/middleware" + + // Register built-in themes + _ "atcr.io/themes/seamark" ) var configFile string @@ -79,7 +82,12 @@ func serveRegistry(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - server, err := appview.NewAppViewServer(cfg, nil) + branding, err := appview.LookupTheme(cfg.UI.Theme) + if err != nil { + return err + } + + server, err := appview.NewAppViewServer(cfg, branding) if err != nil { return fmt.Errorf("failed to initialize server: %w", err) } diff --git a/pkg/appview/config.go b/pkg/appview/config.go index 910b912..fd5e02e 100644 --- a/pkg/appview/config.go +++ b/pkg/appview/config.go @@ -65,6 +65,9 @@ type ServerConfig struct { type UIConfig struct { // SQLite database path. DatabasePath string `yaml:"database_path" comment:"SQLite database for OAuth sessions, stars, pull counts, and device approvals."` + + // Visual theme name (e.g. "seamark"). Empty string uses default atcr.io branding. + Theme string `yaml:"theme" comment:"Visual theme name (e.g. \"seamark\"). Empty uses default atcr.io branding."` } // HealthConfig defines health check and cache settings @@ -136,6 +139,7 @@ func setDefaults(v *viper.Viper) { // UI defaults v.SetDefault("ui.database_path", "/var/lib/atcr/ui.db") + v.SetDefault("ui.theme", "") // Health defaults v.SetDefault("health.cache_ttl", "15m") diff --git a/pkg/appview/themes.go b/pkg/appview/themes.go new file mode 100644 index 0000000..3993a6e --- /dev/null +++ b/pkg/appview/themes.go @@ -0,0 +1,28 @@ +package appview + +import "fmt" + +// themeRegistry maps theme names to their BrandingOverrides constructors. +var themeRegistry = make(map[string]func() *BrandingOverrides) + +// RegisterTheme registers a named theme. Call from init() in theme packages. +func RegisterTheme(name string, fn func() *BrandingOverrides) { + if _, exists := themeRegistry[name]; exists { + panic(fmt.Sprintf("theme %q already registered", name)) + } + themeRegistry[name] = fn +} + +// LookupTheme returns BrandingOverrides for a registered theme name. +// Returns nil for empty name (default branding). Returns an error for +// unknown theme names. +func LookupTheme(name string) (*BrandingOverrides, error) { + if name == "" { + return nil, nil + } + fn, ok := themeRegistry[name] + if !ok { + return nil, fmt.Errorf("unknown theme %q", name) + } + return fn(), nil +} diff --git a/themes/seamark/embed.go b/themes/seamark/embed.go new file mode 100644 index 0000000..639b216 --- /dev/null +++ b/themes/seamark/embed.go @@ -0,0 +1,67 @@ +// Package seamark provides the Seamark visual theme for the ATCR AppView. +// Import with a blank identifier to register: _ "atcr.io/themes/seamark" +// Enable with ATCR_UI_THEME=seamark. +package seamark + +import ( + "embed" + "io/fs" + + "atcr.io/pkg/appview" +) + +//go:embed public +var publicFS embed.FS + +//go:embed theme.css +var themeCSS string + +func init() { + appview.RegisterTheme("seamark", branding) +} + +func branding() *appview.BrandingOverrides { + // Strip the leading "public" directory so the overlay FS matches the + // structure expected by resolvePublicFS (which looks under "public/"). + pubSub, err := fs.Sub(publicFS, "public") + if err != nil { + panic("seamark: bad embed layout: " + err.Error()) + } + + return &appview.BrandingOverrides{ + PublicFS: prefixFS{sub: pubSub}, + ExtraCSS: themeCSS, + } +} + +// prefixFS re-adds the "public/" prefix so the overlay FS works with +// atcr.io's resolvePublicFS which opens paths like "public/favicon.svg". +type prefixFS struct{ sub fs.FS } + +func (p prefixFS) Open(name string) (fs.File, error) { + const prefix = "public/" + if len(name) > len(prefix) && name[:len(prefix)] == prefix { + return p.sub.Open(name[len(prefix):]) + } + // Non-public paths: return not-found so the overlay falls through. + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +} + +func (p prefixFS) ReadDir(name string) ([]fs.DirEntry, error) { + const prefix = "public/" + if name == "public" { + return fs.ReadDir(p.sub, ".") + } + if len(name) > len(prefix) && name[:len(prefix)] == prefix { + return fs.ReadDir(p.sub, name[len(prefix):]) + } + return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist} +} + +func (p prefixFS) ReadFile(name string) ([]byte, error) { + const prefix = "public/" + if len(name) > len(prefix) && name[:len(prefix)] == prefix { + return fs.ReadFile(p.sub, name[len(prefix):]) + } + return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrNotExist} +} diff --git a/themes/seamark/public/apple-touch-icon.png b/themes/seamark/public/apple-touch-icon.png new file mode 100644 index 0000000..53ec455 Binary files /dev/null and b/themes/seamark/public/apple-touch-icon.png differ diff --git a/themes/seamark/public/favicon-96x96.png b/themes/seamark/public/favicon-96x96.png new file mode 100644 index 0000000..3650661 Binary files /dev/null and b/themes/seamark/public/favicon-96x96.png differ diff --git a/themes/seamark/public/favicon.ico b/themes/seamark/public/favicon.ico new file mode 100644 index 0000000..4c69326 Binary files /dev/null and b/themes/seamark/public/favicon.ico differ diff --git a/themes/seamark/public/favicon.svg b/themes/seamark/public/favicon.svg new file mode 100644 index 0000000..c4d9e17 --- /dev/null +++ b/themes/seamark/public/favicon.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + diff --git a/themes/seamark/public/icons.svg b/themes/seamark/public/icons.svg new file mode 100644 index 0000000..cb8b9ca --- /dev/null +++ b/themes/seamark/public/icons.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/themes/seamark/public/robots.txt b/themes/seamark/public/robots.txt new file mode 100644 index 0000000..74aea2d --- /dev/null +++ b/themes/seamark/public/robots.txt @@ -0,0 +1,33 @@ +# Seamark Container Registry - robots.txt + +User-agent: * + +# Allow public pages +Allow: / +Allow: /r/ +Allow: /u/ +Allow: /search +Allow: /install +Allow: /privacy +Allow: /terms + +# Block API endpoints +Disallow: /api/ +Disallow: /v2/ + +# Block auth and session pages +Disallow: /auth/ +Disallow: /login +Disallow: /settings +Disallow: /device/ + +# Block OG image generation endpoints +Disallow: /og/ + +# Block static assets from indexing (optional, saves crawl budget) +Disallow: /js/ +Disallow: /css/ +Disallow: /static/ + +# Sitemap +Sitemap: https://seamark.dev/sitemap.xml diff --git a/themes/seamark/public/site.webmanifest b/themes/seamark/public/site.webmanifest new file mode 100644 index 0000000..848e499 --- /dev/null +++ b/themes/seamark/public/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "Seamark", + "short_name": "Seamark", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#274b70", + "background_color": "#274b70", + "display": "standalone" +} \ No newline at end of file diff --git a/themes/seamark/public/sitemap-static.xml b/themes/seamark/public/sitemap-static.xml new file mode 100644 index 0000000..de43324 --- /dev/null +++ b/themes/seamark/public/sitemap-static.xml @@ -0,0 +1,28 @@ + + + + https://seamark.dev/ + daily + 1.0 + + + https://seamark.dev/search + daily + 0.8 + + + https://seamark.dev/install + monthly + 0.9 + + + https://seamark.dev/privacy + yearly + 0.3 + + + https://seamark.dev/terms + yearly + 0.3 + + diff --git a/themes/seamark/public/sitemap.xml b/themes/seamark/public/sitemap.xml new file mode 100644 index 0000000..d463e44 --- /dev/null +++ b/themes/seamark/public/sitemap.xml @@ -0,0 +1,14 @@ + + + + https://seamark.dev/sitemap-static.xml + + + diff --git a/themes/seamark/public/web-app-manifest-192x192.png b/themes/seamark/public/web-app-manifest-192x192.png new file mode 100644 index 0000000..6fd4f4d Binary files /dev/null and b/themes/seamark/public/web-app-manifest-192x192.png differ diff --git a/themes/seamark/public/web-app-manifest-512x512.png b/themes/seamark/public/web-app-manifest-512x512.png new file mode 100644 index 0000000..25350ec Binary files /dev/null and b/themes/seamark/public/web-app-manifest-512x512.png differ diff --git a/themes/seamark/theme.css b/themes/seamark/theme.css new file mode 100644 index 0000000..ca0cd67 --- /dev/null +++ b/themes/seamark/theme.css @@ -0,0 +1,14 @@ +/* Seamark color overrides (injected after atcr.io's compiled stylesheet) */ +[data-theme="dark"] { + --color-secondary: oklch(90% 0.182 98.111); + --color-secondary-content: oklch(30% 0.182 98.111); + --color-accent: oklch(66.6% 0.121 28); + --color-accent-content: oklch(28% 0.121 28); +} + +[data-theme="light"] { + --color-secondary: oklch(76% 0.095 76.1); + --color-secondary-content: oklch(27.1% 0.026 76.4); + --color-accent: oklch(66.6% 0.121 28); + --color-accent-content: oklch(28% 0.121 28); +}