feat: add expandable extended toolbar with placeholders

This commit is contained in:
Steffen Becker committed 2026-05-25 20:50:08 +02:00
1 parent c9196d252d
commit 211da902f6
3 files changed
+269 -4

No files matched your search

+50 -1
View File
@@ -2,14 +2,16 @@
import type { Editor } from '@tiptap/core';
import EditorComponent from './lib/editor/Editor.svelte';
import Toolbar from './lib/editor/Toolbar.svelte';
import ToolbarExpanded from './lib/editor/ToolbarExpanded.svelte';
import { exportToOdt } from './lib/export/odt';
import { loadTheme, saveTheme, applyTheme, type ThemeMode } from './lib/storage/theme';
import { loadTheme, saveTheme, applyTheme, loadToolbarExpanded, saveToolbarExpanded, type ThemeMode } from './lib/storage/theme';
let editor: Editor | null = $state(null);
let tick: number = $state(0);
let themeMode: ThemeMode = $state(loadTheme());
let themeOpen = $state(false);
let toolbarExpanded = $state(loadToolbarExpanded());
function selectTheme(m: ThemeMode) {
themeMode = m;
@@ -18,6 +20,11 @@
themeOpen = false;
}
function toggleToolbar() {
toolbarExpanded = !toolbarExpanded;
saveToolbarExpanded(toolbarExpanded);
}
function clickOutside(node: HTMLElement) {
function handler(e: MouseEvent) {
if (!node.contains(e.target as Node)) themeOpen = false;
@@ -35,6 +42,22 @@
<header>
<Toolbar {editor} {tick} />
<div class="header-actions">
<button
class="toolbar-toggle-btn"
class:active={toolbarExpanded}
onclick={toggleToolbar}
title={toolbarExpanded ? 'Hide extended toolbar' : 'Show extended toolbar'}
>
<!-- Sliders/settings icon -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<line x1="2" y1="4" x2="14" y2="4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="8" x2="14" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="12" x2="14" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="5" cy="4" r="1.5" fill="var(--color-toolbar-bg)" stroke="currentColor" stroke-width="1.2"/>
<circle cx="10" cy="8" r="1.5" fill="var(--color-toolbar-bg)" stroke="currentColor" stroke-width="1.2"/>
<circle cx="7" cy="12" r="1.5" fill="var(--color-toolbar-bg)" stroke="currentColor" stroke-width="1.2"/>
</svg>
</button>
<div class="theme-wrap" use:clickOutside>
<button
class="theme-btn"
@@ -85,6 +108,9 @@
</button>
</div>
</header>
{#if toolbarExpanded}
<ToolbarExpanded {editor} {tick} />
{/if}
<EditorComponent bind:editor bind:tick />
</main>
@@ -111,6 +137,29 @@
margin-right: 1rem;
}
.toolbar-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: none;
border-radius: var(--radius);
background: transparent;
color: var(--color-text);
cursor: pointer;
transition: background 0.15s;
}
.toolbar-toggle-btn:hover {
background: var(--color-btn-hover);
}
.toolbar-toggle-btn.active {
background: var(--color-primary);
color: white;
}
.theme-wrap {
position: relative;
}
+207
View File
@@ -0,0 +1,207 @@
<script lang="ts">
import type { Editor } from '@tiptap/core';
let { editor, tick }: { editor: Editor | null; tick: number } = $props();
let isAlignLeft = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'left' }));
let isAlignCenter = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'center' }));
let isAlignRight = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'right' }));
let isAlignJustify = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'justify' }));
</script>
<div class="toolbar-expanded">
{#if editor}
<div class="toolbar-group">
<select
class="font-select"
disabled
title="Font family (coming soon)"
>
<option>Sans-serif</option>
</select>
</div>
<div class="toolbar-separator"></div>
<div class="toolbar-group">
<select
class="font-size-select"
disabled
title="Font size (coming soon)"
>
<option>16</option>
</select>
</div>
<div class="toolbar-separator"></div>
<div class="toolbar-group">
<button disabled title="Font color (coming soon)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<text x="3" y="11" font-size="10" font-weight="bold" font-family="sans-serif" fill="currentColor">A</text>
<rect x="2" y="13" width="12" height="2" rx="0.5" fill="currentColor"/>
</svg>
</button>
<button disabled title="Highlight color (coming soon)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="2" y="2" width="12" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="4" y="4" width="8" height="8" rx="1" fill="currentColor" opacity="0.3"/>
</svg>
</button>
</div>
<div class="toolbar-separator"></div>
<div class="toolbar-group">
<button
class:active={isAlignLeft}
disabled
title="Align left (coming soon)"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<line x1="2" y1="3" x2="14" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="7" x2="10" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="11" x2="12" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<button
class:active={isAlignCenter}
disabled
title="Align center (coming soon)"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<line x1="2" y1="3" x2="14" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="4" y1="7" x2="12" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="3" y1="11" x2="13" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<button
class:active={isAlignRight}
disabled
title="Align right (coming soon)"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<line x1="2" y1="3" x2="14" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="6" y1="7" x2="14" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="4" y1="11" x2="14" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<button
class:active={isAlignJustify}
disabled
title="Justify (coming soon)"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<line x1="2" y1="3" x2="14" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="7" x2="14" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="11" x2="14" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="toolbar-separator"></div>
<div class="toolbar-group">
<button disabled title="Insert table (coming soon)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1.5" y="1.5" width="13" height="13" rx="1" stroke="currentColor" stroke-width="1.5"/>
<line x1="1.5" y1="5.5" x2="14.5" y2="5.5" stroke="currentColor" stroke-width="1"/>
<line x1="1.5" y1="9.5" x2="14.5" y2="9.5" stroke="currentColor" stroke-width="1"/>
<line x1="5.5" y1="5.5" x2="5.5" y2="14.5" stroke="currentColor" stroke-width="1"/>
<line x1="10.5" y1="5.5" x2="10.5" y2="14.5" stroke="currentColor" stroke-width="1"/>
</svg>
</button>
</div>
<div class="toolbar-separator"></div>
<div class="toolbar-group">
<button disabled title="Clear formatting (coming soon)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
{/if}
</div>
<style>
.toolbar-expanded {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.4rem 1rem;
background: var(--color-toolbar-bg);
border-bottom: 1px solid var(--color-border);
}
.toolbar-group {
display: flex;
gap: 2px;
}
.toolbar-separator {
width: 1px;
height: 1.5rem;
background: var(--color-border);
margin: 0 0.5rem;
}
button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
border: none;
border-radius: var(--radius);
background: transparent;
color: var(--color-text);
font-size: 0.85rem;
font-family: var(--font-sans);
cursor: pointer;
transition: background 0.15s;
}
button:hover:not(:disabled) {
background: var(--color-btn-hover);
}
button.active {
background: var(--color-primary);
color: white;
}
button:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.font-select {
height: 2rem;
padding: 0 0.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
font-size: 0.8rem;
font-family: var(--font-sans);
cursor: not-allowed;
opacity: 0.35;
max-width: 110px;
}
.font-size-select {
height: 2rem;
padding: 0 0.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
font-size: 0.8rem;
font-family: var(--font-sans);
cursor: not-allowed;
opacity: 0.35;
}
</style>
+12 -3
View File
@@ -1,15 +1,24 @@
export type ThemeMode = 'light' | 'dark' | 'auto';
const STORAGE_KEY = 'odf-editor-theme';
const THEME_KEY = 'odf-editor-theme';
const TOOLBAR_KEY = 'odf-editor-toolbar-expanded';
export function loadTheme(): ThemeMode {
const saved = localStorage.getItem(STORAGE_KEY);
const saved = localStorage.getItem(THEME_KEY);
if (saved === 'light' || saved === 'dark' || saved === 'auto') return saved;
return 'auto';
}
export function saveTheme(mode: ThemeMode): void {
localStorage.setItem(STORAGE_KEY, mode);
localStorage.setItem(THEME_KEY, mode);
}
export function loadToolbarExpanded(): boolean {
return localStorage.getItem(TOOLBAR_KEY) === 'true';
}
export function saveToolbarExpanded(expanded: boolean): void {
localStorage.setItem(TOOLBAR_KEY, String(expanded));
}
function resolveMode(mode: ThemeMode): 'light' | 'dark' {