UI refinement: toolbar icons, gradient CTA, Fraunces title, viewer polish

MarkdownToolbar:
- Replace text labels with SVG icons (Material Design paths)
- Group buttons with visual separators: text / headings / lists / code / links
- Add Strikethrough and Blockquote buttons (previously missing)
- Active state: tinted bg + ring instead of solid fill; hover lift shadow
- Toolbar container: pill shape with border (matches rest of UI)

editor-shared.css:
- btn-save: gradient + glow (linear-gradient(135deg, #6366f1, #4f46e5))
- title-input: borderless with Fraunces font and focus underline

NoteEditorView:
- Back button label standardized to "← Notes" (was "Back")
- Write/Preview tabs and toolbar now share the same row (flex-direction row)

NoteViewerView:
- Shimmer skeleton loader replaces plain "Loading..." text
- Note title uses Fraunces font at 2rem
- Edit button becomes primary gradient CTA
- Meta line shows clock/pencil SVG icons alongside timestamps
- Backlinks section: card grid with colored type badges (note=indigo, task=amber)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 10:57:02 -04:00
parent 810f63e749
commit 978a2627fb
4 changed files with 352 additions and 165 deletions
+30 -9
View File
@@ -51,15 +51,23 @@
color: var(--color-primary); color: var(--color-primary);
} }
.btn-save { .btn-save {
padding: 0.45rem 1rem; padding: 0.45rem 1.1rem;
background: var(--color-primary); background: linear-gradient(135deg, #6366f1, #4f46e5);
color: #fff; color: #fff;
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
font-weight: 600;
font-size: 0.875rem;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.28);
transition: box-shadow 0.15s, opacity 0.15s;
}
.btn-save:hover:not(:disabled) {
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.42);
opacity: 0.95;
} }
.btn-save:disabled { .btn-save:disabled {
opacity: 0.6; opacity: 0.55;
cursor: default; cursor: default;
} }
.btn-delete { .btn-delete {
@@ -86,13 +94,26 @@
color: var(--color-primary); color: var(--color-primary);
} }
.title-input { .title-input {
padding: 0.5rem 0.75rem; padding: 0.4rem 0;
border: 1px solid var(--color-input-border); border: none;
border-radius: var(--radius-sm); border-bottom: 1.5px solid var(--color-border);
font-size: 1.25rem; border-radius: 0;
font-weight: 600; font-size: 1.5rem;
background: var(--color-bg-card); font-weight: 700;
font-family: "Fraunces", Georgia, serif;
background: transparent;
color: var(--color-text); color: var(--color-text);
width: 100%;
transition: border-color 0.15s;
}
.title-input:focus {
outline: none;
border-bottom-color: var(--color-primary);
}
.title-input::placeholder {
color: var(--color-text-muted);
font-style: italic;
font-weight: 400;
} }
.editor-tabs { .editor-tabs {
display: flex; display: flex;
+131 -104
View File
@@ -1,130 +1,157 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Editor } from "@tiptap/vue-3"; import type { Editor } from "@tiptap/vue-3";
const props = defineProps<{ const props = defineProps<{ editor: Editor | null }>();
editor: Editor | null;
}>();
const buttons = [ // SVG icons — Material Design paths (24x24) or custom SVG
{ const ICONS: Record<string, string> = {
label: "B", bold: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg>`,
title: "Bold", italic: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"/></svg>`,
command: () => props.editor?.chain().focus().toggleBold().run(), strike: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z"/></svg>`,
isActive: () => props.editor?.isActive("bold") ?? false, h1: `<svg viewBox="0 0 24 24" width="15" height="15"><text x="1" y="17" font-family="sans-serif" font-weight="800" font-size="16" fill="currentColor">H</text><text x="14" y="17" font-family="sans-serif" font-weight="700" font-size="11" fill="currentColor">1</text></svg>`,
}, h2: `<svg viewBox="0 0 24 24" width="15" height="15"><text x="1" y="17" font-family="sans-serif" font-weight="800" font-size="16" fill="currentColor">H</text><text x="14" y="17" font-family="sans-serif" font-weight="700" font-size="11" fill="currentColor">2</text></svg>`,
{ h3: `<svg viewBox="0 0 24 24" width="15" height="15"><text x="1" y="17" font-family="sans-serif" font-weight="800" font-size="16" fill="currentColor">H</text><text x="14" y="17" font-family="sans-serif" font-weight="700" font-size="11" fill="currentColor">3</text></svg>`,
label: "I", ul: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`,
title: "Italic", ol: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2zm1-9h1V4H2v1h1zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2zm5-3h13V6H7v2zm0 4h13v-2H7v2zm0 4h13v-2H7v2z"/></svg>`,
command: () => props.editor?.chain().focus().toggleItalic().run(), task: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>`,
isActive: () => props.editor?.isActive("italic") ?? false, code: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>`,
}, codeblock: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/></svg>`,
{ quote: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>`,
label: "H1", link: `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>`,
title: "Heading 1", wikilink: `<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3"/><path d="M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"/></svg>`,
command: () => };
props.editor?.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => props.editor?.isActive("heading", { level: 1 }) ?? false, const groups = [
}, [
{ { id: "bold", title: "Bold (Ctrl+B)", isActive: () => props.editor?.isActive("bold") ?? false, command: () => props.editor?.chain().focus().toggleBold().run() },
label: "H2", { id: "italic", title: "Italic (Ctrl+I)", isActive: () => props.editor?.isActive("italic") ?? false, command: () => props.editor?.chain().focus().toggleItalic().run() },
title: "Heading 2", { id: "strike", title: "Strikethrough", isActive: () => props.editor?.isActive("strike") ?? false, command: () => props.editor?.chain().focus().toggleStrike().run() },
command: () => ],
props.editor?.chain().focus().toggleHeading({ level: 2 }).run(), [
isActive: () => props.editor?.isActive("heading", { level: 2 }) ?? false, { id: "h1", title: "Heading 1", isActive: () => props.editor?.isActive("heading", { level: 1 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 1 }).run() },
}, { id: "h2", title: "Heading 2", isActive: () => props.editor?.isActive("heading", { level: 2 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 2 }).run() },
{ { id: "h3", title: "Heading 3", isActive: () => props.editor?.isActive("heading", { level: 3 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 3 }).run() },
label: "Link", ],
title: "Link", [
command: () => { { id: "ul", title: "Bullet List", isActive: () => props.editor?.isActive("bulletList") ?? false, command: () => props.editor?.chain().focus().toggleBulletList().run() },
const ed = props.editor; { id: "ol", title: "Ordered List", isActive: () => props.editor?.isActive("orderedList") ?? false, command: () => props.editor?.chain().focus().toggleOrderedList().run() },
if (!ed) return; { id: "task", title: "Task List", isActive: () => props.editor?.isActive("taskList") ?? false, command: () => props.editor?.chain().focus().toggleTaskList().run() },
if (ed.isActive("link")) { ],
ed.chain().focus().unsetLink().run(); [
} else { { id: "code", title: "Inline Code", isActive: () => props.editor?.isActive("code") ?? false, command: () => props.editor?.chain().focus().toggleCode().run() },
const url = prompt("URL:"); { id: "codeblock", title: "Code Block", isActive: () => props.editor?.isActive("codeBlock") ?? false, command: () => props.editor?.chain().focus().toggleCodeBlock().run() },
if (url) { { id: "quote", title: "Blockquote", isActive: () => props.editor?.isActive("blockquote") ?? false, command: () => props.editor?.chain().focus().toggleBlockquote().run() },
ed.chain().focus().setLink({ href: url }).run(); ],
[
{
id: "link",
title: "Link",
isActive: () => props.editor?.isActive("link") ?? false,
command: () => {
const ed = props.editor;
if (!ed) return;
if (ed.isActive("link")) {
ed.chain().focus().unsetLink().run();
} else {
const url = prompt("URL:");
if (url) ed.chain().focus().setLink({ href: url }).run();
} }
} },
}, },
isActive: () => props.editor?.isActive("link") ?? false, {
}, id: "wikilink",
{ title: "Insert wikilink [[ ]]",
label: "UL", isActive: () => false,
title: "Bullet List", command: () => props.editor?.chain().focus().insertContent("[[").run(),
command: () => props.editor?.chain().focus().toggleBulletList().run(), },
isActive: () => props.editor?.isActive("bulletList") ?? false, ],
},
{
label: "OL",
title: "Numbered List",
command: () => props.editor?.chain().focus().toggleOrderedList().run(),
isActive: () => props.editor?.isActive("orderedList") ?? false,
},
{
label: "`",
title: "Inline Code",
command: () => props.editor?.chain().focus().toggleCode().run(),
isActive: () => props.editor?.isActive("code") ?? false,
},
{
label: "```",
title: "Code Block",
command: () => props.editor?.chain().focus().toggleCodeBlock().run(),
isActive: () => props.editor?.isActive("codeBlock") ?? false,
},
{
label: "☑",
title: "Task List",
command: () => props.editor?.chain().focus().toggleTaskList().run(),
isActive: () => props.editor?.isActive("taskList") ?? false,
},
{
label: "[[",
title: "Insert wikilink (type note title to search)",
command: () => props.editor?.chain().focus().insertContent("[[").run(),
isActive: () => false,
},
]; ];
</script> </script>
<template> <template>
<div class="md-toolbar"> <div class="md-toolbar" role="toolbar" aria-label="Text formatting">
<button <template v-for="(group, gi) in groups" :key="gi">
v-for="btn in buttons" <div class="toolbar-group">
:key="btn.label" <button
:class="['md-btn', { active: btn.isActive() }]" v-for="btn in group"
:title="btn.title" :key="btn.id"
@click="btn.command()" :class="['md-btn', { active: btn.isActive() }]"
> :title="btn.title"
{{ btn.label }} type="button"
</button> @mousedown.prevent="btn.command()"
>
<span class="btn-icon" v-html="ICONS[btn.id]" />
</button>
</div>
<span v-if="gi < groups.length - 1" class="toolbar-sep" aria-hidden="true" />
</template>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.md-toolbar { .md-toolbar {
display: flex; display: flex;
align-items: center;
gap: 2px; gap: 2px;
flex-wrap: wrap; flex-wrap: wrap;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 3px 4px;
} }
.toolbar-group {
display: flex;
align-items: center;
gap: 1px;
}
.toolbar-sep {
display: block;
width: 1px;
height: 18px;
background: var(--color-border);
flex-shrink: 0;
margin: 0 3px;
}
.md-btn { .md-btn {
padding: 0.25rem 0.5rem; display: flex;
border: 1px solid var(--color-input-border); align-items: center;
border-radius: var(--radius-sm); justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 5px;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0;
transition: background 0.12s, color 0.12s, box-shadow 0.12s;
flex-shrink: 0;
}
.md-btn:hover {
background: var(--color-bg-card); background: var(--color-bg-card);
color: var(--color-text); color: var(--color-text);
cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
font-size: 0.8rem;
font-family: inherit;
line-height: 1;
}
.md-btn:hover {
background: var(--color-bg-secondary);
} }
.md-btn.active { .md-btn.active {
background: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 14%, transparent);
color: #fff; color: var(--color-primary);
border-color: var(--color-primary); box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 35%, transparent);
}
.md-btn.active:hover {
background: color-mix(in srgb, var(--color-primary) 22%, transparent);
}
.btn-icon {
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
pointer-events: none;
} }
</style> </style>
+8 -4
View File
@@ -321,7 +321,7 @@ onUnmounted(() => assist.clearSelection());
<main class="editor-page note-editor-page"> <main class="editor-page note-editor-page">
<div class="editor-header"> <div class="editor-header">
<div class="toolbar"> <div class="toolbar">
<router-link to="/notes" class="btn-back">Back</router-link> <router-link to="/notes" class="btn-back"> Notes</router-link>
<button class="btn-save" @click="save" :disabled="saving"> <button class="btn-save" @click="save" :disabled="saving">
{{ saving ? "Saving..." : "Save" }} {{ saving ? "Saving..." : "Save" }}
</button> </button>
@@ -575,8 +575,12 @@ onUnmounted(() => assist.clearSelection());
.body-tabs-row { .body-tabs-row {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 0.25rem; align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-border);
} }
.editor-tabs { .editor-tabs {
@@ -586,7 +590,7 @@ onUnmounted(() => assist.clearSelection());
border-radius: 8px; border-radius: 8px;
padding: 2px; padding: 2px;
gap: 2px; gap: 2px;
align-self: flex-start; flex-shrink: 0;
} }
.tab { .tab {
+183 -48
View File
@@ -138,7 +138,20 @@ async function convertToTask() {
<template> <template>
<div class="viewer-layout"> <div class="viewer-layout">
<main class="viewer"> <main class="viewer">
<p v-if="store.loading">Loading...</p> <div v-if="store.loading" class="viewer-skeleton" aria-label="Loading note">
<div class="skel-toolbar">
<div class="skel-btn"></div>
<div class="skel-btn skel-btn--wide"></div>
</div>
<div class="skel-title"></div>
<div class="skel-meta"></div>
<div class="skel-line"></div>
<div class="skel-line skel-line--short"></div>
<div class="skel-line"></div>
<div class="skel-line skel-line--medium"></div>
<div class="skel-line"></div>
<div class="skel-line skel-line--short"></div>
</div>
<template v-else-if="store.currentNote"> <template v-else-if="store.currentNote">
<div class="toolbar"> <div class="toolbar">
<router-link to="/notes" class="btn-back"> Notes</router-link> <router-link to="/notes" class="btn-back"> Notes</router-link>
@@ -182,11 +195,17 @@ async function convertToTask() {
</span> </span>
</div> </div>
<h1>{{ store.currentNote.title || "Untitled" }}</h1> <h1 class="note-title">{{ store.currentNote.title || "Untitled" }}</h1>
<p class="meta"> <p class="meta">
Updated {{ relativeTime(store.currentNote.updated_at) }} <span class="meta-item">
&middot; <svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67V7z"/></svg>
Created {{ relativeTime(store.currentNote.created_at) }} Updated {{ relativeTime(store.currentNote.updated_at) }}
</span>
<span class="meta-sep" aria-hidden="true">·</span>
<span class="meta-item">
<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
Created {{ relativeTime(store.currentNote.created_at) }}
</span>
</p> </p>
<div class="tags" v-if="store.currentNote.tags.length"> <div class="tags" v-if="store.currentNote.tags.length">
<TagPill <TagPill
@@ -203,18 +222,22 @@ async function convertToTask() {
></div> ></div>
<div v-if="backlinks.length" class="backlinks"> <div v-if="backlinks.length" class="backlinks">
<h2>Backlinks</h2> <h3 class="backlinks-heading">
<ul class="backlinks-list"> <svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
<li v-for="link in backlinks" :key="`${link.type}-${link.id}`" class="backlink-item"> Backlinks
<span class="backlink-type">{{ link.type }}</span> <span class="backlinks-count">{{ backlinks.length }}</span>
<router-link </h3>
:to="`/${link.type === 'note' ? 'notes' : 'tasks'}/${link.id}`" <div class="backlinks-grid">
class="backlink-link" <router-link
> v-for="link in backlinks"
{{ link.title || "Untitled" }} :key="`${link.type}-${link.id}`"
</router-link> :to="`/${link.type === 'note' ? 'notes' : 'tasks'}/${link.id}`"
</li> class="backlink-card"
</ul> >
<span :class="['backlink-type-badge', `badge-${link.type}`]">{{ link.type }}</span>
<span class="backlink-title">{{ link.title || "Untitled" }}</span>
</router-link>
</div>
</div> </div>
</template> </template>
<p v-else>Note not found.</p> <p v-else>Note not found.</p>
@@ -255,8 +278,7 @@ async function convertToTask() {
gap: 0.75rem; gap: 0.75rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.btn-back, .btn-back {
.btn-edit {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 0.45rem 1rem; padding: 0.45rem 1rem;
@@ -268,11 +290,30 @@ async function convertToTask() {
cursor: pointer; cursor: pointer;
font-size: 0.9rem; font-size: 0.9rem;
} }
.btn-back:hover, .btn-back:hover {
.btn-edit:hover {
border-color: var(--color-primary); border-color: var(--color-primary);
color: var(--color-primary); color: var(--color-primary);
} }
.btn-edit {
display: inline-flex;
align-items: center;
padding: 0.45rem 1.1rem;
border: none;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: #fff;
text-decoration: none;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.28);
transition: box-shadow 0.15s, opacity 0.15s;
}
.btn-edit:hover {
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.42);
opacity: 0.95;
color: #fff;
}
.btn-convert { .btn-convert {
margin-left: auto; margin-left: auto;
padding: 0.3rem 0.75rem; padding: 0.3rem 0.75rem;
@@ -293,11 +334,32 @@ async function convertToTask() {
cursor: default; cursor: default;
} }
.note-title {
font-family: "Fraunces", Georgia, serif;
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
margin: 0.25rem 0 0.5rem;
color: var(--color-text);
}
.meta { .meta {
font-size: 0.85rem; display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 0.83rem;
color: var(--color-text-muted); color: var(--color-text-muted);
margin: 0 0 0.75rem; margin: 0 0 0.75rem;
} }
.meta-item {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.meta-sep {
opacity: 0.5;
}
.tags { .tags {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
@@ -305,41 +367,114 @@ async function convertToTask() {
flex-wrap: wrap; flex-wrap: wrap;
} }
.backlinks { .backlinks {
margin-top: 2rem; margin-top: 2.5rem;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
padding-top: 1rem; padding-top: 1.25rem;
} }
.backlinks h2 { .backlinks-heading {
font-size: 1.1rem;
margin: 0 0 0.75rem;
}
.backlinks-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.backlink-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.4rem;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-muted);
margin: 0 0 0.75rem;
} }
.backlink-link { .backlinks-count {
color: var(--color-text); margin-left: 0.2rem;
font-size: 0.72rem;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 999px;
padding: 0 0.4rem;
line-height: 1.4;
}
.backlinks-grid {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.backlink-card {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-md);
background: var(--color-bg-card);
border: 1px solid var(--color-border);
text-decoration: none; text-decoration: none;
font-size: 0.95rem; color: var(--color-text);
transition: border-color 0.15s, box-shadow 0.15s;
font-size: 0.9rem;
} }
.backlink-link:hover { .backlink-card:hover {
border-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
color: var(--color-primary); color: var(--color-primary);
} }
.backlink-type { .backlink-type-badge {
font-size: 0.75rem; font-size: 0.68rem;
text-transform: uppercase; text-transform: uppercase;
color: var(--color-text-muted); letter-spacing: 0.04em;
background: var(--color-bg-secondary); font-weight: 600;
padding: 0.1rem 0.4rem; padding: 0.1rem 0.45rem;
border-radius: var(--radius-sm); border-radius: 999px;
flex-shrink: 0;
} }
.badge-note {
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
color: var(--color-primary);
border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent);
}
.badge-task {
background: color-mix(in srgb, #f59e0b 12%, transparent);
color: #d97706;
border: 1px solid color-mix(in srgb, #f59e0b 30%, transparent);
}
.backlink-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Skeleton loader ── */
@keyframes skel-shine {
to { background-position: 200% center; }
}
.viewer-skeleton {
display: flex;
flex-direction: column;
gap: 0.65rem;
padding-top: 0.5rem;
}
.skel-btn,
.skel-title,
.skel-meta,
.skel-line {
border-radius: var(--radius-sm);
background: linear-gradient(
90deg,
var(--color-bg-secondary) 25%,
color-mix(in srgb, var(--color-text-muted) 18%, var(--color-bg-secondary)) 50%,
var(--color-bg-secondary) 75%
);
background-size: 200% 100%;
animation: skel-shine 1.5s ease infinite;
}
.skel-toolbar {
display: flex;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.skel-btn { width: 70px; height: 32px; }
.skel-btn--wide { width: 90px; }
.skel-title { height: 2.2rem; width: 70%; border-radius: var(--radius-md); }
.skel-meta { height: 0.85rem; width: 40%; }
.skel-line { height: 0.9rem; }
.skel-line--short { width: 55%; }
.skel-line--medium { width: 80%; }
</style> </style>