fix: size small text in rem so it follows the browser and app text size

This commit is contained in:
Linus Rath
2026-10-29 19:38:45 +01:00
parent 7f74fcf9bf
commit 35be105a73
74 changed files with 245 additions and 210 deletions
+8 -8
View File
@@ -1048,7 +1048,7 @@ function LoginPageContent() {
))}
</select>
{domainAutoLocked && (
<p className="text-[11px] text-muted-foreground leading-snug">
<p className="text-[0.6875rem] text-muted-foreground leading-snug">
{t("jmap_server_auto_picked")}
</p>
)}
@@ -1067,7 +1067,7 @@ function LoginPageContent() {
key={s.id}
type="button"
variant="outline"
className="w-full h-11 font-medium text-[15px] rounded-xl border-border/60 hover:bg-muted/50"
className="w-full h-11 font-medium text-[0.9375rem] rounded-xl border-border/60 hover:bg-muted/50"
onClick={() => handleOAuthLogin(s)}
disabled={oauthLoading || isLoading}
>
@@ -1416,7 +1416,7 @@ function LoginPageContent() {
{oauthMetadata ? (
<Button
type="button"
className="w-full h-11 font-medium text-[15px] bg-primary hover:bg-primary/90 transition-all duration-200 rounded-xl shadow-md shadow-primary/15 hover:shadow-lg hover:shadow-primary/20"
className="w-full h-11 font-medium text-[0.9375rem] bg-primary hover:bg-primary/90 transition-all duration-200 rounded-xl shadow-md shadow-primary/15 hover:shadow-lg hover:shadow-primary/20"
onClick={() => handleOAuthLogin()}
disabled={oauthLoading}
>
@@ -1469,7 +1469,7 @@ function LoginPageContent() {
placeholder={t("jmap_endpoint_placeholder")}
required
/>
<p className="text-[11px] text-muted-foreground leading-snug">
<p className="text-[0.6875rem] text-muted-foreground leading-snug">
{t("jmap_endpoint_cors_hint")}
</p>
</div>
@@ -1613,7 +1613,7 @@ function LoginPageContent() {
data-lpignore="true"
{...errorProps(tokenBlamed)}
/>
<p className="text-[11px] text-muted-foreground leading-snug">
<p className="text-[0.6875rem] text-muted-foreground leading-snug">
{t("token_hint")}
</p>
</div>
@@ -1701,7 +1701,7 @@ function LoginPageContent() {
<Button
type="submit"
className="w-full h-11 font-medium text-[15px] bg-primary hover:bg-primary/90 transition-all duration-200 rounded-xl shadow-md shadow-primary/15 hover:shadow-lg hover:shadow-primary/20"
className="w-full h-11 font-medium text-[0.9375rem] bg-primary hover:bg-primary/90 transition-all duration-200 rounded-xl shadow-md shadow-primary/15 hover:shadow-lg hover:shadow-primary/20"
disabled={isLoading || (liteSsoRequired && oauthLoading)}
>
{isLoading || (liteSsoRequired && oauthLoading) ? (
@@ -1732,7 +1732,7 @@ function LoginPageContent() {
<Button
type="button"
variant="outline"
className="w-full h-11 font-medium text-[15px] rounded-xl border-border/60 hover:bg-muted/50"
className="w-full h-11 font-medium text-[0.9375rem] rounded-xl border-border/60 hover:bg-muted/50"
onClick={() => handleOAuthLogin()}
disabled={oauthLoading || isLoading}
>
@@ -1782,7 +1782,7 @@ function LoginPageContent() {
<Button
type="button"
variant="outline"
className="w-full h-11 font-medium text-[15px] rounded-xl border-border/60 hover:bg-muted/50"
className="w-full h-11 font-medium text-[0.9375rem] rounded-xl border-border/60 hover:bg-muted/50"
onClick={handleDemoLogin}
disabled={demoLoading || isLoading}
>
@@ -96,7 +96,7 @@ function emptyDraft(): RowDraft {
};
}
const fieldLabelClass = 'block text-[11px] font-medium text-muted-foreground mb-1';
const fieldLabelClass = 'block text-[0.6875rem] font-medium text-muted-foreground mb-1';
const fieldClass = cn(inputClass, 'w-full');
export function JmapServersSection({ value, source, onChange, onRevert }: Props) {
@@ -146,7 +146,7 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
<div className="min-w-0 sm:pe-4">
<span className="text-sm font-medium text-foreground">Servers</span>
<p className="text-xs text-muted-foreground mt-1">
Each entry appears as an option on the login dropdown. Leave the list empty to fall back to the single <code className="text-[11px]">JMAP server URL</code> above.
Each entry appears as an option on the login dropdown. Leave the list empty to fall back to the single <code className="text-[0.6875rem]">JMAP server URL</code> above.
</p>
</div>
<div className="flex items-center gap-1.5 shrink-0">
@@ -178,7 +178,7 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
placeholder="main"
className={cn(fieldClass, isDuplicate && 'border-destructive')}
/>
{isDuplicate && <p className="text-[10px] text-destructive mt-0.5">Duplicate id</p>}
{isDuplicate && <p className="text-[0.625rem] text-destructive mt-0.5">Duplicate id</p>}
</div>
<div className="sm:col-span-4">
<label htmlFor={labelFor(i)("label")} className={fieldLabelClass}>Label</label>
@@ -280,7 +280,7 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
maxLength={64}
className={fieldClass}
/>
<p className="mt-1 text-[11px] text-muted-foreground">
<p className="mt-1 text-[0.6875rem] text-muted-foreground">
With a client ID, this label gives the server its own sign-in button on the login page, whichever server is selected and even while OAuth is off globally.
</p>
</div>
@@ -294,7 +294,7 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
placeholder="https://auth.example.com/logout"
className={fieldClass}
/>
<p className="mt-1 text-[11px] text-muted-foreground">
<p className="mt-1 text-[0.6875rem] text-muted-foreground">
Where signing out sends the browser to end the provider&apos;s session, instead of the end_session_endpoint from discovery. Set it for providers that advertise none, such as Stalwart. Must be https.
</p>
</div>
+4 -4
View File
@@ -219,11 +219,11 @@ export function AuthPage() {
placeholder="https://webmail.example.com"
className={cn(inputClass, 'w-full')}
/>
<p className="text-[11px] text-muted-foreground mt-1">
<p className="text-[0.6875rem] text-muted-foreground mt-1">
Used to register redirect URIs (one per locale: <code>{setupOrigin.trim().replace(/\/+$/, '') || 'https://…'}/&lt;locale&gt;/auth/callback</code>) on Stalwart.
</p>
{!setupOriginValid && setupOrigin.length > 0 && (
<p className="text-[11px] text-destructive mt-1">Must be like https://host with no path.</p>
<p className="text-[0.6875rem] text-destructive mt-1">Must be like https://host with no path.</p>
)}
</div>
<div>
@@ -239,11 +239,11 @@ export function AuthPage() {
placeholder="https://mail.example.com"
className={cn(inputClass, 'w-full')}
/>
<p className="text-[11px] text-muted-foreground mt-1">
<p className="text-[0.6875rem] text-muted-foreground mt-1">
Where Stalwart serves <code>/.well-known/oauth-authorization-server</code>. Saved as <code>OAUTH_ISSUER_URL</code>. Pre-filled from your JMAP server URL.
</p>
{!setupIssuerValid && setupIssuer.length > 0 && (
<p className="text-[11px] text-destructive mt-1">Must be like https://host with no path.</p>
<p className="text-[0.6875rem] text-destructive mt-1">Must be like https://host with no path.</p>
)}
</div>
<label className="inline-flex items-center gap-2 text-xs text-foreground select-none cursor-pointer">
+2 -2
View File
@@ -89,14 +89,14 @@ export function LogsPage() {
<div key={i} className="rounded-lg border border-border p-3 space-y-1.5">
<div className="flex items-center justify-between gap-2">
<Badge className="font-mono text-xs truncate">{entry.action}</Badge>
<span className="text-[11px] text-muted-foreground whitespace-nowrap">
<span className="text-[0.6875rem] text-muted-foreground whitespace-nowrap">
{new Date(entry.ts).toLocaleString()}
</span>
</div>
<div className="text-xs text-foreground break-words">
{formatDetail(entry.detail)}
</div>
<div className="text-[11px] text-muted-foreground font-mono">
<div className="text-[0.6875rem] text-muted-foreground font-mono">
{entry.ip}
</div>
</div>
+1 -1
View File
@@ -12,7 +12,7 @@ import { EVERYONE, useOrgPolicyDraft, type OrgPolicyDraft } from '@/components/a
import { resolvePolicy, type PolicyScope } from '@/lib/admin/policy-scopes';
import { cn } from '@/lib/utils';
const fieldLabelClass = 'mb-1 block text-[11px] font-medium text-muted-foreground';
const fieldLabelClass = 'mb-1 block text-[0.6875rem] font-medium text-muted-foreground';
const DOMAIN_RE = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
const GROUP_RE = /^[^\s@]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
@@ -203,7 +203,7 @@ export function PluginApprovalsPanel() {
</div>
)}
{/* Focusable, so the keyboard can scroll the code. */}
<pre tabIndex={0} aria-label="Plugin code" className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-all rounded-md border border-border p-4 font-mono text-[11px] leading-relaxed">
<pre tabIndex={0} aria-label="Plugin code" className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-all rounded-md border border-border p-4 font-mono text-[0.6875rem] leading-relaxed">
{source?.error ?? source?.code ?? (<span className="inline-flex items-center gap-2"><Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" /> Loading</span>)}
</pre>
</Dialog>
+3 -3
View File
@@ -58,7 +58,7 @@ const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; d
const PAGE = adminPageDef('policy');
const fieldLabelClass = 'mb-1 block text-[11px] font-medium text-muted-foreground';
const fieldLabelClass = 'mb-1 block text-[0.6875rem] font-medium text-muted-foreground';
const cardClass = 'rounded-md border border-border bg-muted/30 p-3';
/** Mirrors the sanitizer's URL rule so the admin sees the drop before saving. */
@@ -442,7 +442,7 @@ export function PolicyPage() {
className={cn(inputClass, 'w-full', urlInvalid && 'border-destructive')}
/>
{urlInvalid && (
<p className="mt-1 text-[11px] text-destructive">Enter a valid http or https URL</p>
<p className="mt-1 text-[0.6875rem] text-destructive">Enter a valid http or https URL</p>
)}
</div>
<div className="sm:col-span-3">
@@ -467,7 +467,7 @@ export function PolicyPage() {
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground"
title={AppIcon ? app.icon : 'Unknown icon - falls back to Globe'}
>
{AppIcon ? <AppIcon className="h-4 w-4" /> : <span className="text-[10px]">?</span>}
{AppIcon ? <AppIcon className="h-4 w-4" /> : <span className="text-[0.625rem]">?</span>}
</span>
</div>
</div>
+3 -3
View File
@@ -123,7 +123,7 @@ export function SettingsPage() {
<ConfigToggleItem label="Allow custom JMAP endpoint" description="Show a JMAP server URL field on the login form, allowing users to connect to any JMAP server" configKey="allowCustomJmapEndpoint" value={currentValue('allowCustomJmapEndpoint') as boolean} source={config.allowCustomJmapEndpoint?.source} onChange={handleChange} onRevert={handleRevert} />
{!!currentValue('allowCustomJmapEndpoint') && (
<ConfigNote tone="warning">
<strong><Term id="cors">CORS</Term> warning:</strong> External JMAP servers must include this domain in their CORS <code className="text-[11px] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or requests from the browser will be blocked.
<strong><Term id="cors">CORS</Term> warning:</strong> External JMAP servers must include this domain in their CORS <code className="text-[0.6875rem] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or requests from the browser will be blocked.
</ConfigNote>
)}
<ConfigToggleItem label="Stalwart features" description="Enable Stalwart Mail Server-specific features" configKey="stalwartFeaturesEnabled" value={currentValue('stalwartFeaturesEnabled') as boolean} source={config.stalwartFeaturesEnabled?.source} onChange={handleChange} onRevert={handleRevert} />
@@ -158,7 +158,7 @@ export function SettingsPage() {
/>
{Array.isArray(jmapServers) && (jmapServers as JmapServerEntry[]).length > 0 && (
<ConfigNote tone="warning">
<strong>CORS warning:</strong> Each JMAP server must allow this webmail&apos;s origin in its <code className="text-[11px] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or browser requests will be blocked.
<strong>CORS warning:</strong> Each JMAP server must allow this webmail&apos;s origin in its <code className="text-[0.6875rem] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or browser requests will be blocked.
</ConfigNote>
)}
</SettingsSection>
@@ -172,7 +172,7 @@ export function SettingsPage() {
Point the client URL at a WOPI-compatible office suite (Collabora Online, OnlyOffice, EuroOffice, …) to let users edit documents in Files. The client URL may be an internal address: the browser loads the editor from the address its discovery advertises. The host override is only needed when the editor reaches this webmail under a different address than the browser does (e.g. Docker networks); give the address alone, the webmail&apos;s base path is added to it. The editor must also allow this webmail as a WOPI host.
</ConfigNote>
<ConfigNote>
&quot;New file&quot; in Files offers .txt plus every format the editor can edit that a blank template exists for: .docx, .xlsx, .pptx, .odt, .ods, .odp and .pdf are built in. List extensions separated by commas (e.g. <code className="text-[11px] bg-muted px-1 py-0.5 rounded">docx,xlsx,pptx</code>) to offer only those. A file named <code className="text-[11px] bg-muted px-1 py-0.5 rounded">new.&lt;ext&gt;</code> in the template directory on this server replaces the built-in blank file for that format or adds a format, so ONLYOFFICE&apos;s <code className="text-[11px] bg-muted px-1 py-0.5 rounded">document-templates/new/&lt;locale&gt;</code> folder works as one.
&quot;New file&quot; in Files offers .txt plus every format the editor can edit that a blank template exists for: .docx, .xlsx, .pptx, .odt, .ods, .odp and .pdf are built in. List extensions separated by commas (e.g. <code className="text-[0.6875rem] bg-muted px-1 py-0.5 rounded">docx,xlsx,pptx</code>) to offer only those. A file named <code className="text-[0.6875rem] bg-muted px-1 py-0.5 rounded">new.&lt;ext&gt;</code> in the template directory on this server replaces the built-in blank file for that format or adds a format, so ONLYOFFICE&apos;s <code className="text-[0.6875rem] bg-muted px-1 py-0.5 rounded">document-templates/new/&lt;locale&gt;</code> folder works as one.
</ConfigNote>
</SettingsSection>
+1 -1
View File
@@ -1343,7 +1343,7 @@ function BrandingAsset({
{value ? (
<img src={withBasePath(value)} alt="" className="max-w-full max-h-full object-contain" />
) : (
<span className="text-[10px] text-muted-foreground text-center px-1">{t('branding.drop')}</span>
<span className="text-[0.625rem] text-muted-foreground text-center px-1">{t('branding.drop')}</span>
)}
</label>
<div className="flex-1 min-w-0">
+2 -2
View File
@@ -195,7 +195,7 @@ export function AdminShell({ authenticated, isStalwartAutoLogin, configReadOnly,
const badgeFor = (id: AdminPageId): ReactNode => {
if (id === 'plugins' && pendingApprovals > 0) {
return (
<span className="ms-auto inline-flex min-w-[18px] items-center justify-center rounded-full bg-amber-500 px-1.5 text-[10px] font-semibold leading-[18px] text-white" title={`${pendingApprovals} plugin${pendingApprovals === 1 ? '' : 's'} waiting for approval`}>
<span className="ms-auto inline-flex min-w-[18px] items-center justify-center rounded-full bg-amber-500 px-1.5 text-[0.625rem] font-semibold leading-[1.125rem] text-white" title={`${pendingApprovals} plugin${pendingApprovals === 1 ? '' : 's'} waiting for approval`}>
{pendingApprovals}
</span>
);
@@ -420,7 +420,7 @@ export function AdminShell({ authenticated, isStalwartAutoLogin, configReadOnly,
{groupIndex > 0 && <div className="mx-1 my-2 border-t border-border" />}
{group.label && (
<div className="px-3 pb-1 pt-2.5">
<span className="text-[11px] font-semibold text-muted-foreground">{group.label}</span>
<span className="text-[0.6875rem] font-semibold text-muted-foreground">{group.label}</span>
</div>
)}
{group.items.map(({ page, sections }) => {
+1 -1
View File
@@ -25,7 +25,7 @@ export function Badge({ tone = 'muted', icon, children, title, className }: Badg
return (
<span
title={title}
className={cn('inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium leading-4 whitespace-nowrap', TONES[tone], className)}
className={cn('inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[0.6875rem] font-medium leading-4 whitespace-nowrap', TONES[tone], className)}
>
{icon}
{children}
@@ -48,7 +48,7 @@ describe('day view on a phone', () => {
it('keeps the small shared chip on a wide screen', () => {
renderDay(false);
const chip = document.querySelector<HTMLElement>('[data-calendar-task="t1"]')!;
expect(chip).toHaveClass('h-full', 'text-[10px]');
expect(chip).toHaveClass('h-full', 'text-[0.625rem]');
});
it('sizes the all-day row by the day in view, not by a busy neighbour', () => {
+2 -2
View File
@@ -220,13 +220,13 @@ export function CalendarAgendaView({
</span>
<span className="min-w-0 leading-tight">
<span className={cn(
"block text-[11px] font-medium truncate first-letter:uppercase",
"block text-[0.6875rem] font-medium truncate first-letter:uppercase",
today ? "text-primary" : "text-muted-foreground",
)}>
{intlFormatter.dateTime(group.date, { month: "short", weekday: "short" })}
</span>
{relativeLabel && (
<span className={cn("block text-[11px] truncate", today ? "text-primary" : "text-muted-foreground")}>
<span className={cn("block text-[0.6875rem] truncate", today ? "text-primary" : "text-muted-foreground")}>
{relativeLabel}
</span>
)}
+7 -7
View File
@@ -383,7 +383,7 @@ export function CalendarDayView({
aria-label={intlFormatter.dateTime(day, { weekday: "long", month: "long", day: "numeric", year: "numeric" })}
>
<div className={cn(
"w-10 text-center text-[11px] font-medium first-letter:uppercase",
"w-10 text-center text-[0.6875rem] font-medium first-letter:uppercase",
today ? "text-primary" : "text-muted-foreground",
)}>
{intlFormatter.dateTime(day, { weekday: "short" })}
@@ -433,7 +433,7 @@ export function CalendarDayView({
>
{allDayEvents.length > 0 && (
<>
<div className="text-[10px] text-muted-foreground mb-1">{t("events.all_day")}</div>
<div className="text-[0.625rem] text-muted-foreground mb-1">{t("events.all_day")}</div>
<div className="space-y-1">
{allDayEvents.map((ev) => {
const calId = getPrimaryCalendarId(ev);
@@ -458,7 +458,7 @@ export function CalendarDayView({
)}
{dayTasks.length > 0 && (
<>
<div className={cn("text-[10px] text-muted-foreground mb-1", allDayEvents.length > 0 && "mt-2")}>{t("tasks.label")}</div>
<div className={cn("text-[0.625rem] text-muted-foreground mb-1", allDayEvents.length > 0 && "mt-2")}>{t("tasks.label")}</div>
<div className="space-y-0.5">
{/* The same chip as the month and week grids. */}
{dayTasks.map((task) => (
@@ -501,7 +501,7 @@ export function CalendarDayView({
<span aria-hidden="true" className={cn(
"absolute end-2 leading-none",
h > firstHour ? "top-0 -translate-y-1/2" : "top-1",
isMobile ? "text-[10px]" : "text-xs",
isMobile ? "text-[0.625rem]" : "text-xs",
)}>
{formatHour(h)}
</span>
@@ -657,7 +657,7 @@ export function CalendarDayView({
height: ((dragCreate.endMinutes - dragCreate.startMinutes) / 60) * hourHeight,
}}
>
<div className="text-[10px] font-medium text-primary px-1.5 py-0.5">
<div className="text-[0.625rem] font-medium text-primary px-1.5 py-0.5">
<bdi dir="ltr">{formatSnapTime(dragCreate.startMinutes, timeFormat)} – {formatSnapTime(dragCreate.endMinutes, timeFormat)}</bdi>
</div>
</div>
@@ -672,7 +672,7 @@ export function CalendarDayView({
<div className="w-2.5 h-2.5 rounded-full bg-primary -ms-1" />
<div className="flex-1 h-0.5 bg-primary rounded-full" />
</div>
<div className="absolute -top-4 start-2 text-[10px] font-medium text-primary bg-background/90 px-1 rounded shadow-sm">
<div className="absolute -top-4 start-2 text-[0.625rem] font-medium text-primary bg-background/90 px-1 rounded shadow-sm">
{formatSnapTime(dropTarget.minutes, timeFormat)}
</div>
</div>
@@ -702,7 +702,7 @@ export function CalendarDayView({
<div className="font-medium truncate">
{pendingPreview.title}
</div>
<div className="text-[10.5px] opacity-85 truncate">
<div className="text-[0.65625rem] opacity-85 truncate">
<bdi dir="ltr">{formatSnapTime(startMin, timeFormat)} – {formatSnapTime(startMin + durationMin, timeFormat)}</bdi>
</div>
</div>
+5 -5
View File
@@ -370,7 +370,7 @@ export function CalendarMonthView({
{dayHeaderKeys.map((d, index) => (
<div key={d} role="columnheader" aria-label={isMobile ? t(`days.${d}`) : undefined} className={cn(
"text-center text-xs font-medium text-muted-foreground py-2 border-e border-border last:border-e-0",
isMobile && "py-1.5 text-[11px]"
isMobile && "py-1.5 text-[0.6875rem]"
)}>
{isMobile ? narrowDayNames[index] ?? t(`days.${d}`) : t(`days.${d}`)}
</div>
@@ -434,7 +434,7 @@ export function CalendarMonthView({
>
<div className="flex items-center justify-center gap-1 mb-0.5">
{firstOfMonth && (
<span className="text-[10px] font-medium text-muted-foreground">
<span className="text-[0.625rem] font-medium text-muted-foreground">
{t(`months.${monthLabelKeys[getMonth(day)]}`)}
</span>
)}
@@ -645,7 +645,7 @@ function MonthCellEntries({
currentUserEmails={currentUserEmails}
draggable={canEditEvent(segment.event)}
tabbable={spanDays.some(inView)}
className={isMobile ? "text-[10px] px-1" : undefined}
className={isMobile ? "text-[0.625rem] px-1" : undefined}
/>
</div>
);
@@ -689,8 +689,8 @@ function MonthCellEntries({
<div
data-pending-preview
className={cn(
"h-full rounded text-[10px] font-medium truncate border-2 border-dashed",
isMobile ? "leading-[16px] px-1" : "leading-[20px] px-1.5"
"h-full rounded text-[0.625rem] font-medium truncate border-2 border-dashed",
isMobile ? "leading-[1rem] px-1" : "leading-[1.25rem] px-1.5"
)}
style={{ borderColor: color, color, backgroundColor: `${color}10` }}
>
@@ -573,7 +573,7 @@ export function CalendarSidebarPanel({
<div className="mt-1">
{owned.length > 0 && (
<div>
<div className="ps-5 pe-3 py-0.5 text-[11px] font-medium text-muted-foreground">
<div className="ps-5 pe-3 py-0.5 text-[0.6875rem] font-medium text-muted-foreground">
{t('my_calendars')}
</div>
<div>
@@ -583,7 +583,7 @@ export function CalendarSidebarPanel({
)}
{sharedGroups.map((sg) => (
<div key={`${group.key}-shared-${sg.label}`} className="mt-2">
<div className="ps-5 pe-3 py-0.5 text-[11px] font-medium text-muted-foreground flex items-center gap-1">
<div className="ps-5 pe-3 py-0.5 text-[0.6875rem] font-medium text-muted-foreground flex items-center gap-1">
<Share2 className="w-3 h-3" />
{sg.label}
</div>
@@ -612,7 +612,7 @@ export function CalendarSidebarPanel({
{renderSectionHeader('__others__', t('other_calendars'))}
{!collapsedAccountGroups.has('__others__') && sharedAccountGroups.map((group) => (
<div key={group.accountName} className="mt-2">
<div className="ps-5 pe-3 py-0.5 text-[11px] font-medium text-muted-foreground flex items-center gap-1">
<div className="ps-5 pe-3 py-0.5 text-[0.6875rem] font-medium text-muted-foreground flex items-center gap-1">
<Share2 className="w-3 h-3" />
{group.accountName}
</div>
+1 -1
View File
@@ -281,7 +281,7 @@ export function CalendarToolbar({
key={v}
onClick={() => onViewModeChange(v)}
className={cn(
"flex-auto min-h-11 px-1.5 text-[11px] font-medium transition-colors touch-manipulation",
"flex-auto min-h-11 px-1.5 text-[0.6875rem] font-medium transition-colors touch-manipulation",
v === viewMode
? "bg-primary text-primary-foreground"
: "text-muted-foreground active:bg-muted"
+8 -8
View File
@@ -472,7 +472,7 @@ export function CalendarWeekView({
className="flex flex-col items-center gap-0.5 pt-2 pb-1.5 border-e border-border last:border-e-0"
>
<div className={cn(
"text-[11px] font-medium first-letter:uppercase",
"text-[0.6875rem] font-medium first-letter:uppercase",
todayCol ? "text-primary" : "text-muted-foreground",
)}>
{intlFormatter.dateTime(day, { weekday: "short" })}
@@ -507,7 +507,7 @@ export function CalendarWeekView({
<div className="flex border-b border-border" role="row">
<div
role="rowheader"
className={cn(gutterClass, "text-[10px] text-muted-foreground p-1 text-end flex flex-col justify-between")}
className={cn(gutterClass, "text-[0.625rem] text-muted-foreground p-1 text-end flex flex-col justify-between")}
style={{ height: allDayHeight }}
>
<span>{t("events.all_day")}</span>
@@ -517,7 +517,7 @@ export function CalendarWeekView({
aria-label={isAllDayExpanded ? t("events.show_less") : t("events.show_more")}
aria-expanded={isAllDayExpanded}
onClick={() => setIsAllDayExpanded((prev) => !prev)}
className="self-end mt-auto text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-0.5 rounded px-1 py-0.5 hover:bg-muted transition-colors cursor-pointer"
className="self-end mt-auto text-[0.625rem] text-muted-foreground hover:text-foreground flex items-center gap-0.5 rounded px-1 py-0.5 hover:bg-muted transition-colors cursor-pointer"
title={isAllDayExpanded ? t("events.show_less") : t("events.show_more")}
>
{isAllDayExpanded ? (
@@ -593,7 +593,7 @@ export function CalendarWeekView({
style={{ insetInlineStart: 1, width: "calc(100% - 2px)", top: baseRow * 24 + 2, height: 20 }}
>
<div
className="h-full rounded-xs px-1.5 text-xs leading-[16px] font-medium truncate border-2 border-dashed"
className="h-full rounded-xs px-1.5 text-xs leading-[1rem] font-medium truncate border-2 border-dashed"
style={{ borderColor: previewColor, color: previewColor, backgroundColor: `${previewColor}10` }}
>
{pendingPreview!.title}
@@ -644,7 +644,7 @@ export function CalendarWeekView({
<span aria-hidden="true" className={cn(
"absolute end-2 leading-none",
h > firstHour ? "top-0 -translate-y-1/2" : "top-1",
isMobile ? "text-[9px]" : "text-[10px]",
isMobile ? "text-[0.5625rem]" : "text-[0.625rem]",
)}>
{formatHour(h)}
</span>
@@ -800,7 +800,7 @@ export function CalendarWeekView({
height: ((dragCreate.endMinutes - dragCreate.startMinutes) / 60) * hourHeight,
}}
>
<div className="text-[10px] font-medium text-primary px-1.5 py-0.5">
<div className="text-[0.625rem] font-medium text-primary px-1.5 py-0.5">
<bdi dir="ltr">{formatSnapTime(dragCreate.startMinutes, timeFormat)} – {formatSnapTime(dragCreate.endMinutes, timeFormat)}</bdi>
</div>
</div>
@@ -815,7 +815,7 @@ export function CalendarWeekView({
<div className="w-2 h-2 rounded-full bg-primary -ms-1" />
<div className="flex-1 h-0.5 bg-primary rounded-full" />
</div>
<div className="absolute -top-4 start-2 text-[10px] font-medium text-primary bg-background/90 px-1 rounded shadow-sm">
<div className="absolute -top-4 start-2 text-[0.625rem] font-medium text-primary bg-background/90 px-1 rounded shadow-sm">
{formatSnapTime(dropTarget.minutes, timeFormat)}
</div>
</div>
@@ -846,7 +846,7 @@ export function CalendarWeekView({
<div className="font-medium truncate">
{pendingPreview.title}
</div>
<div className="text-[10.5px] opacity-85 truncate">
<div className="text-[0.65625rem] opacity-85 truncate">
<bdi dir="ltr">{formatSnapTime(startMin, timeFormat)} – {formatSnapTime(startMin + durationMin, timeFormat)}</bdi>
</div>
</div>
@@ -33,7 +33,7 @@ export function HiddenEventsIndicator({ count, direction, onReveal, tabbable = t
onClick={(e) => { e.stopPropagation(); onReveal(); }}
className={cn(
"absolute inset-x-0 mx-auto w-fit z-20 flex items-center gap-0.5 rounded-full",
"bg-muted/90 px-1.5 py-0.5 text-[10px] leading-none text-muted-foreground",
"bg-muted/90 px-1.5 py-0.5 text-[0.625rem] leading-none text-muted-foreground",
"hover:bg-accent hover:text-foreground transition-colors cursor-pointer",
direction === "before" ? "top-0.5" : "bottom-0.5",
)}
+4 -4
View File
@@ -320,22 +320,22 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
<div className="flex items-baseline gap-1 min-w-0">
<span dir="auto" className={cn("font-medium truncate", isInactive && "line-through")}>{event.title || t("events.no_title")}</span>
{!event.showWithoutTime && durationMinutes < 45 && (
<span className="hidden @min-[120px]:inline flex-shrink-0 opacity-85 text-[10.5px]">{safeFormat(startDate, timeFmt)}</span>
<span className="hidden @min-[120px]:inline flex-shrink-0 opacity-85 text-[0.65625rem]">{safeFormat(startDate, timeFmt)}</span>
)}
</div>
</div>
{!event.showWithoutTime && durationMinutes >= 45 && (
<div className="opacity-85 text-[10.5px]">
<div className="opacity-85 text-[0.65625rem]">
{/* Start before end in a right-to-left layout too. */}
<bdi dir="ltr">{timeString}</bdi>
</div>
)}
{visibleCalendarName && (
<div data-calendar-name dir="auto" className="opacity-85 text-[10.5px] truncate">{visibleCalendarName}</div>
<div data-calendar-name dir="auto" className="opacity-85 text-[0.65625rem] truncate">{visibleCalendarName}</div>
)}
{getParticipantCount(event) > 0 && (
<div
className="flex items-center gap-0.5 opacity-85 text-[10.5px]"
className="flex items-center gap-0.5 opacity-85 text-[0.65625rem]"
title={t("participants.count", { count: getParticipantCount(event) })}
>
<Users className="w-3 h-3" />
+3 -3
View File
@@ -448,12 +448,12 @@ export function EventDetailPopover({
<bdi>{event.title || t("events.no_title")}</bdi>
</h3>
{event.status === "tentative" && (
<span className="mt-1 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-warning/15 text-amber-600 dark:text-amber-400">
<span className="mt-1 inline-flex items-center px-1.5 py-0.5 rounded text-[0.625rem] font-medium bg-warning/15 text-amber-600 dark:text-amber-400">
{t("detail.tentative")}
</span>
)}
{event.status === "cancelled" && (
<span className="mt-1 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-destructive/10 text-red-600 dark:text-red-400 line-through">
<span className="mt-1 inline-flex items-center px-1.5 py-0.5 rounded text-[0.625rem] font-medium bg-destructive/10 text-red-600 dark:text-red-400 line-through">
{t("detail.cancelled")}
</span>
)}
@@ -588,7 +588,7 @@ export function EventDetailPopover({
</span>
)}
</span>
<ParticipantStatusBadge status={p.status} className="text-[10px]" />
<ParticipantStatusBadge status={p.status} className="text-[0.625rem]" />
</div>
))}
{participants.length > 5 && (
+2 -2
View File
@@ -239,7 +239,7 @@ export function MiniCalendar({
key={d}
scope="col"
abbr={weeks[0]?.[index] ? intlFormatter.dateTime(weeks[0][index], { weekday: "long" }) : undefined}
className="text-center text-[10px] font-medium text-muted-foreground py-1"
className="text-center text-[0.625rem] font-medium text-muted-foreground py-1"
>
{t(`days.${d}`)}
</th>
@@ -252,7 +252,7 @@ export function MiniCalendar({
{showWeekNumbers && (
<th
scope="row"
className="w-5 p-0 text-center text-[9px] text-muted-foreground font-medium"
className="w-5 p-0 text-center text-[0.5625rem] text-muted-foreground font-medium"
>
<span aria-hidden="true">{weekNumberOf(week[0])}</span>
<span className="sr-only">{t("mini_week_number", { week: weekNumberOf(week[0]) })}</span>
+1 -1
View File
@@ -183,7 +183,7 @@ export const ParticipantInput = forwardRef<ParticipantInputHandle, ParticipantIn
i === activeIndex ? "bg-accent text-accent-foreground" : "hover:bg-muted"
}`}
>
<Avatar name={s.name} email={s.email} size="sm" className="shrink-0 w-7 h-7 text-[10px]" />
<Avatar name={s.name} email={s.email} size="sm" className="shrink-0 w-7 h-7 text-[0.625rem]" />
<div className="min-w-0">
<div className="font-medium truncate" title={s.name || s.email}>{s.name || s.email}</div>
{s.name && (
+1 -1
View File
@@ -77,7 +77,7 @@ export function PluginDayLabel({ surface, date, className, compact }: PluginDayL
<div
data-plugin-surface="calendar-day-header"
title={tooltip(withText)}
className={cn("max-w-full truncate text-[10px] leading-tight text-muted-foreground", className)}
className={cn("max-w-full truncate text-[0.625rem] leading-tight text-muted-foreground", className)}
>
{withText.map((l) => l.text).join(" · ")}
</div>
+1 -1
View File
@@ -34,7 +34,7 @@ export function CalendarTaskChip({ task, calendar, onToggleComplete, onSelect, t
data-calendar-task={task.id}
className={cn(
"start-bar ps-[3px] rounded font-medium flex items-center overflow-hidden",
touch ? "min-h-11 text-xs" : "h-full text-[10px]",
touch ? "min-h-11 text-xs" : "h-full text-[0.625rem]",
className,
)}
style={{ backgroundColor: `${color}20`, "--start-bar": color } as React.CSSProperties}
@@ -349,7 +349,7 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS
spellCheck={false}
className="h-12 flex-1 min-w-0 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
/>
<kbd className="hidden sm:inline-flex items-center rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">Esc</kbd>
<kbd className="hidden sm:inline-flex items-center rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[0.625rem] text-muted-foreground">Esc</kbd>
{/* Full screen on a phone: no backdrop to tap and no Escape key (R4). */}
<IconButton label={tCommon("close")} size="icon-lg" className="sm:hidden -me-2" onClick={onClose}>
<X />
@@ -371,7 +371,7 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS
return (
<li key={command.id} role="presentation">
{showGroup && (
<div aria-hidden="true" className="px-3 pt-2 pb-1 text-[11px] font-semibold text-muted-foreground">
<div aria-hidden="true" className="px-3 pt-2 pb-1 text-[0.6875rem] font-semibold text-muted-foreground">
{command.group}
</div>
)}
@@ -398,7 +398,7 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS
// Key hints only where there is a keyboard (R8).
<span className="flex-shrink-0 flex items-center gap-1 pointer-coarse:hidden" aria-hidden="true">
{shortcutAlternatives(command.shortcut).map((key) => (
<kbd key={key} className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">{key}</kbd>
<kbd key={key} className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[0.625rem] text-muted-foreground">{key}</kbd>
))}
</span>
)}
+1 -1
View File
@@ -744,7 +744,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
className="absolute inset-0 rounded-full bg-black/55 text-white flex flex-col items-center justify-center gap-0.5 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition-opacity"
>
<Camera className="w-5 h-5" />
<span className="text-[10px] font-medium leading-none">{t("change_photo")}</span>
<span className="text-[0.625rem] font-medium leading-none">{t("change_photo")}</span>
</span>
<input
ref={photoInputRef}
+2 -2
View File
@@ -435,7 +435,7 @@ export function ContactList({
>
<Filter className="w-4 h-4" />
{!filtersOpen && activeFilters > 0 && (
<span className="absolute -top-1 -end-1 flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-primary-foreground">
<span className="absolute -top-1 -end-1 flex items-center justify-center w-4 h-4 text-[0.625rem] font-bold rounded-full bg-primary text-primary-foreground">
{activeFilters}
</span>
)}
@@ -752,7 +752,7 @@ export function ContactList({
<div>
{groupedSections.map(({ letter, items }) => (
<section key={letter} role="group" aria-label={letter}>
<div aria-hidden className="sticky top-0 z-10 px-4 py-0.5 bg-background/90 backdrop-blur-sm text-[11px] font-medium text-muted-foreground">
<div aria-hidden className="sticky top-0 z-10 px-4 py-0.5 bg-background/90 backdrop-blur-sm text-[0.6875rem] font-medium text-muted-foreground">
{letter}
</div>
{items.map(renderItem)}
+2 -2
View File
@@ -373,7 +373,7 @@ export function ContactsSidebar({
<div className="ps-2">
{owned.length > 0 && (
<div className="mt-1">
<div className="px-3 py-0.5 text-[11px] font-medium text-muted-foreground">
<div className="px-3 py-0.5 text-[0.6875rem] font-medium text-muted-foreground">
{t("address_books.title")}
</div>
{owned.map((book) => (
@@ -391,7 +391,7 @@ export function ContactsSidebar({
)}
{sharedGroups.map((sg) => (
<div key={`${group.key}-shared-${sg.label}`} className="mt-1">
<div className="px-3 py-0.5 text-[11px] font-medium text-muted-foreground flex items-center gap-1">
<div className="px-3 py-0.5 text-[0.6875rem] font-medium text-muted-foreground flex items-center gap-1">
<Share2 className="w-3 h-3" />
{sg.label}
</div>
@@ -893,7 +893,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
{/* Eyebrow + title + collapse */}
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="text-[11px] font-semibold text-muted-foreground">
<div className="text-[0.6875rem] font-semibold text-muted-foreground">
{bannerTitle}
</div>
{summary?.title && (
@@ -908,7 +908,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
<div className="flex items-center gap-1.5 flex-shrink-0">
{parsedEvent?.sequence != null && parsedEvent.sequence > 0 && (
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground whitespace-nowrap">
<span className="rounded-full bg-muted px-2 py-0.5 text-[0.625rem] font-medium text-muted-foreground whitespace-nowrap">
{t('event_updated', { sequence: parsedEvent.sequence })}
</span>
)}
@@ -972,7 +972,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
<div className="flex flex-wrap items-center gap-1.5">
{parsedEvent?.status && parsedEvent.status !== 'confirmed' && (
<span className={cn(
"rounded-full px-2 py-0.5 text-[11px] font-medium",
"rounded-full px-2 py-0.5 text-[0.6875rem] font-medium",
parsedEvent.status === 'cancelled'
? "bg-destructive/15 text-destructive"
: "bg-warning/15 text-warning",
@@ -981,25 +981,25 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
</span>
)}
{existingEvent && (
<span className="rounded-full bg-success/15 px-2 py-0.5 text-[11px] font-medium text-success">
<span className="rounded-full bg-success/15 px-2 py-0.5 text-[0.6875rem] font-medium text-success">
{t('already_in_calendar')}
</span>
)}
{userIsOrganizer && (
<span className="rounded-full bg-info/15 px-2 py-0.5 text-[11px] font-medium text-info">
<span className="rounded-full bg-info/15 px-2 py-0.5 text-[0.6875rem] font-medium text-info">
{t('organizer_role')}
</span>
)}
{participationLabel && myParticipant && (
<span className={cn(
'rounded-full px-2 py-0.5 text-[11px] font-medium',
'rounded-full px-2 py-0.5 text-[0.6875rem] font-medium',
getParticipationTone(currentRsvp),
)}>
{t('your_response', { status: participationLabel })}
</span>
)}
{actionNotice && (
<span className="rounded-full bg-success/15 px-2 py-0.5 text-[11px] font-medium text-success">
<span className="rounded-full bg-success/15 px-2 py-0.5 text-[0.6875rem] font-medium text-success">
{actionNotice}
</span>
)}
+1 -1
View File
@@ -51,7 +51,7 @@ export function ComposerToolbarMenu({ icon: Icon, label, activeCount = 0, testId
screen has no room for the labels either (#1375). */}
<span className="hidden @[40rem]/composer-bar:inline text-sm">{label}</span>
{activeCount > 0 && (
<span className="min-w-4 h-4 rounded-full bg-primary px-1 text-[10px] font-semibold leading-4 text-primary-foreground">
<span className="min-w-4 h-4 rounded-full bg-primary px-1 text-[0.625rem] font-semibold leading-4 text-primary-foreground">
{activeCount}
</span>
)}
+1 -1
View File
@@ -5142,7 +5142,7 @@ const AutocompleteDropdown = React.forwardRef<HTMLDivElement, {
<Users className="w-3.5 h-3.5 text-primary" aria-hidden />
</span>
) : (
<Avatar name={r.name} email={r.email} size="sm" className="shrink-0 w-6 h-6 text-[10px]" />
<Avatar name={r.name} email={r.email} size="sm" className="shrink-0 w-6 h-6 text-[0.625rem]" />
)}
{/* Name above address on a phone: side by side, both were cut to
a few letters in the narrow field ("Use… <userb@exa…"). */}
+24 -24
View File
@@ -784,7 +784,7 @@ function MvConversationCard({ email, onOpen, timeFormat, compact }: { email: Ema
{sender?.name || sender?.email || ''}
</span>
{email.isScheduled && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-medium bg-primary/10 text-primary shrink-0">
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[0.6875rem] font-medium bg-primary/10 text-primary shrink-0">
<CalendarClock className="w-3 h-3" />
{email.scheduledSendAt ? formatScheduledSendAt(email.scheduledSendAt, timeFormat) : tSidebar('scheduled')}
</span>
@@ -905,7 +905,7 @@ export function MvExpandedConversationCard({
<span className="flex items-baseline gap-2">
<span dir="auto" className="text-sm font-semibold truncate">{sender?.name || sender?.email || ''}</span>
{email.isScheduled && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-medium bg-primary/10 text-primary shrink-0">
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[0.6875rem] font-medium bg-primary/10 text-primary shrink-0">
<CalendarClock className="w-3 h-3" />
{email.scheduledSendAt ? formatScheduledSendAt(email.scheduledSendAt, timeFormat) : tSidebar('scheduled')}
</span>
@@ -4382,7 +4382,7 @@ export function EmailViewer({
title={t('tooltips.archive')}
>
<Archive className="w-4 h-4" />
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('archive')}</span>}
{showToolbarLabels && <span className="text-[0.625rem] leading-tight sm:text-sm">{t('archive')}</span>}
</Button>
)}
{/* Delete */}
@@ -4394,7 +4394,7 @@ export function EmailViewer({
title={t('tooltips.delete')}
>
<Trash2 className="w-4 h-4" />
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('delete')}</span>}
{showToolbarLabels && <span className="text-[0.625rem] leading-tight sm:text-sm">{t('delete')}</span>}
</Button>
{/* Move to folder */}
{moveTree.length > 0 && onMoveToMailbox && (
@@ -4414,7 +4414,7 @@ export function EmailViewer({
aria-expanded={moveMenuOpen}
>
<FolderInput className="w-4 h-4" />
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('move')}</span>}
{showToolbarLabels && <span className="text-[0.625rem] leading-tight sm:text-sm">{t('move')}</span>}
</Button>
<ContextMenu ref={moveMenu.menuRef} isOpen={moveMenuOpen} position={moveMenu.contextMenu.position} onClose={closeMoveMenu} label={t('move_to')}>
<div className="w-56 max-h-72 overflow-y-auto">
@@ -4446,7 +4446,7 @@ export function EmailViewer({
aria-expanded={tagMenuOpen}
>
<Tag className="w-4 h-4" />
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('tag')}</span>}
{showToolbarLabels && <span className="text-[0.625rem] leading-tight sm:text-sm">{t('tag')}</span>}
</Button>
<ContextMenu ref={tagMenu.menuRef} isOpen={tagMenuOpen} position={tagMenu.contextMenu.position} onClose={closeTagMenu} label={t('tag')}>
<div className="w-56">
@@ -4481,7 +4481,7 @@ export function EmailViewer({
) : (
<ShieldAlert className={cn("h-4 w-4", !mvViewer && "text-red-600 dark:text-red-400")} />
)}
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}</span>}
{showToolbarLabels && <span className="text-[0.625rem] leading-tight sm:text-sm">{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}</span>}
</Button>
)}
@@ -4503,7 +4503,7 @@ export function EmailViewer({
>
{isUnread ? <MailOpen className="w-4 h-4" /> : <Mail className="w-4 h-4" />}
{showToolbarLabels && (
<span className="grid text-center text-[10px] leading-tight sm:text-sm">
<span className="grid text-center text-[0.625rem] leading-tight sm:text-sm">
<span className={cn("col-start-1 row-start-1", !isUnread && "invisible")} aria-hidden={!isUnread}>{t('read')}</span>
<span className={cn("col-start-1 row-start-1", isUnread && "invisible")} aria-hidden={isUnread}>{t('unread')}</span>
</span>
@@ -4594,7 +4594,7 @@ export function EmailViewer({
}}
>
<MoreVertical className="w-4 h-4 text-muted-foreground" />
{interfaceLayout !== 'mountain-view' && <span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span>}
{interfaceLayout !== 'mountain-view' && <span className="text-[0.625rem] leading-tight sm:hidden">{t('more_actions')}</span>}
</Button>
{readerMenu.contextMenu.isOpen && !isMobile && (
<ReaderMoreMenu
@@ -4791,7 +4791,7 @@ export function EmailViewer({
blocks below are h3 and h4, so no level is skipped. */}
<h2 dir="auto" className={cn(
"text-foreground break-words min-w-0",
mvViewer ? "text-[22px] leading-7 font-normal" : "text-lg lg:text-xl font-bold tracking-tight"
mvViewer ? "text-[1.375rem] leading-7 font-normal" : "text-lg lg:text-xl font-bold tracking-tight"
)}>
{email.subject || t('no_subject')}
</h2>
@@ -5129,7 +5129,7 @@ export function EmailViewer({
)}>
{getAttachmentDisplayName(attachment.name, attachment.type)}
</span>
<span className="text-[10px] text-muted-foreground flex-shrink-0">
<span className="text-[0.625rem] text-muted-foreground flex-shrink-0">
<bdi>{formatFileSize(attachment.size)}</bdi>
</span>
</div>
@@ -5198,7 +5198,7 @@ export function EmailViewer({
<span className="text-xs text-foreground truncate max-w-[180px]">
{getAttachmentDisplayName(attachment.name, attachment.type)}
</span>
<span className="text-[10px] text-muted-foreground ms-auto flex-shrink-0">
<span className="text-[0.625rem] text-muted-foreground ms-auto flex-shrink-0">
<bdi>{formatFileSize(attachment.size)}</bdi>
</span>
<div className="absolute inset-y-0 end-0 rounded-e-md bg-background/95 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto transition-opacity flex items-center justify-center gap-1 px-1.5">
@@ -5393,7 +5393,7 @@ export function EmailViewer({
const totalAttachmentSize = effectiveAttachments.reduce((s, a) => s + (a.size || 0), 0);
const topMimeType = email.bodyStructure?.type;
const SectionHeader = ({ children }: { children: React.ReactNode }) => (
<div className="text-[11px] font-semibold text-muted-foreground mb-1.5">
<div className="text-[0.6875rem] font-semibold text-muted-foreground mb-1.5">
{children}
</div>
);
@@ -5427,7 +5427,7 @@ export function EmailViewer({
>
<Icon className={cn("w-3.5 h-3.5 flex-shrink-0", status.color)} />
<span className="font-medium text-foreground">{name}</span>
<span className={cn("text-[11px]", status.color)}>
<span className={cn("text-[0.6875rem]", status.color)}>
{translateAuthResult(result)}
</span>
{extra && (
@@ -5577,7 +5577,7 @@ export function EmailViewer({
)} />
<span className="font-medium text-foreground">{t('authentication.spam_score')}</span>
<span className={cn(
"text-[11px]",
"text-[0.6875rem]",
email.spamScore > 5 ? "text-red-700 dark:text-red-400" :
email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" :
"text-green-700 dark:text-green-400",
@@ -5788,7 +5788,7 @@ export function EmailViewer({
<ShieldAlert className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="text-[11px] font-semibold text-muted-foreground">
<div className="text-[0.6875rem] font-semibold text-muted-foreground">
{senderVerificationLabel}
</div>
<div className="text-sm font-medium text-foreground break-words">
@@ -5809,7 +5809,7 @@ export function EmailViewer({
</div>
<div className="flex-1 min-w-0 space-y-2">
<div>
<div className="text-[11px] font-semibold text-muted-foreground">
<div className="text-[0.6875rem] font-semibold text-muted-foreground">
External Content
</div>
<div className="text-sm font-medium text-foreground break-words">
@@ -6200,7 +6200,7 @@ export function EmailViewer({
<span className="text-xs text-foreground truncate max-w-[180px]">
{getAttachmentDisplayName(attachment.name, attachment.type)}
</span>
<span className="text-[10px] text-muted-foreground ms-auto flex-shrink-0">
<span className="text-[0.625rem] text-muted-foreground ms-auto flex-shrink-0">
<bdi>{formatFileSize(attachment.size)}</bdi>
</span>
{canSaveToFiles && attachment.blobId && (
@@ -6510,7 +6510,7 @@ export function EmailViewer({
aria-label={t('tooltips.previous')}
>
<ChevronLeft className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t('previous')}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t('previous')}</span>
</button>
{isDraft && onEditDraft ? (
<button
@@ -6519,7 +6519,7 @@ export function EmailViewer({
aria-label={t('tooltips.edit_draft')}
>
<Pencil className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t('edit_draft')}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t('edit_draft')}</span>
</button>
) : (
<>
@@ -6528,21 +6528,21 @@ export function EmailViewer({
className="flex flex-col items-center justify-center gap-1 py-2 px-1 min-h-[44px] grow shrink-0 basis-[64px] text-muted-foreground active:text-foreground transition-colors duration-150"
>
<Reply className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t('reply')}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t('reply')}</span>
</button>
<button
onClick={onReplyAll}
className="flex flex-col items-center justify-center gap-1 py-2 px-1 min-h-[44px] grow shrink-0 basis-[64px] text-muted-foreground active:text-foreground transition-colors duration-150"
>
<ReplyAll className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t('reply_all')}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t('reply_all')}</span>
</button>
<button
onClick={onForward}
className="flex flex-col items-center justify-center gap-1 py-2 px-1 min-h-[44px] grow shrink-0 basis-[64px] text-muted-foreground active:text-foreground transition-colors duration-150"
>
<Forward className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t('forward')}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t('forward')}</span>
</button>
</>)}
<button
@@ -6555,7 +6555,7 @@ export function EmailViewer({
aria-label={t('tooltips.next')}
>
<ChevronRight className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t('next')}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t('next')}</span>
</button>
</div>
</nav>
+1 -1
View File
@@ -37,7 +37,7 @@ export function ReadReceiptBanner({ requestedBy, onSend, onIgnore }: ReadReceipt
</div>
<div className="flex-1 min-w-0 space-y-2">
<div>
<div className="text-[11px] font-semibold text-muted-foreground">
<div className="text-[0.6875rem] font-semibold text-muted-foreground">
Read receipt
</div>
<div className="text-sm font-medium text-foreground break-words">
+2 -2
View File
@@ -224,7 +224,7 @@ export const RecipientMentionList = forwardRef<RecipientMentionListHandle, Recip
command(item);
}}
>
<Avatar name={item.name} email={item.email} size="sm" className="shrink-0 w-6 h-6 text-[10px]" />
<Avatar name={item.name} email={item.email} size="sm" className="shrink-0 w-6 h-6 text-[0.625rem]" />
{/* What will be inserted comes first: for "Nagy János" that may be
the surname, which the full name alone would not show. */}
<span className="font-medium shrink-0">@{item.label}</span>
@@ -234,7 +234,7 @@ export const RecipientMentionList = forwardRef<RecipientMentionListHandle, Recip
{/* Picking a contact also adds them as a recipient; say so
before it happens. */}
{item.addsTo && (
<span className="ms-auto shrink-0 rounded px-1.5 py-0.5 text-[11px] bg-muted text-muted-foreground">
<span className="ms-auto shrink-0 rounded px-1.5 py-0.5 text-[0.6875rem] bg-muted text-muted-foreground">
+ {t(item.addsTo)}
</span>
)}
+1 -1
View File
@@ -116,7 +116,7 @@ export const ReplyQuoteCollapse = Extension.create<{ show: string; hide: string
const button = document.createElement('button');
button.type = 'button';
button.textContent = '•••';
button.className = 'my-1 inline-block cursor-pointer rounded-full bg-foreground/10 px-3 py-[3px] text-[11px] leading-none tracking-[2px] text-muted-foreground hover:bg-foreground/20 hover:text-foreground';
button.className = 'my-1 inline-block cursor-pointer rounded-full bg-foreground/10 px-3 py-[3px] text-[0.6875rem] leading-none tracking-[2px] text-muted-foreground hover:bg-foreground/20 hover:text-foreground';
button.title = open ? labels.hide : labels.show;
button.setAttribute('aria-label', button.title);
button.setAttribute('aria-expanded', String(open));
+1 -1
View File
@@ -876,7 +876,7 @@ export function RichTextEditor({
) : (
<span className="flex items-baseline justify-center leading-none" aria-hidden>
<span className="text-sm font-semibold">A</span>
<span className="text-[9px] font-semibold">A</span>
<span className="text-[0.5625rem] font-semibold">A</span>
</span>
)}
</ToolbarButton>
+2 -2
View File
@@ -20,7 +20,7 @@ export type TagBadgeVariant = "badge" | "dot";
* it rather than approximating its padding and text size.
*/
export const TAG_LOZENGE_CLASS =
"inline-flex min-w-0 shrink-0 items-center rounded-full px-2 py-0.5 text-[11px] font-medium";
"inline-flex min-w-0 shrink-0 items-center rounded-full px-2 py-0.5 text-[0.6875rem] font-medium";
/**
* The row a group of tags sits in. Using it for neighbouring lozenges too keeps
@@ -74,7 +74,7 @@ export function TagBadge({
return (
<span
className={cn(
"inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full border px-0.5 text-[9px] font-bold leading-none",
"inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full border px-0.5 text-[0.5625rem] font-bold leading-none",
color.fill,
color.border,
color.text,
+1 -1
View File
@@ -146,7 +146,7 @@ export function NewFileDialog({ isOpen, onClose, onSubmit, formats, validate }:
>
{getFileIconByName(`file.${ext}`, "md")}
<span className="text-xs font-medium leading-tight">{label(ext)}</span>
<span className="text-[11px] text-muted-foreground">.{ext}</span>
<span className="text-[0.6875rem] text-muted-foreground">.{ext}</span>
</button>
))}
</div>
@@ -139,7 +139,7 @@ export function GlobalSearchPalette({ proShell = true }: { proShell?: boolean })
<div key={kind} role="group" aria-label={label} className="py-1">
<div aria-hidden="true" className="flex items-center gap-2 px-3 pt-2 pb-1 text-xs font-semibold text-muted-foreground">
{label}
<span className="inline-flex min-w-[18px] h-[18px] items-center justify-center rounded-full bg-muted px-1.5 text-[10px] font-semibold tabular-nums normal-case">
<span className="inline-flex min-w-[18px] h-[18px] items-center justify-center rounded-full bg-muted px-1.5 text-[0.625rem] font-semibold tabular-nums normal-case">
{hits.length}
</span>
{status.status === 'loading' && <Loader2 className="w-3 h-3 animate-spin" />}
@@ -70,7 +70,7 @@ export function SearchResultList({ outcome, kinds, flat, selectedKey, onSelect,
<section key={kind} className="pb-2">
<h3 className="flex items-center gap-2 px-3 pt-3 pb-1.5 text-xs font-semibold text-muted-foreground sticky top-0 z-10 bg-background">
{t(`scope_${kind}`)}
<span className="inline-flex min-w-[18px] h-[18px] items-center justify-center rounded-full bg-muted px-1.5 text-[10px] font-semibold tabular-nums normal-case">
<span className="inline-flex min-w-[18px] h-[18px] items-center justify-center rounded-full bg-muted px-1.5 text-[0.625rem] font-semibold tabular-nums normal-case">
{hits.length}{status.hasMore ? '+' : ''}
</span>
{status.status === 'loading' && <Loader2 className="w-3 h-3 animate-spin" aria-label={t('searching')} />}
+1 -1
View File
@@ -468,7 +468,7 @@ export function AccountSwitcher({
account.isConnected ? "bg-green-500" : "bg-muted-foreground/40"
)} />
)}
<span className="text-[10px] text-muted-foreground truncate">
<span className="text-[0.625rem] text-muted-foreground truncate">
{(() => { try { return toUnicodeDomain(new URL(account.serverUrl).hostname); } catch { return account.serverUrl; } })()}
</span>
</div>
+2 -2
View File
@@ -114,7 +114,7 @@ function PanelHeader({ title, subtitle, href, onClose, children }: {
return (
<div className="flex items-start gap-1 px-4 pt-4 pb-2">
<div className="min-w-0 flex-1">
<div className={cn(subtitle ? "text-[11px] font-medium text-muted-foreground" : "text-[22px] leading-7")}>{title}</div>
<div className={cn(subtitle ? "text-[0.6875rem] font-medium text-muted-foreground" : "text-[1.375rem] leading-7")}>{title}</div>
{subtitle && <div className="text-sm font-medium text-primary">{subtitle}</div>}
</div>
{children}
@@ -224,7 +224,7 @@ function CalendarPanel({ onClose }: { onClose: () => void }) {
<div className="relative" style={{ height: 24 * HOUR_PX }}>
{Array.from({ length: 24 }, (_, h) => (
<div key={h} className="absolute inset-x-0 flex" style={{ top: h * HOUR_PX }}>
<span className="w-14 shrink-0 -translate-y-2 pe-2 text-end text-[10px] text-muted-foreground">
<span className="w-14 shrink-0 -translate-y-2 pe-2 text-end text-[0.625rem] text-muted-foreground">
{h === 0 ? "" : new Date(2000, 0, 1, h).toLocaleTimeString(undefined, { hour: "numeric", hour12: timeFormat === "12h" })}
</span>
<span className="flex-1 border-t border-border" />
+2 -2
View File
@@ -218,7 +218,7 @@ export function MvTopBar({
)}
<Link href="/" onClick={(e) => { interceptLeave(e); }} className="flex items-center gap-2 pe-4 min-w-0" aria-label={appName}>
{logoUrl ? <img src={logoUrl} alt="" className="w-8 h-8 object-contain" /> : null}
<span className="text-[22px] leading-none text-foreground truncate">{appName}</span>
<span className="text-[1.375rem] leading-none text-foreground truncate">{appName}</span>
</Link>
</div>
@@ -251,7 +251,7 @@ export function MvTopBar({
>
<SlidersHorizontal className="w-5 h-5" />
{!search.filtersOpen && search.activeFilterCount > 0 && (
<span className="absolute top-1.5 end-1.5 grid place-items-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-primary-foreground">
<span className="absolute top-1.5 end-1.5 grid place-items-center w-4 h-4 text-[0.625rem] font-bold rounded-full bg-primary text-primary-foreground">
{search.activeFilterCount}
</span>
)}
+9 -9
View File
@@ -163,7 +163,7 @@ function StorageQuotaCircle({ quota, usagePercent }: { quota: StorageQuota; usag
style={{ transition: "stroke-dasharray 0.3s" }}
/>
</svg>
<span className="absolute text-[7px] font-bold text-muted-foreground tabular-nums">
<span className="absolute text-[0.4375rem] font-bold text-muted-foreground tabular-nums">
{Math.round(usagePercent)}%
</span>
</button>
@@ -198,7 +198,7 @@ function StorageQuotaCircle({ quota, usagePercent }: { quota: StorageQuota; usag
style={{ width: `${usagePercent}%` }}
/>
</div>
<p className="text-[10px] text-muted-foreground mt-1 tabular-nums">
<p className="text-[0.625rem] text-muted-foreground mt-1 tabular-nums">
{Math.round(usagePercent)}% {t("storage_used").toLowerCase()}
</p>
</div>,
@@ -415,7 +415,7 @@ export function NavigationRail({
{badgeShown(item) && (unreadCountStyle === 'dot' ? (
<span aria-hidden="true" data-testid="rail-unread-dot" className="absolute -top-0.5 -end-1 w-2.5 h-2.5 rounded-full bg-red-500" />
) : (
<span aria-hidden="true" className="absolute -top-1.5 -end-2.5 flex items-center justify-center min-w-[16px] h-4 text-[10px] font-bold rounded-full bg-red-600 text-white px-1">
<span aria-hidden="true" className="absolute -top-1.5 -end-2.5 flex items-center justify-center min-w-[16px] h-4 text-[0.625rem] font-bold rounded-full bg-red-600 text-white px-1">
{item.badge! > 99 ? "99+" : item.badge}
</span>
))}
@@ -423,7 +423,7 @@ export function NavigationRail({
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-4 h-0.5 rounded-full bg-primary" />
)}
</div>
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t(item.labelKey)}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t(item.labelKey)}</span>
</Link>
);
})}
@@ -457,7 +457,7 @@ export function NavigationRail({
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-4 h-0.5 rounded-full bg-primary" />
)}
</div>
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{page.title}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{page.title}</span>
</Link>
);
})}
@@ -492,7 +492,7 @@ export function NavigationRail({
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-4 h-0.5 rounded-full bg-primary" />
)}
</div>
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{app.name}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{app.name}</span>
</button>
);
})}
@@ -523,7 +523,7 @@ export function NavigationRail({
/>
)}
</span>
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t("admin") || "Admin"}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t("admin") || "Admin"}</span>
{updateLabel && <span className="sr-only">{`, ${updateLabel}`}</span>}
</a>
)}
@@ -548,7 +548,7 @@ export function NavigationRail({
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-4 h-0.5 rounded-full bg-primary" />
)}
</div>
<span className="text-[10px] font-medium leading-tight truncate max-w-full">{t("settings")}</span>
<span className="text-[0.625rem] font-medium leading-tight truncate max-w-full">{t("settings")}</span>
</Link>
</nav>
);
@@ -636,7 +636,7 @@ export function NavigationRail({
)} />
) : (
<span aria-hidden="true" className={cn(
"absolute flex items-center justify-center min-w-[16px] h-4 text-[10px] font-bold rounded-full bg-red-600 text-white px-1",
"absolute flex items-center justify-center min-w-[16px] h-4 text-[0.625rem] font-bold rounded-full bg-red-600 text-white px-1",
collapsed ? "-top-0.5 -end-0.5" : "end-1.5"
)}>
{item.badge! > 99 ? "99+" : item.badge}
+3 -3
View File
@@ -209,12 +209,12 @@ function ManagedAppsList() {
<p className="font-medium text-sm truncate">{app.name}</p>
<p className="text-xs text-muted-foreground truncate">{app.url}</p>
</div>
<span className="text-[10px] px-1.5 py-0.5 rounded-full font-medium bg-muted text-muted-foreground inline-flex items-center gap-1">
<span className="text-[0.625rem] px-1.5 py-0.5 rounded-full font-medium bg-muted text-muted-foreground inline-flex items-center gap-1">
<Lock className="w-2.5 h-2.5" />
{t('managed_badge')}
</span>
<span className={cn(
'text-[10px] px-1.5 py-0.5 rounded-full font-medium',
'text-[0.625rem] px-1.5 py-0.5 rounded-full font-medium',
app.openMode === 'inline'
? 'bg-blue-100 text-blue-700 dark:bg-blue-950/40 dark:text-blue-400'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400'
@@ -341,7 +341,7 @@ export function SidebarAppsModal({ isOpen, onClose }: SidebarAppsModalProps) {
<p className="text-xs text-muted-foreground truncate">{app.url}</p>
</div>
<span className={cn(
'text-[10px] px-1.5 py-0.5 rounded-full font-medium',
'text-[0.625rem] px-1.5 py-0.5 rounded-full font-medium',
app.openMode === 'inline'
? 'bg-blue-100 text-blue-700 dark:bg-blue-950/40 dark:text-blue-400'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400'
+2 -2
View File
@@ -1040,7 +1040,7 @@ function DemoBanner() {
<div className="flex items-center gap-1.5">
<button
onClick={handleStartTour}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-primary/10 hover:bg-primary/20 transition-colors"
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[0.625rem] font-medium bg-primary/10 hover:bg-primary/20 transition-colors"
title={t("demo_tour")}
>
<PlayCircle className="w-3 h-3" />
@@ -1049,7 +1049,7 @@ function DemoBanner() {
<button
onClick={handleReset}
disabled={isResetting}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-primary/10 hover:bg-primary/20 transition-colors disabled:opacity-50"
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[0.625rem] font-medium bg-primary/10 hover:bg-primary/20 transition-colors disabled:opacity-50"
title={t("demo_reset")}
>
{isResetting ? (
+1 -1
View File
@@ -5109,7 +5109,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
>
<Filter className="w-4 h-4" />
{!isAdvancedSearchOpen && activeFilterCount(searchFilters) > 0 && (
<span className="absolute -top-1 -end-1 flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-primary-foreground">
<span className="absolute -top-1 -end-1 flex items-center justify-center w-4 h-4 text-[0.625rem] font-bold rounded-full bg-primary text-primary-foreground">
{activeFilterCount(searchFilters)}
</span>
)}
@@ -88,12 +88,12 @@ export function NotificationPanel({ onClose, className, sheet = false }: { onClo
<span className="flex-1 min-w-0">
<span className="flex items-baseline gap-2">
<span className={cn("flex-1 min-w-0 truncate text-sm", !entry.read && "font-semibold")}>{entry.title}</span>
<time dateTime={new Date(entry.time).toISOString()} className="flex-shrink-0 text-[11px] text-muted-foreground tabular-nums">
<time dateTime={new Date(entry.time).toISOString()} className="flex-shrink-0 text-[0.6875rem] text-muted-foreground tabular-nums">
{format.relativeTime(new Date(entry.time), now)}
</time>
</span>
{entry.body && <span className="block text-xs text-muted-foreground line-clamp-2 break-words">{entry.body}</span>}
<span className="block text-[11px] text-muted-foreground truncate">
<span className="block text-[0.6875rem] text-muted-foreground truncate">
{entry.level !== "info" && <span className="sr-only">{t(entry.level === "error" ? "level_error" : "level_warning")}: </span>}
{source}
</span>
@@ -269,7 +269,7 @@ export function NotificationBell({ placement, className }: { placement: "side" |
{unread > 0 && (
<span
className={cn(
"absolute flex items-center justify-center min-w-[16px] h-4 px-1 rounded-full bg-red-600 text-white text-[10px] font-bold",
"absolute flex items-center justify-center min-w-[16px] h-4 px-1 rounded-full bg-red-600 text-white text-[0.625rem] font-bold",
placement === "side" ? "-top-0.5 -end-0.5" : "top-1.5 end-1.5",
)}
aria-hidden="true"
+1 -1
View File
@@ -183,7 +183,7 @@ export function PluginActions({ surface, context, variant = 'buttons', mailboxRo
{action.badge ? (
<span
data-plugin-action-badge=""
className="absolute -top-1 -end-1 flex items-center justify-center min-w-4 h-4 px-1 text-[10px] font-bold rounded-full bg-primary text-primary-foreground"
className="absolute -top-1 -end-1 flex items-center justify-center min-w-4 h-4 px-1 text-[0.625rem] font-bold rounded-full bg-primary text-primary-foreground"
>
{badgeText(action.badge)}
</span>
@@ -46,7 +46,7 @@ export function PluginPermissionList({ permissions, httpOrigins, apiPostPaths, c
{targets && targets.length > 0 && (
<ul className="mt-1 space-y-0.5">
{targets.map((target) => (
<li key={target} className="break-all font-mono text-[11px] text-foreground">{target}</li>
<li key={target} className="break-all font-mono text-[0.6875rem] text-foreground">{target}</li>
))}
</ul>
)}
@@ -145,13 +145,13 @@ export function ProtocolAccountPicker({
{account.displayName || account.label}
</span>
{isActive && (
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary">
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[0.625rem] font-medium text-primary">
{t("active_account")}
</span>
)}
</div>
<p className="truncate text-xs text-muted-foreground">{account.email || account.username}</p>
<p className="truncate text-[10px] text-muted-foreground">{host}</p>
<p className="truncate text-[0.625rem] text-muted-foreground">{host}</p>
</div>
</button>
);
+4 -4
View File
@@ -84,14 +84,14 @@ export function SearchSuggestions({
const heading =
(i === 0 && hasRecent && suggestion.kind === "recent" && (
<div className="px-3 pt-1 pb-0.5 text-[11px] font-medium text-muted-foreground">
<div className="px-3 pt-1 pb-0.5 text-[0.6875rem] font-medium text-muted-foreground">
{t("suggestions_recent")}
</div>
)) ||
(i === firstContactIndex && (
<div
className={cn(
"px-3 pt-1 pb-0.5 text-[11px] font-medium text-muted-foreground",
"px-3 pt-1 pb-0.5 text-[0.6875rem] font-medium text-muted-foreground",
hasRecent && "mt-1 border-t border-border pt-2",
)}
>
@@ -154,7 +154,7 @@ export function SearchSuggestions({
onSelect(suggestion);
}}
>
<Avatar name={suggestion.name} email={suggestion.email} size="sm" className="shrink-0 w-6 h-6 text-[10px]" />
<Avatar name={suggestion.name} email={suggestion.email} size="sm" className="shrink-0 w-6 h-6 text-[0.625rem]" />
<span className="flex-1 min-w-0 flex items-baseline gap-1.5 truncate">
{suggestion.name ? (
<>
@@ -165,7 +165,7 @@ export function SearchSuggestions({
<HighlightedText text={suggestion.email} query={query} className="truncate" />
)}
</span>
<span className="shrink-0 flex items-center gap-1 text-[11px]">
<span className="shrink-0 flex items-center gap-1 text-[0.6875rem]">
<span className="px-1.5 py-0.5 rounded bg-primary/10 text-primary font-medium">{t("from")}</span>
<button
type="button"
+3 -3
View File
@@ -41,7 +41,7 @@ function VersionUpdateTag() {
return (
<span
className={cn(
"ms-2 inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium align-middle",
"ms-2 inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.625rem] font-medium align-middle",
important
? "bg-red-500/15 text-red-700 dark:text-red-300"
: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
@@ -104,7 +104,7 @@ export function AboutFooter() {
/>
<div className="min-w-0">
<p className="text-xs font-medium text-foreground truncate">{branded ? appName : t('about.title')}</p>
<p className="text-[11px] text-muted-foreground truncate">
<p className="text-[0.6875rem] text-muted-foreground truncate">
v{APP_VERSION} <span className="text-muted-foreground">({GIT_COMMIT})</span>
<VersionUpdateTag />
</p>
@@ -114,7 +114,7 @@ export function AboutFooter() {
href={sourceCodeUrl || UPSTREAM_SOURCE_URL}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
className="shrink-0 inline-flex items-center gap-1 text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
>
{sourceCodeUrl ? t('about.source_code') : 'GitHub'} <ExternalLink className="w-3 h-3" />
</a>
@@ -435,7 +435,7 @@ function CredentialRow({ entry, onRemove, isSaving, deleteLabel }: { entry: AppP
{entry.allowedIps.map((ip) => (
<span
key={ip}
className="text-[10px] font-mono bg-background border border-border rounded px-1.5 py-0.5 text-muted-foreground"
className="text-[0.625rem] font-mono bg-background border border-border rounded px-1.5 py-0.5 text-muted-foreground"
>
{ip}
</span>
@@ -595,7 +595,7 @@ function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRem
rows={2}
className="w-full text-xs font-mono px-3 py-2 rounded-md border border-border bg-background focus:outline-hidden"
/>
<p className="text-[10px] text-muted-foreground mt-1">{t('app_passwords.allowed_ips_hint')}</p>
<p className="text-[0.625rem] text-muted-foreground mt-1">{t('app_passwords.allowed_ips_hint')}</p>
</div>
<div className="flex gap-2">
<Button type="submit" size="sm" disabled={isSaving || !newDescription.trim()}>
@@ -891,7 +891,7 @@ function PublicKeysSection() {
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground truncate">{key.description || key.id}</span>
{isEncryptedWithThisKey && (
<span className="text-[10px] bg-green-500/10 text-green-600 dark:text-green-400 font-medium px-1.5 py-0.5 rounded border border-green-500/20">
<span className="text-[0.625rem] bg-green-500/10 text-green-600 dark:text-green-400 font-medium px-1.5 py-0.5 rounded border border-green-500/20">
{encryptionConfig.type}
</span>
)}
@@ -901,7 +901,7 @@ function PublicKeysSection() {
{new Date(key.createdAt).toLocaleDateString()}
</span>
)}
<code className="text-[10px] font-mono bg-background border border-border rounded px-1.5 py-0.5 text-muted-foreground truncate mt-1">
<code className="text-[0.625rem] font-mono bg-background border border-border rounded px-1.5 py-0.5 text-muted-foreground truncate mt-1">
{key.key}
</code>
</div>
@@ -1462,9 +1462,9 @@ export function LinkDeviceSection() {
{copied ? <Check className="w-3 h-3 me-1" /> : <Copy className="w-3 h-3 me-1" />}
<span aria-live="polite">{copied ? t('link_device.copied') : t('link_device.copy_link')}</span>
</Button>
<p className="text-[11px] text-muted-foreground">{t('link_device.copy_hint')}</p>
<p className="text-[0.6875rem] text-muted-foreground">{t('link_device.copy_hint')}</p>
</div>
<p className="text-[11px] text-muted-foreground text-center">
<p className="text-[0.6875rem] text-muted-foreground text-center">
{t('link_device.expires_in', { seconds: remaining })}
</p>
</div>
+1 -1
View File
@@ -425,7 +425,7 @@ function AccountRow({
account.isConnected ? 'bg-green-500' : 'bg-muted-foreground/40'
)} />
)}
<span className="text-[10px] text-muted-foreground truncate">
<span className="text-[0.625rem] text-muted-foreground truncate">
{hostnameOf(account.serverUrl)}
</span>
</div>
+1 -1
View File
@@ -48,7 +48,7 @@ function DensityPreview({ density }: { density: Density }) {
<span className={cn("truncate", row.unread ? "font-semibold text-foreground" : "text-muted-foreground")}>
{row.sender}
</span>
<span className="text-[10px] text-muted-foreground flex-shrink-0 tabular-nums">12:00</span>
<span className="text-[0.625rem] text-muted-foreground flex-shrink-0 tabular-nums">12:00</span>
</div>
<div className={cn("truncate", row.unread ? "font-medium text-foreground" : "text-foreground")}>
{row.subject}
@@ -253,7 +253,7 @@ export function DelegateDialog({
>
<Avatar name={p.name} email={p.email ?? undefined} size="sm" className="shrink-0" />
<PrincipalText principal={p} />
{existingIds.has(p.id) && <span className="shrink-0 text-[10px] text-muted-foreground">{t("has_access")}</span>}
{existingIds.has(p.id) && <span className="shrink-0 text-[0.625rem] text-muted-foreground">{t("has_access")}</span>}
</button>
</li>
))}
@@ -389,7 +389,7 @@ function PrincipalText({ principal }: { principal: Principal }) {
<div className="flex items-center gap-2 truncate text-sm font-medium">
<span className="truncate">{name}</span>
{principal.type === "group" && (
<span className="rounded bg-muted px-1 py-0.5 text-[11px] font-normal text-muted-foreground">{t("group")}</span>
<span className="rounded bg-muted px-1 py-0.5 text-[0.6875rem] font-normal text-muted-foreground">{t("group")}</span>
)}
</div>
{detail && <div className="truncate text-xs text-muted-foreground">{detail}</div>}
@@ -189,7 +189,7 @@ export function DelegatesSettings() {
<div className="flex items-center gap-2 truncate text-sm font-medium">
<span className="truncate">{display.name}</span>
{principal?.type === "group" && (
<span className="rounded bg-muted px-1 py-0.5 text-[11px] font-normal text-muted-foreground">{t("group")}</span>
<span className="rounded bg-muted px-1 py-0.5 text-[0.6875rem] font-normal text-muted-foreground">{t("group")}</span>
)}
</div>
{display.detail && <div className="truncate text-xs text-muted-foreground">{display.detail}</div>}
+5 -5
View File
@@ -86,7 +86,7 @@ function FilesSettingsPreview({ settings }: { settings: FilesSettings }) {
const listView = (
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-3 px-2 py-1 text-[10px] font-medium text-muted-foreground border-b border-border bg-muted/50">
<div className="flex items-center gap-3 px-2 py-1 text-[0.625rem] font-medium text-muted-foreground border-b border-border bg-muted/50">
<span className="flex-1 min-w-0">Name</span>
<span className="w-14 text-end">Size</span>
<span className="w-16 text-end">Modified</span>
@@ -104,13 +104,13 @@ function FilesSettingsPreview({ settings }: { settings: FilesSettings }) {
) : settings.showIcons ? (
getPreviewIcon(file, settings.coloredIcons, "sm")
) : null}
<span className={cn("flex-1 min-w-0 truncate text-[11px]", file.isFolder && "font-medium")}>
<span className={cn("flex-1 min-w-0 truncate text-[0.6875rem]", file.isFolder && "font-medium")}>
{file.name}
</span>
<span className="w-14 text-end text-[10px] text-muted-foreground tabular-nums">
<span className="w-14 text-end text-[0.625rem] text-muted-foreground tabular-nums">
{formatSize(file.size)}
</span>
<span className="w-16 text-end text-[10px] text-muted-foreground tabular-nums">
<span className="w-16 text-end text-[0.625rem] text-muted-foreground tabular-nums">
{file.modified.slice(5)}
</span>
</div>
@@ -136,7 +136,7 @@ function FilesSettingsPreview({ settings }: { settings: FilesSettings }) {
) : (
<div className="w-8 h-8" />
)}
<span className={cn("text-[9px] truncate w-full text-center", file.isFolder && "font-medium")}>
<span className={cn("text-[0.5625rem] truncate w-full text-center", file.isFolder && "font-medium")}>
{file.name}
</span>
</div>
+7 -7
View File
@@ -128,7 +128,7 @@ export function VisualRuleSummary({ rule }: { rule: FilterRule }) {
return (
<div className="mt-1.5 space-y-1 text-xs">
<div className="flex items-baseline gap-1.5 flex-wrap">
<span className="text-[11px] font-semibold text-blue-500 dark:text-blue-400">
<span className="text-[0.6875rem] font-semibold text-blue-500 dark:text-blue-400">
{t("if")}
</span>
{rule.conditions.map((c, i) => {
@@ -139,7 +139,7 @@ export function VisualRuleSummary({ rule }: { rule: FilterRule }) {
return (
<span key={i} className="contents">
{i > 0 && (
<span className="text-[10px] text-muted-foreground italic">{joiner}</span>
<span className="text-[0.625rem] text-muted-foreground italic">{joiner}</span>
)}
<span className="inline-flex min-w-0 max-w-full items-baseline gap-1 px-1.5 py-px rounded-sm bg-muted/60 text-foreground">
<span className="shrink-0 font-medium text-blue-600 dark:text-blue-400">{field}</span>
@@ -165,11 +165,11 @@ export function VisualRuleSummary({ rule }: { rule: FilterRule }) {
</span>
);
})}
<span className="text-[10px] text-muted-foreground italic">({matchLabel})</span>
<span className="text-[0.625rem] text-muted-foreground italic">({matchLabel})</span>
</div>
<div className="flex items-baseline gap-1.5 flex-wrap">
<span className="text-[11px] font-semibold text-emerald-500 dark:text-emerald-400">
<span className="text-[0.6875rem] font-semibold text-emerald-500 dark:text-emerald-400">
{t("then")}
</span>
{rule.actions.map((a, i) => {
@@ -190,14 +190,14 @@ export function VisualRuleSummary({ rule }: { rule: FilterRule }) {
{period && (
<div className="flex items-baseline gap-1.5 flex-wrap">
<span className="text-[11px] font-semibold text-violet-500 dark:text-violet-400">
<span className="text-[0.6875rem] font-semibold text-violet-500 dark:text-violet-400">
{t("period_title")}
</span>
<span className="min-w-0 max-w-full px-1.5 py-px rounded-sm bg-muted/60 text-foreground wrap-anywhere">
{period.range}
</span>
{period.status && (
<span className="text-[10px] text-muted-foreground italic">({period.status})</span>
<span className="text-[0.625rem] text-muted-foreground italic">({period.status})</span>
)}
</div>
)}
@@ -636,7 +636,7 @@ export function FilterSettings() {
<p className="text-sm font-medium text-foreground truncate">
{rule.name}
</p>
<span className="inline-flex items-baseline px-1.5 py-px rounded-sm bg-muted/60 text-muted-foreground text-[10px]">
<span className="inline-flex items-baseline px-1.5 py-px rounded-sm bg-muted/60 text-muted-foreground text-[0.625rem]">
{label}
</span>
</div>
+3 -3
View File
@@ -48,7 +48,7 @@ function MailLayoutPreview({
<div
key={row.subject}
className={cn(
'border-b border-border px-2 py-1.5 text-[10px] last:border-b-0',
'border-b border-border px-2 py-1.5 text-[0.625rem] last:border-b-0',
row.selected && 'bg-primary/10'
)}
>
@@ -72,7 +72,7 @@ function MailLayoutPreview({
<div
key={row.subject}
className={cn(
'border-b border-border px-2 py-1 text-[10px] last:border-b-0',
'border-b border-border px-2 py-1 text-[0.625rem] last:border-b-0',
row.selected && 'bg-primary/10'
)}
>
@@ -92,7 +92,7 @@ function MailLayoutPreview({
<div
key={row.subject}
className={cn(
'border-b border-border px-2 py-1 text-[10px] last:border-b-0',
'border-b border-border px-2 py-1 text-[0.625rem] last:border-b-0',
row.selected && 'bg-primary/10'
)}
>
+8 -8
View File
@@ -241,16 +241,16 @@ function PluginCard({ plugin, isExpanded, isForceEnabled, controlsDisabled, onTo
>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="truncate text-sm font-medium text-foreground">{plugin.name}</span>
<span className={cn('rounded-full px-1.5 py-0.5 text-[10px] font-medium', STATUS_COLORS[plugin.status])}>
<span className={cn('rounded-full px-1.5 py-0.5 text-[0.625rem] font-medium', STATUS_COLORS[plugin.status])}>
{t(`status_${plugin.status}`)}
</span>
{isForceEnabled && (
<span className="flex items-center gap-0.5 rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
<span className="flex items-center gap-0.5 rounded-full bg-amber-100 px-1.5 py-0.5 text-[0.625rem] font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
<Lock className="h-2.5 w-2.5" /> {t('badge_forced')}
</span>
)}
<span className={cn(
'flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium',
'flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[0.625rem] font-medium',
isManaged
? 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400'
: 'bg-muted text-muted-foreground',
@@ -258,7 +258,7 @@ function PluginCard({ plugin, isExpanded, isForceEnabled, controlsDisabled, onTo
{isManaged && <Server className="h-2.5 w-2.5" />} {isManaged ? t('badge_managed') : t('badge_yours')}
</span>
{awaiting && (
<span className="rounded-full bg-orange-100 px-1.5 py-0.5 text-[10px] font-medium text-orange-700 dark:bg-orange-900/30 dark:text-orange-400">
<span className="rounded-full bg-orange-100 px-1.5 py-0.5 text-[0.625rem] font-medium text-orange-700 dark:bg-orange-900/30 dark:text-orange-400">
{t('badge_awaiting')}
</span>
)}
@@ -406,7 +406,7 @@ function PluginSettingField({ fieldKey, schema: raw, value, onChange, translate
<div data-search-label={schema.label} className="flex items-center justify-between gap-3">
<div>
<span className="text-xs text-foreground">{schema.label}</span>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}
{schema.description && <p className="text-[0.625rem] text-muted-foreground">{schema.description}</p>}
</div>
<ToggleSwitch checked={value as boolean} onChange={(v) => onChange(v)} ariaLabel={schema.label} />
</div>
@@ -417,7 +417,7 @@ function PluginSettingField({ fieldKey, schema: raw, value, onChange, translate
<div data-search-label={schema.label} className="flex items-center justify-between gap-3">
<div>
<label htmlFor={inputId} className="text-xs text-foreground">{schema.label}</label>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}
{schema.description && <p className="text-[0.625rem] text-muted-foreground">{schema.description}</p>}
</div>
<select
id={inputId}
@@ -436,7 +436,7 @@ function PluginSettingField({ fieldKey, schema: raw, value, onChange, translate
return (
<div data-search-label={schema.label}>
<label htmlFor={inputId} className="text-xs text-foreground">{schema.label}</label>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}
{schema.description && <p className="text-[0.625rem] text-muted-foreground">{schema.description}</p>}
<input
id={inputId}
type="text"
@@ -452,7 +452,7 @@ function PluginSettingField({ fieldKey, schema: raw, value, onChange, translate
<div data-search-label={schema.label} className="flex items-center justify-between gap-3">
<div>
<label htmlFor={inputId} className="text-xs text-foreground">{schema.label}</label>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}
{schema.description && <p className="text-[0.625rem] text-muted-foreground">{schema.description}</p>}
</div>
<input
id={inputId}
+1 -1
View File
@@ -1146,7 +1146,7 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
{groupIndex > 0 && <div className="mx-1 my-2 border-t border-border" />}
{group.label && (
<div className="px-3 pt-2.5 pb-1">
<span className="text-[11px] font-semibold text-muted-foreground">
<span className="text-[0.6875rem] font-semibold text-muted-foreground">
{group.label}
</span>
</div>
+1 -1
View File
@@ -170,7 +170,7 @@ export function SectionNav() {
function AdvancedBadge() {
const t = useTranslations('settings.advanced_switch');
return (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-accent text-accent-foreground font-medium">
<span className="text-[0.625rem] px-1.5 py-0.5 rounded bg-accent text-accent-foreground font-medium">
{t('badge')}
</span>
);
@@ -291,7 +291,7 @@ export function ShareCollectionDialog({
<div className="text-sm font-medium truncate flex items-center gap-2">
{p.name}
{p.type === "group" && (
<span className="text-[11px] font-normal text-muted-foreground bg-muted rounded px-1 py-0.5">
<span className="text-[0.6875rem] font-normal text-muted-foreground bg-muted rounded px-1 py-0.5">
{t("group")}
</span>
)}
@@ -211,12 +211,12 @@ function ManagedAppsSection() {
<div className="text-sm font-medium truncate">{app.name}</div>
<div dir="ltr" className="text-xs text-muted-foreground truncate rtl:text-right">{app.url}</div>
</div>
<span className="text-[10px] px-1.5 py-0.5 rounded-full flex-shrink-0 bg-muted text-muted-foreground inline-flex items-center gap-1">
<span className="text-[0.625rem] px-1.5 py-0.5 rounded-full flex-shrink-0 bg-muted text-muted-foreground inline-flex items-center gap-1">
<Lock className="w-2.5 h-2.5" />
{tApps("managed_badge")}
</span>
<span className={cn(
"text-[10px] px-1.5 py-0.5 rounded-full flex-shrink-0",
"text-[0.625rem] px-1.5 py-0.5 rounded-full flex-shrink-0",
app.openMode === "inline"
? "bg-blue-500/10 text-blue-600 dark:text-blue-400"
: "bg-muted text-muted-foreground"
@@ -344,7 +344,7 @@ export function SidebarAppsSettings() {
<div dir="ltr" className="text-xs text-muted-foreground truncate rtl:text-right">{app.url}</div>
</div>
<span className={cn(
"text-[10px] px-1.5 py-0.5 rounded-full flex-shrink-0",
"text-[0.625rem] px-1.5 py-0.5 rounded-full flex-shrink-0",
app.openMode === "inline"
? "bg-blue-500/10 text-blue-600 dark:text-blue-400"
: "bg-muted text-muted-foreground"
+2 -2
View File
@@ -720,7 +720,7 @@ function MailCard({
>
{kind === "legit" ? (
<span
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[0.625rem] font-semibold text-white"
style={{ backgroundColor: tone }}
>
{initials(sender.name)}
@@ -737,7 +737,7 @@ function MailCard({
)}
<div className="min-w-0 flex-1 leading-tight">
<p className="truncate text-xs font-semibold text-foreground">{sender.name}</p>
<p className="truncate text-[11px] text-muted-foreground">{sender.address}</p>
<p className="truncate text-[0.6875rem] text-muted-foreground">{sender.address}</p>
</div>
</div>
);
+3 -3
View File
@@ -233,12 +233,12 @@ function ThemeCard({ name, author, preview, css, isDark, isDefaultTheme, isActiv
<span className="text-sm font-medium text-foreground truncate">{name}</span>
<div className="flex items-center gap-1 flex-shrink-0">
{isForceEnabled && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 font-medium flex items-center gap-0.5" title={t('admin_enforced')}>
<span className="text-[0.625rem] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 font-medium flex items-center gap-0.5" title={t('admin_enforced')}>
<Lock className="w-2.5 h-2.5" />
</span>
)}
{isDefault && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-primary/10 text-primary font-medium">{t('default_badge')}</span>
<span className="text-[0.625rem] px-1.5 py-0.5 rounded bg-primary/10 text-primary font-medium">{t('default_badge')}</span>
)}
{isActive && <Check className="w-4 h-4 text-primary" />}
</div>
@@ -247,7 +247,7 @@ function ThemeCard({ name, author, preview, css, isDark, isDefaultTheme, isActiv
{variants && (
<div className="flex gap-1 mt-1">
{variants.map(v => (
<span key={v} className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
<span key={v} className="text-[0.625rem] px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
{tVariant(v)}
</span>
))}
+3 -3
View File
@@ -215,7 +215,7 @@ export function ToastItem({ toast, onClose, held = false, onFocusLost }: ToastPr
}
}}
className={cn(
"text-[13px] font-semibold text-foreground px-1.5 py-0.5 rounded-md transition-colors",
"text-[0.8125rem] font-semibold text-foreground px-1.5 py-0.5 rounded-md transition-colors",
TOUCH_HIT_AREA,
"underline underline-offset-[3px] hover:bg-foreground/5"
)}
@@ -282,9 +282,9 @@ export function ToastItem({ toast, onClose, held = false, onFocusLost }: ToastPr
{/* Content */}
<div className="flex-1 min-w-0 pt-px">
<p id={titleId} className="text-[13.5px] font-medium text-foreground leading-snug">{toast.title}</p>
<p id={titleId} className="text-[0.84375rem] font-medium text-foreground leading-snug">{toast.title}</p>
{toast.message && (
<p className="text-[12.5px] mt-0.5 text-muted-foreground leading-snug">{toast.message}</p>
<p className="text-[0.78125rem] mt-0.5 text-muted-foreground leading-snug">{toast.message}</p>
)}
{toast.tertiaryAction && (
<div className="flex flex-wrap items-center gap-1 mt-1.5 -ms-1.5">
+1 -1
View File
@@ -98,7 +98,7 @@ export const ToolbarSearch = React.forwardRef<HTMLInputElement, ToolbarSearchPro
{showKey && (
<kbd
aria-hidden="true"
className="pointer-events-none mx-1 rounded border border-border px-1.5 font-sans text-[11px] leading-4 text-muted-foreground group-focus-within/search:invisible"
className="pointer-events-none mx-1 rounded border border-border px-1.5 font-sans text-[0.6875rem] leading-4 text-muted-foreground group-focus-within/search:invisible"
>
{shortcut}
</kbd>
+35
View File
@@ -0,0 +1,35 @@
/**
* Text sizes in rem follow both the browser's text size and Settings >
* Accessibility > Text size (WCAG 1.4.4); a `text-[10px]` follows neither.
* The message list rows are left out: they are being rewritten separately.
*/
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'fs';
import path from 'path';
const ROOT = process.cwd();
const SKIP = new Set([
path.join('components', 'email', 'thread-list-item.tsx'),
path.join('components', 'email', 'email-list.tsx'),
]);
function sources(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(path.join(ROOT, dir))) {
const rel = path.join(dir, name);
if (name === '__tests__' || name === 'node_modules' || rel === path.join('app', 'api')) continue;
if (statSync(path.join(ROOT, rel)).isDirectory()) sources(rel, out);
else if (/\.(tsx|ts)$/.test(name) && !SKIP.has(rel)) out.push(rel);
}
return out;
}
describe('text sizes', () => {
it('uses no pixel text sizes or line heights in the UI', () => {
const offenders: string[] = [];
for (const file of [...sources('components'), ...sources('app'), ...sources('lib'), ...sources('hooks')]) {
const text = readFileSync(path.join(ROOT, file), 'utf8');
for (const m of text.matchAll(/(?<![\w-])(?:text|leading)-\[\d+(?:\.\d+)?px\]/g)) offenders.push(`${file}: ${m[0]}`);
}
expect(offenders).toEqual([]);
});
});
+1 -1
View File
@@ -61,7 +61,7 @@ describe('plugin UI kit components', () => {
['components/files/file-views.tsx', 'border-b border-border/60'],
['components/files/files-header.tsx', 'px-1.5 py-1 rounded-md transition-colors truncate max-w-[16rem]'],
['components/email/email-viewer.tsx', 'w-10 h-10 rounded-full'],
['components/email/email-viewer.tsx', 'text-[11px] font-semibold text-muted-foreground'],
['components/email/email-viewer.tsx', 'text-[0.6875rem] font-semibold text-muted-foreground'],
['components/email/email-viewer.tsx', 'inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors min-h-[36px]'],
['components/identity/identity-form.tsx', 'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-all duration-200'],
];
+4 -4
View File
@@ -1136,7 +1136,7 @@ const BADGE_TONE: Record<Tone, string> = {
/** The status pills (calendar invitation, identity "Default"). */
export function Badge({ tone = 'neutral', children }: { tone?: Tone } & Children) {
return <span className={cn('bk inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium whitespace-nowrap', BADGE_TONE[tone])}>{children}</span>;
return <span className={cn('bk inline-flex items-center rounded-full px-2 py-0.5 text-[0.6875rem] font-medium whitespace-nowrap', BADGE_TONE[tone])}>{children}</span>;
}
/** The reader's attachment chip: icon, label, muted detail. Clickable when onClick is set. */
@@ -1187,7 +1187,7 @@ export function InfoBar({ tone = 'neutral', icon, eyebrow, title, children, comp
return (
<div className="bk flex flex-wrap items-center gap-x-3 gap-y-1.5 px-6 py-1.5 border-b border-border bg-muted/30 text-sm">
{icon && <span className={cn('w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0', INFO_TONE[tone])}><Icon name={icon} size="sm" /></span>}
{eyebrow && <span className="text-[11px] font-semibold text-muted-foreground">{eyebrow}</span>}
{eyebrow && <span className="text-[0.6875rem] font-semibold text-muted-foreground">{eyebrow}</span>}
{title && <span className="min-w-0 flex-1 text-foreground break-words">{title}</span>}
{children}
</div>
@@ -1205,7 +1205,7 @@ export function InfoBar({ tone = 'neutral', icon, eyebrow, title, children, comp
<div className="flex-1 min-w-0 space-y-2">
{(eyebrow || title) && (
<div>
{eyebrow && <div className="text-[11px] font-semibold text-muted-foreground">{eyebrow}</div>}
{eyebrow && <div className="text-[0.6875rem] font-semibold text-muted-foreground">{eyebrow}</div>}
{title && <div className="text-sm font-medium text-foreground break-words">{title}</div>}
</div>
)}
@@ -1346,7 +1346,7 @@ export function Panel({ children }: Children) {
/** The small caps heading of the message details' sections. */
export function DetailHeading({ children }: Children) {
return <div className="bk text-[11px] font-semibold text-muted-foreground mb-1.5">{children}</div>;
return <div className="bk text-[0.6875rem] font-semibold text-muted-foreground mb-1.5">{children}</div>;
}
/** Label and value rows like the message details (From, Date, Size...). Children are DetailRows. */