feat(renderer): add fluid chapter reorder motion
This commit is contained in:
parent
a2d2d4858b
commit
40a2fb305e
11 changed files with 695 additions and 38 deletions
|
|
@ -134,6 +134,14 @@ export function createChapterListFeature(ctx: BookCtx): ChapterListFeature {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChapterList() {
|
function renderChapterList() {
|
||||||
|
const previousPositions = animateNextRender
|
||||||
|
? new Map(
|
||||||
|
Array.from(chapterList.querySelectorAll<HTMLElement>(".chapter-row"), (row) => [
|
||||||
|
row.dataset.id ?? "",
|
||||||
|
row.getBoundingClientRect().top,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: null;
|
||||||
chapterList.innerHTML = "";
|
chapterList.innerHTML = "";
|
||||||
for (const ch of state.ordered) {
|
for (const ch of state.ordered) {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
|
|
@ -190,6 +198,26 @@ export function createChapterListFeature(ctx: BookCtx): ChapterListFeature {
|
||||||
});
|
});
|
||||||
chapterList.appendChild(li);
|
chapterList.appendChild(li);
|
||||||
}
|
}
|
||||||
|
if (!previousPositions) return;
|
||||||
|
animateNextRender = false;
|
||||||
|
const rows = Array.from(chapterList.querySelectorAll<HTMLElement>(".chapter-row"));
|
||||||
|
for (const row of rows) {
|
||||||
|
const oldTop = previousPositions.get(row.dataset.id ?? "");
|
||||||
|
if (oldTop === undefined) continue;
|
||||||
|
const delta = oldTop - row.getBoundingClientRect().top;
|
||||||
|
if (!delta) continue;
|
||||||
|
row.classList.add("reordering");
|
||||||
|
row.style.transform = `translateY(${delta}px)`;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
for (const row of rows) row.style.transform = "";
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const row of rows) {
|
||||||
|
row.classList.remove("reordering");
|
||||||
|
row.style.transform = "";
|
||||||
|
}
|
||||||
|
}, 420);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
|
|
@ -305,6 +333,10 @@ export function createChapterListFeature(ctx: BookCtx): ChapterListFeature {
|
||||||
let dragStartY = 0;
|
let dragStartY = 0;
|
||||||
let dragMoved = false;
|
let dragMoved = false;
|
||||||
let dragJustHappened = false;
|
let dragJustHappened = false;
|
||||||
|
let animateNextRender = false;
|
||||||
|
let dragGhost: HTMLElement | null = null;
|
||||||
|
let dragPlaceholder: HTMLElement | null = null;
|
||||||
|
let dragOffsetY = 0;
|
||||||
|
|
||||||
chapterList.addEventListener("pointerdown", (e) => {
|
chapterList.addEventListener("pointerdown", (e) => {
|
||||||
if (e.button !== 0) return; // left button only
|
if (e.button !== 0) return; // left button only
|
||||||
|
|
@ -327,15 +359,38 @@ export function createChapterListFeature(ctx: BookCtx): ChapterListFeature {
|
||||||
const src = chapterList.querySelector(
|
const src = chapterList.querySelector(
|
||||||
`li.chapter-row[data-id="${draggingId}"]`
|
`li.chapter-row[data-id="${draggingId}"]`
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
src?.classList.add("dragging");
|
if (src) {
|
||||||
|
const rect = src.getBoundingClientRect();
|
||||||
|
dragOffsetY = e.clientY - rect.top;
|
||||||
|
dragGhost = src.cloneNode(true) as HTMLElement;
|
||||||
|
dragGhost.classList.add("drag-ghost");
|
||||||
|
dragGhost.style.width = `${rect.width}px`;
|
||||||
|
dragGhost.style.left = `${rect.left}px`;
|
||||||
|
dragGhost.style.top = `${rect.top}px`;
|
||||||
|
document.body.appendChild(dragGhost);
|
||||||
|
|
||||||
|
dragPlaceholder = document.createElement("li");
|
||||||
|
dragPlaceholder.className = "chapter-drop-placeholder";
|
||||||
|
dragPlaceholder.style.height = `${rect.height}px`;
|
||||||
|
src.replaceWith(dragPlaceholder);
|
||||||
|
}
|
||||||
document.body.classList.add("dragging-active");
|
document.body.classList.add("dragging-active");
|
||||||
}
|
}
|
||||||
|
if (dragGhost) dragGhost.style.top = `${e.clientY - dragOffsetY}px`;
|
||||||
const overEl = document.elementFromPoint(e.clientX, e.clientY);
|
const overEl = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
const overLi = overEl?.closest("li.chapter-row") as HTMLElement | null;
|
const overLi = overEl?.closest("li.chapter-row") as HTMLElement | null;
|
||||||
chapterList
|
chapterList.querySelectorAll(".chapter-row").forEach((n) => {
|
||||||
.querySelectorAll(".chapter-row")
|
n.classList.remove("drop-target", "drop-before", "drop-after");
|
||||||
.forEach((n) => n.classList.remove("drop-target"));
|
});
|
||||||
if (overLi && overLi.dataset.id !== draggingId) overLi.classList.add("drop-target");
|
if (overLi && overLi.dataset.id !== draggingId) {
|
||||||
|
overLi.classList.add("drop-target");
|
||||||
|
const before = e.clientY < overLi.getBoundingClientRect().top + overLi.offsetHeight / 2;
|
||||||
|
overLi.classList.add(before ? "drop-before" : "drop-after");
|
||||||
|
if (dragPlaceholder) {
|
||||||
|
if (before) chapterList.insertBefore(dragPlaceholder, overLi);
|
||||||
|
else chapterList.insertBefore(dragPlaceholder, overLi.nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("pointerup", (e) => {
|
document.addEventListener("pointerup", (e) => {
|
||||||
|
|
@ -347,23 +402,26 @@ export function createChapterListFeature(ctx: BookCtx): ChapterListFeature {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dragJustHappened = true;
|
dragJustHappened = true;
|
||||||
const overEl = document.elementFromPoint(e.clientX, e.clientY);
|
const targetIndex = dragPlaceholder
|
||||||
const overLi = overEl?.closest("li.chapter-row") as HTMLElement | null;
|
? Array.from(chapterList.children).indexOf(dragPlaceholder)
|
||||||
const targetId = overLi?.dataset.id ?? null;
|
: -1;
|
||||||
const src = chapterList.querySelector(
|
dragGhost?.remove();
|
||||||
`li.chapter-row[data-id="${draggingId}"]`
|
dragGhost = null;
|
||||||
) as HTMLElement | null;
|
dragPlaceholder?.remove();
|
||||||
src?.classList.remove("dragging");
|
dragPlaceholder = null;
|
||||||
chapterList
|
chapterList.querySelectorAll(".chapter-row").forEach((n) => {
|
||||||
.querySelectorAll(".chapter-row")
|
n.classList.remove("drop-target", "drop-before", "drop-after");
|
||||||
.forEach((n) => n.classList.remove("drop-target"));
|
});
|
||||||
if (targetId && targetId !== draggingId) {
|
let reordered = false;
|
||||||
|
if (targetIndex >= 0) {
|
||||||
const from = state.ordered.findIndex((c) => c.id === draggingId);
|
const from = state.ordered.findIndex((c) => c.id === draggingId);
|
||||||
const to = state.ordered.findIndex((c) => c.id === targetId);
|
if (from >= 0 && targetIndex !== from) {
|
||||||
if (from >= 0 && to >= 0) {
|
const to = targetIndex;
|
||||||
state.ordered = moveInList(state.ordered, from, to);
|
state.ordered = moveInList(state.ordered, from, to);
|
||||||
state.meta!.chapterOrder = state.ordered.map((c) => c.id);
|
state.meta!.chapterOrder = state.ordered.map((c) => c.id);
|
||||||
|
animateNextRender = true;
|
||||||
renderChapterList();
|
renderChapterList();
|
||||||
|
reordered = true;
|
||||||
// Selecting the dragged row must go through selectChapter: assigning
|
// Selecting the dragged row must go through selectChapter: assigning
|
||||||
// selectedId directly would pair the id with whatever document is
|
// selectedId directly would pair the id with whatever document is
|
||||||
// still in the editor, and the next autosave would write that
|
// still in the editor, and the next autosave would write that
|
||||||
|
|
@ -372,6 +430,10 @@ export function createChapterListFeature(ctx: BookCtx): ChapterListFeature {
|
||||||
void persistOrder();
|
void persistOrder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The source row was replaced by the gap while dragging. Restore the list
|
||||||
|
// even when the pointer is released over its original slot or outside a
|
||||||
|
// valid destination.
|
||||||
|
if (!reordered) renderChapterList();
|
||||||
draggingId = null;
|
draggingId = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
markdownToPlain,
|
markdownToPlain,
|
||||||
openEl,
|
openEl,
|
||||||
setActive,
|
setActive,
|
||||||
|
showView,
|
||||||
showToast,
|
showToast,
|
||||||
} from "./ui.js";
|
} from "./ui.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -127,7 +128,7 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature {
|
||||||
clearTimers();
|
clearTimers();
|
||||||
$("fullBookBtn").classList.remove("active");
|
$("fullBookBtn").classList.remove("active");
|
||||||
$("fullBookView").classList.add("hidden");
|
$("fullBookView").classList.add("hidden");
|
||||||
$("chapterView").classList.remove("hidden");
|
showView($("chapterView"));
|
||||||
$("editorPlaceholder").classList.add("hidden");
|
$("editorPlaceholder").classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,8 +131,10 @@ export function createExportsFeature(ctx: BookCtx): ExportsFeature {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const wasOpen = !exportMenu.classList.contains("hidden");
|
const wasOpen = !exportMenu.classList.contains("hidden");
|
||||||
closeExportMenu();
|
closeExportMenu();
|
||||||
if (!wasOpen) openEl(exportMenu);
|
if (!wasOpen) {
|
||||||
exportMenuToggle.classList.toggle("open", wasOpen);
|
openEl(exportMenu);
|
||||||
|
exportMenuToggle.classList.add("open");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
exportMenu.addEventListener("click", (e) => {
|
exportMenu.addEventListener("click", (e) => {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import type { BookMeta } from "../../main/project.js";
|
||||||
import type { WikilinkTarget } from "../wikilinks.js";
|
import type { WikilinkTarget } from "../wikilinks.js";
|
||||||
import type { BookCtx } from "./types.js";
|
import type { BookCtx } from "./types.js";
|
||||||
import { alertMessage } from "../dialog.js";
|
import { alertMessage } from "../dialog.js";
|
||||||
import { $, countWords } from "./ui.js";
|
import { $, countWords, showView } from "./ui.js";
|
||||||
import {
|
import {
|
||||||
createTiptapEditor,
|
createTiptapEditor,
|
||||||
setEditorDoc,
|
setEditorDoc,
|
||||||
|
|
@ -195,7 +195,7 @@ export function createFullBookFeature(ctx: BookCtx): FullBookFeature {
|
||||||
$("fullBookBtn").classList.add("active");
|
$("fullBookBtn").classList.add("active");
|
||||||
$("chapterView").classList.add("hidden");
|
$("chapterView").classList.add("hidden");
|
||||||
$("editorPlaceholder").classList.add("hidden");
|
$("editorPlaceholder").classList.add("hidden");
|
||||||
fullBookView.classList.remove("hidden");
|
showView(fullBookView);
|
||||||
await renderFullBook();
|
await renderFullBook();
|
||||||
fullBookEditor.commands.focus();
|
fullBookEditor.commands.focus();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,16 @@ export function openEl(el: HTMLElement) {
|
||||||
el.classList.remove("hidden", "closing");
|
el.classList.remove("hidden", "closing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reveal a primary surface with a short continuity cue. The surface remains
|
||||||
|
// usable before the animation starts, and the class is removed so later
|
||||||
|
// updates do not replay it accidentally.
|
||||||
|
export function showView(el: HTMLElement) {
|
||||||
|
el.classList.remove("hidden", "view-enter");
|
||||||
|
void el.offsetWidth;
|
||||||
|
el.classList.add("view-enter");
|
||||||
|
el.addEventListener("animationend", () => el.classList.remove("view-enter"), { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
export function closeEl(el: HTMLElement | null) {
|
export function closeEl(el: HTMLElement | null) {
|
||||||
if (!el || el.classList.contains("hidden")) return;
|
if (!el || el.classList.contains("hidden")) return;
|
||||||
el.classList.add("closing");
|
el.classList.add("closing");
|
||||||
|
|
@ -35,7 +45,11 @@ export function closeEl(el: HTMLElement | null) {
|
||||||
el.removeEventListener("animationend", onEnd);
|
el.removeEventListener("animationend", onEnd);
|
||||||
delete (el as unknown as { __close?: () => void }).__close;
|
delete (el as unknown as { __close?: () => void }).__close;
|
||||||
};
|
};
|
||||||
const onEnd = () => finish();
|
const onEnd = (event: AnimationEvent) => {
|
||||||
|
// The modal animation bubbles through its backdrop; only the backdrop's
|
||||||
|
// own exit should decide when the overlay is safe to hide.
|
||||||
|
if (event.target === el) finish();
|
||||||
|
};
|
||||||
el.addEventListener("animationend", onEnd);
|
el.addEventListener("animationend", onEnd);
|
||||||
const timer = setTimeout(finish, 420);
|
const timer = setTimeout(finish, 420);
|
||||||
(el as unknown as { __close?: () => void }).__close = () => {
|
(el as unknown as { __close?: () => void }).__close = () => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import type { BookCtx } from "./types.js";
|
||||||
import type { ChapterEntry, Recent } from "./types.js";
|
import type { ChapterEntry, Recent } from "./types.js";
|
||||||
import type { BookMeta } from "../../main/project.js";
|
import type { BookMeta } from "../../main/project.js";
|
||||||
import { alertMessage, promptInput } from "../dialog.js";
|
import { alertMessage, promptInput } from "../dialog.js";
|
||||||
import { $, showUndoToast } from "./ui.js";
|
import { $, showUndoToast, showView } from "./ui.js";
|
||||||
|
|
||||||
export interface WelcomeFeature {
|
export interface WelcomeFeature {
|
||||||
showWelcome(): Promise<void>;
|
showWelcome(): Promise<void>;
|
||||||
|
|
@ -37,7 +37,7 @@ export function createWelcomeFeature(ctx: BookCtx): WelcomeFeature {
|
||||||
const homeBtn = $("homeBtn") as HTMLButtonElement | null;
|
const homeBtn = $("homeBtn") as HTMLButtonElement | null;
|
||||||
|
|
||||||
async function showWelcome() {
|
async function showWelcome() {
|
||||||
welcomeEl.classList.remove("hidden");
|
showView(welcomeEl);
|
||||||
chapterView.classList.add("hidden");
|
chapterView.classList.add("hidden");
|
||||||
fullBookView.classList.add("hidden");
|
fullBookView.classList.add("hidden");
|
||||||
editorPlaceholder.classList.add("hidden");
|
editorPlaceholder.classList.add("hidden");
|
||||||
|
|
@ -69,7 +69,7 @@ export function createWelcomeFeature(ctx: BookCtx): WelcomeFeature {
|
||||||
chapterView.classList.add("hidden");
|
chapterView.classList.add("hidden");
|
||||||
fullBookView.classList.add("hidden");
|
fullBookView.classList.add("hidden");
|
||||||
fullBookBtn.classList.remove("active");
|
fullBookBtn.classList.remove("active");
|
||||||
editorPlaceholder.classList.remove("hidden");
|
showView(editorPlaceholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentBook(data: { path: string; title: string }) {
|
function setCurrentBook(data: { path: string; title: string }) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// In-app modal dialog used in place of window.prompt/confirm/alert,
|
// In-app modal dialog used in place of window.prompt/confirm/alert,
|
||||||
// which Electron does not support (prompt() throws).
|
// which Electron does not support (prompt() throws).
|
||||||
|
import { closeEl, openEl } from "./book/ui.js";
|
||||||
|
|
||||||
type Mode = "prompt" | "confirm" | "alert";
|
type Mode = "prompt" | "confirm" | "alert";
|
||||||
|
|
||||||
function el(id: string): HTMLElement {
|
function el(id: string): HTMLElement {
|
||||||
|
|
@ -29,7 +31,7 @@ function open(mode: Mode, title: string, body: string, value: string): Promise<s
|
||||||
msgEl.classList.toggle("hidden", mode === "prompt");
|
msgEl.classList.toggle("hidden", mode === "prompt");
|
||||||
cancelBtn.classList.toggle("hidden", mode === "alert");
|
cancelBtn.classList.toggle("hidden", mode === "alert");
|
||||||
okBtn.textContent = mode === "confirm" ? "Delete" : "OK";
|
okBtn.textContent = mode === "confirm" ? "Delete" : "OK";
|
||||||
backdrop.classList.remove("hidden");
|
openEl(backdrop);
|
||||||
if (mode === "prompt") {
|
if (mode === "prompt") {
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
inputEl.select();
|
inputEl.select();
|
||||||
|
|
@ -42,7 +44,7 @@ function open(mode: Mode, title: string, body: string, value: string): Promise<s
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(value: string | null): void {
|
function close(value: string | null): void {
|
||||||
backdrop.classList.add("hidden");
|
closeEl(backdrop);
|
||||||
if (resolver) {
|
if (resolver) {
|
||||||
const r = resolver;
|
const r = resolver;
|
||||||
resolver = null;
|
resolver = null;
|
||||||
|
|
@ -88,4 +90,4 @@ export function confirmMessage(title: string, body?: string): Promise<boolean> {
|
||||||
|
|
||||||
export function alertMessage(title: string, body?: string): Promise<void> {
|
export function alertMessage(title: string, body?: string): Promise<void> {
|
||||||
return open("alert", title, body ?? "", "").then(() => undefined);
|
return open("alert", title, body ?? "", "").then(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,9 @@ html, body {
|
||||||
:root[data-theme="dark-mode"] #addChapterBtn,
|
:root[data-theme="dark-mode"] #addChapterBtn,
|
||||||
:root[data-theme="neon"] #addChapterBtn,
|
:root[data-theme="neon"] #addChapterBtn,
|
||||||
:root[data-theme="meridian"] #addChapterBtn,
|
:root[data-theme="meridian"] #addChapterBtn,
|
||||||
:root[data-theme="impeccable-dark"] #addChapterBtn {
|
:root[data-theme="impeccable-dark"] #addChapterBtn,
|
||||||
|
:root[data-theme="cosmic"] #addChapterBtn,
|
||||||
|
:root[data-theme="cosmic-stardust"] #addChapterBtn {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,7 +405,9 @@ html, body {
|
||||||
:root[data-theme="dark-mode"] #homeBtn .home-label,
|
:root[data-theme="dark-mode"] #homeBtn .home-label,
|
||||||
:root[data-theme="neon"] #homeBtn .home-label,
|
:root[data-theme="neon"] #homeBtn .home-label,
|
||||||
:root[data-theme="meridian"] #homeBtn .home-label,
|
:root[data-theme="meridian"] #homeBtn .home-label,
|
||||||
:root[data-theme="impeccable-dark"] #homeBtn .home-label {
|
:root[data-theme="impeccable-dark"] #homeBtn .home-label,
|
||||||
|
:root[data-theme="cosmic"] #homeBtn .home-label,
|
||||||
|
:root[data-theme="cosmic-stardust"] #homeBtn .home-label {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -422,7 +426,11 @@ html, body {
|
||||||
:root[data-theme="meridian"] .brand-icon-light,
|
:root[data-theme="meridian"] .brand-icon-light,
|
||||||
:root[data-theme="meridian"] .welcome-brand-light,
|
:root[data-theme="meridian"] .welcome-brand-light,
|
||||||
:root[data-theme="impeccable-dark"] .brand-icon-light,
|
:root[data-theme="impeccable-dark"] .brand-icon-light,
|
||||||
:root[data-theme="impeccable-dark"] .welcome-brand-light {
|
:root[data-theme="impeccable-dark"] .welcome-brand-light,
|
||||||
|
:root[data-theme="cosmic"] .brand-icon-light,
|
||||||
|
:root[data-theme="cosmic"] .welcome-brand-light,
|
||||||
|
:root[data-theme="cosmic-stardust"] .brand-icon-light,
|
||||||
|
:root[data-theme="cosmic-stardust"] .welcome-brand-light {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
:root[data-theme="dark-mode"] .brand-icon-dark,
|
:root[data-theme="dark-mode"] .brand-icon-dark,
|
||||||
|
|
@ -432,7 +440,11 @@ html, body {
|
||||||
:root[data-theme="meridian"] .brand-icon-dark,
|
:root[data-theme="meridian"] .brand-icon-dark,
|
||||||
:root[data-theme="meridian"] .welcome-brand-dark,
|
:root[data-theme="meridian"] .welcome-brand-dark,
|
||||||
:root[data-theme="impeccable-dark"] .brand-icon-dark,
|
:root[data-theme="impeccable-dark"] .brand-icon-dark,
|
||||||
:root[data-theme="impeccable-dark"] .welcome-brand-dark {
|
:root[data-theme="impeccable-dark"] .welcome-brand-dark,
|
||||||
|
:root[data-theme="cosmic"] .brand-icon-dark,
|
||||||
|
:root[data-theme="cosmic"] .welcome-brand-dark,
|
||||||
|
:root[data-theme="cosmic-stardust"] .brand-icon-dark,
|
||||||
|
:root[data-theme="cosmic-stardust"] .welcome-brand-dark {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -582,10 +594,50 @@ body.dragging-active * {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drag-ghost {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1001;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--color-panel);
|
||||||
|
border: 1px solid var(--color-accent);
|
||||||
|
box-shadow: 0 10px 24px var(--color-shadow-strong);
|
||||||
|
transform: rotate(1deg) scale(1.02);
|
||||||
|
transition: transform 140ms var(--ease-out), box-shadow 140ms var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-drop-placeholder {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px dashed var(--color-accent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.chapter-row.drop-target {
|
.chapter-row.drop-target {
|
||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
background: var(--color-accent);
|
background: color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||||
color: var(--color-on-accent);
|
}
|
||||||
|
|
||||||
|
/* Keep the destination edge visible while dragging. The FLIP pass carries the
|
||||||
|
rows into their new positions after release. */
|
||||||
|
.chapter-row.drop-before {
|
||||||
|
background-image: linear-gradient(var(--color-accent), var(--color-accent));
|
||||||
|
background-size: 100% 3px;
|
||||||
|
background-position: top;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-row.drop-after {
|
||||||
|
background-image: linear-gradient(var(--color-accent), var(--color-accent));
|
||||||
|
background-size: 100% 3px;
|
||||||
|
background-position: bottom;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-row.reordering {
|
||||||
|
transition: transform 360ms var(--ease-out), background var(--motion-fast) var(--ease-out),
|
||||||
|
color var(--motion-fast) var(--ease-out), border-color var(--motion-fast) var(--ease-out);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-row:focus-visible {
|
.chapter-row:focus-visible {
|
||||||
|
|
@ -1771,6 +1823,10 @@ body.focus-mode .editor {
|
||||||
from { opacity: 0; transform: scale(0.985); }
|
from { opacity: 0; transform: scale(0.985); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
@keyframes viewIn {
|
||||||
|
from { opacity: 0.72; transform: translateY(5px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Dialogs / modals */
|
/* Dialogs / modals */
|
||||||
.modal-backdrop:not(.hidden) .modal {
|
.modal-backdrop:not(.hidden) .modal {
|
||||||
|
|
@ -1803,6 +1859,12 @@ body.focus-mode .editor {
|
||||||
animation: rootIn var(--motion-window) var(--ease-out) both;
|
animation: rootIn var(--motion-window) var(--ease-out) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Primary navigation changes keep the manuscript's spatial continuity without
|
||||||
|
delaying input or hiding the destination before it is ready. */
|
||||||
|
.view-enter {
|
||||||
|
animation: viewIn var(--motion-base) var(--ease-out) both;
|
||||||
|
}
|
||||||
|
|
||||||
/* List item entrances */
|
/* List item entrances */
|
||||||
.recent-list .recent-item {
|
.recent-list .recent-item {
|
||||||
animation: listIn var(--motion-base) var(--ease-out) both;
|
animation: listIn var(--motion-base) var(--ease-out) both;
|
||||||
|
|
@ -1865,7 +1927,7 @@ body:not(.dragging-active) .chapter-row {
|
||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
.modal, .modal-backdrop, .dropdown-menu, .color-popover,
|
.modal, .modal-backdrop, .dropdown-menu, .color-popover,
|
||||||
.recent-item, .chapter-row, #root {
|
.recent-item, .chapter-row, .view-enter, #root {
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@
|
||||||
<option value="meridian">Meridian</option>
|
<option value="meridian">Meridian</option>
|
||||||
<option value="impeccable">Impeccable Light</option>
|
<option value="impeccable">Impeccable Light</option>
|
||||||
<option value="impeccable-dark">Impeccable Dark</option>
|
<option value="impeccable-dark">Impeccable Dark</option>
|
||||||
|
<option value="cosmic">Cosmic</option>
|
||||||
|
<option value="cosmic-light">Cosmic Light</option>
|
||||||
|
<option value="cosmic-stardust">Cosmic Stardust</option>
|
||||||
|
<option value="cosmic-stardust-light">Cosmic Stardust Light</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const LIGHT = "light";
|
||||||
// Themes with a real entry in themes.css + the selector. Anything else that
|
// Themes with a real entry in themes.css + the selector. Anything else that
|
||||||
// might linger in saved settings (a removed theme, a typo) falls back to
|
// might linger in saved settings (a removed theme, a typo) falls back to
|
||||||
// Light so the <select> never desyncs from its <option> list.
|
// Light so the <select> never desyncs from its <option> list.
|
||||||
const KNOWN = new Set(["light", "dark-mode", "neon", "meridian", "impeccable", "impeccable-dark"]);
|
const KNOWN = new Set(["light", "dark-mode", "neon", "meridian", "impeccable", "impeccable-dark", "cosmic", "cosmic-light", "cosmic-stardust", "cosmic-stardust-light"]);
|
||||||
|
|
||||||
function normalizeTheme(id: unknown): string {
|
function normalizeTheme(id: unknown): string {
|
||||||
if (typeof id !== "string" || !id || id === "default") return LIGHT;
|
if (typeof id !== "string" || !id || id === "default") return LIGHT;
|
||||||
|
|
|
||||||
|
|
@ -379,4 +379,514 @@ html[data-theme="impeccable-dark"] .sidebar {
|
||||||
html[data-theme="impeccable-dark"] .chapter-list li.is-active {
|
html[data-theme="impeccable-dark"] .chapter-list li.is-active {
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
border-left-color: var(--color-accent);
|
border-left-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cosmic (Original/Dark) ----
|
||||||
|
Warm, atmospheric dark theme inspired by painted night skies.
|
||||||
|
Deep purple-brown surfaces with rose/gold accents and colorful headings. */
|
||||||
|
:root[data-theme="cosmic"] {
|
||||||
|
--color-bg: #0a0d17;
|
||||||
|
--color-panel: #060910;
|
||||||
|
--color-border: #2a2040;
|
||||||
|
--color-text: #f4ebec;
|
||||||
|
--color-muted: #b8a5cc;
|
||||||
|
--color-accent: #b4797f;
|
||||||
|
--color-accent-hover: #c57e89;
|
||||||
|
|
||||||
|
--color-on-accent: #020412;
|
||||||
|
--color-on-accent-soft: rgba(2, 4, 18, 0.18);
|
||||||
|
--color-on-accent-border: rgba(2, 4, 18, 0.38);
|
||||||
|
|
||||||
|
--color-selection: rgba(180, 121, 127, 0.28);
|
||||||
|
--color-selection-strong: rgba(180, 121, 127, 0.55);
|
||||||
|
|
||||||
|
--color-shadow: rgba(0, 0, 0, 0.55);
|
||||||
|
--color-shadow-strong: rgba(0, 0, 0, 0.7);
|
||||||
|
--color-backdrop: rgba(2, 4, 18, 0.6);
|
||||||
|
|
||||||
|
--color-scrollbar: #2a1838;
|
||||||
|
--color-scrollbar-hover: #45285e;
|
||||||
|
|
||||||
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-serif: "Tirra", "Source Serif 4", Georgia, serif;
|
||||||
|
--font-display: "Yeseva One", "Cormorant Garamond", Georgia, serif;
|
||||||
|
|
||||||
|
--color-h1: #dda2a1;
|
||||||
|
--color-h2: #97c7dc;
|
||||||
|
--color-h3: #d8c2e3;
|
||||||
|
--color-h4: #d8c2e3;
|
||||||
|
--color-h5: #97c7dc;
|
||||||
|
--color-h6: #f2def2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="cosmic"] h1,
|
||||||
|
html[data-theme="cosmic"] h2,
|
||||||
|
html[data-theme="cosmic"] h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic"] h1 { color: var(--color-h1); }
|
||||||
|
html[data-theme="cosmic"] h2 { color: var(--color-h2); }
|
||||||
|
html[data-theme="cosmic"] h3 { color: var(--color-h3); }
|
||||||
|
html[data-theme="cosmic"] h4 { color: var(--color-h4); }
|
||||||
|
html[data-theme="cosmic"] h5 { color: var(--color-h5); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.92em; }
|
||||||
|
html[data-theme="cosmic"] h6 { color: var(--color-h6); text-transform: uppercase; letter-spacing: 0.12em; }
|
||||||
|
|
||||||
|
html[data-theme="cosmic"] .sidebar {
|
||||||
|
border-right-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic"] .chapter-list li.is-active {
|
||||||
|
background: linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent);
|
||||||
|
color: var(--color-text);
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-accent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic"] .button.button-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 74%, var(--color-h2)));
|
||||||
|
color: var(--color-on-accent);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 8px 22px color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic"] .button.button-primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.08) saturate(1.08);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic"] .button.button-ghost {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic"] input[type="text"],
|
||||||
|
html[data-theme="cosmic"] input[type="search"],
|
||||||
|
html[data-theme="cosmic"] select,
|
||||||
|
html[data-theme="cosmic"] textarea,
|
||||||
|
html[data-theme="cosmic"] .modal-input {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic"] code,
|
||||||
|
html[data-theme="cosmic"] .folio-prose code {
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 32%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent) 52%, var(--color-h4));
|
||||||
|
color: var(--color-h1) !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cosmic Light ----
|
||||||
|
Light variant of the Cosmic theme. Warm cream surfaces with deep rose accents. */
|
||||||
|
:root[data-theme="cosmic-light"] {
|
||||||
|
--color-bg: #f0e8f4;
|
||||||
|
--color-panel: #f4ebec;
|
||||||
|
--color-border: #d8cce8;
|
||||||
|
--color-text: #201b43;
|
||||||
|
--color-muted: #3a2e5f;
|
||||||
|
--color-accent: #8b526c;
|
||||||
|
--color-accent-hover: #7e4265;
|
||||||
|
|
||||||
|
--color-on-accent: #f4ebec;
|
||||||
|
--color-on-accent-soft: rgba(244, 235, 236, 0.18);
|
||||||
|
--color-on-accent-border: rgba(244, 235, 236, 0.38);
|
||||||
|
|
||||||
|
--color-selection: rgba(139, 82, 108, 0.25);
|
||||||
|
--color-selection-strong: rgba(139, 82, 108, 0.45);
|
||||||
|
|
||||||
|
--color-shadow: rgba(32, 27, 67, 0.15);
|
||||||
|
--color-shadow-strong: rgba(32, 27, 67, 0.25);
|
||||||
|
--color-backdrop: rgba(32, 27, 67, 0.4);
|
||||||
|
|
||||||
|
--color-scrollbar: #d8cce8;
|
||||||
|
--color-scrollbar-hover: #c8b8d8;
|
||||||
|
|
||||||
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-serif: "Tirra", "Source Serif 4", Georgia, serif;
|
||||||
|
--font-display: "Yeseva One", "Cormorant Garamond", Georgia, serif;
|
||||||
|
|
||||||
|
--color-h1: #8b526c;
|
||||||
|
--color-h2: #30506f;
|
||||||
|
--color-h3: #593670;
|
||||||
|
--color-h4: #593670;
|
||||||
|
--color-h5: #30506f;
|
||||||
|
--color-h6: #412e5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="cosmic-light"] h1,
|
||||||
|
html[data-theme="cosmic-light"] h2,
|
||||||
|
html[data-theme="cosmic-light"] h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-light"] h1 { color: var(--color-h1); }
|
||||||
|
html[data-theme="cosmic-light"] h2 { color: var(--color-h2); }
|
||||||
|
html[data-theme="cosmic-light"] h3 { color: var(--color-h3); }
|
||||||
|
html[data-theme="cosmic-light"] h4 { color: var(--color-h4); }
|
||||||
|
html[data-theme="cosmic-light"] h5 { color: var(--color-h5); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.92em; }
|
||||||
|
html[data-theme="cosmic-light"] h6 { color: var(--color-h6); text-transform: uppercase; letter-spacing: 0.12em; }
|
||||||
|
|
||||||
|
html[data-theme="cosmic-light"] .sidebar {
|
||||||
|
border-right-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-light"] .chapter-list li.is-active {
|
||||||
|
background: linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent);
|
||||||
|
color: var(--color-text);
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-accent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-light"] .button.button-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 74%, var(--color-h2)));
|
||||||
|
color: var(--color-on-accent);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 8px 22px color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-light"] .button.button-primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.08) saturate(1.08);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-light"] .button.button-ghost {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-light"] input[type="text"],
|
||||||
|
html[data-theme="cosmic-light"] input[type="search"],
|
||||||
|
html[data-theme="cosmic-light"] select,
|
||||||
|
html[data-theme="cosmic-light"] textarea,
|
||||||
|
html[data-theme="cosmic-light"] .modal-input {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-light"] code,
|
||||||
|
html[data-theme="cosmic-light"] .folio-prose code {
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-h1) !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cosmic Stardust (Dark) ----
|
||||||
|
Warm amber/gold dark theme. Deep brown-black surfaces with gold accents,
|
||||||
|
teal headings, and coral highlights. Inspired by stardust and cosmic dust clouds. */
|
||||||
|
:root[data-theme="cosmic-stardust"] {
|
||||||
|
--color-bg: #171109;
|
||||||
|
--color-panel: #0d0a06;
|
||||||
|
--color-border: #2a2210;
|
||||||
|
--color-text: #fff5dc;
|
||||||
|
--color-muted: #d5bd8d;
|
||||||
|
--color-accent: #efbd62;
|
||||||
|
--color-accent-hover: #ffdb91;
|
||||||
|
|
||||||
|
--color-on-accent: #0d0a06;
|
||||||
|
--color-on-accent-soft: rgba(13, 10, 6, 0.18);
|
||||||
|
--color-on-accent-border: rgba(13, 10, 6, 0.38);
|
||||||
|
|
||||||
|
--color-selection: rgba(239, 189, 98, 0.3);
|
||||||
|
--color-selection-strong: rgba(239, 189, 98, 0.55);
|
||||||
|
|
||||||
|
--color-shadow: rgba(0, 0, 0, 0.6);
|
||||||
|
--color-shadow-strong: rgba(0, 0, 0, 0.75);
|
||||||
|
--color-backdrop: rgba(13, 10, 6, 0.65);
|
||||||
|
|
||||||
|
--color-scrollbar: #2a2210;
|
||||||
|
--color-scrollbar-hover: #403518;
|
||||||
|
|
||||||
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-serif: "Tirra", "Source Serif 4", Georgia, serif;
|
||||||
|
--font-display: "Yeseva One", "Cormorant Garamond", Georgia, serif;
|
||||||
|
|
||||||
|
--color-h1: #efbd62;
|
||||||
|
--color-h2: #74d7cb;
|
||||||
|
--color-h3: #f08e7d;
|
||||||
|
--color-h4: #d89ce7;
|
||||||
|
--color-h5: #79c7ef;
|
||||||
|
--color-h6: #f5ddae;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="cosmic-stardust"] h1,
|
||||||
|
html[data-theme="cosmic-stardust"] h2,
|
||||||
|
html[data-theme="cosmic-stardust"] h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust"] h1 { color: var(--color-h1); }
|
||||||
|
html[data-theme="cosmic-stardust"] h2 { color: var(--color-h2); }
|
||||||
|
html[data-theme="cosmic-stardust"] h3 { color: var(--color-h3); }
|
||||||
|
html[data-theme="cosmic-stardust"] h4 { color: var(--color-h4); }
|
||||||
|
html[data-theme="cosmic-stardust"] h5 { color: var(--color-h5); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.92em; }
|
||||||
|
html[data-theme="cosmic-stardust"] h6 { color: var(--color-h6); text-transform: uppercase; letter-spacing: 0.12em; }
|
||||||
|
|
||||||
|
html[data-theme="cosmic-stardust"] .sidebar {
|
||||||
|
border-right-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust"] .chapter-list li.is-active {
|
||||||
|
background: linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent);
|
||||||
|
color: var(--color-text);
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-accent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust"] .button.button-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 74%, var(--color-h2)));
|
||||||
|
color: var(--color-on-accent);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 8px 22px color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust"] .button.button-primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.08) saturate(1.08);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust"] .button.button-ghost {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust"] input[type="text"],
|
||||||
|
html[data-theme="cosmic-stardust"] input[type="search"],
|
||||||
|
html[data-theme="cosmic-stardust"] select,
|
||||||
|
html[data-theme="cosmic-stardust"] textarea,
|
||||||
|
html[data-theme="cosmic-stardust"] .modal-input {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust"] code,
|
||||||
|
html[data-theme="cosmic-stardust"] .folio-prose code {
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 32%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent) 52%, var(--color-h4));
|
||||||
|
color: var(--color-h1) !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cosmic Stardust Light ----
|
||||||
|
Light variant of Stardust. Warm cream/parchment surfaces with deep gold accents,
|
||||||
|
teal headings, and terracotta highlights. */
|
||||||
|
:root[data-theme="cosmic-stardust-light"] {
|
||||||
|
--color-bg: #fff8e8;
|
||||||
|
--color-panel: #f5ead2;
|
||||||
|
--color-border: #e8d8b8;
|
||||||
|
--color-text: #44341e;
|
||||||
|
--color-muted: #786344;
|
||||||
|
--color-accent: #8a5b15;
|
||||||
|
--color-accent-hover: #633d0b;
|
||||||
|
|
||||||
|
--color-on-accent: #fff8e8;
|
||||||
|
--color-on-accent-soft: rgba(255, 248, 232, 0.18);
|
||||||
|
--color-on-accent-border: rgba(255, 248, 232, 0.38);
|
||||||
|
|
||||||
|
--color-selection: rgba(138, 91, 21, 0.25);
|
||||||
|
--color-selection-strong: rgba(138, 91, 21, 0.45);
|
||||||
|
|
||||||
|
--color-shadow: rgba(68, 52, 30, 0.15);
|
||||||
|
--color-shadow-strong: rgba(68, 52, 30, 0.25);
|
||||||
|
--color-backdrop: rgba(68, 52, 30, 0.4);
|
||||||
|
|
||||||
|
--color-scrollbar: #d8c8a8;
|
||||||
|
--color-scrollbar-hover: #c8b898;
|
||||||
|
|
||||||
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-serif: "Tirra", "Source Serif 4", Georgia, serif;
|
||||||
|
--font-display: "Yeseva One", "Cormorant Garamond", Georgia, serif;
|
||||||
|
|
||||||
|
--color-h1: #8a5b15;
|
||||||
|
--color-h2: #24756f;
|
||||||
|
--color-h3: #a54c3f;
|
||||||
|
--color-h4: #754c7e;
|
||||||
|
--color-h5: #2e668b;
|
||||||
|
--color-h6: #765525;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="cosmic-stardust-light"] h1,
|
||||||
|
html[data-theme="cosmic-stardust-light"] h2,
|
||||||
|
html[data-theme="cosmic-stardust-light"] h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust-light"] h1 { color: var(--color-h1); }
|
||||||
|
html[data-theme="cosmic-stardust-light"] h2 { color: var(--color-h2); }
|
||||||
|
html[data-theme="cosmic-stardust-light"] h3 { color: var(--color-h3); }
|
||||||
|
html[data-theme="cosmic-stardust-light"] h4 { color: var(--color-h4); }
|
||||||
|
html[data-theme="cosmic-stardust-light"] h5 { color: var(--color-h5); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.92em; }
|
||||||
|
html[data-theme="cosmic-stardust-light"] h6 { color: var(--color-h6); text-transform: uppercase; letter-spacing: 0.12em; }
|
||||||
|
|
||||||
|
html[data-theme="cosmic-stardust-light"] .sidebar {
|
||||||
|
border-right-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust-light"] .chapter-list li.is-active {
|
||||||
|
background: linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent);
|
||||||
|
color: var(--color-text);
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-accent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust-light"] .button.button-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 74%, var(--color-h2)));
|
||||||
|
color: var(--color-on-accent);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 8px 22px color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust-light"] .button.button-primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.08) saturate(1.08);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust-light"] .button.button-ghost {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust-light"] input[type="text"],
|
||||||
|
html[data-theme="cosmic-stardust-light"] input[type="search"],
|
||||||
|
html[data-theme="cosmic-stardust-light"] select,
|
||||||
|
html[data-theme="cosmic-stardust-light"] textarea,
|
||||||
|
html[data-theme="cosmic-stardust-light"] .modal-input {
|
||||||
|
background: var(--color-panel);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html[data-theme="cosmic-stardust-light"] code,
|
||||||
|
html[data-theme="cosmic-stardust-light"] .folio-prose code {
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-h1) !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cosmic Glass Effects ----
|
||||||
|
Meridian-style frosted glass for all Cosmic themes. Each theme gets
|
||||||
|
its own glass textures (grain + sheen) tuned to its palette. */
|
||||||
|
|
||||||
|
:root[data-theme="cosmic"],
|
||||||
|
:root[data-theme="cosmic-light"],
|
||||||
|
:root[data-theme="cosmic-stardust"],
|
||||||
|
:root[data-theme="cosmic-stardust-light"] {
|
||||||
|
--glass-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="cosmic"] {
|
||||||
|
--glass-sheen:
|
||||||
|
radial-gradient(900px 620px at 82% -18%, rgba(212, 158, 154, 0.18), transparent 62%),
|
||||||
|
radial-gradient(820px 640px at -28% 118%, rgba(151, 199, 220, 0.16), transparent 60%),
|
||||||
|
radial-gradient(720px 520px at 50% 45%, rgba(216, 194, 227, 0.12), transparent 65%);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-light"] {
|
||||||
|
--glass-sheen:
|
||||||
|
radial-gradient(900px 620px at 82% -18%, rgba(139, 82, 108, 0.14), transparent 62%),
|
||||||
|
radial-gradient(820px 640px at -28% 118%, rgba(48, 80, 111, 0.12), transparent 60%),
|
||||||
|
radial-gradient(720px 520px at 50% 45%, rgba(89, 54, 112, 0.1), transparent 65%);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-stardust"] {
|
||||||
|
--glass-sheen:
|
||||||
|
radial-gradient(900px 620px at 82% -18%, rgba(239, 189, 98, 0.18), transparent 62%),
|
||||||
|
radial-gradient(820px 640px at -28% 118%, rgba(116, 215, 203, 0.16), transparent 60%),
|
||||||
|
radial-gradient(720px 520px at 50% 45%, rgba(240, 142, 125, 0.12), transparent 65%);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-stardust-light"] {
|
||||||
|
--glass-sheen:
|
||||||
|
radial-gradient(900px 620px at 82% -18%, rgba(138, 91, 21, 0.14), transparent 62%),
|
||||||
|
radial-gradient(820px 640px at -28% 118%, rgba(36, 117, 111, 0.12), transparent 60%),
|
||||||
|
radial-gradient(720px 520px at 50% 45%, rgba(165, 76, 63, 0.1), transparent 65%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient background layer for dark cosmic themes */
|
||||||
|
:root[data-theme="cosmic"] body {
|
||||||
|
background:
|
||||||
|
var(--glass-grain),
|
||||||
|
radial-gradient(1200px 800px at 85% -10%, rgba(212, 158, 154, 0.22), transparent 60%),
|
||||||
|
radial-gradient(1000px 750px at -10% 110%, rgba(151, 199, 220, 0.18), transparent 58%),
|
||||||
|
radial-gradient(720px 520px at 50% 45%, rgba(216, 194, 227, 0.1), transparent 65%),
|
||||||
|
linear-gradient(170deg, #060910, #0a0d17 55%, #080b12) fixed;
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-stardust"] body {
|
||||||
|
background:
|
||||||
|
var(--glass-grain),
|
||||||
|
radial-gradient(1200px 800px at 85% -10%, rgba(239, 189, 98, 0.22), transparent 60%),
|
||||||
|
radial-gradient(1000px 750px at -10% 110%, rgba(116, 215, 203, 0.18), transparent 58%),
|
||||||
|
radial-gradient(720px 520px at 50% 45%, rgba(240, 142, 125, 0.1), transparent 65%),
|
||||||
|
linear-gradient(170deg, #0d0a06, #171109 55%, #120d08) fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome frosting — sidebar, modals, popovers */
|
||||||
|
:root[data-theme="cosmic"] .sidebar,
|
||||||
|
:root[data-theme="cosmic-stardust"] .sidebar {
|
||||||
|
background-image: var(--glass-grain), var(--glass-sheen);
|
||||||
|
-webkit-backdrop-filter: blur(12px) saturate(115%);
|
||||||
|
backdrop-filter: blur(12px) saturate(115%);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic"] .modal,
|
||||||
|
:root[data-theme="cosmic"] .color-popover,
|
||||||
|
:root[data-theme="cosmic-stardust"] .modal,
|
||||||
|
:root[data-theme="cosmic-stardust"] .color-popover {
|
||||||
|
background-image: var(--glass-grain), var(--glass-sheen);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(115%);
|
||||||
|
backdrop-filter: blur(16px) saturate(115%);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic"] .modal,
|
||||||
|
:root[data-theme="cosmic"] .color-popover,
|
||||||
|
:root[data-theme="cosmic-stardust"] .modal,
|
||||||
|
:root[data-theme="cosmic-stardust"] .color-popover {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.09),
|
||||||
|
0 8px 16px var(--color-shadow-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light cosmic themes — lighter frosting */
|
||||||
|
:root[data-theme="cosmic-light"] .sidebar,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .sidebar {
|
||||||
|
background-image: var(--glass-grain), var(--glass-sheen);
|
||||||
|
-webkit-backdrop-filter: blur(10px) saturate(110%);
|
||||||
|
backdrop-filter: blur(10px) saturate(110%);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-light"] .modal,
|
||||||
|
:root[data-theme="cosmic-light"] .color-popover,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .modal,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .color-popover {
|
||||||
|
background-image: var(--glass-grain), var(--glass-sheen);
|
||||||
|
-webkit-backdrop-filter: blur(14px) saturate(110%);
|
||||||
|
backdrop-filter: blur(14px) saturate(110%);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-light"] .modal,
|
||||||
|
:root[data-theme="cosmic-light"] .color-popover,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .modal,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .color-popover {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||||
|
0 8px 16px var(--color-shadow-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor/manuscript pane joins the glass */
|
||||||
|
:root[data-theme="cosmic"] .editor,
|
||||||
|
:root[data-theme="cosmic-stardust"] .editor {
|
||||||
|
background-image: var(--glass-grain), var(--glass-sheen);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic"] .folio-prose,
|
||||||
|
:root[data-theme="cosmic-stardust"] .folio-prose {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic"] .editor-panes .folio-prose.ProseMirror,
|
||||||
|
:root[data-theme="cosmic-stardust"] .editor-panes .folio-prose.ProseMirror {
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light themes — editor with subtle glass */
|
||||||
|
:root[data-theme="cosmic-light"] .editor,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .editor {
|
||||||
|
background-image: var(--glass-grain), var(--glass-sheen);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-light"] .folio-prose,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .folio-prose {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-light"] .editor-panes .folio-prose.ProseMirror,
|
||||||
|
:root[data-theme="cosmic-stardust-light"] .editor-panes .folio-prose.ProseMirror {
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback when backdrop-filter unsupported */
|
||||||
|
@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
|
||||||
|
:root[data-theme="cosmic"] {
|
||||||
|
--color-bg: rgba(6, 9, 16, 0.97);
|
||||||
|
--color-panel: rgba(10, 13, 23, 0.94);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-stardust"] {
|
||||||
|
--color-bg: rgba(13, 10, 6, 0.97);
|
||||||
|
--color-panel: rgba(20, 16, 10, 0.94);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-light"] {
|
||||||
|
--color-bg: rgba(240, 232, 244, 0.97);
|
||||||
|
--color-panel: rgba(244, 235, 236, 0.94);
|
||||||
|
}
|
||||||
|
:root[data-theme="cosmic-stardust-light"] {
|
||||||
|
--color-bg: rgba(255, 248, 232, 0.97);
|
||||||
|
--color-panel: rgba(245, 234, 210, 0.94);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue