diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..bae7132
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,76 @@
+# Building on the That Open Platform — Agent Guide
+
+You are an AI assistant helping a user build a **BIM app** or **cloud component** on the
+That Open Platform using **`@thatopen/services`** (the `thatopen` CLI + client library).
+
+**Read this file first.** It states the rules you must follow, then routes you to the one
+detailed doc for whatever you're doing. Open only the doc you need — don't load everything.
+
+> This guide is tool-agnostic: it works with any AI (Claude, Codex, your own). All docs it
+> references ship inside the `@thatopen/services` package under `docs/`.
+
+---
+
+## How to work
+
+- **New app or component?** The **first step is always the CLI** (`thatopen create`) — never
+ hand-write the scaffold. See [docs/cli-setup.md](./docs/cli-setup.md) then
+ [docs/scaffolding.md](./docs/scaffolding.md).
+- **Propose a short plan and get the user's OK** before changing files. If scope is unclear, ask.
+- Prefer existing engine functionality over custom code (see rule 2).
+
+## Hard rules (always apply)
+
+1. **All business logic lives in a BIM component** (`src/bim-components/`).
+ `setups/` only wire; `ui-components/` only render; `main.ts` only boots. Logic that doesn't
+ fit an existing component → create a new one. Never put logic in a setup, template, or `main.ts`.
+2. **Check engine components first** — `@thatopen/components` (`OBC`) and
+ `@thatopen/components-front` (`OBF`) — before building anything custom.
+3. **Platform built-ins** (`AppManager`, `ViewportsManager`, `UIManager`, …) come from
+ `@thatopen/services` and are available after `client.setup()`. Don't reinvent them.
+
+---
+
+## What are you doing? → open the right doc
+
+### Set up & ship
+| Goal | Doc |
+|---|---|
+| Install the CLI + authenticate | [docs/cli-setup.md](./docs/cli-setup.md) |
+| Scaffold a new app / component | [docs/scaffolding.md](./docs/scaffolding.md) |
+| Preview the app inside the platform | [docs/previewing.md](./docs/previewing.md) |
+| Publish an app or component | [docs/publishing.md](./docs/publishing.md) |
+
+### Structure & wire an app
+| Goal | Doc |
+|---|---|
+| Project structure, architecture rules, component tiers | [docs/app-architecture.md](./docs/app-architecture.md) |
+| Boot / `app.ts` / `main.ts` / `client.setup()` | [docs/app-wiring.md](./docs/app-wiring.md) |
+| Configure layout, add/reorganize grid sections | [docs/app-layout.md](./docs/app-layout.md) |
+| Connect component logic to the UI | [docs/connect-logic-to-ui.md](./docs/connect-logic-to-ui.md) |
+| Update a grid element's state at runtime | [docs/update-grid-elements.md](./docs/update-grid-elements.md) |
+| Access the backend client / project data | [docs/access-backend-data.md](./docs/access-backend-data.md) |
+| Register and use icons | [docs/using-icons.md](./docs/using-icons.md) |
+| Declare and use colors | [docs/using-colors.md](./docs/using-colors.md) |
+
+### Build a custom BIM component
+Start at [docs/bim-components/overview.md](./docs/bim-components/overview.md) — conventions,
+lifecycle (setup/cleanup), exposing events, observable/element collections, per-frame updates,
+saving/restoring state, user-driven object creation.
+
+### Build a UI component
+Start at [docs/ui-components/overview.md](./docs/ui-components/overview.md) — rendering patterns,
+section layout, data tables, inline forms, confirmation dialogs, display text, async actions.
+
+### Cloud components & automations
+[docs/cloud-components.md](./docs/cloud-components.md) — build / run locally / publish a cloud
+component, the execution globals, and event-triggered automations.
+
+---
+
+## Reference (also shipped in this package)
+
+- **Library API** — `PlatformClient` vs `EngineServicesClient`, the permissions contract, and the
+ full method surface → [CONTEXT.md](./CONTEXT.md)
+- **Built-in components API** — config interfaces, method signatures, `@example` blocks →
+ `src/built-in/index.ts` (in the installed package, `node_modules/@thatopen/services/`)
diff --git a/CONTEXT.md b/CONTEXT.md
index 69d593c..75c6d26 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -1,4 +1,4 @@
-# thatopen-services
+# @thatopen/services
Client library and CLI for the That Open Platform — a cloud platform for building BIM (Building Information Modeling) software.
@@ -81,7 +81,7 @@ every request, so Auth0's `getAccessTokenSilently()` and similar
refreshing sources Just Work:
```ts
-import { PlatformClient } from 'thatopen-services';
+import { PlatformClient } from '@thatopen/services';
const client = new PlatformClient(
() => auth0.getAccessTokenSilently(),
'https://api.thatopen.com',
@@ -142,7 +142,7 @@ yarn create-version # Build → changeset → version → publish
| **Item type** | `APP` | `TOOL` |
| **Entry point** | Side effects in `main.ts` (renders UI) | `export async function main()` |
| **Context** | `window.__THATOPEN_CONTEXT__` provides `{ appId, projectId, accessToken, apiUrl }` | Globals: `thatOpenServices`, `executionParams`, `executionContext` (`{ projectId?, executionId, toolId, toolVersion }`), `executionReporter` (`message/error/progress`). `OBC`, `THREE`, `web-ifc`, `fs` are NOT injected — import them and let the bundler include them. |
-| **Build output** | IIFE `dist/bundle.js` (all deps bundled) | IIFE `dist/bundle.js` (only `thatopen-services` externalized) |
+| **Build output** | IIFE `dist/bundle.js` (all deps bundled) | IIFE `dist/bundle.js` (only `@thatopen/services` externalized) |
| **Template** | `bim`, `default`, or `test` | `cloud` or `cloud-test` |
### Authentication
@@ -156,7 +156,7 @@ Two modes, controlled by `useBearer` in the constructor:
Built-in components are platform-hosted UI modules fetched at runtime. Usage pattern:
```ts
-import { AppManager, ViewportManager } from "thatopen-services";
+import { AppManager, ViewportManager } from "@thatopen/services";
// Register all library globals once
client.setBuiltInGlobals({ OBC, OBF, BUI, CUI, THREE, FRAGS });
diff --git a/README.md b/README.md
index 1598a9f..a887abc 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# thatopen-services
+# @thatopen/services
Client library and CLI for building BIM apps and cloud components on the [That Open Platform](https://platform.thatopen.com).
@@ -8,7 +8,7 @@ Client library and CLI for building BIM apps and cloud components on the [That O
```bash
# Install the services package globally
-npm i thatopen-services -g
+npm i @thatopen/services -g
```
Then, create a brand new app repository:
@@ -58,7 +58,7 @@ Use `npx thatopen create .` to scaffold in the current directory instead of crea
## Library usage
```typescript
-import { EngineServicesClient } from 'thatopen-services';
+import { EngineServicesClient } from '@thatopen/services';
const client = new EngineServicesClient(accessToken, apiUrl);
@@ -152,7 +152,7 @@ Cloud components export an `async function main()` that runs on the server. The
Platform-hosted UI components loaded at runtime:
```typescript
-import { AppManager, ViewportManager } from "thatopen-services";
+import { AppManager, ViewportManager } from "@thatopen/services";
// Register all library globals once
client.setBuiltInGlobals({ OBC, OBF, BUI, CUI, THREE, FRAGS });
diff --git a/docs/access-backend-data.md b/docs/access-backend-data.md
new file mode 100644
index 0000000..a149c08
--- /dev/null
+++ b/docs/access-backend-data.md
@@ -0,0 +1,19 @@
+# Access Backend Data
+
+## Accessing backend data via `AppManager`
+
+After `init()`, `AppManager` exposes:
+
+- **`app.client`** — `EngineServicesClient`: backend client for API calls
+- **`app.projectData`** — `ProjectData`: current user info, role, project metadata
+
+Both are resolved before any setup runs. Always access them through `getAppManager(components)`:
+
+```ts
+const app = getAppManager(components)
+if (app.projectData.currentUser?.role.name === "Project Admin") {
+ // render admin-only controls
+}
+
+const files = await app.client.getProjectFiles(app.projectData.id)
+```
diff --git a/docs/app-architecture.md b/docs/app-architecture.md
new file mode 100644
index 0000000..2c36256
--- /dev/null
+++ b/docs/app-architecture.md
@@ -0,0 +1,94 @@
+# App Architecture and Composition
+
+## Architecture rule: logic lives only in BIM components
+
+**All business logic in a platform app must live inside a BIM component** (`src/bim-components/`). This is not a convention — it is the architectural constraint of the platform.
+
+| Layer | What belongs here | What does NOT belong here |
+|---|---|---|
+| `bim-components/` | State, events, async operations, data access, coordination logic | — |
+| `setups/` | Event subscriptions, calling component methods, UI sync | Business logic, data processing, state |
+| `ui-components/` | Rendering, layout, reading component state | Logic, API calls, mutations |
+| `main.ts` | Boot sequence only | Any logic whatsoever |
+
+If logic doesn't fit inside an existing BIM component, the answer is always to create a new one or extend an existing one — never to write it in a setup file, a template, or `main.ts`.
+
+```ts
+// ✗ Forbidden — logic in a setup file
+export const mySetup = (components: OBC.Components) => {
+ const fragments = components.get(OBC.FragmentsManager)
+ fragments.list.onItemSet.add(async (model) => {
+ const data = await fetchSomething(model.uuid) // ← logic here is wrong
+ processData(data)
+ })
+}
+
+// ✓ Correct — setup only wires; logic lives in the component
+export const mySetup = (components: OBC.Components) => {
+ const uis = getUIManager(components)
+ const manager = components.get(MyManager)
+ manager.onDataReady.add(() => uis.custom.get("myPanel").updateInstances())
+}
+```
+
+---
+
+## Component tiers in a platform app
+
+Every platform app works with three tiers of components, all accessible via `components.get()`:
+
+- **Engine components** — public, from `@thatopen/components` (OBC) and `@thatopen/components-front` (OBF). Always check these first before building custom logic.
+- **Platform built-in components** — private components from `@thatopen/services` (e.g. `AppManager`, `UIManager`, `ViewportsManager`). Available after `client.setup()`, already wired up.
+- **Custom components** — see the BIM-component and UI-component guides in `docs/`. Live in `src/bim-components/`, self-register in their constructor via `components.add()`.
+
+---
+
+## Project Structure
+
+Every app has this shape under `src/`. To configure the app layout or add and reorganize sections in the grid, see [./app-layout.md](./app-layout.md).
+
+```
+src/
+├── bim-components/ → custom OBC domain components
+├── ui-components/ → BUI templates, barrel only
+├── setups/ → initialization and wiring
+├── app.ts → typed app identity
+├── globals.ts → global constants (icons, tooltips, colors, etc.)
+└── main.ts → entry point
+```
+
+### `bim-components/`
+
+Each custom OBC component lives in its own folder, built following the BIM-component guide in `docs/` (see [./bim-components/overview.md](./bim-components/overview.md)). The `index.ts` barrel re-exports every component class, its types, and its static `uuid` — this is the single import point for all custom domain logic throughout the app.
+
+### `ui-components/`
+
+Templates created following the UI-component guide in `docs/` (see [./ui-components/overview.md](./ui-components/overview.md)) are re-exported from the barrel. The `index.ts` is a **pure barrel** — only re-exports, nothing else. Registration of templates in the UIManager happens in `setups/ui-manager.ts`.
+
+### `setups/`
+
+One file per component being initialized. `ui-manager.ts` is always present as fixed boilerplate; all other files are custom wiring. See [./connect-logic-to-ui.md](./connect-logic-to-ui.md) for the full implementation reference. To update the state of a grid element at runtime from a setup or event handler, see [./update-grid-elements.md](./update-grid-elements.md).
+
+### `app.ts` and `main.ts`
+
+`app.ts` defines the app's typed shape and the `getAppManager` accessor. `main.ts` boots the platform — no business logic belongs here. See [./app-wiring.md](./app-wiring.md) for structure, `client.setup()`, and `componentSetups`. To access the backend client or project data (`app.client`, `app.projectData`), see [./access-backend-data.md](./access-backend-data.md).
+
+### `globals.ts`
+
+Contains the icon registry, color registry, and any other global constants. All icons must be declared here and accessed through `AppManager`. Colors are declared here and imported directly where needed. See [./using-icons.md](./using-icons.md) and [./using-colors.md](./using-colors.md) for details.
+
+---
+
+## See also
+
+- [Connect logic to the UI](./connect-logic-to-ui.md) — register templates, sync views, etc.
+- [Configure the app layout](./app-layout.md) — add or reorganize sections in the grid.
+- [App wiring](./app-wiring.md) — boot the app, configure `app.ts`, `main.ts`, `client.setup()`, `componentSetups`.
+- [Access backend data](./access-backend-data.md) — the backend client and project data (`app.client`, `app.projectData`).
+- [Using icons](./using-icons.md) — register or use icons in the app.
+- [Using colors](./using-colors.md) — declare or use global colors (highlighter, palette, etc.).
+- [Update grid elements](./update-grid-elements.md) — update the state of a grid element at runtime.
+- [Scaffolding a new app](./scaffolding.md) — scaffold a new app from scratch using the CLI.
+- [Publishing an app](./publishing.md) — publish an app to the platform (login + publish).
+- [CLI setup](./cli-setup.md) — install the CLI and authenticate with a platform token.
+- [Previewing apps](./previewing.md) — preview the app during development inside the platform.
diff --git a/docs/app-layout.md b/docs/app-layout.md
new file mode 100644
index 0000000..aa64246
--- /dev/null
+++ b/docs/app-layout.md
@@ -0,0 +1,139 @@
+# Configure App Layout
+
+## Grid element constraint
+
+Every entry in `grid.elements` must be a single `bim-panel-section`. This is a hard constraint of the platform — the grid only knows how to host panel sections, not arbitrary HTML containers.
+
+**Correct:** one `grid.elements` entry = one `bim-panel-section`
+**Wrong:** a UI component that returns a `
` wrapping multiple `` elements used as a grid entry
+
+If you need multiple sections in one area, register each section separately in `grid.elements` and group them using `panel:` or `tabs:` in the grid template — never wrap them in a container component.
+
+```ts
+// ✗ Wrong — MyDashboard returns
......
+grid.elements = {
+ dashboard: { template: uis.custom.get("myDashboard").template, initialState: { components } },
+}
+
+// ✓ Correct — each section is its own grid element; grouping happens in the template
+grid.elements = {
+ stats: { template: uis.custom.get("statsSection").template, initialState: { components } },
+ charts: { template: uis.custom.get("chartsSection").template, initialState: { components } },
+}
+// Then in the layout template:
+// "panel:right(stats,charts) viewer" 1fr / 22rem 1fr
+```
+
+---
+
+## Grid sync rules
+
+Any change to the grid requires updates in multiple places. Never update one without the others.
+
+**Adding or removing a layout:**
+1. `app.ts` → first argument of `BUI.Grid<[layouts], ...>`
+2. `main.ts` → `grid.layouts` (add/remove the definition) and `grid.layout` if it was the initial layout
+
+**Adding or removing an element:**
+1. `app.ts` → second argument of `BUI.Grid<..., [elements]>`
+2. `main.ts` → `grid.elements`, `grid.areaGroups` (if applicable), and any layout templates that reference it
+
+**Renaming an element** — the name originates in the component, so the change cascades outward:
+1. Rename the folder in `ui-components/` (kebab-case of the new name)
+2. Update all internal identifiers: types (`{ComponentName}State`, `{ComponentName}Component`, `{ComponentName}GridElement`), functions (`{camelCase}Template`, `on{PascalCase}Created`)
+3. Update the `ui-components/index.ts` barrel to point to the renamed folder
+4. `app.ts` → second argument of `BUI.Grid` reflects the new `{ComponentName}GridElement`
+5. `main.ts` → `grid.elements` and all layout templates that use that area name
+
+---
+
+## Defining layouts
+
+```ts
+grid.layouts = {
+ Viewer: {
+ template: `"viewer" 1fr / 1fr`,
+ },
+ Quantities: {
+ icon: app.icons.QUANTITY,
+ template: `
+ "qtos viewer" 1fr
+ /40rem 1fr
+ `,
+ },
+}
+grid.layout = "Collider"
+```
+
+---
+
+## Special area tokens
+
+```
+{groupType}:{areaName}(elementA, elementB)
+```
+
+There are two group types: `tabs` and `panel`.
+
+### `tabs:` — one element at a time
+
+Shows one element at a time with tab switchers. The user picks which section is visible. Supports nested sub-groups:
+
+```
+tabs:{areaName}({groupName}[elementA, elementB], elementC)
+```
+
+```ts
+Collider: {
+ icon: appIcons.COLLISION,
+ template: `
+ "tabs:left(group[collider,qtos],collider) viewer" 1fr
+ /30rem 1fr
+ `,
+}
+```
+
+`areaGroups` options for `tabs:`: `switchersCompact` or `switchersFull`.
+
+### `panel:` — all elements stacked vertically
+
+Stacks all elements and keeps them all visible simultaneously, scrollable as a single panel. Use when sections are related and should all be visible at once (e.g. an inspector with multiple info sections):
+
+```ts
+app: {
+ template: `
+ "tabs:left viewport panel:right" 1fr
+ / 22rem 1fr 20rem
+ `,
+}
+```
+
+`areaGroups` options for `panel:`: `label` to give the panel a title.
+
+### Choosing between `tabs:` and `panel:`
+
+- Use `tabs:` when sections are **alternatives** — the user focuses on one at a time
+- Use `panel:` when sections are **complementary** — the user needs all of them visible together
+
+---
+
+## `grid.elements` and `grid.areaGroups`
+
+```ts
+grid.elements = {
+ viewer: viewport,
+ qtos: {
+ template: uis.custom.get("qtosSection").template,
+ initialState: { components },
+ label: "Quantities",
+ },
+}
+
+grid.areaGroups = {
+ left: { switchersFull: true },
+ right: { label: "Inspector" },
+ group: { label: "Configuration", icon: appIcons.APPLY },
+}
+```
+
+> **Grouping sections always happens at the grid level.** To group, combine, or stack panel sections together, the answer is always `tabs:` or `panel:` in the grid template — never nesting one section inside another in the template code.
diff --git a/docs/app-wiring.md b/docs/app-wiring.md
new file mode 100644
index 0000000..15df409
--- /dev/null
+++ b/docs/app-wiring.md
@@ -0,0 +1,123 @@
+# App Wiring
+
+## `app.ts` — App Identity
+
+Defines the app's typed shape and the typed accessor for `AppManager`. The `App` type uses `GridElement` types exported from `ui-components/`:
+
+```ts
+import * as OBC from "@thatopen/components"
+import * as BUI from "@thatopen/ui"
+import { AppManager } from "@thatopen/services"
+import { icons } from "./globals"
+import { FilesSectionGridElement, QtosSectionGridElement } from "./ui-components"
+
+export type App = {
+ icons: (keyof typeof icons)[]
+ grid: BUI.Grid<
+ ["Viewer", "Quantities", "Collider"],
+ [
+ "viewer",
+ FilesSectionGridElement,
+ QtosSectionGridElement,
+ ]
+ >
+}
+
+export const getAppManager = (components: OBC.Components) =>
+ components.get(AppManager)
+```
+
+`getAppManager` is the **only** valid way to access `AppManager` in an app. Never use `components.get(AppManager)` directly — it drops the `App` type parameter and loses all type information about the app's icons, grid layout, and grid elements.
+
+```ts
+// ✗ Avoid — loses the App type
+const app = components.get(AppManager)
+
+// ✓ Correct — fully typed
+const app = getAppManager(components)
+```
+
+### Grid element types
+
+- Raw elements → typed as a string literal: `"viewer"`
+- Template-based sections → typed as `{ComponentName}GridElement`, defined in the component's `src/types.ts` and exported through `ui-components/index.ts`
+
+```ts
+// ui-components/files-section/src/types.ts
+export type FilesSectionGridElement = {
+ name: "filesSection"
+ state: FilesSectionState
+}
+```
+
+---
+
+## `main.ts` — Entry Point
+
+`main.ts` boots the platform and calls `app.init()`. No business logic belongs here.
+
+```ts
+async function main() {
+ const client = EngineServicesClient.fromPlatformContext()
+
+ const { components } = await client.setup(
+ { OBC, OBF, BUI, CUI, THREE, FRAGS },
+ { uuid: ViewportsManager.uuid },
+ { uuid: AppManager.uuid },
+ { uuid: UIManager.uuid },
+ ) as { components: OBC.Components }
+
+ await viewportsManager(components)
+ const viewports = components.get(ViewportsManager)
+ // @ts-ignore
+ const { element: viewport } = [...viewports._instances.values()][0]
+
+ const app = getAppManager(components)
+ await app.init({
+ client,
+ icons: appIcons,
+ componentSetups: { ... },
+ grid: (grid) => { ... },
+ })
+}
+
+main().catch(console.error)
+```
+
+### `client.setup()` — Platform initialization
+
+- **First argument** — libraries object (`OBC`, `OBF`, `BUI`, `CUI`, `THREE`, `FRAGS`, etc.)
+- **Subsequent arguments** — `{ uuid }` for each platform built-in component needed. Always present: `ViewportsManager`, `AppManager`, `UIManager`
+
+Custom components (from `bim-components/`) do not need to be listed here — they self-register via `components.add()` in their constructor.
+
+---
+
+## `componentSetups` in `app.init()`
+
+```ts
+componentSetups: {
+ core: [uiManager, fragmentsManager, highlighter, ifcLoader],
+ lazy: [
+ { uuid: MyComponent.uuid, fn: myComponentSetup },
+ ]
+}
+```
+
+**`core`** — runs before the grid mounts, in parallel. `uiManager` always goes here. Other engine component setups (fragments, highlighter, IFC loader) also typically go here.
+
+**`lazy`** — runs the first time the corresponding component is instantiated. The `uuid` is imported from the `bim-components/` barrel. Use for custom components that can be loaded on demand.
+
+The `uuid` of each custom component — a static property defined on the class — is imported from the `bim-components/` barrel when referencing it in `lazy`:
+
+```ts
+import { MyComponent } from "../bim-components"
+
+componentSetups: {
+ lazy: [
+ { uuid: MyComponent.uuid, fn: myComponentSetup },
+ ]
+}
+```
+
+When it's not obvious whether a custom component should be `core` or `lazy`, **decide based on whether the component is needed at boot or only on demand.**
diff --git a/docs/bim-components/coding-conventions.md b/docs/bim-components/coding-conventions.md
new file mode 100644
index 0000000..5b05dc8
--- /dev/null
+++ b/docs/bim-components/coding-conventions.md
@@ -0,0 +1,128 @@
+# Coding Conventions
+
+General coding conventions that apply across all BIM components.
+
+---
+
+## No shorthand property declaration in constructors
+
+Declare properties explicitly on the class body. Never use TypeScript's constructor shorthand (`public`, `private`, `readonly` parameter modifiers):
+
+```ts
+// ✗ Avoid
+constructor(public position: THREE.Vector3) {}
+
+// ✓ Correct
+position: THREE.Vector3;
+
+constructor(position: THREE.Vector3) {
+ this.position = position;
+}
+```
+
+---
+
+## Private member naming
+
+Private **properties** use an underscore prefix. Private **methods** do not:
+
+```ts
+// Properties — underscore prefix
+private _enabled = true;
+private _mesh: THREE.Mesh | null = null;
+
+// Methods — no prefix
+private setupEvents() { ... }
+private calculateResult() { ... }
+```
+
+---
+
+## Backing fields with getter/setter
+
+When a property needs to trigger side effects on assignment — firing an event, propagating a value to Three.js objects — use a private backing field with a public getter/setter:
+
+```ts
+private _visible = true;
+
+get visible() {
+ return this._visible;
+}
+
+set visible(value: boolean) {
+ this._visible = value;
+ for (const mesh of this._meshes) {
+ mesh.visible = value;
+ }
+ this.onStateChanged.trigger(["visible"]);
+}
+```
+
+---
+
+## Throw in late-initialization getters
+
+When a property only exists after `setup()` or another explicit initialization step, the getter throws a descriptive error rather than returning `undefined`. This makes the missing initialization obvious at the call site:
+
+```ts
+private _data: Data | null = null;
+
+get data() {
+ if (!this._data) {
+ throw new Error("ActivityTracker: call setup() before accessing data.");
+ }
+ return this._data;
+}
+```
+
+---
+
+## `interface` vs `type`
+
+Use `interface` for object shapes that may be extended or implemented by other classes. Use `type` for everything else — unions, primitive aliases, computed types, and tuples:
+
+```ts
+// ✓ interface — extendable object shape
+export interface ActivityConfig {
+ color: THREE.Color;
+ autoColorize: boolean;
+}
+
+// ✓ type — union
+export type ActivityStatus = "notStarted" | "inProgress" | "completed";
+
+// ✓ type — primitive alias
+export type ActivityId = string;
+
+// ✓ type — tuple / computed
+export type SerializedMap = [string, number[]][];
+```
+
+---
+
+## Prefer `DataMap` and `DataSet` over native `Map` and `Set`
+
+When storing collections inside a component, always use `FRAGS.DataMap` and `FRAGS.DataSet` instead of the native `Map` and `Set`. They have an identical API but additionally emit lifecycle events (`onItemSet`, `onBeforeDelete`, `onItemDeleted`) that other parts of the app can react to.
+
+See [`./observable-collections.md`](./observable-collections.md) for usage patterns.
+
+---
+
+## `async/await` over `.then()/.catch()`
+
+Always prefer `async/await` for asynchronous code. It is more readable and consistent with the rest of the codebase:
+
+```ts
+// ✗ Avoid
+this.loadModel(buffer).then((model) => {
+ this.process(model);
+}).catch((err) => {
+ console.error(err);
+});
+
+// ✓ Correct
+async loadAndProcess(buffer: ArrayBuffer) {
+ const model = await this.loadModel(buffer);
+ await this.process(model);
+}
+```
diff --git a/docs/bim-components/element-collections.md b/docs/bim-components/element-collections.md
new file mode 100644
index 0000000..2e3d3ff
--- /dev/null
+++ b/docs/bim-components/element-collections.md
@@ -0,0 +1,69 @@
+# Element Collections
+
+`OBC.ModelIdMap` is the only acceptable way to reference a collection of elements in a `FragmentsModel`. Any method that operates on items — highlighting, hiding, filtering, calculating — takes or returns a `ModelIdMap`.
+
+---
+
+## `OBC.ModelIdMap`
+
+Maps a model ID to a `Set` of local IDs:
+
+```ts
+// Shape: { [modelId: string]: Set }
+const items: OBC.ModelIdMap = {
+ "model-a": new Set([1, 2, 3]),
+ "model-b": new Set([7, 8]),
+};
+
+// Iterating
+for (const [modelId, localIds] of Object.entries(items)) {
+ const model = fragments.list.get(modelId);
+ for (const localId of localIds) { ... }
+}
+```
+
+---
+
+## `OBC.ModelIdDataMap`
+
+Associates data of type `T` per item — a `DataMap>`. Used when a component stores per-item state:
+
+```ts
+const progress: OBC.ModelIdDataMap<{ startDate: Date }> = new FRAGS.DataMap();
+
+// Write
+let byLocalId = progress.get("model-a");
+if (!byLocalId) {
+ byLocalId = new FRAGS.DataMap();
+ progress.set("model-a", byLocalId);
+}
+byLocalId.set(42, { startDate: new Date() });
+
+// Read
+const itemProgress = progress.get("model-a")?.get(42);
+```
+
+---
+
+## `OBC.ModelIdMapUtils`
+
+Static utility class for operating on `ModelIdMap` collections. Use it to combine, filter, compare, and serialize selections rather than implementing those operations manually:
+
+```ts
+import * as OBC from "@thatopen/components"
+
+// Combine two selections
+const combined = OBC.ModelIdMapUtils.join([selectionA, selectionB]);
+
+// Remove deselected items from current selection
+OBC.ModelIdMapUtils.remove(current, deselected);
+
+// Guard before processing
+if (OBC.ModelIdMapUtils.isEmpty(items)) return;
+
+// Serialize for storage (Set → number[])
+const raw = OBC.ModelIdMapUtils.toRaw(items);
+
+// Restore from storage
+const restored = OBC.ModelIdMapUtils.fromRaw(raw);
+```
diff --git a/docs/bim-components/expose-events.md b/docs/bim-components/expose-events.md
new file mode 100644
index 0000000..d261b31
--- /dev/null
+++ b/docs/bim-components/expose-events.md
@@ -0,0 +1,84 @@
+# Expose Events
+
+## `OBC.Event`
+
+Events are declared as `readonly` properties on the class. The type parameter `T` defines the payload — use `undefined` when there's nothing to pass.
+
+Payload types are defined in `src/types.ts`, not inline, following the `{EventName}Payload` convention:
+
+```ts
+// src/types.ts
+export type OperationCompletedPayload = { result: CalculationResult }
+
+// index.ts
+readonly onOperationCompleted = new OBC.Event();
+```
+
+**Triggering** — always after the fact, once the operation or change is done:
+
+```ts
+this.onOperationCompleted.trigger({ result });
+this.onDisposed.trigger(undefined);
+```
+
+**Subscribing** — done externally by consumers:
+
+```ts
+const tracker = components.get(ActivityTracker);
+tracker.onOperationCompleted.add(({ result }) => { ... });
+```
+
+---
+
+## `onStateChanged` pattern
+
+When a component has several reactive properties, a single `onStateChanged` event with a union payload is cleaner than one event per property:
+
+```ts
+readonly onStateChanged = new OBC.Event<("enabled" | "color" | "mode")[]>();
+
+private _enabled = true;
+
+get enabled() {
+ return this._enabled;
+}
+
+set enabled(value: boolean) {
+ this._enabled = value;
+ this.onStateChanged.trigger(["enabled"]);
+}
+```
+
+Consumers can inspect the payload to react only to the properties they care about:
+
+```ts
+tracker.onStateChanged.add((changed) => {
+ if (changed.includes("color")) updateColorUI();
+});
+```
+
+---
+
+## `OBC.Eventable` and `EventManager`
+
+Any component that exposes events must implement `OBC.Eventable` and use `EventManager`. Register every event in the constructor and call `events.reset()` in `dispose()`:
+
+```ts
+export class ActivityTracker extends OBC.Component implements OBC.Eventable {
+ readonly onDisposed = new OBC.Event();
+ readonly onStateChanged = new OBC.Event();
+
+ readonly events = new OBC.EventManager();
+
+ constructor(components: OBC.Components) {
+ super(components);
+ components.add(ActivityTracker.uuid, this);
+ this.events.list.add(this.onDisposed);
+ this.events.list.add(this.onStateChanged);
+ }
+
+ dispose() {
+ this.events.reset();
+ }
+}
+```
diff --git a/docs/bim-components/library-examples.md b/docs/bim-components/library-examples.md
new file mode 100644
index 0000000..985c242
--- /dev/null
+++ b/docs/bim-components/library-examples.md
@@ -0,0 +1,28 @@
+# Library Examples
+
+Before writing any implementation that uses That Open Engine — whether you know the component name or are trying to figure out how to achieve something — check whether an official example covers it. Official examples are the best starting point: they show correct initialization sequences, idiomatic API usage, and working patterns from the source authors.
+
+## How to find an example
+
+1. Fetch the `paths.json` for each repo below (you can do all three in parallel). Do not summarize the response — retain the full JSON in context and use the descriptions to reason about which examples are relevant.
+2. Use the descriptions in the JSON to reason about which examples best match the user's intent. Prefer semantic matching over path name matching — a description may cover what the user needs even if the component name doesn't match exactly.
+3. Only proceed to fetch an example if its description confirms that it directly covers the user's intent, or that it can serve as a building block for composing a new custom component. Do not fetch speculatively.
+4. Construct the full URL: `{base_url}{path_entry}` and fetch the example file to use as your implementation reference.
+
+Path names are descriptive — `VisibilityOperations`, `EditElements`, `SteelDetailing` — so matching by intent works well even when the user hasn't named a specific component.
+
+---
+
+## Repositories
+
+### engine_components
+Components from `@thatopen/components` (OBC) and `@thatopen/components-front` (OBF).
+
+- **paths.json**: `https://raw.githubusercontent.com/ThatOpen/engine_components/refs/heads/main/examples/paths.json`
+- **Base URL**: `https://raw.githubusercontent.com/ThatOpen/engine_components/refs/heads/main/`
+
+### engine_fragment
+The `@thatopen/fragments` low-level library. There are no named OBC components here — users typically describe what they want to do (e.g., "edit model properties", "modify elements", "work with visibility").
+
+- **paths.json**: `https://raw.githubusercontent.com/ThatOpen/engine_fragment/refs/heads/main/examples/paths.json`
+- **Base URL**: `https://raw.githubusercontent.com/ThatOpen/engine_fragment/refs/heads/main/`
diff --git a/docs/bim-components/observable-collections.md b/docs/bim-components/observable-collections.md
new file mode 100644
index 0000000..9f70d99
--- /dev/null
+++ b/docs/bim-components/observable-collections.md
@@ -0,0 +1,83 @@
+# Observable Collections
+
+`FRAGS.DataMap` and `FRAGS.DataSet` are lifecycle-aware extensions of `Map` and `Set`. The only difference from their native counterparts is that they emit events when items are added or removed, making the collection observable.
+
+Use `DataMap` when there is a clear key to identify each item. Use `DataSet` when the item itself is the identifier. `DataMap` is the default choice in most cases.
+
+---
+
+## `FRAGS.DataMap`
+
+### Declaration
+
+The primary collection of a component is conventionally named `list`:
+
+```ts
+import * as FRAGS from "@thatopen/fragments"
+
+readonly list = new FRAGS.DataMap();
+```
+
+### Reactive events
+
+```ts
+this.list.onItemSet.add(({ key, value }) => {
+ // called after an item is added
+});
+
+this.list.onBeforeDelete.add(({ key, value }) => {
+ // called before an item is removed — use to clean up resources
+});
+
+this.list.onItemDeleted.add(({ key }) => {
+ // called after an item is removed
+});
+```
+
+### Get-or-create
+
+When inserting into a `DataMap` where the entry may not exist yet, use a get-or-create pattern:
+
+```ts
+let entry = this.list.get(key);
+if (!entry) {
+ entry = new SpotElevation();
+ this.list.set(key, entry);
+}
+entry.doSomething();
+```
+
+### Guard
+
+A guard runs before every `set()` call and can block the insertion by returning `false`:
+
+```ts
+this.list.guard = (key, value) => {
+ return value.isValid;
+};
+```
+
+---
+
+## `FRAGS.DataSet`
+
+Same API as `DataMap` but without keys — the item itself is the identifier:
+
+```ts
+readonly list = new FRAGS.DataSet();
+
+this.list.onItemSet.add(({ value }) => { ... });
+this.list.onBeforeDelete.add(({ value }) => { ... });
+```
+
+---
+
+## Collections of auxiliary class instances
+
+When a component manages instances of its own auxiliary classes, cleanup typically happens in `onBeforeDelete`:
+
+```ts
+this.list.onBeforeDelete.add(({ value: spotElevation }) => {
+ spotElevation.dispose();
+});
+```
diff --git a/docs/bim-components/overview.md b/docs/bim-components/overview.md
new file mode 100644
index 0000000..adbea3c
--- /dev/null
+++ b/docs/bim-components/overview.md
@@ -0,0 +1,160 @@
+# BIM Component Creation
+
+Architecture, patterns, and step-by-step guide for creating BIM components with That Open Engine (OBC/OBF) — the domain-logic layer that wraps fragments, IFC data, measurements, selections, or any non-UI behavior.
+
+## Working mode
+
+Before doing anything else, **read [`./library-examples.md`](./library-examples.md) and fetch the `paths.json` for both `engine_components` and `engine_fragment` in parallel.** Review all descriptions to understand what the engine already provides before forming any opinion about what needs to be built.
+
+Only after that, **propose an implementation plan and wait for the user's approval.**
+
+The proposal must describe:
+- Whether the need is covered by an existing engine component (consume via `components.get()`)
+- Whether existing components can be composed to cover it
+- Only if neither applies: which new component(s) will be created, what properties, events, and public methods it will expose, and whether it needs lifecycle, serialization, render loop, or interactive creation interfaces
+
+Only proceed with changes after the user explicitly confirms the plan. If the scope is unclear, ask first — do not assume.
+
+---
+
+## What is a BIM Component?
+
+That Open Engine is the collection of packages that power the app: `@thatopen/components`, `@thatopen/components-front`, `@thatopen/fragments`, `@thatopen/ui`, and `@thatopen/ui-obc`. BIM components are built on top of this ecosystem.
+
+A BIM component is a class that extends `OBC.Component` and encapsulates domain logic — IFC queries, calculations, data management, fragment operations, etc. It exposes that logic through a clean, event-driven interface that UI templates and other components can consume.
+
+### The engine works with Fragments
+
+That Open Engine does not work with IFC STEP files directly. It works with **Fragments** — an open binary format (`.frag`) built for performance: a 2 GB IFC file becomes ~80 MB, loads in seconds at 60fps in the browser.
+
+The runtime representation of a loaded model is a `FragmentsModel`. Its data schema usually mirrors the IFC SCHEMA (local IDs map to IFC express IDs, the spatial structure parallels IFC hierarchy, properties follow IFC attributes and relations), but it is not IFC STEP — it is Fragments. Any component that queries elements, reads properties, or operates on geometry works through the `FragmentsModel` API.
+
+For loading models, querying elements, or reading properties, fetch the relevant example from [`./library-examples.md`](./library-examples.md) before writing any implementation code.
+
+### When to create one
+
+Create a BIM component when custom domain logic is needed.
+
+After reviewing the `paths.json` descriptions fetched in the working mode step:
+
+- If an existing component covers the need → consume it via `this.components.get()` and fetch its example. Do not reimplement.
+- If existing components can be composed to cover it → compose them, fetching the relevant examples as building blocks.
+- Only if the need is genuinely not covered → propose creating a custom component.
+
+Custom components can themselves consume built-in components via `this.components.get()`, so examples remain useful even when building something new.
+
+---
+
+## Step 1 — Define the component
+
+Names should be short but descriptive. Use PascalCase for both folder and class, matching exactly:
+
+| Artifact | Convention | Example |
+|---|---|---|
+| Folder | PascalCase | `ActivityTracker/` |
+| Class | PascalCase | `ActivityTracker` |
+| Types prefix | PascalCase matching class | `ActivityTrackerConfig`, `ActivityTrackerResult` |
+
+Use the `Manager` suffix when the component creates instances of something or governs a broad concern — consistent with `FragmentsManager`, `Classifier`, etc. For focused, single-responsibility components, omit it.
+
+---
+
+## Step 2 — Create the file structure
+
+```
+ActivityTracker/
+ index.ts → class definition + re-exports ./src
+ src/
+ index.ts → re-exports types and support files
+ types.ts → interfaces, type aliases, enums
+ SpotElevation.ts → auxiliary class (if needed)
+```
+
+Only one class per component extends `OBC.Component` — the main one in `index.ts`. Any other classes the component defines or instantiates are plain TypeScript classes that live in `src/` alongside `types.ts`, and never extend `OBC.Component`.
+
+---
+
+## Step 3 — Implement the class in `index.ts`
+
+### Skeleton
+
+Every component needs a unique, hardcoded static UUID — a fixed string literal defined once and never changed. The constructor must call `super(components)` and register the instance with `components.add(UUID, this)` so it becomes globally retrievable via `components.get(ActivityTracker)`.
+
+Before writing the implementation, read [`./coding-conventions.md`](./coding-conventions.md) for naming rules, backing fields, and async patterns that apply throughout the class.
+
+```ts
+import * as OBC from "@thatopen/components"
+
+export class ActivityTracker extends OBC.Component {
+ static readonly uuid = "1b43361b-ef43-4207-9c40-dbed37bec0b6" as const;
+
+ enabled = true;
+
+ constructor(components: OBC.Components) {
+ super(components);
+ components.add(ActivityTracker.uuid, this);
+ }
+}
+
+export * from "./src"
+```
+
+### Interfaces
+
+OBC defines capability interfaces that components can implement to stay consistent with each other and with the engine. Pick the resource that matches your need:
+
+| I need... | Resource |
+|---|---|
+| Initialize the component with config, or clean up resources when it's destroyed | [`./setup-and-cleanup.md`](./setup-and-cleanup.md) |
+| Save and restore state across sessions | [`./save-and-restore-state.md`](./save-and-restore-state.md) |
+| Run logic on every frame of the render loop | [`./per-frame-updates.md`](./per-frame-updates.md) |
+| Implement a flow where the user creates objects in the scene step by step | [`./user-driven-object-creation.md`](./user-driven-object-creation.md) |
+| Expose events or react to state changes | [`./expose-events.md`](./expose-events.md) |
+
+### Accessing other components
+
+Resolve dependencies at call time — never store component instances as fields as it may create memory leaks:
+
+```ts
+const fragments = this.components.get(OBC.FragmentsManager);
+const highlighter = this.components.get(OBF.Highlighter);
+```
+
+---
+
+## Step 4 — Define types in `src/types.ts`
+
+Types are defined in parallel as the class needs them, not upfront. All typing for the component lives here — domain interfaces, event payloads, serialized types — so there is a single place to look.
+
+See [`./type-conventions.md`](./type-conventions.md) for common patterns (runtime vs serialized types, generics, etc.).
+
+---
+
+## Step 5 — Export from the barrel
+
+Re-export the component from the project's barrel index so it's accessible from a single import point:
+
+```ts
+export * from "./ActivityTracker"
+```
+
+---
+
+## Wiring up
+
+Instantiating the component and connecting it to engine events is not the component's responsibility — that belongs in external initialization code. The constructor should never call other components. See [`../app-wiring.md`](../app-wiring.md) for how this is done.
+
+---
+
+## See also
+
+- [`./library-examples.md`](./library-examples.md) — Find official usage examples and discover what the engine provides
+- [`./element-collections.md`](./element-collections.md) — Represent or combine element collections across components
+- [`./observable-collections.md`](./observable-collections.md) — Store component-internal data with reactive notifications
+- [`./expose-events.md`](./expose-events.md) — Expose events for other components or the UI to react to
+- [`./setup-and-cleanup.md`](./setup-and-cleanup.md) — Initialize the component with config or clean up resources on destroy
+- [`./save-and-restore-state.md`](./save-and-restore-state.md) — Persist and restore state across sessions
+- [`./per-frame-updates.md`](./per-frame-updates.md) — Run logic on every frame of the render loop
+- [`./user-driven-object-creation.md`](./user-driven-object-creation.md) — Let the user create objects in the scene step by step
+- [`./type-conventions.md`](./type-conventions.md) — Define component types (runtime vs serialized, generics)
+- [`./coding-conventions.md`](./coding-conventions.md) — Code conventions (naming, backing fields, async)
diff --git a/docs/bim-components/per-frame-updates.md b/docs/bim-components/per-frame-updates.md
new file mode 100644
index 0000000..e917309
--- /dev/null
+++ b/docs/bim-components/per-frame-updates.md
@@ -0,0 +1,20 @@
+# Per-Frame Updates
+
+## `OBC.Updateable`
+
+Implement when the component needs to execute logic on every frame — animating objects, polling state, updating visual feedback.
+
+```ts
+export class ActivityTracker extends OBC.Component implements OBC.Updateable {
+ readonly onBeforeUpdate = new OBC.Event();
+ readonly onAfterUpdate = new OBC.Event();
+
+ update(delta?: number) {
+ this.onBeforeUpdate.trigger(undefined);
+ // per-frame logic
+ this.onAfterUpdate.trigger(undefined);
+ }
+}
+```
+
+The component is driven externally — a world or a setup file calls `update()` each frame. The component never drives itself.
diff --git a/docs/bim-components/save-and-restore-state.md b/docs/bim-components/save-and-restore-state.md
new file mode 100644
index 0000000..2906630
--- /dev/null
+++ b/docs/bim-components/save-and-restore-state.md
@@ -0,0 +1,21 @@
+# Save and Restore State
+
+## `OBC.Serializable`
+
+Implement when the component must persist its state — to a file, to the cloud, or across sessions.
+
+`D` is the runtime type, `S` is the serialized (JSON-safe) equivalent. Both are defined in `src/types.ts`. See [`./type-conventions.md`](./type-conventions.md) for the serialization patterns, including the `localId` → GUID conversion that must happen before storing any item references.
+
+```ts
+export class ActivityTracker extends OBC.Component
+ implements OBC.Serializable {
+
+ export(): SerializedActivityTrackerData {
+ // convert runtime state to JSON-safe structure
+ }
+
+ import(data: SerializedActivityTrackerData) {
+ // restore runtime state from serialized structure
+ }
+}
+```
diff --git a/docs/bim-components/setup-and-cleanup.md b/docs/bim-components/setup-and-cleanup.md
new file mode 100644
index 0000000..8bc94d1
--- /dev/null
+++ b/docs/bim-components/setup-and-cleanup.md
@@ -0,0 +1,64 @@
+# Setup and Cleanup
+
+Two interfaces govern a component's lifecycle: `OBC.Configurable` for one-time initialization before use, and `OBC.Disposable` for cleanup when the component is destroyed.
+
+---
+
+## `OBC.Configurable`
+
+Implement when the component requires one-time initialization. This separates construction (cheap, no side effects) from setup (resolves dependencies, applies config, registers event listeners).
+
+```ts
+export interface ActivityTrackerConfig {
+ color: THREE.Color;
+ autoColorize: boolean;
+}
+
+export class ActivityTracker extends OBC.Component
+ implements OBC.Configurable {
+
+ isSetup = false;
+ readonly onSetup = new OBC.Event();
+
+ protected _defaultConfig: ActivityTrackerConfig = {
+ color: new THREE.Color("#6528d7"),
+ autoColorize: false,
+ };
+
+ setup(config?: Partial) {
+ if (this.isSetup) return;
+ const fullConfig = { ...this._defaultConfig, ...config };
+ // apply config, resolve dependencies...
+ this.isSetup = true;
+ this.onSetup.trigger(undefined);
+ }
+}
+```
+
+The `if (this.isSetup) return` guard ensures setup only runs once. The constructor must never call other components — that belongs in `setup()`.
+
+---
+
+## `OBC.Disposable`
+
+Implement when the component holds resources that must be explicitly released — Three.js geometries and materials, event subscriptions to external components, workers.
+
+```ts
+export class ActivityTracker extends OBC.Component implements OBC.Disposable {
+ readonly onDisposed = new OBC.Event();
+
+ dispose() {
+ // 1. Release Three.js resources
+ this._mesh?.geometry.dispose();
+ this._material.dispose();
+
+ // 2. Reset all events (if OBC.Eventable is also implemented)
+ this.events.reset();
+
+ // 3. Signal disposal — always last
+ this.onDisposed.trigger(undefined);
+ }
+}
+```
+
+`onDisposed` must be triggered at the end of `dispose()`, after all cleanup is done.
diff --git a/docs/bim-components/type-conventions.md b/docs/bim-components/type-conventions.md
new file mode 100644
index 0000000..4c2f9d9
--- /dev/null
+++ b/docs/bim-components/type-conventions.md
@@ -0,0 +1,49 @@
+# Type Conventions
+
+All types for a component live in `src/types.ts` — domain interfaces, event payloads, serialized equivalents — so there is a single place to look.
+
+---
+
+## Runtime vs serialized
+
+Components that implement `OBC.Serializable` need two versions of their data types: a runtime version that uses rich TypeScript types, and a serialized version that is JSON-safe.
+
+The serialized type is named `Serialized{TypeName}`:
+
+```ts
+// Runtime — uses Map, Set, Date, THREE.Color
+export interface Activity {
+ index: number;
+ name: string;
+ color: THREE.Color;
+ items: OBC.ModelIdMap;
+ dates: Set;
+}
+
+// Serialized — JSON-safe equivalent
+export interface SerializedActivity {
+ index: number;
+ name: string;
+ color: string; // THREE.Color → hex string
+ items: [string, number[]][]; // ModelIdMap → tuple array
+ dates: string[]; // Set → array
+}
+```
+
+Conversion happens inside `export()` and `import()` (see [`./save-and-restore-state.md`](./save-and-restore-state.md)).
+
+---
+
+## Generics that require `type`, not `interface`
+
+Some OBC and FRAGS generics enforce that their type parameter is a `type` alias. Passing an `interface` causes a TypeScript error. When in doubt, prefer `type` for types used as generic parameters:
+
+```ts
+// ✗ Fails — interface not assignable to the generic constraint
+export interface ActivityData { name: string; value: number }
+const list = new FRAGS.DataMap(); // error
+
+// ✓ Correct
+export type ActivityData = { name: string; value: number }
+const list = new FRAGS.DataMap();
+```
diff --git a/docs/bim-components/user-driven-object-creation.md b/docs/bim-components/user-driven-object-creation.md
new file mode 100644
index 0000000..0ecc19d
--- /dev/null
+++ b/docs/bim-components/user-driven-object-creation.md
@@ -0,0 +1,56 @@
+# User-Driven Object Creation
+
+## `OBC.Createable`
+
+Implement when the component lets the user create objects in the scene through a multi-step interaction — click to start, click to confirm, Escape to cancel, click existing to delete.
+
+The key characteristic is **statefulness between clicks**: `create()` is called on every user click, but the first click initiates the workflow and the second confirms it by calling `endCreation()` internally.
+
+```ts
+export class ActivityTracker extends OBC.Component implements OBC.Createable {
+ private _temp: { isDragging: boolean; preview?: SomeObject } = {
+ isDragging: false,
+ };
+
+ create = () => {
+ if (!this.enabled) return;
+ if (!this._temp.isDragging) {
+ this._temp.isDragging = true;
+ this._temp.preview = new SomeObject();
+ return;
+ }
+ this.endCreation();
+ };
+
+ endCreation = () => {
+ if (!this._temp.preview) return;
+ this.list.add(this._temp.preview);
+ this._temp.isDragging = false;
+ this._temp.preview = undefined;
+ };
+
+ cancelCreation = () => {
+ this._temp.isDragging = false;
+ this._temp.preview?.dispose();
+ this._temp.preview = undefined;
+ };
+
+ delete = () => {
+ if (!this.world) return;
+ const casters = this.components.get(OBC.Raycasters);
+ const caster = casters.get(this.world);
+ const intersect = caster.castRayToObjects([...this._boundingBoxes]);
+ if (!intersect) return;
+ // identify and remove the hit object from list
+ };
+}
+```
+
+Wire up the Escape key in the component's `enabled` setter so `cancelCreation()` fires automatically when the component is disabled mid-flow:
+
+```ts
+set enabled(value: boolean) {
+ this._enabled = value;
+ if (!value) this.cancelCreation();
+}
+```
diff --git a/docs/cli-setup.md b/docs/cli-setup.md
new file mode 100644
index 0000000..157901d
--- /dev/null
+++ b/docs/cli-setup.md
@@ -0,0 +1,54 @@
+# CLI Setup and Authentication
+
+Before you can scaffold or publish a That Open Platform app, two things must be in place: the `@thatopen/services` CLI installed globally, and a valid platform token for authentication.
+
+---
+
+## Step 1: Install or update the CLI
+
+Run the following command in the terminal. This installs the CLI if it isn't present, or updates it to the latest version if it already is:
+
+```bash
+npm i @thatopen/services@latest -g
+```
+
+Once done, verify it works:
+
+```bash
+thatopen --version
+```
+
+If this returns a version number, the CLI is ready. If `npm` is not found, install Node.js first — download the LTS version from [https://nodejs.org](https://nodejs.org), then retry.
+
+---
+
+## Step 2: Get a platform token
+
+Generate an access token from the platform:
+
+1. Go to [https://dev.platform.thatopen.com](https://dev.platform.thatopen.com)
+2. In the header, click **Data**
+3. Find the **Tokens** card and create a new token
+4. Enable the permissions: **Apps**, **Components** and **Storage**
+5. Copy the token
+
+---
+
+## Step 3: Authenticate
+
+Once you have the token, run:
+
+```bash
+npm run login -- --token
+```
+
+where `` is the token you copied. This stores credentials globally at `~/.thatopen/config.json` and persists across sessions — you only need to do this once per machine.
+
+---
+
+## When to use this guide
+
+Read this guide when:
+- You are starting a new app and haven't confirmed the CLI is installed
+- A `thatopen` command fails with a "command not found" or "not authenticated" error
+- You have never built a platform app before on this machine
diff --git a/docs/cloud-components.md b/docs/cloud-components.md
new file mode 100644
index 0000000..70ccff9
--- /dev/null
+++ b/docs/cloud-components.md
@@ -0,0 +1,74 @@
+# Cloud components & automations
+
+A **cloud component** is a piece of logic that runs **server-side** (Node.js) on the platform,
+on the shared project context, via the API. Use them to automate real operations — clash checks,
+validations, document generation — and to let apps offload heavy work.
+
+## Apps vs cloud components
+
+| | Apps | Cloud components |
+|---|---|---|
+| **Runs in** | Browser (iframe on the platform) | Server (Node.js child process) |
+| **Item type** | `APP` | `TOOL` |
+| **Entry point** | Side effects in `main.ts` (renders UI) | `export async function main()` |
+| **Build output** | IIFE `dist/bundle.js` (all deps bundled) | IIFE `dist/bundle.js` (only `@thatopen/services` externalized) |
+| **Template** | `bim`, `default`, `test` | `cloud`, `cloud-test` |
+
+## Scaffold
+
+```bash
+thatopen create my-component --template cloud
+cd my-component
+```
+
+The component is an `export async function main()` that runs on the server. The execution engine
+injects these globals (do **not** import them; for `OBC`/`THREE`/`web-ifc` import them so the
+bundler includes them):
+
+| Global | Purpose |
+|--------|---------|
+| `thatOpenServices` | Authenticated `EngineServicesClient` |
+| `executionParams` | Parameters passed by the caller |
+| `executionContext` | `{ projectId?, executionId, toolId, toolVersion }` |
+| `executionReporter` | `{ message(msg), error(msg), progress(pct) }` for live feedback |
+| `OBC` | `@thatopen/components` — BIM engine (import it) |
+| `THREE` | `three` — 3D math/geometry (import it) |
+| `fs` | Node.js filesystem (import it) |
+
+## Run locally
+
+```bash
+npm run run # build + test locally
+npx thatopen run --params '{"inputFile":"model.ifc"}' # pass parameters
+```
+
+`thatopen local-server` starts an API-compatible local execution server (default `:4001`) so an
+app can call the component before it's deployed (set `client.localServerUrl`).
+
+## Authenticate & publish
+
+```bash
+npm run login -- --token
+npm run publish
+```
+
+## Calling a component from an app
+
+```ts
+const { executionId } = await client.executeComponent(componentId, { param: "value" }, versionTag?);
+client.onExecutionProgress(executionId, (data) => {
+ // data.progressUpdate — percentage
+ // data.messageUpdate — status messages
+});
+```
+
+Include `projectId` in the execution params to scope the run to a project (the backend validates
+the component is linked to that project). See [CONTEXT.md](../CONTEXT.md) for the permissions contract.
+
+## Automations (event-triggered components)
+
+An **automation** is a cloud component that runs automatically in response to a platform **event**
+(rather than being invoked by hand) — e.g. "run the clash check whenever a model is updated."
+The component is the same; what differs is the trigger. Add it to a project, choose the event(s)
+that fire it, and the platform executes it on the shared project context, reporting progress and
+logs through `executionReporter`. Build and publish it exactly like any other cloud component above.
diff --git a/docs/connect-logic-to-ui.md b/docs/connect-logic-to-ui.md
new file mode 100644
index 0000000..90a1059
--- /dev/null
+++ b/docs/connect-logic-to-ui.md
@@ -0,0 +1,113 @@
+# Connect Logic to UI
+
+Setup files are the bridge between custom BIM components (logic) and the platform UI. This is where event subscriptions, UI sync, and component wiring live — not in the component itself.
+
+## Structure
+
+```
+setups/
+├── index.ts → barrel (always present)
+├── ui-manager.ts → fixed boilerplate (always present)
+└── {custom}.ts → one per custom BIM component
+```
+
+One file per component being initialized. Each file exports a single function that receives `components: OBC.Components`.
+
+---
+
+## `setups/ui-manager.ts` — Fixed boilerplate
+
+This is where all UI templates get registered in the platform (see the UI-component guide in `docs/`: [./ui-components/overview.md](./ui-components/overview.md)). The file always has this shape — the content changes (which templates are registered), but the structure never does.
+
+### `CustomUIs` type
+
+Maps every UI template to its element type and state. Add one entry per registered template:
+
+```ts
+export type CustomUIs = {
+ filesSection: { type: BUI.PanelSection; state: FilesSectionState }
+ collisionsTable: { type: BUI.Table; state: CollisionsTableState }
+ // one entry per registered template
+}
+```
+
+### `getUIManager` — typed accessor
+
+The typed accessor for `UIManager`. Defined here because `CustomUIs` lives here. Used everywhere else in `setups/` to interact with the UI registry — never access `UIManager` directly:
+
+```ts
+export const getUIManager = (components: OBC.Components) =>
+ components.get(UIManager)
+```
+
+`getUIManager` is the **only** valid way to access `UIManager` in an app. Never use `components.get(UIManager)` directly — it drops the `CustomUIs` type parameter and loses all type information about the registered templates.
+
+```ts
+// ✗ Avoid — loses the CustomUIs type
+const uis = components.get(UIManager)
+
+// ✓ Correct — fully typed
+const uis = getUIManager(components)
+```
+
+### `uiManager` setup function
+
+Registers each template and its optional `onInstanceCreated` callback, exported from the UI component:
+
+```ts
+export const uiManager = (components: OBC.Components) => {
+ const uis = getUIManager(components)
+ uis.registerTemplate("filesSection", {
+ template: filesSectionTemplate,
+ onInstanceCreated: onFilesSectionCreated
+ })
+ uis.registerTemplate("collisionsTable", {
+ template: collisionsTableTemplate
+ })
+}
+```
+
+### Composing UI components
+
+Templates can embed other registered components by creating instances via `uis.custom.get()`:
+
+```ts
+export const filesSectionTemplate: FilesSectionComponent = (state) => {
+ const uis = state.components.get(UIManager)
+ const [modelsList] = uis.custom.get("modelsList").create(state)
+ return BUI.html`
+
+ ${modelsList}
+
+ `
+}
+```
+
+### Keeping UIs in sync with `updateInstances()`
+
+To push a state update to all existing instances of a template, call `updateInstances()` from within a setup file — typically in response to engine events:
+
+```ts
+const updateUIs = () => uis.custom.get("modelsList").updateInstances()
+fragments.list.onItemSet.add(updateUIs)
+fragments.list.onItemDeleted.add(updateUIs)
+```
+
+---
+
+## Custom setup files
+
+Each setup file is the bridge between a custom BIM component and the platform (see the BIM-component guide in `docs/`: [./bim-components/overview.md](./bim-components/overview.md)). This is where wiring lands: event listeners, UI updates, and configuration go here — not in the component's constructor.
+
+```ts
+// setups/qto-manager.ts
+export const qtoManager = (components: OBC.Components) => {
+ const uis = getUIManager(components)
+ const qtoManager = components.get(QtoManager)
+
+ qtoManager.onStateChanged.add((states) => {
+ if (!states.includes("value")) return
+ uis.custom.get("qtosTable").updateInstances()
+ })
+}
+```
diff --git a/docs/previewing.md b/docs/previewing.md
new file mode 100644
index 0000000..9369bbd
--- /dev/null
+++ b/docs/previewing.md
@@ -0,0 +1,45 @@
+# Previewing Apps During Development
+
+That Open Platform apps can only be previewed correctly within the platform context — opening `localhost:4000` directly in the browser will just bring the app bundle. The app must be loaded from inside a project on the platform as it gives context.
+
+---
+
+## Step 1: Start the dev server
+
+In the project root, run:
+
+```bash
+npm run dev
+```
+
+This invokes `thatopen serve` under the hood — the same CLI installed during setup. It performs a special build and serves the bundle on **port 4000**. Keep this process running throughout your development session.
+
+---
+
+## Step 2: Open the app in the platform
+
+Once the dev server is running:
+
+1. Go to [https://dev.platform.thatopen.com](https://dev.platform.thatopen.com) and enter a project. If no project exists yet, create one first.
+2. In the project sidebar, click **Local App**.
+3. Click **Get Started**.
+
+The platform will load the locally-running app from port 4000 and render it inside the project context, with full access to its models, data, and users.
+
+The resulting URL follows this pattern:
+
+```
+https://dev.platform.thatopen.com/dashboard/projects/{projectId}/apps/local-app
+```
+
+where `{projectId}` is the ID of the project. Bookmark this URL to return quickly during development.
+
+> The dev server must be running before opening this URL. If the bundle is not being served on port 4000, the platform will fail to load the app.
+
+---
+
+## When to read this guide
+
+- When you want to see, run, or preview the app
+- When you have run `npm run dev` and don't know what to do next
+- When the app is not showing up in the platform
diff --git a/docs/publishing.md b/docs/publishing.md
new file mode 100644
index 0000000..70f3549
--- /dev/null
+++ b/docs/publishing.md
@@ -0,0 +1,35 @@
+# Publishing an App
+
+Publishing to the That Open Platform is a two-step process: authenticate first, then publish.
+
+## Step 1: Authenticate
+
+```bash
+thatopen login
+```
+
+This stores credentials globally at `~/.thatopen/config.json`. Authentication persists across sessions — you only need to do this once per machine.
+
+**Options:**
+
+| Flag | Purpose |
+|---|---|
+| `--token ` | Non-interactive login (CI, scripts) |
+| `--local` | Store credentials in the project directory (`.thatopen`) instead of globally |
+
+## Step 2: Publish
+
+```bash
+thatopen publish
+```
+
+This builds the app and uploads it to the platform. The command handles the build step automatically unless told otherwise.
+
+**Common options:**
+
+| Flag | Purpose |
+|---|---|
+| `--name ` | Override the published app name |
+| `--version-tag ` | Tag the release (e.g. `v1.2.0`) |
+| `--skip-build` | Skip the build step if the bundle is already built |
+| `--app-id ` | Publish to a specific existing app |
diff --git a/docs/scaffolding.md b/docs/scaffolding.md
new file mode 100644
index 0000000..59383f0
--- /dev/null
+++ b/docs/scaffolding.md
@@ -0,0 +1,54 @@
+# Scaffolding a New App
+
+When starting a new platform app from scratch, **always use the CLI to scaffold the project — never create the files manually.** The CLI produces the exact project structure these docs describe, correctly configured and with dependencies installed. Writing the scaffold by hand defeats the purpose of the tool and risks inconsistencies.
+
+## Command
+
+```bash
+thatopen create
+```
+
+Use `.` as the name to scaffold in the current directory:
+
+```bash
+thatopen create .
+```
+
+This copies the template files into the target directory and runs `npm install` automatically.
+
+## Templates
+
+Pass `--template ` to choose a template. The default is `bim`.
+
+| Template | When to use |
+|---|---|
+| `bim` (default) | Standard BIM viewer app — Three.js viewport, BIM viewer, platform UI components. This is the right starting point for almost every app. |
+| `default` | Minimal shell — just shows platform context. Use only when you explicitly want to start from scratch without any viewer. |
+
+If no template is specified, use `bim`.
+
+## What the scaffold produces
+
+The `bim` template generates the full structure described in the **Project Structure** section of [./app-architecture.md](./app-architecture.md):
+
+```
+src/
+├── bim-components/
+├── ui-components/
+├── setups/
+├── app.ts
+├── globals.ts
+└── main.ts
+```
+
+All platform built-ins (`AppManager`, `UIManager`, `ViewportsManager`) are already wired up in the generated `main.ts`. There is nothing to manually wire at the entry point — just extend from there.
+
+## Starting the dev server
+
+After scaffolding, start the local dev server:
+
+```bash
+thatopen serve
+```
+
+This launches an esbuild watch process that rebundles on every file change and serves the app with live reload. No configuration needed.
diff --git a/docs/ui-components/async-actions.md b/docs/ui-components/async-actions.md
new file mode 100644
index 0000000..c2d3499
--- /dev/null
+++ b/docs/ui-components/async-actions.md
@@ -0,0 +1,18 @@
+# Async Actions
+
+## Async button handler
+
+When a button triggers an async operation, set `target.loading = true` at the start and `false` when done — including in error paths:
+
+```ts
+const onClick = async ({ target }: { target: BUI.Button }) => {
+ target.loading = true
+ try {
+ await doSomething()
+ } finally {
+ target.loading = false
+ }
+}
+```
+
+Always restore `target.loading = false` in error paths. If an exception is thrown and loading is not reset, the button stays stuck in the loading state.
diff --git a/docs/ui-components/confirmation-dialog.md b/docs/ui-components/confirmation-dialog.md
new file mode 100644
index 0000000..ee1c3e9
--- /dev/null
+++ b/docs/ui-components/confirmation-dialog.md
@@ -0,0 +1,39 @@
+# Confirmation Dialog
+
+The standard way to ask the user to confirm a destructive or irreversible action is a `contextMenuTemplate` on a `bim-button`. The menu opens lazily when clicked and contains Confirm / Cancel buttons.
+
+## Setup
+
+`contextMenuTemplate` must always be assigned via `BUI.ref` — never inline in `BUI.html`:
+
+```ts
+const onBtnCreated = (e?: Element) => {
+ if (!e) return
+ const btn = e as BUI.Button
+ btn.contextMenuTemplate = () => {
+ const onConfirm = async ({ target }: { target: BUI.Button }) => {
+ target.loading = true
+ await doSomething()
+ target.loading = false
+ BUI.ContextMenu.removeMenus()
+ }
+
+ const onCancel = () => {
+ BUI.ContextMenu.removeMenus()
+ }
+
+ return BUI.html`
+
+
+
+
+ `
+ }
+}
+
+return BUI.html``
+```
+
+## Closing the menu
+
+Call `BUI.ContextMenu.removeMenus()` after the action completes (or is cancelled) to dismiss the popup. Without it, the menu stays open after the user clicks.
diff --git a/docs/ui-components/data-table.md b/docs/ui-components/data-table.md
new file mode 100644
index 0000000..428dabf
--- /dev/null
+++ b/docs/ui-components/data-table.md
@@ -0,0 +1,185 @@
+# Data Table
+
+> **Structural rule:** A `` always lives in its own dedicated UI component (e.g., `collisionsTable`, `elementsTable`). Never define a table inline inside another template such as a `panel-section`. The consuming template receives the table component as an external reference and composes it in.
+
+`BUI.Table` is the standard component for displaying tabular data. `TData` is a record type where each key is a column name and each value is the cell's data type.
+
+## Data Type
+
+Define a named type following the `{PascalCase(identifier)}Data` convention and export it from `src/types.ts`:
+
+```ts
+export type MyComponentTableData = {
+ Name: string
+ Count: number
+ Actions: string // reserved for per-row interaction buttons
+}
+```
+
+`Actions: string` is the conventional column for per-row interaction buttons — typed as `string` (placeholder) and rendered via `dataTransform`.
+
+Column names follow a casing rule: **PascalCase** for columns displayed to the user (`Name`, `Count`), **camelCase** for internal data columns not meant to be shown (`id`, `modelId`).
+
+## Template
+
+The table element is created via `BUI.html` and configured imperatively once mounted using `BUI.ref`:
+
+```ts
+export const myTableTemplate: BUI.StatefullComponent = ({ components }) => {
+ const onCreated = (e?: Element) => {
+ if (!e) return
+ const table = e as BUI.Table
+ table.loadFunction = async () => {
+ // return BUI.TableGroupData[]
+ }
+ table.loadData(true)
+ }
+
+ return BUI.html`
+
+ `
+}
+```
+
+**For async data** — assign `loadFunction` and call `loadData()`. Passing `true` forces a fresh fetch. `loadData()` does nothing if data already exists — to re-trigger, first reset: `table.data = []`.
+
+**For synchronous data** — assign `table.data` directly:
+
+```ts
+table.data = items.map(item => ({ data: { Name: item.name, Count: item.count } }))
+```
+
+### Data Structure
+
+```ts
+{
+ data: { Name: "Juan", Count: 5 }, // required
+ children?: [...] // optional — nested rows
+}
+```
+
+The preferred pattern for hierarchical data is a **flat list combined with `groupedBy`**. Reserve `children` for fixed, explicit hierarchies.
+
+## Empty State
+
+```ts
+return BUI.html`
+
+
+ No data loaded.
+ table.loadData()} label="Load Data">
+
+
+`
+```
+
+## Column Configuration and dataTransform
+
+Column layout, visibility, and `dataTransform` are fixed properties — configure them in `onInstanceCreated`, not in the template.
+
+```ts
+table.headersHidden = true
+table.noIndentation = true
+table.columns = [
+ "Name",
+ "Count",
+ { name: "Actions", width: "auto" },
+]
+table.groupedBy = ["Type"]
+table.preserveStructureOnFilter = true
+
+table.dataTransform = {
+ Name: (value) => BUI.html`${value}`,
+ Actions: (_, rowData) => {
+ const { id } = rowData
+ if (!id) return _
+ return BUI.html` doSomething(id)}>`
+ }
+}
+```
+
+> `noIndentation` and `groupedBy` are incompatible.
+
+**Column visibility:**
+```ts
+table.visibleColumns = ["Name", "Actions"] // most columns hidden
+table.hiddenColumns = ["id", "extension"] // most columns visible
+```
+
+> Everything rendered inside a table lives in the Shadow DOM. CSS classes cannot be applied — all styling must use inline styles.
+
+`rowData` can be mutated directly inside a transform:
+
+```ts
+table.dataTransform = {
+ LoadBearing: (value, rowData) => {
+ const onChange = (e: Event) => {
+ const input = e.target
+ if (!(input instanceof BUI.Checkbox)) return
+ rowData.LoadBearing = input.checked
+ }
+ return BUI.html``
+ }
+}
+```
+
+## Grouping
+
+```ts
+table.groupedBy = ["Discipline", "State"] // apply
+table.groupedBy = [] // clear
+```
+
+`groupingTransform` maps column values to arrays defining multi-level hierarchy:
+
+```ts
+table.groupingTransform = {
+ State: (value) => {
+ if (value === "S0") return ["Work in Progress"]
+ if (value.includes(".")) return ["Shared States", parentState, value]
+ return ["Shared States", value]
+ }
+}
+```
+
+`BUI.Table.flattenData` flattens a nested group structure — useful for computing aggregate values:
+
+```ts
+const children = BUI.Table.flattenData(group.data.children)
+const total = children.reduce((sum, { data }) => sum + (Number(data.Count) || 0), 0)
+```
+
+## Row Events (rowcreated)
+
+```ts
+const rowHandlers = new WeakMap void }>()
+
+table.addEventListener("rowcreated", (e) => {
+ if (!("detail" in e)) return
+ const { row } = (e as { detail: BUI.RowCreatedEventDetail }).detail
+
+ const existing = rowHandlers.get(row)
+ if (existing) row.removeEventListener("click", existing.handleClick)
+
+ const handleClick = () => {
+ if (!row.groupData?.data) return
+ const { Name } = row.groupData.data
+ }
+
+ rowHandlers.set(row, { handleClick })
+ row.addEventListener("click", handleClick)
+})
+```
diff --git a/docs/ui-components/display-text.md b/docs/ui-components/display-text.md
new file mode 100644
index 0000000..0c382a6
--- /dev/null
+++ b/docs/ui-components/display-text.md
@@ -0,0 +1,39 @@
+# Display Text
+
+Always use `` for all text content inside templates.
+
+## Static text
+
+```ts
+return BUI.html`Fixed text`
+```
+
+## Dynamic text (template interpolation)
+
+```ts
+return BUI.html`${someText}`
+```
+
+## Updating text imperatively
+
+Use `.textContent` to update a `bim-label` from outside the template. Never use `.label` — that property belongs to other components (`bim-button`, `bim-option`, etc.), not to `bim-label`:
+
+```ts
+const label = section.querySelector("bim-label")!
+label.textContent = "Updated text" // ✓ correct
+label.label = "Updated text" // ✗ avoid — .label is not a property of bim-label
+```
+
+## The label attribute belongs to other components
+
+`label` is an attribute/property of interactive components that display a text caption alongside an icon or control:
+
+```ts
+// ✓ correct — label attribute on button, option, etc.
+BUI.html``
+BUI.html``
+BUI.html``
+
+// ✗ avoid — label is not a property of bim-label
+BUI.html``
+```
diff --git a/docs/ui-components/inline-form.md b/docs/ui-components/inline-form.md
new file mode 100644
index 0000000..00fdacd
--- /dev/null
+++ b/docs/ui-components/inline-form.md
@@ -0,0 +1,44 @@
+# Inline Form (Context Menu)
+
+To let the user create or edit an entity without leaving the current view, place a `` acting as a form inside a `contextMenuTemplate`. The form opens as a popup anchored to the trigger button.
+
+```ts
+const onBtnCreated = (e?: Element) => {
+ if (!e) return
+ const btn = e as BUI.Button
+ btn.contextMenuTemplate = () => {
+ const onSubmit = async ({ target }: { target: BUI.Button }) => {
+ const section = target.parentElement as BUI.PanelSection
+ section.valueTransform = {
+ name: (value: string) => value.trim(),
+ description: (value: string) => value.trim() || undefined,
+ }
+ const data = section.value as { name: string; description?: string }
+ target.loading = true
+ await doSomething(data)
+ target.loading = false
+ BUI.ContextMenu.removeMenus()
+ }
+
+ return BUI.html`
+
+
+
+
+
+
+
+ `
+ }
+}
+
+return BUI.html``
+```
+
+## Rules
+
+- `contextMenuTemplate` must be assigned via `BUI.ref`, never inline in `BUI.html`.
+- Inputs inside the form require the `name` attribute for `section.value` to pick them up.
+- The `` acting as a form always has `fixed` so it can't be collapsed.
+- Use `section.valueTransform` to sanitize or coerce values before reading them.
+- Call `BUI.ContextMenu.removeMenus()` after a successful submit to close the popup.
diff --git a/docs/ui-components/library-examples.md b/docs/ui-components/library-examples.md
new file mode 100644
index 0000000..81f1d62
--- /dev/null
+++ b/docs/ui-components/library-examples.md
@@ -0,0 +1,24 @@
+# Library Examples
+
+Before writing any BUI component implementation, check whether an official example covers the pattern you need. Official examples are the best starting point: they show correct usage of BUI elements, proper rendering patterns, and idiomatic API usage.
+
+## How to find an example
+
+1. Fetch the `paths.json` below. Do not summarize the response — retain the full JSON in context and use the descriptions to reason about which examples are relevant.
+2. Use the descriptions in the JSON to reason about which examples best match the user's intent. Prefer semantic matching over path name matching — a description may cover what the user needs even if the component name doesn't match exactly.
+3. Only proceed to fetch an example if its description confirms that it directly covers the user's intent, or that it can serve as a building block for composing a new custom component. Do not fetch speculatively.
+4. Construct the full URL: `{base_url}{path_entry}` and fetch the example file to use as your implementation reference.
+
+Note that some components have multiple examples for different use cases — `Table` for instance has separate entries for `Searching`, `Grouping`, `ExportingData`, and `DataTransform`. Check all relevant ones.
+
+---
+
+## Repository
+
+### engine_ui-components
+BUI primitives from `@thatopen/ui` — the `packages/core/` entries.
+
+- **paths.json**: `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/examples/paths.json`
+- **Base URL**: `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/`
+
+> The paths.json also contains entries under `packages/obc/` (OBC-connected tables and charts like `SpatialTree`, `ModelsList`, etc.). Those are also valid UI components — feel free to check them if the user needs that kind of structured data display.
diff --git a/docs/ui-components/overview.md b/docs/ui-components/overview.md
new file mode 100644
index 0000000..3572554
--- /dev/null
+++ b/docs/ui-components/overview.md
@@ -0,0 +1,194 @@
+# UI Component Creation
+
+Guides and best practices for creating UI components with That Open Engine (BUI/OBC) — panels, tables, dropdowns, buttons, inputs, or any visual element.
+
+## Working mode
+
+Before doing anything else, **read [`./library-examples.md`](./library-examples.md) and fetch the `paths.json` for `engine_ui-components` to understand what BUI components are already available.**
+
+Only after that, **propose an implementation plan and wait for the user's approval.**
+
+The proposal must describe:
+- Which templates will be created or modified
+- What state (`State`) each template will need
+- Whether any template needs an `onInstanceCreated` callback
+- Whether it composes other existing templates
+
+Only proceed with changes after the user explicitly confirms the plan. If the scope is unclear, ask first — do not assume.
+
+---
+
+## Creating a UI Component
+
+### Step 1 — Define the component name
+
+The component name is the source of truth. Everything else derives from it.
+
+Given a name like *Files Section* or *Models List*, the full naming cascade is:
+
+| Artifact | Convention | Example |
+|---|---|---|
+| Folder | kebab-case | `files-section/`, `models-list/` |
+| Component identifier | camelCase | `filesSection`, `modelsList` |
+| Types prefix | PascalCase | `FilesSection`, `ModelsList` |
+| Template function | `{camelCase}Template` | `filesSectionTemplate` |
+| Callback | `on{PascalCase}Created` | `onFilesSectionCreated` |
+
+The identifier suffix describes what the component *is* from the consumer's perspective — typically the BUI element name:
+
+| Identifier | Root element |
+|---|---|
+| `filesSection` | `` |
+| `collisionsTable` | `` |
+| `modelsDropdown` | `` |
+| `actionsToolbar` | `
` |
+
+> **Exception:** `` uses the suffix `*Section`, not `*PanelSection`.
+
+---
+
+### Step 2 — Create the file structure
+
+Each component lives in its own folder:
+
+```
+files-section/
+ index.ts → template, onInstanceCreated (if needed), re-exports ./src
+ src/
+ index.ts → re-exports types and support files
+ types.ts → state types and any supporting types
+```
+
+The `src/` subfolder holds types and any support files the template needs — helpers, constants, sub-types, etc. If `index.ts` is growing too long, move the extra logic into `src/`.
+
+---
+
+### Step 3 — Define types in `src/types.ts`
+
+Every component needs at minimum these types:
+
+```ts
+import * as OBC from "@thatopen/components"
+import * as BUI from "@thatopen/ui"
+
+export interface FilesSectionState {
+ components: OBC.Components
+ // ...additional state
+}
+
+export type FilesSectionComponent = BUI.StatefullComponent
+```
+
+- **`{ComponentName}State`** — the reactive state passed to the template on every render. Must always include `components: OBC.Components`.
+- **`{ComponentName}Component`** — type alias for `BUI.StatefullComponent<{ComponentName}State>`. Used to type the template function.
+
+Additional types depend on the component. For example, tables also need `{ComponentName}Data` — which must be a `type` alias, not an `interface`, as `BUI.Table` requires a type alias:
+
+```ts
+// ✗ Avoid — interface does not satisfy BUI.Table
+export interface MyTableData { Name: string }
+
+// ✓ Correct
+export type MyTableData = { Name: string }
+```
+
+#### `components` is the only entry point to the engine
+
+Inside a template, any engine component must be accessed via `components.get()`. State must never receive component instances directly:
+
+```ts
+// ✗ Avoid — receiving a component instance directly
+const myTemplate = (state: { highlighter: OBF.Highlighter }) => { ... }
+
+// ✓ Correct — access everything through components
+const myTemplate = ({ components }: FilesSectionState) => {
+ const highlighter = components.get(OBF.Highlighter)
+}
+```
+
+#### How much logic belongs in a template?
+
+In the ideal case, a template is a thin facade: it reads state from BIM components via `components.get()` and triggers actions on them — nothing more.
+
+In practice, some UI-level logic is fine to keep in the template:
+
+- **Local coordination** — passing data from one sub-component to another within the same section
+- **Transient UI state** — loading flags, intermediate form values before the user confirms
+- **Orchestration** — calling several BIM components in sequence for a single user action
+
+What doesn't belong in a template is **domain logic**: calculations, data transformations, business rules. If a template starts computing things beyond what's needed to render, that logic belongs in a BIM component instead.
+
+The guiding question: *does this logic exist because the UI needs it, or because the domain requires it?* If the domain requires it, move it to a BIM component. See [`../connect-logic-to-ui.md`](../connect-logic-to-ui.md) for how UI templates connect to BIM components.
+
+---
+
+### Step 4 — Implement the template in `index.ts`
+
+### Rules
+
+- **Always prefer BUI components over native HTML elements.** Before using `