3.7 KiB
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.
// 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.
// 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)
// 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.
<!-- 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:
{
"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).
<!-- 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
<!-- 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 checkpassesnpm run buildpasses- All user-visible strings routed through
$t - Feature only depends on shared modules (
$domain,$state,$api,$ui,$map) — not on other features' internals index.tsexports only what the outside world needs- Panel matches existing visual language (CollapsibleCard, Bootstrap sm form controls, same spacing)