Improved the Right to Left Direction implementation for relevant languages. (#2583)

* Added support for right to left direction of bullet points in the page render and text editor

* Added right to left direction support by adding a toggle button in the layout and enhancing the css element of the page render to support it without affecting the default left to right direction

* Moved the Right to Life Direction Toggle feature to be integrated with the Languages section using locale

* change all occurrances of pl, pr, ml, mr, left, right to start/end equivalents

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Ibrahim
2026-01-25 22:18:20 +05:00
committed by GitHub
parent 0c65612368
commit 3f55c24e36
125 changed files with 484 additions and 264 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ function RouteComponent() {
size="lg"
variant="secondary"
disabled={isPrinting}
className="fixed right-4 bottom-4 z-50 hidden rounded-full px-4 md:inline-flex print:hidden"
className="fixed end-4 bottom-4 z-50 hidden rounded-full px-4 md:inline-flex print:hidden"
onClick={handleDownload}
>
{isPrinting ? <Spinner /> : <DownloadSimpleIcon />}
+4 -4
View File
@@ -131,8 +131,8 @@ export const DonationBanner = () => (
<FloatingIcon icon={HeartIcon} className="bottom-[20%] left-[20%]" delay={2.5} />
{/* Gradient Orbs */}
<div className="absolute top-1/4 -left-32 size-64 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute -right-32 bottom-1/4 size-64 rounded-full bg-rose-500/5 blur-3xl" />
<div className="absolute -start-32 top-1/4 size-64 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute -end-32 bottom-1/4 size-64 rounded-full bg-rose-500/5 blur-3xl" />
</div>
<div className="container relative px-8">
@@ -146,8 +146,8 @@ export const DonationBanner = () => (
>
<div aria-hidden="true" className="relative mb-6">
<PulsingHeart />
<SparkleEffect className="-top-2 -right-4" />
<SparkleEffect className="bottom-0 -left-3" />
<SparkleEffect className="-end-4 -top-2" />
<SparkleEffect className="-start-3 bottom-0" />
</div>
<motion.h2
+3 -3
View File
@@ -66,11 +66,11 @@ export function FAQ() {
return (
<section
id="frequently-asked-questions"
className="flex flex-col gap-x-16 gap-y-6 p-4 md:p-8 lg:flex-row lg:gap-x-24 xl:py-16"
className="flex flex-col gap-x-16 gap-y-6 p-4 md:p-8 lg:flex-row lg:gap-x-18 xl:py-16"
>
<motion.h2
className={cn(
"font-semibold text-2xl tracking-tight md:text-4xl xl:text-5xl",
"flex-1 font-semibold text-2xl tracking-tight md:text-4xl xl:text-5xl",
"flex shrink-0 flex-wrap items-center gap-x-1.5 lg:flex-col lg:items-start",
)}
initial={{ opacity: 0, x: -20 }}
@@ -86,7 +86,7 @@ export function FAQ() {
</motion.h2>
<motion.div
className="max-w-2xl flex-1 lg:ml-auto 2xl:max-w-3xl"
className="max-w-2xl flex-2 lg:ml-auto 2xl:max-w-3xl"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
+1 -1
View File
@@ -134,7 +134,7 @@ function FooterLink({ url, label }: FooterLinkItem) {
initial={{ width: 0, opacity: 0 }}
animate={isHovered ? { width: "100%", opacity: 1 } : { width: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
className="pointer-events-none absolute -bottom-0.5 left-0 h-px rounded bg-primary"
className="pointer-events-none absolute start-0 -bottom-0.5 h-px rounded bg-primary"
/>
</a>
</li>
+1 -1
View File
@@ -119,7 +119,7 @@ export function Hero() {
<motion.div
aria-hidden="true"
role="presentation"
className="absolute bottom-8 left-1/2 -translate-x-1/2"
className="absolute start-1/2 bottom-8 -translate-x-1/2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 2, duration: 1 }}
+2 -2
View File
@@ -7,8 +7,8 @@ export function Prefooter() {
<section id="prefooter" className="relative overflow-hidden py-16 md:py-24">
{/* Background decoration */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="absolute top-0 left-1/4 size-96 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute right-1/4 bottom-0 size-96 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute start-1/4 top-0 size-96 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute end-1/4 bottom-0 size-96 rounded-full bg-primary/5 blur-3xl" />
</div>
<div className="relative space-y-8">
@@ -33,7 +33,7 @@ export function ProductHuntBanner() {
href="https://www.producthunt.com/products/reactive-resume/launches/reactive-resume-v5-2?embed=true&amp;utm_source=badge-featured&amp;utm_medium=badge&amp;utm_campaign=badge-reactive-resume-v5-2"
>
<Trans>Reactive Resume is launching on Product Hunt today, head over to show some love!</Trans>
<HeartIcon weight="fill" color="#DA552F" className="ml-2 size-3.5" />
<HeartIcon weight="fill" color="#DA552F" className="ms-2 size-3.5" />
</a>
);
}
+2 -2
View File
@@ -38,7 +38,7 @@ function StatisticCard({ statistic, index }: StatisticCardProps) {
return (
<motion.div
className="group relative flex flex-col items-center justify-center gap-y-4 border-r border-b p-8 transition-colors last:border-r-0 hover:bg-secondary/30 sm:border-b-0 xl:py-12"
className="group relative flex flex-col items-center justify-center gap-y-4 border-r border-b p-8 transition-colors last:border-e-0 hover:bg-secondary/30 sm:border-b-0 xl:py-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
@@ -47,7 +47,7 @@ function StatisticCard({ statistic, index }: StatisticCardProps) {
{/* Background decoration */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<motion.div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-primary/2"
className="absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-primary/2"
initial={{ scale: 0.8, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
+1 -1
View File
@@ -23,7 +23,7 @@ function TemplateItem({ metadata }: TemplateItemProps) {
<div className="absolute inset-0 bg-linear-to-t from-black/70 via-black/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
{/* Template name on hover */}
<div className="absolute right-0 bottom-0 left-0 translate-y-full p-4 transition-transform duration-300 group-hover:translate-y-0">
<div className="absolute start-0 end-0 bottom-0 translate-y-full p-4 transition-transform duration-300 group-hover:translate-y-0">
<p className="font-semibold text-white drop-shadow-lg">{metadata.name}</p>
</div>
+2 -2
View File
@@ -115,10 +115,10 @@ export function Testimonials() {
<div className="relative">
{/* Left fade */}
<div className="pointer-events-none absolute top-0 bottom-0 left-0 z-10 w-16 bg-linear-to-r from-background to-transparent sm:w-24 md:w-32 lg:w-48" />
<div className="pointer-events-none absolute start-0 top-0 bottom-0 z-10 w-16 bg-linear-to-r from-background to-transparent sm:w-24 md:w-32 lg:w-48" />
{/* Right fade */}
<div className="pointer-events-none absolute top-0 right-0 bottom-0 z-10 w-16 bg-linear-to-l from-background to-transparent sm:w-24 md:w-32 lg:w-48" />
<div className="pointer-events-none absolute end-0 top-0 bottom-0 z-10 w-16 bg-linear-to-l from-background to-transparent sm:w-24 md:w-32 lg:w-48" />
<div className="flex flex-col gap-y-6">
<MarqueeRow testimonials={row1} rowId="row1" direction="left" duration={50} />
+1 -1
View File
@@ -11,7 +11,7 @@ function RouteComponent() {
<>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-100 focus:rounded-md focus:bg-background focus:px-4 focus:py-2 focus:text-foreground focus:shadow-lg focus:ring-2 focus:ring-ring"
className="sr-only focus:not-sr-only focus:fixed focus:start-4 focus:top-4 focus:z-100 focus:rounded-md focus:bg-background focus:px-4 focus:py-2 focus:text-foreground focus:shadow-lg focus:ring-2 focus:ring-ring"
>
<Trans>Skip to main content</Trans>
</a>
@@ -10,7 +10,7 @@ export function BuilderSidebarEdge({ side, children }: Props) {
<div
className={cn(
"absolute inset-y-0 hidden w-12 flex-col items-center justify-between bg-popover py-2.5 sm:flex",
side === "left" ? "left-0 border-r" : "right-0 border-l",
side === "left" ? "start-0 border-r" : "end-0 border-l",
)}
>
{children}
@@ -44,9 +44,9 @@ export function BuilderHeader() {
<HouseSimpleIcon />
</Link>
</Button>
<span className="mr-2.5 text-muted-foreground">/</span>
<span className="me-2.5 text-muted-foreground">/</span>
<h2 className="flex-1 truncate font-medium">{name}</h2>
{isLocked && <LockSimpleIcon className="ml-2 text-muted-foreground" />}
{isLocked && <LockSimpleIcon className="ms-2 text-muted-foreground" />}
<BuilderHeaderDropdown />
</div>
@@ -131,24 +131,24 @@ function BuilderHeaderDropdown() {
<DropdownMenuContent>
<DropdownMenuItem disabled={isLocked} onSelect={handleUpdate}>
<PencilSimpleLineIcon className="mr-2" />
<PencilSimpleLineIcon className="me-2" />
<Trans>Update</Trans>
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleDuplicate}>
<CopySimpleIcon className="mr-2" />
<CopySimpleIcon className="me-2" />
<Trans>Duplicate</Trans>
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleToggleLock}>
{isLocked ? <LockSimpleOpenIcon className="mr-2" /> : <LockSimpleIcon className="mr-2" />}
{isLocked ? <LockSimpleOpenIcon className="me-2" /> : <LockSimpleIcon className="me-2" />}
{isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" disabled={isLocked} onSelect={handleDelete}>
<TrashSimpleIcon className="mr-2" />
<TrashSimpleIcon className="me-2" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -54,7 +54,7 @@ export function BuilderSidebarLeft() {
<>
<SidebarEdge scrollAreaRef={scrollAreaRef} />
<ScrollArea ref={scrollAreaRef} className="@container h-[calc(100svh-3.5rem)] bg-background sm:ml-12">
<ScrollArea ref={scrollAreaRef} className="@container h-[calc(100svh-3.5rem)] bg-background sm:ms-12">
<div className="space-y-4 p-4">
{leftSidebarSections.map((section) => (
<Fragment key={section}>
@@ -58,7 +58,7 @@ export function CustomFieldsSection({ onSubmit }: Props) {
<FormControl>
<IconPicker
{...field}
className="rounded-r-none! border-r-0!"
className="rounded-r-none! border-e-0!"
onChange={(icon) => {
field.onChange(icon);
form.handleSubmit(onSubmit)();
@@ -90,7 +90,7 @@ export function CustomFieldsSection({ onSubmit }: Props) {
<Popover>
<PopoverTrigger asChild>
<Button size="icon" variant="ghost" className="ml-1">
<Button size="icon" variant="ghost" className="ms-1">
<LinkIcon />
</Button>
</PopoverTrigger>
@@ -155,7 +155,7 @@ function CustomFieldItem({ field, children }: CustomFieldItemProps) {
<Button
size="icon"
variant="ghost"
className="mr-2 touch-none"
className="me-2 touch-none"
onPointerDown={(e) => {
e.preventDefault();
controls.start(e);
@@ -36,7 +36,7 @@ export function SectionBase({ type, className, ...props }: Props) {
>
<AccordionItem value={type} className="group/accordion space-y-4">
<div className="flex items-center">
<AccordionTrigger asChild className="mr-2 items-center justify-center">
<AccordionTrigger asChild className="me-2 items-center justify-center">
<Button size="icon" variant="ghost">
<CaretRightIcon />
</Button>
@@ -244,7 +244,7 @@ export function SectionItem<T extends SectionItemType>({ type, item, title, subt
<button
onClick={onUpdate}
className={cn(
"flex flex-1 flex-col items-start justify-center space-y-0.5 pl-1.5 text-start opacity-100 transition-opacity hover:bg-secondary/40 focus:outline-none focus-visible:ring-1",
"flex flex-1 flex-col items-start justify-center space-y-0.5 ps-1.5 text-start opacity-100 transition-opacity hover:bg-secondary/40 focus:outline-none focus-visible:ring-1",
item.hidden && "opacity-50",
)}
>
@@ -47,7 +47,7 @@ export function BuilderSidebarRight() {
<>
<SidebarEdge scrollAreaRef={scrollAreaRef} />
<ScrollArea ref={scrollAreaRef} className="@container h-[calc(100svh-3.5rem)] bg-background sm:mr-12">
<ScrollArea ref={scrollAreaRef} className="@container h-[calc(100svh-3.5rem)] bg-background sm:me-12">
<div className="space-y-4 p-4">
{rightSidebarSections.map((section) => (
<Fragment key={section}>
@@ -24,13 +24,15 @@ const formSchema = metadataSchema.shape.layout.omit({ pages: true });
type FormValues = z.infer<typeof formSchema>;
function LayoutSectionForm() {
const sidebarWidth = useResumeStore((state) => state.resume.data.metadata.layout.sidebarWidth);
const layout = useResumeStore((state) => state.resume.data.metadata.layout);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
mode: "onChange",
resolver: zodResolver(formSchema),
defaultValues: { sidebarWidth },
defaultValues: {
sidebarWidth: layout.sidebarWidth,
},
});
const onSubmit = (data: FormValues) => {
@@ -293,7 +293,7 @@ function LayoutColumn({ pageIndex, columnId, items, disabled = false }: LayoutCo
return (
<SortableContext id={droppableId} items={items} strategy={verticalListSortingStrategy}>
<div className={cn("space-y-1.5", disabled && "opacity-50")}>
<div className="@md:row-start-1 pl-4 font-medium text-xs">{getColumnLabel(columnId)}</div>
<div className="@md:row-start-1 ps-4 font-medium text-xs">{getColumnLabel(columnId)}</div>
<div
ref={setNodeRef}
@@ -24,7 +24,7 @@ export function SectionBase({ type, className, ...props }: Props) {
>
<AccordionItem value={type} className="group/accordion space-y-4">
<div className="flex items-center">
<AccordionTrigger asChild className="mr-2 items-center justify-center">
<AccordionTrigger asChild className="me-2 items-center justify-center">
<Button size="icon" variant="ghost">
<CaretRightIcon />
</Button>
+1 -1
View File
@@ -11,7 +11,7 @@ type Props = {
export function DashboardHeader({ title, icon: IconComponent, className }: Props) {
return (
<div className={cn("relative flex items-center justify-center gap-x-2.5 md:justify-start", className)}>
<SidebarTrigger className="absolute left-0 md:hidden" />
<SidebarTrigger className="absolute start-0 md:hidden" />
<IconComponent weight="light" className="size-5" />
<h1 className="font-medium text-xl tracking-tight">{title}</h1>
</div>
+2 -2
View File
@@ -95,7 +95,7 @@ function SidebarItemList({ items }: SidebarItemListProps) {
<SidebarMenuButton asChild title={i18n.t(item.label)}>
<Link to={item.href} activeProps={{ className: "bg-sidebar-accent" }}>
{item.icon}
<span className="shrink-0 transition-[margin,opacity] duration-200 ease-in-out group-data-[collapsible=icon]:-ml-8 group-data-[collapsible=icon]:opacity-0">
<span className="shrink-0 transition-[margin,opacity] duration-200 ease-in-out group-data-[collapsible=icon]:-ms-8 group-data-[collapsible=icon]:opacity-0">
{i18n.t(item.label)}
</span>
</Link>
@@ -161,7 +161,7 @@ export function DashboardSidebar() {
</AvatarFallback>
</Avatar>
<div className="transition-[margin,opacity] duration-200 ease-in-out group-data-[collapsible=icon]:-ml-8 group-data-[collapsible=icon]:opacity-0">
<div className="transition-[margin,opacity] duration-200 ease-in-out group-data-[collapsible=icon]:-ms-8 group-data-[collapsible=icon]:opacity-0">
<p className="font-medium">{session.user.name}</p>
<p className="text-muted-foreground text-xs">{session.user.email}</p>
</div>
+2 -2
View File
@@ -73,7 +73,7 @@ function RouteComponent() {
<Separator />
<div className="flex items-center">
<div className="flex items-center gap-x-4">
<Combobox
value={sort}
options={sortOptions}
@@ -116,7 +116,7 @@ function RouteComponent() {
}}
/>
<Tabs className="ml-auto" value={view} onValueChange={onViewChange}>
<Tabs className="ltr:ms-auto rtl:me-auto" value={view} onValueChange={onViewChange}>
<TabsList>
<TabsTrigger value="grid" className="rounded-r-none">
<GridFourIcon />
+1 -1
View File
@@ -29,7 +29,7 @@ function RouteComponent() {
<SidebarProvider open={sidebarState} onOpenChange={handleSidebarOpenChange}>
<DashboardSidebar />
<main className="@container flex-1 p-4 md:pl-2">
<main className="@container flex-1 p-4 md:ps-2">
<Outlet />
</main>
</SidebarProvider>
@@ -104,7 +104,7 @@ function PasskeyItem({ passkey }: PasskeyItemProps) {
<TrashSimpleIcon />
</Button>
<span className="mx-2 truncate text-nowrap border-r pr-2 font-medium">{passkey.name ?? "1Password"}</span>
<span className="mx-2 truncate text-nowrap border-r pe-2 font-medium">{passkey.name ?? "1Password"}</span>
<span className="flex-1 truncate text-nowrap text-muted-foreground text-xs">
<Trans>Added on {passkey.createdAt.toLocaleDateString()}</Trans>