# Technical Documentation: umbraco/Umbraco-CMS

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/umbraco/Umbraco-CMS)

### Overview (`.github/README.md`)

# [Umbraco CMS](https://umbraco.com)

[](../LICENSE.md) 
[](https://www.nuget.org/packages/Umbraco.Cms)
[](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301) 
[](CONTRIBUTING.md) 
[](https://forum.umbraco.com) 
[](https://discord.gg/umbraco) 



### Umbraco is a free and open source .NET content management system. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social. 

Learn more at [umbraco.com](https://umbraco.com)

<p align="center">
    <img src="img/logo.png" alt="Umbraco Logo" />
</p>

## <a name="install"></a>Looking to install Umbraco?

You can get started using the following commands on Windows, Linux and MacOS (after installing the [.NET Runtime and SDK](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/requirements)):

```
dotnet new install Umbraco.Templates
dotnet new umbraco --name MyProject
cd MyProject
dotnet run
```

## Documentation

Our [comprehensive documentation](https://docs.umbraco.com/umbraco-cms) takes you from the fundamentals on how to start with Umbraco to deploying it to production.

Some important documentation links to get you started: 

- [Installing Umbraco CMS](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/install)
- [Getting to know Umbraco](https://docs.umbraco.com/umbraco-cms/fundamentals/get-to-know-umbraco)
- [Tutorials for creating a basic website and customizing the editing experience](https://docs.umbraco.com/umbraco-cms/tutorials/overview)

## Backoffice Preview

Want to see the latest backoffice UI in action? Check out our live preview:

**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**

This preview is automatically deployed from the main branch and showcases the latest backoffice features and improvements.  It runs from mock data and persistent edits are not supported.

## Get help

If you need a bit of feedback while building your Umbraco projects, we are [chatty on Discord](https://discord.umbraco.com). Our Discord server serves as a social space for all Umbracians. If you have any questions or need some help with a problem, head over to our [dedicated forum](https://forum.umbraco.com/) where the Umbraco Community will be happy to help. 

## Looking to contribute back to Umbraco?

You came to the right place! Our GitHub repository is available for all kinds of contributions:

- [Create a bug report](https://github.com/umbraco/Umbraco-CMS/issues)
- [Create a feature request](https://github.com/umbraco/Umbraco-CMS/discussions)

Umbraco is contribution-focused and community-driven. If you want to contribute back to the Umbraco source code, please check out our [guide to contributing](CONTRIBUTING.md).

### Tip: You should not run Umbraco from source code found here. Umbraco is extremely extensible and can do whatever you need. Instead, [install Umbraco as noted above](#looking-to-install-umbraco) and then [extend it any way you want to](https://docs.umbraco.com/umbraco-cms/extending/).

---

### Overview (`.github/codespaces-readme.md`)

# GitHub CodeSpaces
Umbraco source code can be edited inside the browser with VSCode and CodeSpaces.

This development environment comes with all the tools you need to build Umbraco source code.


## Debugging and Running
From VSCode browse to the Run and Debug section and then click the green button. This will build the Umbraco source code and attach a debugger and launch the site.

## Default Umbraco credentials
Username: test@umbraco.com
Password: password1234

## Test Email Server
 A SMTP4Dev instance for testing email is available on port 5000.

## SQLite Database
The SQLite extension is preinstalled and allows you to open, query, edit the data inside the Umbraco SQLite database for ease of use.

---

### Overview (`templates/readme.md`)

# Contributing to the Umbraco Templates

If you're making changes to any of the templates in this folder, please note that installing them directly from this location to test will not work. You need to 'pack' the source into a nuget package (.nupkg) in a local folder, and install from there.

For example, using a folder location of `c:\nuget.local`:

```
# Pack the templates to a local folder
dotnet pack Umbraco.Templates.csproj -c Release --no-build -o "c:\nuget.local"

# Add this folder as a local nuget source 
dotnet nuget add source "c:\nuget.local" --name "local nuget"

# Make sure you don't have the global templates installed
dotnet new uninstall Umbraco.Templates

# Install your version of the templates, having checked/updated the name of the generated .nupkg file
dotnet new install "c:\nuget.local\Umbraco.Templates.XXXX.nupkg"
```

You can now test your template changes using the appropriate `dotnet new` command.

---

### Overview (`src/Umbraco.Web.UI.Client/README.md`)

# @umbraco-cms/backoffice

This package contains the types for the Umbraco Backoffice.

## Preview

A live preview of the latest backoffice build from the main branch is available at:

**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**

This preview is automatically deployed via GitHub Actions whenever changes are pushed to main or version branches.

## Installation

```bash
npm install -D @umbraco-cms/backoffice
```

## Usage

### Vanilla JavaScript

Create an umbraco-package.json file in the root of your package.

```json
{
	"name": "My.Package",
	"version": "0.1.0",
	"extensions": [
		{
			"type": "dashboard",
			"alias": "my.custom.dashboard",
			"name": "My Dashboard",
			"js": "/App_Plugins/MyPackage/dashboard.js",
			"weight": -1,
			"meta": {
				"label": "My Dashboard",
				"pathname": "my-dashboard"
			},
			"conditions": [
				{
					"alias": "Umb.Condition.SectionAlias",
					"match": "Umb.Section.Content"
				}
			]
		}
	]
}
```

Then create a dashboard.js file the same folder.

```javascript
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';

const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host {
      padding: 20px;
      display: block;
      box-sizing: border-box;
    }
  </style>

  <uui-box>
    <h1>Welcome to my dashboard</h1>
    <p>Example of vanilla JS code</p>

    <uui-button label="Click me" id="clickMe" look="secondary"></uui-button>
  </uui-box>
`;

export default class MyDashboardElement extends UmbElementMixin(HTMLElement) {
	/** @type {import('@umbraco-cms/backoffice/notification').UmbNotificationContext} */
	#notificationContext;

	constructor() {
		super();
		this.attachShadow({ mode: 'open' });
		this.shadowRoot.appendChild(template.content.cloneNode(true));

		this.shadowRoot.getElementById('clickMe').addEventListener('click', this.onClick.bind(this));

		this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => {
			this.#notificationContext = _instance;
		});
	}

	onClick = () => {
		this.#notificationContext?.peek('positive', { data: { headline: 'Hello' } });
	};
}

customElements.define('my-custom-dashboard', MyDashboardElement);
```

### TypeScript with Lit

First install Lit and Vite. This command will create a new folder called `my-package` which will have the Vite tooling and Lit for WebComponent development setup.

```bash
npm create vite@latest -- --template lit-ts my-package
```

Go to the new folder and install the backoffice package.

```bash
cd my-package
npm install -D @umbraco-cms/backoffice
```

Then go to the element located in `src/my-element.ts` and replace it with the following code.

```typescript
// src/my-element.ts
import { LitElement, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';

@customElement('my-element')
export default class MyElement extends UmbElementMixin(LitElement) {
	private _notificationContext?: UmbNotificationContext;

	constructor() {
		super();
		this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => {
			this._notificationContext = _instance;
		});
	}

	onClick() {
		this._notificationContext?.peek('positive', { data: { message: '#h5yr' } });
	}

	override render() {
		return html`
			<uui-box headline="Welcome">
				<p>A TypeScript Lit Dashboard</p>
				<uui-button look="primary" label="Click me" @click=${() => this.onClick()}></uui-button>
			</uui-box>
		`;
	}
}

declare global {
	interface HTMLElementTagNameMap {
		'my-element': MyElement;
	}
}
```

Finally add an umbraco-package.json file in the root of your package folder `my-package`.

```json
{
	"name": "My.Package",
	"version": "0.1.0",
	"extensions": [
		{
			"type": "dashboard",
			"alias": "my.custom.dashboard",
			"name": "My Dashboard",
			"js": "/App_Plugins/MyPackage/dist/my-package.js",
			"weight": -1,
			"meta": {
				"label": "My Dashboard",
				"pathname": "my-dashboard"
			},
			"conditions": [
				{
					"alias": "Umb.Condition.SectionAlias",
					"match": "Umb.Section.Content"
				}
			]
		}
	]
}
```

---

### Overview (`tests/Umbraco.TestData/readme.md`)

## Umbraco Test Data

This project is a utility to be able to generate large amounts of content and media in an
Umbraco installation for testing.

## Usage

You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data.

It has to be enabled by an appSetting:

```json
{
  "Umbraco": {
    "CMS": {
      "TestData": {
        "Enabled" : true
      }
    }
  }
}
```

After this, you also need to add a reference to the `Umbraco.TestData.csproj`, from `Umbraco.Web.UI.csproj` so the composers will be recognized.
```xml
  <ItemGroup>
    <ProjectReference Include="..\..\tests\Umbraco.TestData\Umbraco.TestData.csproj" />
  </ItemGroup>
```

Once this is done this endpoint can be executed:

`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5`

The query string options are:

* `count` = the number of content and media nodes to create
* `depth` = how deep the trees created will be
* `locale` (optional, default = "en") = the language that the data will be generated in

This creates a content and associated media tree (hierarchy). Each content item created is associated
to a media item via a media picker and therefore a relation is created between the two. Each content and
media tree created have the same root node name so it's easy to know which content branch relates to
which media branch.

All values are generated using the very handy `Bogus` package. 

## Schema

This will install some schema items:

* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type

For media, the normal folder and image is used

## Media

This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value.
This works when viewing the media item in the media section and the image will show up and with recent changes this will also work
when editing content to view the thumbnail for the picked media.

---

### Overview (`tests/Umbraco.Tests.AcceptanceTest/README.md`)

# Umbraco Acceptance Tests

End-to-end acceptance tests for Umbraco CMS using [Playwright](https://playwright.dev/).

You can watch a video following these instructions [here](https://www.youtube.com/watch?v=N4hBKB0U-d8) and a longer UmbraCollab recording [here](https://www.youtube.com/watch?v=hvoI28s_fDI). Make sure to use the latest recommended `main` branch rather than v10 that's mentioned in the video.

> **npm package**: This project is also published to npm as [`@umbraco/acceptance-test-helpers`](https://www.npmjs.com/package/@umbraco/acceptance-test-helpers). See [README.npm.md](./README.npm.md) for the package documentation.

---

## Prerequisites

- **Node.js 24** (see [`.nvmrc`](./.nvmrc))
- **A running installed Umbraco instance** on URL: [https://localhost:44339](https://localhost:44339) (default development port)
  - Install using `SqlServer`/`LocalDb` as the tests execute too fast for `SQLite` to handle

---

## Getting Started

1. Navigate to the test project folder:
   ```bash
   cd tests/Umbraco.Tests.AcceptanceTest
   ```

2. Install dependencies and Playwright browsers:
   ```bash
   npm ci
   npx playwright install
   ```

3. The setup script will prompt you to enter credentials for a superadmin user of your Umbraco CMS.

---

## Executing Tests

### Available NPM Scripts

| Command | Description |
|---------|-------------|
| `npm run test` | Execute DefaultConfig tests headlessly |
| `npm run ui` | Open Playwright UI mode with browser |
| `npm run smokeTest` | Run quick smoke tests (`@smoke` tagged) |
| `npm run releaseTest` | Run comprehensive release tests (`@release` tagged) |
| `npm run all` | Run all test suites |
| `npm run testSqlite` | Run tests excluding User tests (SQLite limitation) |
| `npm run testWindows` | Run tests excluding RelationType tests |
| `npm run createTest <name>` | Generate a new test file template |
| `npm run config` | Reconfigure environment settings |

### Running Single Tests

Run a specific test file:
```bash
npx playwright test tests/DefaultConfig/Content/Content.spec.ts
```

Run a single test by name:
```bash
npx playwright test -g "can create content with the document link"
```

Run tests with visible browser:
```bash
npx playwright test --headed tests/DefaultConfig/Content/Content.spec.ts
```

### UI Mode

For an interactive testing experience with step-by-step visualization:

```bash
npx playwright test --ui
```

Or specify a test directory:
```bash
npx playwright test --ui tests/DefaultConfig
```

> **Note**: In UI mode, if you only see the authenticate test, click on 'Projects' and select 'defaultConfig' to see all tests.

---

## Writing Tests

### Test Structure (AAA Pattern)

All tests follow the Arrange-Act-Assert pattern:

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Test Tags

Tag tests for selective execution:

```typescript
test('critical path test', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
  // Quick smoke test
});

test('comprehensive test', { tag: '@release' }, async ({ umbracoApi, umbracoUi }) => {
  // Full release validation
});
```

### Creating a New Test

Use the generator script:
```bash
npm run createTest MyFeatureName
```

This creates `tests/MyFeatureName.spec.ts` with a template.

### Test Conventions

1. **Idempotent cleanup**: Use `ensureNameNotExists()` instead of `delete()` - won't fail if item doesn't exist
2. **API for setup**: Create test data via API (faster than UI)
3. **UI for validation**: Test actual user workflows through the UI
4. **Test independence**: Each test should run standalone without depending on other tests
5. **Descriptive names**: Use clear, descriptive test and variable names
6. **Clean up**: Always clean up test data in `afterEach`

---

## Environment Configuration

The environment configuration is set up by the npm installation script, creating a `.env` file (git-ignored):

```bash
UMBRACO_USER_LOGIN=email@example.com
UMBRACO_USER_PASSWORD=yourpassword
URL=https://localhost:44339
```

To reconfigure:
```bash
npm run config
```

---

## Debugging Tests

### Pause Execution

```typescript
await page.pause();  // Opens Playwright Inspector
```

### Take Screenshots

```typescript
await page.screenshot({ path: 'debug.png' });
```

### View Traces

Failed tests automatically save traces. View them with:
```bash
npx playwright show-trace results/trace.zip
```

### Run in Debug Mode

```bash
PWDEBUG=1 npx playwright test tests/DefaultConfig/Content/Content.spec.ts
```

---

## Test Projects

The test suite is organized into multiple Playwright projects (see `playwright.config.ts`):

| Project | Description |
|---------|-------------|
| `setup` | Authentication setup (runs first) |
| `defaultConfig` | Main test suite (depends on setup) |
| `extensionRegistry` | Extension registry tests |
| `entityDataPicker` | Entity data picker tests |
| `deliveryApi` | Delivery API tests |
| `externalLoginAzureADB2C` | Azure AD B2C authentication tests |
| `unattendedInstallConfig` | Installation tests (no auth required) |
| `smtp` | Email/SMTP tests |

---

## Configuration Details

Key settings in `playwright.config.ts`:

- **Test timeout**: 30 seconds per test
- **Expect timeout**: 5 seconds for assertions
- **Retries**: 2 retries on CI (0 locally)
- **Workers**: 1 (sequential execution for state consistency)
- **Browser**: Desktop Chrome with HTTPS
- **Test identifier**: `data-mark` attribute

---

## Documentation

- [Playwright Documentation](https://playwright.dev/docs/intro)
- [Umbraco Documentation](https://docs.umbraco.com/)
- [@umbraco/acceptance-test-helpers](https://www.npmjs.com/package/@umbraco/acceptance-test-helpers) (published from this project)

---

### Overview (`src/Umbraco.Web.UI.Client/.github/README.md`)

# Guide for working with Umbraco Backoffice UI/Client

This is for you as a contributor who would like to make changes to the Backoffice UI project.

## Installation

1. Make sure you have [NodeJS](https://nodejs.org/en/download) installed.
2. Run `npm install` to install the dependencies.

## Ways to run a Front-end Development Server

A. If you will be working from VS Code, there is a script to [run the servers in debug mode](#debug-via-vs-code).

B. Otherwise choose to [run the servers individually](#run-a-front-end-server-against-a-local-umbraco-instance).

### Debug via VS Code

Open the project with VS Code, then under `Run and Debug`, you can use the `launch.json` file to start the development server. This will also start the Umbraco instance and open the browser.

You should run the task **Backoffice Launch (Vite + .NET Core)** in the **Run and Debug** panel, which will start the Vite server and the Umbraco instance. It automatically configures Umbraco (using environment variables) to use the Vite server as the Backoffice host. This task will also open a browser window, so you can start developing right away. The first time you run this task, it will take a little longer to start the Umbraco instance, but subsequent runs will be faster. Keep an eye on the Debug Console to see when the Umbraco instance is ready and then refresh the browser.

If you want to run the Vite server only, you can run the task **Backoffice Launch Vite**, which will start the Vite server only and launch a browser.

If you have an existing Vite server running, you can run the task **Backoffice Attach Vite** to attach the debugger to the Vite server.

### Run a Front-end server against a local Umbraco instance

#### 1. Configure Umbraco instance

Enable the front-end server communicating with the Backend server(Umbraco instance) you need need to correct the `appsettings.json` of your project.

For code contributions use the backend project of `/src/Umbraco.Web.UI`.
Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json` and add these 4 fields to the `Umbraco > CMS > Security`:

```json
"Umbraco": {
	"CMS": {
		"Security":{
			"BackOfficeHost": "http://localhost:5173",
			"AuthorizeCallbackPathName": "/oauth_complete",
			"AuthorizeCallbackLogoutPathName": "/logout",
			"AuthorizeCallbackErrorPathName": "/error",,
			"BackOfficeTokenCookie": {
				"SameSite": "None"
			}
		},
	},
}
```

This will override the backoffice host URL, enabling the Client to run from a different origin.

> [!NOTE]
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.

#### 2. Start Umbraco

Then start the backend server by running the command: `dotnet run` in the `/src/Umbraco.Web.UI` folder.

#### 3. Start Frontend server

Now start the frontend server by running the command: `npm run dev:server` in the `/src/Umbraco.Web.UI.Client` folder.

Finally open `http://localhost:5173` in your browser.

## Contributing

If you want to get started on contributing, please read the [contributing guidelines](/.github/contributing-backoffice.md).

A list of issues can be found on the [Umbraco-CMS Issue Tracker](https://github.com/umbraco/Umbraco-CMS/issues). Many of them are marked as `community/up-for-grabs` which means they are up for grabs for anyone to work on.

## Documentation

The documentation can be found on [Umbraco Docs](https://docs.umbraco.com/umbraco-cms).

## Advanced

### Storybook

You can test the Backoffice UI Storybook locally by running `npm run storybook`. This will start the server and open a browser window with the Storybook Web Interface.

Storybook is an excellent tool to test out UI components in isolation and to document them. It is also a great way to test the responsiveness and accessibility of the components.

### Front-end server configuration

When running `npm run dev` the default default environment and is used when running. In this case all API calls are mocked and the Umbraco Backoffice is served from the `src` folder.

This section describes how this can be used and how you can make your own configuration.

#### Environment variables

You can setup your own environment variables for the development front-end server, which is based on Vite.

The development environment supports `.env` files, so in order to set your own make a copy
of `.env` and name it `.env.local` and set the variables you need.

As an example to show the installer instead of the login screen, set the following
in the `.env.local` file to indicate that Umbraco has not been installed:

```bash
VITE_UMBRACO_INSTALL_STATUS=must-install
```

#### Run against a local backend server

If you like the default script `npm run dev` to not use mock server and run against a server of your own configuration, this can be done via Environment variables.

Create a `.env.local` file and set the following variables:

```bash
VITE_UMBRACO_API_URL=https://localhost:44339 # This will be the URL to your Umbraco instance
VITE_UMBRACO_USE_MSW=off # Indicate that you want all API calls to bypass MSW (mock-service-worker)
```

This example above, is identical to what happens when running `npm run dev:server`.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/README.md`)

# Backoffice Examples

This folder contains example packages showcasing the usage of extensions in Backoffice.

The purpose of these projects includes serving as demonstration or example for
packages, as well as testing to make sure the extension points continue
to work in these situations and to assist in developing new integrations.

---

### Overview (`templates/UmbracoExtension/.template.config/readme.md`)

# Customising the umbraco-extension template

## Source Name

The source name is set to `Umbraco.Extension`

The templating engine will rename any folder or file whose name contains `Umbraco.Extension` replacing it with the provided name. 

The templating engine will replace the text in any file as follows:

- `Umbraco.Extension` with the safe namespace for the provided name
- `Umbraco_Extension` with the safe default class name for the provided name
- `umbraco.extension` with the safe namespace for the provided name, in lower case
- `umbraco_extension` with the safe default class name for the provided name, in lower case

## Custom Replacements

The following custom placeholders have been configured in `template.json`:

- `UmbracoExtension` will be replaced with the safe namespace but without . or _
- `umbracoextension` will be replaced with the safe namespace but without . or _ , in lower case
- `umbraco-extension` will be replaced with the kebab case transform of the provided name
- `Umbraco Extension` will be replaced with a 'friendly' version of the provided name, e.g. MyProject > My Project. NB it will render a trailing space so you don't need to add one.

The first three custom placeholders have been configured to replace the text in both files and filenames.

---

### Overview (`src/Umbraco.Web.UI.Client/devops/icon-manager/README.md`)

# Icon Manager

> **Experimental**: This tool is experimental and may change or be removed without notice.

A browser-based curation UI for the Umbraco backoffice icon dictionary
(`src/packages/core/icon-registry/icon-dictionary.json`). It loads the
dictionary, lets you edit metadata (keywords, groups, names), and exports a
new JSON blob to download.

The tool is its own self-contained Vite project — it has its own
`package.json`, lockfile, and `node_modules`. It only reads the parent
backoffice source tree; it does not write back automatically.

## Setup

```bash
npm install
```

## Usage

From this folder:

```bash
npm run dev
```

Or from the backoffice client root (`src/Umbraco.Web.UI.Client/`):

```bash
npm run dev:icon-manager
```

Both open the tool at `/icon-manager.html` in a browser.

## Applying changes

1. Edit icons in the UI.
2. Click **Export JSON** — the browser downloads an updated dictionary.
3. Replace `src/packages/core/icon-registry/icon-dictionary.json` with the
   downloaded file.
4. Re-run `npm run generate:icons` from the backoffice client root to
   regenerate the icon modules consumed at runtime.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/block-custom-view/README.md`)

# Property Dataset Dashboard Example

This example is a work in progress example of how to write a property editor.

This example covers a few points:

- Using an existing Property Editor Schema

---

### Overview (`src/Umbraco.Web.UI.Client/examples/block-workspace-context/README.md`)

# Consuming Variant Context in a Block Workspace View Example
This example demonstrates how to consume and display the contextual variant information (culture and segment) from the `UMB_VARIANT_CONTEXT` within a block workspace view extension.

## What this example includes
- **Workspace View** – A custom workspace view that reads the current variant context (culture and segment) and displays it to the user.

## How it works
The workspace view extension uses the `UMB_VARIANT_CONTEXT` context token to access the current variant's culture and segment. This allows the extension to react to changes in the variant context and display the relevant information.
This pattern is useful for extension developers who need to make their workspace views aware of the current variant, enabling context-sensitive UI and logic.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/collection/README.md`)

# Collection Example

This example demonstrates how to register a collection with collection views.

The example includes:

- Collection Registration
- Collection Repository
- Collection Pagination
- Table Collection View
- Card Collection View
- Collection as a Dashboard
- Collection as a Workspace View

TODO: This example is not complete, it is missing the following features:

- Collection Action
- Collection Filtering
- Entity Actions
- Selection + Bulk Actions

---

### Overview (`src/Umbraco.Web.UI.Client/examples/custom-validation-workspace-context/README.md`)

# Custom Validation Workspace Context Example

This example demonstrates how you can append extra validation messages to the Validation Context based on the value of a Property of a property Editor that this Customization is not interested in overriding/replacing.

See this having an effect on the "Rich Text Editor" page, where too long words are invalid.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/README.md`)

# Property Dataset Dashboard Example

This example demonstrates the essence of the Property Dataset.

This dashboard implements such, to display a few selected Property Editors and bind the data back to the Dashboard.


## SVG code of Icons

Make sure to use currentColor for fill or stroke color, as that will make the icon adapt to the font color of where its begin used.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/icons/README.md`)

# Icons Example

This example demonstrates how to registerer your own icons.

Currently they have to be made as JavaScript files that exports an SVG string.

Declared as part of a Icon Dictionary in a JavaScript file.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/manifest-picker/README.md`)

# Manifest Picker Example

This example demonstrates the Manifest picker input UI.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/README.md`)

# Manipulate Document Property Value Permissions

This example demonstrates the essence of the Document Property Value Permissions by using three Workspace Actions.

Use the toggles to change rules for the Rich Text Editor in the 'All Properties' Document.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/menu-item/README.md`)

# Menu Item Example

This example demonstrates how to register a Menu Item.

The example includes:

- Action Menu Item Registration
- Link Menu Item Registration
- Entity Menu Item Registration

---

### Overview (`src/Umbraco.Web.UI.Client/examples/multi-brand-block-views/README.md`)

# Multi Brand Block Views Example

Custom Block Grid views for the blocks of the "Multi Brand Clothing Shop" mock data set.

Each view renders its block as it would look on the site, instead of the default block card. The styling is editorial and gallery-like: image-led cards, bold tightly-set headings, small uppercase labels and hairline borders, using UUI colour variables so it follows the theme.

- **Hero Block** - hero image with headline, subheadline and a link button.
- **Image Block** - the image with its caption.
- **Text Block** - the rendered rich text, sanitized before it is rendered.
- **Product Teaser** - the picked product's picture, name, price and stock status, plus the block's label.
- **Article Teaser** - the picked article's hero image, title and teaser.

The two teaser views also apply when the teasers are inserted into a Rich Text Editor, as in the article text.

- **One / Two Column Layout** - a dashed frame with a label, and extra spacing around the areas.

## How it works

- Every view is registered as a `blockEditorCustomView` extension for one Element Type alias, limited to the `block-grid` editor.
- The block's own values arrive as `content`, already resolved for the culture being edited.
- The teasers only hold a reference to a document, so they request the referenced document and its media, using the current culture from the variant context.

## Run it

1. `npm run example:mock` and pick `multi-brand-block-views`.
2. The example switches the mock data set to "Multi Brand Clothing Shop" for you and hides the mock set switcher.
3. Open "Little Ones Clothing Shop" or "The Outdoor Shop" in the Content section.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/property-editor/README.md`)

# Property Dataset Dashboard Example

This example is a work in progress example of how to write a property editor.

This example covers a few points:

- Using an existing Property Editor Schema

---

### Overview (`src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/README.md`)

# Property Dataset Dashboard Example

This example demonstrates how to set up the Sorter and how it can be used in nested setups.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/README.md`)

# Property Dataset Dashboard Example

This example demonstrates how to set up the Sorter, and how it can be linked with another Sorter.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/tree/README.md`)

# Tree Example

This example demonstrates how to register a tree

The example includes:

- Tree + Tree Item Registration
- Tree Repository + Store Registration
- A Dashboard to show how to render a tree anywhere in the backoffice
- How to use the tree in the sidebar menu

---

### Overview (`src/Umbraco.Web.UI.Client/examples/ufm-custom-component/README.md`)

# UFM Custom Component

This example demonstrates how to write a custom Umbraco-Flavored Markdown (UFM) component.

https://docs.umbraco.com/umbraco-cms/reference/umbraco-flavored-markdown#custom-ufm-components

---

### Overview (`src/Umbraco.Web.UI.Client/examples/workspace-context-counter/README.md`)

# Workspace Context Communication Across Extensions Example

The Workspace Context serves as the central communication hub for all Workspace extensions. In this example, the Context manages a counter that can be manipulated and displayed by different Extension Types, showcasing the power of shared state management in Workspace extensions.

## Extension types included

This example includes:

- **Workspace Context** - Manages shared counter state and provides communication between extensions
- **Workspace Action** - Primary "Increment" button that increases the counter value
- **Workspace Action Menu Item** - "Reset Counter" dropdown option that resets the counter to zero
- **Workspace View** - Dedicated tab that displays the current counter value
- **Workspace Footer App** - Status indicator showing the counter value in the workspace footer

## How it works

All extensions communicate through the shared Workspace Context. When you increment or reset the counter using the actions, the Workspace view and Footer app update to show the new value, demonstrating reactive state management across Workspace extensions.

This pattern shows how various Workspace extensions can communicate via a Workspace Context as their mediator.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/workspace-context-initial-name/README.md`)

# Workspace Context for setting an Initial Name

This example of a Workspace Context demonstrates how to manipulate the name of the workspaces entity.

---

### Overview (`src/Umbraco.Web.UI.Client/examples/workspace-view-hint/README.md`)

# Workspace View Badge Example

This example demonstrates the essence of the Workspace View Navigation Context. And how to append a Status that will be displayed as a badge on the Workspace Views Tab Navigation.

---

### Overview (`src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/README.md`)

# Umbraco static assets

The files in this directory are static assets that are used by the Umbraco backoffice, installer, public-facing sites, and login screen. These files are served by the Umbraco backend and are not intended to be used by the front-end of your website.

## Structure

The assets are structured in the following way:

| Name | Description                                                        | Usage                                                                           |
| ---- |--------------------------------------------------------------------|---------------------------------------------------------------------------------|
| `login.jpg` | The background image for the login screen.                         | /umbraco/management/api/v1/security/back-office/graphics/login-background       |
| `logo.svg` | The Umbraco logo for the Backoffice and other public facing sites. | /umbraco/management/api/v1/security/back-office/graphics/logo                   |
| `logo_dark.svg` | The Umbraco logo in dark mode for the login screen.                | /umbraco/management/api/v1/security/back-office/graphics/login-logo-alternative |
| `logo_light.svg` | The Umbraco logo in light mode for the login screen.               | /umbraco/management/api/v1/security/back-office/graphics/login-logo             |

All assets are linked up through the BackOfficeGraphicsController which uses the constants defined in [ContentSettings](../../../../Umbraco.Core/Configuration/Models/ContentSettings.cs).

---

### Overview (`src/Umbraco.Web.UI.Client/examples/menu-item/tree/README.md`)

#Tree Menu Item

See the Tree Example of how to register a tree and use it as a menu item in the sidebar.

---

### Overview (`src/Umbraco.Web.UI.Client/src/packages/core/validation/README.md`)

# Backoffice Validation System

The validation system works around a system of Validation Messages, provided via Validation Contexts and connected to the application via Validators.

The system both supports handling front-end validation, server-validation and other things can as well be hooked into it.

## Validation Context

The core of the system is a Validation Context, this holds the messages and more.

## Validation Messages

A Validation message consist of a type, path and body. This typically looks like this:

```
{
	type: "client",
	path: "$.values[?(@.alias == 'my-property-alias')].value",
	message: "Must contain at least 3 words"
}
```

Because each validation issue is presented in the Validation Context as a Message, its existence will be available for anyone to observe.
One benefit of this is that Elements that are removed from screen can still have their validation messages preventing the submission of a dataset.
As well Tabs and other navigation can use this to be highlighted, so the user can be guide to the location.

### Path aka. Data Path

The Path also named Data Path, A Path pointing to the related data in the model.
A massage uses this to point to the location in the model that the message is concerned.

The following models headline can be target with this path:

Data:
```
{
	settings: {
		title: 'too short'
	}
}
```

JsonPath:
```
"$.settings.title"
```

The following example shows how we use JsonPath Queries to target entries of an array:

Data:
```
{
	values: [
		{
			alias: 'my-alias',
			value: 'my-value'
		}
	]
}
```

JsonPath:
```
"$.values.[?(@.alias == 'my-alias')].value"
```

Paths are based on JSONPath, using JSON Path Queries when looking up data of an Array. Using Queries enables Paths to not point to specific index, but what makes a entry unique.

Messages are set via Validators, which is the glue between a the context and a validation source.

## Validators

Messages can be set by Validators, a Validator gets assigned to the Validation Context. Making the Context aware about the Validators.

When the validation context is asked to Validate, it can then call the `validate` method on all the Validators.

The Validate method can be async, meaning it can request the server or other way figure out its state before resolving.

We provide a few built in Validators which handles most cases.

### Form Control Validator

This Validator binds a Form Control Element with the Validation Context. When the Form Control becomes Invalid, its Validation Message is appended to the Validation Context.

Notice this one also comes as a Lit Directive called `umbBindToValidation`.

Also notice this does not bind server validation to the Form Control, see `UmbBindServerValidationToFormControl`

### Server Model Validator

This Validator can asks a end-point for validation of the model.

The Server Model Validator stores the data that was send to the server on the Validation Context. This is then later used by Validation Path Translators to convert index based paths to Json Path Queries.

This is needed to allow the user to make changes to the data, without loosing the accuracy of the messages coming from server validation.

## Validation Path Translator

Validation Path Translator translate Message Paths into a format that is independent of the actual current data. But compatible with mutations of that data model.
This enables the user to retrieve validation messages from the server, and then the user can insert more items and still have the validation appearing in the right spots.
This would not be possible with index based paths, which is why we translate those into JSON Path Queries.

Such conversation could be from this path:
```
"$.values.[5].value"
```

To this path:
```
"$.values.[?(@.alias == 'my-alias')].value"
```

Once this path is converted to use Json Path Queries, the Data can be changed. The concerned entry might get another index. Without that affecting the accuracy of the path.

### Late registered Path Translators

Translators can be registered late. This means that a Property Editor that has a complex value structure, can register a Path Translator for its part of the data. Such Translator will appear late because the Property might not be rendered in the users current view, but first when the user navigates there.
This is completely fine, as messages can be partly translated and then enhanced by late coming Path Translators.

This fact enables a property to observe if there is any Message Paths that start with the same path as the Data Path for the Property. In this was a property can know that it contains a Validation Message without the Message Path begin completely translated.



## Binders

Validators represent a component of the Validation to be considered, but it does not represent other messages of its path.
To display messages from a given data-path, a Binder is needed. We bring a few to make this happen:

### UmbBindServerValidationToFormControl

This binder takes a Form Control Element and a data-path.
The Data Path is a JSON Path defining where the data of this input is located in the model sent to the server.

```
	this.#validationMessageBinder = new UmbBindServerValidationToFormControl(
		this,
		this.querySelector('#myInput"),
		"$.values.[?(@.alias == 'my-input-alias')].value",
	);
```

Once the binder is initialized you need to keep it updated with the value your form control represents. Notice we do not recommend using events from the form control to notify about the changes.
Instead observe the value in of your data model.

This example is just a dummy example of how that could look:
```
	this.observe(
		this.#value,
		(value) => {
			this.#validationMessageBinder.value = value;
		},
	);
```

---

### Src Umbraco.Web.UI.Client Docs Agentic Workflow (`src/Umbraco.Web.UI.Client/docs/agentic-workflow.md`)

# Agentic Workflow
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)

---

### Phase 1: Analysis & Planning

**Understand Requirements**:
1. Read user request carefully
2. Identify acceptance criteria
3. Ask clarifying questions if ambiguous
4. Determine scope (new feature, bug fix, refactor, etc.)

**Research Codebase**:
1. Find similar implementations in codebase
2. Identify patterns and conventions used
3. Locate relevant packages and modules
4. Review existing tests for similar features
5. Check for existing utilities or helpers

**Identify Technical Approach**:
1. Which packages need changes?
   - New package or modify existing?
   - Core infrastructure or feature package?
2. What patterns to use?
   - Web Component, Controller, Repository, Context?
   - Extension type? (dashboard, workspace, modal, etc.)
3. Dependencies needed?
   - New npm packages?
   - Internal package dependencies?
4. API changes needed?
   - New OpenAPI endpoints?
   - Changes to existing endpoints?

**Break Down Implementation**:
1. **Models/Types** - Define TypeScript interfaces and types
2. **API Client** - Update OpenAPI client if backend changes
3. **Repository** - Data access layer
4. **Store/Context** - State management
5. **Controller** - Business logic
6. **Element** - UI component
7. **Manifest** - Extension registration
8. **Tests** - Unit and integration tests
9. **Documentation** - JSDoc and examples
10. **Storybook** (if applicable) - Component stories

**Consider Architecture**:
- Does this follow project patterns?
- Are dependencies correct? (libs → packages → apps)
- Will this create circular dependencies?
- Is this extensible for future needs?
- Performance implications?

**Document Plan**:
- Write brief implementation plan
- Identify potential issues or blockers
- Get approval from user if significant changes

### Phase 2: Incremental Implementation

**For New Feature (Component/Package)**:

**Step 1: Define Types**
```typescript
// 1. Create model interface
export interface UmbMyModel {
	id: string;
	name: string;
	description?: string;
}

// 2. Create manifest type
export interface UmbMyManifest extends UmbManifestBase {
	type: 'myType';
	// ... specific properties
}
```

**Verify**: TypeScript compiles, no errors

**Step 2: Create Repository**
```typescript
// 3. Data access layer
export class UmbMyRepository {
	async requestById(id: string) {
		// Fetch from API
		// Return { data } or { error }
	}
}
```

**Verify**: Repository compiles, basic structure correct

**Step 3: Create Store/Context**
```typescript
// 4. State management
export class UmbMyStore extends UmbStoreBase {
	// Observable state
}

export const UMB_MY_CONTEXT = new UmbContextToken<UmbMyContext>('UmbMyContext');

export class UmbMyContext extends UmbControllerBase {
	#repository = new UmbMyRepository();
	#store = new UmbMyStore(this);

	// Public API
}
```

**Verify**: Context compiles, can be consumed

**Step 4: Create Element**
```typescript
// 5. UI component
@customElement('umb-my-element')
export class UmbMyElement extends UmbLitElement {
	#context?: typeof UMB_MY_CONTEXT.TYPE;

	constructor() {
		super();
		this.consumeContext(UMB_MY_CONTEXT, (context) => {
			this.#context = context;
		});
	}

	render() {
		return html`<div>My Element</div>`;
	}
}
```

**Verify**: Element renders in browser, no console errors

**Step 5: Wire Up Interactions**
```typescript
// 6. Connect user interactions to logic
@customElement('umb-my-element')
export class UmbMyElement extends UmbLitElement {
    @state()
    private _data?: UmbMyModel;

	async #handleClick() {
		const { data, error } = await this.#context?.load();
		if (error) {
			this._error = error;
			return;
		}
		this._data = data;
	}

	render() {
		return html`
			<uui-button @click=${this.#handleClick} label="Load"></uui-button>
			${this._data ? html`<p>${this._data.name}</p>` : nothing}
		`;
	}
}
```

**Verify**: Interactions work, data flows correctly

**Step 6: Add Extension Manifest**
```typescript
// 7. Register extension
export const manifest: UmbManifestMyType = {
	type: 'myType',
	alias: 'My.Extension',
	name: 'My Extension',
	element: () => import('./my-element.element.js'),
};
```

**Verify**: Extension loads, manifest is valid

**Step 7: Write Tests**
```typescript
// 8. Unit tests
describe('UmbMyElement', () => {
	it('should render', async () => {
		const element = await fixture(html`<umb-my-element></umb-my-element>`);
		expect(element).to.exist;
	});

	it('should load data', async () => {
		// Test data loading
	});
});
```

**Verify**: Tests pass

**Step 8: Add Error Handling**
```typescript
// 9. Handle errors gracefully
async #handleClick() {
	try {
		this._loading = true;
		const { data, error } = await this.#context?.load();
		if (error) {
			this._error = 'Failed to load data';
			return;
		}
		this._data = data;
	} catch (error) {
		this._error = 'Unexpected error occurred';
		console.error('Load failed:', error);
	} finally {
		this._loading = false;
	}
}
```

**Step 9: Add localization**
```
/* Detailed source-code truncated for AI context efficiency. */
```bash
# 1. Run all tests
npm test

# 2. Run lint
npm run lint:errors

# 3. Type check
npm run compile

# 4. Build
npm run build

# 5. Check circular dependencies
npm run check:circular

# 6. Validate package exports
npm run package:validate
```

**Code Review Checklist**:
- [ ] Code follows style guide
- [ ] All tests pass
- [ ] New tests added for new code
- [ ] TypeScript types are correct
- [ ] No `any` types (or justified)
- [ ] JSDoc comments on public APIs
- [ ] Error handling in place
- [ ] Edge cases handled
- [ ] No console.log in production code
- [ ] Accessibility considered (for UI components)
- [ ] Performance acceptable
- [ ] No memory leaks (subscriptions cleaned up)
- [ ] Follows existing patterns
- [ ] No circular dependencies introduced

**Documentation**:
- [ ] JSDoc on public APIs
- [ ] Update README if needed
- [ ] Add Storybook story (for components)
- [ ] Add example code (for APIs)
- [ ] Update CLAUDE.md (for significant changes)

### Reasoning & Decision-Making

**Architecture Decisions**:
- **Follow existing patterns** - Don't invent new patterns unless necessary
- **Use Context API** - For cross-component state and dependencies
- **Controllers for logic** - Separate business logic from UI
- **Repositories for data** - Abstract data access
- **Manifests for extensions** - Register features via extension system

**Component Design Decisions**:
- **Small components** - Break complex UI into smaller components
- **Single responsibility** - Each component does one thing well
- **Composition** - Combine small components to build complex UI
- **Props down, events up** - Data flows down via properties, changes flow up via events

**State Management Decisions**:
- **Local state** - `@state()` for component-specific state
- **Shared state** - Context and stores for cross-component state
- **Observables** - RxJS for reactive data streams
- **Immutability** - Don't mutate state, create new objects

**API Design Decisions**:
- **Async by default** - All data operations are async
- **Error objects** - Return `{ data, error }` not throw exceptions
- **TypeScript types** - Strong typing for all APIs
- **Consistent naming** - Follow existing naming conventions

### Think-Aloud Process

**Example: "I need to add a bulk action for content"**

**Analysis**:
> "I need to add a bulk action for content. Let me think through this:
>
> 1. What is a bulk action? It's an action that operates on multiple items.
> 2. Where do bulk actions appear? In collection views (document tree, list view).
> 3. What pattern is used? Let me check existing bulk actions...
> 4. Found: Block editor has bulk actions. Let me review that code.
> 5. Architecture: Bulk actions are registered via manifests (type: 'entityBulkAction')
> 6. Implementation needs:
>    - Manifest to register action
>    - Element to show confirmation dialog
>    - Repository method to perform bulk operation
>    - Permission check (can user do this?)
> 7. Which package? This goes in `packages/documents/documents/` since it's document-specific.
> 8. Dependencies: Need `@umbraco-cms/backoffice/entity-bulk-action`, modal, repository."

**Planning**:
> "Implementation steps:
> 1. Create bulk action manifest in documents package
> 2. Create modal element for confirmation
> 3. Add repository method for bulk operation
> 4. Add permission check
> 5. Wire up action to modal
> 6. Test with multiple documents
> 7. Handle errors (partial success, permissions, etc.)
> 8. Add loading state
> 9. Show success/error notification
> 10. Write tests"

**Implementation**:
> "Starting with step 1: Create manifest.
> Looking at existing bulk actions, I see the pattern uses `UmbEntityBulkActionBase`.
> Let me create the manifest following that pattern..."

### Error Recovery

**If Tests Fail**:
1. Read the error message carefully
2. Reproduce the failure locally
3. Debug with console.log or debugger
4. Understand root cause (don't just fix symptoms)
5. Fix the underlying issue
6. Verify fix doesn't break other tests
7. Add test for the bug (if it was a real bug)

**If Architecture Feels Wrong**:
1. Pause and reconsider
2. Review similar implementations in codebase
3. Discuss with team (via PR comments)
4. Don't force a pattern that doesn't fit
5. Refactor if needed

**If Introducing Breaking Changes**:
1. Discuss with user first
2. Document the breaking change
3. Provide migration path
4. Update CHANGELOG
5. Consider deprecation instead

**If Stuck**:
1. Ask for clarification from user
2. Review documentation
3. Look at similar code in repository
4. Break problem into smaller pieces
5. Try a simpler approach first

### Quality Gates Checklist

Before marking work as complete:

**Code Quality**:
- [ ] All new code has unit tests
- [ ] Integration tests for workflows (if applicable)
- [ ] All tests pass (including existing tests)
- [ ] Code follows style guide (ESLint passes)
- [ ] Prettier formatting correct
- [ ] No TypeScript errors
- [ ] JSDoc comments on public functions
- [ ] No `console.log` in production code
- [ ] No commented-out code

**Security**:
- [ ] Input validation in place
- [ ] No XSS vulnerabilities
- [ ] No sensitive data logged
- [ ] User input sanitized
- [ ] Permissions checked (for sensitive operations)

**Performance**:
- [ ] No obvious performance issues
- [ ] No memory leaks (subscriptions cleaned up)
- [ ] Efficient algorithms used
- [ ] Large lists use virtualization (if needed)

**Architecture**:
- [ ] Follows existing patterns
- [ ] No circular dependencies
- [ ] Correct package dependencies
- [ ] Extension manifest correct (if applicable)

**Documentation**:
- [ ] JSDoc on public APIs
- [ ] README updated (if needed)
- [ ] Examples added (if new API)
- [ ] Breaking changes documented

**User Experience** (for UI changes):
- [ ] Accessible (keyboard navigation, screen readers)
- [ ] Responsive (works on different screen sizes)
- [ ] Loading states shown
- [ ] Errors handled gracefully
- [ ] Success feedback provided

### Communication

**After Each Phase**:
- Summarize what was implemented
- Highlight key decisions and why
- Call out any blockers or questions
- Show progress (code snippets, screenshots for UI)

**When Making Decisions**:
- Explain reasoning
- Reference existing patterns
- Call out trade-offs
- Ask for confirmation on significant changes

**When Blocked**:
- Clearly state the blocker
- Explain what you've tried
- Ask specific questions
- Suggest potential approaches

**When Complete**:
- Summarize what was delivered
- Note any deviations from plan (and why)
- Highlight testing performed
- Mention any follow-up work needed

### For Large Features

**Multi-Session Features**:
1. Create feature branch (if spanning multiple PRs)
2. Break into multiple PRs if very large:
   - PR 1: Infrastructure (types, repository, context)
   - PR 2: UI components
   - PR 3: Extensions and integration
3. Keep main stable - only merge completed, tested features
4. Consider feature flags for gradual rollout
5. Document progress and remaining work in PR description

**Coordination**:
- Update PR description with progress
- Use GitHub task lists in PR description
- Comment on PR with status updates
- Request early feedback on architecture

---

---

### Src Umbraco.Web.UI.Client Docs Architecture (`src/Umbraco.Web.UI.Client/docs/architecture.md`)

# Architecture

---

### Technology Stack

> Exact versions are in `package.json` (root and workspace packages). Only major versions that affect API usage are listed here.

- **Runtime**: Node.js >=22, npm >=10 (see `engines` in `package.json`)
- **Language**: TypeScript 5.x (strict mode, ESM — all imports must use `.js` extensions)
- **Framework**: **Lit 3** — Web Components with reactive templates, decorators, and `html`/`css` tagged template literals
- **UI Library**: **@umbraco-ui/uui 2.x** — 80+ Web Components (`<uui-button>`, `<uui-input>`, `<uui-box>`, etc.). Always prefer UUI components over native HTML or custom implementations.
- **Rich Text**: **TipTap 3** — ProseMirror-based editor (see `src/packages/tiptap/`)
- **Build**: Vite 7 (config in `vite.config.ts`)
- **State**: RxJS 7 wrapped by UmbState classes — see [Core Primitives](./core-primitives.md)
- **Context/DI**: DOM-event-based Context API — see [Core Primitives](./core-primitives.md)
- **Testing**: @web/test-runner + Playwright, @open-wc/testing — see [Testing](./testing.md)
- **Mocking**: MSW 2 — see [Testing](./testing.md)
- **Code Quality**: ESLint 9 (flat config), Prettier
- **Key Libraries**: Luxon 3, Monaco Editor (code editing), DOMPurify 3 (sanitization — see [Security](./security.md)), Marked (Markdown→HTML), SignalR (real-time server events)

### Application Type

SPA packaged as npm library (`@umbraco-cms/backoffice`) for typings and built as a bundle copied to `src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice`. Provides extensible Web Components, API libraries, TypeScript types, and a manifest-based extension system.

### Architecture Pattern

**Modular Package Architecture**:

```
src/
├── apps/              # Entry points (app, backoffice, installer, preview, upgrader)
├── libs/              # Core API libraries (context-api, controller-api, element-api, extension-api, observable-api, localization-api, etc.)
├── packages/          # Feature packages (50+)
│   ├── core/         # UI framework (auth, http, router, modals, notifications, etc.)
│   ├── documents/    # Document types & editing
│   ├── media/        # Media management
│   └── ...           # 30+ more domain packages
├── external/          # Dependency wrappers (lit, rxjs, luxon, monaco-editor, etc.)
├── mocks/             # MSW mock handlers & test data
└── assets/            # Static assets (fonts, images, localization)
```

---

## Design Philosophy

**Extension-first architecture**: HQ builds extensions the same way third-party developers do. All UI (sections, dashboards, property editors, workspaces, trees, actions) is registered via manifests. Any default behavior can be replaced, overridden, or removed. Consumers can use any framework that outputs a Web Component.

*Note: The extension-first mindset is the guiding principle, and the frontend fully embraces this by treating all features as manifest-registered, replaceable extensions.*

Extension-first also constrains how shared code is written: it must serve any implementation of an extension point, not only the one that prompted the change. See [General-Purpose by Default](../../../CLAUDE.md#2-general-purpose-by-default).

---

## Developer Roles & Import Boundaries

The directory a file lives in determines which import rules apply:

| Role | Directory | Can import from | Cannot import from |
|------|-----------|----------------|--------------------|
| Library Developer | `src/libs/` | Other libs | Packages, apps |
| Core Developer | `src/packages/core/` | Libs, other core modules | Non-core packages |
| HQ Package Developer | `src/packages/*` (not core) | Core, libs, other packages (carefully) | — |

**Library Developer** (`src/libs/`): Framework-agnostic infrastructure. No knowledge of the backoffice, CMS, or UI framework. Third-party dependencies included carefully.

**Core Developer** (`src/packages/core/`): UI framework implementation — extension registry, routing, modals, notifications, workspace infrastructure. Does not implement CMS-specific features.

**HQ Package Developer** (`src/packages/*` except `core/`): CMS feature packages (documents, media, members, templating). Cross-package imports increase coupling — minimize them. Use public `index.ts` exports, never import another package's internal files.

---

## The Package System

Each directory under `src/packages/` is a self-contained domain module. Design principle: each package could theoretically be uninstalled independently. `core` provides foundational APIs; every other package builds on top.

### Module Exports

Packages expose subpath exports (e.g., `@umbraco-cms/backoffice/dashboard`, `@umbraco-cms/backoffice/media`). Each maps to an `index.ts` barrel file. See `package.json` exports field for the full list.

---

## Design Patterns

1. **Web Components** - Custom elements with Shadow DOM
2. **Context API** - DOM-event-based dependency injection (provider/consumer)
3. **Controller Pattern** - Lifecycle-aware logic via `UmbControllerBase`
4. **Extension System** - Manifest-based plugin architecture
5. **Observable Pattern** - Reactive state via UmbState classes
6. **Repository Pattern** - Data access abstraction
7. **Mixin Pattern** - Composable behaviors (`UmbElementMixin`, etc.)

### Extension Registry

All UI and a few other customizations are registered as **Extension Manifests** — plain objects declaring type, alias, behavior, and activation conditions. The registry is mutable at runtime — extensions can be added, removed, or replaced.

For the full manifest shape, alias conventions, registration methods, and how aliases connect extensions, see **[Manifests & Aliases](./manifests.md)**.

### Kinds

Kinds are **generic, reusable manifests of an extension type**. A kind can provide a pre-built `element`, `api`, or both for a specific purpose. Extensions reference a kind to inherit its implementation, then customize behavior through `meta`.

**How it works**: The registry merges kind defaults with the extension manifest. Extension properties override kind properties; `meta` is shallow-merged (extension meta extends/overrides kind meta).

**Defining a kind** (in a `*.kind.ts` file):

```typescript
export const manifest: UmbExtensionManifestKind = {
  type: 'kind',
  alias: 'Umb.Kind.EntityAction.Delete',
  matchKind: 'delete',
  matchType: 'entityAction',
  manifest: {
    type: 'entityAction',
    kind: 'delete',
    api: () => import('./delete.action.js'),
    meta: {
      icon: 'icon-trash',
      label: '#actions_delete',
      itemRepositoryAlias: '',       // to be filled by each integration
      detailRepositoryAlias: '',
    },
  },
};
```

**Using a kind** — the extension only specifies what differs:

```typescript
{
  type: 'entityAction',
  kind: 'delete',                    // inherits element, api, icon, label from the kind
  alias: 'Umb.EntityAction.DocumentType.Delete',
  name: 'Delete Document-Type Entity Action',
  forEntityTypes: [UMB_DOCUMENT_TYPE_ENTITY_TYPE],
  meta: {                            // customize via meta
    itemRepositoryAlias: UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS,
    detailRepositoryAlias: UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS,
  },
}
```

**Best practices:**
- Kinds are for reusability — use them when multiple extensions share the same element/api implementation with only meta differences.
- Any package (core or feature) can register kinds. Core provides common kinds (delete, moveTo, duplicate, default section, default workspace, etc.); feature packages can define domain-specific kinds.
- Individual integrations customize behavior through `meta` — not by overriding `element` or `api` (unless replacing the implementation entirely).
- Kinds can extend other kinds by spreading their manifest: `manifest: { ...UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST.manifest, kind: 'delete', ... }`.
- **Changing a kind affects all extensions that use it.** Treat kind implementations (element, api, meta shape) as a shared contract — changes have a wide blast radius across packages.

### Context API, Observable State & Controllers

These core primitives are documented in detail in **[Core Primitives](./core-primitives.md)**. In brief:

- **Context API** — DOM-event-based dependency injection. Providers expose typed contexts to descendant elements via `provideContext()`, consumers access them via `consumeContext()` or `getContext()`.
- **Observable State** — Reactive state containers (`UmbStringState`, `UmbArrayState`, `UmbObjectState`, etc.). Elements subscribe via `this.observe()` which handles lifecycle automatically.
- **Controllers** — `UmbControllerBase` for logic not tied to rendering. Attach to a host element and participate in its lifecycle.

**Important**: Avoid using RxJS operators (`map`, `filter`, `combineLatest`, `switchMap`) directly. Use UmbState classes and `observe()` — they handle subscription lifecycle automatically. RxJS is an implementation detail.

### Repository Pattern & Data Flow

No element or context should call the Management API directly. All data flows through repositories:

`Element` → observes → `Context` → calls → `Repository` → delegates to → `Data Source` → calls → `Generated API Client`

See **[Data Flow](./data-flow.md)** for the complete implementation pattern with base classes, `tryExecute()`, and a worked example.

---

The backoffice is independently-packaged Web Components glued by the Extension Registry, communicating via the Context API, with state managed through Observables — all built to be replaceable and extensible by default.

---

### Src Umbraco.Web.UI.Client Docs Clean Code (`src/Umbraco.Web.UI.Client/docs/clean-code.md`)

# Clean Code
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)

---


### Function Design

**Function Length**:
- Target: ≤30 lines per function
- Max: 50 lines (enforce via code review)
- Extract complex logic to separate functions
- Use early returns to reduce nesting

**Single Responsibility**:
```typescript
// Good - Single responsibility
private _validateName(name: string): boolean {
	return name.length > 0 && name.length <= 100;
}

private _sanitizeName(name: string): string {
	return name.trim().replace(/[<>]/g, '');
}

// Bad - Multiple responsibilities
private _processName(name: string): { valid: boolean; sanitized: string } {
	// Doing too much in one function
}
```

**Descriptive Names**:
- Use verb names for functions: `loadData`, `validateInput`, `handleClick`
- Boolean functions: `is`, `has`, `can`, `should` prefix
- Event handlers: `handle`, `on` prefix: `handleSubmit`, `onClick`

**Parameters**:
- Limit to 3-4 parameters
- Use options object for more parameters:

```typescript
// Good - Options object
interface LoadOptions {
	id: string;
	includeChildren?: boolean;
	depth?: number;
	culture?: string;
}

private _load(options: LoadOptions) { }

// Bad - Too many parameters
private _load(id: string, includeChildren: boolean, depth: number, culture: string) { }
```

**Early Returns**:

```typescript
// Good - Early returns reduce nesting
private _validate(): boolean {
	if (!this._data) return false;
	if (!this._data.name) return false;
	if (this._data.name.length === 0) return false;
	return true;
}

// Bad - Nested conditions
private _validate(): boolean {
	if (this._data) {
		if (this._data.name) {
			if (this._data.name.length > 0) {
				return true;
			}
		}
	}
	return false;
}
```

### Class Design

**Single Responsibility**:
- Each class should have one reason to change
- Controllers handle one aspect of behavior
- Repositories handle one entity type
- Components handle one UI concern

**Small Classes**:
- Target: <300 lines per class
- Extract complex logic to separate controllers/utilities
- Use composition over inheritance

**Encapsulation**:

```typescript
export class UmbMyElement extends LitElement {
	// Public API - reactive properties
	@property({ type: String })
	value = '';

	// Private state
	@state()
	private _loading = false;

	// Private fields (not reactive)
	#controller = new UmbMyController(this);

	// Private methods
	private _loadData() { }
}
```

### SOLID Principles (Adapted for TypeScript/Lit)

**S - Single Responsibility**:
- One component = one UI responsibility
- One controller = one behavior responsibility
- One repository = one entity type

**O - Open/Closed**:
- Extend via composition and mixins
- Extension API for plugins
- Avoid modifying existing components, create new ones

**L - Liskov Substitution**:
- Subclasses should honor base class contracts
- Use interfaces for polymorphism

**I - Interface Segregation**:
- Small, focused interfaces
- Use TypeScript `interface` for contracts

**D - Dependency Inversion**:
- Depend on abstractions (interfaces) not concrete classes
- Use Context API for dependency injection
- Controllers receive dependencies via constructor

### Dependency Injection

**Context API** (Preferred):

```typescript
export class UmbMyElement extends UmbElementMixin(LitElement) {
	#authContext?: UmbAuthContext;

	constructor() {
		super();

		// Consume context (dependency injection)
		this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
			this.#authContext = context;
		});
	}
}
```

**Controller Pattern**:

```typescript
export class UmbMyController extends UmbControllerBase {
	#repository: UmbContentRepository;

	constructor(host: UmbControllerHost, repository: UmbContentRepository) {
		super(host);
		this.#repository = repository;
	}
}

// Usage
#controller = new UmbMyController(this, new UmbContentRepository());
```

### Event Listener Cleanup Pattern

**Critical for Memory Leak Prevention**:

When adding event listeners, you must ensure they can be properly removed. Using `.bind(this)` creates a new function reference each time, making cleanup impossible.

**Wrong Pattern - Memory Leak**:

```typescript
export class UmbMyElement extends LitElement {
	constructor() {
		super();
		// ❌ BAD: Each .bind(this) creates a NEW function reference
		window.addEventListener('storage', this.#onStorageEvent.bind(this));
	}

	disconnectedCallback() {
		// ❌ This creates ANOTHER function reference - doesn't remove the original listener!
		window.removeEventListener('storage', this.#onStorageEvent.bind(this));
	}

	#onStorageEvent(evt: StorageEvent) {
		// Handler logic
	}
}
```

**Correct Pattern - Arrow Function Property**:

```typescript
export class UmbMyElement extends LitElement {
	// ✅ GOOD: Arrow function property maintains consistent reference
	#onStorageEvent = async (evt: StorageEvent) => {
		// Handler logic
		if (evt.key === 'myKey') {
			await this.handleStorageChange();
		}
	};

	constructor() {
		super();
		// ✅ GOOD: No .bind() needed - arrow function preserves 'this'
		window.addEventListener('storage', this.#onStorageEvent);
	}

	disconnectedCallback() {
		// ✅ GOOD: Removes the SAME function reference
		window.removeEventListener('storage', this.#onStorageEvent);
	}
}
```

**Key Points**:
- Use arrow function properties for event handlers
- Arrow functions automatically bind `this` without creating new references
- Each `addEventListener` and `removeEventListener` must use the SAME function reference
- Always remove event listeners in `disconnectedCallback()` for custom elements
- This pattern prevents memory leaks by ensuring proper cleanup

**Common Event Listener Scenarios**:

```typescript
// Document/window events
document.addEventListener('dragenter', this.#onDragEnter);
window.addEventListener('storage', this.#onStorage);

// Programmatically created elements
this.#element = new MyElement();
this.#element.addEventListener('action-executed', this.#onAction);

// Always clean up in disconnectedCallback
disconnectedCallback() {
	document.removeEventListener('dragenter', this.#onDragEnter);
	window.removeEventListener('storage', this.#onStorage);
	if (this.#element) {
		this.#element.removeEventListener('action-executed', this.#onAction);
	}
}
```

### Avoid Code Smells

**Magic Numbers/Strings**:

```typescript
// Bad
if (status === 200) { }
if (type === 'document') { }

// Good
const HTTP_OK = 200;
const CONTENT_TYPE_DOCUMENT = 'document';

if (status === HTTP_OK) { }
if (type === CONTENT_TYPE_DOCUMENT) { }

// Or use enums
enum ContentType {
	Document = 'document',
	Media = 'media',
}
```

**Long Parameter Lists**:

```typescript
// Bad
function create(name: string, type: string, parent: string, culture: string, template: string) { }

// Good
interface CreateOptions {
	name: string;
	type: string;
	parent?: string;
	culture?: string;
	template?: string;
}

function create(options: CreateOptions) { }
```

**Duplicate Code**:
- Extract to shared functions
- Use composition and mixins
- Create utility modules

**Deeply Nested Code**:
- Use early returns
- Extract to separate functions
- Use guard clauses

**Callback Hell** (N/A - use async/await)

### Modern Patterns

**Async/Await**:

```typescript
// Good - Clean async code
async loadContent() {
	try {
		this._loading = true;
		const { data, error } = await this.repository.requestById(this.id);
		if (error) {
			this._error = error;
			return;
		}
		this._content = data;
	} finally {
		this._loading = false;
	}
}
```

**Optional Chaining**:

```typescript
// Good - Safe property access
const name = this._content?.variants?.[0]?.name;

// Bad - Manual null checks
const name = this._content && this._content.variants &&
	this._content.variants[0] && this._content.variants[0].name;
```

**Destructuring**:

```typescript
// Good - Destructure for clarity
const { name, description, icon } = this._content;

// Good - With defaults
const { name = 'Untitled', description = '' } = this._content;
```

**Immutability**:

```typescript
// Good - Spread operator for immutability
this._items = [...this._items, newItem];
this._config = { ...this._config, newProp: value };

// Bad - Mutation
this._items.push(newItem);
this._config.newProp = value;
```

**Pure Functions**:

```typescript
// Good - Pure function (no side effects)
function calculateTotal(items: Item[]): number {
	return items.reduce((sum, item) => sum + item.price, 0);
}

// Bad - Impure (modifies input)
function calculateTotal(items: Item[]): number {
	items.sort((a, b) => a.price - b.price); // Mutation!
	return items.reduce((sum, item) => sum + item.price, 0);
}
```

### TypeScript-Specific Patterns

**Type Guards**:

```typescript
function isContentModel(value: unknown): value is UmbContentModel {
	return typeof value === 'object' && value !== null && 'id' in value;
}

// Usage
if (isContentModel(data)) {
	// TypeScript knows data is UmbContentModel
	console.log(data.id);
}
```

**Discriminated Unions**:

```typescript
type Result<T> =
	| { success: true; data: T }
	| { success: false; error: Error };

function handleResult<T>(result: Result<T>) {
	if (result.success) {
		console.log(result.data); // TypeScript knows this exists
	} else {
		console.error(result.error); // TypeScript knows this exists
	}
}
```

**Utility Types**:

```typescript
// Partial - Make all properties optional
type PartialContent = Partial<UmbContentModel>;

// Pick - Select specific properties
type ContentSummary = Pick<UmbContentModel, 'id' | 'name' | 'icon'>;

// Omit - Remove specific properties
type ContentWithoutId = Omit<UmbContentModel, 'id'>;

// Readonly - Make immutable
type ReadonlyContent = Readonly<UmbContentModel>;
```

### Comments and Documentation

> See the [project-wide Code Comment Policy](../../../CLAUDE.md#9-code-comment-policy) for the full rules, and [General-Purpose by Default](../../../CLAUDE.md#2-general-purpose-by-default) for the design principle behind them. The summary below applies them to TypeScript / Lit code.

**Default to no comment.** Well-named functions, methods, and variables are the primary documentation. A line calling `resetState()` does not need `// Reset state` above it. A method named `validateInput` does not need `// Validate input` above it.

**When NOT to write a comment**:
- Restating what the code does. If the comment is a paraphrase of the next two lines, delete it.
- Narrating a sequence of calls. The order is already in the code.
- Pointing at the current task, fix, callers, or PR (`// Used by X`, `// Added for the Y flow`, `// Fix for issue Z`). That metadata belongs in the commit message and PR description, not in source — it rots.
- Explaining a recognized idiom (a guard clause, a null check, an early return). It reads as itself no matter which caller or framework has a reason to trigger it redundantly — that reason belongs in whichever layer it's actually true of, not here.

**When a comment IS justified** — only when removing it would leave a future reader confused. State the rule at the altitude of the code it's in: generic and shared code describes its contract, never a specific extension's implementation of it.
- A non-obvious **WHY**: a hidden constraint, ordering requirement, or business rule that is not visible from the code, and not already accounted for by a recognized idiom.
- A **workaround** for a specific bug or browser quirk. Anchor it to an issue (`(#21996)`) so it can be deleted when the upstream fix lands.
- A subtle **invariant** the type system does not enforce.
- An **edge case** the code intentionally handles that would surprise a reader.
- **Public API docs** — JSDoc on exported symbols and Lit components (required for web-component-analyzer).

**JSDoc for Web Components** (still required — this is the public contract):

```typescript
/**
 * A button component that triggers document actions.
 * @element umb-document-action-button
 * @fires {CustomEvent} action-click - Fired when action is clicked
 * @slot - Default slot for button content
 * @cssprop --umb-button-color - Button text color
 */
export class UmbDocumentActionButton extends LitElement {
	/**
	 * The action identifier
	 * @attr
	 * @type {string}
	 */
	@property({ type: String })
	action = '';
}
```

**TODOs**:

```typescript
// TODO: Implement pagination [NL]
// TODO: Memory leak in subscription [JOV]
// TODO: Temporary workaround for API bug [LK]
```

A TODO with no author or no trigger is dead text — require one of them.

**Remove Dead Code**:
- Remove unused imports, functions, variables
- Clean up `console.log` calls before committing

### Feature Parity with Core Equivalents

When implementing a new component that mirrors an existing core or sibling implementation (e.g. a new collection action alongside document/media/member equivalents), do a side-by-side comparison against the core version before considering it done.

**Checklist**:

1. **Full API surface**: Read the reference implementation's controller/API usage line by line. Every method call, callback, and property access on controllers in the reference must have a counterpart in the new implementation — don't cherry-pick only the obvious ones.
2. **Error handling**: Match the core's error handling pattern (e.g. `.catch(() => {})` on `execute()` calls) to prevent unhandled promise rejections.
3. **Type hierarchy**: Before defining new interfaces, search for existing base types (e.g. `UmbNamedEntityModel` vs `UmbEntityModel`) that already declare the fields you need. Use the most specific base.
4. **UI consistency**: Check CSS from sibling implementations (e.g. `max-height` on scroll containers, icon fallbacks like `?? 'icon-document'`).
5. **Context-appropriate strings**: Don't reuse localization keys from another domain (e.g. `create_noDocumentTypes` in an elements context). Either create a new key or use only the inline fallback text.

**Core components to compare against** (common patterns):
- Collection create actions: `src/packages/core/collection/action/create/collection-create-action.element.ts`
- Entity create option modals: `src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts`
- Document/media/member equivalents in their respective `src/packages/` directories

### Patterns to Avoid

**Don't**:
- Use `var` (use `const`/`let`)
- Modify prototypes of built-in objects
- Use global variables
- Block the main thread (use web workers for heavy computation)
- Create deeply nested structures
- Use `any` type (use `unknown` or proper types)
- Use non-null assertions `!` unless absolutely necessary
- Ignore TypeScript errors with `@ts-ignore`

---

### Src Umbraco.Web.UI.Client Docs Commands (`src/Umbraco.Web.UI.Client/docs/commands.md`)

# Commands
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)

---


### Installation

```bash
# Install dependencies (must use npm, not yarn/pnpm due to workspaces)
npm install

# Note: Requires Node >=22.17.1 and npm >=10.9.2
```

### Build Commands

```bash
# TypeScript compilation only
npm run build

# Build for CMS (production build + copy to .NET project)
npm run build:for:cms

# Build for npm distribution (with type declarations)
npm run build:for:npm

# Build with Vite (alternative build method)
npm run build:vite

# Build workspaces
npm run build:workspaces

# Build Storybook documentation
npm run build-storybook
```

### Development Commands

```bash
# Start dev server (with live reload)
npm run dev

# Start dev server connected to real backend
npm run dev:server

# Start dev server with MSW mocks (default)
npm run dev:mock

# Pick an example and start the dev server with it loaded
npm run example

# Same, but with MSW mocks
npm run example:mock

# Preview production build
npm run preview
```

### Test Commands

```bash
# Run all unit tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests in development mode
npm run test:dev

# Run tests in watch mode (dev config)
npm run test:dev-watch

# Run E2E tests with Playwright
npm run test:e2e

# Run example tests
npm run test:examples

# Run example tests in watch mode
npm run test:examples:watch

# Run example tests in browser
npm run test:examples:browser
```

### Code Quality Commands

```bash
# Lint TypeScript files
npm run lint

# Lint and show only errors
npm run lint:errors

# Lint and auto-fix issues
npm run lint:fix

# Format code
npm run format

# Format and auto-fix
npm run format:fix

# Type check
npm run compile

# Run all checks (lint, compile, build-storybook, jsonschema)
npm run check
```

### Code Generation Commands

```bash
# Generate TypeScript config
npm run generate:tsconfig

# Generate OpenAPI client from backend API
npm run generate:server-api

# Generate icons
npm run generate:icons

# Generate package manifest
npm run generate:manifest

# Generate JSON schema for umbraco-package.json
npm run generate:jsonschema

# Generate JSON schema to dist
npm run generate:jsonschema:dist

# Generate UI API docs with TypeDoc
npm run generate:ui-api-docs

# Generate const check tests
npm run generate:check-const-test
```

### Analysis Commands

```bash
# Check for circular dependencies
npm run check:circular

# Check module dependencies
npm run check:module-dependencies

# Check path lengths
npm run check:paths

# Analyze web components
npm run wc-analyze

# Analyze web components for VS Code
npm run wc-analyze:vscode
```

### Storybook Commands

```bash
# Start Storybook dev server
npm run storybook

# Build Storybook
npm run storybook:build

# Build and preview Storybook
npm run storybook:preview
```

### Package Management

```bash
# Validate package exports and the extension template's @hey-api/openapi-ts version
npm run package:validate

# Prepare for npm publish
npm run prepack
```

### Environment Setup

**Prerequisites**:
- Node.js >=22.17.1
- npm >=10.9.2
- Modern browser (Chrome, Firefox, Safari)

**Initial Setup**:

1. Clone repository
   ```bash
   git clone https://github.com/umbraco/Umbraco-CMS.git
   cd Umbraco-CMS/src/Umbraco.Web.UI.Client
   ```

2. Install dependencies
   ```bash
   npm install
   ```

3. Configure environment (optional)
   ```bash
   cp .env .env.local
   # Edit .env.local with your settings
   ```

4. Start development
   ```bash
   npm run dev
   ```

**Environment Variables** (see `.env` file):

- `VITE_UMBRACO_USE_MSW` - Enable/disable Mock Service Worker (`on`/`off`)
- `VITE_UMBRACO_API_URL` - Backend API URL (e.g., `https://localhost:44339`)
- `VITE_UMBRACO_INSTALL_STATUS` - Install status (`running`, `must-install`, `must-upgrade`)
- `VITE_UMBRACO_EXTENSION_MOCKS` - Enable extension mocks (`on`/`off`)

---

### Src Umbraco.Web.UI.Client Docs Core Primitives (`src/Umbraco.Web.UI.Client/docs/core-primitives.md`)

# Core Primitives

The foundational building blocks that all backoffice code is built on. These live in `src/libs/` and are framework-agnostic (no CMS domain knowledge).

---

## UmbLitElement

**The base class for all web components in the backoffice.** Never extend `LitElement` directly.

```typescript
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';

@customElement('umb-my-element')
export class UmbMyElement extends UmbLitElement {
	// All primitives below are available on `this`
}
```

UmbLitElement extends Lit's `LitElement` with core capabilities:

### Observe a State

Lifecycle-managed observable subscription. Automatically unsubscribes when the element disconnects from the DOM — no manual cleanup needed.

```typescript
// Basic usage — subscribe to an observable, update @state
this.observe(context.items, (items) => {
	this._items = items;
});

// With explicit alias — allows replacement or removal by name
this.observe(context.items, (items) => {
	this._items = items;
}, '_observeItems');

// Later, stop observing by alias:
this.removeUmbControllerByAlias('_observeItems');

// Combining multiple observables (emits when ANY source changes)
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';

this.observe(
	observeMultiple([context.items, context.filter]),
	([items, filter]) => {
		this._items = items;
		this._filter = filter;
	},
	'_observeItemsAndFilter',
);
```

**Alias behavior:**

- If omitted and callback exists: auto-generated hash of the callback function — guards against accidental duplicate subscriptions when the same `observe(...)` line runs more than once
- If `null`: no alias — use this when the call site is one-shot (e.g., set up once in a constructor and never invoked again). Intentional; not a missing-alias smell.
- If provided: explicit string/symbol — re-running the same `observe(...)` line replaces the existing controller with the same alias, and you can remove it actively via `removeUmbControllerByAlias('_observeItems')`

### Retrieve a Context

**Default to `consumeContext`.** Use it whenever a controller or element needs the context at setup time — both for ongoing observation of values and for one-time reads on resolve. The callback runs as soon as the context is available and re-runs if the context is replaced; cleanup is tied to the controller lifecycle.

**Use `getContext` only when the context is not needed until a user action runs.** Typical cases: an event handler, a property action's `execute()`, an entity action's `execute()`, a modal-open handler. Inside these, awaiting `getContext` keeps the call site flat and avoids registering a long-lived consumer for a one-off operation.

#### consumeContext(alias, callback)

Subscribe to a context provided by an ancestor element. The callback fires when the context becomes available (and re-fires if the context changes).

```typescript
#myContext?: typeof UMB_MY_CONTEXT.TYPE;

constructor() {
	super();
	this.consumeContext(UMB_MY_CONTEXT, (context) => {
		this.#myContext = context;
		// Set up observations on the context here
		this.observe(context.items, (items) => {
			this._items = items;
		});
	});
}
```

#### getContext(alias, options?)

Promise-based one-time context retrieval. Use for user-triggered actions where you don't need continuous observation.

```typescript
async #handleClick() {
	const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
	notificationContext?.peek('positive', { data: { message: 'Done!' } });
}
```

### Provide a Context

Make a context instance available to all descendant elements by extending UmbContextBase.

```typescript
class UmbMyContext extends UmbContextBase {
	constructor(host: UmbControllerHost) {
		super(host, UMB_MY_CONTEXT);
	}
}
```

### Localization

Built-in localization controller. Available on every UmbLitElement.

```typescript
// In templates
html`<span>${this.localize.term('general_close')}</span>`

// In code
const label = this.localize.term('content_createBlueprintFrom');
```

---

## Observable State

Reactive state containers built on RxJS `BehaviorSubject`. **All `Umb*State` classes only emit when the new value differs from the current value** — observers are not invoked on `setValue(x)` when the state already holds `x`. This means you do not need to write "is this a redundant re-emit?" guards inside observer callbacks.

| Class | Holds | Comparison |
|-------|-------|-----------|
| `UmbStringState` / `UmbNumberState` / `UmbBooleanState` | Primitives | `===` |
| `UmbObjectState` / `UmbArrayState` / `UmbDeepState` | Plain JSON data, deep-frozen | `JSON.stringify` deep compare |
| `UmbClassState` | Class instance with `.equal()` | `oldValue.equal(newValue)` (when both non-null) |
| `UmbBasicState` | Anything | `===` (reference) |

```typescript
import { UmbStringState, UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';

// Private state, public read-only observable
#name = new UmbStringState<string>('');
readonly name = this.#name.asObservable();
this.#name.setValue('new name');
```

For per-type comparison details, change-detection caveats (`UmbBasicState` holding objects, `UmbClassState` with `undefined`), `asObservablePart` memoization, mute/unmute, and the rationale for not writing emission guards in observers, see **[State System](./state-system.md)**.

---

## Context API

Framework-agnostic dependency injection via DOM events. Contexts flow **down** the DOM tree from provider to consumer.

### UmbContextToken

Type-safe identifier for a context. The generic parameters flow through to `consumeContext()` and `getContext()` return types.

```typescript
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';

// Simple token
export const UMB_MY_CONTEXT = new UmbContextToken<UmbMyContext>('UmbMyContext');

// Token with discriminator (for type narrowing)
export const UMB_MY_SPECIFIC_CONTEXT = new UmbContextToken<UmbMyBaseContext, UmbMySpecificContext>(
	'UmbMyContext',          // contextAlias — matches the provider alias
	'UmbMySpecificContext',  // apiAlias — distinguishes variant
	(context): context is UmbMySpecificContext => context instanceof UmbMySpecificContext,
);
```

**The `.TYPE` property** gives you the resolved type for variable declarations:

```typescript
#myContext?: typeof UMB_MY_CONTEXT.TYPE;
```

### How It Works

1. **Provider** registers a context instance on a host element
2. **Consumer** dispatches a DOM event that bubbles up the tree
3. When the event reaches a provider with a matching alias, the provider calls back with the instance
4. The consumer's callback fires with the typed context

### Creating a Context

```typescript
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';

export const UMB_MY_CONTEXT = new UmbContextToken<UmbMyContext>('UmbMyContext');

export class UmbMyContext extends UmbContextBase<UmbMyContext> {
	// State, observables, methods...

	constructor(host: UmbControllerHost) {
		super(host, UMB_MY_CONTEXT);
	}
}
```

`UmbContextBase` automatically calls `provideContext()` in its constructor, making the context available to descendants.

### Common Built-in Contexts

```typescript
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
```

---

## Controller Pattern

Lifecycle-aware logic units that attach to a host (element or other controller). Controllers implement `UmbController` and are managed by the host's controller system.

### UmbControllerBase

Base class for non-element controllers. Has the same `observe()`, `consumeContext()`, `provideContext()`, and `getContext()` methods as `UmbLitElement`.

```typescript
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';

export class UmbMyController extends UmbControllerBase {
	constructor(host: UmbControllerHost) {
		super(host, 'my-controller-alias'); // alias is optional
	}

	// Has full access to observe(), consumeContext(), etc.
}
```

### Controller Lifecycle

```
host.addUmbController(ctrl)  →  ctrl.hostConnected()
                                       ↓
                              (subscriptions active, DOM events listening)
                                       ↓
host disconnects from DOM     →  ctrl.hostDisconnected()
                                       ↓
                              (subscriptions paused)
                                       ↓
host reconnects to DOM        →  ctrl.hostConnected()
                                       ↓
host.removeUmbController(ctrl) → ctrl.destroy()
```

### Controller Aliases

When a controller is added with an alias that already exists on the host, the **old controller is destroyed and replaced**. This is how `observe()` avoids duplicate subscriptions when called multiple times:

```typescript
// First call: creates observer with alias '_observeItems'
this.observe(context.items, (items) => { ... }, '_observeItems');

// Second call: destroys previous '_observeItems' observer, creates new one
this.observe(context.items, (items) => { ... }, '_observeItems');
```



---

## Quick Reference

| Primitive | Import Path | Purpose |
|-----------|-------------|---------|
| `UmbLitElement` | `@umbraco-cms/backoffice/lit-element` | Base class for all elements |
| `UmbControllerBase` | `@umbraco-cms/backoffice/class-api` | Base class for non-element controllers |
| `UmbContextBase` | `@umbraco-cms/backoffice/class-api` | Base class for contexts (auto-provides) |
| `UmbContextToken` | `@umbraco-cms/backoffice/context-api` | Type-safe context identifier |
| `UmbStringState` | `@umbraco-cms/backoffice/observable-api` | String state container |
| `UmbNumberState` | `@umbraco-cms/backoffice/observable-api` | Number state container |
| `UmbBooleanState` | `@umbraco-cms/backoffice/observable-api` | Boolean state container |
| `UmbArrayState` | `@umbraco-cms/backoffice/observable-api` | Array state container (requires unique key) |
| `UmbObjectState` | `@umbraco-cms/backoffice/observable-api` | Object state container |
| `observeMultiple` | `@umbraco-cms/backoffice/observable-api` | Combine multiple observables |

---

### Src Umbraco.Web.UI.Client Docs Data Flow (`src/Umbraco.Web.UI.Client/docs/data-flow.md`)

# Data Flow & API Integration

How data moves between the server and UI components. This covers the internal implementation pattern using base classes — the same pattern used across all entity types in the backoffice. For how repositories are categorized, organized, and structured across features, see [Repositories](./repositories.md).

---

## The Data Flow Chain

```
Element ──observe()──> Workspace Context ──> Repository ──> Data Source ──> Generated API Client ──> Server
   ↑                        ↑                    ↑
   └── @state updates ──────┘                    │
                                                 ↓
                                              Store (cache)
```

Each layer has a single responsibility:

| Layer | Responsibility |
|-------|---------------|
| **Element** | Renders UI, handles user input |
| **Context** | Coordinates data and lifecycle for a feature |
| **Repository** | Orchestrates data source + store |
| **Data Source** | Maps between server API and domain models |
| **Store** | Caches entities, provides observables (see note below) |
| **Generated Client** | Typed HTTP calls from OpenAPI spec |

Each layer has domain-specific base classes depending on the type of data operation (detail CRUD, item listing, collection, tree, etc.). See [Repositories](./repositories.md) for a complete guide to all repository categories and when to use each. The example below uses the **detail** variant.

### Store Direction

Stores are being phased out. Only **Detail Store** and **Item Store** remain in active use. Do not introduce new store types. New features should manage state within the context/workspace layer instead, using observable state classes directly. The existing detail and item stores will be addressed in a future version.

---

## Generated API Client

API clients are auto-generated from the backend OpenAPI specification.

### Generation

```bash
npm run generate:server-api
```

This reads `src/packages/core/openapi-ts.config.ts` and outputs typed service classes and models to `src/packages/core/backend-api/`.

### Usage

Generated services are imported from the `external/backend-api` subpath:

```typescript
import { WebhookService } from '@umbraco-cms/backoffice/external/backend-api';
import type { CreateWebhookRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
```

**Never call generated services directly from elements or contexts.** Always go through a data source and repository.

---

## Caching & Request Deduplication

Caching and request deduplication live **between the data source and the API client** — not in the repository or context. This is the layer that knows the API transport details needed to deduplicate and cache effectively.

```
Data Source ──> Request Manager ──> Generated API Client
                   ├─ Data cache: returns cached items immediately
                   ├─ Inflight cache: deduplicates concurrent requests for the same data
                   └─ Server event sync: invalidates cache when the server broadcasts changes
```

The `management-api` package provides base classes for this: `UmbManagementApiItemDataRequestManager` handles the caching/deduplication logic, and concrete implementations wire it to a specific API endpoint and cache instance. See the document, media, and member item data sources for examples.

This pattern is specific to the Management API transport. Other data sources (e.g., a custom API or offline source) would implement their own caching strategy in the same position — between data source and transport.

---

## tryExecute

The standard wrapper for all API calls. Handles errors, shows notifications, and returns a `{ data, error }` tuple — never throws.

```typescript
import { tryExecute } from '@umbraco-cms/backoffice/resources';

const { data, error } = await tryExecute(
	this.#host,
	WebhookService.getWebhookById({ path: { id: unique } }),
);

if (error || !data) {
	return { error };
}
```

**Parameters:**
- `host` — Controller host (for notification context access)
- `promise` — The API call promise
- `opts?` — Optional: `{ abortSignal?, disableNotifications? }`

**Behavior:**
- Returns `{ data }` on success
- Returns `{ error }` on failure (never throws)
- Automatically shows error notifications to the user
- Silently handles 401/403/404 (UI is expected to handle these)
- Returns `{ error: UmbCancelError }` without a notification as soon as `abortSignal` aborts, or when the request rejects with an `AbortError` or a `TimeoutError` (from `AbortSignal.timeout()`). A request aborted with a custom reason rejects with that reason, which is reported as an error unless the signal is also passed as `abortSignal`. Pass the same signal to the request (for example `umbHttpClient.get({ url, signal })`) to stop the fetch itself

---

## Detail Data Flow Example

A generic example showing the structural pattern for each layer. Replace `MyEntity` with the actual entity name. When implementing, find an existing entity in the codebase that is similar to your task and follow its patterns.

### 1. Domain Model

```typescript
export interface UmbMyEntityDetailModel {
	entityType: string;
	unique: string;
	name: string;
	// ... domain-specific properties
}
```

### 2. Data Source

Implements the data source interface for the domain. Responsible for calling the generated API client and mapping between server types and the domain model.

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Key pattern:** The data source is the only layer that knows about server API types. It maps in both directions — server → domain on reads, domain → server on writes.

### 3. Store

```typescript
import type { UmbMyEntityDetailModel } from '../../types.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbDetailStoreBase } from '@umbraco-cms/backoffice/store';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';

export class UmbMyEntityDetailStore extends UmbDetailStoreBase<UmbMyEntityDetailModel> {
	constructor(host: UmbControllerHost) {
		super(host, UMB_MY_ENTITY_DETAIL_STORE_CONTEXT);
	}
}

export const UMB_MY_ENTITY_DETAIL_STORE_CONTEXT = new UmbContextToken<UmbMyEntityDetailStore>(
	'UmbMyEntityDetailStore',
);
```

### 4. Repository

Wires data source + store together. The base class handles all CRUD orchestration.

```typescript
import type { UmbMyEntityDetailModel } from '../../types.js';
import { UmbMyEntityDetailServerDataSource } from './my-entity-detail.server.data-source.js';
import { UMB_MY_ENTITY_DETAIL_STORE_CONTEXT } from './my-entity-detail.store.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';

export class UmbMyEntityDetailRepository extends UmbDetailRepositoryBase<UmbMyEntityDetailModel> {
	constructor(host: UmbControllerHost) {
		super(host, UmbMyEntityDetailServerDataSource, UMB_MY_ENTITY_DETAIL_STORE_CONTEXT);
	}
}

export { UmbMyEntityDetailRepository as api };
```

### 5. Workspace Context

Manages entity lifecycle and exposes observable state for elements.

```typescript
import { UMB_MY_ENTITY_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js';
import { UMB_MY_ENTITY_ENTITY_TYPE, UMB_MY_ENTITY_WORKSPACE_ALIAS } from '../../entity.js';
import type { UmbMyEntityDetailModel } from '../types.js';
import { UmbEntityNamedDetailWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';

export class UmbMyEntityWorkspaceContext
	extends UmbEntityNamedDetailWorkspaceContextBase<UmbMyEntityDetailModel>
{
	// Derived observables — only emit when the specific property changes
	readonly myProperty = this._data.createObservablePartOfCurrent((data) => data?.myProperty);

	constructor(host: UmbControllerHost) {
		super(host, {
			workspaceAlias: UMB_MY_ENTITY_WORKSPACE_ALIAS,
			entityType: UMB_MY_ENTITY_ENTITY_TYPE,
			detailRepositoryAlias: UMB_MY_ENTITY_DETAIL_REPOSITORY_ALIAS,
		});
	}

	// Setters mutate the current data — triggers observable updates
	setMyProperty(value: string) {
		this._data.updateCurrent({ myProperty: value });
	}
}

export { UmbMyEntityWorkspaceContext as api };
```

### 6. Element

Consumes context, observes data, renders UI, dispatches user actions back to context.

```typescript
import { UMB_MY_ENTITY_WORKSPACE_CONTEXT } from '../my-entity-workspace.context-token.js';
import type { UmbMyEntityDetailModel } from '../../types.js';
import { customElement, html, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';

@customElement('umb-my-entity-workspace-view')
export class UmbMyEntityWorkspaceViewElement extends UmbLitElement {
	@state()
	private _data?: UmbMyEntityDetailModel;

	#workspaceContext?: typeof UMB_MY_ENTITY_WORKSPACE_CONTEXT.TYPE;

	constructor() {
		super();

		this.consumeContext(UMB_MY_ENTITY_WORKSPACE_CONTEXT, (context) => {
			this.#workspaceContext = context;

			this.observe(context.data, (data) => {
				this._data = data;
			});
		});
	}

	#onChange(event: Event) {
		const value = (event.target as HTMLInputElement).value;
		this.#workspaceContext?.setMyProperty(value);
	}

	override render() {
		if (!this._data) return nothing;

		return html`
			<uui-box>
				<umb-property-layout label=${this.localize.term('myEntity_myProperty')}>
					<uui-input
						slot="editor"
						.value=${this._data.myProperty}
						@input=${this.#onChange}></uui-input>
				</umb-property-layout>
			</uui-box>
		`;
	}
}
```

### 7. Manifests

```typescript
export const manifests: Array<UmbExtensionManifest> = [
	{
		type: 'repository',
		alias: 'Umb.Repository.MyEntity.Detail',
		name: 'My Entity Detail Repository',
		api: () => import('./my-entity-detail.repository.js'),
	},
	{
		type: 'store',
		alias: 'Umb.Store.MyEntity.Detail',
		name: 'My Entity Detail Store',
		api: () => import('./my-entity-detail.store.js'),
	},
];
```

Manifests are lazy-loaded — the `api: () => import(...)` pattern ensures code is only loaded when needed.

---

## Client-Side Models

All layers above the data source work exclusively with **client-side domain models** — never with types from external systems (generated API types, third-party SDK types, etc.). The data source is the boundary where external types are mapped to client-side models and vice versa.

**Why:** External models are shaped by their system's concerns (API versioning, serialization format, naming conventions). Client-side models are shaped by what the UI needs. Coupling the UI to external models means changes in any external system ripple through the entire frontend.

**What to map:**

| External concept | Client-side convention |
|-----------------|----------------------|
| `id`, `key`, `path`, `isoCode`, or any identifier | `unique` (standardized across all models) |
| Entity classification | `entityType` (attached to models that represent entities) |
| Server-specific field names | Domain-appropriate property names |
| Nullable server fields with defaults | Resolved to concrete values where possible |

**Where mapping happens:** Only in data sources. The data source reads external types, constructs a client-side model, and returns it. On writes, it does the reverse — takes a client-side model and constructs the external request type.

```typescript
// Data source maps server response → client-side model
const model: UmbMyEntityDetailModel = {
    entityType: UMB_MY_ENTITY_ENTITY_TYPE,
    unique: data.id,        // server "id" → client "unique"
};
```

**Rule:** If generated API types (`CreateXRequestModel`, `XResponseModel`, etc.) appear in a repository, context, or element import — the data source mapping layer is missing or incomplete.

---

## Key Rules

1. **Never call generated API services directly from elements or contexts** — always go through data source → repository
2. **Always use `tryExecute()` for API calls** — it handles errors and notifications
3. **Data sources are the only layer that knows about external types** — they map between external system types and client-side domain models. No other layer should import or reference generated API types.
4. **All return values use `{ data, error }` tuples** — never throw from async data operations
5. **Export as `api` from repositories** — classes bound to a manifest use `export { ClassName as api }`, which enables lazy-loading via `api: () => import(...)`
6. **Do not introduce new store types** — stores are being phased out. Manage state in the context layer using observable state classes directly

---

### Src Umbraco.Web.UI.Client Docs Deprecation (`src/Umbraco.Web.UI.Client/docs/deprecation.md`)

# Deprecation & Breaking Changes

---

The `@umbraco-cms/backoffice` npm package, Extension Registry, Context tokens, exported types, and global components form a public contract. Breaking changes require the deprecation process below.

## SemVer & The 2-Major-Version Rule

> If something is deprecated in **vN**, the earliest it can be removed is **vN+2**.
>
> Example: deprecated in v16 -> removed in v18 at the earliest.

---

## How to Deprecate

Every deprecation must use **both** mechanisms:

1. **JSDoc `@deprecated` tag** — for IDE warnings and documentation
2. **Runtime warning via `UmbDeprecation`** — console output so consumers are notified at runtime

For step-by-step implementation instructions, use the `general-deprecate-api` skill.

---

## What Counts as a Breaking Change

Any of the following to a **publicly exported** symbol:

- Removing/renaming an exported class, function, type, constant, or context token
- Removing/renaming a public method or property
- Changing a public method signature (required params, return type)
- Removing/renaming a subpath export from `package.json`
- Changing an extension type alias or manifest schema
- Removing/renaming a global component's tag name
- Changing public API behavior in unexpected ways

**Not breaking**: Internal code (`local-components/`, unexported classes, private methods) can be changed freely.

---

### Src Umbraco.Web.UI.Client Docs Design Choices (`src/Umbraco.Web.UI.Client/docs/design-choices.md`)

# Design Choices
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../../CLAUDE.md)

---

UI stays behind. User content takes the visual front seat. Default to restraint on every visual decision.

When in doubt: leave the icon out, leave the colour off, cut the word.

This document covers visual design choices for the backoffice client (`src/Umbraco.Web.UI.Client/`). For naming and formatting conventions see [style-guide.md](./style-guide.md).

---

## Icons

Default: **no icon**. Add one only when the affordance cannot be carried by text — Entity Actions, toolbars without room for labels, drag handles, rich-text editor toolbars.

Don't decorate buttons, list rows, headings, or labels with icons for ornament. Don't pair an icon with a clear text label "for clarity" — pick one.

### Recognizable icons

When an icon *is* needed, use one of these. They're chosen because most users already recognize the symbol; reaching outside this list adds cognitive load.

**Action**
`icon-add` · `icon-trash` · `icon-edit` · `icon-search` · `icon-settings` · `icon-check` · `icon-undo` · `icon-clipboard-copy` · `icon-clipboard-paste` · `icon-link` · `icon-out` (sign out)

**Content type**
`icon-document` · `icon-folder` · `icon-picture` · `icon-user` · `icon-users` · `icon-globe` · `icon-cloud` · `icon-home`

**State**
`icon-lock` · `icon-unlocked` · `icon-alert` (warning) · `icon-time` (scheduled / pending) · `icon-circle-dotted` (generic fallback for entities without a specific icon)

**Navigation**
`icon-arrow-left` · `icon-arrow-up` · `icon-arrow-down`

**Drag handle**
`icon-grip`

**Rich-text editor toolbars only**
`icon-bold` · `icon-italic` · `icon-underline` · `icon-bulleted-list` · `icon-ordered-list` · `icon-blockquote` · `icon-code` · `icon-anchor`

### Looking for something else

If — and only if — none of the above fits and you're certain an icon is justified, browse `src/packages/core/icon-registry/icons/` for available aliases. The same set is exposed via `<umb-icon>` and the icon picker modal. Never invent an alias; if it isn't in the registry, use text.

For the upstream catalogue of UUI icons (used directly only for chrome like buttons), see the [UUI library](https://github.com/umbraco/Umbraco.UI).

---

## Colours

Default: **no colour**. UI chrome should be neutral — surface, border, body text. Reserve colour for the **single primary action** on a screen.

- No accent colours on info banners, hover effects beyond UUI defaults, or status pills for non-critical state.
- No hex codes, no raw colour names, no inventing CSS variables.
- Use UUI tokens (`--uui-color-…`) when colour is justified. The [UUI library](https://github.com/umbraco/Umbraco.UI) is the source of truth — don't enumerate tokens here, link there.

If you're reaching for colour to express hierarchy, the hierarchy probably belongs in layout, weight, or copy instead.

---

## Buttons

Only **one** button on screen has a non-default look. That's the primary call-to-action for the current view.

- Empty / placeholder views → the call-to-action is the primary.
- Everything else → secondary, or default.
- Don't repeat the primary look on multiple buttons to "balance the layout." Pick one.

### `look="placeholder"` — reserved usage

Use the placeholder look only when the button itself sits in the *visual destination* of its action — the button is a preview of where its result will land:

- "Add block" inside an empty grid slot
- "Add property" inside an empty schema row
- "Upload" inside an empty media drop zone

Don't use placeholder for buttons in toolbars, headers, dialogs, or anywhere disconnected from the result location.

---

## UX writing

Super short. Cut anything inferable from context.

- Use contextual reference: in the Members section, the button is "New", not "New member". On a delete confirm inside a Document workspace, "Delete" is enough.
- No marketing voice, no "please", no "in order to", no explanations.
- One verb beats two. One sentence beats two.

### Before / after

| Don't | Do |
|-------|----|
| "Create a new member" | "New" |
| "Please click here to upload your image" | "Upload" |
| "Search for content here…" | "Search" |
| "Successfully saved your changes" | "Saved" |

---

## Dialogs

A dialog already implies confirmation — don't say "are you sure". Sharp, concrete copy. Two options: do, or cancel.

Source: [UUI Dialog Style Guide](https://uui.umbraco.com/?path=/story/design-style-guide--dialog).

### Headline

The action in short form. Include the target only if it sits naturally at the end.

| Don't | Do |
|-------|----|
| "Confirm deleting 'My Page 1'" | "Delete 'My Page 1'" |
| "Publish 'My Page 1' with descendants" | "Publish with descendants" |
| "Transfer document" | "Transfer to 'Development'" |

### Description

Clarify the *effect* of the action without repeating the headline. Don't assume the user knows internal vocabulary — unfold words like "publish" or "descendants" with their concrete consequence.

Use `<strong>` for target names (easy to anchor on visually); use `<i>` for secondary effects.

| Don't | Do |
|-------|----|
| "Are you sure you want to delete this content and all content items underneath?" | "<strong>My Page 1</strong> will be transferred to the trash bin, including <i>all content items underneath</i>." |
| "Are you sure you want to publish My Page 1 and all descendant items." | "Make <strong>My Page 1</strong> publicly available, including <i>all content items underneath</i>." |

### Actions

Two buttons: cancel (default look) and the action (primary, `color="positive"` or `color="danger"`). The button label is just the verb — `Delete`, `Publish`, `Transfer` — matching the word used in the headline or description so the eye connects them.

Never put the target name in the action button. The text carries the target; the button carries the verb.

---

## Layout & spacing

Use UUI spacing tokens (`--uui-size-*`), prefer (`--uui-size-space-*`), not hardcoded pixels. The system is built on a 6px base-unit; tokens compose into a vertical rhythm that keeps the UI calm.

The space between elements declares their relationship — related elements closer, unrelated elements farther. If no token fits, rethink the layout instead of inventing a value.
Optical adjustments are welcome, but need careful consideration before being introduced.

Source: [UUI Layout Style Guide](https://uui.umbraco.com/?path=/story/design-style-guide--layout).

---

## Typography

Use UUI's type system. Don't invent font sizes.

- Apply `.uui-text` to the root of a UUI typography region.
- Headings: native `<h1>`–`<h5>` or the equivalent `.uui-h1`–`.uui-h5` classes.
- Lead paragraph: `<p class="uui-lead">` for a summary opener.
- `<small>` for inline descriptions.

Hierarchy comes from size and weight — never colour.

Source: [UUI Typography Style Guide](https://uui.umbraco.com/?path=/story/design-style-guide--typography).

---

### Src Umbraco.Web.UI.Client Docs Edge Cases (`src/Umbraco.Web.UI.Client/docs/edge-cases.md`)

# Edge Cases
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)

---


### Null/Undefined Handling

**Always Check**:

```typescript
// Use optional chaining
const name = this._content?.name;

// Use nullish coalescing for defaults
const name = this._content?.name ?? 'Untitled';

// Check before accessing
if (!this._content) {
	return html`<p>No content</p>`;
}
```

**TypeScript Strict Null Checks** (enabled):
- Variables are non-nullable by default
- Use `Type | undefined` or `Type | null` explicitly
- TypeScript forces null checks

**Function Parameters**:

```typescript
// Make nullable parameters explicit
function load(id: string, culture?: string) {
	const cultureCode = culture ?? 'en-US'; // Default value
}
```

### Array Edge Cases

**Empty Arrays**:

```typescript
// Check length before access
if (this._items.length === 0) {
	return html`<p>No items</p>`;
}

// Safe array methods
const first = this._items[0]; // Could be undefined
const first = this._items.at(0); // Also could be undefined

// Use optional chaining
const firstId = this._items[0]?.id;
```

**Single vs Multiple Items**:

```typescript
// Handle both cases
const items = Array.isArray(data) ? data : [data];
```

**Array Methods on Undefined**:

```typescript
// Guard against undefined
const ids = this._items?.map(item => item.id) ?? [];

// Or check first
if (!this._items) {
	return [];
}
return this._items.map(item => item.id);
```

**Sparse Arrays** (rare in this codebase):

```typescript
// Use filter to remove empty slots
const dense = sparse.filter(() => true);
```

### String Edge Cases

**Empty Strings**:

```typescript
// Check for empty strings
if (!name || name.trim().length === 0) {
	return 'Untitled';
}

// Or use default
const displayName = name?.trim() || 'Untitled';
```

**String vs Null/Undefined**:

```typescript
// Distinguish between empty string and null
const hasValue = value !== null && value !== undefined;
const isEmpty = value === '';

// Or use optional chaining
const length = value?.length ?? 0;
```

**Trim Whitespace**:

```typescript
// Always trim user input
const cleanName = this._name.trim();

// Validate after trimming
if (cleanName.length === 0) {
	// Invalid
}
```

**String Encoding**:
- Use UTF-8 everywhere
- Be aware of Unicode characters (emojis, etc.)
- Use `textContent` not `innerHTML` for plain text

**Internationalization**:

```typescript
// Use localization API
const label = this.localize.term('general_submit');

// Not hardcoded strings
// const label = 'Submit'; // ❌
```

### Number Edge Cases

**NaN Checks**:

```typescript
// Use Number.isNaN, not isNaN
if (Number.isNaN(value)) {
	// Handle NaN
}

// isNaN coerces, Number.isNaN doesn't
isNaN('hello'); // true (coerces to NaN)
Number.isNaN('hello'); // false (not a number)
```

**Infinity**:

```typescript
if (!Number.isFinite(value)) {
	// Handle Infinity or NaN
}
```

**Parsing**:

```typescript
// parseInt/parseFloat can return NaN
const num = parseInt(input, 10);
if (Number.isNaN(num)) {
	// Handle invalid input
}

// Or use Number constructor with validation
const num = Number(input);
if (!Number.isFinite(num)) {
	// Invalid
}
```

**Floating Point Precision**:

```typescript
// Don't compare floats with ===
const isEqual = Math.abs(a - b) < 0.0001;

// Or use integers for currency (cents, not dollars)
const priceInCents = 1099; // $10.99
```

**Division by Zero**:

```typescript
// JavaScript returns Infinity, not error
const result = 10 / 0; // Infinity

// Check denominator
if (denominator === 0) {
	// Handle division by zero
	return 0; // Or throw error
}
```

**Safe Integer Range**:

```typescript
// JavaScript integers are safe up to Number.MAX_SAFE_INTEGER
const isSafe = Number.isSafeInteger(value);

// For IDs, use strings not numbers
interface UmbContentModel {
	id: string; // Not number
}
```

### Object Edge Cases

**Property Existence**:

```typescript
// Use optional chaining
const value = obj?.property;

// Or check explicitly
if ('property' in obj) {
	const value = obj.property;
}

// hasOwnProperty (not inherited)
if (Object.hasOwn(obj, 'property')) {
	// Property exists on object itself
}
```

**Null vs Undefined vs {}**:

```typescript
// Distinguish between missing and empty
const isEmpty = obj !== null && obj !== undefined && Object.keys(obj).length === 0;

// Or use optional chaining
const hasData = obj && Object.keys(obj).length > 0;
```

**Shallow vs Deep Copy**:

```typescript
// Shallow copy
const copy = { ...original };

// Deep copy (for simple objects)
const deepCopy = JSON.parse(JSON.stringify(original));

// Deep copy with structuredClone (modern browsers)
const deepCopy = structuredClone(original);

// Note: Functions, symbols, and undefined are not copied by JSON.stringify
```

**Object Freezing**:

```typescript
// Prevent modification
const frozen = Object.freeze(obj);

// Check if frozen
if (Object.isFrozen(obj)) {
	// Can't modify
}
```

### Async/Await Edge Cases

**Unhandled Promise Rejections**:

```typescript
// Always catch errors
try {
	await asyncOperation();
} catch (error) {
	console.error('Failed:', error);
}

// Or use .catch()
asyncOperation().catch(error => {
	console.error('Failed:', error);
});

// For fire-and-forget, explicitly catch
void asyncOperation().catch(error => console.error(error));
```

**Promise.all Fails Fast**:

```typescript
// Promise.all rejects if ANY promise rejects
try {
	const results = await Promise.all([op1(), op2(), op3()]);
} catch (error) {
	// One failed, others may still be running
}

// Use Promise.allSettled to wait for all (even if some fail)
const results = await Promise.allSettled([op1(), op2(), op3()]);
results.forEach((result, index) => {
	if (result.status === 'fulfilled') {
		console.log(`Op ${index} succeeded:`, result.value);
	} else {
		console.error(`Op ${index} failed:`, result.reason);
	}
});
```

**Race Conditions**:

```typescript
// Avoid race conditions with sequential operations
this._loading = true;
try {
	const data1 = await fetchData1();
	const data2 = await fetchData2(data1.id);
	this._result = processData(data1, data2);
} finally {
	this._loading = false;
}

// For parallel operations, use Promise.all
const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
```

**Timeout Handling**:

```typescript
// Implement timeout for operations
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
	return Promise.race([
		promise,
		new Promise<T>((_, reject) =>
			setTimeout(() => reject(new Error('Timeout')), ms)
		),
	]);
}

// Usage
try {
	const data = await withTimeout(fetchData(), 5000);
} catch (error) {
	// Handle timeout or other errors
}
```

**Memory Leaks with Event Listeners**:

```typescript
export class UmbMyElement extends LitElement {
	#controller = new UmbMyController(this);

	// Controllers automatically clean up on disconnect
	constructor() {
		super();
		this.#controller.observe(dataSource$, (value) => {
			this._data = value;
		});
	}

	// Lit lifecycle handles this automatically
	disconnectedCallback() {
		super.disconnectedCallback();
		// Controllers are destroyed automatically
	}
}
```

### Web Component Edge Cases

**Custom Element Not Defined**:

```typescript
// Check if element is defined
if (!customElements.get('umb-my-element')) {
	// Not defined yet
	await customElements.whenDefined('umb-my-element');
}

// Or use upgrade
await customElements.upgrade(element);
```

**Shadow DOM Queries**:

```typescript
// Query shadow root, not document
const button = this.shadowRoot?.querySelector('button');

// Use Lit decorators
@query('#myButton')
private _button!: HTMLButtonElement;
```

**Property vs Attribute Sync**:

```typescript
// Lit keeps properties and attributes in sync
@property({ type: String })
name = ''; // Syncs with name="" attribute

// But complex types don't sync to attributes
@property({ type: Object })
data = {}; // No attribute sync

// State doesn't sync to attributes
@state()
private _loading = false;
```

**Reactive Update Timing**:

```typescript
// Wait for update to complete
this.value = 'new value';
await this.updateComplete;
// Now DOM is updated

// Or use requestUpdate
this.requestUpdate();
await this.updateComplete;
```

### Date/Time Edge Cases

**Use Luxon** (not Date):

```typescript
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';

// Create dates
const now = DateTime.now();
const utc = DateTime.utc();
const parsed = DateTime.fromISO('2024-01-15T10:30:00Z');

// Always store dates in UTC
const isoString = now.toUTC().toISO();

// Format for display
const formatted = now.toLocaleString(DateTime.DATETIME_MED);

// Timezone handling
const local = utc.setZone('local');
```

**Date Comparison**:

```typescript
// Compare DateTime objects
if (date1 < date2) { }
if (date1.equals(date2)) { }

// Or compare timestamps
if (date1.toMillis() < date2.toMillis()) { }
```

**Date Parsing Can Fail**:

```typescript
const date = DateTime.fromISO(input);
if (!date.isValid) {
	console.error('Invalid date:', date.invalidReason);
	// Handle invalid date
}
```

### JSON Edge Cases

**JSON.parse Can Throw**:

```typescript
// Always wrap in try/catch
try {
	const data = JSON.parse(jsonString);
} catch (error) {
	console.error('Invalid JSON:', error);
	// Handle parse error
}
```

**Circular References**:

```typescript
// JSON.stringify throws on circular references
const obj = { a: 1 };
obj.self = obj;

try {
	JSON.stringify(obj);
} catch (error) {
	// TypeError: Converting circular structure to JSON
}
```

**Date Objects**:

```typescript
// Dates become strings
const data = { date: new Date() };
const json = JSON.stringify(data);
const parsed = JSON.parse(json);
// parsed.date is a string, not Date

// Use ISO format explicitly
const isoDate = new Date().toISOString();
```

**Undefined Values**:

```typescript
// Undefined values are omitted
const obj = { a: 1, b: undefined };
JSON.stringify(obj); // '{"a":1}'

// Use null for explicit absence
const obj = { a: 1, b: null };
JSON.stringify(obj); // '{"a":1,"b":null}'
```

### Handling Strategy

**Guard Clauses**:

```typescript
// Check preconditions early
if (!this._data) return;
if (this._data.length === 0) return;
if (!this._data[0].name) return;

// Now can safely use data
this.processData(this._data[0].name);
```

**Defensive Programming**:

```typescript
// Validate inputs
function process(items: unknown) {
	if (!Array.isArray(items)) {
		throw new Error('Expected array');
	}
	// Safe to use items as array
}

// Use type guards
if (isContentModel(data)) {
	// TypeScript knows data is UmbContentModel
}
```

**Fail Fast**:

```typescript
// Throw errors early for programmer mistakes
if (this._repository === undefined) {
	throw new Error('Repository not initialized');
}

// Handle expected errors gracefully
try {
	const data = await this._repository.loadById(id);
} catch (error) {
	this._error = 'Failed to load content';
	return;
}
```

**Document Edge Cases**:

```typescript
/**
 * Loads content by ID.
 * @throws {UmbContentNotFoundError} If content doesn't exist
 * @throws {UmbUnauthorizedError} If user lacks permission
 * @returns {Promise<UmbContentModel>} The content model
 *
 * @remarks
 * This method returns cached data if available.
 * Pass `{ skipCache: true }` to force a fresh load.
 */
async loadById(id: string, options?: LoadOptions): Promise<UmbContentModel> {
	// ...
}
```

---

### Auth & Cross-tab Coordination

**`window.opener` is set for ANY window opened with `window.open()`**

Not just OAuth popups. The preview window, for example, also has `window.opener` set. Do not use `window.opener` alone as a signal that you are in the OAuth code exchange flow — check the pathname too:

```typescript
// ❌ Too broad — breaks preview window, any other window.open() target
if (window.opener) return;

// ✅ Specific to the OAuth code exchange popup
const pathname = pathWithoutBasePath({ start: true, end: false });
if (window.opener && pathname === '/oauth_complete') return;
```

This mistake caused issue #22083 (preview window stuck loading) — the `window.opener` guard in `#setAuthStatus()` prevented `setInitialState()` from running in the preview window, so `isAuthorized` never became `true`.

**BroadcastChannel does NOT deliver messages to the sender's own tab**

Only other tabs/windows receive the message. Do not call methods that broadcast inside a BroadcastChannel handler — this causes N² message storms:

```typescript
// ❌ This tab sent 'sessionUpdate', now it receives it back and broadcasts again
this.#channel.onmessage = (evt) => {
	if (evt.data.type === 'sessionUpdate') {
		this.#updateSession(...); // #updateSession also calls postMessage — storm!
	}
};

// ✅ Use the local-only setter inside handlers, not the broadcasting wrapper
this.#channel.onmessage = (evt) => {
	if (evt.data.type === 'sessionUpdate') {
		this.#setSessionLocally(...); // no broadcast
	}
};
```

**`sessionRequest` must guard against sharing expired sessions**

When a new tab asks for the current session via BroadcastChannel `sessionRequest`, only respond if the session is still valid. Sharing an expired session causes the recipient to believe it is already authorized and skip the auth flow:

```typescript
// ✅ Guard with isSessionValid() before responding
case 'sessionRequest': {
	if (this.isSessionValid()) {
		this.#channel.postMessage({ type: 'sessionResponse', session: this.#session.getValue()! });
	}
	break;
}
```

**Web Lock `umb:token-refresh` deduplicates concurrent `/token` calls across tabs**

Only one tab at a time should call `/token`. If your code needs to wait for an ongoing refresh in another tab before sending a request (to avoid using a token that is about to be revoked), query the lock state first and then conditionally wait:

```typescript
// Check if another tab is currently refreshing
const state = await navigator.locks.query();
if (state.held?.some((l) => l.name === 'umb:token-refresh')) {
	// Wait for it to finish (no-op inside the lock)
	await navigator.locks.request('umb:token-refresh', async () => {});
}
// Now safe to proceed — cookie reflects the latest token
```

Note: there is a TOCTOU gap between `query()` and `request()`. If the lock releases between the two calls, `request()` acquires and releases immediately — this is harmless.

---

### Src Umbraco.Web.UI.Client Docs Entities (`src/Umbraco.Web.UI.Client/docs/entities.md`)

# Entities & Entity Types

The entity system is the **primary discriminator** that ties together workspaces, trees, actions, pickers, and routing. Most extension types in the backoffice are scoped to one or more entity types.

---

## Core Concept

An **entity** is anything in the backoffice that needs to be identified and have extensions scoped to it. This includes obvious things like documents, media, and users, but also structural concepts like tree roots (`document-root`), folders (`data-type-folder`), and virtual groupings (`clipboard-root`, `extension-root`). Every entity is identified by two values:

```typescript
interface UmbEntityModel {
  entityType: string;       // Type discriminator (e.g., 'document', 'media-root', 'data-type-folder')
  unique: string | null;    // Instance identifier (GUID string, or null for roots/singletons)
}
```

**entityType** is a string constant that classifies what something is. The extension system uses it to determine which actions, tree items, workspaces, and other extensions apply. It is the glue between independently-registered extensions.

---

## Defining Entity Types

Each domain package defines its entity type constants in an `entity.ts` file:

```typescript
// src/packages/documents/documents/entity.ts
export const UMB_DOCUMENT_ENTITY_TYPE = 'document';
export const UMB_DOCUMENT_ROOT_ENTITY_TYPE = 'document-root';

export type UmbDocumentEntityType = typeof UMB_DOCUMENT_ENTITY_TYPE;
export type UmbDocumentRootEntityType = typeof UMB_DOCUMENT_ROOT_ENTITY_TYPE;
```

**Conventions:**
- Constant: `UMB_{DOMAIN}_ENTITY_TYPE` (e.g., `UMB_MEDIA_ENTITY_TYPE`, `UMB_USER_ENTITY_TYPE`)
- Root type: `UMB_{DOMAIN}_ROOT_ENTITY_TYPE` — represents the tree root (unique is `null`)
- TypeScript type: `Umb{Domain}EntityType` — literal type derived via `typeof` for type-safe filtering
- Union type: `Umb{Domain}EntityTypeUnion` — combines all entity types for a domain

---

## Entity Context

`UmbEntityContext` (provided via `UMB_ENTITY_CONTEXT`) propagates the current entity's type and unique through the component tree. It is set by workspaces, tree items, and other entity-aware containers.

```typescript
// Consuming entity context in an element
this.consumeContext(UMB_ENTITY_CONTEXT, (context) => {
  this.observe(context.entityType, (entityType) => { /* react to type */ });
  this.observe(context.unique, (unique) => { /* react to identity */ });
});
```

Key file: `src/packages/core/entity/entity.context.ts`

---

## How Entity Types Connect Extensions

Entity types are the **primary filter** for several extension types. Extensions declare which entity types they support via `forEntityTypes: string[]`:

### Entity Actions

Actions that appear in context menus and action bars for specific entity types:

```typescript
{
  type: 'entityAction',
  kind: 'delete',
  alias: 'Umb.EntityAction.Document.Delete',
  forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
  meta: { /* ... */ },
}
```

### Tree Items

Custom tree item renderers for specific entity types:

```typescript
{
  type: 'treeItem',
  alias: 'Umb.TreeItem.Document',
  forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
  element: () => import('./document-tree-item.element.js'),
}
```

### Entity Bulk Actions

Bulk operations on multiple selected entities of a type:

```typescript
{
  type: 'entityBulkAction',
  alias: 'Umb.EntityBulkAction.Document.Delete',
  forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
}
```

### Entity Item Refs

Custom renderers for entity references (used in pickers, relation displays):

```typescript
{
  type: 'entityItemRef',
  alias: 'Umb.EntityItemRef.Member',
  forEntityTypes: [UMB_MEMBER_ENTITY_TYPE],
  element: () => import('./member-item-ref.element.js'),
}
```

At runtime, the system filters extensions by checking `extension.forEntityTypes.includes(currentEntityType)`.

---

## Workspaces & Routing

Workspaces register with a specific entity type in their manifest `meta`:

```typescript
{
  type: 'workspace',
  kind: 'routable',
  alias: 'Umb.Workspace.Document',
  meta: {
    entityType: UMB_DOCUMENT_ENTITY_TYPE,   // Routes to this workspace
  },
}
```

Entity type is part of the URL path pattern:

```
/section/{sectionName}/workspace/{entityType}/edit/{unique}
```

For example: `/section/content/workspace/document/edit/abc-123`

The workspace context sets the entity type on `UMB_ENTITY_CONTEXT`, making it available to all child extensions (views, actions, sidebar apps).

---

## Entity Type Conditions

Two built-in conditions control extension visibility based on entity state:

**EntityTypeCondition** — matches the current entity context's type:
```typescript
conditions: [
  { alias: 'Umb.Condition.EntityType', match: 'document' }
]
// or match multiple:
conditions: [
  { alias: 'Umb.Condition.EntityType', oneOf: ['document', 'media'] }
]
```

**WorkspaceEntityTypeCondition** — matches the workspace's entity type (useful for workspace views/actions that should only appear for specific entity types):
```typescript
conditions: [
  { alias: 'Umb.Condition.WorkspaceEntityType', match: 'document' }
]
```

---

## Adding a New Entity Type

When creating a new domain feature, define its entity type first — everything else builds on it:

1. **Define constants** in `entity.ts` (type constant + root type constant + TypeScript types)
2. **Register extensions** with `forEntityTypes` pointing to your constant (tree items, actions, item refs)
3. **Register workspace** with `meta.entityType` pointing to your constant
4. **Define paths** using `UMB_WORKSPACE_PATH_PATTERN` with your entity type
5. **Use entity context** in workspace contexts and elements to propagate identity

---

### Src Umbraco.Web.UI.Client Docs Error Handling (`src/Umbraco.Web.UI.Client/docs/error-handling.md`)

# Error Handling
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)

---


### Diagnosis Process

1. **Check browser console** for errors and warnings
2. **Reproduce consistently** - Identify exact steps
3. **Check network tab** for failed API calls
4. **Inspect element** to verify DOM structure
5. **Check Lit reactive update cycle** - Use `element.updateComplete`
6. **Verify context availability** - Check context providers
7. **Review event listeners** - Check event propagation
8. **Use browser debugger** with source maps

**Common Web Component Issues**:
- Component not rendering: Check if custom element is defined
- Properties not updating: Verify `@property()` decorator and reactive update cycle
- Events not firing: Check event names and listeners
- Shadow DOM issues: Use `shadowRoot.querySelector()` not `querySelector()`
- Styles not applied: Check Shadow DOM style encapsulation
- Context not available: Ensure context provider is ancestor in DOM tree

### Error Handling Standards

**Error Classes**:

```typescript
// Use built-in Error class or extend it
throw new Error('Failed to load content');

// Custom error classes for domain errors
export class UmbContentNotFoundError extends Error {
	constructor(id: string) {
		super(`Content with id "${id}" not found`);
		this.name = 'UmbContentNotFoundError';
	}
}
```

**Repository Pattern Error Handling**:

```typescript
async requestById(id: string): Promise<{ data?: UmbContentModel; error?: Error }> {
	try {
		const response = await this._apiClient.getById({ id });
		return { data: response.data };
	} catch (error) {
		return { error: error as Error };
	}
}

// Usage
const { data, error } = await repository.requestById('123');
if (error) {
	// Handle error
	console.error('Failed to load content:', error);
	return;
}
// Use data
```

**Observable Error Handling**:

```typescript
this.observe(dataSource$, (value) => {
	// Success handler
	this._data = value;
}).catch((error) => {
	// Error handler
	console.error('Observable error:', error);
});
```

**Promise Error Handling**:

```typescript
// Always use try/catch with async/await
async myMethod() {
	try {
		const result = await this.fetchData();
		return result;
	} catch (error) {
		console.error('Failed to fetch data:', error);
		throw error; // Re-throw or handle
	}
}

// Or use .catch()
this.fetchData()
	.then(result => this.handleResult(result))
	.catch(error => this.handleError(error));
```

### Web Component Error Handling

**Lifecycle Errors**:

```typescript
export class UmbMyElement extends UmbElementMixin(LitElement) {
	constructor() {
		try {
			super();
			// Initialization that might throw
		} catch (error) {
			console.error('Failed to initialize element:', error);
		}
	}

	async connectedCallback() {
		try {
			super.connectedCallback();
			// Async initialization
			await this.loadData();
		} catch (error) {
			console.error('Failed to connect element:', error);
			this._errorMessage = 'Failed to load component';
		}
	}
}
```

**Render Errors**:

```typescript
override render() {
	if (this._error) {
		return html`<umb-error-message .error=${this._error}></umb-error-message>`;
	}

	if (!this._data) {
		return html`<uui-loader></uui-loader>`;
	}

	return html`
		
	`;
}
```

### Logging Standards

**Console Logging**:

```typescript
// Development only - Remove before production
console.log('Debug info:', data);

// Errors - Kept in production but sanitized
console.error('Failed to load:', error);

// Warnings
console.warn('Deprecated API usage:', method);

// Avoid console.log in production code (ESLint warning)
```

**Custom Logging** (if needed):

```typescript
// Use debug flag for verbose logging
if (this._debug) {
	console.log('[UmbMyComponent]', 'State changed:', this._state);
}
```

**Don't Log Sensitive Data**:
- User credentials
- API tokens
- Personal information (PII)
- Session IDs
- Full error stack traces in production

### Development Environment

- **Source Maps**: Enabled in Vite config for debugging
- **Error Overlay**: Vite provides error overlay in development
- **Hot Module Replacement**: Instant feedback on code changes
- **Detailed Errors**: Full stack traces with source locations
- **TypeScript Checking**: Real-time type checking in IDE

### Production Environment

- **Sanitized Errors**: No stack traces exposed to users
- **User-Friendly Messages**: Show helpful error messages
- **Error Boundaries**: Catch errors at component boundaries
- **Graceful Degradation**: Fallback UI when errors occur
- **Error Reporting**: Log errors to console (browser dev tools)

**Production Error Display**:

```typescript
private _errorMessage?: string;

override render() {
	if (this._errorMessage) {
		return html`
			<uui-box>
				<p class="error">${this._errorMessage}</p>
				<uui-button @click=${this._retry} label="Try Again"></uui-button>
			</uui-box>
		`;
	}
	// ... normal render
}
```

### Context-Specific Error Handling

**HTTP Client Errors** (OpenAPI):

```typescript
try {
	const response = await this._apiClient.getDocument({ id });
	return response.data;
} catch (error) {
	if (error.status === 404) {
		throw new UmbContentNotFoundError(id);
	}
	if (error.status === 403) {
		throw new UmbUnauthorizedError('Access denied');
	}
	throw error;
}
```

**Observable Subscription Errors**:

```typescript
this._subscription = this._dataSource.asObservable().subscribe({
	next: (value) => this._data = value,
	error: (error) => {
		console.error('Observable error:', error);
		this._errorMessage = 'Failed to load data';
	},
	complete: () => console.log('Observable completed'),
});
```

---

### Src Umbraco.Web.UI.Client Docs Manifests (`src/Umbraco.Web.UI.Client/docs/manifests.md`)

# Manifests & Aliases

Every extension in the Backoffice is described by a **manifest** — a plain object that declares what the extension is, how it behaves, and when it should be active. Manifests are the fundamental building blocks of the extension-first architecture: all UI (sections, dashboards, property editors, workspaces, trees, actions) are registered via manifests. Any default behavior can be replaced, overridden, or removed.

**Prerequisites**: [Architecture](./architecture.md)

---

## Manifest Shape

All manifests extend `ManifestBase`:

```typescript
interface ManifestBase {
  type: string;       // Extension type (e.g., 'dashboard', 'workspace', 'entityAction')
  alias: string;      // Unique identifier — the extension's address in the registry
  name: string;       // Human-readable label (metadata, not used for lookup)
  kind?: string;      // Optional kind — inherits a manifest base from a kind-manifest
  weight?: number;    // Ordering — higher numbers appear first (default: 0)
}
```

Most extension types add further properties on top of `ManifestBase`:

- **`element`** / **`js`** — lazy-loaded UI component: `element: () => import('./my-element.js')`
- **`api`** — lazy-loaded API class (contexts, actions, conditions): `api: () => import('./my-api.js')`
- **`meta`** — type-specific configuration object (label, icon, pathname, repository aliases, etc.)
- **`conditions`** — declarative rules for when the extension is active
- **`overwrites`** — alias(es) of extensions this one replaces

### Minimal Example

```typescript
const manifest: UmbExtensionManifest = {
  type: 'dashboard',
  alias: 'My.Dashboard.Analytics',
  name: 'Analytics Dashboard',
  element: () => import('./analytics-dashboard.element.js'),
  weight: 100,
  meta: {
    label: 'Analytics',
    pathname: 'analytics',
  },
  conditions: [
    { alias: 'Umb.Condition.SectionAlias', match: 'Umb.Section.Content' },
  ],
};
```

---

## The Alias

The **alias** is the single most important property on a manifest. It is:

- **Required** — registration is rejected without it (console error, no exception thrown)
- **Unique** — duplicate aliases are rejected; the second registration is ignored (console error, no exception thrown)
- **The reference handle** — other manifests point to an extension by its alias (conditions, `overwrites`, `forWorkspaceActions`, repository aliases in `meta`)
- **Stable** — changing an alias is a breaking change because external consumers may reference it

### Alias vs. Name

| Property | Purpose | Used for lookup | Example |
|----------|---------|-----------------|---------|
| `alias` | Unique identifier | Yes | `'Umb.Workspace.Document'` |
| `name` | Human-readable description | No | `'Document Workspace'` |

The `name` is metadata for debugging and documentation. The `alias` is the functional identity.

### Naming Convention

Aliases are **dot-separated PascalCase segments**:

```
{Domain}.{ExtensionType}.{Subject}[.{Discriminator}]
```

- **Domain** — `Umb` for all core extensions. Third-party packages use their own prefix (company name, product name). **Exception**: `propertyEditorSchema` aliases use `Umbraco` as the domain (e.g., `Umbraco.Plain.String`) — these mirror server-side schema aliases.
- **ExtensionType** — the extension type in PascalCase (e.g., `Workspace`, `EntityAction`, `WorkspaceView`). For kind definitions, this becomes two segments: `Kind.{ExtensionType}` (e.g., `Kind.EntityAction`, `Kind.WorkspaceAction`).
- **Subject** — the entity type (e.g., `Document`, `Media`, `User`) or feature name (e.g., `HealthCheck`, `WorkspaceAlias`, `Clipboard`) depending on the extension type.
- **Discriminator** (optional) — distinguishes multiple extensions of the same type for the same subject (e.g., `Delete`, `Detail`, `Save`).

**Examples**:

| Alias | Domain | ExtensionType | Subject | Discriminator |
|-------|--------|---------------|---------|---------------|
| `Umb.Workspace.Document` | `Umb` | `Workspace` | `Document` | — |
| `Umb.EntityAction.Document.Delete` | `Umb` | `EntityAction` | `Document` | `Delete` |
| `Umb.Repository.Document.Detail` | `Umb` | `Repository` | `Document` | `Detail` |
| `Umb.Kind.EntityAction.Delete` | `Umb` | `Kind.EntityAction` | `Delete` | — |
| `Umb.Kind.WorkspaceAction.Default` | `Umb` | `Kind.WorkspaceAction` | `Default` | — |
| `Umb.Condition.WorkspaceAlias` | `Umb` | `Condition` | `WorkspaceAlias` | — |
| `Umb.Dashboard.HealthCheck` | `Umb` | `Dashboard` | `HealthCheck` | — |
| `Umb.Modal.BlockCatalogue` | `Umb` | `Modal` | `BlockCatalogue` | — |
| `Umb.GlobalContext.Clipboard` | `Umb` | `GlobalContext` | `Clipboard` | — |
| `Umb.Bundle.Documents` | `Umb` | `Bundle` | `Documents` | — |

### Alias Constants

Aliases that are referenced by other code are defined as exported constants in a `constants.ts` file:

```typescript
// src/packages/documents/documents/workspace/constants.ts
export const UMB_DOCUMENT_WORKSPACE_ALIAS = 'Umb.Workspace.Document';
```

**Naming**: `UMB_{DOMAIN}_{TYPE}_ALIAS` — all caps, `UMB_` prefix, matches the alias structure.

Constants are imported wherever the alias is referenced — in manifest definitions, conditions, workspace context constructors, and action configurations:

```typescript
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './constants.js';

// In a manifest
{ alias: UMB_DOCUMENT_WORKSPACE_ALIAS, ... }

// In a condition
{ alias: UMB_WORKSPACE_CONDITION_ALIAS, match: UMB_DOCUMENT_WORKSPACE_ALIAS }

// In a workspace context constructor
super(host, { workspaceAlias: UMB_DOCUMENT_WORKSPACE_ALIAS, ... });
```

When an alias is only used within its own manifest declaration and never referenced elsewhere, a constant is not necessary — the string literal in the manifest is sufficient.

---

## How Aliases Connect Extensions

Aliases are the glue between independently-registered extensions. One extension references another by alias to create relationships at runtime.

### Conditions — Scoping to a Context

Conditions use aliases in two ways: the condition's own `alias` identifies the condition type, and its config properties (`match`, `oneOf`) reference the alias of the extension being matched:

```typescript
conditions: [
  // "Only active when the current workspace alias is Umb.Workspace.Document"
  { alias: 'Umb.Condition.WorkspaceAlias', match: 'Umb.Workspace.Document' },

  // "Only active in the Content or Media section"
  { alias: 'Umb.Condition.SectionAlias', oneOf: ['Umb.Section.Content', 'Umb.Section.Media'] },
]
```

Common conditions and their alias references:

| Condition | Config | References |
|-----------|--------|------------|
| `Umb.Condition.WorkspaceAlias` | `match` / `oneOf` | Workspace alias |
| `Umb.Condition.SectionAlias` | `match` / `oneOf` | Section alias |
| `Umb.Condition.WorkspaceEntityType` | `match` / `oneOf` | Entity type string |
| `Umb.Condition.WorkspaceContentTypeAlias` | `match` / `oneOf` | Content type alias |

### Repository Aliases in Meta

Actions and kinds reference repositories by alias in their `meta` configuration:

```typescript
{
  type: 'entityAction',
  kind: 'delete',
  alias: 'Umb.EntityAction.Document.Delete',
  forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
  meta: {
    detailRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS,  // 'Umb.Repository.Document.Detail'
    itemRepositoryAlias: UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS,      // 'Umb.Repository.Document.Item'
  },
}
```

The action's implementation resolves the repository from the extension registry at runtime using these aliases. This decouples the action from specific repository implementations.

### forWorkspaceActions — Linking Menu Items to Actions

`workspaceActionMenuItem` extensions attach to workspace actions by alias:

```typescript
{
  type: 'workspaceActionMenuItem',
  alias: 'Umb.Document.WorkspaceActionMenuItem.Unpublish',
  forWorkspaceActions: 'Umb.WorkspaceAction.Document.SaveAndPublish',  // target action alias
  meta: { label: '#actions_unpublish' },
}
```

The workspace action element filters menu items by matching `forWorkspaceActions` against its own alias.

### Overwrites — Replacing Extensions

The `overwrites` property references one or more aliases to replace:

```typescript
{
  type: 'entitySign',
  alias: 'Umb.EntitySign.Document.HasScheduledPublish',
  overwrites: 'Umb.EntitySign.Document.HasPendingChanges',  // replaces this extension
  // ...
}
```

When an extension with `overwrites` is permitted (conditions pass), the overwritten extension is removed from the active set. If the overwriting extension's conditions become false, the original reappears. Accepts a single alias string or an array of aliases.

---

## Registration

Three mechanisms exist for registering manifests, appropriate for different scenarios.

### Package Bundles (Internal Packages)

The standard pattern for packages within the monorepo. A `umbraco-package.ts` imports manifests from each sub-feature, assembles them into a static array, and passes that array directly to the bundle extension:

```typescript
// umbraco-package.ts
import { manifests as sectionManifests } from './section/manifests.js';
import { manifests as dashboardManifests } from './dashboard/manifests.js';
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';

export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [
  ...sectionManifests,
  ...dashboardManifests,
];

export const name = 'Umbraco.Documents';
export const extensions = [
  {
    type: 'bundle',
    alias: 'Umb.Bundle.Documents',
    name: 'Documents Bundle',
    js: {
      manifests,
    },
  },
];
```

#### Manifest Bundling

Each sub-feature exports its own `manifests` array from its local `manifests.ts`. These are imported and spread directly into the package-level `manifests` array in `umbraco-package.ts` — there is no separate root-level `manifests.ts` aggregator file.

The static `js: { manifests }` reference (instead of a dynamic `js: () => import('./manifests.js')`) allows Vite to bundle the manifest registrations together with the package file, eliminating a network round-trip per bundle at startup.

### Static Package Manifest (External Packages)

External packages use a `umbraco-package.json` file — a static JSON manifest discovered by the server.

### Runtime Registration

For dynamic or programmatic registration:

```typescript
umbExtensionsRegistry.register(manifest);
umbExtensionsRegistry.registerMany(manifests);
```

---

## Kinds

Kinds are **reusable base manifests** for an extension type. A kind provides pre-built `element`, `api`, and/or `meta` that extensions inherit — extensions only specify what differs.

Kinds are documented in detail in [Architecture — Kinds](./architecture.md#kinds).

**How kind matching uses aliases**: A kind manifest has its own `alias` (e.g., `'Umb.Kind.EntityAction.Delete'`), plus `matchType` and `matchKind` that tell the registry which extensions to merge it with. The extension references the kind by its `kind` property (e.g., `kind: 'delete'`), not by the kind's alias.

```typescript
// Kind definition — has its own alias, matches by type+kind
{
  type: 'kind',
  alias: 'Umb.Kind.EntityAction.Delete',
  matchType: 'entityAction',
  matchKind: 'delete',
  manifest: { /* defaults */ },
}

// Extension using the kind — references 'delete', not the kind alias
{
  type: 'entityAction',
  kind: 'delete',
  alias: 'Umb.EntityAction.Document.Delete',
  meta: { /* overrides */ },
}
```

The registry merges them: kind properties are the base, extension properties override, and `meta` is shallow-merged (extension meta extends kind meta).

---

## Key Extension Types

For reference, the most common extension types and their typical alias patterns:

| Extension Type | Alias Pattern | Key Properties |
|----------------|---------------|----------------|
| `workspace` | `Umb.Workspace.{Domain}` | `kind`, `api`, `meta.entityType` |
| `workspaceView` | `Umb.WorkspaceView.{Domain}.{View}` | `element`, `meta.label`, `meta.pathname`, conditions |
| `workspaceAction` | `Umb.WorkspaceAction.{Domain}.{Action}` | `kind`, `api`, `meta.label`, conditions |
| `workspaceActionMenuItem` | `Umb.{Domain}.WorkspaceActionMenuItem.{Item}` | `forWorkspaceActions`, `api`, `meta.label` |
| `workspaceContext` | `Umb.WorkspaceContext.{Domain}.{Feature}` | `api`, conditions |
| `dashboard` | `Umb.Dashboard.{Name}` | `element`, `meta.label`, `meta.pathname`, conditions |
| `section` | `Umb.Section.{Name}` | `meta.label`, `meta.pathname` |
| `sectionView` | `Umb.SectionView.{Section}.{View}` | `element`, `meta.label`, conditions |
| `entityAction` | `Umb.EntityAction.{Domain}.{Action}` | `kind`, `forEntityTypes`, `meta` |
| `entityBulkAction` | `Umb.EntityBulkAction.{Domain}.{Action}` | `forEntityTypes`, `meta` |
| `tree` | `Umb.Tree.{Domain}` | `meta.repositoryAlias` |
| `treeItem` | `Umb.TreeItem.{Domain}` | `forEntityTypes`, `element` |
| `propertyEditorUi` | `Umb.PropertyEditorUi.{Name}` | `element`, `meta.label`, `meta.icon` |
| `repository` | `Umb.Repository.{Domain}.{Category}` | `api` |
| `store` | `Umb.Store.{Domain}.{Category}` | `api` |
| `condition` | `Umb.Condition.{Name}` | `api` |
| `kind` | `Umb.Kind.{Type}.{Name}` | `matchType`, `matchKind`, `manifest` |
| `bundle` | `Umb.Bundle.{Package}` | `js` (lazy import) |
| `globalContext` | `Umb.GlobalContext.{Name}` | `api` |
| `modal` | `Umb.Modal.{Name}` | `element` |

---

## Files & Source

| File | Purpose |
|------|---------|
| `src/libs/extension-api/types/manifest-base.interface.ts` | `ManifestBase` — defines `alias`, `type`, `name`, `kind`, `weight` |
| `src/libs/extension-api/types/manifest-kind.interface.ts` | `ManifestKind` — defines kind structure with `matchType`, `matchKind` |
| `src/libs/extension-api/types/condition.types.ts` | `ManifestWithDynamicConditions` — adds `conditions` and `overwrites` |
| `src/libs/extension-api/registry/extension.registry.ts` | `UmbExtensionRegistry` — registration, validation, lookup, kind merging |
| `src/libs/extension-api/controller/base-extension-initializer.controller.ts` | Single extension lifecycle, condition evaluation, `overwrites` handling |
| `src/libs/extension-api/controller/base-extensions-initializer.controller.ts` | Multi-extension lifecycle, overwrite resolution across extensions |

---

### Src Umbraco.Web.UI.Client Docs Package Development (`src/Umbraco.Web.UI.Client/docs/package-development.md`)

# Package Development

---

How to structure, organize, and build packages within the Umbraco Backoffice frontend. For architecture concepts (extension registry, context API, repository pattern), see [Architecture](./architecture.md). For import boundary rules, see [Developer Roles](./architecture.md#developer-roles--import-boundaries).

---

## Package & Module Structure

Each top-level folder under `src/packages/` is a **package root**. A package contains one or more **modules**, each in its own subfolder.

```
src/packages/media/          <- package root
├── media/                   <- module (media management)
│   ├── index.ts
│   ├── manifests.ts
│   └── ...
├── imaging/                 <- module (image processing)
│   ├── index.ts
│   ├── manifests.ts
│   └── ...
└── umbraco-package.ts       <- aggregates module manifests + bundle entry point
```

### Public vs. Private Modules

A module becomes public by adding an import map entry in the root `package.json` exports field (see [Registering Package Module Export](#registering-package-module-export)). Only export modules with a clear external use case.

- The public API (`index.ts` + `package.json` export) is a contract with consumers.
- Keep code private until needed — non-exported code can be freely refactored.
- Once exported, changes require the [deprecation process](./deprecation.md).

---

## Folder Structure Conventions

**Sub-folders are organized by sub-feature, not by technical role.** Each sub-feature folder contains everything related to that feature — elements, repository, store, modals, actions, constants, types, and manifests. No top-level `repositories/`, `modals/`, or `actions/` folders.

- Every sub-feature is self-contained (full vertical slice). Deleting the folder should break nothing outside it (apart from the missing manifest registration).
- Every folder with registerable items has its own `manifests.ts`. Child manifests bubble up to the package root.
- Every sub-feature has `index.ts`, `manifests.ts`, and `constants.ts` at minimum. Add `types.ts` for shared types.

### `local-components/`

Elements only used within a sub-feature go in `local-components/`. Never exported — private implementation details.

### `global-components/`

Reusable elements for other packages. Must be registered as custom elements at startup and exported from the package's `index.ts`.

```typescript
@customElement('umb-feature')
export class UmbFeatureElement extends UmbLitElement {
	render() {
		return html`Feature Element`;
	}
}

declare global {
	interface HTMLElementTagNameMap {
		'umb-feature': UmbFeatureElement;
	}
}
```

### Manifest Bundling

Each sub-feature exports its own `manifests` array from its local `manifests.ts`. These bubble up to `umbraco-package.ts`, which assembles them and registers the bundle — there is no separate root-level `manifests.ts`. See [Manifests & Aliases — Package Bundles](./manifests.md#package-bundles-internal-packages) for the full pattern.

---

## The Package's Public API (index.ts)

Only export what constitutes the public contract — context tokens, types, constants, and global component classes. **Manifests are NOT exported from `index.ts`** — they are registered internally via the bundle mechanism.

---

## Registering Package Module Export

To make a module importable by other packages:

1. Add the subpath to `package.json` exports:

```json
{ "exports": { "./module": "./dist-cms/packages/package/module/index.js" } }
```

2. Run the generators:

```bash
npm i && npm run generate:tsconfig
```

This ensures TypeScript, the browser import map, and test configs all resolve the new package.

---

## Localization

No hardcoded UI-facing strings. All user-visible text must go through the localization system. Keys live in `src/assets/lang/en.ts`, grouped by feature area, referenced as `group_keyName` (e.g., `this.localize.term('actions_create')`).

For step-by-step instructions on adding localization keys and using them in elements or controllers, use the `general-add-localization` skill.

### Type-safe localization keys

`this.localize.term()`, `termOrDefault()`, and the `<umb-localize key="…">` element are typed against the `en.ts` localization.

After adding or renaming a key in `en.ts`, run:
```bash
npm run generate:localization-keys
```
The script also runs automatically as a `prebuild` hook, and as part of the test scripts.

### Active language

The active language is driven by the shell elements `<umb-app>` and `<umb-auth>`, not by `<html lang>`:

- Razor sets `lang` on the shell element from `GlobalSettings.DefaultUILanguage`. The shell reads its own `lang` on connect and calls `umbLocalizationRegistry.loadLanguage(this.lang)`.
- After login, `current-user.context` calls `loadLanguage(user.languageIsoCode)` and the shell mirrors the new value back onto its own `lang` attribute via `umbLocalizationRegistry.currentLanguage`.
- `<html lang>` is the static `"en"` for the noscript fallback text. Don't conflate it with the dynamic UI language.
- Locale-sensitive formatting can use a different locale from the requested one. `Intl` resolves a language-only tag to that language's default region, which is wrong where the dictionary we ship is written for another region — `en` is UK English but resolves to US conventions — so the registry corrects those cases. `umbLocalizationManager.documentLanguage` holds the corrected locale; `this.localize.date()` and `.number()` already use it.

If you're adding a new shell-like element (rare — most code lives inside `<umb-app>`), give it a `lang` attribute and the same subscribe-and-mirror pattern. For everything else, just use `this.localize` and the inherited context resolves the rest.

### Default UI language vs fallback culture

Two separate concepts — don't conflate them:

- **Active UI locale** — `GlobalSettings.DefaultUILanguage` (default `en-US`). What Razor renders as `lang` on the shell, what `loadLanguage()` is called with. The locale users actually see.
- **Fallback culture** — `en` (`UMB_DEFAULT_LOCALIZATION_CULTURE`). The culture the canonical `en.ts` dictionary ships under (`Umb.Localization.EN`). Always loaded _alongside_ the active locale so any missing key falls back to English.

A third-party language pack overriding canonical keys for a default install should declare `culture: 'en-US'` (matches active locale), not `culture: 'en'`. The keys come _from_ `en.ts`, but the override extension's `culture` must match the active locale, otherwise the registry filters it out.

---

## Conventions & Rules

1. **Never import from `dist-cms/` directly.** Always use `@umbraco-cms/backoffice/<subpath>`.
2. **Every new module export must be added to `package.json` exports** and regenerated.
3. **Use `UmbLitElement` or `UmbElementMixin`** as base class — never raw `LitElement`.
4. **Export `api`** from files providing a class for the extension registry to instantiate (workspace contexts, actions).
5. **Use element lazy loading** in manifests: `element: () => import('./path.js')`.
6. **Use `@umbraco-ui/uui` components** for UI consistency (`<uui-box>`, `<uui-button>`, `<uui-input>`, etc.).
7. **Declare custom manifest types** on `UmbExtensionManifestMap` so other packages can extend yours.
8. **No hardcoded UI strings** — use the localization system. See [Localization](#localization) above and the `general-add-localization` skill.
9. **Element naming**: `umb-` prefix for core. Package developers use their own prefix.
10. **Extension aliases**: Dot-separated namespace — see [Manifests & Aliases — Naming Convention](./manifests.md#naming-convention).

---

### Src Umbraco.Web.UI.Client Docs Plugin Development (`src/Umbraco.Web.UI.Client/docs/plugin-development.md`)

#### Adding plugin-specific localization keys

A plugin that wants its own typed localization keys needs two pieces — runtime registration and (optionally) type declaration. They're decoupled: register-only works fine, the type declaration just enables autocomplete and arg-type inference on the plugin's own keys.

**1. Register the strings at runtime** via a `localization` extension. The runtime registers them under the active culture, flattens `group.key` into `group_key`, and merges into the dictionary the controller looks up.

Inline form (best for small overrides):

```json
// umbraco-package.json
{
    "alias": "mypkg.extensions",
    "name": "MyPkg",
    "extensions": [
        {
            "type": "localization",
            "alias": "Mypkg.Localize.EnUS",
            "name": "English",
            "meta": {
                "culture": "en-US",
                "localizations": {
                    "mypkg": {
                        "anything": "Some string",
                        "greeting": "Hello, %0%!"
                    }
                }
            }
        }
    ]
}
```

For larger packs, point `js` at a separate JavaScript file (e.g. `"js": "/App_Plugins/MyPkg/en-us.js"`) that default-exports the same `{ group: { key: value } }` shape. The JS file form supports function entries that take typed arguments — string entries use the runtime `%0%` / `{0}` placeholder pattern.

**2. Declare the matching types globally** (optional, recommended) — same pattern as `UmbExtensionManifestMap`, plain interface merging, no module-path boilerplate:

```ts
// types.d.ts (or anywhere in the plugin's source tree)
declare global {
    interface UmbKnownLocalizationSet {
        mypkg_anything: string;
        mypkg_greeting: (name: string) => string;
    }
}
```

Plugin authors typically drop this in a single `types.d.ts` at the package root and ship it alongside the npm package — consumers of the plugin get autocomplete on the augmented keys automatically, with no extra config.

**3. Use them like built-in keys:**

```ts
this.localize.term('mypkg_anything');           // autocompletes alongside the built-ins
this.localize.term('mypkg_greeting', 'Alice');  // 'Alice: string' inferred from the declaration
html`<umb-localize key="mypkg_anything"></umb-localize>`;
```

Plugins that skip step 2 still work — their keys hit the `(string & {})` escape hatch in `term()`'s signature, so `localize.term('mypkg_anything')` compiles. They just lose autocomplete and arg-type inference on those keys.

---

### Src Umbraco.Web.UI.Client Docs Repositories (`src/Umbraco.Web.UI.Client/docs/repositories.md`)

# Repositories

How data access is organized across features. Repositories separate **where data comes from** from the UI that consumes it — the UI never knows whether data comes from a server API, a manifest, or a local cache.

For the full data flow chain (element → context → repository → data source → API client), see [Data Flow](./data-flow.md). This document focuses on how repositories are structured, categorized, and organized within the codebase.

---

## Core Concept

A repository is a **domain-specific, feature-scoped** data access layer. It provides a clean interface for a specific operation (CRUD, listing, tree navigation, or a specialized action) without exposing transport details.

**Key principles:**

- **Feature-scoped** — A repository lives with the feature that uses it, both in naming and file location. A publishing repository lives inside the publishing feature folder, not in a generic repository folder.
- **Extension-registered** — Repositories are registered as `type: 'repository'` extensions with lazy-loaded `api` imports. Any extension can override a repository by registering the same alias with a higher weight.
- **Data source delegation** — The repository orchestrates but doesn't call APIs directly. It delegates to a data source that handles mapping between server types and domain models. See [Data Flow](./data-flow.md) for the full delegation pattern.
- **One concern per repository** — A detail repository handles CRUD. A publishing repository handles publish/unpublish. Don't mix concerns.

---

## Repository Categories

Repositories fall into two groups: **standard** (backed by base classes) and **action-specific** (custom, feature-driven).

### Standard Repositories

These have base classes in `src/packages/core/` that handle the boilerplate.

| Category | Base Class | Purpose | Interface |
|----------|-----------|---------|-----------|
| **Detail** | `UmbDetailRepositoryBase<T>` | CRUD for a single entity | `UmbDetailRepository<T>` |
| **Item** | `UmbItemRepositoryBase<T>` | Batch fetch multiple items by unique IDs | `UmbItemRepository<T>` |
| **Tree** | `UmbTreeRepositoryBase<T, R>` | Hierarchical navigation (root, children, ancestors) | `UmbTreeRepository<T, R>` |
| **Collection** | *(no base class — extend `UmbRepositoryBase`)* | Paginated/filtered lists | `UmbCollectionRepository<T, F>` |

**Detail** is the most common — every entity that can be created, read, updated, or deleted needs one.

**Item** is for lightweight lookups — fetching display info (name, icon, entity type) for a set of known IDs. Used by pickers, references, and breadcrumbs.

**Tree** is for entities with hierarchical navigation in the sidebar — documents, media, document types.

**Collection** is for flat or filtered lists — the grid/table view of entities.

### Action-Specific Repositories

These are custom repositories for operations that don't fit CRUD. They extend `UmbRepositoryBase` or `UmbControllerBase` directly and define their own methods.

Examples from the document entity:

| Repository | Purpose | Methods |
|-----------|---------|---------|
| `UmbDocumentPublishingRepository` | Publish/unpublish variants | `publish()`, `unpublish()`, `publishWithDescendants()`, `published()` |
| `UmbDuplicateDocumentRepository` | Duplicate a document | `requestDuplicate()` |
| `UmbDocumentCultureAndHostnamesRepository` | Domain/culture assignments | `readCultureAndHostnames()`, `updateCultureAndHostnames()` |
| `UmbBulkMoveToDocumentRepository` | Bulk move action | `requestBulkMoveTo()` |

Action-specific repositories follow the same data source delegation pattern but define domain-appropriate methods instead of generic CRUD.

---

## File Structure & Naming

Repositories live **with their feature**, not in a separate data-access layer. The folder structure mirrors the feature hierarchy.

### Standard repositories (detail, item)

```
{package}/{entity}/
├── detail/
│   └── data/
│       ├── {entity}-detail.repository.ts
│       ├── {entity}-detail.server.data-source.ts
│       ├── {entity}-detail.store.ts
│       ├── {entity}-detail.store.context-token.ts
│       ├── manifests.ts
│       └── constants.ts
├── item/                                  # Optional — only if entity needs item lookups
│   └── data/
│       ├── {entity}-item.repository.ts
│       ├── {entity}-item.server.data-source.ts
│       ├── {entity}-item.store.ts
│       ├── {entity}-item.store.context-token.ts
│       └── manifests.ts
```

### Tree repository

```
{package}/{entity}/
├── tree/
│   └── data/
│       ├── {entity}-tree.repository.ts
│       ├── {entity}-tree.server.data-source.ts
│       ├── {entity}-tree.store.context-token.ts
│       └── manifests.ts
```

### Collection repository

```
{package}/{entity}/
├── collection/
│   └── data/
│       ├── {entity}-collection.repository.ts
│       ├── {entity}-collection.server.data-source.ts
│       └── manifests.ts
```

### Action-specific repositories

```
{package}/{entity}/
├── entity-actions/
│   ├── duplicate/
│   │   └── data/
│   │       ├── {entity}-duplicate.repository.ts
│   │       ├── {entity}-duplicate.server.data-source.ts
│   │       └── types.ts
│   ├── move-to/
│   │   └── data/
│   │       └── ...
│   └── culture-and-hostnames/
│       └── data/
│           └── ...
├── entity-bulk-actions/
│   ├── move-to/
│   │   └── data/
│   │       └── ...
│   └── duplicate-to/
│       └── data/
│           └── ...
├── publishing/
│   └── data/
│       ├── {entity}-publishing.repository.ts
│       └── {entity}-publishing.server.data-source.ts
```

### Naming conventions

| Item | Pattern | Example |
|------|---------|---------|
| Repository class | `Umb{EntityName}{Category}Repository` | `UmbDocumentDetailRepository` |
| Data source class | `Umb{EntityName}{Category}ServerDataSource` | `UmbDocumentDetailServerDataSource` |
| Repository file | `{entity}-{category}.repository.ts` | `document-detail.repository.ts` |
| Data source file | `{entity}-{category}.server.data-source.ts` | `document-detail.server.data-source.ts` |
| Manifest alias | `Umb.Repository.{EntityName}.{Category}` | `Umb.Repository.Document.Detail` |
| Store alias | `Umb.Store.{EntityName}.{Category}` | `Umb.Store.Document.Detail` |

---

## When to Create Which Repository Type

| Scenario | Repository type | Base class |
|----------|----------------|------------|
| Entity with create/read/update/delete | Detail | `UmbDetailRepositoryBase<T>` |
| Fetching display info for known IDs (pickers, breadcrumbs) | Item | `UmbItemRepositoryBase<T>` |
| Sidebar tree navigation | Tree | `UmbTreeRepositoryBase<T, R>` |
| Paginated/filtered listing | Collection | `UmbRepositoryBase` + `UmbCollectionRepository<T>` |
| Domain action (publish, duplicate, move, sort) | Action-specific | `UmbRepositoryBase` or `UmbControllerBase` |

**Rule of thumb:** If there's a base class for it, use it. If the operation is unique to the domain, create a custom repository with just the methods needed.

---

## Reference Examples

Study these when implementing repositories:

| Pattern | Example path | What it shows |
|---------|-------------|---------------|
| Simple detail + item | `src/packages/webhook/webhook/repository/` | Minimal CRUD entity |
| Full entity (detail + item + tree + collection) | `src/packages/documents/documents/` | All repository categories |
| Action-specific (simple) | `src/packages/documents/documents/entity-actions/duplicate/repository/` | Single-action with notification |
| Action-specific (domain-rich) | `src/packages/documents/documents/publishing/repository/` | Multiple domain methods |
| Action-specific (read/write) | `src/packages/documents/documents/entity-actions/culture-and-hostnames/repository/` | Custom read + update |
| Data type as reference | `src/packages/data-type/` | Detail + item + tree + collection |

---

## Key Rules

1. **Repositories live with their feature** — colocated in the feature's directory, not in a shared `repositories/` folder
2. **One concern per repository** — detail CRUD, publishing, duplication, and tree navigation are separate repositories
3. **Use base classes when they exist** — only create custom repositories for operations without a base class
4. **Always delegate to a data source** — see [Data Flow](./data-flow.md) for the delegation pattern, `tryExecute`, and `{ data, error }` tuple conventions

---

## Classify by Operations, Not by Neighbors

When creating or reviewing a repository, determine its **category** based on what operations it performs — not based on what nearby files look like. Then verify it uses the correct base class, interfaces, and data flow conventions for that category.

Use the [When to Create Which Repository Type](#when-to-create-which-repository-type) table as the decision guide. If the operations map to a standard category, the repository must use the corresponding base class and interfaces. A repository should only extend `UmbRepositoryBase` directly when no standard category fits.

---

### Src Umbraco.Web.UI.Client Docs Security (`src/Umbraco.Web.UI.Client/docs/security.md`)

# Security

[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)

---

### Input Validation

**Validate All User Input**:

```typescript
// Use validation in forms
import { UmbValidationController } from '@umbraco-cms/backoffice/validation';

#validation = new UmbValidationController(this);

async #handleSubmit() {
	if (!this.#validation.validate()) {
		return; // Show validation errors
	}
	// Proceed with submission
}
```

**String Validation**:

```typescript
private _validateName(name: string): boolean {
	// Length check
	if (name.length === 0 || name.length > 100) {
		return false;
	}

	// Pattern check (example: alphanumeric and spaces)
	if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
		return false;
	}

	return true;
}
```

### Authentication & Authorization

**OpenID Connect with PKCE** (v17+):

- Backoffice uses PKCE authorization code flow
- Real tokens are stored exclusively in `__Host-umbAccessToken` / `__Host-umbRefreshToken` httpOnly cookies — JavaScript cannot read them
- The client-side bearer token value is always the literal string `'[redacted]'` — the server (`HideBackOfficeTokensHandler`) swaps it for the real cookie on each request
- Never try to read or store real tokens client-side; they are intentionally inaccessible

**Configuring API clients**:

```typescript
// ✅ One-liner for @hey-api/openapi-ts clients — handles auth + credentials automatically
authContext.configureClient(myClient);

// ✅ For manual fetch calls
const config = authContext.getOpenApiConfiguration();

// ❌ getLatestToken() is deprecated (returns '[redacted]' anyway) — scheduled for removal v19
const token = await authContext.getLatestToken();
```

**Do NOT call `validateToken()` per request**:
`validateToken()` forces a `/token` network call and revokes the previous access token as a side effect (OpenIddict reference tokens). Calling it on every API request causes token churn and ID2019 "token no longer valid" errors in concurrent requests. Use `configureClient()` instead — it has a built-in guard that only refreshes when the access token is actually expired.

**Authorization Checks**:

```typescript
// Check user permissions before actions
#authContext?: UmbAuthContext;

async #handleDelete() {
	const hasPermission = await this.#authContext?.hasPermission('delete');
	if (!hasPermission) {
		// Show error or hide action
		return;
	}
	// Proceed with deletion
}
```

**Context Security**:

- Use Context API for auth state (`UMB_AUTH_CONTEXT`)
- Never store tokens in localStorage, sessionStorage, or JS variables
- Backend handles token refresh via httpOnly cookies
- All API requests must use `credentials: 'include'` (handled automatically by `configureClient()`)

### API Security

**HTTP Client Security**:

```typescript
// OpenAPI client handles:
// - CSRF tokens
// - Request headers
// - Credentials
// - Error handling

// Use generated OpenAPI client
import { ContentResource } from '@umbraco-cms/backoffice/external/backend-api';

const client = new ContentResource();
const response = await client.getById({ id });
```

**CORS** (Backend Configuration):

- Configured in .NET backend
- Backoffice follows same-origin policy
- API calls to same origin

**Rate Limiting** (Backend):

- Handled by .NET backend
- Backoffice respects rate limit headers

### XSS Prevention

**Template Security** (Lit):

```typescript
import { html, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
import { sanitizeHTML, escapeHTML } from '@umbraco-cms/backoffice/utils';

// Lit automatically escapes content in templates
render() {

	// UNSAFE - This is only safe if you are 100% sure that htmlContent is sanitized properly before being set, and that it cannot be manipulated by user input in any way. Use with extreme caution.
	return html`<div>${unsafeHTML(this.htmlContent)}</div>`;

	// Safe - Automatically escaped
	return html`<div>${this.userContent}</div>`;

	// Safe - Use with sanitized content
	return html`<div>${unsafeHTML(sanitizeHTML(this.htmlContent))}</div>`;

	// Safe - Use with escaped content, which is essentially what Lit does by default
	return html`<div>${unsafeHTML(escapeHTML(this.htmlContent))}</div>`;

	// Safe - localize.htmlString() escapes interpolated args and wraps in unsafeHTML for rendering
	return html`<div>${this.localize.htmlString('#someKey_withHtml', this.userContent)}</div>`;

	// Safe - <umb-localize> component automatically escapes arguments
	return html`<umb-localize key="someKey_withHtml" .args=${[ this.userContent ]}></umb-localize>`;
}
```

**Localized HTML — `localize.string()` vs `localize.htmlString()`**:

- `localize.string(text, ...args)` — returns a plain string. Use for **non-HTML** contexts: attribute bindings (Lit auto-escapes), notification messages, button labels, log strings. Args are NOT escaped because Lit (or the consumer) handles the appropriate escaping for the context.
- `localize.htmlString(text, ...args)` — returns a Lit directive that renders via `unsafeHTML` with all args HTML-escaped. Use whenever the localized value contains HTML markup that must be rendered (e.g. `<a>` links, `<strong>` emphasis) — this is the only safe path when interpolating user-controlled args into HTML output.

```typescript
// ✅ Plain text — string() is correct (Lit escapes the attribute itself)
html`<uui-button label=${this.localize.string('#actions_delete')}></uui-button>`;

// ✅ HTML rendering — htmlString() escapes args + wraps in unsafeHTML
html`<p>${this.localize.htmlString('#defaultdialogs_confirmdelete', userControlledName)}</p>`;

// ❌ Manually combining string() + unsafeHTML leaves args un-escaped — XSS hazard
html`<p>${unsafeHTML(this.localize.string('#defaultdialogs_confirmdelete', userControlledName))}</p>`;
```

**Modal `content` field** (e.g. `umbConfirmModal`) renders strings via `unsafeHTML` internally. When passing a localized string with user-controlled args, either pre-escape the args (preferred, `content` stays a plain string) or wrap an `htmlString()` call in a template for keys containing markup. Note `term()`/`string()` take a **bare** key — only `string()`/`htmlString()` resolve a leading `#`, so `term()` never uses one:

```typescript
// ✅ Preferred — term() returns a plain string; escape user-controlled args at the call site
umbConfirmModal(this, {
	headline: '#actions_delete',
	content: this.localize.term('defaultdialogs_confirmdelete', escapeHTML(item.name ?? '')),
});

// ✅ Also safe — htmlString escapes args, html`...` wraps the directive in a TemplateResult
umbConfirmModal(this, {
	headline: '#actions_delete',
	content: html`${this.#localize.htmlString('#defaultdialogs_confirmdelete', item.name)}`,
});
```

**Notification `htmlMessage` field**: notification `data.message` (`peek()`, `stay()`, `umbPeekError`) is always rendered as plain text. To render HTML in a toast, set `data.htmlMessage` instead — it takes precedence over `message`. A string value is sanitized with `sanitizeHTML` before rendering (scripts and event handlers are stripped); a `TemplateResult` renders as-is since Lit escapes its bindings. Sanitization does not prevent markup injection (links, images), so never interpolate user-controlled content into an `htmlMessage` string — use a `TemplateResult` (or `localize.htmlString()`) so the interpolated values are escaped:

```typescript
// ✅ Safe — static HTML string, sanitized at render
notificationContext.peek('positive', {
	data: { message: 'Import finished', htmlMessage: 'Import finished — <a href="/report">view the report</a>' },
});

// ✅ Safe — TemplateResult: Lit escapes item.name
notificationContext.peek('positive', {
	data: { message: `${item.name} published`, htmlMessage: html`<strong>${item.name}</strong> published` },
});

// ❌ Unsafe — user-controlled content in an HTML string survives sanitization as markup (e.g. links)
notificationContext.peek('positive', {
	data: { message: 'Published', htmlMessage: `<strong>${item.name}</strong> published` },
});
```

**Attribute Binding**:

```typescript
// Safe - Lit escapes attribute values
render() {
	return html`<input value=${this.userInput} />`;
}
```

**Event Handlers**:

```typescript
// Safe - Event handlers are not strings
render() {
	return html`<button @click=${this.#handleClick}>Click</button>`;
}

// NEVER do this (code injection risk)
// render() {
//   return html`<button onclick="${this.userCode}">Click</button>`;
// }
```

### Content Security Policy

**CSP Headers** (Backend Configuration):

- Configured in .NET backend
- Restricts script sources
- Prevents inline scripts (except with nonce)
- Reports violations

**Backoffice Compliance**:

- No inline scripts
- No `eval()` or `Function()` constructor
- Monaco Editor uses web workers (CSP compliant)

### Dependencies Security

**Package Management**:

```bash
# Check for vulnerabilities
npm audit

# Fix automatically
npm audit fix

# Update dependencies carefully
npm update
```

**Dependency Security Practices**:

- Renovate bot automatically creates PRs for updates
- Review dependency changes before merging
- Only use packages from npm registry
- Verify package integrity
- Keep dependencies updated

**Known Vulnerabilities**:

- CI checks for vulnerabilities on every PR
- Security advisories reviewed regularly

### Common Vulnerabilities

**XSS (Cross-Site Scripting)**:

- ✅ Lit templates automatically escape content
- ✅ DOMPurify for HTML sanitization
- ❌ Never use `unsafeHTML` with user input directly
- ❌ Never set `innerHTML` with user input

**CSRF (Cross-Site Request Forgery)**:

- ✅ Backend sends CSRF tokens
- ✅ OpenAPI client includes tokens automatically
- ✅ SameSite cookies

**Injection Attacks**:

- ✅ Backend uses parameterized queries
- ✅ Input validation on both frontend and backend
- ✅ OpenAPI client prevents injection

**Prototype Pollution**:

- ❌ Never use `Object.assign` with user input as source
- ❌ Never use `_.merge` with untrusted data
- ✅ Validate object shapes before using

**ReDoS (Regular Expression Denial of Service)**:

- ✅ Review complex regex patterns
- ✅ Test regex with long inputs
- ❌ Avoid backtracking in regex

### Secure Coding Practices

**Don't Trust Client Data**:

- Validate on backend (primary defense)
- Frontend validation is UX, not security

**Principle of Least Privilege**:

- Only request permissions needed
- Check permissions before sensitive operations
- Hide UI for unavailable actions

**Sanitize Output**:

- Always sanitize HTML before rendering
- Escape special characters in user content
- Use Lit's automatic escaping

**Secure Defaults**:

- Forms should validate by default
- Sensitive operations require confirmation
- Errors don't expose sensitive information

**Defense in Depth**:

- Multiple layers of security
- Frontend validation + Backend validation
- Input sanitization + Output escaping
- Authentication + Authorization

### Security Anti-Patterns to Avoid

❌ **Never do this**:

```typescript
// XSS vulnerability
element.innerHTML = userInput;

// Code injection
eval(userCode);

// Exposing sensitive data
console.log('Token:', authToken);

// Storing secrets
localStorage.setItem('apiKey', key);

// Disabling validation
// @ts-ignore
// eslint-disable-next-line

// Trusting user input
const url = userInput; // Could be javascript:alert()
window.location.href = url;
```

✅ **Do this instead**:

```typescript
// Safe HTML rendering
element.textContent = userInput;
// or
render() {
	return html`<div>${userInput}</div>`;
}

// No eval needed
// Use proper JavaScript patterns

// Don't log sensitive data
console.log('Operation completed');

// Backend manages secrets
// Frontend receives tokens via httpOnly cookies

// Fix TypeScript/ESLint issues properly
// Don't suppress warnings

// Validate and sanitize URLs
const url = new URL(userInput, window.location.origin);
if (url.protocol === 'https:' || url.protocol === 'http:') {
	window.location.href = url.href;
}
```

---

### Src Umbraco.Web.UI.Client Docs State System (`src/Umbraco.Web.UI.Client/docs/state-system.md`)

# State System

Reactive state containers built on RxJS `BehaviorSubject`. The canonical way to hold and expose mutable data in contexts, controllers, and elements.

For the broader observable lifecycle (`this.observe(...)`, controller cleanup, `observeMultiple`), see [core-primitives.md](./core-primitives.md).

---


## State Types

| Class | Holds | Comparison |
|-------|-------|-----------|
| `UmbStringState` / `UmbNumberState` / `UmbBooleanState` | Primitives | `===` |
| `UmbObjectState` / `UmbArrayState` / `UmbDeepState` | Plain JSON data, deep-frozen | `JSON.stringify` deep compare |
| `UmbClassState` | Class instance with `.equal()` | `===` first, falling back to `oldValue.equal(newValue)` |
| `UmbBasicState` | Anything | `===` |

`UmbObjectState`/`UmbArrayState`/`UmbDeepState` deep-freeze their value — mutate by replacing, never in place. Use `UmbClassState` for class instances; `JSON.stringify` strips prototypes.

Comparison determines whether a new value differs from the current one — subscribers are only notified when it does, so observer callbacks don't need their own re-emit guards.

---

## Standard Pattern

Private state, public observable, immutable updates:

```typescript
#name = new UmbStringState<string>('');
#items = new UmbArrayState<MyItem>([], (item) => item.unique);
#data = new UmbObjectState<MyModel | undefined>(undefined);

readonly name = this.#name.asObservable();
readonly items = this.#items.asObservable();
readonly data = this.#data.asObservable();

this.#name.setValue('new name');
this.#data.update({ count: 1 }); // shallow merge
this.#items.appendOne(newItem);
```

---

## `asObservablePart`

Available on every state class. Project to a sub-value with its own deduplication layer:

```typescript
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly itemCount = this.#items.asObservablePart((items) => items.length);
```

Default memoization handles both objects (deep-equal via JSON) and primitives (`===`). Pass a custom memoization function as the second arg when the default is too coarse or too fine.

---

## Mute / Unmute (UmbDeepState only)

Batch multiple writes into a single emission:

```typescript
this.#data.mute();
this.#data.update({ name: 'a' });
this.#data.update({ count: 1 });
this.#data.unmute(); // single emission
```

Available on `UmbDeepState`, `UmbObjectState`, `UmbArrayState`. `getMutePromise()` resolves on the next `unmute`.

---

All state classes are imported from `@umbraco-cms/backoffice/observable-api`.

---

### Src Umbraco.Web.UI.Client Docs Style Guide (`src/Umbraco.Web.UI.Client/docs/style-guide.md`)

# Style Guide
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)

---

This guide covers naming and formatting conventions. For visual design choices — icons, colours, buttons, and UX copy — see [design-choices.md](./design-choices.md).

---


### Naming Conventions

**Files**:
- Web components: `my-component.element.ts`
- Tests: `my-component.test.ts`
- Stories: `my-component.stories.ts`
- Controllers: `my-component.controller.ts`
- Contexts: `my-component.context.ts`
- Modals: `my-component.modal.ts`
- Workspaces: `my-component.workspace.ts`
- Repositories: `my-component.repository.ts`
- Index files: `index.ts` (barrel exports)

**Classes & Types**:
- Classes: `PascalCase` with `Umb` prefix: `UmbMyComponent`
- Interfaces: `PascalCase` with `Umb` prefix: `UmbMyInterface`
- Types: `PascalCase` with `Umb`, `Ufm`, `Manifest`, `Meta`, or `Example` prefix
- Exported types MUST have approved prefix
- Example types for docs: `ExampleMyType`

**Variables & Functions**:
- Public members: `camelCase` without underscore: `myVariable`, `myMethod`
- Private members: `camelCase` with leading underscore: `_myPrivateVariable`
- #private members: `camelCase` without underscore: `#myPrivateField`
- Protected members: `camelCase` with optional underscore: `myProtected` or `_myProtected`
- Constants (exported): `UPPER_SNAKE_CASE` with `UMB_` prefix: `UMB_MY_CONSTANT`
- Local constants: `UPPER_CASE` or `camelCase`

**Custom Elements**:
- Element tag names: kebab-case with `umb-` prefix: `umb-my-component`
- Must be registered in global `HTMLElementTagNameMap`

### File Organization

- One class/component per file
- Use barrel exports (`index.ts`) for package public APIs
- Import order (enforced by ESLint):
  1. External dependencies
  2. Parent imports
  3. Sibling imports
  4. Index imports
  5. Type-only imports (separate)

### Code Formatting (Prettier)

```json
{
  "printWidth": 120,
  "singleQuote": true,
  "semi": true,
  "bracketSpacing": true,
  "bracketSameLine": true,
  "useTabs": true
}
```

- **Indentation**: Tabs (not spaces)
- **Line length**: 120 characters max
- **Quotes**: Single quotes
- **Semicolons**: Required
- **Trailing commas**: Yes (default)

### TypeScript Conventions

**Strict Mode** (enabled in `tsconfig.json`):
- `strict: true`
- `noImplicitReturns: true`
- `noFallthroughCasesInSwitch: true`
- `noImplicitOverride: true`

**Type Features**:
- Use TypeScript types over JSDoc when possible
- BUT: Lit components use JSDoc for web-component-analyzer compatibility
- Use `type` for unions/intersections: `type MyType = A | B`
- Use `interface` for object shapes and extension: `interface MyInterface extends Base`
- Prefer `const` over `let`, never use `var`
- Use `readonly` for immutable properties
- Use generics for reusable code
- Avoid `any` (lint warning), use `unknown` instead
- Use type guards for narrowing
- Use `as const` for literal types

**Module Syntax**:
- ES Modules only: `import`/`export`
- Use consistent type imports: `import type { MyType } from '...'`
- Use consistent type exports: `export type { MyType }`
- No side-effects in imports

**Decorators**:
- `@customElement('umb-my-element')` - Register custom element
- `@property({ type: String })` - Reactive properties
- `@state()` - Internal reactive state
- `@query('#myId')` - Query shadow DOM
- Experimental decorators enabled

### Modern TypeScript Features to Use

- **Async/await** over callbacks
- **Optional chaining**: `obj?.property?.method?.()`
- **Nullish coalescing**: `value ?? defaultValue`
- **Template literals**: `` `Hello ${name}` ``
- **Destructuring**: `const { a, b } = obj`
- **Spread operator**: `{ ...obj, newProp: value }`
- **Arrow functions**: `const fn = () => {}`
- **Array methods**: `map`, `filter`, `reduce`, `find`, `some`, `every`
- **Object methods**: `Object.keys`, `Object.values`, `Object.entries`
- **Private fields**: `#privateField`

### Event Handler Guidelines

**Event handlers must be arrow function properties** to prevent memory leaks:

```typescript
// ✅ GOOD: Arrow function property
export class UmbMyElement extends LitElement {
	#onStorageEvent = async (evt: StorageEvent) => {
		// Handler logic
	};

	constructor() {
		super();
		window.addEventListener('storage', this.#onStorageEvent);
	}

	disconnectedCallback() {
		window.removeEventListener('storage', this.#onStorageEvent);
	}
}

// ❌ BAD: Using .bind(this) creates new function references
export class UmbBadElement extends LitElement {
	constructor() {
		super();
		// Each .bind(this) creates a NEW reference!
		window.addEventListener('storage', this.#onStorageEvent.bind(this));
	}

	disconnectedCallback() {
		// This creates ANOTHER reference - doesn't remove the original!
		window.removeEventListener('storage', this.#onStorageEvent.bind(this));
	}

	#onStorageEvent(evt: StorageEvent) {
		// Handler logic
	}
}
```

**Placement in Class**:
- Place event handler arrow functions near the top of the class with other properties
- Place them after state properties but before constructor

**Naming**:
- Private handlers: `#onEventName` or `#handleEventName`
- Protected handlers: `_onEventName` or `_handleEventName`
- Use descriptive names: `#onStorageEvent`, `#handleDragEnter`, `#onActionExecuted`

### Language Features to Avoid

- `var` (use `const`/`let`)
- `eval()` or `Function()` constructor
- `with` statement
- `arguments` object (use rest parameters)
- Deeply nested callbacks (use async/await)
- Non-null assertions unless absolutely necessary: `value!`
- Type assertions unless necessary: `value as Type`
- `@ts-ignore` (use `@ts-expect-error` with comment)

### Custom ESLint Rules

**Project-Specific Rules**:
- `prefer-static-styles-last` - Static styles property must be last in class
- `enforce-umbraco-external-imports` - External dependencies must be imported via `@umbraco-cms/backoffice/external/*`
- Private members MUST have leading underscore
- Exported types MUST have approved prefix (Umb, Ufm, Manifest, Meta, Example)
- Exported string constants MUST have UMB_ prefix
- Semicolons required
- No `var` keyword
- No circular dependencies (max depth 6)
- No self-imports
- Consistent type imports/exports

**Allowed JSDoc Tags** (for web-component-analyzer):
- `@element` - Element name
- `@attr` - HTML attribute
- `@fires` - Custom events
- `@prop` - Properties
- `@slot` - Slots
- `@cssprop` - CSS custom properties
- `@csspart` - CSS parts

### Third-Party Library Names

Use the correct capitalization for third-party libraries:

| Library | Correct | Incorrect              |
|---------|---------|------------------------|
| Tiptap  | Tiptap  | TipTap, tipTap, TIPTAP |
| Lit     | Lit     | LIT, lit (in prose)    |
| RxJS    | RxJS    | rxjs (in prose), RXJS  |

### Documentation

- **Default to no comment.** Self-descriptive code (good names, small functions) is the primary documentation. See [Code Comment Policy](../../../CLAUDE.md#9-code-comment-policy) and [Clean Code → Comments and Documentation](./clean-code.md#comments-and-documentation) for the full rules and worked examples.
- **Public APIs**: JSDoc comments with `@description`, `@param`, `@returns`, `@example`
- **Web Components**: JSDoc with web-component-analyzer tags
- **Inline comments**: only for the non-obvious WHY (hidden constraints, ordering, workarounds, edge cases). Never restate what the code does.
- **TODOs**: Format as `// TODO: description [initials]`
- **Deprecated**: Use `@deprecated` tag with migration instructions

---

### Src Umbraco.Web.UI.Client Docs Testing (`src/Umbraco.Web.UI.Client/docs/testing.md`)

# Testing

---

## Testing Strategy

The project uses two layers of testing:

1. **Client-side tests** (this project) — Unit and integration tests running in-browser via `@web/test-runner`. API calls are intercepted by **MSW (Mock Service Worker)** request handlers, providing realistic mocked responses without a running backend.
2. **E2E acceptance tests** (`tests/Umbraco.Tests.AcceptanceTest/`) — Playwright tests running against a real CMS installation. These live outside the client project and cover full user journeys end-to-end.

### Testing Priority by Code Area

All tests matter, but some code changes carry more risk than others. The closer code is to the foundation, the wider the blast radius when it breaks — and the more critical it is to have test coverage.

| Priority | Area | Rationale |
|----------|------|-----------|
| **Critical** | `src/libs/` | Framework foundations used by everything. Changes here **must** have tests. |
| **High** | `src/packages/core/` | UI framework infrastructure. Classes (contexts, controllers, managers, repositories) should be covered by unit tests. Element tests are important but secondary. |
| **Standard** | `src/packages/*` (CMS packages) | Domain features. Tests are valuable and encouraged, but these areas also have coverage from the E2E acceptance tests. |

This is not a reason to skip tests for CMS packages — it's a guide for where to invest testing effort first when time is limited.

### Testing Philosophy

- **Unit tests** for business logic and utilities
- **Component tests** for web components
- **Integration tests** for workflows
- **E2E tests** for critical user journeys
- **Coverage target**: No strict requirement, focus on meaningful tests
- **Test pyramid**: Many unit tests, fewer integration tests, few E2E tests

### Testing Frameworks

**Unit/Component Testing**:
- `@web/test-runner` - Fast test runner for web components
- `@open-wc/testing` - Testing utilities, includes Chai assertions
- `@types/chai` - Assertion library
- `@types/mocha` - Test framework (used by @web/test-runner)
- `@web/test-runner-playwright` - Browser launcher
- `element-internals-polyfill` - Polyfill for form-associated custom elements

**E2E Testing**:
- `@playwright/test` - End-to-end testing in real browsers
- Playwright MSW integration for API mocking

**Test Utilities**:
- `@umbraco-cms/internal/test-utils` - Shared test utilities
- MSW (Mock Service Worker) - API mocking
- Fixtures in `src/mocks/data/` - Test data

### Test Project Organization

```
src/
├── **/*.test.ts              # Unit tests co-located with source
├── mocks/
│   ├── data/                 # Mock data & in-memory databases
│   └── handlers/             # MSW request handlers
├── examples/
│   └── **/*.test.ts          # Example tests
└── utils/
    └── test-utils.ts         # Shared test utilities

e2e/
├── **/*.spec.ts              # Playwright E2E tests
└── fixtures/                 # E2E test fixtures
```

### Test Naming Convention

```typescript
describe('UmbMyComponent', () => {
	describe('initialization', () => {
		it('should create element', async () => {
			// test
		});

		it('should set default properties', async () => {
			// test
		});
	});

	describe('user interactions', () => {
		it('should emit event when button clicked', async () => {
			// test
		});
	});
});
```

### Test Structure (AAA Pattern)

```typescript
it('should do something', async () => {
	// Arrange - Set up test data and conditions
	const element = await fixture<UmbMyElement>(html`<umb-my-element></umb-my-element>`);
	const spy = sinon.spy();
	element.addEventListener('change', spy);

	// Act - Perform the action
	element.value = 'new value';
	await element.updateComplete;

	// Assert - Verify the results
	expect(spy.calledOnce).to.be.true;
	expect(element.value).to.equal('new value');
});
```

**Assertion style**: for length/count checks, prefer Chai's `lengthOf` matcher over asserting on `.length` directly — it works on any value with a numeric `length` (arrays, strings, etc.) and gives a clearer failure message that reports the actual length:

```typescript
// ✅ Reports "expected [ '1', '2' ] to have a length of 3 but got 2"
expect(element.selection).to.have.lengthOf(3);

// ❌ Reports only "expected 2 to equal 3" — loses the collection context
expect(element.selection.length).to.equal(3);
```

### Unit Test Guidelines

**What to Test**:
- Public API methods and properties
- User interactions (clicks, inputs, etc.)
- State changes
- Event emissions
- Error handling
- Edge cases

**What NOT to Test**:
- Private implementation details
- Framework/library code
- Generated code (e.g., OpenAPI clients)
- Third-party dependencies

**Best Practices**:
- One assertion per test (when practical)
- Test behavior, not implementation
- Use meaningful test names (describe what should happen)
- Keep tests fast (<100ms each)
- Isolate tests (no shared state between tests)
- Use fixtures for DOM elements
- Use spies/stubs for external dependencies
- Clean up after tests (auto-handled by @web/test-runner)

**Web Component Testing**:

```typescript
import { fixture, html, expect } from '@open-wc/testing';
import { UmbMyElement } from './my-element.element';

describe('UmbMyElement', () => {
	let element: UmbMyElement;

	beforeEach(async () => {
		element = await fixture(html`<umb-my-element></umb-my-element>`);
	});

	it('should render', () => {
		expect(element).to.exist;
		expect(element.shadowRoot).to.exist;
	});

	it('should be accessible', async () => {
		await expect(element).to.be.accessible();
	});
});
```

### Integration Test Guidelines

Integration tests verify interactions between multiple components/systems:

```typescript
describe('UmbContentRepository', () => {
	let repository: UmbContentRepository;
	let mockContext: UmbMockContext;

	beforeEach(() => {
		mockContext = new UmbMockContext();
		repository = new UmbContentRepository(mockContext);
	});

	it('should fetch and cache content', async () => {
		const result = await repository.requestById('content-123');
		expect(result.data).to.exist;
		// Verify caching behavior
		const cached = await repository.requestById('content-123');
		expect(cached.data).to.equal(result.data);
	});
});
```

### E2E Test Guidelines

E2E tests with Playwright:

```typescript
import { test, expect } from '@playwright/test';

test.describe('Content Editor', () => {
	test('should create new document', async ({ page }) => {
		await page.goto('/');
		await page.click('[data-test="create-document"]');
		await page.fill('[data-test="document-name"]', 'My New Page');
		await page.click('[data-test="save"]');

		await expect(page.locator('[data-test="success-notification"]')).toBeVisible();
	});
});
```

### Mocking Best Practices

**MSW (Mock Service Worker)** for API mocking. MSW intercepts requests at the network level — tests hit the same code paths as production, with realistic responses. Handlers are defined in `src/mocks/handlers/` and loaded globally before tests run.

```typescript
const { http, HttpResponse } = window.MockServiceWorker;

export const handlers = [
	http.get('/umbraco/management/api/v1/document/:id', ({ params }) => {
		return HttpResponse.json({
			id: params.id,
			name: 'Test Document',
		});
	}),
];
```

**Context Mocking**:

```typescript
import { UmbMockContext } from '@umbraco-cms/internal/test-utils';

const mockContext = new UmbMockContext();
mockContext.provideContext(UMB_AUTH_CONTEXT, mockAuthContext);
```

### Running Tests

**Local Development**:

```bash
# Run all tests once
npm test

# Run in watch mode
npm run test:watch

# Run with dev config (faster, less strict)
npm run test:dev

# Run in watch mode with dev config
npm run test:dev-watch

# Run specific test file pattern
npm test -- --files "**/my-component.test.ts"
```

**E2E Tests**:

```bash
# Run E2E tests
npm run test:e2e

# Run in headed mode (see browser)
npx playwright test --headed

# Run specific test
npx playwright test e2e/content-editor.spec.ts

# Debug mode
npx playwright test --debug
```

**CI/CD**:
- All tests run on pull requests
- E2E tests run on Chromium in CI
- Retries: 2 attempts on CI, 0 locally
- Parallel: Sequential in CI, parallel locally

### Coverage

Coverage reporting is currently disabled (see `web-test-runner.config.mjs`):

```javascript
/* TODO: fix coverage report
coverageConfig: {
	reporters: ['lcovonly', 'text-summary'],
},
*/
```

**What to Exclude from Coverage**:
- Test files themselves
- Mock data and handlers
- Generated code (OpenAPI clients, icons)
- External wrapper modules
- Type declaration files

---

### Src Umbraco.Web.UI.Client Docs Value Summary (`src/Umbraco.Web.UI.Client/docs/value-summary.md`)

# Value Summary

**Module**: `@umbraco-cms/backoffice/value-summary`
**Location**: `src/packages/core/value-summary/`

Depends on: [Value Type](./value-type.md)

---

Defines the `valueSummary` extension type and provides base classes, a batching coordinator, and built-in summaries. Used to render compact, formatted value representations in collection views.

## Extension type: `valueSummary`

```typescript
interface ManifestValueSummary
  extends ManifestElementAndApi<UmbValueSummaryElement, UmbValueSummaryApi<unknown>> {
  type: 'valueSummary';
  forValueType: keyof UmbValueTypeMap;
  valueResolver?: UmbValueSummaryResolverLoaderProperty;
}
```

`forValueType` is the link between a value type key and its renderer. The extension system finds the manifest whose `forValueType` matches the requested type. **Consumers never reference a `valueSummary` manifest by alias.**

## Global component: `<umb-value-summary-extension>`

The only element consumers use. Takes `valueType` and `value`, renders the registered summary.

```html
<umb-value-summary-extension
  .valueType=${UMB_BOOLEAN_VALUE_TYPE}
  .value=${true}>
</umb-value-summary-extension>
```

Falls back to rendering the raw value as text when no summary is registered.

## Base classes

All implementations **must** extend these. Never implement the interfaces from scratch.
* **`UmbValueSummaryApiBase`**
* **`UmbValueSummaryElementBase<T>`**

## Simple summary (no API resolution)

Use when the raw value can be rendered directly — dates, booleans, strings, color codes, etc.

```typescript
@customElement('umb-boolean-value-summary')
export class UmbBooleanValueSummaryElement extends UmbValueSummaryElementBase<boolean> {
  override render() {
    return html`<umb-icon .name=${this._value ? 'icon-check' : 'icon-wrong'}></umb-icon>`;
  }
}
```

Manifest — always `kind: 'default'`, omit `valueResolver`:

```typescript
{
  type: 'valueSummary',
  kind: 'default',
  alias: 'Umb.ValueSummary.Boolean',
  name: 'Boolean Value Summary',
  forValueType: UMB_BOOLEAN_VALUE_TYPE,
  element: () => import('./boolean-value-summary.element.js'),
}
```

## Summary with resolver (batch API resolution)

Use when displaying the value requires a server call — e.g., resolving reference uniques to display names. The resolver receives all raw values from the entire column in **one batch call**.

### Resolver interface

```typescript
interface UmbValueSummaryResolver<TValue, TResolved> extends UmbApi {
  resolveValues(
    values: ReadonlyArray<TValue>,
  ): Promise<{
    data: ReadonlyArray<TResolved>; // positional — same order and length as input
    asObservable?: () => Observable<ReadonlyArray<TResolved>>;
  }>;
}
```

`data` must be the **same length** as `values` and **positionally aligned**. The resolver module must export the class as `valueResolver` (named export):

```typescript
export { UmbMyFeatureValueSummaryResolver as valueResolver } from './my-feature-value-summary.resolver.js';
```

Manifest — add `valueResolver`:

```typescript
{
  type: 'valueSummary',
  kind: 'default',
  alias: 'Umb.ValueSummary.UserGroup.References',
  name: 'User Group References Value Summary',
  forValueType: UMB_USER_GROUP_REFERENCES_VALUE_TYPE,
  element: () => import('./user-group-value-summary.element.js'),
  valueResolver: () => import('./user-group-value-summary.resolver.js'),
}
```

Provide `asObservable` only when the resolved data can change after initial load (e.g., linked entities that can be renamed).

## Coordinator (automatic — do not configure)

`UmbValueSummaryCoordinatorContext` is instantiated on the collection default context automatically. It batches all calls and passes the values to a resolver.

## Collection integration

There are two distinct ways `<umb-value-summary-extension>` is driven from collection views.

### Document collection (automatic — driven by server data)

The document collection table and card views receive an `editorAlias` from the server for each property value. They pass it directly as `valueType` to `<umb-value-summary-extension>`:

```typescript
// document-table-column-property-value.element.ts (simplified)
const { value, editorAlias } = this.#getPropertyValueByAlias();
return editorAlias
  ? html`<umb-value-summary-extension .valueType=${editorAlias} .value=${value}></umb-value-summary-extension>`
  : value;
```

Property editors do not configure collection manifests. Registering a `valueSummary` manifest with `forValueType` equal to the schema alias is sufficient — the document collection picks it up automatically.

### Generic table collection kind (manifest-driven)

`MetaCollectionViewTableKindColumn` has an optional `valueType` field set statically in the collection view manifest:

```typescript
interface MetaCollectionViewTableKindColumn {
  field: string;
  label: string;
  valueType?: keyof UmbValueTypeMap;
}
```

This is used for non-property-editor columns where the value type is known at manifest time — e.g. user state, last login date, user group references. When set, the table renders `<umb-value-summary-extension>` per cell. When omitted, the raw value is rendered as-is.

## Rules

1. Always use `kind: 'default'` on `valueSummary` manifests unless creating a new kind.
2. Always extend `UmbValueSummaryApiBase` and `UmbValueSummaryElementBase` — do not implement the interfaces from scratch.
3. Never reference a `valueSummary` manifest by alias — consumers look up by `forValueType`.
4. Resolver `data` array must be positionally aligned with the `values` input — same length, same order.
5. Use `asObservable` in resolvers only when resolved data can change (e.g., linked entities that can be renamed).
6. Export the resolver class as `valueResolver` (named export) — the loader checks for this name.

## File structure

```
my-feature/
└── value-summary/
    ├── manifests.ts
    ├── my-feature-value-summary.element.ts
    └── my-feature-value-summary.resolver.ts   # only if resolver is needed
```

The `value-type/` folder lives alongside `value-summary/` — see [Value Type](./value-type.md) for its structure.

## Reference implementations

| Pattern | Location |
|---|---|
| Simple (no resolver) | `src/packages/core/value-summary/value-types/boolean/` |
| Simple (date formatting) | `src/packages/core/value-summary/value-types/date-time/` |
| Resolver with `asObservable` | `src/packages/user/user-group/value-summary/` |

---

### Src Umbraco.Web.UI.Client Docs Value Type (`src/Umbraco.Web.UI.Client/docs/value-type.md`)

# Value Type

**Module**: `@umbraco-cms/backoffice/value-type`
**Location**: `src/packages/core/value-type/`

---

A compile-time type map that assigns a TypeScript type to each named value type. No runtime behaviour — purely a TypeScript contract.

## How it works

A global interface `UmbValueTypeMap` maps string keys to TypeScript types using **declaration merging** — the same pattern as `UmbExtensionConditionConfigMap`. Any package can extend it. There is no runtime registry.

```typescript
// base declaration in types.ts
declare global {
  interface UmbValueTypeMap {}
}
export type UmbValueType = keyof UmbValueTypeMap;
```

Each value type is a typed string constant that extends the map:

```typescript
export const UMB_BOOLEAN_VALUE_TYPE = 'Umb.ValueType.Boolean' as const;
declare global {
  interface UmbValueTypeMap {
    [UMB_BOOLEAN_VALUE_TYPE]: boolean;
  }
}
```

Any field typed `keyof UmbValueTypeMap` gets compile-time autocomplete and a type error if an undeclared key is used.

## Built-in types

| Constant | Key | TypeScript type |
|---|---|---|
| `UMB_STRING_VALUE_TYPE` | `Umb.ValueType.String` | `string` |
| `UMB_BOOLEAN_VALUE_TYPE` | `Umb.ValueType.Boolean` | `boolean` |
| `UMB_DATE_TIME_VALUE_TYPE` | `Umb.ValueType.DateTime` | `string` (ISO 8601) |

Import from `@umbraco-cms/backoffice/value-type`.

## Naming convention

| Category | Pattern | Example |
|---|---|---|
| System / primitive | `Umb.ValueType.{Type}` | `Umb.ValueType.DateTime` |
| Domain — reference shapes | `Umb.ValueType.{Entity}.{Shape}` | `Umb.ValueType.UserGroup.References` |
| Property editor | Property editor schema alias | `Umbraco.ColorPicker` |

Property editor value types use the schema alias as the key because document collection views receive an `editorAlias` field from the server for each property value and pass it directly as the `valueType` lookup key. The key must therefore match the schema alias exactly.

## Rules

- The key is always a string constant — always `as const`.
- The `declare global` block lives in the **same file** as the constant.
- **One value type per file.** File is named `constants.ts` inside a `value-type/` sub-folder.
- Export the constant from the package `index.ts` so other packages can import it.
- Property editor value types always use the schema alias — do not invent a different key.

---

### Src Umbraco.Web.UI.Client Docs Workspaces (`src/Umbraco.Web.UI.Client/docs/workspaces.md`)

# Workspaces

Workspaces are the primary editing surfaces in the backoffice. Each workspace holds a **context** (state + behavior), **views** (tabs), and **actions** (toolbar buttons) for a specific entity or feature. All workspace infrastructure lives in `src/packages/core/workspace/`; each entity implements its own workspace in its domain package.

Workspaces are registered and composed through the **extension system** — the same mechanism used for all backoffice UI. This means any workspace can be extended, overridden, or replaced by any package.

**Prerequisites**: [Manifests & Aliases](./manifests.md), [Entities](./entities.md), [Core Primitives](./core-primitives.md), [Data Flow](./data-flow.md)

---

## Extension Types for Workspaces

Workspace *views, actions, contexts, and footer apps* typically use **conditions** to scope to a specific workspace, while the `workspace` manifest itself is matched by `meta.entityType`.

| Extension Type | Purpose | Key Config |
|---|---|---|
| `workspace` | The workspace itself | `kind` (`default`/`routable`), `api` (context class), `meta.entityType` |
| `workspaceView` | Tabs/views within workspace | `element`/`js` (lazy), `meta.label`, `meta.pathname`, `meta.icon`, `weight`, `conditions` |
| `workspaceAction` | Toolbar actions (save, publish) | `kind`, `api` (action class), `meta.look`, `meta.color`, `meta.label`, `conditions` |
| `workspaceActionMenuItem` | Dropdown items on actions | `forWorkspaceActions` (target action alias), `conditions` |
| `workspaceContext` | Additional composable contexts | `api` (lazy), `conditions` |
| `workspaceFooterApp` | Footer components | `kind` (`menuBreadcrumb`, `variantMenuBreadcrumb`), `conditions` |

### How extensions are discovered

1. `umb-workspace` element finds the `workspace` extension matching the entity type
2. Once the workspace context (API) is created, `UmbExtensionsApiInitializer` loads all matching `workspaceContext` extensions
3. `umb-workspace-editor` discovers `workspaceView` extensions via `UmbExtensionsManifestInitializer`
4. `umb-workspace-footer` discovers `workspaceAction` extensions via `umb-extension-with-api-slot`

Key file: `src/packages/core/workspace/workspace.element.ts`

---

## Workspace Kinds

Two built-in kinds provide the workspace shell and context. Start with `default`; use `routable` when you need create/edit URL routing.

### `default` — The Foundation

The simplest workspace. Provides a headline, renders `<umb-workspace-editor>`, and supports workspace views, actions, workspace contexts, and footer apps — all with **zero custom code**. Just register a manifest.

```typescript
{
  type: 'workspace',
  kind: 'default',
  alias: 'Umb.Workspace.WebhookRoot',
  name: 'Webhook Root Workspace',
  meta: {
    entityType: UMB_WEBHOOK_ROOT_ENTITY_TYPE,
    headline: '#treeHeaders_webhooks',
  },
}
```

**What you get out of the box:**
- `UmbDefaultWorkspaceContext` — implements `UmbWorkspaceContext`, sets up `UmbEntityContext` and `UmbViewContext`
- `<umb-default-workspace>` element — renders `<umb-workspace-editor>` with the localized headline
- Full support for `workspaceView`, `workspaceAction`, `workspaceContext`, and `workspaceFooterApp` extensions scoped via conditions
- Entity context propagation (`entityType` + `unique`)

**No custom context class or element needed.** The kind provides both.

**`meta` config:**
- `entityType` (required) — the entity type constant
- `headline` (required) — workspace title, supports localization keys (e.g., `'#treeHeaders_webhooks'`)

**Common pattern:** Default workspaces often pair with a `workspaceView` of `kind: 'collection'` to list child entities. See `src/packages/webhook/webhook-root/workspace/manifests.ts` for a complete example.

**Kind implementation:** `src/packages/core/workspace/kinds/default/`

**Base interface** (`UmbWorkspaceContext`):
```typescript
interface UmbWorkspaceContext extends UmbApi, UmbContextMinimal {
  readonly workspaceAlias: string;
  getEntityType(): string;
}
```

This is the minimal contract all workspace contexts satisfy. The default kind implements it automatically.

### `routable` — Entity Detail Editing

Used when the workspace needs internal URL routing (create/edit flows). The workspace element renders an `umb-router-slot` that delegates to routes defined by the workspace context. **Requires a custom context class** that sets up routes.

```typescript
{
  type: 'workspace',
  kind: 'routable',
  alias: 'Umb.Workspace.Webhook',
  name: 'Webhook Workspace',
  api: () => import('./webhook-workspace.context.js'),
  meta: { entityType: UMB_WEBHOOK_ENTITY_TYPE },
}
```

**What differs from default:**
- You must provide `api` — a context class that implements `UmbRoutableWorkspaceContext` and calls `this.routes.setRoutes(...)`
- You must provide a custom editor element (rendered by the routes)
- The kind only provides the router shell (`<umb-router-slot>`), not the workspace editor UI

**Kind implementation:** `src/packages/core/workspace/kinds/routable/routable-workspace.kind.ts`

---

## Base Class Hierarchy (for Routable Workspaces)

When building a routable workspace, you typically extend one of these base classes. Each layer adds functionality:

```
UmbContextBase
  └── UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
       └── UmbEntityDetailWorkspaceContextBase<DetailModelType, DetailRepositoryType, CreateArgsType>
            └── UmbEntityNamedDetailWorkspaceContextBase<NamedDetailModelType, ...>
```

### UmbSubmittableWorkspaceContextBase

**File**: `src/packages/core/workspace/submittable/submittable-workspace-context-base.ts`

Foundation for any workspace that supports form submission:
- `workspaceAlias` — identifier matching the manifest alias
- `routes` — `UmbWorkspaceRouteManager` for internal routing
- `isNew` observable — tracks whether the entity is being created
- `requestSubmit()` → `validate()` → abstract `submit()` pipeline
- Multiple `UmbValidationContext` support for composable validation
- Keyboard shortcut: Ctrl+S triggers submit
- Modal integration (workspace can be opened in a modal dialog)

### UmbEntityDetailWorkspaceContextBase

**File**: `src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts`

Adds entity detail CRUD lifecycle on top of submittable:
- `UmbEntityWorkspaceDataManager` — dual-state data management (persisted vs. current) with change detection
- `load(unique)` — fetch entity from repository, observe for real-time updates
- `createScaffold(args)` — create new entity with optional preset data and parent
- `submit()` — routes to `_create()` or `_update()` based on `isNew`
- `delete(unique)` — delete entity via repository
- `validationContext` — base validation context (always present)
- `loading` / `forbidden` state managers
- Navigation-away guard (unsaved changes modal)
- Event dispatching for tree refresh after create/update

**Constructor args** (`UmbEntityDetailWorkspaceContextArgs`):
```typescript
{ workspaceAlias: string, entityType: string, detailRepositoryAlias: string }
```

### UmbEntityNamedDetailWorkspaceContextBase

**File**: `src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts`

Adds name management for entities with a `name` property:
- `name` observable — derived from current data
- `getName()` / `setName()` convenience methods
- `nameWriteGuard` — `UmbNameWriteGuardManager` for permission checking
- Auto-syncs view title from name changes

**This is the base class most entity detail workspaces extend.**

**Editor element pattern**: Use `<umb-workspace-header-name-editable slot="header">` inside `<umb-entity-detail-workspace-editor>`. This built-in element auto-consumes the workspace context for name binding, validation (`umbBindToValidation`), write guard permissions, auto-focus, and localization — no manual name wiring needed. See `src/packages/core/workspace/components/workspace-header-name-editable/`.

---

## Workspace Context Extensions (Modularity & Reuse)

**This is the most important pattern for maintaining package boundaries and enabling cross-workspace reuse.**

`workspaceContext` extensions let any package add capabilities to a workspace without modifying the workspace's own code. The owning package registers the extension; it activates only when the target workspace is open. This works for both `default` and `routable` workspaces.

Workspace contexts serve two purposes:
1. **Modularity** — Keep features in their own package, not in the workspace's package
2. **Reuse** — The same capability (publishing, menu structure, permissions) can be added to multiple workspaces via separate registrations or shared base classes

### Registration pattern

```typescript
// In: src/packages/documents/documents/publishing/workspace-context/manifests.ts
{
  type: 'workspaceContext',
  alias: 'Umb.WorkspaceContext.Document.Publishing',
  api: () => import('./document-publishing.workspace-context.js'),
  conditions: [
    { alias: UMB_WORKSPACE_CONDITION_ALIAS, match: UMB_DOCUMENT_WORKSPACE_ALIAS },
  ],
}
```

### Implementation pattern

The context class extends `UmbContextBase`, consumes the parent workspace context, and provides its own context token:

```typescript
export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase
  implements UmbPublishableWorkspaceContext {

  constructor(host: UmbControllerHost) {
    super(host, UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT);

    this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (workspaceContext) => {
      // Access workspace data and add publishing capabilities
    });
  }

  async saveAndPublish() { /* ... */ }
  async publish() { /* ... */ }
  async unpublish() { /* ... */ }
}
```

Other components consume this via its token:
```typescript
this.consumeContext(UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT, (ctx) => { /* ... */ });
```

### Deep example: Document Publishing

The best example of a full-stack workspace context extension. The publishing feature has its own repository, data source, workspace actions, and keyboard shortcuts — all layered on top of the document workspace via a `workspaceContext` registration.

**Key principles:** own repository & endpoints (separate from document detail), consumes parent workspace context for entity data, implements a core interface (`UmbPublishableWorkspaceContext` in `src/packages/core/workspace/contexts/tokens/`), and registers its own workspace actions that delegate to the publishing context.

**Reference:** `src/packages/documents/documents/publishing/`

### Reuse patterns

1. **Shared base classes** — A base class in core provides common logic; entity-specific subclasses just pass configuration (e.g., `UmbMenuVariantTreeStructureWorkspaceContextBase` used by both document and media menu structure contexts, each passing their own tree repository alias).

2. **Multiple registrations** — The same context class registered for different workspaces with different conditions (e.g., property value permissions registered for both document and block workspaces).

### Package boundary rules

- **The package that owns the feature registers the workspaceContext**, not the workspace's package
- Views and actions from other packages use conditions to target the workspace alias
- Never import internal files from another package — use public `index.ts` exports

### Kinds for workspace contexts

Workspace contexts can use kinds for shared patterns:

```typescript
{
  type: 'workspaceContext',
  kind: 'menuStructure',  // Provides pre-built breadcrumb/menu behavior
  alias: 'Umb.Context.Media.Menu.Structure',
  api: () => import('./media-menu-structure.context.js'),
  meta: { menuItemAlias: UMB_MEDIA_MENU_ITEM_ALIAS },
  conditions: [{ alias: UMB_WORKSPACE_CONDITION_ALIAS, match: UMB_MEDIA_WORKSPACE_ALIAS }],
}
```

---

## Save/Submit Flow

### Simple save (most entities)

1. User clicks **Save** button (or Ctrl+S)
2. `UmbSubmitWorkspaceAction.execute()` calls `workspaceContext.requestSubmit()`
3. `requestSubmit()` runs all registered `UmbValidationContext` validators
4. If valid → calls abstract `submit()`
5. `submit()` in `UmbEntityDetailWorkspaceContextBase`:
   - If `isNew` → `_create(data, parent)` → `repository.create(data, parentUnique)`
   - If existing → `_update(data)` → `repository.save(data)`
6. Updates persisted + current data from response
7. Dispatches events: `UmbRequestReloadChildrenOfEntityEvent` (create), `UmbRequestReloadStructureForEntityEvent`, `UmbEntityUpdatedEvent` (update)
8. If opened in modal → resolves modal value and closes

### Built-in action classes

| Class | File | Consumes | Calls |
|---|---|---|---|
| `UmbSubmitWorkspaceAction` | `src/packages/core/workspace/components/workspace-action/common/submit/submit.action.ts` | `UMB_SUBMITTABLE_WORKSPACE_CONTEXT` | `requestSubmit()` |
| `UmbSaveWorkspaceAction` | `src/packages/core/workspace/components/workspace-action/common/save/save.action.ts` | `UMB_SAVEABLE_WORKSPACE_CONTEXT` | `requestSave()` |

Most simple entities use `UmbSubmitWorkspaceAction` directly (no custom action class needed). Complex entities like documents define custom action classes for variant dialogs, permission checks, etc.

### Button state when the action opens a modal

If your action opens a confirmation/picker modal before doing real work, the legacy "set `waiting` on click" path produces a spinner during the modal and a success tick the moment the user cancels — both wrong. Workspace actions can opt in to a deferred-feedback contract instead.

The contract has three pieces:

| Piece | Where | Role |
|---|---|---|
| `UmbWorkspaceActionExecutionOptions` | `@umbraco-cms/backoffice/workspace` | Public type with `onActionStarting?: () => void` |
| `notifyWorkspaceActionStarting(options)` | `@umbraco-cms/backoffice/workspace` | Fires `options?.onActionStarting?.()` once, at the point real work begins |
| `UmbWorkspaceActionBase.setExecuting(value)` | `workspace-action-base.controller.ts` | Materialises an `isExecuting` observable on first call; the button element observes it |

**Action side.** Subclass `UmbWorkspaceActionBase` (or one of its specialisations like `UmbSaveWorkspaceAction`), opt in in the constructor, then forward the callback through the workspace-context method:

```ts
export class MyWorkspaceAction extends UmbWorkspaceActionBase {
    constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs) {
        super(host, args);
        // Opt in — exposes `isExecuting` so the button waits for real work.
        this.setExecuting(false);
    }

    override async execute() {
        try {
            await this._workspaceContext?.doTheirThing({
                onActionStarting: () => this.setExecuting(true),
            });
        } finally {
            this.setExecuting(false);
        }
    }
}
```

**Context side.** A workspace-context method that gates work behind a modal accepts the options and calls `notifyWorkspaceActionStarting` after the gate:

```ts
public async doTheirThing(options?: UmbWorkspaceActionExecutionOptions): Promise<void> {
    const result = await umbOpenModal(this, MY_CONFIRM_MODAL, { /* ... */ })
        .catch(() => undefined);
    if (!result) return; // user cancelled — silent return, no spinner, no tick

    notifyWorkspaceActionStarting(options);
    // real work below
    await this.#repository.doTheirThing(result);
}
```

What the button element does with this:

- `isExecuting` is undefined (action didn't opt in) → legacy behaviour: `waiting` on click, `success` or `failed` on resolve/reject.
- `isExecuting` exists but never flips to `true` → button stays idle. This is the cancel path.
- `isExecuting` flips to `true` → `waiting` shown. Action resolves → `success`. Action rejects → `failed`.

Pre-flight rejections (modal cancel, missing context, `throw new Error('...')` before `setExecuting(true)`) leave the button idle — the warning still reaches the console via `#runApiAction`'s catch, but the user isn't shown a red cross for something they can't act on.

**Overriding `_handleSave` / context methods in subclasses.** If you extend a base workspace context and override a method that accepts `UmbWorkspaceActionExecutionOptions`, forward the argument:

```ts
protected override async _handleSave(executionOptions?: UmbWorkspaceActionExecutionOptions) {
    // ... your override logic ...
    await super._handleSave(executionOptions); // forward — not super._handleSave()
}
```

TypeScript won't warn if you drop the parameter; the spinner just silently stops working.

Reference implementations:

- `UmbSaveWorkspaceAction` — `src/packages/core/workspace/components/workspace-action/common/save/save.action.ts`
- `UmbDocumentSaveAndPublishWorkspaceAction` — `src/packages/documents/documents/publishing/publish/workspace-action/save-and-publish.action.ts`
- `UmbContentDetailWorkspaceContextBase._handleSave` and `UmbDocumentPublishingWorkspaceContext.saveAndPublish` for the context-side handshake.

---

## Route Patterns

Routable workspaces define internal routes in the context constructor via `this.routes.setRoutes()`. Two patterns exist:

- **Simple** (flat entities) — `create` route with fixed parent, `edit/:unique` route for existing entities
- **Parent-aware** (tree entities) — `create/parent/:entityType/:parentUnique` route that extracts parent from URL params

Both patterns use **`UmbWorkspaceIsNewRedirectController`** in the create route — it observes the workspace's `isNew` state and replaces the URL from `/create` to `/edit/:unique` after the first save. The edit route must call `removeUmbControllerByAlias(UmbWorkspaceIsNewRedirectControllerAlias)` to clean up.

File: `src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts`

---

## Workspace Conditions

Built-in conditions for scoping extensions to specific workspaces:

| Condition Alias | Config | Purpose |
|---|---|---|
| `Umb.Condition.WorkspaceAlias` | `match: string` or `oneOf: string[]` | Match workspace by alias |
| `Umb.Condition.WorkspaceEntityType` | `match: string` or `oneOf: string[]` | Match entity type |
| `Umb.Condition.WorkspaceEntityIsNew` | — | True when entity is being created |
| `Umb.Condition.WorkspaceIsLoaded` | — | True when workspace data has loaded |

The most common is `Umb.Condition.WorkspaceAlias` (constant: `UMB_WORKSPACE_CONDITION_ALIAS`), used by virtually all workspace views and actions.

Files: `src/packages/core/workspace/conditions/`

---

## Context Token Pattern

Every routable workspace exposes a typed context token so views, actions, and workspace context extensions can consume it with type safety. The token uses `UmbContextToken` with two generics: the base interface (`UmbSubmittableWorkspaceContext`) and the concrete context class. The context string is always `'UmbWorkspaceContext'`, and a discriminator function checks `getEntityType()` to find the right workspace context in the DOM tree.

Re-export the token from `constants.ts` for the public API. Default workspaces don't need a custom context token — they use `UMB_WORKSPACE_CONTEXT` from core.

---

## Data Management

The `UmbEntityWorkspaceDataManager` (`src/packages/core/workspace/entity/entity-workspace-data-manager.ts`) provides dual-state data tracking for routable entity detail workspaces:

- **`persisted`** — last saved state from the server
- **`current`** — working copy with user edits

Key methods on workspace context (via `this._data`):
```typescript
// Create observables for specific properties
readonly myProp = this._data.createObservablePartOfCurrent((data) => data?.myProp);

// Read/write current data
this._data.getCurrent()?.myProp
this._data.updateCurrent({ myProp: newValue })

// Change detection
this._data.getHasUnpersistedChanges()  // JSON comparison
this._data.resetCurrent()              // Revert to persisted
```

---

## Reference Examples

| Kind | Complexity | Path | Notes |
|---|---|---|---|
| `default` | — | `src/packages/webhook/webhook-root/workspace/` | Root + collection view |
| `routable` | Simple | `src/packages/webhook/webhook/workspace/` | Single view, flat data |
| `routable` | Medium | `src/packages/data-type/workspace/` | Two views, invariant dataset |
| `routable` | Complex | `src/packages/documents/documents/workspace/` | Variants, publishing, permissions |

---

--- METRICS ---
- Files Extracted: 53
- Estimated Token Budget: ~58724 tokens
- Canonical Reference: https://codewiki.google/github.com/umbraco/Umbraco-CMS
