tokens.css defined colours, radii and font families but not a single --text-*
step, so font sizes had no baseline to align to and grew wherever they were
needed: 51 distinct sizes across web + desktop, 370 written as arbitrary
values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px).
text-xs and text-sm carried nearly all UI text while the range between them —
11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not
come from having more sizes; past a handful, each extra size makes the
hierarchy blurrier.
Add ten role-named steps, each with its own line-height so leading cannot
fragment the way size did, and move every product-UI call site onto them.
Steps are named for what the text is for, not for a t-shirt size, because
that is what keeps the scale from drifting again.
Six steps deliberately keep the exact size/line-height pairs of the Tailwind
defaults they replace, so the ~1,900-call-site rename moves nothing on screen.
The visible changes are confined to former arbitrary values snapping to a step:
8/9/10px -> micro (11px) on badges and overlines; 17 -> 18; 22 -> 24; 30
(text-3xl) -> 36 on headings and stat numbers; 12.8px -> label (13px) on small
buttons and toggles. Half-pixel sizes are gone.
This supersedes #6108, which was reverted by #6116 because the sidebar group
labels rendered at the inherited 16px. The cause was not the scale but cn():
`text-<x>` is ambiguous in Tailwind, and tailwind-merge resolves it against a
table listing only the default sizes, so it filed every role step under
text-colour and dropped whichever of `text-caption` /
`text-sidebar-foreground/70` came first. Registering the steps as a font-size
class group restores the real conflict groups — size beats size, colour beats
colour, the two coexist — and a test pins the list against the scale, since
the failure is silent in source.
Hand-written CSS is covered too. The transcript kept a 12.5px body long after
every Tailwind call site was on the scale, so the "no half-pixel sizes" claim
was true of the classes and false of the product; the editor's prose, code and
mermaid ramps had the same blind spot, and seven of their eight values already
equalled a step exactly. All now reference var(--text-*). The guard test reads
raw `font-size:` declarations as well as class names, exempting only the 16px
iOS input-zoom workaround in base.css and the landing pages' marketing ramp.
apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system)
keep Tailwind's default scale and are untouched. Landing display type
(rem/clamp, 2.2-6.4rem) stays on its separate ramp, as do four decorative
emoji / serif-hero sizes.
Verified on a running local stack: pinned sidebar rows and group labels
measure 12px/16px, nav items 14px/20px — identical to pre-migration. An audit
of every rendered font size across the product surfaces finds nothing off the
scale; the only exceptions are avatar initials and emoji, which
actor-avatar.tsx sizes proportionally to the avatar diameter by design.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
tokens.css defined colours, radii and font families but not a single
--text-* step, so font sizes had no baseline to align to and grew wherever
they were needed: 51 distinct sizes across web + desktop, 370 of them written
as arbitrary text-[Npx] values, six at half a pixel (10.5 / 11.5 / 12.5 /
13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while
the range between them — 11, 13, 15px — could only be reached with arbitrary
values. Hierarchy does not come from having more sizes; past a handful, each
extra size makes the hierarchy blurrier.
Add ten role-named steps, each with its own line-height so leading cannot
fragment the way size did, and move every product-UI call site onto them.
Steps are named for what the text is for, not for a t-shirt size, because
that is what keeps the scale from drifting again.
Six steps deliberately keep the exact size/line-height pairs of the Tailwind
defaults they replace, so the 1,900-call-site rename (text-sm -> text-body
and friends) moves nothing on screen. The visible changes are confined to
former arbitrary values snapping to a step:
- 8 / 9 / 10px -> micro (11px): 102 sites, mostly badges and overline
labels. Deliberate — under 12px is a counter role, not a text role.
- 17px -> title (18px), 22px -> display-sm (24px), 30px (text-3xl) ->
display (36px): 21 sites, all headings or stat numbers in flexible
containers.
- Half-pixel steps are gone entirely.
Arbitrary sizes also inherited whatever line-height was above them; the
tokens now pin one, which removes latent overflow risk in the fixed-height
h-4/h-5 badges those sizes were used in.
apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system)
keep Tailwind's default scale and are untouched. Landing-page display type
(rem/clamp, 2.2-6.4rem) is marketing typography on a separate ramp and stays
out of the scale, as do four decorative emoji / serif-hero sizes.
A guard test fails the build on any font size written outside the scale,
reporting file:line — without it nothing in a Tailwind build makes an
off-scale value look wrong, which is how the drift happened.
Verified against a local stack: typecheck, lint and the full TS suite show no
new failures, and before/after screenshots of issues, issue detail, runtimes
(populated), usage, agents, inbox, my-issues and settings differ only in the
intended 10px -> 11px labels, with no row-height, truncation or layout shift.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
navigator.clipboard is only exposed in a secure context (https or
localhost). On self-hosted instances served over plain http:// it is
undefined, so every copy / "copy all" / export button silently failed and
left the clipboard empty (GitHub #3781).
Add a shared copyText(text): Promise<boolean> helper in
@multica/ui/lib/clipboard that prefers the async Clipboard API and falls
back to a hidden <textarea> + document.execCommand('copy') for non-secure
contexts. Migrate all direct navigator.clipboard.writeText call sites
(code blocks, agent transcript copy-all, token / webhook / issue-link
copy, etc.) to it, gating success side-effects on the returned boolean,
and remove the now-redundant copyMarkdown wrapper. Secure-context users
keep the native path unchanged.
MUL-3068
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Extracts CODE_LIGATURE_CLASS and CODE_LIGATURE_DESCENDANT_CLASS into
packages/ui/lib/code-style.ts. Non-markdown CLI command surfaces
(onboarding/cli-install-instructions, runtimes/connect-remote-dialog)
can now import the class strings without pulling in the shiki +
react-markdown + katex dependency graph via the markdown barrel.
CodeBlock and Markdown continue to consume the constants from the
new module; the markdown barrel no longer re-exports CODE_LIGATURE_CLASS.
MUL-2793
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The four user-visible strings exposed by packages/ui rendered untranslated
on every page that used them:
- file-upload-button.tsx — "Attach file" aria-label/title
- sidebar.tsx — "Toggle Sidebar" sr-only label/aria-label/title
- pagination.tsx — "Go to previous/next page" aria-labels
- CodeBlock.tsx — "plain text" language fallback + "Copy code" aria-label/tooltip
Root cause: the package had no i18n hookup at all because the package
boundary rule forbids importing @multica/core. Replicating the pattern
five times would have been the same hack five times. Hooking up
react-i18next directly is the structurally clean fix — i18next is a
generic library, not business logic, and the upstream I18nextProvider
already exposes the instance via context.
To let packages/ui typecheck the selector form standalone (i.e. without
the views resource-types augmentation in scope), the augmentation is
split: views declares everything except the `ui` namespace on a new
global `I18nResources` interface, and packages/ui contributes the `ui`
slice via declaration merging in packages/ui/types/i18next.ts. Views'
resources-types side-effect-imports that file so both packages see the
merged shape during downstream typechecks.
Scope intentionally excludes:
- packages/ui/components/common/error-boundary.tsx — keeping its fallback
in English so a render-time crash never depends on i18n being healthy.
- apps/desktop/src/renderer/src/components/update-notification.tsx —
ships with the next desktop release, not via this PR.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>