🌿 Collaborative wiki on ATProto lichen.wiki
atproto
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rewrite theme constants

juprodh 11507bcb d3c61ac1

+59 -84
+1 -1
src/views/bookmark.ts
··· 29 29 <input type="hidden" name="wikiAtUri" value="${escapeHtml(wikiAtUri)}"> 30 30 <input type="hidden" name="action" value="${action}"> 31 31 <button type="submit" 32 - class="inline-flex items-center gap-1.5 px-2 py-1 text-sm ${THEME.textMuted} ${THEME.accentLightHoverBg} rounded" 32 + class="inline-flex items-center gap-1.5 px-2 py-1 text-sm ${THEME.textMuted} ${THEME.accentSoft} rounded" 33 33 title="${escapeHtml(label)}"> 34 34 <svg class="w-4 h-4 ${fillClass}" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 35 35 <path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" />
+1 -1
src/views/explore.ts
··· 34 34 msg.home.explorePublicWikis, 35 35 ` 36 36 <div class="max-w-6xl mx-auto px-6 py-10"> 37 - <h1 class="text-2xl font-bold ${THEME.textPrimary} mb-6">${msg.home.explorePublicWikis}</h1> 37 + <h1 class="text-2xl font-bold ${THEME.text} mb-6">${msg.home.explorePublicWikis}</h1> 38 38 ${toolbar} 39 39 <div id="wiki-grid"> 40 40 ${gridHtml}
+1 -1
src/views/home.ts
··· 26 26 "Lichen", 27 27 ` 28 28 <section class="text-center py-8 px-6"> 29 - <p class="text-xl font-semibold ${THEME.textPrimary} mb-2">${msg.home.tagline}</p> 29 + <p class="text-xl font-semibold ${THEME.text} mb-2">${msg.home.tagline}</p> 30 30 <p class="text-sm ${THEME.textMuted} max-w-2xl mx-auto mb-6">${msg.home.heroDescription}</p> 31 31 <div class="flex flex-col items-center gap-1 mb-6 text-sm ${THEME.textSecondary}"> 32 32 <span><span class="font-semibold ${THEME.accentText}">1.</span> <a href="/login" class="hover:underline">${msg.home.step1}</a></span>
+14 -14
src/views/layout.ts
··· 54 54 const noteLinks = notes 55 55 .map((n) => { 56 56 const isCurrent = n.slug === options.currentNoteSlug; 57 - return `<li><a href="/wiki/${options.wikiSlug}/${n.slug}" class="${isCurrent ? `font-bold ${THEME.accentDarkText}` : `${THEME.textSecondary} ${THEME.accentHoverText}`} block truncate text-sm">${escapeHtml(n.title)}</a></li>`; 57 + return `<li><a href="/wiki/${options.wikiSlug}/${n.slug}" class="${isCurrent ? `font-bold text-teal-800` : `${THEME.textSecondary} hover:text-teal-700`} block truncate text-sm">${escapeHtml(n.title)}</a></li>`; 58 58 }) 59 59 .join("\n"); 60 60 ··· 69 69 ? `<a href="/wiki/${options.wikiSlug}/new" class="block mb-2 ${outlineSmallButtonClass} text-center">${msg.wiki.newNote}</a>` 70 70 : ""; 71 71 72 - const dropdownItemClass = `block px-3 py-1.5 text-sm ${THEME.textSecondary} ${THEME.accentLightHoverBg} rounded`; 72 + const dropdownItemClass = `block px-3 py-1.5 text-sm ${THEME.textSecondary} ${THEME.accentSoft} rounded`; 73 73 const noteExportItem = options.currentNoteSlug 74 74 ? `<a href="${exportNoteUrl(options.wikiSlug ?? "", options.currentNoteSlug)}" class="${dropdownItemClass}">${msg.wiki.exportNote}</a>` 75 75 : ""; ··· 80 80 ${ICONS.download} 81 81 ${msg.wiki.export} 82 82 </button> 83 - <div id="export-menu" class="hidden absolute left-0 bottom-full mb-1 ${THEME.bgSurface} border ${THEME.borderDefault} rounded-lg shadow-lg py-1.5 z-20 w-max"> 83 + <div id="export-menu" class="hidden absolute left-0 bottom-full mb-1 ${THEME.bgSurface} border ${THEME.border} rounded-lg shadow-lg py-1.5 z-20 w-max"> 84 84 ${noteExportItem} 85 85 ${wikiExportItem} 86 86 </div> ··· 136 136 137 137 const session = options?.session; 138 138 139 - const navBtnClass = `flex items-center justify-center gap-2 h-9 px-2.5 border ${THEME.borderDefault} rounded-lg ${THEME.bgSurface} ${THEME.textSecondary} ${THEME.textSecondaryHover} ${THEME.accentLightHoverBg} cursor-pointer text-sm`; 140 - const dropdownClass = `hidden absolute right-0 mt-1 ${THEME.bgSurface} border ${THEME.borderDefault} rounded-lg shadow-lg py-1.5 z-20 w-max`; 141 - const dropdownItemClass = `flex items-center gap-2.5 px-4 py-2 text-sm ${THEME.textSecondary} ${THEME.accentLightHoverBg} rounded`; 139 + const navBtnClass = `flex items-center justify-center gap-2 h-9 px-2.5 border ${THEME.border} rounded-lg ${THEME.bgSurface} ${THEME.textSecondary} hover:text-stone-700 ${THEME.accentSoft} cursor-pointer text-sm`; 140 + const dropdownClass = `hidden absolute right-0 mt-1 ${THEME.bgSurface} border ${THEME.border} rounded-lg shadow-lg py-1.5 z-20 w-max`; 141 + const dropdownItemClass = `flex items-center gap-2.5 px-4 py-2 text-sm ${THEME.textSecondary} ${THEME.accentSoft} rounded`; 142 142 143 143 const localeItems = SUPPORTED_LOCALES.map( 144 144 (l) => 145 - `<a href="#" class="block px-3 py-2 text-sm ${l === locale ? `font-semibold ${THEME.accentText}` : THEME.textSecondary} ${THEME.accentLightHoverBg} rounded" onclick="event.preventDefault();fetch('/set-locale',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'locale=${l}'}).then(()=>location.reload())">${LOCALE_LABELS[l] ?? l.toUpperCase()}</a>`, 145 + `<a href="#" class="block px-3 py-2 text-sm ${l === locale ? `font-semibold ${THEME.accentText}` : THEME.textSecondary} ${THEME.accentSoft} rounded" onclick="event.preventDefault();fetch('/set-locale',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'locale=${l}'}).then(()=>location.reload())">${LOCALE_LABELS[l] ?? l.toUpperCase()}</a>`, 146 146 ).join("\n"); 147 147 const localePicker = `<div class="relative" id="locale-picker"> 148 148 <button type="button" onclick="document.getElementById('locale-menu').classList.toggle('hidden')" class="${navBtnClass}" aria-label="Change language"> ··· 196 196 <script src="/public/htmx.min.js" defer></script> 197 197 ${extraScripts} 198 198 </head> 199 - <body class="${THEME.bgPage} ${THEME.textPrimary} min-h-screen flex flex-col"> 200 - <nav class="shrink-0 sticky top-0 z-20 ${THEME.bgSurface} border-b ${THEME.borderDefault}"> 199 + <body class="${THEME.bg} ${THEME.text} min-h-screen flex flex-col"> 200 + <nav class="shrink-0 sticky top-0 z-20 ${THEME.bgSurface} border-b ${THEME.border}"> 201 201 <div class="max-w-6xl mx-auto px-6 py-3 flex items-center justify-between gap-4"> 202 202 <div class="flex items-center gap-2 min-w-0"> 203 203 <a href="/" class="flex items-center gap-2 shrink-0"> ··· 212 212 /> 213 213 <span class="text-lg font-semibold ${THEME.accentText}">Lichen</span> 214 214 </a> 215 - ${options?.wikiName ? `<span class="${THEME.textMuted}">/</span><a href="/wiki/${options.wikiSlug}" class="text-lg font-medium ${THEME.textSecondary} ${THEME.accentHoverText} truncate">${escapeHtml(options.wikiName)}</a>` : ""} 215 + ${options?.wikiName ? `<span class="${THEME.textMuted}">/</span><a href="/wiki/${options.wikiSlug}" class="text-lg font-medium ${THEME.textSecondary} hover:text-teal-700 truncate">${escapeHtml(options.wikiName)}</a>` : ""} 216 216 </div> 217 217 <div class="flex items-center gap-2 shrink-0"> 218 218 ${profileOrLogin} ··· 226 226 <div class="relative"> 227 227 <input id="search-input" type="text" placeholder="${msg.search.searchNotes}" autocomplete="off" 228 228 aria-label="${msg.search.searchNotes}" 229 - class="w-full px-3 py-2 pr-10 text-sm border ${THEME.borderInput} rounded-lg outline-none ${THEME.accentFocusBorder} focus:ring-2 ${THEME.accentSubtleRing}" 229 + class="w-full px-3 py-2 pr-10 text-sm border ${THEME.borderInput} rounded-lg outline-none focus:border-teal-600 focus:ring-2 focus:ring-teal-200" 230 230 hx-get="/search${options?.wikiSlug ? `?wiki=${options.wikiSlug}` : ""}" 231 231 hx-trigger="input changed delay:150ms" 232 232 hx-target="#search-results" ··· 276 276 ${ 277 277 options?.sidebarNotes 278 278 ? renderWithSidebar( 279 - `${options?.pageTitle ? `<div class="text-center pb-6 mb-6 border-b ${THEME.borderSubtle}"><h1 class="text-3xl font-bold ${THEME.textPrimary}">${escapeHtml(options.pageTitle)}</h1></div>` : ""}${body}`, 279 + `${options?.pageTitle ? `<div class="text-center pb-6 mb-6 border-b ${THEME.borderSubtle}"><h1 class="text-3xl font-bold ${THEME.text}">${escapeHtml(options.pageTitle)}</h1></div>` : ""}${body}`, 280 280 options, 281 281 ) 282 - : `<main class="w-full max-w-6xl mx-auto px-6 py-8">${options?.pageTitle ? `<div class="text-center pb-6 mb-6 border-b ${THEME.borderSubtle}"><h1 class="text-3xl font-bold ${THEME.textPrimary}">${escapeHtml(options.pageTitle)}</h1></div>` : ""}${body}</main>` 282 + : `<main class="w-full max-w-6xl mx-auto px-6 py-8">${options?.pageTitle ? `<div class="text-center pb-6 mb-6 border-b ${THEME.borderSubtle}"><h1 class="text-3xl font-bold ${THEME.text}">${escapeHtml(options.pageTitle)}</h1></div>` : ""}${body}</main>` 283 283 } 284 284 </div> 285 - <footer class="shrink-0 sticky bottom-0 z-10 ${THEME.bgPage} border-t ${THEME.borderSubtle} py-4 text-center text-xs ${THEME.textMuted}"> 285 + <footer class="shrink-0 sticky bottom-0 z-10 ${THEME.bg} border-t ${THEME.borderSubtle} py-4 text-center text-xs ${THEME.textMuted}"> 286 286 <a href="https://atproto.com" target="_blank" rel="noopener noreferrer" class="hover:underline">${msg.home.builtOnAtproto}</a> 287 287 <span class="mx-2">&middot;</span> 288 288 <a href="https://tangled.org/juprodh.bsky.social/lichen.wiki" target="_blank" rel="noopener noreferrer" class="hover:underline">${msg.home.sourceCode}</a>
+1 -1
src/views/login.ts
··· 18 18 msg.login.heading, 19 19 ` 20 20 <div class="max-w-sm mx-auto mt-12"> 21 - <h1 class="text-2xl font-bold ${THEME.textPrimary} mb-2">${msg.login.heading}</h1> 21 + <h1 class="text-2xl font-bold ${THEME.text} mb-2">${msg.login.heading}</h1> 22 22 <p class="text-sm ${THEME.textMuted} mb-6">${msg.login.description}</p> 23 23 ${errorHtml} 24 24 <form method="POST" action="/login" class="space-y-4">
+2 -2
src/views/new-note.ts
··· 53 53 `${msg.wiki.newNote} — ${wikiName}`, 54 54 scrollFixStyle + 55 55 ` 56 - <h1 class="text-3xl font-bold ${THEME.textPrimary} mb-6">${msg.wiki.newNote}</h1> 56 + <h1 class="text-3xl font-bold ${THEME.text} mb-6">${msg.wiki.newNote}</h1> 57 57 ${errorHtml} 58 - <div class="mb-6 pb-4 border-b ${THEME.borderDefault}"> 58 + <div class="mb-6 pb-4 border-b ${THEME.border}"> 59 59 <label class="block text-sm font-medium ${THEME.textSecondary} mb-1">${msg.editor.importFile}</label> 60 60 <input 61 61 type="file"
+1 -1
src/views/new-wiki.ts
··· 38 38 msg.createWiki.heading, 39 39 ` 40 40 <div class="max-w-lg mx-auto"> 41 - <h1 class="text-3xl font-bold ${THEME.textPrimary} mb-6">${msg.createWiki.heading}</h1> 41 + <h1 class="text-3xl font-bold ${THEME.text} mb-6">${msg.createWiki.heading}</h1> 42 42 ${errorHtml} 43 43 <form method="POST" action="/wiki/new" enctype="multipart/form-data" class="space-y-4"> 44 44 <div>
+4 -4
src/views/profile.ts
··· 36 36 </div>`; 37 37 38 38 const nameHtml = displayName 39 - ? `<h1 class="text-2xl font-bold ${THEME.textPrimary}">${displayName}</h1>` 39 + ? `<h1 class="text-2xl font-bold ${THEME.text}">${displayName}</h1>` 40 40 : ""; 41 41 42 42 const handleHtml = handle ··· 71 71 </div> 72 72 73 73 <section class="mb-10"> 74 - <h2 class="text-lg font-semibold ${THEME.textPrimary} mb-4">${msg.profile.myWikis}</h2> 74 + <h2 class="text-lg font-semibold ${THEME.text} mb-4">${msg.profile.myWikis}</h2> 75 75 ${ownedSection} 76 76 </section> 77 77 78 78 <section class="mb-10"> 79 - <h2 class="text-lg font-semibold ${THEME.textPrimary} mb-4">${msg.profile.collaborating}</h2> 79 + <h2 class="text-lg font-semibold ${THEME.text} mb-4">${msg.profile.collaborating}</h2> 80 80 ${collaboratingSection} 81 81 </section> 82 82 83 83 <section class="mb-10"> 84 - <h2 class="text-lg font-semibold ${THEME.textPrimary} mb-4">${msg.profile.bookmarks}</h2> 84 + <h2 class="text-lg font-semibold ${THEME.text} mb-4">${msg.profile.bookmarks}</h2> 85 85 ${bookmarkSection} 86 86 </section> 87 87 </div>
+2 -2
src/views/search-results.ts
··· 33 33 const snippetHtml = r.snippet 34 34 ? `<div class="text-xs ${THEME.textMuted} mt-0.5 line-clamp-2">${highlightMatch(r.snippet, query)}</div>` 35 35 : ""; 36 - return `<a href="/wiki/${r.wiki_slug}/${r.slug}" class="search-result block px-4 py-2.5 ${THEME.accentLightHoverBg} ${THEME.accentLightFocusBg} outline-none" data-search-result> 37 - <div class="text-sm font-medium ${THEME.textPrimary}">${highlightMatch(r.title, query)}</div> 36 + return `<a href="/wiki/${r.wiki_slug}/${r.slug}" class="search-result block px-4 py-2.5 ${THEME.accentSoft} focus:bg-teal-50 outline-none" data-search-result> 37 + <div class="text-sm font-medium ${THEME.text}">${highlightMatch(r.title, query)}</div> 38 38 ${snippetHtml} 39 39 </a>`; 40 40 })
+11 -11
src/views/settings.ts
··· 80 80 const roleCell = isOwner 81 81 ? `<span class="text-sm">${roleLabel("owner")}</span>` 82 82 : `<form method="POST" action="/wiki/${wikiSlug}/-/members/${encodeURIComponent(m.did)}/change-role" class="inline-flex items-center gap-1"> 83 - <select name="role" aria-label="${msg.access.role}" data-original="${m.role}" onchange="this.nextElementSibling.classList.toggle('hidden', this.value === this.dataset.original)" class="text-xs border ${THEME.borderDefault} rounded px-1 py-0.5"> 83 + <select name="role" aria-label="${msg.access.role}" data-original="${m.role}" onchange="this.nextElementSibling.classList.toggle('hidden', this.value === this.dataset.original)" class="text-xs border ${THEME.border} rounded px-1 py-0.5"> 84 84 ${renderRoleOptions(m.role, roleLabel)} 85 85 </select> 86 - <button type="submit" class="hidden p-1 ${THEME.accentBg} text-white rounded ${THEME.accentDarkHoverBg} cursor-pointer" title="${msg.access.saveRole}">${ICONS.check}</button> 86 + <button type="submit" class="hidden p-1 bg-teal-700 text-white rounded hover:bg-teal-800 cursor-pointer" title="${msg.access.saveRole}">${ICONS.check}</button> 87 87 </form>`; 88 88 const removeButton = isOwner 89 89 ? "" ··· 110 110 <td class="py-2 pr-4 text-sm ${THEME.textMuted}">${escapeHtml(r.created_at)}</td> 111 111 <td class="py-2 text-sm"> 112 112 <form method="POST" action="/wiki/${wikiSlug}/-/members/${encodeURIComponent(r.did)}/approve" class="inline-flex items-center gap-2"> 113 - <select name="role" aria-label="${msg.access.role}" class="text-xs border ${THEME.borderDefault} rounded px-1 py-0.5"> 113 + <select name="role" aria-label="${msg.access.role}" class="text-xs border ${THEME.border} rounded px-1 py-0.5"> 114 114 <option value="contributor">${roleLabel("contributor")}</option> 115 115 <option value="admin">${roleLabel("admin")}</option> 116 116 <option value="viewer">${roleLabel("viewer")}</option> ··· 132 132 <label class="text-xs font-medium ${THEME.textMuted}">${msg.access.addMemberLabel}</label> 133 133 <input type="text" name="did" required autocomplete="off" 134 134 placeholder="${msg.access.addMemberPlaceholder}" 135 - class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none ${THEME.accentInputFocusBorder} w-72" /> 135 + class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none focus:border-teal-500 w-72" /> 136 136 </div> 137 137 <div class="flex flex-col gap-1"> 138 138 <label class="text-xs font-medium ${THEME.textMuted}">${msg.access.role}</label> 139 - <select name="role" class="text-sm border ${THEME.borderInput} rounded px-2 py-1.5 focus:outline-none ${THEME.accentInputFocusBorder}"> 139 + <select name="role" class="text-sm border ${THEME.borderInput} rounded px-2 py-1.5 focus:outline-none focus:border-teal-500"> 140 140 <option value="contributor">${roleLabel("contributor")}</option> 141 141 <option value="admin">${roleLabel("admin")}</option> 142 142 <option value="viewer">${roleLabel("viewer")}</option> ··· 183 183 <div class="overflow-x-auto"> 184 184 <table class="w-full mb-6"> 185 185 <thead> 186 - <tr class="border-b ${THEME.borderDefault} text-left text-xs font-semibold ${THEME.textMuted} uppercase tracking-wide"> 186 + <tr class="border-b ${THEME.border} text-left text-xs font-semibold ${THEME.textMuted} uppercase tracking-wide"> 187 187 <th class="pb-2 pr-4">${msg.access.member}</th> 188 188 <th class="pb-2 pr-4">${msg.access.role}</th> 189 189 <th class="pb-2 pr-4">${msg.access.joined}</th> ··· 202 202 <div class="overflow-x-auto"> 203 203 <table class="w-full mb-6"> 204 204 <thead> 205 - <tr class="border-b ${THEME.borderDefault} text-left text-xs font-semibold ${THEME.textMuted} uppercase tracking-wide"> 205 + <tr class="border-b ${THEME.border} text-left text-xs font-semibold ${THEME.textMuted} uppercase tracking-wide"> 206 206 <th class="pb-2 pr-4">${msg.access.member}</th> 207 207 <th class="pb-2 pr-4">${msg.access.requested}</th> 208 208 <th class="pb-2"></th> ··· 227 227 const msg = t(locale as "en" | "fr"); 228 228 229 229 if (!isOwner) { 230 - return `<section class="mt-10 border ${THEME.borderDefault} rounded-lg p-6"> 230 + return `<section class="mt-10 border ${THEME.border} rounded-lg p-6"> 231 231 <h2 class="text-lg font-semibold ${THEME.textMuted} mb-2">${msg.settings.dangerZone}</h2> 232 232 <p class="text-sm ${THEME.textMuted}">${msg.settings.deleteWikiOwnerOnly}</p> 233 233 </section>`; ··· 244 244 ${fmt(msg.settings.typeToConfirm, { name: `<strong>${escapeHtml(wikiName)}</strong>` })} 245 245 </label> 246 246 <input type="text" name="confirm" autocomplete="off" 247 - class="block w-full max-w-sm px-3 py-1.5 text-sm border ${THEME.borderInput} rounded mb-3 focus:outline-none ${THEME.accentInputFocusBorder}" 247 + class="block w-full max-w-sm px-3 py-1.5 text-sm border ${THEME.borderInput} rounded mb-3 focus:outline-none focus:border-teal-500" 248 248 placeholder="${escapeHtml(wikiName)}" /> 249 249 <button type="submit" class="${dangerButtonClass}">${msg.settings.deleteWiki}</button> 250 250 </form> ··· 270 270 <div class="flex flex-col gap-1"> 271 271 <label class="text-xs font-medium ${THEME.textMuted}">${msg.settings.nameLabel}</label> 272 272 <input type="text" name="name" required autocomplete="off" value="${escapeHtml(wikiName)}" 273 - class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none ${THEME.accentInputFocusBorder}" /> 273 + class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none focus:border-teal-500" /> 274 274 </div> 275 275 <div class="flex flex-col gap-1"> 276 276 <label class="text-xs font-medium ${THEME.textMuted}">${msg.settings.descriptionLabel}</label> 277 277 <textarea name="description" rows="3" 278 - class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none ${THEME.accentInputFocusBorder}">${escapeHtml(wikiDescription)}</textarea> 278 + class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none focus:border-teal-500">${escapeHtml(wikiDescription)}</textarea> 279 279 </div> 280 280 <div> 281 281 <button type="submit" class="${primarySmallButtonClass}">${msg.settings.save}</button>
+16 -41
src/views/theme.ts
··· 1 1 import { escapeHtml } from "../lib/html.ts"; 2 2 3 3 export const THEME = { 4 - // Accent 5 - accentBg: "bg-teal-700", 6 - accentText: "text-teal-700", 7 - accentBorder: "border-teal-700", 8 - accentHoverText: "hover:text-teal-700", 9 - accentDarkHoverBg: "hover:bg-teal-800", 10 - accentDarkText: "text-teal-800", 11 - accentLightHoverBg: "hover:bg-teal-50", 12 - accentLightFocusBg: "focus:bg-teal-50", 13 - accentFocusRing: "focus:ring-teal-600", 14 - accentFocusBorder: "focus:border-teal-600", 15 - accentInputFocusBorder: "focus:border-teal-500", 16 - accentSubtleRing: "focus:ring-teal-200", 17 - 18 4 // Text 19 - textPrimary: "text-stone-900", 5 + text: "text-stone-900", 20 6 textSecondary: "text-stone-700", 21 7 textMuted: "text-stone-500", 22 - textSecondaryHover: "hover:text-stone-700", 23 8 24 9 // Backgrounds 25 - bgPage: "bg-stone-50", 10 + bg: "bg-stone-50", 26 11 bgSurface: "bg-white", 27 12 bgPlaceholder: "bg-stone-200", 28 13 29 14 // Borders 30 - borderDefault: "border-stone-200", 31 - borderInput: "border-stone-300", 15 + border: "border-stone-200", 32 16 borderSubtle: "border-stone-100", 17 + borderInput: "border-stone-300", 33 18 34 - // Status 35 - statusPublicBg: "bg-green-100", 36 - statusPublicText: "text-green-700", 37 - statusPrivateBg: "bg-yellow-100", 38 - statusPrivateText: "text-yellow-700", 19 + // Accent 20 + accentText: "text-teal-700", 21 + accentSoft: "hover:bg-teal-50", 39 22 40 23 // Error 41 24 errorBg: "bg-red-50", 42 25 errorBorder: "border-red-200", 43 26 errorText: "text-red-700", 44 - errorActionBg: "bg-red-700", 45 - errorActionDarkHoverBg: "hover:bg-red-800", 46 - 47 - // Success (banner/flash confirmations — uses the accent family on purpose) 48 - successBg: "bg-teal-50", 49 - successBorder: "border-teal-200", 50 - successText: "text-teal-800", 51 27 52 28 // Fonts 53 29 fontMono: "font-mono", 54 - fontSans: "", 55 30 } as const; 56 31 57 - export const inputClass = `w-full border ${THEME.borderInput} rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 ${THEME.accentFocusRing}`; 32 + export const inputClass = `w-full border ${THEME.borderInput} rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-teal-600`; 58 33 59 - export const primaryButtonClass = `px-4 py-2 ${THEME.accentBg} text-white text-sm font-medium rounded ${THEME.accentDarkHoverBg}`; 34 + export const primaryButtonClass = `px-4 py-2 bg-teal-700 text-white text-sm font-medium rounded hover:bg-teal-800`; 60 35 61 - export const primarySmallButtonClass = `px-3 py-1.5 ${THEME.accentBg} text-white text-sm font-medium rounded ${THEME.accentDarkHoverBg}`; 36 + export const primarySmallButtonClass = `px-3 py-1.5 bg-teal-700 text-white text-sm font-medium rounded hover:bg-teal-800`; 62 37 63 - export const outlineSmallButtonClass = `px-3 py-1.5 border ${THEME.accentBorder} ${THEME.accentText} text-sm font-medium rounded ${THEME.accentLightHoverBg}`; 38 + export const outlineSmallButtonClass = `px-3 py-1.5 border border-teal-700 ${THEME.accentText} text-sm font-medium rounded ${THEME.accentSoft}`; 64 39 65 - export const dangerButtonClass = `px-4 py-2 ${THEME.errorActionBg} text-white text-sm font-medium rounded ${THEME.errorActionDarkHoverBg}`; 40 + export const dangerButtonClass = `px-4 py-2 bg-red-700 text-white text-sm font-medium rounded hover:bg-red-800`; 66 41 67 - export const dangerSmallButtonClass = `px-2.5 py-1 ${THEME.errorActionBg} text-white text-xs font-medium rounded ${THEME.errorActionDarkHoverBg}`; 42 + export const dangerSmallButtonClass = `px-2.5 py-1 bg-red-700 text-white text-xs font-medium rounded hover:bg-red-800`; 68 43 69 - export const sidebarButtonClass = `w-full flex items-center gap-2 px-3 py-1.5 text-sm border ${THEME.borderDefault} rounded-lg ${THEME.bgSurface} ${THEME.textMuted} ${THEME.accentLightHoverBg} cursor-pointer`; 44 + export const sidebarButtonClass = `w-full flex items-center gap-2 px-3 py-1.5 text-sm border ${THEME.border} rounded-lg ${THEME.bgSurface} ${THEME.textMuted} ${THEME.accentSoft} cursor-pointer`; 70 45 71 - export const sidebarLinkClass = `flex items-center gap-2 px-3 py-1.5 text-sm ${THEME.textSecondary} ${THEME.accentLightHoverBg} rounded-lg`; 46 + export const sidebarLinkClass = `flex items-center gap-2 px-3 py-1.5 text-sm ${THEME.textSecondary} ${THEME.accentSoft} rounded-lg`; 72 47 73 48 export const kbdClass = `text-xs ${THEME.bgPlaceholder} px-1.5 py-0.5 rounded ${THEME.textMuted}`; 74 49 ··· 79 54 80 55 export function successBanner(message: string | undefined): string { 81 56 if (!message) return ""; 82 - return `<div class="mb-4 p-3 ${THEME.successBg} border ${THEME.successBorder} ${THEME.successText} text-sm rounded">${escapeHtml(message)}</div>`; 57 + return `<div class="mb-4 p-3 bg-teal-50 border border-teal-200 text-teal-800 text-sm rounded">${escapeHtml(message)}</div>`; 83 58 } 84 59 85 60 export const THEME_HEX = {
+1 -1
src/views/wiki-card.ts
··· 33 33 ? `<div class="flex items-center gap-1.5 min-w-0 max-w-[50%] justify-end">${ownerHandle}${ownerAvatar}</div>` 34 34 : ""; 35 35 36 - return `<a href="/wiki/${wiki.slug}" class="flex flex-col h-full ${THEME.bgSurface} border ${THEME.borderDefault} rounded-lg p-4 hover:shadow-md transition-shadow"> 36 + return `<a href="/wiki/${wiki.slug}" class="flex flex-col h-full ${THEME.bgSurface} border ${THEME.border} rounded-lg p-4 hover:shadow-md transition-shadow"> 37 37 <div class="flex items-start justify-between gap-2"> 38 38 <h3 class="font-semibold ${THEME.accentText} truncate">${escapeHtml(wiki.name)}</h3> 39 39 ${languageBadge}
+4 -4
src/views/wiki-list.ts
··· 22 22 const { sort, gridTargetId, limit } = options; 23 23 24 24 const sortDropdown = `<select id="sort-select" name="sort" 25 - class="px-2 py-1.5 text-sm border ${THEME.borderDefault} rounded-lg outline-none ${THEME.accentInputFocusBorder} focus:ring-1 ${THEME.accentSubtleRing} ${THEME.bgSurface} shrink-0" 25 + class="px-2 py-1.5 text-sm border ${THEME.border} rounded-lg outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-200 ${THEME.bgSurface} shrink-0" 26 26 onchange="htmx.trigger(document.getElementById('wiki-search'), 'filterChange')"> 27 27 <option value="updated"${sort === "updated" ? " selected" : ""}>${msg.home.recentlyUpdated}</option> 28 28 <option value="created"${sort === "created" ? " selected" : ""}>${msg.home.recentlyCreated}</option> ··· 31 31 const searchInput = `<div class="relative flex-1 min-w-0"> 32 32 <input id="wiki-search" type="text" placeholder="${msg.search.searchWikis}" autocomplete="off" 33 33 aria-label="${msg.search.searchWikis}" 34 - class="w-full px-3 py-1.5 pr-8 text-sm border ${THEME.borderDefault} rounded-lg outline-none ${THEME.accentInputFocusBorder} focus:ring-1 ${THEME.accentSubtleRing} ${THEME.bgSurface}" 34 + class="w-full px-3 py-1.5 pr-8 text-sm border ${THEME.border} rounded-lg outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-200 ${THEME.bgSurface}" 35 35 hx-get="/search" hx-trigger="input changed delay:150ms, filterChange" hx-target="#${gridTargetId}" name="q" 36 36 hx-include="#lang-select, #sort-select, #limit-input" 37 37 hx-indicator="#wiki-search-spinner"> ··· 64 64 .join("\n"); 65 65 66 66 return `<select id="lang-select" name="lang" 67 - class="px-2 py-1.5 text-sm border ${THEME.borderDefault} rounded-lg outline-none ${THEME.accentInputFocusBorder} focus:ring-1 ${THEME.accentSubtleRing} ${THEME.bgSurface} shrink-0" 67 + class="px-2 py-1.5 text-sm border ${THEME.border} rounded-lg outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-200 ${THEME.bgSurface} shrink-0" 68 68 onchange="htmx.trigger(document.getElementById('wiki-search'), 'filterChange')"> 69 69 <option value="">${msg.home.allLanguages}</option> 70 70 ${options} ··· 118 118 const hasNext = page < totalPages; 119 119 120 120 const btnBase = `px-3 py-1.5 text-sm font-medium rounded-lg border`; 121 - const btnEnabled = `${btnBase} ${THEME.borderDefault} ${THEME.textSecondary} ${THEME.accentLightHoverBg} cursor-pointer`; 121 + const btnEnabled = `${btnBase} ${THEME.border} ${THEME.textSecondary} ${THEME.accentSoft} cursor-pointer`; 122 122 const btnDisabled = `${btnBase} ${THEME.borderSubtle} ${THEME.textMuted} opacity-50 cursor-not-allowed`; 123 123 124 124 const prevBtn = hasPrev