| name | description |
|---|---|
coding-standard |
Enforces the project's JavaScript coding standard and UI architecture rules. Use this skill when writing any JavaScript, HTML, or CSS code to ensure no inline scripts, no inline styles, and proper Web Component patterns are followed. |
Modern JavaScript coding standard for reliability and maintainability, plus UI architecture rules for this project.
For architectural principles (SOLID, SoC, platform independence, dependency management), see
architecture-guidelines. For ESLint fix procedures, seeeslint-fix-protocol.
- When writing any JavaScript, HTML, or CSS code
- When creating or modifying components or pages
For static site generation, always prefer build-time over runtime processing.
Rules:
- Generate at build, not at runtime — transform Markdown, process data, and generate HTML during build. Never fetch/parse content client-side.
- Static template values over dynamic arguments — resolve template variables at build time. Avoid passing runtime configuration that could be determined during build.
- Leverage Eleventy's capabilities first — use collections, data files, and filters before writing custom build scripts.
Examples:
| Scenario | ❌ Runtime | ✅ Build-time |
|---|---|---|
| Displaying changelog | Fetch CHANGELOG.md client-side, parse with JS | Copy CHANGELOG.md to articles/ with frontmatter, let Eleventy generate page |
| Template configuration | Pass dynamic site name to templates at runtime | Use SITE env var to select correct layout during build |
| Content transformation | Parse Markdown in browser with library | Let Eleventy process .md files to HTML |
| Data formatting | Format dates/numbers client-side | Use Eleventy filters in templates |
constby default. Only useletfor reassignment. Never usevar.- No globals. Encapsulate in modules or closures.
- Literals over constructors:
{},[]— notnew Object(). - Prefer standard APIs (
EventTarget,URLSearchParams,URL,Intl) before writing custom utilities.
| Convention | Use for | Example |
|---|---|---|
camelCase |
Variables, functions, methods | userProfile, calculateHeartRate |
PascalCase |
Classes, constructors | HealthMonitor, AndromanBiometryLog |
UPPER_SNAKE_CASE |
Constants | MAX_RETRY_ATTEMPTS, API_BASE_URL |
is/has/can prefix |
Booleans | isVisible, hasAccess |
on prefix |
Event handlers | onSubmit, onDownloadClick |
- No Magic Strings: Move user-facing display text to
constdeclarations at the top of the file. Technical HTML strings (selectors, tag names) may remain inline unless heavily repeated.
- Strict equality only:
===and!==. Never==/!=. - Explicit coercion:
String(),Number()— not implicit.
- Arrow functions for callbacks and anonymous functions.
async/awaitover raw.then()/.catch().for...ofor array methods (.map(),.filter(),.reduce()) for iteration.
Rules:
- Always
awaitcalls toasyncfunctions (no floating promises). - If using
await, the enclosing function MUST beasync. - Propagate
asyncup the entire call chain. - Never mix
awaitwith.then()in the same function.
// ❌ Floating promise — bug!
async function bad() {
saveMetric('key', 'value'); // Not awaited!
console.log('Saved!'); // Runs BEFORE save completes!
}
// ✅ Always await
async function good() {
await saveMetric('key', 'value');
console.log('Saved!');
}Refactoring async code:
- If a function is
async, keep itasyncunless ALL async operations removed. - Never remove
awaitwithout confirming the callee is now synchronous. - Search all call sites when changing async signatures.
-
Data First, UI Second: Never update the DOM until the data operation has fully completed (
await). -
No Optimistic UI unless explicitly required.
-
Async event handlers: Declare
async,awaitinternally, useisLoadingflags to prevent re-entry. -
Halt on Terminal Side Effects: If an async operation triggers a reload or redirect, the current execution thread MUST halt. See
architecture-guidelinesSection 8.
// ✅ Correct sequencing
async function handleDateChange(newDate) {
showLoadingSpinner();
try {
await recalculateBiometrics(newDate);
updateDashboard();
} finally {
hideLoadingSpinner();
}
}- No
innerHTMLwith user-generated content. UsetextContent. - Sanitize all external inputs at entry points.
- No silent failures: Never use empty
catchblocks. - Always log: At minimum
console.errororconsole.warn. - Report critical errors: Re-throw or emit via global event (e.g.,
persistence-error). - Safe defaults: If returning a fallback on error, log the reason.
// ❌ NEVER
try {
executeCalculation();
} catch (e) {}
// ✅ At minimum
try {
executeCalculation();
} catch (e) {
console.warn('Calculation failed, using fallback:', e);
return FALLBACK_VALUE;
}- Vendor CDNs for SDKs (e.g.,
js.monitor.azure.com). - Stable versions only: Never
@nightly,@beta, or unversioned links. - Minimize dependencies: Prefer Bootstrap 5 / standard Web APIs first.
- Preload critical assets:
<link rel="preload">for core CSS.
No Inline JavaScript — always external files:
<!-- ❌ NEVER -->
<script>
document.addEventListener('DOMContentLoaded', () => { ... });
</script>
<!-- ✅ ALWAYS -->
<script type="module" src="/js/pages/my-page.js"></script>No Inline CSS — no <style> tags, no style="" attributes:
<!-- ❌ -->
<div style="margin: 10px;">...</div>
<!-- ✅ -->
<div class="m-3 text-danger">...</div>Components either have no custom CSS or use Shadow DOM with fully encapsulated CSS.
- Default (no Shadow DOM): All styling via Bootstrap 5 utility classes. No custom CSS files for the component.
- Shadow DOM components: When a component uses
<template shadowrootmode="open">, it may include its own CSS file loaded inside the shadow root. The CSS is fully encapsulated and does not leak. - Prefer standard Bootstrap utilities (e.g.,
border-5) before adding classes toinline-utilities.css. - Only use
inline-utilities.csswhen no reasonable Bootstrap alternative exists.
- Semantic elements over
<div>: Use<article>,<section>,<nav>,<main>,<aside>,<header>,<footer>where they convey meaning. - Heading hierarchy: One
<h1>per page, then<h2>→<h3>in order. Never skip levels. - Form labels: Every
<input>must have a<label for="...">oraria-label. - Alt text: All
<img>elements must have descriptivealtattributes (oralt=""for decorative images). - ARIA: Use native HTML semantics first. Only add
aria-*attributes when no native element conveys the role (e.g., custom widgets). - Keyboard navigability: Interactive elements must be focusable and operable
via keyboard. Use
<button>for actions,<a>for navigation — never<div onclick>.
- Use
type="text"for inputs that display non-numeric characters (e.g., arrows↑,↓, units%, prefixesp). - Avoid
type="number"for derived fields, as browsers strictly enforce numeric values and will strip or reject formatted strings. - Read-only computed fields: Always use
readonlyandtabindex="-1"to prevent user interaction.
- Explicit Column Widths: Use percentage widths (e.g.,
width="15%") for data columns to ensure consistent alignment across multiple tables. - No
width="1%": Avoid the "shrink to fit" hack (width="1%") for data columns, as it causes layout instability when content varies. - Compact Inputs: Use
form-control-sminside tables to maximize data density. - Prevent Wrapping: Use
text-nowrapon headers and unit columns to maintain single-line vertical rhythm. - Visual Feedback: Calculated fields should default to a "safe" or "neutral" visual state (e.g., green/success) when empty or not yet calculated, rather than looking broken.
This project uses three component patterns. Build-time components are preferred because the HTML is visible immediately — before JavaScript runs.
| Type | Location | When to use |
|---|---|---|
| Build-time (preferred) | packages/shared-ui/_includes/components/{name}/ |
Default — static HTML structure rendered at build time, JS enhances it |
| Build-time (parameterized) | Same, with .html.js shortcode file |
Multi-instance with identical structure but different config (e.g., tier) |
| Runtime-only / base class | packages/shared-ui/js/components/{name}/ |
Base classes, or components with zero meaningful static HTML |
Which pattern to use:
- Does the component have static HTML structure (tables, headings, wrappers)? → Yes: Build-time component. HTML lives in the DOM, JS enhances it. → No: Runtime-only component (JS creates all DOM).
- Is the build-time component used multiple times on the same page with
different config but identical structure? → Yes: Use a parameterized
component (
.html.jsshortcode). → No: Use a regular single-instance build-time component.
The component's static structure lives as real HTML inside the custom element tag — visible immediately on page load. JavaScript only enhances it (populates data, adds interactivity).
my-component/
├── my-component.html # Static HTML inside custom element tag + script
└── my-component.js # Web Component class
<!-- Static structure is REAL DOM, not hidden in a <template> -->
<androman-my-component>
<table class="table table-sm">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody></tbody>
</table>
<!-- Row template for repeating elements only -->
<template id="my-row-template">
<tr>
<td data-field="name"></td>
<td data-field="value"></td>
</tr>
</template>
<script
type="module"
src="/components/my-component/my-component.js"
></script>
</androman-my-component>[!IMPORTANT] The
<template>tag is only for repeating elements (e.g., table rows cloned per data item). The component's static structure (table headers, wrappers, headings) must be real DOM — not hidden inside a<template>.
- Private fields/methods: Use
#prefix for all non-public members. - No
DOMContentLoaded: UseconnectedCallback()for initialization. - Encapsulation: Only access own DOM via
this.querySelector().
export class AndromanMyComponent extends HTMLElement {
#data = null;
connectedCallback() {
this.#initialize();
}
disconnectedCallback() {
// Cleanup
}
#initialize() {
// Internal implementation
}
}
customElements.define('androman-my-component', AndromanMyComponent);Single-instance components — include once:
{% include "components/my-component/my-component.html" %}When a component is used multiple times with different configuration but
identical structure, use a .html.js shortcode file to avoid duplicating HTML.
my-tier-table/
├── my-tier-table.html # Shared row templates + script tag
├── my-tier-table.html.js # Build-time HTML generator (default export)
└── my-tier-table.js # Web Component class
The .html.js extension indicates a JS file that returns an HTML string. Uses a
default export function that takes parameters and returns the component's
static HTML via a template literal.
// my-tier-table.html.js
export default function (tier) {
return `<androman-my-tier-table tier="${tier}">
<div class="table-responsive">
<table class="table table-sm small">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</androman-my-tier-table>`;
}[!NOTE] The "no HTML in JS strings" rule does not apply to
.html.jsfiles. These are build-time generators — their output becomes static HTML in the built page, equivalent to Nunjucks templates.
// eleventy.config.js
const myTierTable = (
await import('./shared-ui/_includes/components/my-tier-table/my-tier-table.html.js')
).default;
eleventyConfig.addShortcode('myTierTable', myTierTable);{%- comment -%} Include shared templates + script once {%- endcomment -%}
{% include "components/my-tier-table/my-tier-table.html" %}
{%- comment -%} Instances via shortcode — build-time rendered {%- endcomment -%}
{% myTierTable "1" %}
{% myTierTable "2" %}
{% myTierTable "3" %}For parameterized components, the .html file contains only the shared
resources that all instances need — row templates and the script tag:
<!-- Shared row template for all instances -->
<template id="my-tier-row-template">
<tr>
<td data-field="name"></td>
<td data-field="value"></td>
</tr>
</template>
<script type="module" src="/components/my-tier-table/my-tier-table.js"></script>When a component depends entirely on runtime data and has no meaningful static
HTML, it lives in packages/shared-ui/js/components/{component-name}/:
my-component/
└── my-component.js # Web Component class (extends HTMLElement)
These components generate their entire DOM programmatically in
connectedCallback(). They follow the same JS patterns (private fields,
androman- prefix, encapsulation) as build-time components.
Use runtime-only components when:
- The component's content is fully determined by runtime data (e.g., user session, API responses)
- There is no useful static HTML to render at build time
- The component serves as a base class for other components
Existing base classes:
| Base class | Purpose | Subclasses |
|---|---|---|
save-form |
Form lifecycle, state sync, calculation, auto-save (5 layers) | Calculator components |
cognitive-test-base |
Test screen management, fullscreen, result saving | Cognitive test components |
chart-base |
Chart.js setup, BiometricsUpdatedEvent, score-to-color | Radar chart components |
data-table-base |
Build-time table structure + runtime row population | Data table components |
[!IMPORTANT] Prefer build-time components. Only use runtime-only components when the page cannot render any meaningful HTML for the component at build time. Build-time components provide faster perceived load times and better SEO because the HTML is already in the page before JavaScript executes.
- Custom elements: Prefix
androman-(e.g.,<androman-biometry-log>) - Classes: Prefix
Andromanin PascalCase (e.g.,AndromanBiometryLog)
Components interact only with their own internal DOM or data passed via attributes/properties/imports:
// ❌ Reaching outside
document.querySelector('#some-external-element').value = 'test';
// ✅ Own content only
this.querySelector('.my-internal-element').textContent = 'test';No HTML in JS strings (except .html.js shortcode files). Use <template>
elements for repeating content that must be cloned at runtime.
<template> is for repeating elements only — content that is cloned N times
from data (e.g., table rows, list items). Static structure (table headers,
wrappers) belongs in the real DOM.
<!-- ✅ Static structure in DOM, only rows as <template> -->
<androman-my-table>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody></tbody>
</table>
<template id="my-row-template">
<tr>
<td data-field="name"></td>
<td data-field="value"></td>
</tr>
</template>
</androman-my-table>// In connectedCallback: find existing tbody, clone row templates, append
const tbody = this.querySelector('tbody');
const rowTemplate = this.querySelector('#my-row-template');
data.forEach(item => {
const row = rowTemplate.content.cloneNode(true);
row.querySelector('[data-field="name"]').textContent = item.name;
tbody.appendChild(row);
});Shared JS modules in packages/shared-ui/js/ (e.g., auth.js,
storage-service.js).
Services must be classes with # private methods, exported as singletons:
class AuthHelper {
#user = null;
async getUserInfo() {
/* public API */
}
#validateToken() {
/* private */
}
}
const auth = new AuthHelper();
export { AuthHelper, auth };Services have NO knowledge of the DOM:
- ❌ No
document.querySelector(), no DOM manipulation - ✅ Only manage data, state, and API calls
- ✅ Return data — let components handle UI
- ✅ Environment-agnostic (no direct browser globals in core logic)
Always use absolute paths starting with /js/:
// ❌ import { auth } from '../auth.js';
// ✅ import { auth } from '/js/auth.js';When moving or renaming JS files:
- Before moving:
grep -r "from '/js/old/path" --include="*.js" packages/to find all import references. - Update ALL imports to the new path.
- After moving: Re-run the grep — should return no results.
- Run build (
npm run build) and verify no 404s in browser console.
Import references may exist in: components (components/*/), page scripts
(js/pages/), services (js/), and calculators.
Every class and public function must have JSDoc:
/**
* Calculate the user's biological age based on biomarkers.
* @param {Object} biomarkers - The biomarker data
* @param {number} biomarkers.vo2max - VO2 max value
* @returns {number} Calculated biological age
*/
function calculateBiologicalAge(biomarkers) { ... }| Content Type | Location |
|---|---|
| Site pages | packages/{site-name}/ |
| Component folder (2 files) | packages/shared-ui/_includes/components/{name}/ |
| Runtime-only components | packages/shared-ui/js/components/{name}/ |
| Global/shared CSS | packages/shared-ui/css/ |
| Page-specific CSS | packages/shared-ui/css/pages/ |
| Shared JS modules / Services | packages/shared-ui/js/ |
| Page-specific JS | packages/shared-ui/js/pages/ |
- Create HTML in the appropriate site package
- Create page CSS in
packages/shared-ui/css/pages/{page-name}.css - Create page JS in
packages/shared-ui/js/pages/{page-name}.js - Reference external files with
<link>and<script type="module" src="...">
All coding standard rules are enforced by ESLint. See eslint-fix-protocol
skill for fix procedures.
Key enforced rules:
| Rule | What It Checks |
|---|---|
eqeqeq |
Must use ===/!== |
no-var |
Must use const/let |
prefer-const |
Use const when never reassigned |
curly |
Braces required for multi-line blocks |
no-unused-vars |
No unused vars (prefix _ if intentional) |
no-unsanitized/property |
No unsafe innerHTML |
Disabling rules (last resort) — always include justification:
// eslint-disable-next-line no-unsanitized/property -- trusted static HTML
element.innerHTML = templateContent;The packages/api/ directory uses TypeScript with stricter rules.
No any type (@typescript-eslint/no-explicit-any = error):
// ❌ function processData(data: any): any { ... }
// ✅ Use proper types
interface DataPayload {
value: string;
timestamp: number;
}
function processData(data: DataPayload): string {
return data.value;
}
// ✅ For unknown data, use 'unknown' + type guards
function handleUnknown(data: unknown): string {
if (typeof data === 'object' && data !== null && 'value' in data) {
return String((data as { value: unknown }).value);
}
throw new Error('Invalid data format');
}Type alternatives to any:
Instead of any |
Use |
|---|---|
| Unknown JSON response | unknown + type guards |
| Dynamic keys | Record<string, ValueType> |
| Accepts anything | Generic <T> |
| Mixed array | Array<string | number> (union) |