Files
InkNote/frontend/src/editor/editor.css
T
marcandClaude Opus 5 d68b0775c2 One dialog per thing, and rows dragged into a chapter
A separator had three menu entries — rename, colour, line — plus an input
that appeared in the row itself: three shapes of interaction for one small
thing, and none of them showed the result. Choosing "dotted" from a list
of words is a guess. It is one Edit now, and the dialog draws the line as
it will look with every shape drawn beside its name, from the very rules
the list draws with: a sample that can disagree with the result is worse
than none.

A chapter goes the same way — name, mark and colour in one dialog, the
same one whether it is being made or changed, with the row drawn at the
top. Asked from three entries, getting a new chapter to look right took
three visits. The name dialog it used is gone.

A row is put into a chapter by dragging it onto it: the chapter lights
up and takes what is dropped rather than growing a line above it, since
a chapter is a place and not a neighbour. Notes and separators both, and
the "+ Separator" inside a chapter goes with it — a second way to make
the same thing in a second place is a second thing to keep straight.

A link now wears the mark of the note it points at, in the text and in
the @ menu. It travels when the note's mark changes, as the label
already did.

Two alignment faults: a task list's checkbox was nudged down by a fixed
0.35em measured once against one font, so it floated above the line it
belonged to — it is centred in a box one line tall now. And the title
bar's mark was spaced like the buttons at the far end and grew with its
contents, so picking an emoji shifted the title sideways.

Fourteen suites green: 374 browser checks, 241 unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:23:26 +02:00

344 lines
10 KiB
CSS

/* Editor content styling.
*
* The rest of the app styles inline, but ProseMirror renders its own DOM, so
* these nodes can only be reached from a stylesheet. Colours come from custom
* properties the editor container sets per theme. */
.note-editor {
--ne-font: "Newsreader", Georgia, serif;
--ne-text: #1f2328;
}
.note-editor[data-font="sans"] {
--ne-font: "DM Sans", system-ui, sans-serif;
}
.note-editor[data-font="mono"] {
--ne-font: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
}
.note-editor {
--ne-muted: #656d76;
--ne-faint: #afb8c1;
--ne-border: #d0d7de;
--ne-accent: #0969da;
--ne-surface: #ffffff;
--ne-subtle: #f6f8fa;
/* A table's header row, which needs to read as one at a glance. Its own
tokens because "slightly off the page colour" is enough for a code block
sitting in the middle of text and not enough for a row that has to be told
from the rows under it. */
--ne-th-bg: #eaeef2;
--ne-th-rule: #afb8c1;
/* One line of body text. Named because the task list needs to know it: a
checkbox is centred against a line, and a line is this tall. */
--ne-line: 1.7;
}
.note-editor[data-theme="dark"] {
--ne-text: #e6edf3;
--ne-muted: #8b949e;
--ne-faint: #484f58;
--ne-border: #30363d;
--ne-accent: #388bfd;
--ne-surface: #0d1117;
--ne-subtle: #161b22;
/* Dark is where this mattered: #161b22 on a #0d1117 sheet is a difference you
have to look for, so a header row read as just another row. */
--ne-th-bg: #262c36;
--ne-th-rule: #6a7480;
}
.note-editor .ProseMirror {
/* About 73 characters to the line at this size — measured, not guessed. The
text used to run the full width of the pane, past 84, which is beyond the
point where the eye reliably finds the start of the next line. Capped here
rather than on the container so it holds on a page as well as off one. */
/* Roughly 80 characters to the line, leaving about 8% of the sheet as margin
on either side — a document's margin, not a picture frame. It was 527px,
which left 117px of empty paper down each edge and made the writing look
lost on the page. Past about 84 characters the eye stops reliably finding
the start of the next line, so this is close to as wide as it should go. */
max-width: 640px;
/* Centred on the sheet. Pinning the column to the left edge put every line in
the same place, but on a wide window it left the whole right half of the
pane as one dead block. */
margin-left: auto;
margin-right: auto;
outline: none;
color: var(--ne-text);
/* Set per account — see UserPreferences. The stack is the fallback chain, so
an unset preference still reads as the serif it always was. */
font-family: var(--ne-font, "Newsreader", Georgia, serif);
font-size: 15px;
line-height: var(--ne-line);
padding: 20px 24px;
min-height: 100%;
word-break: break-word;
}
.note-editor .ProseMirror > * + * {
margin-top: 0.6em;
}
/* The gap between two sheets, held open inside the text flow so no paragraph is
cut in half by it. Margins of its own would make the space it adds depend on
its neighbours — see pagination.js, which computes the height on the
assumption that it has none. */
.note-editor .ProseMirror > .ne-page-spacer {
margin: 0 !important;
padding: 0;
border: none;
pointer-events: none;
user-select: none;
}
/* The placeholder extension styles `[contenteditable]:empty::before`, and a
spacer is exactly that: empty, and not editable. So "Start writing..." was
printed at the foot of every page that had a break after it. */
.note-editor .ProseMirror > .ne-page-spacer::before,
.note-editor .ProseMirror > .ne-page-spacer::after {
content: none !important;
}
.note-editor .ProseMirror p {
margin: 0;
}
.note-editor .ProseMirror h1 {
font-size: 1.7em;
font-weight: 700;
line-height: 1.3;
margin-top: 0.8em;
}
.note-editor .ProseMirror h2 {
font-size: 1.4em;
font-weight: 700;
line-height: 1.3;
margin-top: 0.7em;
}
.note-editor .ProseMirror h3 {
font-size: 1.15em;
font-weight: 700;
line-height: 1.4;
margin-top: 0.6em;
}
.note-editor .ProseMirror ul,
.note-editor .ProseMirror ol {
padding-left: 1.4em;
}
.note-editor .ProseMirror li p {
margin: 0;
}
.note-editor .ProseMirror blockquote {
border-left: 3px solid var(--ne-border);
padding-left: 12px;
color: var(--ne-muted);
}
.note-editor .ProseMirror code {
background: var(--ne-subtle);
border: 1px solid var(--ne-border);
border-radius: 4px;
padding: 1px 5px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.88em;
}
.note-editor .ProseMirror pre {
background: var(--ne-subtle);
border: 1px solid var(--ne-border);
border-radius: 10px;
padding: 12px 14px;
overflow-x: auto;
}
.note-editor .ProseMirror pre code {
background: none;
border: none;
padding: 0;
}
.note-editor .ProseMirror hr {
border: none;
border-top: 1px solid var(--ne-border);
margin: 1.2em 0;
}
.note-editor .ProseMirror img {
max-width: 100%;
height: auto;
border-radius: 8px;
display: block;
}
.note-editor .ProseMirror img.note-image {
background: var(--ne-subtle);
min-height: 24px;
}
/* An attachment whose bytes could not be fetched — visible, not a blank gap */
.note-editor .ProseMirror img[data-missing="true"] {
border: 1px dashed var(--ne-border);
padding: 12px;
min-width: 120px;
}
.note-editor .ProseMirror a {
color: var(--ne-accent);
}
/* Placeholder on the first empty line */
.note-editor .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--ne-faint);
float: left;
height: 0;
pointer-events: none;
}
/* The spacer that carries a table across the gap between two sheets. It is a
row, so it inherits every cell rule below — and has to shed all of them, or
the gap shows a tall bordered box. */
.note-editor .ProseMirror tr.ne-page-spacer-row,
.note-editor .ProseMirror tr.ne-page-spacer-row td {
border: none !important;
padding: 0 !important;
background: transparent !important;
}
.note-editor .ProseMirror tr.ne-page-spacer-row td::before,
.note-editor .ProseMirror tr.ne-page-spacer-row td::after {
content: none !important;
}
/* ── Tables ── */
.note-editor .ProseMirror table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0.8em 0;
overflow: hidden;
}
.note-editor .ProseMirror td,
.note-editor .ProseMirror th {
border: 1px solid var(--ne-border);
padding: 7px 10px;
vertical-align: top;
position: relative;
min-width: 1em;
}
.note-editor .ProseMirror th {
background: var(--ne-th-bg);
font-weight: 700;
text-align: left;
/* The rule under the header does most of the work — the fill alone cannot be
made much stronger without the header turning into a banner. */
border-bottom: 2px solid var(--ne-th-rule);
}
.note-editor .ProseMirror .selectedCell::after {
content: "";
position: absolute;
inset: 0;
background: color-mix(in srgb, var(--ne-accent) 18%, transparent);
pointer-events: none;
}
/* Drag handle between columns */
.note-editor .ProseMirror .column-resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: -2px;
width: 4px;
background: var(--ne-accent);
pointer-events: none;
}
.note-editor .ProseMirror.resize-cursor {
cursor: col-resize;
}
/* ── Task lists ── */
.note-editor .ProseMirror ul[data-type="taskList"] {
list-style: none;
padding-left: 0;
}
.note-editor .ProseMirror ul[data-type="taskList"] li {
display: flex;
align-items: flex-start;
gap: 8px;
}
/* The box sits in the middle of the *first line* beside it, not at a fixed
nudge from the top. `margin-top: 0.35em` was measured once against one font at
one size; changing the note's typeface, or writing a task that wraps, left the
box floating above the text it belongs to. Giving the label the height of one
line and centring inside it holds for every size. */
.note-editor .ProseMirror ul[data-type="taskList"] li > label {
flex: 0 0 auto;
display: flex;
align-items: center;
height: calc(1em * var(--ne-line));
user-select: none;
}
.note-editor .ProseMirror ul[data-type="taskList"] li > div {
flex: 1 1 auto;
}
.note-editor .ProseMirror ul[data-type="taskList"] input[type="checkbox"] {
accent-color: var(--ne-accent);
width: 15px;
height: 15px;
cursor: pointer;
}
.note-editor .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div {
color: var(--ne-muted);
text-decoration: line-through;
}
/* ── Note links ── */
.note-editor .ProseMirror a.note-link,
.note-link {
display: inline-flex;
align-items: baseline;
gap: 3px;
padding: 1px 6px;
border-radius: 6px;
background: color-mix(in srgb, var(--ne-accent) 14%, transparent);
color: var(--ne-accent);
text-decoration: none;
font-family: "DM Sans", system-ui, sans-serif;
font-size: 0.9em;
white-space: nowrap;
cursor: pointer;
}
.note-editor .ProseMirror a.note-link:hover {
background: color-mix(in srgb, var(--ne-accent) 26%, transparent);
}
/* A link whose target is gone — still readable, visibly broken */
.note-editor .ProseMirror a.note-link[data-missing="true"] {
background: color-mix(in srgb, #cf222e 14%, transparent);
color: #cf222e;
text-decoration: line-through;
}
/* ── @-mention popup ── */
.note-link-popup {
position: fixed;
z-index: 3000;
max-height: 240px;
overflow-y: auto;
padding: 5px;
border-radius: 12px;
border: 1px solid var(--popup-border, #d0d7de);
background: var(--popup-bg, #ffffff);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
font-family: "DM Sans", system-ui, sans-serif;
}
.note-link-option {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 7px 10px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--popup-text, #1f2328);
font-size: 13px;
text-align: left;
cursor: pointer;
}
.note-link-option.is-selected {
background: var(--popup-hover, #eaeef2);
}
.note-link-option .note-link-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note-link-empty {
padding: 9px 11px;
font-size: 12px;
color: var(--popup-muted, #656d76);
}