Files
Khalid Abdi d237504af9 frontend: add Somali, Arabic (RTL) & German languages
Add three UI locales (so/ar/de) with full ~1,660-key translations alongside
en/fr, selectable in Settings → Profile. Arabic gets full right-to-left support:

- config.ts registers the locales and exports a `dirFor` helper; an inline
  <head> script in layout.tsx sets <html dir/lang> before first paint (no RTL
  flash), and i18n-provider keeps them in sync on language change.
- ~160 physical direction utilities converted to logical (ms/me/ps/pe/
  start/end/text-start/text-end); directional chevrons/arrows get rtl:rotate-180;
  chat-bubble align variants fixed to logical.
- IBM Plex Sans Arabic appended to the sans/heading font stacks for
  per-character Arabic fallback.
- Language persists to the backend user_settings and re-applies on sign-in so it
  roams across devices (localStorage stays the offline source of truth).
- New scripts/check-locales.mjs (npm run check-locales) enforces key/placeholder
  parity and Arabic CLDR plural completeness.

Bump to 0.3.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:03:13 +03:00

360 lines
12 KiB
CSS

@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
/* Append the Arabic face so Arabic codepoints fall through per-character even
in LTR locales; Latin text still renders in Inter. See app/layout.tsx. */
--font-sans: var(--font-sans), var(--font-arabic);
--font-mono: var(--font-mono);
--font-heading: var(--font-heading), var(--font-arabic);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
--color-warning-foreground: var(--warning-foreground);
--color-warning: var(--warning);
--color-success-foreground: var(--success-foreground);
--color-success: var(--success);
--color-info-foreground: var(--info-foreground);
--color-info: var(--info);
--color-destructive-foreground: var(--destructive-foreground);
--animate-skeleton: skeleton 2s -1s infinite linear;
@keyframes skeleton {
to {
background-position: -200% 0;
}
}
--animate-toast-success-odd: toast-success-odd 0.32s cubic-bezier(0.5, 1, 0.89, 1);
--animate-toast-success-even: toast-success-even 0.32s cubic-bezier(0.5, 1, 0.89, 1);
--animate-toast-error-odd: toast-error-odd 0.28s cubic-bezier(0.5, 1, 0.89, 1);
--animate-toast-error-even: toast-error-even 0.28s cubic-bezier(0.5, 1, 0.89, 1)
;
--color-chart-label: var(--chart-label);
--color-chart-ring-background: var(--chart-ring-background);
--color-chart-marker-foreground: var(--chart-marker-foreground);
--color-chart-marker-border: var(--chart-marker-border);
--color-chart-marker-background: var(--chart-marker-background);
--color-chart-tooltip-muted: var(--chart-tooltip-muted);
--color-chart-tooltip-foreground: var(--chart-tooltip-foreground);
--color-chart-tooltip-background: var(--chart-tooltip-background);
--chart-brush-border: var(--chart-brush-border);
--color-chart-grid: var(--chart-grid);
--color-chart-crosshair: var(--chart-crosshair);
--chart-line-secondary: var(--chart-line-secondary);
--chart-line-primary: var(--chart-line-primary);
--color-chart-foreground-muted: var(--chart-foreground-muted);
--color-chart-foreground: var(--chart-foreground);
--color-chart-background: var(--chart-background);
@keyframes toast-success-odd {
0% {
scale: 1;}
30% {
scale: 1.025;}
60% {
scale: 0.99;}
100% {
scale: 1;}}
@keyframes toast-error-odd {
0% {
translate: 0 0;}
25% {
translate: -3px 0;}
50% {
translate: 3px 0;}
75% {
translate: -3px 0;}
100% {
translate: 0 0;}}
@keyframes toast-success-even {
0% {
scale: 1;}
30% {
scale: 1.025;}
60% {
scale: 0.99;}
100% {
scale: 1;}}
@keyframes toast-error-even {
0% {
translate: 0 0;}
25% {
translate: -3px 0;}
50% {
translate: 3px 0;}
75% {
translate: -3px 0;}
100% {
translate: 0 0;}}}
:root {
--card: var(--color-white);
--card-foreground: var(--color-neutral-800);
--popover: var(--color-white);
--popover-foreground: var(--color-neutral-800);
--primary: var(--color-neutral-800);
--primary-foreground: var(--color-neutral-50);
--secondary: --alpha(var(--color-black) / 4%);
--secondary-foreground: var(--color-neutral-800);
--muted: --alpha(var(--color-black) / 4%);
--muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black));
--accent: --alpha(var(--color-black) / 4%);
--accent-foreground: var(--color-neutral-800);
--destructive: var(--color-red-500);
--border: --alpha(var(--color-black) / 8%);
--input: --alpha(var(--color-black) / 10%);
--ring: var(--color-neutral-400);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--radius: 0.875rem;
--sidebar: var(--color-neutral-50);
--sidebar-foreground: var(--color-neutral-800);
--sidebar-primary: var(--color-neutral-800);
--sidebar-primary-foreground: var(--color-neutral-50);
--sidebar-accent: --alpha(var(--color-black) / 4%);
--sidebar-accent-foreground: var(--color-neutral-800);
--sidebar-border: --alpha(var(--color-black) / 8%);
--sidebar-ring: var(--color-neutral-400);
--background: var(--color-white);
--foreground: var(--color-neutral-800);
--destructive-foreground: var(--color-red-700);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-700);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-700);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-700);
--chart-background: oklch(1 0 0);
--chart-foreground: oklch(0.145 0.004 285);
--chart-foreground-muted: oklch(0.55 0.014 260);
--chart-line-primary: var(--chart-1);
--chart-line-secondary: var(--chart-2);
--chart-crosshair: oklch(0.4 0.1828 274.34);
--chart-grid: oklch(0.9 0 0);
--chart-brush-border: var(--chart-grid);
--chart-tooltip-background: oklch(0.21 0.006 285 / 0.8);
--chart-tooltip-foreground: oklch(0.985 0 0);
--chart-tooltip-muted: oklch(0.65 0.01 260);
--chart-marker-background: oklch(0.97 0.005 260);
--chart-marker-border: oklch(0.85 0.01 260);
--chart-marker-foreground: oklch(0.3 0.01 260);
--chart-ring-background: oklch(0.9 0.005 260 / 0.25);
--chart-label: oklch(0.45 0.01 260);
}
/* COSS neutral dark palette: near-black canvas, neutral accent, hairline borders. */
.dark {
--background: color-mix(in srgb, var(--color-neutral-950) 95%, var(--color-white));
--foreground: var(--color-neutral-100);
--card: color-mix(in srgb, var(--background) 98%, var(--color-white));
--card-foreground: var(--color-neutral-100);
--popover: color-mix(in srgb, var(--background) 98%, var(--color-white));
--popover-foreground: var(--color-neutral-100);
--primary: var(--color-neutral-100);
--primary-foreground: var(--color-neutral-800);
--secondary: --alpha(var(--color-white) / 4%);
--secondary-foreground: var(--color-neutral-100);
--muted: --alpha(var(--color-white) / 4%);
--muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white));
--accent: --alpha(var(--color-white) / 4%);
--accent-foreground: var(--color-neutral-100);
--destructive: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
--border: --alpha(var(--color-white) / 6%);
--input: --alpha(var(--color-white) / 8%);
--ring: var(--color-neutral-500);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--sidebar: var(--color-neutral-950);
--sidebar-foreground: var(--color-neutral-100);
--sidebar-primary: var(--color-neutral-100);
--sidebar-primary-foreground: var(--color-neutral-800);
--sidebar-accent: --alpha(var(--color-white) / 4%);
--sidebar-accent-foreground: var(--color-neutral-100);
--sidebar-border: --alpha(var(--color-white) / 6%);
--sidebar-ring: var(--color-neutral-500);
--destructive-foreground: var(--color-red-400);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-400);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-400);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-400);
--chart-background: oklch(0.145 0 0);
--chart-foreground: oklch(0.45 0 0);
--chart-foreground-muted: oklch(0.65 0.01 260);
--chart-crosshair: oklch(0.45 0 0);
--chart-grid: oklch(0.25 0 0);
--chart-brush-border: var(--chart-grid);
--chart-marker-background: oklch(0.25 0.01 260);
--chart-marker-border: oklch(0.4 0.01 260);
--chart-marker-foreground: oklch(0.9 0 0);
--chart-ring-background: oklch(0.35 0.01 260 / 0.25);
--chart-label: oklch(0.75 0.01 260);
}
@layer base {
* {
@apply border-border outline-ring/50;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--foreground) 20%, transparent) transparent;
}
*::-webkit-scrollbar {
width: 8px;
height: 8px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--foreground) 20%, transparent);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in srgb, var(--foreground) 32%, transparent);
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
/*
* Rich-text styling for the Notes Tiptap editor. The app has no typography
* plugin, and Tailwind's preflight resets headings/lists — so without this,
* H1/H2/lists render identically to body text. Scoped to `.note-content`
* (set on the editor's contenteditable) and themed via semantic tokens.
*/
.note-content {
@apply text-foreground;
font-size: 0.95rem;
line-height: 1.7;
}
.note-content:focus {
outline: none;
}
.note-content > :first-child {
margin-top: 0;
}
.note-content p {
margin: 0.5rem 0;
}
.note-content h1 {
@apply font-heading font-bold;
font-size: 1.6rem;
line-height: 1.25;
margin: 1.2rem 0 0.6rem;
}
.note-content h2 {
@apply font-heading font-semibold;
font-size: 1.3rem;
line-height: 1.3;
margin: 1rem 0 0.5rem;
}
.note-content h3 {
@apply font-heading font-semibold;
font-size: 1.1rem;
margin: 0.9rem 0 0.4rem;
}
.note-content ul,
.note-content ol {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
.note-content ul {
list-style: disc;
}
.note-content ol {
list-style: decimal;
}
.note-content li {
margin: 0.2rem 0;
}
.note-content li > p {
margin: 0;
}
.note-content blockquote {
@apply border-border text-muted-foreground;
border-left-width: 3px;
margin: 0.75rem 0;
padding-left: 1rem;
}
.note-content a {
@apply text-primary underline underline-offset-2;
}
.note-content strong {
font-weight: 600;
}
.note-content code {
@apply bg-muted font-mono;
border-radius: 0.375rem;
font-size: 0.85em;
padding: 0.1rem 0.35rem;
}
.note-content pre {
@apply bg-muted font-mono;
border-radius: 0.6rem;
margin: 0.75rem 0;
overflow-x: auto;
padding: 0.75rem 1rem;
}
.note-content pre code {
background: transparent;
padding: 0;
}
.note-content hr {
@apply border-border;
border-top-width: 1px;
margin: 1.25rem 0;
}
/* Tiptap placeholder (empty document) */
.note-content p.is-editor-empty:first-child::before {
@apply text-muted-foreground;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}