- Private properties and methods end with
_suffix (e.g.,domCache_,syncDomWithState_()) - Use
readonlyon properties set in constructor that should never change (e.g.,containerEl,boundHandlers) - Use
protected(notprivate) for properties that subclasses or UI layers need to access - Branded types (
Hertz,dB,Degrees) require explicit casting - check type definitions early - State handlers typed as
(state: Partial<T>) => void, neverFunction | null - Don't use bracket notation (
obj['method']()) to access methods - make them public instead
- CSS custom properties use
--mc-*namespace for Mission Control semantics - Import order:
@tabler/core→tabler-overrides.css→index.css - Use Tabler utility classes (
d-flex,justify-content-between,mb-2,fw-bold,font-monospace) - Cards:
card h-100for equal heights, withcard-headerandcard-body - Forms:
form-rangefor sliders,form-check form-switchfor toggles
- Equipment modules use Core/UI separation:
*-core.ts(business logic) and*-ui-standard.ts(DOM/UI) - Adapters: DOM caching (
domCache_Map), extract handlers to private methods (not inline),dispose()cleanup - UI components created BEFORE
super()call; components needinguniqueIdcreated AFTER - RotaryKnob uses callback-in-constructor; PowerSwitch/ToggleSwitch use
addEventListeners()method - Factories return base Core type for polymorphism
Events.UPDATE- Fires on each simulation tick; use for periodic state sync in adaptersEvents.DRAW- Fires for canvas rendering only; do NOT use for DOM updates- When using
.bind(this)for event handlers, store the bound reference to properly remove it later:
private readonly boundUpdateHandler_: () => void;
constructor() {
this.boundUpdateHandler_ = this.syncDomWithState_.bind(this);
EventBus.getInstance().on(Events.UPDATE, this.boundUpdateHandler_);
}
dispose(): void {
EventBus.getInstance().off(Events.UPDATE, this.boundUpdateHandler_);
}For adapters listening to Events.UPDATE, throttle DOM updates to avoid performance issues:
private static readonly UPDATE_INTERVAL_MS = 1000;
private lastSyncTime_: number = 0;
private throttledSync_(): void {
const now = Date.now();
if (now - this.lastSyncTime_ < ClassName.UPDATE_INTERVAL_MS) return;
this.lastSyncTime_ = now;
this.syncDomWithState_();
}Direct user actions (button clicks, toggles) should bypass throttling for immediate feedback.
When syncDomWithState_() updates input fields, it can overwrite what the user is typing. Always check document.activeElement before updating inputs, selects, or textareas:
// Skip updating if user is focused on this input
const input = qs<HTMLInputElement>('#my-input', this.dom_);
if (input && document.activeElement !== input) {
input.value = state.someValue.toString();
}Apply this pattern to all user-editable fields in sync methods.
For numeric equipment controls (frequency, gain, power, etc.), use the equip-adjust-control pattern:
<div class="equip-adjust-control">
<label class="equip-adjust-label">CONTROL NAME</label>
<div class="equip-adjust-row">
<div class="equip-adjust-buttons equip-adjust-decrease">
<button id="xxx-dec-coarse" class="btn-equip">-N</button>
<button id="xxx-dec-fine" class="btn-equip">-n</button>
</div>
<div class="equip-adjust-display">
<input type="number" id="xxx-value" class="equip-adjust-input" />
</div>
<div class="equip-adjust-buttons equip-adjust-increase">
<button id="xxx-inc-fine" class="btn-equip">+n</button>
<button id="xxx-inc-coarse" class="btn-equip">+N</button>
</div>
<span class="equip-adjust-unit">UNIT</span>
</div>
</div>RF equipment changes must use staged values + Apply button to prevent accidental changes:
private stagedValue_: number = DEFAULT;
private adjustStagedValue_(delta: number): void {
this.stagedValue_ = Math.max(MIN, Math.min(MAX, this.stagedValue_ + delta));
const input = this.domCache_.get('valueInput') as HTMLInputElement;
if (input) input.value = this.stagedValue_.toString();
}
private applyHandler_(): void {
this.module.handleValueChange(this.stagedValue_);
this.syncDomWithState_(this.module.state);
}- Input field IS the display - don't create separate display elements
- Use fixed
width: 3.5remon.btn-equipfor vertical alignment - Status indicators use text spans (e.g., "Locked"/"Unlocked"), not LED circles
- Toggle handlers must check state before calling toggle methods:
if (state !== isChecked) - The
qs()function throws on missing elements - remove refs when removing HTML elements - Use
cacheElement_(htmlId, cacheKey)helper when HTML IDs differ from cache keys
The Satellite class constructor takes signal arrays in a specific order:
new Satellite(
noradId: number,
uplinkSignals: RfSignal[], // First array: signals satellite RECEIVES
downlinkSignals: RfSignal[], // Second array: signals satellite TRANSMITS directly
config: { az, el, rotation, frequencyOffset }
)Key Points:
- First array (uplinks): Use
origin: SignalOrigin.SATELLITE_RX- these are signals the satellite receives and transponds - Second array (downlinks): Use
origin: SignalOrigin.TRANSMITTER- these are signals the satellite transmits directly (e.g., beacons) - Transponder: Automatically converts uplink signals to downlinks using
frequencyOffset- Example: Uplink at 5943 MHz with
frequencyOffset: 2.225e9→ Downlink at 3718 MHz
- Example: Uplink at 5943 MHz with
- Don't duplicate: If a signal is in the uplink array, the transponder creates the downlink automatically. Don't add it to both arrays.
Always use the npm script to check for TypeScript errors:
npm run type-checkDo NOT run tsc directly on individual files:
# WRONG - will fail with module resolution errors
npx tsc --noEmit src/campaigns/nats/scenario5.tsThis project uses @app/* path aliases (e.g., @app/types, @app/equipment/...) that require the full tsconfig.json configuration. Running tsc on individual files bypasses this and produces false "Cannot find module" errors.
- Do NOT add
Co-Authored-Bylines to commit messages - Use conventional commit format:
type(scope): description - Use emoji in commit titles for clarity:
- Example: feat: ✨ Add new frequency adjustment control
- ✨
:sparkles:for new features - 🐛
:bug:for bug fixes - ♻️
:recycle:for refactoring - 📝
:memo:for documentation changes
- Common types:
feat,fix,refactor,test,docs,chore
When you use Plan Mode or create multi-step plans in this repo:
-
Store each plan as a Markdown file under
./plans/in this project.- Filename convention:
phase-<n>-<short-topic>-plan.md - Example:
phase-1-auth-refactor-plan.md
- Filename convention:
-
After completing a phase:
- Write a brief retrospective to
./retrospectives/in this project. - Filename convention:
phase-<n>-<short-topic>-retro.md - Include sections:
What worked,What didn’t,What to change next time.
- Write a brief retrospective to
-
Never write plans or retrospectives into the home directory; always use project-relative paths.