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>
193 lines
7.1 KiB
React
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];
|