@ -1,34 +1,15 @@
< script lang = "ts" >
< script lang = "ts" >
import { Textarea , Button } from "flowbite-svelte";
import { Textarea , Toolbar , ToolbarGroup , ToolbarButton } from "flowbite-svelte";
import { EyeOutline } from "flowbite-svelte-icons";
import { Bold , Italic , Link2 , Image as ImageIcon , FilePlus , Eye } from "@lucide/svelte";
import {
import { extractSmartMetadata , parseAsciiDocWithMetadata , type AsciiDocMetadata , metadataToTags } from "$lib/utils/asciidoc_metadata";
extractSmartMetadata,
import asciidoctor from "asciidoctor";
parseAsciiDocWithMetadata,
import { AAlert } from "$lib/a";
type AsciiDocMetadata,
import { onMount } from 'svelte';
metadataToTags,
} from "$lib/utils/asciidoc_metadata";
import asciidoctor from "asciidoctor";
// Component props
// Component props
let {
let {
content = "",
content = "",
placeholder = `== Note Title
placeholder = `== Note Title\n:author: Your Name\n:version: 1.0\n:published_on: 2024-01-01\n:published_by: Alexandria\n:summary: A brief description of this note\n:tags: note, example, metadata\n:image: https://example.com/image.jpg\n\nnote content here...\n\n== Note Title 2\nSome Other Author (this works even if there is no :author: attribute)\n:keywords: second, note, example (keywords are converted to tags)\n:description: This is a description of the note (description is converted to a summary tag)\nNote content here...\n `,
:author: Your Name
:version: 1.0
:published_on: 2024-01-01
:published_by: Alexandria
:summary: A brief description of this note
:tags: note, example, metadata
:image: https://example.com/image.jpg
note content here...
== Note Title 2
Some Other Author (this weeks even if there is no :author: attribute)
:keywords: second, note, example (keywords are converted to tags)
:description: This is a description of the note (description is converted to a summary tag)
Note content here...
`,
showPreview = false,
showPreview = false,
onContentChange = (content: string) => {} ,
onContentChange = (content: string) => {} ,
onPreviewToggle = (show: boolean) => {} ,
onPreviewToggle = (show: boolean) => {} ,
@ -40,12 +21,61 @@ Note content here...
onPreviewToggle?: (show: boolean) => void;
onPreviewToggle?: (show: boolean) => void;
}>();
}>();
// --- New toolbar insertion helpers ---
let wrapper: HTMLElement | null = null;
function getTextarea(): HTMLTextAreaElement | null {
return wrapper?.querySelector('textarea') as HTMLTextAreaElement | null;
}
function insertMarkup(prefix: string, suffix: string) {
const ta = getTextarea();
if (!ta || ta.disabled) return;
const start = ta.selectionStart;
const end = ta.selectionEnd;
const selected = content.substring(start, end);
content = content.slice(0, start) + prefix + selected + suffix + content.slice(end);
onContentChange(content);
queueMicrotask(() => {
ta.focus();
const pos = start + prefix.length + selected.length + suffix.length;
ta.selectionStart = ta.selectionEnd = pos;
});
}
function insertPlain(text: string) {
const ta = getTextarea();
if (!ta || ta.disabled) return;
const start = ta.selectionStart;
const end = ta.selectionEnd;
content = content.slice(0, start) + text + content.slice(end);
onContentChange(content);
queueMicrotask(() => {
ta.focus();
const pos = start + text.length;
ta.selectionStart = ta.selectionEnd = pos;
});
}
const newNoteTemplate = `\n== Note Title\n:author: \n:version: 1.0\n:published_on: 2024-01-01\n:published_by: Alexandria\n:summary: \n:tags: \n:image: \n\nNote content here...\n`;
function insertNoteTemplate() {
const ta = getTextarea();
if (!ta || ta.disabled) return;
const start = ta.selectionStart;
const before = content.slice(0, start);
const needsNL = before.length > 0 & & !before.endsWith('\n\n');
insertPlain((needsNL ? (before.endsWith('\n') ? '\n' : '\n\n') : '') + newNoteTemplate);
}
const markupButtons = [
{ label : 'Bold' , icon : Bold , action : () => insertMarkup ( '*' , '*' ) } ,
{ label : 'Italic' , icon : Italic , action : () => insertMarkup ( '_' , '_' ) } ,
{ label : 'Link' , icon : Link2 , action : () => insertPlain ( 'link:url[Text]' ) } ,
{ label : 'Image' , icon : ImageIcon , action : () => insertPlain ( 'image::url[]' ) } ,
{ label : 'Insert Note Template' , icon : FilePlus , action : () => insertNoteTemplate () } ,
];
// Parse sections for preview using the smart metadata service
// Parse sections for preview using the smart metadata service
let parsedSections = $derived.by(() => {
let parsedSections = $derived.by(() => {
if (!content.trim()) return [];
if (!content.trim()) return [];
// Use smart metadata extraction that handles both document headers and section-only content
// Use smart metadata extraction that handles both document headers and section-only content
const { metadata : docMetadata } = extractSmartMetadata(content);
const { metadata : _ docMetadata } = extractSmartMetadata(content);
// Parse the content using the standardized parser
// Parse the content using the standardized parser
const parsed = parseAsciiDocWithMetadata(content);
const parsed = parseAsciiDocWithMetadata(content);
@ -93,6 +123,14 @@ Note content here...
onPreviewToggle(newShowPreview);
onPreviewToggle(newShowPreview);
}
}
// Auto-open preview on desktop (lg >= 1024px) first load
onMount(() => {
const mq = window.matchMedia('(min-width: 1024px)');
if (mq.matches && !showPreview) {
onPreviewToggle(true);
}
});
// Handle content changes
// Handle content changes
function handleContentChange(event: Event) {
function handleContentChange(event: Event) {
const target = event.target as HTMLTextAreaElement;
const target = event.target as HTMLTextAreaElement;
@ -100,14 +138,14 @@ Note content here...
}
}
< / script >
< / script >
< div class = "flex flex-col space-y-4" >
< div class = "w-full flex flex-col space-y-4 mt-3 " bind:this = { wrapper } >
<!-- Error message for publication format -->
<!-- Error message for publication format -->
{ #if hasPublicationHeader }
{ #if hasPublicationHeader }
< div class = "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4" >
< div class = "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4" >
< div class = "flex items-start space-x-3" >
< div class = "flex items-start space-x-3" >
< div class = "flex-shrink-0" >
< div class = "flex-shrink-0" >
< svg class = "w-5 h-5 text-red-600 dark:text-red-400" fill = "currentColor" viewBox = "0 0 20 20" >
< svg class = "w-5 h-5 text-red-600 dark:text-red-400" fill = "currentColor" viewBox = "0 0 20 20" >
< path fill-rule = "evenodd" d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9 a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule = "evenodd" > < / path >
< path fill-rule = "evenodd" d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zm-4 4 a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule = "evenodd" > < / path >
< / svg >
< / svg >
< / div >
< / div >
< div class = "flex-1" >
< div class = "flex-1" >
@ -153,157 +191,91 @@ Note content here...
< / div >
< / div >
{ : else }
{ : else }
<!-- Informative text about ZettelEditor purpose -->
<!-- Informative text about ZettelEditor purpose -->
< div class = "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4" >
< AAlert color = "blue" classes = "max-w-lg self-center" >
< div class = "flex items-start space-x-3" >
{ # snippet title ()} Note-Taking Tool{ /snippet }
< div class = "flex-shrink-0" >
< p class = "text-sm mb-3" >
< svg class = "w-5 h-5 text-blue-600 dark:text-blue-400" fill = "currentColor" viewBox = "0 0 20 20" >
< path fill-rule = "evenodd" d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 00-1-1H9z" clip-rule = "evenodd" > < / path >
< / svg >
< / div >
< div class = "flex-1" >
< h3 class = "text-sm font-medium text-blue-800 dark:text-blue-200 mb-1" >
Note-Taking Tool
< / h3 >
< p class = "text-sm text-blue-700 dark:text-blue-300 mb-3" >
This editor is for creating individual notes (30041 events) only. Each section becomes a separate note event.
This editor is for creating individual notes (30041 events) only. Each section becomes a separate note event.
You can add metadata like author, version, publication date, summary, and tags using AsciiDoc attributes.
You can add metadata like author, version, publication date, summary, and tags using AsciiDoc attributes.
To create structured publications with a 30040 index event that ties multiple notes together,
To create structured publications with a 30040 index event that ties multiple notes together,
use the < a href = "/events?kind=30040" class = "font-medium underline hover:text-blue-600 dark:hover:text-blue-400 " > Events form< / a > .
use the < a href = "/events/compose?kind=30040" class = "font-medium underline" > Events form< / a > .
< / p >
< / p >
< div class = "flex space-x-2" >
< div class = "flex space-x-2" >
< a
< a
href="/events?kind=30040"
href="/events/compose?kind=30040"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-800 border border-blue-200 dark:border-blue-700 rounded-md hover:bg-blue-200 dark:hover:bg-blue-700 transition-colors"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-800 border border-blue-200 dark:border-blue-700 rounded-md hover:bg-blue-200 dark:hover:bg-blue-700 transition-colors"
>
>
Create Publication
Create Publication
< / a >
< / a >
< / div >
< / div >
< / div >
< / AAlert >
< / div >
< / div >
{ /if }
< div class = "flex items-center justify-between" >
< Button
color="light"
size="sm"
onclick={ togglePreview }
class="flex items-center space-x-1"
disabled={ hasPublicationHeader }
>
{ #if showPreview }
< EyeOutline class = "w-4 h-4" / >
< span > Hide Preview< / span >
{ : else }
< EyeOutline class = "w-4 h-4" / >
< span > Show Preview< / span >
{ /if }
{ /if }
< / Button >
< / div >
< div class = "flex flex-col lg:flex-row lg:space-x-4 { showPreview ? 'lg:h-96' : '' } " >
<!-- Editor + Preview (two columns on desktop) -->
<!-- Editor Panel -->
< div class = { `w-full grid gap-4 ${ showPreview && ! hasPublicationHeader ? 'lg:grid-cols-2' : 'lg:grid-cols-1' } ` } >
< div class = " { showPreview ? 'lg:w-1/2' : 'w-full' } flex flex-col space-y-4" >
< div class = "flex-1" >
< Textarea
< Textarea
bind:value={ content }
bind:value={ content }
on: input={ handleContentChange }
oninput={ handleContentChange }
{ placeholder }
{ placeholder }
class="h-full min-h-64 resize-none { hasPublicationHeader ? 'opacity-50 cursor-not-allowed' : '' } "
rows={ 20 }
rows={ 12 }
disabled={ hasPublicationHeader }
disabled={ hasPublicationHeader }
/>
classes={{
< / div >
wrapper: '!m-0 p-0 h-full',
< / div >
inner: '!m-0 !bg-transparent !dark:bg-transparent',
header: '!m-0 !bg-transparent !dark:bg-transparent',
footer: '!m-0 !bg-transparent',
addon: '!m-0 top-3 hidden md:flex',
div: '!m-0 !bg-transparent !dark:bg-transparent focus:!ring-0 h-full',
}}
>
{ # snippet header ()}
< Toolbar embedded class = "flex-row !m-0 !dark:bg-transparent !bg-transparent overflow-x-auto" >
< ToolbarGroup class = "flex-row flex-nowrap gap-1 !m-0" >
{ #each markupButtons as btn }
{ @const Icon = btn . icon }
< ToolbarButton title = { btn . label } color="dark" size = "sm" onclick = { btn . action } disabled= { hasPublicationHeader } >
< Icon size = { 24 } / >
< / ToolbarButton >
{ /each }
< ToolbarButton title = { showPreview ? 'Hide Preview' : 'Show Preview' } color="dark" size = "sm" onclick = { togglePreview } disabled= { hasPublicationHeader } >
< Eye size = { 24 } / >
< / ToolbarButton >
< / ToolbarGroup >
< / Toolbar >
{ /snippet }
< / Textarea >
<!-- Preview Panel -->
{ #if showPreview && ! hasPublicationHeader }
{ #if showPreview && ! hasPublicationHeader }
< div class = "lg:w-1/2 lg:border-l lg:border-gray-200 lg:dark:border-gray-700 lg:pl-4 mt-4 lg:mt-0" >
< div class = "flex flex-col max-h-[600px] " >
< div class = "lg:sticky lg:top-4" >
< div class = "lg:sticky lg:top-4" >
< h3
< h3 class = "text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100 hidden" > AsciiDoc Preview< / h3 >
class="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100"
< div class = "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 max-h-[560px] overflow-y-auto" >
>
AsciiDoc Preview
< / h3 >
< div
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 max-h-80 overflow-y-auto"
>
{ #if ! content . trim ()}
{ #if ! content . trim ()}
< div class = "text-gray-500 dark:text-gray-400 text-sm" >
< div class = "text-gray-500 dark:text-gray-400 text-sm" > Start typing to see the preview...< / div >
Start typing to see the preview...
< / div >
{ : else }
{ : else }
< div class = "prose prose-sm dark:prose-invert max-w-none" >
< div class = "prose prose-sm dark:prose-invert max-w-none" >
{ #each parsedSections as section , index }
{ #each parsedSections as section , index }
< div class = "mb-6" >
< div class = "mb-6" >
< div
< div class = "text-sm text-gray-800 dark:text-gray-200 asciidoc-content" > { @html asciidoctor (). convert ( `== $ { section . title } \ n \ n$ { section . content } `, { standalone :false , doctype : 'article' , attributes : { showtitle :true , sectids :true } })} </ div >
class="text-sm text-gray-800 dark:text-gray-200 asciidoc-content"
>
{ @html asciidoctor (). convert (
`== ${ section . title } \n\n${ section . content } `,
{
standalone: false,
doctype: "article",
attributes: {
showtitle: true,
sectids: true,
},
},
)}
< / div >
<!-- Gray area with tag bubbles for all sections -->
< div class = "my-4 relative" >
< div class = "my-4 relative" >
<!-- Gray background area -->
< div class = "bg-gray-200 dark:bg-gray-700 rounded-lg p-3 mb-2" >
< div
class="bg-gray-200 dark:bg-gray-700 rounded-lg p-3 mb-2"
>
< div class = "flex flex-wrap gap-2 items-center" >
< div class = "flex flex-wrap gap-2 items-center" >
{ #if section . tags && section . tags . length > 0 }
{ #if section . tags && section . tags . length > 0 }
{ #each section . tags as tag }
{ #each section . tags as tag }
< div
< div class = "bg-amber-900 text-amber-100 px-2 py-1 rounded-full text-xs font-medium flex items-baseline" >< span class = "font-mono" > { tag [ 0 ]} :</ span >< span > { tag [ 1 ]} </ span ></ div >
class="bg-amber-900 text-amber-100 px-2 py-1 rounded-full text-xs font-medium flex items-baseline"
>
< span class = "font-mono" > { tag [ 0 ]} :</ span >
< span > { tag [ 1 ]} </ span >
< / div >
{ /each }
{ /each }
{ : else }
{ : else }
< span
< span class = "text-gray-500 dark:text-gray-400 text-xs italic" > No tags< / span >
class="text-gray-500 dark:text-gray-400 text-xs italic"
>No tags< /span
>
{ /if }
{ /if }
< / div >
< / div >
< / div >
< / div >
{ #if index < parsedSections . length - 1 }
{ #if index < parsedSections . length - 1 }
<!-- Event boundary line only between sections -->
< div class = "border-t-2 border-dashed border-blue-400 relative" > < div class = "absolute -top-2 left-1/2 -translate-x-1/2 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded text-xs font-medium" > Event Boundary< / div > < / div >
< div
class="border-t-2 border-dashed border-blue-400 relative"
>
< div
class="absolute -top-2 left-1/2 transform -translate-x-1/2 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded text-xs font-medium"
>
Event Boundary
< / div >
< / div >
{ /if }
{ /if }
< / div >
< / div >
< / div >
< / div >
{ /each }
{ /each }
< / div >
< / div >
< div class = "mt-2 text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 p-2 rounded border" >< strong > Event Count:</ strong > { parsedSections . length } event{ parsedSections . length !== 1 ? 's' : '' } </ div >
< div
class="mt-4 text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 p-2 rounded border"
>
< strong > Event Count:< / strong >
{ parsedSections . length } event{ parsedSections . length !== 1
? "s"
: ""}
< br / >
< / div >
{ /if }
{ /if }
< / div >
< / div >
< / div >
< / div >