leaflet_svelte/docs/ADDING_A_FEATURE.md
2026-04-22 01:27:38 +09:00

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 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)