132 lines
4.1 KiB
Markdown
132 lines
4.1 KiB
Markdown
# Conventions
|
||
|
||
## TypeScript
|
||
|
||
- Every file is `.ts` or `<script lang="ts">`. No plain JS outside build config.
|
||
- Types live next to the code they describe. Cross-feature types go in
|
||
`src/lib/domain/`.
|
||
- Prefer `interface` for object shapes, `type` for unions/aliases.
|
||
- Avoid `any`. `unknown` + a narrowing cast is preferred.
|
||
|
||
## File & folder naming
|
||
|
||
- **Components**: `PascalCase.svelte`. Exception: pseudo-routes `+page.svelte`
|
||
etc. per SvelteKit.
|
||
- **Plain modules**: `kebab-or-camel.ts`.
|
||
- **Feature directories**: lowercase (`workspaces`, `prediction`).
|
||
- Each feature folder has an `index.ts` that re-exports its public surface;
|
||
outside code imports from the index, not from individual files.
|
||
|
||
## Component naming inside `<script>`
|
||
|
||
- `$state`/`$derived` variables — camelCase, no prefix.
|
||
```ts
|
||
let isCollapsed = $state(false);
|
||
let currentPoint = $derived($pointsStore.find(p => p.id === id));
|
||
```
|
||
- Component instance refs — camelCase + `Ref`.
|
||
```ts
|
||
let pointEditorRef: PointEditor | null = $state(null);
|
||
```
|
||
- Event handlers — `handleXxx`.
|
||
```ts
|
||
function handleSave() { … }
|
||
```
|
||
- Props that are event callbacks — `onXxx`.
|
||
```svelte
|
||
let { onSelect = () => {} }: Props = $props();
|
||
```
|
||
- HTML `id` attributes — kebab-case, prefixed with a 2–3 letter component code
|
||
(`cp-start-time` for ControlPanel). This keeps IDs globally unique.
|
||
- Stores — PascalCase + `Store` (for top-level domain stores) OR camelCase
|
||
(for feature-scoped stores). Both are fine; be consistent within a module.
|
||
|
||
## Props contract
|
||
|
||
Use the Svelte 5 `Props` interface pattern:
|
||
|
||
```ts
|
||
interface Props {
|
||
title?: string;
|
||
collapsed?: boolean;
|
||
onToggle?: () => void;
|
||
children?: Snippet;
|
||
}
|
||
let {
|
||
title = '',
|
||
collapsed = $bindable(false),
|
||
onToggle = () => {},
|
||
children,
|
||
}: Props = $props();
|
||
```
|
||
|
||
Do not use `export let` in new components.
|
||
|
||
## State
|
||
|
||
- Persisted state (survives reload, syncs across tabs): `persisted(key, initial)`
|
||
from `$state`.
|
||
- Ephemeral UI state (modal open, which tab is active): plain `$state`.
|
||
- Cross-component state that isn't user-facing persistence: create a small
|
||
factory module under the feature folder (`store.ts`) — never a bare
|
||
`writable()` in a `.svelte` file.
|
||
|
||
## Imports
|
||
|
||
Use path aliases (`$api`, `$auth`, `$domain`, `$map`, `$state`, `$ui`, `$i18n`,
|
||
`$features`). Example:
|
||
|
||
```ts
|
||
import { api } from '$api';
|
||
import { authStore, requireAuthenticated } from '$auth';
|
||
import type { Prediction } from '$domain';
|
||
import { Map, plotPrediction } from '$map';
|
||
import { CollapsibleCard, addToast } from '$ui';
|
||
import { t } from '$i18n';
|
||
import { workspacesStore } from '$features/workspaces';
|
||
```
|
||
|
||
Avoid importing from `src/lib/components/...` (it no longer exists).
|
||
|
||
## Strings
|
||
|
||
No hardcoded Russian/English text in new code. Add the key to both
|
||
`src/lib/i18n/locales/ru.json` and `en.json` and read it with `$t('...')`.
|
||
|
||
Placeholders use `{name}`:
|
||
|
||
```ts
|
||
$t('workspaces.deleteConfirm', { name: w.name })
|
||
```
|
||
|
||
## Comments
|
||
|
||
Default is no comment. Add one only when the *why* is non-obvious (hidden
|
||
invariants, workarounds, API quirks). Don’t restate what the code does.
|
||
|
||
Module-level docstrings are welcome: put a short paragraph at the top of a
|
||
file that describes the role of the module, mentioning any non-obvious design
|
||
choices. See `src/lib/state/persisted.ts` for an example.
|
||
|
||
## CSS
|
||
|
||
- Global chrome lives in `src/app.css`.
|
||
- Component-scoped styles go inside the `.svelte` file.
|
||
- Use Bootstrap utility classes when possible; only add custom CSS when the
|
||
design requires it.
|
||
- Never use `!important` except to override third-party libs (MapLibre,
|
||
Bootstrap).
|
||
|
||
## Testing
|
||
|
||
Not set up yet. When tests arrive: feature modules should be testable without
|
||
Svelte — that's why `domain/` and `state/` are pure.
|
||
|
||
## Don't
|
||
|
||
- Don’t reach into `maplibre-gl` directly from a feature component — extend
|
||
`IMap` or add a helper in `$map` instead.
|
||
- Don’t mutate localStorage from a component. Use a persisted store.
|
||
- Don’t perform fetches from a component. Call a `$api` module.
|
||
- Don’t redirect the user on auth failures from individual pages. Use
|
||
`requireAuthenticated()` at the top of `onMount`.
|