diff --git a/src/app.css b/src/app.css index f4e3225d3b..9646c0f9ce 100644 --- a/src/app.css +++ b/src/app.css @@ -30,8 +30,33 @@ font-display: swap; } +/* --app-text-scale is updated via the UI Scale slider (Interface.svelte) */ +:root { + --app-text-scale: 1; +} + html { word-break: break-word; + /* font-size scales the entire document via the same UI control */ + font-size: calc(1rem * var(--app-text-scale, 1)); +} + +#sidebar-chat-item { + /* sidebar item sizing scales for the chat list entries */ + min-height: calc(32px * var(--app-text-scale, 1)); + padding-inline: calc(11px * var(--app-text-scale, 1)); + padding-block: calc(6px * var(--app-text-scale, 1)); +} + +#sidebar-chat-item div[dir='auto'] { + /* chat title line height follows the text scale */ + height: calc(20px * var(--app-text-scale, 1)); + line-height: calc(20px * var(--app-text-scale, 1)); +} + +#sidebar-chat-item input { + /* editing state input height is kept in sync */ + min-height: calc(20px * var(--app-text-scale, 1)); } code { diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index 15debe669c..1b0d978ae5 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -1,6 +1,6 @@ @@ -331,6 +393,64 @@ +
+
+
+ + +
+ {textScaleDisplay}x +
+
+ +
+ + +
+ +
+ + +
+
+
+
diff --git a/src/lib/components/common/Switch.svelte b/src/lib/components/common/Switch.svelte index 2b433973fb..7a4c936c96 100644 --- a/src/lib/components/common/Switch.svelte +++ b/src/lib/components/common/Switch.svelte @@ -28,7 +28,7 @@ bind:checked={state} {id} aria-labelledby={ariaLabelledbyId} - class="flex h-[18px] min-h-[18px] w-8 shrink-0 cursor-pointer items-center rounded-full px-1 mx-[1px] transition {($settings?.highContrastMode ?? + class="flex h-[1.125rem] min-h-[1.125rem] w-8 shrink-0 cursor-pointer items-center rounded-full px-1 mx-[1px] transition {($settings?.highContrastMode ?? false) ? 'focus:outline focus:outline-2 focus:outline-gray-800 focus:dark:outline-gray-200' : 'outline outline-1 outline-gray-100 dark:outline-gray-800'} {state diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index 726455cd6f..d7d54d1c7a 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -401,8 +401,7 @@ {:else}
-
+
{title}
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index b569e0ac2e..742f447721 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -832,6 +832,9 @@ "Hide Model": "", "High": "", "High Contrast Mode": "", + "Decrease Text Size": "", + "Increase Text Size": "", + "Text Size": "", "Home": "", "Host": "", "How can I help you today?": "", diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 27ca354ec5..1606553277 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -5,6 +5,7 @@ import type { Banner } from '$lib/types'; import type { Socket } from 'socket.io-client'; import emojiShortCodes from '$lib/emoji-shortcodes.json'; +import { resolveTextScale, setDocumentTextScale } from '$lib/utils/text-scale'; // Backend export const WEBUI_NAME = writable(APP_NAME); @@ -68,6 +69,13 @@ export const banners: Writable = writable([]); export const settings: Writable = writable({}); +if (typeof window !== 'undefined') { + settings.subscribe(($settings) => { + const clampedScale = resolveTextScale($settings?.textScale ?? 1); + setDocumentTextScale(clampedScale); + }); +} + export const audioQueue = writable(null); export const showSidebar = writable(false); @@ -161,6 +169,7 @@ type Settings = { notifications?: any; imageCompression?: boolean; imageCompressionSize?: any; + textScale?: number; widescreenMode?: null; largeTextAsFile?: boolean; promptAutocomplete?: boolean; diff --git a/src/lib/utils/text-scale.ts b/src/lib/utils/text-scale.ts new file mode 100644 index 0000000000..3b68cf773c --- /dev/null +++ b/src/lib/utils/text-scale.ts @@ -0,0 +1,55 @@ +const TEXT_SCALE_VALUES = [1, 1.1, 1.2, 1.3, 1.4, 1.5] as const; + +export type TextScale = (typeof TEXT_SCALE_VALUES)[number]; + +export const TEXT_SCALE_MIN = TEXT_SCALE_VALUES[0]; +export const TEXT_SCALE_MAX = TEXT_SCALE_VALUES[TEXT_SCALE_VALUES.length - 1]; + +export const DEFAULT_TEXT_SCALE: TextScale = 1; +export const DEFAULT_TEXT_SCALE_INDEX = TEXT_SCALE_VALUES.findIndex( + (scale) => scale === DEFAULT_TEXT_SCALE +); + +export const getScaleFromIndex = (index: number): TextScale => { + if (!Number.isFinite(index)) { + return TEXT_SCALE_VALUES[DEFAULT_TEXT_SCALE_INDEX]; + } + + return TEXT_SCALE_VALUES[index] ?? TEXT_SCALE_VALUES[DEFAULT_TEXT_SCALE_INDEX]; +}; + +export const findClosestTextScaleIndex = (value: unknown): number => { + const numeric = Number(value); + + if (!Number.isFinite(numeric)) { + return DEFAULT_TEXT_SCALE_INDEX; + } + + let closestIndex = DEFAULT_TEXT_SCALE_INDEX; + let smallestDistance = Number.POSITIVE_INFINITY; + + TEXT_SCALE_VALUES.forEach((scale, idx) => { + const distance = Math.abs(scale - numeric); + + if (distance < smallestDistance) { + closestIndex = idx; + smallestDistance = distance; + } + }); + + return closestIndex; +}; + +export const resolveTextScale = (value: unknown): TextScale => { + return TEXT_SCALE_VALUES[findClosestTextScaleIndex(value)] ?? DEFAULT_TEXT_SCALE; +}; + +export const setDocumentTextScale = (scale: TextScale) => { + if (typeof document === 'undefined') { + return; + } + + document.documentElement.style.setProperty('--app-text-scale', scale.toString()); +}; + +export { TEXT_SCALE_VALUES };