From 384928c49a9881fba4374e38afb654d06603b5fe Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 12 Jul 2026 18:28:07 +0800 Subject: [PATCH] feat(web): add badge size theme axis and polish mobile log cards and skeletons - New data-theme-badge axis (default/lg/xl, site default lg) with config drawer section, cookie persistence, and unlayered CSS overrides; unpin h-5! on model badge so the setting takes effect - Right-align and unify type ramp of compound cells inside mobile log card rows via in-data-[slot=data-table-card-value] variants - Fix skeleton overflow on narrow screens at call sites (min-w-0, shrink-0, max-w-full) and mirror real toolbar/stat layouts - Show theme settings trigger on mobile header --- web/default/src/components/config-drawer.tsx | 103 ++++++++++++++++-- .../layout/components/app-header.tsx | 68 ++++++------ .../context/theme-customization-provider.tsx | 39 ++++++- .../pricing/components/loading-skeleton.tsx | 7 +- .../components/checkin-calendar-card.tsx | 10 +- .../profile/components/profile-header.tsx | 14 ++- .../columns/common-logs-columns.tsx | 16 +-- .../columns/drawing-logs-columns.tsx | 6 +- .../components/columns/task-logs-columns.tsx | 8 +- .../components/common-logs-filter-bar.tsx | 7 +- .../components/common-logs-stats.tsx | 11 +- .../usage-logs/components/model-badge.tsx | 7 +- .../components/timing-metrics-cell.tsx | 7 +- web/default/src/i18n/locales/en.json | 2 + web/default/src/i18n/locales/fr.json | 2 + web/default/src/i18n/locales/ja.json | 2 + web/default/src/i18n/locales/ru.json | 2 + web/default/src/i18n/locales/vi.json | 2 + web/default/src/i18n/locales/zh-TW.json | 2 + web/default/src/i18n/locales/zh.json | 2 + web/default/src/lib/theme-customization.ts | 17 +++ web/default/src/styles/theme-presets.css | 36 ++++++ 22 files changed, 287 insertions(+), 83 deletions(-) diff --git a/web/default/src/components/config-drawer.tsx b/web/default/src/components/config-drawer.tsx index d37f1cf0e3..71bc5f1cb3 100644 --- a/web/default/src/components/config-drawer.tsx +++ b/web/default/src/components/config-drawer.tsx @@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import { Radio as RadioPrimitive } from '@base-ui/react/radio' import { RadioGroup as Radio } from '@base-ui/react/radio-group' import { CircleCheck, Palette, RotateCcw } from 'lucide-react' -import { type SVGProps } from 'react' +import type { SVGProps } from 'react' import { useTranslation } from 'react-i18next' import { IconDir } from '@/assets/custom/icon-dir' @@ -56,6 +56,7 @@ import { useTheme } from '@/context/theme-provider' import { type ContentLayout, THEME_PRESETS, + type ThemeBadgeSize, type ThemeFont, type ThemePreset, type ThemeRadius, @@ -121,6 +122,7 @@ export function ConfigDrawer({ + {showLayoutControls && ( <> @@ -517,13 +519,15 @@ function ScalePreview(props: { rows: number; rowGap: string }) { className='absolute inset-2.5 flex flex-col justify-center' style={{ gap: props.rowGap }} > - {Array.from({ length: props.rows }).map((_, i) => ( - - ))} + {Array.from({ length: props.rows }, (_, i) => `${85 - i * 10}%`).map( + (width) => ( + + ) + )} ) } @@ -589,6 +593,89 @@ function ScaleConfig() { ) } +/** + * Mock pill rendered inside the badge-size preview tiles. Each option shows + * the pill at the proportions that size will actually produce. + */ +function BadgeSizePreview(props: { height: string; fontSize: string }) { + return ( + + ) +} + +function BadgeSizeConfig() { + const { t } = useTranslation() + const { defaults, customization, setBadgeSize } = useThemeCustomization() + const badgeSizeOptions: { + value: ThemeBadgeSize + label: string + height: string + fontSize: string + }[] = [ + { value: 'default', label: t('Compact'), height: '20px', fontSize: '11px' }, + { value: 'lg', label: t('Comfortable'), height: '24px', fontSize: '12px' }, + { value: 'xl', label: t('Super Large'), height: '28px', fontSize: '13px' }, + ] + return ( +
+ setBadgeSize(defaults.badgeSize)} + /> + setBadgeSize(v as ThemeBadgeSize)} + className='grid w-full grid-cols-3 gap-3' + aria-label={t('Select badge size')} + > + {badgeSizeOptions.map((option) => ( + +
+
+
+ {option.label} +
+
+ ))} +
+
+ ) +} + function SidebarConfig() { const { t } = useTranslation() const { defaultVariant, variant, setVariant } = useLayout() diff --git a/web/default/src/components/layout/components/app-header.tsx b/web/default/src/components/layout/components/app-header.tsx index bdf244c830..31d446e070 100644 --- a/web/default/src/components/layout/components/app-header.tsx +++ b/web/default/src/components/layout/components/app-header.tsx @@ -25,7 +25,7 @@ import { useNotifications } from '@/hooks/use-notifications' import { useTopNavLinks } from '@/hooks/use-top-nav-links' import { defaultTopNavLinks } from '../config/top-nav.config' -import { type TopNavLink } from '../types' +import type { TopNavLink } from '../types' import { Header } from './header' import { SystemBrand } from './system-brand' import { TopNav } from './top-nav' @@ -111,40 +111,40 @@ export function AppHeader({ const notifications = useNotifications() return ( - <> -
- +
+ - {leftContent ? ( -
{leftContent}
- ) : null} + {leftContent ? ( +
{leftContent}
+ ) : null} - {rightContent ?? ( -
- {showTopNav && ( -
- -
- )} - {showSearch && } - {showNotifications && ( - - )} - - {showConfigDrawer && } - {showProfileDropdown && } -
- )} -
- + {rightContent ?? ( +
+ {showTopNav && ( +
+ +
+ )} + {showSearch && } + {showNotifications && ( + + )} + + {showConfigDrawer && ( + + )} + {showProfileDropdown && } +
+ )} +
) } diff --git a/web/default/src/context/theme-customization-provider.tsx b/web/default/src/context/theme-customization-provider.tsx index 8889f07d0c..167ad269e7 100644 --- a/web/default/src/context/theme-customization-provider.tsx +++ b/web/default/src/context/theme-customization-provider.tsx @@ -31,11 +31,13 @@ import { type ContentLayout, DEFAULT_THEME_CUSTOMIZATION, resolveThemeFont, + THEME_BADGE_SIZE_VALUES, THEME_COOKIE_KEYS, THEME_FONT_VALUES, THEME_PRESET_VALUES, THEME_RADIUS_VALUES, THEME_SCALE_VALUES, + type ThemeBadgeSize, type ThemeCustomization, type ThemeFont, type ThemePreset, @@ -72,6 +74,7 @@ type ThemeCustomizationContextType = { setFont: (font: ThemeFont) => void setRadius: (radius: ThemeRadius) => void setScale: (scale: ThemeScale) => void + setBadgeSize: (badgeSize: ThemeBadgeSize) => void setContentLayout: (contentLayout: ContentLayout) => void resetCustomization: () => void } @@ -87,6 +90,7 @@ const FALLBACK_CONTEXT: ThemeCustomizationContextType = { setFont: () => {}, setRadius: () => {}, setScale: () => {}, + setBadgeSize: () => {}, setContentLayout: () => {}, resetCustomization: () => {}, } @@ -125,6 +129,13 @@ export function ThemeCustomizationProvider(props: { DEFAULT_THEME_CUSTOMIZATION.scale ) ) + const [badgeSize, _setBadgeSize] = useState(() => + readCookie( + THEME_COOKIE_KEYS.badgeSize, + THEME_BADGE_SIZE_VALUES, + DEFAULT_THEME_CUSTOMIZATION.badgeSize + ) + ) const [contentLayout, _setContentLayout] = useState(() => readCookie( THEME_COOKIE_KEYS.contentLayout, @@ -168,6 +179,17 @@ export function ThemeCustomizationProvider(props: { ) }, [scale]) + // 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 + // attribute for `lg` would silently drop its CSS). + useLayoutEffect(() => { + applyAttribute( + 'data-theme-badge', + badgeSize === 'default' ? null : badgeSize + ) + }, [badgeSize]) + useLayoutEffect(() => { applyAttribute('data-theme-content-layout', contentLayout) }, [contentLayout]) @@ -208,6 +230,15 @@ export function ThemeCustomizationProvider(props: { } }, []) + const setBadgeSize = useCallback((value: ThemeBadgeSize) => { + _setBadgeSize(value) + if (value === DEFAULT_THEME_CUSTOMIZATION.badgeSize) { + removeCookie(THEME_COOKIE_KEYS.badgeSize) + } else { + setCookie(THEME_COOKIE_KEYS.badgeSize, value, COOKIE_MAX_AGE) + } + }, []) + const setContentLayout = useCallback((value: ContentLayout) => { _setContentLayout(value) if (value === DEFAULT_THEME_CUSTOMIZATION.contentLayout) { @@ -222,17 +253,19 @@ export function ThemeCustomizationProvider(props: { setFont(DEFAULT_THEME_CUSTOMIZATION.font) setRadius(DEFAULT_THEME_CUSTOMIZATION.radius) setScale(DEFAULT_THEME_CUSTOMIZATION.scale) + setBadgeSize(DEFAULT_THEME_CUSTOMIZATION.badgeSize) setContentLayout(DEFAULT_THEME_CUSTOMIZATION.contentLayout) - }, [setPreset, setFont, setRadius, setScale, setContentLayout]) + }, [setPreset, setFont, setRadius, setScale, setBadgeSize, setContentLayout]) const value = useMemo( () => ({ defaults: DEFAULT_THEME_CUSTOMIZATION, - customization: { preset, font, radius, scale, contentLayout }, + customization: { preset, font, radius, scale, badgeSize, contentLayout }, setPreset, setFont, setRadius, setScale, + setBadgeSize, setContentLayout, resetCustomization, }), @@ -241,11 +274,13 @@ export function ThemeCustomizationProvider(props: { font, radius, scale, + badgeSize, contentLayout, setPreset, setFont, setRadius, setScale, + setBadgeSize, setContentLayout, resetCustomization, ] diff --git a/web/default/src/features/pricing/components/loading-skeleton.tsx b/web/default/src/features/pricing/components/loading-skeleton.tsx index 1702ee2d60..796c896904 100644 --- a/web/default/src/features/pricing/components/loading-skeleton.tsx +++ b/web/default/src/features/pricing/components/loading-skeleton.tsx @@ -81,8 +81,9 @@ function CardContentSkeleton() { function FilterBarSkeleton() { return (
-
-
+ {/* Mirrors PricingToolbar: stacked on mobile, single row from lg. */} +
+
{[80, 90, 75, 85, 70].map((width) => ( ))}
-
+
diff --git a/web/default/src/features/profile/components/checkin-calendar-card.tsx b/web/default/src/features/profile/components/checkin-calendar-card.tsx index 2f7a31fbf4..0024b478b5 100644 --- a/web/default/src/features/profile/components/checkin-calendar-card.tsx +++ b/web/default/src/features/profile/components/checkin-calendar-card.tsx @@ -228,11 +228,13 @@ export function CheckinCalendarCard({
-
- - + {/* min-w-0 + max-w-full let the fixed-width lines clamp to the + card on narrow screens instead of overflowing. */} +
+ +
- +
diff --git a/web/default/src/features/profile/components/profile-header.tsx b/web/default/src/features/profile/components/profile-header.tsx index 0fb76c20ab..f146ba6f31 100644 --- a/web/default/src/features/profile/components/profile-header.tsx +++ b/web/default/src/features/profile/components/profile-header.tsx @@ -42,10 +42,12 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
- -
- - + + {/* min-w-0 + max-w-full let the fixed-width lines clamp to the + card on narrow screens instead of overflowing. */} +
+ +
@@ -53,8 +55,8 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
{['balance', 'usage', 'requests'].map((key) => (
- - + +
))}
diff --git a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx index 87d109d721..346a9802ce 100644 --- a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -294,8 +294,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { const config = getLogTypeConfig(log.type) return ( -
- + // In mobile cards this cell sits in a label-left/value-right row + // (data-table-card-value); align both lines to the right edge there. +
+ {formatTimestampToDate(timestamp)} @@ -369,7 +371,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { +
} >
@@ -516,7 +518,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { + } > {sensitiveVisible ? log.username : '••••'} @@ -728,7 +730,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { return (
- + {promptTokens.toLocaleString()} /{' '} {completionTokens.toLocaleString()} @@ -851,13 +853,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { <>