{/if}
```
## Styling
### Tailwind Configuration
```javascript
// tailwind.config.js
import sharedConfig from '@manacore/shared-tailwind';
export default {
presets: [sharedConfig],
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {
// Project-specific overrides
},
},
};
```
### Global Styles
```css
/* src/app.css */
@import 'tailwindcss';
@import '@manacore/shared-tailwind/theme.css';
/* Custom utilities */
@layer utilities {
.scrollbar-thin {
scrollbar-width: thin;
}
}
/* Custom components */
@layer components {
.btn-primary {
@apply px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors;
}
}
```
## Form Handling
```svelte
```
## Environment Variables
### Build-Time vs Runtime Variables
SvelteKit has **two types** of environment variables:
1. **Build-time** (`$env/static/public`) - Baked into the bundle at build time
2. **Runtime** (`process.env`) - Available at runtime in server code
**CRITICAL**: For Docker deployments, browser-facing URLs must use **runtime injection** because:
- Docker images are built once but deployed to different environments (staging, production)
- Build-time variables would require rebuilding the image for each environment
- The browser cannot access `process.env` - it needs values injected into the HTML
### ❌ WRONG - Hardcoded or Build-Time URLs
```typescript
// ❌ BAD - Hardcoded URL (won't work in Docker)
const MANA_AUTH_URL = 'http://localhost:3001';
// ❌ BAD - Build-time variable (works locally, breaks in Docker)
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
// ❌ BAD - import.meta.env is also build-time
const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
```
### ✅ CORRECT - Runtime Injection Pattern
**Step 1: Create `hooks.server.ts`** to inject env vars into HTML:
```typescript
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
// Get client-side URLs from Docker runtime environment
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
const PUBLIC_BACKEND_URL_CLIENT =
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
export const handle: Handle = async ({ event, resolve }) => {
return resolve(event, {
transformPageChunk: ({ html }) => {
// Inject runtime environment variables into the HTML
const envScript = ``;
return html.replace('', `${envScript}`);
},
});
};
```
**Step 2: Read from `window` in client code:**
```typescript
// src/lib/stores/auth.svelte.ts
import { browser } from '$app/environment';
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
// Client-side: use injected window variable
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
return injectedUrl || 'http://localhost:3001';
}
// Server-side (SSR): use Docker internal URL
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
}
// Use in auth service initialization
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
```
**Step 3: Set environment variables in `docker-compose.staging.yml`:**
```yaml
services:
myapp-web:
environment:
# Server-side URLs (Docker internal network)
PUBLIC_BACKEND_URL: http://myapp-backend:3000
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
# Client-side URLs (browser access via public IP)
PUBLIC_BACKEND_URL_CLIENT: https://myapp.mana.how:3000
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://myapp.mana.how:3001
```
### Why Two URLs?
| Variable | Purpose | Example |
| ---------------------------------- | --------------------------------- | ----------------------------- |
| `PUBLIC_MANA_CORE_AUTH_URL` | Server-to-server (SSR, API calls) | `http://mana-core-auth:3001` |
| `PUBLIC_MANA_CORE_AUTH_URL_CLIENT` | Browser to server | `https://myapp.mana.how:3001` |
Docker containers can reach each other by service name (`mana-core-auth`), but browsers need the public IP/domain.
### Apps Using This Pattern Correctly
All web apps with backends now use the runtime injection pattern:
- ✅ `chat/apps/web`
- ✅ `picture/apps/web`
- ✅ `quotes/apps/web`
- ✅ `contacts/apps/web`
- ✅ `calendar/apps/web`
- ✅ `clock/apps/web`
- ✅ `todo/apps/web`
### Apps That May Need Fixing
- ❓ `cards/apps/web` - Check if using dynamic URLs
- ❓ `manacore/apps/web` - Check if using dynamic URLs
### Quick Checklist for New SvelteKit Apps
- [ ] Create `src/hooks.server.ts` with env injection
- [ ] Update `auth.svelte.ts` to use `getAuthUrl()` pattern
- [ ] Update `user-settings.svelte.ts` to use `getAuthUrl()` pattern
- [ ] Update any feedback services to use runtime URL
- [ ] Add both `_CLIENT` and non-client env vars to `docker-compose.staging.yml`
- [ ] Never hardcode `localhost:3001` anywhere
### Simple .env (for local development only)
```env
PUBLIC_BACKEND_URL=http://localhost:3016
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
```
These work locally because both the browser and server access `localhost`.
## Anti-Patterns to Avoid
### Don't Use Old Svelte Syntax
```svelte
```
### Don't Create Stores in Components
```svelte
```
### Don't Fetch in Render
```svelte
{#await promise}...{/await}
```
## i18n
The unified Mana app supports `de` (default + fallback), `en`, `it`,
`fr`, `es`. Three CI gates protect the i18n surface — read them before
adding new strings.
### Strings → svelte-i18n
```svelte
```
Never hard-code German into `.svelte` markup or attributes. The
`validate:i18n-hardcoded` baseline ratchets per-file: if a file's
count of placeholder/title/aria-label/text-content strings with
umlauts goes up, CI fails. Run `pnpm run validate:i18n-hardcoded -- --update`
only after intentionally adding non-translated dev-only UI.
Adding a new key:
1. Drop it into `apps/mana/apps/web/src/lib/i18n/locales//de.json`
2. Add the same path to `en.json`, `it.json`, `fr.json`, `es.json`
— the `validate:i18n-parity` check requires identical key-sets
across all 5 locales.
3. New namespace = create the folder + 5 JSONs. Registration is
automatic via `import.meta.glob`; no edits to `i18n/index.ts`.
A `$_('typo.key')` call that resolves to nothing is caught by
`validate:i18n-keys`. Existing 315 broken refs are baselined so you
can fix them gradually; new misses fail the build.
### Dates and numbers → format helpers
```svelte
```
Never call `.toLocaleDateString('de-DE', …)` or `Intl.NumberFormat('de-DE', …)`
directly — those pin output to German regardless of the active locale.
The helpers in `$lib/i18n/format.ts` pull the active locale from the
svelte-i18n store and map (`de → de-DE`, `en → en-US`, …).
For date-fns formatters that need a locale object, use
`getDateFnsLocale()` from the same module instead of importing
`{ de }` from `'date-fns/locale'`.