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

4.1 KiB
Raw Blame History

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.
    let isCollapsed = $state(false);
    let currentPoint = $derived($pointsStore.find(p => p.id === id));
    
  • Component instance refs — camelCase + Ref.
    let pointEditorRef: PointEditor | null = $state(null);
    
  • Event handlers — handleXxx.
    function handleSave() {  }
    
  • Props that are event callbacks — onXxx.
    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:

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:

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}:

$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.