fix: one colour picker everywhere, and its panel lands under its button
The note-separator, page-decoration and table-line colours were native <input type=color> or a 20-swatch grid; they now open the app's own picker, which grew a swatch-only variant (no icon) for a dialog field plus a disabled state. Its panels move to the top layer: a transformed ancestor is the containing block for position:fixed, and the floating table toolbar has one — cell shading opened its palette in the bottom-right corner.
This commit is contained in:
1 parent
bf564fbab4
commit
2e9c3b20ed
4 files changed
+113
-87
No files matched your search
@@ -12,6 +12,7 @@
|
||||
chevronTitle,
|
||||
clearLabel,
|
||||
open = $bindable(false),
|
||||
disabled = false,
|
||||
onApply,
|
||||
onClear,
|
||||
onOpen,
|
||||
@@ -25,10 +26,12 @@
|
||||
chevronTitle: string;
|
||||
clearLabel?: string;
|
||||
open?: boolean;
|
||||
disabled?: boolean;
|
||||
onApply: (color: string, range: { from: number; to: number }) => void;
|
||||
onClear: (range: { from: number; to: number }) => void;
|
||||
onOpen?: () => void;
|
||||
icon: Snippet;
|
||||
// A toolbar button's glyph. A dialog field passes none and is the swatch alone.
|
||||
icon?: Snippet;
|
||||
} = $props();
|
||||
|
||||
// Row 1 = greyscale, row 2 = standard hues. Rows 3–7 are generated per column
|
||||
@@ -80,6 +83,10 @@
|
||||
// The panels are `fixed` and placed from the button's rect, so a scrolling
|
||||
// ancestor (the style manager's field column) can't clip them.
|
||||
let split = $state<HTMLElement>();
|
||||
// …and they live in the top layer, whose containing block is the viewport whatever
|
||||
// sits above the button: a transform makes an ancestor the containing block for
|
||||
// `fixed`, and the floating table toolbar has one (it hangs itself above the table).
|
||||
const topLayer = (node: HTMLElement) => node.showPopover();
|
||||
let panel = $state<HTMLElement>();
|
||||
let pos = $state({ top: 0, left: 0 });
|
||||
|
||||
@@ -416,18 +423,20 @@
|
||||
|
||||
<div class="color-picker" use:colorPickerClickOutside>
|
||||
<div class="color-split" bind:this={split}>
|
||||
<button class="color-main" onclick={quickApplyColor} {title}>
|
||||
{@render icon()}
|
||||
<span class="color-bar" style="background: {lastColor}"></span>
|
||||
<!-- A toolbar button re-applies its last colour and shows it; a dialog field is one
|
||||
value, so its swatch reads that value and every click opens the palette. -->
|
||||
<button class="color-main" class:swatch={!icon} onclick={icon ? quickApplyColor : openPicker} {title} {disabled}>
|
||||
{#if icon}{@render icon()}{/if}
|
||||
<span class="color-bar" style="background: {icon ? lastColor : (currentColor || defaultColor)}"></span>
|
||||
</button>
|
||||
<button class="color-chevron" onclick={openPicker} tabindex="-1" title={chevronTitle}>
|
||||
<button class="color-chevron" onclick={openPicker} tabindex="-1" title={chevronTitle} {disabled}>
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
|
||||
<path d="M1 2.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{#if open && view === 'palette'}
|
||||
<div class="color-dropdown" bind:this={panel} style="top: {pos.top}px; left: {pos.left}px">
|
||||
<div class="color-dropdown" popover="manual" use:topLayer bind:this={panel} style="top: {pos.top}px; left: {pos.left}px">
|
||||
<div class="color-title">{title}</div>
|
||||
<button
|
||||
class="color-automatic"
|
||||
@@ -473,6 +482,7 @@
|
||||
{/if}
|
||||
{#if open && view === 'custom'}
|
||||
<div class="color-window" role="dialog" aria-label={t().color.chooseColor}
|
||||
popover="manual" use:topLayer
|
||||
bind:this={panel} style="top: {pos.top}px; left: {pos.left}px">
|
||||
<div class="color-window-body">
|
||||
<div
|
||||
@@ -555,6 +565,18 @@
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* No glyph above it, so the bar is the button: a swatch, outlined so white still
|
||||
reads against the surface. */
|
||||
.color-main.swatch .color-bar {
|
||||
width: 22px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
box-shadow: inset 0 0 0 1px var(--color-border);
|
||||
}
|
||||
|
||||
.color-split:has(button:disabled) { opacity: 0.5; }
|
||||
.color-split button:disabled { cursor: default; }
|
||||
|
||||
.color-chevron {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -575,6 +597,15 @@
|
||||
background: var(--color-btn-hover);
|
||||
}
|
||||
|
||||
/* A popover carries the UA's own centring (`inset: 0; margin: auto`), which would
|
||||
beat the top/left placed above. */
|
||||
.color-dropdown,
|
||||
.color-window {
|
||||
inset: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.color-dropdown {
|
||||
position: fixed;
|
||||
background: var(--color-surface);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
DEFAULT_NOTE_SETTINGS, type NoteKind, type NoteNumFormat, type NoteRestart, type NoteSettings,
|
||||
} from '../storage/noteSettings';
|
||||
import { noteSettings, setNoteSettings } from '../storage/notes.svelte';
|
||||
import ColorPicker from './ColorPicker.svelte';
|
||||
import { styleSheet } from '../styles/sheet.svelte';
|
||||
import { formatOrdinal } from '../utils/orderedListTypes';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
@@ -152,10 +153,18 @@
|
||||
{#each ALIGNS as a}<option value={a}>{t().align[a]}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="row">
|
||||
<div class="row">
|
||||
<span>{t().notesDialog.sepColor}</span>
|
||||
<input type="color" value={sep.color} onchange={(e) => patchSep({ color: e.currentTarget.value })} />
|
||||
</label>
|
||||
<ColorPicker
|
||||
editor={null}
|
||||
currentColor={sep.color}
|
||||
defaultColor={sep.color}
|
||||
title={t().notesDialog.sepColor}
|
||||
chevronTitle={t().notesDialog.sepColor}
|
||||
onApply={(c) => patchSep({ color: c })}
|
||||
onClear={() => patchSep({ color: DEFAULT_NOTE_SETTINGS.separator.color })}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
{/if}
|
||||
|
||||
@@ -224,7 +233,6 @@
|
||||
padding: 0 6px;
|
||||
font: inherit;
|
||||
}
|
||||
input[type='color'] { width: 44px; padding: 2px; }
|
||||
.affix { width: 68px; }
|
||||
|
||||
.actions { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
type PageBorder, type PageDecor, type Watermark,
|
||||
} from '../storage/pageDecor';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import ColorPicker from './ColorPicker.svelte';
|
||||
|
||||
// LibreOffice's Format ▸ Page Style ▸ Area and Borders plus its Format ▸ Watermark,
|
||||
// in one panel — all three decorate the page and all three ride the page layout.
|
||||
@@ -13,6 +14,8 @@
|
||||
} = $props();
|
||||
|
||||
let dialogEl = $state<HTMLDialogElement | null>(null);
|
||||
// The colour a page background starts at, LibreOffice's own first "recent" yellow.
|
||||
const DEFAULT_BACKGROUND = '#FFFFCC';
|
||||
|
||||
$effect(() => {
|
||||
const el = dialogEl;
|
||||
@@ -43,32 +46,49 @@
|
||||
|
||||
<fieldset>
|
||||
<legend>{t().pageDecor.background}</legend>
|
||||
<label class="row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!decor.background}
|
||||
onchange={(e) => (decor = { ...decor, background: e.currentTarget.checked ? (decor.background ?? '#ffffcc') : null })}
|
||||
<div class="row">
|
||||
<label class="grow">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!decor.background}
|
||||
onchange={(e) => (decor = { ...decor, background: e.currentTarget.checked ? (decor.background ?? DEFAULT_BACKGROUND) : null })}
|
||||
/>
|
||||
<span>{t().pageDecor.useBackground}</span>
|
||||
</label>
|
||||
<ColorPicker
|
||||
editor={null}
|
||||
currentColor={decor.background ?? null}
|
||||
defaultColor={decor.background ?? DEFAULT_BACKGROUND}
|
||||
title={t().pageDecor.background}
|
||||
chevronTitle={t().pageDecor.background}
|
||||
onApply={(c) => (decor = { ...decor, background: c })}
|
||||
onClear={() => (decor = { ...decor, background: null })}
|
||||
/>
|
||||
<span>{t().pageDecor.useBackground}</span>
|
||||
<input
|
||||
type="color"
|
||||
value={decor.background ?? '#ffffcc'}
|
||||
onchange={(e) => (decor = { ...decor, background: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>{t().pageDecor.border}</legend>
|
||||
<label class="row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!decor.border}
|
||||
onchange={(e) => (decor = { ...decor, border: e.currentTarget.checked ? (decor.border ?? DEFAULT_PAGE_BORDER) : null })}
|
||||
<div class="row">
|
||||
<label class="grow">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!decor.border}
|
||||
onchange={(e) => (decor = { ...decor, border: e.currentTarget.checked ? (decor.border ?? DEFAULT_PAGE_BORDER) : null })}
|
||||
/>
|
||||
<span>{t().pageDecor.useBorder}</span>
|
||||
</label>
|
||||
<ColorPicker
|
||||
editor={null}
|
||||
currentColor={decor.border?.color ?? DEFAULT_PAGE_BORDER.color}
|
||||
defaultColor={decor.border?.color ?? DEFAULT_PAGE_BORDER.color}
|
||||
title={t().pageDecor.border}
|
||||
chevronTitle={t().pageDecor.border}
|
||||
disabled={!decor.border}
|
||||
onApply={(c) => patchBorder({ color: c })}
|
||||
onClear={() => patchBorder({ color: DEFAULT_PAGE_BORDER.color })}
|
||||
/>
|
||||
<span>{t().pageDecor.useBorder}</span>
|
||||
<input type="color" value={decor.border?.color ?? DEFAULT_PAGE_BORDER.color} onchange={(e) => patchBorder({ color: e.currentTarget.value })} />
|
||||
</label>
|
||||
</div>
|
||||
<label class="row">
|
||||
<span class="grow">{t().pageDecor.borderWidth}</span>
|
||||
<input type="number" min="0.25" max="20" step="0.25" disabled={!decor.border}
|
||||
@@ -95,11 +115,19 @@
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label class="row">
|
||||
<div class="row">
|
||||
<span class="grow">{t().pageDecor.watermarkColor}</span>
|
||||
<input type="color" value={decor.watermark?.color ?? DEFAULT_WATERMARK.color} disabled={!decor.watermark}
|
||||
onchange={(e) => patchWatermark({ color: e.currentTarget.value })} />
|
||||
</label>
|
||||
<ColorPicker
|
||||
editor={null}
|
||||
currentColor={decor.watermark?.color ?? DEFAULT_WATERMARK.color}
|
||||
defaultColor={decor.watermark?.color ?? DEFAULT_WATERMARK.color}
|
||||
title={t().pageDecor.watermarkColor}
|
||||
chevronTitle={t().pageDecor.watermarkColor}
|
||||
disabled={!decor.watermark}
|
||||
onApply={(c) => patchWatermark({ color: c })}
|
||||
onClear={() => patchWatermark({ color: DEFAULT_WATERMARK.color })}
|
||||
/>
|
||||
</div>
|
||||
<label class="row">
|
||||
<span class="grow">{t().pageDecor.watermarkAngle}</span>
|
||||
<input type="number" min="-180" max="180" step="5" disabled={!decor.watermark}
|
||||
@@ -153,6 +181,7 @@
|
||||
.row { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
||||
.row .grow { flex: 1; color: var(--color-text-muted); }
|
||||
.row span { color: var(--color-text-muted); }
|
||||
.row label.grow { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
input[type='text'], input[type='number'] {
|
||||
height: 26px;
|
||||
@@ -164,7 +193,6 @@
|
||||
padding: 0 6px;
|
||||
font: inherit;
|
||||
}
|
||||
input[type='color'] { width: 44px; height: 26px; padding: 2px; border: 1px solid var(--color-border); border-radius: var(--radius); background: var(--color-surface); }
|
||||
input:disabled { opacity: 0.5; }
|
||||
|
||||
.actions { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { activeBorderPresets, type BorderPreset } from '../editor/extensions/tableCellBorders';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import ColorPicker from './ColorPicker.svelte';
|
||||
|
||||
let { editor, tick }: { editor: Editor | null; tick: number } = $props();
|
||||
|
||||
@@ -11,12 +12,6 @@
|
||||
let color = $state('#000000');
|
||||
|
||||
const WIDTHS = [0.5, 1, 1.5, 2.25, 3];
|
||||
// Greyscale + standard hues (the first two ColorPicker palette rows).
|
||||
const COLORS = [
|
||||
['#000000', '#1A1A1A', '#333333', '#4D4D4D', '#666666', '#808080', '#999999', '#B3B3B3', '#CCCCCC', '#FFFFFF'],
|
||||
['#C00000', '#FF0000', '#FFC000', '#FFFF00', '#92D050', '#00B050', '#00B0F0', '#0070C0', '#002060', '#7030A0'],
|
||||
];
|
||||
|
||||
// Preset icons: 6 segments (outer edges + the two inner grid lines); "on" segments
|
||||
// draw solid, the rest as faint guides.
|
||||
type Seg = 'top' | 'right' | 'bottom' | 'left' | 'h' | 'v';
|
||||
@@ -136,22 +131,16 @@
|
||||
</div>
|
||||
|
||||
<div class="bp-title">{t().borders.lineColor}</div>
|
||||
<div class="bp-colors">
|
||||
{#each COLORS as row}
|
||||
<div class="bp-color-row">
|
||||
{#each row as c}
|
||||
<button
|
||||
class="bp-color"
|
||||
class:active={color === c}
|
||||
style="background: {c}"
|
||||
title={c}
|
||||
aria-label={c}
|
||||
onclick={() => (color = c)}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<ColorPicker
|
||||
editor={null}
|
||||
currentColor={color}
|
||||
defaultColor={color}
|
||||
title={t().borders.lineColor}
|
||||
chevronTitle={t().borders.lineColor}
|
||||
clearLabel={t().color.automatic}
|
||||
onApply={(c) => (color = c)}
|
||||
onClear={() => (color = '#000000')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -279,34 +268,4 @@
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.bp-colors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bp-color-row {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bp-color {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-width: unset;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.05s;
|
||||
}
|
||||
|
||||
.bp-color:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.bp-color.active {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user