app/lib/writing/status.ts
45 lines and 5 functions in DrDustinEdwards/carrel, at commit d5d5e82 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| list | 10 to 10 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:10> | 10 to 10 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| labelFor | 27 to 31 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| <anonymous:30> | 30 to 30 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| cleanLabels | 37 to 53 | 10 | 90% | WatchCRAP 10.1 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by app/components/writing/status-label.tsx, app/lib/writing.server.ts, app/routes/book.outliner.tsx.
Source
// Status lists (ruling 8). Each project starts from its kind's list and may edit it: each label has
// one of Capsomer's eight series colors and filters the outliner. A file's `status:` is matched to a
// label ignoring case; a value the list does not carry is shown as written, in a neutral pill, and
// never refused.
export type StatusLabel = { label: string; color: number };
export type ProjectKind = "book" | "blog" | "manuscript" | "script";
const list = (...labels: string[]): StatusLabel[] => labels.map((label, i) => ({ label, color: (i % 8) + 1 }));
export const STARTING_STATUSES: Record<ProjectKind, StatusLabel[]> = {
book: list("Idea", "Outlined", "Drafting", "First draft", "Revised", "Final"),
// Published comes from the site, so it is not a label Carrel sets.
blog: list("Idea", "Drafting", "Ready", "Scheduled"),
manuscript: list("Drafting", "Draft done", "With coauthors", "Revising", "Final"),
script: list("Idea", "Outlined", "Drafting", "Revised", "Locked"),
};
/** The names a person sees for the eight colors, in token order (--series-1 to --series-8). */
export const COLOR_NAMES = ["Violet", "Orange", "Teal", "Green", "Indigo", "Pine", "Pink", "Wine"] as const;
export const MAX_LABELS = 12;
const MAX_LABEL_LENGTH = 40;
/** The label a file's `status:` names, or null for an empty or unknown value. */
export function labelFor(labels: StatusLabel[], status: string): StatusLabel | null {
const wanted = status.trim().toLowerCase();
if (!wanted) return null;
return labels.find((l) => l.label.toLowerCase() === wanted) ?? null;
}
/**
* A list as a person submitted it: blank rows dropped, labels trimmed, duplicates (ignoring case)
* refused, colors kept to 1 to 8. Returns the clean list or why it was refused.
*/
export function cleanLabels(rows: { label: string; color: string | number }[]): { ok: true; labels: StatusLabel[] } | { ok: false; error: string } {
const labels: StatusLabel[] = [];
const seen = new Set<string>();
for (const row of rows) {
const label = row.label.trim().replace(/\s+/g, " ");
if (!label) continue;
if (label.length > MAX_LABEL_LENGTH) return { ok: false, error: `Keep each label to ${MAX_LABEL_LENGTH} characters: "${label.slice(0, 20)}..." is longer.` };
const key = label.toLowerCase();
if (seen.has(key)) return { ok: false, error: `"${label}" is in the list twice.` };
seen.add(key);
const color = Number(row.color);
labels.push({ label, color: Number.isInteger(color) && color >= 1 && color <= 8 ? color : (labels.length % 8) + 1 });
}
if (labels.length === 0) return { ok: false, error: "Keep at least one label." };
if (labels.length > MAX_LABELS) return { ok: false, error: `Keep the list to ${MAX_LABELS} labels.` };
return { ok: true, labels };
}