mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
d237504af9
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>
328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
|
import { ChevronRightIcon } from "lucide-react";
|
|
import type * as React from "react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export const MenuCreateHandle: typeof MenuPrimitive.createHandle =
|
|
MenuPrimitive.createHandle;
|
|
|
|
export const Menu: typeof MenuPrimitive.Root = MenuPrimitive.Root;
|
|
|
|
export const MenuPortal: typeof MenuPrimitive.Portal = MenuPrimitive.Portal;
|
|
|
|
export function MenuTrigger({
|
|
className,
|
|
children,
|
|
...props
|
|
}: MenuPrimitive.Trigger.Props): React.ReactElement {
|
|
return (
|
|
<MenuPrimitive.Trigger
|
|
className={className}
|
|
data-slot="menu-trigger"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</MenuPrimitive.Trigger>
|
|
);
|
|
}
|
|
|
|
export function MenuPopup({
|
|
children,
|
|
className,
|
|
sideOffset = 4,
|
|
align = "center",
|
|
alignOffset,
|
|
side = "bottom",
|
|
anchor,
|
|
portalProps,
|
|
...props
|
|
}: MenuPrimitive.Popup.Props & {
|
|
align?: MenuPrimitive.Positioner.Props["align"];
|
|
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
|
|
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
|
|
side?: MenuPrimitive.Positioner.Props["side"];
|
|
anchor?: MenuPrimitive.Positioner.Props["anchor"];
|
|
portalProps?: MenuPrimitive.Portal.Props;
|
|
}): React.ReactElement {
|
|
return (
|
|
<MenuPortal {...portalProps}>
|
|
<MenuPrimitive.Positioner
|
|
align={align}
|
|
alignOffset={alignOffset}
|
|
anchor={anchor}
|
|
className="z-50"
|
|
data-slot="menu-positioner"
|
|
side={side}
|
|
sideOffset={sideOffset}
|
|
>
|
|
<MenuPrimitive.Popup
|
|
className={cn(
|
|
"relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 outline-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] focus:outline-none dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
|
|
className,
|
|
)}
|
|
data-slot="menu-popup"
|
|
{...props}
|
|
>
|
|
<div className="max-h-(--available-height) w-full overflow-y-auto p-1">
|
|
{children}
|
|
</div>
|
|
</MenuPrimitive.Popup>
|
|
</MenuPrimitive.Positioner>
|
|
</MenuPortal>
|
|
);
|
|
}
|
|
|
|
export function MenuGroup(
|
|
props: MenuPrimitive.Group.Props,
|
|
): React.ReactElement {
|
|
return <MenuPrimitive.Group data-slot="menu-group" {...props} />;
|
|
}
|
|
|
|
export function MenuItem({
|
|
className,
|
|
inset,
|
|
variant = "default",
|
|
...props
|
|
}: MenuPrimitive.Item.Props & {
|
|
inset?: boolean;
|
|
variant?: "default" | "destructive";
|
|
}): React.ReactElement {
|
|
return (
|
|
<MenuPrimitive.Item
|
|
className={cn(
|
|
"flex min-h-8 cursor-default select-none items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-inset:ps-8 data-[variant=destructive]:text-destructive-foreground data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&>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]:-mx-0.5 [&>svg]:shrink-0",
|
|
className,
|
|
)}
|
|
data-inset={inset}
|
|
data-slot="menu-item"
|
|
data-variant={variant}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MenuCheckboxItem({
|
|
className,
|
|
children,
|
|
checked,
|
|
variant = "default",
|
|
...props
|
|
}: MenuPrimitive.CheckboxItem.Props & {
|
|
variant?: "default" | "switch";
|
|
}): React.ReactElement {
|
|
return (
|
|
<MenuPrimitive.CheckboxItem
|
|
checked={checked}
|
|
className={cn(
|
|
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 ps-2 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
variant === "switch"
|
|
? "grid-cols-[1fr_auto] gap-4 pe-1.5"
|
|
: "grid-cols-[.75rem_1fr] pe-4",
|
|
className,
|
|
)}
|
|
data-slot="menu-checkbox-item"
|
|
{...props}
|
|
>
|
|
{variant === "switch" ? (
|
|
<>
|
|
<span className="col-start-1">{children}</span>
|
|
<MenuPrimitive.CheckboxItemIndicator
|
|
className="inset-shadow-[0_1px_--theme(--color-black/4%)] inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(4)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(3)]"
|
|
keepMounted
|
|
>
|
|
<span className="pointer-events-none block aspect-square h-full in-[[data-slot=menu-checkbox-item][data-checked]]:origin-[var(--thumb-size)_50%] origin-left in-[[data-slot=menu-checkbox-item][data-checked]]:translate-x-[calc(var(--thumb-size)-4px)] in-[[data-slot=menu-checkbox-item]:active]:not-data-disabled:scale-x-110 in-[[data-slot=menu-checkbox-item]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.10)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s]" />
|
|
</MenuPrimitive.CheckboxItemIndicator>
|
|
</>
|
|
) : (
|
|
<>
|
|
<MenuPrimitive.CheckboxItemIndicator className="col-start-1 -ms-0.5">
|
|
<svg
|
|
aria-hidden="true"
|
|
fill="none"
|
|
height="24"
|
|
stroke="currentColor"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
viewBox="0 0 24 24"
|
|
width="24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
|
|
</svg>
|
|
</MenuPrimitive.CheckboxItemIndicator>
|
|
<span className="col-start-2">{children}</span>
|
|
</>
|
|
)}
|
|
</MenuPrimitive.CheckboxItem>
|
|
);
|
|
}
|
|
|
|
export function MenuRadioGroup(
|
|
props: MenuPrimitive.RadioGroup.Props,
|
|
): React.ReactElement {
|
|
return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
|
|
}
|
|
|
|
export function MenuRadioItem({
|
|
className,
|
|
children,
|
|
...props
|
|
}: MenuPrimitive.RadioItem.Props): React.ReactElement {
|
|
return (
|
|
<MenuPrimitive.RadioItem
|
|
className={cn(
|
|
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[.75rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
className,
|
|
)}
|
|
data-slot="menu-radio-item"
|
|
{...props}
|
|
>
|
|
<MenuPrimitive.RadioItemIndicator className="col-start-1 -ms-0.5">
|
|
<svg
|
|
aria-hidden="true"
|
|
fill="none"
|
|
height="24"
|
|
stroke="currentColor"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
viewBox="0 0 24 24"
|
|
width="24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
|
|
</svg>
|
|
</MenuPrimitive.RadioItemIndicator>
|
|
<span className="col-start-2">{children}</span>
|
|
</MenuPrimitive.RadioItem>
|
|
);
|
|
}
|
|
|
|
export function MenuGroupLabel({
|
|
className,
|
|
inset,
|
|
...props
|
|
}: MenuPrimitive.GroupLabel.Props & {
|
|
inset?: boolean;
|
|
}): React.ReactElement {
|
|
return (
|
|
<MenuPrimitive.GroupLabel
|
|
className={cn(
|
|
"px-2 py-1.5 font-medium text-muted-foreground text-xs data-inset:ps-9 sm:data-inset:ps-8",
|
|
className,
|
|
)}
|
|
data-inset={inset}
|
|
data-slot="menu-label"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MenuSeparator({
|
|
className,
|
|
...props
|
|
}: MenuPrimitive.Separator.Props): React.ReactElement {
|
|
return (
|
|
<MenuPrimitive.Separator
|
|
className={cn("mx-2 my-1 h-px bg-border", className)}
|
|
data-slot="menu-separator"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MenuShortcut({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"kbd">): React.ReactElement {
|
|
return (
|
|
<kbd
|
|
className={cn(
|
|
"ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest",
|
|
className,
|
|
)}
|
|
data-slot="menu-shortcut"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MenuSub(
|
|
props: MenuPrimitive.SubmenuRoot.Props,
|
|
): React.ReactElement {
|
|
return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />;
|
|
}
|
|
|
|
export function MenuSubTrigger({
|
|
className,
|
|
inset,
|
|
children,
|
|
...props
|
|
}: MenuPrimitive.SubmenuTrigger.Props & {
|
|
inset?: boolean;
|
|
}): React.ReactElement {
|
|
return (
|
|
<MenuPrimitive.SubmenuTrigger
|
|
className={cn(
|
|
"flex min-h-8 items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&>svg:not(:last-child)]:-mx-0.5 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
|
className,
|
|
)}
|
|
data-inset={inset}
|
|
data-slot="menu-sub-trigger"
|
|
{...props}
|
|
>
|
|
{children}
|
|
<ChevronRightIcon className="ms-auto -me-0.5 opacity-80 rtl:rotate-180" />
|
|
</MenuPrimitive.SubmenuTrigger>
|
|
);
|
|
}
|
|
|
|
export function MenuSubPopup({
|
|
className,
|
|
sideOffset = 0,
|
|
alignOffset,
|
|
align = "start",
|
|
...props
|
|
}: MenuPrimitive.Popup.Props & {
|
|
align?: MenuPrimitive.Positioner.Props["align"];
|
|
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
|
|
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
|
|
}): React.ReactElement {
|
|
const defaultAlignOffset = align !== "center" ? -5 : undefined;
|
|
|
|
return (
|
|
<MenuPopup
|
|
align={align}
|
|
alignOffset={alignOffset ?? defaultAlignOffset}
|
|
className={className}
|
|
data-slot="menu-sub-content"
|
|
side="inline-end"
|
|
sideOffset={sideOffset}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
MenuPrimitive,
|
|
MenuCreateHandle as DropdownMenuCreateHandle,
|
|
Menu as DropdownMenu,
|
|
MenuPortal as DropdownMenuPortal,
|
|
MenuTrigger as DropdownMenuTrigger,
|
|
MenuPopup as DropdownMenuContent,
|
|
MenuGroup as DropdownMenuGroup,
|
|
MenuItem as DropdownMenuItem,
|
|
MenuCheckboxItem as DropdownMenuCheckboxItem,
|
|
MenuRadioGroup as DropdownMenuRadioGroup,
|
|
MenuRadioItem as DropdownMenuRadioItem,
|
|
MenuGroupLabel as DropdownMenuLabel,
|
|
MenuSeparator as DropdownMenuSeparator,
|
|
MenuShortcut as DropdownMenuShortcut,
|
|
MenuSub as DropdownMenuSub,
|
|
MenuSubTrigger as DropdownMenuSubTrigger,
|
|
MenuSubPopup as DropdownMenuSubContent,
|
|
};
|