feat: polish

This commit is contained in:
Anatoly Antonov 2026-04-22 01:27:38 +09:00
parent 2e6177fe74
commit 4bd927bb4e
137 changed files with 6357 additions and 137560 deletions

132
docs/CONVENTIONS.md Normal file
View file

@ -0,0 +1,132 @@
# 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 23 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). Dont 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
- Dont reach into `maplibre-gl` directly from a feature component — extend
`IMap` or add a helper in `$map` instead.
- Dont mutate localStorage from a component. Use a persisted store.
- Dont perform fetches from a component. Call a `$api` module.
- Dont redirect the user on auth failures from individual pages. Use
`requireAuthenticated()` at the top of `onMount`.