nico-martin HF Staff commited on
Commit
0842d68
·
0 Parent(s):

Initial commit: TranslateGemma browser translator with Transformers.js

Browse files

- Complete translation UI with 56 languages
- Mobile-responsive design
- Private & offline-capable translation
- URL hash sharing functionality
- Auto-translate with debounce
- Copy and share features
- Google Translate-inspired design

.agents/skills/tailwind-v4/SKILL.md ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: tailwind-v4
3
+ description: Tailwind CSS v4 with CSS-first configuration and design tokens. Use when setting up Tailwind v4, defining theme variables, using OKLCH colors, or configuring dark mode. Triggers on @theme, @tailwindcss/vite, oklch, CSS variables, --color-, tailwind v4.
4
+ ---
5
+
6
+ # Tailwind CSS v4 Best Practices
7
+
8
+ ## Quick Reference
9
+
10
+ **Vite Plugin Setup**:
11
+ ```ts
12
+ // vite.config.ts
13
+ import tailwindcss from '@tailwindcss/vite';
14
+ import { defineConfig } from 'vite';
15
+
16
+ export default defineConfig({
17
+ plugins: [tailwindcss()],
18
+ });
19
+ ```
20
+
21
+ **CSS Entry Point**:
22
+ ```css
23
+ /* src/index.css */
24
+ @import 'tailwindcss';
25
+ ```
26
+
27
+ **@theme Inline Directive**:
28
+ ```css
29
+ @theme inline {
30
+ --color-primary: oklch(60% 0.24 262);
31
+ --color-surface: oklch(98% 0.002 247);
32
+ }
33
+ ```
34
+
35
+ ## Key Differences from v3
36
+
37
+ | Feature | v3 | v4 |
38
+ |---------|----|----|
39
+ | Configuration | tailwind.config.js | @theme in CSS |
40
+ | Build Tool | PostCSS plugin | @tailwindcss/vite |
41
+ | Colors | rgb() / hsl() | oklch() (default) |
42
+ | Theme Extension | extend: {} in JS | CSS variables |
43
+ | Dark Mode | darkMode config option | CSS variants |
44
+
45
+ ## @theme Directive Modes
46
+
47
+ ### default (standard mode)
48
+ Generates CSS variables that can be referenced elsewhere:
49
+ ```css
50
+ @theme {
51
+ --color-brand: oklch(60% 0.24 262);
52
+ }
53
+
54
+ /* Generates: :root { --color-brand: oklch(...); } */
55
+ /* Usage: text-brand → color: var(--color-brand) */
56
+ ```
57
+
58
+ **Note**: You can also use `@theme default` explicitly to mark theme values that can be overridden by non-default @theme declarations.
59
+
60
+ ### inline
61
+ Inlines values directly without CSS variables (better performance):
62
+ ```css
63
+ @theme inline {
64
+ --color-brand: oklch(60% 0.24 262);
65
+ }
66
+
67
+ /* Usage: text-brand → color: oklch(60% 0.24 262) */
68
+ ```
69
+
70
+ ### reference
71
+ Inlines values as fallbacks without emitting CSS variables:
72
+ ```css
73
+ @theme reference {
74
+ --color-internal: oklch(50% 0.1 180);
75
+ }
76
+
77
+ /* No :root variable, but utilities use fallback */
78
+ /* Usage: bg-internal → background-color: var(--color-internal, oklch(50% 0.1 180)) */
79
+ ```
80
+
81
+ ## OKLCH Color Format
82
+
83
+ OKLCH provides perceptually uniform colors with better consistency across hues:
84
+
85
+ ```css
86
+ oklch(L% C H)
87
+ ```
88
+
89
+ - **L (Lightness)**: 0% (black) to 100% (white)
90
+ - **C (Chroma)**: 0 (gray) to ~0.4 (vibrant)
91
+ - **H (Hue)**: 0-360 degrees (red → yellow → green → blue → magenta)
92
+
93
+ **Examples**:
94
+ ```css
95
+ --color-sky-500: oklch(68.5% 0.169 237.323); /* Bright blue */
96
+ --color-red-600: oklch(57.7% 0.245 27.325); /* Vibrant red */
97
+ --color-zinc-900: oklch(21% 0.006 285.885); /* Near-black gray */
98
+ ```
99
+
100
+ ## CSS Variable Naming
101
+
102
+ Tailwind v4 uses double-dash CSS variable naming conventions:
103
+
104
+ ```css
105
+ @theme {
106
+ /* Colors: --color-{name}-{shade} */
107
+ --color-primary-500: oklch(60% 0.24 262);
108
+
109
+ /* Spacing: --spacing multiplier */
110
+ --spacing: 0.25rem; /* Base unit for spacing scale */
111
+
112
+ /* Fonts: --font-{family} */
113
+ --font-display: 'Inter Variable', system-ui, sans-serif;
114
+
115
+ /* Breakpoints: --breakpoint-{size} */
116
+ --breakpoint-lg: 64rem;
117
+
118
+ /* Custom animations: --animate-{name} */
119
+ --animate-fade-in: fade-in 0.3s ease-out;
120
+ }
121
+ ```
122
+
123
+ ## No Config Files Needed
124
+
125
+ Tailwind v4 eliminates configuration files:
126
+
127
+ - **No `tailwind.config.js`** - Use @theme in CSS instead
128
+ - **No `postcss.config.js`** - Use @tailwindcss/vite plugin
129
+ - **TypeScript support** - Add `@types/node` for path resolution
130
+
131
+ ```json
132
+ {
133
+ "devDependencies": {
134
+ "@tailwindcss/vite": "^4.0.0",
135
+ "@types/node": "^22.0.0",
136
+ "tailwindcss": "^4.0.0",
137
+ "vite": "^6.0.0"
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## Progressive Disclosure
143
+
144
+ - **Setup & Installation**: See [references/setup.md](references/setup.md) for Vite plugin configuration, package setup, TypeScript config
145
+ - **Theming & Design Tokens**: See [references/theming.md](references/theming.md) for @theme modes, color palettes, custom fonts, animations
146
+ - **Dark Mode Strategies**: See [references/dark-mode.md](references/dark-mode.md) for media queries, class-based, attribute-based approaches
147
+
148
+ ## Decision Guide
149
+
150
+ ### When to use @theme inline vs default
151
+
152
+ **Use `@theme inline`**:
153
+ - Better performance (no CSS variable overhead)
154
+ - Static color values that won't change
155
+ - Animation keyframes with multiple values
156
+ - Utilities that need direct value inlining
157
+
158
+ **Use `@theme` (default)**:
159
+ - Dynamic theming with JavaScript
160
+ - CSS variable references in custom CSS
161
+ - Values that change based on context
162
+ - Better debugging (inspect CSS variables in DevTools)
163
+
164
+ ### When to use @theme reference
165
+
166
+ **Use `@theme reference`**:
167
+ - Provide fallback values without CSS variable overhead
168
+ - Values that should work even if variable isn't defined
169
+ - Reducing :root bloat while maintaining utility support
170
+ - Combining with inline for direct value substitution
171
+
172
+ ## Common Patterns
173
+
174
+ ### Two-Tier Variable System
175
+
176
+ Semantic variables that map to design tokens:
177
+
178
+ ```css
179
+ @theme {
180
+ /* Design tokens (OKLCH colors) */
181
+ --color-blue-600: oklch(54.6% 0.245 262.881);
182
+ --color-slate-800: oklch(27.9% 0.041 260.031);
183
+
184
+ /* Semantic mappings */
185
+ --color-primary: var(--color-blue-600);
186
+ --color-surface: var(--color-slate-800);
187
+ }
188
+
189
+ /* Usage: bg-primary, bg-surface */
190
+ ```
191
+
192
+ ### Custom Font Configuration
193
+
194
+ ```css
195
+ @theme {
196
+ --font-display: 'Inter Variable', system-ui, sans-serif;
197
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
198
+
199
+ --font-display--font-variation-settings: 'wght' 400;
200
+ --font-display--font-feature-settings: 'cv02', 'cv03', 'cv04';
201
+ }
202
+
203
+ /* Usage: font-display, font-mono */
204
+ ```
205
+
206
+ ### Animation Keyframes
207
+
208
+ ```css
209
+ @theme inline {
210
+ --animate-beacon: beacon 2s ease-in-out infinite;
211
+
212
+ @keyframes beacon {
213
+ 0%, 100% {
214
+ opacity: 1;
215
+ transform: scale(1);
216
+ }
217
+ 50% {
218
+ opacity: 0.5;
219
+ transform: scale(1.05);
220
+ }
221
+ }
222
+ }
223
+
224
+ /* Usage: animate-beacon */
225
+ ```
.agents/skills/tailwind-v4/references/dark-mode.md ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dark Mode Strategies
2
+
3
+ ## Contents
4
+
5
+ - [Media Query Strategy](#media-query-strategy)
6
+ - [Class-Based Strategy](#class-based-strategy)
7
+ - [Attribute-Based Strategy](#attribute-based-strategy)
8
+ - [Theme Switching Implementation](#theme-switching-implementation)
9
+ - [Respecting User Preferences](#respecting-user-preferences)
10
+
11
+ ---
12
+
13
+ ## Media Query Strategy
14
+
15
+ Use the system preference for dark mode detection.
16
+
17
+ ### Configuration
18
+
19
+ **Default behavior** (v4):
20
+ ```css
21
+ /* No configuration needed - dark: variant works by default */
22
+ @import 'tailwindcss';
23
+ ```
24
+
25
+ **Generated CSS**:
26
+ ```css
27
+ @media (prefers-color-scheme: dark) {
28
+ .dark\:bg-slate-900 {
29
+ background-color: oklch(20.8% 0.042 265.755);
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Usage
35
+
36
+ ```tsx
37
+ export function Card({ children }: { children: React.ReactNode }) {
38
+ return (
39
+ <div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-50">
40
+ {children}
41
+ </div>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ### Pros & Cons
47
+
48
+ **Pros**:
49
+ - Respects system preference automatically
50
+ - No JavaScript needed
51
+ - Simple implementation
52
+ - No FOUC (flash of unstyled content)
53
+
54
+ **Cons**:
55
+ - Users can't override system preference
56
+ - No manual toggle control
57
+ - Changes when system setting changes
58
+
59
+ ### When to Use
60
+
61
+ - Documentation sites
62
+ - Content-focused websites
63
+ - Apps where system preference is preferred
64
+ - No need for manual theme switching
65
+
66
+ ## Class-Based Strategy
67
+
68
+ Toggle dark mode with a `.dark` class on the root element.
69
+
70
+ ### Configuration
71
+
72
+ **Pure v4 approach**: Use a v3 config file with darkMode setting:
73
+
74
+ ```js
75
+ // tailwind.config.js (for v3 compatibility)
76
+ module.exports = {
77
+ darkMode: 'class', // or 'selector' (same as 'class')
78
+ };
79
+ ```
80
+
81
+ **Note**: In pure v4, the default `dark:` variant uses media queries (`prefers-color-scheme: dark`). To use class-based dark mode, you need to either:
82
+ 1. Use a v3 config file with `darkMode: 'class'` (shown above)
83
+ 2. Use `@import "tailwindcss/compat"` and provide a config
84
+ 3. Define a custom variant with `@custom-variant`
85
+
86
+ ### Generated CSS
87
+
88
+ ```css
89
+ .dark .dark\:bg-slate-900 {
90
+ background-color: oklch(20.8% 0.042 265.755);
91
+ }
92
+ ```
93
+
94
+ ### Usage
95
+
96
+ ```tsx
97
+ export function App() {
98
+ const [isDark, setIsDark] = useState(false);
99
+
100
+ useEffect(() => {
101
+ if (isDark) {
102
+ document.documentElement.classList.add('dark');
103
+ } else {
104
+ document.documentElement.classList.remove('dark');
105
+ }
106
+ }, [isDark]);
107
+
108
+ return (
109
+ <div className="bg-white dark:bg-slate-900">
110
+ <button onClick={() => setIsDark(!isDark)}>
111
+ Toggle Theme
112
+ </button>
113
+ </div>
114
+ );
115
+ }
116
+ ```
117
+
118
+ ### Pros & Cons
119
+
120
+ **Pros**:
121
+ - Full JavaScript control
122
+ - User can override system preference
123
+ - Easy to implement manual toggle
124
+ - Widely supported pattern
125
+
126
+ **Cons**:
127
+ - Requires JavaScript
128
+ - Potential FOUC without SSR handling
129
+ - Class management overhead
130
+
131
+ ### When to Use
132
+
133
+ - Applications with theme toggle
134
+ - User preference override needed
135
+ - Dashboard/admin interfaces
136
+ - Apps with per-user theme settings
137
+
138
+ ## Attribute-Based Strategy
139
+
140
+ Use a `data-theme` attribute for more semantic theming.
141
+
142
+ ### Configuration (v3 compat)
143
+
144
+ ```js
145
+ // tailwind.config.js (v3 compat mode)
146
+ module.exports = {
147
+ darkMode: ['class', '[data-theme="dark"]'],
148
+ };
149
+ ```
150
+
151
+ ### Generated CSS
152
+
153
+ ```css
154
+ [data-theme="dark"] .dark\:bg-slate-900 {
155
+ background-color: oklch(20.8% 0.042 265.755);
156
+ }
157
+ ```
158
+
159
+ ### Usage
160
+
161
+ ```tsx
162
+ export function App() {
163
+ const [theme, setTheme] = useState<'light' | 'dark'>('light');
164
+
165
+ useEffect(() => {
166
+ document.documentElement.setAttribute('data-theme', theme);
167
+ }, [theme]);
168
+
169
+ return (
170
+ <div className="bg-white dark:bg-slate-900">
171
+ <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
172
+ Toggle Theme
173
+ </button>
174
+ </div>
175
+ );
176
+ }
177
+ ```
178
+
179
+ ### Multiple Themes
180
+
181
+ Extend beyond light/dark with multiple theme attributes:
182
+
183
+ ```tsx
184
+ type Theme = 'light' | 'dark' | 'aviation' | 'high-contrast';
185
+
186
+ export function App() {
187
+ const [theme, setTheme] = useState<Theme>('light');
188
+
189
+ useEffect(() => {
190
+ document.documentElement.setAttribute('data-theme', theme);
191
+ }, [theme]);
192
+
193
+ return (
194
+ <div className="bg-white dark:bg-slate-900 [&[data-theme='aviation']]:bg-blue-950">
195
+ <select value={theme} onChange={(e) => setTheme(e.target.value as Theme)}>
196
+ <option value="light">Light</option>
197
+ <option value="dark">Dark</option>
198
+ <option value="aviation">Aviation</option>
199
+ <option value="high-contrast">High Contrast</option>
200
+ </select>
201
+ </div>
202
+ );
203
+ }
204
+ ```
205
+
206
+ ### Pros & Cons
207
+
208
+ **Pros**:
209
+ - Semantic HTML attribute
210
+ - Supports multiple themes (not just light/dark)
211
+ - Easy to inspect in DevTools
212
+ - Clear intent
213
+
214
+ **Cons**:
215
+ - Requires JavaScript
216
+ - More verbose selector in CSS
217
+ - Less common pattern
218
+
219
+ ### When to Use
220
+
221
+ - Multi-theme applications
222
+ - Semantic HTML preferences
223
+ - Complex theming systems
224
+ - Better DevTools debugging
225
+
226
+ ## Theme Switching Implementation
227
+
228
+ Complete implementation with persistence and SSR support.
229
+
230
+ ### React Hook
231
+
232
+ ```tsx
233
+ // hooks/use-theme.ts
234
+ import { useEffect, useState } from 'react';
235
+
236
+ type Theme = 'light' | 'dark' | 'system';
237
+
238
+ export function useTheme() {
239
+ const [theme, setTheme] = useState<Theme>(() => {
240
+ if (typeof window === 'undefined') return 'system';
241
+ return (localStorage.getItem('theme') as Theme) || 'system';
242
+ });
243
+
244
+ useEffect(() => {
245
+ const root = document.documentElement;
246
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
247
+ ? 'dark'
248
+ : 'light';
249
+
250
+ const effectiveTheme = theme === 'system' ? systemTheme : theme;
251
+
252
+ root.classList.remove('light', 'dark');
253
+ root.classList.add(effectiveTheme);
254
+
255
+ localStorage.setItem('theme', theme);
256
+ }, [theme]);
257
+
258
+ // Listen for system theme changes
259
+ useEffect(() => {
260
+ if (theme !== 'system') return;
261
+
262
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
263
+ const handleChange = () => {
264
+ const systemTheme = mediaQuery.matches ? 'dark' : 'light';
265
+ document.documentElement.classList.remove('light', 'dark');
266
+ document.documentElement.classList.add(systemTheme);
267
+ };
268
+
269
+ mediaQuery.addEventListener('change', handleChange);
270
+ return () => mediaQuery.removeEventListener('change', handleChange);
271
+ }, [theme]);
272
+
273
+ return { theme, setTheme };
274
+ }
275
+ ```
276
+
277
+ ### Theme Provider Component
278
+
279
+ ```tsx
280
+ // components/theme-provider.tsx
281
+ import { createContext, useContext, type ReactNode } from 'react';
282
+ import { useTheme } from '@/hooks/use-theme';
283
+
284
+ type ThemeContextValue = ReturnType<typeof useTheme>;
285
+
286
+ const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
287
+
288
+ export function ThemeProvider({ children }: { children: ReactNode }) {
289
+ const value = useTheme();
290
+
291
+ return (
292
+ <ThemeContext.Provider value={value}>
293
+ {children}
294
+ </ThemeContext.Provider>
295
+ );
296
+ }
297
+
298
+ export function useThemeContext() {
299
+ const context = useContext(ThemeContext);
300
+ if (!context) {
301
+ throw new Error('useThemeContext must be used within ThemeProvider');
302
+ }
303
+ return context;
304
+ }
305
+ ```
306
+
307
+ ### Theme Toggle Component
308
+
309
+ ```tsx
310
+ // components/theme-toggle.tsx
311
+ import { Moon, Sun, Monitor } from 'lucide-react';
312
+ import { useThemeContext } from '@/components/theme-provider';
313
+ import { Button } from '@/components/ui/button';
314
+
315
+ export function ThemeToggle() {
316
+ const { theme, setTheme } = useThemeContext();
317
+
318
+ const cycleTheme = () => {
319
+ const themes: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system'];
320
+ const currentIndex = themes.indexOf(theme);
321
+ const nextIndex = (currentIndex + 1) % themes.length;
322
+ setTheme(themes[nextIndex]);
323
+ };
324
+
325
+ const Icon = theme === 'light' ? Sun : theme === 'dark' ? Moon : Monitor;
326
+
327
+ return (
328
+ <Button
329
+ variant="outline"
330
+ size="icon"
331
+ onClick={cycleTheme}
332
+ aria-label={`Current theme: ${theme}. Click to cycle themes.`}
333
+ >
334
+ <Icon className="h-4 w-4" />
335
+ </Button>
336
+ );
337
+ }
338
+ ```
339
+
340
+ ### SSR Script (Prevent FOUC)
341
+
342
+ Inject this script before any styled content to prevent flash:
343
+
344
+ ```tsx
345
+ // app/layout.tsx (Next.js example)
346
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
347
+ return (
348
+ <html lang="en" suppressHydrationWarning>
349
+ <head>
350
+ <script
351
+ dangerouslySetInnerHTML={{
352
+ __html: `
353
+ (function() {
354
+ const theme = localStorage.getItem('theme') || 'system';
355
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
356
+ const effectiveTheme = theme === 'system' ? systemTheme : theme;
357
+ document.documentElement.classList.add(effectiveTheme);
358
+ })();
359
+ `,
360
+ }}
361
+ />
362
+ </head>
363
+ <body>
364
+ <ThemeProvider>
365
+ {children}
366
+ </ThemeProvider>
367
+ </body>
368
+ </html>
369
+ );
370
+ }
371
+ ```
372
+
373
+ ## Respecting User Preferences
374
+
375
+ ### Reduced Motion
376
+
377
+ Always respect `prefers-reduced-motion`:
378
+
379
+ ```css
380
+ @media (prefers-reduced-motion: reduce) {
381
+ *,
382
+ *::before,
383
+ *::after {
384
+ animation-duration: 0.01ms !important;
385
+ animation-iteration-count: 1 !important;
386
+ transition-duration: 0.01ms !important;
387
+ scroll-behavior: auto !important;
388
+ }
389
+ }
390
+ ```
391
+
392
+ **Usage in components**:
393
+ ```tsx
394
+ export function Card() {
395
+ return (
396
+ <div className="transition-all duration-300 motion-reduce:transition-none">
397
+ Content
398
+ </div>
399
+ );
400
+ }
401
+ ```
402
+
403
+ ### High Contrast
404
+
405
+ Support high contrast mode:
406
+
407
+ ```css
408
+ @media (prefers-contrast: high) {
409
+ .button {
410
+ border-width: 2px;
411
+ }
412
+ }
413
+ ```
414
+
415
+ **Tailwind utilities**:
416
+ ```html
417
+ <button class="border contrast-more:border-2">
418
+ High Contrast Button
419
+ </button>
420
+ ```
421
+
422
+ ### Forced Colors
423
+
424
+ Respect forced colors mode (Windows High Contrast):
425
+
426
+ ```tsx
427
+ export function Card() {
428
+ return (
429
+ <div className="bg-white dark:bg-slate-900 forced-colors:bg-[Canvas] forced-colors:border forced-colors:border-[CanvasText]">
430
+ Content
431
+ </div>
432
+ );
433
+ }
434
+ ```
435
+
436
+ ### Combined Example
437
+
438
+ ```tsx
439
+ export function AccessibleCard({ children }: { children: React.ReactNode }) {
440
+ return (
441
+ <div
442
+ className={`
443
+ bg-white dark:bg-slate-900
444
+ text-slate-900 dark:text-slate-50
445
+ rounded-lg
446
+ transition-colors duration-200
447
+ motion-reduce:transition-none
448
+ border border-transparent
449
+ contrast-more:border-slate-300
450
+ forced-colors:bg-[Canvas]
451
+ forced-colors:border-[CanvasText]
452
+ `}
453
+ >
454
+ {children}
455
+ </div>
456
+ );
457
+ }
458
+ ```
.agents/skills/tailwind-v4/references/setup.md ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Setup & Installation
2
+
3
+ ## Contents
4
+
5
+ - [Package Installation](#package-installation)
6
+ - [Vite Plugin Configuration](#vite-plugin-configuration)
7
+ - [TypeScript Configuration](#typescript-configuration)
8
+ - [CSS Entry Point](#css-entry-point)
9
+ - [Why No Config Files](#why-no-config-files)
10
+
11
+ ---
12
+
13
+ ## Package Installation
14
+
15
+ Install Tailwind CSS v4 with the Vite plugin:
16
+
17
+ ```bash
18
+ pnpm add -D tailwindcss@next @tailwindcss/vite@next
19
+ ```
20
+
21
+ **Complete package.json example**:
22
+ ```json
23
+ {
24
+ "name": "amelia-dashboard",
25
+ "type": "module",
26
+ "scripts": {
27
+ "dev": "vite",
28
+ "build": "vite build",
29
+ "preview": "vite preview"
30
+ },
31
+ "dependencies": {
32
+ "react": "^19.0.0",
33
+ "react-dom": "^19.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@tailwindcss/vite": "^4.0.0",
37
+ "@types/node": "^22.0.0",
38
+ "@vitejs/plugin-react": "^5.0.0",
39
+ "tailwindcss": "^4.0.0",
40
+ "typescript": "^5.6.0",
41
+ "vite": "^6.0.0"
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## Vite Plugin Configuration
47
+
48
+ Use the `@tailwindcss/vite` plugin (NOT the PostCSS plugin):
49
+
50
+ ```ts
51
+ // vite.config.ts
52
+ import tailwindcss from '@tailwindcss/vite';
53
+ import react from '@vitejs/plugin-react';
54
+ import { defineConfig } from 'vite';
55
+
56
+ export default defineConfig({
57
+ plugins: [
58
+ react(),
59
+ tailwindcss(),
60
+ ],
61
+ });
62
+ ```
63
+
64
+ **Plugin options**:
65
+ ```ts
66
+ export type PluginOptions = {
67
+ /**
68
+ * Optimize and minify the output CSS.
69
+ * Default: true in build mode, false in dev mode
70
+ */
71
+ optimize?: boolean | { minify?: boolean };
72
+ };
73
+
74
+ // Example with options
75
+ tailwindcss({
76
+ optimize: {
77
+ minify: true,
78
+ },
79
+ });
80
+ ```
81
+
82
+ **How it works**:
83
+ - Scans source files for Tailwind class candidates
84
+ - Intercepts CSS files containing `@import 'tailwindcss'`
85
+ - Generates utilities based on detected classes
86
+ - Watches for file changes in dev mode
87
+ - Optimizes and minifies in build mode
88
+
89
+ ## TypeScript Configuration
90
+
91
+ Add `@types/node` for path resolution in Vite config:
92
+
93
+ ```json
94
+ {
95
+ "compilerOptions": {
96
+ "target": "ES2020",
97
+ "useDefineForClassFields": true,
98
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
99
+ "module": "ESNext",
100
+ "skipLibCheck": true,
101
+
102
+ /* Bundler mode */
103
+ "moduleResolution": "bundler",
104
+ "allowImportingTsExtensions": true,
105
+ "isolatedModules": true,
106
+ "moduleDetection": "force",
107
+ "noEmit": true,
108
+ "jsx": "react-jsx",
109
+
110
+ /* Linting */
111
+ "strict": true,
112
+ "noUnusedLocals": true,
113
+ "noUnusedParameters": true,
114
+ "noFallthroughCasesInSwitch": true,
115
+
116
+ /* Path resolution */
117
+ "types": ["node"],
118
+ "baseUrl": ".",
119
+ "paths": {
120
+ "@/*": ["./src/*"]
121
+ }
122
+ },
123
+ "include": ["src"]
124
+ }
125
+ ```
126
+
127
+ **Why `@types/node` is needed**:
128
+ - Vite uses Node.js path resolution APIs
129
+ - Required for `import.meta.env` types
130
+ - Enables `path.resolve()` in config files
131
+
132
+ ## CSS Entry Point
133
+
134
+ Create a single CSS file that imports Tailwind:
135
+
136
+ ```css
137
+ /* src/index.css */
138
+ @import 'tailwindcss';
139
+ ```
140
+
141
+ **That's it.** No other imports or configuration needed.
142
+
143
+ **Import in your app**:
144
+ ```tsx
145
+ // src/main.tsx
146
+ import './index.css';
147
+ import React from 'react';
148
+ import ReactDOM from 'react-dom/client';
149
+ import App from './App';
150
+
151
+ ReactDOM.createRoot(document.getElementById('root')!).render(
152
+ <React.StrictMode>
153
+ <App />
154
+ </React.StrictMode>
155
+ );
156
+ ```
157
+
158
+ **Advanced: Multiple entry points**:
159
+ ```css
160
+ /* src/index.css */
161
+ @import 'tailwindcss';
162
+
163
+ /* Custom theme for this entry point */
164
+ @theme {
165
+ --color-primary: oklch(60% 0.24 262);
166
+ }
167
+
168
+ /* Custom utilities */
169
+ @layer utilities {
170
+ .content-auto {
171
+ content-visibility: auto;
172
+ }
173
+ }
174
+ ```
175
+
176
+ ## Why No Config Files
177
+
178
+ Tailwind v4 eliminates separate configuration files in favor of CSS-first configuration.
179
+
180
+ ### No `tailwind.config.js`
181
+
182
+ **v3 approach** (separate JS config):
183
+ ```js
184
+ // tailwind.config.js
185
+ module.exports = {
186
+ theme: {
187
+ extend: {
188
+ colors: {
189
+ primary: '#3b82f6',
190
+ },
191
+ },
192
+ },
193
+ };
194
+ ```
195
+
196
+ **v4 approach** (CSS-first):
197
+ ```css
198
+ @theme {
199
+ --color-primary: oklch(60% 0.24 262);
200
+ }
201
+ ```
202
+
203
+ **Benefits**:
204
+ - Configuration lives with styles
205
+ - No build-time JS evaluation
206
+ - Better CSS tooling support (syntax highlighting, autocomplete)
207
+ - Easier to understand what CSS gets generated
208
+ - No context switching between files
209
+
210
+ ### No `postcss.config.js`
211
+
212
+ **v3 approach** (PostCSS plugin):
213
+ ```js
214
+ // postcss.config.js
215
+ module.exports = {
216
+ plugins: {
217
+ tailwindcss: {},
218
+ autoprefixer: {},
219
+ },
220
+ };
221
+ ```
222
+
223
+ **v4 approach** (Vite plugin):
224
+ ```ts
225
+ // vite.config.ts
226
+ import tailwindcss from '@tailwindcss/vite';
227
+
228
+ export default defineConfig({
229
+ plugins: [tailwindcss()],
230
+ });
231
+ ```
232
+
233
+ **Benefits**:
234
+ - Faster builds (no PostCSS overhead)
235
+ - Integrated with Vite's dev server
236
+ - Better HMR (Hot Module Replacement)
237
+ - Automatic source map generation
238
+ - Native ES modules support
239
+
240
+ ### Content Detection
241
+
242
+ **v3 approach** (manual content paths):
243
+ ```js
244
+ // tailwind.config.js
245
+ module.exports = {
246
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
247
+ };
248
+ ```
249
+
250
+ **v4 approach** (automatic scanning):
251
+ ```css
252
+ /* Auto-scans all files by default */
253
+ @import 'tailwindcss';
254
+
255
+ /* Optional: Custom source patterns */
256
+ @source "src/**/*.{js,ts,jsx,tsx}";
257
+ @source "components/**/*.vue";
258
+ ```
259
+
260
+ **Benefits**:
261
+ - Zero configuration by default
262
+ - Explicit control when needed
263
+ - CSS-based configuration
264
+ - Easier to understand and debug
.agents/skills/tailwind-v4/references/theming.md ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Theming & Design Tokens
2
+
3
+ ## Contents
4
+
5
+ - [@theme Directive Modes](#theme-directive-modes)
6
+ - [CSS Variable Naming Conventions](#css-variable-naming-conventions)
7
+ - [OKLCH Color System](#oklch-color-system)
8
+ - [Aviation Theme Example](#aviation-theme-example)
9
+ - [Two-Tier Variable System](#two-tier-variable-system)
10
+ - [Custom Font Configuration](#custom-font-configuration)
11
+ - [Animation Keyframes](#animation-keyframes)
12
+
13
+ ---
14
+
15
+ ## @theme Directive Modes
16
+
17
+ Tailwind v4 provides multiple modes for defining theme values. Modes can be combined (e.g., `@theme default inline`, `@theme inline reference`).
18
+
19
+ ### @theme (default mode)
20
+
21
+ Generates CSS variables that can be referenced in custom CSS:
22
+
23
+ ```css
24
+ @theme {
25
+ --color-brand: oklch(60% 0.24 262);
26
+ --spacing: 0.25rem;
27
+ }
28
+ ```
29
+
30
+ **Generated CSS**:
31
+ ```css
32
+ :root {
33
+ --color-brand: oklch(60% 0.24 262);
34
+ --spacing: 0.25rem;
35
+ }
36
+ ```
37
+
38
+ **Usage in utilities**:
39
+ ```html
40
+ <div class="text-brand">Uses var(--color-brand)</div>
41
+ ```
42
+
43
+ **Usage in custom CSS**:
44
+ ```css
45
+ .custom-element {
46
+ color: var(--color-brand);
47
+ padding: calc(var(--spacing) * 4);
48
+ }
49
+ ```
50
+
51
+ ### @theme inline
52
+
53
+ Inlines values directly without CSS variable indirection:
54
+
55
+ ```css
56
+ @theme inline {
57
+ --color-brand: oklch(60% 0.24 262);
58
+ }
59
+ ```
60
+
61
+ **Generated CSS** (when `text-brand` is used):
62
+ ```css
63
+ .text-brand {
64
+ color: oklch(60% 0.24 262);
65
+ }
66
+ ```
67
+
68
+ **When to use**:
69
+ - Better performance (no `var()` lookups)
70
+ - Static values that won't change
71
+ - Utilities with multiple values (animations, shadows)
72
+ - Production builds with no runtime theming
73
+
74
+ ### @theme reference
75
+
76
+ Inlines values as fallbacks without emitting CSS variables to :root:
77
+
78
+ ```css
79
+ @theme reference {
80
+ --color-internal: oklch(50% 0.1 180);
81
+ }
82
+ ```
83
+
84
+ **Generated CSS** (when `bg-internal` is used):
85
+ ```css
86
+ .bg-internal {
87
+ background-color: var(--color-internal, oklch(50% 0.1 180));
88
+ }
89
+ ```
90
+
91
+ **Key behavior**: No `:root` variable is created, but the utility still works by using the value as a fallback in `var()`.
92
+
93
+ **When to use**:
94
+ - Provide fallback values without CSS variable overhead
95
+ - Reduce :root bloat while maintaining utility functionality
96
+ - Values that should work even if the variable isn't defined elsewhere
97
+ - Combine with `inline` for direct value substitution (e.g., `@theme reference inline`)
98
+
99
+ ### @theme default
100
+
101
+ Explicitly marks theme values as defaults that can be overridden:
102
+
103
+ ```css
104
+ @theme default {
105
+ --color-primary: oklch(60% 0.24 262);
106
+ }
107
+
108
+ /* Later in the file or another file */
109
+ @theme {
110
+ --color-primary: oklch(70% 0.20 180); /* This overrides the default */
111
+ }
112
+ ```
113
+
114
+ **Generated CSS**:
115
+ ```css
116
+ :root, :host {
117
+ --color-primary: oklch(70% 0.20 180);
118
+ }
119
+ ```
120
+
121
+ **When to use**:
122
+ - Providing base theme values that can be customized
123
+ - Library or framework default themes
124
+ - Creating overridable design systems
125
+ - Used extensively in Tailwind's built-in `theme.css`
126
+
127
+ **Mode combinations**:
128
+ - `@theme default inline` - Default values, inlined directly
129
+ - `@theme default reference` - Default fallbacks without :root emission
130
+ - `@theme default inline reference` - All three combined
131
+
132
+ ## CSS Variable Naming Conventions
133
+
134
+ Tailwind v4 uses consistent naming patterns for theme variables:
135
+
136
+ ### Colors
137
+
138
+ ```css
139
+ --color-{name}-{shade}
140
+ ```
141
+
142
+ **Examples**:
143
+ ```css
144
+ @theme {
145
+ --color-primary-500: oklch(60% 0.24 262);
146
+ --color-surface-900: oklch(21% 0.006 286);
147
+ --color-success-600: oklch(62.7% 0.194 149);
148
+ }
149
+
150
+ /* Usage: text-primary-500, bg-surface-900, border-success-600 */
151
+ ```
152
+
153
+ ### Spacing
154
+
155
+ ```css
156
+ --spacing: {base-unit}
157
+ ```
158
+
159
+ **Example**:
160
+ ```css
161
+ @theme {
162
+ --spacing: 0.25rem; /* Base unit (4px at 16px root) */
163
+ }
164
+
165
+ /* Generated scale:
166
+ p-1 → padding: calc(0.25rem * 1) → 4px
167
+ p-4 → padding: calc(0.25rem * 4) → 16px
168
+ p-12 → padding: calc(0.25rem * 12) → 48px
169
+ */
170
+ ```
171
+
172
+ ### Fonts
173
+
174
+ ```css
175
+ --font-{family}
176
+ --font-{family}--{feature}
177
+ ```
178
+
179
+ **Examples**:
180
+ ```css
181
+ @theme {
182
+ --font-sans: ui-sans-serif, system-ui, sans-serif;
183
+ --font-mono: 'JetBrains Mono', monospace;
184
+ --font-display: 'Inter Variable', system-ui;
185
+
186
+ --font-display--font-variation-settings: 'wght' 400;
187
+ --font-display--font-feature-settings: 'cv02', 'cv03';
188
+ }
189
+
190
+ /* Usage: font-sans, font-mono, font-display */
191
+ ```
192
+
193
+ ### Breakpoints
194
+
195
+ ```css
196
+ --breakpoint-{size}: {value}
197
+ ```
198
+
199
+ **Examples**:
200
+ ```css
201
+ @theme {
202
+ --breakpoint-sm: 40rem; /* 640px */
203
+ --breakpoint-md: 48rem; /* 768px */
204
+ --breakpoint-lg: 64rem; /* 1024px */
205
+ --breakpoint-xl: 80rem; /* 1280px */
206
+ --breakpoint-2xl: 96rem; /* 1536px */
207
+ }
208
+ ```
209
+
210
+ ### Animations
211
+
212
+ ```css
213
+ --animate-{name}: {animation-value}
214
+ ```
215
+
216
+ **Examples**:
217
+ ```css
218
+ @theme inline {
219
+ --animate-spin: spin 1s linear infinite;
220
+ --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
221
+ --animate-beacon: beacon 2s ease-in-out infinite;
222
+
223
+ @keyframes spin {
224
+ to { transform: rotate(360deg); }
225
+ }
226
+
227
+ @keyframes pulse {
228
+ 50% { opacity: 0.5; }
229
+ }
230
+
231
+ @keyframes beacon {
232
+ 0%, 100% { opacity: 1; transform: scale(1); }
233
+ 50% { opacity: 0.5; transform: scale(1.05); }
234
+ }
235
+ }
236
+
237
+ /* Usage: animate-spin, animate-pulse, animate-beacon */
238
+ ```
239
+
240
+ ## OKLCH Color System
241
+
242
+ OKLCH (Oklab LCH) provides perceptually uniform colors with consistent lightness across all hues.
243
+
244
+ ### Syntax
245
+
246
+ ```css
247
+ oklch(L% C H / A)
248
+ ```
249
+
250
+ - **L (Lightness)**: 0% (black) to 100% (white)
251
+ - **C (Chroma)**: 0 (gray) to ~0.4 (vibrant)
252
+ - **H (Hue)**: 0-360 degrees
253
+ - **A (Alpha)**: Optional, 0-1
254
+
255
+ ### Hue Wheel
256
+
257
+ ```
258
+ 0° / 360° - Red
259
+ 30° - Orange
260
+ 60° - Yellow
261
+ 120° - Green
262
+ 180° - Cyan
263
+ 240° - Blue
264
+ 270° - Indigo
265
+ 300° - Magenta
266
+ ```
267
+
268
+ ### Complete Color Palette
269
+
270
+ ```css
271
+ @theme {
272
+ /* Blue scale (H ≈ 260) */
273
+ --color-blue-50: oklch(97% 0.014 254.604);
274
+ --color-blue-100: oklch(93.2% 0.032 255.585);
275
+ --color-blue-200: oklch(88.2% 0.059 254.128);
276
+ --color-blue-300: oklch(80.9% 0.105 251.813);
277
+ --color-blue-400: oklch(70.7% 0.165 254.624);
278
+ --color-blue-500: oklch(62.3% 0.214 259.815);
279
+ --color-blue-600: oklch(54.6% 0.245 262.881);
280
+ --color-blue-700: oklch(48.8% 0.243 264.376);
281
+ --color-blue-800: oklch(42.4% 0.199 265.638);
282
+ --color-blue-900: oklch(37.9% 0.146 265.522);
283
+ --color-blue-950: oklch(28.2% 0.091 267.935);
284
+
285
+ /* Slate scale (neutral with slight blue tint) */
286
+ --color-slate-50: oklch(98.4% 0.003 247.858);
287
+ --color-slate-100: oklch(96.8% 0.007 247.896);
288
+ --color-slate-200: oklch(92.9% 0.013 255.508);
289
+ --color-slate-300: oklch(86.9% 0.022 252.894);
290
+ --color-slate-400: oklch(70.4% 0.04 256.788);
291
+ --color-slate-500: oklch(55.4% 0.046 257.417);
292
+ --color-slate-600: oklch(44.6% 0.043 257.281);
293
+ --color-slate-700: oklch(37.2% 0.044 257.287);
294
+ --color-slate-800: oklch(27.9% 0.041 260.031);
295
+ --color-slate-900: oklch(20.8% 0.042 265.755);
296
+ --color-slate-950: oklch(12.9% 0.042 264.695);
297
+ }
298
+ ```
299
+
300
+ ### Chroma Guidelines
301
+
302
+ - **0**: Pure gray (achromatic)
303
+ - **0.01-0.05**: Subtle tint (slate, zinc)
304
+ - **0.10-0.15**: Muted colors (good for backgrounds)
305
+ - **0.15-0.25**: Vibrant colors (good for UI elements)
306
+ - **0.25-0.40**: Maximum saturation (use sparingly)
307
+
308
+ ## Aviation Theme Example
309
+
310
+ Custom color palette for an aviation-themed dashboard:
311
+
312
+ ```css
313
+ @theme {
314
+ /* Flight status colors */
315
+ --color-on-time: oklch(72.3% 0.219 149.579); /* Green-600 */
316
+ --color-delayed: oklch(76.9% 0.188 70.08); /* Amber-500 */
317
+ --color-cancelled: oklch(63.7% 0.237 25.331); /* Red-500 */
318
+ --color-diverted: oklch(68.5% 0.169 237.323); /* Sky-500 */
319
+
320
+ /* Navigation colors */
321
+ --color-runway: oklch(87.1% 0.006 286.286); /* Zinc-300 */
322
+ --color-taxiway: oklch(70.5% 0.015 286.067); /* Zinc-400 */
323
+ --color-apron: oklch(55.2% 0.016 285.938); /* Zinc-500 */
324
+
325
+ /* Radar colors */
326
+ --color-primary-radar: oklch(74.6% 0.16 232.661); /* Sky-400 */
327
+ --color-secondary-radar: oklch(76.5% 0.177 163.223); /* Emerald-400 */
328
+
329
+ /* Map layers */
330
+ --color-airspace-class-a: oklch(70.7% 0.165 254.624); /* Blue-400 */
331
+ --color-airspace-class-b: oklch(84.1% 0.238 128.85); /* Lime-400 */
332
+ --color-airspace-class-c: oklch(71.8% 0.202 349.761); /* Pink-400 */
333
+ }
334
+ ```
335
+
336
+ ## Two-Tier Variable System
337
+
338
+ Separate design tokens from semantic naming:
339
+
340
+ ```css
341
+ @theme {
342
+ /* Tier 1: Design tokens (OKLCH primitives) */
343
+ --color-blue-600: oklch(54.6% 0.245 262.881);
344
+ --color-slate-50: oklch(98.4% 0.003 247.858);
345
+ --color-slate-800: oklch(27.9% 0.041 260.031);
346
+ --color-slate-900: oklch(20.8% 0.042 265.755);
347
+ --color-emerald-500: oklch(69.6% 0.17 162.48);
348
+
349
+ /* Tier 2: Semantic mappings */
350
+ --color-primary: var(--color-blue-600);
351
+ --color-surface: var(--color-slate-900);
352
+ --color-surface-raised: var(--color-slate-800);
353
+ --color-text: var(--color-slate-50);
354
+ --color-success: var(--color-emerald-500);
355
+ }
356
+
357
+ /* Usage in components */
358
+ .button-primary {
359
+ background-color: var(--color-primary);
360
+ color: var(--color-text);
361
+ }
362
+
363
+ .card {
364
+ background-color: var(--color-surface-raised);
365
+ }
366
+ ```
367
+
368
+ **Benefits**:
369
+ - Design tokens maintain consistency
370
+ - Semantic names convey intent
371
+ - Easy theme switching (just remap tier 2)
372
+ - Clear separation of concerns
373
+
374
+ ## Custom Font Configuration
375
+
376
+ Configure custom fonts with variable font features:
377
+
378
+ ```css
379
+ @theme {
380
+ /* Font families */
381
+ --font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
382
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
383
+ --font-display: 'Manrope Variable', system-ui, sans-serif;
384
+
385
+ /* Variable font settings for --font-display */
386
+ --font-display--font-variation-settings: 'wght' 600;
387
+
388
+ /* OpenType features for --font-display */
389
+ --font-display--font-feature-settings: 'ss01', 'ss02', 'cv05';
390
+
391
+ /* Font weights */
392
+ --font-weight-normal: 400;
393
+ --font-weight-medium: 500;
394
+ --font-weight-semibold: 600;
395
+ --font-weight-bold: 700;
396
+
397
+ /* Letter spacing */
398
+ --tracking-tight: -0.025em;
399
+ --tracking-normal: 0em;
400
+ --tracking-wide: 0.025em;
401
+ }
402
+ ```
403
+
404
+ **Load fonts in HTML**:
405
+ ```html
406
+ <link rel="preconnect" href="https://fonts.googleapis.com">
407
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
408
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
409
+ ```
410
+
411
+ **Usage**:
412
+ ```html
413
+ <h1 class="font-display font-semibold tracking-tight">Aviation Dashboard</h1>
414
+ <code class="font-mono text-sm">ATC-1234</code>
415
+ ```
416
+
417
+ ## Animation Keyframes
418
+
419
+ Define custom animations with `@theme inline` and `@keyframes`:
420
+
421
+ ```css
422
+ @theme inline {
423
+ /* Simple animations */
424
+ --animate-fade-in: fade-in 0.3s ease-out;
425
+ --animate-slide-up: slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1);
426
+
427
+ /* Complex animations */
428
+ --animate-beacon: beacon 2s ease-in-out infinite;
429
+ --animate-pulse-glow: pulse-glow 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
430
+
431
+ /* Keyframe definitions */
432
+ @keyframes fade-in {
433
+ from {
434
+ opacity: 0;
435
+ }
436
+ to {
437
+ opacity: 1;
438
+ }
439
+ }
440
+
441
+ @keyframes slide-up {
442
+ from {
443
+ transform: translateY(10px);
444
+ opacity: 0;
445
+ }
446
+ to {
447
+ transform: translateY(0);
448
+ opacity: 1;
449
+ }
450
+ }
451
+
452
+ @keyframes beacon {
453
+ 0%, 100% {
454
+ opacity: 1;
455
+ transform: scale(1);
456
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
457
+ }
458
+ 50% {
459
+ opacity: 0.9;
460
+ transform: scale(1.05);
461
+ box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
462
+ }
463
+ }
464
+
465
+ @keyframes pulse-glow {
466
+ 0%, 100% {
467
+ box-shadow: 0 0 8px 2px rgba(34, 197, 94, 0.4);
468
+ }
469
+ 50% {
470
+ box-shadow: 0 0 16px 4px rgba(34, 197, 94, 0.8);
471
+ }
472
+ }
473
+ }
474
+ ```
475
+
476
+ **Usage**:
477
+ ```html
478
+ <div class="animate-fade-in">Fades in on mount</div>
479
+ <div class="animate-beacon">Pulsing beacon effect</div>
480
+ <div class="animate-pulse-glow">Glowing status indicator</div>
481
+ ```
482
+
483
+ **Respecting prefers-reduced-motion**:
484
+ ```css
485
+ @media (prefers-reduced-motion: reduce) {
486
+ * {
487
+ animation-duration: 0.01ms !important;
488
+ animation-iteration-count: 1 !important;
489
+ transition-duration: 0.01ms !important;
490
+ }
491
+ }
492
+ ```
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
.prettierignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ build
4
+ coverage
5
+ .idea
6
+ *.log
7
+ package-lock.json
.prettierrc ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "singleQuote": false,
3
+ "tabWidth": 2,
4
+ "trailingComma": "es5",
5
+ "printWidth": 80,
6
+ "arrowParens": "always"
7
+ }
README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: TranslateGemma Browser Translator
3
+ emoji: 🌐
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: static
7
+ pinned: false
8
+ license: apache-2.0
9
+ short_description: High-quality translations across 56 languages with Google's TranslateGemma, running entirely in your browser
10
+ app_file: dist/index.html
11
+ header: mini
12
+ models:
13
+ - google/translategemma-4b-it
14
+ - onnx-community/translategemma-text-4b-it-ONNX
15
+ ---
16
+
17
+ # TranslateGemma Browser Translator
18
+
19
+ A web-based translation application powered by Google's TranslateGemma model, running entirely in your browser with Transformers.js and ONNX Runtime Web.
20
+
21
+ ## ✨ Features
22
+
23
+ - 🌍 **56 Languages** - Translate between 56 different languages
24
+ - 🔒 **Completely Private** - All processing happens in your browser, no data sent to servers
25
+ - 📴 **Offline-Capable** - Works offline after initial model download
26
+ - ⚡ **Real-time Translation** - Auto-translate with 500ms debounce
27
+ - 🔗 **Shareable Links** - Share translations with URL hash parameters
28
+ - 📱 **Mobile Responsive** - Optimized for both desktop and mobile devices
29
+ - 💾 **Local Caching** - Model cached locally for instant subsequent loads
30
+
31
+ ## 🤖 TranslateGemma
32
+
33
+ This application uses [Google's TranslateGemma](https://blog.google/technology/developers/gemma-open-models/), a state-of-the-art language model specifically designed for translation tasks. TranslateGemma is part of Google's Gemma family of open models, delivering high-quality translations across 56 languages directly in your browser using [Transformers.js](https://huggingface.co/docs/transformers.js) and ONNX Runtime Web.
34
+
35
+ ## 🔒 Completely Private & Offline-Capable
36
+
37
+ Your translations are processed entirely in your browser with **no data sent to any server**. Once the model is downloaded, you can use this translator completely offline. Your text never leaves your device, ensuring complete privacy and security. The model is cached locally, so subsequent visits will load instantly without any downloads.
eslint.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ }
28
+ )
index.html ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+
8
+ <!-- SEO Meta Tags -->
9
+ <title>
10
+ TranslateGemma Browser Translator - Private & Offline Translation
11
+ </title>
12
+ <meta
13
+ name="description"
14
+ content="High-quality translations across 56 languages powered by Google's TranslateGemma model. Runs entirely in your browser with complete privacy and offline capability using Transformers.js."
15
+ />
16
+ <meta
17
+ name="keywords"
18
+ content="translation, translator, TranslateGemma, Transformers.js, offline translator, private translation, browser translation, Gemma, ONNX"
19
+ />
20
+
21
+ <!-- Open Graph Meta Tags -->
22
+ <meta property="og:title" content="TranslateGemma Browser Translator" />
23
+ <meta
24
+ property="og:description"
25
+ content="High-quality translations across 56 languages powered by Google's TranslateGemma model, running entirely in your browser with complete privacy."
26
+ />
27
+ <meta property="og:type" content="website" />
28
+ <meta property="og:image" content="/favicon.svg" />
29
+
30
+ <!-- Twitter Card Meta Tags -->
31
+ <meta name="twitter:card" content="summary_large_image" />
32
+ <meta name="twitter:title" content="TranslateGemma Browser Translator" />
33
+ <meta
34
+ name="twitter:description"
35
+ content="High-quality translations across 56 languages powered by Google's TranslateGemma model, running entirely in your browser with complete privacy."
36
+ />
37
+ <meta name="twitter:image" content="/favicon.svg" />
38
+
39
+ <!-- Fonts -->
40
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
41
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
42
+ <link
43
+ href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap"
44
+ rel="stylesheet"
45
+ />
46
+ </head>
47
+ <body>
48
+ <div id="root"></div>
49
+ <script type="module" src="/src/main.tsx"></script>
50
+ </body>
51
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "transformersjs-translategemma",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\""
12
+ },
13
+ "dependencies": {
14
+ "@huggingface/transformers": "^4.0.0-next.3",
15
+ "idb": "^8.0.2",
16
+ "lucide-react": "^0.564.0",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0",
19
+ "react-hook-form": "^7.54.2"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.17.0",
23
+ "@tailwindcss/vite": "^4.1.18",
24
+ "@types/react": "^19.0.7",
25
+ "@types/react-dom": "^19.0.3",
26
+ "@vitejs/plugin-react": "^4.3.4",
27
+ "eslint": "^9.17.0",
28
+ "eslint-plugin-react-hooks": "^5.0.0",
29
+ "eslint-plugin-react-refresh": "^0.4.16",
30
+ "globals": "^15.14.0",
31
+ "prettier": "^3.4.2",
32
+ "tailwindcss": "^4.1.18",
33
+ "typescript": "~5.7.2",
34
+ "typescript-eslint": "^8.18.2",
35
+ "vite": "^6.0.7"
36
+ }
37
+ }
public/favicon.svg ADDED
public/gemma.svg ADDED
public/hf-logo.svg ADDED
src/App.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Translate from "./Translate";
2
+ import { useState } from "react";
3
+ import Translator from "./ai/Translator.ts";
4
+ import Initialize from "./Initialize.tsx";
5
+
6
+ function App() {
7
+ const [translator, setTranslator] = useState<Translator>(null);
8
+ const [progress, setProgress] = useState<number>(0);
9
+ const [isInitializing, setIsInitializing] = useState<boolean>(false);
10
+
11
+ const init = async () => {
12
+ setIsInitializing(true);
13
+ const t = Translator.getInstance();
14
+ const loaded = new Map<string, number>();
15
+ let newProgress = 0;
16
+ await t.init((e) => {
17
+ if (e.status === "progress") {
18
+ loaded.set(e.file, e.loaded);
19
+ const allLoaded = Array.from(loaded.values()).reduce(
20
+ (acc: number, curr: number) => acc + curr,
21
+ 0
22
+ );
23
+ const percentLoaded = Math.round((100 / Translator.size) * allLoaded);
24
+ if (newProgress !== percentLoaded) {
25
+ newProgress = percentLoaded;
26
+ setProgress(newProgress);
27
+ }
28
+ }
29
+ });
30
+ setTranslator(t);
31
+ setIsInitializing(false);
32
+ };
33
+
34
+ return (
35
+ <div className="min-h-screen bg-neutral-100 flex flex-col justify-between gap-2">
36
+ <header className="bg-white border-b border-border shadow-sm p-4">
37
+ <h1 className="text-md md:text-3xl font-sans text-center flex justify-center items-center gap-2 md:gap-6">
38
+ <span className="font-bold flex items-center justify-center gap-1">
39
+ <img
40
+ src="gemma.svg"
41
+ alt="Gemma Logo"
42
+ className="block"
43
+ style={{
44
+ width: "1.2em",
45
+ height: "1.2em",
46
+ }}
47
+ />
48
+ <span>
49
+ Translate<span className="text-primary">Gemma</span>
50
+ </span>
51
+ </span>
52
+ <span>//</span>
53
+ <span className="font-bold flex items-center justify-center gap-1">
54
+ <img
55
+ src="hf-logo.svg"
56
+ alt="Gemma Logo"
57
+ className="block"
58
+ style={{
59
+ width: "1.2em",
60
+ height: "1.2em",
61
+ }}
62
+ />
63
+ Transformers.js
64
+ </span>
65
+ </h1>
66
+ </header>
67
+ {translator ? (
68
+ <Translate className="w-full" translator={translator} />
69
+ ) : (
70
+ <Initialize
71
+ onInitialize={init}
72
+ progress={progress}
73
+ isInitializing={isInitializing}
74
+ />
75
+ )}
76
+ <footer className="p-8 pt-0 text-center text-muted-foreground text-xs md:text-sm">
77
+ <p>
78
+ High-quality translations across 56 languages powered by{" "}
79
+ <a
80
+ href="https://blog.google/technology/developers/gemma-open-models/"
81
+ target="_blank"
82
+ rel="noopener noreferrer"
83
+ className="text-primary hover:underline"
84
+ >
85
+ Google's TranslateGemma
86
+ </a>{" "}
87
+ model, running entirely in your browser with{" "}
88
+ <a
89
+ href="https://huggingface.co/docs/transformers.js"
90
+ target="_blank"
91
+ rel="noopener noreferrer"
92
+ className="text-primary hover:underline"
93
+ >
94
+ Transformers.js
95
+ </a>{" "}
96
+ and complete privacy.
97
+ </p>
98
+ </footer>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ export default App;
src/Initialize.tsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "./theme";
2
+ import cn from "./utils/classnames.ts";
3
+ import { formatBytes } from "./utils/format.ts";
4
+ import Translator from "./ai/Translator.ts";
5
+
6
+ interface InitializeProps {
7
+ progress: number;
8
+ onInitialize: () => Promise<void>;
9
+ isInitializing?: boolean;
10
+ className?: string;
11
+ }
12
+
13
+ export default function Initialize({
14
+ progress,
15
+ onInitialize,
16
+ isInitializing = false,
17
+ className = "",
18
+ }: InitializeProps) {
19
+ return (
20
+ <div className={cn("max-w-2xl mx-auto px-8", className)}>
21
+ <div className="bg-white rounded-lg shadow-md p-8 border border-border">
22
+ <div className="flex flex-col items-center gap-6">
23
+ {/* Description */}
24
+ <div className="space-y-4 text-left w-full">
25
+ <div>
26
+ <h3 className="font-semibold text-base mb-1">
27
+ About TranslateGemma 4B
28
+ </h3>
29
+ <p className="text-sm text-secondary-foreground">
30
+ TranslateGemma is a family of translation models from Google,
31
+ built on top of Gemma 2B. This 4B parameter model supports
32
+ translations across 100+ languages with state-of-the-art
33
+ quality.{" "}
34
+ <a
35
+ href="https://blog.google/innovation-and-ai/technology/developers-tools/translategemma/"
36
+ target="_blank"
37
+ rel="noopener noreferrer"
38
+ className="text-primary hover:underline"
39
+ >
40
+ Learn more
41
+ </a>
42
+ </p>
43
+ </div>
44
+
45
+ <div>
46
+ <h3 className="font-semibold text-base mb-1">
47
+ Completely private & offline-capable
48
+ </h3>
49
+ <p className="text-sm text-secondary-foreground">
50
+ Everything runs entirely in your browser with 🤗 Transformers.js
51
+ and ONNX Runtime Web — no data is ever sent to a server. Once
52
+ loaded, it works offline.
53
+ </p>
54
+ </div>
55
+
56
+ <div>
57
+ <h3 className="font-semibold text-base mb-1">
58
+ Experimental — WebGPU required
59
+ </h3>
60
+ <p className="text-sm text-secondary-foreground">
61
+ This is experimental and requires a browser with WebGPU support
62
+ and enough VRAM to run the model.
63
+ </p>
64
+ </div>
65
+ </div>
66
+
67
+ {/* Initialize button or progress indicator */}
68
+ {isInitializing ? (
69
+ <div className="w-full space-y-2">
70
+ <div className="flex justify-between items-center">
71
+ <span className="text-sm font-medium text-muted-foreground">
72
+ Loading model...
73
+ </span>
74
+ <span className="text-sm font-medium text-primary">
75
+ {progress}%
76
+ </span>
77
+ </div>
78
+ <div className="w-full bg-muted rounded-full h-2 overflow-hidden">
79
+ <div
80
+ className="bg-primary h-full transition-all duration-300 ease-out"
81
+ style={{ width: `${progress}%` }}
82
+ />
83
+ </div>
84
+ <p className="text-xs text-muted-foreground text-center">
85
+ This may take a few moments. Please don't close the page.
86
+ </p>
87
+ </div>
88
+ ) : (
89
+ <Button
90
+ variant="primary"
91
+ onClick={onInitialize}
92
+ disabled={isInitializing}
93
+ className="text-lg mt-8"
94
+ >
95
+ {isInitializing
96
+ ? "Initializing..."
97
+ : `Download TranslateGemma (${formatBytes(Translator.size)})`}
98
+ </Button>
99
+ )}
100
+ </div>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
src/Translate.tsx ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { ArrowLeftRight, Copy, Check, Share2, Trash } from "lucide-react";
3
+ import { Textarea, Button, Loader } from "./theme";
4
+ import cn from "./utils/classnames.ts";
5
+ import { type LanguageCode, LANGUAGES } from "./constants";
6
+ import LanguageSelector from "./components/LanguageSelector";
7
+ import Translator from "./ai/Translator.ts";
8
+ import { formatTime, formatNumber } from "./utils/format";
9
+ import { countWords } from "./utils/countWords.ts";
10
+
11
+ const MAX_INPUT_LENGTH = 1000;
12
+
13
+ interface TranslateProps {
14
+ className?: string;
15
+ translator: Translator;
16
+ }
17
+
18
+ export default function Translate({
19
+ className = "",
20
+ translator,
21
+ }: TranslateProps) {
22
+ // Initialize from URL hash
23
+ const getInitialState = () => {
24
+ const hash = window.location.hash.slice(1); // Remove the # character
25
+ const params = new URLSearchParams(hash);
26
+
27
+ const sourceLang = params.get("sl");
28
+ const targetLang = params.get("tl");
29
+ const text = params.get("text");
30
+
31
+ // Validate language codes
32
+ const isValidLanguage = (code: string | null): code is LanguageCode => {
33
+ if (!code) return false;
34
+ return LANGUAGES.some((lang) => lang.code === code);
35
+ };
36
+
37
+ return {
38
+ sourceLanguage: isValidLanguage(sourceLang)
39
+ ? sourceLang
40
+ : ("en" as LanguageCode),
41
+ targetLanguage: isValidLanguage(targetLang)
42
+ ? targetLang
43
+ : ("de_DE" as LanguageCode),
44
+ sourceText: text ? decodeURIComponent(text) : "",
45
+ };
46
+ };
47
+
48
+ const initialState = getInitialState();
49
+
50
+ const [sourceText, setSourceText] = useState(initialState.sourceText);
51
+ const [targetText, setTargetText] = useState("");
52
+ const [sourceLanguage, setSourceLanguage] = useState<LanguageCode>(
53
+ initialState.sourceLanguage
54
+ );
55
+ const [targetLanguage, setTargetLanguage] = useState<LanguageCode>(
56
+ initialState.targetLanguage
57
+ );
58
+ const [translating, setTranslating] = useState<boolean>(false);
59
+ const [copied, setCopied] = useState<boolean>(false);
60
+ const [shared, setShared] = useState<boolean>(false);
61
+ const abortControllerRef = useRef<AbortController | null>(null);
62
+ const [translationTime, setTranslationTime] = useState<number>(0);
63
+ const [translationWords, setTranslationWords] = useState<number>(0);
64
+
65
+ const handleSwapLanguages = () => {
66
+ // Swap languages
67
+ setSourceLanguage(targetLanguage);
68
+ setTargetLanguage(sourceLanguage);
69
+
70
+ // Swap text content
71
+ setSourceText(targetText);
72
+ setTargetText(sourceText);
73
+ };
74
+
75
+ const handleCopy = async () => {
76
+ if (!targetText) return;
77
+
78
+ try {
79
+ await navigator.clipboard.writeText(targetText);
80
+ setCopied(true);
81
+ setTimeout(() => setCopied(false), 2000);
82
+ } catch (error) {
83
+ console.error("Failed to copy:", error);
84
+ }
85
+ };
86
+
87
+ const handleShare = async () => {
88
+ try {
89
+ const currentUrl = window.location.href;
90
+ await navigator.clipboard.writeText(currentUrl);
91
+ setShared(true);
92
+ setTimeout(() => setShared(false), 2000);
93
+ } catch (error) {
94
+ console.error("Failed to share:", error);
95
+ }
96
+ };
97
+
98
+ const translate = async (
99
+ text: string,
100
+ sourceLang: LanguageCode,
101
+ targetLang: LanguageCode
102
+ ) => {
103
+ if (!text.trim()) {
104
+ setTargetText("");
105
+ setTranslationTime(0);
106
+ setTranslationWords(0);
107
+ return;
108
+ }
109
+
110
+ const started = performance.now();
111
+
112
+ if (abortControllerRef.current) {
113
+ abortControllerRef.current.abort();
114
+ }
115
+
116
+ abortControllerRef.current = new AbortController();
117
+ const currentController = abortControllerRef.current;
118
+
119
+ setTranslating(true);
120
+
121
+ try {
122
+ const translation = await translator.translate(
123
+ text,
124
+ sourceLang,
125
+ targetLang
126
+ );
127
+
128
+ if (!currentController.signal.aborted) {
129
+ setTargetText(translation);
130
+ setTranslationTime(Math.round(performance.now() - started));
131
+ setTranslationWords(countWords(text));
132
+ }
133
+ } catch (error) {
134
+ if (!currentController.signal.aborted) {
135
+ console.error("Translation error:", error);
136
+ }
137
+ } finally {
138
+ if (!currentController.signal.aborted) {
139
+ setTranslating(false);
140
+ }
141
+ }
142
+ };
143
+
144
+ // Update URL hash when languages or text change
145
+ useEffect(() => {
146
+ const params = new URLSearchParams();
147
+ params.set("sl", sourceLanguage);
148
+ params.set("tl", targetLanguage);
149
+ if (sourceText) {
150
+ params.set("text", encodeURIComponent(sourceText));
151
+ }
152
+
153
+ const newHash = `#${params.toString()}`;
154
+ window.history.replaceState({}, "", newHash);
155
+ }, [sourceLanguage, targetLanguage, sourceText]);
156
+
157
+ useEffect(() => {
158
+ const timer = setTimeout(() => {
159
+ translate(sourceText, sourceLanguage, targetLanguage);
160
+ }, 500);
161
+
162
+ return () => {
163
+ clearTimeout(timer);
164
+ };
165
+ }, [sourceText, sourceLanguage, targetLanguage]);
166
+
167
+ return (
168
+ <div className={cn("max-w-6xl mx-auto p-2 md:p-4 relative", className)}>
169
+ <div className="flex flex-col md:flex-row w-full gap-4 md:gap-8">
170
+ <div className="flex flex-col gap-3 w-full md:w-1/2 relative">
171
+ <LanguageSelector
172
+ value={sourceLanguage}
173
+ onChange={setSourceLanguage}
174
+ />
175
+ <Textarea
176
+ value={sourceText}
177
+ onChange={(e) => setSourceText(e.target.value)}
178
+ placeholder="Enter text to translate..."
179
+ className="h-48 md:h-70 pb-10"
180
+ variant="default"
181
+ maxLength={MAX_INPUT_LENGTH}
182
+ />
183
+ <div className="p-2 flex justify-between items-center -mt-4">
184
+ <p className="text-xs text-muted-foreground opacity-70">
185
+ {formatNumber(sourceText.length)} /{" "}
186
+ {formatNumber(MAX_INPUT_LENGTH)}
187
+ </p>
188
+ <div className="flex gap-2">
189
+ <button
190
+ onClick={handleShare}
191
+ className="p-2 text-muted-foreground hover:text-primary hover:bg-muted rounded-md transition-colors"
192
+ aria-label="Share translation"
193
+ >
194
+ {shared ? (
195
+ <span className="flex text-xs gap-2">
196
+ link copied
197
+ <Check className="w-4 h-4 text-primary" />
198
+ </span>
199
+ ) : (
200
+ <Share2 className="w-4 h-4" />
201
+ )}
202
+ </button>
203
+ <button
204
+ onClick={() => setSourceText("")}
205
+ className="p-2 text-muted-foreground hover:text-primary hover:bg-muted rounded-md transition-colors"
206
+ aria-label="clear text"
207
+ >
208
+ <Trash className="w-4 h-4" />
209
+ </button>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <div className="hidden md:block absolute left-1/2 top-5 -translate-x-1/2">
215
+ <Button
216
+ variant="ghost"
217
+ icon={ArrowLeftRight}
218
+ onClick={handleSwapLanguages}
219
+ aria-label="Swap languages"
220
+ />
221
+ </div>
222
+
223
+ <div className="flex md:hidden justify-center -my-2">
224
+ <Button
225
+ variant="ghost"
226
+ icon={ArrowLeftRight}
227
+ onClick={handleSwapLanguages}
228
+ aria-label="Swap languages"
229
+ className="rotate-90"
230
+ />
231
+ </div>
232
+
233
+ <div className="flex flex-col gap-3 w-full md:w-1/2">
234
+ <div className="flex items-center justify-between">
235
+ <LanguageSelector
236
+ value={targetLanguage}
237
+ onChange={setTargetLanguage}
238
+ />
239
+ {translating && <Loader size={20} />}
240
+ </div>
241
+ <div>
242
+ <Textarea
243
+ value={targetText}
244
+ disabled
245
+ placeholder="Translation will appear here..."
246
+ className="h-48 md:h-70"
247
+ variant="default"
248
+ />
249
+ <div className="p-2 flex justify-between items-center -mt-4">
250
+ {translationTime > 0 ? (
251
+ <p className="text-xs text-muted-foreground opacity-70">
252
+ Translated <b>{formatNumber(translationWords)} words</b> in{" "}
253
+ <b>{formatTime(translationTime)}</b>
254
+ </p>
255
+ ) : (
256
+ <p />
257
+ )}
258
+ <div>
259
+ <button
260
+ onClick={handleCopy}
261
+ className="p-2 text-muted-foreground hover:text-primary hover:bg-muted rounded-md transition-colors"
262
+ aria-label="Copy translation"
263
+ >
264
+ {copied ? (
265
+ <span className="flex text-xs gap-2">
266
+ translation copied
267
+ <Check className="w-4 h-4 text-primary" />
268
+ </span>
269
+ ) : (
270
+ <Copy className="w-4 h-4" />
271
+ )}
272
+ </button>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ );
280
+ }
src/ai/Translator.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pipeline, DataType } from "@huggingface/transformers";
2
+ import { ProgressCallback } from "../types/transformers.ts";
3
+
4
+ class Translator {
5
+ private static instance: Translator | null = null;
6
+ private pipeline: any = null;
7
+ private static modelId: string =
8
+ "onnx-community/translategemma-text-4b-it-ONNX";
9
+ private static dtype: DataType = "q4";
10
+ public static size: number = 3111894696;
11
+
12
+ private constructor() {}
13
+
14
+ public static getInstance(): Translator {
15
+ if (!Translator.instance) {
16
+ Translator.instance = new Translator();
17
+ }
18
+ return Translator.instance;
19
+ }
20
+
21
+ public async init(onProgress?: ProgressCallback) {
22
+ if (this.pipeline) return;
23
+
24
+ this.pipeline = await pipeline("text-generation", Translator.modelId, {
25
+ progress_callback: onProgress,
26
+ device: "webgpu",
27
+ dtype: Translator.dtype,
28
+ });
29
+ }
30
+
31
+ public async translate(
32
+ text: string,
33
+ sourceLang: string,
34
+ targetLang: string
35
+ ): Promise<string> {
36
+ if (!this.pipeline) {
37
+ throw new Error("Translator not initialized. Call init() first.");
38
+ }
39
+
40
+ console.log(`Translating from ${sourceLang} to ${targetLang}`, text);
41
+
42
+ const messages = [
43
+ {
44
+ role: "user",
45
+ content: [
46
+ {
47
+ type: "text",
48
+ source_lang_code: sourceLang,
49
+ target_lang_code: targetLang,
50
+ text,
51
+ },
52
+ ],
53
+ },
54
+ ];
55
+
56
+ const output = await this.pipeline(messages, {
57
+ max_new_tokens: 1024,
58
+ });
59
+
60
+ return output[0].generated_text.pop().content;
61
+ }
62
+ }
63
+
64
+ export default Translator;
src/components/LanguageSelector.tsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { ChevronDown } from "lucide-react";
3
+ import {
4
+ LANGUAGES,
5
+ type LanguageCode,
6
+ LANGUAGES_WITH_AUTO,
7
+ } from "../constants";
8
+
9
+ interface LanguageSelectorProps {
10
+ value: LanguageCode;
11
+ onChange: (language: LanguageCode) => void;
12
+ includeAuto?: boolean;
13
+ }
14
+
15
+ export default function LanguageSelector({
16
+ value,
17
+ onChange,
18
+ includeAuto = false,
19
+ }: LanguageSelectorProps) {
20
+ const [isOpen, setIsOpen] = useState(false);
21
+ const [search, setSearch] = useState("");
22
+ const containerRef = useRef<HTMLDivElement>(null);
23
+ const searchInputRef = useRef<HTMLInputElement>(null);
24
+
25
+ const languages = includeAuto ? LANGUAGES_WITH_AUTO : LANGUAGES;
26
+
27
+ const getLanguageDisplay = (code: LanguageCode) => {
28
+ return languages.find((lang) => lang.code === code)?.name || code;
29
+ };
30
+
31
+ const filterLanguages = (searchTerm: string) => {
32
+ const searchLower = searchTerm.toLowerCase();
33
+ return languages.filter(
34
+ (lang) =>
35
+ lang.name.toLowerCase().includes(searchLower) ||
36
+ lang.code.toLowerCase().includes(searchLower)
37
+ );
38
+ };
39
+
40
+ const filteredLanguages = search ? filterLanguages(search) : languages;
41
+
42
+ const handleSelect = (language: LanguageCode) => {
43
+ onChange(language);
44
+ setIsOpen(false);
45
+ setSearch("");
46
+ };
47
+
48
+ useEffect(() => {
49
+ const handleClickOutside = (event: MouseEvent) => {
50
+ if (
51
+ containerRef.current &&
52
+ !containerRef.current.contains(event.target as Node)
53
+ ) {
54
+ setIsOpen(false);
55
+ setSearch("");
56
+ }
57
+ };
58
+
59
+ if (isOpen) {
60
+ document.addEventListener("mousedown", handleClickOutside);
61
+ setTimeout(() => searchInputRef.current?.focus(), 0);
62
+ }
63
+
64
+ return () => {
65
+ document.removeEventListener("mousedown", handleClickOutside);
66
+ };
67
+ }, [isOpen]);
68
+
69
+ return (
70
+ <div className="relative" ref={containerRef}>
71
+ <button
72
+ onClick={() => setIsOpen(!isOpen)}
73
+ className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-primary hover:bg-muted rounded-md transition-colors"
74
+ >
75
+ <span>{getLanguageDisplay(value)}</span>
76
+ <ChevronDown
77
+ className={`w-4 h-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
78
+ />
79
+ </button>
80
+
81
+ {isOpen && (
82
+ <div className="absolute top-full left-0 mt-2 w-64 bg-white border border-border rounded-md shadow-lg z-50">
83
+ <div className="p-2 border-b border-border">
84
+ <input
85
+ ref={searchInputRef}
86
+ type="text"
87
+ placeholder="Search languages..."
88
+ value={search}
89
+ onChange={(e) => setSearch(e.target.value)}
90
+ className="w-full px-3 py-2 text-sm border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
91
+ />
92
+ </div>
93
+
94
+ <div className="max-h-60 overflow-auto">
95
+ {filteredLanguages.map((lang) => (
96
+ <button
97
+ key={lang.code}
98
+ onClick={() => handleSelect(lang.code)}
99
+ className={`w-full px-3 py-2 text-left text-sm hover:bg-muted transition-colors ${
100
+ value === lang.code ? "bg-primary-50 text-primary-800" : ""
101
+ }`}
102
+ >
103
+ {lang.name}
104
+ </button>
105
+ ))}
106
+ {filteredLanguages.length === 0 && (
107
+ <div className="px-3 py-4 text-sm text-muted-foreground text-center">
108
+ No languages found
109
+ </div>
110
+ )}
111
+ </div>
112
+ </div>
113
+ )}
114
+ </div>
115
+ );
116
+ }
src/constants.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const LANGUAGES = [
2
+ { code: "ar_EG", name: "Arabic (Egypt)" },
3
+ { code: "ar_SA", name: "Arabic (Saudi Arabia)" },
4
+ { code: "bg_BG", name: "Bulgarian" },
5
+ { code: "bn_IN", name: "Bengali" },
6
+ { code: "ca_ES", name: "Catalan" },
7
+ { code: "cs_CZ", name: "Czech" },
8
+ { code: "da_DK", name: "Danish" },
9
+ { code: "de_DE", name: "German" },
10
+ { code: "el_GR", name: "Greek" },
11
+ { code: "en", name: "English" },
12
+ { code: "es_XX", name: "Spanish" },
13
+ { code: "et_EE", name: "Estonian" },
14
+ { code: "fa_IR", name: "Persian" },
15
+ { code: "fi_FI", name: "Finnish" },
16
+ { code: "fil_PH", name: "Filipino" },
17
+ { code: "fr_CA", name: "French (Canada)" },
18
+ { code: "fr_FR", name: "French" },
19
+ { code: "gu_IN", name: "Gujarati" },
20
+ { code: "he_IL", name: "Hebrew" },
21
+ { code: "hi_IN", name: "Hindi" },
22
+ { code: "hr_HR", name: "Croatian" },
23
+ { code: "hu_HU", name: "Hungarian" },
24
+ { code: "id_ID", name: "Indonesian" },
25
+ { code: "is_IS", name: "Icelandic" },
26
+ { code: "it_IT", name: "Italian" },
27
+ { code: "ja_JP", name: "Japanese" },
28
+ { code: "kn_IN", name: "Kannada" },
29
+ { code: "ko_KR", name: "Korean" },
30
+ { code: "lt_LT", name: "Lithuanian" },
31
+ { code: "lv_LV", name: "Latvian" },
32
+ { code: "ml_IN", name: "Malayalam" },
33
+ { code: "mr_IN", name: "Marathi" },
34
+ { code: "nl_NL", name: "Dutch" },
35
+ { code: "no_NO", name: "Norwegian" },
36
+ { code: "pa_IN", name: "Punjabi" },
37
+ { code: "pl_PL", name: "Polish" },
38
+ { code: "pt_BR", name: "Portuguese (Brazil)" },
39
+ { code: "pt_PT", name: "Portuguese (Portugal)" },
40
+ { code: "ro_RO", name: "Romanian" },
41
+ { code: "ru_RU", name: "Russian" },
42
+ { code: "sk_SK", name: "Slovak" },
43
+ { code: "sl_SI", name: "Slovenian" },
44
+ { code: "sr_RS", name: "Serbian" },
45
+ { code: "sv_SE", name: "Swedish" },
46
+ { code: "sw_KE", name: "Swahili" },
47
+ { code: "sw_TZ", name: "Swahili (Tanzania)" },
48
+ { code: "ta_IN", name: "Tamil" },
49
+ { code: "te_IN", name: "Telugu" },
50
+ { code: "th_TH", name: "Thai" },
51
+ { code: "tr_TR", name: "Turkish" },
52
+ { code: "uk_UA", name: "Ukrainian" },
53
+ { code: "ur_PK", name: "Urdu" },
54
+ { code: "vi_VN", name: "Vietnamese" },
55
+ { code: "zh_CN", name: "Chinese (Simplified)" },
56
+ { code: "zh_TW", name: "Chinese (Traditional)" },
57
+ { code: "zu_ZA", name: "Zulu" },
58
+ ] as const;
59
+
60
+ export const LANGUAGES_WITH_AUTO = [
61
+ {
62
+ code: "auto",
63
+ name: "Auto",
64
+ },
65
+ ...LANGUAGES,
66
+ ] as const;
67
+
68
+ export type LanguageCode = (typeof LANGUAGES_WITH_AUTO)[number]["code"];
69
+
70
+ export const getLanguageName = (code: LanguageCode): string => {
71
+ return LANGUAGES.find((lang) => lang.code === code)?.name || code;
72
+ };
src/index.css ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ /* Custom font family - Google Sans */
5
+ --font-sans: "Google Sans", system-ui, -apple-system, sans-serif;
6
+
7
+ /* Google Translate inspired primary colors */
8
+ --color-primary-50: #e8f0fe;
9
+ --color-primary-100: #d3e3fd;
10
+ --color-primary-200: #aecbfa;
11
+ --color-primary-300: #8ab4f8;
12
+ --color-primary-400: #669df6;
13
+ --color-primary-500: #1a73e8; /* Main primary */
14
+ --color-primary-600: #1557b0;
15
+ --color-primary-700: #0d47a1;
16
+ --color-primary-800: #0842a0;
17
+ --color-primary-900: #0b57d0;
18
+
19
+ /* Semantic colors */
20
+ --color-primary: var(--color-primary-500);
21
+ --color-primary-foreground: #ffffff;
22
+ --color-secondary: oklch(95% 0.01 262);
23
+ --color-secondary-foreground: oklch(20% 0.05 262);
24
+ --color-muted: #f1f3f4;
25
+ --color-muted-foreground: #5f6368;
26
+ --color-accent: oklch(95% 0.01 220);
27
+ --color-accent-foreground: oklch(20% 0.05 220);
28
+ --color-destructive: oklch(58% 0.24 27);
29
+ --color-destructive-foreground: oklch(98% 0.002 27);
30
+ --color-border: #dadce0;
31
+ --color-input: oklch(90% 0.01 262);
32
+ --color-ring: var(--color-primary-500);
33
+
34
+ /* Font sizes - increased base size */
35
+ --font-size-xs: 0.8125rem; /* 13px */
36
+ --font-size-sm: 0.9375rem; /* 15px */
37
+ --font-size-base: 1.0625rem; /* 17px - increased from default 16px */
38
+ --font-size-lg: 1.1875rem; /* 19px */
39
+ --font-size-xl: 1.3125rem; /* 21px */
40
+ --font-size-2xl: 1.5625rem; /* 25px */
41
+ --font-size-3xl: 1.9375rem; /* 31px */
42
+ --font-size-4xl: 2.3125rem; /* 37px */
43
+ --font-size-5xl: 3.0625rem; /* 49px */
44
+ --font-size-6xl: 3.8125rem; /* 61px */
45
+ --font-size-7xl: 4.8125rem; /* 77px */
46
+ --font-size-8xl: 6.0625rem; /* 97px */
47
+ --font-size-9xl: 8.0625rem; /* 129px */
48
+
49
+ /* Spacing base */
50
+ --spacing: 0.25rem;
51
+ }
52
+
53
+ body {
54
+ margin: 0;
55
+ padding: 0;
56
+ -webkit-font-smoothing: antialiased;
57
+ -moz-osx-font-smoothing: grayscale;
58
+ }
59
+
60
+ * {
61
+ box-sizing: border-box;
62
+ }
63
+
64
+ #root {
65
+ min-height: 100vh;
66
+ }
src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App.tsx";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
src/theme/button/Button.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from "react";
2
+ import { LucideIcon } from "lucide-react";
3
+ import cn from "../../utils/classnames.ts";
4
+
5
+ interface ButtonProps extends DetailedHTMLProps<
6
+ ButtonHTMLAttributes<HTMLButtonElement>,
7
+ HTMLButtonElement
8
+ > {
9
+ variant?:
10
+ | "primary"
11
+ | "outlined"
12
+ | "pill"
13
+ | "pill-selected"
14
+ | "text"
15
+ | "ghost";
16
+ icon?: LucideIcon;
17
+ iconPosition?: "left" | "right";
18
+ children?: ReactNode;
19
+ }
20
+
21
+ export default function Button({
22
+ variant = "primary",
23
+ icon: Icon,
24
+ iconPosition = "left",
25
+ children,
26
+ className = "",
27
+ ...props
28
+ }: ButtonProps) {
29
+ const baseClasses =
30
+ "inline-flex items-center justify-center font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2";
31
+
32
+ const variantClasses = {
33
+ primary:
34
+ "bg-primary text-primary-foreground hover:bg-primary-600 active:bg-primary-700 rounded-md shadow-sm hover:shadow focus:ring-primary/50",
35
+ outlined:
36
+ "bg-transparent text-primary hover:bg-muted active:bg-primary-50 border border-border rounded-[48px] focus:ring-primary/30",
37
+ pill: "bg-transparent text-primary hover:bg-primary-50 active:bg-muted border border-border rounded-[48px] focus:ring-primary/30",
38
+ "pill-selected":
39
+ "bg-primary-100 text-primary-800 hover:bg-primary-200 active:bg-primary-200 border border-primary-100 rounded-[48px] shadow-sm focus:ring-primary-800/30",
40
+ text: "bg-transparent text-primary hover:bg-muted active:bg-primary-50 rounded-md focus:ring-primary/30",
41
+ ghost:
42
+ "bg-transparent text-muted-foreground hover:bg-muted hover:text-primary active:bg-primary-50 rounded-md focus:ring-primary/30",
43
+ };
44
+
45
+ const sizeClasses = {
46
+ primary: "[padding:0.625em_1.5em] [gap:0.5em]",
47
+ outlined: "[padding:0.5em_0.75em] [gap:0.5em]",
48
+ pill: "[padding:0.5em_0.75em] [gap:0.5em]",
49
+ "pill-selected": "[padding:0.5em_0.75em] [gap:0.5em]",
50
+ text: "[padding:0.375em_0.5em] [gap:0.5em]",
51
+ ghost: "[padding:0.5em_0.75em] [gap:0.5em]",
52
+ };
53
+
54
+ const disabledClasses =
55
+ "disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-current disabled:hover:shadow-none";
56
+
57
+ return (
58
+ <button
59
+ className={cn(
60
+ "cursor-pointer",
61
+ baseClasses,
62
+ variantClasses[variant],
63
+ sizeClasses[variant],
64
+ disabledClasses,
65
+ className
66
+ )}
67
+ {...props}
68
+ >
69
+ {Icon && iconPosition === "left" && (
70
+ <Icon className="[width:1.25em] [height:1.25em]" />
71
+ )}
72
+ {children}
73
+ {Icon && iconPosition === "right" && (
74
+ <Icon className="[width:1.25em] [height:1.25em]" />
75
+ )}
76
+ </button>
77
+ );
78
+ }
src/theme/form/Textarea.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DetailedHTMLProps, TextareaHTMLAttributes, useMemo } from "react";
2
+ import cn from "../../utils/classnames";
3
+
4
+ interface TextareaProps extends DetailedHTMLProps<
5
+ TextareaHTMLAttributes<HTMLTextAreaElement>,
6
+ HTMLTextAreaElement
7
+ > {
8
+ variant?: "default" | "minimal";
9
+ error?: boolean;
10
+ }
11
+
12
+ export default function Textarea({
13
+ variant = "default",
14
+ error = false,
15
+ className = "",
16
+ value,
17
+ ...props
18
+ }: TextareaProps) {
19
+ const baseClasses =
20
+ "w-full font-sans transition-all duration-200 resize-none focus:outline-none";
21
+
22
+ const variantClasses = {
23
+ default:
24
+ "px-4 py-3 border border-border rounded-md bg-white hover:border-primary-300 focus:border-primary focus:ring-2 focus:ring-primary/20",
25
+ minimal:
26
+ "px-4 py-3 border-0 bg-transparent hover:bg-muted/30 focus:bg-white focus:shadow-sm rounded-md",
27
+ };
28
+
29
+ const errorClasses = error
30
+ ? "border-destructive focus:border-destructive focus:ring-destructive/20"
31
+ : "";
32
+
33
+ const disabledClasses = "disabled:cursor-default";
34
+
35
+ const fontSizeClass = useMemo(() => {
36
+ const length = typeof value === "string" ? value.length : 0;
37
+ console.log(length);
38
+
39
+ if (length === 0) return "text-2xl";
40
+ if (length < 50) return "text-2xl";
41
+ if (length < 200) return "text-xl";
42
+ if (length < 300) return "text-lg";
43
+ return "text-base";
44
+ }, [value]);
45
+
46
+ return (
47
+ <textarea
48
+ className={cn(
49
+ baseClasses,
50
+ variantClasses[variant],
51
+ errorClasses,
52
+ disabledClasses,
53
+ fontSizeClass,
54
+ className
55
+ )}
56
+ value={value}
57
+ {...props}
58
+ />
59
+ );
60
+ }
src/theme/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export { default as Button } from "./button/Button";
2
+ export { default as Textarea } from "./form/Textarea";
3
+ export { default as Loader } from "./misc/Loader";
src/theme/misc/Loader.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Loader2 } from "lucide-react";
2
+
3
+ interface LoaderProps {
4
+ size?: number;
5
+ className?: string;
6
+ }
7
+
8
+ export default function Loader({ size = 20, className = "" }: LoaderProps) {
9
+ return (
10
+ <Loader2
11
+ className={`animate-spin text-primary ${className}`}
12
+ style={{ width: size, height: size }}
13
+ />
14
+ );
15
+ }
src/types/transformers.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type InitiateProgressInfo = {
2
+ status: "initiate";
3
+ /**
4
+ * The model id or directory path.
5
+ */
6
+ name: string;
7
+ /**
8
+ * The name of the file.
9
+ */
10
+ file: string;
11
+ };
12
+ export type DownloadProgressInfo = {
13
+ status: "download";
14
+ /**
15
+ * The model id or directory path.
16
+ */
17
+ name: string;
18
+ /**
19
+ * The name of the file.
20
+ */
21
+ file: string;
22
+ };
23
+ export type ProgressStatusInfo = {
24
+ status: "progress";
25
+ /**
26
+ * The model id or directory path.
27
+ */
28
+ name: string;
29
+ /**
30
+ * The name of the file.
31
+ */
32
+ file: string;
33
+ /**
34
+ * A number between 0 and 100.
35
+ */
36
+ progress: number;
37
+ /**
38
+ * The number of bytes loaded.
39
+ */
40
+ loaded: number;
41
+ /**
42
+ * The total number of bytes to be loaded.
43
+ */
44
+ total: number;
45
+ };
46
+ export type DoneProgressInfo = {
47
+ status: "done";
48
+ /**
49
+ * The model id or directory path.
50
+ */
51
+ name: string;
52
+ /**
53
+ * The name of the file.
54
+ */
55
+ file: string;
56
+ };
57
+ export type ReadyProgressInfo = {
58
+ status: "ready";
59
+ /**
60
+ * The loaded task.
61
+ */
62
+ task: string;
63
+ /**
64
+ * The loaded model.
65
+ */
66
+ model: string;
67
+ };
68
+ export type ProgressInfo =
69
+ | InitiateProgressInfo
70
+ | DownloadProgressInfo
71
+ | ProgressStatusInfo
72
+ | DoneProgressInfo
73
+ | ReadyProgressInfo;
74
+ /**
75
+ * A callback function that is called with progress information.
76
+ */
77
+ export type ProgressCallback = (progressInfo: ProgressInfo) => void;
src/utils/classnames.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const cn = (...classes: Array<Record<string, boolean> | string>): string =>
2
+ classes
3
+ .map((entry) =>
4
+ typeof entry === "string"
5
+ ? entry
6
+ : Object.entries(entry || {})
7
+ .filter(([, append]) => append)
8
+ .map(([cl]) => cl)
9
+ .join(" ")
10
+ )
11
+ .filter((e) => e !== "")
12
+ .join(" ");
13
+ export default cn;
src/utils/countWords.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function countWords(text: string): number {
2
+ if (!text || text.trim().length === 0) {
3
+ return 0;
4
+ }
5
+
6
+ // Remove extra whitespace and split by whitespace
7
+ const words = text
8
+ .trim()
9
+ .split(/\s+/)
10
+ .filter((word) => word.length > 0);
11
+
12
+ return words.length;
13
+ }
src/utils/format.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function formatBytes(bytes: number, decimals: number = 1): string {
2
+ if (bytes === 0) return "0 Bytes";
3
+
4
+ const k = 1024;
5
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
6
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
7
+
8
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))}${sizes[i]}`;
9
+ }
10
+
11
+ export function formatTime(ms: number): string {
12
+ if (ms < 1000) {
13
+ return `${Math.round(ms)}ms`;
14
+ }
15
+
16
+ const seconds = ms / 1000;
17
+ if (seconds < 60) {
18
+ return `${seconds.toFixed(1)}s`;
19
+ }
20
+
21
+ const minutes = Math.floor(seconds / 60);
22
+ const remainingSeconds = Math.round(seconds % 60);
23
+ return `${minutes}m ${remainingSeconds}s`;
24
+ }
25
+
26
+ export function formatNumber(num: number): string {
27
+ return new Intl.NumberFormat("en-US").format(num);
28
+ }
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
tsconfig.app.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "Bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "strictNullChecks": false
24
+ },
25
+ "include": ["src"]
26
+ }
tsconfig.app.tsbuildinfo ADDED
@@ -0,0 +1 @@
 
 
1
+ {"root":["./src/app.tsx","./src/initialize.tsx","./src/translate.tsx","./src/constants.ts","./src/main.tsx","./src/vite-env.d.ts","./src/ai/translator.ts","./src/components/languageselector.tsx","./src/theme/index.ts","./src/theme/button/button.tsx","./src/theme/form/textarea.tsx","./src/theme/misc/loader.tsx","./src/types/transformers.ts","./src/utils/classnames.ts","./src/utils/countwords.ts","./src/utils/format.ts"],"version":"5.7.3"}
tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+
8
+ /* Bundler mode */
9
+ "moduleResolution": "Bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+
15
+ /* Linting */
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noUncheckedSideEffectImports": true
21
+ },
22
+ "include": ["vite.config.ts"]
23
+ }
tsconfig.node.tsbuildinfo ADDED
@@ -0,0 +1 @@
 
 
1
+ {"root":["./vite.config.ts"],"version":"5.7.3"}
vite.config.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ })