Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"@clickhouse/click-ui": "0.15.0",
"@fontsource-variable/vazirmatn": "^5.3.0",
"@librechat/data-schemas": "^0.0.74",
"@radix-ui/react-dialog": "1.3.1",
"@tailwindcss/vite": "^4.3.3",
Expand Down
21 changes: 20 additions & 1 deletion src/components/CommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { Title as DialogTitle, Description as DialogDescription } from '@radix-ui/react-dialog';
import type * as t from '@/types';
import { matchesQuery, searchConfigIndex } from './configuration/search';
import { useSearchIndex, useLocalize, useReturnFocus } from '@/hooks';
import { useSearchIndex, useLocalize, useLanguage, useReturnFocus } from '@/hooks';
import { CONFIG_TABS } from './configuration/configMeta';
import { LANGUAGES } from '@/constants';
import { pageTitle, restoreFocusLater } from '@/utils';
import { CommandDialog } from '@/components/shared';
import { useTheme } from '@/contexts/ThemeContext';
Expand All @@ -24,6 +25,7 @@ export function CommandMenu({ open, onOpenChange }: t.CommandMenuProps) {
const localize = useLocalize();
const router = useRouter();
const { setTheme } = useTheme();
const { setLanguage } = useLanguage();
const { items: settings } = useSearchIndex(localize, open);

const [search, setSearch] = useState('');
Expand Down Expand Up @@ -111,6 +113,14 @@ export function CommandMenu({ open, onOpenChange }: t.CommandMenuProps) {
[setTheme, close],
);

const selectLanguage = useCallback(
(value: t.LanguageCode) => {
setLanguage(value);
close();
},
[setLanguage, close],
);

const navigation: t.StaticCommand[] = [
{
id: 'nav-home',
Expand Down Expand Up @@ -190,6 +200,15 @@ export function CommandMenu({ open, onOpenChange }: t.CommandMenuProps) {
keywords: ['theme', 'system', 'auto', 'mode', 'appearance'],
onSelect: () => selectTheme('system'),
},
...LANGUAGES.map(
(option): t.StaticCommand => ({
id: `language-${option.code}`,
icon: 'globe',
label: localize('com_cmdk_set_language', { language: option.nativeName }),
keywords: ['language', 'locale', 'translation', option.code, option.nativeName],
onSelect: () => selectLanguage(option.code),
}),
),
];

const query = search.trim();
Expand Down
2 changes: 1 addition & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function Header({ title, onSearchClick, children }: t.HeaderProps) {
<div />
)}
</div>
<div className="ml-auto flex max-w-full min-w-0 items-center justify-end gap-2">
<div className="ms-auto flex max-w-full min-w-0 items-center justify-end gap-2">
{children}
<div id="header-actions-portal" className="contents" />
{onSearchClick && (
Expand Down
4 changes: 2 additions & 2 deletions src/components/InputOTP.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ const InputOTPSlot = React.forwardRef<
ref={ref}
className={clsx(
'relative flex h-12 w-11 items-center justify-center',
'border-y border-r border-(--cui-color-stroke-default)',
'border-y border-e border-(--cui-color-stroke-default)',
'bg-(--cui-color-background-default) text-(--cui-color-text-default)',
'text-lg font-medium shadow-sm transition-all',
'first:rounded-l-lg first:border-l last:rounded-r-lg',
'first:rounded-s-lg first:border-s last:rounded-e-lg',
isActive
? 'z-10 border-(--cui-color-outline) ring-1 ring-(--cui-color-outline)'
: 'hover:border-(--cui-color-stroke-intense)',
Expand Down
24 changes: 24 additions & 0 deletions src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Icon } from '@clickhouse/click-ui';
import { useLocalize, useLanguage } from '@/hooks';
import { LANGUAGES } from '@/constants';

/** Switches to the next shipped language, labeled with that language's own name. */
export function LanguageSelector() {
const localize = useLocalize();
const { language, setLanguage } = useLanguage();
const index = LANGUAGES.findIndex((option) => option.code === language);
const next = LANGUAGES[(index + 1) % LANGUAGES.length];

return (
<button
type="button"
lang={next.code}
className="flex items-center gap-2 rounded-lg p-3 text-sm text-(--cui-color-text-default) hover:bg-(--cui-color-background-hover)"
aria-label={localize('com_ui_switch_language', { language: next.nativeName })}
onClick={() => setLanguage(next.code)}
>
<Icon name="globe" size="md" aria-hidden="true" />
<span>{next.nativeName}</span>
</button>
);
}
33 changes: 32 additions & 1 deletion src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Dialog } from '@clickhouse/click-ui';
import type * as t from '@/types';
import { useLocalize, useReturnFocus } from '@/hooks';
import { useLocalize, useLanguage, useReturnFocus } from '@/hooks';
import { useTheme } from '@/contexts/ThemeContext';
import { LANGUAGES } from '@/constants';
import { cn } from '@/utils';

const THEME_OPTIONS: t.ThemeOption[] = ['system', 'light', 'dark'];
Expand All @@ -15,6 +16,7 @@ export function SettingsDialog({ open, onClose }: t.SettingsDialogProps) {
const localize = useLocalize();
const returnFocus = useReturnFocus(open);
const { theme, setTheme } = useTheme();
const { language, setLanguage } = useLanguage();

return (
<Dialog
Expand Down Expand Up @@ -59,6 +61,35 @@ export function SettingsDialog({ open, onClose }: t.SettingsDialogProps) {
))}
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium text-(--cui-color-text-default)">
{localize('com_nav_language')}
</span>
<span className="text-xs text-(--cui-color-text-muted)">
{localize('com_settings_language_desc')}
</span>
</div>
<div className="flex gap-1 rounded-lg border border-(--cui-color-stroke-default) p-0.5">
{LANGUAGES.map((opt) => (
<button
key={opt.code}
type="button"
lang={opt.code}
onClick={() => setLanguage(opt.code)}
className={cn(
'cursor-pointer rounded-md px-3 py-1 text-xs font-medium transition-colors',
language === opt.code
? 'bg-(--cui-color-background-active) text-(--cui-color-text-default)'
: 'text-(--cui-color-text-muted) hover:text-(--cui-color-text-default)',
)}
aria-pressed={language === opt.code}
>
{opt.nativeName}
</button>
))}
</div>
</div>
</div>
</Dialog.Content>
</Dialog>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export function Sidebar({ user, collapsed, onToggle }: t.SidebarProps) {
<aside
aria-label={localize('com_a11y_admin_panel')}
className={cn(
'sticky top-0 z-(--z-floating) flex h-screen shrink-0 flex-col overflow-hidden border-r border-(--cui-color-stroke-default) bg-(--cui-color-background-panel) transition-[width] duration-200',
'sticky top-0 z-(--z-floating) flex h-screen shrink-0 flex-col overflow-hidden border-e border-(--cui-color-stroke-default) bg-(--cui-color-background-panel) transition-[width] duration-200',
collapsed ? 'w-14' : 'w-63',
)}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ThemeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default function ThemeSelector({ returnThemeOnly }: { returnThemeOnly?: b

return (
<div className="flex flex-col items-center justify-center sm:pt-0">
<div className="absolute bottom-0 left-0 m-4">
<div className="absolute bottom-0 inset-s-0 m-4">
<ThemeButton theme={theme} onChange={changeTheme} />
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/access/GroupsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function GroupsTab({ onCreateGroup }: t.GroupsTabProps) {
}

return (
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto py-2 pr-1">
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto py-2 pe-1">
<div className="flex items-center justify-between gap-3">
<SearchInput
inputRef={searchRef}
Expand Down Expand Up @@ -130,7 +130,7 @@ export function GroupsTab({ onCreateGroup }: t.GroupsTabProps) {
<button
type="button"
onClick={() => setEditTarget(group)}
className="-my-2 -ml-2 min-w-0 flex-1 cursor-pointer rounded py-3 pl-3 text-left outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline) focus-visible:outline-solid"
className="-my-2 -ms-2 min-w-0 flex-1 cursor-pointer rounded py-3 ps-3 text-start outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline) focus-visible:outline-solid"
>
<div className="text-sm font-medium text-(--cui-color-text-default) hover:underline">
{group.name}
Expand Down
2 changes: 1 addition & 1 deletion src/components/access/RolePermissionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function RolePermissionsPanel({
id={panelId}
className="border-t border-(--cui-color-stroke-default) px-4 py-2"
>
<div className="flex flex-col gap-1 pl-6">
<div className="flex flex-col gap-1 ps-6">
{perms.map((perm) => (
<label
key={perm}
Expand Down
4 changes: 2 additions & 2 deletions src/components/access/RolesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function RolesTab({ onCreateRole }: t.RolesTabProps) {
}

return (
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto py-2 pr-1">
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto py-2 pe-1">
<div className="flex items-center justify-between gap-3">
<SearchInput
inputRef={searchRef}
Expand Down Expand Up @@ -123,7 +123,7 @@ export function RolesTab({ onCreateRole }: t.RolesTabProps) {
<button
type="button"
onClick={() => setEditTarget(role)}
className="-my-2 -ml-2 min-w-0 flex-1 cursor-pointer rounded py-3 pl-3 text-left outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline) focus-visible:outline-solid"
className="-my-2 -ms-2 min-w-0 flex-1 cursor-pointer rounded py-3 ps-3 text-start outline-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline) focus-visible:outline-solid"
>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-(--cui-color-text-default) hover:underline">
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/ConfigLoadError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function ConfigLoadError({
);

return (
<div className="py-6 pr-4">
<div className="py-6 pe-4">
<div
ref={alertRef}
role="alert"
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ export function ConfigPage({

<div className="flex min-h-0 flex-1 overflow-hidden">
<div className="relative min-h-0 min-w-0 flex-1">
<div className="h-full scrollbar-gutter-stable overflow-auto pl-4" ref={setScrollEl}>
<div className="h-full scrollbar-gutter-stable overflow-auto ps-4" ref={setScrollEl}>
{baseline.status === 'error' && (
<ConfigLoadError error={baseline.error} onRetry={baseline.retry} />
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/ConfigSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function ConfigContentSkeleton({ rows = ROW_WIDTHS.length }: t.ConfigCont
role="status"
aria-busy="true"
aria-live="polite"
className="flex flex-col gap-3 py-4 pr-4 motion-safe:animate-pulse"
className="flex flex-col gap-3 py-4 pe-4 motion-safe:animate-pulse"
>
<span className="sr-only">{localize('com_config_shell_loading')}</span>
<div className="flex flex-col gap-5 rounded-lg border border-(--cui-color-stroke-default) p-5">
Expand Down
8 changes: 4 additions & 4 deletions src/components/configuration/ConfigTableOfContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { entryHasPath } from './toc';
import { cn } from '@/utils';

const ITEM =
'toc-item block w-full min-w-0 cursor-pointer truncate border-none bg-transparent text-left text-(--cui-color-text-muted) transition-colors hover:text-(--cui-color-text-default)';
'toc-item block w-full min-w-0 cursor-pointer truncate border-none bg-transparent text-start text-(--cui-color-text-muted) transition-colors hover:text-(--cui-color-text-default)';

function Markers({ dirty, error }: { dirty: boolean; error: boolean }) {
const localize = useLocalize();
if (!dirty && !error) return null;
return (
<span className="ml-1.5 inline-flex shrink-0 items-center gap-1 align-middle">
<span className="ms-1.5 inline-flex shrink-0 items-center gap-1 align-middle">
{error && (
<span className="inline-block size-1.5 rounded-full bg-(--cui-color-text-danger)">
<span className="sr-only">{localize('com_config_shell_toc_error')}</span>
Expand Down Expand Up @@ -77,8 +77,8 @@ export const ConfigTableOfContents = memo(function ConfigTableOfContents({
className={cn(
ITEM,
isSection
? 'toc-item-parent py-1.5 pr-1 pl-3 text-[13px] font-medium'
: 'toc-item-child py-1 pr-1 pl-6 text-xs',
? 'toc-item-parent py-1.5 pe-1 ps-3 text-[13px] font-medium'
: 'toc-item-child py-1 pe-1 ps-6 text-xs',
)}
>
{entry.label}
Expand Down
6 changes: 3 additions & 3 deletions src/components/configuration/ContentToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function ContentToolbar({
aria-expanded={filtersOpen}
aria-controls={filtersId}
onClick={() => setFiltersOpen((open) => !open)}
className="config-add-btn ml-0"
className="config-add-btn ms-0"
>
<DecorativeIcon name="filter" size="sm" />
<span>{localize('com_config_shell_filters')}</span>
Expand Down Expand Up @@ -113,7 +113,7 @@ export function ContentToolbar({
<JumpToSelect targets={jumpTargets} onJump={onJump} />
)}
</div>
<div className="ml-auto hidden items-center gap-1 md:flex">
<div className="ms-auto hidden items-center gap-1 md:flex">
<Button
htmlType="button"
type="empty"
Expand All @@ -129,7 +129,7 @@ export function ContentToolbar({
onClick={handleCollapseAll}
/>
</div>
<div className="ml-auto flex items-center gap-1 md:hidden">
<div className="ms-auto flex items-center gap-1 md:hidden">
<button
type="button"
onClick={handleExpandAll}
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/ScopeBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function ScopeBar({
return (
<section
aria-label={localize('com_scope_bar_label')}
className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border border-l-4 border-(--cui-color-stroke-default) bg-(--cui-color-background-muted) px-3 py-2"
className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border border-s-4 border-(--cui-color-stroke-default) bg-(--cui-color-background-muted) px-3 py-2"
style={{ borderLeftColor: config.color }}
>
<div className="flex min-w-0 flex-1 items-start gap-2.5">
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/ScopeOverridesDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export function ScopeOverridesDialog({
{localize('com_scope_overrides_empty')}
</p>
) : (
<div className="flex max-h-[60vh] flex-col gap-4 overflow-y-auto pr-1">
<div className="flex max-h-[60vh] flex-col gap-4 overflow-y-auto pe-1">
{onRevert && (
<p className="text-xs text-(--cui-color-text-muted)">
{localize('com_scope_overrides_revert_hint')}
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/fields/MimeListField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function MimeListField({
{editing && (
<button
type="button"
className="settings-row-action -ml-1.5"
className="settings-row-action -ms-1.5"
onClick={() => {
focusNextRef.current = 'customize';
setCustomizing(false);
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/fields/MultiEnumField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function MultiEnumField({
onCheckedChange={(next) => toggle(option.value, next === true)}
/>
{option.description && (
<span className="pl-6 text-xs text-(--cui-color-text-muted)">{option.description}</span>
<span className="ps-6 text-xs text-(--cui-color-text-muted)">{option.description}</span>
)}
</div>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/configuration/fields/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function NumberField({
hideControls
endContent={
unit ? (
<span id={unitId} className="pr-1 text-xs text-(--cui-color-text-muted)">
<span id={unitId} className="pe-1 text-xs text-(--cui-color-text-muted)">
{localize(`com_config_ctl_unit_${unit}`)}
</span>
) : undefined
Expand Down
4 changes: 2 additions & 2 deletions src/components/configuration/fields/ObjectEntryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export function ObjectEntryCard({
>
<div
className={cn(
'@container flex w-full items-center gap-1 rounded-lg pr-2 transition-colors hover:bg-(--cui-color-background-hover)',
'@container flex w-full items-center gap-1 rounded-lg pe-2 transition-colors hover:bg-(--cui-color-background-hover)',
isExpanded && 'sticky top-0 z-5 rounded-t-lg bg-(--cui-color-background-default)',
)}
>
Expand Down Expand Up @@ -280,7 +280,7 @@ export function ObjectEntryCard({
aria-expanded={isExpanded}
aria-controls={panelId}
onClick={toggle}
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-lg border-none bg-transparent px-3 py-2.5 text-left select-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline)"
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-lg border-none bg-transparent px-3 py-2.5 text-start select-none focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-(--cui-color-outline)"
>
<span
aria-hidden="true"
Expand Down
Loading