From 8b25539e183d3dedb8a701946d93b769fcfdcd76 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 12 Jul 2026 19:03:11 +0800 Subject: [PATCH] feat(web): add text size theme axis and font license attribution - New data-theme-text axis (sm/default/lg/xl/2xl) that overrides only the --text-* type ramp, composing with density and presets; config drawer section with per-tier "Aa" previews, cookie persistence, i18n for all seven locales - Add one-line font licensing note under the font picker linking Public Sans, Lora, and JetBrains Mono sources and the SIL OFL 1.1 license text --- web/default/src/components/config-drawer.tsx | 113 ++++++++++++++++++ .../context/theme-customization-provider.tsx | 51 +++++++- web/default/src/i18n/locales/en.json | 6 + web/default/src/i18n/locales/fr.json | 6 + web/default/src/i18n/locales/ja.json | 6 + web/default/src/i18n/locales/ru.json | 6 + web/default/src/i18n/locales/vi.json | 6 + web/default/src/i18n/locales/zh-TW.json | 6 + web/default/src/i18n/locales/zh.json | 6 + web/default/src/lib/theme-customization.ts | 18 +++ web/default/src/styles/theme-presets.css | 50 +++++++- 11 files changed, 270 insertions(+), 4 deletions(-) diff --git a/web/default/src/components/config-drawer.tsx b/web/default/src/components/config-drawer.tsx index 71bc5f1cb3..00cfd1f9b5 100644 --- a/web/default/src/components/config-drawer.tsx +++ b/web/default/src/components/config-drawer.tsx @@ -61,6 +61,7 @@ import { type ThemePreset, type ThemeRadius, type ThemeScale, + type ThemeTextSize, } from '@/lib/theme-customization' import { cn } from '@/lib/utils' @@ -122,6 +123,7 @@ export function ConfigDrawer({ + {showLayoutControls && ( <> @@ -436,10 +438,52 @@ function FontConfig() { ))} + ) } +/** + * One-line licensing attribution for the bundled webfonts. All three faces + * ship under the SIL Open Font License 1.1, which permits commercial use; + * each name links to its official source and the license text. + */ +function FontLicenseNote() { + const { t } = useTranslation() + const fontSources: { name: string; href: string }[] = [ + { name: 'Public Sans', href: 'https://github.com/uswds/public-sans' }, + { name: 'Lora', href: 'https://fonts.google.com/specimen/Lora' }, + { name: 'JetBrains Mono', href: 'https://www.jetbrains.com/lp/mono/' }, + ] + return ( +

+ {fontSources.map((font, index) => ( + + {index > 0 && ' · '} + + {font.name} + + + ))} + {' — '} + + SIL OFL 1.1 + + {`, ${t('free for commercial use')}`} +

+ ) +} + const RADIUS_OPTIONS: { value: ThemeRadius label: string @@ -593,6 +637,75 @@ function ScaleConfig() { ) } +function TextSizeConfig() { + const { t } = useTranslation() + const { defaults, customization, setTextSize } = useThemeCustomization() + // Preview font sizes mirror each tier's `--text-base` so the tiles show + // the actual relative difference between tiers. + const textSizeOptions: { + value: ThemeTextSize + label: string + previewSize: string + }[] = [ + { value: 'sm', label: t('Small'), previewSize: '0.88rem' }, + { value: 'default', label: t('Default'), previewSize: '1rem' }, + { value: 'lg', label: t('Large'), previewSize: '1.075rem' }, + { value: 'xl', label: t('Extra Large'), previewSize: '1.125rem' }, + { value: '2xl', label: t('Super Large'), previewSize: '1.21rem' }, + ] + return ( +
+ setTextSize(defaults.textSize)} + /> + setTextSize(v as ThemeTextSize)} + className='grid w-full grid-cols-5 gap-2' + aria-label={t('Select text size')} + > + {textSizeOptions.map((option) => ( + +
+
+
+ {option.label} +
+
+ ))} +
+
+ ) +} + /** * Mock pill rendered inside the badge-size preview tiles. Each option shows * the pill at the proportions that size will actually produce. diff --git a/web/default/src/context/theme-customization-provider.tsx b/web/default/src/context/theme-customization-provider.tsx index 167ad269e7..b760e31d3f 100644 --- a/web/default/src/context/theme-customization-provider.tsx +++ b/web/default/src/context/theme-customization-provider.tsx @@ -37,12 +37,14 @@ import { THEME_PRESET_VALUES, THEME_RADIUS_VALUES, THEME_SCALE_VALUES, + THEME_TEXT_SIZE_VALUES, type ThemeBadgeSize, type ThemeCustomization, type ThemeFont, type ThemePreset, type ThemeRadius, type ThemeScale, + type ThemeTextSize, } from '@/lib/theme-customization' const COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year @@ -74,6 +76,7 @@ type ThemeCustomizationContextType = { setFont: (font: ThemeFont) => void setRadius: (radius: ThemeRadius) => void setScale: (scale: ThemeScale) => void + setTextSize: (textSize: ThemeTextSize) => void setBadgeSize: (badgeSize: ThemeBadgeSize) => void setContentLayout: (contentLayout: ContentLayout) => void resetCustomization: () => void @@ -90,6 +93,7 @@ const FALLBACK_CONTEXT: ThemeCustomizationContextType = { setFont: () => {}, setRadius: () => {}, setScale: () => {}, + setTextSize: () => {}, setBadgeSize: () => {}, setContentLayout: () => {}, resetCustomization: () => {}, @@ -129,6 +133,13 @@ export function ThemeCustomizationProvider(props: { DEFAULT_THEME_CUSTOMIZATION.scale ) ) + const [textSize, _setTextSize] = useState(() => + readCookie( + THEME_COOKIE_KEYS.textSize, + THEME_TEXT_SIZE_VALUES, + DEFAULT_THEME_CUSTOMIZATION.textSize + ) + ) const [badgeSize, _setBadgeSize] = useState(() => readCookie( THEME_COOKIE_KEYS.badgeSize, @@ -179,6 +190,13 @@ export function ThemeCustomizationProvider(props: { ) }, [scale]) + useLayoutEffect(() => { + applyAttribute( + 'data-theme-text', + textSize === DEFAULT_THEME_CUSTOMIZATION.textSize ? null : textSize + ) + }, [textSize]) + // Unlike the other axes, the *site default* for badge size is `lg`, which // is a styled value — so the attribute is keyed on the literal unstyled // `default` tier rather than DEFAULT_THEME_CUSTOMIZATION (removing the @@ -230,6 +248,15 @@ export function ThemeCustomizationProvider(props: { } }, []) + const setTextSize = useCallback((value: ThemeTextSize) => { + _setTextSize(value) + if (value === DEFAULT_THEME_CUSTOMIZATION.textSize) { + removeCookie(THEME_COOKIE_KEYS.textSize) + } else { + setCookie(THEME_COOKIE_KEYS.textSize, value, COOKIE_MAX_AGE) + } + }, []) + const setBadgeSize = useCallback((value: ThemeBadgeSize) => { _setBadgeSize(value) if (value === DEFAULT_THEME_CUSTOMIZATION.badgeSize) { @@ -253,18 +280,36 @@ export function ThemeCustomizationProvider(props: { setFont(DEFAULT_THEME_CUSTOMIZATION.font) setRadius(DEFAULT_THEME_CUSTOMIZATION.radius) setScale(DEFAULT_THEME_CUSTOMIZATION.scale) + setTextSize(DEFAULT_THEME_CUSTOMIZATION.textSize) setBadgeSize(DEFAULT_THEME_CUSTOMIZATION.badgeSize) setContentLayout(DEFAULT_THEME_CUSTOMIZATION.contentLayout) - }, [setPreset, setFont, setRadius, setScale, setBadgeSize, setContentLayout]) + }, [ + setPreset, + setFont, + setRadius, + setScale, + setTextSize, + setBadgeSize, + setContentLayout, + ]) const value = useMemo( () => ({ defaults: DEFAULT_THEME_CUSTOMIZATION, - customization: { preset, font, radius, scale, badgeSize, contentLayout }, + customization: { + preset, + font, + radius, + scale, + textSize, + badgeSize, + contentLayout, + }, setPreset, setFont, setRadius, setScale, + setTextSize, setBadgeSize, setContentLayout, resetCustomization, @@ -274,12 +319,14 @@ export function ThemeCustomizationProvider(props: { font, radius, scale, + textSize, badgeSize, contentLayout, setPreset, setFont, setRadius, setScale, + setTextSize, setBadgeSize, setContentLayout, resetCustomization, diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 18231bbb55..5ba2469325 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -1776,6 +1776,7 @@ "External operations mode": "External operations mode", "External Speed Test": "External Speed Test", "Extra": "Extra", + "Extra Large": "Extra Large", "Extra Notes (Optional)": "Extra Notes (Optional)", "Extra visible": "Extra visible", "Extra visible to {{group}}": "Extra visible to {{group}}", @@ -2056,6 +2057,7 @@ "Forward requests directly to upstream providers without any post-processing.": "Forward requests directly to upstream providers without any post-processing.", "Frames per second": "Frames per second", "Free": "Free", + "free for commercial use": "free for commercial use", "Free: {{free}} / Total: {{total}}": "Free: {{free}} / Total: {{total}}", "Frequency Penalty": "Frequency Penalty", "Friendly name to identify this channel": "Friendly name to identify this channel", @@ -2417,6 +2419,7 @@ "Language preference saved": "Language preference saved", "Language Preferences": "Language Preferences", "Language preferences sync across your signed-in devices and affect API error messages.": "Language preferences sync across your signed-in devices and affect API error messages.", + "Large": "Large", "Last 24h usage": "Last 24h usage", "Last 30 days uptime": "Last 30 days uptime", "Last check time": "Last check time", @@ -4081,6 +4084,7 @@ "Select sync channels to compare prices": "Select sync channels to compare prices", "Select sync channels to compare ratios": "Select sync channels to compare ratios", "Select Sync Source": "Select Sync Source", + "Select text size": "Select text size", "Select the API endpoint region": "Select the API endpoint region", "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.": "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.", "Select theme preference": "Select theme preference", @@ -4201,6 +4205,7 @@ "Slug can only contain letters, numbers, hyphens, and underscores": "Slug can only contain letters, numbers, hyphens, and underscores", "Slug is required": "Slug is required", "Slug must be less than 100 characters": "Slug must be less than 100 characters", + "Small": "Small", "Smallest USD amount users can recharge (Epay)": "Smallest USD amount users can recharge (Epay)", "SMTP Email": "SMTP Email", "SMTP encryption": "SMTP encryption", @@ -4445,6 +4450,7 @@ "Text Input": "Text Input", "Text or array of texts to embed": "Text or array of texts to embed", "Text Output": "Text Output", + "Text size": "Text size", "Text to Video": "Text to Video", "Text tokens": "Text tokens", "The admin configured three groups and one special ratio rule:": "The admin configured three groups and one special ratio rule:", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 02171bf441..bccf27b513 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -1776,6 +1776,7 @@ "External operations mode": "Mode opérations externes", "External Speed Test": "Test de vitesse externe", "Extra": "Supplémentaire", + "Extra Large": "Extra grand", "Extra Notes (Optional)": "Notes supplémentaires (facultatif)", "Extra visible": "Visible en plus", "Extra visible to {{group}}": "Visible en plus pour {{group}}", @@ -2056,6 +2057,7 @@ "Forward requests directly to upstream providers without any post-processing.": "Transférer les requêtes directement aux fournisseurs amont sans aucun post-traitement.", "Frames per second": "Images par seconde", "Free": "Libre", + "free for commercial use": "usage commercial autorisé", "Free: {{free}} / Total: {{total}}": "Disponible : {{free}} / Total : {{total}}", "Frequency Penalty": "Pénalité de fréquence", "Friendly name to identify this channel": "Nom convivial pour identifier ce canal", @@ -2417,6 +2419,7 @@ "Language preference saved": "Préférence de langue enregistrée", "Language Preferences": "Préférences de langue", "Language preferences sync across your signed-in devices and affect API error messages.": "Les préférences de langue se synchronisent sur vos appareils connectés et affectent les messages d'erreur de l'API.", + "Large": "Grand", "Last 24h usage": "Utilisation 24h", "Last 30 days uptime": "Disponibilité 30 derniers jours", "Last check time": "Dernière vérification", @@ -4081,6 +4084,7 @@ "Select sync channels to compare prices": "Sélectionner les canaux de synchronisation pour comparer les prix", "Select sync channels to compare ratios": "Sélectionner les canaux de synchronisation pour comparer les ratios", "Select Sync Source": "Sélectionner la source de synchronisation", + "Select text size": "Sélectionner la taille du texte", "Select the API endpoint region": "Sélectionner la région du point de terminaison API", "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.": "Sélectionnez les champs que vous souhaitez écraser avec les données en amont. Les champs non sélectionnés conservent leurs valeurs locales.", "Select theme preference": "Sélectionner la préférence de thème", @@ -4201,6 +4205,7 @@ "Slug can only contain letters, numbers, hyphens, and underscores": "Le slug ne peut contenir que des lettres, des chiffres, des tirets et des underscores", "Slug is required": "Le slug est requis", "Slug must be less than 100 characters": "Le slug doit contenir moins de 100 caractères", + "Small": "Petit", "Smallest USD amount users can recharge (Epay)": "Montant minimum en USD que les utilisateurs peuvent recharger (Epay)", "SMTP Email": "E-mail SMTP", "SMTP encryption": "Chiffrement SMTP", @@ -4445,6 +4450,7 @@ "Text Input": "Entrée texte", "Text or array of texts to embed": "Texte ou tableau de textes à vectoriser", "Text Output": "Sortie texte", + "Text size": "Taille du texte", "Text to Video": "Texte vers vidéo", "Text tokens": "Jetons texte", "The admin configured three groups and one special ratio rule:": "L’administrateur a configuré trois groupes et une règle de taux spécial :", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 6a7f567547..6f7ceac945 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -1776,6 +1776,7 @@ "External operations mode": "外部運用モード", "External Speed Test": "外部スピードテスト", "Extra": "追加", + "Extra Large": "特大", "Extra Notes (Optional)": "追加のメモ (オプション)", "Extra visible": "追加表示", "Extra visible to {{group}}": "{{group}} に追加表示", @@ -2056,6 +2057,7 @@ "Forward requests directly to upstream providers without any post-processing.": "ポストプロセスなしで、リクエストをアップストリームプロバイダーに直接転送します。", "Frames per second": "フレームレート", "Free": "空き", + "free for commercial use": "商用利用可", "Free: {{free}} / Total: {{total}}": "空き容量: {{free}} / 合計: {{total}}", "Frequency Penalty": "頻度ペナルティ", "Friendly name to identify this channel": "このチャネルを識別するための表示名", @@ -2417,6 +2419,7 @@ "Language preference saved": "言語設定を保存しました", "Language Preferences": "言語設定", "Language preferences sync across your signed-in devices and affect API error messages.": "言語設定はログイン中のすべてのデバイスで同期され、API のエラーメッセージ言語にも反映されます。", + "Large": "大", "Last 24h usage": "直近24時間の使用量", "Last 30 days uptime": "直近 30 日の稼働率", "Last check time": "最終チェック時刻", @@ -4081,6 +4084,7 @@ "Select sync channels to compare prices": "価格比較のために同期チャネルを選択してください", "Select sync channels to compare ratios": "比率を比較するために同期チャネルを選択", "Select Sync Source": "同期元を選択", + "Select text size": "文字サイズを選択", "Select the API endpoint region": "APIエンドポイントのリージョンを選択", "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.": "アップストリームデータで上書きしたいフィールドを選択してください。選択されていないフィールドはローカル値を保持します。", "Select theme preference": "テーマの好みを選択", @@ -4201,6 +4205,7 @@ "Slug can only contain letters, numbers, hyphens, and underscores": "スラッグには英数字、ハイフン、アンダースコアのみ使用できます", "Slug is required": "スラッグは必須です", "Slug must be less than 100 characters": "スラッグは100文字以内にしてください", + "Small": "小", "Smallest USD amount users can recharge (Epay)": "ユーザーがチャージできる最小USD金額 (Epay)", "SMTP Email": "SMTPメール", "SMTP encryption": "SMTP 暗号化方式", @@ -4445,6 +4450,7 @@ "Text Input": "テキスト入力", "Text or array of texts to embed": "ベクトル化するテキストまたは配列", "Text Output": "テキスト出力", + "Text size": "文字サイズ", "Text to Video": "テキストから動画", "Text tokens": "テキストトークン", "The admin configured three groups and one special ratio rule:": "管理者は3つのグループと1つの特別倍率ルールを設定しました:", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 1c7a95aad3..2f15b15f18 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -1776,6 +1776,7 @@ "External operations mode": "Режим внешних операций", "External Speed Test": "Внешний тест скорости", "Extra": "Дополнительно", + "Extra Large": "Очень крупный", "Extra Notes (Optional)": "Дополнительные примечания (необязательно)", "Extra visible": "Дополнительно видимая", "Extra visible to {{group}}": "Дополнительно видима для {{group}}", @@ -2056,6 +2057,7 @@ "Forward requests directly to upstream providers without any post-processing.": "Перенаправлять запросы напрямую upstream-провайдерам без какой-либо постобработки.", "Frames per second": "Кадров в секунду", "Free": "Свободно", + "free for commercial use": "коммерческое использование разрешено", "Free: {{free}} / Total: {{total}}": "Свободно: {{free}} / Всего: {{total}}", "Frequency Penalty": "Штраф за частоту", "Friendly name to identify this channel": "Дружественное имя для идентификации этого канала", @@ -2417,6 +2419,7 @@ "Language preference saved": "Языковая настройка сохранена", "Language Preferences": "Языковые настройки", "Language preferences sync across your signed-in devices and affect API error messages.": "Языковые настройки синхронизируются на всех ваших устройствах после входа и влияют на язык сообщений об ошибках API.", + "Large": "Крупный", "Last 24h usage": "Расход за 24ч", "Last 30 days uptime": "Доступность за 30 дней", "Last check time": "Время последней проверки", @@ -4081,6 +4084,7 @@ "Select sync channels to compare prices": "Выберите каналы синхронизации для сравнения цен", "Select sync channels to compare ratios": "Выбрать каналы синхронизации для сравнения соотношений", "Select Sync Source": "Выбрать источник синхронизации", + "Select text size": "Выберите размер текста", "Select the API endpoint region": "Выбрать регион конечной точки API", "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.": "Выберите поля, которые вы хотите перезаписать данными из вышестоящего источника. Невыбранные поля сохранят свои локальные значения.", "Select theme preference": "Выбрать предпочтение темы", @@ -4201,6 +4205,7 @@ "Slug can only contain letters, numbers, hyphens, and underscores": "Slug может содержать только буквы, цифры, дефисы и подчёркивания", "Slug is required": "Slug обязателен", "Slug must be less than 100 characters": "Slug должен содержать менее 100 символов", + "Small": "Мелкий", "Smallest USD amount users can recharge (Epay)": "Минимальная сумма в USD, которую пользователи могут пополнить (Epay)", "SMTP Email": "Электронная почта SMTP", "SMTP encryption": "Шифрование SMTP", @@ -4445,6 +4450,7 @@ "Text Input": "Текстовый вход", "Text or array of texts to embed": "Текст или массив текстов для векторизации", "Text Output": "Текстовый выход", + "Text size": "Размер текста", "Text to Video": "Текст в видео", "Text tokens": "Текстовые токены", "The admin configured three groups and one special ratio rule:": "Администратор настроил три группы и одно правило особого коэффициента:", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 4e9aa6d21a..48376377f0 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -1776,6 +1776,7 @@ "External operations mode": "Chế độ vận hành bên ngoài", "External Speed Test": "Kiểm tra tốc độ bên ngoài", "Extra": "Thêm", + "Extra Large": "Cực lớn", "Extra Notes (Optional)": "Ghi chú bổ sung (Tùy chọn)", "Extra visible": "Hiển thị thêm", "Extra visible to {{group}}": "Hiển thị thêm cho {{group}}", @@ -2056,6 +2057,7 @@ "Forward requests directly to upstream providers without any post-processing.": "Chuyển tiếp các yêu cầu trực tiếp đến các nhà cung cấp ngược dòng mà không cần xử lý hậu kỳ nào.", "Frames per second": "Khung hình / giây", "Free": "Trống", + "free for commercial use": "được phép dùng thương mại", "Free: {{free}} / Total: {{total}}": "Còn trống: {{free}} / Tổng: {{total}}", "Frequency Penalty": "Phạt tần suất", "Friendly name to identify this channel": "Tên thân thiện để nhận dạng kênh này", @@ -2417,6 +2419,7 @@ "Language preference saved": "Đã lưu tùy chọn ngôn ngữ", "Language Preferences": "Tùy chọn ngôn ngữ", "Language preferences sync across your signed-in devices and affect API error messages.": "Tùy chọn ngôn ngữ sẽ đồng bộ trên các thiết bị đã đăng nhập và ảnh hưởng đến ngôn ngữ thông báo lỗi API.", + "Large": "Lớn", "Last 24h usage": "Sử dụng 24h qua", "Last 30 days uptime": "Uptime 30 ngày qua", "Last check time": "Thời gian kiểm tra gần nhất", @@ -4081,6 +4084,7 @@ "Select sync channels to compare prices": "Chọn kênh đồng bộ để so sánh giá", "Select sync channels to compare ratios": "Chọn kênh đồng bộ để so sánh tỷ lệ", "Select Sync Source": "Chọn Nguồn Đồng Bộ", + "Select text size": "Chọn cỡ chữ", "Select the API endpoint region": "Chọn khu vực điểm cuối API", "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.": "Chọn các trường bạn muốn ghi đè bằng dữ liệu thượng nguồn. Các trường không được chọn sẽ giữ nguyên giá trị cục bộ của chúng.", "Select theme preference": "Chọn chủ đề ưu tiên", @@ -4201,6 +4205,7 @@ "Slug can only contain letters, numbers, hyphens, and underscores": "Slug chỉ có thể chứa chữ cái, số, dấu gạch ngang và dấu gạch dưới", "Slug is required": "Slug là bắt buộc", "Slug must be less than 100 characters": "Slug phải ít hơn 100 ký tự", + "Small": "Nhỏ", "Smallest USD amount users can recharge (Epay)": "Số tiền USD tối thiểu người dùng có thể nạp (Epay)", "SMTP Email": "Email SMTP", "SMTP encryption": "Mã hóa SMTP", @@ -4445,6 +4450,7 @@ "Text Input": "Đầu vào văn bản", "Text or array of texts to embed": "Văn bản hoặc mảng văn bản cần vector hoá", "Text Output": "Đầu ra văn bản", + "Text size": "Cỡ chữ", "Text to Video": "Văn bản sang video", "Text tokens": "Token văn bản", "The admin configured three groups and one special ratio rule:": "Quản trị viên đã cấu hình ba nhóm và một quy tắc hệ số đặc biệt:", diff --git a/web/default/src/i18n/locales/zh-TW.json b/web/default/src/i18n/locales/zh-TW.json index 0a1c72794c..9b67c3200e 100644 --- a/web/default/src/i18n/locales/zh-TW.json +++ b/web/default/src/i18n/locales/zh-TW.json @@ -1776,6 +1776,7 @@ "External operations mode": "對外運營模式", "External Speed Test": "外部速度測試", "Extra": "額外", + "Extra Large": "特大", "Extra Notes (Optional)": "額外備註(可選)", "Extra visible": "額外可見", "Extra visible to {{group}}": "對 {{group}} 額外可見", @@ -2056,6 +2057,7 @@ "Forward requests directly to upstream providers without any post-processing.": "將請求直接轉發給上游供應商,不進行任何後處理。", "Frames per second": "幀率", "Free": "可用", + "free for commercial use": "允許商用", "Free: {{free}} / Total: {{total}}": "可用空間: {{free}} / 總空間: {{total}}", "Frequency Penalty": "頻率懲罰", "Friendly name to identify this channel": "用於識別此渠道的友好名稱", @@ -2417,6 +2419,7 @@ "Language preference saved": "語言偏好已儲存", "Language Preferences": "語言偏好", "Language preferences sync across your signed-in devices and affect API error messages.": "語言偏好會同步到您登入的所有設備,並影響 API 錯誤訊息語言。", + "Large": "大", "Last 24h usage": "近 24 小時消耗", "Last 30 days uptime": "近 30 天可用率", "Last check time": "上次檢測時間", @@ -4081,6 +4084,7 @@ "Select sync channels to compare prices": "選擇同步渠道以對比價格", "Select sync channels to compare ratios": "選擇同步渠道以比較比率", "Select Sync Source": "選擇同步源", + "Select text size": "選擇文字大小", "Select the API endpoint region": "選擇 API 終端節點區域", "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.": "選擇要使用上游數據覆蓋的欄位。未選擇的欄位將保留其本地值。", "Select theme preference": "選擇主題偏好", @@ -4201,6 +4205,7 @@ "Slug can only contain letters, numbers, hyphens, and underscores": "Slug 只能包含字母、數字、連字符和底線", "Slug is required": "Slug 不能為空", "Slug must be less than 100 characters": "Slug 不能超過 100 個字元", + "Small": "小", "Smallest USD amount users can recharge (Epay)": "用戶可以儲值的最小美元金額 (Epay)", "SMTP Email": "SMTP 電郵", "SMTP encryption": "SMTP 加密方式", @@ -4445,6 +4450,7 @@ "Text Input": "文字輸入", "Text or array of texts to embed": "需要向量化的文字或文字陣列", "Text Output": "文字輸出", + "Text size": "文字大小", "Text to Video": "文生影片", "Text tokens": "文字 Token", "The admin configured three groups and one special ratio rule:": "管理員設定了三個分組和一條特殊倍率規則:", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index f73e536e56..be9a5cb62b 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -1776,6 +1776,7 @@ "External operations mode": "对外运营模式", "External Speed Test": "外部速度测试", "Extra": "额外", + "Extra Large": "特大", "Extra Notes (Optional)": "额外备注(可选)", "Extra visible": "额外可见", "Extra visible to {{group}}": "对 {{group}} 额外可见", @@ -2056,6 +2057,7 @@ "Forward requests directly to upstream providers without any post-processing.": "将请求直接转发给上游提供商,不进行任何后处理。", "Frames per second": "帧率", "Free": "可用", + "free for commercial use": "允许商用", "Free: {{free}} / Total: {{total}}": "可用空间: {{free}} / 总空间: {{total}}", "Frequency Penalty": "频率惩罚", "Friendly name to identify this channel": "用于识别此渠道的友好名称", @@ -2417,6 +2419,7 @@ "Language preference saved": "语言偏好已保存", "Language Preferences": "语言偏好", "Language preferences sync across your signed-in devices and affect API error messages.": "语言偏好会同步到您登录的所有设备,并影响 API 错误消息语言。", + "Large": "大", "Last 24h usage": "近 24 小时消耗", "Last 30 days uptime": "近 30 天可用率", "Last check time": "上次检测时间", @@ -4081,6 +4084,7 @@ "Select sync channels to compare prices": "选择同步渠道以对比价格", "Select sync channels to compare ratios": "选择同步渠道以比较比率", "Select Sync Source": "选择同步源", + "Select text size": "选择文字大小", "Select the API endpoint region": "选择 API 终端节点区域", "Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.": "选择要使用上游数据覆盖的字段。未选择的字段将保留其本地值。", "Select theme preference": "选择主题偏好", @@ -4201,6 +4205,7 @@ "Slug can only contain letters, numbers, hyphens, and underscores": "Slug 只能包含字母、数字、连字符和下划线", "Slug is required": "Slug 不能为空", "Slug must be less than 100 characters": "Slug 不能超过 100 个字符", + "Small": "小", "Smallest USD amount users can recharge (Epay)": "用户可以充值的最小美元金额 (Epay)", "SMTP Email": "SMTP 邮箱", "SMTP encryption": "SMTP 加密方式", @@ -4445,6 +4450,7 @@ "Text Input": "文字输入", "Text or array of texts to embed": "需要向量化的文本或文本数组", "Text Output": "文字输出", + "Text size": "文字大小", "Text to Video": "文生视频", "Text tokens": "文本 Token", "The admin configured three groups and one special ratio rule:": "管理员配置了三个分组和一条特殊倍率规则:", diff --git a/web/default/src/lib/theme-customization.ts b/web/default/src/lib/theme-customization.ts index 9c909fb0c1..a3b6f227de 100644 --- a/web/default/src/lib/theme-customization.ts +++ b/web/default/src/lib/theme-customization.ts @@ -83,6 +83,13 @@ export type ThemePreset = (typeof THEME_PRESETS)[number]['value'] export type ThemeRadius = 'default' | 'none' | 'sm' | 'md' | 'lg' | 'xl' export type ThemeScale = 'default' | 'sm' | 'lg' | 'xl' +/** + * Text size axis. Overrides only the `--text-*` type ramp, independently of + * the density scale (which adjusts spacing and text together). `default` + * follows the active preset/density ramp; explicit tiers win over both. + */ +export type ThemeTextSize = 'default' | 'sm' | 'lg' | 'xl' | '2xl' + /** * Badge size axis. Controls how "chubby" badges/pills (Badge, StatusBadge) * read, independently of the global density scale. `default` is the compact @@ -119,6 +126,7 @@ export type ThemeCustomization = { font: ThemeFont radius: ThemeRadius scale: ThemeScale + textSize: ThemeTextSize badgeSize: ThemeBadgeSize contentLayout: ContentLayout } @@ -128,6 +136,7 @@ export const DEFAULT_THEME_CUSTOMIZATION: ThemeCustomization = { font: 'default', radius: 'default', scale: 'default', + textSize: 'default', badgeSize: 'lg', contentLayout: 'full', } @@ -159,6 +168,14 @@ export const THEME_SCALE_VALUES: ReadonlySet = new Set([ 'xl', ]) +export const THEME_TEXT_SIZE_VALUES: ReadonlySet = new Set([ + 'default', + 'sm', + 'lg', + 'xl', + '2xl', +]) + export const THEME_BADGE_SIZE_VALUES: ReadonlySet = new Set([ 'default', 'lg', @@ -175,6 +192,7 @@ export const THEME_COOKIE_KEYS = { font: 'theme_font', radius: 'theme_radius', scale: 'theme_scale', + textSize: 'theme_text_size', badgeSize: 'theme_badge_size', contentLayout: 'theme_content_layout', } as const diff --git a/web/default/src/styles/theme-presets.css b/web/default/src/styles/theme-presets.css index c44b004a56..5d9d7fd850 100644 --- a/web/default/src/styles/theme-presets.css +++ b/web/default/src/styles/theme-presets.css @@ -728,6 +728,50 @@ For commercial licensing, please contact support@quantumnous.com --spacing: 0.3rem; } +/* ── Text size ────────────────────────────────────────────────────────── */ +/* Dedicated type-ramp axis (`data-theme-text` on ). Unlike the density + * scale above — which moves text and spacing together — this only overrides + * the `--text-*` ramp, so it composes with any density/preset choice. It is + * placed AFTER the preset and density blocks on purpose: an explicit text + * size wins over the ramps those ship (e.g. simple-large, scale sm/lg/xl). + * `default` removes the attribute and defers to the preset/density ramp. */ +[data-theme-text='sm'] { + --text-xs: 0.7rem; + --text-sm: 0.78rem; + --text-base: 0.88rem; + --text-lg: 1rem; + --text-xl: 1.13rem; + --text-2xl: 1.38rem; + --text-3xl: 1.7rem; +} +[data-theme-text='lg'] { + --text-xs: 0.84rem; + --text-sm: 0.95rem; + --text-base: 1.075rem; + --text-lg: 1.2rem; + --text-xl: 1.35rem; + --text-2xl: 1.65rem; + --text-3xl: 2rem; +} +[data-theme-text='xl'] { + --text-xs: 0.9rem; + --text-sm: 1rem; + --text-base: 1.125rem; + --text-lg: 1.25rem; + --text-xl: 1.45rem; + --text-2xl: 1.75rem; + --text-3xl: 2.15rem; +} +[data-theme-text='2xl'] { + --text-xs: 0.96rem; + --text-sm: 1.08rem; + --text-base: 1.21rem; + --text-lg: 1.35rem; + --text-xl: 1.55rem; + --text-2xl: 1.9rem; + --text-3xl: 2.3rem; +} + /* ── Badge size ───────────────────────────────────────────────────────── */ /* Independent axis for how "chubby" badges/pills read (`data-theme-badge` * on , driven by ThemeCustomizationProvider). The redesign moved @@ -747,7 +791,8 @@ For commercial licensing, please contact support@quantumnous.com border-radius: calc(infinity * 1px); } [data-theme-badge='lg'] [data-slot='badge'], -[data-theme-badge='lg'] [data-slot='status-badge']:not([data-appearance='plain']) { +[data-theme-badge='lg'] + [data-slot='status-badge']:not([data-appearance='plain']) { height: calc(var(--spacing) * 6); gap: calc(var(--spacing) * 1.5); padding-inline: calc(var(--spacing) * 2); @@ -758,7 +803,8 @@ For commercial licensing, please contact support@quantumnous.com border-radius: calc(infinity * 1px); } [data-theme-badge='xl'] [data-slot='badge'], -[data-theme-badge='xl'] [data-slot='status-badge']:not([data-appearance='plain']) { +[data-theme-badge='xl'] + [data-slot='status-badge']:not([data-appearance='plain']) { height: calc(var(--spacing) * 7); gap: calc(var(--spacing) * 1.5); padding-inline: calc(var(--spacing) * 2.5);