Skip to content

Commit 2b05464

Browse files
authored
Merge pull request #69 from DEEIX-AI/font_size
feat: implement font size preferences
2 parents cd43f80 + 71141d8 commit 2b05464

13 files changed

Lines changed: 430 additions & 18 deletions

File tree

backend/internal/application/auth/service.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -738,47 +738,64 @@ func (s *Service) UpdateProfile(ctx context.Context, userID uint, input UpdatePr
738738
}
739739
if input.AppearancePreferences != nil {
740740
appearancePreferences := strings.TrimSpace(*input.AppearancePreferences)
741-
if err := validateAppearancePreferences(appearancePreferences); err != nil {
741+
normalizedAppearancePreferences, err := normalizeAppearancePreferences(appearancePreferences)
742+
if err != nil {
742743
return nil, err
743744
}
744-
updateInput.AppearancePreferences = &appearancePreferences
745+
updateInput.AppearancePreferences = &normalizedAppearancePreferences
745746
}
746747

747748
return s.repo.UpdateProfile(ctx, userID, updateInput)
748749
}
749750

750-
func validateAppearancePreferences(raw string) error {
751+
func normalizeAppearancePreferences(raw string) (string, error) {
751752
if raw == "" {
752-
return nil
753+
return "", nil
753754
}
754755

755756
var payload map[string]string
756757
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
757-
return ErrInvalidAppearancePreferences
758+
return "", ErrInvalidAppearancePreferences
758759
}
760+
761+
normalized := make(map[string]string, len(payload))
759762
for key, value := range payload {
760763
switch key {
761764
case "theme":
762765
if value != "light" && value != "dark" && value != "system" {
763-
return ErrInvalidAppearancePreferences
766+
return "", ErrInvalidAppearancePreferences
764767
}
768+
normalized[key] = value
765769
case "preset":
766770
if value != "default" && value != "azure" && value != "cobalt" && value != "graphite" && value != "lagoon" && value != "ink" && value != "ochre" && value != "sepia" {
767-
return ErrInvalidAppearancePreferences
771+
return "", ErrInvalidAppearancePreferences
768772
}
773+
normalized[key] = value
769774
case "chatFont":
770775
if value != "default" && value != "songti" && value != "heiti" && value != "mono" {
771-
return ErrInvalidAppearancePreferences
776+
return "", ErrInvalidAppearancePreferences
772777
}
778+
normalized[key] = value
773779
case "chatFontWeight":
774780
if value != "regular" && value != "medium" && value != "semibold" && value != "bold" {
775-
return ErrInvalidAppearancePreferences
781+
return "", ErrInvalidAppearancePreferences
782+
}
783+
normalized[key] = value
784+
case "fontSize":
785+
if value != "small" && value != "standard" && value != "medium" && value != "large" {
786+
value = "standard"
776787
}
788+
normalized[key] = value
777789
default:
778-
return ErrInvalidAppearancePreferences
790+
return "", ErrInvalidAppearancePreferences
779791
}
780792
}
781-
return nil
793+
794+
next, err := json.Marshal(normalized)
795+
if err != nil {
796+
return "", ErrInvalidAppearancePreferences
797+
}
798+
return string(next), nil
782799
}
783800

784801
func normalizeLocale(raw string) (string, error) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package auth
2+
3+
import "testing"
4+
5+
func TestNormalizeAppearancePreferencesAllowsFontSize(t *testing.T) {
6+
for _, fontSize := range []string{"small", "standard", "medium", "large"} {
7+
payload := `{"theme":"system","preset":"default","chatFont":"default","chatFontWeight":"regular","fontSize":"` + fontSize + `"}`
8+
9+
if _, err := normalizeAppearancePreferences(payload); err != nil {
10+
t.Fatalf("expected fontSize %q appearance preference to be valid, got %v", fontSize, err)
11+
}
12+
}
13+
}
14+
15+
func TestNormalizeAppearancePreferencesDefaultsInvalidFontSize(t *testing.T) {
16+
payload := `{"fontSize":"huge"}`
17+
18+
normalized, err := normalizeAppearancePreferences(payload)
19+
if err != nil {
20+
t.Fatalf("expected invalid fontSize appearance preference to fall back, got %v", err)
21+
}
22+
if normalized != `{"fontSize":"standard"}` {
23+
t.Fatalf("expected invalid fontSize to fall back to standard, got %s", normalized)
24+
}
25+
}
26+
27+
func TestNormalizeAppearancePreferencesRejectsUnknownKey(t *testing.T) {
28+
payload := `{"fontSize":"standard","unknown":"value"}`
29+
30+
if _, err := normalizeAppearancePreferences(payload); err == nil {
31+
t.Fatal("expected unknown appearance preference key to be rejected")
32+
}
33+
}

frontend/app/globals.css

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
--font-heiti: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", "Source Han Sans SC", sans-serif;
5252
--font-chat: var(--font-sans);
5353
--font-chat-weight: 400;
54+
--ui-font-scale: 1;
55+
--chat-font-scale: 1;
5456
--radius: 0.5rem;
5557
--shadow-x: 0;
5658
--shadow-y: 1px;
@@ -931,6 +933,23 @@
931933
--font-serif: var(--font-serif);
932934
--font-economist: var(--font-economist);
933935

936+
--text-xs: calc(0.75rem * var(--ui-font-scale));
937+
--text-sm: calc(0.875rem * var(--ui-font-scale));
938+
--text-base: calc(1rem * var(--ui-font-scale));
939+
--text-lg: calc(1.125rem * var(--ui-font-scale));
940+
--text-xl: calc(1.25rem * var(--ui-font-scale));
941+
--text-2xl: calc(1.5rem * var(--ui-font-scale));
942+
--text-3xl: calc(1.875rem * var(--ui-font-scale));
943+
--text-4xl: calc(2.25rem * var(--ui-font-scale));
944+
--text-xs--line-height: calc(1rem * var(--ui-font-scale));
945+
--text-sm--line-height: calc(1.25rem * var(--ui-font-scale));
946+
--text-base--line-height: calc(1.5rem * var(--ui-font-scale));
947+
--text-lg--line-height: calc(1.75rem * var(--ui-font-scale));
948+
--text-xl--line-height: calc(1.75rem * var(--ui-font-scale));
949+
--text-2xl--line-height: calc(2rem * var(--ui-font-scale));
950+
--text-3xl--line-height: calc(2.25rem * var(--ui-font-scale));
951+
--text-4xl--line-height: calc(2.5rem * var(--ui-font-scale));
952+
934953
--radius-sm: calc(var(--radius) - 4px);
935954
--radius-md: calc(var(--radius) - 2px);
936955
--radius-lg: var(--radius);
@@ -1055,7 +1074,78 @@
10551074
--font-chat-weight: 700;
10561075
}
10571076

1077+
:root[data-font-size="small"] {
1078+
--ui-font-scale: 0.88;
1079+
--chat-font-scale: 0.88;
1080+
}
1081+
1082+
:root[data-font-size="medium"] {
1083+
--ui-font-scale: 1.12;
1084+
--chat-font-scale: 1.12;
1085+
}
1086+
1087+
:root[data-font-size="large"] {
1088+
--ui-font-scale: 1.24;
1089+
--chat-font-scale: 1.24;
1090+
}
1091+
10581092
@layer utilities {
1093+
.text-\[8px\] {
1094+
font-size: calc(8px * var(--ui-font-scale));
1095+
}
1096+
1097+
.text-\[9px\] {
1098+
font-size: calc(9px * var(--ui-font-scale));
1099+
}
1100+
1101+
.text-\[10px\] {
1102+
font-size: calc(10px * var(--ui-font-scale));
1103+
}
1104+
1105+
.text-\[11px\] {
1106+
font-size: calc(11px * var(--ui-font-scale));
1107+
}
1108+
1109+
.text-\[12px\] {
1110+
font-size: calc(12px * var(--ui-font-scale));
1111+
}
1112+
1113+
.text-\[12\.5px\] {
1114+
font-size: calc(12.5px * var(--ui-font-scale));
1115+
}
1116+
1117+
.text-\[13px\] {
1118+
font-size: calc(13px * var(--ui-font-scale));
1119+
}
1120+
1121+
.text-\[15px\] {
1122+
font-size: calc(15px * var(--ui-font-scale));
1123+
}
1124+
1125+
.text-\[22px\] {
1126+
font-size: calc(22px * var(--ui-font-scale));
1127+
}
1128+
1129+
.text-\[32px\] {
1130+
font-size: calc(32px * var(--ui-font-scale));
1131+
}
1132+
1133+
.text-\[0\.8rem\] {
1134+
font-size: calc(0.8rem * var(--ui-font-scale));
1135+
}
1136+
1137+
.chat-font-content.text-\[15px\],
1138+
.chat-font-content .text-\[15px\],
1139+
textarea.text-\[15px\] {
1140+
font-size: calc(15px * var(--chat-font-scale));
1141+
}
1142+
1143+
@media (min-width: 48rem) {
1144+
.md\:text-\[32px\] {
1145+
font-size: calc(32px * var(--ui-font-scale));
1146+
}
1147+
}
1148+
10591149
.chat-font-content {
10601150
font-family: var(--font-chat);
10611151
font-weight: var(--font-chat-weight);

frontend/app/layout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33

44
import { ChatFontProvider } from "@/features/layouts/components/providers/chat-font-provider";
5+
import { FontSizeProvider } from "@/features/layouts/components/providers/font-size-provider";
56
import { WorkspaceShell } from "@/features/layouts/components/sections/workspace-shell";
67
import { AppI18nProvider } from "@/i18n/app-i18n-provider";
78
import { DevtoolsBrandBanner } from "@/shared/components/devtools-brand-banner";
@@ -49,12 +50,14 @@ export default function RootLayout({
4950
>
5051
<AppI18nProvider>
5152
<ThemeProvider>
52-
<ChatFontProvider>
53-
<WorkspaceShell>{children}</WorkspaceShell>
54-
<Toaster />
55-
{webVitalsEnabled ? <WebVitals /> : null}
56-
<DevtoolsBrandBanner />
57-
</ChatFontProvider>
53+
<FontSizeProvider>
54+
<ChatFontProvider>
55+
<WorkspaceShell>{children}</WorkspaceShell>
56+
<Toaster />
57+
{webVitalsEnabled ? <WebVitals /> : null}
58+
<DevtoolsBrandBanner />
59+
</ChatFontProvider>
60+
</FontSizeProvider>
5861
</ThemeProvider>
5962
</AppI18nProvider>
6063
</body>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
5+
import {
6+
applyFontSizePreference,
7+
useFontSizePreference,
8+
} from "@/features/settings/utils/font-size";
9+
10+
export function FontSizeProvider({
11+
children,
12+
}: {
13+
children: React.ReactNode;
14+
}) {
15+
const fontSize = useFontSizePreference();
16+
17+
React.useEffect(() => {
18+
applyFontSizePreference(fontSize);
19+
}, [fontSize]);
20+
21+
return children;
22+
}

frontend/features/settings/components/appearance-preferences-sync.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
writeChatFontPreference,
1010
writeChatFontWeightPreference,
1111
} from "@/features/settings/utils/chat-font";
12+
import {
13+
readFontSizePreference,
14+
writeFontSizePreference,
15+
} from "@/features/settings/utils/font-size";
1216
import { useAuthSession } from "@/shared/auth/auth-session-context";
1317
import { useTheme } from "@/shared/components/theme-provider";
1418

@@ -39,6 +43,9 @@ export function AppearancePreferencesSync() {
3943
if (next.chatFontWeight !== readChatFontWeightPreference()) {
4044
writeChatFontWeightPreference(next.chatFontWeight);
4145
}
46+
if (next.fontSize !== readFontSizePreference()) {
47+
writeFontSizePreference(next.fontSize);
48+
}
4249
}, [setPreset, setTheme, user, userStatus]);
4350

4451
return null;

0 commit comments

Comments
 (0)