mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
feat(i18n): add RTL layout support for Arabic and other RTL locales
- Export `isLocaleRTL` from `@sofa/i18n` to detect right-to-left locales - Native: call `I18nManager.allowRTL` / `forceRTL` on locale init and on locale change; reload the app immediately if the current layout direction doesn't match the persisted locale, and prompt the user to restart when switching between LTR and RTL locales - Native: flip the `SettingsRow` chevron to `IconChevronLeft` when `I18nManager.isRTL` is true - Native: add Arabic Intl polyfill locale data (PluralRules, NumberFormat, DateTimeFormat, RelativeTimeFormat) - Web: enable `"rtl": true` in `components.json` and regenerate all shadcn UI components to use CSS logical properties (`ms-*`, `me-*`, `ps-*`, `pe-*`, `text-start`, `text-end`) instead of physical `ml-*`/`mr-*`/`text-left` equivalents - Web: apply the same logical-property conversions to non-generated components (nav-bar, settings sections, title cards, hero banner, continue-watching card) - Web: set `document.dir` based on the active locale's RTL flag in the i18n initialiser and root route - Add `../../packages/i18n/src/po.d.ts` to native `tsconfig.json` includes so TypeScript accepts `.po` imports
This commit is contained in:
@@ -64,7 +64,7 @@
|
||||
"expo-system-ui": "55.0.10",
|
||||
"expo-tracking-transparency": "55.0.9",
|
||||
"expo-web-browser": "55.0.10",
|
||||
"posthog-react-native": "4.37.4",
|
||||
"posthog-react-native": "4.37.5",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-native": "0.83.2",
|
||||
|
||||
@@ -84,34 +84,38 @@ export default function LoginScreen() {
|
||||
|
||||
return (
|
||||
<AuthScreen title="Sofa" subtitle={t`Sign in to continue`}>
|
||||
{showOidc && (
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(100)} className="mb-4">
|
||||
<Button
|
||||
onPress={() => {
|
||||
authClient.signIn.oauth2({
|
||||
providerId: "oidc",
|
||||
callbackURL: "/(tabs)/(home)",
|
||||
});
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
<ButtonLabel>
|
||||
<Trans>Sign in with {authConfig.data?.oidcProviderName ?? "SSO"}</Trans>
|
||||
</ButtonLabel>
|
||||
</Button>
|
||||
{showOidc &&
|
||||
(() => {
|
||||
const providerName = authConfig.data?.oidcProviderName ?? "SSO";
|
||||
return (
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(100)} className="mb-4">
|
||||
<Button
|
||||
onPress={() => {
|
||||
authClient.signIn.oauth2({
|
||||
providerId: "oidc",
|
||||
callbackURL: "/(tabs)/(home)",
|
||||
});
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
<ButtonLabel>
|
||||
<Trans>Sign in with {providerName}</Trans>
|
||||
</ButtonLabel>
|
||||
</Button>
|
||||
|
||||
{showPasswordLogin && (
|
||||
<View className="my-4 flex-row items-center">
|
||||
<View className="bg-border h-px flex-1" />
|
||||
<Text className="text-muted-foreground px-3 text-xs">
|
||||
<Trans>OR</Trans>
|
||||
</Text>
|
||||
<View className="bg-border h-px flex-1" />
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
)}
|
||||
{showPasswordLogin && (
|
||||
<View className="my-4 flex-row items-center">
|
||||
<View className="bg-border h-px flex-1" />
|
||||
<Text className="text-muted-foreground px-3 text-xs">
|
||||
<Trans>OR</Trans>
|
||||
</Text>
|
||||
<View className="bg-border h-px flex-1" />
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
})()}
|
||||
|
||||
{showPasswordLogin && (
|
||||
<View className="gap-3">
|
||||
|
||||
@@ -111,7 +111,10 @@ export default function DashboardScreen() {
|
||||
<View className="gap-3">
|
||||
<View className="flex-row gap-3">
|
||||
<StatsCard
|
||||
label={t`Movies ${periodLabels[moviePeriod]}`}
|
||||
label={(() => {
|
||||
const period = periodLabels[moviePeriod];
|
||||
return t`Movies ${period}`;
|
||||
})()}
|
||||
value={movieCount}
|
||||
icon={IconMovie}
|
||||
color="text-primary"
|
||||
@@ -122,7 +125,10 @@ export default function DashboardScreen() {
|
||||
onPeriodChange={setMoviePeriod}
|
||||
/>
|
||||
<StatsCard
|
||||
label={t`Episodes ${periodLabels[episodePeriod]}`}
|
||||
label={(() => {
|
||||
const period = periodLabels[episodePeriod];
|
||||
return t`Episodes ${period}`;
|
||||
})()}
|
||||
value={episodeCount}
|
||||
icon={IconDeviceTvOld}
|
||||
color="text-status-watching"
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
IconWorld,
|
||||
} from "@tabler/icons-react-native";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { reloadAppAsync } from "expo";
|
||||
import * as Application from "expo-application";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { useRouter } from "expo-router";
|
||||
@@ -54,7 +55,7 @@ import { isAnalyticsEnabled, setAnalyticsEnabled } from "@/lib/posthog";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { authClient, getServerUrl } from "@/lib/server";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { activateLocale, type SupportedLocale } from "@sofa/i18n";
|
||||
import { activateLocale, isLocaleRTL, type SupportedLocale } from "@sofa/i18n";
|
||||
import { LOCALE_INFO } from "@sofa/i18n/locales";
|
||||
|
||||
const settingsContentContainerStyle = {
|
||||
@@ -421,7 +422,10 @@ export default function SettingsScreen() {
|
||||
label={t`Database`}
|
||||
value={
|
||||
systemHealth.data?.database
|
||||
? t`${plural(systemHealth.data.database.titleCount, { one: "# title", other: "# titles" })}`
|
||||
? plural(systemHealth.data.database.titleCount, {
|
||||
one: "# title",
|
||||
other: "# titles",
|
||||
})
|
||||
: "—"
|
||||
}
|
||||
icon={IconDatabase}
|
||||
@@ -435,7 +439,10 @@ export default function SettingsScreen() {
|
||||
label={t`Image Cache`}
|
||||
value={
|
||||
systemHealth.data?.imageCache
|
||||
? t`${plural(systemHealth.data.imageCache.imageCount, { one: "# image", other: "# images" })}`
|
||||
? plural(systemHealth.data.imageCache.imageCount, {
|
||||
one: "# image",
|
||||
other: "# images",
|
||||
})
|
||||
: "—"
|
||||
}
|
||||
icon={IconPhoto}
|
||||
@@ -472,13 +479,16 @@ export default function SettingsScreen() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{updateCheck.data?.updateCheck?.updateAvailable && (
|
||||
<View className="py-3.5">
|
||||
<Text className="text-status-completed font-sans text-sm font-medium">
|
||||
<Trans>Update available: {updateCheck.data.updateCheck.latestVersion}</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{(() => {
|
||||
const latestVersion = updateCheck.data?.updateCheck?.latestVersion;
|
||||
return updateCheck.data?.updateCheck?.updateAvailable ? (
|
||||
<View className="py-3.5">
|
||||
<Text className="text-status-completed font-sans text-sm font-medium">
|
||||
<Trans>Update available: {latestVersion}</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : null;
|
||||
})()}
|
||||
</SettingsSection>
|
||||
</Animated.View>
|
||||
)}
|
||||
@@ -512,8 +522,18 @@ export default function SettingsScreen() {
|
||||
}))}
|
||||
onSelect={(locale) => {
|
||||
setLanguageModalOpen(false);
|
||||
const previousLocale = i18n.locale;
|
||||
activateLocale(locale as SupportedLocale).then(
|
||||
() => setPersistedLocale(locale as SupportedLocale),
|
||||
() => {
|
||||
setPersistedLocale(locale as SupportedLocale);
|
||||
if (isLocaleRTL(locale) !== isLocaleRTL(previousLocale)) {
|
||||
Alert.alert(
|
||||
t`Restart Required`,
|
||||
t`Sofa needs to restart to apply the new layout direction.`,
|
||||
[{ text: t`Restart`, onPress: () => reloadAppAsync() }],
|
||||
);
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Icon } from "@tabler/icons-react-native";
|
||||
import { IconChevronRight } from "@tabler/icons-react-native";
|
||||
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react-native";
|
||||
import type { ReactNode } from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { I18nManager, Pressable, View } from "react-native";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
|
||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||
@@ -53,7 +53,12 @@ export function SettingsRow({
|
||||
</Text>
|
||||
) : null}
|
||||
{!right && onPress && (
|
||||
<ScaledIcon icon={IconChevronRight} size={16} color={mutedFgColor} accessible={false} />
|
||||
<ScaledIcon
|
||||
icon={I18nManager.isRTL ? IconChevronLeft : IconChevronRight}
|
||||
size={16}
|
||||
color={mutedFgColor}
|
||||
accessible={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -99,6 +99,9 @@ export function SeasonAccordion({
|
||||
[watchedEpisodeIds, unwatchEpisode, watchEpisode],
|
||||
);
|
||||
|
||||
const seasonNumber = season.seasonNumber;
|
||||
const episodeCount = episodes.length;
|
||||
|
||||
return (
|
||||
<View
|
||||
className="bg-card mb-2 overflow-hidden rounded-xl border"
|
||||
@@ -110,16 +113,16 @@ export function SeasonAccordion({
|
||||
<Pressable
|
||||
onPress={toggleExpanded}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${season.name ?? t`Season ${season.seasonNumber}`}, ${t`${watchedCount} of ${episodes.length} episodes watched`}`}
|
||||
accessibilityLabel={`${season.name ?? t`Season ${seasonNumber}`}, ${t`${watchedCount} of ${episodeCount} episodes watched`}`}
|
||||
accessibilityState={{ expanded }}
|
||||
className="flex-row items-center justify-between p-4"
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-foreground font-sans text-base font-medium">
|
||||
{season.name ?? t`Season ${season.seasonNumber}`}
|
||||
{season.name ?? t`Season ${seasonNumber}`}
|
||||
</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-xs">
|
||||
{t`${watchedCount}/${episodes.length} ${plural(episodes.length, { one: "episode", other: "episodes" })}`}
|
||||
{t`${watchedCount}/${plural(episodeCount, { one: "# episode", other: "# episodes" })}`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -81,9 +81,10 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
const updateRating = useMutation(
|
||||
orpc.titles.updateRating.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
const stars = input.stars;
|
||||
const defaultMsg =
|
||||
input.stars > 0
|
||||
? t`Rated ${input.stars} ${plural(input.stars, { one: "star", other: "stars" })}`
|
||||
stars > 0
|
||||
? t`Rated ${plural(stars, { one: "# star", other: "# stars" })}`
|
||||
: t`Rating removed`;
|
||||
toast.success(resolveToast(toastOverrides?.updateRating, defaultMsg, input));
|
||||
// Rating only invalidates title queries, not dashboard
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { reloadAppAsync } from "expo";
|
||||
import * as Localization from "expo-localization";
|
||||
import { I18nManager } from "react-native";
|
||||
|
||||
import { activateLocale, SUPPORTED_LOCALES, type SupportedLocale } from "@sofa/i18n";
|
||||
import { activateLocale, isLocaleRTL, SUPPORTED_LOCALES, type SupportedLocale } from "@sofa/i18n";
|
||||
|
||||
import { globalStorage } from "./mmkv";
|
||||
|
||||
@@ -23,10 +25,21 @@ export function getPersistedLocale(): SupportedLocale {
|
||||
|
||||
export function setPersistedLocale(locale: SupportedLocale): void {
|
||||
globalStorage.set(LOCALE_STORAGE_KEY, locale);
|
||||
const rtl = isLocaleRTL(locale);
|
||||
I18nManager.allowRTL(rtl);
|
||||
I18nManager.forceRTL(rtl);
|
||||
}
|
||||
|
||||
export function initLocale(): Promise<void> {
|
||||
const locale = getPersistedLocale();
|
||||
const rtl = isLocaleRTL(locale);
|
||||
I18nManager.allowRTL(rtl);
|
||||
I18nManager.forceRTL(rtl);
|
||||
// forceRTL only takes effect on the next launch — if the current layout
|
||||
// doesn't match (e.g. fresh install on an RTL device), reload immediately.
|
||||
if (I18nManager.isRTL !== rtl) {
|
||||
reloadAppAsync();
|
||||
}
|
||||
if (locale !== "en") {
|
||||
return activateLocale(locale);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ import "@formatjs/intl-pluralrules/locale-data/de.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/es.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/it.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/pt.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/ar.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/nl.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/he.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/zh.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/ja.js";
|
||||
import "@formatjs/intl-pluralrules/locale-data/ko.js";
|
||||
// 4. NumberFormat (depends on PluralRules, Locale)
|
||||
import "@formatjs/intl-numberformat/polyfill.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/en.js";
|
||||
@@ -28,6 +34,12 @@ import "@formatjs/intl-numberformat/locale-data/de.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/es.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/it.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/pt.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/ar.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/nl.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/he.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/zh.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/ja.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/ko.js";
|
||||
// 5. DateTimeFormat (depends on Locale)
|
||||
import "@formatjs/intl-datetimeformat/polyfill.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/en.js";
|
||||
@@ -36,6 +48,12 @@ import "@formatjs/intl-datetimeformat/locale-data/de.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/es.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/it.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/pt.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/ar.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/nl.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/he.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/zh.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/ja.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/ko.js";
|
||||
// 6. RelativeTimeFormat (depends on PluralRules, Locale)
|
||||
import "@formatjs/intl-relativetimeformat/polyfill.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/en.js";
|
||||
@@ -44,3 +62,9 @@ import "@formatjs/intl-relativetimeformat/locale-data/de.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/es.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/it.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/pt.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/ar.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/nl.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/he.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/zh.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/ja.js";
|
||||
import "@formatjs/intl-relativetimeformat/locale-data/ko.js";
|
||||
|
||||
@@ -7,5 +7,12 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "uniwind-types.d.ts"]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts",
|
||||
"uniwind-types.d.ts",
|
||||
"../../packages/i18n/src/po.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "tabler",
|
||||
"rtl": false,
|
||||
"rtl": true,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-hotkeys": "0.4.2",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.167.5",
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
"better-auth": "catalog:",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
@@ -37,7 +37,7 @@
|
||||
"motion": "12.38.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"shadcn": "4.0.8",
|
||||
"shadcn": "4.1.0",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "catalog:",
|
||||
"thumbhash": "catalog:",
|
||||
@@ -51,8 +51,8 @@
|
||||
"@tanstack/devtools-vite": "0.6.0",
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@tanstack/react-router-devtools": "1.166.9",
|
||||
"@tanstack/router-plugin": "1.166.14",
|
||||
"@tanstack/react-router-devtools": "1.166.10",
|
||||
"@tanstack/router-plugin": "1.167.1",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
|
||||
@@ -48,6 +48,7 @@ export function AuthForm({
|
||||
const isRegister = mode === "register";
|
||||
const showOidc = authConfig?.oidcEnabled ?? false;
|
||||
const showPasswordForm = !(authConfig?.passwordLoginDisabled ?? false);
|
||||
const oidcProviderName = authConfig?.oidcProviderName || "SSO";
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
@@ -139,7 +140,7 @@ export function AuthForm({
|
||||
{oidcLoading ? (
|
||||
<Trans>Redirecting…</Trans>
|
||||
) : (
|
||||
<Trans>Sign in with {authConfig?.oidcProviderName || "SSO"}</Trans>
|
||||
<Trans>Sign in with {oidcProviderName}</Trans>
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
@@ -27,6 +27,8 @@ export function ContinueWatchingCard({ item }: { item: ContinueWatchingItemProps
|
||||
const { t } = useLingui();
|
||||
const stillUrl = item.nextEpisode?.stillPath ?? item.title.backdropPath ?? null;
|
||||
const progress = item.totalEpisodes > 0 ? (item.watchedEpisodes / item.totalEpisodes) * 100 : 0;
|
||||
const watchedEpisodes = item.watchedEpisodes;
|
||||
const totalEpisodes = item.totalEpisodes;
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -63,7 +65,7 @@ export function ContinueWatchingCard({ item }: { item: ContinueWatchingItemProps
|
||||
{t`Up next`}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-sm font-medium text-white">
|
||||
<span className="mr-0.5 font-mono text-xs text-white/60 [word-spacing:-0.25em]">
|
||||
<span className="me-0.5 font-mono text-xs text-white/60 [word-spacing:-0.25em]">
|
||||
S{item.nextEpisode.seasonNumber} E{item.nextEpisode.episodeNumber}
|
||||
</span>{" "}
|
||||
{item.nextEpisode.name}
|
||||
@@ -75,7 +77,7 @@ export function ContinueWatchingCard({ item }: { item: ContinueWatchingItemProps
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{item.title.title}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t`${item.watchedEpisodes}/${item.totalEpisodes} ${plural(item.totalEpisodes, { one: "episode", other: "episodes" })}`}
|
||||
{t`${watchedEpisodes}/${plural(totalEpisodes, { one: "# episode", other: "# episodes" })}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors">
|
||||
|
||||
@@ -70,7 +70,7 @@ export function HeroBanner({
|
||||
<Trans>Trending today</Trans>
|
||||
</span>
|
||||
</div>
|
||||
<Link to="/titles/$id" params={{ id }} className="group/title text-left">
|
||||
<Link to="/titles/$id" params={{ id }} className="group/title text-start">
|
||||
<h2 className="font-display group-hover/title:text-primary text-3xl tracking-tight text-balance transition-colors sm:text-4xl">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
@@ -188,7 +188,7 @@ export function NavBar({
|
||||
>
|
||||
<IconSearch aria-hidden={true} className="size-3.5" />
|
||||
<span>{t`Search…`}</span>
|
||||
<Kbd className="ml-2.5">⌘ K</Kbd>
|
||||
<Kbd className="ms-2.5">⌘ K</Kbd>
|
||||
</button>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
@@ -219,7 +219,7 @@ export function NavBar({
|
||||
<p className="text-foreground truncate text-sm leading-tight font-medium">
|
||||
{userName}
|
||||
{userRole === "admin" && (
|
||||
<Badge className="bg-primary/10 text-primary mb-0.5 ml-1.5 rounded-md border-0 align-middle">
|
||||
<Badge className="bg-primary/10 text-primary ms-1.5 mb-0.5 rounded-md border-0 align-middle">
|
||||
<Trans>Admin</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
@@ -291,7 +291,7 @@ export function MobileTabBar() {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Primary"
|
||||
className="border-border/50 bg-background/90 fixed right-0 bottom-0 left-0 z-50 border-t pr-[env(safe-area-inset-right)] pl-[env(safe-area-inset-left)] backdrop-blur-xl sm:hidden"
|
||||
className="border-border/50 bg-background/90 fixed right-0 bottom-0 left-0 z-50 border-t ps-[env(safe-area-inset-left)] pe-[env(safe-area-inset-right)] backdrop-blur-xl sm:hidden"
|
||||
>
|
||||
<div ref={containerRef} className="relative flex h-14 items-stretch">
|
||||
{mobileTabs.map((tab, i) => {
|
||||
|
||||
@@ -294,7 +294,7 @@ export function AccountSection({
|
||||
transition={{ duration: 0.1 }}
|
||||
type="button"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
className="group/name hover:text-primary inline-flex items-center gap-1.5 rounded-md px-0 text-left transition-colors"
|
||||
className="group/name hover:text-primary inline-flex items-center gap-1.5 rounded-md px-0 text-start transition-colors"
|
||||
>
|
||||
{displayName}
|
||||
<IconPencil className="group-hover/name:text-muted-foreground size-3 text-transparent transition-colors" />
|
||||
@@ -305,7 +305,7 @@ export function AccountSection({
|
||||
<CardDescription>
|
||||
{user.email}
|
||||
{user.role === "admin" && (
|
||||
<Badge className="bg-primary/10 text-primary ml-1.5 rounded-md border-0 align-middle">
|
||||
<Badge className="bg-primary/10 text-primary ms-1.5 rounded-md border-0 align-middle">
|
||||
<Trans>Admin</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -218,31 +218,28 @@ export function BackupScheduleSection() {
|
||||
<CardDescription>
|
||||
{enabled ? (
|
||||
<span className="inline-flex flex-wrap items-baseline" suppressHydrationWarning>
|
||||
{formatNextBackup(frequency, time, dow)}.{" "}
|
||||
<Trans>
|
||||
Keeping{" "}
|
||||
<Select
|
||||
value={String(maxRetention)}
|
||||
onValueChange={(v) => v && changeMaxRetention(Number(v))}
|
||||
modal={false}
|
||||
>
|
||||
<SelectTrigger className="decoration-muted-foreground/50 hover:text-foreground hover:decoration-foreground/50 focus-visible:decoration-foreground mr-0.5 ml-1.5 !h-auto w-auto gap-0.5 rounded-none border-0 bg-transparent p-0 underline decoration-dotted underline-offset-4 shadow-none hover:bg-transparent focus-visible:decoration-solid focus-visible:ring-0 dark:bg-transparent dark:hover:bg-transparent">
|
||||
<SelectValue>
|
||||
{(value: string | null) =>
|
||||
value === "0" ? t`unlimited` : value ? t`last ${value}` : null
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" alignItemWithTrigger={false} className="p-1">
|
||||
{[3, 5, 7, 14, 30, 0].map((n) => (
|
||||
<SelectItem key={n} value={String(n)}>
|
||||
{n === 0 ? t`unlimited` : t`last ${n}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>{" "}
|
||||
backups.
|
||||
</Trans>
|
||||
{formatNextBackup(frequency, time, dow)}. <Trans>Keeping</Trans>{" "}
|
||||
<Select
|
||||
value={String(maxRetention)}
|
||||
onValueChange={(v) => v && changeMaxRetention(Number(v))}
|
||||
modal={false}
|
||||
>
|
||||
<SelectTrigger className="decoration-muted-foreground/50 hover:text-foreground hover:decoration-foreground/50 focus-visible:decoration-foreground ms-1.5 me-0.5 !h-auto w-auto gap-0.5 rounded-none border-0 bg-transparent p-0 underline decoration-dotted underline-offset-4 shadow-none hover:bg-transparent focus-visible:decoration-solid focus-visible:ring-0 dark:bg-transparent dark:hover:bg-transparent">
|
||||
<SelectValue>
|
||||
{(value: string | null) =>
|
||||
value === "0" ? t`unlimited` : value ? t`last ${value}` : null
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" alignItemWithTrigger={false} className="p-1">
|
||||
{[3, 5, 7, 14, 30, 0].map((n) => (
|
||||
<SelectItem key={n} value={String(n)}>
|
||||
{n === 0 ? t`unlimited` : t`last ${n}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>{" "}
|
||||
<Trans>backups.</Trans>
|
||||
</span>
|
||||
) : (
|
||||
<Trans>Automatically back up your database on a schedule</Trans>
|
||||
|
||||
@@ -93,9 +93,10 @@ export function BackupSection() {
|
||||
const creating = createMutation.isPending;
|
||||
const deleting = deleteMutation.isPending ? (deleteMutation.variables?.filename ?? null) : null;
|
||||
|
||||
const backupCount = displayBackups.length;
|
||||
const backupCountLabel =
|
||||
displayBackups.length > 0
|
||||
? t`${displayBackups.length} ${plural(displayBackups.length, { one: "backup", other: "backups" })} stored`
|
||||
backupCount > 0
|
||||
? t`${plural(backupCount, { one: "# backup", other: "# backups" })} stored`
|
||||
: t`No backups yet`;
|
||||
|
||||
return (
|
||||
@@ -126,136 +127,136 @@ export function BackupSection() {
|
||||
{displayBackups.length > 0 && (
|
||||
<CardContent className="border-border/30 border-t pt-4">
|
||||
<div className="space-y-1.5">
|
||||
{displayBackups.map((backup: BackupInfo) => (
|
||||
<motion.div
|
||||
key={backup.filename}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="group hover:bg-muted/40 flex items-center gap-3 rounded-md px-2.5 py-1.5 transition-colors">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className="text-muted-foreground flex shrink-0 items-center" />
|
||||
}
|
||||
>
|
||||
{backup.source === "scheduled" ? (
|
||||
<IconClock aria-hidden={true} className="size-3.5" />
|
||||
) : backup.source === "pre-restore" ? (
|
||||
<IconShieldCheck aria-hidden={true} className="size-3.5" />
|
||||
) : (
|
||||
<IconPointer aria-hidden={true} className="size-3.5" />
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{backup.source === "scheduled"
|
||||
? t`Scheduled backup`
|
||||
: backup.source === "pre-restore"
|
||||
? t`Pre-restore backup`
|
||||
: t`Manual backup`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-foreground text-xs font-medium">
|
||||
{formatBackupDate(backup.createdAt)}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-[11px]">
|
||||
{formatBytesI18n(backup.sizeBytes)}
|
||||
</span>
|
||||
<span
|
||||
className="text-muted-foreground/50 text-[11px]"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{formatRelativeTime(backup.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100">
|
||||
{displayBackups.map((backup: BackupInfo) => {
|
||||
const backupDate = formatBackupDate(backup.createdAt);
|
||||
return (
|
||||
<motion.div
|
||||
key={backup.filename}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="group hover:bg-muted/40 flex items-center gap-3 rounded-md px-2.5 py-1.5 transition-colors">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a
|
||||
href={`/api/backup/${backup.filename}`}
|
||||
download
|
||||
aria-label={t`Download backup`}
|
||||
>
|
||||
<IconCloudDownload />
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
<span className="text-muted-foreground flex shrink-0 items-center" />
|
||||
}
|
||||
/>
|
||||
>
|
||||
{backup.source === "scheduled" ? (
|
||||
<IconClock aria-hidden={true} className="size-3.5" />
|
||||
) : backup.source === "pre-restore" ? (
|
||||
<IconShieldCheck aria-hidden={true} className="size-3.5" />
|
||||
) : (
|
||||
<IconPointer aria-hidden={true} className="size-3.5" />
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>Download</Trans>
|
||||
{backup.source === "scheduled"
|
||||
? t`Scheduled backup`
|
||||
: backup.source === "pre-restore"
|
||||
? t`Pre-restore backup`
|
||||
: t`Manual backup`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<AlertDialog>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-foreground text-xs font-medium">{backupDate}</span>
|
||||
<span className="text-muted-foreground text-[11px]">
|
||||
{formatBytesI18n(backup.sizeBytes)}
|
||||
</span>
|
||||
<span
|
||||
className="text-muted-foreground/50 text-[11px]"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{formatRelativeTime(backup.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100">
|
||||
<Tooltip>
|
||||
<AlertDialogTrigger
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<TooltipTrigger
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={t`Delete backup`}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
disabled={deleting === backup.filename}
|
||||
/>
|
||||
<a
|
||||
href={`/api/backup/${backup.filename}`}
|
||||
download
|
||||
aria-label={t`Download backup`}
|
||||
>
|
||||
<IconCloudDownload />
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{deleting === backup.filename ? <Spinner /> : <IconTrash />}
|
||||
</AlertDialogTrigger>
|
||||
/>
|
||||
<TooltipContent>
|
||||
<Trans>Delete</Trans>
|
||||
<Trans>Download</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans>Delete backup?</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans>
|
||||
This will permanently delete the backup from{" "}
|
||||
<strong>{formatBackupDate(backup.createdAt)}</strong>. This cannot
|
||||
be undone.
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>Cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
deleteMutation.mutate({
|
||||
filename: backup.filename,
|
||||
})
|
||||
<AlertDialog>
|
||||
<Tooltip>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={t`Delete backup`}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
disabled={deleting === backup.filename}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{deleting === backup.filename ? <Spinner /> : <IconTrash />}
|
||||
</AlertDialogTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>Delete</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans>Delete backup?</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans>
|
||||
This will permanently delete the backup from{" "}
|
||||
<strong>{backupDate}</strong>. This cannot be undone.
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>Cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
deleteMutation.mutate({
|
||||
filename: backup.filename,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trans>Delete</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
@@ -277,8 +277,10 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
|
||||
warnings: event.job.warnings,
|
||||
});
|
||||
setStep("done");
|
||||
if (event.job.importedCount > 0) {
|
||||
toast.success(t`Imported ${event.job.importedCount} items from ${config.label}`);
|
||||
const importedCount = event.job.importedCount;
|
||||
const sourceLabel = config.label;
|
||||
if (importedCount > 0) {
|
||||
toast.success(t`Imported ${importedCount} items from ${sourceLabel}`);
|
||||
}
|
||||
} else if (event.type === "timeout") {
|
||||
receivedComplete = true;
|
||||
@@ -366,8 +368,9 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
const sourceLabel = config.label;
|
||||
throw new Error(
|
||||
(err as { error?: string }).error ?? t`Failed to start ${config.label} connection`,
|
||||
(err as { error?: string }).error ?? t`Failed to start ${sourceLabel} connection`,
|
||||
);
|
||||
}
|
||||
const data = (await res.json()) as DeviceCodeInfo;
|
||||
@@ -546,14 +549,16 @@ function ChooseStep({
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const sourceLabel = config.label;
|
||||
const acceptFormat = config.accept;
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Import from {config.label}</Trans>
|
||||
<Trans>Import from {sourceLabel}</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>Choose how to import your {config.label} data.</Trans>
|
||||
<Trans>Choose how to import your {sourceLabel} data.</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -566,7 +571,7 @@ function ChooseStep({
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="border-border/50 hover:bg-muted/50 flex w-full items-center gap-3 rounded-lg border p-4 text-left transition-colors"
|
||||
className="border-border/50 hover:bg-muted/50 flex w-full items-center gap-3 rounded-lg border p-4 text-start transition-colors"
|
||||
onClick={onConnect}
|
||||
>
|
||||
<div className="bg-primary/10 flex h-10 w-10 shrink-0 items-center justify-center rounded-lg">
|
||||
@@ -574,17 +579,17 @@ function ChooseStep({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
<Trans>Connect with {config.label}</Trans>
|
||||
<Trans>Connect with {sourceLabel}</Trans>
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans>Authorize Sofa to read your {config.label} library. No password shared.</Trans>
|
||||
<Trans>Authorize Sofa to read your {sourceLabel} library. No password shared.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="border-border/50 hover:bg-muted/50 flex w-full items-center gap-3 rounded-lg border p-4 text-left transition-colors"
|
||||
className="border-border/50 hover:bg-muted/50 flex w-full items-center gap-3 rounded-lg border p-4 text-start transition-colors"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
// Small delay so the dialog closes before file picker opens
|
||||
@@ -600,7 +605,7 @@ function ChooseStep({
|
||||
<Trans>Upload export file</Trans>
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t`Upload a ${config.accept} export from your ${config.label} account settings.`}
|
||||
{t`Upload a ${acceptFormat} export from your ${sourceLabel} account settings.`}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
@@ -725,6 +730,12 @@ function PreviewStep({
|
||||
(options.importWatchlist ? stats.watchlist : 0) +
|
||||
(options.importRatings ? stats.ratings : 0);
|
||||
|
||||
const movieCount = stats.movies;
|
||||
const episodeCount = stats.episodes;
|
||||
const watchlistCount = stats.watchlist;
|
||||
const ratingCount = stats.ratings;
|
||||
const unresolvedCount = preview.diagnostics?.unresolved ?? 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
@@ -738,18 +749,18 @@ function PreviewStep({
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<StatBadge label={t`Movies`} count={stats.movies} />
|
||||
<StatBadge label={t`Episodes`} count={stats.episodes} />
|
||||
<StatBadge label={t`Watchlist`} count={stats.watchlist} />
|
||||
<StatBadge label={t`Ratings`} count={stats.ratings} />
|
||||
<StatBadge label={t`Movies`} count={movieCount} />
|
||||
<StatBadge label={t`Episodes`} count={episodeCount} />
|
||||
<StatBadge label={t`Watchlist`} count={watchlistCount} />
|
||||
<StatBadge label={t`Ratings`} count={ratingCount} />
|
||||
</div>
|
||||
|
||||
{preview.diagnostics && preview.diagnostics.unresolved > 0 && (
|
||||
{preview.diagnostics && unresolvedCount > 0 && (
|
||||
<div className="bg-muted/50 rounded-lg p-3">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans>
|
||||
{preview.diagnostics.unresolved} items have no external IDs and will be resolved by
|
||||
title search, which may be less accurate.
|
||||
{unresolvedCount} items have no external IDs and will be resolved by title search,
|
||||
which may be less accurate.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
@@ -761,19 +772,19 @@ function PreviewStep({
|
||||
</p>
|
||||
<OptionCheckbox
|
||||
label={t`Watch history`}
|
||||
description={t`${stats.movies} movies, ${stats.episodes} episodes`}
|
||||
description={t`${movieCount} movies, ${episodeCount} episodes`}
|
||||
checked={options.importWatches}
|
||||
onChange={(v) => setOptions({ ...options, importWatches: v })}
|
||||
/>
|
||||
<OptionCheckbox
|
||||
label={t`Watchlist`}
|
||||
description={t`${stats.watchlist} items`}
|
||||
description={t`${watchlistCount} items`}
|
||||
checked={options.importWatchlist}
|
||||
onChange={(v) => setOptions({ ...options, importWatchlist: v })}
|
||||
/>
|
||||
<OptionCheckbox
|
||||
label={t`Ratings`}
|
||||
description={t`${stats.ratings} ratings`}
|
||||
description={t`${ratingCount} ratings`}
|
||||
checked={options.importRatings}
|
||||
onChange={(v) => setOptions({ ...options, importRatings: v })}
|
||||
/>
|
||||
@@ -863,6 +874,9 @@ function DoneStep({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const errorCount = result.errors.length;
|
||||
const warningCount = result.warnings.length;
|
||||
const remainingErrors = errorCount - 50;
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
@@ -881,28 +895,28 @@ function DoneStep({
|
||||
<StatBadge label={t`Failed`} count={result.failed} />
|
||||
</div>
|
||||
|
||||
{result.errors.length > 0 && (
|
||||
{errorCount > 0 && (
|
||||
<div className="bg-destructive/10 max-h-40 overflow-y-auto rounded-lg p-3">
|
||||
<p className="text-destructive mb-1 text-xs font-medium">
|
||||
<Trans>Errors ({result.errors.length})</Trans>
|
||||
<Trans>Errors ({errorCount})</Trans>
|
||||
</p>
|
||||
<ul className="text-destructive/80 space-y-0.5 text-xs">
|
||||
{result.errors.slice(0, 50).map((e, i) => (
|
||||
<li key={i}>{e}</li>
|
||||
))}
|
||||
{result.errors.length > 50 && (
|
||||
{errorCount > 50 && (
|
||||
<li>
|
||||
<Trans>...and {result.errors.length - 50} more</Trans>
|
||||
<Trans>...and {remainingErrors} more</Trans>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.warnings.length > 0 && (
|
||||
{warningCount > 0 && (
|
||||
<div className="max-h-32 overflow-y-auto rounded-lg bg-yellow-500/10 p-3">
|
||||
<p className="mb-1 text-xs font-medium text-yellow-600">
|
||||
<Trans>Warnings ({result.warnings.length})</Trans>
|
||||
<Trans>Warnings ({warningCount})</Trans>
|
||||
</p>
|
||||
<ul className="space-y-0.5 text-xs text-yellow-600/80">
|
||||
{result.warnings.slice(0, 20).map((w, i) => (
|
||||
|
||||
@@ -147,7 +147,7 @@ export function IntegrationCard({
|
||||
<div className="bg-primary/10 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg">
|
||||
<Icon className="text-primary size-4" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-start">
|
||||
<CardTitle>{config.label}</CardTitle>
|
||||
<CardDescription>
|
||||
{connection ? config.connectedStatus(connection.lastEventAt) : t`Not configured`}
|
||||
@@ -172,7 +172,7 @@ export function IntegrationCard({
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
{connecting ? <Trans>Connecting...</Trans> : <Trans>Connect {config.label}</Trans>}
|
||||
{connecting ? <Trans>Connecting...</Trans> : <Trans>Connect {label}</Trans>}
|
||||
</Button>
|
||||
) : (
|
||||
<AnimatePresence>
|
||||
@@ -251,10 +251,10 @@ export function IntegrationCard({
|
||||
<div className="border-border/50 bg-muted/30 text-muted-foreground mt-2 rounded-lg border p-3 text-xs leading-relaxed">
|
||||
<ol className="list-inside list-decimal space-y-1.5">{config.setupSteps}</ol>
|
||||
{config.docsUrl && (
|
||||
<p className="mt-2 -ml-0.5">
|
||||
<p className="-ms-0.5 mt-2">
|
||||
<IconBook2
|
||||
aria-hidden={true}
|
||||
className="mr-1 inline-block size-3 translate-y-[-1px]"
|
||||
className="me-1 inline-block size-3 translate-y-[-1px]"
|
||||
/>
|
||||
<Trans>Need more help?</Trans>{" "}
|
||||
<a
|
||||
@@ -285,14 +285,18 @@ export function IntegrationCard({
|
||||
|
||||
/** Status line for webhook integrations (shows last event time). */
|
||||
export function webhookStatus(lastEventAt: string | null): string {
|
||||
return lastEventAt
|
||||
? i18n._(msg`Last event ${formatRelativeTime(lastEventAt)}`)
|
||||
: i18n._(msg`Ready — nothing received yet`);
|
||||
if (lastEventAt) {
|
||||
const relativeTime = formatRelativeTime(lastEventAt);
|
||||
return i18n._(msg`Last event ${relativeTime}`);
|
||||
}
|
||||
return i18n._(msg`Ready — nothing received yet`);
|
||||
}
|
||||
|
||||
/** Status line for list integrations (shows last event time). */
|
||||
export function listStatus(lastEventAt: string | null): string {
|
||||
return lastEventAt
|
||||
? i18n._(msg`Last polled ${formatRelativeTime(lastEventAt)}`)
|
||||
: i18n._(msg`Ready — not polled yet`);
|
||||
if (lastEventAt) {
|
||||
const relativeTime = formatRelativeTime(lastEventAt);
|
||||
return i18n._(msg`Last polled ${relativeTime}`);
|
||||
}
|
||||
return i18n._(msg`Ready — not polled yet`);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function LanguageSection() {
|
||||
<div className="bg-primary/10 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg">
|
||||
<IconLanguage className="text-primary size-4" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-start">
|
||||
<CardTitle>
|
||||
<Trans>Language</Trans>
|
||||
</CardTitle>
|
||||
@@ -46,28 +46,31 @@ export function LanguageSection() {
|
||||
<CollapsibleContent className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0">
|
||||
<CardContent className="border-border/30 border-t pt-4">
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
{LOCALE_INFO.map((info) => (
|
||||
<button
|
||||
key={info.code}
|
||||
type="button"
|
||||
onClick={() => handleLocaleChange(info.code)}
|
||||
className={`relative flex items-center gap-2 rounded-lg border px-3 py-2 text-left text-sm transition-colors ${
|
||||
currentLocale === info.code
|
||||
? "border-primary bg-primary/5 text-foreground"
|
||||
: "border-border hover:border-primary/40 hover:bg-primary/5"
|
||||
}`}
|
||||
aria-label={t`Switch to ${info.name}`}
|
||||
aria-pressed={currentLocale === info.code}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium">{info.nativeName}</div>
|
||||
<div className="text-muted-foreground truncate text-xs">{info.name}</div>
|
||||
</div>
|
||||
{currentLocale === info.code && (
|
||||
<IconCheck className="text-primary ml-auto size-4 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
{LOCALE_INFO.map((info) => {
|
||||
const name = info.name;
|
||||
return (
|
||||
<button
|
||||
key={info.code}
|
||||
type="button"
|
||||
onClick={() => handleLocaleChange(info.code)}
|
||||
className={`relative flex items-center gap-2 rounded-lg border px-3 py-2 text-start text-sm transition-colors ${
|
||||
currentLocale === info.code
|
||||
? "border-primary bg-primary/5 text-foreground"
|
||||
: "border-border hover:border-primary/40 hover:bg-primary/5"
|
||||
}`}
|
||||
aria-label={t`Switch to ${name}`}
|
||||
aria-pressed={currentLocale === info.code}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium">{info.nativeName}</div>
|
||||
<div className="text-muted-foreground truncate text-xs">{info.name}</div>
|
||||
</div>
|
||||
{currentLocale === info.code && (
|
||||
<IconCheck className="text-primary ml-auto size-4 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
|
||||
@@ -80,7 +80,7 @@ export function SkeletonCards() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{["status", "jobs", "storage"].map((s) => (
|
||||
<Card key={s} className="border-l-primary/30 border-l-2">
|
||||
<Card key={s} className="border-s-primary/30 border-s-2">
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-3">
|
||||
<Skeleton className="mt-0.5 h-8 w-8 rounded-lg" />
|
||||
@@ -181,7 +181,7 @@ function SystemStatusCard({
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -313,7 +313,8 @@ function BackgroundJobsCard({
|
||||
const triggerJobMutation = useMutation(
|
||||
orpc.admin.triggerJob.mutationOptions({
|
||||
onSuccess: (_, { name }) => {
|
||||
toast.success(t`${JOB_LABELS[name] ?? name} triggered`);
|
||||
const jobLabel = JOB_LABELS[name] ?? name;
|
||||
toast.success(t`${jobLabel} triggered`);
|
||||
setTimeout(onRefresh, 1500);
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -334,9 +335,10 @@ function BackgroundJobsCard({
|
||||
});
|
||||
const activeJobs = jobs.filter((j) => !j.disabled);
|
||||
const healthyCount = activeJobs.filter((j) => j.lastStatus === "success").length;
|
||||
const activeJobCount = activeJobs.length;
|
||||
|
||||
return (
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -349,7 +351,7 @@ function BackgroundJobsCard({
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans>
|
||||
{healthyCount} of {activeJobs.length} jobs healthy
|
||||
{healthyCount} of {activeJobCount} jobs healthy
|
||||
</Trans>
|
||||
</CardDescription>
|
||||
</div>
|
||||
@@ -361,7 +363,7 @@ function BackgroundJobsCard({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b-border/30 hover:bg-transparent">
|
||||
<TableHead className="text-muted-foreground h-8 pl-5 text-[10px] font-medium tracking-wider uppercase">
|
||||
<TableHead className="text-muted-foreground h-8 ps-5 text-[10px] font-medium tracking-wider uppercase">
|
||||
<Trans>Job</Trans>
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground h-8 text-[10px] font-medium tracking-wider uppercase">
|
||||
@@ -373,7 +375,7 @@ function BackgroundJobsCard({
|
||||
<TableHead className="text-muted-foreground h-8 text-[10px] font-medium tracking-wider uppercase">
|
||||
<Trans>Next run</Trans>
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground h-8 pr-5 text-right text-[10px] font-medium tracking-wider uppercase">
|
||||
<TableHead className="text-muted-foreground h-8 pe-5 text-end text-[10px] font-medium tracking-wider uppercase">
|
||||
<span className="sr-only">
|
||||
<Trans>Actions</Trans>
|
||||
</span>
|
||||
@@ -388,7 +390,7 @@ function BackgroundJobsCard({
|
||||
return (
|
||||
<TableRow key={job.jobName} className="border-b-border/20 hover:bg-muted/30">
|
||||
{/* Job name + status */}
|
||||
<TableCell className="pl-5">
|
||||
<TableCell className="ps-5">
|
||||
<div className="flex items-center gap-2">
|
||||
{job.disabled ? (
|
||||
<StatusDot status="inactive" label={t`Disabled`} />
|
||||
@@ -472,7 +474,7 @@ function BackgroundJobsCard({
|
||||
</TableCell>
|
||||
|
||||
{/* Trigger button */}
|
||||
<TableCell className="pr-5 text-right">
|
||||
<TableCell className="pe-5 text-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
@@ -524,8 +526,11 @@ function StorageCard({
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const cachedImageCount = imageCache.imageCount.toLocaleString();
|
||||
const backupCount = backups.backupCount;
|
||||
const lastBackupAt = backups.lastBackupAt;
|
||||
return (
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -560,7 +565,7 @@ function StorageCard({
|
||||
{imageCache.enabled ? (
|
||||
<>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{t`${imageCache.imageCount.toLocaleString()} cached images`}
|
||||
{t`${cachedImageCount} cached images`}
|
||||
</p>
|
||||
<p className="text-muted-foreground/50 mt-0.5 text-[10px] leading-relaxed">
|
||||
{Object.entries(imageCache.categories)
|
||||
@@ -582,17 +587,16 @@ function StorageCard({
|
||||
<span className="text-muted-foreground/70 text-[11px] font-medium tracking-wider uppercase">
|
||||
<Trans>Backups</Trans>
|
||||
</span>
|
||||
{backups.backupCount > 0 && (
|
||||
{backupCount > 0 && (
|
||||
<span className="text-muted-foreground/50 font-mono text-[11px]">
|
||||
{formatBytes(backups.totalSizeBytes)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{backups.backupCount > 0 ? (
|
||||
{backupCount > 0 ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>
|
||||
{backups.backupCount} backups · last{" "}
|
||||
<LiveTimeAgo date={backups.lastBackupAt} fallback={t`unknown`} />
|
||||
{backupCount} backups · last <LiveTimeAgo date={lastBackupAt} fallback={t`unknown`} />
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -115,7 +115,7 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
className="absolute top-2 right-2 z-10 flex size-8 cursor-default items-center justify-center rounded-full bg-black/50 text-white backdrop-blur-sm"
|
||||
className="absolute end-2 top-2 z-10 flex size-8 cursor-default items-center justify-center rounded-full bg-black/50 text-white backdrop-blur-sm"
|
||||
render={<div />}
|
||||
>
|
||||
<StatusIcon className="size-4" />
|
||||
@@ -129,7 +129,7 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
onClick={handleClick}
|
||||
className="absolute top-2 right-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 text-white opacity-60 backdrop-blur-sm transition-opacity hover:bg-black/70 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
|
||||
className="absolute end-2 top-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 text-white opacity-60 backdrop-blur-sm transition-opacity hover:bg-black/70 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
|
||||
render={<button type="button" />}
|
||||
>
|
||||
{!quickAddMutation.isPending && <IconPlus className="size-4" />}
|
||||
|
||||
@@ -6,13 +6,18 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { thumbHashToUrl } from "@/lib/thumbhash";
|
||||
import type { CastMember } from "@sofa/api/schemas";
|
||||
|
||||
function EpisodeCountLabel({ count }: { count: number }) {
|
||||
const { t } = useLingui();
|
||||
const suffix = count !== 1 ? "s" : "";
|
||||
return <p className="text-muted-foreground/70 text-[10px]">{t`${count} ep${suffix}`}</p>;
|
||||
}
|
||||
|
||||
interface CastCarouselProps {
|
||||
actors: CastMember[];
|
||||
titleType: "movie" | "tv";
|
||||
}
|
||||
|
||||
export function CastCarousel({ actors, titleType }: CastCarouselProps) {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -74,9 +79,7 @@ export function CastCarousel({ actors, titleType }: CastCarouselProps) {
|
||||
</p>
|
||||
)}
|
||||
{titleType === "tv" && member.episodeCount && (
|
||||
<p className="text-muted-foreground/70 text-[10px]">
|
||||
{t`${member.episodeCount} ep${member.episodeCount !== 1 ? "s" : ""}`}
|
||||
</p>
|
||||
<EpisodeCountLabel count={member.episodeCount} />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -161,7 +161,7 @@ export function TitleSeasons({
|
||||
setOpenSeason(isOpen ? null : season.seasonNumber);
|
||||
}
|
||||
}}
|
||||
className="group/season hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between p-4 text-left transition-colors"
|
||||
className="group/season hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between p-4 text-start transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">
|
||||
@@ -253,7 +253,7 @@ export function TitleSeasons({
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" />
|
||||
<span className="absolute bottom-2 left-3 font-mono text-[10px] text-white/70">
|
||||
<span className="absolute start-3 bottom-2 font-mono text-[10px] text-white/70">
|
||||
E{String(ep.episodeNumber).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -65,8 +65,9 @@ export function useTitleActions() {
|
||||
try {
|
||||
await batchWatchMutation.mutateAsync({ episodeIds });
|
||||
await queryClient.invalidateQueries({ queryKey: userInfoKey });
|
||||
const count = episodeIds.length;
|
||||
toast.success(
|
||||
t`Caught up — marked ${episodeIds.length} ${plural(episodeIds.length, { one: "episode", other: "episodes" })} as watched`,
|
||||
t`Caught up — marked ${plural(count, { one: "# episode", other: "# episodes" })} as watched`,
|
||||
);
|
||||
} catch {
|
||||
setUserInfo((old) => ({
|
||||
@@ -112,7 +113,7 @@ export function useTitleActions() {
|
||||
});
|
||||
toast.success(
|
||||
ratingStars > 0
|
||||
? t`Rated ${ratingStars} ${plural(ratingStars, { one: "star", other: "stars" })}`
|
||||
? t`Rated ${plural(ratingStars, { one: "# star", other: "# stars" })}`
|
||||
: t`Rating removed`,
|
||||
);
|
||||
} catch {
|
||||
@@ -195,7 +196,7 @@ export function useTitleActions() {
|
||||
if (previousUnwatched.length > 0) {
|
||||
const count = previousUnwatched.length;
|
||||
toast.success(t`Watched S${seasonNum} E${epNum}`, {
|
||||
description: t`${count} earlier ${plural(count, { one: "episode", other: "episodes" })} unwatched`,
|
||||
description: t`${plural(count, { one: "# earlier episode", other: "# earlier episodes" })} unwatched`,
|
||||
action: {
|
||||
label: t`Catch up`,
|
||||
onClick: () => catchUp(previousUnwatched),
|
||||
@@ -263,11 +264,12 @@ export function useTitleActions() {
|
||||
}
|
||||
}
|
||||
|
||||
const seasonLabel = season.name ?? t`Season ${season.seasonNumber}`;
|
||||
const seasonNumber = season.seasonNumber;
|
||||
const seasonLabel = season.name ?? t`Season ${seasonNumber}`;
|
||||
if (previousUnwatched.length > 0) {
|
||||
const count = previousUnwatched.length;
|
||||
toast.success(t`Watched all of ${seasonLabel}`, {
|
||||
description: t`${count} earlier ${plural(count, { one: "episode", other: "episodes" })} unwatched`,
|
||||
description: t`${plural(count, { one: "# earlier episode", other: "# earlier episodes" })} unwatched`,
|
||||
action: {
|
||||
label: t`Catch up`,
|
||||
onClick: () => catchUp(previousUnwatched),
|
||||
@@ -302,7 +304,9 @@ export function useTitleActions() {
|
||||
|
||||
try {
|
||||
await unwatchSeasonMutation.mutateAsync({ id: season.id });
|
||||
toast.success(t`Unwatched all of ${season.name ?? t`Season ${season.seasonNumber}`}`);
|
||||
const seasonNumber = season.seasonNumber;
|
||||
const seasonLabel = season.name ?? t`Season ${seasonNumber}`;
|
||||
toast.success(t`Unwatched all of ${seasonLabel}`);
|
||||
} catch {
|
||||
setUserInfo((old) => ({
|
||||
...old,
|
||||
|
||||
@@ -29,7 +29,7 @@ function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"group/accordion-trigger **:data-[slot=accordion-trigger-icon]:text-muted-foreground relative flex flex-1 items-start justify-between gap-6 border border-transparent p-2 text-left text-xs/relaxed font-medium transition-all outline-none hover:underline aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4",
|
||||
"group/accordion-trigger **:data-[slot=accordion-trigger-icon]:text-muted-foreground relative flex flex-1 items-start justify-between gap-6 border border-transparent p-2 text-start text-xs/relaxed font-medium transition-all outline-none hover:underline aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ms-auto **:data-[slot=accordion-trigger-icon]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -43,7 +43,7 @@ function AlertDialogContent({
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 group/alert-dialog-content bg-background ring-foreground/10 data-closed:animate-out data-open:animate-in fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl p-4 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-64 data-[size=default]:sm:max-w-sm",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 group/alert-dialog-content bg-background ring-foreground/10 data-closed:animate-out data-open:animate-in fixed start-1/2 top-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl p-4 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-64 data-[size=default]:sm:max-w-sm rtl:translate-x-1/2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -57,7 +57,7 @@ function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">)
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-start sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2 py-1.5 text-left text-xs/relaxed has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-3.5",
|
||||
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2 py-1.5 text-start text-xs/relaxed has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pe-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-3.5",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -62,11 +62,7 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
|
||||
|
||||
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-action"
|
||||
className={cn("absolute top-1.5 right-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div data-slot="alert-action" className={cn("absolute end-2 top-1.5", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
|
||||
"bg-primary text-primary-foreground ring-background absolute end-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium whitespace-nowrap transition-all focus-visible:ring-[3px] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:pointer-events-none [&>svg]:size-2.5!",
|
||||
"group/badge focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium whitespace-nowrap transition-all focus-visible:ring-[3px] has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:pointer-events-none [&>svg]:size-2.5!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -6,12 +6,12 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
|
||||
"*:data-slot:rounded-e-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-e-md! [&>[data-slot]~[data-slot]]:rounded-s-none [&>[data-slot]~[data-slot]]:border-s-0",
|
||||
vertical:
|
||||
"flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
|
||||
},
|
||||
|
||||
@@ -21,10 +21,10 @@ const buttonVariants = cva(
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
|
||||
sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
|
||||
"h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-2.5",
|
||||
sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-4",
|
||||
icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
|
||||
"icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
||||
|
||||
@@ -102,7 +102,7 @@ function ComboboxContent({
|
||||
data-slot="combobox-content"
|
||||
data-chips={!!anchor}
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in *:data-[slot=input-group]:bg-input/20 dark:bg-popover relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-7 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:shadow-none",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 group/combobox-content bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in *:data-[slot=input-group]:bg-input/20 dark:bg-popover relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-7 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:shadow-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -138,7 +138,7 @@ function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.
|
||||
{children}
|
||||
<ComboboxPrimitive.ItemIndicator
|
||||
render={
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center" />
|
||||
<span className="pointer-events-none absolute end-2 flex items-center justify-center" />
|
||||
}
|
||||
>
|
||||
<IconCheck className="pointer-events-none" />
|
||||
@@ -218,7 +218,7 @@ function ComboboxChip({
|
||||
<ComboboxPrimitive.Chip
|
||||
data-slot="combobox-chip"
|
||||
className={cn(
|
||||
"bg-muted-foreground/10 text-foreground flex h-[calc(--spacing(4.75))] w-fit items-center justify-center gap-1 rounded-[calc(var(--radius-sm)-2px)] px-1.5 text-xs/relaxed font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
|
||||
"bg-muted-foreground/10 text-foreground flex h-[calc(--spacing(4.75))] w-fit items-center justify-center gap-1 rounded-[calc(var(--radius-sm)-2px)] px-1.5 text-xs/relaxed font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pe-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -227,7 +227,7 @@ function ComboboxChip({
|
||||
{showRemove && (
|
||||
<ComboboxPrimitive.ChipRemove
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
className="-ml-1 opacity-50 hover:opacity-100"
|
||||
className="-ms-1 opacity-50 hover:opacity-100"
|
||||
data-slot="combobox-chip-remove"
|
||||
>
|
||||
<IconX className="pointer-events-none" />
|
||||
|
||||
@@ -148,7 +148,7 @@ function CommandItem({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<IconCheck className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||
<IconCheck className="ms-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||
</CommandPrimitive.Item>
|
||||
);
|
||||
}
|
||||
@@ -158,7 +158,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"span">)
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-[0.625rem] tracking-widest",
|
||||
"text-muted-foreground group-data-selected/command-item:text-foreground ms-auto text-[0.625rem] tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -26,7 +26,7 @@ function ContextMenuContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = 4,
|
||||
side = "right",
|
||||
side = "inline-end",
|
||||
sideOffset = 0,
|
||||
...props
|
||||
}: ContextMenuPrimitive.Popup.Props &
|
||||
@@ -43,7 +43,7 @@ function ContextMenuContent({
|
||||
<ContextMenuPrimitive.Popup
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -68,7 +68,7 @@ function ContextMenuLabel({
|
||||
<ContextMenuPrimitive.GroupLabel
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-inset:pl-7.5", className)}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-inset:ps-7.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -89,7 +89,7 @@ function ContextMenuItem({
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"group/context-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"group/context-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -114,13 +114,13 @@ function ContextMenuSubTrigger({
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<IconChevronRight className="ml-auto" />
|
||||
<IconChevronRight className="ms-auto" />
|
||||
</ContextMenuPrimitive.SubmenuTrigger>
|
||||
);
|
||||
}
|
||||
@@ -130,7 +130,7 @@ function ContextMenuSubContent({ ...props }: React.ComponentProps<typeof Context
|
||||
<ContextMenuContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className="shadow-lg"
|
||||
side="right"
|
||||
side="inline-end"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -150,13 +150,13 @@ function ContextMenuCheckboxItem({
|
||||
data-slot="context-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 ps-2 pe-8 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||
<span className="pointer-events-none absolute end-2 flex items-center justify-center">
|
||||
<ContextMenuPrimitive.CheckboxItemIndicator>
|
||||
<IconCheck />
|
||||
</ContextMenuPrimitive.CheckboxItemIndicator>
|
||||
@@ -183,12 +183,12 @@ function ContextMenuRadioItem({
|
||||
data-slot="context-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 ps-2 pe-8 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||
<span className="pointer-events-none absolute end-2 flex items-center justify-center">
|
||||
<ContextMenuPrimitive.RadioItemIndicator>
|
||||
<IconCheck />
|
||||
</ContextMenuPrimitive.RadioItemIndicator>
|
||||
@@ -213,7 +213,7 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest",
|
||||
"text-muted-foreground group-focus/context-menu-item:text-accent-foreground ms-auto text-[0.625rem] tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -48,7 +48,7 @@ function DialogContent({
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 data-closed:animate-out data-open:animate-in fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 text-xs/relaxed ring-1 duration-100 outline-none sm:max-w-sm",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 data-closed:animate-out data-open:animate-in fixed start-1/2 top-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 text-xs/relaxed ring-1 duration-100 outline-none sm:max-w-sm rtl:translate-x-1/2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -57,7 +57,7 @@ function DialogContent({
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
render={<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm" />}
|
||||
render={<Button variant="ghost" className="absolute end-2 top-2" size="icon-sm" />}
|
||||
>
|
||||
<IconX />
|
||||
<span className="sr-only">Close</span>
|
||||
|
||||
@@ -37,7 +37,7 @@ function DropdownMenuContent({
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -62,7 +62,7 @@ function DropdownMenuLabel({
|
||||
<MenuPrimitive.GroupLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-inset:pl-7.5", className)}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-inset:ps-7.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -83,7 +83,7 @@ function DropdownMenuItem({
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"group/dropdown-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"group/dropdown-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -108,13 +108,13 @@ function DropdownMenuSubTrigger({
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-popup-open:bg-accent data-open:text-accent-foreground data-popup-open:text-accent-foreground flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-popup-open:bg-accent data-open:text-accent-foreground data-popup-open:text-accent-foreground flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<IconChevronRight className="ml-auto" />
|
||||
<IconChevronRight className="ms-auto" />
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ function DropdownMenuSubTrigger({
|
||||
function DropdownMenuSubContent({
|
||||
align = "start",
|
||||
alignOffset = -3,
|
||||
side = "right",
|
||||
side = "inline-end",
|
||||
sideOffset = 0,
|
||||
className,
|
||||
...props
|
||||
@@ -157,14 +157,14 @@ function DropdownMenuCheckboxItem({
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 ps-2 pe-8 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
className="pointer-events-none absolute end-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
@@ -193,13 +193,13 @@ function DropdownMenuRadioItem({
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 ps-2 pe-8 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
className="pointer-events-none absolute end-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
@@ -226,7 +226,7 @@ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"spa
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest",
|
||||
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ms-auto text-[0.625rem] tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -122,7 +122,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"text-muted-foreground text-left text-xs/relaxed leading-normal font-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
||||
"text-muted-foreground text-start text-xs/relaxed leading-normal font-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
||||
"last:mt-0 nth-last-2:-mt-1",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className,
|
||||
@@ -186,7 +186,7 @@ function FieldError({
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
<ul className="ms-4 flex list-disc flex-col gap-1">
|
||||
{uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ function HoverCardContent({
|
||||
<PreviewCardPrimitive.Popup
|
||||
data-slot="hover-card-content"
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 w-72 origin-(--transform-origin) rounded-lg p-2.5 text-xs/relaxed shadow-md ring-1 outline-hidden duration-100",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 w-72 origin-(--transform-origin) rounded-lg p-2.5 text-xs/relaxed shadow-md ring-1 outline-hidden duration-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -12,7 +12,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group border-input bg-input/20 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 relative flex h-7 w-full min-w-0 items-center rounded-md border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-2 has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||
"group/input-group border-input bg-input/20 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 relative flex h-7 w-full min-w-0 items-center rounded-md border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-2 has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -25,8 +25,8 @@ const inputGroupAddonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start": "order-first pl-2 has-[>button]:ml-[-0.275rem] has-[>kbd]:ml-[-0.275rem]",
|
||||
"inline-end": "order-last pr-2 has-[>button]:mr-[-0.275rem] has-[>kbd]:mr-[-0.275rem]",
|
||||
"inline-start": "order-first ps-2 has-[>button]:ms-[-0.275rem] has-[>kbd]:ms-[-0.275rem]",
|
||||
"inline-end": "order-last pe-2 has-[>button]:me-[-0.275rem] has-[>kbd]:me-[-0.275rem]",
|
||||
"block-start":
|
||||
"order-first w-full justify-start px-2 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
|
||||
"block-end":
|
||||
|
||||
@@ -140,7 +140,7 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<p
|
||||
data-slot="item-description"
|
||||
className={cn(
|
||||
"text-muted-foreground [&>a:hover]:text-primary line-clamp-2 text-left text-xs/relaxed font-normal [&>a]:underline [&>a]:underline-offset-4",
|
||||
"text-muted-foreground [&>a:hover]:text-primary line-clamp-2 text-start text-xs/relaxed font-normal [&>a]:underline [&>a]:underline-offset-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -69,7 +69,7 @@ function MenubarContent({
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover text-popover-foreground ring-foreground/10 data-open:animate-in min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100",
|
||||
"data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 bg-popover text-popover-foreground ring-foreground/10 data-open:animate-in min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -89,7 +89,7 @@ function MenubarItem({
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"group/menubar-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive! min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed data-disabled:opacity-50 data-inset:pl-7.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"group/menubar-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive! min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed data-disabled:opacity-50 data-inset:ps-7.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -111,13 +111,13 @@ function MenubarCheckboxItem({
|
||||
data-slot="menubar-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-2 pl-7.5 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 ps-7.5 pe-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
|
||||
<span className="pointer-events-none absolute start-2 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<IconCheck />
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
@@ -144,12 +144,12 @@ function MenubarRadioItem({
|
||||
data-slot="menubar-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-2 pl-7.5 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 ps-7.5 pe-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:ps-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
|
||||
<span className="pointer-events-none absolute start-2 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<IconCheck />
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
@@ -170,7 +170,7 @@ function MenubarLabel({
|
||||
<DropdownMenuLabel
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-inset:pl-7.5", className)}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-inset:ps-7.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -197,7 +197,7 @@ function MenubarShortcut({
|
||||
<DropdownMenuShortcut
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground group-focus/menubar-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest",
|
||||
"text-muted-foreground group-focus/menubar-item:text-accent-foreground ms-auto text-[0.625rem] tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -221,7 +221,7 @@ function MenubarSubTrigger({
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs data-inset:pl-7.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs data-inset:ps-7.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -68,7 +68,7 @@ function NavigationMenuTrigger({
|
||||
>
|
||||
{children}{" "}
|
||||
<IconChevronDown
|
||||
className="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180"
|
||||
className="relative top-px ms-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
@@ -80,7 +80,7 @@ function NavigationMenuContent({ className, ...props }: NavigationMenuPrimitive.
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-open:animate-in h-full w-auto p-1.5 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-xl group-data-[viewport=false]/navigation-menu:shadow-md group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
"data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 data-ending-style:data-activation-direction=left:translate-x-[50%] rtl:data-ending-style:data-activation-direction=left:-translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] rtl:data-ending-style:data-activation-direction=right:-translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] rtl:data-starting-style:data-activation-direction=left:-translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] rtl:data-starting-style:data-activation-direction=right:-translate-x-[50%] data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-open:animate-in h-full w-auto p-1.5 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-xl group-data-[viewport=false]/navigation-menu:shadow-md group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -104,7 +104,7 @@ function NavigationMenuPositioner({
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
className={cn(
|
||||
"isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0",
|
||||
"isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none data-[side=bottom]:before:start-0 data-[side=bottom]:before:end-0 data-[side=bottom]:before:top-[-10px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -143,7 +143,7 @@ function NavigationMenuIndicator({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-ss-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Icon>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ function PaginationPrevious({
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("pl-2!", className)}
|
||||
className={cn("ps-2!", className)}
|
||||
{...props}
|
||||
>
|
||||
<IconChevronLeft data-icon="inline-start" />
|
||||
@@ -80,7 +80,7 @@ function PaginationNext({
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("pr-2!", className)}
|
||||
className={cn("pe-2!", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">{text}</span>
|
||||
|
||||
@@ -32,7 +32,7 @@ function PopoverContent({
|
||||
<PopoverPrimitive.Popup
|
||||
data-slot="popover-content"
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-lg p-2.5 text-xs shadow-md ring-1 outline-hidden duration-100",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-lg p-2.5 text-xs shadow-md ring-1 outline-hidden duration-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -54,7 +54,7 @@ function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
|
||||
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
|
||||
return (
|
||||
<ProgressPrimitive.Value
|
||||
className={cn("text-muted-foreground ml-auto text-xs/relaxed tabular-nums", className)}
|
||||
className={cn("text-muted-foreground ms-auto text-xs/relaxed tabular-nums", className)}
|
||||
data-slot="progress-value"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -27,7 +27,7 @@ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
|
||||
data-slot="radio-group-indicator"
|
||||
className="flex size-4 items-center justify-center"
|
||||
>
|
||||
<span className="bg-primary-foreground absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full" />
|
||||
<span className="bg-primary-foreground absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full rtl:translate-x-1/2" />
|
||||
</RadioPrimitive.Indicator>
|
||||
</RadioPrimitive.Root>
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ function ScrollArea({
|
||||
"no-scrollbar focus-visible:ring-ring/50 flex-1 rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:outline-1 data-has-overflow-x:overscroll-x-contain",
|
||||
scrollFade &&
|
||||
"mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] [--fade-size:1.5rem]",
|
||||
scrollbarGutter && "data-has-overflow-x:pb-2.5 data-has-overflow-y:pr-2.5",
|
||||
scrollbarGutter && "data-has-overflow-x:pb-2.5 data-has-overflow-y:pe-2.5",
|
||||
)}
|
||||
>
|
||||
<ScrollAreaPrimitive.Content data-slot="scroll-area-content" ref={contentRef}>
|
||||
|
||||
@@ -20,7 +20,7 @@ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
data-slot="select-value"
|
||||
className={cn("flex flex-1 text-left", className)}
|
||||
className={cn("flex flex-1 text-start", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -80,7 +80,7 @@ function SelectContent({
|
||||
data-slot="select-content"
|
||||
data-align-trigger={alignItemWithTrigger}
|
||||
className={cn(
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none",
|
||||
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 bg-popover text-popover-foreground ring-foreground/10 data-closed:animate-out data-open:animate-in relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -119,7 +119,7 @@ function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Prop
|
||||
</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator
|
||||
render={
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center" />
|
||||
<span className="pointer-events-none absolute end-2 flex items-center justify-center" />
|
||||
}
|
||||
>
|
||||
<IconCheck className="pointer-events-none" />
|
||||
|
||||
@@ -51,7 +51,7 @@ function SheetContent({
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 bg-background data-closed:animate-out data-open:animate-in fixed z-50 flex flex-col bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
|
||||
"data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 bg-background data-closed:animate-out data-open:animate-in fixed z-50 flex flex-col bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-e data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-s data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -60,7 +60,7 @@ function SheetContent({
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close
|
||||
data-slot="sheet-close"
|
||||
render={<Button variant="ghost" className="absolute top-4 right-4" size="icon-sm" />}
|
||||
render={<Button variant="ghost" className="absolute end-4 top-4" size="icon-sm" />}
|
||||
>
|
||||
<IconX />
|
||||
<span className="sr-only">Close</span>
|
||||
|
||||
@@ -225,7 +225,7 @@ function Sidebar({
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-e group-data-[side=right]:border-s",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -277,11 +277,11 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=left]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize rtl:[[data-side=left][data-state=collapsed]_&]:cursor-w-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize rtl:[[data-side=right][data-state=collapsed]_&]:cursor-e-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:start-full rtl:group-data-[collapsible=offcanvas]:-translate-x-0",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-end-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-start-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -294,7 +294,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
"bg-background relative flex w-full flex-1 flex-col md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ms-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -405,7 +405,7 @@ function SidebarGroupAction({
|
||||
props: mergeProps<"button">(
|
||||
{
|
||||
className: cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute end-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
className,
|
||||
),
|
||||
},
|
||||
@@ -453,7 +453,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button group/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-[calc(var(--radius-sm)+2px)] p-2 text-left text-xs outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-active:font-medium [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||
"peer/menu-button group/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-[calc(var(--radius-sm)+2px)] p-2 text-start text-xs outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pe-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-active:font-medium [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -542,7 +542,7 @@ function SidebarMenuAction({
|
||||
props: mergeProps<"button">(
|
||||
{
|
||||
className: cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-[calc(var(--radius-sm)-2px)] p-0 outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute end-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-[calc(var(--radius-sm)-2px)] p-0 outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
showOnHover &&
|
||||
"peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 aria-expanded:opacity-100 md:opacity-0",
|
||||
className,
|
||||
@@ -564,7 +564,7 @@ function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">)
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-[calc(var(--radius-sm)-2px)] px-1 text-xs font-medium tabular-nums select-none group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1",
|
||||
"text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute end-1 flex h-5 min-w-5 items-center justify-center rounded-[calc(var(--radius-sm)-2px)] px-1 text-xs font-medium tabular-nums select-none group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -611,7 +611,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-s px-2.5 py-0.5 group-data-[collapsible=icon]:hidden rtl:-translate-x-px",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -646,7 +646,7 @@ function SidebarMenuSubButton({
|
||||
props: mergeProps<"a">(
|
||||
{
|
||||
className: cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden group-data-[collapsible=icon]:hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-xs data-[size=sm]:text-xs [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden group-data-[collapsible=icon]:hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-xs data-[size=sm]:text-xs rtl:translate-x-px [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
className,
|
||||
),
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ function Switch({
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className="bg-background dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-3.5 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=default]/switch:data-unchecked:translate-x-px group-data-[size=sm]/switch:data-unchecked:translate-x-px"
|
||||
className="bg-background dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-3.5 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=default]/switch:data-unchecked:translate-x-px group-data-[size=sm]/switch:data-unchecked:translate-x-px rtl:group-data-[size=default]/switch:data-checked:-translate-x-[calc(100%-1px)] rtl:group-data-[size=sm]/switch:data-checked:-translate-x-[calc(100%-1px)] rtl:group-data-[size=default]/switch:data-unchecked:-translate-x-px rtl:group-data-[size=sm]/switch:data-unchecked:-translate-x-px"
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
||||
"text-foreground h-10 px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -68,7 +68,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
||||
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -52,7 +52,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
||||
"text-foreground/60 hover:text-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50 dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-xs font-medium whitespace-nowrap transition-colors group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:py-[calc(--spacing(1.25))] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-end-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -70,7 +70,7 @@ function ToggleGroupItem({
|
||||
data-size={context.size || size}
|
||||
data-spacing={context.spacing}
|
||||
className={cn(
|
||||
"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
|
||||
"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-s-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-e-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-s-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-s group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
|
||||
@@ -36,13 +36,13 @@ function TooltipContent({
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-foreground text-background data-[state=delayed-open]:animate-in data-closed:animate-out data-open:animate-in z-50 w-fit max-w-xs origin-(--transform-origin) rounded-md px-3 py-1.5 text-xs **:data-[slot=kbd]:rounded-md",
|
||||
"data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 bg-foreground text-background data-[state=delayed-open]:animate-in data-closed:animate-out data-open:animate-in z-50 w-fit max-w-xs origin-(--transform-origin) rounded-md px-3 py-1.5 text-xs **:data-[slot=kbd]:rounded-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:-start-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:-end-1 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -15,8 +15,10 @@ export function UpdateToast({ data }: { data: UpdateCheckResult | null }) {
|
||||
if (dismissedVersion === data.latestVersion) return;
|
||||
|
||||
setDismissedVersion(data.latestVersion);
|
||||
toast.info(t`Sofa v${data.latestVersion} is available`, {
|
||||
description: t`You're running v${data.currentVersion}.`,
|
||||
const latestVersion = data.latestVersion;
|
||||
const currentVersion = data.currentVersion;
|
||||
toast.info(t`Sofa v${latestVersion} is available`, {
|
||||
description: t`You're running v${currentVersion}.`,
|
||||
duration: 15_000,
|
||||
action: data.releaseUrl
|
||||
? {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { activateLocale, SUPPORTED_LOCALES, type SupportedLocale } from "@sofa/i18n";
|
||||
import { activateLocale, getDirection, SUPPORTED_LOCALES, type SupportedLocale } from "@sofa/i18n";
|
||||
|
||||
const LOCALE_STORAGE_KEY = "sofa:locale";
|
||||
|
||||
@@ -17,6 +17,7 @@ export function getPersistedLocale(): SupportedLocale {
|
||||
export function setPersistedLocale(locale: SupportedLocale): void {
|
||||
localStorage.setItem(LOCALE_STORAGE_KEY, locale);
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = getDirection(locale);
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
@@ -26,6 +27,7 @@ export async function initLocale(): Promise<void> {
|
||||
initialized = true;
|
||||
const locale = getPersistedLocale();
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = getDirection(locale);
|
||||
if (locale !== "en") {
|
||||
await activateLocale(locale);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
@@ -11,11 +11,12 @@ import { MotionConfig } from "motion/react";
|
||||
|
||||
import { NavigationProgress } from "@/components/navigation-progress";
|
||||
import { SofaLogo } from "@/components/sofa-logo";
|
||||
import { DirectionProvider } from "@/components/ui/direction";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { initLocale } from "@/lib/i18n";
|
||||
import type { orpc } from "@/lib/orpc/client";
|
||||
import { i18n } from "@sofa/i18n";
|
||||
import { getDirection, i18n } from "@sofa/i18n";
|
||||
|
||||
import "@/styles/globals.css";
|
||||
|
||||
@@ -53,13 +54,7 @@ function RootComponent() {
|
||||
<HeadContent />
|
||||
<StoreProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MotionConfig reducedMotion="user">
|
||||
<NavigationProgress />
|
||||
<TooltipProvider>
|
||||
<Outlet />
|
||||
</TooltipProvider>
|
||||
<Toaster position="bottom-right" />
|
||||
</MotionConfig>
|
||||
<AppShell />
|
||||
</I18nProvider>
|
||||
</StoreProvider>
|
||||
<TanStackDevtools
|
||||
@@ -78,6 +73,23 @@ function RootComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
function AppShell() {
|
||||
const { i18n: linguiI18n } = useLingui();
|
||||
const direction = getDirection(linguiI18n.locale);
|
||||
|
||||
return (
|
||||
<DirectionProvider direction={direction}>
|
||||
<MotionConfig reducedMotion="user">
|
||||
<NavigationProgress />
|
||||
<TooltipProvider>
|
||||
<Outlet />
|
||||
</TooltipProvider>
|
||||
<Toaster position={direction === "rtl" ? "bottom-left" : "bottom-right"} />
|
||||
</MotionConfig>
|
||||
</DirectionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function GlobalNotFound() {
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-6">
|
||||
|
||||
@@ -41,7 +41,7 @@ function AppLayout() {
|
||||
<div className="bg-primary/3 pointer-events-none fixed top-1/4 left-1/2 h-[600px] w-[800px] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[200px]" />
|
||||
<main
|
||||
id="main-content"
|
||||
className="relative mx-auto max-w-6xl py-6 pr-[max(1rem,env(safe-area-inset-right))] pl-[max(1rem,env(safe-area-inset-left))] sm:pr-[max(1.5rem,env(safe-area-inset-right))] sm:pl-[max(1.5rem,env(safe-area-inset-left))]"
|
||||
className="relative mx-auto max-w-6xl py-6 ps-[max(1rem,env(safe-area-inset-left))] pe-[max(1rem,env(safe-area-inset-right))] sm:ps-[max(1.5rem,env(safe-area-inset-left))] sm:pe-[max(1.5rem,env(safe-area-inset-right))]"
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -173,10 +173,10 @@ function SettingsPage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<RegistrationSection />
|
||||
</Card>
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<UpdateCheckSection />
|
||||
</Card>
|
||||
</div>
|
||||
@@ -196,13 +196,13 @@ function SettingsPage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<BackupSection />
|
||||
</Card>
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<BackupScheduleSection />
|
||||
</Card>
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<BackupRestoreSection />
|
||||
</Card>
|
||||
</div>
|
||||
@@ -221,7 +221,7 @@ function SettingsPage() {
|
||||
Admin only
|
||||
</span>
|
||||
</div>
|
||||
<Card className="border-l-primary/30 border-l-2">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<CacheSection />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
},
|
||||
"types": ["bun"]
|
||||
},
|
||||
"include": ["src", "vite.config.ts"],
|
||||
"include": ["src", "vite.config.ts", "../../packages/i18n/src/po.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user