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>
97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useSyncExternalStore } from "react";
|
|
|
|
const BREAKPOINTS = {
|
|
"2xl": 1536,
|
|
"3xl": 1600,
|
|
"4xl": 2000,
|
|
lg: 1024,
|
|
md: 800,
|
|
sm: 640,
|
|
xl: 1280,
|
|
} as const;
|
|
|
|
type Breakpoint = keyof typeof BREAKPOINTS;
|
|
|
|
type BreakpointQuery =
|
|
| Breakpoint
|
|
| `max-${Breakpoint}`
|
|
| `${Breakpoint}:max-${Breakpoint}`;
|
|
|
|
function resolveMin(value: Breakpoint | number): string {
|
|
const px = typeof value === "number" ? value : BREAKPOINTS[value];
|
|
return `(min-width: ${px}px)`;
|
|
}
|
|
|
|
function resolveMax(value: Breakpoint | number): string {
|
|
const px = typeof value === "number" ? value : BREAKPOINTS[value];
|
|
return `(max-width: ${px - 1}px)`;
|
|
}
|
|
|
|
function parseQuery(
|
|
query: BreakpointQuery | MediaQueryInput | (string & {}),
|
|
): string {
|
|
if (typeof query !== "string") {
|
|
const parts: string[] = [];
|
|
if (query.min != null) parts.push(resolveMin(query.min));
|
|
if (query.max != null) parts.push(resolveMax(query.max));
|
|
if (query.pointer === "coarse") parts.push("(pointer: coarse)");
|
|
if (query.pointer === "fine") parts.push("(pointer: fine)");
|
|
if (parts.length === 0) return "(min-width: 0px)";
|
|
return parts.join(" and ");
|
|
}
|
|
|
|
if (query.startsWith("(")) return query;
|
|
|
|
const parts: string[] = [];
|
|
for (const segment of query.split(":")) {
|
|
if (segment.startsWith("max-")) {
|
|
const bp = segment.slice(4);
|
|
if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint));
|
|
} else if (segment in BREAKPOINTS) {
|
|
parts.push(resolveMin(segment as Breakpoint));
|
|
}
|
|
}
|
|
|
|
return parts.length > 0 ? parts.join(" and ") : query;
|
|
}
|
|
|
|
function getServerSnapshot(): boolean {
|
|
return false;
|
|
}
|
|
|
|
export type MediaQueryInput = {
|
|
min?: Breakpoint | number;
|
|
max?: Breakpoint | number;
|
|
/** Touch-like input (finger). Use "fine" for mouse/trackpad. */
|
|
pointer?: "coarse" | "fine";
|
|
};
|
|
|
|
export function useMediaQuery(
|
|
query: BreakpointQuery | MediaQueryInput | (string & {}),
|
|
): boolean {
|
|
const mediaQuery = parseQuery(query);
|
|
|
|
const subscribe = useCallback(
|
|
(callback: () => void) => {
|
|
if (typeof window === "undefined") return () => {};
|
|
const mql = window.matchMedia(mediaQuery);
|
|
mql.addEventListener("change", callback);
|
|
return () => mql.removeEventListener("change", callback);
|
|
},
|
|
[mediaQuery],
|
|
);
|
|
|
|
const getSnapshot = useCallback(() => {
|
|
if (typeof window === "undefined") return false;
|
|
return window.matchMedia(mediaQuery).matches;
|
|
}, [mediaQuery]);
|
|
|
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
}
|
|
|
|
export function useIsMobile(): boolean {
|
|
return useMediaQuery("max-md");
|
|
}
|