Skip to content

Commit 8917acd

Browse files
committed
Implemented font size switching in settings
1 parent e1383dd commit 8917acd

File tree

7 files changed

+252
-55
lines changed

7 files changed

+252
-55
lines changed

src/lib/components/furniture/SettingsPanel.svelte

Lines changed: 142 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { customCss } from '$lib/stores/customCss';
1010
import { siteCustomization } from '$lib/stores/siteCustomization';
1111
import { primaryColor } from '$lib/stores/primaryColor';
12+
import { fontScale, fontScaleOptions, type FontScaleLevel } from '$lib/stores/fontScale';
1213
import { storage } from '$lib/utils/localStorage';
1314
1415
interface Props {
@@ -30,6 +31,7 @@
3031
let currentNavbarDisplay = $state(navbarDisplay);
3132
let currentHomepageLayout = $state(homepageLayout);
3233
let currentCustomCss = $state(customCss);
34+
let currentFontScale = $state(fontScale);
3335
3436
// Form inputs
3537
let cssInput = $state('');
@@ -120,6 +122,8 @@
120122
navbarDisplay.setMode((e.currentTarget as HTMLSelectElement).value as NavbarDisplayMode),
121123
homepageChange: (e: Event) =>
122124
homepageLayout.setMode((e.currentTarget as HTMLSelectElement).value as HomepageLayoutMode),
125+
fontScaleChange: (e: Event) =>
126+
fontScale.setLevel(parseInt((e.currentTarget as HTMLInputElement).value, 10) as FontScaleLevel),
123127
linkClick: () => onClose?.(),
124128
125129
applyCustomCss: () => {
@@ -272,6 +276,29 @@
272276
{/if}
273277
</div>
274278

279+
{#if standalone}
280+
<div class="settings-section font-size-section">
281+
<h3>Font Scale</h3>
282+
<div class="font-scale-slider">
283+
<input
284+
type="range"
285+
min="0"
286+
max="4"
287+
step="1"
288+
value={$currentFontScale}
289+
oninput={handlers.fontScaleChange}
290+
aria-label="Font scale"
291+
class="slider"
292+
/>
293+
<div class="slider-labels">
294+
{#each fontScaleOptions as option (option.level)}
295+
<span class="slider-label" class:active={$currentFontScale === option.level}>{option.label}</span>
296+
{/each}
297+
</div>
298+
</div>
299+
</div>
300+
{/if}
301+
275302
<!-- Language Selection -->
276303
<div class="settings-section language-section">
277304
<h3>Language</h3>
@@ -548,6 +575,9 @@
548575
margin: 0;
549576
550577
&.theme-section {
578+
grid-row: span 3;
579+
}
580+
&.language-section {
551581
grid-row: span 2;
552582
}
553583
&.accessibility-section {
@@ -639,20 +669,8 @@
639669
color: var(--text-secondary);
640670
641671
&:hover:not(.disabled) {
642-
background: var(--surface-hover);
643672
border-color: var(--border-secondary);
644673
}
645-
646-
&.active {
647-
background: color-mix(in srgb, var(--color-primary), transparent 90%);
648-
border-color: var(--color-primary);
649-
color: var(--text-primary);
650-
}
651-
652-
&.disabled {
653-
opacity: 0.6;
654-
cursor: not-allowed;
655-
}
656674
}
657675
658676
.theme-preview {
@@ -662,23 +680,104 @@
662680
border: 1px solid var(--border-secondary);
663681
}
664682
683+
.font-scale-slider {
684+
display: flex;
685+
flex-direction: column;
686+
gap: var(--spacing-sm);
687+
688+
.slider {
689+
width: 100%;
690+
height: 0.375rem;
691+
background: linear-gradient(to right, var(--bg-tertiary) 0%, var(--border-primary) 50%, var(--bg-tertiary) 100%);
692+
border-radius: var(--radius-sm);
693+
outline: none;
694+
-webkit-appearance: none;
695+
appearance: none;
696+
cursor: pointer;
697+
position: relative;
698+
699+
&::-webkit-slider-thumb {
700+
-webkit-appearance: none;
701+
appearance: none;
702+
width: 1.25rem;
703+
height: 1.25rem;
704+
background: var(--color-primary);
705+
border: 2px solid var(--bg-secondary);
706+
border-radius: 50%;
707+
cursor: pointer;
708+
transition: all var(--transition-normal);
709+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
710+
711+
&:hover {
712+
transform: scale(1.1);
713+
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
714+
}
715+
}
716+
717+
&::-moz-range-thumb {
718+
width: 1.25rem;
719+
height: 1.25rem;
720+
background: var(--color-primary);
721+
border: 2px solid var(--bg-secondary);
722+
border-radius: 50%;
723+
cursor: pointer;
724+
transition: all var(--transition-normal);
725+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
726+
727+
&:hover {
728+
transform: scale(1.1);
729+
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
730+
}
731+
}
732+
733+
&:focus {
734+
&::-webkit-slider-thumb {
735+
outline: 2px solid var(--color-primary);
736+
outline-offset: 2px;
737+
}
738+
&::-moz-range-thumb {
739+
outline: 2px solid var(--color-primary);
740+
outline-offset: 2px;
741+
}
742+
}
743+
}
744+
745+
.slider-labels {
746+
display: flex;
747+
justify-content: space-between;
748+
font-size: var(--font-size-sm);
749+
color: var(--text-tertiary);
750+
margin-top: var(--spacing-xs);
751+
752+
.slider-label {
753+
flex: 1;
754+
text-align: center;
755+
transition: all var(--transition-fast);
756+
font-size: var(--font-size-xs);
757+
758+
&.active {
759+
color: var(--color-primary);
760+
font-weight: 500;
761+
}
762+
763+
&:first-child {
764+
text-align: left;
765+
}
766+
&:last-child {
767+
text-align: right;
768+
}
769+
}
770+
}
771+
}
772+
665773
.language-dropdown {
666774
gap: var(--spacing-xs);
667775
display: grid;
668776
grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
669777
}
670778
671-
.language-option {
672-
padding: var(--spacing-sm);
673-
background: var(--bg-tertiary);
674-
border: 1px solid var(--border-primary);
675-
border-radius: var(--radius-sm);
676-
text-align: left;
677-
cursor: pointer;
678-
transition: all var(--transition-normal);
679-
font-size: var(--font-size-sm);
680-
color: var(--text-secondary);
681-
779+
.language-option,
780+
.theme-option {
682781
&:hover:not(.disabled) {
683782
background: var(--surface-hover);
684783
color: var(--text-primary);
@@ -696,6 +795,18 @@
696795
}
697796
}
698797
798+
.language-option {
799+
padding: var(--spacing-sm);
800+
background: var(--bg-tertiary);
801+
border: 1px solid var(--border-primary);
802+
border-radius: var(--radius-sm);
803+
text-align: left;
804+
cursor: pointer;
805+
transition: all var(--transition-normal);
806+
font-size: var(--font-size-sm);
807+
color: var(--text-secondary);
808+
}
809+
699810
.navbar-select-wrapper {
700811
position: relative;
701812
display: block;
@@ -857,11 +968,11 @@
857968
overflow: hidden;
858969
}
859970
860-
.show-more-btn {
971+
.show-more-btn,
972+
.custom-color-toggle {
861973
display: flex;
862974
align-items: center;
863975
gap: var(--spacing-xs);
864-
width: 100%;
865976
padding: var(--spacing-sm);
866977
background: transparent;
867978
border: 1px solid var(--border-primary);
@@ -870,14 +981,18 @@
870981
font-size: var(--font-size-xs);
871982
cursor: pointer;
872983
transition: all var(--transition-normal);
873-
margin-top: var(--spacing-sm);
874-
justify-content: center;
875984
876985
&:hover {
877986
background: var(--surface-hover);
878987
color: var(--text-primary);
879988
border-color: var(--border-secondary);
880989
}
990+
}
991+
992+
.show-more-btn {
993+
width: 100%;
994+
margin-top: var(--spacing-sm);
995+
justify-content: center;
881996
882997
&[aria-expanded='true'] {
883998
color: var(--color-primary);
@@ -1006,24 +1121,9 @@
10061121
10071122
.custom-color-toggle {
10081123
flex: 1;
1009-
display: flex;
1010-
align-items: center;
10111124
justify-content: center;
1012-
gap: var(--spacing-xs);
10131125
padding: var(--spacing-sm) var(--spacing-md);
1014-
background: var(--bg-tertiary);
1015-
border: 1px solid var(--border-primary);
1016-
border-radius: var(--radius-sm);
1017-
color: var(--text-secondary);
10181126
font-size: var(--font-size-sm);
1019-
cursor: pointer;
1020-
transition: all var(--transition-normal);
1021-
1022-
&:hover {
1023-
background: var(--surface-hover);
1024-
color: var(--text-primary);
1025-
border-color: var(--border-secondary);
1026-
}
10271127
}
10281128
10291129
.custom-color-inputs {

src/lib/config/customizable-settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,10 @@ export const DEFAULT_LANGUAGE = env.NTB_DEFAULT_LANGUAGE ?? 'en';
6666
*/
6767
export const PRIMARY_COLOR = env.NTB_PRIMARY_COLOR ?? '';
6868

69+
/**
70+
* Default font scale level
71+
* Options: 0 (Very Small), 1 (Small), 2 (Normal), 3 (Large), 4 (Very Large)
72+
*/
73+
export const DEFAULT_FONT_SCALE = parseInt(env.NTB_FONT_SCALE ?? '2', 10);
74+
6975
export const SHOW_TIPS_ON_HOMEPAGE = !!env.NTB_SHOW_TIPS_ON_HOMEPAGE;

src/lib/stores/fontScale.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { writable } from 'svelte/store';
2+
import { browser } from '$app/environment';
3+
import { storage } from '$lib/utils/localStorage';
4+
import { DEFAULT_FONT_SCALE } from '$lib/config/customizable-settings';
5+
6+
export type FontScaleLevel = 0 | 1 | 2 | 3 | 4;
7+
8+
export interface FontScaleOption {
9+
level: FontScaleLevel;
10+
label: string;
11+
scale: number;
12+
}
13+
14+
export const fontScaleOptions: FontScaleOption[] = [
15+
{ level: 0, label: 'Extra Small', scale: 0.85 },
16+
{ level: 1, label: 'Small', scale: 0.925 },
17+
{ level: 2, label: 'Normal', scale: 1.0 },
18+
{ level: 3, label: 'Large', scale: 1.075 },
19+
{ level: 4, label: 'Extra Large', scale: 1.15 },
20+
];
21+
22+
const STORAGE_KEY = 'font-scale';
23+
24+
function isValidLevel(level: unknown): level is FontScaleLevel {
25+
return typeof level === 'number' && level >= 0 && level <= 4;
26+
}
27+
28+
function applyFontScale(level: FontScaleLevel) {
29+
if (!browser) return;
30+
31+
const option = fontScaleOptions.find((opt) => opt.level === level);
32+
if (!option) return;
33+
34+
document.documentElement.style.setProperty('--font-scale', option.scale.toString());
35+
}
36+
37+
function createFontScaleStore() {
38+
const defaultLevel = isValidLevel(DEFAULT_FONT_SCALE) ? DEFAULT_FONT_SCALE : 2;
39+
const { subscribe, set } = writable<FontScaleLevel>(defaultLevel);
40+
41+
return {
42+
subscribe,
43+
44+
// Initialize from localStorage or default
45+
init: () => {
46+
if (browser) {
47+
const saved = localStorage.getItem(STORAGE_KEY);
48+
const savedLevel = saved ? parseInt(saved, 10) : null;
49+
const initialLevel = savedLevel !== null && isValidLevel(savedLevel) ? savedLevel : defaultLevel;
50+
51+
set(initialLevel);
52+
applyFontScale(initialLevel);
53+
54+
return initialLevel;
55+
}
56+
return defaultLevel;
57+
},
58+
59+
// Set font scale level
60+
setLevel: (level: FontScaleLevel) => {
61+
if (!isValidLevel(level)) return;
62+
63+
set(level);
64+
65+
if (browser) {
66+
storage.setItem(STORAGE_KEY, level.toString(), { serialize: false });
67+
applyFontScale(level);
68+
}
69+
},
70+
71+
// Get scale value for a level
72+
getScale: (level: FontScaleLevel): number => {
73+
const option = fontScaleOptions.find((opt) => opt.level === level);
74+
return option?.scale ?? 1.0;
75+
},
76+
};
77+
}
78+
79+
export const fontScale = createFontScaleStore();

0 commit comments

Comments
 (0)