diff --git a/internal/generator/html.go b/internal/generator/html.go index 65bf8a6..908076b 100644 --- a/internal/generator/html.go +++ b/internal/generator/html.go @@ -12,6 +12,26 @@ import ( "gitcitadel-online/internal/nostr" ) +// getTemplateFuncs returns the common template functions +func getTemplateFuncs() template.FuncMap { + return template.FuncMap{ + "year": func() int { return time.Now().Year() }, + "json": func(v interface{}) (string, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(b), nil + }, + "hasPrefix": func(s, prefix string) bool { + return len(s) >= len(prefix) && s[:len(prefix)] == prefix + }, + "shortenPubkey": func(pubkey string) string { + return nostr.ShortenPubkey(pubkey) + }, + } +} + // HTMLGenerator generates HTML pages from wiki events type HTMLGenerator struct { templates *template.Template @@ -86,21 +106,18 @@ type EBookInfo struct { TimeISO string // ISO time } +// UserBadgeInfo represents user badge data for display +type UserBadgeInfo struct { + Pubkey string + Picture string + DisplayName string + Name string + ShortNpub string +} + // NewHTMLGenerator creates a new HTML generator func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaultImage string) (*HTMLGenerator, error) { - tmpl := template.New("base").Funcs(template.FuncMap{ - "year": func() int { return time.Now().Year() }, - "json": func(v interface{}) (string, error) { - b, err := json.Marshal(v) - if err != nil { - return "", err - } - return string(b), nil - }, - "hasPrefix": func(s, prefix string) bool { - return len(s) >= len(prefix) && s[:len(prefix)] == prefix - }, - }) + tmpl := template.New("base").Funcs(getTemplateFuncs()) // Load all templates templateFiles := []string{ @@ -246,19 +263,7 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems } // Use renderTemplate but with custom data - renderTmpl := template.New("blog-render").Funcs(template.FuncMap{ - "year": func() int { return time.Now().Year() }, - "json": func(v interface{}) (string, error) { - b, err := json.Marshal(v) - if err != nil { - return "", err - } - return string(b), nil - }, - "hasPrefix": func(s, prefix string) bool { - return len(s) >= len(prefix) && s[:len(prefix)] == prefix - }, - }) + renderTmpl := template.New("blog-render").Funcs(getTemplateFuncs()) files := []string{ filepath.Join(g.templateDir, "components.html"), @@ -392,19 +397,7 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event } // Parse base.html together with contact.html to ensure correct blocks are used - renderTmpl := template.New("render").Funcs(template.FuncMap{ - "year": func() int { return time.Now().Year() }, - "json": func(v interface{}) (string, error) { - b, err := json.Marshal(v) - if err != nil { - return "", err - } - return string(b), nil - }, - "hasPrefix": func(s, prefix string) bool { - return len(s) >= len(prefix) && s[:len(prefix)] == prefix - }, - }) + renderTmpl := template.New("render").Funcs(getTemplateFuncs()) // Parse base.html, components.html, and contact.html together files := []string{ @@ -515,19 +508,7 @@ func (g *HTMLGenerator) GenerateErrorPage(statusCode int, feedItems []FeedItemIn func (g *HTMLGenerator) renderTemplate(templateName string, data PageData) (string, error) { // Parse base.html together with the specific template to ensure correct blocks are used // This avoids the issue where all templates are parsed together and the last "content" block wins - renderTmpl := template.New("render").Funcs(template.FuncMap{ - "year": func() int { return time.Now().Year() }, - "json": func(v interface{}) (string, error) { - b, err := json.Marshal(v) - if err != nil { - return "", err - } - return string(b), nil - }, - "hasPrefix": func(s, prefix string) bool { - return len(s) >= len(prefix) && s[:len(prefix)] == prefix - }, - }) + renderTmpl := template.New("render").Funcs(getTemplateFuncs()) // Parse base.html, components.html, and the specific template together // This ensures the correct "content" block and reusable components are available diff --git a/internal/nostr/profile.go b/internal/nostr/profile.go index 4c44d23..9482be6 100644 --- a/internal/nostr/profile.go +++ b/internal/nostr/profile.go @@ -10,6 +10,36 @@ import ( "github.com/nbd-wtf/go-nostr/nip19" ) +// ShortenNpub shortens an npub to npub1...xyz format (first 8 + last 4 characters) +func ShortenNpub(npub string) string { + if len(npub) <= 12 { + return npub + } + return npub[:8] + "..." + npub[len(npub)-4:] +} + +// PubkeyToNpub converts a hex pubkey to npub format +func PubkeyToNpub(pubkey string) (string, error) { + npub, err := nip19.EncodePublicKey(pubkey) + if err != nil { + return "", fmt.Errorf("failed to encode pubkey to npub: %w", err) + } + return npub, nil +} + +// ShortenPubkey converts a pubkey to shortened npub format +func ShortenPubkey(pubkey string) string { + npub, err := PubkeyToNpub(pubkey) + if err != nil { + // Fallback: shorten hex pubkey + if len(pubkey) > 12 { + return pubkey[:8] + "..." + pubkey[len(pubkey)-4:] + } + return pubkey + } + return ShortenNpub(npub) +} + // Profile represents a parsed kind 0 profile event type Profile struct { Event *nostr.Event diff --git a/static/css/main.css b/static/css/main.css index 094ca01..dbb1709 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -3,14 +3,16 @@ :root { --bg-primary: #2d2d2d; --bg-secondary: #1e1e1e; - --text-primary: #e8e8e8; - --text-secondary: #b8b8b8; + --text-primary: #f0f0f0; /* Improved contrast from #e8e8e8 */ + --text-secondary: #c0c0c0; /* Improved contrast from #b8b8b8 */ --link-color: #7c9eff; --link-hover: #9bb3ff; --link-visited: #a58fff; --accent-color: #7c9eff; --border-color: #404040; --focus-color: #9bb3ff; + --focus-outline: 3px solid var(--focus-color); /* Thicker focus outline */ + --focus-offset: 3px; /* More visible offset */ --error-color: #ff6b6b; } @@ -113,11 +115,14 @@ header { transition: color 0.2s; } -.nav-menu a:hover, -.nav-menu a:focus { +.nav-menu a:hover { color: var(--link-hover); - outline: 2px solid var(--focus-color); - outline-offset: 2px; +} + +.nav-menu a:focus-visible { + color: var(--link-hover); + outline: var(--focus-outline); + outline-offset: var(--focus-offset); } .mobile-menu-toggle { @@ -125,9 +130,16 @@ header { flex-direction: column; gap: 4px; background: transparent; - border: none; + border: 2px solid transparent; cursor: pointer; padding: 8px; + border-radius: 4px; +} + +.mobile-menu-toggle:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-offset); + border-color: var(--focus-color); } .mobile-menu-toggle span { @@ -184,6 +196,9 @@ header { .wiki-layout { /* Three-column layout for wiki pages: wiki-sidebar | main-content | feed-sidebar */ + /* Layout handled by flexbox in .layout-container */ + /* No additional styles needed - flexbox handles the layout */ + display: flex; } .wiki-sidebar { @@ -230,9 +245,9 @@ header { color: var(--link-hover); } -.wiki-menu a:focus { - outline: 2px solid var(--focus-color); - outline-offset: 2px; +.wiki-menu a:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-offset); } .wiki-menu a.active { @@ -261,12 +276,15 @@ h1, h2, h3, h4, h5, h6 { color: var(--text-primary); line-height: 1.2; margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; } -h1 { font-size: 2.5rem; } -h2 { font-size: 2rem; } -h3 { font-size: 1.5rem; } -h4 { font-size: 1.25rem; } +h1 { font-size: 1.5rem; } +h2 { font-size: 1.25rem; } +h3 { font-size: 1.1rem; } +h4 { font-size: 1rem; } p { margin-bottom: 1rem; @@ -322,8 +340,17 @@ a:focus { } .btn:focus { - outline: 2px solid var(--focus-color); - outline-offset: 2px; + outline: var(--focus-outline); + outline-offset: var(--focus-offset); +} + +.btn:focus:not(:focus-visible) { + outline: none; +} + +.btn:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-offset); } /* Articles and Pages */ @@ -732,19 +759,25 @@ li { margin-bottom: 0.5rem; } -/* Focus styles for accessibility */ -*:focus { - outline: 2px solid var(--focus-color); - outline-offset: 2px; +/* Focus styles for accessibility - only show for keyboard navigation */ +*:focus:not(:focus-visible) { + outline: none; } -button:focus, -a:focus, -input:focus, -select:focus, -textarea:focus { - outline: 2px solid var(--focus-color); - outline-offset: 2px; +*:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-offset); + border-radius: 2px; +} + +button:focus-visible, +a:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-offset); + border-radius: 2px; } /* Contact Form Styles */ @@ -789,7 +822,13 @@ textarea:focus { .form-group input[type="text"]:focus, .form-group textarea:focus { border-color: var(--focus-color); - outline: none; +} + +.form-group input[type="text"]:focus-visible, +.form-group textarea:focus-visible { + border-color: var(--focus-color); + outline: var(--focus-outline); + outline-offset: var(--focus-offset); } .form-group textarea { @@ -972,6 +1011,55 @@ textarea:focus { flex-shrink: 0; } +/* User Badge Styles */ +.user-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} + +.user-badge-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.user-badge-avatar-placeholder { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--bg-primary); + border: 1px solid var(--border-color); + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.user-badge-avatar-placeholder .icon-inline { + width: 14px; + height: 14px; + margin: 0; + color: var(--text-secondary); +} + +.user-badge-name { + font-weight: 500; + color: var(--text-primary); + font-size: 0.9rem; +} + +.user-badge-handle { + color: var(--text-secondary); + font-size: 0.85rem; +} + /* Ensure icons align properly in flex containers */ a, button, span, p, h1, h2, h3, h4, h5, h6, li { display: inline-flex; diff --git a/templates/base.html b/templates/base.html index da8f756..1253ab2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -67,17 +67,7 @@