🌿 Collaborative wiki on ATProto lichen.wiki
atproto
14
fork

Configure Feed

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

Refactor theme file

juprodh 31b34ca4 11507bcb

+204 -119
+1 -1
public/editor/editor.ts
··· 1 1 import { markdown } from "@codemirror/lang-markdown"; 2 2 import { EditorState } from "@codemirror/state"; 3 3 import { basicSetup, EditorView } from "codemirror"; 4 - import { THEME_HEX } from "../../src/views/theme.ts"; 4 + import { THEME_HEX } from "../../src/views/theme/index.ts"; 5 5 import { renderPreview } from "./preview.ts"; 6 6 import { createToolbar } from "./toolbar.ts"; 7 7 import { showToast, syncBlobMetadata, uploadImage } from "./upload.ts";
+1 -1
public/editor/toolbar.ts
··· 1 1 import type { EditorView } from "codemirror"; 2 - import { THEME_HEX } from "../../src/views/theme.ts"; 2 + import { THEME_HEX } from "../../src/views/theme/index.ts"; 3 3 4 4 function wrapSelection(view: EditorView, before: string, after: string): void { 5 5 const { from, to } = view.state.selection.main;
+1 -1
public/viz/renderers/force-graph.ts
··· 1 1 declare const d3: typeof import("d3"); 2 2 3 3 import type { ForceGraphNode as BaseForceGraphNode } from "../../../src/shared/viz-types.ts"; 4 - import { THEME_HEX } from "../../../src/views/theme.ts"; 4 + import { THEME_HEX } from "../../../src/views/theme/index.ts"; 5 5 6 6 type ForceGraphNode = BaseForceGraphNode & d3.SimulationNodeDatum; 7 7
+1 -1
public/viz/renderers/sunburst.ts
··· 4 4 SunburstData, 5 5 SunburstNode, 6 6 } from "../../../src/shared/viz-types.ts"; 7 - import { THEME_HEX } from "../../../src/views/theme.ts"; 7 + import { THEME_HEX } from "../../../src/views/theme/index.ts"; 8 8 9 9 const COLORS = d3.schemeTableau10; 10 10
+1 -1
src/views/access-denied.ts
··· 1 1 import { escapeHtml } from "../lib/html.ts"; 2 2 import { t } from "../lib/i18n/index.ts"; 3 3 import { type LayoutOptions, layout } from "./layout.ts"; 4 - import { primaryButtonClass, THEME } from "./theme.ts"; 4 + import { primaryButtonClass, THEME } from "./theme/index.ts"; 5 5 6 6 interface AccessDeniedOptions extends LayoutOptions { 7 7 hasPendingRequest?: boolean;
+1 -1
src/views/bookmark.ts
··· 1 1 import { escapeHtml } from "../lib/html.ts"; 2 2 import type { Messages } from "../lib/i18n/index.ts"; 3 3 import { isBookmarked } from "../server/db/queries/index.ts"; 4 - import { THEME } from "./theme.ts"; 4 + import { THEME } from "./theme/index.ts"; 5 5 6 6 export function resolveBookmarkHtml( 7 7 did: string | null,
+1 -1
src/views/edit-note.ts
··· 1 1 import { escapeHtml } from "../lib/html.ts"; 2 2 import { t } from "../lib/i18n/index.ts"; 3 3 import { type LayoutOptions, layout } from "./layout.ts"; 4 - import { inputClass, primaryButtonClass, THEME } from "./theme.ts"; 4 + import { inputClass, primaryButtonClass, THEME } from "./theme/index.ts"; 5 5 6 6 export function editNotePage( 7 7 wikiName: string,
+1 -1
src/views/explore.ts
··· 5 5 WikiWithNoteCount, 6 6 } from "../server/db/queries/index.ts"; 7 7 import { type LayoutOptions, layout } from "./layout.ts"; 8 - import { THEME } from "./theme.ts"; 8 + import { THEME } from "./theme/index.ts"; 9 9 import { wikiGridWithPagination, wikiToolbar } from "./wiki-list.ts"; 10 10 11 11 export function explorePage(
+5 -1
src/views/home.ts
··· 3 3 import { profileUrl } from "../lib/urls.ts"; 4 4 import type { WikiWithNoteCount } from "../server/db/queries/index.ts"; 5 5 import { type LayoutOptions, layout } from "./layout.ts"; 6 - import { outlineSmallButtonClass, primaryButtonClass, THEME } from "./theme.ts"; 6 + import { 7 + outlineSmallButtonClass, 8 + primaryButtonClass, 9 + THEME, 10 + } from "./theme/index.ts"; 7 11 import { wikiGridWithExploreLink, wikiToolbar } from "./wiki-list.ts"; 8 12 9 13 export function homePage(
+8 -6
src/views/layout.ts
··· 10 10 sidebarButtonClass, 11 11 sidebarLinkClass, 12 12 THEME, 13 - } from "./theme.ts"; 13 + themeStyleAttr, 14 + themes, 15 + } from "./theme/index.ts"; 14 16 15 17 const LOCALE_LABELS: Record<string, string> = { 16 18 en: "English", ··· 54 56 const noteLinks = notes 55 57 .map((n) => { 56 58 const isCurrent = n.slug === options.currentNoteSlug; 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>`; 59 + return `<li><a href="/wiki/${options.wikiSlug}/${n.slug}" class="${isCurrent ? `font-bold text-[var(--accent-hover)]` : `${THEME.textSecondary} hover:text-[var(--accent)]`} block truncate text-sm">${escapeHtml(n.title)}</a></li>`; 58 60 }) 59 61 .join("\n"); 60 62 ··· 136 138 137 139 const session = options?.session; 138 140 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`; 141 + 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-[var(--text-secondary)] ${THEME.accentSoft} cursor-pointer text-sm`; 140 142 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 143 const dropdownItemClass = `flex items-center gap-2.5 px-4 py-2 text-sm ${THEME.textSecondary} ${THEME.accentSoft} rounded`; 142 144 ··· 196 198 <script src="/public/htmx.min.js" defer></script> 197 199 ${extraScripts} 198 200 </head> 199 - <body class="${THEME.bg} ${THEME.text} min-h-screen flex flex-col"> 201 + <body style="${themeStyleAttr(themes.light)}" class="${THEME.bg} ${THEME.text} min-h-screen flex flex-col"> 200 202 <nav class="shrink-0 sticky top-0 z-20 ${THEME.bgSurface} border-b ${THEME.border}"> 201 203 <div class="max-w-6xl mx-auto px-6 py-3 flex items-center justify-between gap-4"> 202 204 <div class="flex items-center gap-2 min-w-0"> ··· 212 214 /> 213 215 <span class="text-lg font-semibold ${THEME.accentText}">Lichen</span> 214 216 </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>` : ""} 217 + ${options?.wikiName ? `<span class="${THEME.textMuted}">/</span><a href="/wiki/${options.wikiSlug}" class="text-lg font-medium ${THEME.textSecondary} hover:text-[var(--accent)] truncate">${escapeHtml(options.wikiName)}</a>` : ""} 216 218 </div> 217 219 <div class="flex items-center gap-2 shrink-0"> 218 220 ${profileOrLogin} ··· 226 228 <div class="relative"> 227 229 <input id="search-input" type="text" placeholder="${msg.search.searchNotes}" autocomplete="off" 228 230 aria-label="${msg.search.searchNotes}" 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" 231 + class="w-full px-3 py-2 pr-10 text-sm border ${THEME.borderInput} rounded-lg outline-none focus:border-[var(--accent-focus)] focus:ring-2 focus:ring-[var(--accent-soft-border)]" 230 232 hx-get="/search${options?.wikiSlug ? `?wiki=${options.wikiSlug}` : ""}" 231 233 hx-trigger="input changed delay:150ms" 232 234 hx-target="#search-results"
+6 -1
src/views/login.ts
··· 1 1 import { t } from "../lib/i18n/index.ts"; 2 2 import { type LayoutOptions, layout } from "./layout.ts"; 3 - import { errorBanner, inputClass, primaryButtonClass, THEME } from "./theme.ts"; 3 + import { 4 + errorBanner, 5 + inputClass, 6 + primaryButtonClass, 7 + THEME, 8 + } from "./theme/index.ts"; 4 9 5 10 interface LoginOptions extends LayoutOptions { 6 11 error?: string;
+1 -1
src/views/new-note.ts
··· 7 7 outlineSmallButtonClass, 8 8 primaryButtonClass, 9 9 THEME, 10 - } from "./theme.ts"; 10 + } from "./theme/index.ts"; 11 11 12 12 interface NewNoteOptions extends LayoutOptions { 13 13 error?: string;
+1 -1
src/views/new-wiki.ts
··· 8 8 outlineSmallButtonClass, 9 9 primaryButtonClass, 10 10 THEME, 11 - } from "./theme.ts"; 11 + } from "./theme/index.ts"; 12 12 13 13 interface NewWikiOptions extends LayoutOptions { 14 14 error?: string;
+1 -1
src/views/profile.ts
··· 3 3 import type { ProfileInfo } from "../lib/profile.ts"; 4 4 import type { WikiWithNoteCount } from "../server/db/queries/index.ts"; 5 5 import { type LayoutOptions, layout } from "./layout.ts"; 6 - import { THEME } from "./theme.ts"; 6 + import { THEME } from "./theme/index.ts"; 7 7 import { wikiGridCards } from "./wiki-list.ts"; 8 8 9 9 export function profilePage(
+2 -2
src/views/search-results.ts
··· 1 1 import { escapeHtml } from "../lib/html.ts"; 2 2 import { fmt, type Locale, t } from "../lib/i18n/index.ts"; 3 3 import type { NoteSearchResult } from "../server/db/queries/index.ts"; 4 - import { THEME } from "./theme.ts"; 4 + import { THEME } from "./theme/index.ts"; 5 5 6 6 function highlightMatch(text: string, query: string): string { 7 7 const escaped = escapeHtml(text); ··· 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.accentSoft} focus:bg-teal-50 outline-none" data-search-result> 36 + return `<a href="/wiki/${r.wiki_slug}/${r.slug}" class="search-result block px-4 py-2.5 ${THEME.accentSoft} focus:bg-[var(--accent-soft)] outline-none" data-search-result> 37 37 <div class="text-sm font-medium ${THEME.text}">${highlightMatch(r.title, query)}</div> 38 38 ${snippetHtml} 39 39 </a>`;
+7 -7
src/views/settings.ts
··· 11 11 primarySmallButtonClass, 12 12 successBanner, 13 13 THEME, 14 - } from "./theme.ts"; 14 + } from "./theme/index.ts"; 15 15 16 16 interface SettingsPageOptions extends LayoutOptions { 17 17 wikiDid: string; ··· 83 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 bg-teal-700 text-white rounded hover:bg-teal-800 cursor-pointer" title="${msg.access.saveRole}">${ICONS.check}</button> 86 + <button type="submit" class="hidden p-1 bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] cursor-pointer" title="${msg.access.saveRole}">${ICONS.check}</button> 87 87 </form>`; 88 88 const removeButton = isOwner 89 89 ? "" ··· 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 focus:border-teal-500 w-72" /> 135 + class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none focus:border-[var(--accent-focus-input)] 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 focus:border-teal-500"> 139 + <select name="role" class="text-sm border ${THEME.borderInput} rounded px-2 py-1.5 focus:outline-none focus:border-[var(--accent-focus-input)]"> 140 140 <option value="contributor">${roleLabel("contributor")}</option> 141 141 <option value="admin">${roleLabel("admin")}</option> 142 142 <option value="viewer">${roleLabel("viewer")}</option> ··· 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 focus:border-teal-500" 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-[var(--accent-focus-input)]" 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 focus:border-teal-500" /> 273 + class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none focus:border-[var(--accent-focus-input)]" /> 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 focus:border-teal-500">${escapeHtml(wikiDescription)}</textarea> 278 + class="px-3 py-1.5 text-sm border ${THEME.borderInput} rounded focus:outline-none focus:border-[var(--accent-focus-input)]">${escapeHtml(wikiDescription)}</textarea> 279 279 </div> 280 280 <div> 281 281 <button type="submit" class="${primarySmallButtonClass}">${msg.settings.save}</button>
+1 -1
src/views/share.ts
··· 1 1 import { escapeHtml } from "../lib/html.ts"; 2 2 import type { Messages } from "../lib/i18n/index.ts"; 3 - import { sidebarLinkClass } from "./theme.ts"; 3 + import { sidebarLinkClass } from "./theme/index.ts"; 4 4 5 5 export function shareOnBlueskyButton( 6 6 noteTitle: string,
-84
src/views/theme.ts
··· 1 - import { escapeHtml } from "../lib/html.ts"; 2 - 3 - export const THEME = { 4 - // Text 5 - text: "text-stone-900", 6 - textSecondary: "text-stone-700", 7 - textMuted: "text-stone-500", 8 - 9 - // Backgrounds 10 - bg: "bg-stone-50", 11 - bgSurface: "bg-white", 12 - bgPlaceholder: "bg-stone-200", 13 - 14 - // Borders 15 - border: "border-stone-200", 16 - borderSubtle: "border-stone-100", 17 - borderInput: "border-stone-300", 18 - 19 - // Accent 20 - accentText: "text-teal-700", 21 - accentSoft: "hover:bg-teal-50", 22 - 23 - // Error 24 - errorBg: "bg-red-50", 25 - errorBorder: "border-red-200", 26 - errorText: "text-red-700", 27 - 28 - // Fonts 29 - fontMono: "font-mono", 30 - } as const; 31 - 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`; 33 - 34 - export const primaryButtonClass = `px-4 py-2 bg-teal-700 text-white text-sm font-medium rounded hover:bg-teal-800`; 35 - 36 - export const primarySmallButtonClass = `px-3 py-1.5 bg-teal-700 text-white text-sm font-medium rounded hover:bg-teal-800`; 37 - 38 - export const outlineSmallButtonClass = `px-3 py-1.5 border border-teal-700 ${THEME.accentText} text-sm font-medium rounded ${THEME.accentSoft}`; 39 - 40 - export const dangerButtonClass = `px-4 py-2 bg-red-700 text-white text-sm font-medium rounded hover:bg-red-800`; 41 - 42 - export const dangerSmallButtonClass = `px-2.5 py-1 bg-red-700 text-white text-xs font-medium rounded hover:bg-red-800`; 43 - 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`; 45 - 46 - export const sidebarLinkClass = `flex items-center gap-2 px-3 py-1.5 text-sm ${THEME.textSecondary} ${THEME.accentSoft} rounded-lg`; 47 - 48 - export const kbdClass = `text-xs ${THEME.bgPlaceholder} px-1.5 py-0.5 rounded ${THEME.textMuted}`; 49 - 50 - export function errorBanner(message: string | undefined): string { 51 - if (!message) return ""; 52 - return `<div class="mb-4 p-3 ${THEME.errorBg} border ${THEME.errorBorder} ${THEME.errorText} text-sm rounded">${escapeHtml(message)}</div>`; 53 - } 54 - 55 - export function successBanner(message: string | undefined): string { 56 - if (!message) return ""; 57 - return `<div class="mb-4 p-3 bg-teal-50 border border-teal-200 text-teal-800 text-sm rounded">${escapeHtml(message)}</div>`; 58 - } 59 - 60 - export const THEME_HEX = { 61 - // Editor 62 - toolbarBg: "#f5f5f4", 63 - toolbarBorder: "#d6d3d1", 64 - buttonBg: "#ffffff", 65 - buttonBorder: "#d6d3d1", 66 - paneBorder: "#d6d3d1", 67 - previewBg: "#ffffff", 68 - 69 - // Viz 70 - vizText: "#44403c", 71 - graphLinkStroke: "#a8a29e", 72 - graphNodeStroke: "#ffffff", 73 - graphDefaultColor: "#0f766e", 74 - graphGroupColors: [ 75 - "#0f766e", 76 - "#b45309", 77 - "#047857", 78 - "#dc2626", 79 - "#7c3aed", 80 - "#0e7490", 81 - "#c2410c", 82 - "#be185d", 83 - ], 84 - } as const;
+10
src/views/theme/apply.ts
··· 1 + import type { Theme } from "./themes.ts"; 2 + 3 + const kebab = (s: string): string => 4 + s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); 5 + 6 + export function themeStyleAttr(theme: Theme): string { 7 + return Object.entries(theme) 8 + .map(([k, v]) => `--${kebab(k)}: ${v}`) 9 + .join("; "); 10 + }
+17
src/views/theme/index.ts
··· 1 + export { themeStyleAttr } from "./apply.ts"; 2 + export { themes } from "./themes.ts"; 3 + export { 4 + dangerButtonClass, 5 + dangerSmallButtonClass, 6 + errorBanner, 7 + inputClass, 8 + kbdClass, 9 + outlineSmallButtonClass, 10 + primaryButtonClass, 11 + primarySmallButtonClass, 12 + sidebarButtonClass, 13 + sidebarLinkClass, 14 + successBanner, 15 + THEME, 16 + THEME_HEX, 17 + } from "./tokens.ts";
+47
src/views/theme/themes.ts
··· 1 + export type Theme = { 2 + text: string; 3 + textSecondary: string; 4 + textMuted: string; 5 + bg: string; 6 + surface: string; 7 + placeholder: string; 8 + border: string; 9 + borderSubtle: string; 10 + borderInput: string; 11 + accent: string; 12 + accentHover: string; 13 + accentSoft: string; 14 + accentSoftBorder: string; 15 + accentFocus: string; 16 + accentFocusInput: string; 17 + dangerBg: string; 18 + dangerBorder: string; 19 + dangerText: string; 20 + dangerAction: string; 21 + dangerActionHover: string; 22 + }; 23 + 24 + export const themes = { 25 + light: { 26 + text: "#1c1917", 27 + textSecondary: "#44403c", 28 + textMuted: "#78716c", 29 + bg: "#fafaf9", 30 + surface: "#ffffff", 31 + placeholder: "#e7e5e4", 32 + border: "#e7e5e4", 33 + borderSubtle: "#f5f5f4", 34 + borderInput: "#d6d3d1", 35 + accent: "#0f766e", 36 + accentHover: "#115e59", 37 + accentSoft: "#f0fdfa", 38 + accentSoftBorder: "#99f6e4", 39 + accentFocus: "#0d9488", 40 + accentFocusInput: "#14b8a6", 41 + dangerBg: "#fef2f2", 42 + dangerBorder: "#fecaca", 43 + dangerText: "#b91c1c", 44 + dangerAction: "#b91c1c", 45 + dangerActionHover: "#991b1b", 46 + }, 47 + } as const satisfies Record<string, Theme>;
+84
src/views/theme/tokens.ts
··· 1 + import { escapeHtml } from "../../lib/html.ts"; 2 + 3 + export const THEME = { 4 + // Text 5 + text: "text-[var(--text)]", 6 + textSecondary: "text-[var(--text-secondary)]", 7 + textMuted: "text-[var(--text-muted)]", 8 + 9 + // Backgrounds 10 + bg: "bg-[var(--bg)]", 11 + bgSurface: "bg-[var(--surface)]", 12 + bgPlaceholder: "bg-[var(--placeholder)]", 13 + 14 + // Borders 15 + border: "border-[var(--border)]", 16 + borderSubtle: "border-[var(--border-subtle)]", 17 + borderInput: "border-[var(--border-input)]", 18 + 19 + // Accent 20 + accentText: "text-[var(--accent)]", 21 + accentSoft: "hover:bg-[var(--accent-soft)]", 22 + 23 + // Error 24 + errorBg: "bg-[var(--danger-bg)]", 25 + errorBorder: "border-[var(--danger-border)]", 26 + errorText: "text-[var(--danger-text)]", 27 + 28 + // Fonts 29 + fontMono: "font-mono", 30 + } as const; 31 + 32 + export const inputClass = `w-full border ${THEME.borderInput} rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent-focus)]`; 33 + 34 + export const primaryButtonClass = `px-4 py-2 bg-[var(--accent)] text-white text-sm font-medium rounded hover:bg-[var(--accent-hover)]`; 35 + 36 + export const primarySmallButtonClass = `px-3 py-1.5 bg-[var(--accent)] text-white text-sm font-medium rounded hover:bg-[var(--accent-hover)]`; 37 + 38 + export const outlineSmallButtonClass = `px-3 py-1.5 border border-[var(--accent)] ${THEME.accentText} text-sm font-medium rounded ${THEME.accentSoft}`; 39 + 40 + export const dangerButtonClass = `px-4 py-2 bg-[var(--danger-action)] text-white text-sm font-medium rounded hover:bg-[var(--danger-action-hover)]`; 41 + 42 + export const dangerSmallButtonClass = `px-2.5 py-1 bg-[var(--danger-action)] text-white text-xs font-medium rounded hover:bg-[var(--danger-action-hover)]`; 43 + 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`; 45 + 46 + export const sidebarLinkClass = `flex items-center gap-2 px-3 py-1.5 text-sm ${THEME.textSecondary} ${THEME.accentSoft} rounded-lg`; 47 + 48 + export const kbdClass = `text-xs ${THEME.bgPlaceholder} px-1.5 py-0.5 rounded ${THEME.textMuted}`; 49 + 50 + export function errorBanner(message: string | undefined): string { 51 + if (!message) return ""; 52 + return `<div class="mb-4 p-3 ${THEME.errorBg} border ${THEME.errorBorder} ${THEME.errorText} text-sm rounded">${escapeHtml(message)}</div>`; 53 + } 54 + 55 + export function successBanner(message: string | undefined): string { 56 + if (!message) return ""; 57 + return `<div class="mb-4 p-3 bg-[var(--accent-soft)] border border-[var(--accent-soft-border)] text-[var(--accent-hover)] text-sm rounded">${escapeHtml(message)}</div>`; 58 + } 59 + 60 + export const THEME_HEX = { 61 + // Editor 62 + toolbarBg: "#f5f5f4", 63 + toolbarBorder: "#d6d3d1", 64 + buttonBg: "#ffffff", 65 + buttonBorder: "#d6d3d1", 66 + paneBorder: "#d6d3d1", 67 + previewBg: "#ffffff", 68 + 69 + // Viz 70 + vizText: "#44403c", 71 + graphLinkStroke: "#a8a29e", 72 + graphNodeStroke: "#ffffff", 73 + graphDefaultColor: "#0f766e", 74 + graphGroupColors: [ 75 + "#0f766e", 76 + "#b45309", 77 + "#047857", 78 + "#dc2626", 79 + "#7c3aed", 80 + "#0e7490", 81 + "#c2410c", 82 + "#be185d", 83 + ], 84 + } as const;
+1 -1
src/views/wiki-card.ts
··· 1 1 import { escapeHtml } from "../lib/html.ts"; 2 2 import { fmt, type Locale, t } from "../lib/i18n/index.ts"; 3 3 import type { WikiWithNoteCount } from "../server/db/queries/index.ts"; 4 - import { THEME } from "./theme.ts"; 4 + import { THEME } from "./theme/index.ts"; 5 5 6 6 export function wikiCard(wiki: WikiWithNoteCount, locale: Locale): string { 7 7 const msg = t(locale);
+4 -4
src/views/wiki-list.ts
··· 4 4 WikiSort, 5 5 WikiWithNoteCount, 6 6 } from "../server/db/queries/index.ts"; 7 - import { THEME } from "./theme.ts"; 7 + import { THEME } from "./theme/index.ts"; 8 8 import { wikiCard } from "./wiki-card.ts"; 9 9 10 10 interface WikiToolbarOptions { ··· 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.border} rounded-lg outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-200 ${THEME.bgSurface} shrink-0" 25 + class="px-2 py-1.5 text-sm border ${THEME.border} rounded-lg outline-none focus:border-[var(--accent-focus-input)] focus:ring-1 focus:ring-[var(--accent-soft-border)] ${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.border} rounded-lg outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-200 ${THEME.bgSurface}" 34 + class="w-full px-3 py-1.5 pr-8 text-sm border ${THEME.border} rounded-lg outline-none focus:border-[var(--accent-focus-input)] focus:ring-1 focus:ring-[var(--accent-soft-border)] ${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.border} rounded-lg outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-200 ${THEME.bgSurface} shrink-0" 67 + class="px-2 py-1.5 text-sm border ${THEME.border} rounded-lg outline-none focus:border-[var(--accent-focus-input)] focus:ring-1 focus:ring-[var(--accent-soft-border)] ${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}
+1 -1
src/views/wiki.ts
··· 1 1 import { canEdit } from "../lib/access.ts"; 2 2 import { t } from "../lib/i18n/index.ts"; 3 3 import { type LayoutOptions, layout } from "./layout.ts"; 4 - import { primarySmallButtonClass, THEME } from "./theme.ts"; 4 + import { primarySmallButtonClass, THEME } from "./theme/index.ts"; 5 5 6 6 export function wikiPage( 7 7 wikiName: string,