🌿 Collaborative wiki on ATProto lichen.wiki
atproto
14
fork

Configure Feed

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

Update UI buttons

juprodh 456e92fc 2da4b2f2

+57 -36
+4 -5
src/lib/i18n/en.ts
··· 67 67 newNotePlaceholder: "# My Note Title\n\nWrite your content here...", 68 68 titlePlaceholder: "My Note Title", 69 69 importFile: "Import from file", 70 - importFileHelp: "Select a .md file to pre-fill the editor.", 70 + importFileBrowse: "Browse", 71 71 }, 72 72 createWiki: { 73 73 heading: "Create a Wiki", ··· 79 79 visibility: "Visibility", 80 80 privateWarning: 81 81 "Your wiki will be hidden on Lichen, but the underlying records on your PDS remain publicly accessible. Private data on ATProto is not yet available, fully private wikis will come when the protocol supports it.", 82 - create: "Create Wiki", 82 + create: "Create wiki", 83 83 cancel: "Cancel", 84 - importZip: "Import from zip (optional)", 85 - importZipHelp: 86 - "Upload a .zip of .md files (max 100). Obsidian vaults work.", 84 + importZip: "Import your existing .md files from a zip (optional)", 85 + importZipBrowse: "Browse", 87 86 }, 88 87 access: { 89 88 noAccess: "You don't have access to this wiki.",
+3 -4
src/lib/i18n/fr.ts
··· 68 68 newNotePlaceholder: "# Titre de ma note\n\nÉcrivez votre contenu ici...", 69 69 titlePlaceholder: "Titre de ma note", 70 70 importFile: "Importer depuis un fichier", 71 - importFileHelp: "Selectionnez un fichier .md pour pre-remplir l'editeur.", 71 + importFileBrowse: "Parcourir", 72 72 }, 73 73 createWiki: { 74 74 heading: "Créer un wiki", ··· 82 82 "Votre wiki sera masqué sur Lichen, mais les enregistrements sur votre PDS restent accessibles publiquement. Les données privées sur ATProto ne sont pas encore disponibles, les wikis entièrement privés arriveront quand le protocole le permettra.", 83 83 create: "Créer le wiki", 84 84 cancel: "Annuler", 85 - importZip: "Importer depuis un zip (optionnel)", 86 - importZipHelp: 87 - "Envoyez un .zip de fichiers .md (max 100). Les coffres Obsidian fonctionnent.", 85 + importZip: "Importez vos fichiers .md depuis un zip (optionnel)", 86 + importZipBrowse: "Parcourir", 88 87 }, 89 88 access: { 90 89 noAccess: "Vous n'avez pas accès à ce wiki.",
+2 -2
src/lib/i18n/index.ts
··· 68 68 newNotePlaceholder: string; 69 69 titlePlaceholder: string; 70 70 importFile: string; 71 - importFileHelp: string; 71 + importFileBrowse: string; 72 72 }; 73 73 createWiki: { 74 74 heading: string; ··· 82 82 create: string; 83 83 cancel: string; 84 84 importZip: string; 85 - importZipHelp: string; 85 + importZipBrowse: string; 86 86 }; 87 87 access: { 88 88 noAccess: string;
+1
src/views/icons.ts
··· 6 6 login: `<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>`, 7 7 logout: `<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>`, 8 8 check: `<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>`, 9 + download: `<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`, 9 10 } as const;
+10 -2
src/views/layout.ts
··· 1 1 import { type AccessLevel, canEdit, canManage } from "../lib/access.ts"; 2 2 import { escapeHtml } from "../lib/html.ts"; 3 3 import { type Locale, SUPPORTED_LOCALES, t } from "../lib/i18n/index.ts"; 4 - import { profileUrl } from "../lib/urls.ts"; 4 + import { exportUrl, profileUrl } from "../lib/urls.ts"; 5 5 import { ICONS } from "./icons.ts"; 6 6 import { 7 7 kbdClass, ··· 64 64 ? `<a href="/wiki/${options.wikiSlug}/new" class="block mb-2 ${outlineSmallButtonClass} text-center">${msg.wiki.newNote}</a>` 65 65 : ""; 66 66 67 + const exportLink = `<a href="${exportUrl(options.wikiSlug ?? "")}" class="${sidebarLinkClass}"> 68 + ${ICONS.download} 69 + ${msg.wiki.export} 70 + </a>`; 71 + 67 72 const adminLinks = canManage(level) 68 73 ? `<div class="pt-3 border-t ${THEME.borderSubtle} shrink-0"> 74 + ${exportLink} 69 75 <a href="/wiki/${options.wikiSlug}/-/settings" class="${sidebarLinkClass}"> 70 76 ${ICONS.settings} 71 77 ${msg.settings.heading} 72 78 </a> 73 79 </div>` 74 - : ""; 80 + : `<div class="pt-3 border-t ${THEME.borderSubtle} shrink-0"> 81 + ${exportLink} 82 + </div>`; 75 83 76 84 return `<div class="w-full max-w-6xl mx-auto px-6 py-8 flex flex-col md:flex-row md:flex-1 gap-8"> 77 85 <aside class="order-2 md:order-first w-full md:w-44 md:shrink-0 md:sticky md:top-0 md:max-h-screen md:py-4 flex flex-col">
+14 -4
src/views/new-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 { errorBanner, inputClass, primaryButtonClass, THEME } from "./theme.ts"; 4 + import { 5 + errorBanner, 6 + inputClass, 7 + outlineSmallButtonClass, 8 + primaryButtonClass, 9 + THEME, 10 + } from "./theme.ts"; 5 11 6 12 interface NewNoteOptions extends LayoutOptions { 7 13 error?: string; ··· 28 34 <h1 class="text-3xl font-bold ${THEME.textPrimary} mb-6">${msg.wiki.newNote}</h1> 29 35 ${errorHtml} 30 36 <div class="mb-6 pb-4 border-b ${THEME.borderDefault}"> 31 - <label for="import-file" class="block text-sm font-medium ${THEME.textSecondary} mb-1">${msg.editor.importFile}</label> 37 + <label class="block text-sm font-medium ${THEME.textSecondary} mb-1">${msg.editor.importFile}</label> 32 38 <input 33 39 type="file" 34 40 id="import-file" 35 41 accept=".md,.markdown,.txt" 36 - class="block w-full text-sm ${THEME.textSecondary} file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-medium file:${THEME.accentBg} file:text-white hover:file:opacity-90 file:cursor-pointer" 42 + class="hidden" 43 + onchange="document.getElementById('import-filename').textContent=this.files[0]?.name||''" 37 44 > 38 - <p class="text-xs ${THEME.textMuted} mt-1">${msg.editor.importFileHelp}</p> 45 + <div class="flex items-center gap-3"> 46 + <button type="button" onclick="document.getElementById('import-file').click()" class="${outlineSmallButtonClass}">${msg.editor.importFileBrowse}</button> 47 + <span id="import-filename" class="text-sm ${THEME.textMuted} truncate"></span> 48 + </div> 39 49 </div> 40 50 <form method="POST" action="/wiki/${wikiSlug}/new" class="space-y-4"> 41 51 <div>
+22 -12
src/views/new-wiki.ts
··· 2 2 import { escapeHtml } from "../lib/html.ts"; 3 3 import { t } from "../lib/i18n/index.ts"; 4 4 import { type LayoutOptions, layout } from "./layout.ts"; 5 - import { errorBanner, inputClass, primaryButtonClass, THEME } from "./theme.ts"; 5 + import { 6 + errorBanner, 7 + inputClass, 8 + outlineSmallButtonClass, 9 + primaryButtonClass, 10 + THEME, 11 + } from "./theme.ts"; 6 12 7 13 interface NewWikiOptions extends LayoutOptions { 8 14 error?: string; ··· 59 65 >${descriptionValue}</textarea> 60 66 </div> 61 67 <div> 68 + <label class="block text-sm font-medium ${THEME.textSecondary} mb-1">${msg.createWiki.importZip}</label> 69 + <input 70 + id="zipFile" 71 + name="zipFile" 72 + type="file" 73 + accept=".zip" 74 + class="hidden" 75 + onchange="document.getElementById('zip-filename').textContent=this.files[0]?.name||''" 76 + > 77 + <div class="flex items-center gap-3"> 78 + <button type="button" onclick="document.getElementById('zipFile').click()" class="${outlineSmallButtonClass}">${msg.createWiki.importZipBrowse}</button> 79 + <span id="zip-filename" class="text-sm ${THEME.textMuted} truncate"></span> 80 + </div> 81 + </div> 82 + <div> 62 83 <label for="language" class="block text-sm font-medium ${THEME.textSecondary} mb-1">${msg.createWiki.language}</label> 63 84 <select 64 85 id="language" ··· 85 106 <div id="private-warning" class="${selectedVis === "private" ? "" : "hidden "}mt-3 rounded-md border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800"> 86 107 ${msg.createWiki.privateWarning} 87 108 </div> 88 - </div> 89 - <div> 90 - <label for="zipFile" class="block text-sm font-medium ${THEME.textSecondary} mb-1">${msg.createWiki.importZip}</label> 91 - <input 92 - id="zipFile" 93 - name="zipFile" 94 - type="file" 95 - accept=".zip" 96 - class="block w-full text-sm ${THEME.textSecondary} file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-medium file:${THEME.accentBg} file:text-white hover:file:opacity-90 file:cursor-pointer" 97 - > 98 - <p class="text-xs ${THEME.textMuted} mt-1">${msg.createWiki.importZipHelp}</p> 99 109 </div> 100 110 <div class="flex items-center gap-3"> 101 111 <button
+1 -7
src/views/wiki.ts
··· 1 1 import { canEdit } from "../lib/access.ts"; 2 2 import { t } from "../lib/i18n/index.ts"; 3 - import { exportUrl } from "../lib/urls.ts"; 4 3 import { type LayoutOptions, layout } from "./layout.ts"; 5 4 import { primarySmallButtonClass, THEME } from "./theme.ts"; 6 5 ··· 36 35 ? `<a href="/wiki/${wikiSlug}/new" class="${primarySmallButtonClass}">${msg.wiki.newNote}</a>` 37 36 : ""; 38 37 39 - const exportButton = `<a href="${exportUrl(wikiSlug)}" class="text-sm ${THEME.textMuted} hover:underline">${msg.wiki.export}</a>`; 40 - 41 38 return layout( 42 39 wikiName, 43 40 ` ··· 45 42 <p class="${THEME.textMuted} mb-6">/${wikiSlug}</p> 46 43 <div class="flex items-center justify-between mb-3"> 47 44 <h2 class="text-lg font-semibold">${msg.wiki.notes}</h2> 48 - <div class="flex items-center gap-3"> 49 - ${exportButton} 50 - ${newNoteButton} 51 - </div> 45 + ${newNoteButton} 52 46 </div> 53 47 <ul class="space-y-2"> 54 48 ${noteList || `<li class="${THEME.textMuted}">${msg.wiki.noNotesYet}</li>`}