4.1 KiB
Conventions
TypeScript
- Every file is
.tsor<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
interfacefor object shapes,typefor unions/aliases. - Avoid
any.unknown+ a narrowing cast is preferred.
File & folder naming
- Components:
PascalCase.svelte. Exception: pseudo-routes+page.svelteetc. per SvelteKit. - Plain modules:
kebab-or-camel.ts. - Feature directories: lowercase (
workspaces,prediction). - Each feature folder has an
index.tsthat re-exports its public surface; outside code imports from the index, not from individual files.
Component naming inside <script>
$state/$derivedvariables — 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
idattributes — kebab-case, prefixed with a 2–3 letter component code (cp-start-timefor 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 barewritable()in a.sveltefile.
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). Don’t 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
.sveltefile. - Use Bootstrap utility classes when possible; only add custom CSS when the design requires it.
- Never use
!importantexcept 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
- Don’t reach into
maplibre-gldirectly from a feature component — extendIMapor add a helper in$mapinstead. - Don’t mutate localStorage from a component. Use a persisted store.
- Don’t perform fetches from a component. Call a
$apimodule. - Don’t redirect the user on auth failures from individual pages. Use
requireAuthenticated()at the top ofonMount.