diff --git a/src/css/documentload.scss b/src/css/documentload.scss index 7b66ac28..6ef051a3 100644 --- a/src/css/documentload.scss +++ b/src/css/documentload.scss @@ -37,8 +37,9 @@ @layer base, override; @layer override { - * { - font-family: Rubik, sans-serif !important; + .legacy-root, + .legacy-root * { + font-family: var(--betterseqta-font-family, Rubik), sans-serif !important; } .iconFamily, diff --git a/src/css/injected.scss b/src/css/injected.scss index 978cd32b..f0a21ae9 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -119,7 +119,8 @@ select option { #container { background: var(--auto-background) !important; } -:root * { +.legacy-root, +.legacy-root * { font-family: Rubik, sans-serif !important; --theme-fg-parts: white; } diff --git a/src/interface/components/FontPickerModal.svelte b/src/interface/components/FontPickerModal.svelte new file mode 100644 index 00000000..45486725 --- /dev/null +++ b/src/interface/components/FontPickerModal.svelte @@ -0,0 +1,141 @@ + + + +
{ + if (event.key === "Enter" || event.key === " ") handleBackdropClick(event as unknown as MouseEvent); + }} + role="presentation" + transition:fade={{ duration: 200 }} +> +
event.stopPropagation()} + onkeydown={(event) => event.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="font-picker-title" + > +
+
+ + +
+
+

+ Choose a font +

+

+ Choose a typeface for SEQTA Learn. +

+
+
+ +
+ {#each FONT_PRESETS as preset (preset.id)} + + {/each} +
+
+
diff --git a/src/interface/components/fontPickerModal.css b/src/interface/components/fontPickerModal.css new file mode 100644 index 00000000..3647ab81 --- /dev/null +++ b/src/interface/components/fontPickerModal.css @@ -0,0 +1,311 @@ +/* Font picker — analytics design tokens & components */ + +.bsplus-font-picker-overlay { + position: fixed; + inset: 0; + z-index: 50000; + display: flex; + align-items: center; + justify-content: center; + padding: 1.25rem; + cursor: pointer; + background: color-mix(in srgb, #000 52%, transparent); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} + +.bsplus-font-picker-root { + --bsplus-analytics-radius: 16px; + --bsplus-analytics-radius-sm: 12px; + --bsplus-analytics-ease: cubic-bezier(0.4, 0, 0.2, 1); + --bsplus-analytics-surface: var(--background-primary, #ffffff); + --bsplus-analytics-surface-2: var(--background-secondary, #f8fafc); + --bsplus-analytics-text: var(--text-primary, #1a1a1a); + --bsplus-analytics-muted: color-mix( + in srgb, + var(--bsplus-analytics-text) 55%, + transparent + ); + --bsplus-analytics-border: color-mix( + in srgb, + var(--theme-offset-bg, var(--background-secondary, #e2e8f0)) 78%, + transparent + ); + --bsplus-analytics-shadow: 0 5px 16px 6px rgba(0, 0, 0, 0.12); + --bsplus-analytics-shadow-hover: 0 8px 24px 8px rgba(0, 0, 0, 0.16); + --bsplus-analytics-accent: var(--better-main, #007bff); + + font-family: Rubik, system-ui, sans-serif; + font-size: 0.875rem; + color: var(--bsplus-analytics-text); +} + +.bsplus-font-picker-root.dark { + --bsplus-analytics-shadow: 0 5px 20px 6px rgba(0, 0, 0, 0.45); + --bsplus-analytics-shadow-hover: 0 10px 28px 10px rgba(0, 0, 0, 0.55); +} + +@keyframes bsplus-font-picker-in { + from { + opacity: 0; + transform: translateY(18px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.bsplus-font-picker-dialog { + width: min(100%, 22rem); + max-height: min(88vh, 820px); + display: flex; + flex-direction: column; + overflow: hidden; + cursor: auto; + border-radius: var(--bsplus-analytics-radius); + background: var(--bsplus-analytics-surface); + border: 1px solid var(--bsplus-analytics-border); + box-shadow: var(--bsplus-analytics-shadow-hover); + animation: bsplus-font-picker-in 0.45s var(--bsplus-analytics-ease) forwards; +} + +@media (min-width: 640px) { + .bsplus-font-picker-dialog { + width: min(92vw, 22rem); + } +} + +@media (prefers-reduced-motion: reduce) { + .bsplus-font-picker-dialog { + animation: none; + } +} + +.bsplus-font-picker-header { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.85rem; + flex-shrink: 0; + padding: 1.15rem 1.25rem; + border-bottom: 1px solid var(--bsplus-analytics-border); +} + +.bsplus-font-picker-header-actions { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: flex-end; + gap: 0.5rem; + flex-wrap: wrap; +} + +.bsplus-font-picker-header-text { + min-width: 0; +} + +.bsplus-font-picker-title { + margin: 0; + font-size: 1.1rem; + font-weight: 700; + color: var(--bsplus-analytics-text); +} + +.bsplus-font-picker-desc { + margin: 0.35rem 0 0; + font-size: 0.8125rem; + color: var(--bsplus-analytics-muted); + line-height: 1.5; +} + +.bsplus-font-picker-reset { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.65rem 1rem; + border-radius: var(--bsplus-analytics-radius-sm); + border: 2px solid var(--bsplus-analytics-border); + background: transparent; + font-family: inherit; + font-size: 0.8125rem; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + color: var(--bsplus-analytics-text); + transition: + transform 0.2s var(--bsplus-analytics-ease), + background 0.2s var(--bsplus-analytics-ease), + border-color 0.2s var(--bsplus-analytics-ease), + opacity 0.2s ease; +} + +.bsplus-font-picker-reset:hover:not(:disabled) { + transform: scale(1.02); + background: color-mix( + in srgb, + var(--bsplus-analytics-surface-2) 80%, + transparent + ); +} + +.bsplus-font-picker-reset:active:not(:disabled) { + transform: scale(0.98); +} + +.bsplus-font-picker-reset:focus-visible { + outline: none; + box-shadow: 0 0 0 3px + color-mix(in srgb, var(--bsplus-analytics-accent) 35%, transparent); +} + +.bsplus-font-picker-reset:disabled { + opacity: 0.45; + cursor: not-allowed; + transform: none; +} + +.bsplus-font-picker-done { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0.65rem 1.25rem; + border-radius: var(--bsplus-analytics-radius-sm); + border: none; + font-family: inherit; + font-size: 0.875rem; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + background: var(--bsplus-analytics-accent); + color: var(--text-color, #ffffff); + box-shadow: 0 2px 8px + color-mix(in srgb, var(--bsplus-analytics-accent) 40%, transparent); + transition: + transform 0.2s var(--bsplus-analytics-ease), + box-shadow 0.2s var(--bsplus-analytics-ease); +} + +.bsplus-font-picker-done:hover { + transform: scale(1.03); + box-shadow: 0 4px 14px + color-mix(in srgb, var(--bsplus-analytics-accent) 45%, transparent); +} + +.bsplus-font-picker-done:active { + transform: scale(0.97); +} + +.bsplus-font-picker-done:focus-visible { + outline: none; + box-shadow: 0 0 0 3px + color-mix(in srgb, var(--bsplus-analytics-accent) 35%, transparent); +} + +.bsplus-font-picker-list { + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + padding: 1rem 1rem 1.25rem; + display: flex; + flex-direction: column; + gap: 0.65rem; + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--bsplus-analytics-accent) 35%, transparent) + transparent; +} + +.bsplus-font-picker-list::-webkit-scrollbar { + width: 8px; +} + +.bsplus-font-picker-list::-webkit-scrollbar-thumb { + border-radius: 999px; + background: color-mix(in srgb, var(--bsplus-analytics-accent) 35%, transparent); +} + +.bsplus-font-picker-option { + width: 100%; + padding: 0.9rem 1rem; + text-align: left; + border-radius: var(--bsplus-analytics-radius-sm); + border: 1px solid var(--bsplus-analytics-border); + background: var(--bsplus-analytics-surface); + box-shadow: var(--bsplus-analytics-shadow); + cursor: pointer; + font-family: Rubik, system-ui, sans-serif; + flex-shrink: 0; + transition: + transform 0.25s var(--bsplus-analytics-ease), + box-shadow 0.25s var(--bsplus-analytics-ease), + border-color 0.2s ease, + background 0.2s ease; +} + +.bsplus-font-picker-option:hover { + transform: translateY(-2px); + box-shadow: var(--bsplus-analytics-shadow-hover); + background: color-mix( + in srgb, + var(--bsplus-analytics-surface-2) 55%, + var(--bsplus-analytics-surface) + ); +} + +.bsplus-font-picker-option:focus-visible { + outline: none; + box-shadow: + var(--bsplus-analytics-shadow-hover), + 0 0 0 3px color-mix(in srgb, var(--bsplus-analytics-accent) 30%, transparent); +} + +.bsplus-font-picker-option.is-selected { + border-color: color-mix( + in srgb, + var(--bsplus-analytics-accent) 45%, + var(--bsplus-analytics-border) + ); + background: color-mix( + in srgb, + var(--bsplus-analytics-accent) 10%, + var(--bsplus-analytics-surface) + ); + box-shadow: 0 4px 16px + color-mix(in srgb, var(--bsplus-analytics-accent) 22%, transparent); +} + +.bsplus-font-picker-option-head { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0; +} + +.bsplus-font-picker-root .bsplus-font-picker-option-name { + font-size: 1rem; + font-weight: 700; + color: var(--bsplus-analytics-text); + text-align: left; + min-width: 0; +} + +.bsplus-font-picker-badge { + display: inline-flex; + align-items: center; + flex-shrink: 0; + margin-left: auto; + padding: 0.2rem 0.65rem; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + background: color-mix( + in srgb, + var(--bsplus-analytics-accent) 18%, + transparent + ); + color: var(--bsplus-analytics-accent); +} diff --git a/src/interface/pages/settings.svelte b/src/interface/pages/settings.svelte index bcff0e80..db6aed8d 100644 --- a/src/interface/pages/settings.svelte +++ b/src/interface/pages/settings.svelte @@ -15,6 +15,7 @@ //import { OpenMinecraftServerPopup } from "@/seqta/utils/Openers/OpenMinecraftServerPopup"; import ColourPicker from "../components/ColourPicker.svelte"; + import FontPickerModal from "../components/FontPickerModal.svelte"; import CloudPanel from "../components/CloudPanel.svelte"; import DisclaimerModal from "../components/DisclaimerModal.svelte"; import { settingsPopup } from "../hooks/SettingsPopup"; @@ -47,6 +48,10 @@ showColourPicker = true; }; + const openFontPicker = () => { + showFontPicker = true; + }; + const openChangelog = () => { OpenWhatsNewPopup(); closeExtensionPopup(); @@ -69,6 +74,7 @@ let { standalone } = $props<{ standalone?: boolean }>(); let showColourPicker = $state(false); + let showFontPicker = $state(false); let showCloudPanel = $state(false); const openCloudPanel = () => { @@ -85,6 +91,7 @@ onMount(() => { settingsPopup.addListener(() => { showColourPicker = false; + showFontPicker = false; showCloudPanel = false; }); @@ -95,7 +102,7 @@
@@ -293,7 +300,7 @@ { title: "Settings", Content: Settings, - props: { showColourPicker: openColourPicker, showDisclaimer, showCloudPanel: openCloudPanel }, + props: { showColourPicker: openColourPicker, showFontPicker: openFontPicker, showDisclaimer, showCloudPanel: openCloudPanel }, }, { title: "Shortcuts", Content: Shortcuts }, { title: "Themes", Content: Theme }, @@ -318,6 +325,14 @@ {/if}
+{#if showFontPicker} + { + showFontPicker = false; + }} + /> +{/if} + {#if showDisclaimerModal && disclaimerCallbacks} void; + showFontPicker: () => void; showDisclaimer: (onConfirm: () => void, onCancel: () => void, title?: string, message?: string) => void; showCloudPanel: () => void; }>(); @@ -192,6 +193,16 @@ onClick: showColourPicker } }, + { + title: "Interface Font", + description: "Choose the typeface used across SEQTA Learn", + id: 16, + Component: Button, + props: { + onClick: showFontPicker, + text: "Change" + } + }, { title: "Icon Only Sidebar", description: "Show only icons in the sidebar for a compact layout", diff --git a/src/interface/pages/settings/theme.svelte b/src/interface/pages/settings/theme.svelte index 2f14890f..fece578a 100644 --- a/src/interface/pages/settings/theme.svelte +++ b/src/interface/pages/settings/theme.svelte @@ -36,4 +36,4 @@ {/if} - \ No newline at end of file + diff --git a/src/interface/utils/portal.ts b/src/interface/utils/portal.ts index d634d3be..6124639e 100644 --- a/src/interface/utils/portal.ts +++ b/src/interface/utils/portal.ts @@ -4,16 +4,21 @@ import type { Action } from "svelte/action"; * Svelte action that moves the element to a different DOM target. * Defaults to the nearest ShadowRoot so styles remain intact when the app * is rendered inside a shadow DOM. Falls back to document.body otherwise. - * Keeps all Svelte reactivity/events intact while escaping ancestor stacking contexts. + * Pass `document.body` to escape transformed/contained settings popups entirely. */ -export const portal: Action = (node, target) => { +export const portal: Action = ( + node, + target, +) => { const root = node.getRootNode(); const dest = target ?? (root instanceof ShadowRoot ? root : document.body); dest.appendChild(node); return { update(newTarget) { - (newTarget ?? dest).appendChild(node); + const nextDest = + newTarget ?? (root instanceof ShadowRoot ? root : document.body); + nextDest.appendChild(node); }, destroy() { node.remove(); diff --git a/src/interface/utils/syncPageTheme.ts b/src/interface/utils/syncPageTheme.ts new file mode 100644 index 00000000..7e45a197 --- /dev/null +++ b/src/interface/utils/syncPageTheme.ts @@ -0,0 +1,74 @@ +import { settingsState } from "@/seqta/utils/listeners/SettingsState"; + +const THEME_CSS_VARS = [ + "--better-main", + "--better-pale", + "--better-light", + "--text-color", + "--background-primary", + "--background-secondary", + "--text-primary", + "--theme-offset-bg", + "--better-sub", +] as const; + +const ACCENT_CSS_VARS = [ + "--better-main", + "--accent-color-value", + "--accentColor", + "--colour-betterseqta-blue", +] as const; + +function extractSolidColor(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed || trimmed === "initial") return null; + if ( + trimmed.startsWith("#") || + trimmed.startsWith("rgb") || + trimmed.startsWith("hsl") + ) { + return trimmed; + } + if (trimmed.includes("gradient")) { + const match = trimmed.match( + /#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgba?\([^)]+\)/i, + ); + return match?.[0] ?? null; + } + return null; +} + +function resolvePageAccentColor(): string { + const computed = getComputedStyle(document.documentElement); + for (const name of ACCENT_CSS_VARS) { + const solid = extractSolidColor(computed.getPropertyValue(name)); + if (solid) return solid; + } + const fromSettings = settingsState.selectedColor?.trim(); + if (fromSettings) { + const solid = extractSolidColor(fromSettings); + if (solid) return solid; + } + return "#007bff"; +} + +/** Copy SEQTA page theme tokens onto a portaled UI root (matches analytics sync). */ +export function syncPageThemeToElement(target: HTMLElement): void { + const computed = getComputedStyle(document.documentElement); + + for (const name of THEME_CSS_VARS) { + const value = computed.getPropertyValue(name).trim(); + if (value) { + target.style.setProperty(name, value); + } + } + + const accent = resolvePageAccentColor(); + target.style.setProperty("--bsplus-analytics-accent", accent); + target.style.setProperty("--better-main", accent); + + target.classList.toggle( + "dark", + document.documentElement.classList.contains("dark"), + ); +} diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index 82f707e9..0783c539 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -17,6 +17,7 @@ import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"; import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners"; import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements"; import { updateAllColors } from "@/seqta/ui/colors/Manager"; +import { applySelectedFont } from "@/seqta/ui/fonts/Manager"; import loading from "@/seqta/ui/Loading"; import { SendNewsPage } from "@/seqta/utils/SendNewsPage"; import { getEngageRoutePage } from "@/seqta/utils/engageRoute"; @@ -697,6 +698,7 @@ export function init() { new MessageHandler(); void updateAllColors(); + applySelectedFont(); window.addEventListener("hashchange", () => { if (settingsState.adaptiveThemeColour) void updateAllColors(); diff --git a/src/seqta/ui/fonts/Manager.ts b/src/seqta/ui/fonts/Manager.ts new file mode 100644 index 00000000..808e4efd --- /dev/null +++ b/src/seqta/ui/fonts/Manager.ts @@ -0,0 +1,136 @@ +import { settingsState } from "@/seqta/utils/listeners/SettingsState"; +import { FONT_PRESETS, getFontPreset, type FontPreset } from "./presets"; + +const FONT_STYLE_ID = "betterseqta-font-override"; +const FONT_PICKER_BATCH_ID = "betterseqta-font-picker-preview"; +const loadedFontIds = new Set(); +let pickerFontsPromise: Promise | null = null; + +/** Elements that show per-font previews must stay outside the global override. */ +export const FONT_PICKER_ROOT_CLASS = "bsplus-font-picker-root"; + +function googleFamilyParam(preset: FontPreset): string | null { + if (!preset.googleUrl) return null; + const name = + preset.stack.split(",")[0]?.trim().replace(/^"|"$/g, "") ?? ""; + if (!name) return null; + return `family=${encodeURIComponent(name)}:wght@400;500;600;700`; +} + +function injectStylesheet(id: string, href: string): Promise { + if (document.getElementById(id)) return Promise.resolve(); + + return new Promise((resolve) => { + const link = document.createElement("link"); + link.id = id; + link.rel = "stylesheet"; + link.href = href; + link.setAttribute("data-betterseqta-font-picker-batch", "true"); + link.onload = () => resolve(); + link.onerror = () => resolve(); + document.head.appendChild(link); + }); +} + +/** Load all Google Fonts for picker previews (batched, awaited). */ +export function ensureFontPickerFontsLoaded(): Promise { + if (!pickerFontsPromise) { + pickerFontsPromise = (async () => { + const params = FONT_PRESETS.map(googleFamilyParam).filter( + (param): param is string => param !== null, + ); + + const chunkSize = 10; + for (let i = 0; i < params.length; i += chunkSize) { + const chunk = params.slice(i, i + chunkSize); + const url = `https://fonts.googleapis.com/css2?${chunk.join("&")}&display=swap`; + await injectStylesheet(`${FONT_PICKER_BATCH_ID}-${i / chunkSize}`, url); + } + + try { + await document.fonts.ready; + } catch { + /* FontFaceSet unsupported or blocked */ + } + })(); + } + + return pickerFontsPromise; +} + +export function ensureFontLoaded(preset: FontPreset): void { + if (!preset.googleUrl || loadedFontIds.has(preset.id)) return; + + if (document.querySelector(`link[data-betterseqta-font="${preset.id}"]`)) { + loadedFontIds.add(preset.id); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = preset.googleUrl; + link.setAttribute("data-betterseqta-font", preset.id); + document.head.appendChild(link); + loadedFontIds.add(preset.id); +} + +export function buildFontPreviewCss(): string { + return FONT_PRESETS.map( + (preset) => ` +.bsplus-font-picker-option[data-font-id="${preset.id}"] .bsplus-font-picker-option-name { + font-family: ${preset.stack} !important; +}`, + ).join("\n"); +} + +const SEQTA_FONT_SCOPE = ` + .legacy-root, + .legacy-root input, + .legacy-root textarea, + .legacy-root button, + .legacy-root select, + .legacy-root option, + .legacy-root .input, + .legacy-root *, + #container, + #container * +`; + +function buildFontOverrideCss(family: string): string { + const rule = `font-family: ${family} !important;`; + + return ` + ${SEQTA_FONT_SCOPE} { + ${rule} + } + + .iconFamily, + .iconFamily *, + button.uiButton.timetable-zoom.iconFamily, + [class~="iconFamily"], + [class~="iconFamily"] * { + font-family: "IconFamily" !important; + } + `; +} + +export function applySelectedFont(fontId?: string | null): void { + if (typeof document === "undefined") return; + + const preset = getFontPreset(fontId ?? settingsState.selectedFont); + ensureFontLoaded(preset); + + document.documentElement.style.setProperty( + "--betterseqta-font-family", + preset.stack.split(",")[0]?.trim().replace(/^"|"$/g, "") ?? "Rubik", + ); + + let style = document.getElementById(FONT_STYLE_ID) as HTMLStyleElement | null; + if (!style) { + style = document.createElement("style"); + style.id = FONT_STYLE_ID; + document.head.appendChild(style); + } + + style.textContent = buildFontOverrideCss(preset.stack); +} diff --git a/src/seqta/ui/fonts/presets.ts b/src/seqta/ui/fonts/presets.ts new file mode 100644 index 00000000..e03b68d1 --- /dev/null +++ b/src/seqta/ui/fonts/presets.ts @@ -0,0 +1,265 @@ +export const DEFAULT_FONT_ID = "rubik"; + +export interface FontPreset { + id: string; + name: string; + stack: string; + googleUrl?: string; + sample: string; +} + +export const FONT_PRESETS: FontPreset[] = [ + { + id: "rubik", + name: "Rubik", + stack: "Rubik, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "inter", + name: "Inter", + stack: "Inter, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "poppins", + name: "Poppins", + stack: "Poppins, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "nunito", + name: "Nunito", + stack: "Nunito, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "montserrat", + name: "Montserrat", + stack: "Montserrat, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "open-sans", + name: "Open Sans", + stack: '"Open Sans", sans-serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "lato", + name: "Lato", + stack: "Lato, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "source-sans-3", + name: "Source Sans 3", + stack: '"Source Sans 3", sans-serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "raleway", + name: "Raleway", + stack: "Raleway, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "dm-sans", + name: "DM Sans", + stack: '"DM Sans", sans-serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "plus-jakarta-sans", + name: "Plus Jakarta Sans", + stack: '"Plus Jakarta Sans", sans-serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "outfit", + name: "Outfit", + stack: "Outfit, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "roboto", + name: "Roboto", + stack: "Roboto, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "work-sans", + name: "Work Sans", + stack: '"Work Sans", sans-serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "manrope", + name: "Manrope", + stack: "Manrope, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "figtree", + name: "Figtree", + stack: "Figtree, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "lexend", + name: "Lexend", + stack: "Lexend, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Lexend:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "ubuntu", + name: "Ubuntu", + stack: "Ubuntu, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "karla", + name: "Karla", + stack: "Karla, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Karla:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "quicksand", + name: "Quicksand", + stack: "Quicksand, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "ibm-plex-sans", + name: "IBM Plex Sans", + stack: '"IBM Plex Sans", sans-serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "space-grotesk", + name: "Space Grotesk", + stack: '"Space Grotesk", sans-serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "mulish", + name: "Mulish", + stack: "Mulish, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Mulish:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "cabin", + name: "Cabin", + stack: "Cabin, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Cabin:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "oswald", + name: "Oswald", + stack: "Oswald, sans-serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "merriweather", + name: "Merriweather", + stack: "Merriweather, serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "playfair-display", + name: "Playfair Display", + stack: '"Playfair Display", serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "lora", + name: "Lora", + stack: "Lora, serif", + googleUrl: + "https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "crimson-pro", + name: "Crimson Pro", + stack: '"Crimson Pro", serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "libre-baskerville", + name: "Libre Baskerville", + stack: '"Libre Baskerville", serif', + googleUrl: + "https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&display=swap", + sample: "Assessment due tomorrow at 3:30pm", + }, + { + id: "system", + name: "System Default", + stack: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + sample: "Assessment due tomorrow at 3:30pm", + }, +]; + +export function getFontPreset(id?: string | null): FontPreset { + return ( + FONT_PRESETS.find((preset) => preset.id === id) ?? + FONT_PRESETS.find((preset) => preset.id === DEFAULT_FONT_ID)! + ); +} diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index 2d9ccf06..ed5316c8 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -1,5 +1,6 @@ import { settingsState } from "./SettingsState"; import { updateAllColors } from "@/seqta/ui/colors/Manager"; +import { applySelectedFont } from "@/seqta/ui/fonts/Manager"; // Shortcuts rendering import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts"; @@ -40,6 +41,7 @@ export class StorageChangeHandler { "iconOnlySidebar", this.handleIconOnlySidebarChange.bind(this), ); + settingsState.register("selectedFont", () => applySelectedFont()); } private handleIconOnlySidebarChange(newValue: boolean | undefined) { diff --git a/src/types/storage.ts b/src/types/storage.ts index 9ee34640..705f1d93 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -57,6 +57,8 @@ export interface SettingsState { adaptiveThemeColour?: boolean; adaptiveThemeGradient?: boolean; adaptiveThemeColourTransition?: boolean; + /** Google Font preset id for SEQTA interface typography (`rubik` default). */ + selectedFont?: string; // depreciated keys animatedbk: boolean;