Fix note color swatches outside teleported editor panel.

Use hex palettes so Keep-style colors render when the create panel is portaled to body.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 19:41:39 +03:30
co-authored by Cursor
parent 4e3dbb4e00
commit f43aa125ea
+138 -35
View File
@@ -67,6 +67,7 @@ const draftActivityId = ref('');
const draftReminder = ref('');
const draftLocation = ref('');
const draftAnalyze = ref(true);
const draftPinned = ref(false);
const draftChecklist = ref<NoteChecklistItemDto[]>([]);
const draftFiles = ref<File[]>([]);
const existingAttachments = ref<NoteAttachmentDto[]>([]);
@@ -130,17 +131,18 @@ const draftColorCss = computed(() => colorToCss(draftColor.value));
const personName = (id: string) =>
people.value.find((p) => p.id === id)?.fullName ?? '';
/** Hex only — panel is Teleported outside `.notes-page`, so CSS vars would not inherit. */
const NOTE_COLOR_HEX: Record<ColorKey, string> = {
sand: '#fff8e1',
rose: '#fce4ec',
sky: '#e3f2fd',
mint: '#e0f2f1',
lavender: '#f3e5f5',
default: '#ffffff',
};
function colorToCss(raw: string | null | undefined): string {
const key = normalizeColor(raw);
const map: Record<ColorKey, string> = {
sand: 'var(--note-sand)',
rose: 'var(--note-rose)',
sky: 'var(--note-sky)',
mint: 'var(--note-mint)',
lavender: 'var(--note-lavender)',
default: 'var(--note-default)',
};
return map[key];
return NOTE_COLOR_HEX[normalizeColor(raw)];
}
function normalizeColor(raw: string | null | undefined): ColorKey {
@@ -359,6 +361,7 @@ function resetDraft() {
draftReminder.value = '';
draftLocation.value = '';
draftAnalyze.value = true;
draftPinned.value = false;
draftChecklist.value = [];
draftFiles.value = [];
existingAttachments.value = [];
@@ -381,6 +384,7 @@ function openEditor(mode: OpenMode = 'default', note?: NoteDto) {
draftReminder.value = note.reminderAt ? note.reminderAt.slice(0, 16) : '';
draftLocation.value = note.locationText ?? '';
draftAnalyze.value = note.analyzeEnabled !== false;
draftPinned.value = note.pinned;
draftChecklist.value = [...(note.checklist ?? [])];
draftFiles.value = [];
existingAttachments.value = [...note.attachments];
@@ -496,6 +500,7 @@ async function saveNote() {
: null,
locationText: draftLocation.value.trim() || null,
analyzeEnabled: draftAnalyze.value,
pinned: draftPinned.value,
checklist,
};
const pending = [...draftFiles.value];
@@ -534,6 +539,24 @@ async function togglePin(id: string) {
menuOpenId.value = null;
await api.patch(`/notes/${id}/pin`);
await load();
if (editingId.value === id) {
const n = notes.value.find((x) => x.id === id);
if (n) draftPinned.value = n.pinned;
}
}
async function onPinClick() {
const next = !draftPinned.value;
draftPinned.value = next;
if (!editingId.value) return;
const note = notes.value.find((n) => n.id === editingId.value);
if (note && note.pinned === next) return;
try {
await api.patch(`/notes/${editingId.value}/pin`);
if (note) note.pinned = next;
} catch {
draftPinned.value = !next;
}
}
async function removeNote(id: string) {
@@ -864,7 +887,13 @@ onBeforeUnmount(() => {
class="panel-root desktop-panel"
>
<div class="panel-backdrop" @click="closeEditor" />
<aside class="editor-panel" :style="{ '--canvas': draftColorCss }">
<aside
class="editor-panel"
:style="{
'--canvas': draftColorCss,
background: draftColorCss,
}"
>
<header class="panel-head">
<h2>{{ editorTitle }}</h2>
<button type="button" class="icon-btn" @click="closeEditor">
@@ -887,18 +916,38 @@ onBeforeUnmount(() => {
placeholder="یادداشت خود را بنویسید…"
/>
<div class="swatches">
<div class="palette-row">
<div class="swatches" role="listbox" aria-label="رنگ یادداشت">
<button
v-for="sw in colorSwatches"
:key="sw.value"
type="button"
class="swatch"
:class="[
`tone-${sw.value}`,
{ selected: draftColor === sw.value },
]"
:style="{ backgroundColor: NOTE_COLOR_HEX[sw.value] }"
:title="sw.label"
:aria-label="sw.label"
:aria-selected="draftColor === sw.value"
@click="draftColor = sw.value"
>
<UiIcon
v-if="draftColor === sw.value"
name="check-plain"
:size="14"
/>
</button>
</div>
<button
v-for="sw in colorSwatches"
:key="sw.value"
type="button"
class="swatch"
:class="{ selected: draftColor === sw.value }"
:style="{ background: colorToCss(sw.value) }"
:title="sw.label"
@click="draftColor = sw.value"
class="media-btn pin-btn"
:class="{ on: draftPinned }"
title="سنجاق"
@click="onPinClick"
>
<UiIcon v-if="draftColor === sw.value" name="check-plain" :size="14" />
سنجاق
</button>
</div>
@@ -1059,7 +1108,7 @@ onBeforeUnmount(() => {
<div
v-if="editorOpen && mobileEditor"
class="mobile-editor"
:style="{ '--canvas': draftColorCss }"
:style="{ '--canvas': draftColorCss, background: draftColorCss }"
>
<header class="mob-head">
<button type="button" class="icon-btn" @click="closeEditor">
@@ -1089,17 +1138,34 @@ onBeforeUnmount(() => {
placeholder="یادداشت خود را بنویسید…"
/>
<div class="swatches">
<div class="palette-row">
<div class="swatches" role="listbox" aria-label="رنگ یادداشت">
<button
v-for="sw in colorSwatches"
:key="`m-${sw.value}`"
type="button"
class="swatch"
:class="[`tone-${sw.value}`, { selected: draftColor === sw.value }]"
:style="{ backgroundColor: NOTE_COLOR_HEX[sw.value] }"
:title="sw.label"
:aria-label="sw.label"
:aria-selected="draftColor === sw.value"
@click="draftColor = sw.value"
>
<UiIcon
v-if="draftColor === sw.value"
name="check-plain"
:size="14"
/>
</button>
</div>
<button
v-for="sw in colorSwatches"
:key="`m-${sw.value}`"
type="button"
class="swatch"
:class="{ selected: draftColor === sw.value }"
:style="{ background: colorToCss(sw.value) }"
@click="draftColor = sw.value"
class="media-btn pin-btn"
:class="{ on: draftPinned }"
@click="onPinClick"
>
<UiIcon v-if="draftColor === sw.value" name="check-plain" :size="14" />
سنجاق
</button>
</div>
@@ -1825,23 +1891,60 @@ onBeforeUnmount(() => {
.swatches {
display: flex;
gap: 0.45rem;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.palette-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.65rem;
flex-wrap: wrap;
}
.swatch {
width: 28px;
height: 28px;
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid rgba(15, 23, 42, 0.12);
border: 1.5px solid rgba(15, 23, 42, 0.18);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--teal);
color: #0f766e;
padding: 0;
flex-shrink: 0;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
}
.swatch.tone-default {
border-color: #94a3b8;
background-image: linear-gradient(
45deg,
#f1f5f9 25%,
transparent 25%,
transparent 75%,
#f1f5f9 75%
);
background-size: 8px 8px;
}
.swatch.selected {
box-shadow: 0 0 0 2px var(--teal);
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--teal, #0d9488);
}
.pin-btn.on {
border-color: #f59e0b;
color: #b45309;
background: #fffbeb;
}
.panel-root {
--note-sand: #fff8e1;
--note-rose: #fce4ec;
--note-sky: #e3f2fd;
--note-mint: #e0f2f1;
--note-lavender: #f3e5f5;
--note-default: #ffffff;
--teal: #0d9488;
--teal-soft: #ccfbf1;
}
.media-row {