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

152
docs/ADDING_A_FEATURE.md Normal file
View file

@ -0,0 +1,152 @@
# Adding a feature
Walk-through for building a new side-panel feature. We'll use a hypothetical
"NOTAMs" panel (no-fly-zone overlays) as the example.
## 1. Pick a feature folder
```
src/lib/features/notams/
├── index.ts
├── store.ts
├── types.ts
├── NotamsPanel.svelte
└── NotamRenderer.svelte # if you draw on the map
```
Re-export the public surface from `index.ts`.
## 2. Define the domain
If the feature introduces a durable type (something saved to a server or
localStorage), put the type in `src/lib/domain/notam.ts` and re-export from
`src/lib/domain/index.ts`. Ephemeral state types can live in the feature's
`types.ts`.
```ts
// src/lib/domain/notam.ts
export interface Notam {
id: string;
code: string;
polygon: LatLngTuple[];
validFrom: string;
validTo: string;
}
```
## 3. Add state
Feature-scoped stores go in the feature folder. Use `persisted()` if the user
should not lose the state on reload, plain `writable()` otherwise.
```ts
// src/lib/features/notams/store.ts
import { persisted } from '$state';
import type { Notam } from '$domain';
export const notamsStore = persisted<Notam[]>('notams', []);
```
## 4. Add API (if needed)
```ts
// src/lib/api/notams.ts
import { api } from './client';
import type { Notam } from '$domain';
export const notamsApi = { list: () => api.get<Notam[]>('/notams/') };
```
And export from `src/lib/api/index.ts`.
## 5. Build the panel
Re-use UI primitives. Wrap everything in `CollapsibleCard` so it lands in the
side panel.
```svelte
<!-- src/lib/features/notams/NotamsPanel.svelte -->
<script lang="ts">
import { CollapsibleCard } from '$ui';
import { t } from '$i18n';
import { notamsStore } from './store';
</script>
<CollapsibleCard title={$t('notams.title')}>
{#each $notamsStore as notam (notam.id)}
<div>{notam.code}</div>
{/each}
</CollapsibleCard>
```
## 6. Add i18n keys
Append to `src/lib/i18n/locales/ru.json` and `en.json`. The lookup is
dot-separated:
```json
{
"notams": {
"title": "NOTAMs",
"empty": "Нет активных NOTAMs"
}
}
```
## 7. Draw on the map (optional)
Every on-map drawing goes through a Scene. One scene per feature, or per sub-
entity if the feature manages many independent overlays (like workspaces).
```svelte
<!-- src/lib/features/notams/NotamRenderer.svelte -->
<script lang="ts">
import { getMap } from '$map';
import { notamsStore } from './store';
const map = getMap();
if (!map) throw new Error('NotamRenderer requires a <Map /> ancestor');
$effect(() => {
const scene = map.scene('notams');
scene.clear();
for (const n of $notamsStore) {
// scene.addLine/addCircle/etc.
}
});
</script>
```
Add `<NotamRenderer />` as a child of `<MapView />` in the predict route.
## 8. Wire into the route
```svelte
<!-- src/routes/predict/+page.svelte -->
<MapView>
<WorkspaceRenderer />
<NotamRenderer />
<PanelContainer position="right">
<TabBar tabs={[...] } bind:active={rightTab} />
{#if rightTab === 'notams'}
<NotamsPanel />
{/if}
</PanelContainer>
</MapView>
```
## 9. Add a settings entry (optional)
If the feature should be toggleable, extend `src/lib/features/settings/store.ts`
and `schema.ts`. Keep the field declarative (kind + labelKey + path) — the
SettingsPanel renders it automatically.
## 10. Checklist before you open a PR
- [ ] `npm run check` passes
- [ ] `npm run build` passes
- [ ] All user-visible strings routed through `$t`
- [ ] Feature only depends on shared modules (`$domain`, `$state`, `$api`,
`$ui`, `$map`) — not on other features' internals
- [ ] `index.ts` exports only what the outside world needs
- [ ] Panel matches existing visual language (CollapsibleCard, Bootstrap sm
form controls, same spacing)