mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
4d6a5dc008
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>
146 lines
3.2 KiB
TypeScript
146 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import { Button as InputGroupButton } from "@/components/ui/button";
|
|
import {
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupInput,
|
|
InputGroupText,
|
|
} from "@/components/ui/input-group";
|
|
import { cn } from "@/lib/utils";
|
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
import type { ComponentProps } from "react";
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
|
|
interface SnippetContextType {
|
|
code: string;
|
|
}
|
|
|
|
const SnippetContext = createContext<SnippetContextType>({
|
|
code: "",
|
|
});
|
|
|
|
export type SnippetProps = ComponentProps<typeof InputGroup> & {
|
|
code: string;
|
|
};
|
|
|
|
export const Snippet = ({
|
|
code,
|
|
className,
|
|
children,
|
|
...props
|
|
}: SnippetProps) => {
|
|
const contextValue = useMemo(() => ({ code }), [code]);
|
|
|
|
return (
|
|
<SnippetContext.Provider value={contextValue}>
|
|
<InputGroup className={cn("font-mono", className)} {...props}>
|
|
{children}
|
|
</InputGroup>
|
|
</SnippetContext.Provider>
|
|
);
|
|
};
|
|
|
|
export type SnippetAddonProps = ComponentProps<typeof InputGroupAddon>;
|
|
|
|
export const SnippetAddon = (props: SnippetAddonProps) => (
|
|
<InputGroupAddon {...props} />
|
|
);
|
|
|
|
export type SnippetTextProps = ComponentProps<typeof InputGroupText>;
|
|
|
|
export const SnippetText = ({ className, ...props }: SnippetTextProps) => (
|
|
<InputGroupText
|
|
className={cn("pl-2 font-normal text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
|
|
export type SnippetInputProps = Omit<
|
|
ComponentProps<typeof InputGroupInput>,
|
|
"readOnly" | "value"
|
|
>;
|
|
|
|
export const SnippetInput = ({ className, ...props }: SnippetInputProps) => {
|
|
const { code } = useContext(SnippetContext);
|
|
|
|
return (
|
|
<InputGroupInput
|
|
className={cn("text-foreground", className)}
|
|
readOnly
|
|
value={code}
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export type SnippetCopyButtonProps = ComponentProps<typeof InputGroupButton> & {
|
|
onCopy?: () => void;
|
|
onError?: (error: Error) => void;
|
|
timeout?: number;
|
|
};
|
|
|
|
export const SnippetCopyButton = ({
|
|
onCopy,
|
|
onError,
|
|
timeout = 2000,
|
|
children,
|
|
className,
|
|
...props
|
|
}: SnippetCopyButtonProps) => {
|
|
const [isCopied, setIsCopied] = useState(false);
|
|
const timeoutRef = useRef<number>(0);
|
|
const { code } = useContext(SnippetContext);
|
|
|
|
const copyToClipboard = useCallback(async () => {
|
|
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
|
onError?.(new Error("Clipboard API not available"));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (!isCopied) {
|
|
await navigator.clipboard.writeText(code);
|
|
setIsCopied(true);
|
|
onCopy?.();
|
|
timeoutRef.current = window.setTimeout(
|
|
() => setIsCopied(false),
|
|
timeout
|
|
);
|
|
}
|
|
} catch (error) {
|
|
onError?.(error as Error);
|
|
}
|
|
}, [code, onCopy, onError, timeout, isCopied]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
window.clearTimeout(timeoutRef.current);
|
|
},
|
|
[]
|
|
);
|
|
|
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
|
|
|
return (
|
|
<InputGroupButton
|
|
aria-label="Copy"
|
|
className={className}
|
|
onClick={copyToClipboard}
|
|
size="icon-sm"
|
|
title="Copy"
|
|
{...props}
|
|
>
|
|
{children ?? <Icon className="size-3.5" size={14} />}
|
|
</InputGroupButton>
|
|
);
|
|
};
|