diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5be05..82f2f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ What changed in each release of SCN Chat, for the people who use it, run it, or ### Changed - The new chat's "What can I help with?" draws in a touch, as other titles do. For forks: `shared/ui/Text.tsx` has a new `Text`, sized on one `xs` to `3xl` ladder over the type scale, with a `color` and `weight`, and `shared/ui/Title.tsx` a new `Title`, whose `order` is its heading level and `size` its look. Page titles, hints, and other plain text now use them. +- Each step of the type scale now carries its own line height, from 1.4 for captions to 1.2 for the largest titles, and the headings and titles their letter spacing, so text that inherited the page's 1.55 sits a little tighter in settings and admin. For forks: set them in `theme.css` as `--text-*--line-height` and `--text-*--letter-spacing`. A `leading-*` or `tracking-*` utility still overrides them. ## [0.7.2] - 2026-10-27 diff --git a/apps/web/src/features/chat-list/components/SearchResults.tsx b/apps/web/src/features/chat-list/components/SearchResults.tsx index f152069..83ffbe1 100644 --- a/apps/web/src/features/chat-list/components/SearchResults.tsx +++ b/apps/web/src/features/chat-list/components/SearchResults.tsx @@ -200,7 +200,7 @@ function ChatResults(props: ChatResultsProps) { {result.role === 'user' && ( // Who said it, as a small grey badge before their words. - + You: )} diff --git a/apps/web/src/features/sharing/pages/SharedStates.tsx b/apps/web/src/features/sharing/pages/SharedStates.tsx index 6543f6e..47a113b 100644 --- a/apps/web/src/features/sharing/pages/SharedStates.tsx +++ b/apps/web/src/features/sharing/pages/SharedStates.tsx @@ -35,7 +35,7 @@ export function SharedPageSkeleton() {
- +
diff --git a/apps/web/src/parts/TextView.tsx b/apps/web/src/parts/TextView.tsx index 62163fc..99f7b48 100644 --- a/apps/web/src/parts/TextView.tsx +++ b/apps/web/src/parts/TextView.tsx @@ -38,7 +38,7 @@ function UserText(props: UserTextProps) { // biome-ignore lint/suspicious/noArrayIndexKey: blocks have no identity beyond their place. key={i} dir="auto" - className="-mx-1.75 mb-2.25 rounded-[calc(var(--radius-bubble)-(--spacing(2.5)))] bg-bubble-quote px-2.75 py-1.5 font-normal text-ui leading-[1.45] last:mb-0" + className="-mx-1.75 mb-2.25 rounded-[calc(var(--radius-bubble)-(--spacing(2.5)))] bg-bubble-quote px-2.75 py-1.5 font-normal text-ui last:mb-0" > {block.text} diff --git a/apps/web/src/shared/schema-fields/SchemaFields.tsx b/apps/web/src/shared/schema-fields/SchemaFields.tsx index aaea163..776332d 100644 --- a/apps/web/src/shared/schema-fields/SchemaFields.tsx +++ b/apps/web/src/shared/schema-fields/SchemaFields.tsx @@ -101,7 +101,7 @@ function SchemaField(props: SchemaFieldProps) { {label} {property.description && ( - + {property.description} )} diff --git a/apps/web/src/shared/ui/Select.tsx b/apps/web/src/shared/ui/Select.tsx index 63df7a1..58acd94 100644 --- a/apps/web/src/shared/ui/Select.tsx +++ b/apps/web/src/shared/ui/Select.tsx @@ -304,7 +304,7 @@ export function Select(props: Props) { {/* The list's name already says this to screen readers, so it is only seen. */} {props.heading && (
{props.heading} diff --git a/apps/web/src/shared/ui/SettingsPage.tsx b/apps/web/src/shared/ui/SettingsPage.tsx index ef28ba3..dea374f 100644 --- a/apps/web/src/shared/ui/SettingsPage.tsx +++ b/apps/web/src/shared/ui/SettingsPage.tsx @@ -175,7 +175,7 @@ export function Choice(props: ChoiceProps) { ) } -const description = 'mt-0.5 text-muted text-small leading-[1.45]' +const description = 'mt-0.5 text-muted text-small' interface RowProps { label: ReactNode diff --git a/apps/web/src/shared/ui/SettingsPageSkeleton.tsx b/apps/web/src/shared/ui/SettingsPageSkeleton.tsx index ad27619..83fb26b 100644 --- a/apps/web/src/shared/ui/SettingsPageSkeleton.tsx +++ b/apps/web/src/shared/ui/SettingsPageSkeleton.tsx @@ -24,7 +24,7 @@ export function SettingsPageSkeleton() { return (
- +
@@ -57,7 +57,7 @@ function SettingRowShape(props: ShapeProps) {
- +
diff --git a/apps/web/src/shared/ui/Title.tsx b/apps/web/src/shared/ui/Title.tsx index d0c066c..09bbe21 100644 --- a/apps/web/src/shared/ui/Title.tsx +++ b/apps/web/src/shared/ui/Title.tsx @@ -2,16 +2,6 @@ import type { ComponentProps } from 'react' import { cn } from '../cn.ts' import { type TextSize, textSizes } from './Text.tsx' -/** - * What a size needs to read as a title: big titles draw in a touch. Once the theme's `--text-*` - * tokens carry their own line height and letter spacing, these move there and leave the sizes. - */ -const titleSizes: Partial> = { - xl: 'leading-tight tracking-heading', - '2xl': 'leading-tight tracking-title', - '3xl': 'leading-tight tracking-title', -} - const tags = { 1: 'h1', 2: 'h2', 3: 'h3', 4: 'h4' } as const interface Props extends ComponentProps<'h1'> { @@ -21,19 +11,17 @@ interface Props extends ComponentProps<'h1'> { size?: TextSize } -/** A heading in ink at semibold, its lines balanced, sized apart from its level, as Mantine's is. */ +/** + * A heading in ink at semibold, its lines balanced, sized apart from its level, as Mantine's is. + * Its size brings its own line height and tracking from the theme. + */ export function Title(props: Props) { const { order, size = '2xl', className, ...rest } = props const Tag = tags[order] return ( ) } diff --git a/apps/web/src/shared/ui/menu.ts b/apps/web/src/shared/ui/menu.ts index 321528a..6401003 100644 --- a/apps/web/src/shared/ui/menu.ts +++ b/apps/web/src/shared/ui/menu.ts @@ -10,4 +10,4 @@ export const floating = /** A row of a floating menu, such as an action or an option. */ export const menuRow = - 'flex min-h-row items-center gap-2.25 rounded-row px-2.25 py-1.25 text-start font-medium text-ink text-ui leading-[1.45]' + 'flex min-h-row items-center gap-2.25 rounded-row px-2.25 py-1.25 text-start font-medium text-ink text-ui' diff --git a/apps/web/src/shared/ui/sidebar.ts b/apps/web/src/shared/ui/sidebar.ts index 9a8c822..be77ef6 100644 --- a/apps/web/src/shared/ui/sidebar.ts +++ b/apps/web/src/shared/ui/sidebar.ts @@ -15,7 +15,7 @@ export const sidebarSection = 'mt-4.5 first:mt-0' * What the sidebar's headings share: small text in line with the rows' text. They scroll with * their rows, since one held at the top would sit in the list's fade. */ -const heading = 'mb-1 flex items-center gap-1.5 px-row-inset py-0.75 text-caption leading-[1.4]' +const heading = 'mb-1 flex items-center gap-1.5 px-row-inset py-0.75 text-caption' /** A section's heading, such as Tags or Chats, a step bolder and darker than those inside it. */ export const sectionHeading = `${heading} font-semibold text-ink` diff --git a/apps/web/src/styles/theme.css b/apps/web/src/styles/theme.css index 6a8aa71..fad8250 100644 --- a/apps/web/src/styles/theme.css +++ b/apps/web/src/styles/theme.css @@ -188,22 +188,34 @@ --default-mono-font-family: var(--font-mono); /* The type scale. Every size in the app is one of these steps, and code inside text sizes - itself against them in em. Weights are 400 for reading, 500 for controls and emphasis, and - 600 for headings and the main action. */ + itself against them in em. Each step carries its line height, looser for small text and + tighter for big, and the big ones their tracking, so a size reads right wherever it is used. + Weights are 400 for reading, 500 for controls and emphasis, and 600 for headings and the main + action. */ /* Badges, timestamps, counts, and small labels, at the 12px floor. */ --text-caption: 0.78rem; + --text-caption--line-height: 1.4; /* Secondary text: hints, menus, step rows, and details under a name. */ --text-small: 0.86rem; + --text-small--line-height: 1.45; /* The sidebar, titles in a bar, and the controls around a chat. */ --text-ui: 0.92rem; + --text-ui--line-height: 1.45; /* Messages, replies, and the composer. */ --text-body: 1rem; + --text-body--line-height: 1.55; /* A section's heading. */ --text-heading: 1.25rem; + --text-heading--line-height: 1.25; + --text-heading--letter-spacing: var(--tracking-heading); /* The settings title beside its sections, and each section's own title. */ --text-title: 1.5rem; + --text-title--line-height: 1.25; + --text-title--letter-spacing: var(--tracking-title); /* The app's name on the login page. */ --text-display: 1.75rem; + --text-display--line-height: 1.2; + --text-display--letter-spacing: var(--tracking-title); /* Code inside text, against the text around it, and a block of code a step smaller. */ --text-code: 0.85em; --text-code-block: 0.8em; diff --git a/apps/web/tests/styles/theme.test.ts b/apps/web/tests/styles/theme.test.ts index 6491fea..07da07f 100644 --- a/apps/web/tests/styles/theme.test.ts +++ b/apps/web/tests/styles/theme.test.ts @@ -17,9 +17,11 @@ const sheets = files.filter( ) /** The names the theme defines in one Tailwind namespace, such as ui in --text-ui. */ +// A step's own properties, such as --text-small--line-height, belong to its utility, not a new name. function namesIn(namespace: string) { const pattern = new RegExp(`^\\s*--${namespace}-([a-z][a-z-]*):`, 'gm') - return [...new Set(Array.from(theme.matchAll(pattern), (match) => match[1]))] + const names = Array.from(theme.matchAll(pattern), (match) => match[1] ?? '') + return [...new Set(names.filter((name) => !name.includes('--')))] } // A color, or a length or duration, written into a utility instead of taken from the theme. diff --git a/specs/styling.md b/specs/styling.md index 394ee93..64e679a 100644 --- a/specs/styling.md +++ b/specs/styling.md @@ -56,7 +56,7 @@ Every color is `light-dark(, )` in oklch, so dark mode needs no `da | Highlight | `highlight`, `highlight-ink` | any color utility | | Code | `code-background`, `code-foreground`, `code-token-*` | Shiki's CSS variables theme, through `parts/highlight.ts` | | Fonts | `--font-ui`, `--font-prose`, `--font-mono`, with `--default-font-family` set to the UI font | `font-*` | -| Type | `--text-caption`, `small`, `ui`, `body`, `heading`, `title`, `display` | `text-*` | +| Type | `--text-caption`, `small`, `ui`, `body`, `heading`, `title`, `display`, each with its `--line-height`, from 1.4 for a caption to 1.2 for display, and the heading, title, and display with their `--letter-spacing` | `text-*`, which sets all three | | Radius | `--radius-panel` (was `--radius`), `card`, `small`, `badge`, `row`, `menu`, `bubble`, `composer`, with Tailwind's own `rounded-full` for pills | `rounded-*` | | Shadows | `--shadow-float`, `--shadow-lift` | `shadow-*` | | Motion | `--ease-out`, `--ease-slide` for the sidebar, and `--animate-*` with their `@keyframes`: fade-in, fade-out, panel-in, panel-up, landed, breathe for live work, skeleton for content on its way, and spin for a busy control | `ease-*`, `animate-*` | @@ -104,7 +104,7 @@ Spacing literals from `design-reference` snap to the nearest quarter step of the - `CloseButton.tsx`: `CloseButton`, an `IconButton` with a ×, which removes, clears, or closes what its label names, and never turns red. It takes the `IconButton`'s sizes and radius: `xs` with `radius="sm"` inside a chip, `sm` in a file chip, a quote, or a heading's action, `md` at a row's end, and `lg` round for the composer's Clear. - `ExternalLink.tsx`: `ExternalLink`, a link out of the app, such as a reply's link, a source, or Report an issue, opened in a new tab with `rel="noreferrer"`. It draws nothing of its own. A link within the app is the router's `Link`, which renders a plain anchor for an outside URL but leaves the new tab to its caller and rejects a written-out URL in its types. - `Text.tsx`: `Text`, a run of text in a `size`, `color`, and `weight` from the theme, such as a hint in small grey, as a span, or with `as` a paragraph, a `small`, or a block, its `className` placing, truncating, or balancing it. Its sizes, `textSizes`, are the type scale as one ladder, as Mantine's are: `xs` caption, `sm` small, `md` ui, `lg` body, `xl` heading, `2xl` title, and `3xl` display. Text in a component's own look, such as a row's or a chip's, keeps its utilities. -- `Title.tsx`: `Title`, a heading in ink at semibold with balanced lines, whose `order` is its level in the page and whose `size`, from the same ladder, is `2xl` for a page's title unless it says otherwise, so a page's `h2` can wear a title's size. Its `xl` and up draw in a touch, with a tight line height and the `heading` or `title` tracking. Those belong to the sizes rather than to titles, and move into the `--text-*` tokens as their `--line-height` and `--letter-spacing` once the theme carries them, which leaves `Title` only its weight, color, and balance. +- `Title.tsx`: `Title`, a heading in ink at semibold with balanced lines, whose `order` is its level in the page and whose `size`, from the same ladder, is `2xl` for a page's title unless it says otherwise, so a page's `h2` can wear a title's size. Its line height and tracking come with its size from the theme. - `Code.tsx`: `Code`, code inside a line of text outside a reply, such as a model ID, a package, or a role's name, in plain monospace a step smaller than the text around it, as in a reply, but never under the caption size. - `TimeAgo.tsx`: `TimeAgo`, how long ago something happened, such as "Last used 5 minutes ago", after an optional prefix or capitalized without one, with the full date and time on hover. It draws only in the browser, whose locale and time zone the server lacks. - `ItemList.tsx`: lists in a card, imported as `import * as ItemList` and composed from its parts: `Root`, each `Item` with a `Mark`, its `Names` over a line of details, and its actions, and `LinkItem`, a row of the same parts that opens the page its title links to.