agents/code-quality.md
# Code Quality Agent
## Role
Use this helper agent to review Fusion Framework app code against quality standards. Delegates to the `fusion-code-conventions` skill for all convention rules — do not duplicate rule logic here.
## Inputs
- `file_paths`: source files to review
- `project_standards`: path to project-specific code standards (`contribute/`, `.github/copilot-instructions.md`), if available
## Process
### Step 1 — Discover project standards
1. Check for project-specific overrides: `contribute/`, `.github/copilot-instructions.md`, `AGENTS.md`.
2. Check for formatter/linter config: `biome.json`, `.editorconfig`.
3. Project-specific rules take precedence over skill defaults.
### Step 2 — Delegate to fusion-code-conventions
Invoke the `fusion-code-conventions` skill on each file. It will activate the appropriate language agents in parallel:
- TypeScript and React agents for convention, naming, TSDoc, type system, and code style
- Intent agent for inline comment quality across all files
- Constitution agent when the project has ADRs or contributor docs
Do not reimplement convention rules here — `fusion-code-conventions` is the authoritative source.
**If `fusion-code-conventions` is not available:** Prompt the user to install it before continuing:
> "`fusion-code-conventions` is required for a full convention review but does not appear to be installed. Install it with:
> ```
> npx -y skills add fusion-skills --skill fusion-code-conventions --agent github-copilot
> ```
> Once installed, re-run this review for complete coverage including ADR and contributor-doc enforcement.
> Alternatively, confirm you'd like a partial review using `assets/review-checklist.md` (covers TypeScript, TSDoc, naming, and code quality — but not constitution checks)."
Do not silently fall back to the checklist without offering the install option first.
### Step 3 — Report findings
Aggregate and surface findings from `fusion-code-conventions` in context of the Fusion app:
- **Required** — must fix before merge: TSDoc missing on exports, `any` types, naming violations, constitutional violations
- **Recommended** — should fix: weak intent comments, undocumented magic values, unjustified suppressions
- **Advisory** — consider: missing decision records, stale ADRs, refactoring opportunities
Include Fusion-specific context where relevant (e.g. a naming violation on a hook that wraps `useHttpClient`).
agents/data-display.md
# Data Display Agent
## Role
Use this helper agent to review or advise on data display implementation — choosing between AG Grid (tabular) and AG Charts (visual), configuring either surface, combining them on dashboard-style pages, and ensuring the data pipeline from API to rendered output follows Fusion Framework patterns.
## Inputs
- `file_paths`: source files to review (grid/chart components, `config.ts`, data hooks)
- `question`: specific data display question, if any
- `data_shape`: the API response or data structure being displayed, if known
## MCP tooling
When the Fusion MCP server is available, **prefer `mcp_fusion_search_framework`** to look up AG Grid and AG Charts packages, cookbook examples, and module configuration. This is more reliable than relying on memory alone.
Example queries:
- `mcp_fusion_search_framework` → `"enableAgGrid AgGridReact useTheme fusion-framework-react-ag-grid"`
- `mcp_fusion_search_framework` → `"ColDef columnDefs defaultColDef ag-grid column definition"`
- `mcp_fusion_search_framework` → `"fusion-framework-react-ag-charts AgCharts AgChartOptions"`
- `mcp_fusion_search_framework` → `"charts cookbook bar line pie area visualization"`
- `mcp_fusion_search_framework` → `"IntegratedChartsModule AG Grid charts enableAgGrid"`
- `mcp_fusion_search_framework` → `"AgChartsEnterpriseModule enterprise chart types"`
- `mcp_fusion_search_eds` → `"Table EDS simple read-only table"`
## Process
### Step 1: Determine the data display approach
Decide which surface fits the task:
| Need | Surface | Reference |
|---|---|---|
| Sortable, filterable, editable tabular data | AG Grid | `references/using-ag-grid.md` |
| Simple read-only table, few rows | EDS `Table` | `references/using-fusion-react-components.md` |
| Charts from standalone data (bar, line, pie, area) | AG Charts | `references/using-ag-charts.md` |
| Ad-hoc charts created from grid data | AG Grid integrated charts | `references/using-ag-grid-charts.md` |
| Lightweight prototype chart | Chart.js | `references/using-ag-charts.md` (alternative section) |
| Dashboard with both grid and charts | AG Grid + AG Charts | combine references |
When the user says "display this data" without specifying a format, ask whether tabular or visual is the primary view. Use `assets/charts-decision-matrix.md` when the chart library choice is unclear.
### Step 2: Verify setup
**For AG Grid:**
- `@equinor/fusion-framework-react-ag-grid` is installed.
- `enableAgGrid(configurator)` is called in `config.ts`.
- Feature modules are registered via `builder.setModules()` (since AG Grid 33).
- `useTheme()` provides the Fusion/EDS theme to `<AgGridReact theme={theme} />`.
**For AG Charts standalone:**
- `@equinor/fusion-framework-react-ag-charts` is installed.
- `ModuleRegistry.registerModules([AllCommunityModule])` is called once at application startup.
- Enterprise import from `/enterprise` sub-path only when enterprise chart types are needed.
**For AG Grid integrated charts:**
- Both `@equinor/fusion-framework-react-ag-grid` and `@equinor/fusion-framework-react-ag-charts` are installed.
- `IntegratedChartsModule.with(AgChartsEnterpriseModule)` is included in the `enableAgGrid` module list.
- Enterprise license is available (integrated charts require enterprise).
### Step 3: Review AG Grid implementation
When reviewing grid code, check:
- **Imports**: come from `@equinor/fusion-framework-react-ag-grid`, not directly from `ag-grid-*`.
- **Theming**: `useTheme()` is used, not inline theme objects or raw CSS overrides.
- **Column definitions**: typed with `ColDef<T>[]`, using `field` + `headerName` at minimum.
- **Default column defs**: shared defaults (`resizable`, `filter`, `flex`, `sortable`) reduce repetition.
- **Value formatters**: dates, numbers, and enums use `valueFormatter`, not string interpolation in cell renderers.
- **Cell renderers**: custom render logic uses `cellRenderer`, kept minimal.
- **Row data typing**: `rowData` prop is typed, not `any[]`.
- **Module registration**: only needed modules are registered (tree-shaking).
### Step 4: Review chart implementation
When reviewing chart code, check:
- **Imports**: AG Charts imports come from `@equinor/fusion-framework-react-ag-charts`, not directly from `ag-charts-*`.
- **Data typing**: chart data has a typed interface, not inline untyped objects.
- **Options typing**: AG Charts uses `AgChartOptions`; Chart.js uses the library's option types.
- **Separation of concerns**: data fetching, data transformation, and rendering are in separate layers.
- **Responsive sizing**: AG Charts uses container-based sizing; Chart.js uses `responsive: true` with a sized wrapper.
- **Labels and legends**: charts have meaningful axis labels, series names, and tooltips.
- **Color usage**: prefers EDS design tokens or a consistent palette over random hex values.
- **Declarative options**: AG Charts uses `AgChartOptions` objects, not imperative API calls.
- **Reactive updates**: `useState<AgChartOptions>` for chart options that change.
### Step 5: Review combined grid + chart pages
When a page has both grid and chart:
- Data is fetched once and shared, not fetched separately for each display.
- Grid and chart use the same typed data model.
- If the chart shows a summary of grid data, the transform is explicit and tested.
- Layout uses EDS spacing tokens and responsive containers.
### Step 6: Report findings
Produce a concise list:
- **Correct**: patterns following data display conventions
- **Issues**: problems with specific fix recommendations
- **Suggestions**: surface alternatives, data flow improvements, accessibility notes
Reference `references/using-ag-grid.md`, `references/using-ag-charts.md`, and `references/using-ag-grid-charts.md` for the canonical patterns.
agents/design.md
# Design Agent
## Role
Review or advise on **page and view structure** in a Fusion Portal app — shell composition, layout zone nesting, empty/loading state patterns, side panel usage, and structural anti-patterns conflicting with the Fusion Portal shell.
Does **not** review individual component token usage or EDS component selection — delegate those to `agents/styling.md`.
Design ground truth comes from `equinor-design-system` system skill. When installed, consult for authoritative token names, layout zone conventions, and empty/loading state patterns.
## Inputs
- `file_paths`: component or page-level files to review (e.g. top-level route components, layout wrappers, `App.tsx`)
- `question`: specific structural or layout question, if any
## MCP tooling
- Use **`mcp_fusion_search_eds`** for EDS component questions: tokens, props, usage examples, and layout primitives (e.g. `Typography`, `Button`, `Icon`, `Progress`, `TopBar`).
- Use **`mcp_fusion_search_framework`** for Fusion-specific component and shell questions (e.g. `SideSheet` from `@equinor/fusion-react-side-sheet`, how portal wraps the app, navigation zones, module APIs).
## Process
### Step 1: Identify structural scope
Read target files. Determine:
1. Root/layout component (`App.tsx`, top-level route) or leaf component?
2. Establishes layout containers (flex, grid, scrollable wrappers)?
3. Uses side panel or overlay?
4. Defines loading or empty states?
Focus structural review on root and near-root components. Leaf components rarely have structural issues.
### Step 2: Check shell composition
Verify app does not replicate portal-owned zones:
- **No custom top navigation bar** — Fusion Portal owns global header. Flag any custom fixed/sticky header at viewport top.
- **No custom left rail or sidebar navigation** — portal shell owns left rail.
- **No outer margin/padding on root app element** — portal provides content inset; extra outer spacing creates double-guttering.
- **No fixed full-viewport-height container** — allow content to stretch naturally; do not hard-code `height: 100vh` on app root.
### Step 3: Check layout zone usage
Verify layout follows three-zone convention from `equinor-design-system`:
| Zone | Expected | Anti-pattern |
|---|---|---|
| Main content | `<main>` or app root `<div>` spanning full available area | Custom flexbox wrapper that clips or limits available height |
| Side panel | `@equinor/fusion-react-side-sheet` | Custom `position: fixed` or `position: absolute` right panel |
| Spacing | `--eds-spacing-*` / `--eds-container-space-*` tokens | Arbitrary `margin: 24px`, `padding: 16px` raw pixel values |
For spacing violations, identify specific token (refer to `equinor-design-system` spacing table or `mcp_fusion_search_eds`):
- `var(--eds-spacing-horizontal-sm)` / `var(--eds-spacing-vertical-sm)` — tight gaps (8px)
- `var(--eds-container-space-horizontal)` / `var(--eds-container-space-vertical)` — container padding
- `var(--eds-page-space-horizontal)` / `var(--eds-page-space-vertical)` — page-level padding
### Step 4: Check structural anti-patterns
Flag:
- **Nested full-page scrollable containers** — page content wrapped in `overflow-y: auto` when browser scroll is intended. Deliberate AG Grid or fixed-height table regions are acceptable.
- **Custom positioned overlays instead of SideSheet** — `position: fixed` right-side panels, custom drawers, or dialog-like components where `@equinor/fusion-react-side-sheet` or EDS `Dialog` should be used.
- **Shadow or color outside EDS tokens** — raw `box-shadow`, hex codes, or named CSS colors instead of `--eds-color-bg-*`, `--eds-color-text-*`, and `--eds-color-border-*` tokens. EDS v2 has no elevation CSS variables; use EDS `Paper` component or JS token import for elevation.
- **Layout replicating EDS primitives** — manual flex/grid containers duplicating EDS `Grid`, `Divider`, or `Stack`-like behavior.
For component-level token violations (button color, typography variant, icon size), delegate to `agents/styling.md`.
### Step 5: Check empty and loading states
Verify correct state patterns:
| State | Expected pattern | Anti-pattern |
|---|---|---|
| Loading | EDS `Progress.Circular` or `Progress.Dots` (`import { Progress } from '@equinor/eds-core-react'`) — centered in content area | Blank screen, spinner in corner, or `display: none` |
| Empty (no data) | `Typography` + optional primary action `Button` — centered or top-aligned | Omitted state (user sees nothing), custom illustration without text |
| Error | `Typography` with danger semantic color + retry action | Raw `alert()`, uncaught exception, or blank component |
If state missing entirely (component renders nothing when loading or empty), flag as structural gap.
### Step 6: Report findings
Produce structured report:
**Structure** — shell composition and zone usage:
- ✅ Correct patterns
- 🚫 Issues (structural violations) with specific fix
**Anti-patterns** — things to remove or replace
**Empty / loading states** — present, missing, or incorrect
**Delegated to `styling.md`** — component-level EDS/token issues spotted but out of scope
If no issues, state: "Shell composition and layout zones follow Fusion Portal conventions."
agents/framework.md
# Framework Agent
## Role
Use this helper agent to review or advise on Fusion Framework integration — module configuration, HTTP clients, authentication, context, navigation, settings, bookmarks, analytics, runtime config, and the app bootstrap lifecycle.
When the question is mainly about framework package ownership, exact API behavior, or finding a supporting example, use the companion skill `fusion-research` first and then apply that evidence during the integration review.
## Inputs
- `file_paths`: source files to review (typically `config.ts`, `index.ts`, `app.config.ts`, hooks using framework modules)
- `question`: specific framework question or concern, if any
## MCP tooling
When the Fusion MCP server is available, **prefer `mcp_fusion_search_framework`** to look up Fusion Framework APIs, module configuration, hooks, and package documentation. Use `mcp_fusion_search_docs` for general Fusion platform guidance (onboarding, concepts, operations). This is more reliable than relying on memory alone.
If the runtime supports companion skills, follow the `fusion-research` workflow for evidence gathering: choose the right search lane, capture `metadata.source` plus the supporting excerpt, and stop after one refinement pass when results stay weak.
Example queries:
- `mcp_fusion_search_framework` → `"configureHttpClient useHttpClient configure http module"`
- `mcp_fusion_search_framework` → `"renderApp makeComponent AppModuleInitiator entry point"`
- `mcp_fusion_search_framework` → `"useCurrentContext context module"`
- `mcp_fusion_search_framework` → `"useCurrentBookmark enableBookmark bookmark module"`
- `mcp_fusion_search_framework` → `"useAppSetting useAppSettings settings module"`
- `mcp_fusion_search_docs` → `"analytics useTrackFeature portal analytics"`
- `mcp_fusion_search_docs` → `"app configuration endpoints environment"`
## Process
### Step 1: Read the framework surface
1. Read the project's `src/config.ts` to understand existing module configuration.
2. Read `app.config.ts` and `app.manifest.ts` for endpoint and environment setup.
3. Read `src/index.ts` to confirm the bootstrap pattern in use.
4. Identify which Fusion modules are already configured and which hooks are in use.
5. If the review is blocked on uncertain framework behavior, gather source-backed evidence via `fusion-research` before deciding whether the code is correct.
### Step 2: Check against framework API
Validate that the code follows current Fusion Framework patterns:
- **Entry point**: uses `renderApp(App, configure)` or the lower-level `makeComponent` pattern.
- **HTTP clients**: registered via `configureHttpClient` in `config.ts` or via `endpoints` in `app.config.ts` (auto-registered).
- **Client access**: uses `useHttpClient(name)` from `@equinor/fusion-framework-react-app/http`. Direct module access (`framework.modules.http.createClient`) is only acceptable in non-React contexts like route loaders.
- **Context**: uses `useCurrentContext()` from `@equinor/fusion-framework-react-app/context`.
- **Auth**: uses `useCurrentAccount()` / `useAccessToken()` from `@equinor/fusion-framework-react-app/msal` — never manages tokens manually.
- **Navigation**: uses `useRouter()` from `@equinor/fusion-framework-react-app/navigation`.
- **Environment variables**: uses `useAppEnvironmentVariables()` for runtime config.
- **Settings**: uses `useAppSetting()` / `useAppSettings()` for per-user preferences.
- **Bookmarks**: uses `enableBookmark()` in configuration and `useCurrentBookmark()` for shareable view state.
- **Analytics**: uses `useTrackFeature()` for user-facing instrumentation.
### Step 3: Identify issues
Flag:
- Duplicate HTTP client registrations (same client in both `app.config.ts` and `config.ts`)
- Missing MSAL scopes for authenticated endpoints
- Direct `fetch()` calls that should use the Fusion HTTP client (misses auth, interceptors, observables)
- Module access outside of React component context (hooks must be called inside components/hooks)
- Hardcoded URLs that should come from `app.config.ts` endpoints or environment variables
- New code using deprecated bookmark patterns where `useCurrentBookmark()` is the better current surface
- Shareable view state stored in app settings instead of bookmarks
- Custom telemetry calls where `useTrackFeature()` would match framework analytics
### Step 4: Report findings
Produce a concise list:
- **Correct**: patterns that follow the framework API properly
- **Issues**: problems with specific fix recommendations
- **Suggestions**: optional improvements (e.g. adding error boundaries, using environment variables)
Reference `references/create-fusion-app.md`, `references/configure-services.md`, `references/using-framework-modules.md`, `references/using-settings.md`, `references/using-bookmarks.md`, `references/using-assets-and-environment.md`, and `references/using-analytics.md` for the canonical patterns.
agents/person-components.md
# Person Components Agent
## Role
Use this helper agent when a developer asks how to display, search, pick, or list people in a Fusion Framework app using `@equinor/fusion-react-person`. It covers component selection, usage patterns, `PersonCell` in AG Grid, common pitfalls, and the correct import paths.
Delegate token-level EDS styling questions to `agents/styling.md`. Delegate AG Grid module setup questions to `agents/data-display.md`.
## Inputs
- `question`: what the developer is trying to build (e.g. "show a person avatar", "ag-grid column with person names", "multi-person picker")
- `file_paths` (optional): source files already in the project to review for existing patterns
## MCP tooling
When Fusion MCP is available, use **`mcp_fusion_search_eds`** for EDS component props and **`mcp_fusion_search_framework`** for framework integration questions. Cross-check against the [fusion-react-components Storybook](https://equinor.github.io/fusion-react-components/?path=/docs/person-docs--docs) when component behaviour is unclear.
## Process
### Step 1: Identify the UI need
Map the developer's request to a component using the decision guide:
| UI need | Component |
|---|---|
| Avatar only, hover for details | `PersonAvatar` |
| Full detail view (card, panel, popover) | `PersonCard` |
| One-line person row in a list | `PersonListItem` |
| Person row with action button (remove, edit) | `PersonListItem` with children |
| Pick a single person | `PersonPicker` or `PersonSelect` |
| Pick multiple people | `PeoplePicker` |
| Display a selected collection | `PeopleViewer` |
| AG Grid cell | `PersonCell` |
If the need is still ambiguous after reading the request, ask: _"Are you looking to display a person's info, let the user pick a person, or show people in a data grid?"_
### Step 2: Confirm import paths
All person components are imported from `@equinor/fusion-react-person`:
```typescript
import {
PersonAvatar,
PersonCard,
PersonListItem,
PersonPicker,
PersonSelect,
PeoplePicker,
PeopleViewer,
PersonCell,
} from '@equinor/fusion-react-person';
import type {
PersonInfo,
PersonAddedEvent,
PersonRemovedEvent,
} from '@equinor/fusion-react-person';
```
### Step 3: Apply the usage example
Refer to `references/using-fusion-react-components.md` for complete copy-pasteable examples for each component.
Key patterns to reinforce:
**Display components resolve people data automatically** — `PersonAvatar`, `PersonCard`, `PersonListItem`, and `PersonCell` accept an `azureId` (preferred) or `upn` prop. Do not pass name strings or fetch people data manually.
**Picker and viewer components work with `PersonInfo` objects** — `PersonPicker`, `PeoplePicker`, and `PeopleViewer` accept a `person` (`PersonInfo`) or `people` (`PersonInfo[]`) prop. Do not pass raw `azureId`/`upn` strings to these components.
**Picker/selector event pattern** — person picker/selector components fire custom DOM events, not standard React callbacks. Always access the person via `e.detail`:
```typescript
onPersonAdded={(e: PersonAddedEvent) => {
const person: PersonInfo = e.detail;
}}
```
**`person-click` DOM event** — display components (`PersonCard`, `PersonListItem`, `PersonCell`) fire a `person-click` custom DOM event when the user clicks the person element. Access the clicked person via `e.detail`:
```typescript
element.addEventListener('person-click', (e: PersonClickEvent) => {
const person: PersonInfo = e.detail;
});
```
**`PersonCell` in AG Grid** — the cell renderer reads the cell value as an `azureId` string. If the `azureId` is nested, use `valueGetter` to extract it:
```typescript
{ cellRenderer: PersonCell, valueGetter: ({ data }) => data?.owner?.azureId }
```
### Step 4: Check for common pitfalls
- **Fetching person data manually**: unnecessary — all components resolve from the Fusion People API by `azureId`. Remove any manual fetch + state pattern.
- **Using `onPersonAdded` as a plain function callback**: wrong — it is a DOM event handler. Access the person via `e.detail`, not the argument directly.
- **Passing a name string as `azureId`**: will silently fail. Ensure the value is a valid Azure AD object ID (UUID format).
- **`PersonCell` receives an object instead of a string**: use `valueGetter` to extract the `azureId` before passing to the renderer.
- **Using `PeopleViewer` without `PeoplePicker`**: `PeopleViewer` only displays — it does not manage selection state. Pair with `PeoplePicker` when selection is also needed.
### Step 5: Report
Produce:
- **Component chosen** with one-line rationale
- **Usage snippet** (copy-pasteable)
- **Pitfalls found** (if reviewing existing code)
- **References**: `references/using-fusion-react-components.md`, [Storybook docs](https://equinor.github.io/fusion-react-components/?path=/docs/person-docs--docs)
agents/styling.md
# Styling Agent
## Role
Use this helper agent to review or advise on styling decisions — EDS component selection, styled-components usage, design token application, and visual consistency with the Equinor Design System.
## Inputs
- `file_paths`: component files to review
- `question`: specific styling question, if any
- `component_name`: EDS component being considered, if looking for guidance
## MCP tooling
When the Fusion MCP server is available, **prefer `mcp_fusion_search_eds`** to look up EDS component documentation, props, usage examples, and accessibility guidance. This is more reliable than relying on memory alone.
Example queries:
- `mcp_fusion_search_eds` → `"Button props variants color disabled"`
- `mcp_fusion_search_eds` → `"Dialog modal usage examples"`
- `mcp_fusion_search_eds` → `"Typography variants heading body"`
- `mcp_fusion_search_eds` → `"color tokens CSS variables light dark"`
- `mcp_fusion_search_eds` → `"EdsDataGrid ag-grid wrapper"`
## Process
### Step 1: Read the styling surface
1. Read the target component files.
2. Check which EDS components and tokens are imported.
3. Identify any non-styled-components styling (CSS files, inline styles, other CSS-in-JS).
4. Check if a `Styled` object pattern is used consistently.
### Step 2: Evaluate EDS usage
Check:
- **Component selection**: Is there an EDS component that fits this use case? Prefer EDS `Button`, `Typography`, `Dialog`, `Table`, `Tabs`, `Chip`, `Card`, `Search`, `Banner`, `Snackbar`, `Menu`, `Tooltip`, `Progress`, `Autocomplete` over custom implementations.
- **Icon usage**: Uses `@equinor/eds-icons` data objects with `<Icon data={...} />`, not raw SVGs or icon fonts.
- **Token usage**: Colors, spacing, typography, and elevation come from EDS tokens (CSS custom properties like `--eds-color-*`, `--eds-spacing-*` or JS imports from `@equinor/eds-tokens`), not hardcoded values.
- **Density**: Uses `<EdsProvider density="comfortable|compact">` when the project needs density switching, not manual sizing.
### Step 3: Evaluate styled-components patterns
Check:
- **`Styled` object pattern**: Styled elements are grouped in a `const Styled = { ... }` at the top of the file.
- **Extending EDS**: Custom styling on EDS components uses `styled(EdsComponent)`, not class overrides or `!important`.
- **Responsive design**: Media queries are inside styled-component template literals, not in separate CSS.
- **No forbidden patterns**: No CSS Modules, no global CSS imports for component styling, no Tailwind in the Fusion ecosystem.
### Step 4: Check accessibility
- Interactive elements have accessible labels (`aria-label`, `aria-labelledby`, or visible text).
- Color is not the sole way to convey information.
- EDS components handle most accessibility automatically — verify custom elements meet the same standard.
- `aria-disabled` is preferred over `disabled` for buttons that need tooltip support.
### Step 5: Report findings
Produce a concise list:
- **Correct**: patterns following EDS and styled-components conventions
- **Issues**: problems with specific fix recommendations (e.g. "replace hardcoded `#007079` with `var(--eds-color-bg-accent-fill-emphasis-default)`")
- **Suggestions**: EDS component alternatives, token replacements, accessibility improvements
Reference `references/styling-with-eds.md` and `references/styled-components.md` for the canonical patterns.
assets/charts-decision-matrix.md
# Charts Library Decision Matrix
Use this matrix when a developer asks about charting in a Fusion Framework app and the library choice is not already decided.
## Decision table
| Factor | AG Charts | Chart.js |
|---|---|---|
| **Fusion alignment** | First-class via `@equinor/fusion-framework-react-ag-charts` | Third-party; no Fusion wrapper |
| **Version management** | Centrally managed across all Fusion apps | Managed per-app in `package.json` |
| **AG Grid integration** | Native via `IntegratedChartsModule` | Not supported |
| **Community chart types** | Bar, line, area, pie, donut, scatter, bubble | Bar, line, area, pie, doughnut, scatter, bubble, radar, polar |
| **Enterprise chart types** | Waterfall, heatmap, sunburst, treemap, stock, maps | Not available |
| **TypeScript support** | Full type definitions via Fusion wrapper | Full type definitions |
| **Bundle size** | Larger; offset by shared singleton in Fusion portal | Smaller; tree-shakeable per component |
| **Interactivity** | Built-in pan, zoom, crosshairs, tooltips | Basic tooltips; plugins needed for advanced interaction |
| **License** | Community: free; Enterprise: commercial license required | MIT |
## Default recommendation
**Recommend AG Charts** for new Fusion Framework apps. The Fusion wrapper ensures version consistency across the portal.
## When to recommend Chart.js instead
- Quick prototype where bundle size matters
- The user explicitly prefers Chart.js
- Radar or polar area charts are needed (AG Charts community does not include these)
- Simple one-off chart that does not justify the AG Charts dependency
## When to recommend AG Grid integrated charts
- The page already has an AG Grid showing the same dataset
- Users should be able to create ad-hoc charts from grid data
- The chart and grid need to stay in sync
## Sources
- [Charts Cookbook — "When to Use Each Library"](https://github.com/equinor/fusion-framework/blob/main/cookbooks/app-react-charts/README.md)
- [@equinor/fusion-framework-react-ag-charts — "Who Should Use This"](https://github.com/equinor/fusion-framework/blob/main/packages/react/ag-charts/README.md)
assets/follow-up-questions.md
# Follow-Up Questions
Clarifying questions to ask before implementing, organized by domain. Pick the relevant section based on what the user is building. Skip questions the user already answered.
## Components (UI / Presentation)
- What data does this component display? Is the shape already defined in `src/types/`?
- Which EDS components are closest to the desired look? (e.g. `Card`, `Table`, `Dialog`, `Tabs`)
- Does this component need to handle loading/error states, or does a parent handle that?
- Should it support both compact and comfortable density, or only one?
- Is this a standalone component or a child of an existing page/layout?
- Are there interactive elements (buttons, inputs, toggles)? What actions do they trigger?
## Hooks (State & Side Effects)
- What triggers this hook? (mount, context change, user action, interval)
- Does the hook wrap an API call, or purely manage local state?
- Should the result be cached/shared across components (React Query), or local to one component?
- Does the hook depend on the current Fusion context (`useCurrentContext`)?
- What should happen on error — retry, fallback value, or propagate to the caller?
## Data Fetching / API Integration
- Is the API endpoint already registered in `app.config.ts` or `config.ts`?
- What is the base path and HTTP method? (e.g. `GET /api/items/:id`)
- Does the endpoint require authentication scopes? Which ones?
- What does the response shape look like? Is there a type/interface already?
- Should results be cached? What's a reasonable stale time?
- Does the data depend on the current Fusion context (project/facility)?
- Is this a read (query) or write (mutation) operation?
## Routing
- How many pages/views does this feature need?
- What URL structure makes sense? (e.g. `/items`, `/items/:id`, `/items/:id/edit`)
- Does each page need its own data loader (`clientLoader`)?
- Is there a shared layout (nav bar, sidebar) across these pages?
- Should routes be documented in the app manifest (`app.manifest.ts`)?
- Are there any route parameters or search params to handle?
## Context (Fusion Context Module)
- Which context type does the app operate on? (`ProjectMaster`, `Facility`, custom?)
- Does the app need to sync with the portal's context picker, or manage its own?
- Should data refetch automatically when the user switches context?
- Does the app need related contexts (e.g. tasks under a project)?
- Is there a custom context backend, or does it use the standard Fusion context API?
## Styling
- Is there a design spec, mockup, or reference to follow?
- Which EDS components should be used as the base?
- Does the component need responsive behavior? At which breakpoints?
- Are there any custom colors or spacing beyond EDS tokens?
- Should the component adapt to density switching (`EdsProvider`)?
## AG Grid
- What data populates the grid? Is the type defined?
- Which columns are needed? Any custom formatters or cell renderers?
- Does the grid need sorting, filtering, or column reordering?
- Is row selection needed? Single or multi-select?
- Should the grid export data (Excel, clipboard)?
- Are Enterprise features required, or is Community sufficient?
## Charts & Visualization
- What data should the chart display? Is the shape already defined?
- Which chart type fits the data? (bar, line, area, pie, scatter, or let the user choose?)
- Is the chart standalone, or should it be created from AG Grid data (integrated charts)?
- Does the chart need to update reactively when data changes?
- Are enterprise chart types needed (waterfall, heatmap, sunburst, treemap)?
- Is there a library preference (AG Charts vs Chart.js), or should we go with the default (AG Charts)?
- How many charts appear on the page? Is this a dashboard layout?
- Does the chart need interactive features (zoom, pan, crosshairs, drill-down)?
## Module Configuration
- Which Fusion modules does this feature need? (HTTP, context, navigation, feature flags, settings)
- Are there existing modules configured in `config.ts` that this feature should reuse?
- Does this feature introduce a new API backend that needs an HTTP client?
- Are there environment-specific differences (dev vs. prod endpoints, scopes)?
## Dev Server / Mocking
- Is a real backend available, or do we need mock routes in `dev-server.config.ts`?
- If proxying, what is the upstream URL? Does it need authentication?
- Should the mock return static data or generated/dynamic data?
- Does the mock need to simulate error responses for testing?
assets/new-app-checklist.md
# New App Checklist
Step-by-step checklist for creating a new Fusion Framework React app from scratch. Use as a progress tracker — tick items off as you go.
## 1. Scaffold
- [ ] Create the app with `fusion-framework-cli app create <name>` or manually scaffold the directory
- [ ] Verify `package.json` has the correct `name`, `scripts`, and `@equinor/fusion-framework-cli` devDependency
- [ ] Confirm `tsconfig.json` uses strict mode and `"moduleResolution": "bundler"`
- [ ] Confirm the entry point pattern in `src/index.ts` (`renderApp(App, configure)`)
## 2. Configuration files
- [ ] `app.manifest.ts` — set `appKey` and `displayName`
- [ ] `app.config.ts` — define `environment` and `endpoints` for API backends
- [ ] `src/config.ts` — export `configure: AppModuleInitiator` (can start empty)
- [ ] `src/App.tsx` — root component rendering the layout shell
## 3. Module setup (in `config.ts`, as needed)
- [ ] **HTTP clients** — registered via `app.config.ts` endpoints (auto) or `configureHttpClient` (custom)
- [ ] **Context** — `enableContext(configurator, (builder) => { builder.setContextType([...]) })`
- [ ] **Navigation** — `enableNavigation(configurator, env.basename)` if using routing
- [ ] **Feature flags** — `configurator.enableFeatureFlag({ key, title })` if toggling features
- [ ] **AG Grid** — `enableAgGrid(configurator)` if using data grids
## 4. Routing (if multi-page)
- [ ] Install `@equinor/fusion-framework-react-router`
- [ ] Create `src/routes.ts` using the DSL (`layout`, `index`, `route`, `prefix`)
- [ ] Create `src/Router.tsx` with `<Router routes={routes} />`
- [ ] Add the Vite plugin `reactRouterPlugin()` if using the route DSL
- [ ] Wire router into `App.tsx`
- [ ] Update `app.manifest.ts` with `routes: await toRouteSchema(routes)` for portal discovery
## 5. Dev server
- [ ] Create `dev-server.config.ts` if mocking APIs or proxying to a local backend
- [ ] Verify the dev server starts without errors (e.g. `bun run dev` / `pnpm dev`)
- [ ] Confirm the app loads in the browser at the dev server URL
## 6. First feature
- [ ] Define types in `src/types/`
- [ ] Create API layer in `src/api/` (hooks wrapping `useHttpClient` or React Query)
- [ ] Create components in `src/components/`
- [ ] Create hooks in `src/hooks/` for state and side-effect logic
- [ ] Wire everything together in a page or the root `App.tsx`
## 7. Styling
- [ ] EDS components (`@equinor/eds-core-react`) used as the base for UI elements
- [ ] `styled-components` used for custom styling with the `Styled` object pattern
- [ ] EDS design tokens used for colors, spacing, and typography (no hardcoded values)
- [ ] Icons imported from `@equinor/eds-icons` as data objects
## 8. Code quality
- [ ] TSDoc on every exported function, component, hook, and type
- [ ] Inline *why* comments on iterators, decision gates, and complex logic
- [ ] No `any` types — TypeScript strict mode enforced
- [ ] Explicit return types on exported functions
- [ ] Conventional commits used for all changes
## 9. Validation
- [ ] Typecheck — zero errors (e.g. `bun run typecheck` / `pnpm typecheck`)
- [ ] Lint — zero violations (e.g. `bun run lint` / `biome check src/`)
- [ ] Production build succeeds (e.g. `bun run build` / `pnpm build`)
- [ ] No new dependencies added without justification
- [ ] No secrets or credentials in source files
## 10. Documentation
- [ ] `README.md` updated with app description, setup instructions, and available scripts
- [ ] ADR created in `docs/adr/` for significant architectural decisions
- [ ] `contribute/` directory reviewed for project-specific code standards
assets/review-checklist.md
# Post-Generation Review Checklist
Run through this checklist after generating or modifying code. Skip items that don't apply to the change.
## TypeScript
- [ ] No `any` types — use specific types, generics, or `unknown` with narrowing
- [ ] No type assertions (`as`) where proper typing is possible
- [ ] No non-null assertions (`!`) without clear justification
- [ ] Explicit return types on all exported functions
- [ ] Discriminated unions used for narrowing instead of type casting
- [ ] Generic constraints are specific, not `T extends any`
## TSDoc
- [ ] Every exported function, component, hook, class, and type has TSDoc
- [ ] Summary explains *intent and why*, not restating the name
- [ ] `@param` for every parameter (without restating the type)
- [ ] `@returns` for every non-void function
- [ ] `@template` for every generic type parameter
- [ ] `@throws` for meaningful error paths
- [ ] `@example` for user-facing and non-trivial public APIs
## Naming
- [ ] Components: PascalCase file and export name
- [ ] Hooks: `use` prefix, camelCase file name
- [ ] Types/interfaces: PascalCase, no `I` prefix
- [ ] Constants: SCREAMING_SNAKE_CASE
- [ ] Variables and functions: camelCase
- [ ] Names are descriptive and self-documenting
## Code Quality
- [ ] Each function/component has a single responsibility
- [ ] Immutable patterns used (`map`, `filter`, `reduce`) over mutable accumulators
- [ ] No dead code: unused imports, unreachable branches, commented-out blocks
- [ ] Inline *why* comments on iterators, decision gates, complex logic
- [ ] No comments that restate syntax or obvious control flow
- [ ] Non-trivial inline callbacks extracted into named helpers with TSDoc
- [ ] Error handling uses specific error types with context, not bare `Error`
## EDS & Styling
- [ ] EDS components used before building custom UI elements
- [ ] `styled-components` used for custom styling (not CSS Modules, Tailwind, global CSS)
- [ ] `Styled` object pattern used for co-located styled components
- [ ] EDS design tokens used for colors, spacing, typography (no hardcoded hex/px values)
- [ ] Icons from `@equinor/eds-icons` with `<Icon data={...} />`
- [ ] Inline `style` props only for one-off tweaks, not reusable patterns
## Accessibility
- [ ] Interactive elements have accessible labels (`aria-label`, `aria-labelledby`, or visible text)
- [ ] `aria-disabled` used instead of `disabled` on buttons that need tooltip support
- [ ] `title` provided on `<Icon>` for screen readers
- [ ] Color is not the sole way to convey information
## Fusion Framework
- [ ] HTTP clients accessed via `useHttpClient(name)`, not raw `fetch()`
- [ ] No duplicate client registrations (same client in both `app.config.ts` and `config.ts`)
- [ ] Context read from `useCurrentContext()`, not duplicated into local state
- [ ] No hardcoded URLs — endpoints come from `app.config.ts` or environment variables
- [ ] No manual token management — MSAL module handles auth
- [ ] Per-user preferences use app settings, not ad hoc runtime config or local storage by default
- [ ] Bookmark payloads are serializable and limited to shareable view state
- [ ] Analytics events use `useTrackFeature()` or the project's approved wrapper
- [ ] Module hooks (`useHttpClient`, `useCurrentContext`, etc.) called inside React components/hooks only
## Data Fetching
- [ ] React Query used for server state, React state for client UI state
- [ ] Query keys derived from API path + parameters
- [ ] Queries disabled (`enabled: false`) when required parameters are missing
- [ ] Mutations invalidate related queries on success
- [ ] Loading and error states handled in the UI
## Charts & Visualization
- [ ] AG Charts imports come from `@equinor/fusion-framework-react-ag-charts`, not directly from `ag-charts-*`
- [ ] `ModuleRegistry.registerModules([AllCommunityModule])` called once at startup before any chart renders
- [ ] Chart data has a typed interface, not inline untyped objects
- [ ] Chart options use `AgChartOptions` type (AG Charts) or library option types (Chart.js)
- [ ] Data fetching, transformation, and chart rendering are in separate layers
- [ ] Charts are in a responsive container with explicit sizing
- [ ] Enterprise imports only when enterprise features are needed and license is available
- [ ] AG Grid integrated charts use `IntegratedChartsModule.with(AgChartsEnterpriseModule)` in `enableAgGrid`
## Dependencies
- [ ] No new dependencies added without explicit justification
- [ ] No secrets or credentials in source files
- [ ] No direct DOM manipulation — React patterns used
## Validation
- [ ] Typecheck passes with zero errors (e.g. `bun run typecheck` / `pnpm typecheck`)
- [ ] Lint passes with zero violations (e.g. `bun run lint` / `biome check src/`)
- [ ] Conventional commit message prepared (`feat:`, `fix:`, `refactor:`, etc.)
CHANGELOG.md
# Changelog
## 0.3.0 - 2026-05-07
### minor
- [#860](https://github.com/equinor/fusion-skills/pull/860) [`96f1768`](https://github.com/equinor/fusion-skills/commit/96f1768e3f1cdf988caa4a398776d8753b4cb77f) - Add design agent to fusion-developer-app
- Add `agents/design.md` helper agent covering Fusion Portal shell composition, layout zone nesting, side panel usage (SideSheet), empty/loading state patterns, and structural anti-patterns
- Update `SKILL.md` Step 4 to reference `design.md` for page/view structure review and `styling.md` for component-level checks
- Update helper agents section to include `design.md` with clear scope boundary vs `styling.md`
Agent references `equinor-design-system` system skill for authoritative token and layout zone ground truth, and delegates component-level EDS checks to `agents/styling.md`.
resolves equinor/fusion-core-tasks#860
### patch
- [#170](https://github.com/equinor/fusion-skills/pull/170) [`5e43223`](https://github.com/equinor/fusion-skills/commit/5e432232917b2b1642431d80cf1698bbefe80ee8) - Apply caveman-compress prose style to SKILL.md and all references.
- Drop articles, filler, hedging from SKILL.md activation body
- Compress all using-*.md, configure-*.md, styled-components, styling-with-eds, project-structure references
## 0.2.0 - 2026-05-05
### minor
- [#753](https://github.com/equinor/fusion-skills/pull/753) [`88878fe`](https://github.com/equinor/fusion-skills/commit/88878fe1c7d4fbfdd6b49139f3718e68d8e5aad7) - Add custom Fusion Framework module authoring reference
- Add `references/using-custom-modules.md` covering the IModule contract, wiring into `config.ts`, accessing via `useAppModule`, module lifecycle, file structure, and common pitfalls (naming conflicts, outside-React access, un-awaited config)
- Update `SKILL.md` Step 6 module table with a `using-custom-modules.md` entry
- Add custom-module-related activation triggers
resolves equinor/fusion-core-tasks#753
- [#160](https://github.com/equinor/fusion-skills/pull/160) [`0428056`](https://github.com/equinor/fusion-skills/commit/042805616fba13256265b1622c0cdd344f59197a) - Add Fusion Framework people service reference
- Add `references/using-people-service.md` covering the preferred `azureId`-only integration pattern, picker components, `PersonCell` for AG Grid, and guidance on what NOT to do (manual API calls, caching PersonInfo)
- Update `SKILL.md` Step 6 module table with a `using-people-service.md` entry
- Add people-related activation triggers to the skill description
resolves equinor/fusion-core-tasks#748
- [#858](https://github.com/equinor/fusion-skills/pull/858) [`50bfdf6`](https://github.com/equinor/fusion-skills/commit/50bfdf6451059951e94e8ec4b69fdc33e77d4c7a) - Add person component training wheels to fusion-developer-app
- Expand `references/using-fusion-react-components.md` with `PersonCard`, `PersonListItem`, and `PersonCell` (AG Grid) usage examples
- Add a decision guide table covering all person components and their intended use cases
- Add new `agents/person-components.md` helper agent covering component selection, the custom DOM event pattern, `PersonCell` valueGetter setup, and common pitfalls
- Update `SKILL.md` helper agents section and Step 6 module table to reference the new agent
resolves equinor/fusion-core-tasks#858
- add companion-skill metadata for `fusion-research-framework`
- delegate framework API and package research before choosing app implementation patterns
- align the framework helper agent with the shared source-backed research workflow
## 0.1.0 - 2026-03-18
### minor
- [#97](https://github.com/equinor/fusion-skills/pull/97) [`da1c011`](https://github.com/equinor/fusion-skills/commit/da1c011b803f79ba159313d54b531ab9dbcc6708) - Add fusion-app-react-dev skill to the catalog
Guides feature development in Fusion Framework React apps — scaffolding
components, hooks, services, and types that follow EDS conventions and
Fusion Framework patterns. Includes helper agents for framework review,
styling review, and code-quality review, plus reference docs and asset
checklists.
resolves equinor/fusion-core-tasks#799
references/configure-mocking.md
# Configure Dev Server (Mocking & Proxying)
How to mock API responses and proxy to local backends during development using `dev-server.config.ts`.
All dev-time API configuration lives in `dev-server.config.ts` at the project root — **not** in `app.config.ts`. The Fusion CLI dev server loads this file automatically and merges it with its defaults.
## Mock API routes
Define middleware routes that return mock data without a real backend:
```typescript
// dev-server.config.ts
import { defineDevServerConfig } from '@equinor/fusion-framework-cli/dev-server';
export default defineDevServerConfig(() => ({
api: {
routes: [
{
match: '/api/items',
middleware: (_req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify([
{ id: '1', name: 'Item A' },
{ id: '2', name: 'Item B' },
]));
},
},
{
match: '/api/items/:id',
middleware: (req, res) => {
const id = req.params?.id;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ id, name: `Item ${id}` }));
},
},
],
},
}));
```
App code uses `fetch('/api/items')` or `useHttpClient(name)` — dev server intercepts matching routes before network calls.
## Proxy to a local backend (simple)
For a local API (e.g. Docker Compose), add a proxy route:
```typescript
// dev-server.config.ts
import { defineDevServerConfig } from '@equinor/fusion-framework-cli/dev-server';
const MY_API_TARGET = process.env.MY_API_URL ?? 'http://localhost:3000';
export default defineDevServerConfig(() => ({
api: {
routes: [
{
match: '/my-api/*path',
proxy: {
target: MY_API_TARGET,
rewrite: (path) => path.replace(/^\/my-api/, ''),
changeOrigin: true,
secure: false,
},
},
],
},
}));
```
**What happens:**
1. Request to `/my-api/v1/example` matches `/my-api/*path`
2. Proxy strips the `/my-api` prefix
3. Forwards to `http://localhost:3000/v1/example`
### Optional: named HTTP client
No app config is required — `fetch('/my-api/...')` works directly. If you want a named client:
```typescript
// src/config.ts
export const configure: AppModuleInitiator = async (configurator) => {
configurator.useHttpClient('my-api', {
baseUri: '/my-api',
});
};
```
Then use `useHttpClient('my-api')` instead of raw `fetch()`.
## Proxy with service discovery (authenticated)
When the backend should appear in Fusion service discovery (so the HTTP module acquires tokens automatically), use `processServices`:
```typescript
// dev-server.config.ts
import { defineDevServerConfig, processServices } from '@equinor/fusion-framework-cli/dev-server';
const MY_API_KEY = 'my-api';
const MY_API_TARGET = process.env.MY_API_URL ?? 'https://api.example.com';
const MY_API_SCOPES = ['api://my-api/.default'];
const MY_API_MATCH = `/${MY_API_KEY}/*path`;
export default defineDevServerConfig(() => ({
api: {
processServices: (data, args) => {
// Keep all existing service discovery proxies
const existing = processServices(data, args);
// Build a URI that points back to the local dev server
const referer = args.request.headers.referer ?? 'http://localhost';
const localUri = new URL(`${args.route}/${MY_API_KEY}`, referer).href;
return {
data: [
...existing.data.filter((svc) => svc.key !== MY_API_KEY),
{
key: MY_API_KEY,
name: 'My API (local dev proxy)',
uri: localUri,
scopes: MY_API_SCOPES,
defaultScopes: MY_API_SCOPES,
},
],
routes: [
...(existing.routes ?? []),
{
match: MY_API_MATCH,
proxy: {
target: MY_API_TARGET,
rewrite: (path) => path.replace(`/${MY_API_KEY}`, ''),
changeOrigin: true,
secure: false,
},
},
],
};
},
},
}));
```
### App setup
Register the discovered service once:
```typescript
// src/config.ts
export const configure: AppModuleInitiator = async (configurator) => {
await configurator.useFrameworkServiceClient('my-api');
};
```
Then in components:
```typescript
import { useHttpClient } from '@equinor/fusion-framework-react-app/http';
const client = useHttpClient('my-api');
// The HTTP module acquires a bearer token for the configured scopes
// and the dev-server proxy forwards the authenticated request upstream
```
### Request flow
```
App startup
→ configurator.useFrameworkServiceClient('my-api')
→ service discovery resolves my-api (injected by dev-server)
Component code
→ useHttpClient('my-api')
→ HTTP module acquires token for api://my-api/.default
→ GET {local proxy uri}/v1/example with Authorization header
→ dev-server proxy matches /my-api/*path
→ proxy strips prefix, forwards to https://api.example.com/v1/example
→ upstream API receives the bearer token
```
## Choosing a strategy
| Strategy | Auth? | Service discovery? | Use when |
|---|---|---|---|
| Mock routes | No | No | No backend yet, need static/fake data |
| Simple proxy | Manual | No | Local backend (Docker), auth optional |
| Service discovery proxy | Automatic | Yes | Backend needs bearer tokens via MSAL |
## Configuration format
`dev-server.config.ts` supports two export styles:
```typescript
// Object export (simple)
export default {
api: { routes: [...] },
};
// Function export (conditional logic, access to base config)
import { defineDevServerConfig } from '@equinor/fusion-framework-cli/dev-server';
export default defineDevServerConfig(({ base }) => ({
...base,
api: { routes: [...] },
}));
```
Routes with identical `match` paths are replaced (yours wins). Other arrays are deduplicated automatically.
references/configure-services.md
# Configure Services (HTTP Clients)
How to register and consume HTTP clients in a Fusion Framework app.
## Registration strategies
There are three ways to register a named HTTP client. Choose the simplest one that fits:
| Strategy | Where | When to use |
|---|---|---|
| `app.config.ts` endpoints | `app.config.ts` | Default — auto-registered, no code in `config.ts` |
| `configureHttpClient` | `config.ts` | Custom transport behavior (headers, guards, custom client class) |
| Service discovery | `config.ts` | Framework-managed Fusion platform services |
### Via `app.config.ts` (auto-registration)
Endpoints defined here are **automatically registered** as named HTTP clients — no extra code in `config.ts`:
```typescript
import { defineAppConfig } from '@equinor/fusion-framework-cli/app';
export default defineAppConfig((_env, _args) => ({
environment: {},
endpoints: {
myApi: {
url: 'https://api.example.com',
scopes: ['api://my-api/.default'],
},
},
}));
```
After initialization, use the client directly:
```typescript
const client = useHttpClient('myApi');
const data = await client.json('/items');
```
### Via `configureHttpClient` in `config.ts`
Use when the endpoint needs custom transport behavior or is not in `app.config.ts`:
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
export const configure: AppModuleInitiator = (configurator, env) => {
configurator.configureHttpClient('my-api', {
baseUri: 'https://api.example.com',
defaultScopes: ['api://my-api/.default'],
onCreate: (client) => {
client.requestHandler.setHeader('X-App-Name', 'my-app');
},
});
};
```
### Multiple clients in one step
Access `configurator.http` directly when registering several backends in one block:
```typescript
configurator.http.configureClient('catalog', {
baseUri: '/api/catalog',
});
configurator.http.configureClient('search', {
baseUri: '/api/search',
});
```
### Custom client class
Extend `HttpClient` for domain-specific methods:
```typescript
import { HttpClient } from '@equinor/fusion-framework-module-http/client';
class ApiClient extends HttpClient {
getHealth(): Promise<{ status: string }> {
return this.json('/health');
}
}
configurator.configureHttpClient('api', {
baseUri: '/api',
ctor: ApiClient,
});
// Usage:
const client = framework.modules.http.createCustomClient<ApiClient>('api');
const health = await client.getHealth();
```
### Via service discovery
For Fusion platform services:
```typescript
configurator.useFrameworkServiceClient('people');
```
## Consuming HTTP clients
In React components and hooks, prefer `@equinor/fusion-framework-react-app/*` hooks over direct module access. Reserve `useAppModule` and `framework.modules.*` for non-React contexts (route loaders, scripts).
### Preferred: `useHttpClient` hook
```typescript
import { useHttpClient } from '@equinor/fusion-framework-react-app/http';
const MyComponent = () => {
const client = useHttpClient('my-api');
// client.json('/endpoint') — returns parsed JSON (Promise)
// client.fetchAsync('/endpoint') — returns raw Response (Promise)
// client.fetch$('/endpoint') — returns an Observable
};
```
### Fallback: `useAppModule`
Use only when a dedicated react-app hook is not available:
```typescript
import { useAppModule } from '@equinor/fusion-framework-react-app';
const http = useAppModule('http');
const client = http.createClient('my-api');
```
### Ad-hoc clients
For one-off calls without a named configuration:
```typescript
// In config.ts, inside your Fusion configuration callback where `framework` is provided:
const inlineClient = framework.modules.http.createClient({
baseUri: '/api/search',
});
// Or with an absolute URL:
const urlClient = framework.modules.http.createClient('https://api.example.com');
```
references/create-fusion-app.md
# Create a Fusion App
Bootstrapping and configuring Fusion Framework apps at entry-point level.
## File structure
Every Fusion app has these top-level configuration files:
```
src/index.ts → renderApp() — mounts the app into the Fusion Portal
src/config.ts → AppModuleInitiator — configures Fusion modules before render
src/App.tsx → Root React component
app.config.ts → Runtime config (endpoints, environment)
app.manifest.ts → Portal metadata (appKey, displayName)
```
## Entry point (`src/index.ts`)
Modern pattern:
```typescript
import { renderApp } from '@equinor/fusion-framework-react-app';
import { App } from './App';
import { configure } from './config';
export const render = renderApp(App, configure);
export default render;
```
Older codebases use `makeComponent` + `createRoot`. Check project's `index.ts` and follow its convention.
## Module configuration (`src/config.ts`)
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
export const configure: AppModuleInitiator = (configurator, { env }) => {
// Module configuration is added here as features are built
};
export default configure;
```
See `configure-services.md` for HTTP client setup and `using-framework-modules.md` for other modules.
## App manifest (`app.manifest.ts`)
Metadata for Fusion Portal:
```typescript
import { defineAppManifest } from '@equinor/fusion-framework-cli/app';
export default defineAppManifest(async (_env, { base }) => ({
...base,
appKey: 'my-app',
displayName: 'My Application',
}));
```
## Runtime config (`app.config.ts`)
Environment variables and endpoints, resolved at dev-server startup or build time:
```typescript
import { defineAppConfig } from '@equinor/fusion-framework-cli/app';
export default defineAppConfig((_env, _args) => ({
environment: {},
endpoints: {},
}));
```
See `configure-services.md` for adding endpoints.
## CLI commands
The Fusion Framework CLI (`@equinor/fusion-framework-cli`) handles development and builds:
| Command | Purpose |
|---|---|
| `fusion-framework-cli app dev` | Start dev server with HMR |
| `fusion-framework-cli app build` | Production build |
| `fusion-framework-cli app pack` | Bundle into a zip archive |
| `fusion-framework-cli app publish` | Upload to Fusion app service |
| `fusion-framework-cli app create <name>` | Scaffold a new app from template |
Projects typically alias these in `package.json` scripts (e.g. `dev`, `build`). Check the project's `package.json` for the exact commands available.
references/project-structure.md
# Project Structure
Directory layout, layer responsibilities, file placement, tooling, and commit conventions for Fusion Framework apps.
## Directory structure
Typical Fusion app layout:
```
src/
├── App.tsx # Root component — layout shell, routing
├── config.ts # Fusion module configuration
├── index.ts # Entry point (do not modify)
├── components/ # React components (presentation layer)
├── hooks/ # Custom React hooks
├── api/ # API clients, data transforms, business logic
└── types/ # TypeScript interfaces, type aliases, enums
```
Projects may add directories (e.g. `pages/`, `utils/`, `context/`). Check existing structure first.
## Layer responsibilities
| Layer | Purpose | Example files |
|--------------|--------------------------------------------|------------------------------|
| `components` | Presentation: render UI, handle user events| `DataGrid.tsx`, `Header.tsx` |
| `hooks` | State & side effects: encapsulate logic | `useItems.ts`, `useFilter.ts`|
| `api` | API clients, data transforms, business logic | `itemApi.ts` |
| `types` | Type definitions: shared interfaces/enums | `item.ts`, `api.ts` |
## File placement rules
- One primary export per file (component, hook, service, or type module).
- Co-locate related files: a page component and its dedicated hook can share a directory.
- Barrel exports (`index.ts`) are optional — use when a directory has 3+ exports.
## Tooling
Check the project's `package.json` scripts and installed devDependencies for exact commands. Common Fusion app tooling:
| Tool | Typical command | Purpose |
|---------------|----------------------------|------------------------------|
| TypeScript | `typecheck` / `tsc --noEmit` | Type checking (strict mode)|
| Biome | `biome check src/` | Lint + format validation |
| ESLint | `eslint src/` | Lint (if project uses it) |
| Dev server | `dev` | Start Fusion CLI dev server |
| Build | `build` | Production build |
Run commands via the project's package manager (`bun run`, `pnpm`, `npm run`).
## Commit conventions
Use conventional commits for all changes:
| Prefix | When to use |
|-------------|----------------------------------------------|
| `feat:` | New feature or capability |
| `fix:` | Bug fix |
| `refactor:` | Code restructuring without behavior change |
| `docs:` | Documentation only |
| `style:` | Formatting, whitespace, missing semicolons |
| `test:` | Adding or fixing tests |
| `chore:` | Build, tooling, dependency updates |
## Architecture Decision Records
Check for ADRs in `docs/adr/`. Follow them. Propose new ADRs for architectural decisions not already covered.
references/styled-components.md
# styled-components Patterns
Using styled-components for custom styling in Fusion Framework apps.
## The `Styled` object pattern
Co-locate styled components in a `Styled` namespace object at top of file:
```typescript
import { styled } from 'styled-components';
import { Typography } from '@equinor/eds-core-react';
const Styled = {
Root: styled.section`
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
`,
Title: styled(Typography)`
color: var(--eds-color-text-neutral-strong);
`,
};
```
Then use in the component:
```typescript
const MyComponent = () => (
<Styled.Root>
<Styled.Title variant="h2">Hello</Styled.Title>
</Styled.Root>
);
```
**Why this pattern:**
- Groups all styled elements visually at the top
- Clear namespace avoids collision with imported components
- Easy to scan which elements have custom styling
## Using EDS tokens in styled-components
### CSS custom properties (preferred)
```typescript
const Styled = {
Card: styled.div`
background: var(--eds-color-bg-neutral-surface);
border-radius: var(--eds-shape-corners-border-radius);
padding: var(--eds-spacing-comfortable-medium);
`,
};
```
### JS token imports
```typescript
import { tokens } from '@equinor/eds-tokens';
const Styled = {
Card: styled.div`
background: ${tokens.colors.ui.background__default.rgba};
border-radius: ${tokens.shape.corners.borderRadius};
padding: ${tokens.spacings.comfortable.medium};
box-shadow: ${tokens.elevation.raised};
`,
};
```
## Extending EDS components
Wrap EDS components with `styled()` for layout or theme tweaks:
```typescript
import { Button, Dialog } from '@equinor/eds-core-react';
const Styled = {
WideButton: styled(Button)`
min-width: 200px;
`,
FullWidthDialog: styled(Dialog)`
width: 90vw;
max-width: 800px;
`,
};
```
## Responsive design
Use standard CSS media queries inside styled-components:
```typescript
const Styled = {
Grid: styled.div`
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1200px) {
grid-template-columns: repeat(3, 1fr);
}
`,
};
```
## What NOT to do
- **No CSS Modules** (`.module.css` files) unless the project explicitly uses them
- **No global CSS files** (`.css` imports) for component styling
- **No Tailwind or utility-class libraries** unless the project adopts them
- **No Emotion, Stitches, or other CSS-in-JS alternatives** in Fusion apps
- **No inline styles for reusable patterns** — extract to `Styled` object instead
- **No hardcoded colors or spacing** — use EDS tokens (see `styling-with-eds.md`)
references/styling-with-eds.md
# Styling with EDS
Using EDS components and design tokens in Fusion Framework apps.
## Packages
| Package | Purpose |
|---|---|
| `@equinor/eds-core-react` | Core UI components (Button, Dialog, Typography, etc.) |
| `@equinor/eds-icons` | SVG icon data objects |
| `@equinor/eds-tokens` | Design tokens — colors, spacing, typography, elevation |
| `@equinor/eds-data-grid-react` | EDS-themed AG Grid wrapper (see `using-ag-grid.md`) |
| `@equinor/fusion-react-*` | Fusion-specific React components not in EDS (see `using-fusion-react-components.md`) |
## Component catalog
Frequently used components from `@equinor/eds-core-react`:
| Component | Use for |
|---------------|-------------------------------------|
| `Typography` | All text (headings, body, labels) |
| `Button` | Actions and CTAs |
| `Dialog` | Modal dialogs |
| `Table` | Simple non-interactive tables |
| `Tabs` | Tab navigation |
| `Chip` | Tags, filters, status badges |
| `Card` | Content containers |
| `Search` | Search input fields |
| `Autocomplete`| Filtered dropdown selection |
| `Icon` | Icons from `@equinor/eds-icons` |
| `Progress` | Loading indicators (`.Dots`, `.Circular`) |
| `Banner` | Informational banners |
| `Snackbar` | Toast notifications |
| `Menu` | Dropdown menus |
| `Tooltip` | Hover information |
| `Checkbox` | Toggle inputs |
| `Radio` | Single selection from a group |
| `TextField` | Text input fields |
| `Switch` | On/off toggle |
| `EdsProvider` | Density switching (compact/comfortable) |
Check `@equinor/eds-core-react` before building custom UI elements.
## Design tokens
Use EDS tokens for all visual values — never hardcode colors, spacing, or typography.
### CSS custom properties (preferred)
When EDS theme is active (Fusion Portal provides this):
```css
background: var(--eds-color-bg-neutral-surface);
color: var(--eds-color-text-neutral-strong);
border: 1px solid var(--eds-color-border-neutral-subtle);
padding: var(--eds-spacing-comfortable-medium);
```
Common token categories:
- `--eds-color-bg-*` — background colors (neutral, accent, success, info, warning, danger)
- `--eds-color-text-*` — text colors
- `--eds-color-border-*` — border colors
- `--eds-spacing-*` — spacing values
### JS token imports
When CSS custom properties are not available:
```typescript
import { tokens } from '@equinor/eds-tokens';
tokens.colors.ui.background__default.rgba // background color
tokens.shape.corners.borderRadius // border radius
tokens.spacings.comfortable.medium // spacing
tokens.elevation.raised // box shadow
```
## Icons
Use `@equinor/eds-icons` for icon data and the `Icon` component to render:
```typescript
import { Icon } from '@equinor/eds-core-react';
import { edit, save, delete_to_trash, search, close } from '@equinor/eds-icons';
<Icon data={edit} title="Edit" />
<Icon data={save} title="Save" />
```
Import names use `snake_case`. Browse available icons at [eds.equinor.com](https://eds.equinor.com).
## Density
Toggle density:
```typescript
import { EdsProvider } from '@equinor/eds-core-react';
<EdsProvider density="compact">
{/* All child EDS components render in compact mode */}
</EdsProvider>
```
## Accessibility
- EDS components handle most accessibility automatically.
- Use `aria-disabled` instead of `disabled` on buttons that need tooltip support.
- Always provide `title` on `<Icon>` for screen readers, or `aria-label` on icon buttons.
- Color must not be the sole way to convey information.
references/using-ag-charts.md
# Using AG Charts
Rendering standalone charts with `@equinor/fusion-framework-react-ag-charts`.
## Quick start
### Install
```sh
# use the project's package manager (bun / pnpm / npm)
bun add @equinor/fusion-framework-react-ag-charts
```
### Register modules
Register chart modules **once** before rendering any chart:
```ts
import {
AllCommunityModule,
ModuleRegistry,
} from '@equinor/fusion-framework-react-ag-charts/community';
ModuleRegistry.registerModules([AllCommunityModule]);
```
Place in app entry point or top-level initializer. Charts fail silently without module registration.
### Render a chart
```tsx
import { useState } from 'react';
import { AgCharts } from '@equinor/fusion-framework-react-ag-charts';
import type { AgChartOptions } from '@equinor/fusion-framework-react-ag-charts/community';
const SalesChart = () => {
const [chartOptions] = useState<AgChartOptions>({
data: [
{ month: 'Jan', avgTemp: 2.3, iceCreamSales: 162000 },
{ month: 'Mar', avgTemp: 6.3, iceCreamSales: 302000 },
{ month: 'May', avgTemp: 16.2, iceCreamSales: 800000 },
{ month: 'Jul', avgTemp: 22.8, iceCreamSales: 1254000 },
{ month: 'Sep', avgTemp: 14.5, iceCreamSales: 950000 },
{ month: 'Nov', avgTemp: 8.9, iceCreamSales: 200000 },
],
series: [{ type: 'bar', xKey: 'month', yKey: 'iceCreamSales' }],
});
return <AgCharts options={chartOptions} />;
};
```
## Packages and entry points
| Sub-path | What it provides | Upstream package |
|---|---|---|
| `@equinor/fusion-framework-react-ag-charts` | `AgCharts` React component and React-level utilities | `ag-charts-react` |
| `@equinor/fusion-framework-react-ag-charts/community` | `AllCommunityModule`, `ModuleRegistry`, `AgChartOptions`, `AgChartTheme` | `ag-charts-community` |
| `@equinor/fusion-framework-react-ag-charts/enterprise` | `AgChartsEnterpriseModule` and enterprise-only chart types | `ag-charts-enterprise` |
Always import from the Fusion wrapper, not from `ag-charts-*` directly — wrapper ensures a single centrally managed version.
## Multi-series chart
```tsx
const chartOptions: AgChartOptions = {
data: [
{ quarter: 'Q1', revenue: 45000, expenses: 30000 },
{ quarter: 'Q2', revenue: 52000, expenses: 35000 },
{ quarter: 'Q3', revenue: 61000, expenses: 38000 },
{ quarter: 'Q4', revenue: 58000, expenses: 36000 },
],
series: [
{ type: 'bar', xKey: 'quarter', yKey: 'revenue', yName: 'Revenue' },
{ type: 'bar', xKey: 'quarter', yKey: 'expenses', yName: 'Expenses' },
],
};
```
## Common chart types
### Line chart
```tsx
series: [
{ type: 'line', xKey: 'month', yKey: 'temperature', yName: 'Avg Temperature' },
]
```
### Area chart
```tsx
series: [
{ type: 'area', xKey: 'month', yKey: 'revenue', yName: 'Revenue' },
]
```
### Pie chart
```tsx
series: [
{ type: 'pie', angleKey: 'share', legendItemKey: 'segment' },
]
```
## Enterprise chart types
Requires a valid AG Charts license. Import from `/enterprise`:
```ts
import { AgChartsEnterpriseModule } from '@equinor/fusion-framework-react-ag-charts/enterprise';
```
Enterprise-only types include: waterfall, heatmap, sunburst, treemap, stock charts, and maps.
## Reactive chart updates
Use `useState` for chart options that change with user interaction or data updates:
```tsx
const [chartOptions, setChartOptions] = useState<AgChartOptions>({
data: initialData,
series: [{ type: 'bar', xKey: 'category', yKey: 'value' }],
});
// Update data reactively
const handleDataUpdate = (newData: ChartDataItem[]) => {
setChartOptions((prev) => ({ ...prev, data: newData }));
};
```
## Data fetching pattern
Separate data fetching from chart rendering:
```tsx
import { useHttpClient } from '@equinor/fusion-framework-react-app/http';
import { useQuery } from '@tanstack/react-query';
interface MetricItem {
period: string;
value: number;
}
const useMetrics = () => {
const httpClient = useHttpClient('my-api');
return useQuery({
queryKey: ['metrics'],
queryFn: () => httpClient.json<MetricItem[]>('/api/metrics'),
});
};
const MetricsChart = () => {
const { data, isLoading } = useMetrics();
const [chartOptions] = useState<AgChartOptions>({
data: data ?? [],
series: [{ type: 'bar', xKey: 'period', yKey: 'value' }],
});
if (isLoading) return <Progress.Linear />;
return <AgCharts options={chartOptions} />;
};
```
## Responsive sizing
AG Charts sizes to its container. Wrap in a sized element:
```tsx
<div style={{ width: '100%', height: 400 }}>
<AgCharts options={chartOptions} />
</div>
```
Or with styled-components:
```tsx
const Styled = {
ChartContainer: styled.div`
width: 100%;
height: 400px;
`,
};
<Styled.ChartContainer>
<AgCharts options={chartOptions} />
</Styled.ChartContainer>
```
## Key concepts
- **Module registration** — AG Charts uses a modular architecture. Register modules via `ModuleRegistry.registerModules()` before any chart renders. `AllCommunityModule` is the easiest starting point.
- **Chart options** — Charts are configured declaratively through an `AgChartOptions` object describing data, series types, axes, legends, and themes.
- **Thin wrapper** — `@equinor/fusion-framework-react-ag-charts` re-exports upstream AG Charts packages so Fusion apps share a single centrally managed version.
## Sources
- [@equinor/fusion-framework-react-ag-charts README](https://github.com/equinor/fusion-framework/blob/main/packages/react/ag-charts/README.md)
- [Charts Cookbook](https://github.com/equinor/fusion-framework/blob/main/cookbooks/app-react-charts/README.md)
- [AG Charts upstream docs](https://www.ag-grid.com/charts/)
references/using-ag-grid-charts.md
# Using AG Grid Integrated Charts
How to enable chart creation from AG Grid data in a Fusion Framework app, using the AG Grid `IntegratedChartsModule` with AG Charts enterprise.
## Prerequisites
- `@equinor/fusion-framework-react-ag-grid` installed
- `@equinor/fusion-framework-react-ag-charts` installed
- A valid AG Charts Enterprise license (integrated charts require enterprise)
## Enable integrated charts
Add `IntegratedChartsModule.with(AgChartsEnterpriseModule)` to the AG Grid module list in `config.ts`:
```ts
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import { enableAgGrid } from '@equinor/fusion-framework-react-ag-grid';
import {
AllCommunityModule,
ClientSideRowModelModule,
} from '@equinor/fusion-framework-react-ag-grid/community';
import {
IntegratedChartsModule,
ColumnsToolPanelModule,
FiltersToolPanelModule,
MenuModule,
} from '@equinor/fusion-framework-react-ag-grid/enterprise';
import { AgChartsEnterpriseModule } from '@equinor/fusion-framework-react-ag-charts/enterprise';
export const configure: AppModuleInitiator = (configurator) => {
enableAgGrid(configurator, (builder) => {
builder.setModules([
AllCommunityModule,
ClientSideRowModelModule,
ColumnsToolPanelModule,
FiltersToolPanelModule,
MenuModule,
IntegratedChartsModule.with(AgChartsEnterpriseModule),
]);
});
};
```
## How users create charts from the grid
Once integrated charts are enabled, users can:
1. Select one or more columns in the grid.
2. Right-click on the selection.
3. Choose "Chart Range" from the context menu.
4. Select a chart type (bar, line, pie, area, etc.).
5. Customize using the chart toolbar.
No additional chart component code is needed — AG Grid renders the chart in a popup or docked panel.
## Enable chart menu item
Ensure `enableCharts: true` is set on the grid (this is the default when `IntegratedChartsModule` is registered):
```tsx
<AgGridReact
theme={theme}
rowData={rows}
columnDefs={columns}
enableCharts={true}
enableRangeSelection={true}
/>
```
`enableRangeSelection` allows users to select cell ranges, which is the typical trigger for chart creation.
## Programmatic chart creation
Create charts from code using the grid API:
```tsx
const gridRef = useRef<AgGridReact>(null);
const createChart = () => {
gridRef.current?.api.createRangeChart({
chartType: 'groupedBar',
cellRange: {
columns: ['category', 'value'],
},
});
};
```
## When to use integrated charts vs standalone AG Charts
| Scenario | Approach |
|---|---|
| Users explore grid data visually on demand | Integrated charts |
| Fixed chart layout on a dashboard page | Standalone AG Charts |
| Chart data comes from a different source than the grid | Standalone AG Charts |
| Both grid and chart show the same dataset | Integrated charts |
## Sources
- [AG Grid Cookbook](https://github.com/equinor/fusion-framework/blob/main/cookbooks/app-react-ag-grid/README.md)
- [@equinor/fusion-framework-react-ag-charts README](https://github.com/equinor/fusion-framework/blob/main/packages/react/ag-charts/README.md)
- [AG Grid Charts Integration docs](https://www.ag-grid.com/react-data-grid/integrated-charts/)
references/using-ag-grid.md
# Using AG Grid
Displaying tabular data with AG Grid in Fusion Framework apps using `@equinor/fusion-framework-react-ag-grid`.
## Quick start
### Install
```sh
# use the project's package manager (bun / pnpm / npm)
bun add @equinor/fusion-framework-react-ag-grid
```
### Enable the module
Register in `src/config.ts`:
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import { enableAgGrid } from '@equinor/fusion-framework-react-ag-grid';
export const configure: AppModuleInitiator = (configurator) => {
enableAgGrid(configurator);
};
```
### Render a grid
```typescript
import { AgGridReact } from '@equinor/fusion-framework-react-ag-grid';
import { useTheme } from '@equinor/fusion-framework-react-ag-grid';
import type { ColDef } from '@equinor/fusion-framework-react-ag-grid/community';
interface Item {
id: string;
name: string;
status: string;
}
const columnDefs: ColDef<Item>[] = [
{ field: 'name', headerName: 'Name', flex: 2 },
{ field: 'status', headerName: 'Status', width: 120 },
];
/**
* Renders a data grid with Fusion/EDS theming.
*
* @param props.items - The list of items to display.
* @returns A themed AG Grid component.
*/
const ItemGrid = ({ items }: { items: Item[] }) => {
const theme = useTheme();
return (
<AgGridReact
theme={theme}
rowData={items}
columnDefs={columnDefs}
/>
);
};
```
## Registering feature modules
Since AG Grid 33, explicitly register feature modules for tree-shaking.
Use `builder.setModules()` inside the `enableAgGrid` callback:
```typescript
import { enableAgGrid } from '@equinor/fusion-framework-react-ag-grid';
import {
ClientSideRowModelModule,
ValidationModule,
} from '@equinor/fusion-framework-react-ag-grid/community';
export const configure: AppModuleInitiator = (configurator) => {
enableAgGrid(configurator, (builder) => {
builder.setModules([
ClientSideRowModelModule,
ValidationModule,
]);
});
};
```
With Enterprise license:
```typescript
import {
ClipboardModule,
ColumnsToolPanelModule,
ExcelExportModule,
FiltersToolPanelModule,
MenuModule,
} from '@equinor/fusion-framework-react-ag-grid/enterprise';
builder.setModules([
ClientSideRowModelModule,
ClipboardModule,
ColumnsToolPanelModule,
ExcelExportModule,
FiltersToolPanelModule,
MenuModule,
]);
```
Only import what you need — unregistered modules are tree-shaken out.
## Theming
The module provides `fusionTheme` by default — Equinor-branded theme based on AG Grid Alpine with EDS accent colors.
### Using the default theme
```typescript
import { useTheme } from '@equinor/fusion-framework-react-ag-grid';
const MyGrid = ({ rows }) => {
const theme = useTheme();
return <AgGridReact theme={theme} rowData={rows} columnDefs={columns} />;
};
```
### Customizing the theme
```typescript
enableAgGrid(configurator, (builder) => {
builder.setTheme((theme) => {
return theme.withParams({
backgroundColor: '#1f2836',
browserColorScheme: 'dark',
foregroundColor: '#FFF',
headerFontSize: 14,
});
});
});
```
### Per-instance customization
```typescript
import { useTheme } from '@equinor/fusion-framework-react-ag-grid';
const MyGrid = () => {
const baseTheme = useTheme();
const theme = useMemo(
() => baseTheme.withParams({ cellTextColor: '#FF0000' }),
[baseTheme],
);
return <AgGridReact theme={theme} rowData={rows} columnDefs={columns} />;
};
```
## Default column definitions
Shared column defaults to reduce repetition:
```typescript
const defaultColDef: ColDef = {
resizable: true,
filter: true,
flex: 1,
minWidth: 100,
sortable: true,
};
<AgGridReact
theme={theme}
rowData={rows}
columnDefs={columns}
defaultColDef={defaultColDef}
/>
```
## Column definition patterns
```typescript
const columnDefs: ColDef<Item>[] = [
// Flex sizing — fills available space proportionally
{ field: 'name', headerName: 'Name', flex: 2 },
// Fixed width
{ field: 'status', headerName: 'Status', width: 120 },
// Value formatter
{
field: 'createdAt',
headerName: 'Created',
valueFormatter: ({ value }) => new Date(value).toLocaleDateString(),
},
// Cell renderer for custom content
{
field: 'actions',
headerName: '',
cellRenderer: ({ data }) => <Button variant="ghost">Edit</Button>,
width: 80,
sortable: false,
filter: false,
},
];
```
## When to use AG Grid vs EDS Table
| Use case | Component |
|---|---|
| Sorting, filtering, resizable/reorderable columns | `AgGridReact` via fusion-react-ag-grid |
| Simple read-only table, few rows | `Table` from `@equinor/eds-core-react` |
| Key-value display | `Table` or definition list |
## Packages and entry points
| Import path | Purpose |
|---|---|
| `@equinor/fusion-framework-react-ag-grid` | Main: `AgGridReact`, `enableAgGrid`, `useTheme`, `fusionTheme` |
| `@equinor/fusion-framework-react-ag-grid/community` | Re-exports from `ag-grid-community` (ColDef, events, etc.) |
| `@equinor/fusion-framework-react-ag-grid/enterprise` | Re-exports from `ag-grid-enterprise` (requires license) |
| `@equinor/fusion-framework-react-ag-grid/themes` | `fusionTheme`, `createTheme`, `createThemeFromTheme` |
## TypeScript note
Set `"moduleResolution": "bundler"` in `tsconfig.json` to resolve AG Grid types correctly from this package.
references/using-analytics.md
# Using Analytics
Instrumenting Fusion apps with Fusion Framework analytics.
## What the framework provides
Common tracking API; platform handles collection. Portal already includes analytics support — most app work starts with the hook.
## Basic tracking
```typescript
import { useEffect } from 'react';
import { useTrackFeature } from '@equinor/fusion-framework-react-app/analytics';
export const DashboardPage = () => {
const trackFeature = useTrackFeature();
useEffect(() => {
trackFeature('dashboard-page-loaded');
}, [trackFeature]);
return null;
};
```
## Track user actions and screen loads
```typescript
import { useEffect } from 'react';
import { useTrackFeature } from '@equinor/fusion-framework-react-app/analytics';
export const DashboardPage = () => {
const trackFeature = useTrackFeature();
useEffect(() => {
trackFeature('dashboard-page-loaded');
}, [trackFeature]);
return (
<button type="button" onClick={() => trackFeature('dashboard-filter-opened')}>
Open filters
</button>
);
};
```
## Send small structured metadata
```typescript
trackFeature('dashboard-filter-applied', {
filterCount: 3,
section: 'overview',
});
```
Use small, intentional metadata. Never include secrets, tokens, or personal data.
Consistent event names: `kebab-case` (e.g. `dashboard-page-loaded`, `dashboard-filter-opened`, `dashboard-filter-applied`).
## When to instrument
Good analytics events usually capture:
- page or feature entry points
- meaningful user actions such as apply, create, approve, or export
- important workflow milestones or failures
- rollout observation for newly introduced features
Avoid tracking every render or low-value UI noise.
## Local testing
To inspect analytics locally:
1. Enable the `Log Fusion Analytics` feature in the portal profile.
2. Refresh the page.
3. Inspect console output prefixed with `Analytics::Adapter::Console`.
## Advanced configuration
Most apps rely on portal's existing analytics setup. Custom collectors/adapters: configure in app config and check analytics module docs.
## Review guidance
- Prefer `useTrackFeature()` over ad hoc analytics wrappers unless the project already standardizes one.
- Reuse stable event names when extending an existing workflow.
- Keep one event naming scheme across page-load and interaction events.
- Track business-significant interactions, not implementation details.
## Relevant sources
- Fusion docs analytics guide
- analytics module documentation
references/using-assets-and-environment.md
# Using Assets and Runtime Configuration
Separating bundled assets from runtime configuration in Fusion Framework apps.
## Put runtime values in app config
Use `app.config.ts` for:
- environment-specific flags and numeric or string settings
- API endpoints and scopes
- values that may differ between `ci`, `test`, `prod`, or PR environments
```typescript
import { defineAppConfig } from '@equinor/fusion-framework-cli/app';
export default defineAppConfig((env, _args) => ({
environment: {
logLevel: env.environment === 'ci' ? 0 : 4,
environmentName: env.environment,
},
endpoints: {
myApi: {
url: 'https://api.example.com',
scopes: ['api://my-api/.default'],
},
},
}));
```
Environment-specific files such as `app.config.prod.ts` override `app.config.ts` when present.
## Use runtime config in `config.ts`
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
interface AppEnvironment {
logLevel: number;
}
export const configure: AppModuleInitiator = (configurator, env) => {
configurator.configureHttpClient('myApi', {
baseUri: env.config?.getEndpoint('myApi')?.url,
defaultScopes: env.config?.getEndpoint('myApi')?.scopes,
});
const logLevel = (env.config?.environment as AppEnvironment)?.logLevel ?? 4;
configurator.logger.level = logLevel;
};
```
## Use runtime config in React components
```typescript
import { useAppEnvironmentVariables } from '@equinor/fusion-framework-react-app';
export const EnvironmentDisplay = () => {
const { value, complete, error } = useAppEnvironmentVariables();
if (!complete) {
return <p>Loading runtime config...</p>;
}
if (error) {
return <p>Unable to load runtime config</p>;
}
return <pre>{JSON.stringify(value, null, 2)}</pre>;
};
```
## Endpoints and service discovery
Use `endpoints` in `app.config.ts` for explicit URLs or scopes.
Defining an endpoint key overrides service discovery for the same key (useful for PR environments, but be deliberate).
## Static assets
Fusion serves application assets through the app lifecycle and App Proxy path after the app is deployed. Treat images, icons, and other static resources as bundled application assets, not runtime configuration.
Use the project's existing asset import pattern for:
- local images and illustrations
- icons or logos shipped with the app
- static JSON or other bundled resources
Don't put deployment-specific values in bundled assets. Use `app.config.ts` for per-environment selection instead of hardcoding environment logic in components.
## Choosing the right storage
| Concern | Use |
|---|---|
| API base URL, scopes, log level, environment name | `app.config.ts` |
| Per-user preference | App settings |
| Shareable view state | Bookmarks |
| Images, logos, bundled static files | Application assets in source |
## Pitfalls
- Hardcoded URLs that should come from `app.config.ts`
- Storing secrets in source-controlled config or asset files
- Using app settings for deployment config
- Using bookmarks for non-shareable runtime values
## Relevant sources
- Fusion docs app config guide
- Fusion docs app lifecycle guide
- environment variables cookbook
references/using-bookmarks.md
# Using Bookmarks
Saving and restoring shareable view state with Fusion Framework bookmarks.
## When bookmarks are the right fit
Use bookmarks for:
- filters, pagination, selected entities, and dashboard layout that users want to revisit
- links that should reproduce a complex screen state for another user
- support and debugging flows where you need to load the same view state as the reporter
Do not use bookmarks for:
- secrets, tokens, or personal-only preferences
- large cached datasets
- deployment config or API endpoint selection
## Enable the bookmark module
```typescript
import { enableBookmark } from '@equinor/fusion-framework-react-app/bookmark';
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
export const configure: AppModuleInitiator = (configurator) => {
enableBookmark(configurator);
};
```
## Read the current bookmark and restore state
```typescript
import { useCurrentBookmark } from '@equinor/fusion-framework-react-app/bookmark';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
interface FiltersBookmark {
query: string;
includeClosed: boolean;
}
export const FiltersPanel = () => {
const [filters, setFilters] = useState<FiltersBookmark>({
query: '',
includeClosed: false,
});
const latestFilters = useRef(filters);
const { currentBookmark } = useCurrentBookmark<FiltersBookmark>(
useCallback(() => latestFilters.current, []),
);
useLayoutEffect(() => {
setFilters({
query: currentBookmark?.payload?.query ?? '',
includeClosed: currentBookmark?.payload?.includeClosed ?? false,
});
}, [currentBookmark]);
useLayoutEffect(() => {
latestFilters.current = filters;
}, [filters]);
return (
<form>
<input
value={filters.query}
onChange={(event) =>
setFilters((current) => ({ ...current, query: event.target.value }))
}
placeholder="Search"
/>
<label>
<input
type="checkbox"
checked={filters.includeClosed}
onChange={(event) =>
setFilters((current) => ({
...current,
includeClosed: event.target.checked,
}))
}
/>
Include closed
</label>
</form>
);
};
```
The payload generator returns serializable state for the bookmark system to save and restore.
## Choosing bookmark payload
Good bookmark payloads:
- filters and sort order
- selected IDs or tabs
- current page or panel state
- other serializable view-model data another user can reproduce
Keep route identity in the router. Bookmarks are for page state layered on top of routes.
## Multi-page flows
For multi-page bookmark behavior:
- keep navigation source of truth in route params and route structure
- keep bookmark payload focused on the page or workflow state that needs restoring
- if several pages participate in one workflow, define a stable payload shape that each page can read defensively
- validate against the advanced bookmark cookbook before introducing cross-page assumptions
## Current API note
Fusion docs highlight `useCurrentBookmark` as the current hook. Older `useBookmark` usage exists in some codebases, but the bookmark rework notes mark it as deprecated. Prefer the `useCurrentBookmark`-based flow when adding new behavior.
## Relevant sources
- Fusion docs bookmark guide
- `cookbooks/app-react-bookmark`
- `cookbooks/app-react-bookmark-advanced`
- bookmark rework release notes
references/using-context.md
# Using Context
Enabling, configuring, and consuming the Fusion context module.
Context: the entity the user is working with — a project, facility, contract, or other domain object selected in Fusion Portal.
## Enable context in `config.ts`
Not enabled by default. Register with `enableContext`:
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import { enableContext } from '@equinor/fusion-framework-module-context';
export const configure: AppModuleInitiator = (configurator) => {
enableContext(configurator, (builder) => {
// Only accept ProjectMaster context items
builder.setContextType(['ProjectMaster']);
});
};
```
### Builder options
The `builder` callback exposes several configuration methods:
| Method | Purpose |
|---|---|
| `setContextType(types)` | Restrict which context types the app accepts |
| `setContextFilter(fn)` | Post-query filter on search results |
| `setContextParameterFn(fn)` | Custom parameter mapping for the search API |
| `setValidateContext(fn)` | Custom validation logic for context items |
| `setResolveContext(fn)` | Custom context resolution (e.g. resolve related items) |
| `connectParentContext(bool)` | Enable/disable sync with the portal's context (default: `true`) |
| `setContextClient(client)` | Replace the default context API with a custom `get`/`query` client |
| `setContextPathExtractor(fn)` | Extract context ID from URL path |
| `setContextPathGenerator(fn)` | Generate URL path from context item |
| `setResolveInitialContext(fn)` | Override initial context resolution on app load |
Minimal setup only needs `setContextType`. Add other methods as needed.
## Read the current context
Use `useCurrentContext` hook:
```typescript
import { useCurrentContext } from '@equinor/fusion-framework-react-app/context';
const MyComponent = () => {
const context = useCurrentContext();
if (!context) return <p>No context selected</p>;
return <p>Working on: {context.title}</p>;
};
```
`context` is `undefined` when no context is selected. Updates automatically on portal context change.
## Use context in data fetching
Common: pass context ID to API queries so data refreshes on context change.
### With React Query
```typescript
import { useQuery } from '@tanstack/react-query';
import { useHttpClient } from '@equinor/fusion-framework-react-app/http';
import { useCurrentContext } from '@equinor/fusion-framework-react-app/context';
/**
* Fetches items scoped to the current context.
*
* @returns A React Query result containing the item list, or disabled when no context is selected.
*/
export const useContextItems = () => {
const context = useCurrentContext();
const client = useHttpClient('my-api');
return useQuery({
queryKey: ['items', context?.id],
queryFn: () => client.json(`/projects/${context?.id}/items`),
enabled: !!context?.id,
});
};
```
### With plain HTTP client
```typescript
import { useEffect, useState } from 'react';
import { useHttpClient } from '@equinor/fusion-framework-react-app/http';
import { useCurrentContext } from '@equinor/fusion-framework-react-app/context';
const MyComponent = () => {
const context = useCurrentContext();
const client = useHttpClient('my-api');
const [items, setItems] = useState([]);
useEffect(() => {
if (!context?.id) return;
client.json(`/projects/${context.id}/items`).then(setItems);
}, [context?.id, client]);
return <ul>{items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
};
```
## Query related contexts
Access entities related to the current context (e.g. tasks under a project):
```typescript
import { useMemo } from 'react';
import { EMPTY } from 'rxjs';
import {
type ContextItem,
type ContextModule,
useModuleCurrentContext,
} from '@equinor/fusion-framework-react-module-context';
import { useAppModule } from '@equinor/fusion-framework-react-app';
import { useObservableState } from '@equinor/fusion-observable/react';
/**
* Queries contexts related to the current context.
*
* @param type - Optional context type filter (e.g. `['EquinorTask']`).
* @returns An observable state containing related context items.
*/
export const useRelatedContext = (type?: string[]) => {
const { currentContext } = useModuleCurrentContext();
const provider = useAppModule<ContextModule>('context');
return useObservableState(
useMemo(() => {
if (!currentContext) return EMPTY;
return provider.relatedContexts({
item: currentContext,
filter: { type },
});
}, [provider, currentContext, type]),
);
};
```
## Context types
Common context types in the Fusion ecosystem:
| Type | Represents |
|---|---|
| `ProjectMaster` | A project entity |
| `Facility` | A physical facility |
| `Contract` | A contract |
| `EquinorTask` | A task or work order |
The exact types available depend on the Fusion environment and service discovery configuration.
## Common patterns
### Guard a page that requires context
```typescript
import type { ReactNode } from 'react';
const ContextGuard = ({ children }: { children: ReactNode }) => {
const context = useCurrentContext();
if (!context) {
return <Banner variant="info">Select a project to continue</Banner>;
}
return <>{children}</>;
};
```
### Extract context ID for URL routing
The context module can extract a context ID from the URL path and sync it:
```typescript
enableContext(configurator, (builder) => {
builder.setContextType(['ProjectMaster']);
builder.setContextPathExtractor((path) => path.split('/')[2]);
builder.setContextPathGenerator((ctx, path) =>
path.replace(/\/context\/[^/]+/, `/context/${ctx.id}`),
);
});
```
## Custom context client (bring your own context)
When the app's context doesn't come from the standard Fusion context API — for example it uses a custom backend, a different data model, or needs to query a domain-specific service — replace the default client with `setContextClient`:
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import { enableContext } from '@equinor/fusion-framework-module-context';
export const configure: AppModuleInitiator = (configurator) => {
enableContext(configurator, (builder) => {
// Disable parent context sync — this app manages its own context
builder.connectParentContext(false);
// Provide a fully custom client that replaces the default Fusion context API
builder.setContextClient({
get: async (args) => {
const res = await fetch(`/api/my-context/${args.id}`);
return res.json();
},
query: async (args) => {
const res = await fetch(`/api/my-context?q=${args.search}`);
return res.json();
},
});
});
};
```
The custom client must satisfy the context client interface:
| Method | Signature | Purpose |
|---|---|---|
| `get` | `(args: { id: string }) => Promise<ContextItem>` | Fetch a single context item by ID |
| `query` | `(args: { search: string }) => Promise<ContextItem[]>` | Search for context items |
Once registered, `useCurrentContext()`, `setCurrentContextByIdAsync()`, and `queryContextAsync()` all use the custom client transparently — component code does not change.
### When to use a custom client
- The app uses a **domain-specific entity** (e.g. wells, assets, campaigns) as context instead of the standard Fusion types
- Context data lives in a **separate backend** not registered in Fusion service discovery
- The app needs **custom search logic** (full-text, OData filters, GraphQL) beyond what the Fusion context API supports
- The app must **not sync with the portal's context picker** (set `connectParentContext(false)`)
### Combining with standard context
If the app should still appear in the portal's context picker but needs extra data, keep `connectParentContext(true)` (default) and only override `get`/`query` to enrich the results:
```typescript
enableContext(configurator, (builder) => {
builder.setContextType(['ProjectMaster']);
// Enrich standard context with domain-specific data
builder.setContextClient({
get: async (args) => {
const [ctx, extra] = await Promise.all([
fetch(`/api/context/${args.id}`).then((r) => r.json()),
fetch(`/api/my-domain/${args.id}/metadata`).then((r) => r.json()),
]);
return { ...ctx, value: { ...ctx.value, ...extra } };
},
query: (args) =>
fetch(`/api/context?q=${args.search}`).then((r) => r.json()),
});
});
```
## What not to do
- **Don't bypass the HTTP client**: Using `fetch(service.uri + '/path')` with a discovered service URI skips token injection. Always use `useHttpClient(name)`.
- **Don't store context in local state**: The framework manages context state. Read it from `useCurrentContext()`, don't copy it into `useState`.
- **Don't hardcode context IDs**: Always derive them from `useCurrentContext()` or URL parameters.
references/using-custom-modules.md
# Authoring Custom Fusion Framework Modules
Reusable cross-cutting modules: lifecycle, configuration, usage.
**Fusion docs**: [Fusion Framework modules](https://docs.equinor.com/fusion-framework/)
**Cookbook**: Custom module cookbook in Fusion Framework source (`packages/modules/`)
## When to use a custom module
Use when:
- cross-cutting behavior needed by multiple components/routes
- depends on Fusion bootstrap lifecycle (needs framework instance at startup)
- decouple infrastructure (caching, subscriptions, service clients) from components
Not appropriate for:
- single-component state — use React state/context
- single-page data fetching — use React Query hook
- generic utilities — use plain TypeScript module
## Module contract
Implement `IModule` from `@equinor/fusion-framework-module`:
```typescript
import type { IModule } from '@equinor/fusion-framework-module';
export const MODULE_NAME = 'myModule' as const; // Unique module key — must not conflict with built-in modules
export interface IMyModuleConfig {
// Configuration shape for your module
apiBaseUrl: string;
}
export interface IMyModule {
// Public API your module exposes to the app
getClient(): MyApiClient;
}
export const module: IModule<IMyModule, IMyModuleConfig> = {
name: MODULE_NAME,
initialize: async ({ config, instance }): Promise<IMyModule> => {
const cfg = await config;
const client = new MyApiClient(cfg.apiBaseUrl);
return { getClient: () => client };
},
};
```
## Wiring a custom module into an app
Register in `src/config.ts`:
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import { module as myModule } from './modules/myModule';
export const configure: AppModuleInitiator = (configurator) => {
// Built-in modules
configurator.useFrameworkServiceClient('myService');
// Custom module
configurator.addModule(myModule, (moduleConfigurator) => {
moduleConfigurator.setConfig({
apiBaseUrl: 'https://api.example.com',
});
});
};
```
## Accessing a custom module in components
Use `useAppModule` with module name key:
```typescript
import { useAppModule } from '@equinor/fusion-framework-react-app';
import type { IMyModule } from '../modules/myModule';
import { MODULE_NAME } from '../modules/myModule';
const MyFeatureComponent = () => {
// Type the module by its interface
const myModule = useAppModule<IMyModule>(MODULE_NAME);
const client = myModule.getClient();
// Use the client in a React Query hook or useEffect
};
```
> `useAppModule` is a hook — only inside components/hooks.
## Module lifecycle
Modules initialized before React mounts:
1. `initialize` — called once at bootstrap with merged config and parent framework instance
2. Module stored in framework instance under `name` key
3. Components access via `useAppModule(name)` after mount
No `destroy` lifecycle on most modules. Clean up subscriptions in components via `useEffect`.
## File structure
Module lives alongside app source:
```
src/
modules/
myModule/
index.ts — module definition + exports
MyApiClient.ts — implementation
config.ts — register module here
```
## Common pitfalls
- **Naming conflicts**: `name` must not collide with built-in Fusion modules (`http`, `auth`, `context`, `navigation`, `featureFlag`, `settings`, `bookmarks`, `analytics`).
- **Outside React**: `useAppModule` is a hook — can't call outside components. Loaders/non-React: use `framework.modules.<name>`.
- **Config not awaited**: `initialize` receives `Promise<IMyModuleConfig>` — always `await config`.
- **Over-engineering**: single-component need? Use hook/context instead.
## When to use `fusion-research` first
Uncertain about API signatures or built-in module config? Use `fusion-research` with `mcp_fusion_search_framework` first.
Example query: `mcp_fusion_search_framework` → `"IModule initialize configure AppModuleInitiator custom module"`
references/using-feature-flags.md
# Using Feature Flags
Using Fusion Framework feature toggling in React apps.
## When feature flags are the right fit
Use feature flags for:
- incremental rollout of features that need to be enabled or disabled at runtime
- temporary toggles that will be removed after rollout is complete
- developer or debug toggles that should persist locally (`createLocalStoragePlugin`)
- test or override toggles controlled via URL query parameters (`createUrlPlugin`)
Do not use feature flags for:
- permanent configuration (use `app.config.ts` or App Settings)
- per-user preferences that should survive long-term (use App Settings)
- authorization or access control (use Fusion context and role checks)
## App-level setup
For most Fusion apps, start with the app-level helper.
```ts
// src/config.ts
import { enableFeatureFlag } from '@equinor/fusion-framework-react-app/feature-flag';
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
export enum Feature {
NewDashboard = 'new-dashboard',
}
export const configure: AppModuleInitiator = (appConfigurator) => {
enableFeatureFlag(appConfigurator, [
{
key: Feature.NewDashboard,
title: 'New dashboard',
description: 'Enable the redesigned dashboard layout.',
},
]);
};
```
- Prefer shared enum or string-constant object over raw strings
- Add `title` and `description` when surfaced to users or code reviews
- Use `value` only when behavior requires more than on/off
## Framework-level setup with plugins
When participating in framework-wide providers or needing local persistence and URL override, use the module-level API.
```ts
// framework-level configuration (for example in src/framework-config.ts)
import { enableFeatureFlagging } from '@equinor/fusion-framework-module-feature-flag';
import {
createLocalStoragePlugin,
createUrlPlugin,
} from '@equinor/fusion-framework-module-feature-flag/plugins';
export const configureFeatureFlags = (configurator) => {
enableFeatureFlagging(configurator, (builder) => {
builder.addPlugin(
createLocalStoragePlugin([{ key: 'fusionDebug', title: 'Fusion debug log' }]),
);
builder.addPlugin(createUrlPlugin(['fusionDebug']));
});
};
```
- `createLocalStoragePlugin` — persists toggle state in `localStorage`; suitable for developer or debug flags.
- `createUrlPlugin` — reads flag state from the URL query string; useful for QA override and test automation.
## Consuming flags in components
Use `useFeature` in React components:
```tsx
// src/components/Dashboard.tsx
import { useFeature } from '@equinor/fusion-framework-react-app/feature-flag';
import { Feature } from '../config';
const Dashboard = () => {
const { feature, toggleFeature } = useFeature(Feature.NewDashboard);
if (!feature?.enabled) {
return <LegacyDashboard />;
}
return <NewDashboard />;
};
```
- Access `feature.enabled` for boolean gating
- Access `feature.value` for typed configuration value
- Call `toggleFeature()` for user-controlled toggling
- Guard against `feature` being `undefined`; use optional chaining or null check
### Provider-based hook variant
Provider-based variant for explicit provider control:
```ts
import { useFeature } from '@equinor/fusion-framework-react/feature-flag';
const value = useFeature(provider, 'my-flag-key');
```
Use only at framework level outside the app package scope.
## Known API ambiguity
Public sources show inconsistent spelling for the read-only flag property:
- Some sources use `readonly`
- Older docs mention `readOnly`
Verify against local types or Fusion MCP before finalizing code that reads this property.
## Rollout and cleanup
Feature flags are temporary — every flag needs an owner and a removal plan.
- **Define an owner** in a comment or in the flag `description`
- **State removal milestone** at which the flag will be removed
- **Test both paths** — app must work with flag on and off
- **Remove completely** — delete flag definition, all consumers, and dead code branches in one PR
- **Avoid long-lived flags** — flags that outlive rollout become maintenance debt
### Rollout checklist
- [ ] Flag key defined in a shared enum or constant.
- [ ] `title` and `description` filled in.
- [ ] Entry point chosen: `enableFeatureFlag` (app) or `enableFeatureFlagging` (framework/plugin).
- [ ] Plugin added if local persistence or URL override is required.
- [ ] Component reads `feature?.enabled` with null-safe access.
- [ ] Both enabled and disabled rendering paths tested.
- [ ] Owner and removal milestone documented.
- [ ] `readonly` vs `readOnly` verified against local types.
## MCP-grounded research
For live API references and up-to-date examples, query Fusion MCP:
```
mcp_fusion_search_framework: "feature flag enableFeatureFlag useFeature"
mcp_fusion_search_framework: "feature toggling plugin createLocalStoragePlugin createUrlPlugin"
```
If Fusion MCP is unavailable, use the public Fusion Framework sources below.
## Public source map
- Module README: https://github.com/equinor/fusion-framework/blob/main/packages/modules/feature-flag/README.md
- App guide: https://github.com/equinor/fusion-framework/blob/main/vue-press/src/guide/app/feature-flag.md
- Cookbook example config: https://github.com/equinor/fusion-framework/blob/main/cookbooks/app-react-feature-flag/src/config.ts
- Cookbook component example: https://github.com/equinor/fusion-framework/blob/main/cookbooks/app-react-feature-flag/src/FeatureFlags.tsx
- App hook: https://github.com/equinor/fusion-framework/blob/main/packages/react/app/src/feature-flag/useFeature.ts
- Framework hook: https://github.com/equinor/fusion-framework/blob/main/packages/react/framework/src/feature-flag/useFeature.ts
references/using-framework-modules.md
# Using Framework Modules
How to access Fusion Framework modules (context, auth, navigation, feature flags, settings, environment, bookmarks, analytics) in components.
## Module access patterns
Most module hooks are available from sub-path imports of `@equinor/fusion-framework-react-app`. Runtime config access is currently exposed from the package root:
```typescript
import { useAppEnvironmentVariables, useAppModule } from '@equinor/fusion-framework-react-app'; // root exports
import { useHttpClient } from '@equinor/fusion-framework-react-app/http'; // HTTP
import { useCurrentContext } from '@equinor/fusion-framework-react-app/context'; // context
import { useCurrentAccount } from '@equinor/fusion-framework-react-app/msal'; // auth
import { useRouter } from '@equinor/fusion-framework-react-app/navigation'; // routing
import { useFeature } from '@equinor/fusion-framework-react-app/feature-flag'; // flags
import { useAppSetting } from '@equinor/fusion-framework-react-app/settings'; // settings
import { useCurrentBookmark } from '@equinor/fusion-framework-react-app/bookmark'; // bookmark
import { useTrackFeature } from '@equinor/fusion-framework-react-app/analytics'; // analytics
```
## Context
Access the current Fusion context (project, facility, etc.):
```typescript
import { useCurrentContext } from '@equinor/fusion-framework-react-app/context';
const MyComponent = () => {
const context = useCurrentContext();
if (!context) return <p>No context selected</p>;
return <p>Selected: {context.title}</p>;
};
```
## Authentication (MSAL)
The MSAL module is configured by the host portal — apps only consume the hooks:
```typescript
import { useCurrentAccount, useAccessToken } from '@equinor/fusion-framework-react-app/msal';
const UserInfo = () => {
const user = useCurrentAccount();
const { token } = useAccessToken({ scopes: ['User.Read'] });
if (!user) return <p>Not signed in</p>;
return <p>Welcome, {user.name}</p>;
};
```
## Navigation
```typescript
import { useRouter, useNavigationModule } from '@equinor/fusion-framework-react-app/navigation';
const MyComponent = () => {
const router = useRouter();
// router.navigate('/path')
};
```
## Feature flags
```typescript
import { useFeature } from '@equinor/fusion-framework-react-app/feature-flag';
// In config.ts — enable a flag
configurator.enableFeatureFlag({ key: 'new-dashboard', title: 'New Dashboard' });
// In components — read the flag
const MyComponent = () => {
const [isEnabled] = useFeature('new-dashboard');
if (!isEnabled) return null;
return <NewDashboard />;
};
```
## App settings
Per-user settings stored by the Fusion platform:
```typescript
import { useAppSetting, useAppSettings } from '@equinor/fusion-framework-react-app/settings';
const MyComponent = () => {
const [theme, setTheme] = useAppSetting('theme', 'light');
// theme is the current value, setTheme persists a new value
};
```
Use app settings for per-user preferences that should survive between sessions. See `using-settings.md` for how to decide between settings, bookmarks, and runtime config.
## Environment variables
Access runtime configuration from `app.config.ts`:
```typescript
import { useAppEnvironmentVariables } from '@equinor/fusion-framework-react-app';
const MyComponent = () => {
const { value, complete, error } = useAppEnvironmentVariables();
// value contains keys defined in app.config.ts environment: {}
};
```
Use `app.config.ts` for deployment-specific values and endpoint definitions. See `using-assets-and-environment.md` for config/file-placement guidance.
## Bookmarks
```typescript
import { useCurrentBookmark } from '@equinor/fusion-framework-react-app/bookmark';
```
Use bookmarks for shareable, restorable view state such as filters, selected tabs, and dashboard layout. Prefer `useCurrentBookmark` for new work. See `using-bookmarks.md` for module setup and payload guidance.
## Analytics
```typescript
import { useEffect } from 'react';
import { useTrackFeature } from '@equinor/fusion-framework-react-app/analytics';
const DashboardPage = () => {
const trackFeature = useTrackFeature();
useEffect(() => {
trackFeature('dashboard-page-loaded');
}, [trackFeature]);
return null;
};
```
The portal provides analytics support out of the box. Use the hook for feature and interaction events, and see `using-analytics.md` for event design and local testing guidance.
## Generic module access
Access any module by key when a dedicated hook is not available:
```typescript
import { useAppModule, useAppModules } from '@equinor/fusion-framework-react-app';
const auth = useAppModule('auth');
const allModules = useAppModules();
```
references/using-fusion-react-components.md
# Fusion React Components
Domain-specific React components complementing EDS. Integrate with Fusion platform APIs — functionality not in `@equinor/eds-core-react`.
**Repository**: [equinor/fusion-react-components](https://github.com/equinor/fusion-react-components)
**Storybook**: [equinor.github.io/fusion-react-components](https://equinor.github.io/fusion-react-components/)
## When to use
Use `@equinor/fusion-react-*` when EDS has no equivalent. Check EDS first.
## Packages
| Package | Purpose |
|---|---|
| `@equinor/fusion-react-person` | Person display, search, and selection (People API integration) |
| `@equinor/fusion-react-side-sheet` | Slide-over side panel |
| `@equinor/fusion-react-progress-indicator` | Progress / loading indicators |
## Person components (`@equinor/fusion-react-person`)
Most used package. Pass `azureId`/`upn`; components handle fetch.
### Component catalog
| Component | Use for |
|---|---|
| `PersonAvatar` | Profile image with account-type border; hover/click shows details |
| `PersonCard` | Full person info — department, positions, tasks, manager |
| `PersonListItem` | Compact person row with optional action buttons |
| `PersonSelect` | Searchable dropdown to pick a single person |
| `PeoplePicker` | Multi-person search & selection (pills or table display) |
| `PersonPicker` | Single-person variant of `PeoplePicker` |
| `PeopleViewer` | Display and manage a collection of selected people |
### Key types
```typescript
import type {
PersonInfo,
PersonAddedEvent,
PersonRemovedEvent,
} from '@equinor/fusion-react-person';
```
### Usage examples
#### Display a person avatar
```typescript
import { PersonAvatar } from '@equinor/fusion-react-person';
<PersonAvatar azureId="00000000-0000-0000-0000-000000000000" />
```
#### Single person picker
```typescript
import { PersonPicker, type PersonInfo } from '@equinor/fusion-react-person';
import { useState } from 'react';
const ManagerField = () => {
const [manager, setManager] = useState<PersonInfo | null>(null);
return (
<PersonPicker
person={manager}
placeholder="Search for a manager…"
subtitle="jobTitle"
onPersonAdded={(e) => setManager(e.detail)}
onPersonRemoved={() => setManager(null)}
/>
);
};
```
#### Multi-person picker with viewer
```typescript
import {
PeoplePicker,
PeopleViewer,
type PersonInfo,
type PersonAddedEvent,
type PersonRemovedEvent,
} from '@equinor/fusion-react-person';
import { useCallback, useState } from 'react';
const TeamBuilder = () => {
const [people, setPeople] = useState<PersonInfo[]>([]);
const handleAdded = useCallback(
(e: PersonAddedEvent) => setPeople((prev) => [...prev, e.detail]),
[],
);
const handleRemoved = useCallback(
(e: PersonRemovedEvent) =>
setPeople((prev) => prev.filter((p) => p.azureId !== e.detail.azureId)),
[],
);
return (
<>
<PeoplePicker
people={people}
placeholder="Search for team members…"
multiple={true}
showSelectedPeople={false}
subtitle="department"
onPersonAdded={handleAdded}
onPersonRemoved={handleRemoved}
/>
<PeopleViewer
people={people}
subtitle="department"
display="list"
displayToggle={true}
onPersonRemoved={handleRemoved}
/>
</>
);
};
```
### PeoplePicker props reference
| Prop | Type | Description |
|---|---|---|
| `people` | `PersonInfo[]` | Currently selected people |
| `placeholder` | `string` | Input placeholder text |
| `multiple` | `boolean` | Allow selecting more than one person |
| `showSelectedPeople` | `boolean` | Show selection inline below the input |
| `subtitle` | `string` | Field shown as subtitle (`jobTitle`, `department`, etc.) |
| `secondarySubtitle` | `string` | Optional secondary subtitle field |
| `systemAccounts` | `boolean` | Include system accounts in search |
| `display` | `'list' \| 'table'` | Layout for selected people |
| `displayToggle` | `boolean` | Show toggle between list and table views |
| `tableColumns` | `string[]` | Columns for table view |
| `noResultTitle` | `string` | Empty-state heading |
| `noResultSubtitle` | `string` | Empty-state subtext |
| `onPersonAdded` | `(e: PersonAddedEvent) => void` | Fired when a person is selected |
| `onPersonRemoved` | `(e: PersonRemovedEvent) => void` | Fired when a person is deselected |
### Event pattern
Person components use **custom DOM events**, not standard React callback signatures:
```typescript
// The event detail carries the PersonInfo object
<PersonPicker
onPersonAdded={(e: PersonAddedEvent) => {
const person: PersonInfo = e.detail;
console.log(person.azureId, person.name);
}}
/>
```
#### Display a person card
`PersonCard` renders full person details (name, department, positions, tasks, manager) resolved from Fusion People API by `azureId`.
```typescript
import { PersonCard } from '@equinor/fusion-react-person';
const OwnerCard = ({ azureId }: { azureId: string }) => (
<PersonCard azureId={azureId} />
);
```
Use `PersonCard` for full-detail views. Prefer `PersonAvatar` for compact inline display.
#### Display a person list item
`PersonListItem` renders a compact single-row entry. Action buttons optional.
```typescript
import { PersonListItem } from '@equinor/fusion-react-person';
import { Button } from '@equinor/eds-core-react';
// Display only
const AssigneeRow = ({ azureId }: { azureId: string }) => (
<PersonListItem azureId={azureId} />
);
// With an action button
const ReviewerRow = ({
azureId,
onRemove,
}: {
azureId: string;
onRemove: () => void;
}) => (
<PersonListItem azureId={azureId}>
<Button variant="ghost_icon" onClick={onRemove}>
Remove
</Button>
</PersonListItem>
);
```
#### Person column in AG Grid (PersonCell)
`PersonCell` is an AG Grid cell renderer: avatar + name inline. Cell value must be `azureId` string.
```typescript
import { PersonCell } from '@equinor/fusion-react-person';
import type { ColDef } from '@equinor/fusion-framework-react-ag-grid/community';
interface WorkItem {
id: string;
title: string;
ownerAzureId: string;
}
const columnDefs: ColDef<WorkItem>[] = [
{ field: 'title', headerName: 'Title', flex: 2 },
{
field: 'ownerAzureId',
headerName: 'Owner',
width: 200,
cellRenderer: PersonCell,
},
];
```
If `azureId` is nested in the row object, use `valueGetter` to extract it:
```typescript
{
headerName: 'Owner',
width: 200,
cellRenderer: PersonCell,
valueGetter: ({ data }) => data?.owner?.azureId,
}
```
### Decision guide: which person component?
| UI need | Component |
|---|---|
| Compact inline display (avatar only, hover for details) | `PersonAvatar` |
| Full person detail view (card, panel, popover) | `PersonCard` |
| One-line person row in a list | `PersonListItem` |
| Person row with action buttons | `PersonListItem` with children |
| Pick a single person | `PersonPicker` or `PersonSelect` |
| Pick multiple people | `PeoplePicker` |
| Display a selected set of people | `PeopleViewer` |
| Person column in an AG Grid | `PersonCell` |
## Decision guide: EDS vs Fusion React
| Need | Use |
|---|---|
| Buttons, inputs, dialogs, typography, icons | `@equinor/eds-core-react` |
| Design tokens (colors, spacing) | `@equinor/eds-tokens` or EDS CSS variables |
| Person avatars, cards, pickers | `@equinor/fusion-react-person` |
| Fusion-specific side sheets | `@equinor/fusion-react-side-sheet` |
references/using-people-service.md
# Using the Fusion People Service
Fusion People API: people discovery, person details, integration.
**Source**: `@equinor/fusion-react-person` + Fusion Framework people service
**Storybook**: [equinor.github.io/fusion-react-components — person](https://equinor.github.io/fusion-react-components/?path=/docs/person-docs--docs)
## When to use the people service
Use when:
- search/pick people (assignees, owners, reviewers)
- display name, avatar, department, role
- show manager/reporting relationship
- person column in AG Grid
Most cases: `@equinor/fusion-react-person` components handle it — provide `azureId`, they resolve the rest.
## The preferred path: let components resolve people
Components resolve via `azureId` (preferred) or `upn`. `azureId` is stable, unambiguous.
```typescript
import { PersonAvatar, PersonCard, PersonListItem } from '@equinor/fusion-react-person';
// Correct — pass azureId, let the component resolve the rest
<PersonAvatar azureId={item.ownerAzureId} />
<PersonCard azureId={item.ownerAzureId} />
<PersonListItem azureId={item.ownerAzureId} />
```
Don't fetch person data manually and pass as props — components handle loading, caching, errors.
See `references/using-fusion-react-components.md` for full component usage examples (pickers, card, list item, ag-grid cell).
## Storing person references
Store only `azureId` in data model. Resolve display at render via components.
```typescript
// Data layer — store azureId only
interface WorkItem {
id: string;
title: string;
ownerAzureId: string; // Azure AD object ID (UUID)
}
// UI layer — resolve at render time
<PersonCard azureId={item.ownerAzureId} />
```
Don't cache or persist resolved `PersonInfo` objects — components re-resolve from the API.
## People search via PersonPicker / PeoplePicker
For user selection, use picker components — query API automatically on input.
```typescript
import { PersonPicker, type PersonInfo } from '@equinor/fusion-react-person';
import { useState } from 'react';
// Single-person picker
const AssigneeField = ({
onSelect,
}: {
onSelect: (azureId: string) => void;
}) => {
const [person, setPerson] = useState<PersonInfo | null>(null);
return (
<PersonPicker
person={person}
placeholder="Search for a person…"
subtitle="jobTitle"
onPersonAdded={(e) => {
setPerson(e.detail);
onSelect(e.detail.azureId);
}}
onPersonRemoved={() => {
setPerson(null);
onSelect('');
}}
/>
);
};
```
> **Event pattern**: picker components use custom DOM events (`PersonAddedEvent`, `PersonRemovedEvent`). Access the person via `e.detail`, not the argument directly.
## Integration points
| Need | Component / hook |
|---|---|
| Display a person (avatar, hover details) | `PersonAvatar` |
| Full person detail (card, panel) | `PersonCard` |
| Person in a list row | `PersonListItem` |
| Person in an AG Grid column | `PersonCell` |
| Let user pick one person | `PersonPicker` or `PersonSelect` |
| Let user pick multiple people | `PeoplePicker` |
| Display a selected collection | `PeopleViewer` |
## System accounts
Person search excludes system accounts by default. To include (e.g. service principals):
```typescript
<PersonPicker
systemAccounts={true}
onPersonAdded={(e) => onSelect(e.detail.azureId)}
onPersonRemoved={() => onSelect('')}
/>
```
## Non-goals
- **Do not** build a custom people API client using `useHttpClient` — the framework and components handle this internally.
- **Do not** manage people search state manually — the picker components handle input, debouncing, and results.
- **Do not** pass `PersonInfo` objects directly into non-picker components — pass `azureId` and let the component resolve.
references/using-react-query.md
# Using React Query
Managing server state with `@tanstack/react-query`.
## Setup
Wrap the app (or a subtree) in `QueryClientProvider`:
```typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
retry: 1,
},
},
});
const App = () => (
<QueryClientProvider client={queryClient}>
{/* app content */}
</QueryClientProvider>
);
```
## Custom query hooks
Create thin wrappers in `src/hooks/`:
```typescript
import { useQuery } from '@tanstack/react-query';
import { useHttpClient } from '@equinor/fusion-framework-react-app/http';
interface Item {
id: string;
name: string;
}
/**
* Fetches all items from the API.
*
* @returns A React Query result containing the item list.
*/
export const useItems = () => {
const client = useHttpClient('my-api');
return useQuery<Item[]>({
queryKey: ['items'],
queryFn: () => client.json('/items'),
});
};
```
## Query key conventions
- Derive keys from the API path and parameters: `['items']`, `['items', itemId]`
- Nest keys for filtered queries: `['items', { status: 'active' }]`
- Use consistent prefixes to enable bulk invalidation: `['items', ...]`
## Parameterized queries
```typescript
/**
* Fetches a single item by ID.
*
* @param itemId - The unique item identifier.
* @returns A React Query result containing the item.
*/
export const useItem = (itemId: string) => {
const client = useHttpClient('my-api');
return useQuery<Item>({
queryKey: ['items', itemId],
queryFn: () => client.json(`/items/${itemId}`),
enabled: !!itemId,
});
};
```
## Mutations
Use `useMutation` for writes. Invalidate related queries on success:
```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
/**
* Submits an item update to the API.
*
* @returns A React Query mutation result.
*/
export const useUpdateItem = () => {
const queryClient = useQueryClient();
const client = useHttpClient('my-api');
return useMutation({
mutationFn: (item: Item) =>
client.fetchAsync(`/items/${item.id}`, {
method: 'PUT',
body: JSON.stringify(item),
headers: { 'Content-Type': 'application/json' },
}),
onSuccess: (_data, variables) => {
// Refresh the list and the individual item cache
queryClient.invalidateQueries({ queryKey: ['items'] });
},
});
};
```
## State separation
- **React Query** = server state (API data, loading, error, caching, background refetch)
- **React state/context** = client UI state (selected tab, filter input text, modal open/close)
Never store UI state in React Query. Never manage server state in `useState`.
## DevTools
DevTools for debugging (excluded in production):
```typescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const App = () => (
<QueryClientProvider client={queryClient}>
{/* app content */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
```
## Error handling
Component-level:
```typescript
const { data, error, isLoading } = useItems();
if (isLoading) return <Progress.Dots />;
if (error) return <Banner variant="warning">Failed to load items</Banner>;
```
Or global handler on `QueryClient`:
```typescript
import { QueryClient, QueryCache } from '@tanstack/react-query';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
console.error('Query failed:', error);
},
}),
});
```
references/using-router.md
# Using the Fusion Router
Client-side routing in Fusion Framework apps with `@equinor/fusion-framework-react-router`.
Wraps React Router v7: navigation module wiring, typed `fusion` context in loaders, route DSL for code splitting, manifest-ready schemas.
## Prerequisites
1. Install the package:
```sh
# use the project's package manager (bun / pnpm / npm)
bun add @equinor/fusion-framework-react-router
```
2. Enable the navigation module in `config.ts`:
```typescript
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import { enableNavigation } from '@equinor/fusion-framework-module-navigation';
export const configure: AppModuleInitiator = (configurator, { env }) => {
enableNavigation(configurator, env.basename);
};
```
## Route DSL
Define routes via `/routes` helpers. Files lazy-loaded.
```typescript
// src/routes.ts
import { layout, index, route, prefix } from '@equinor/fusion-framework-react-router/routes';
export const routes = layout('./pages/Root.tsx', [
index('./pages/HomePage.tsx'),
prefix('items', [
index('./pages/ItemsPage.tsx'),
route(':id', './pages/ItemPage.tsx'),
]),
prefix('settings', [
route('general', './pages/GeneralSettings.tsx'),
route('advanced', './pages/AdvancedSettings.tsx'),
]),
]);
```
### DSL helpers
| Helper | Purpose |
|---|---|
| `layout(file, children)` | Layout wrapping children — the component must render `<Outlet />` |
| `index(file, schema?)` | Index route rendered at the parent's path |
| `route(path, file, children?, schema?)` | Standard route with a URL path and optional children |
| `prefix(path, children)` | Path-only grouping — prepends a segment without rendering a component |
### Vite plugin
Enable Vite plugin for static DSL-to-RouteObject transform at build time:
```typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { reactRouterPlugin } from '@equinor/fusion-framework-react-router/vite-plugin';
export default defineConfig({
plugins: [react(), reactRouterPlugin()],
});
```
## Mount the Router
Create a Router component and render it in `App.tsx`:
```typescript
// src/Router.tsx
import { Router } from '@equinor/fusion-framework-react-router';
import { routes } from './routes';
export default function AppRouter() {
return <Router routes={routes} />;
}
```
The `<Router>` component accepts:
- `routes` — route tree (DSL nodes or `RouteObject[]`)
- `loader` — optional JSX shown during navigation transitions
- `context` — optional custom data accessible in all loaders via `fusion.context`
## Page components
Each page: default component export with `RouteComponentProps` (`loaderData`, `fusion`):
```typescript
// src/pages/ItemPage.tsx
import type { RouteComponentProps, RouterHandle } from '@equinor/fusion-framework-react-router';
export const handle = {
route: {
description: 'Displays a single item',
params: { id: 'The item identifier' },
},
} as const satisfies RouterHandle;
export default function ItemPage({ loaderData }: RouteComponentProps) {
const { item } = loaderData;
return <h1>{item.name}</h1>;
}
```
## Client loaders
Export `clientLoader` to fetch before component renders. Receives `params`, `request`, `fusion` (framework modules + custom context):
```typescript
import type { LoaderFunctionArgs } from '@equinor/fusion-framework-react-router';
export const clientLoader = async ({ params, fusion }: LoaderFunctionArgs) => {
// Route loaders run outside React — use module access instead of hooks
const client = fusion.modules.http.createClient('my-api');
const item = await client.json(`/items/${params.id}`);
if (!item) {
throw new Response('Item not found', { status: 404 });
}
return { item };
};
```
Returns data as `loaderData`.
## Error handling
Export `ErrorElement` from any page file to catch loader/component errors:
```typescript
import type { ErrorElementProps } from '@equinor/fusion-framework-react-router';
import { useNavigate } from '@equinor/fusion-framework-react-router';
import { Button, Banner } from '@equinor/eds-core-react';
export function ErrorElement({ error }: ErrorElementProps) {
const navigate = useNavigate();
return (
<Banner variant="warning">
<Banner.Message>{error.message}</Banner.Message>
<Banner.Actions>
<Button variant="ghost" onClick={() => navigate(0)}>Retry</Button>
<Button variant="ghost" onClick={() => navigate('/')}>Home</Button>
</Banner.Actions>
</Banner>
);
}
```
Without `ErrorElement`: bubbles to parent error boundary.
## Navigation
Router re-exports React Router navigation primitives:
```typescript
import { Link, useNavigate, useParams, useSearchParams, useLocation } from '@equinor/fusion-framework-react-router';
```
### Links
```typescript
<Link to="/">Home</Link>
<Link to="/items/123">Item 123</Link>
<Link to="/items?status=active">Active Items</Link>
```
### Programmatic navigation
```typescript
const navigate = useNavigate();
navigate('/items/123'); // absolute
navigate(-1); // back
navigate(0); // reload current route
```
## Layout routes
Layout route wraps children with shared UI. Component must render `<Outlet />`:
```typescript
// src/pages/Root.tsx
import { Outlet, Link } from '@equinor/fusion-framework-react-router';
export default function Root() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/items">Items</Link>
</nav>
<main>
<Outlet />
</main>
</div>
);
}
```
## Route schemas for manifests
Generate a flat schema from the route tree for `app.manifest.ts`:
```typescript
// app.manifest.ts
import { defineAppManifest } from '@equinor/fusion-framework-cli/app';
import { toRouteSchema } from '@equinor/fusion-framework-react-router/schema';
import { routes } from './src/routes';
export default defineAppManifest(async () => ({
routes: await toRouteSchema(routes),
}));
```
## Custom router context
Pass custom data (e.g. a QueryClient) to all loaders via module augmentation:
```typescript
// src/router-context.d.ts
declare module '@equinor/fusion-framework-react-router' {
interface RouterContext {
queryClient: import('@tanstack/react-query').QueryClient;
}
}
```
```typescript
// src/Router.tsx
import { Router } from '@equinor/fusion-framework-react-router';
import { QueryClient } from '@tanstack/react-query';
import { routes } from './routes';
const queryClient = new QueryClient();
export default function AppRouter() {
return <Router routes={routes} context={{ queryClient }} />;
}
```
Loaders then access `fusion.context.queryClient` with full type safety.
## Choosing a routing approach
| Approach | When to use |
|---|---|
| Fusion Router DSL | Multi-page apps — code splitting, loaders, schemas |
| Plain `RouteObject[]` with `<Router>` | When you prefer manual route objects over DSL |
| No router (single-page) | Simple apps with no URL-based navigation |
references/using-settings.md
# Using App Settings
Per-user app settings in Fusion Framework.
## When app settings are the right fit
Use for:
- per-user preferences that survive sessions
- view options not shared via bookmark URL
- app toggles (density, preferred tab, optional panels)
Don't use for:
- deployment/environment values — use `app.config.ts`
- shareable view state — use bookmarks
- secrets, access tokens, or large cached API responses
## Basic hook
```typescript
import { useAppSetting } from '@equinor/fusion-framework-react-app/settings';
const ToggleFunMode = () => {
const [funMode, setFunMode] = useAppSetting('fun_mode', false);
return (
<Button onClick={() => setFunMode((current) => !current)}>
{funMode ? 'No more fun' : 'Have more fun'}
</Button>
);
};
```
Default value required; stored per user per app.
## Read all settings
```typescript
import { useAppSettings } from '@equinor/fusion-framework-react-app/settings';
const SettingsDebug = () => {
const [settings] = useAppSettings();
return <pre>{JSON.stringify(settings, null, 2)}</pre>;
};
```
## Where settings fit in the app model
| Concern | Store it in |
|---|---|
| Per-user preference that should persist between sessions | App settings |
| Shareable view state for support or collaboration | Bookmarks |
| Deployment-specific URLs, scopes, log levels | `app.config.ts` |
| Temporary component state | React state |
## Design guidance
- Stable keys — renaming needs migration plan
- JSON-serializable, small values
- Define defaults in code for new users and legacy payloads
- Stored shape is app contract; handle missing/legacy fields defensively
- Only current user can read/update own settings; don't model shared workflow state
## Review questions
When reviewing settings changes:
- Should this be a bookmark instead?
- Truly per-user, or runtime config?
- What happens if setting is missing or from older stored shape?
## Relevant sources
- Fusion docs app settings guide and hook examples
- Fusion docs app service user-settings behavior