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
Binary file added .github/ui-review/chat-mobile-type-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/ui-review/chat-mobile-type-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ This repository is a VERY EARLY WIP. Proposing sweeping changes that improve lon
* Optimal performance on both frontend and backend.
* Excellent security and privacy, as long as it doesn't hurt UX too much.
## Maintainability
Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.
Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.
## Typography
Text is sized from the named scale in `apps/web/src/styles.css` (`text-meta` … `text-display-lg`), never with an arbitrary `text-[Npx]`. Two reasons: the tokens are in `rem`, so they follow the reader's browser font-size preference, and a scale of eleven named steps can be retuned in one place. The floor is 11px, and any text-entry field must reach 16px on mobile or iOS Safari zooms the viewport on focus.
Secondary copy on an accent fill uses `text-on-accent-muted`, not `text-on-accent` at reduced alpha — the accent barely clears 4.5:1 with full-strength text, so alpha puts it under the contrast minimum.
5 changes: 4 additions & 1 deletion apps/web/public/marketing.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ body {
color-mix(in srgb, var(--accent) 16%, transparent),
transparent 70%
), var(--bg);
font-size: 16px;
/* `1rem`, not `16px`: the rest of this sheet sizes text in `rem`, so
pinning the root to a pixel value would silently discard the reader's
browser font-size preference for the whole page. */
font-size: 1rem;
line-height: 1.6;
}

Expand Down
45 changes: 43 additions & 2 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
--color-accent-press: var(--c-accent-press);
--color-accent-soft: var(--c-accent-soft);
--color-on-accent: var(--c-on-accent);
--color-on-accent-muted: var(--c-on-accent-muted);

--color-danger: var(--c-danger);
--color-danger-soft: var(--c-danger-soft);
Expand All @@ -43,6 +44,31 @@
--font-display: "Bricolage Grotesque Variable", "Hanken Grotesk Variable", sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

/* Type scale.

Every size is in `rem`, never `px`: a `px` size is frozen at whatever the
designer typed, so a reader who raised the default font size in their
browser gets nothing. `rem` tracks that preference (WCAG 1.4.4 asks that
text survive a 200% resize; honouring the *setting* is the part a zoom
gesture can't stand in for).

The floor is 11px. Both platform guidelines put their smallest text there
— Apple's HIG says never below 11pt, Material's `labelSmall` is 11sp — and
the 10px timestamps this app used to ship were under it. Sizes are named
by role rather than by measurement so the scale can move without a
find-and-replace across every component. */
--text-meta: 0.6875rem; /* 11px — timestamps, badges, status pills */
--text-caption: 0.78125rem; /* 12.5px — secondary metadata */
--text-note: 0.84375rem; /* 13.5px — dense labels, list rows */
--text-body: 0.90625rem; /* 14.5px — default reading size */
--text-body-lg: 0.9375rem; /* 15px — document base */
--text-lead: 1rem; /* 16px — mobile reading size, inputs */
--text-title-sm: 1.0625rem; /* 17px */
--text-title: 1.1875rem; /* 19px */
--text-title-lg: 1.375rem; /* 22px */
--text-display: 1.5rem; /* 24px */
--text-display-lg: 1.625rem; /* 26px */

--animate-spin-fast: spin 0.65s linear infinite;
}

Expand All @@ -53,6 +79,12 @@
--c-accent-soft: color-mix(in srgb, var(--c-accent) 12%, transparent);
--c-accent-glow: color-mix(in srgb, var(--c-accent) 45%, transparent);
--c-on-accent: #ffffff;
/* Secondary copy on an accent fill. This has to be a solid colour, not
`on-accent` at reduced alpha: the accent background only clears white by
5.4:1 to begin with, so any alpha under ~90% drops the text below the
4.5:1 that WCAG 1.4.3 asks of small text. Verified at 4.74:1 on `accent`
and 6.08:1 on the darkened attachment surface inside a sent bubble. */
--c-on-accent-muted: #f0f0fb;

--c-bg: #f6f6f7;
--c-surface: #ffffff;
Expand Down Expand Up @@ -89,6 +121,11 @@
--c-accent-soft: color-mix(in srgb, var(--c-accent) 16%, transparent);
--c-accent-glow: color-mix(in srgb, var(--c-accent) 40%, transparent);
--c-on-accent: #0c0c14;
/* Dark theme inverts the relationship — the accent is light and the text
on it is near-black — but the margin is just as thin, so this is a
solid colour here too. 5.03:1 on `accent`, 6.25:1 on the lightened
attachment surface. */
--c-on-accent-muted: #17172a;

--c-bg: #0a0a0c;
--c-surface: #141417;
Expand Down Expand Up @@ -154,7 +191,7 @@
font-family: var(--font-sans);
background: var(--c-bg);
color: var(--c-ink);
font-size: 15px;
font-size: var(--text-body-lg);
line-height: 1.5;
letter-spacing: -0.006em;
-webkit-font-smoothing: antialiased;
Expand Down Expand Up @@ -244,8 +281,12 @@
@apply bg-surface shadow-soft dark:bg-surface-2 dark:ring-1 dark:ring-inset dark:ring-line;
}

/* Safari on iOS zooms the viewport when a field smaller than 16px takes
focus, and never zooms back out — so every text entry point in the app
steps up to `lead` on narrow screens. It reads better under a thumb
anyway. */
.field-input {
@apply w-full rounded-card border border-line-strong bg-surface px-3.5 py-3 text-[14.5px] text-ink transition;
@apply w-full rounded-card border border-line-strong bg-surface px-3.5 py-3 text-body text-ink transition max-md:text-lead;
}
.field-input::placeholder {
color: var(--c-muted);
Expand Down
28 changes: 14 additions & 14 deletions apps/web/src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ function StartupError({ message }: { message: string }): JSX.Element {
<div class="mx-auto grid size-12 place-items-center rounded-[14px] bg-danger-soft text-danger [&_svg]:size-6">
<AlertTriangle />
</div>
<h1 class="mt-5 font-display text-[24px] font-semibold tracking-[-0.025em]">
<h1 class="mt-5 font-display text-display font-semibold tracking-[-0.025em]">
Secure connection required
</h1>
<p class="mt-3 text-[14px] leading-6 text-subtle">{message}</p>
<p class="mt-3 text-[13px] leading-5 text-muted">
<p class="mt-3 text-body leading-6 text-subtle">{message}</p>
<p class="mt-3 text-note leading-5 text-muted">
For local development, open the app through <code>http://localhost</code> or configure
HTTPS for the host you are using.
</p>
Expand Down Expand Up @@ -141,7 +141,7 @@ function SpaceView({ section }: { section: SpaceSection }): JSX.Element {
onClick={() => setRenaming(true)}
class="group mr-auto flex min-w-0 items-center gap-1.5 rounded-[10px] px-1.5 py-[7px] text-left transition hover:bg-surface-3"
>
<span class="truncate font-display text-[15px] font-semibold tracking-[-0.022em]">
<span class="truncate font-display text-body-lg font-semibold tracking-[-0.022em]">
{spaceName}
</span>
<Pencil class="size-[13px] flex-none text-muted opacity-0 transition group-hover:opacity-100" />
Expand All @@ -162,7 +162,7 @@ function SpaceView({ section }: { section: SpaceSection }): JSX.Element {
))}
</nav>

<div class="mt-auto flex items-center gap-2.5 px-[25px] pt-2 font-mono text-[10.5px] font-medium uppercase tracking-[0.14em] text-muted">
<div class="mt-auto flex items-center gap-2.5 px-[25px] pt-2 font-mono text-meta font-medium uppercase tracking-[0.14em] text-muted">
<span
class={cx(
"size-2 flex-none rounded-full",
Expand All @@ -186,7 +186,7 @@ function SpaceView({ section }: { section: SpaceSection }): JSX.Element {
) : (
<BackLink to={spacePath(spaceId)} label={`Back to ${spaceName}`} />
)}
<span class="truncate px-1.5 font-display text-[15px] font-semibold tracking-[-0.022em]">
<span class="truncate px-1.5 font-display text-body-lg font-semibold tracking-[-0.022em]">
{section === "chat" ? spaceName : sectionLabel(section)}
</span>
</div>
Expand All @@ -202,7 +202,7 @@ function SpaceView({ section }: { section: SpaceSection }): JSX.Element {

{/* Desktop view header */}
<div class="hidden h-[60px] flex-none items-center justify-between gap-3 border-b border-line px-6 md:flex">
<div class="font-display text-[17px] font-semibold tracking-[-0.022em]">
<div class="font-display text-title-sm font-semibold tracking-[-0.022em]">
{sectionLabel(section)}
</div>
</div>
Expand All @@ -218,11 +218,11 @@ function SpaceView({ section }: { section: SpaceSection }): JSX.Element {
<Modal title="Leave this space?" onClose={() => setConfirmLeaveOpen(false)}>
<div class="flex gap-3 rounded-card border border-danger/25 bg-danger-soft p-3.5 text-danger">
<AlertTriangle class="mt-0.5 size-[19px] flex-none" />
<p class="text-[13.5px] font-medium leading-5">
<p class="text-note font-medium leading-5">
This will remove the space, messages, files, and encryption keys from this device.
</p>
</div>
<p class="text-[13.5px] leading-5 text-subtle">
<p class="text-note leading-5 text-subtle">
Other devices in the space will keep their access, and your other spaces on this device
are untouched. You can link this device again later from another device.
</p>
Expand Down Expand Up @@ -344,7 +344,7 @@ function RenameSpaceModal({ name, onClose }: { name: string; onClose: () => void
}}
>
<label class="flex flex-col gap-1.5 text-left">
<span class="text-[13px] font-medium text-subtle">Space name</span>
<span class="text-note font-medium text-subtle">Space name</span>
<input
type="text"
class="field-input"
Expand All @@ -355,7 +355,7 @@ function RenameSpaceModal({ name, onClose }: { name: string; onClose: () => void
onInput={(e) => setValue((e.target as HTMLInputElement).value)}
/>
</label>
<p class="text-[12.5px] leading-5 text-muted">
<p class="text-caption leading-5 text-muted">
The name is stored on this device only, and shared with a device when you link it.
</p>
<div class="flex flex-col-reverse gap-2.5 sm:flex-row sm:justify-end">
Expand All @@ -381,12 +381,12 @@ function RevokedNotice(): JSX.Element {
<Modal title="This device is no longer linked">
<div class="flex gap-3 rounded-card border border-danger/25 bg-danger-soft p-3.5 text-danger">
<AlertTriangle class="mt-0.5 size-[19px] flex-none" />
<p class="text-[13.5px] font-medium leading-5">
<p class="text-note font-medium leading-5">
Its access was revoked from another device, or the space no longer exists. Messages and
files can't be sent or received until you link it again.
</p>
</div>
<p class="text-[13.5px] leading-5 text-subtle">
<p class="text-note leading-5 text-subtle">
Linking again starts a new session on this device: the messages, files and encryption keys
stored here for this space are removed first.
</p>
Expand Down Expand Up @@ -419,7 +419,7 @@ function NavItem({
aria-current={active ? "page" : undefined}
onClick={(event) => followLink(event as unknown as MouseEvent, href)}
class={cx(
"flex w-full items-center gap-[11px] rounded-[10px] px-[11px] py-[9px] text-left text-[14px] font-medium transition [&_svg]:size-[18px] [&_svg]:flex-none [&_svg]:opacity-85",
"flex w-full items-center gap-[11px] rounded-[10px] px-[11px] py-[9px] text-left text-body font-medium transition [&_svg]:size-[18px] [&_svg]:flex-none [&_svg]:opacity-85",
active
? "bg-accent-soft text-accent [&_svg]:opacity-100"
: "text-subtle hover:bg-surface-3 hover:text-ink",
Expand Down
Loading
Loading