Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 85 additions & 15 deletions src/components/TextEditor/TextEditor.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,38 +33,107 @@
box-sizing: border-box;
}

.ol,
.ul {
list-style-position: outside;
}

.ol {
padding: 0;
margin: 0;
padding-left: 24px;
list-style-type: decimal;
}

.ul {
padding: 0;
margin: 0;
padding-left: 24px;
list-style-type: disc;
}

.nestedListItem {
list-style-type: none;
}

/* ── Prose rhythm ──
Markdown blank lines import as empty <p><br></p> blocks, so paragraphs sit
flush and every structural block owns its own spacing. Rules are scoped to
`.input` so they outrank the shared Typography primitives without those
primitives gaining margins that would disturb the rest of the app. */

.input > * {
max-width: var(--prose-measure);
}

.input > .paragraph {
margin: 0;
line-height: var(--type-leading-relaxed);
}

.input > .heading {
margin: var(--prose-gap-heading) 0 0;
}

.input > .heading:first-child {
margin-top: 0;
}

.input > .h1 {
font-size: var(--type-size-4xl);
letter-spacing: var(--type-tracking-tight);
}

.input > .h2 {
font-size: var(--type-size-2xl);
}

.input > .h3 {
font-size: var(--type-size-lg);
margin-top: var(--prose-gap-subheading);
}

.input > .list {
margin: var(--prose-gap-block) 0;
padding: 0 0 0 var(--prose-list-indent);
}

.list .list {
margin: var(--prose-list-item-gap) 0 0;
}

.listItem {
margin: 6px 0;
line-height: 1.5;
margin: var(--prose-list-item-gap) 0;
padding-left: 4px;
line-height: var(--type-leading-relaxed);
font-family: var(--font-sans);
font-size: 16px;
font-size: var(--type-size-md);
color: var(--ui-TextSecondary);
}

.nestedListItem {
list-style-type: none;
.listItem::marker {
color: var(--ui-TextLabel);
font-variant-numeric: tabular-nums;
}

.input > .quote {
margin: var(--prose-gap-block) 0;
padding-left: var(--prose-quote-indent);
border-left: var(--prose-quote-rule) solid var(--brand-Primary);
line-height: var(--type-leading-relaxed);
color: var(--ui-TextLabel);
font-style: italic;
}

.hr {
margin: var(--prose-gap-rule) 0;
border: none;
border-top: 1px solid var(--ui-Divider);
height: 0;
}

/* ── Table ── */
.table {
border-collapse: collapse;
width: 100%;
margin: 16px 0;
margin: var(--prose-gap-block) 0;
font-family: var(--font-sans);
font-size: 14px;
font-size: var(--type-size-sm);
line-height: var(--type-leading-normal);
}

.tableRow {
Expand All @@ -76,10 +145,11 @@
padding: 8px 12px;
vertical-align: top;
min-width: 80px;
color: var(--ui-TextPrimary);
color: var(--ui-TextSecondary);
}

.tableCellHeader {
background: var(--ui-ControlHover);
font-weight: 600;
font-weight: var(--type-weight-semibold);
color: var(--ui-TextPrimary);
}
28 changes: 28 additions & 0 deletions src/components/TextEditor/textEditorConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createEditor } from "lexical";

import {
editorTheme,
exportRecipeMarkdown,
importRecipeMarkdown,
recipeEditorNodes,
Expand Down Expand Up @@ -42,3 +43,30 @@ describe("recipe markdown serialization", () => {
expect(secondSave).toBe(markdown);
});
});

describe("editor theme", () => {
it("gives each heading level a distinct class so hierarchy is visible", () => {
const { h1, h2, h3 } = editorTheme.heading;

expect(new Set([h1, h2, h3]).size).toBe(3);
});

it("styles quotes apart from paragraphs", () => {
expect(editorTheme.quote).not.toBe(editorTheme.paragraph);
});

it("themes every block that carries prose rhythm", () => {
expect(editorTheme.hr).toBeTruthy();
expect(editorTheme.paragraph).toBeTruthy();
expect(editorTheme.list.ul).toBeTruthy();
expect(editorTheme.list.ol).toBeTruthy();
});

it("keeps ordered and unordered lists on a shared rhythm class", () => {
const shared = editorTheme.list.ul
.split(" ")
.filter((className) => editorTheme.list.ol.split(" ").includes(className));

expect(shared).toHaveLength(1);
});
});
20 changes: 13 additions & 7 deletions src/components/TextEditor/textEditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,29 @@ export const recipeEditorNodes = [
TableRowNode,
];

/** Lexical theme values accept a space-separated class list, so each block keeps
* the shared Typography primitive for family, weight and colour, and adds an
* editor-scoped class that owns rhythm, indent and rules. */
const prose = (...classNames: string[]): string => classNames.join(" ");

export const editorTheme = {
paragraph: typography.bodyText,
paragraph: prose(typography.bodyText, styles.paragraph),
heading: {
h1: typography.sectionHeading,
h2: typography.sectionHeading,
h3: typography.subsectionHeading,
h1: prose(typography.sectionHeading, styles.heading, styles.h1),
h2: prose(typography.sectionHeading, styles.heading, styles.h2),
h3: prose(typography.subsectionHeading, styles.heading, styles.h3),
},
text: {
bold: typography.bold,
italic: typography.italic,
underline: typography.underline,
strikethrough: typography.strikethrough,
},
quote: typography.bodyText,
quote: prose(typography.bodyText, styles.quote),
hr: styles.hr,
list: {
ol: styles.ol,
ul: styles.ul,
ol: prose(styles.list, styles.ol),
ul: prose(styles.list, styles.ul),
listitem: styles.listItem,
nested: {
listitem: styles.nestedListItem,
Expand Down
48 changes: 29 additions & 19 deletions src/components/Typography/Typography.module.css
Original file line number Diff line number Diff line change
@@ -1,60 +1,70 @@
.recipeTitle {
font-family: var(--font-sans);
font-size: 1.875em;
font-weight: 600;
line-height: 1.3;
font-size: var(--type-size-5xl);
font-weight: var(--type-weight-semibold);
line-height: var(--type-leading-tight);
letter-spacing: var(--type-tracking-tight);
color: var(--ui-TextPrimary);
text-wrap: balance;
}

.pageTitle {
font-family: var(--font-sans);
font-size: 1.875em;
font-weight: 600;
line-height: 1.3;
font-size: var(--type-size-5xl);
font-weight: var(--type-weight-semibold);
line-height: var(--type-leading-tight);
letter-spacing: var(--type-tracking-tight);
color: var(--ui-TextPrimary);
text-wrap: balance;
}

.sectionHeading {
font-family: var(--font-sans);
font-size: 1.5em;
font-weight: 600;
line-height: 1.3;
font-size: var(--type-size-3xl);
font-weight: var(--type-weight-semibold);
line-height: var(--type-leading-tight);
letter-spacing: var(--type-tracking-snug);
color: var(--ui-TextPrimary);
text-wrap: balance;
}

.subsectionHeading {
font-family: var(--font-sans);
font-size: 1.25em;
font-weight: 600;
line-height: 1.3;
font-size: var(--type-size-xl);
font-weight: var(--type-weight-semibold);
line-height: var(--type-leading-tight);
letter-spacing: var(--type-tracking-snug);
color: var(--ui-TextPrimary);
text-wrap: balance;
}

.focusStep {
font-family: var(--font-sans);
font-size: 28px;
font-weight: 500;
font-size: var(--type-size-4xl);
font-weight: var(--type-weight-medium);
line-height: var(--type-leading-snug);
letter-spacing: var(--type-tracking-snug);
color: var(--ui-TextPrimary);
}

.bodyText {
font-family: var(--font-sans);
font-size: 16px;
line-height: 1.5;
font-size: var(--type-size-md);
line-height: var(--type-leading-normal);
color: var(--ui-TextSecondary);
font-variant-numeric: lining-nums tabular-nums;
}

.metaLabel {
font-family: var(--font-sans);
font-size: 14px;
font-weight: 500;
font-size: var(--type-size-sm);
font-weight: var(--type-weight-medium);
color: var(--ui-TextCaption);
font-variant-numeric: tabular-nums;
}

.bold {
font-weight: 600;
font-weight: var(--type-weight-semibold);
color: var(--ui-TextPrimary);
}

Expand Down
41 changes: 41 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,47 @@
--font-sans: "Inter", system-ui, sans-serif;
--font-serif: "Merriweather", "Georgia", serif;

/* Type scale */
--type-size-xs: 0.8125rem;
--type-size-sm: 0.875rem;
--type-size-md: 1rem;
--type-size-lg: 1.125rem;
--type-size-xl: 1.25rem;
--type-size-2xl: 1.375rem;
--type-size-3xl: 1.5rem;
--type-size-4xl: 1.75rem;
--type-size-5xl: 1.875rem;

/* Line height */
--type-leading-tight: 1.25;
--type-leading-snug: 1.4;
--type-leading-normal: 1.5;
--type-leading-relaxed: 1.65;

/* Letter spacing */
--type-tracking-tight: -0.02em;
--type-tracking-snug: -0.011em;

/* Font weight */
--type-weight-regular: 400;
--type-weight-medium: 500;
--type-weight-semibold: 600;

/* Prose rhythm — owns the editor's vertical spacing so no block relies on
user-agent defaults. Markdown blank lines import as empty paragraphs, so
consecutive text lines sit flush and structural blocks space themselves.
The measure is expressed in rem, not ch, so every block wraps at the same
width instead of scaling with its own font size. */
--prose-measure: 39rem;
--prose-gap-heading: 28px;
--prose-gap-subheading: 20px;
--prose-gap-block: 14px;
--prose-gap-rule: 28px;
--prose-list-indent: 26px;
--prose-list-item-gap: 6px;
--prose-quote-indent: 16px;
--prose-quote-rule: 3px;

/* Animation */
--motion-default: 0.2s ease;
--motion-spring: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
Expand Down
Loading