Files
InkNote/frontend/src/ui/constants.jsx
T
marcandClaude Opus 5 d74fea4905 Chapters as rows, in two views, in their own colours
Three things Marc asked for, and one of them turned out to be the hinge
the other two hang from.

**A chapter is a row like any other.** Same grip, same drag, same place
in a hand-made arrangement as a note or a separator. That needed notes
and chapters numbered from *one* counter: numbered separately they are
two sequences that cannot be interleaved again, and a chapter dragged
between two notes springs back to the top. Positions therefore have to be
named rather than read off an array index, so POST /notes/reorder now
takes `positions: [{id, order}]` beside the older `ids`, and chapters
have a reorder route of their own.

**Two ways for a chapter to open**, chosen per account beside the
typeface, for the same reason that one lives there: it is a choice about
how your own notes are laid out and it should follow you to the phone.
Beside the list, its notes in a further column the way OneNote lays out
pages — the notebook's own list carrying on to the left, chapter rows and
all. Or unfolded under the chapter's own row, indented against a rule in
its colour. A phone has room for neither and fills the list instead, with
a way back, which is what it did before there was a choice.

**Its own colour**, from eight named ones rather than the full palette:
the point of chapters is that they are different places, and twenty
near-identical blues do not help anyone tell them apart.

The colour is validated on the way in — it is written by a browser, so
only a plain #rrggbb gets through and anything else becomes no colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:20:14 +02:00

193 lines
7.1 KiB
React

/** Shared visual constants: palette, notebook covers, sidebar folders. */
export const COLORS = [
"#0d1117",
"#e94560",
"#0f3460",
"#533483",
"#e07c24",
"#21bf73",
"#fd5e53",
"#3282b8",
"#ff6b6b",
"#4ecdc4",
"#f7dc6f",
"#bb8fce",
"#85c1e9",
"#f8c471",
"#82e0aa",
"#f1948a",
"#aeb6bf",
"#ffffff",
"#f39c12",
"#000000",
];
export const COVERS = [
{ bg: "linear-gradient(135deg,#0f0c29,#302b63,#24243e)", spine: "#1a1540", accent: "#6c63ff" },
{ bg: "linear-gradient(135deg,#1a1a2e,#16213e,#0f3460)", spine: "#0a1628", accent: "#4fc3f7" },
{ bg: "linear-gradient(135deg,#134e5e,#71b280)", spine: "#0e3d4a", accent: "#a5d6a7" },
{ bg: "linear-gradient(135deg,#8e2de2,#4a00e0)", spine: "#3a0099", accent: "#ce93d8" },
{ bg: "linear-gradient(135deg,#fc4a1a,#f7b733)", spine: "#c43d15", accent: "#ffe082" },
{ bg: "linear-gradient(135deg,#c94b4b,#4b134f)", spine: "#3d0f40", accent: "#f48fb1" },
{ bg: "linear-gradient(135deg,#11998e,#38ef7d)", spine: "#0d7a71", accent: "#b9f6ca" },
{ bg: "linear-gradient(135deg,#373b44,#4286f4)", spine: "#2a2e35", accent: "#90caf9" },
{ bg: "linear-gradient(135deg,#141e30,#243b55)", spine: "#0e1520", accent: "#78909c" },
{ bg: "linear-gradient(135deg,#f12711,#f5af19)", spine: "#c01f0d", accent: "#fff59d" },
{ bg: "linear-gradient(135deg,#2c3e50,#3498db)", spine: "#1e2b38", accent: "#81d4fa" },
{ bg: "linear-gradient(135deg,#0f2027,#203a43,#2c5364)", spine: "#0a171c", accent: "#80cbc4" },
{ bg: "linear-gradient(135deg,#aa4b6b,#6b6b83,#3b8d99)", spine: "#7a3550", accent: "#f48fb1" },
{ bg: "linear-gradient(135deg,#1f1c2c,#928dab)", spine: "#15131f", accent: "#b39ddb" },
{ bg: "linear-gradient(135deg,#654ea3,#eaafc8)", spine: "#4e3c7e", accent: "#f8bbd0" },
];
/**
* Colours a chapter's icon can be given.
*
* A short, named list rather than the full palette: the point is telling one
* chapter from another at a glance, and twenty near-identical blues do not help
* with that. Each is legible on both themes.
*/
export const CHAPTER_COLORS = [
{ label: "Red", value: "#e5534b" },
{ label: "Orange", value: "#e07c24" },
{ label: "Yellow", value: "#d4a017" },
{ label: "Green", value: "#2da44e" },
{ label: "Teal", value: "#1f9c96" },
{ label: "Blue", value: "#4a8ff5" },
{ label: "Purple", value: "#a371f7" },
{ label: "Pink", value: "#db61a2" },
];
/* The icon is a key rather than an emoji: the sidebar draws it from the app's
* own set, so it matches the stroke weight of everything around it and looks
* the same on every machine. */
export const DEFAULT_FOLDERS = [
{ id: "all", name: "All Notes", icon: "stack" },
{ id: "starred", name: "Starred", icon: "star" },
{ id: "trash", name: "Trash", icon: "trash" },
];
/**
* A pattern laid over a cover, chosen separately from its colour.
*
* Two decisions rather than one list: a cover is a colour *and* a texture, and
* offering every combination as its own entry would mean fifteen gradients times
* a dozen patterns in a grid nobody could take in. Picking each in turn is both
* shorter and gives every combination, not the handful someone wrote out.
*
* `layers` are CSS background layers laid **over** the gradient — no image
* files, nothing to upload, crisp at any size and offline like the rest of the
* app. They are deliberately faint: a cover is read at the size of a thumb with
* a title across it, and anything louder fights the title. One idea each, too —
* two crossed diagonals meant as a chevron came out as noisy plaid at that size.
*/
export const PATTERNS = [
{ emblem: "ring", id: "plain", label: "Plain", layers: "" },
{
id: "ruled",
emblem: "none",
label: "Ruled",
layers: "repeating-linear-gradient(0deg,rgba(255,255,255,.12) 0 1px,transparent 1px 17px)",
},
{
id: "grid",
emblem: "square",
label: "Grid",
layers:
"repeating-linear-gradient(0deg,rgba(255,255,255,.10) 0 1px,transparent 1px 20px), repeating-linear-gradient(90deg,rgba(255,255,255,.10) 0 1px,transparent 1px 20px)",
},
{
id: "pinstripe",
emblem: "diamond",
label: "Pinstripe",
layers: "repeating-linear-gradient(45deg,rgba(255,255,255,.09) 0 6px,transparent 6px 14px)",
},
{
id: "bands",
emblem: "none",
label: "Bands",
layers: "repeating-linear-gradient(0deg,rgba(0,0,0,.18) 0 13px,transparent 13px 26px)",
},
{
id: "dots",
emblem: "dot",
label: "Dots",
layers: "radial-gradient(rgba(255,255,255,.18) 1.4px,transparent 1.5px) 0 0/13px 13px",
},
{
id: "hatch",
emblem: "cross",
label: "Cross-hatch",
layers:
"repeating-linear-gradient(45deg,rgba(255,255,255,.08) 0 1px,transparent 1px 11px), repeating-linear-gradient(-45deg,rgba(255,255,255,.08) 0 1px,transparent 1px 11px)",
},
{
id: "linen",
emblem: "arc",
label: "Linen",
layers:
"repeating-linear-gradient(0deg,rgba(255,255,255,.07) 0 1px,transparent 1px 4px), repeating-linear-gradient(90deg,rgba(0,0,0,.07) 0 1px,transparent 1px 4px)",
},
{
id: "ledger",
emblem: "bars",
label: "Ledger",
layers: "repeating-linear-gradient(90deg,rgba(255,255,255,.09) 0 1px,transparent 1px 26px)",
},
{
id: "glow",
emblem: "none",
label: "Corner light",
layers: "radial-gradient(120% 85% at 18% 0%,rgba(255,255,255,.20),transparent 62%)",
},
{
id: "foot",
emblem: "square",
label: "Foot band",
layers: "linear-gradient(0deg,rgba(0,0,0,.34) 0 22%,transparent 22%)",
},
];
/**
* Covers written before patterns were their own choice.
*
* Twelve patterned entries were appended to `COVERS` for a day, so a notebook
* may still hold an index into them. Rather than let those repaint themselves,
* each is read back as the colour-and-pattern pair it was drawn as.
*/
const LEGACY_COVER = {
15: [11, 1], 16: [2, 2], 17: [3, 3], 18: [9, 4], 19: [5, 5],
20: [8, 6], 21: [12, 7], 22: [4, 4], 23: [8, 9], 24: [0, 9],
25: [2, 10], 26: [8, 8],
};
/** The colour and pattern a notebook is actually drawn with. */
export function coverParts(cover, pattern) {
const legacy = LEGACY_COVER[cover];
if (legacy) return { cover: legacy[0], pattern: legacy[1] };
return { cover: ((cover % COVERS.length) + COVERS.length) % COVERS.length, pattern: pattern || 0 };
}
/** The CSS `background` for a cover: its pattern laid over its colour. */
export function coverBackground(cover, pattern) {
const parts = coverParts(cover, pattern);
const base = COVERS[parts.cover];
const layers = PATTERNS[parts.pattern]?.layers;
return layers ? `${layers}, ${base.bg}` : base.bg;
}
/**
* The mark in the middle of a cover.
*
* It used to be the same pair of rings on every notebook, whatever cover was
* chosen — which, with the same title frame and the same three dots, is why a
* shelf of them all looked alike. It belongs to the design now: some wear a
* ring, some a diamond, and the ones whose pattern already fills the face wear
* nothing at all.
*/
export const coverEmblem = (cover, pattern) => PATTERNS[coverParts(cover, pattern).pattern]?.emblem || "none";
/** The rest of a cover's colours — the spine, and the accent used beside it. */
export const coverStyle = (cover, pattern) => COVERS[coverParts(cover, pattern).cover];