diff --git a/.cursor/rules/dyolink-overview.mdc b/.cursor/rules/dyolink-overview.mdc index 581523d..08edc17 100644 --- a/.cursor/rules/dyolink-overview.mdc +++ b/.cursor/rules/dyolink-overview.mdc @@ -24,6 +24,8 @@ Monorepo: `backend/` (NestJS + Prisma), `frontend/` (Next.js + next-intl), `infr All user-visible strings: `frontend/messages/en.json`, `fa.json`, `nl.json` — add keys to **all three**. +Dates/times/numbers: `frontend/src/lib/i18n/format.ts` + `useLocale()`. **Form date fields:** `AppDateInput` only (no native ``); wire format `YYYY-MM-DD`. **Filter selects:** `FORM_SELECT_CLASS` from `components/shared/formSelectStyles.ts` (chevron via `globals.css`). **Tables:** `components/ui/shared/Table.tsx` — use `text-start`/`text-end`/`text-center`, never physical `text-left`/`text-right`. Appointments: `ScheduleDayPicker`. Skill: `.cursor/skills/i18n-formatting/SKILL.md`. + ## Treatment / appointment colors Treatment-type colors and labels: `components/shared/treatmentTypeDisplay.ts` + `catalog-type-colors.ts`. UI badges: `components/ui/treatment/TreatmentTypeBadge.tsx`. diff --git a/.cursor/rules/frontend-components.mdc b/.cursor/rules/frontend-components.mdc index 2714891..f338867 100644 --- a/.cursor/rules/frontend-components.mdc +++ b/.cursor/rules/frontend-components.mdc @@ -51,3 +51,14 @@ Reference: `app/.../treatment/page.tsx` + `components/ui/treatment/TreatmentWork 1. Check `components/ui/shared/` for an existing primitive. 2. Check the feature's `ui/{feature}/` folder for an existing pattern. 3. Add i18n keys to en, fa, and nl. + +## Shared form & table primitives + +| Need | Use | +|------|-----| +| Date filter / due date field | `AppDateInput` (`ui/shared/`) — not `` | +| Filter or inline `` | `FORM_SELECT_CLASS` from `components/shared/formSelectStyles.ts` | +| Tiny select (sort dir, etc.) | `FORM_SELECT_COMPACT_CLASS` | +| Desktop data table | `Table` (`ui/shared/Table.tsx`) — logical alignment only | + +Styles for `.form-select` chevrons live in `styles/globals.css`. Do not duplicate chevron icons on raw selects. diff --git a/.cursor/skills/i18n-formatting/SKILL.md b/.cursor/skills/i18n-formatting/SKILL.md new file mode 100644 index 0000000..086c398 --- /dev/null +++ b/.cursor/skills/i18n-formatting/SKILL.md @@ -0,0 +1,80 @@ +# i18n formatting (dates, times, numbers) + +Use when adding or changing user-visible dates/times/numbers, RTL layout, or locale-specific pickers. + +## Display formatting + +- Module: `frontend/src/lib/i18n/format.ts` +- Hook: `frontend/src/lib/hooks/useAppFormatters.ts` (`useLocale()` inside) +- **Never** use raw `toLocaleDateString` / `toLocaleTimeString` / bare `Intl.DateTimeFormat(undefined, …)` in UI. + +| Helper | Use for | +|--------|---------| +| `formatAppDate` | Date-only labels | +| `formatAppTime` | Time-only labels | +| `formatAppDateTime` | Combined stamp | +| `formatAppTimeRange` | Appointment ranges | +| `formatAppTableDate` | Table cells | +| `formatAppNumber` | Counts, amounts (grouped) | +| `formatAppInteger` | Calendar year/day — no comma grouping | +| `formatAppPickerDateLabel` | Calendar trigger button label | +| `APP_DATE.*` | Shared preset option objects | + +## Form controls (dates & selects) + +| Piece | Location / use | +|-------|----------------| +| **`AppDateInput`** | All form/filter date fields — **every locale**; masked typing + calendar popup; wire value `YYYY-MM-DD` or empty | +| **`FORM_DATE_INPUT_CLASS`** | Visual shell for date input (same padding/inset as selects; no CSS chevron) | +| **`FORM_SELECT_CLASS`** | Native `` filters/fields — `ps-3 pe-10`, chevron from `globals.css` | +| **`FORM_SELECT_COMPACT_CLASS`** | Tiny selects (e.g. sort direction `↓`/`↑`) — symmetric `px-2`, no chevron gutter | +| **`Dropdown`** | Labeled form select — only where already used; prefer `FORM_SELECT_CLASS` for new filters | +| **`CompactSelect`** | Year/month/day sub-selects inside calendar panels | + +**`AppDateInput` behavior** + +- `fa`: Jalali display `YYYY/MM/DD` (Persian digits), parse/mask in `persianCalendar.ts` +- `en` / `nl`: Gregorian display `YYYY-MM-DD`, parse/mask in `dateInputFormat.ts` +- Calendar icon at **`end-3`** (matches select chevron inset); text uses **`text-start`** (logical — right in RTL, left in LTR) +- Popup: **`CalendarDayPartsPanel`** (shared with schedule picker) +- **Do not** add native `` — one component for all locales + +**Select chevron** + +- Defined once in `frontend/src/styles/globals.css` on `.form-select:not(.form-select-no-chevron)` +- RTL: `background-position: left 0.75rem center`; LTR: `right 0.75rem center` +- `text-align: start` on selects + +## Calendar / appointment pickers + +| Component | Use for | +|-----------|---------| +| `ScheduleDayPicker` | Appointments strip — nav arrows + today toggle + expandable panel | +| `CalendarDaySelect` | Navigator wrapper (arrows + panel) | +| `CalendarDayPartsPanel` | Year / month / day row — used by schedule picker and `AppDateInput` | + +Persian (`fa`): Jalali calendar + `arabext` digits via Intl (`usesPersianCalendar`). Internal model stays **`Date` at local midnight** (Gregorian) — APIs unchanged. + +- Conversion: `frontend/src/lib/i18n/persianCalendar.ts` +- Gregorian typing: `frontend/src/lib/i18n/dateInputFormat.ts` +- Gregorian month labels: `schedule.monthJanuary` … message keys + +## RTL + +- `dir` / `lang` on `` from `app/[locale]/layout.tsx` +- `isRtlLocale` in `frontend/src/i18n/routing.ts` +- Use **logical** CSS: `text-start`, `text-end`, `ps-*`, `pe-*`, `ms-*`, `me-*` +- **`Table`**: default `[&_th]:text-start [&_td]:text-start`; override with `text-center` or `text-end` on cells — **never** `text-left` / `text-right` on headers (causes header/body column drift in RTL) +- Minimal overrides in `globals.css` — avoid double-mirroring (no extra `row-reverse` on shells that already inherit `direction: rtl`) + +## i18n strings + +- User-facing copy: `frontend/messages/{en,fa,nl}.json` — all three locales. + +## Verify + +```bash +cd frontend && npx tsc --noEmit +``` + +Manual: switch to Persian — Cases date filters, Tasks filter row (single line + sort visible), Staff/Orgs table columns aligned; switch to English — date fields match adjacent dropdown alignment. diff --git a/AGENTS.md b/AGENTS.md index 7154132..dfe31c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,8 @@ frontend/src/ **Example thin page:** `app/.../treatment/page.tsx` → imports `TreatmentWorkspace` from `components/ui/treatment/`. +**i18n formatting:** Display dates/times/numbers via `lib/i18n/format.ts` + `useLocale()`. Form dates: **`AppDateInput`** (all locales — same component, masked typing + calendar popup). Appointments strip: **`ScheduleDayPicker`**. Filter ``s: **`FORM_SELECT_CLASS`**; data tables: **`Table`** with logical `text-start`/`text-end` (not `text-left`/`text-right`). Skill: `.cursor/skills/i18n-formatting/SKILL.md`. + **Treatment tab:** Preview and editable form are **separate** until the user clicks **Load into workspace** on a history item. See `.cursor/skills/treatment-workspace/SKILL.md` before changing that flow. **Treatment lab rules (quick ref):** @@ -75,6 +77,7 @@ Errors: `AppException` + `ErrorCode` → frontend `getUserFacingError()`. Never | `.cursor/skills/lab-notifications/` | Tab badges: LabCaseActivity, tab-counts API, read cursors | | `.cursor/skills/frontend-structure/` | Moving components, auditing folder layout | | `.cursor/skills/api-errors/` | New backend errors + frontend translations | +| `.cursor/skills/i18n-formatting/` | Dates, times, numbers, Jalali picker, RTL formatting | ## Subagents (Task tool) diff --git a/frontend/src/app/[locale]/(dashboard)/layout.tsx b/frontend/src/app/[locale]/(dashboard)/layout.tsx index d16b2c1..7879d56 100644 --- a/frontend/src/app/[locale]/(dashboard)/layout.tsx +++ b/frontend/src/app/[locale]/(dashboard)/layout.tsx @@ -72,7 +72,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod return ( - + {sidebarOpen ? ( ({ locale })); } +const vazirmatn = Vazirmatn({ + subsets: ['arabic'], + variable: '--font-vazirmatn', + display: 'swap', +}); + +const notoSansArabic = Noto_Sans_Arabic({ + subsets: ['arabic'], + variable: '--font-noto-sans-arabic', + display: 'swap', +}); + export default async function LocaleLayout({ children, params, @@ -42,9 +55,20 @@ export default async function LocaleLayout({ const messages = await getMessages(); const themeInit = `(function(){try{var k=${JSON.stringify(THEME_STORAGE_KEY)};var t=localStorage.getItem(k);document.documentElement.setAttribute('data-theme',t==='light'||t==='dark'?t:'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();`; + const dir = isRtlLocale(locale) ? 'rtl' : 'ltr'; + const fontSans = isRtlLocale(locale) + ? 'var(--font-vazirmatn), var(--font-noto-sans-arabic), system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif' + : 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif'; return ( - +