mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 10:56:58 +00:00
frontend: add Somali, Arabic (RTL) & German languages
Add three UI locales (so/ar/de) with full ~1,660-key translations alongside en/fr, selectable in Settings → Profile. Arabic gets full right-to-left support: - config.ts registers the locales and exports a `dirFor` helper; an inline <head> script in layout.tsx sets <html dir/lang> before first paint (no RTL flash), and i18n-provider keeps them in sync on language change. - ~160 physical direction utilities converted to logical (ms/me/ps/pe/ start/end/text-start/text-end); directional chevrons/arrows get rtl:rotate-180; chat-bubble align variants fixed to logical. - IBM Plex Sans Arabic appended to the sans/heading font stacks for per-character Arabic fallback. - Language persists to the backend user_settings and re-applies on sign-in so it roams across devices (localStorage stays the offline source of truth). - New scripts/check-locales.mjs (npm run check-locales) enforces key/placeholder parity and Arabic CLDR plural completeness. Bump to 0.3.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ function DetailRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="shrink-0 text-muted-foreground text-xs">{label}</span>
|
||||
<span className="text-right text-foreground text-sm">{value}</span>
|
||||
<span className="text-end text-foreground text-sm">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -212,7 +212,7 @@ export function ActivityView() {
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
"-mx-2 flex-1 rounded-lg px-2 py-1 text-left transition-colors hover:bg-accent/40",
|
||||
"-mx-2 flex-1 rounded-lg px-2 py-1 text-start transition-colors hover:bg-accent/40",
|
||||
isLast ? "pb-1" : "mb-5",
|
||||
)}
|
||||
onClick={() => setSelected(entry)}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function TrendCard({
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="w-full text-left"
|
||||
className="w-full text-start"
|
||||
disabled={!hasData}
|
||||
onClick={() => setOpen(true)}
|
||||
type="button"
|
||||
|
||||
@@ -314,9 +314,9 @@ export function AppointmentsView() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
className="w-full ps-9 sm:w-64"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={t("appointments.searchPlaceholder")}
|
||||
value={query}
|
||||
|
||||
@@ -145,7 +145,7 @@ export function CalendarDialog({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronLeft />
|
||||
<ChevronLeft className="rtl:rotate-180" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Next month"
|
||||
@@ -154,7 +154,7 @@ export function CalendarDialog({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronRight />
|
||||
<ChevronRight className="rtl:rotate-180" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,7 +182,7 @@ export function CalendarDialog({
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex min-h-22 flex-col gap-1 rounded-lg border p-1.5 text-left align-top transition-colors hover:bg-accent/50",
|
||||
"flex min-h-22 flex-col gap-1 rounded-lg border p-1.5 text-start align-top transition-colors hover:bg-accent/50",
|
||||
inMonth
|
||||
? "bg-card/30"
|
||||
: "bg-transparent text-muted-foreground/40",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type ReactNode, useEffect, useRef } from "react";
|
||||
|
||||
import { useAiAccess } from "@/lib/ai-policy";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { applyStoredLanguage } from "@/lib/language";
|
||||
import { canAccessRoute, defaultLandingFor, useActiveRole } from "@/lib/roles";
|
||||
|
||||
// Authoritative client-side gate for the app shell. Requires a session and an
|
||||
@@ -24,6 +25,16 @@ export function AppAuthGuard({ children }: { children: ReactNode }) {
|
||||
const hasUser = Boolean(session?.user);
|
||||
const activeOrgId = session?.session?.activeOrganizationId ?? null;
|
||||
|
||||
// Adopt the language saved on the backend once signed in, so the UI language
|
||||
// roams across devices. Best-effort and one-shot; localStorage stays the
|
||||
// offline source of truth.
|
||||
const languageSynced = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!hasUser || languageSynced.current) return;
|
||||
languageSynced.current = true;
|
||||
void applyStoredLanguage();
|
||||
}, [hasUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPending) return;
|
||||
if (!hasUser) {
|
||||
|
||||
@@ -269,7 +269,7 @@ export function ActionPreviewCard({
|
||||
</span>
|
||||
{editable ? (
|
||||
<Button
|
||||
className="ml-auto"
|
||||
className="ms-auto"
|
||||
onClick={() => setEditOpen(true)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
||||
@@ -60,7 +60,7 @@ export function AiSetupNotice() {
|
||||
</div>
|
||||
<button
|
||||
aria-label={t("chat.setupNotice.dismiss")}
|
||||
className="-mr-1 shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
className="-me-1 shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
onClick={() => setDismissed(true)}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -115,7 +115,7 @@ export function BatchActionPreviewCard({
|
||||
<span className="font-medium text-sm">
|
||||
{t("chat.actionCard.batch.title", { count: items.length })}
|
||||
</span>
|
||||
<Badge className="ml-auto gap-1" variant="secondary">
|
||||
<Badge className="ms-auto gap-1" variant="secondary">
|
||||
<Sparkles className="size-3" />
|
||||
AI
|
||||
</Badge>
|
||||
|
||||
@@ -104,9 +104,9 @@ export function ChatHistoryPanel() {
|
||||
{t("chat.history.startNew")}
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-2.5 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-2.5 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-8"
|
||||
className="ps-8"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t("chat.history.search")}
|
||||
value={query}
|
||||
@@ -123,7 +123,7 @@ export function ChatHistoryPanel() {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"group flex items-center gap-2 rounded-md px-2 py-2 text-left text-sm transition-colors hover:bg-accent",
|
||||
"group flex items-center gap-2 rounded-md px-2 py-2 text-start text-sm transition-colors hover:bg-accent",
|
||||
active
|
||||
? "bg-accent text-foreground"
|
||||
: "text-muted-foreground",
|
||||
|
||||
@@ -242,7 +242,7 @@ export function ChatInput({
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className={cn(contextPill, "ml-0.5")}
|
||||
className={cn(contextPill, "ms-0.5")}
|
||||
onClick={() => {
|
||||
setAddKey((k) => k + 1);
|
||||
setAddOpen(true);
|
||||
@@ -258,7 +258,7 @@ export function ChatInput({
|
||||
<ModePicker
|
||||
mode={mode}
|
||||
onModeChange={onModeChange}
|
||||
triggerClassName={cn(pillButton, "mr-1")}
|
||||
triggerClassName={cn(pillButton, "me-1")}
|
||||
/>
|
||||
<button
|
||||
aria-label={
|
||||
|
||||
@@ -421,7 +421,7 @@ export function ChatPanel() {
|
||||
</div>
|
||||
<button
|
||||
aria-label={t("chat.error.dismiss")}
|
||||
className="-mr-1 shrink-0 rounded-md p-1 text-destructive-foreground/70 transition-colors hover:bg-destructive/10 hover:text-destructive-foreground"
|
||||
className="-me-1 shrink-0 rounded-md p-1 text-destructive-foreground/70 transition-colors hover:bg-destructive/10 hover:text-destructive-foreground"
|
||||
onClick={() => setErrorDismissed(true)}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -196,7 +196,7 @@ export function ImportPreviewCard({
|
||||
<span className="text-sm font-medium">{t("chat.importCard.title")}</span>
|
||||
{status === "pending" && records.length > 0 ? (
|
||||
<Button
|
||||
className="ml-auto"
|
||||
className="ms-auto"
|
||||
onClick={() => setReviewOpen(true)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -289,7 +289,7 @@ export function ImportPreviewCard({
|
||||
t("chat.importCard.unnamed");
|
||||
return (
|
||||
<button
|
||||
className="flex items-start gap-2 rounded-xl border bg-card/30 p-3 text-left transition-colors hover:bg-accent"
|
||||
className="flex items-start gap-2 rounded-xl border bg-card/30 p-3 text-start transition-colors hover:bg-accent"
|
||||
key={index}
|
||||
onClick={() => setEditingIndex(index)}
|
||||
type="button"
|
||||
|
||||
@@ -16,7 +16,7 @@ export function InventoryListCard({ items }: { items: InventoryItem[] }) {
|
||||
<div className="flex items-center gap-2 border-b px-4 py-3">
|
||||
<Boxes className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium text-sm">{t("chat.lists.inventory")}</span>
|
||||
<Badge className="ml-auto" variant="secondary">
|
||||
<Badge className="ms-auto" variant="secondary">
|
||||
{items.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -71,11 +71,11 @@ const statusVariant: Record<Patient["status"], BadgeVariant> = {
|
||||
// plus a subtle clickable affordance (they open a detail dialog). Compact cards
|
||||
// size to their own (short) content — see `items-start` in PatientResult.
|
||||
const rowCard =
|
||||
"w-72 shrink-0 cursor-pointer gap-0 text-left outline-none transition hover:bg-accent/30 hover:ring-foreground/20 focus-visible:ring-2 focus-visible:ring-ring";
|
||||
"w-72 shrink-0 cursor-pointer gap-0 text-start outline-none transition hover:bg-accent/30 hover:ring-foreground/20 focus-visible:ring-2 focus-visible:ring-ring";
|
||||
|
||||
// Same footprint as `rowCard` but with no clickable affordance — used when a
|
||||
// card has nothing extra to reveal, so it shouldn't promise "Click for more".
|
||||
const rowCardStatic = "w-72 shrink-0 gap-0 text-left";
|
||||
const rowCardStatic = "w-72 shrink-0 gap-0 text-start";
|
||||
|
||||
// COSS Card has no `size` variant; recreate the old compact ("sm") density by
|
||||
// tightening the inner section padding from p-6 → p-4 via data-slot selectors.
|
||||
@@ -196,7 +196,7 @@ function ExpandableCard({
|
||||
{children}
|
||||
<div className="flex items-center gap-1 px-4 pt-2 pb-3 text-muted-foreground text-xs">
|
||||
{t("patientCard.clickForMore")}
|
||||
<ArrowRight className="size-3" />
|
||||
<ArrowRight className="size-3 rtl:rotate-180" />
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogPopup className="max-h-[80dvh] sm:max-w-lg">
|
||||
|
||||
@@ -724,7 +724,7 @@ export function PatientFormDialog({
|
||||
|
||||
<DialogFooter className="flex-col items-stretch gap-2 sm:flex-row sm:items-center">
|
||||
{error && (
|
||||
<p className="text-sm text-destructive sm:mr-auto">{error}</p>
|
||||
<p className="text-sm text-destructive sm:me-auto">{error}</p>
|
||||
)}
|
||||
<DialogClose render={<Button type="button" variant="outline" />}>
|
||||
{t("patientForm.cancel")}
|
||||
|
||||
@@ -31,7 +31,7 @@ function Shell({
|
||||
<div className="flex items-center gap-2 border-b px-4 py-3">
|
||||
<Icon className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium text-sm">{title}</span>
|
||||
<Badge className="ml-auto" variant="secondary">
|
||||
<Badge className="ms-auto" variant="secondary">
|
||||
{rows.length}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@ export function AppointmentListCard({
|
||||
{range ? (
|
||||
<span className="text-muted-foreground text-xs">{range}</span>
|
||||
) : null}
|
||||
<Badge className="ml-auto" variant="secondary">
|
||||
<Badge className="ms-auto" variant="secondary">
|
||||
{appointments.length}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -157,7 +157,7 @@ export function AppointmentListCard({
|
||||
</span>
|
||||
<Button render={<Link href={href} />} size="sm" variant="ghost">
|
||||
{t("chat.lists.viewInCalendar")}
|
||||
<ChevronRight className="size-4" />
|
||||
<ChevronRight className="size-4 rtl:rotate-180" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import type * as React from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import i18n from "@/lib/i18n/config";
|
||||
import i18n, { dirFor } from "@/lib/i18n/config";
|
||||
|
||||
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
// Keep <html lang/dir> in sync with the active language. The inline script in
|
||||
// app/layout.tsx sets these before first paint (avoiding an RTL flash); this
|
||||
// effect keeps them correct after hydration and on every language switch.
|
||||
useEffect(() => {
|
||||
const apply = (lng: string) => {
|
||||
const root = document.documentElement;
|
||||
root.lang = lng;
|
||||
root.dir = dirFor(lng);
|
||||
};
|
||||
apply(i18n.resolvedLanguage ?? i18n.language);
|
||||
i18n.on("languageChanged", apply);
|
||||
return () => {
|
||||
i18n.off("languageChanged", apply);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export function InvoiceDetailSheet({
|
||||
<SheetTitle className="flex items-center gap-2">
|
||||
{invoice.number}
|
||||
<AiBadge source={invoice.source} />
|
||||
<Badge className="ml-auto" variant={statusVariant[invoice.status]}>
|
||||
<Badge className="ms-auto" variant={statusVariant[invoice.status]}>
|
||||
{t(`invoices.status.${invoice.status}`)}
|
||||
</Badge>
|
||||
</SheetTitle>
|
||||
@@ -222,7 +222,7 @@ export function InvoiceDetailSheet({
|
||||
<span className="shrink-0 text-muted-foreground text-xs tabular-nums">
|
||||
{li.quantity} × {formatMoney(li.unitPrice)}
|
||||
</span>
|
||||
<span className="w-20 shrink-0 text-right font-medium text-foreground tabular-nums">
|
||||
<span className="w-20 shrink-0 text-end font-medium text-foreground tabular-nums">
|
||||
{formatMoney(li.quantity * li.unitPrice)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -135,9 +135,9 @@ export function InvoicesView() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
className="w-full ps-9 sm:w-64"
|
||||
onChange={(event) => {
|
||||
setQuery(event.target.value);
|
||||
setPage(1);
|
||||
@@ -180,7 +180,7 @@ export function InvoicesView() {
|
||||
<div className="divide-y divide-border overflow-hidden rounded-2xl border bg-card/30">
|
||||
{pageRows.map((inv) => (
|
||||
<button
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50"
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-start transition-colors hover:bg-accent/50"
|
||||
key={inv.id}
|
||||
onClick={() => openInvoice(inv)}
|
||||
type="button"
|
||||
|
||||
@@ -92,7 +92,7 @@ export function LabIntegrationCard({
|
||||
{t("integrations.fhir.cardTitle")}
|
||||
</h2>
|
||||
<Badge
|
||||
className="ml-auto"
|
||||
className="ms-auto"
|
||||
variant={
|
||||
config.status === "connected"
|
||||
? "secondary"
|
||||
@@ -141,9 +141,9 @@ export function LabIntegrationCard({
|
||||
) : (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-9"
|
||||
className="ps-9"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t("integrations.fhir.searchPlaceholder")}
|
||||
value={query}
|
||||
@@ -153,7 +153,7 @@ export function LabIntegrationCard({
|
||||
<div className="flex flex-col gap-1">
|
||||
{matches.map((p) => (
|
||||
<button
|
||||
className="flex items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-accent"
|
||||
className="flex items-center gap-3 rounded-lg px-2 py-2 text-start transition-colors hover:bg-accent"
|
||||
key={p.fileNumber}
|
||||
onClick={() => setSelected(p)}
|
||||
type="button"
|
||||
|
||||
@@ -336,7 +336,7 @@ function AddResultDialog({
|
||||
{t("lab.addResult.patient")}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-activedescendant={
|
||||
matches[activeIndex]
|
||||
@@ -344,7 +344,7 @@ function AddResultDialog({
|
||||
: undefined
|
||||
}
|
||||
autoFocus
|
||||
className="pl-9"
|
||||
className="ps-9"
|
||||
onChange={(event) => {
|
||||
setPatientQuery(event.target.value);
|
||||
setActiveIndex(0);
|
||||
@@ -359,7 +359,7 @@ function AddResultDialog({
|
||||
{matches.map((p, index) => (
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors",
|
||||
"flex items-center gap-3 rounded-lg px-2 py-2 text-start transition-colors",
|
||||
index === activeIndex
|
||||
? "bg-accent"
|
||||
: "hover:bg-accent",
|
||||
@@ -650,7 +650,7 @@ export function LabView() {
|
||||
}
|
||||
onClick={() => toggle(task.id)}
|
||||
/>
|
||||
<CollapsibleTrigger className="group flex min-w-0 flex-1 items-center gap-3 text-left">
|
||||
<CollapsibleTrigger className="group flex min-w-0 flex-1 items-center gap-3 text-start">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span
|
||||
className={cn(
|
||||
@@ -679,7 +679,7 @@ export function LabView() {
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-3 px-4 pb-4 pl-12 text-sm">
|
||||
<div className="space-y-3 px-4 pb-4 ps-12 text-sm">
|
||||
<p
|
||||
className={cn(
|
||||
"whitespace-pre-wrap",
|
||||
|
||||
@@ -124,7 +124,7 @@ export function LoginForm({
|
||||
{t("auth.login.passwordLabel")}
|
||||
</FieldLabel>
|
||||
<Link
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
className="ms-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
href="/forgot-password"
|
||||
>
|
||||
{t("auth.login.forgotPassword")}
|
||||
|
||||
@@ -285,10 +285,10 @@ export function MeetingRoom({
|
||||
</DialogHeader>
|
||||
<DialogPanel className="flex flex-col gap-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t("meetings.invite.search")}
|
||||
className="pl-9"
|
||||
className="ps-9"
|
||||
onChange={(e) => setMemberQuery(e.target.value)}
|
||||
placeholder={t("meetings.invite.search")}
|
||||
size="sm"
|
||||
|
||||
@@ -246,13 +246,13 @@ export function MeetingsView() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-1 rounded-lg pr-1 transition-colors hover:bg-accent/50",
|
||||
"group flex w-full items-center gap-1 rounded-lg pe-1 transition-colors hover:bg-accent/50",
|
||||
activeRoom?.id === room.id && "bg-accent hover:bg-accent",
|
||||
)}
|
||||
key={room.id}
|
||||
>
|
||||
<button
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-lg px-2 py-2 text-left"
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-lg px-2 py-2 text-start"
|
||||
onClick={() => setActiveRoom(room)}
|
||||
type="button"
|
||||
>
|
||||
@@ -352,7 +352,7 @@ export function MeetingsView() {
|
||||
<div className="flex flex-col gap-1">
|
||||
{upcoming.map((e) => (
|
||||
<button
|
||||
className="flex flex-col gap-0.5 rounded-xl border bg-card px-2.5 py-2 text-left transition-colors hover:bg-accent/50"
|
||||
className="flex flex-col gap-0.5 rounded-xl border bg-card px-2.5 py-2 text-start transition-colors hover:bg-accent/50"
|
||||
key={e.id}
|
||||
onClick={() => setSelectedDay(new Date(`${e.date}T00:00:00`))}
|
||||
type="button"
|
||||
|
||||
@@ -159,7 +159,7 @@ export function ScheduleMeetingDialog({
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-accent/50",
|
||||
"flex items-center gap-3 rounded-lg px-2 py-1.5 text-start transition-colors hover:bg-accent/50",
|
||||
on && "bg-accent",
|
||||
)}
|
||||
key={m.id}
|
||||
|
||||
@@ -28,7 +28,7 @@ function Row({ label, value }: { label: string; value: ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<dt className="text-muted-foreground text-sm">{label}</dt>
|
||||
<dd className="text-right text-foreground text-sm">{value}</dd>
|
||||
<dd className="text-end text-foreground text-sm">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ function SentAttachment({ att }: { att: MessageAttachment }) {
|
||||
<AttachmentContent>
|
||||
<AttachmentTitle>{att.fileName}</AttachmentTitle>
|
||||
</AttachmentContent>
|
||||
<AttachmentActions className="pr-1.5">
|
||||
<AttachmentActions className="pe-1.5">
|
||||
<AttachmentAction
|
||||
aria-label={t("messages.attach.download")}
|
||||
onClick={() => {
|
||||
@@ -491,10 +491,10 @@ export function MessagesView() {
|
||||
</div>
|
||||
<div className="border-border border-b px-3 py-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t("messages.searchPlaceholder")}
|
||||
className="pl-9"
|
||||
className="ps-9"
|
||||
onChange={(e) => setInboxQuery(e.target.value)}
|
||||
placeholder={t("messages.searchPlaceholder")}
|
||||
size="sm"
|
||||
@@ -520,7 +520,7 @@ export function MessagesView() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center gap-1 rounded-lg pr-2 transition-colors hover:bg-accent/50",
|
||||
"flex w-full items-center gap-1 rounded-lg pe-2 transition-colors hover:bg-accent/50",
|
||||
selected?.id === c.id && "bg-accent hover:bg-accent",
|
||||
)}
|
||||
key={c.id}
|
||||
@@ -544,7 +544,7 @@ export function MessagesView() {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex min-w-0 flex-1 items-center gap-3 rounded-lg py-2 pr-1 text-left"
|
||||
className="flex min-w-0 flex-1 items-center gap-3 rounded-lg py-2 pe-1 text-start"
|
||||
onClick={() => open(c.id)}
|
||||
type="button"
|
||||
>
|
||||
@@ -880,10 +880,10 @@ export function MessagesView() {
|
||||
</DialogHeader>
|
||||
<DialogPanel className="flex flex-col gap-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t("messages.compose.searchPlaceholder")}
|
||||
className="pl-9"
|
||||
className="ps-9"
|
||||
onChange={(e) => setMemberQuery(e.target.value)}
|
||||
placeholder={t("messages.compose.searchPlaceholder")}
|
||||
size="sm"
|
||||
@@ -902,7 +902,7 @@ export function MessagesView() {
|
||||
) : (
|
||||
visibleMembers.map((m) => (
|
||||
<button
|
||||
className="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-accent"
|
||||
className="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-start transition-colors hover:bg-accent"
|
||||
key={m.id}
|
||||
onClick={() => startConversation(m.id)}
|
||||
type="button"
|
||||
@@ -932,10 +932,10 @@ export function MessagesView() {
|
||||
</DialogHeader>
|
||||
<DialogPanel className="flex flex-col gap-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t("messages.attach.apptSearchPlaceholder")}
|
||||
className="pl-9"
|
||||
className="ps-9"
|
||||
onChange={(e) => setApptQuery(e.target.value)}
|
||||
placeholder={t("messages.attach.apptSearchPlaceholder")}
|
||||
size="sm"
|
||||
@@ -954,7 +954,7 @@ export function MessagesView() {
|
||||
) : (
|
||||
visibleAppts.map((a) => (
|
||||
<button
|
||||
className="flex w-full flex-col gap-0.5 rounded-lg px-2 py-2 text-left transition-colors hover:bg-accent"
|
||||
className="flex w-full flex-col gap-0.5 rounded-lg px-2 py-2 text-start transition-colors hover:bg-accent"
|
||||
key={a.id}
|
||||
onClick={() => attachAppointment(a)}
|
||||
type="button"
|
||||
|
||||
@@ -153,7 +153,7 @@ export function NotesView() {
|
||||
<div className="divide-y divide-border overflow-hidden rounded-2xl border bg-card/30">
|
||||
{notes.map((n) => (
|
||||
<button
|
||||
className="flex w-full flex-col items-start gap-0.5 px-4 py-3 text-left transition-colors hover:bg-accent/50"
|
||||
className="flex w-full flex-col items-start gap-0.5 px-4 py-3 text-start transition-colors hover:bg-accent/50"
|
||||
key={n.id}
|
||||
onClick={() => openNote(n)}
|
||||
type="button"
|
||||
|
||||
@@ -296,7 +296,7 @@ export function PatientDetail({
|
||||
{onDelete && (
|
||||
<Button
|
||||
aria-label={t("patients.delete.action")}
|
||||
className="ml-auto"
|
||||
className="ms-auto"
|
||||
onClick={onDelete}
|
||||
size="sm"
|
||||
type="button"
|
||||
@@ -358,7 +358,7 @@ export function PatientDetail({
|
||||
<div className="divide-y divide-border overflow-hidden rounded-xl border bg-card/30">
|
||||
{files.map((file) => (
|
||||
<button
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-left transition-colors hover:bg-accent"
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-start transition-colors hover:bg-accent"
|
||||
key={file.id}
|
||||
onClick={() => setOpenFile(file)}
|
||||
type="button"
|
||||
|
||||
@@ -210,7 +210,7 @@ export function AttachmentsSection({
|
||||
key={attachment.id}
|
||||
>
|
||||
<button
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 text-left"
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 text-start"
|
||||
onClick={() => setPreview(attachment)}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -115,9 +115,9 @@ export function PatientsView() {
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
className="w-full ps-9 sm:w-64"
|
||||
onChange={(event) => {
|
||||
setQuery(event.target.value);
|
||||
setPage(1);
|
||||
@@ -159,7 +159,7 @@ export function PatientsView() {
|
||||
<div className="mt-8 overflow-hidden rounded-2xl border border-border bg-card/30">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-border border-b text-left text-xs text-muted-foreground uppercase">
|
||||
<tr className="border-border border-b text-start text-xs text-muted-foreground uppercase">
|
||||
<th className="px-4 py-3 font-medium">{t("patients.columns.name")}</th>
|
||||
<th className="px-4 py-3 font-medium">{t("patients.columns.mrn")}</th>
|
||||
<th className="px-4 py-3 font-medium">
|
||||
|
||||
@@ -35,7 +35,7 @@ function Row({ label, value }: { label: string; value: ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
<span className="text-muted-foreground text-sm">{label}</span>
|
||||
<span className="text-right text-foreground text-sm">{value}</span>
|
||||
<span className="text-end text-foreground text-sm">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export function InventoryDetailDialog({
|
||||
</span>
|
||||
<span className="min-w-0 truncate">{item.name}</span>
|
||||
<Badge
|
||||
className="ml-auto shrink-0"
|
||||
className="ms-auto shrink-0"
|
||||
variant={availabilityVariant[availability]}
|
||||
>
|
||||
{t(`inventory.availability.${availability}`)}
|
||||
|
||||
@@ -67,7 +67,7 @@ function ItemRow({
|
||||
const descriptor = [item.strength, item.form].filter(Boolean).join(" · ");
|
||||
return (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50"
|
||||
className="flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-start transition-colors hover:bg-accent/50"
|
||||
onClick={onOpen}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
@@ -205,9 +205,9 @@ export function InventoryView() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
className="w-full ps-9 sm:w-64"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={t("inventory.searchPlaceholder")}
|
||||
value={query}
|
||||
|
||||
@@ -330,9 +330,9 @@ export function PharmacyView() {
|
||||
<p className="text-muted-foreground text-sm">{t("pharmacy.subtitle")}</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
className="w-full ps-9 sm:w-64"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={t("pharmacy.searchPlaceholder")}
|
||||
value={query}
|
||||
|
||||
@@ -43,7 +43,7 @@ function Field({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex flex-col gap-1.5 text-left">
|
||||
<label className="flex flex-col gap-1.5 text-start">
|
||||
<span className="font-medium text-foreground text-sm">{label}</span>
|
||||
{children}
|
||||
</label>
|
||||
@@ -122,7 +122,7 @@ function ChooseStep({ onPick }: { onPick: (step: Step) => void }) {
|
||||
<div className="grid w-full gap-4 sm:grid-cols-2">
|
||||
{cards.map((c) => (
|
||||
<button
|
||||
className="group flex flex-col items-start gap-4 rounded-3xl border bg-card/40 p-6 text-left transition-colors hover:border-primary/50 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="group flex flex-col items-start gap-4 rounded-3xl border bg-card/40 p-6 text-start transition-colors hover:border-primary/50 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
key={c.step}
|
||||
onClick={() => onPick(c.step)}
|
||||
type="button"
|
||||
@@ -133,7 +133,7 @@ function ChooseStep({ onPick }: { onPick: (step: Step) => void }) {
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="flex items-center gap-1 font-semibold text-lg text-foreground">
|
||||
{c.title}
|
||||
<ChevronRight className="size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
||||
<ChevronRight className="size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5 rtl:rotate-180" />
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm">{c.desc}</span>
|
||||
</span>
|
||||
@@ -151,7 +151,7 @@ function BackButton({ onBack }: { onBack: () => void }) {
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
<ArrowLeft className="size-4 rtl:rotate-180" />
|
||||
{t("portal.back")}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -378,11 +378,11 @@ export function AddPrescriptionDialog({
|
||||
) : (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
aria-autocomplete="list"
|
||||
autoFocus
|
||||
className="pl-9"
|
||||
className="ps-9"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={onPatientKeyDown}
|
||||
placeholder={t("prescriptions.dialog.searchPlaceholder")}
|
||||
@@ -399,7 +399,7 @@ export function AddPrescriptionDialog({
|
||||
matches.map((p, i) => (
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left transition-colors",
|
||||
"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-start transition-colors",
|
||||
i === activeIndex
|
||||
? "bg-accent"
|
||||
: "hover:bg-accent",
|
||||
@@ -442,7 +442,7 @@ export function AddPrescriptionDialog({
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left transition-colors",
|
||||
"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-start transition-colors",
|
||||
i === medIndex ? "bg-accent" : "hover:bg-accent",
|
||||
)}
|
||||
// Use onMouseDown so the pick fires before the input's
|
||||
|
||||
@@ -147,7 +147,7 @@ export function PrescriptionDetailSheet({
|
||||
<SheetFooter>
|
||||
{onDelete && (
|
||||
<Button
|
||||
className="sm:mr-auto"
|
||||
className="sm:me-auto"
|
||||
onClick={onDelete}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
|
||||
@@ -244,9 +244,9 @@ export function PrescriptionsView() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 start-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
className="w-full ps-9 sm:w-64"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={t("prescriptions.searchPlaceholder")}
|
||||
value={query}
|
||||
|
||||
@@ -422,7 +422,7 @@ export function EmployeeDetailDialog({
|
||||
<DialogFooter>
|
||||
{editable && member && (
|
||||
<Button
|
||||
className="sm:mr-auto"
|
||||
className="sm:me-auto"
|
||||
onClick={() => onRemove(member)}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
|
||||
@@ -109,7 +109,7 @@ export function SigningPanel() {
|
||||
: t("settings.signing.rotateKey")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="sm:text-right">
|
||||
<div className="sm:text-end">
|
||||
<p className="text-3xl font-semibold tracking-tight">Ed25519</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{key
|
||||
|
||||
@@ -164,11 +164,11 @@ export function CareTeamPanel({
|
||||
{initials(m.name, m.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<div className="min-w-0 flex-1 text-start">
|
||||
<p className="truncate text-sm font-medium">
|
||||
{m.name || m.email || m.userId}
|
||||
{isSelf && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
<span className="ms-1 text-xs text-muted-foreground">
|
||||
{t("settings.careTeam.you")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -179,7 +179,7 @@ function IntegrationCard({
|
||||
: t("settings.integrations.test")}
|
||||
</Button>
|
||||
{config.lastSyncAt ? (
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
<span className="ms-auto text-xs text-muted-foreground">
|
||||
{t("settings.integrations.lastSync", {
|
||||
when: new Date(config.lastSyncAt).toLocaleString(),
|
||||
})}
|
||||
|
||||
@@ -108,7 +108,7 @@ export function CopyField({
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex h-9 w-full items-center gap-2 rounded-3xl bg-input/50 pr-1 pl-3 sm:w-80">
|
||||
<div className="flex h-9 w-full items-center gap-2 rounded-3xl bg-input/50 pe-1 ps-3 sm:w-80">
|
||||
<span className="flex-1 truncate text-sm text-muted-foreground">
|
||||
{value}
|
||||
</span>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@/components/settings/settings-parts";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { supportedLanguages } from "@/lib/i18n/config";
|
||||
import { persistLanguage } from "@/lib/language";
|
||||
import {
|
||||
getSettings,
|
||||
saveSettings,
|
||||
@@ -341,7 +342,10 @@ export function ProfilePanel() {
|
||||
: undefined
|
||||
}
|
||||
onConfirm={() => {
|
||||
if (pendingLang) void i18n.changeLanguage(pendingLang);
|
||||
if (pendingLang) {
|
||||
void i18n.changeLanguage(pendingLang);
|
||||
void persistLanguage(pendingLang);
|
||||
}
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setPendingLang(null);
|
||||
|
||||
@@ -21,7 +21,7 @@ export function MobileSidebarTrigger() {
|
||||
return (
|
||||
<Button
|
||||
aria-label={t("nav.openSidebar")}
|
||||
className="fixed top-3 right-3 z-50 size-9 rounded-full bg-background/80 shadow-sm backdrop-blur md:hidden"
|
||||
className="fixed top-3 end-3 z-50 size-9 rounded-full bg-background/80 shadow-sm backdrop-blur md:hidden"
|
||||
onClick={toggleSidebar}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) {
|
||||
>
|
||||
{route.icon}
|
||||
{!isCollapsed && (
|
||||
<span className="ml-2 flex-1 truncate font-medium text-sm">
|
||||
<span className="ms-2 flex-1 truncate font-medium text-sm">
|
||||
{route.title}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function NotificationsPopover() {
|
||||
>
|
||||
<BellIcon className="size-5" />
|
||||
{unread > 0 && (
|
||||
<span className="-top-0.5 -right-0.5 absolute flex min-w-4 items-center justify-center rounded-full bg-primary px-1 font-medium text-[10px] text-primary-foreground leading-4">
|
||||
<span className="-top-0.5 -end-0.5 absolute flex min-w-4 items-center justify-center rounded-full bg-primary px-1 font-medium text-[10px] text-primary-foreground leading-4">
|
||||
{unread > 9 ? "9+" : unread}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -146,13 +146,13 @@ export function NavUser() {
|
||||
</Avatar>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<div className="grid flex-1 text-start text-sm leading-tight">
|
||||
<span className="truncate font-medium">{name}</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{secondary}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
<ChevronsUpDown className="ms-auto size-4" />
|
||||
</>
|
||||
)}
|
||||
</MenuTrigger>
|
||||
@@ -167,7 +167,7 @@ export function NavUser() {
|
||||
<Avatar className="size-8">
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<div className="grid flex-1 text-start text-sm leading-tight">
|
||||
<span className="truncate font-medium">{name}</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{secondary}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function AccordionTrigger({
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
className={cn(
|
||||
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180",
|
||||
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-start font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
data-slot="accordion-trigger"
|
||||
|
||||
@@ -94,8 +94,8 @@ const bubbleReactionsVariants = cva(
|
||||
bottom: "bottom-0 translate-y-3/4",
|
||||
},
|
||||
align: {
|
||||
start: "left-3",
|
||||
end: "right-3",
|
||||
start: "start-3",
|
||||
end: "end-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -76,7 +76,7 @@ export function ListPagination({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
<ChevronLeft className="size-4 rtl:rotate-180" />
|
||||
<span className="max-sm:hidden">
|
||||
{t("common.pagination.previous")}
|
||||
</span>
|
||||
@@ -115,7 +115,7 @@ export function ListPagination({
|
||||
<span className="max-sm:hidden">
|
||||
{t("common.pagination.next")}
|
||||
</span>
|
||||
<ChevronRight className="size-4" />
|
||||
<ChevronRight className="size-4 rtl:rotate-180" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
|
||||
@@ -275,7 +275,7 @@ export function MenuSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ms-auto -me-0.5 opacity-80" />
|
||||
<ChevronRightIcon className="ms-auto -me-0.5 opacity-80 rtl:rotate-180" />
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export function PaginationPrevious({
|
||||
size="default"
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon className="sm:-ms-1" />
|
||||
<ChevronLeftIcon className="sm:-ms-1 rtl:rotate-180" />
|
||||
<span className="max-sm:hidden">Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
@@ -107,7 +107,7 @@ export function PaginationNext({
|
||||
{...props}
|
||||
>
|
||||
<span className="max-sm:hidden">Next</span>
|
||||
<ChevronRightIcon className="sm:-me-1" />
|
||||
<ChevronRightIcon className="sm:-me-1 rtl:rotate-180" />
|
||||
</PaginationLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { cn } from "@/lib/utils";
|
||||
export const Select: typeof SelectPrimitive.Root = SelectPrimitive.Root;
|
||||
|
||||
export const selectTriggerVariants = cva(
|
||||
"relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-between gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base text-foreground shadow-xs/5 outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
|
||||
"relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-between gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-start text-base text-foreground shadow-xs/5 outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
|
||||
@@ -33,7 +33,7 @@ const SIDEBAR_WIDTH_ICON: string = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT: string = "b";
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-lg p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0",
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-lg p-2 text-start text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
|
||||
@@ -40,7 +40,7 @@ export function UpdateBanner() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed right-4 bottom-4 z-50 flex max-w-sm items-start gap-3 rounded-2xl border border-border bg-card px-4 py-3 shadow-lg">
|
||||
<div className="fixed end-4 bottom-4 z-50 flex max-w-sm items-start gap-3 rounded-2xl border border-border bg-card px-4 py-3 shadow-lg">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-sm text-foreground">
|
||||
{t("settings.version.banner", { version: latest })}
|
||||
@@ -55,7 +55,7 @@ export function UpdateBanner() {
|
||||
</div>
|
||||
<button
|
||||
aria-label={t("settings.version.bannerDismiss")}
|
||||
className="-mr-1 shrink-0 rounded-lg p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
className="-me-1 shrink-0 rounded-lg p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
onClick={dismiss}
|
||||
type="button"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user