feat: add expandable extended toolbar with placeholders
This commit is contained in:
1 parent
c9196d252d
commit
211da902f6
3 files changed
+269
-4
No files matched your search
+50
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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' {
|
||||
|
||||
Reference in new issue
Block a user