mirror of
https://github.com/temetro/temetro.git
synced 2026-09-03 22:35:27 +00:00
Migrate UI to COSS components + COSS neutral theme; add i18next
Components: - Replace components/ui primitives with COSS equivalents from the @coss/* shadcn registry. Add menu/preview-card/group; remove the superseded dropdown-menu, hover-card, button-group; keep carousel (no COSS equivalent). - Migrate live call sites to canonical COSS APIs preserving layout/behavior: sidebar menus (nav-user, team-switcher, nav-notifications), chat-input radio menu, patient dialogs/cards, auth forms (FieldGroup -> flex column), settings. - ai-elements: migrate the live conversation/message with behavior parity (Tooltip via render, Group/GroupText); repoint dormant files (hover-card -> preview-card, dropdown-menu -> menu, button-group -> group, InputGroupButton -> Button). Residual pre-existing Base UI type drift stays behind ignoreBuildErrors. Theme: - Adopt COSS default neutral tokens in globals.css with light + dark palettes. - Add next-themes (defaultTheme=dark, enableSystem) and drop the forced dark class. Align font variables to the COSS contract (--font-sans/-heading/-mono). Make scrollbars theme-aware. i18n: - Add i18next + react-i18next (lib/i18n/config.ts, locales/en/translation.json, components/i18n-provider.tsx mounted in layout). Convert auth forms, sidebar nav, and settings tabs to useTranslation() as the reference pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
"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" />
|
||||
</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,
|
||||
};
|
||||
Reference in New Issue
Block a user