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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { startNotifications } from "./lib/notify";
import { startRinger } from "./lib/ringer";
import { startUpdateChecks } from "./lib/update";
import { resetPresence } from "./lib/presence";
import { applyAccent, load as loadSettings } from "./lib/settings";
import { applyAccent, applyTypography, load as loadSettings } from "./lib/settings";
import type { RoomSummary } from "./lib/types";
import { useEdgeSwipeBack, useIsMobile, useKeyboardSafeArea } from "./lib/viewport";
import { selectActiveRoom, useStore } from "./store";
Expand Down Expand Up @@ -37,10 +37,12 @@ export default function App() {
})),
);

// Paint the saved accent before anything renders, so the app never flashes
// the default colour on the way to the chosen one.
// Paint the saved accent and fonts before anything renders, so the app never
// flashes the defaults on the way to the chosen ones.
useEffect(() => {
applyAccent(loadSettings().accent);
const saved = loadSettings();
applyAccent(saved.accent);
applyTypography(saved);
}, []);

// Try to pick up where we left off before showing anything, so a returning
Expand Down
12 changes: 4 additions & 8 deletions src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,12 @@ export function Composer({
const cameraInput = useRef<HTMLInputElement>(null);
const [attachOpen, setAttachOpen] = useState(false);

const { draft, setDraft, clearDraft, showBanner, settings, packs } = useStore(
const { draft, setDraft, clearDraft, showBanner, packs } = useStore(
useShallow((s) => ({
draft: selectDraft(s, key),
setDraft: s.setDraft,
clearDraft: s.clearDraft,
showBanner: s.showBanner,
settings: s.settings,
packs: s.packs,
})),
);
Expand Down Expand Up @@ -499,11 +498,8 @@ export function Composer({
}
}

// Either Enter sends and Shift+Enter breaks the line, or the other
// way round — whichever the user picked in settings.
const sends = settings.sendOnEnter
? e.key === "Enter" && !e.shiftKey && !e.metaKey
: e.key === "Enter" && (e.metaKey || e.ctrlKey);
// Enter sends; Shift+Enter breaks the line.
const sends = e.key === "Enter" && !e.shiftKey && !e.metaKey;

if (sends) {
e.preventDefault();
Expand All @@ -524,7 +520,7 @@ export function Composer({
outline: "none",
resize: "none",
color: "var(--text-primary)",
fontSize: 14.5,
fontSize: "var(--text-message)",
lineHeight: 1.5,
padding: "9px 6px",
maxHeight: 160,
Expand Down
3 changes: 2 additions & 1 deletion src/components/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,8 @@ function ActionButton({

function ContentBody({ content, emojiOnly }: { content: Content; emojiOnly: boolean }) {
const textStyle = {
fontSize: emojiOnly ? 34 : 14.5,
// Emoji-only messages keep their ratio to the text around them.
fontSize: emojiOnly ? "calc(var(--text-message) * 34 / 14.5)" : "var(--text-message)",
lineHeight: emojiOnly ? 1.2 : 1.55,
color: "var(--text-primary)",
maxWidth: "min(640px, 100%)",
Expand Down
123 changes: 113 additions & 10 deletions src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { mediaUrl } from "../lib/ipc";
import { invalidateProfile } from "../lib/profiles";
import {
ACCENT_SWATCHES,
DEFAULTS,
FONT_PRESETS,
listMediaDevices,
MESSAGE_FONT_SIZE_RANGE,
type Accent,
type AudioDevice,
type MediaDevices,
Expand Down Expand Up @@ -1137,23 +1140,123 @@ function AppearanceSection() {
</Field>
</Card>

<Heading>composer</Heading>
<Card>
<Row
icon="keyboard"
title="send with enter"
subtitle="off: enter makes a new line, cmd+enter sends"
<FontPicker
value={settings.fontFamily}
onChange={(fontFamily) => updateSettings({ fontFamily })}
/>

<Field
label={`message text — ${settings.messageFontSize}px`}
hint="the size of messages and the composer. the rest of the app stays put."
>
<Toggle
on={settings.sendOnEnter}
onToggle={(next) => updateSettings({ sendOnEnter: next })}
/>
</Row>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<input
type="range"
min={MESSAGE_FONT_SIZE_RANGE.min}
max={MESSAGE_FONT_SIZE_RANGE.max}
step={MESSAGE_FONT_SIZE_RANGE.step}
value={settings.messageFontSize}
onChange={(e) => updateSettings({ messageFontSize: Number(e.target.value) })}
style={{ flex: 1, accentColor: "var(--accent-primary)" }}
/>
<Button
variant="ghost"
size="sm"
disabled={settings.messageFontSize === DEFAULTS.messageFontSize}
onClick={() => updateSettings({ messageFontSize: DEFAULTS.messageFontSize })}
>
reset
</Button>
</div>
</Field>

<div
style={{
background: "var(--surface-inset)",
border: "1px solid var(--border-subtle)",
borderRadius: 14,
padding: "12px 14px",
fontSize: "var(--text-message)",
lineHeight: 1.55,
}}
>
the quick brown fox jumps over the lazy dog~ ✨
</div>
</Card>
</>
);
}

/**
* The body font: a row of presets, and a box for the name of anything else
* installed. The box keeps its own draft so typing doesn't repaint the whole
* app with every half-written font name.
*/
function FontPicker({
value,
onChange,
}: {
value: string;
onChange: (family: string) => void;
}) {
const preset = FONT_PRESETS.find((p) => p.family === value);
const [draft, setDraft] = useState(preset ? "" : value);

// Picking a preset clears the box; a value set elsewhere shows up in it.
useEffect(() => {
setDraft(FONT_PRESETS.some((p) => p.family === value) ? "" : value);
}, [value]);

const commit = () => {
const next = draft.trim();
if (next !== value) onChange(next);
};

return (
<Field
label="font"
hint="type the name of any font installed on this machine. one that isn't falls back to nunito."
>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 10 }}>
{FONT_PRESETS.map((p) => {
const active = p.family === value;
return (
<button
key={p.label}
onClick={() => onChange(p.family)}
style={{
padding: "7px 14px",
borderRadius: 999,
cursor: "pointer",
fontFamily: p.family || "'Nunito Variable'",
fontSize: 13.5,
fontWeight: 700,
color: active ? "var(--text-on-accent)" : "var(--text-primary)",
background: active ? "var(--accent-primary)" : "var(--surface-inset)",
border: `1px solid ${active ? "var(--accent-primary)" : "var(--border-subtle)"}`,
}}
>
{p.label}
</button>
);
})}
</div>
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onBlur={commit}
onKeyDown={(e) => {
if (e.key === "Enter") commit();
}}
placeholder="custom font, e.g. Comic Sans MS"
spellCheck={false}
style={{ ...inputStyle, fontFamily: draft.trim() || undefined }}
/>
</Field>
);
}

// ---------------------------------------------------------------------------
// about
// ---------------------------------------------------------------------------
Expand Down
45 changes: 42 additions & 3 deletions src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export type Accent = "acid" | "pink" | "violet" | "cyan";

export interface Settings {
accent: Accent;
/**
* The CSS font family for body text, or "" for the design's own. Anything
* installed on the machine works; a name that isn't falls back to the default.
*/
fontFamily: string;
/** Size of message text, in px. The rest of the UI keeps its own sizes. */
messageFontSize: number;
/** `deviceId` of the microphone, or "" for the system default. */
audioInput: string;
/** `deviceId` of the speaker, or "" for the system default. */
Expand All @@ -20,8 +27,6 @@ export interface Settings {
* quieter than this. At `GATE_OFF_DB` or below, nothing is ever cut.
*/
inputSensitivity: number;
/** Send on Enter (Discord-style) vs. Cmd+Enter. */
sendOnEnter: boolean;
/** Show the room info panel beside the timeline. */
showInfoPanel: boolean;
/** Which skin tone the emoji picker offers. 0 is the default yellow. */
Expand Down Expand Up @@ -64,13 +69,14 @@ export const MAX_RECENT_REACTIONS = 6;

export const DEFAULTS: Settings = {
accent: "acid",
fontFamily: "",
messageFontSize: 14.5,
audioInput: "",
audioOutput: "",
videoInput: "",
// Wide open until someone drags it up: a threshold set wrong cuts people off
// mid-word, which is worse than the background noise it was meant to hide.
inputSensitivity: GATE_OFF_DB,
sendOnEnter: true,
showInfoPanel: true,
skinTone: 0,
notifyMessages: true,
Expand All @@ -84,6 +90,17 @@ export const DEFAULTS: Settings = {
recentReactions: ["💜", "😹", "🥺", "✨", "👀", "🔥"],
};

/** The range the message text size slider covers, in px. */
export const MESSAGE_FONT_SIZE_RANGE = { min: 12, max: 22, step: 0.5 } as const;

/** One-click choices for the font picker; anything else is typed in by name. */
export const FONT_PRESETS: { label: string; family: string }[] = [
{ label: "nunito", family: "" },
{ label: "system", family: "system-ui" },
{ label: "baloo", family: "'Baloo 2'" },
{ label: "mono", family: "'JetBrains Mono Variable'" },
];

const STORAGE_KEY = "uwum:settings";

/**
Expand Down Expand Up @@ -138,6 +155,28 @@ export function applyAccent(accent: Accent): void {
root.style.setProperty("--accent-primary-press", `var(${press})`);
}

/** The font stack `tokens/typography.css` gives body text. */
const DEFAULT_BODY_FONT = "'Nunito Variable', 'Nunito', ui-sans-serif, system-ui, sans-serif";

/**
* Paint the chosen font and message size onto the document.
*
* A custom family is put in front of the design's stack rather than replacing
* it, so a misspelt or uninstalled font lands on Nunito instead of Times.
*/
export function applyTypography({
fontFamily,
messageFontSize,
}: Pick<Settings, "fontFamily" | "messageFontSize">): void {
const root = document.documentElement;
const family = fontFamily.trim();
root.style.setProperty(
"--font-body",
family ? `${family}, ${DEFAULT_BODY_FONT}` : DEFAULT_BODY_FONT,
);
root.style.setProperty("--text-message", `${messageFontSize}px`);
}

export interface AudioDevice {
deviceId: string;
label: string;
Expand Down
3 changes: 3 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ export const useStore = create<State & Actions>((set, get) => ({
const settings = { ...s.settings, ...patch };
prefs.save(settings);
if (patch.accent) prefs.applyAccent(patch.accent);
if (patch.fontFamily !== undefined || patch.messageFontSize !== undefined) {
prefs.applyTypography(settings);
}
return { settings };
}),

Expand Down
5 changes: 3 additions & 2 deletions src/styles/tokens/typography.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
:root{
--font-display:'Baloo 2',ui-rounded,'Segoe UI',sans-serif;
--font-rave:'Unbounded',ui-sans-serif,sans-serif;
--font-body:'Nunito',ui-sans-serif,system-ui,sans-serif;
--font-mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',monospace;
--font-body:'Nunito Variable','Nunito',ui-sans-serif,system-ui,sans-serif;
--font-mono:'JetBrains Mono Variable','JetBrains Mono',ui-monospace,'SFMono-Regular',monospace;
--text-display-xl:clamp(40px,6vw,72px);
--text-display-lg:40px;--text-display-md:32px;--text-display-sm:26px;
--text-body-lg:18px;--text-body-md:15px;--text-body-sm:13px;--text-caption:11.5px;
--text-message:14.5px;
--lh-tight:1.08;/* @kind other */--lh-snug:1.25;/* @kind other */--lh-normal:1.5;/* @kind other */--lh-relaxed:1.65;/* @kind other */
--ls-tight:-0.01em;/* @kind other */--ls-wide:0.06em;/* @kind other */--ls-widest:0.14em;/* @kind other */
--fw-regular:400;/* @kind other */--fw-medium:500;/* @kind other */--fw-semibold:600;/* @kind other */--fw-bold:700;/* @kind other */--fw-black:800;/* @kind other */
Expand Down
Loading