Files
temetro/frontend/hooks/use-media-query.ts
T
Khalid Abdi 4d6a5dc008 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>
2026-06-03 18:39:20 +03:00

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");
}