# AGENTS.md

If `scripts/init.js` exists, ask the user to run `npm run init` first.

## Commands

- `npm run build` - Build production bundle (includes locale:build)
- `npm run watch` - Dev server with hot reload (runs lit-localize build first)
- `npm run serve` - Preview production build
- `npm run cy:run` - Run all Cypress component tests (builds locales first)
- `npm run cy:run -- --spec "cypress/component/file.cy.ts"` - Run single test
- `npm run cy:open` - Open Cypress interactive test runner
- `npm run storybook` - Storybook dev server (port 6006, builds locales first)
- `npm run build-storybook` - Build Storybook static files
- `npm run locale:extract` - Extract translatable strings to XLIFF files
- `npm run locale:build` - Build localization for production
- `npx biome check .` - Check code for linting and formatting issues
- `npx biome check . --write` - Auto-fix and format code

Always run `npx biome check . --write` before commits. Build requires `locale:build`.

## Code Style

**Formatting (Biome.js):** 2-space indent, double quotes, semicolons always, imports auto-organized. Biome enforces `noInferrableTypes`, `noUnusedLocals`, `noUnusedParameters`, `noParameterAssign`, `useLiteralKeys: off`, `noExplicitAny: off`.

**TypeScript:** Strict mode enabled. Avoid inferrable types (`noInferrableTypes` enforced). `any` allowed but prefer explicit types. Path aliases: `@components/*`, `@cypress/*`, `@helpers/*`, `@mocks/*`, `@stories/*`, `@styles/*`, `@src/*`. Experimental decorators enabled, `useDefineForClassFields: false`.

**Naming Conventions:**

- Custom element names: kebab-case with `solid-` or `tems-` prefix (`solid-xyz`, `tems-card`)
- Class names: PascalCase (`TemsCardService`, `TemsNotification`)
- Properties in HTML: kebab-case (`data-src`, `img-left`)
- Properties in JS: camelCase (`dataSrc`, `imgLeft`)
- Private methods: underscore prefix (`_navigate`, `_getProxyValue`)
- Private state: use `@state()` decorator

**Imports:**

- Prefer namespace imports for utils: `import * as utils from "@helpers"`
- Explicit imports from lit: `import { html, css, nothing, unsafeCSS } from "lit"`
- SCSS imports with `?inline`: `import Style from "@styles/component.scss?inline"`
- Use `unsafeCSS()` to wrap imported styles or use `css` template literals

**Component Structure:**

```typescript
@customElement("solid-xyz")
@localized()  // Add for i18n components
export class XyzComponent extends utils.OrbitComponent {
  static styles = css`${unsafeCSS(ComponentStyle)}`;

  @property({ attribute: "data-src", reflect: true, type: String })
  dataSrc?: string;  // Public HTML attribute

  @property({ attribute: false, type: Object })
  object: ObjectType;  // Internal object property

  @state()
  private ready = false;  // Private reactive state

  render() {
    return html`<div>${content}</div>`;
  }
}
```

**Component Base Classes:**

- `ComponentObjectHandler` - Single resource display (`object` prop), no data fetching, extends `LitElement`
- `ComponentObjectsHandler` - Array/collection display (`objects` prop), no data fetching, extends `LitElement`
- `OrbitComponent` - Full Orbit integration with data fetching, routing, caching, extends `ComponentObjectsHandler`

**OrbitComponent Patterns:**

- Define `cherryPickedProperties: PropertiesPicker[] = [{ key, value, expand?, cast? }]`
- Use `await this._getProxyValue(url, recursive, targetProperties)` to fetch data
- Always call `this.gatekeeper()` at start of `render()` to check route/data readiness
- Use `Task` from `@lit/task` for async operations:

  ```typescript
  _loadData = new Task(this, {
    task: async ([dataSrc]) => { /* fetch logic */ },
    args: () => [this.dataSrc, this.currentRoute],
  });
  render() {
    return this.gatekeeper() || this._loadData.render({
      pending: () => html`<solid-loader></solid-loader>`,
      complete: (data) => html`...`,
    });
  }
  ```

**Event Handlers:** Always call `e.preventDefault()` on form submissions and click handlers. Use arrow functions for inline handlers: `@click=${(e) => this._handleClick(e)}`.

**i18n:**

- Import: `import { msg, str } from "@lit/localize"`
- Static strings: `msg("Translate this")`
- Variables: `html`<p>${str`Hello ${userName}`}</p>``
- Add `@localized()` decorator to components using translations
- Run `npm run locale:extract` to update translation files

**Error Handling:**

- Wrap async methods in try-catch
- Log only in development: `if (import.meta.env.DEV) console.error(e)`
- Avoid logging in production

**Conditional Rendering:**

- Use `nothing` from `lit` to avoid falsey attributes: `disabled=${condition || nothing}`
- Conditional content: `condition ? html`...`: nothing`

**Navigation:**

- Use `utils.requestNavigation(route, resource["@id"])`
- For navigation targets: add `navigation-target`, `navigation-resource`, `navigation-rdf-type` attributes
- Handle navigation via `this._navigate(e)` inherited from OrbitComponent

**Testing:**

- Cypress with `cypress-ct-lit` adapter
- Tests in `cypress/component/*.cy.ts` matching component directory structure
- Use `cy.mount(html`<component></component>`)` for component testing
- Describe block: `describe("<component-name>", () => { ... })`

**Helper Functions:**

- `utils.formatDate(date, options)` - Date formatting
- `utils.sort(array, property, order)` - Array sorting
- `utils.setupCacheInvalidation(component, { keywords })` - Cache invalidation
- `utils.setupComponentSubscriptions({ component, defaultRoute, ignoreRouter })` - Orbit subscriptions

**Architecture Notes:**

- This is a library component - no default index.html, use Storybook for development
- Components use Orbit for data management and routing
- Shared types and utilities in `@startinblox/solid-tems-shared`
- Use `@lit/task` for async operations instead of raw Promises in render
- Cache invalidation uses keyword-based invalidation on resource changes
