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) => (
+ -
+
+
+
+ Aa
+
+
+
+ {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);