@public-ui/mcp 4.5.0-rc.3 → 4.5.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "metadata": {
3
- "generatedAt": "2026-09-29T11:09:38.269Z",
3
+ "generatedAt": "2026-10-06T13:52:06.167Z",
4
4
  "buildMode": "ci",
5
5
  "counts": {
6
6
  "total": 341,
@@ -10,7 +10,7 @@
10
10
  "totalScenarios": 20
11
11
  },
12
12
  "repo": {
13
- "commit": "d4fafc2566cee7454a30e98e000ea15d3e763515",
13
+ "commit": "c8c411be1b515cc1f67263845b66a987ce27b5d4",
14
14
  "branch": "develop",
15
15
  "repoUrl": "https://github.com/public-ui/kolibri"
16
16
  }
@@ -21,7 +21,7 @@
21
21
  "group": "docs",
22
22
  "name": "AGENTS",
23
23
  "path": "AGENTS.md",
24
- "code": "# Agent Instructions\n\nThis repository is a monorepo managed with **pnpm** and **Nx**. It contains multiple packages under `packages/` such as web components, themes, adapters, samples and tooling.\n\n## Handling hints\n\nWe have a monorepo structure with multiple packages, each with its own `package.json`. The root `package.json` contains shared dependencies and scripts. Use `pnpm` commands to manage dependencies and run scripts across packages.\n\n- To install dependencies, use `pnpm i` at the root level. This will install all dependencies for all packages.\n- If you change a dependency in a package:\n - Use only exact version numbers in `package.json`. Other peers will not be able to use the package if you can use a range version.\n - You need to run `pnpm i` at the root level. This updates the lockfile and ensures all packages are using the correct versions.\n- Never add a `packageManager` field to any `package.json` file.\n- Avoid that branch name may contain hidden characters.\n- Development workflow: `git pull` → `pnpm i` → `pnpm -r build` → `pnpm dev` only in the packages being changed → work on the code. No `dev` or `preview` script builds dependencies. Never run a build while a watcher is active, the components build clears the watcher's output. See [CONTRIBUTING.md](CONTRIBUTING.md#daily-workflow).\n- If something does not work, check in the event of an error whether all dependent submodules have been built.\n- To build a single package faster, run commands with downstream dependents using `pnpm --filter ...<package>` (e.g., `pnpm --filter ...@public-ui/sample-react build`). A package's build never builds another package: `pnpm -r build` builds each package exactly once in topological order. Build a package together with its dependencies using `pnpm --filter <package>... build` (e.g. components, which needs the kolicons font from `@public-ui/icons`).\n\n## 🚨 Format-first rule\n\n> **Stop before you commit:** run the formatter so CI never rejects your patch for style drift.\n\n1. Run `pnpm format` from the repo root whenever you change code, docs or configs.\n2. If you only touched one package, you may instead run `pnpm --filter <package> format` for a quicker pass.\n3. Re-stage the affected files (`git add -u`) so the formatted result is what lands in the commit.\n\nNo package scripts in this repo need extra flags such as `-- --write`; the scripts already know when to write changes versus just check.\n\n## Semantic Versioning\n\nThis repository follows **Semantic Versioning** (SemVer) for all packages. Each package version is defined in its own `package.json` file. The versioning scheme is as follows:\n\n- **Major version**: Incremented for incompatible API changes.\n- **Minor version**: Incremented for adding functionality in a backwards-compatible manner.\n- **Patch version**: Incremented for backwards-compatible bug fixes.\n\nIf we deprecate a feature, we will mark it as deprecated in the code and documentation, but we will not remove it immediately. Instead, we will provide a migration guide (migration\\*.md) for users to transition to the new feature. Also we provide a migration tool in the `packages/tools/kolibri-cli` package to help with the migration process. You have to add a migration task from the previous version to the new version in the `packages/tools/kolibri-cli/src/migrations` folder. In the migration package, are a lot of migration tasks already implemented, so you can use them as a reference.\n\n## Project Structure\n\n- `packages/components` – Stencil based web components\n - `packages/components/src/component` – components\n - `packages/components/src/schema` – schema definitions for all components\n- `packages/samples` – sample applications demonstrating usage\n - `packages/samples/angular` – Angular sample app; do not edit\n - `packages/samples/react` – React sample app; all samples; write component samples here\n- `packages/adapters/*` – generated framework integration packages; do not edit\n- `packages/themes` – style themes and assets\n - `packages/themes/default` – primary maintained standard theme\n - All other themes are not actively maintained\n- `packages/tools/kolibri-cli` – helper CLI for migration\n- Documentation lives in `docs/`.\n- Do always ignore and do not edit all `assets` folders in all packages, as these are generated by the build process and should not be edited manually. If you need to change something in the assets, you have to change it in the source code and rebuild the package.\n\n## Theming\n\nThe theming is realized with adopted style sheets on web components and will be adopted at the mounted hook of the components. All following styling rules are only relevant for the `components` and `themes` packages.\n\n### The 5 styling layers\n\n1. **A11y Preset layer**: This layer comes out of the `adopted-style-sheets` package and contains the basic styles for accessibility. It is applied to all components.\n2. **Basis Global layer**: This layer contains the basis global styles for all components and comes out of the `@public-ui/components` package. It is applied only component specific layout styles without margins and paddings. Generally, the styling works without colors, as the colors should only be set through the custom Theme Layer. It never contains a color scheme (dark/light).\n3. **Basis Component layer**: This layer contains the basis styles for one component and comes out of the `@public-ui/components` package. It is applied only component specific layout styles without margins and paddings. Generally, the styling works without colors, as the colors should only be set through the custom Theme Layer. It never contains a color scheme (dark/light).\n4. **Theme Global layer**: This layer contains the global styles for all components of a theme and comes out of a own theme package, like `@public-ui/theme-default`. Color schemes (dark/light) are defined here via theme tokens.\n5. **Theme Component layer**: This layer contains the component specific styles for one component of a theme and comes out of a own theme package, like `@public-ui/theme-default`.\n\n> **Base styling vs. theming — dark/light lives in the theme.** The base styling of the components package (layers 1–3) is responsible for layout and structure only. It knows no dark/light color scheme: no `prefers-color-scheme`, no `color-scheme`, no `light-dark()`, no scheme tokens. Black and white in the base layers are a contrast fallback, not a color design. Dark/light theming is anchored and implemented exclusively in the theme packages (layers 4–5). See [`docs/BASE_STYLING_VS_THEMING_CONCEPT.md`](docs/BASE_STYLING_VS_THEMING_CONCEPT.md).\n\n### Global accessibility styles\n\n```css\n/*\n * This file contains all rules for accessibility.\n */\n@layer kol-global {\n\t:host {\n\t\t/*\n\t\t * Minimum size of interactive elements.\n\t\t */\n\t\t--a11y-min-size: #{rem(44)};\n\t\t/*\n\t\t * No element should be used without a background and font color whose contrast ratio has\n\t\t * not been checked. By initially setting the background color to white and the font color\n\t\t * to black, the contrast ratio is ensured and explicit adjustment is forced.\n\t\t */\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t}\n\n\t* {\n\t\t/*\n\t\t * This rule enables the word dividing for all texts. That is important for high zoom levels.\n\t\t */\n\t\thyphens: auto;\n\t\t/*\n\t\t * Verdana is an accessible font that can be used without requiring additional loading time.\n\t\t */\n\t\tfont-family: Verdana;\n\t\t/*\n\t\t * Letter spacing is required for all texts.\n\t\t */\n\t\tletter-spacing: inherit;\n\t\t/*\n\t\t * This rule enables the word dividing for all texts. That is important for high zoom levels.\n\t\t */\n\t\tword-break: break-word;\n\t\t/*\n\t\t * Word spacing is required for all texts.\n\t\t */\n\t\tword-spacing: inherit;\n\t}\n\n\t/*\n\t * All interactive elements should have a minimum size of rem(44).\n\t */\n\t/* input:not([type='checkbox'], [type='radio'], [type='range']), */\n\t/* option, */\n\t/* select, */\n\t/* textarea, */\n\t[role='button'],\n\tbutton:not([role='link']),\n\t.kol-input .input {\n\t\tmin-height: var(--a11y-min-size);\n\t\tmin-width: var(--a11y-min-size);\n\t}\n\n\t/*\n\t * Some interactive elements should not inherit the font-family and font-size.\n\t */\n\ta,\n\tbutton,\n\th1,\n\th2,\n\th3,\n\th4,\n\th5,\n\th6,\n\tinput,\n\toption,\n\tselect,\n\ttextarea {\n\t\t/*\n\t\t * All elements should inherit the font family from his parent element.\n\t\t */\n\t\tfont-family: inherit;\n\t\t/*\n\t\t * All elements should inherit the font size from his parent element.\n\t\t */\n\t\tfont-size: inherit;\n\t}\n}\n\n/**\n * Sometimes we need the semantic element for accessibility reasons,\n * but we don't want to show it.\n *\n * - https://www.a11yproject.com/posts/how-to-hide-content/\n */\n.visually-hidden {\n\tclip: rect(0 0 0 0);\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n```\n\n### Styling rules\n\nThe basis global layer set the default font-size and box-sizing for all components.\n\n```css\n@layer kol-global {\n\t:host {\n\t\tfont-size: rem(16);\n\t\t/*\n\t\t * The max-width is needed to prevent the table from overflowing the\n\t\t * parent node, if the table is wider than the parent node.\n\t\t */\n\t\tmax-width: 100%;\n\t}\n\n\t* {\n\t\t/*\n\t\t * We prefer to box-sizing: border-box for all elements.\n\t\t */\n\t\tbox-sizing: border-box;\n\t}\n ...\n}\n```\n\n### Custom Theming rules\n\nThe custom theme layer is used to set the colors and other theme specific styles. The custom theme layer should not contain any layout styles, as these are already set in the basis global and component layers.\n\nDark/light color schemes are implemented in the theme layers only. Define all colors that differ between light and dark as tokens on `:host` in the theme global layer and switch the scheme there (`color-scheme` + `light-dark()`, `@media (prefers-color-scheme: dark)`, or an opt-in attribute). The theme component layer only references those tokens and never contains its own scheme media queries. Alternatively, a theme may ship a separate dark export that is registered as an additional theme and switched via the `kol-theme` attribute. The base styling of the components package must never be involved in this.\n\nFor example, generally the font-family is set in the theme global layer, on the `:host` element, so that all components inherit the font-family from the theme. The font-size is set in the basis global layer, so that all components inherit the font-size from the basis global layer. But it is possible to set a other base font-size in the theme global layer, if needed.\n\n```css\n@layer kol-theme-global {\n\t:host {\n\t\t--font-family: var(--kolibri-font-family, Verdana, Arial, Calibri, Helvetica, sans-serif);\n\t\t--font-size: var(--kolibri-font-size, #{rem(16)});\n ...\n\t}\n\n\t:host {\n\t\tfont-size: var(--font-size);\n\n\t\t* {\n\t\t\tfont-family: var(--font-family);\n\t\t}\n\t}\n ...\n}\n```\n\nIn the theme component layer, you can set what ever you need to realize your own custom style guidelines. For example, you can set the colors, borders, shadows, etc. for the component.\n\n```css\n@layer kol-theme-component {\n ...\n}\n```\n\n### Color schemes\n\nA theme carries one palette per color scheme in a single declaration, resolved by the CSS\n`light-dark()` function:\n\n```scss\n@layer kol-theme-global {\n\t:host {\n\t\t--color-text: var(--kolibri-color-text, light-dark(#202020, #{$dark-color-text}));\n\t}\n}\n```\n\nRules that hold for every theme:\n\n- **Never declare `color-scheme` in a theme**, and never write `@media (prefers-color-scheme: …)`.\n `color-scheme` is an inherited property and inheritance follows the flat tree, so it crosses the\n shadow boundary on its own: each component resolves `light-dark()` against whatever the consuming\n application has in effect where the component sits. A declaration on `:host` would replace that\n inherited value, and the page and the components in it could then disagree — which is exactly the\n bug this rule exists to prevent.\n- The application owns the scheme, in plain CSS: `:root { color-scheme: light dark }` to follow the\n operating system, `color-scheme: dark` on any element to force a subtree. Consequently an\n application that declares nothing stays light, whatever the operating system says. Dark mode is\n opt-in; `color-scheme.css` in a theme package ships that one line plus the page colors.\n- Sass does not evaluate variables inside `var()`. Interpolate them (`#{$dark-color-text}`).\n- An application stylesheet that uses `light-dark()` must not be downlevelled. A CSS minifier\n targeting older browsers rewrites it into a `prefers-color-scheme` media query with space\n toggles, and that replacement ignores the `color-scheme` property. A theme's CSS is a string\n adopted into the shadow roots at runtime and is never processed by the application's CSS\n pipeline, so the page would follow the operating system while the components follow\n `color-scheme`. Both sample host apps therefore pin `build.cssTarget` in their Vite config.\n- The base layer hardcodes `black` and `white` in a few places. Where it routes them through a\n token, override the token (`--kol-a11y-font-color` / `--kol-a11y-background-color` in `a11y.scss`\n are the sanctioned hook for the host box of every component); elsewhere restate the declaration in\n a theme layer, which sits above `kol-a11y`, `kol-global` and `kol-component`. Keep the light\n branch byte identical to the base value so no light snapshot moves.\n- What a theme layer cannot reach are the color stops of a `@keyframes` rule declared in the base\n layer: an animation's own values win over any normal declaration, whatever the layer. Such a case\n needs a change in `packages/components`.\n- Depth is a surface scale, not a shadow: a lighter surface sits higher. A light palette collapses\n that scale onto white and draws depth with a shadow instead; a dark one carries it in the surfaces\n and replaces the shadow with a hard edge. Since `light-dark()` is a color function and cannot\n switch a `box-shadow` geometry, state both branches at once and switch them by their colors — the\n shadow token transparent in dark mode, the edge token transparent in light mode. A container that\n paints a surface of its own passes it down through `--kol-surface`, which reaches slotted\n components too, so their host box matches the surface they were placed on.\n\n### CSS Custom Properties and SASS Variables\n\nCSS custom properties remain part of the global cascade and are not isolated by the Shadow DOM.\nOverusing them (in theme files as well as in component styles) can collide with variables defined\non a host page, and every custom property you declare is implicitly overridable from outside,\nwhether you intended that or not.\n\n**Prefer plain CSS over a custom property.** Before adding a custom property, check whether the\nplatform already gives the consumer a way to control it — the standard box model (`width`,\n`height`), inherited properties (`color`, `font-size` via `em`), or relative units (`%`, container\nquery units). A component that simply fills its host (`width: 100%; height: 100%` inside, a sane\ndefault `width`/`height` on `:host`) can be resized with plain CSS, exactly like a native element —\nno bespoke API, no documentation burden, nothing to keep in sync. Only reach for a custom property\nwhen the value genuinely cannot be derived this way (e.g. a semantic color token that isn't just\n`currentColor`, or a value with no corresponding native CSS mechanism).\n\n**Rule:** only declare a CSS custom property when it is a deliberate, documented customization\npoint (a `--kol-<component>-<name>` design token). For anything else — internal aliases,\nintermediate values, ratios, or derived defaults — use a SASS `$variable` instead. SASS variables\nare resolved at build time, can't leak into the global cascade, and can't be overridden from a host\npage, so they can't accidentally become part of the public API.\n\nDo not introduce a second, internal custom property just to make the code more readable:\n\n```scss\n// ❌ Bad: `--color-bar` is an unintended, additional public custom property.\n// It reacts to overrides from a host page exactly like `--kol-progress-color-bar`,\n// even though only the latter was meant to be part of the API.\n.kol-progress {\n\t--color-bar: var(--kol-progress-color-bar, #0075ff);\n\n\t&__bar-progress {\n\t\tfill: var(--color-bar);\n\t}\n}\n```\n\n```scss\n// ✅ Good: the SASS variable holds the default value; the only custom property\n// is `--kol-progress-color-bar`, referenced directly wherever needed.\n$kol-progress-default-color-bar: #0075ff;\n\n.kol-progress {\n\t&__bar-progress {\n\t\tfill: var(--kol-progress-color-bar, #{$kol-progress-default-color-bar});\n\t}\n}\n```\n\nAny further unit conversion or math that doesn't depend on a runtime custom property (e.g. `to-rem()`,\nratios, breakpoints) belongs in SASS, not in an extra layer of `calc()`/custom-property indirection.\n\n`kol-avatar` is an example of preferring plain CSS: instead of a `--kol-avatar-size` custom property,\n`:host` gets a default `width` and an `aspect-ratio: 1`, and the inner element derives its size from the\nhost via container query units (`cqw`/`cqh`) instead of a separate ratio calculation. Consumers resize it\nthe same way they'd resize any block-level element — with plain `width`/`height`.\n\nThe avatar must always stay square, even if `width` and `height` end up different (e.g. both are set\nexplicitly to different values), so the inner element uses `max(100cqw, 100cqh)` for both its own `width`\nand `height` — the larger of the two wins, and `:host` centers it (`align-items`/`justify-content`), so it\nmay visually extend beyond the host on the smaller axis. Since CSS can't distinguish \"our default\" from\n\"an explicit author value\" on two independent properties, only setting `width` reliably resizes the avatar\nbelow its 100px default; setting only `height` below 100px does not shrink it, because our own default\n`width` is still in effect and wins the `max()`. This is a deliberate, documented trade-off, not a bug —\nachieving full symmetry would require either a custom property or JavaScript, both of which cost more\nthan the trade-off is worth.\n\n### SCSS Architecture Guidelines: BEM with Smart Nesting\n\nWhen writing component theme styles, follow these principles for clean, maintainable SCSS:\n\n#### 1. **Flat BEM Structure (Root Level)**\n\nAll BEM modifiers and independent elements belong at **root level** of the mixin. Never nest BEM class names:\n\n```scss\n@mixin kol-alert-theme {\n\t// ✅ All BEM selectors on root (flat)\n\t.kol-alert { ... }\n\t.kol-alert--variant-msg { ... }\n\t.kol-alert--variant-card { ... }\n\t.kol-alert--type-default { ... }\n\t.kol-alert__container { ... }\n\t.kol-alert__heading { ... }\n}\n```\n\n**Why?** BEM classnames are independent selectors. Each class should be predictable and flat.\n\n#### 2. **Contextual Nesting (When Variants Change Element Behavior)**\n\nNest **only when a modifier changes how a child element behaves**. Keep all rules for an element together:\n\n```scss\n.kol-alert__closer {\n\tplace-self: center;\n\n\t.kol-button {\n\t\tborder-radius: 50%;\n\t\twidth: var(--a11y-min-size);\n\t\theight: var(--a11y-min-size);\n\t\tcursor: pointer;\n\n\t\t// ✅ Variant changes button styling - nest here\n\t\t.kol-alert--variant-msg & {\n\t\t\t--text-color: var(--alert-accent-color);\n\t\t}\n\n\t\t.kol-alert--variant-card & {\n\t\t\t--text-color: var(--color-light);\n\t\t}\n\t}\n}\n```\n\n**Rule of thumb**: All rules for `.kol-button` in `.kol-alert__closer` stay in one place. This keeps related styles together and makes maintenance easier.\n\n#### 3. **Never Use `$root` or `@at-root`**\n\n❌ **Outdated pattern (removed):**\n\n```scss\n.kol-alert {\n\t$root: &;\n\t&__closer {\n\t\t@at-root #{$root}--variant-msg & { ... }\n\t}\n}\n```\n\n✅ **Modern pattern (direct nesting):**\n\n```scss\n.kol-alert__closer {\n\t.kol-button {\n\t\t.kol-alert--variant-msg & { ... }\n\t}\n}\n```\n\nThis is clearer and doesn't require Sass variable gymnastics.\n\n#### 4. **Structure Template**\n\n```scss\n@mixin kol-component-theme {\n\t// 1. Base component (no children)\n\t.kol-component {\n\t\tdisplay: flex;\n\t\twidth: 100%;\n\t}\n\n\t// 2. All BEM modifiers at root (flat)\n\t.kol-component--variant-a {\n\t\t...\n\t}\n\n\t.kol-component--type-default {\n\t\t...\n\t}\n\n\t// 3. Elements with contextual nesting for variants\n\t.kol-component__heading {\n\t\tfont-weight: bold;\n\n\t\t.kol-component--variant-a & {\n\t\t\tcolor: red;\n\t\t}\n\t}\n\n\t.kol-component__content {\n\t\tpadding: 1rem;\n\n\t\t.kol-component--variant-a & {\n\t\t\tbackground: white;\n\t\t}\n\t}\n\n\t// 4. Child components with variant context\n\t.kol-component__button {\n\t\t.kol-button {\n\t\t\tcursor: pointer;\n\n\t\t\t.kol-component--variant-a & {\n\t\t\t\t--text-color: var(--accent);\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n### General rules for custom themes\n\n- Do not use `!important` in your styles, as this will override the styles of the basis global and component layers.\n- Do only overwrite styling definitions you will really customize. Do not set styling definitions that are already set (redundant) in the basis global and component layers, as this will override the styles of the basis global and component layers.\n- Do not `inherit` styles over the `:host` element, as this will override the styles of the basis global and component layers. This makes your component less robust from outside environment styles. Only the `kol-icon` inherits some specific styles, like `color`, `font-size`, `font-family` and `line-height`, as these are needed for the icon to be displayed correctly inline to this neighbored elements.\n- Do not set the default `font-family`, `font-size` or `box-sizing` in the basis or theme component layer (redundant), as these are already set in the basis global layers. If you need to set a different font-family or font-size, you can do this in the theme global layer.\n- Do not set `margin` or `padding` in the basis global and component layers. If you need to set a different margin or padding, you can do this in the theme global or component layers.\n- Do not use `@media (prefers-color-scheme: …)`, the `color-scheme` property or the `light-dark()` function in the basis global and component layers. Dark/light color schemes are a theme concern and belong exclusively in the theme global layer (see [`docs/BASE_STYLING_VS_THEMING_CONCEPT.md`](docs/BASE_STYLING_VS_THEMING_CONCEPT.md)). `forced-colors` (high contrast) is a separate accessibility mechanism and stays in the basis `kol-forced-colors` layer.\n- Do not use `overflow: hidden` in styling or theming, as it often causes issues for reuse and should be avoided.\n- **Do not use `@layer` declarations in utility files**: Helper files, mixin files, and partial files (starting with `_`) should not contain `@layer` declarations. These files are utilities and should be layer-agnostic. This is enforced by the custom Stylelint rule `kolibri/no-layer-in-utility-files`.\n- **Never use `$root` variables or `@at-root` in component mixins**: All selectors should be explicit. Use direct child/descendant nesting only when a modifier changes element behavior within a specific context.\n\n## Samples\n\nThe samples are located in `packages/samples/react` and demonstrate how to use the components in react. Each component has its own folder and the basic sample are in `basic.tsx`. Other stories can be added in the same folder. All samples of a component are registered in the `routes.ts` file.\n\n## Coding Conventions\n\n- Formatting is enforced via **Prettier** with settings defined in `prettier.config.js` (print width 160, single quotes, tabs).\n- `.editorconfig` sets `indent_style = tab` and `max_line_length = 160` for code files. Markdown and YAML files use spaces.\n- ESLint and Stylelint are run using `pnpm lint`. Lint rules should **not** be disabled via inline comments. Instead, describe the problem and work towards a clean solution.\n- Lists and enumerations in code should be kept in alphanumeric order. This also applies to import specifiers and union type literals.\n- Do not disable ESLint, Stylelint or TypeScript rules inline. Fix the code instead of turning such rules off.\n- ESLint and Stylelint are run using `pnpm lint`.\n- Lists and enumerations in code should be kept in alphabetical order (see `docs/tutorials/NEW_COMPONENT.md`).\n- Commit messages follow the **Conventional Commits** specification.\n- See also the [Contributing Guide](CONTRIBUTING.md) for more details on coding conventions and best practices.\n- Spell \"KoliBri\" with this casing in all documentation and code. The only exception is the component named KolKolibri.\n- Use ESM import syntax in browser code and scripts whenever supported, instead of `require` imports.\n- Do not create barrel files (e.g. `index.ts` that re-export modules). Import modules directly instead.\n- Do not place constant declarations before import statements; imports must always be at the very top of the file.\n- **Scripts must be platform-independent**: All scripts in the `scripts/` folder must work on Windows, macOS, and Linux without requiring external tools or platform-specific dependencies. Use Node.js built-in modules instead of external command-line tools like `rg`, `grep`, `find`, etc.\n- Inline code documentation follows [Inline code documentation](#inline-code-documentation).\n\n### Inline code documentation\n\nWe encourage inline code documentation (JSDoc in TypeScript/TSX, comments in SCSS and scripts), but only as much as necessary and as little as possible. This rule applies to every new or changed comment; when you touch code, bring the comments of the touched code in line with it.\n\n- **Document what the code cannot say itself**: the purpose of public API (`@Prop`, `@Method`, `@Event`, exported functions and types), constraints, a non-obvious reason (the \"why\") and known limitations.\n- **Do not repeat the code**: no comment that restates a name, a type or the next line. The TypeScript signature is the source of truth, so no `@param {string}` or `@returns {void}` type annotations.\n- **Describe the present and the future, never the past**: state what the code is and does and, where relevant, where it is heading (`@deprecated` with its replacement, a `TODO` with an issue link). Do not write history such as \"previously\", \"changed to\", \"fixed\", \"new:\", and do not refer to the diff, the pull request or a predecessor implementation. History belongs in the commit message, the pull request and the changelog. If an earlier state still matters, state the constraint that follows from it today.\n- **Write for human and AI readers alike**: clear, easy to follow and free of contradictions. One statement per fact, terms named exactly as in the code, no vague wording (\"maybe\", \"somehow\", \"for now\"). A comment never contradicts the code, another comment or the documentation. Update or delete a comment together with the code it describes, and link the authoritative document instead of copying it.\n- **Published API documentation stays stable**: the JSDoc of public `@Prop`, `@Method` and `@Event` members is published (`custom-elements.json`, `docs-vscode`, adapter IntelliSense). Refactorings and migrations keep it unchanged (see [Public API Contract](packages/components/src/components/_skeleton/ARC42.md#public-api-contract-migration-parity)); rewording it is a change of its own.\n\n```ts\n// ❌ Bad: repeats the signature and tells history.\n/**\n * Sets the label.\n * @param {string} value - the label\n * Previously this was handled by the controller; changed in the skeleton migration.\n */\nprivate setLabel(value: string): void {}\n\n// ✅ Good: states the constraint the code cannot express.\n/**\n * Runs before the first render, because the behaviors read the normalized label in `componentWillLoad`.\n */\nprivate setLabel(value: string): void {}\n```\n\n## Linting and Formatting\n\n- Run `pnpm lint` to check for linting errors across all packages. This script runs ESLint, Stylelint and TypeScript checks.\n - ⚠️ **Note**: TypeScript type checking in the lint script can require built artifacts. If you've made source code changes, lint will handle any necessary compilation. Explicit pre-build is typically unnecessary.\n - You can try to automatically fix linting issues with `pnpm lint:eslint --fix`, but this may not resolve all issues.\n- Run `pnpm format` to format all code files using Prettier. You can try to automatically fix linting issues with `pnpm format -w`, but this may not resolve all issues.\n- If your pull request only modifies Markdown files, skip `pnpm build`, `pnpm lint` and `pnpm test`. Just format the Markdown using `pnpm format` or Prettier.\n\n### Pre-commit checklist\n\n- **Always run `pnpm format` (or `pnpm --filter <package> format` for a single workspace) right before committing.** Formatting failures are one of the most common reasons for blocked quality gates, so make this the last step before `git commit` even for documentation-only changes.\n- **For SCSS changes**: Always run `pnpm lint:stylelint --fix` (or `pnpm --filter <package> lint:stylelint --fix`) to automatically correct formatting, property order, and selector structure rules before committing. This ensures compliance with BEM structure and style consistency.\n- After formatting, re-stage affected files with `git add -u` so the formatted content is what gets committed.\n\n## Testing\n\n- Run `pnpm test` from the repository root to execute all unit and integration tests.\n - ⚠️ **Note**: Test runners (Vitest, Jest, Playwright, etc.) execute an implicit build automatically before running tests. **Do NOT run a separate `pnpm build` beforehand** — this wastes time. The test scripts handle compilation and type checking internally.\n- Text snapshots of the unit tests can be updated with `pnpm test:update:unit` or via the `update-snapshots.yml` GitHub workflow. Visual screenshots are **not** committed: differences are approved on the review page of the pull request (see `docs/visual-review.md`); `pnpm snapshots:pull` fetches the current baseline for local runs.\n- Individual packages provide their own test scripts (e.g. `pnpm --filter @public-ui/components test:unit`).\n - These also perform implicit builds, so explicit pre-build is unnecessary.\n\n## Pull Request Guidelines\n\n- PR titles should be meaningful as they appear in the release notes.\n- Every PR must link to its issue and contain only changes related to that issue.\n- Ensure automated tests pass and manual testing is completed when required.\n- Update documentation or migration guides if your changes affect them.\n",
24
+ "code": "# Agent Instructions\n\nThis repository is a monorepo managed with **pnpm** and **Nx**. It contains multiple packages under `packages/` such as web components, themes, adapters, samples and tooling.\n\n## Handling hints\n\nWe have a monorepo structure with multiple packages, each with its own `package.json`. The root `package.json` contains shared dependencies and scripts. Use `pnpm` commands to manage dependencies and run scripts across packages.\n\n- To install dependencies, use `pnpm i` at the root level. This will install all dependencies for all packages.\n- If you change a dependency in a package:\n - Use only exact version numbers in `package.json`. Other peers will not be able to use the package if you can use a range version.\n - You need to run `pnpm i` at the root level. This updates the lockfile and ensures all packages are using the correct versions.\n- Never add a `packageManager` field to any `package.json` file.\n- Avoid that branch name may contain hidden characters.\n- Development workflow: `git pull` → `pnpm i` → `pnpm -r build` → `pnpm dev` only in the packages being changed → work on the code. No `dev` or `preview` script builds dependencies. Never run a build while a watcher is active, the components build clears the watcher's output. See [CONTRIBUTING.md](CONTRIBUTING.md#daily-workflow).\n- If something does not work, check in the event of an error whether all dependent submodules have been built.\n- To build a single package faster, run commands with downstream dependents using `pnpm --filter ...<package>` (e.g., `pnpm --filter ...@public-ui/sample-react build`). A package's build never builds another package: `pnpm -r build` builds each package exactly once in topological order. Build a package together with its dependencies using `pnpm --filter <package>... build` (e.g. components, which needs the kolicons font from `@public-ui/icons`).\n\n## 🚨 Format-first rule\n\n> **Stop before you commit:** run the formatter so CI never rejects your patch for style drift.\n\n1. Run `pnpm format` from the repo root whenever you change code, docs or configs.\n2. If you only touched one package, you may instead run `pnpm --filter <package> format` for a quicker pass.\n3. Re-stage the affected files (`git add -u`) so the formatted result is what lands in the commit.\n\nNo package scripts in this repo need extra flags such as `-- --write`; the scripts already know when to write changes versus just check.\n\n## Semantic Versioning\n\nThis repository follows **Semantic Versioning** (SemVer) for all packages. Each package version is defined in its own `package.json` file. The versioning scheme is as follows:\n\n- **Major version**: Incremented for incompatible API changes.\n- **Minor version**: Incremented for adding functionality in a backwards-compatible manner.\n- **Patch version**: Incremented for backwards-compatible bug fixes.\n\nThe public API of a component is its props, methods, events, slots and exported types, including their behaviour. Its internal DOM structure and the BEM classes on it are a **theming contract** instead: changing them is a **theming breaking change**, not a hard breaking change. It ships in at least a minor release, carries the label `release:theming` (changelog section _Theming_), lists the old and new classes in the pull request and adapts all themes of this repository in the same change. Theme authors adapt their themes through their own visual snapshots and the zero-visual-delta workflow. See [Theming Contract](packages/components/src/components/_skeleton/ARC42.md#theming-contract-shadow-dom-structure-and-bem-classes).\n\nIf we deprecate a feature, we will mark it as deprecated in the code and documentation, but we will not remove it immediately. Instead, we will provide a migration guide (migration\\*.md) for users to transition to the new feature. Also we provide a migration tool in the `packages/tools/kolibri-cli` package to help with the migration process. You have to add a migration task from the previous version to the new version in the `packages/tools/kolibri-cli/src/migrations` folder. In the migration package, are a lot of migration tasks already implemented, so you can use them as a reference.\n\n## Project Structure\n\n- `packages/components` – Stencil based web components\n - `packages/components/src/component` – components\n - `packages/components/src/schema` – schema definitions for all components\n- `packages/samples` – sample applications demonstrating usage\n - `packages/samples/angular` – Angular sample app; do not edit\n - `packages/samples/react` – React sample app; all samples; write component samples here\n- `packages/adapters/*` – generated framework integration packages; do not edit\n- `packages/themes` – style themes and assets\n - `packages/themes/default` – primary maintained standard theme\n - All other themes are not actively maintained\n- `packages/tools/kolibri-cli` – helper CLI for migration\n- Documentation lives in `docs/`.\n- `kol-tooltip-wc` (`packages/components/src/components/tooltip/component.tsx`) is the only `-wc` web component (`shadow: false`). It stays deprecated and is kept only for DESYBRI: do not remove, migrate or replace it, and do not use it in new code. Every other component that embeds another one renders that component's functional component through an item (see the skeleton ARC42, _Embedded Components (Items)_).\n- Do always ignore and do not edit all `assets` folders in all packages, as these are generated by the build process and should not be edited manually. If you need to change something in the assets, you have to change it in the source code and rebuild the package.\n\n## Theming\n\nThe theming is realized with adopted style sheets on web components and will be adopted at the mounted hook of the components. All following styling rules are only relevant for the `components` and `themes` packages.\n\n### The 5 styling layers\n\n1. **A11y Preset layer**: This layer comes out of the `adopted-style-sheets` package and contains the basic styles for accessibility. It is applied to all components.\n2. **Basis Global layer**: This layer contains the basis global styles for all components and comes out of the `@public-ui/components` package. It is applied only component specific layout styles without margins and paddings. Generally, the styling works without colors, as the colors should only be set through the custom Theme Layer. It never contains a color scheme (dark/light).\n3. **Basis Component layer**: This layer contains the basis styles for one component and comes out of the `@public-ui/components` package. It is applied only component specific layout styles without margins and paddings. Generally, the styling works without colors, as the colors should only be set through the custom Theme Layer. It never contains a color scheme (dark/light).\n4. **Theme Global layer**: This layer contains the global styles for all components of a theme and comes out of a own theme package, like `@public-ui/theme-default`. Color schemes (dark/light) are defined here via theme tokens.\n5. **Theme Component layer**: This layer contains the component specific styles for one component of a theme and comes out of a own theme package, like `@public-ui/theme-default`.\n\n> **Base styling vs. theming — dark/light lives in the theme.** The base styling of the components package (layers 1–3) is responsible for layout and structure only. It knows no dark/light color scheme: no `prefers-color-scheme`, no `color-scheme`, no `light-dark()`, no scheme tokens. Black and white in the base layers are a contrast fallback, not a color design. Dark/light theming is anchored and implemented exclusively in the theme packages (layers 4–5). See [`docs/BASE_STYLING_VS_THEMING_CONCEPT.md`](docs/BASE_STYLING_VS_THEMING_CONCEPT.md).\n\n### Global accessibility styles\n\n```css\n/*\n * This file contains all rules for accessibility.\n */\n@layer kol-global {\n\t:host {\n\t\t/*\n\t\t * Minimum size of interactive elements.\n\t\t */\n\t\t--a11y-min-size: #{rem(44)};\n\t\t/*\n\t\t * No element should be used without a background and font color whose contrast ratio has\n\t\t * not been checked. By initially setting the background color to white and the font color\n\t\t * to black, the contrast ratio is ensured and explicit adjustment is forced.\n\t\t */\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t}\n\n\t* {\n\t\t/*\n\t\t * This rule enables the word dividing for all texts. That is important for high zoom levels.\n\t\t */\n\t\thyphens: auto;\n\t\t/*\n\t\t * Verdana is an accessible font that can be used without requiring additional loading time.\n\t\t */\n\t\tfont-family: Verdana;\n\t\t/*\n\t\t * Letter spacing is required for all texts.\n\t\t */\n\t\tletter-spacing: inherit;\n\t\t/*\n\t\t * This rule enables the word dividing for all texts. That is important for high zoom levels.\n\t\t */\n\t\tword-break: break-word;\n\t\t/*\n\t\t * Word spacing is required for all texts.\n\t\t */\n\t\tword-spacing: inherit;\n\t}\n\n\t/*\n\t * All interactive elements should have a minimum size of rem(44).\n\t */\n\t/* input:not([type='checkbox'], [type='radio'], [type='range']), */\n\t/* option, */\n\t/* select, */\n\t/* textarea, */\n\t[role='button'],\n\tbutton:not([role='link']),\n\t.kol-input .input {\n\t\tmin-height: var(--a11y-min-size);\n\t\tmin-width: var(--a11y-min-size);\n\t}\n\n\t/*\n\t * Some interactive elements should not inherit the font-family and font-size.\n\t */\n\ta,\n\tbutton,\n\th1,\n\th2,\n\th3,\n\th4,\n\th5,\n\th6,\n\tinput,\n\toption,\n\tselect,\n\ttextarea {\n\t\t/*\n\t\t * All elements should inherit the font family from his parent element.\n\t\t */\n\t\tfont-family: inherit;\n\t\t/*\n\t\t * All elements should inherit the font size from his parent element.\n\t\t */\n\t\tfont-size: inherit;\n\t}\n}\n\n/**\n * Sometimes we need the semantic element for accessibility reasons,\n * but we don't want to show it.\n *\n * - https://www.a11yproject.com/posts/how-to-hide-content/\n */\n.visually-hidden {\n\tclip: rect(0 0 0 0);\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n```\n\n### Styling rules\n\nThe basis global layer set the default font-size and box-sizing for all components.\n\n```css\n@layer kol-global {\n\t:host {\n\t\tfont-size: rem(16);\n\t\t/*\n\t\t * The max-width is needed to prevent the table from overflowing the\n\t\t * parent node, if the table is wider than the parent node.\n\t\t */\n\t\tmax-width: 100%;\n\t}\n\n\t* {\n\t\t/*\n\t\t * We prefer to box-sizing: border-box for all elements.\n\t\t */\n\t\tbox-sizing: border-box;\n\t}\n ...\n}\n```\n\n### Custom Theming rules\n\nThe custom theme layer is used to set the colors and other theme specific styles. The custom theme layer should not contain any layout styles, as these are already set in the basis global and component layers.\n\nDark/light color schemes are implemented in the theme layers only. Define all colors that differ between light and dark as tokens on `:host` in the theme global layer and switch the scheme there (`color-scheme` + `light-dark()`, `@media (prefers-color-scheme: dark)`, or an opt-in attribute). The theme component layer only references those tokens and never contains its own scheme media queries. Alternatively, a theme may ship a separate dark export that is registered as an additional theme and switched via the `kol-theme` attribute. The base styling of the components package must never be involved in this.\n\nFor example, generally the font-family is set in the theme global layer, on the `:host` element, so that all components inherit the font-family from the theme. The font-size is set in the basis global layer, so that all components inherit the font-size from the basis global layer. But it is possible to set a other base font-size in the theme global layer, if needed.\n\n```css\n@layer kol-theme-global {\n\t:host {\n\t\t--font-family: var(--kolibri-font-family, Verdana, Arial, Calibri, Helvetica, sans-serif);\n\t\t--font-size: var(--kolibri-font-size, #{rem(16)});\n ...\n\t}\n\n\t:host {\n\t\tfont-size: var(--font-size);\n\n\t\t* {\n\t\t\tfont-family: var(--font-family);\n\t\t}\n\t}\n ...\n}\n```\n\nIn the theme component layer, you can set what ever you need to realize your own custom style guidelines. For example, you can set the colors, borders, shadows, etc. for the component.\n\n```css\n@layer kol-theme-component {\n ...\n}\n```\n\n### Color schemes\n\nA theme carries one palette per color scheme in a single declaration, resolved by the CSS\n`light-dark()` function:\n\n```scss\n@layer kol-theme-global {\n\t:host {\n\t\t--color-text: var(--kolibri-color-text, light-dark(#202020, #{$dark-color-text}));\n\t}\n}\n```\n\nRules that hold for every theme:\n\n- **Never declare `color-scheme` in a theme**, and never write `@media (prefers-color-scheme: …)`.\n `color-scheme` is an inherited property and inheritance follows the flat tree, so it crosses the\n shadow boundary on its own: each component resolves `light-dark()` against whatever the consuming\n application has in effect where the component sits. A declaration on `:host` would replace that\n inherited value, and the page and the components in it could then disagree — which is exactly the\n bug this rule exists to prevent.\n- The application owns the scheme, in plain CSS: `:root { color-scheme: light dark }` to follow the\n operating system, `color-scheme: dark` on any element to force a subtree. Consequently an\n application that declares nothing stays light, whatever the operating system says. Dark mode is\n opt-in; `color-scheme.css` in a theme package ships that one line plus the page colors.\n- Sass does not evaluate variables inside `var()`. Interpolate them (`#{$dark-color-text}`).\n- An application stylesheet that uses `light-dark()` must not be downlevelled. A CSS minifier\n targeting older browsers rewrites it into a `prefers-color-scheme` media query with space\n toggles, and that replacement ignores the `color-scheme` property. A theme's CSS is a string\n adopted into the shadow roots at runtime and is never processed by the application's CSS\n pipeline, so the page would follow the operating system while the components follow\n `color-scheme`. Both sample host apps therefore pin `build.cssTarget` in their Vite config.\n- The base layer hardcodes `black` and `white` in a few places. Where it routes them through a\n token, override the token (`--kol-a11y-font-color` / `--kol-a11y-background-color` in `a11y.scss`\n are the sanctioned hook for the host box of every component); elsewhere restate the declaration in\n a theme layer, which sits above `kol-a11y`, `kol-global` and `kol-component`. Keep the light\n branch byte identical to the base value so no light snapshot moves.\n- What a theme layer cannot reach are the color stops of a `@keyframes` rule declared in the base\n layer: an animation's own values win over any normal declaration, whatever the layer. Such a case\n needs a change in `packages/components`.\n- Depth is a surface scale, not a shadow: a lighter surface sits higher. A light palette collapses\n that scale onto white and draws depth with a shadow instead; a dark one carries it in the surfaces\n and replaces the shadow with a hard edge. Since `light-dark()` is a color function and cannot\n switch a `box-shadow` geometry, state both branches at once and switch them by their colors — the\n shadow token transparent in dark mode, the edge token transparent in light mode. A container that\n paints a surface of its own passes it down through `--kol-surface`, which reaches slotted\n components too, so their host box matches the surface they were placed on.\n\n### CSS Custom Properties and SASS Variables\n\nCSS custom properties remain part of the global cascade and are not isolated by the Shadow DOM.\nOverusing them (in theme files as well as in component styles) can collide with variables defined\non a host page, and every custom property you declare is implicitly overridable from outside,\nwhether you intended that or not.\n\n**Prefer plain CSS over a custom property.** Before adding a custom property, check whether the\nplatform already gives the consumer a way to control it — the standard box model (`width`,\n`height`), inherited properties (`color`, `font-size` via `em`), or relative units (`%`, container\nquery units). A component that simply fills its host (`width: 100%; height: 100%` inside, a sane\ndefault `width`/`height` on `:host`) can be resized with plain CSS, exactly like a native element —\nno bespoke API, no documentation burden, nothing to keep in sync. Only reach for a custom property\nwhen the value genuinely cannot be derived this way (e.g. a semantic color token that isn't just\n`currentColor`, or a value with no corresponding native CSS mechanism).\n\n**Rule:** only declare a CSS custom property when it is a deliberate, documented customization\npoint (a `--kol-<component>-<name>` design token). For anything else — internal aliases,\nintermediate values, ratios, or derived defaults — use a SASS `$variable` instead. SASS variables\nare resolved at build time, can't leak into the global cascade, and can't be overridden from a host\npage, so they can't accidentally become part of the public API.\n\nDo not introduce a second, internal custom property just to make the code more readable:\n\n```scss\n// ❌ Bad: `--color-bar` is an unintended, additional public custom property.\n// It reacts to overrides from a host page exactly like `--kol-progress-color-bar`,\n// even though only the latter was meant to be part of the API.\n.kol-progress {\n\t--color-bar: var(--kol-progress-color-bar, #0075ff);\n\n\t&__bar-progress {\n\t\tfill: var(--color-bar);\n\t}\n}\n```\n\n```scss\n// ✅ Good: the SASS variable holds the default value; the only custom property\n// is `--kol-progress-color-bar`, referenced directly wherever needed.\n$kol-progress-default-color-bar: #0075ff;\n\n.kol-progress {\n\t&__bar-progress {\n\t\tfill: var(--kol-progress-color-bar, #{$kol-progress-default-color-bar});\n\t}\n}\n```\n\nAny further unit conversion or math that doesn't depend on a runtime custom property (e.g. `to-rem()`,\nratios, breakpoints) belongs in SASS, not in an extra layer of `calc()`/custom-property indirection.\n\n`kol-avatar` is an example of preferring plain CSS: instead of a `--kol-avatar-size` custom property,\n`:host` gets a default `width` and an `aspect-ratio: 1`, and the inner element derives its size from the\nhost via container query units (`cqw`/`cqh`) instead of a separate ratio calculation. Consumers resize it\nthe same way they'd resize any block-level element — with plain `width`/`height`.\n\nThe avatar must always stay square, even if `width` and `height` end up different (e.g. both are set\nexplicitly to different values), so the inner element uses `max(100cqw, 100cqh)` for both its own `width`\nand `height` — the larger of the two wins, and `:host` centers it (`align-items`/`justify-content`), so it\nmay visually extend beyond the host on the smaller axis. Since CSS can't distinguish \"our default\" from\n\"an explicit author value\" on two independent properties, only setting `width` reliably resizes the avatar\nbelow its 100px default; setting only `height` below 100px does not shrink it, because our own default\n`width` is still in effect and wins the `max()`. This is a deliberate, documented trade-off, not a bug —\nachieving full symmetry would require either a custom property or JavaScript, both of which cost more\nthan the trade-off is worth.\n\n### SCSS Architecture Guidelines: BEM with Smart Nesting\n\nWhen writing component theme styles, follow these principles for clean, maintainable SCSS:\n\n#### 1. **Flat BEM Structure (Root Level)**\n\nAll BEM modifiers and independent elements belong at **root level** of the mixin. Never nest BEM class names:\n\n```scss\n@mixin kol-alert-theme {\n\t// ✅ All BEM selectors on root (flat)\n\t.kol-alert { ... }\n\t.kol-alert--variant-msg { ... }\n\t.kol-alert--variant-card { ... }\n\t.kol-alert--type-default { ... }\n\t.kol-alert__container { ... }\n\t.kol-alert__heading { ... }\n}\n```\n\n**Why?** BEM classnames are independent selectors. Each class should be predictable and flat.\n\n#### 2. **Contextual Nesting (When Variants Change Element Behavior)**\n\nNest **only when a modifier changes how a child element behaves**. Keep all rules for an element together:\n\n```scss\n.kol-alert__closer {\n\tplace-self: center;\n\n\t.kol-button {\n\t\tborder-radius: 50%;\n\t\twidth: var(--a11y-min-size);\n\t\theight: var(--a11y-min-size);\n\t\tcursor: pointer;\n\n\t\t// ✅ Variant changes button styling - nest here\n\t\t.kol-alert--variant-msg & {\n\t\t\t--text-color: var(--alert-accent-color);\n\t\t}\n\n\t\t.kol-alert--variant-card & {\n\t\t\t--text-color: var(--color-light);\n\t\t}\n\t}\n}\n```\n\n**Rule of thumb**: All rules for `.kol-button` in `.kol-alert__closer` stay in one place. This keeps related styles together and makes maintenance easier.\n\n#### 3. **Never Use `$root` or `@at-root`**\n\n❌ **Outdated pattern (removed):**\n\n```scss\n.kol-alert {\n\t$root: &;\n\t&__closer {\n\t\t@at-root #{$root}--variant-msg & { ... }\n\t}\n}\n```\n\n✅ **Modern pattern (direct nesting):**\n\n```scss\n.kol-alert__closer {\n\t.kol-button {\n\t\t.kol-alert--variant-msg & { ... }\n\t}\n}\n```\n\nThis is clearer and doesn't require Sass variable gymnastics.\n\n#### 4. **Structure Template**\n\n```scss\n@mixin kol-component-theme {\n\t// 1. Base component (no children)\n\t.kol-component {\n\t\tdisplay: flex;\n\t\twidth: 100%;\n\t}\n\n\t// 2. All BEM modifiers at root (flat)\n\t.kol-component--variant-a {\n\t\t...\n\t}\n\n\t.kol-component--type-default {\n\t\t...\n\t}\n\n\t// 3. Elements with contextual nesting for variants\n\t.kol-component__heading {\n\t\tfont-weight: bold;\n\n\t\t.kol-component--variant-a & {\n\t\t\tcolor: red;\n\t\t}\n\t}\n\n\t.kol-component__content {\n\t\tpadding: 1rem;\n\n\t\t.kol-component--variant-a & {\n\t\t\tbackground: white;\n\t\t}\n\t}\n\n\t// 4. Child components with variant context\n\t.kol-component__button {\n\t\t.kol-button {\n\t\t\tcursor: pointer;\n\n\t\t\t.kol-component--variant-a & {\n\t\t\t\t--text-color: var(--accent);\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n### General rules for custom themes\n\n- Do not use `!important` in your styles, as this will override the styles of the basis global and component layers.\n- Do only overwrite styling definitions you will really customize. Do not set styling definitions that are already set (redundant) in the basis global and component layers, as this will override the styles of the basis global and component layers.\n- Do not `inherit` styles over the `:host` element, as this will override the styles of the basis global and component layers. This makes your component less robust from outside environment styles. Only the `kol-icon` inherits some specific styles, like `color`, `font-size`, `font-family` and `line-height`, as these are needed for the icon to be displayed correctly inline to this neighbored elements.\n- Do not set the default `font-family`, `font-size` or `box-sizing` in the basis or theme component layer (redundant), as these are already set in the basis global layers. If you need to set a different font-family or font-size, you can do this in the theme global layer.\n- Do not set `margin` or `padding` in the basis global and component layers. If you need to set a different margin or padding, you can do this in the theme global or component layers.\n- Do not use `@media (prefers-color-scheme: …)`, the `color-scheme` property or the `light-dark()` function in the basis global and component layers. Dark/light color schemes are a theme concern and belong exclusively in the theme global layer (see [`docs/BASE_STYLING_VS_THEMING_CONCEPT.md`](docs/BASE_STYLING_VS_THEMING_CONCEPT.md)). `forced-colors` (high contrast) is a separate accessibility mechanism and stays in the basis `kol-forced-colors` layer.\n- Do not use `overflow: hidden` in styling or theming, as it often causes issues for reuse and should be avoided.\n- **Do not use `@layer` declarations in utility files**: Helper files, mixin files, and partial files (starting with `_`) should not contain `@layer` declarations. These files are utilities and should be layer-agnostic. This is enforced by the custom Stylelint rule `kolibri/no-layer-in-utility-files`.\n- **Never use `$root` variables or `@at-root` in component mixins**: All selectors should be explicit. Use direct child/descendant nesting only when a modifier changes element behavior within a specific context.\n\n## Samples\n\nThe samples are located in `packages/samples/react` and demonstrate how to use the components in react. Each component has its own folder and the basic sample are in `basic.tsx`. Other stories can be added in the same folder. All samples of a component are registered in the `routes.ts` file.\n\n## Coding Conventions\n\n- Formatting is enforced via **Prettier** with settings defined in `prettier.config.js` (print width 160, single quotes, tabs).\n- `.editorconfig` sets `indent_style = tab` and `max_line_length = 160` for code files. Markdown and YAML files use spaces.\n- ESLint and Stylelint are run using `pnpm lint`. Lint rules should **not** be disabled via inline comments. Instead, describe the problem and work towards a clean solution.\n- Lists and enumerations in code should be kept in alphanumeric order. This also applies to import specifiers and union type literals.\n- Do not disable ESLint, Stylelint or TypeScript rules inline. Fix the code instead of turning such rules off.\n- ESLint and Stylelint are run using `pnpm lint`.\n- Lists and enumerations in code should be kept in alphabetical order (see `docs/tutorials/NEW_COMPONENT.md`).\n- Commit messages follow the **Conventional Commits** specification.\n- See also the [Contributing Guide](CONTRIBUTING.md) for more details on coding conventions and best practices.\n- Spell \"KoliBri\" with this casing in all documentation and code. The only exception is the component named KolKolibri.\n- Use ESM import syntax in browser code and scripts whenever supported, instead of `require` imports.\n- Do not create barrel files (e.g. `index.ts` that re-export modules). Import modules directly instead.\n- Do not place constant declarations before import statements; imports must always be at the very top of the file.\n- **Scripts must be platform-independent**: All scripts in the `scripts/` folder must work on Windows, macOS, and Linux without requiring external tools or platform-specific dependencies. Use Node.js built-in modules instead of external command-line tools like `rg`, `grep`, `find`, etc.\n- Inline code documentation follows [Inline code documentation](#inline-code-documentation).\n\n### Inline code documentation\n\nWe encourage inline code documentation (JSDoc in TypeScript/TSX, comments in SCSS and scripts), but only as much as necessary and as little as possible. This rule applies to every new or changed comment; when you touch code, bring the comments of the touched code in line with it.\n\n- **Document what the code cannot say itself**: the purpose of public API (`@Prop`, `@Method`, `@Event`, exported functions and types), constraints, a non-obvious reason (the \"why\") and known limitations.\n- **Do not repeat the code**: no comment that restates a name, a type or the next line. The TypeScript signature is the source of truth, so no `@param {string}` or `@returns {void}` type annotations.\n- **Describe the present and the future, never the past**: state what the code is and does and, where relevant, where it is heading (`@deprecated` with its replacement, a `TODO` with an issue link). Do not write history such as \"previously\", \"changed to\", \"fixed\", \"new:\", and do not refer to the diff, the pull request or a predecessor implementation. History belongs in the commit message, the pull request and the changelog. If an earlier state still matters, state the constraint that follows from it today.\n- **Write for human and AI readers alike**: clear, easy to follow and free of contradictions. One statement per fact, terms named exactly as in the code, no vague wording (\"maybe\", \"somehow\", \"for now\"). A comment never contradicts the code, another comment or the documentation. Update or delete a comment together with the code it describes, and link the authoritative document instead of copying it.\n- **Published API documentation stays stable**: the JSDoc of public `@Prop`, `@Method` and `@Event` members is published (`custom-elements.json`, `docs-vscode`, adapter IntelliSense). Refactorings and migrations keep it unchanged (see [Public API Contract](packages/components/src/components/_skeleton/ARC42.md#public-api-contract-migration-parity)); rewording it is a change of its own.\n\n```ts\n// ❌ Bad: repeats the signature and tells history.\n/**\n * Sets the label.\n * @param {string} value - the label\n * Previously this was handled by the controller; changed in the skeleton migration.\n */\nprivate setLabel(value: string): void {}\n\n// ✅ Good: states the constraint the code cannot express.\n/**\n * Runs before the first render, because the behaviors read the normalized label in `componentWillLoad`.\n */\nprivate setLabel(value: string): void {}\n```\n\n## Linting and Formatting\n\n- Run `pnpm lint` to check for linting errors across all packages. This script runs ESLint, Stylelint and TypeScript checks.\n - ⚠️ **Note**: TypeScript type checking in the lint script can require built artifacts. If you've made source code changes, lint will handle any necessary compilation. Explicit pre-build is typically unnecessary.\n - You can try to automatically fix linting issues with `pnpm lint:eslint --fix`, but this may not resolve all issues.\n- Run `pnpm format` to format all code files using Prettier. You can try to automatically fix linting issues with `pnpm format -w`, but this may not resolve all issues.\n- If your pull request only modifies Markdown files, skip `pnpm build`, `pnpm lint` and `pnpm test`. Just format the Markdown using `pnpm format` or Prettier.\n\n### Pre-commit checklist\n\n- **Always run `pnpm format` (or `pnpm --filter <package> format` for a single workspace) right before committing.** Formatting failures are one of the most common reasons for blocked quality gates, so make this the last step before `git commit` even for documentation-only changes.\n- **For SCSS changes**: Always run `pnpm lint:stylelint --fix` (or `pnpm --filter <package> lint:stylelint --fix`) to automatically correct formatting, property order, and selector structure rules before committing. This ensures compliance with BEM structure and style consistency.\n- After formatting, re-stage affected files with `git add -u` so the formatted content is what gets committed.\n\n## Testing\n\n- Run `pnpm test` from the repository root to execute all unit and integration tests.\n - ⚠️ **Note**: Test runners (Vitest, Jest, Playwright, etc.) execute an implicit build automatically before running tests. **Do NOT run a separate `pnpm build` beforehand** — this wastes time. The test scripts handle compilation and type checking internally.\n- Text snapshots of the unit tests can be updated with `pnpm test:update:unit` or via the `update-snapshots.yml` GitHub workflow. Visual screenshots are **not** committed: differences are approved on the review page of the pull request (see `docs/visual-review.md`); `pnpm snapshots:pull` fetches the current baseline for local runs.\n- Individual packages provide their own test scripts (e.g. `pnpm --filter @public-ui/components test:unit`).\n - These also perform implicit builds, so explicit pre-build is unnecessary.\n\n## Pull Request Guidelines\n\n- PR titles should be meaningful as they appear in the release notes.\n- Every PR must link to its issue and contain only changes related to that issue.\n- Ensure automated tests pass and manual testing is completed when required.\n- Update documentation or migration guides if your changes affect them.\n",
25
25
  "kind": "doc"
26
26
  },
27
27
  {
@@ -237,7 +237,7 @@
237
237
  "group": "docs",
238
238
  "name": "ARIA_LABELLEDBY_EXTERNAL_CAPTION_CONCEPT",
239
239
  "path": "docs/ARIA_LABELLEDBY_EXTERNAL_CAPTION_CONCEPT.md",
240
- "code": "# Externe Tabellenbeschriftung via `_ariaLabelledby`\n\n## Problem\n\n`aria-labelledby` mit einer String-ID ist **tree-scoped**: Eine `<table>` im Shadow DOM kann ein `<h2>` im Host-Dokument per IDREF nicht erreichen.\n\n## Lösung\n\n`ariaLabelledByElements` ist Teil von `ARIAMixin` und steht auf **jedem** `Element` — nicht nur auf `ElementInternals`. Element-Referenzen (JS-Objekte) sind nicht tree-scoped und überqueren Shadow-Grenzen problemlos.\n\n## Ablauf\n\n```\n_ariaLabelledby=\"heading-id\"\n │\n ▼\nresolveTargets(host, value)\n getRootNode() → findet Elemente im korrekten Tree-Scope des Wrappers\n CSS.escape() → IDs mit Sonderzeichen werden robust behandelt\n │\n ▼\ntable.ariaLabelledByElements = [<h2>] ← ARIAMixin direkt auf <table>\ninternals.ariaLabelledByElements = [<h2>] ← zusätzlich auf dem Wrapper selbst\n```\n\nDie aufgelösten Elemente werden als `externalLabelElements`-Prop an die interne WC-Implementierung weitergereicht, die sie per Ref-Callback auf das native `<table>`-Element setzt.\n\nIst das externe Element beim ersten Render noch nicht im DOM (z. B. späte Headline), wird einmalig mit 50 ms Verzögerung nachgeladen.\n\n## Mehrere IDs\n\n`_ariaLabelledby` akzeptiert mehrere IDs (whitespace-getrennt), genau wie das native Attribut:\n\n```jsx\n<KolTableStateless _ariaLabelledby=\"heading-id subheading-id\" … />\n```\n\n`resolveTargets` teilt den String auf, löst jede ID im Tree-Scope des Wrappers auf und gibt ein `HTMLElement[]` zurück. Alle gefundenen Elemente werden gemeinsam in `ariaLabelledByElements` gesetzt — Assistive Technology verkettet deren Texte in Array-Reihenfolge zum zugänglichen Namen.\n\n## Caption-Verhalten\n\nDie `<caption>` wird immer gerendert — sie ist das Fokus-Element der Tabelle (`tabindex`).\n\n| Zustand | `<caption>` | `aria-labelledby` auf `<table>` | `ariaLabelledByElements` |\n| -------------------------- | -------------------- | ------------------------------- | ------------------------ |\n| Kein `_ariaLabelledby` | sichtbar | `\"caption\"` | — |\n| Element gefunden | `aria-hidden=\"true\"` | — | `[externes Element]` |\n| Element **nicht** gefunden | sichtbar | `\"caption\"` | — |\n\n## `attachInternals` — noch relevant?\n\n`attachInternals()` wird weiterhin auf den Shadow-Wrapper-Komponenten (`kol-table-stateless`, `kol-table-stateful`) aufgerufen. Es ist dort nicht Teil des Labeling-Mechanismus auf `<table>`, sondern dient:\n\n- **Zugänglicher Name des Wrappers selbst**: `internals.ariaLabelledByElements` gibt dem Custom-Element-Host (`<kol-table-stateless>`) einen eigenen zugänglichen Namen — nützlich für AT, die den Host inspizieren, und für Accessibility-Tree-Debugging.\n- **Vorbereitung für weitere ARIA-Properties**: `ElementInternals` ist der richtige Ort, um role, ariaRequired, ariaInvalid etc. auf einem Shadow-Host zu setzen, sobald KoliBri weitere solcher Props bekommt (→ nächster Abschnitt).\n\nAuf `kol-table-stateless-wc` (shadow: false) wurde `attachInternals` entfernt, da das Element selbst kein Shadow-Host ist — die ARIAMixin-Zuweisung direkt auf `<table>` reicht.\n\n### Wichtige Warnung zu Host-Rollen\n\nDas Setzen einer Rolle auf dem Custom-Element-Host (z. B. `internals.role = 'table'`) kann Screenreader blockieren.\nIn einigen Screenreader-/Browser-Kombinationen wird der innere Inhalt der Web Component dann nicht mehr korrekt aufgelöst und vorgelesen.\n\nDringende Empfehlung:\n\n- Keine Rollen am Host setzen.\n- Semantik stattdessen auf dem nativen inneren Element setzen (z. B. auf dem tatsächlichen `<table>`).\n\n## Generalisierung auf weitere ARIA-Properties\n\nDasselbe Muster funktioniert für alle ARIA-Relationship-Properties, die in `ARIAMixin` als Element-Referenz-Variante verfügbar sind:\n\n| ARIA-Attribut | ARIAMixin-Property | Typischer Anwendungsfall |\n| ----------------------- | ----------------------------- | -------------------------------------------------------- |\n| `aria-labelledby` | `ariaLabelledByElements` | Externe Überschrift als Tabellen-Label |\n| `aria-describedby` | `ariaDescribedByElements` | Hilfetexte oder Fehlermeldungen außerhalb des Shadow DOM |\n| `aria-controls` | `ariaControlsElements` | Tabpanel, Disclosure oder Popup in anderem Tree |\n| `aria-owns` | `ariaOwnsElements` | Logische Elternschaft über Shadow-Grenzen |\n| `aria-errormessage` | `ariaErrorMessageElements` | Fehlermeldung außerhalb des Formularfeldes |\n| `aria-activedescendant` | `ariaActiveDescendantElement` | Fokus-Tracking bei Listboxen, Comboboxen |\n\nFür jede dieser Properties gilt das gleiche Rezept:\n\n1. Prop akzeptiert einen ID-String (space-separated wo sinnvoll).\n2. `resolveTargets(host, value)` löst IDs im Tree-Scope des Wrappers auf.\n3. `element.ariaXxxElements = resolvedElements` direkt auf dem Ziel-HTML-Element setzen (ARIAMixin, keine IDREFs nötig).\n4. Auf dem Shadow-Host zusätzlich `internals.ariaXxxElements` setzen (via `attachInternals`).\n\nDie einzige Besonderheit: `ariaActiveDescendantElement` ist singular (ein Element, kein Array), alle anderen sind Arrays.\n\n## Nutzung\n\n```jsx\n<h2 id=\"orders-heading\">Bestellungen</h2>\n<KolTableStateless\n _ariaLabelledby=\"orders-heading\"\n _label=\"Fallback falls Element nicht gefunden\"\n _headerCells={headers}\n _data={data}\n/>\n```\n\n## Browser-Support\n\n| Screen Reader | Browser | Funktioniert |\n| ------------- | ------------------------- | ------------ |\n| NVDA, JAWS | Chrome, Firefox (Desktop) | ✅ |\n| VoiceOver | Safari, Chrome (macOS) | ✅ |\n| TalkBack | Chrome (Android) | ❌ |\n\nTalkBack folgt `ariaLabelledByElements` nicht — Chrome übergibt Element-Referenzen nicht ans Android Accessibility Framework. Für TalkBack-Unterstützung: `_label` verwenden.\n",
240
+ "code": "# Externe Tabellenbeschriftung via `_ariaLabelledby`\n\n## Problem\n\n`aria-labelledby` mit einer String-ID ist **tree-scoped**: Eine `<table>` im Shadow DOM kann ein `<h2>` im Host-Dokument per IDREF nicht erreichen.\n\n## Lösung\n\n`ariaLabelledByElements` ist Teil von `ARIAMixin` und steht auf **jedem** `Element` — nicht nur auf `ElementInternals`. Element-Referenzen (JS-Objekte) sind nicht tree-scoped und überqueren Shadow-Grenzen problemlos.\n\n## Ablauf\n\n```\n_ariaLabelledby=\"heading-id\"\n │\n ▼\nresolveTargets(host, value)\n getRootNode() → findet Elemente im korrekten Tree-Scope des Wrappers\n CSS.escape() → IDs mit Sonderzeichen werden robust behandelt\n │\n ▼\ntable.ariaLabelledByElements = [<h2>] ← ARIAMixin direkt auf <table>\ninternals.ariaLabelledByElements = [<h2>] ← zusätzlich auf dem Wrapper selbst\n```\n\nDie aufgelösten Elemente werden als `externalLabelElements`-Prop an die interne WC-Implementierung weitergereicht, die sie per Ref-Callback auf das native `<table>`-Element setzt.\n\nIst das externe Element beim ersten Render noch nicht im DOM (z. B. späte Headline), wird einmalig mit 50 ms Verzögerung nachgeladen.\n\n## Mehrere IDs\n\n`_ariaLabelledby` akzeptiert mehrere IDs (whitespace-getrennt), genau wie das native Attribut:\n\n```jsx\n<KolTableStateless _ariaLabelledby=\"heading-id subheading-id\" … />\n```\n\n`resolveTargets` teilt den String auf, löst jede ID im Tree-Scope des Wrappers auf und gibt ein `HTMLElement[]` zurück. Alle gefundenen Elemente werden gemeinsam in `ariaLabelledByElements` gesetzt — Assistive Technology verkettet deren Texte in Array-Reihenfolge zum zugänglichen Namen.\n\n## Caption-Verhalten\n\nDie `<caption>` wird immer gerendert — sie ist das Fokus-Element der Tabelle (`tabindex`).\n\n| Zustand | `<caption>` | `aria-labelledby` auf `<table>` | `ariaLabelledByElements` |\n| -------------------------- | -------------------- | ------------------------------- | ------------------------ |\n| Kein `_ariaLabelledby` | sichtbar | `\"caption\"` | — |\n| Element gefunden | `aria-hidden=\"true\"` | — | `[externes Element]` |\n| Element **nicht** gefunden | sichtbar | `\"caption\"` | — |\n\n## `attachInternals` — noch relevant?\n\n`attachInternals()` wird weiterhin auf den Shadow-Wrapper-Komponenten (`kol-table-stateless`, `kol-table-stateful`) aufgerufen. Es ist dort nicht Teil des Labeling-Mechanismus auf `<table>`, sondern dient:\n\n- **Zugänglicher Name des Wrappers selbst**: `internals.ariaLabelledByElements` gibt dem Custom-Element-Host (`<kol-table-stateless>`) einen eigenen zugänglichen Namen — nützlich für AT, die den Host inspizieren, und für Accessibility-Tree-Debugging.\n- **Vorbereitung für weitere ARIA-Properties**: `ElementInternals` ist der richtige Ort, um role, ariaRequired, ariaInvalid etc. auf einem Shadow-Host zu setzen, sobald KoliBri weitere solcher Props bekommt (→ nächster Abschnitt).\n\nBeide Tabellen rendern das `<table>` direkt in ihrem eigenen Shadow-Root; `attachInternals` gehört deshalb nur an ihren Host, die ARIAMixin-Zuweisung liegt direkt auf `<table>`.\n\n### Wichtige Warnung zu Host-Rollen\n\nDas Setzen einer Rolle auf dem Custom-Element-Host (z. B. `internals.role = 'table'`) kann Screenreader blockieren.\nIn einigen Screenreader-/Browser-Kombinationen wird der innere Inhalt der Web Component dann nicht mehr korrekt aufgelöst und vorgelesen.\n\nDringende Empfehlung:\n\n- Keine Rollen am Host setzen.\n- Semantik stattdessen auf dem nativen inneren Element setzen (z. B. auf dem tatsächlichen `<table>`).\n\n## Generalisierung auf weitere ARIA-Properties\n\nDasselbe Muster funktioniert für alle ARIA-Relationship-Properties, die in `ARIAMixin` als Element-Referenz-Variante verfügbar sind:\n\n| ARIA-Attribut | ARIAMixin-Property | Typischer Anwendungsfall |\n| ----------------------- | ----------------------------- | -------------------------------------------------------- |\n| `aria-labelledby` | `ariaLabelledByElements` | Externe Überschrift als Tabellen-Label |\n| `aria-describedby` | `ariaDescribedByElements` | Hilfetexte oder Fehlermeldungen außerhalb des Shadow DOM |\n| `aria-controls` | `ariaControlsElements` | Tabpanel, Disclosure oder Popup in anderem Tree |\n| `aria-owns` | `ariaOwnsElements` | Logische Elternschaft über Shadow-Grenzen |\n| `aria-errormessage` | `ariaErrorMessageElements` | Fehlermeldung außerhalb des Formularfeldes |\n| `aria-activedescendant` | `ariaActiveDescendantElement` | Fokus-Tracking bei Listboxen, Comboboxen |\n\nFür jede dieser Properties gilt das gleiche Rezept:\n\n1. Prop akzeptiert einen ID-String (space-separated wo sinnvoll).\n2. `resolveTargets(host, value)` löst IDs im Tree-Scope des Wrappers auf.\n3. `element.ariaXxxElements = resolvedElements` direkt auf dem Ziel-HTML-Element setzen (ARIAMixin, keine IDREFs nötig).\n4. Auf dem Shadow-Host zusätzlich `internals.ariaXxxElements` setzen (via `attachInternals`).\n\nDie einzige Besonderheit: `ariaActiveDescendantElement` ist singular (ein Element, kein Array), alle anderen sind Arrays.\n\n## Nutzung\n\n```jsx\n<h2 id=\"orders-heading\">Bestellungen</h2>\n<KolTableStateless\n _ariaLabelledby=\"orders-heading\"\n _label=\"Fallback falls Element nicht gefunden\"\n _headerCells={headers}\n _data={data}\n/>\n```\n\n## Browser-Support\n\n| Screen Reader | Browser | Funktioniert |\n| ------------- | ------------------------- | ------------ |\n| NVDA, JAWS | Chrome, Firefox (Desktop) | ✅ |\n| VoiceOver | Safari, Chrome (macOS) | ✅ |\n| TalkBack | Chrome (Android) | ❌ |\n\nTalkBack folgt `ariaLabelledByElements` nicht — Chrome übergibt Element-Referenzen nicht ans Android Accessibility Framework. Für TalkBack-Unterstützung: `_label` verwenden.\n",
241
241
  "kind": "doc"
242
242
  },
243
243
  {
@@ -293,7 +293,7 @@
293
293
  "group": "docs",
294
294
  "name": "CLICK_EVENT_BUBBLE_CONCEPT",
295
295
  "path": "docs/CLICK_EVENT_BUBBLE_CONCEPT.md",
296
- "code": "# Click Event Bubble/Emission Concept\n\nBeschreibt, wie Click-Events durch die KoliBri-Komponentenschichten propagiert werden: HTML5-Element → Light DOM WC → Host-Element (als Custom Event).\n\n## Überblick\n\nKoliBri Web Components verwenden Shadow DOM für Style-Isolation. Native Click-Events von Elementen innerhalb eines Shadow DOM geben zwar durch `composed: true` den Shadow-Grenzwert durch, berichten jedoch den Shadow Host als `event.target` – das eigentliche innere Element ist außen nicht sichtbar. Außerdem würde bei mehrfach verschachtelten Komponenten ein unkontrolliertes Durchsickern von Click-Events zu unerwünschtem Verhalten führen.\n\nKoliBri löst dieses Problem mit einem kontrollierten Zwei-Kanal-System:\n\n1. **Callback-Kanal** (`_on.onClick`) – direkter JavaScript-Callback, erhält das korrigierte Event-Target\n2. **DOM-Event-Kanal** – ein `CustomEvent` mit `composed: true` wird vom Host-Element neu ausgelöst\n\n## Architektur\n\n### Der Zwei-Kanal-Ansatz\n\n```\n┌──────────────────────────────────────────────────────────────────┐\n│ Äußeres Konsumenten-Element │\n│ │\n│ element.addEventListener('click', handler) (2) │\n│ OR: _on={{ onClick: handler }} (1) │\n└──────────────────────────────┬───────────────────────────────────┘\n CustomEvent│ bubbles, composed (2)\n │ dispatchDomEvent(host, KolEvent.click)\n┌──────────────────────────────┴───────────────────────────────────┐\n│ Light DOM Component: kol-button-wc / kol-link-wc (shadow: false) │\n│ │\n│ onClick(event) { │\n│ event.stopPropagation() ← native click abfangen │\n│ setEventTarget(event, innerRef) ← Target korrigieren (1) │\n│ _on?.onClick(event, value) ← Callback aufrufen (1) │\n│ dispatchDomEvent(host, KolEvent.click, value) ← Event (2) │\n│ } │\n└──────────────────────────────┬───────────────────────────────────┘\n native │ click (stopPropagation verhindert Austritt)\n MouseEvent │\n┌──────────────────────────────┴───────────────────────────────────┐\n│ HTML5 Element: <button> / <a> │\n│ Ursprung des nativen Click-Events │\n└──────────────────────────────────────────────────────────────────┘\n```\n\n### Warum `event.stopPropagation()` bei Buttons?\n\nNative Click-Events haben `composed: true`, können also die Shadow-DOM-Grenze passieren.\nBei `kol-button-wc` (light DOM) ist das kein Shadow-DOM-Problem, aber das Durchsickern des\nnativen Events nach außen würde:\n\n- das falsche `event.target` (das `<button>`-Element statt des WC-Hosts) liefern\n- die Callback-/DOM-Event-Dualität untergraben, da Konsumenten beide Signale erhalten würden\n\nDeshalb wird die native Propagation mit `stopPropagation()` unterbrochen und ein kontrolliertes\nCustom Event vom Host-Element neu ausgelöst.\n\n> **Hinweis zu Links:** `kol-link-wc` ruft kein `stopPropagation()` auf, da die native Navigation\n> des `<a>`-Elements über `event.preventDefault()` gesteuert wird (bei `_disabled: true`).\n\n## Utility-Funktionen und Typen\n\n### `dispatchDomEvent(target, event, detail)` — `packages/components/src/utils/events.ts`\n\nErstellt ein `CustomEvent` und dispatcht es vom Ziel-Element.\n\n```typescript\nfunction dispatchDomEvent<T>(target: HTMLElement, event: KolEvent, detail?: T) {\n\ttarget.dispatchEvent(createKoliBriEvent<T>(event, detail));\n}\n```\n\nDas zugrundeliegende Event wird mit diesen Optionen erstellt:\n\n```typescript\nconst DEFAULT_OPTIONS = {\n\tbubbles: true,\n\tcancelable: true,\n\tcomposed: true,\n} as const;\n```\n\n- **`bubbles: true`** — Das Event steigt im DOM nach oben\n- **`composed: true`** — Das Event kann Shadow-DOM-Grenzen passieren\n- **`cancelable: true`** — Das Event kann mit `preventDefault()` abgebrochen werden\n- **`detail`** — Trägt den Komponentenwert (z. B. `_value` bei Buttons, `_href` bei Links)\n\n### `KolEvent` — `packages/components/src/utils/events.ts`\n\nEnum aller Event-Namen, die KoliBri dispatcht:\n\n```typescript\nenum KolEvent {\n\tblur = 'blur',\n\tchange = 'change',\n\tclick = 'click',\n\tfocus = 'focus',\n\tinput = 'input',\n\t// ...weitere Events\n}\n```\n\nDer Event-Name `KolEvent.click` entspricht dem String `\"click\"`.\n\n### `setEventTarget(event, element)` — `packages/components/src/schema`\n\nKorrigiert das `target` eines Events auf das angegebene Element. Wird vor dem Callback-Aufruf\nverwendet, damit der Konsument das innere HTML5-Element (z. B. `<button>`, `<a>`) als Target erhält.\n\n### Callback-Typen — `packages/components/src/schema/types/callbacks.ts`\n\n```typescript\nexport type EventCallback<E extends Event> = (event: E) => void;\nexport type EventValueOrEventCallback<E extends Event, V> = ((event: E, value: V) => void) | EventCallback<E>;\n```\n\n- `EventCallback<E>` — einfacher Event-Handler ohne Wert\n- `EventValueOrEventCallback<E, V>` — Handler mit optionalem Komponentenwert als zweitem Argument\n\n## Umsetzung in Komponenten\n\n### Interaktive Elemente (Button, Link)\n\n#### `kol-button-wc` (`shadow: false`)\n\n```typescript\n@Component({ tag: 'kol-button-wc', shadow: false })\nexport class KolButtonWc implements ButtonAPI, FocusableElement {\n\t@Element() private readonly host?: HTMLKolButtonWcElement;\n\tprivate buttonRef?: HTMLButtonElement;\n\n\tprivate readonly setButtonRef = (ref?: HTMLButtonElement) => {\n\t\tthis.buttonRef = ref;\n\t};\n\n\tprivate readonly onClick = (event: MouseEvent) => {\n\t\tevent.stopPropagation(); // Natives Event abfangen\n\n\t\t// ...Tooltip-Logik und Form-Handling...\n\n\t\tif (typeof this.state._on?.onClick === 'function') {\n\t\t\tsetEventTarget(event, this.buttonRef); // Target korrigieren\n\t\t\tthis.state._on?.onClick(event, this.state._value); // (1) Callback\n\t\t}\n\t\tif (this.host) {\n\t\t\tdispatchDomEvent(this.host, KolEvent.click, this.state._value); // (2) DOM-Event\n\t\t}\n\t};\n\n\tpublic render(): JSX.Element {\n\t\treturn (\n\t\t\t<Host>\n\t\t\t\t<button ref={this.setButtonRef} onClick={this.onClick}>\n\t\t\t\t\t{/* Inhalt */}\n\t\t\t\t</button>\n\t\t\t</Host>\n\t\t);\n\t}\n}\n```\n\n#### `kol-link-wc` (`shadow: false`)\n\n```typescript\n@Component({ tag: 'kol-link-wc', shadow: false })\nexport class KolLinkWc implements InternalLinkAPI, FocusableElement {\n\t@Element() private readonly host?: HTMLKolLinkElement;\n\tprivate anchorRef?: HTMLAnchorElement;\n\n\tprivate readonly setAnchorRef = (ref?: HTMLAnchorElement) => {\n\t\tthis.anchorRef = ref;\n\t};\n\n\tprivate readonly onClick = (event: Event) => {\n\t\tif (this.state._disabled === true) {\n\t\t\tevent.preventDefault(); // Navigation verhindern\n\t\t} else {\n\t\t\tif (typeof this.state._on?.onClick === 'function') {\n\t\t\t\tsetEventTarget(event, this.anchorRef); // Target korrigieren\n\t\t\t\tthis.state._on?.onClick(event, this.state._href); // (1) Callback\n\t\t\t}\n\t\t\tif (this.host) {\n\t\t\t\tdispatchDomEvent(this.host, KolEvent.click, this.state._href); // (2) DOM-Event\n\t\t\t}\n\t\t}\n\t};\n\n\tpublic render(): JSX.Element {\n\t\treturn (\n\t\t\t<Host>\n\t\t\t\t<a ref={this.setAnchorRef} onClick={this.onClick} onKeyPress={this.onClick}>\n\t\t\t\t\t{/* Inhalt */}\n\t\t\t\t</a>\n\t\t\t</Host>\n\t\t);\n\t}\n}\n```\n\n### Input-Elemente\n\nInput-Elemente (z. B. `kol-input-text`, `kol-input-checkbox`) leiten Click-Events ebenfalls\nüber den `_on`-Prop-Mechanismus weiter. Der `InputTypeOnClick`-Typ ist Teil des Standard-Event-Sets:\n\n```typescript\n// packages/components/src/schema/types/input/types.ts\ntype InputTypeOnClick = {\n\t[Callback.onClick]?: EventCallback<Event>;\n};\n\nexport type InputTypeOnDefault = InputTypeOnBlur & InputTypeOnClick & InputTypeOnChange & InputTypeOnFocus & InputTypeOnInput & InputTypeOnKeyDown;\n```\n\nDas Muster für Input-Komponenten ist analog zu Buttons: `onClick` im `_on`-Objekt + `dispatchDomEvent`.\n\n## Konsumenten-Perspektive\n\n```typescript\n// (1) Callback-Kanal — typsicher, erhält den Komponentenwert:\n<KolButton _label=\"Senden\" _on={{ onClick: (event, value) => console.log(value) }} />\n\n// (2) DOM-Event-Kanal — Standard-Web-API, z. B. für Framework-Integration:\ndocument.querySelector('kol-button').addEventListener('click', (event: CustomEvent) => {\n\tconst value = event.detail; // Komponentenwert aus event.detail\n});\n```\n\n### Vergleich der beiden Kanäle\n\n| Eigenschaft | Callback (`_on.onClick`) | DOM CustomEvent (`addEventListener`) |\n| --------------- | --------------------------------------------- | ----------------------------------------------- |\n| API | KoliBri-spezifisch | Standard Web API |\n| Event-Target | Korrigiert auf inneres HTML5-Element | Host-Element (`kol-button`, `kol-link`, etc.) |\n| Komponentenwert | Als zweites Argument `value` | In `event.detail` |\n| Typ | `EventValueOrEventCallback<MouseEvent, V>` | `CustomEvent<V>` |\n| Bubbles | Nein (direkter Funktionsaufruf) | Ja (`bubbles: true, composed: true`) |\n| Geeignet für | Direkte Framework-Integration (React, Vue...) | Vanilla JS, Event-Delegation, Framework-Adapter |\n\n## Zusammenfassung\n\n| Schritt | Was passiert |\n| ------- | ---------------------------------------------------------------------------- |\n| 1 | Nutzer klickt → nativer `MouseEvent` auf `<button>` / `<a>` |\n| 2 | WC-Handler fängt Event ab, ruft `event.stopPropagation()` (nur Button) |\n| 3 | `setEventTarget(event, innerRef)` korrigiert das Event-Target |\n| 4 | `_on?.onClick(event, value)` — Callback-Kanal wird bedient |\n| 5 | `dispatchDomEvent(host, KolEvent.click, value)` — neues Custom Event am Host |\n| 6 | Das Custom Event bubblet mit `composed: true` durch den DOM-Baum |\n\n**Wichtig:**\n\n- Das native Click-Event verlässt die Komponente **nicht** unkontrolliert (außer bei Links mit `onKeyPress`)\n- Das Custom Event trägt stets den **Komponentenwert** in `event.detail` (kein DOM-Wert)\n- `setEventTarget` stellt sicher, dass der Callback das **innere Element** als Target erhält\n- Im Gegensatz zu Focus benötigt das **Bubbling des Click-Custom-Events** kein Warten auf die Theme-Bereitschaft (`data-themed`). Programmatische/delegierte Clicks (z. B. über `delegateClick()`) warten hingegen bewusst auf `data-themed`, um konsistentes visuelles Feedback und Fokus-Styling sicherzustellen.\n",
296
+ "code": "# Click Event Bubble/Emission Concept\n\nBeschreibt, wie Click-Events durch die KoliBri-Komponentenschichten propagiert werden: HTML5-Element im Shadow DOM → Host-Element (als Custom Event).\n\n## Überblick\n\nKoliBri Web Components verwenden Shadow DOM für Style-Isolation. Native Click-Events von Elementen innerhalb eines Shadow DOM geben zwar durch `composed: true` den Shadow-Grenzwert durch, berichten jedoch den Shadow Host als `event.target` – das eigentliche innere Element ist außen nicht sichtbar. Außerdem würde bei mehrfach verschachtelten Komponenten ein unkontrolliertes Durchsickern von Click-Events zu unerwünschtem Verhalten führen.\n\nKoliBri löst dieses Problem mit einem kontrollierten Zwei-Kanal-System:\n\n1. **Callback-Kanal** (`_on.onClick`) – direkter JavaScript-Callback, erhält das korrigierte Event-Target\n2. **DOM-Event-Kanal** – ein `CustomEvent` mit `composed: true` wird vom Host-Element neu ausgelöst\n\n## Architektur\n\n### Der Zwei-Kanal-Ansatz\n\n```\n┌──────────────────────────────────────────────────────────────────┐\n│ Äußeres Konsumenten-Element │\n│ │\n│ element.addEventListener('click', handler) (2) │\n│ OR: _on={{ onClick: handler }} (1) │\n└──────────────────────────────┬───────────────────────────────────┘\n CustomEvent│ bubbles, composed (2)\n │ dispatchDomEvent(host, KolEvent.click)\n┌──────────────────────────────┴───────────────────────────────────┐\n│ Shadow Component: kol-button / kol-link (shadow: true) │\n│ Handler aus BaseButtonWebComponent / BaseLinkWebComponent │\n│ │\n│ handleClick(event) { │\n│ event.stopPropagation() ← native click abfangen │\n│ setEventTarget(event, ctaRef.el) ← Target korrigieren (1) │\n│ _on?.onClick(event, value) ← Callback aufrufen (1) │\n│ dispatchDomEvent(host, KolEvent.click, value) ← Event (2) │\n│ } │\n└──────────────────────────────┬───────────────────────────────────┘\n native │ click (stopPropagation verhindert Austritt)\n MouseEvent │\n┌──────────────────────────────┴───────────────────────────────────┐\n│ HTML5 Element: <button> / <a> (Functional Component) │\n│ Ursprung des nativen Click-Events │\n└──────────────────────────────────────────────────────────────────┘\n```\n\n### Warum `event.stopPropagation()` bei Buttons?\n\nNative Click-Events haben `composed: true` und passieren die Shadow-DOM-Grenze; außen erscheint dann der Host als `event.target`. Das Durchsickern des nativen Events nach außen würde:\n\n- neben dem Custom Event ein zweites `click` am Host auslösen\n- die Callback-/DOM-Event-Dualität untergraben, da Konsumenten beide Signale erhalten würden\n\nDeshalb wird die native Propagation mit `stopPropagation()` unterbrochen und ein kontrolliertes\nCustom Event vom Host-Element neu ausgelöst.\n\n> **Hinweis zu Links:** Der Link-Handler ruft kein `stopPropagation()` auf. Die native Navigation\n> des `<a>`-Elements wird bei `_disabled: true` über `event.preventDefault()` unterbunden; in diesem\n> Fall entfallen Callback und Custom Event.\n\n### Eingebettete Komponenten\n\nRendert eine Komponente eine andere Komponente in ihrem Shadow DOM (z. B. die Sortier-Buttons von `kol-table-stateless` oder die Buttons von `kol-pagination`), rendert sie deren Functional Component über ein Item (`createButtonItem`, `createLinkItem`, …, siehe [ARC42 – Embedded Components (Items)](../packages/components/src/components/_skeleton/ARC42.md#embedded-components-items)). Das Item übernimmt dieselben Schritte wie der Handler oben, dispatcht das Custom Event aber auf dem Wurzelknoten der Functional Component (`.kol-button`, `.kol-link`). Von dort bubblet es mit `composed: true` durch den Shadow DOM der einbettenden Komponente und über deren Host nach außen.\n\n## Utility-Funktionen und Typen\n\n### `dispatchDomEvent(target, event, detail)` — `packages/components/src/utils/events.ts`\n\nErstellt ein `CustomEvent` und dispatcht es vom Ziel-Element.\n\n```typescript\nfunction dispatchDomEvent<T>(target: HTMLElement, event: KolEvent, detail?: T) {\n\ttarget.dispatchEvent(createKoliBriEvent<T>(event, detail));\n}\n```\n\nDas zugrundeliegende Event wird mit diesen Optionen erstellt:\n\n```typescript\nconst DEFAULT_OPTIONS = {\n\tbubbles: true,\n\tcancelable: true,\n\tcomposed: true,\n} as const;\n```\n\n- **`bubbles: true`** — Das Event steigt im DOM nach oben\n- **`composed: true`** — Das Event kann Shadow-DOM-Grenzen passieren\n- **`cancelable: true`** — Das Event kann mit `preventDefault()` abgebrochen werden\n- **`detail`** — Trägt den Komponentenwert (z. B. `_value` bei Buttons, `_href` bei Links)\n\n### `KolEvent` — `packages/components/src/utils/events.ts`\n\nEnum aller Event-Namen, die KoliBri dispatcht:\n\n```typescript\nenum KolEvent {\n\tblur = 'blur',\n\tchange = 'change',\n\tclick = 'click',\n\tfocus = 'focus',\n\tinput = 'input',\n\t// ...weitere Events\n}\n```\n\nDer Event-Name `KolEvent.click` entspricht dem String `\"click\"`.\n\n### `setEventTarget(event, element)` — `packages/components/src/schema`\n\nKorrigiert das `target` eines Events auf das angegebene Element. Wird vor dem Callback-Aufruf\nverwendet, damit der Konsument das innere HTML5-Element (z. B. `<button>`, `<a>`) als Target erhält.\n\n### Callback-Typen — `packages/components/src/schema/types/callbacks.ts`\n\n```typescript\nexport type EventCallback<E extends Event> = (event: E) => void;\nexport type EventValueOrEventCallback<E extends Event, V> = ((event: E, value: V) => void) | EventCallback<E>;\n```\n\n- `EventCallback<E>` — einfacher Event-Handler ohne Wert\n- `EventValueOrEventCallback<E, V>` — Handler mit optionalem Komponentenwert als zweitem Argument\n\n## Umsetzung in Komponenten\n\n### Interaktive Elemente (Button, Link)\n\nDie Handler liegen in den gemeinsamen Basisklassen und werden der Functional Component als `handleClick` bzw. `handleAnchorClick` übergeben.\n\n#### Button — `BaseButtonWebComponent` (`packages/components/src/components/button/base.tsx`)\n\n```typescript\nprotected readonly handleClick = (event: MouseEvent): void => {\n\tevent.stopPropagation(); // Natives Event abfangen\n\tthis.tooltipBehavior.hideTooltip();\n\n\tconst type = this.getRenderProp('type');\n\tif (type === 'submit') {\n\t\tpropagateSubmitEventToForm({ form: this.host, ref: this.ctaRef.el });\n\t} else if (type === 'reset') {\n\t\tpropagateResetEventToForm({ form: this.host, ref: this.ctaRef.el });\n\t} else {\n\t\tthis.formAssociation.setFormAssociatedValue(this.formValue);\n\n\t\tconst onClick = this.getRenderProp('on').onClick;\n\t\tif (typeof onClick === 'function') {\n\t\t\tsetEventTarget(event, this.ctaRef.el); // Target korrigieren\n\t\t\tonClick(event, this.formValue); // (1) Callback\n\t\t}\n\t}\n\n\tif (this.host) {\n\t\tdispatchDomEvent(this.host, KolEvent.click, this.formValue); // (2) DOM-Event\n\t}\n};\n```\n\n#### Link — `BaseLinkWebComponent` (`packages/components/src/components/link/base.tsx`)\n\n```typescript\nprotected readonly handleAnchorClick = (event: Event): void => {\n\tthis.tooltipBehavior.hideTooltip();\n\tif (this.getRenderProp('disabled') === true) {\n\t\tevent.preventDefault(); // Navigation verhindern\n\t\treturn;\n\t}\n\tconst href = this.getRenderProp('href');\n\tconst on = this.getRenderProp('on');\n\tif (typeof on?.onClick === 'function') {\n\t\tsetEventTarget(event, this.ctaRef.el); // Target korrigieren\n\t\ton.onClick(event, href); // (1) Callback\n\t}\n\tif (this.host) {\n\t\tdispatchDomEvent(this.host, KolEvent.click, href); // (2) DOM-Event\n\t}\n};\n```\n\nDie Tastaturbedienung braucht keinen eigenen Handler: Enter auf einem `<a href>` und Enter/Space auf einem `<button>` lösen nativ ein `click` aus.\n\n### Input-Elemente\n\nInput-Elemente (z. B. `kol-input-text`, `kol-input-checkbox`) leiten Click-Events ebenfalls\nüber den `_on`-Prop-Mechanismus weiter. Der `InputTypeOnClick`-Typ ist Teil des Standard-Event-Sets:\n\n```typescript\n// packages/components/src/schema/types/input/types.ts\ntype InputTypeOnClick = {\n\t[Callback.onClick]?: EventCallback<Event>;\n};\n\nexport type InputTypeOnDefault = InputTypeOnBlur & InputTypeOnClick & InputTypeOnChange & InputTypeOnFocus & InputTypeOnInput & InputTypeOnKeyDown;\n```\n\nDas Muster für Input-Komponenten ist analog zu Buttons: `onClick` im `_on`-Objekt + `dispatchDomEvent`.\n\n## Konsumenten-Perspektive\n\n```typescript\n// (1) Callback-Kanal — typsicher, erhält den Komponentenwert:\n<KolButton _label=\"Senden\" _on={{ onClick: (event, value) => console.log(value) }} />\n\n// (2) DOM-Event-Kanal — Standard-Web-API, z. B. für Framework-Integration:\ndocument.querySelector('kol-button').addEventListener('click', (event: CustomEvent) => {\n\tconst value = event.detail; // Komponentenwert aus event.detail\n});\n```\n\n### Vergleich der beiden Kanäle\n\n| Eigenschaft | Callback (`_on.onClick`) | DOM CustomEvent (`addEventListener`) |\n| --------------- | --------------------------------------------- | ----------------------------------------------- |\n| API | KoliBri-spezifisch | Standard Web API |\n| Event-Target | Korrigiert auf inneres HTML5-Element | Host-Element (`kol-button`, `kol-link`, etc.) |\n| Komponentenwert | Als zweites Argument `value` | In `event.detail` |\n| Typ | `EventValueOrEventCallback<MouseEvent, V>` | `CustomEvent<V>` |\n| Bubbles | Nein (direkter Funktionsaufruf) | Ja (`bubbles: true, composed: true`) |\n| Geeignet für | Direkte Framework-Integration (React, Vue...) | Vanilla JS, Event-Delegation, Framework-Adapter |\n\n## Zusammenfassung\n\n| Schritt | Was passiert |\n| ------- | ---------------------------------------------------------------------------- |\n| 1 | Nutzer klickt → nativer `MouseEvent` auf `<button>` / `<a>` |\n| 2 | Handler fängt Event ab, ruft `event.stopPropagation()` (nur Button) |\n| 3 | `setEventTarget(event, ctaRef.el)` korrigiert das Event-Target |\n| 4 | `_on?.onClick(event, value)` — Callback-Kanal wird bedient |\n| 5 | `dispatchDomEvent(host, KolEvent.click, value)` — neues Custom Event am Host |\n| 6 | Das Custom Event bubblet mit `composed: true` durch den DOM-Baum |\n\n**Wichtig:**\n\n- Das native Click-Event eines Buttons verlässt die Komponente **nicht** unkontrolliert\n- Das Custom Event trägt stets den **Komponentenwert** in `event.detail` (kein DOM-Wert)\n- `setEventTarget` stellt sicher, dass der Callback das **innere Element** als Target erhält\n- Im Gegensatz zu Focus benötigt das **Bubbling des Click-Custom-Events** kein Warten auf die Theme-Bereitschaft (`data-themed`). Programmatische/delegierte Clicks (z. B. über `delegateClick()`) warten hingegen bewusst auf `data-themed`, um konsistentes visuelles Feedback und Fokus-Styling sicherzustellen.\n",
297
297
  "kind": "doc"
298
298
  },
299
299
  {
@@ -301,7 +301,7 @@
301
301
  "group": "docs",
302
302
  "name": "CLICK_PROPAGATION_CONCEPT",
303
303
  "path": "docs/CLICK_PROPAGATION_CONCEPT.md",
304
- "code": "# Click Delegation Concept\n\nBeschreibt ein analoges Delegationskonzept zu Focus: Ein Klick auf den KoliBri-Host wird gezielt auf das primaere interaktive Innenelement weitergeleitet.\n\n## Status\n\nDieses Dokument beschreibt das Click-Delegationskonzept sowie noch geplante Erweiterungen; Teile sind bereits implementiert, weitere folgen sukzessive.\n\n- Ist: Für Komponenten mit `delegateClick/setClick` und `@Method() click()` ist die Click-Delegation vom Host auf das primäre Innenelement bereits umgesetzt.\n- Soll: Für alle (noch nicht migrierten) interaktiven Host-Elemente soll wie bei `focus()` eine zentrale Delegate-Strategie für `click` bereitgestellt werden.\n\n## Ziel\n\nWenn ein Konsument auf das Host-Element klickt (z. B. `kol-button`), soll nicht nur ein Event nach aussen signalisiert werden, sondern der Klick nach innen auf das primaere interaktive Element delegiert werden.\n\nBeispiele:\n\n- `kol-button` -> primaer: `<button>` in `kol-button-wc`\n- `kol-link` -> primaer: `<a>` in `kol-link-wc`\n- `kol-input-text` -> primaer: `<input>`\n\n## Architektur\n\n### Delegationsfluss (analog zu Focus)\n\nVariante A: Shadow -> Light DOM WC -> HTML5-Element\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Shadow Component: kol-button (shadow: true) │\n│ @Method click() -> delegateClick(host, () => setClick(wc))\n└─────────────────────────┬───────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────┐\n│ Light DOM Component: kol-button-wc (shadow: false) │\n│ click() -> setClick(buttonRef) │\n└─────────────────────────┬───────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────┐\n│ HTML5 Element: <button> │\n│ tatsächlich aktiviert (native click action) │\n└─────────────────────────────────────────────────────────┘\n```\n\nVariante B: Shadow -> HTML5-Element direkt\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Shadow Component: kol-input-text (shadow: true) │\n│ @Method click() -> delegateClick(host, () => setClick(input))\n└─────────────────────────┬───────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────┐\n│ HTML5 Element: <input> │\n│ tatsaechlich aktiviert │\n└─────────────────────────────────────────────────────────┘\n```\n\n## Kernideen\n\n1. Host-initiierter Klick wird nach innen delegiert, statt nur als Event nach aussen gebubbelt.\n2. Delegation fordert vor der Propagation immer `data-themed` am Host.\n3. (Geplant) Ein Reentrancy-Schutz soll Endlosschleifen durch intern erneut entstehende Click-Events verhindern; die aktuellen Hilfsfunktionen `delegateClick`/`setClick` enthalten diesen Schutz noch nicht.\n4. Nur primaere Interaktionselemente sind Delegationsziele.\n\n### Warum `data-themed` auch für Click zwingend ist\n\nTechnisch kann ein `element.click()` oft auch schon vor abgeschlossener Theme-Anwendung funktionieren.\nDas waere aber aus UX- und Test-Sicht nicht authentisch:\n\n- Der Nutzer klickt real auf sichtbare, fertig gerenderte Controls.\n- Ohne `data-themed` besteht das Risiko, dass Tests auf ein noch unsichtbares oder semantisch nicht final aufgebautes Element klicken.\n- Dadurch entstehen false positives: Der Test ist gruen, obwohl die reale Interaktion noch nicht stabil waere.\n\nDeshalb gilt analog zu Focus: Click-Delegation darf erst starten, wenn `data-themed` gesetzt ist.\n\n## Utility Functions (Vorschlag)\n\nDatei (neu): `packages/components/src/utils/element-click.ts`\n\n### `delegateClick(host, callback)`\n\nVerantwortlich für Shadow Components (`shadow: true`). Wartet zwingend auf Theme-Readiness und führt dann die eigentliche Klick-Aktion aus.\n\n```typescript\nexport async function delegateClick(host: HTMLElement, callback: () => Promise<void>): Promise<void> {\n\ttry {\n\t\tif (!host.hasAttribute('data-themed')) {\n\t\t\tawait waitForThemed(host);\n\t\t}\n\t\tawait callback();\n\t} catch {\n\t\tthrow new Error(\n\t\t\t'The interactive element inside the KoliBri web component could not be clicked. Try calling the click method on the web component after a short delay again.',\n\t\t);\n\t}\n}\n```\n\n### `setClick(element)`\n\nFuehrt den nativen Klick auf dem Ziel-Element aus und validiert, dass die Aktivierung erfolgt ist.\n\n```typescript\nexport async function setClick(element: HTMLElement): Promise<void> {\n\tlet attempts = 0;\n\tdo {\n\t\tif (element) {\n\t\t\telement.click();\n\t\t}\n\t\tawait new Promise((r) => requestAnimationFrame(r));\n\t\tattempts++;\n\t} while (!isElementVisible(element) && attempts < MAX_CLICK_ATTEMPTS);\n}\n```\n\n### `isElementVisible(element)`\n\nPrueft, ob das Element sichtbar ist (Groesse > 0). Dies stellt sicher, dass das Element vor dem Klick vorhanden und sichtbar ist.\n\n### `waitForThemed(host)`\n\nKann 1:1 aus dem Focus-Utility wiederverwendet werden.\n\n### Konstanten\n\n```typescript\nconst MAX_CLICK_ATTEMPTS = 3;\nconst MAX_TIMEOUT_DURATION = 5000;\n```\n\n## Komponentenregeln\n\n### Interface\n\nAlle klickbaren Komponenten implementieren das `ClickableElement`-Interface aus `packages/components/src/utils/element-click.ts`:\n\n```typescript\nexport interface ClickableElement {\n\tclick(): Promise<void>;\n}\n```\n\n### API-Kontrakt für alle Web Components\n\nIn allen klickbaren Web Components gilt derselbe oeffentliche Methodenvertrag:\n\n```typescript\n@Method()\npublic async click(): Promise<void> {\n\t// komponentenspezifische Delegation\n}\n```\n\nDamit erzwingen wir API-Homogenitaet analog zum HTML-Standard.\n\n### Regel 1: Shadow Component (`shadow: true`)\n\nDer Host delegiert nach innen via `delegateClick`:\n\n```typescript\n@Component({ tag: 'kol-button', shadow: true })\nexport class KolButton implements ClickableElement {\n\t@Element() private readonly host?: HTMLKolButtonElement;\n\tprivate buttonWcRef?: HTMLKolButtonWcElement;\n\n\t@Method()\n\tpublic async click(): Promise<void> {\n\t\treturn delegateClick(this.host!, () => setClick(this.buttonWcRef!));\n\t}\n}\n```\n\nHinweis:\n\n- `delegateClick` wartet zwingend auf `data-themed` vor der Klick-Delegation, um konsistentes visuelles Feedback zu sichern.\n\n### Regel 2: Light DOM Component (`shadow: false`)\n\nKein `delegateClick` notwendig. Nur `setClick` auf dem primaeren HTML-Element:\n\n```typescript\n@Component({ tag: 'kol-button-wc', shadow: false })\nexport class KolButtonWc implements ClickableElement {\n\tprivate buttonRef?: HTMLButtonElement;\n\n\t@Method()\n\tpublic async click(): Promise<void> {\n\t\treturn setClick(this.buttonRef!);\n\t}\n}\n```\n\n### Regel 3: Primaeres Ziel je Komponente festlegen\n\nJede interaktive Komponente dokumentiert genau ein primaeres Klickziel:\n\n- `kol-button`: interner `<button>`\n- `kol-link`: interner `<a>`\n- `kol-input-*`: interner `<input>` bzw. `<textarea>`\n- `kol-select`: interner triggernder Button/Control\n\n## Event-Semantik\n\nDas Delegationskonzept ersetzt nicht die bestehende Event-API (`_on.onClick`, `dispatchDomEvent`), sondern ordnet die Reihenfolge:\n\n1. Host-Klick wird nach innen delegiert.\n2. Das innere Element verarbeitet den nativen Klick.\n3. Bestehende Callback-/DOM-Event-Mechanismen bleiben unveraendert aktiv.\n\nDamit bleibt die oeffentliche API kompatibel.\n\n## Abgrenzung zu aktuellem Bubble-Konzept\n\n- Bubble-Konzept: Ereignis von innen nach aussen signalisieren.\n- Delegationskonzept: Interaktion von aussen nach innen ausfuehren.\n\nBeides ist kombinierbar und sinnvoll:\n\n- nach innen delegieren fuer robuste Aktivierung durch Host-Interaktion\n- nach aussen emittieren fuer API/Framework-Kompatibilitaet\n\n## Akzeptanzkriterien\n\n1. Klick auf den Host (`event.target === host`) aktiviert das primaere Innenelement.\n2. Klick direkt auf das Innenelement behaelt sein heutiges Verhalten.\n3. Keine doppelte Ausfuehrung von Click-Callbacks.\n4. Keine Rekursion/Endlosschleife durch delegierte Klicks.\n5. Verhalten ist identisch fuer Maus und Tastaturaktivierung (Enter/Space), soweit komponentenspezifisch anwendbar.\n\n## Teststrategie\n\n### Unit Tests\n\n- `delegateClick` wartet verpflichtend auf `data-themed` analog zu `delegateFocus`.\n- Timeout-Fehler wird mit nutzerfreundlicher Message geworfen.\n- Reentrancy-Guard verhindert zweite Delegation.\n- Kein Click-Versuch vor `data-themed` (verhindert Interaktion mit unsichtbaren/fruehen DOM-Zustaenden).\n\n### E2E Tests\n\n- Host direkt klicken -> primaere Aktion wird einmal ausgefuehrt.\n- Inneres Element klicken -> Aktion wird ebenfalls einmal ausgefuehrt.\n- `_on.onClick` und DOM-Event werden jeweils genau einmal beobachtet.\n\n#### TypeScript-Typisierung in E2E Tests\n\nBei DOM-Zugriffen in E2E-Tests muss zwischen KoliBri Web Components und nativen HTML-Elementen unterschieden werden, da der `click()`-Aufruf auf KoliBri-Komponenten **asynchron** ist (im Gegensatz zum synchronen nativen `click()`):\n\n- **KoliBri Web Components** erhalten den spezifischen KoliBri-Elementtyp, z. B. `HTMLKolButtonElement`, `HTMLKolLinkElement`, `HTMLKolInputTextElement`. Nur so steht die typisierte `async click()`-Methode der Komponente zur Verfuegung.\n- **Native HTML-Elemente** erhalten den entsprechenden Standard-Typ, z. B. `HTMLButtonElement`, `HTMLAnchorElement`, `HTMLInputElement`.\n\nBeispiel:\n\n```typescript\n// KoliBri Web Component: click() ist async und muss awaited werden\nconst kolButton = document.querySelector('kol-button') as HTMLKolButtonElement;\nawait kolButton.click();\n\n// Natives HTML-Element: click() ist synchron\nconst nativeButton = document.querySelector('button') as HTMLButtonElement;\nnativeButton.click();\n```\n\n**Falsch** (kein await, falscher Typ):\n\n```typescript\n// Fehler: Element.click() ist synchron und nicht die KoliBri-Delegation\nconst kolButton = document.querySelector('kol-button') as HTMLElement;\nkolButton.click(); // loest keinen delegierten Klick aus\n```\n\n**Regel**: Immer `await` verwenden, wenn `click()` auf einer KoliBri Web Component aufgerufen wird. Das Weglassen von `await` fuehrt dazu, dass der Klick vor abgeschlossener Theme-Bereitschaft und Delegation ausgefuehrt werden koennte oder der Test zu frueh weiterlaeuft.\n\n## Zusammenfassung\n\nAnalog zu Focus sollte Click als echte Delegation von Host nach innen modelliert werden.\n\n- Focus: Host `focus()` -> inneres Element `focus()`\n- Click (Soll): Host-Klick -> inneres Element `click()`\n\nSo wird die Komponente aus Konsumentensicht konsistent: Der Host ist die oeffentliche Interaktionsflaeche, die innere Aktivierung bleibt korrekt und kontrolliert.\n",
304
+ "code": "# Click Delegation Concept\n\nBeschreibt ein analoges Delegationskonzept zu Focus: Ein Klick auf den KoliBri-Host wird gezielt auf das primaere interaktive Innenelement weitergeleitet.\n\n## Status\n\nDieses Dokument beschreibt das Click-Delegationskonzept sowie noch geplante Erweiterungen; Teile sind bereits implementiert, weitere folgen sukzessive.\n\n- Ist: Komponenten mit `@Method() click()` delegieren den Klick über den Decorator `@delegateClick('ctaRef')` (`packages/components/src/utils/element-interaction.ts`) mit `delegateClick`/`setClick` (`packages/components/src/utils/element-click.ts`) auf ihr primäres Innenelement.\n- Soll: Ein Reentrancy-Schutz gegen intern erneut entstehende Click-Events (siehe Kernidee 3).\n\n## Ziel\n\nWenn ein Konsument auf das Host-Element klickt (z. B. `kol-button`), soll nicht nur ein Event nach aussen signalisiert werden, sondern der Klick nach innen auf das primaere interaktive Element delegiert werden.\n\nBeispiele:\n\n- `kol-button` -> primaer: `<button class=\"kol-button__interactive-element\">` im Shadow DOM\n- `kol-link-button` -> primaer: `<a class=\"kol-link__interactive-element\">` im Shadow DOM\n- `kol-input-text` -> primaer: `<input>` im Shadow DOM\n\n## Architektur\n\n### Delegationsfluss (analog zu Focus)\n\nDie Functional Component einer Komponente rendert das primaere HTML5-Element direkt in den Shadow DOM der Komponente. Die Delegation hat deshalb genau eine Stufe:\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Shadow Component: kol-button (shadow: true) │\n│ @Method() @delegateClick('ctaRef') click() │\n└─────────────────────────┬───────────────────────────────┘\n ↓ wartet auf data-themed, dann setClick(ctaRef.el)\n┌─────────────────────────────────────────────────────────┐\n│ HTML5 Element: <button> │\n│ tatsaechlich aktiviert (native click action) │\n└─────────────────────────────────────────────────────────┘\n```\n\n### Eingebettete Komponenten\n\nRendert eine Komponente eine andere Komponente in ihrem Shadow DOM (z. B. die Buttons von `kol-pagination`), dann rendert sie deren Functional Component ueber ein Item (`createButtonItem`, `createLinkItem`, …, siehe [ARC42 – Embedded Components (Items)](../packages/components/src/components/_skeleton/ARC42.md#embedded-components-items)). Ein eingebetteter Button ist kein eigenes Element und hat keine eigene `click()`-Methode; das `click()` der einbettenden Komponente zielt auf deren eigenes primaeres Element.\n\n## Kernideen\n\n1. Host-initiierter Klick wird nach innen delegiert, statt nur als Event nach aussen gebubbelt.\n2. Delegation fordert vor der Propagation immer `data-themed` am Host.\n3. (Geplant) Ein Reentrancy-Schutz soll Endlosschleifen durch intern erneut entstehende Click-Events verhindern; die aktuellen Hilfsfunktionen `delegateClick`/`setClick` enthalten diesen Schutz noch nicht.\n4. Nur primaere Interaktionselemente sind Delegationsziele.\n\n### Warum `data-themed` auch für Click zwingend ist\n\nTechnisch kann ein `element.click()` oft auch schon vor abgeschlossener Theme-Anwendung funktionieren.\nDas waere aber aus UX- und Test-Sicht nicht authentisch:\n\n- Der Nutzer klickt real auf sichtbare, fertig gerenderte Controls.\n- Ohne `data-themed` besteht das Risiko, dass Tests auf ein noch unsichtbares oder semantisch nicht final aufgebautes Element klicken.\n- Dadurch entstehen false positives: Der Test ist gruen, obwohl die reale Interaktion noch nicht stabil waere.\n\nDeshalb gilt analog zu Focus: Click-Delegation darf erst starten, wenn `data-themed` gesetzt ist.\n\n## Utility Functions\n\n### `delegateClick(host, callback)` — `packages/components/src/utils/element-click.ts`\n\nWartet zwingend auf Theme-Readiness und fuehrt dann die eigentliche Klick-Aktion aus.\n\n```typescript\nexport async function delegateClick(host: HTMLElement, callback: () => Promise<void>): Promise<void> {\n\ttry {\n\t\tif (!host.hasAttribute('data-themed')) {\n\t\t\tawait waitForThemed(host);\n\t\t}\n\t\tawait callback();\n\t} catch {\n\t\tthrow new Error(\n\t\t\t'The interactive element inside the KoliBri web component could not be clicked. Try calling the click method on the web component after a short delay again.',\n\t\t);\n\t}\n}\n```\n\n### `setClick(element)` — `packages/components/src/utils/element-click.ts`\n\nFuehrt den nativen Klick auf dem Ziel-Element aus und wiederholt ihn pro Animation Frame, solange das Element noch nicht sichtbar ist.\n\n```typescript\nexport async function setClick(element: HTMLElement): Promise<void> {\n\tlet attempts = 0;\n\tdo {\n\t\tif (element) {\n\t\t\telement.click();\n\t\t}\n\t\tawait new Promise((r) => requestAnimationFrame(r));\n\t\tattempts++;\n\t} while (!isElementVisible(element) && attempts < MAX_CLICK_ATTEMPTS);\n}\n```\n\n### `isElementVisible(element)` (intern)\n\nPrueft ueber `getBoundingClientRect()`, ob das Element sichtbar ist (Breite und Hoehe > 0).\n\n### `waitForThemed(host)` — `packages/components/src/utils/element-themed.ts`\n\nWird gemeinsam mit der Focus-Delegation genutzt; Timeout nach 5000 ms.\n\n### Konstanten\n\n```typescript\nconst MAX_CLICK_ATTEMPTS = 3;\n```\n\n## Komponentenregeln\n\n### Interface\n\nAlle klickbaren Komponenten implementieren das `ClickableElement`-Interface aus `packages/components/src/schema/interfaces/ClickableElement.ts`:\n\n```typescript\nexport interface ClickableElement {\n\tclick(): Promise<void>;\n}\n```\n\n### API-Kontrakt für alle Web Components\n\nIn allen klickbaren Web Components gilt derselbe oeffentliche Methodenvertrag:\n\n```typescript\n@Method()\npublic async click(): Promise<void> {\n\t// komponentenspezifische Delegation\n}\n```\n\nDamit erzwingen wir API-Homogenitaet analog zum HTML-Standard.\n\n### Regel 1: Delegation ueber den Decorator\n\nDas primaere Element haelt ein `CtaRef` aus `createCtaRef()`, die Methode entsteht ueber `@delegateClick`:\n\n```typescript\n@Component({ tag: 'kol-button', shadow: true })\nexport class KolButton extends BaseButtonWebComponent implements ButtonProps, ClickableElement {\n\t@Element() protected readonly host?: HTMLKolButtonElement;\n\n\t// In der Basisklasse: protected readonly ctaRef = createCtaRef<HTMLButtonElement>();\n\t// Die Functional Component setzt ihn am nativen Element: <button ref={ctaRef} …>\n\n\t@Method()\n\t@delegateClick('ctaRef')\n\tpublic async click(): Promise<void> {}\n}\n```\n\nHinweise:\n\n- `delegateClick` wartet zwingend auf `data-themed` vor der Klick-Delegation, um konsistentes visuelles Feedback zu sichern.\n- Liefert das Praedikat von `createCtaRef(isInactive)` `true`, liest sich `ctaRef.el` als `undefined` und `click()` tut nichts, wie bei einem nativ deaktivierten Control.\n\n### Regel 2: Primaeres Ziel je Komponente festlegen\n\nJede interaktive Komponente dokumentiert genau ein primaeres Klickziel:\n\n- `kol-button`: interner `<button>`\n- `kol-link`: interner `<a>`\n- `kol-input-*`: interner `<input>` bzw. `<textarea>`\n- `kol-combobox`: interner `<input>`\n\n## Event-Semantik\n\nDas Delegationskonzept ersetzt nicht die bestehende Event-API (`_on.onClick`, `dispatchDomEvent`), sondern ordnet die Reihenfolge:\n\n1. Host-Klick wird nach innen delegiert.\n2. Das innere Element verarbeitet den nativen Klick.\n3. Bestehende Callback-/DOM-Event-Mechanismen bleiben unveraendert aktiv.\n\nDamit bleibt die oeffentliche API kompatibel.\n\n## Abgrenzung zu aktuellem Bubble-Konzept\n\n- Bubble-Konzept: Ereignis von innen nach aussen signalisieren.\n- Delegationskonzept: Interaktion von aussen nach innen ausfuehren.\n\nBeides ist kombinierbar und sinnvoll:\n\n- nach innen delegieren fuer robuste Aktivierung durch Host-Interaktion\n- nach aussen emittieren fuer API/Framework-Kompatibilitaet\n\n## Akzeptanzkriterien\n\n1. Klick auf den Host (`event.target === host`) aktiviert das primaere Innenelement.\n2. Klick direkt auf das Innenelement behaelt sein heutiges Verhalten.\n3. Keine doppelte Ausfuehrung von Click-Callbacks.\n4. Keine Rekursion/Endlosschleife durch delegierte Klicks.\n5. Verhalten ist identisch fuer Maus und Tastaturaktivierung (Enter/Space), soweit komponentenspezifisch anwendbar.\n\n## Teststrategie\n\n### Unit Tests\n\n- `delegateClick` wartet verpflichtend auf `data-themed` analog zu `delegateFocus`.\n- Timeout-Fehler wird mit nutzerfreundlicher Message geworfen.\n- Reentrancy-Guard verhindert zweite Delegation.\n- Kein Click-Versuch vor `data-themed` (verhindert Interaktion mit unsichtbaren/fruehen DOM-Zustaenden).\n\n### E2E Tests\n\n- Host direkt klicken -> primaere Aktion wird einmal ausgefuehrt.\n- Inneres Element klicken -> Aktion wird ebenfalls einmal ausgefuehrt.\n- `_on.onClick` und DOM-Event werden jeweils genau einmal beobachtet.\n\n#### TypeScript-Typisierung in E2E Tests\n\nBei DOM-Zugriffen in E2E-Tests muss zwischen KoliBri Web Components und nativen HTML-Elementen unterschieden werden, da der `click()`-Aufruf auf KoliBri-Komponenten **asynchron** ist (im Gegensatz zum synchronen nativen `click()`):\n\n- **KoliBri Web Components** erhalten den spezifischen KoliBri-Elementtyp, z. B. `HTMLKolButtonElement`, `HTMLKolLinkElement`, `HTMLKolInputTextElement`. Nur so steht die typisierte `async click()`-Methode der Komponente zur Verfuegung.\n- **Native HTML-Elemente** erhalten den entsprechenden Standard-Typ, z. B. `HTMLButtonElement`, `HTMLAnchorElement`, `HTMLInputElement`.\n\nBeispiel:\n\n```typescript\n// KoliBri Web Component: click() ist async und muss awaited werden\nconst kolButton = document.querySelector('kol-button') as HTMLKolButtonElement;\nawait kolButton.click();\n\n// Natives HTML-Element: click() ist synchron\nconst nativeButton = document.querySelector('button') as HTMLButtonElement;\nnativeButton.click();\n```\n\n**Falsch** (kein await, falscher Typ):\n\n```typescript\n// Fehler: Element.click() ist synchron und nicht die KoliBri-Delegation\nconst kolButton = document.querySelector('kol-button') as HTMLElement;\nkolButton.click(); // loest keinen delegierten Klick aus\n```\n\n**Regel**: Immer `await` verwenden, wenn `click()` auf einer KoliBri Web Component aufgerufen wird. Das Weglassen von `await` fuehrt dazu, dass der Klick vor abgeschlossener Theme-Bereitschaft und Delegation ausgefuehrt werden koennte oder der Test zu frueh weiterlaeuft.\n\n## Zusammenfassung\n\nAnalog zu Focus sollte Click als echte Delegation von Host nach innen modelliert werden.\n\n- Focus: Host `focus()` -> inneres Element `focus()`\n- Click (Soll): Host-Klick -> inneres Element `click()`\n\nSo wird die Komponente aus Konsumentensicht konsistent: Der Host ist die oeffentliche Interaktionsflaeche, die innere Aktivierung bleibt korrekt und kontrolliert.\n",
305
305
  "kind": "doc"
306
306
  },
307
307
  {
@@ -333,7 +333,7 @@
333
333
  "group": "docs",
334
334
  "name": "component-dependencies-popover",
335
335
  "path": "docs/component-dependencies-popover.md",
336
- "code": "# Komponentenabhängigkeiten: Split-Button\n\n## Abhängigkeitsdiagramm\n\n```mermaid\ngraph TB\n subgraph SB[\"kol-split-button\"]\n BTN1[\"kol-button-wc (Primär-Button)\"]\n subgraph PB_WC[\"kol-popover-button-wc\"]\n BTN2[\"kol-button-wc (Trigger-Button)\"]\n PFC[\"PopoverFC\"]\n PCTRL[\"PopoverController\"]\n end\n end\n```\n\n## Beschreibung\n\n| Komponente | Rolle | Abhängigkeiten |\n| ----------------------- | ---------------------------------------- | ------------------------------------------------- |\n| `kol-split-button` | Primärer Einstiegspunkt | `kol-button-wc`, `kol-popover-button-wc` |\n| `kol-popover-button-wc` | Dropdown-Logik | `kol-button-wc`, `PopoverFC`, `PopoverController` |\n| `kol-button-wc` | Button (Primär & Trigger) | – |\n| `PopoverFC` | Internes Functional Component | – |\n| `PopoverController` | Steuerung (öffnen/schließen/Ausrichtung) | – |\n",
336
+ "code": "# Komponentenabhängigkeiten: Split-Button\n\n## Abhängigkeitsdiagramm\n\n```mermaid\ngraph TB\n subgraph SB[\"kol-split-button\"]\n SBFC[\"SplitButtonFC\"]\n BTN1[\"ButtonFC (Primär-Button)\"]\n subgraph ITEM[\"createPopoverButtonItem\"]\n PBFC[\"PopoverButtonFC\"]\n BTN2[\"ButtonFC (Trigger-Button)\"]\n PFC[\"PopoverFC\"]\n PB[\"PopoverBehavior\"]\n TB[\"TooltipBehavior\"]\n end\n end\n SBFC --> BTN1\n SBFC --> PBFC\n PBFC --> BTN2\n PBFC --> PFC\n```\n\n## Beschreibung\n\n| Baustein | Rolle | Abhängigkeiten |\n| ------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------- |\n| `kol-split-button` | Primärer Einstiegspunkt | `SplitButtonFC`, `createPopoverButtonItem` |\n| `createPopoverButtonItem` | Orchestriert den Trigger-Button samt Popover (Props, Refs, Listener) | `PopoverButtonFC`, `PopoverBehavior`, `TooltipBehavior` |\n| `PopoverButtonFC` | Trigger-Button und Popover | `ButtonFC`, `PopoverFC` |\n| `PopoverFC` | Natives Popover-Element (`popover=\"auto\"`) mit Pfeil | – |\n| `PopoverBehavior` | Öffnen, Schließen und Ausrichten am Trigger (auch `kol-popover-button`) | – |\n",
337
337
  "kind": "doc"
338
338
  },
339
339
  {
@@ -365,7 +365,7 @@
365
365
  "group": "docs",
366
366
  "name": "FOCUS_PROPAGATION_CONCEPT",
367
367
  "path": "docs/FOCUS_PROPAGATION_CONCEPT.md",
368
- "code": "# Focus Propagation Concept\n\nBeschreibt, wie der Focus durch die KoliBri-Komponentenschichten delegiert wird: Shadow DOM → (optional Light DOM) → HTML5-Element.\n\n## Überblick\n\nKoliBri Web Components verwenden Shadow DOM für Style-Isolation. Da der Browser fokussierbare Elemente innerhalb eines Shadow DOM nicht direkt ansteuern kann, muss der Focus programmatisch von der öffentlichen Shadow-Komponente an das tatsächlich fokussierbare HTML5-Element weitergeleitet werden.\n\nDie zentrale Herausforderung: Bevor der Focus gesetzt werden kann, müssen die Adopted Style Sheets geladen und angewendet sein. Ohne diese Absicherung kann es zu Race Conditions kommen — der Focus wird auf ein Element gesetzt, das noch nicht vollständig gerendert ist.\n\nTechnisch kann ein früher `focus()` im Browser teilweise trotzdem funktionieren. Das ist jedoch nicht authentisch zum realen Nutzerverhalten: Interaktion soll erst auf final sichtbaren und stabil gerenderten Controls stattfinden. Für Tests bedeutet das: Kein Focus auf potenziell noch unsichtbare oder semantisch unvollständige Elemente. Deshalb wird `data-themed` als verbindliche Readiness-Bedingung verwendet.\n\n## Architektur\n\n### Zwei Varianten der Focus-Delegation\n\nEs gibt zwei Varianten, je nach Komponentenaufbau:\n\n**Variante A: Shadow → Light DOM WC → HTML5-Element** (z. B. `kol-button`)\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Shadow Component: kol-button (shadow: true) │\n│ focus() → delegateFocus(host, () => setFocus(wcRef)) │\n└─────────────────────────┬───────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────┐\n│ Light DOM Component: kol-button-wc (shadow: false) │\n│ focus() → setFocus(buttonRef) │\n└─────────────────────────┬───────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────┐\n│ HTML5 Element: <button> │\n│ Tatsächlich fokussierbar │\n└─────────────────────────────────────────────────────────┘\n```\n\n**Variante B: Shadow → HTML5-Element direkt** (z. B. `kol-input-text`)\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Shadow Component: kol-input-text (shadow: true) │\n│ focus() → delegateFocus(host, () => setFocus(inputRef)) │\n└─────────────────────────┬───────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────┐\n│ HTML5 Element: <input> │\n│ Tatsächlich fokussierbar │\n└─────────────────────────────────────────────────────────┘\n```\n\n### Das `data-themed`-Attribut\n\nDas Theme-System setzt das `data-themed`-Attribut auf Shadow-Komponenten, sobald die Adopted Style Sheets geladen sind. Der Ablauf:\n\n1. **Theme-Registrierung:** Beim Bootstrapping werden Themes über `register()` aus dem `adopted-style-sheets`-Paket registriert (`packages/components/src/core/bootstrap.ts`).\n\n2. **Style-Anwendung:** Stencils `setMode()`-Callback wird für jede Komponente aufgerufen (`packages/components/src/global/script.ts`). Dort wird `setThemeStyle(elm, getThemeDetails(elm))` aufgerufen, um die Adopted Style Sheets in den Shadow DOM zu injizieren.\n\n3. **Markierung:** `setThemeStyle()` setzt nach erfolgreicher Style-Anwendung das `data-themed`-Attribut auf dem Host-Element.\n\n## Utility Functions\n\nDatei: `packages/components/src/utils/element-focus.ts`\n\n### `delegateFocus(host, callback)`\n\nZentrale Focus-Delegations-Funktion für **Shadow Components** (`shadow: true`). Wartet auf die Theme-Bereitschaft des Host-Elements und führt dann die Focus-Callback-Funktion aus.\n\n```typescript\nexport async function delegateFocus(host: HTMLElement, callback: () => Promise<void>): Promise<void> {\n\ttry {\n\t\tif (!host.hasAttribute('data-themed')) {\n\t\t\tawait waitForThemed(host);\n\t\t}\n\t\tawait callback();\n\t} catch {\n\t\tthrow new Error(\n\t\t\t`The interactive element inside the KoliBri web component could not be focused. Try calling the focus method on the web component after a short delay again.`,\n\t\t);\n\t}\n}\n```\n\n**Parameter:**\n\n- `host` — Das Shadow Component Host-Element (`this.host!`)\n- `callback` — Async Funktion, die `setFocus()` auf dem Ziel-Element aufruft\n\n**Verhalten:**\n\n1. Prüft, ob `data-themed` bereits gesetzt ist\n2. Falls nicht: wartet über `waitForThemed()` (MutationObserver) bis maximal 5 Sekunden\n3. Ruft dann den `callback` auf, der den eigentlichen Focus setzt\n4. Bei Fehler (Timeout oder Focus-Fehler): wirft einen benutzerfreundlichen Fehler\n\nDie Wartephase ist nicht nur technisch motiviert, sondern auch eine Qualitätsgrenze: Sie verhindert, dass Focus-Interaktionen in Tests oder in schneller Initialisierung auf UI-Zuständen stattfinden, die ein Nutzer so noch nicht sieht.\n\n### `setFocus(element)`\n\nFokussiert ein HTML-Element durch wiederholte Versuche pro Animation Frame. Wird sowohl in Shadow- als auch in Light-DOM-Komponenten verwendet.\n\n```typescript\nexport async function setFocus(element: HTMLElement): Promise<void> {\n\tlet attempts = 0;\n\tdo {\n\t\tif (element) {\n\t\t\telement.focus();\n\t\t}\n\t\tawait new Promise((r) => requestAnimationFrame(r));\n\t\tattempts++;\n\t} while (!isActiveElement(element) && attempts < MAX_FOCUS_ATTEMPTS);\n}\n```\n\n**Verhalten:**\n\n- Ruft `element.focus()` auf und prüft pro Animation Frame, ob das Element fokussiert ist\n- Maximal `MAX_FOCUS_ATTEMPTS` (10) Versuche\n- Nutzt `isActiveElement()` für korrekte Focus-Erkennung innerhalb von Shadow DOMs\n\n### `isActiveElement(element)` (intern)\n\nPrüft, ob ein Element aktuell fokussiert ist. Berücksichtigt dabei korrekt die Shadow-DOM-Grenze.\n\n```typescript\nfunction isActiveElement(element: HTMLElement): boolean {\n\tconst root = element.getRootNode();\n\tif (root instanceof ShadowRoot) {\n\t\treturn root.activeElement === element;\n\t}\n\treturn document.activeElement === element;\n}\n```\n\n**Warum nötig:** `document.activeElement` zeigt bei fokussierten Elementen innerhalb eines Shadow DOM nur den Shadow Host, nicht das tatsächlich fokussierte Element. Daher wird `shadowRoot.activeElement` geprüft.\n\n### `waitForThemed(host)` (intern)\n\nWartet per MutationObserver darauf, dass das `data-themed`-Attribut auf dem Host-Element gesetzt wird.\n\n```typescript\nfunction waitForThemed(host: HTMLElement): Promise<void> {\n\treturn new Promise<void>((resolve, reject) => {\n\t\tconst observer = new MutationObserver(() => {\n\t\t\tif (host.hasAttribute('data-themed')) {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t\tobserver.disconnect();\n\t\t\t\tresolve();\n\t\t\t}\n\t\t});\n\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\tobserver.disconnect();\n\t\t\treject(new Error('Timeout waiting for data-themed attribute'));\n\t\t}, MAX_TIMEOUT_DURATION);\n\n\t\tobserver.observe(host, {\n\t\t\tattributes: true,\n\t\t\tattributeFilter: ['data-themed'],\n\t\t});\n\t});\n}\n```\n\n**Verhalten:**\n\n- Beobachtet Attribut-Änderungen auf dem Host-Element\n- Resolved sofort, wenn `data-themed` gesetzt wird\n- Timeout nach `MAX_TIMEOUT_DURATION` (5000 ms) mit Error\n\n### Konstanten\n\n```typescript\nconst MAX_FOCUS_ATTEMPTS = 10;\nconst MAX_TIMEOUT_DURATION = 5000;\n```\n\n## Umsetzung in Komponenten\n\n### Interface\n\nAlle fokussierbaren Komponenten implementieren das `FocusableElement`-Interface:\n\n```typescript\nexport interface FocusableElement {\n\tfocus(): Promise<void>;\n}\n```\n\n### Regel 1: Shadow Component (`shadow: true`)\n\n**Immer** `delegateFocus()` mit `setFocus()` verwenden.\n\n#### Variante A: Mit innerem WC-Element\n\nWenn die Shadow-Komponente eine Light-DOM-Komponente (`-wc`) rendert:\n\n```typescript\n@Component({ tag: 'kol-button', shadow: true })\nexport class KolButton implements ButtonProps, FocusableElement {\n\t@Element() private readonly host?: HTMLKolButtonElement;\n\tprivate buttonWcRef?: HTMLKolButtonWcElement;\n\n\tprivate readonly setButtonWcRef = (ref?: HTMLKolButtonWcElement) => {\n\t\tthis.buttonWcRef = ref;\n\t};\n\n\t@Method()\n\tpublic async focus(): Promise<void> {\n\t\treturn delegateFocus(this.host!, () => setFocus(this.buttonWcRef!));\n\t}\n\n\tpublic render(): JSX.Element {\n\t\treturn (\n\t\t\t<KolButtonWcTag ref={this.setButtonWcRef} /* ...props */>\n\t\t\t\t<slot name=\"expert\" slot=\"expert\"></slot>\n\t\t\t</KolButtonWcTag>\n\t\t);\n\t}\n}\n```\n\nWeitere Beispiele: `kol-link`, `kol-button-link`, `kol-link-button`, `kol-select`, `kol-accordion`, `kol-details`\n\n#### Variante B: Direkt auf HTML5-Element\n\nWenn die Shadow-Komponente das fokussierbare HTML5-Element direkt rendert (ohne `-wc`-Zwischenschicht):\n\n```typescript\n@Component({ tag: 'kol-input-text', shadow: true })\nexport class KolInputText implements InputTextAPI, FocusableElement {\n\t@Element() private readonly host?: HTMLKolInputTextElement;\n\tprivate inputRef?: HTMLInputElement;\n\n\tprivate readonly setInputRef = (ref?: HTMLInputElement) => {\n\t\tthis.inputRef = ref;\n\t};\n\n\t@Method()\n\tpublic async focus(): Promise<void> {\n\t\treturn delegateFocus(this.host!, () => setFocus(this.inputRef!));\n\t}\n}\n```\n\nWeitere Beispiele: `kol-input-email`, `kol-input-number`, `kol-input-file`, `kol-textarea`, `kol-combobox`, `kol-single-select`, `kol-input-radio`\n\n### Regel 2: Light DOM Component (`shadow: false`)\n\n**Nur** `setFocus()` verwenden. Kein `delegateFocus()` nötig, da kein Shadow DOM vorhanden ist und das Theme-System nicht abgewartet werden muss.\n\n```typescript\n@Component({ tag: 'kol-button-wc', shadow: false })\nexport class KolButtonWc implements ButtonAPI, FocusableElement {\n\tprivate buttonRef?: HTMLButtonElement;\n\n\tprivate readonly setButtonRef = (ref?: HTMLButtonElement) => {\n\t\tthis.buttonRef = ref;\n\t};\n\n\t@Method()\n\tpublic async focus(): Promise<void> {\n\t\treturn setFocus(this.buttonRef!);\n\t}\n\n\tpublic render(): JSX.Element {\n\t\treturn (\n\t\t\t<Host>\n\t\t\t\t<button ref={this.setButtonRef}>\n\t\t\t\t\t{/* content */}\n\t\t\t\t</button>\n\t\t\t</Host>\n\t\t);\n\t}\n}\n```\n\n### Zusammenfassung der Regeln\n\n| Komponente | `shadow` | Focus-Methode |\n| ------------------- | -------- | ------------------------------------------------ |\n| Shadow Component | `true` | `delegateFocus(this.host!, () => setFocus(ref))` |\n| Light DOM Component | `false` | `setFocus(ref)` |\n\n**Wichtig:**\n\n- `delegateFocus` darf **nur** in `shadow: true`-Komponenten verwendet werden\n- `setFocus` wird **in beiden Fällen** als eigentliche Focus-Funktion genutzt\n- Ref-Callbacks speichern die Referenz zum Ziel-Element (`ref={this.setXxxRef}`)\n- Alle `focus()`-Methoden sind `async` und geben `Promise<void>` zurück\n- Die `@Method()`-Dekorator macht die Methode auf dem Custom Element aufrufbar\n",
368
+ "code": "# Focus Propagation Concept\n\nBeschreibt, wie der Focus von einer KoliBri-Komponente an das fokussierbare HTML5-Element in ihrem Shadow DOM delegiert wird.\n\n## Überblick\n\nKoliBri Web Components verwenden Shadow DOM für Style-Isolation. Ein `focus()` auf dem Host-Element fokussiert nicht automatisch das innere interaktive Element, deshalb leitet die Komponente den Focus programmatisch an das tatsächlich fokussierbare HTML5-Element weiter.\n\nDie zentrale Herausforderung: Bevor der Focus gesetzt werden kann, müssen die Adopted Style Sheets geladen und angewendet sein. Ohne diese Absicherung kann es zu Race Conditions kommen — der Focus wird auf ein Element gesetzt, das noch nicht vollständig gerendert ist.\n\nTechnisch kann ein früher `focus()` im Browser teilweise trotzdem funktionieren. Das ist jedoch nicht authentisch zum realen Nutzerverhalten: Interaktion soll erst auf final sichtbaren und stabil gerenderten Controls stattfinden. Für Tests bedeutet das: Kein Focus auf potenziell noch unsichtbare oder semantisch unvollständige Elemente. Deshalb wird `data-themed` als verbindliche Readiness-Bedingung verwendet.\n\n## Architektur\n\nDie Functional Component einer Komponente rendert das fokussierbare HTML5-Element direkt in den Shadow DOM der Komponente. Die Delegation hat deshalb genau eine Stufe:\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Shadow Component: kol-button (shadow: true) │\n│ @Method() @delegateFocus('ctaRef') focus() │\n└─────────────────────────┬───────────────────────────────┘\n ↓ wartet auf data-themed, dann setFocus(ctaRef.el)\n┌─────────────────────────────────────────────────────────┐\n│ HTML5 Element: <button class=\"kol-button__interactive-element\"> │\n│ Tatsächlich fokussierbar │\n└─────────────────────────────────────────────────────────┘\n```\n\nDasselbe gilt für alle anderen interaktiven Komponenten, z. B. `kol-link` (`<a>`), `kol-input-text` (`<input>`) oder `kol-select` (`<select>`).\n\n### Eingebettete Komponenten\n\nRendert eine Komponente eine andere Komponente in ihrem Shadow DOM (z. B. die Sortier-Buttons von `kol-table-stateless` oder die Buttons von `kol-pagination`), dann rendert sie deren Functional Component über ein Item (`createButtonItem`, `createLinkItem`, …, siehe [ARC42 – Embedded Components (Items)](../packages/components/src/components/_skeleton/ARC42.md#embedded-components-items)). Ein eingebetteter Button ist kein eigenes Element und hat kein eigenes `focus()`. Der Browser fokussiert sein natives `<button>` direkt, per Tab-Navigation oder Klick. Das `focus()` der einbettenden Komponente zielt auf deren eigenes primäres Element.\n\n### Das `data-themed`-Attribut\n\nDas Theme-System setzt das `data-themed`-Attribut auf Shadow-Komponenten, sobald die Adopted Style Sheets geladen sind. Der Ablauf:\n\n1. **Theme-Registrierung:** Beim Bootstrapping werden Themes über `register()` aus dem `adopted-style-sheets`-Paket registriert (`packages/components/src/core/bootstrap.ts`).\n\n2. **Style-Anwendung:** Stencils `setMode()`-Callback wird für jede Komponente aufgerufen (`packages/components/src/global/script.ts`). Dort wird `setThemeStyle(elm, getThemeDetails(elm))` aufgerufen, um die Adopted Style Sheets in den Shadow DOM zu injizieren.\n\n3. **Markierung:** `setThemeStyle()` setzt nach erfolgreicher Style-Anwendung das `data-themed`-Attribut auf dem Host-Element.\n\n## Utility Functions\n\n### `delegateFocus(host, callback)` — `packages/components/src/utils/element-focus.ts`\n\nZentrale Focus-Delegations-Funktion für Shadow Components. Wartet auf die Theme-Bereitschaft des Host-Elements und führt dann die Focus-Callback-Funktion aus.\n\n```typescript\nexport async function delegateFocus(host: HTMLElement, callback: () => Promise<void>): Promise<void> {\n\ttry {\n\t\tif (!host.hasAttribute('data-themed')) {\n\t\t\tawait waitForThemed(host);\n\t\t}\n\t\tawait callback();\n\t} catch {\n\t\tthrow new Error(\n\t\t\t`The interactive element inside the KoliBri web component could not be focused. Try calling the focus method on the web component after a short delay again.`,\n\t\t);\n\t}\n}\n```\n\n**Parameter:**\n\n- `host` — Das Host-Element der Komponente\n- `callback` — Async Funktion, die `setFocus()` auf dem Ziel-Element aufruft\n\n**Verhalten:**\n\n1. Prüft, ob `data-themed` bereits gesetzt ist\n2. Falls nicht: wartet über `waitForThemed()` (MutationObserver) bis maximal 5 Sekunden\n3. Ruft dann den `callback` auf, der den eigentlichen Focus setzt\n4. Bei Fehler (Timeout oder Focus-Fehler): wirft einen benutzerfreundlichen Fehler\n\nDie Wartephase ist nicht nur technisch motiviert, sondern auch eine Qualitätsgrenze: Sie verhindert, dass Focus-Interaktionen in Tests oder in schneller Initialisierung auf UI-Zuständen stattfinden, die ein Nutzer so noch nicht sieht.\n\n### `setFocus(element, options?)` — `packages/components/src/utils/element-focus.ts`\n\nFokussiert ein HTML-Element durch wiederholte Versuche pro Animation Frame (vereinfacht dargestellt):\n\n```typescript\nexport async function setFocus(element: HTMLElement): Promise<void> {\n\tlet attempts = 0;\n\tdo {\n\t\tif (element) {\n\t\t\telement.focus({ preventScroll: true });\n\t\t}\n\t\tawait new Promise((r) => requestAnimationFrame(r));\n\t\tattempts++;\n\t} while (!isActiveElement(element) && attempts < MAX_FOCUS_ATTEMPTS);\n}\n```\n\n**Verhalten:**\n\n- Ruft `element.focus()` auf und prüft pro Animation Frame, ob das Element fokussiert ist\n- Maximal `MAX_FOCUS_ATTEMPTS` (10) Versuche\n- Nutzt `isActiveElement()` für korrekte Focus-Erkennung innerhalb von Shadow DOMs\n- Der Browser scrollt beim Fokussieren nicht (`preventScroll`). Enthalten die `KolFocusOptions` `behavior`, `block` oder `inline`, scrollt `setFocus` das Element danach selbst per `scrollIntoView`.\n- `afterFocus` wird aufgerufen, sobald das Element fokussiert ist, bei `behavior: 'smooth'` erst nach dem Ende des Scrollens\n\n### `isActiveElement(element)` (intern)\n\nPrüft, ob ein Element aktuell fokussiert ist. Berücksichtigt dabei korrekt die Shadow-DOM-Grenze.\n\n```typescript\nfunction isActiveElement(element: HTMLElement): boolean {\n\tconst root = element.getRootNode();\n\tif (root instanceof ShadowRoot) {\n\t\treturn root.activeElement === element;\n\t}\n\treturn document.activeElement === element;\n}\n```\n\n**Warum nötig:** `document.activeElement` zeigt bei fokussierten Elementen innerhalb eines Shadow DOM nur den Shadow Host, nicht das tatsächlich fokussierte Element. Daher wird `shadowRoot.activeElement` geprüft.\n\n### `waitForThemed(host)` — `packages/components/src/utils/element-themed.ts`\n\nWartet per MutationObserver darauf, dass das `data-themed`-Attribut auf dem Host-Element gesetzt wird.\n\n**Verhalten:**\n\n- Beobachtet Attribut-Änderungen auf dem Host-Element\n- Resolved sofort, wenn `data-themed` bereits gesetzt ist oder gesetzt wird\n- Timeout nach `MAX_TIMEOUT_DURATION` (5000 ms) mit Error\n\n## Umsetzung in Komponenten\n\n### Interface\n\nAlle fokussierbaren Komponenten implementieren das `FocusableElement`-Interface (`packages/components/src/schema/interfaces/FocusableElement.ts`):\n\n```typescript\nexport interface FocusableElement {\n\tfocus(options?: KolFocusOptions): Promise<void>;\n}\n```\n\n### Ref und Decorator\n\nDas Ziel-Element hält ein `CtaRef` aus `createCtaRef()`; die öffentliche Methode entsteht über einen Method-Decorator aus `packages/components/src/utils/element-interaction.ts`:\n\n```typescript\n@Component({ tag: 'kol-button', shadow: true })\nexport class KolButton extends BaseButtonWebComponent implements ButtonProps, FocusableElement {\n\t@Element() protected readonly host?: HTMLKolButtonElement;\n\n\t// In der Basisklasse: protected readonly ctaRef = createCtaRef<HTMLButtonElement>();\n\t// Die Functional Component setzt ihn am nativen Element: <button ref={ctaRef} …>\n\n\t@Method()\n\t@delegateFocus('ctaRef')\n\tpublic async focus(options?: KolFocusOptions): Promise<void> {}\n}\n```\n\n`createCtaRef(isInactive?)` nimmt optional ein Prädikat entgegen. Solange es `true` liefert, liest sich `ctaRef.el` als `undefined` und `focus()` tut nichts. So bleibt ein deaktiviertes Element ohne natives `disabled` (z. B. `<a>`) für die öffentliche Methode unerreichbar.\n\n### Die Decorators\n\n| Decorator | Verhalten |\n| ----------------------- | ---------------------------------------------------------------------------------------------------- |\n| `@delegateFocus('ref')` | Wartet über `delegateFocus` auf `data-themed` und fokussiert dann `ref.el` mit `setFocus`. Standard. |\n| `@directFocus('ref')` | Fokussiert `ref.el` mit `setFocus`, ohne auf `data-themed` zu warten (heute nur `kol-tree-item`). |\n\n**Wichtig:**\n\n- Der Decorator ersetzt den Methodenrumpf; die Methode selbst bleibt leer.\n- `setFocus` ist in beiden Fällen die eigentliche Focus-Funktion.\n- Alle `focus()`-Methoden sind `async` und geben `Promise<void>` zurück.\n- Der `@Method()`-Decorator macht die Methode auf dem Custom Element aufrufbar.\n",
369
369
  "kind": "doc"
370
370
  },
371
371
  {
@@ -373,7 +373,7 @@
373
373
  "group": "docs",
374
374
  "name": "FORM_FIELD_SKELETON_MIGRATION_PLAN",
375
375
  "path": "docs/FORM_FIELD_SKELETON_MIGRATION_PLAN.md",
376
- "code": "# Migrationsplan: Formularfelder auf Skeleton\n\nÜbergreifender Plan für die Skeleton-Migration der 14 Formularfelder von `kol-combobox` bis `kol-textarea` (Epic #9559). Er schneidet die Felder in Gruppen, legt das gemeinsame Fundament fest und ordnet die Arbeit in parallele Spuren. Jede Gruppe erhält vor der Umsetzung einen eigenen Detailplan.\n\nStand: 27.09.2026, `@public-ui/components` 4.5.0-rc.0.\n\n## Ausgangslage\n\nKeines der 14 Felder ist bisher migriert. Alle laufen noch auf dem Legacy-Stack:\n\n- Controller-Kette `AssociatedInputController → ControlledInputController → InputController → InputIconController → Feld-Controller`\n- State-Wrapper in `functional-component-wrappers/`, die die alten FCs in `functional-components/` rendern\n- Validatoren aus `schema/props/`\n\nAus dem Formularumfeld ist bisher nur `kol-form` auf Skeleton umgestellt.\n\n| Feld | LOC `shadow.tsx` | Render-Stack heute | Besonderheit |\n| -------------------- | --------------------------- | ------------------------------------------- | --------------------------------------------------------------- |\n| `kol-input-color` | 380 | FormField + InputContainer + Input („Trio\") | dünnstes Feld, kein `_required` |\n| `kol-input-email` | 489 | Trio | Controller erbt von Text → Password, Counter |\n| `kol-input-password` | 511 | Trio + IconButton (`KolButtonWcTag`) | `_visibilityToggle`, Basisklasse für Text und Email |\n| `kol-input-text` | 620 | Trio + Clear-Button | `_type`, Selection-API-Methoden |\n| `kol-textarea` | 498 | FormField + InputContainer + TextArea | `_adjustHeight`, `_resize`, `_rows`, Counter |\n| `kol-input-number` | 551 | Trio + Step-Buttons | Feature-Flag `inputNumberButtons`, genutzt von `table-settings` |\n| `kol-input-range` | 499 | 2× Input (range + number) + Suggestions | min/max/step, zwei synchrone Inputs |\n| `kol-input-date` | 513 (+ 219 Controller) | Trio | Date↔ISO, 5 `_type`s, `reset()` |\n| `kol-input-file` | 450 | Trio + Browse-Button (`KolButtonWcTag`) | kein `_value`, FileList, `_accept`, Drag & Drop, `reset()` |\n| `kol-input-checkbox` | 457 | FormField + FieldControl + Checkbox | Varianten default/switch/button, genutzt von `table-settings` |\n| `kol-input-radio` | 458 | FormField (fieldset) + FieldControl + Radio | `_options`, `_orientation`, alte Utils `element-click`/`-focus` |\n| `kol-select` | 222 + 404 (`kol-select-wc`) | FormField + InputContainer + NativeSelect | `pagination` rendert `kol-select-wc` direkt |\n| `kol-combobox` | 756 | Trio + CustomSuggestions + `@Listen` | Listbox, nahezu Zwilling von single-select |\n| `kol-single-select` | 849 | Trio + CustomSuggestions + `@Listen` | größtes Feld, offene Bugs #10501/#10617 |\n\n## Rahmenbedingungen\n\n- **Zero visual delta, BEM bleibt.** DOM und Klassen (`.kol-form-field`, `.kol-input-container`, `.kol-field-control` …) bleiben byte-identisch. Themes werden nicht angefasst, es gibt keinen CLI-/SCSS-Migrations-Task. Das Pixel-Gate verlangt 0 Diffs pro Theme.\n- **Formular-Anbindung 1:1 als Behavior.** Die heutige Semantik (versteckte native Elemente im Light DOM, `_syncValueBySelector`, `ariaDetails` über `attachInternals`) wandert aus der Controller-Kette in ein wiederverwendbares `FormAssociationBehavior`, das auch `kol-button` nutzt. Einen Umstieg auf natives `formAssociated` gibt es in dieser Migration nicht.\n- **Identische öffentliche API.** Gleiche `@Prop`/`@Method`-Member, Alias-Typen, Defaults und JSDoc. Die Oberfläche ist vor der Migration in `_skeleton/public-api/<komponente>.spec.ts` festgenagelt.\n\n## Leitplanken\n\n- **Strangler statt Big Bang.** Der neue Stack entsteht unter `internal/functional-components/form-field/` neben dem alten. Ein Legacy-Modul wird in dem PR gelöscht, der seinen letzten Import entfernt.\n- **Keine Bugfixes in Migrations-PRs.** Bekannte Fehler (z. B. #10501, #10617) bekommen eigene PRs, vor oder nach der Migration.\n- **Innere Tags bleiben.** `KolButtonWcTag` (IconButton, Clear-, Browse- und Visibility-Button) und `KolPopoverButtonWcTag` (Label-`_infoPopover`) bleiben wegen Zero-Delta stehen. Das ist eine begründete Ausnahme nach Fallstrick 8 im Skill `migrate-to-skeleton`.\n- **Grenze von DD16.** Stencil liest Decorators nur aus der konkreten Klasse. Alle `@Prop`/`@Watch`/`@Method` samt JSDoc bleiben in jeder Komponente, Basisklassen bündeln nur Logik. Höchstens zwei Ebenen unter `BaseWebComponent`.\n- **Basisklasse nach G1 eingefroren.** Änderungen an `BaseFormFieldWebComponent` kommen nur als eigenes PR, mit Pixel-Gate über alle bereits migrierten Felder.\n- **Konfliktstellen.** `internal/props/index.ts`, `schema/bem-registry.ts` und die Pin-Dateien: nur anhängen, Reihenfolge zwischen den Spuren absprechen.\n\n### Abnahme-Gates je Komponenten-PR\n\n1. Der Public-API-Pin ist unverändert und zeigt jetzt auf `component.tsx`. Die Klasse implementiert `*Props`.\n2. Die Jest-`.snap`-Dateien sind unverändert.\n3. `node scripts/snapshots-docker.mjs <theme> --check` meldet 0 Diffs für default, bwst, desy, kern und ecl (ec/eu).\n4. Die Verhaltensverträge aus G0 sind grün.\n5. Die geteilten E2E-Helfer `src/e2e/{input-msg,input-value-reflection,input-character-limit}.ts` sind grün.\n6. Keine Imports mehr aus `@deprecated/input`, `input-adapter-leanup`, `functional-component-wrappers` oder den alten `functional-components/*`.\n7. `form.e2e` ist grün: Ein Klick in der Fehlerliste fokussiert das Feld über seinen `focus()`.\n\n## Reihenfolge und Abhängigkeiten\n\n```\nG0 ─► G1.1 ─┬─► G1.2 ──┐\n └─► G1.3 ──┴─► G1.4 (Pilot input-color)\n ├─► G2 Textfelder (Spur A)\n ├─► G3 a | b | c (Spur B)\n └─► G4-Vorbereitung ─► G4 ─► G5 (Spur C)\nRückbau-Inkremente am Ende jeder Gruppe; finaler Rückbau (G6) nach G2–G5\n```\n\n## Gruppen\n\n### G0 – Absicherung\n\nKein Produktivcode, Voraussetzung für alles. Die Gates greifen nur, wenn vorher festgehalten ist, was „unverändert\" heißt: API, Verhalten und Pixel.\n\n- ✅ Public-API-Pins für alle 14 Tags und `kol-select-wc` (als interner Vertrag), zunächst gegen `shadow.tsx`. Jede Komponente hat eine eigene Datei unter `_skeleton/public-api/`, damit parallele Spuren keine Merge-Konflikte bekommen.\n- ✅ Verhaltensverträge pro Feld: Reihenfolge und Payload von `kolChange`/`kolInput`/`kolFocus`/`kolBlur`/`kolKeydown`/`kolClick` und der `_on`-Callbacks; `FormData` eines nativen `<form>` für Checkbox, `select` multiple, File und Radio mit Objektwerten; `_syncValueBySelector`; `_touched` nach Blur. Die Verträge sind Playwright-Tests (`*.e2e.ts`), weil versteckte Light-DOM-Elemente, `attachInternals`, `FormData` sowie Fokus und Tastatur nur im echten Browser verlässlich prüfbar sind. Sie nutzen die Helfer aus `src/e2e/`. Reine Umrechnungen ohne Browserverhalten (z. B. Date↔ISO in G3) werden als Jest-Tests der Hilfsfunktionen geprüft.\n - Der gemeinsame Helfer `testInputBehaviorContract` in `src/e2e/input-behavior-contract.ts` zeichnet Events und Callbacks auf; jedes Feld pinnt darin sein heutiges Verhalten. Alle 14 Felder sind gepinnt, dazu die Varianten Radio mit Objektwerten und `kol-select` mit `_multiple` (Option `variant`).\n - Befunde aus den Verträgen: Die gepinnten Abläufe beschreiben den heutigen Stand, auch wo er inkonsistent ist. Korrekturen kommen als eigene PRs nach der jeweiligen Migration.\n - Standard (G1.4, alle Felder außer den unten genannten Abweichungen): Je Aktion kommt zuerst das KoliBri-Event am Host, dann der `_on`-Callback, dann das native Event. Natives `input`/`change` endet an der Shadow-Grenze.\n - Startwert ohne Vorgabe: `kol-input-color` `#000000` (aus dem inneren Input in `componentDidLoad`), `kol-input-range` `50`, `kol-input-checkbox` `true`, `kol-input-radio` und `kol-single-select` `null`, alle anderen `undefined`.\n - G3: Payloads sind typisiert – number und range liefern Zahlen, date den ISO-String, file eine `FileList`. Für file bleibt ein Text-Input als `_syncValueBySelector`-Ziel leer.\n - G4: checkbox und radio senden kein KoliBri-`click` und keinen `onClick` (`onClick: undefined` in `shadow.tsx`). Stattdessen erreicht das native `click` den Host vor `input`/`change`.\n - G5: `kol-single-select` sendet beim Blur zuerst das native Event, dann das KoliBri-Event und den Callback. `kol-select` setzt `_touched` am eigenen Host nicht, das innere `kol-select-wc` hält den Zustand. Außerdem überträgt `kol-select` einen Einzelwert weder in `FormData` noch in das `_syncValueBySelector`-Ziel, weil das versteckte `<select multiple>` nur Array-Werte übernimmt. Mit `_multiple` stehen alle gewählten Werte in `FormData`. Issues: #11014 (`_touched`), #11015 (Einzelwert).\n - Radio mit Objektwerten liefert das Objekt als Payload und überträgt es als JSON-String in `FormData` und an `_syncValueBySelector`.\n - Formular-Anbindung heute: Das versteckte Element im Light DOM entsteht nur mit `register(…, { reflectInputValues: true })`, seinen `name` setzt der Controller nur im Experimental-Mode. Nur mit beidem steht das Feld in `FormData`. Der Vertrag pinnt alle drei Fälle, der Test ruft `register` dafür aus dem Build unter Test auf.\n- ✅ Visual-Samples ergänzen für msg, hint, disabled, hideLabel, infoPopover, Counter, Icons, `inputNumberButtons` an/aus und die Checkbox-Varianten. Neue Samples erzeugen neue Baselines und gehören in ein eigenes PR vor der ersten Migration.\n - Vorhanden: msg, hint, disabled, hideLabel, infoPopover und Icons für alle 14 Felder über `partials/cases.tsx`/`variants.tsx`, Counter bei `input-text` und `textarea`, Checkbox-Varianten `basic`/`button`/`switch`.\n - `inputNumberButtons` setzt das Theme (`theme-default` `'show'`, `theme-kern` `'hide'`); die bestehenden `input-number`-Baselines decken damit beide Zustände ab.\n - Ergänzt in #11017: Counter-Routen für `kol-input-email` und `kol-input-password`, Hint-Block für `kol-input-number`.\n- ✅ Toten Code löschen: `functional-components/inputs/Combobox/Combobox.tsx`.\n\n### G1 – Fundament + Pilot `kol-input-color` (#9673, #9577)\n\nAlle 14 Felder teilen Basis-Props, die Label/Hint/Msg-Hülle, die Formular-Anbindung und die Event-Logik. Das wird einmal gebaut und am dünnsten Feld geprüft, damit die Basisklasse an einem echten Fall entsteht. G1 besteht aus sechs PRs:\n\n```\nG1.0 Testlücken ───────────────┬──────────────────────────────────────────────┐\nG1.1 Props ─┬─► G1.2 FormAssociationBehavior (+ kol-button) ─► SSR-Fix (eigener PR)\n │ (braucht G1.0 und G1.1) │\n └─► G1.3a FormField ─► G1.3b Container/Input ──────────────────────┴─► G1.4 Basis + kol-input-color\n```\n\n0. **Testlücken** (nur Tests):\n - Formular-Anbindung von `kol-button` gepinnt. Es entsteht kein verstecktes Formularelement, weil der Adapter in `button/base.tsx` `_name` bei der Konstruktion nicht kennt (#11036); `_syncValueBySelector` wirkt im Experimental-Mode.\n - Gate 7 mit einem `kol-input-color` als Ziel der Fehlerliste.\n - Tooltip bei `_hideLabel` für `kol-input-color`. Befund: Hat das Feld den Fokus, schließt Escape den Tooltip nicht. Das Feld sendet sein eigenes `keydown`-CustomEvent am Host, das vor der nativen Taste am Dokument ankommt und den einmaligen Escape-Listener des Tooltips verbraucht (#11032, verwandt #11033).\n - Jest-Snapshots aller Felder zusätzlich mit `_hideLabel`, `_infoPopover`, sichtbarer Msg und `_variant`.\n1. **Basis-Props** in `internal/props/`: ariaDetails, autoComplete, hideMsg, hint, `horizontalIconsProp` (Icons-Objekt der Inputs, `icons.ts` bleibt unverändert), infoPopover, `inputCallbacksProp` (`_on`), msg, readOnly, required, suggestions, syncValueBySelector (nur für das Behavior), touched und der String-Wert. `tooltipAlignProp` wird wiederverwendet, der Feld-Default `'top'` im `apply` neu gesetzt. Der veröffentlichte Typ `FormFieldLabelInfoPopoverProps` zieht in einem Schritt für alle Felder nach `schema/props/`; ein teilweiser Umzug würde in `components.d.ts` einen Alias `…1` erzeugen.\n2. **`FormAssociationBehavior`**: übernimmt `input-adapter-leanup/associated.controller.ts` 1:1, einschließlich der späten Host-Zuweisung im Konstruktor. `AssociatedInputController` wird zur Fassade über das Behavior, damit die G0-Verträge es sofort für alle Felder prüfen. `button/base.tsx` wird im selben PR umgestellt; der Button-Pin bleibt unverändert.\n3. **Shell-FCs** unter `internal/functional-components/form-field/`, nur für die Hülle von `kol-input-color`: G1.3a FormField (Label, Hint, Msg, Counter, Tooltip, Zeichengrenzen-Hinweis) mit dem ARIA-Helfer aus `getRenderStates`, G1.3b InputContainer mit Adornments, IconButton, Input und Suggestions. Neue Blöcke in `schema/bem-registry.ts`. Die alten FCs werden Adapter auf die neuen, damit Hydrate-Snapshot und Pixel-Gate die Hülle sofort über alle Felder prüfen. TextArea kommt in G2, FieldControl/Checkbox/Radio in G4, NativeSelect/Option(List)/CustomSuggestions in G5. Die `fieldset`-Wurzel von radio bleibt bis G4 auf dem alten Pfad, weil `BemRootNodeFC` nur `div` rendert.\n4. **`BaseFormFieldWebComponent`** (DD16): `apply*` der Basis-Props, ein eigenes `TooltipBehavior` pro Instanz statt der modulglobalen Map in `FormField.tsx`, Formular-Anbindung, Event-Behandlung als Ersatz für `InputController` (Reihenfolge: KoliBri-Event, dann Callback) und Render-Helfer. Damit wird `kol-input-color` migriert.\n\nEntscheidungen:\n\n- Die Basis-Config enthält nur Props, die alle 14 Felder identisch haben: ariaDetails, disabled, hideLabel, hideMsg, hint, infoPopover, label, msg, name, on, tooltipAlign, touched. `WebComponentInterface` verlangt für jede Config-Prop einen Watcher.\n- Die Basisklasse ist generisch über die Feld-API. TypeScript löst `ResolvedProps<Api>` für ein generisches `Api` nicht auf, deshalb greift sie über einen einzigen, dokumentierten Self-Cast auf die Basis-Props zu.\n- Bewusste Abweichungen der Prop-Factory, wie in allen bisherigen Skeleton-Migrationen: `undefined` führt zum Default statt den alten Wert zu behalten, `normalizeBoolean` akzeptiert `'true'`/`'false'`, ungültige Werte werden verworfen.\n- `data-testid=\"input-counter\"`/`\"input-counter-aria\"` bleiben bis G2, weil `e2e/input-character-limit.ts` sie braucht und der DOM byte-gleich bleiben muss.\n\nRisiken: Zusammenbau von `aria-describedby` (hint, msg, Counter, Hinweis zur Zeichengrenze; heutige Eigenheiten in #11035); Attribut- und Klassenreihenfolge im Hydrate-Snapshot; die beiden Fokus-Flags aus Controller und Komponente; `attachInternals(undefined)` bei SSR/Hydrate.\n\n### G2 – Textfelder: email → password → text, danach textarea (#9579, #9582, #9585, #9602)\n\nDie vier Felder hängen heute in der Kette `InputPasswordController → InputTextEmailController → InputText/InputEmail` und teilen placeholder, autoComplete, pattern, maxLength/hasCounter/maxLengthBehavior, spellCheck und den Counter.\n\n- `BaseTextInputWebComponent` für email, password und text.\n- `CounterBehavior` ersetzt `utils/counter-dom-updater.ts`; das direkte DOM-Update ohne Re-Render bleibt erhalten.\n- textarea erbt direkt von `BaseFormFieldWebComponent` und nutzt das CounterBehavior.\n- Risiken: Selection-API und `_type` search/url/tel von input-text; Clear-Button und Visibility-Toggle; Datalist-IDs; `_adjustHeight`/`_resize` bei textarea.\n- Zusätzliche Abnahme: `input-character-limit` und `input-text.clear-button.e2e` grün; alle 16 React-Routen von input-text pixelgleich.\n\n### G3 – Wertetypen (#9581, #9584, #9578, #9580)\n\nDrei parallele Spuren: a) number → range, b) date, c) file. Die Felder nutzen die Standard-Hülle, müssen aber ihren Wert umwandeln.\n\n- Eigene Input-Props für min/max/step; die vorhandenen Meter-Props (Defaults 0/100) passen nicht.\n- a) Step-Buttons und Feature-Flag `inputNumberButtons`; range mit zwei synchronen Inputs und Suggestions.\n- b) Date↔ISO-Logik aus `input-date/controller.ts` in Hilfsfunktionen, alle 5 `_type`s, Zeitzonen.\n- c) FileList, Drag-&-Drop-Modifier, Browse-Button, übertragener Formularwert.\n- Zusätzliche Abnahme: `table-settings` pixelgleich; `reset()` bei date und file; Flag an und aus im Pixel-Gate.\n\n### G4 – Auswahl-Controls: checkbox → radio (#9576, #9583)\n\nNur diese beiden nutzen den FieldControl-Stack statt InputContainer und teilen `InputCheckboxRadioController`.\n\n- Vorbereitungs-PR: `options`-Prop und `fillKeyOptionMap` ziehen aus `input-radio/controller.ts` nach `form-field/options.ts`; G5 braucht beides ebenfalls.\n- FieldControlFC einführen, `InputCheckboxRadioController` auflösen.\n- Risiken: Theme-SCSS der Checkbox (300 LOC, verschachtelte Varianten); `indeterminate` nur als Property; Formularwert bei checked/unchecked; Objektwerte, Tastatur- und Fokus-Navigation bei Radio.\n- Zusätzliche Abnahme: alle drei Checkbox-Varianten je Theme; Tastatur-E2E für Radio; `table-settings` pixelgleich.\n\n### G5 – Listen: select → combobox + single-select (#9594, #9569, #9595)\n\n- select: `BaseSelectWebComponent` für `kol-select` und das Übergangs-Tag `kol-select-wc` (DD16, Übergangsmuster `shadow: false`).\n- combobox und single-select: gemeinsame Basis oder ein `ListboxBehavior` für Tastatur, open/close und focusin/out. `@Listen`-Handler folgen der Event-Handler-Policy.\n- Risiken: Options-Sync des versteckten `<select multiple>`; `pagination` hängt am Light DOM von `kol-select-wc`; `:has()`-Selektoren im CustomSuggestions-SCSS.\n- Zusätzliche Abnahme: `pagination` pixelgleich; Tastatur- und Maus-E2E für combobox und single-select; bekannte Bugs bestehen nachweislich unverändert weiter oder wurden vorher gefixt.\n\n### G6 – Rückbau\n\nGelöscht wird, sobald der letzte Import weg ist. Veröffentlichte Schema-Typen bleiben stehen.\n\n| Zeitpunkt | Was gelöscht wird |\n| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| nach G2 | `utils/counter-dom-updater.ts`, `InputPasswordController`, `InputTextEmailController`, `TextAreaStateWrapper`, alte `inputs/TextArea` |\n| nach G4 | Checkbox-/Radio-Controller inkl. `InputCheckboxRadioController`, `Checkbox`-/`RadioStateWrapper`, altes `FieldControl`, alte `inputs/Checkbox`/`inputs/Radio`, alte Radio-Utils |\n| nach G5 | `SelectStateWrapper`, `NativeSelect`/`NativeOption(List)`, `CustomSuggestions*`, `Suggestions`; `kol-select-wc` erst, wenn `pagination` das FC rendert |\n| final | `@deprecated/input/*`, `input-adapter-leanup/`, `functional-component-wrappers/` inkl. `getRenderStates`, Adapter aus G1.3, alte FormField-/Input-FCs, `*Watches`-Interfaces |\n\n### G7 – Folge-Epics (außerhalb dieser Migration)\n\n- `KolButtonWcTag`/`KolPopoverButtonWcTag` durch `ButtonFC`/`PopoverButtonFC` ersetzen (erfordert Theme-Änderungen).\n- Umstieg auf natives `formAssociated` mit ElementInternals (Verhaltensänderung, eventuell Breaking).\n- BEM-Bereinigung mit CLI-Tasks `ScssRename*` und Migrationsguide für Custom-Themes.\n\n## Offene Fragen für die Detailpläne\n\n1. ~~Sind die Shell-Adapter (alte FC → neue FC) als Zwischenschicht akzeptabel? (G1.3)~~ Entschieden: ja, siehe G1.3.\n2. ~~ID-Strategie: bisherige Erzeugung für Snapshot-Parität oder DD12 `createUniqueId`? (G1.4)~~ Kein Konflikt: Legacy nutzt bereits `createUniqueId` und `createRelatedUniqueId`. Die Basis-ID wird einmal pro Instanz als `@State()` erzeugt.\n3. ~~`_touched` als `@State` oder als Render-Prop? (G1.4)~~ Entschieden: Render-Prop. `_touched` bleibt `@Prop({ mutable: true, reflect: true })`, der Blur-Handler schreibt die Prop (Vorbild `_open` in `kol-details`).\n4. ~~`_on`: eine gemeinsame Callback-Prop oder typisiert pro Feld? (G1.1)~~ Entschieden: gemeinsam, alle Felder deklarieren `InputTypeOnDefault`.\n5. ~~SSR-Absturz von `attachInternals(undefined)` 1:1 übernehmen oder mit eigenem PR über einen Guard absichern? (G1.2)~~ Entschieden: 1:1 übernehmen, Fix als eigener PR nach G1.2 (#11034). Ursache ist nicht ein leeres `@Element()`: In mock-doc greift `instanceof Element` in `findHostWithShadowRoot` nicht. Betroffen sind im SSR die inneren `kol-button-wc`/`kol-popover-button-wc` und mit `serializeShadowRoot: 'scoped'` alle Legacy-Felder.\n6. ~~Verhaltensverträge in Jest oder in Playwright? (G0)~~ Entschieden: Playwright, siehe G0.\n7. Namen der Input-Props für min/max/step. (G3) Das Icons-Objekt heißt `horizontalIconsProp` (G1.1).\n8. `kol-select-wc` als Übergangs-Tag behalten oder `pagination` direkt auf das FC umstellen? (G5)\n9. combobox und single-select: gemeinsame DD16-Basis oder `ListboxBehavior`? (G5)\n10. #10501 und #10617 vor oder nach der Migration fixen? (G5)\n\n## Grundlagen\n\n- `packages/components/src/components/_skeleton/ARC42.md`\n- Skill `.claude/skills/migrate-to-skeleton/` (inkl. `reference/patterns.md`, `reference/pitfalls.md`)\n- Skill `.claude/skills/zero-visual-delta-handoff/`\n",
376
+ "code": "# Migrationsplan: Formularfelder auf Skeleton\n\nÜbergreifender Plan für die Skeleton-Migration der 14 Formularfelder von `kol-combobox` bis `kol-textarea` (Epic #9559). Er schneidet die Felder in Gruppen, legt das gemeinsame Fundament fest und ordnet die Arbeit in parallele Spuren. Jede Gruppe erhält vor der Umsetzung einen eigenen Detailplan.\n\nStand: 27.09.2026, `@public-ui/components` 4.5.0-rc.0.\n\n## Ausgangslage\n\nVor der Migration lief keines der 14 Felder auf Skeleton. Der Legacy-Stack besteht aus:\n\n- Controller-Kette `AssociatedInputController → ControlledInputController → InputController → InputIconController → Feld-Controller`\n- State-Wrapper in `functional-component-wrappers/`, die die alten FCs in `functional-components/` rendern\n- Validatoren aus `schema/props/`\n\nAus dem Formularumfeld ist bisher nur `kol-form` auf Skeleton umgestellt.\n\n| Feld | LOC `shadow.tsx` | Render-Stack heute | Besonderheit |\n| -------------------- | --------------------------- | ------------------------------------------- | --------------------------------------------------------------- |\n| `kol-input-color` | 380 | FormField + InputContainer + Input („Trio\") | dünnstes Feld, kein `_required` |\n| `kol-input-email` | 489 | Trio | Controller erbt von Text → Password, Counter |\n| `kol-input-password` | 511 | Trio + IconButton (`KolButtonWcTag`) | `_visibilityToggle`, Basisklasse für Text und Email |\n| `kol-input-text` | 620 | Trio + Clear-Button | `_type`, Selection-API-Methoden |\n| `kol-textarea` | 498 | FormField + InputContainer + TextArea | `_adjustHeight`, `_resize`, `_rows`, Counter |\n| `kol-input-number` | 551 | Trio + Step-Buttons | Feature-Flag `inputNumberButtons`, genutzt von `table-settings` |\n| `kol-input-range` | 499 | 2× Input (range + number) + Suggestions | min/max/step, zwei synchrone Inputs |\n| `kol-input-date` | 513 (+ 219 Controller) | Trio | Date↔ISO, 5 `_type`s, `reset()` |\n| `kol-input-file` | 450 | Trio + Browse-Button (`KolButtonWcTag`) | kein `_value`, FileList, `_accept`, Drag & Drop, `reset()` |\n| `kol-input-checkbox` | 457 | FormField + FieldControl + Checkbox | Varianten default/switch/button, genutzt von `table-settings` |\n| `kol-input-radio` | 458 | FormField (fieldset) + FieldControl + Radio | `_options`, `_orientation`, alte Utils `element-click`/`-focus` |\n| `kol-select` | 222 + 404 (`kol-select-wc`) | FormField + InputContainer + NativeSelect | `pagination` rendert `kol-select-wc` direkt |\n| `kol-combobox` | 756 | Trio + CustomSuggestions + `@Listen` | Listbox, nahezu Zwilling von single-select |\n| `kol-single-select` | 849 | Trio + CustomSuggestions + `@Listen` | größtes Feld, offene Bugs #10501/#10617 |\n\n## Rahmenbedingungen\n\n- **Zero visual delta, BEM bleibt.** DOM und Klassen (`.kol-form-field`, `.kol-input-container`, `.kol-field-control` …) bleiben byte-identisch. Themes werden nicht angefasst, es gibt keinen CLI-/SCSS-Migrations-Task. Das Pixel-Gate verlangt 0 Diffs pro Theme.\n- **Formular-Anbindung 1:1 als Behavior.** Die heutige Semantik (versteckte native Elemente im Light DOM, `_syncValueBySelector`, `ariaDetails` über `attachInternals`) wandert aus der Controller-Kette in ein wiederverwendbares `FormAssociationBehavior`, das auch `kol-button` nutzt. Einen Umstieg auf natives `formAssociated` gibt es in dieser Migration nicht.\n- **Identische öffentliche API.** Gleiche `@Prop`/`@Method`-Member, Alias-Typen, Defaults und JSDoc. Die Oberfläche ist vor der Migration in `_skeleton/public-api/<komponente>.spec.ts` festgenagelt.\n\n## Leitplanken\n\n- **Strangler statt Big Bang.** Der neue Stack entsteht unter `internal/functional-components/form-field/` neben dem alten. Ein Legacy-Modul wird in dem PR gelöscht, der seinen letzten Import entfernt.\n- **Keine Bugfixes in Migrations-PRs.** Bekannte Fehler (z. B. #10501, #10617) bekommen eigene PRs, vor oder nach der Migration.\n- **Innere Tags bleiben.** `KolButtonWcTag` (IconButton, Clear-, Browse- und Visibility-Button) und `KolPopoverButtonWcTag` (Label-`_infoPopover`) bleiben wegen Zero-Delta stehen. Das ist eine begründete Ausnahme nach Fallstrick 8 im Skill `migrate-to-skeleton`.\n- **Grenze von DD16.** Stencil liest Decorators nur aus der konkreten Klasse. Alle `@Prop`/`@Watch`/`@Method` samt JSDoc bleiben in jeder Komponente, Basisklassen bündeln nur Logik. Höchstens zwei Ebenen unter `BaseWebComponent`.\n- **Basisklasse nach G1 eingefroren.** Änderungen an `BaseFormFieldWebComponent` kommen nur als eigenes PR, mit Pixel-Gate über alle bereits migrierten Felder.\n- **Konfliktstellen.** `internal/props/index.ts`, `schema/bem-registry.ts` und die Pin-Dateien: nur anhängen, Reihenfolge zwischen den Spuren absprechen.\n\n### Abnahme-Gates je Komponenten-PR\n\n1. Der Public-API-Pin ist unverändert und zeigt jetzt auf `component.tsx`. Die Klasse implementiert `*Props`.\n2. Die Jest-`.snap`-Dateien sind unverändert.\n3. `node scripts/snapshots-docker.mjs <theme> --check` meldet 0 Diffs für default, bwst, desy, kern und ecl (ec/eu).\n4. Die Verhaltensverträge aus G0 sind grün.\n5. Die geteilten E2E-Helfer `src/e2e/{input-msg,input-value-reflection,input-character-limit}.ts` sind grün.\n6. Keine Imports mehr aus `@deprecated/input`, `input-adapter-leanup`, `functional-component-wrappers` oder den alten `functional-components/*`.\n7. `form.e2e` ist grün: Ein Klick in der Fehlerliste fokussiert das Feld über seinen `focus()`.\n\n## Reihenfolge und Abhängigkeiten\n\n```\nG0 ─► G1.1 ─┬─► G1.2 ──┐\n └─► G1.3 ──┴─► G1.4 (Pilot input-color)\n ├─► G2 Textfelder (Spur A)\n ├─► G3 a | b | c (Spur B)\n └─► G4-Vorbereitung ─► G4 ─► G5 (Spur C)\nRückbau-Inkremente am Ende jeder Gruppe; finaler Rückbau (G6) nach G2–G5\n```\n\n## Gruppen\n\n### G0 – Absicherung\n\nKein Produktivcode, Voraussetzung für alles. Die Gates greifen nur, wenn vorher festgehalten ist, was „unverändert\" heißt: API, Verhalten und Pixel.\n\n- ✅ Public-API-Pins für alle 14 Tags und `kol-select-wc` (als interner Vertrag), zunächst gegen `shadow.tsx`. Jede Komponente hat eine eigene Datei unter `_skeleton/public-api/`, damit parallele Spuren keine Merge-Konflikte bekommen.\n- ✅ Verhaltensverträge pro Feld: Reihenfolge und Payload von `kolChange`/`kolInput`/`kolFocus`/`kolBlur`/`kolKeydown`/`kolClick` und der `_on`-Callbacks; `FormData` eines nativen `<form>` für Checkbox, `select` multiple, File und Radio mit Objektwerten; `_syncValueBySelector`; `_touched` nach Blur. Die Verträge sind Playwright-Tests (`*.e2e.ts`), weil versteckte Light-DOM-Elemente, `attachInternals`, `FormData` sowie Fokus und Tastatur nur im echten Browser verlässlich prüfbar sind. Sie nutzen die Helfer aus `src/e2e/`. Reine Umrechnungen ohne Browserverhalten (z. B. Date↔ISO in G3) werden als Jest-Tests der Hilfsfunktionen geprüft.\n - Der gemeinsame Helfer `testInputBehaviorContract` in `src/e2e/input-behavior-contract.ts` zeichnet Events und Callbacks auf; jedes Feld pinnt darin sein heutiges Verhalten. Alle 14 Felder sind gepinnt, dazu die Varianten Radio mit Objektwerten und `kol-select` mit `_multiple` (Option `variant`).\n - Befunde aus den Verträgen: Die gepinnten Abläufe beschreiben den heutigen Stand, auch wo er inkonsistent ist. Korrekturen kommen als eigene PRs nach der jeweiligen Migration.\n - Standard (G1.4, alle Felder außer den unten genannten Abweichungen): Je Aktion kommt zuerst das KoliBri-Event am Host, dann der `_on`-Callback, dann das native Event. Natives `input`/`change` endet an der Shadow-Grenze.\n - Startwert ohne Vorgabe: `kol-input-color` `#000000` (aus dem inneren Input in `componentDidLoad`), `kol-input-range` `50`, `kol-input-checkbox` `true`, `kol-input-radio` und `kol-single-select` `null`, alle anderen `undefined`.\n - G3: Payloads sind typisiert – number und range liefern Zahlen, date den ISO-String, file eine `FileList`. Für file bleibt ein Text-Input als `_syncValueBySelector`-Ziel leer.\n - G4: checkbox und radio senden kein KoliBri-`click` und keinen `onClick` (`onClick: undefined` in `shadow.tsx`). Stattdessen erreicht das native `click` den Host vor `input`/`change`.\n - G5: `kol-single-select` sendet beim Blur zuerst das native Event, dann das KoliBri-Event und den Callback. `kol-select` setzt `_touched` am eigenen Host nicht, das innere `kol-select-wc` hält den Zustand. Außerdem überträgt `kol-select` einen Einzelwert weder in `FormData` noch in das `_syncValueBySelector`-Ziel, weil das versteckte `<select multiple>` nur Array-Werte übernimmt. Mit `_multiple` stehen alle gewählten Werte in `FormData`. Issues: #11014 (`_touched`, behoben in G5.2), #11015 (Einzelwert, behoben nach G5.4).\n - Radio mit Objektwerten liefert das Objekt als Payload und überträgt es als JSON-String in `FormData` und an `_syncValueBySelector`.\n - Formular-Anbindung heute: Das versteckte Element im Light DOM entsteht nur mit `register(…, { reflectInputValues: true })`, seinen `name` setzt der Controller nur im Experimental-Mode. Nur mit beidem steht das Feld in `FormData`. Der Vertrag pinnt alle drei Fälle, der Test ruft `register` dafür aus dem Build unter Test auf.\n- ✅ Visual-Samples ergänzen für msg, hint, disabled, hideLabel, infoPopover, Counter, Icons, `inputNumberButtons` an/aus und die Checkbox-Varianten. Neue Samples erzeugen neue Baselines und gehören in ein eigenes PR vor der ersten Migration.\n - Vorhanden: msg, hint, disabled, hideLabel, infoPopover und Icons für alle 14 Felder über `partials/cases.tsx`/`variants.tsx`, Counter bei `input-text` und `textarea`, Checkbox-Varianten `basic`/`button`/`switch`.\n - `inputNumberButtons` setzt das Theme (`theme-default` `'show'`, `theme-kern` `'hide'`); die bestehenden `input-number`-Baselines decken damit beide Zustände ab.\n - Ergänzt in #11017: Counter-Routen für `kol-input-email` und `kol-input-password`, Hint-Block für `kol-input-number`.\n- ✅ Toten Code löschen: `functional-components/inputs/Combobox/Combobox.tsx`.\n\n### G1 – Fundament + Pilot `kol-input-color` (#9673, #9577)\n\nAlle 14 Felder teilen Basis-Props, die Label/Hint/Msg-Hülle, die Formular-Anbindung und die Event-Logik. Das wird einmal gebaut und am dünnsten Feld geprüft, damit die Basisklasse an einem echten Fall entsteht. G1 besteht aus sechs PRs: G1.0 #11037, G1.1 #11038, G1.2 #11044, G1.3a #11045, G1.3b #11046 und G1.4 #11049. Alle sind gemergt.\n\n```\nG1.0 Testlücken ───────────────┬──────────────────────────────────────────────┐\nG1.1 Props ─┬─► G1.2 FormAssociationBehavior (+ kol-button) ─► SSR-Fix (eigener PR)\n │ (braucht G1.0 und G1.1) │\n └─► G1.3a FormField ─► G1.3b Container/Input ──────────────────────┴─► G1.4 Basis + kol-input-color\n```\n\n0. **Testlücken** (nur Tests):\n - Formular-Anbindung von `kol-button` gepinnt. Es entsteht kein verstecktes Formularelement, weil der Adapter in `button/base.tsx` `_name` bei der Konstruktion nicht kennt (#11036); `_syncValueBySelector` wirkt im Experimental-Mode.\n - Gate 7 mit einem `kol-input-color` als Ziel der Fehlerliste.\n - Tooltip bei `_hideLabel` für `kol-input-color`. Befund: Hat das Feld den Fokus, schließt Escape den Tooltip nicht. Das Feld sendet sein eigenes `keydown`-CustomEvent am Host, das vor der nativen Taste am Dokument ankommt und den einmaligen Escape-Listener des Tooltips verbraucht (#11032, verwandt #11033).\n - Jest-Snapshots aller Felder zusätzlich mit `_hideLabel`, `_infoPopover`, sichtbarer Msg und `_variant`.\n1. **Basis-Props** in `internal/props/`: ariaDetails, autoComplete, hideMsg, hint, `horizontalIconsProp` (Icons-Objekt der Inputs, `icons.ts` bleibt unverändert), infoPopover, `inputCallbacksProp` (`_on`), msg, readOnly, required, suggestions, syncValueBySelector (nur für das Behavior), touched und der String-Wert. `tooltipAlignProp` wird wiederverwendet, der Feld-Default `'top'` im `apply` neu gesetzt. Der veröffentlichte Typ `FormFieldLabelInfoPopoverProps` zieht in einem Schritt für alle Felder nach `schema/props/`; ein teilweiser Umzug würde in `components.d.ts` einen Alias `…1` erzeugen.\n2. **`FormAssociationBehavior`**: übernimmt `input-adapter-leanup/associated.controller.ts` 1:1, einschließlich der späten Host-Zuweisung im Konstruktor. `AssociatedInputController` wird zur Fassade über das Behavior, damit die G0-Verträge es sofort für alle Felder prüfen. `button/base.tsx` wird im selben PR umgestellt; der Button-Pin bleibt unverändert.\n3. **Shell-FCs** unter `internal/functional-components/form-field/`, nur für die Hülle von `kol-input-color`: G1.3a FormField (Label, Hint, Msg, Counter, Tooltip, Zeichengrenzen-Hinweis) mit dem ARIA-Helfer aus `getRenderStates`, G1.3b InputContainer mit Adornments, IconButton, Input und Suggestions. Neue Blöcke in `schema/bem-registry.ts`. Die alten FCs werden Adapter auf die neuen, damit Hydrate-Snapshot und Pixel-Gate die Hülle sofort über alle Felder prüfen. TextArea kommt in G2, FieldControl/Checkbox/Radio in G4, NativeSelect/Option(List)/CustomSuggestions in G5. Die `fieldset`-Wurzel von radio bleibt bis G4 auf dem alten Pfad, weil `BemRootNodeFC` nur `div` rendert.\n4. **`BaseFormFieldWebComponent`** (DD16): `apply*` der Basis-Props, ein eigenes `TooltipBehavior` pro Instanz statt der modulglobalen Map in `FormField.tsx`, Formular-Anbindung, Event-Behandlung als Ersatz für `InputController` (Reihenfolge: KoliBri-Event, dann Callback) und Render-Helfer. Damit wird `kol-input-color` migriert.\n - Die Basis ist generisch über die Feld-API. Sie greift über einen dokumentierten Self-Cast (`shared`) auf die Basis-Props zu und liefert die FormField-Props mit `getFormFieldProps()`. Gerendert wird in der konkreten Klasse, wie bei `BaseButtonWebComponent`.\n - Die Props an `InputFC` bildet die konkrete Klasse in der Schlüsselreihenfolge der alten State-Wrapper. Props, die der Legacy-State nur bei gesetztem Wert enthielt, zum Beispiel `accessKey` und `aria-keyshortcuts`, übergibt sie nur, wenn ein Wert gesetzt ist. Nur so bleiben die Attributreihenfolge im Hydrate-Snapshot und das DOM unverändert.\n - `_label={false}` bleibt wie bisher der Expert-Slot (`''`). `labelWithExpertSlotProp` allein würde daraus den String `'false'` machen.\n\nEntscheidungen:\n\n- Die Basis-Config enthält nur Props, die alle 14 Felder identisch haben: ariaDetails, disabled, hideLabel, hideMsg, hint, infoPopover, label, msg, name, on, tooltipAlign, touched. `WebComponentInterface` verlangt für jede Config-Prop einen Watcher.\n- Die Basisklasse ist generisch über die Feld-API. TypeScript löst `ResolvedProps<Api>` für ein generisches `Api` nicht auf, deshalb greift sie über einen einzigen, dokumentierten Self-Cast auf die Basis-Props zu.\n- Bewusste Abweichungen der Prop-Factory, wie in allen bisherigen Skeleton-Migrationen: `undefined` führt zum Default statt den alten Wert zu behalten, `normalizeBoolean` akzeptiert `'true'`/`'false'`, ungültige Werte werden verworfen.\n- `data-testid=\"input-counter\"`/`\"input-counter-aria\"` bleiben bis G2, weil `e2e/input-character-limit.ts` sie braucht und der DOM byte-gleich bleiben muss.\n\nRisiken: Zusammenbau von `aria-describedby` (hint, msg, Counter, Hinweis zur Zeichengrenze; heutige Eigenheiten in #11035); Attribut- und Klassenreihenfolge im Hydrate-Snapshot; die beiden Fokus-Flags aus Controller und Komponente; `attachInternals(undefined)` bei SSR/Hydrate.\n\n### G2 – Textfelder: email → password → text, danach textarea (#9579, #9582, #9585, #9602)\n\nDie vier Felder hängen heute in der Kette `InputPasswordController → InputTextEmailController → InputText/InputEmail` und teilen placeholder, autoComplete, maxLength/hasCounter/maxLengthBehavior und den Counter. pattern haben nur die drei Inputs. `spellCheck` rendert nur input-text; textarea validiert die Prop, gibt sie aber nicht an das `<textarea>` weiter (#10863).\n\nG2 besteht aus sechs PRs:\n\n```\nG2.0 Testlücken (nur Tests) ────────────────────────────────────────────────────────┐\nG1.4 ─► G2.1 Props + CounterBehavior + TextAreaFC ─► G2.2 Basis + email ─► G2.3 password ─► G2.4 text ─► G2.5 textarea\n```\n\n- G2.0: Jest-Snapshots für Zähler, Zeichengrenze, Clear-Button, Visibility-Toggle, `_multiple`, `_pattern`, `_required` und `_resize`. Bisher pinnt kein Snapshot diese Fälle. Befunde, die die Migration 1:1 übernimmt: #10863 (spellCheck), #11051 (`_adjustHeight` schrumpft nicht), #11052 (Zähler bei `_hasCounter`/`_maxLengthBehavior` zur Laufzeit), #11053 (`has-value` uneinheitlich), #11054 (devHint zu `_autoComplete`).\n- G2.1: Props der Textfelder in `internal/props/`; `CounterBehavior` unter `internal/functional-components/counter/` (besitzt `hasCounter`, `maxLength` und `maxLengthBehavior`, `CounterDomUpdater` ist bis G2.5 Fassade darüber); `TextAreaFC` mit dem neuen Block `kol-textarea` und das alte `inputs/TextArea` als Adapter; `getFormFieldProps()` der Basis reicht zusätzlich `required`, `readOnly`, `maxLength` und `counter` durch.\n- G2.2: `BaseTextInputWebComponent` (`internal/functional-components/text-input/`) für email, password und text, damit wird `kol-input-email` migriert.\n - Die Zähler-Props gehören dem `CounterBehavior` und stehen nicht in der Props-Config der Felder; die Watcher der Felder geben sie an das Behavior weiter.\n - Die Props an `InputFC` folgen der Schlüsselreihenfolge von `InputStateWrapper`. Props, die der Legacy-State nur gesetzt enthielt (`accessKey`, `maxlength`, `placeholder`, `pattern`, `aria-keyshortcuts`), werden nur übergeben, wenn ein Wert gesetzt ist; ein leerer Name wird nicht gerendert. Den Unterschied sieht nur der Hydrate-Snapshot, Jest verwirft leere Attribute.\n - `has-value` ist wie im Legacy-Code ein einfaches Feld ohne Re-Render (#11053).\n - Beim Laden werden Wert und Maximum ohne Zähler-Update übernommen, der Zähler wird in `componentDidLoad` gefüllt.\n - Icons und Smart-Button der Input-Container bildet `getInputAdornments()` (`form-field/adornments.tsx`) für alle migrierten Felder, auch für `kol-input-color`.\n- G2.3: `kol-input-password` auf `BaseTextInputWebComponent`, mit `visibilityToggle` und dem State `passwordVisible`. Der Kompatibilitätszweig `_variant === 'visibility-toggle'` (#10247) entfällt: Legacy verglich den bereits zu einem Array normalisierten `_variant` mit einem String, der Zweig griff also nie. Der G2.0-Snapshot pinnt das. `input-password/controller.ts` bleibt bis G2.4, weil `InputTextController` davon erbt.\n- G2.4: `kol-input-text` auf `BaseTextInputWebComponent`, mit `type` (auch als Root-Klasse), `spellCheck`, `suggestions`, Clear-Button bei `type=\"search\"` und den fünf Selection-Methoden. `has-value` folgt hier wie bisher jeder Wertänderung. Mit dem Feld fällt die Controller-Kette `InputPasswordController → InputTextEmailController → InputTextController` weg.\n- G2.5: `kol-textarea` erbt direkt von `BaseFormFieldWebComponent` und komponiert das `CounterBehavior`; die drei Zähler-Watcher wiederholen die Einzeiler der Textbasis, eine dritte Basisebene ist nicht erlaubt. Übernommen werden die Zeilenberechnung von `_adjustHeight` (wächst nur, #11051), das `setTimeout` in `componentDidLoad` für `_rows` und `style.resize`; `spellCheck` wird wie bisher nicht gerendert (#10863). Mit dem Feld fallen `CounterDomUpdater`, `TextAreaStateWrapper` und das alte `inputs/TextArea` samt Adapter-Test weg.\n- `CounterBehavior` ersetzt `utils/counter-dom-updater.ts`; das direkte DOM-Update ohne Re-Render bleibt erhalten.\n- textarea erbt direkt von `BaseFormFieldWebComponent` und nutzt das CounterBehavior.\n- Risiken: Selection-API und `_type` search/url/tel von input-text; Clear-Button und Visibility-Toggle; Datalist-IDs; `_adjustHeight`/`_resize` bei textarea.\n- Zusätzliche Abnahme: `input-character-limit` und `input-text.clear-button.e2e` grün; alle 16 React-Routen von input-text pixelgleich.\n\n### G3 – Wertetypen (#9581, #9584, #9578, #9580)\n\nDrei parallele Spuren: a) number → range, b) date, c) file. Die Felder nutzen die Standard-Hülle, müssen aber ihren Wert umwandeln.\n\n- Eigene Input-Props für min/max/step; die vorhandenen Meter-Props (Defaults 0/100) passen nicht.\n- a) Step-Buttons und Feature-Flag `inputNumberButtons`; range mit zwei synchronen Inputs und Suggestions. G3a besteht aus vier PRs:\n\n ```\n G2.5 ─► G3a.0 Testlücken ─► G3a.1 Props + Zahlen-Helfer ─► G3a.2 number ─► G3a.3 range\n ```\n\n - G3a.0: Jest-Snapshots für min/max/step, `_value` als Zahl, `NumberString`, `null` und `0`, die Namen und die Breitenformel von range. Befunde, die die Migration 1:1 übernimmt: #10861 (range klemmt nicht bei einer Grenze 0), #11075 (range synchronisiert beim Ziehen nicht), #11076 (range übernimmt ohne `_value` den Browser-Mittelwert), #11077 (negative und Exponent-Strings werden verworfen), #11053 (`has-value` fehlt bei number für `0`).\n - G3a.1: `inputMinProp`, `inputMaxProp`, `stepProp` und der Zahlenwert in `internal/props/` mit einer Normalisierung, die `validateNumber` 1:1 nachbildet; reine Hilfsfunktionen für den gemerkten Werttyp, das Parsen und die Klemmung von range; `getInputAdornments()` bekommt ein `startAdornment` vor dem linken Icon.\n - G3a.2: `kol-input-number` erbt direkt von `BaseFormFieldWebComponent`. Die Step-Buttons bleiben native `<button>` und folgen bei jedem Render dem Flag `inputNumberButtons`.\n - G3a.3: `kol-input-range` erbt direkt von `BaseFormFieldWebComponent`, mit zwei `InputFC` und dem Datalist neben dem Wrapper.\n - Eine gemeinsame Basis für number und range gibt es nicht: number schreibt `_value` bei jedem `input`, range erst bei `change` und klemmt dabei.\n\n- b) Date↔ISO-Logik aus `input-date/controller.ts` in Hilfsfunktionen, alle 5 `_type`s, Zeitzonen. G3b besteht aus drei PRs:\n\n ```\n G3a.3 ─► G3b.0 Testlücken ─► G3b.1 Props + Datums-Helfer ─► G3b.2 date\n ```\n\n - G3b.0: Jest-Snapshots für alle fünf `_type`s mit Wert, `_min`/`_max`, `_step` bei `time`, einen zum Typ unpassenden Wert, `null` sowie `_readOnly` und `_required`. Werte als `Date` pinnen weiter die e2e-Tests und `controller.spec.ts`, weil ein `Date` im Namen eines Snapshots von der Zeitzone abhängt.\n - G3b.1: der Typ und die Datums-Props (`_min`, `_max`, `_value`, abhängig von `_type` und `_step`) in `internal/props/`; die Umwandlung `Date` → ISO-String, die Kalenderwoche und die Formatprüfung je Typ als reine Hilfsfunktionen, die der Legacy-Controller sofort nutzt.\n - G3b.2: `kol-input-date` erbt direkt von `BaseFormFieldWebComponent`, mit `reset()`, dem Rückschreiben von `_value` beim Blur, wenn der Wert zwischen leer und gesetzt wechselt, Enter-Submit außer bei fokussiertem Kalender-Icon und dem unterdrückten Leerzeichen bei `_readOnly`.\n - Befund: `InputDateController.validateOn` legt einen Wrapper um `_on.onChange` in den State, den nichts liest; die Events rufen das `_on` der Prop. Die Migration übernimmt den Wrapper nicht, das Verhalten bleibt gleich.\n\n- c) FileList, Drag-&-Drop-Modifier, Browse-Button, übertragener Formularwert. G3c besteht aus drei PRs:\n\n ```\n G3b.2 ─► G3c.0 Testlücken ─► G3c.1 accept-Prop + Dateinamen-Helfer ─► G3c.2 file\n ```\n\n - G3c.0: Jest-Snapshots für `_accept`, `_multiple`, `_required`, ein Feld ohne Namen und `_accept` mit `_multiple`, Icons und Smart-Button. e2e für mehrere Dateien, `--has-file` bei Auswahl und `reset()`, den Formularwert nach Auswahl und Drop sowie den Ist-Stand beim Drop: Dateiname ohne `--has-file`, `change` vor `input` (#10865). Befund, den die Migration 1:1 übernimmt: `reset()` wirft bei Formular-Zuordnung einen TypeError (#11110).\n - G3c.1: `acceptProp` in `internal/props/` und ein reiner Helfer für den angezeigten Dateinamen, den die Legacy-Komponente sofort nutzt. `multipleProp` und `requiredProp` gibt es schon.\n - G3c.2: `kol-input-file` erbt direkt von `BaseFormFieldWebComponent`. Der Browse-Button bleibt `KolButtonWcTag`, weil die Themes `.kol-input-container__button .kol-button` als Vorfahr-Beziehung selektieren (G7). Die Drag-Listener hängen dann als JSX-Listener am Container; die Legacy-Komponente hängt sie in `componentDidLoad` an und entfernt sie nie.\n\n- Zusätzliche Abnahme: `table-settings` pixelgleich; `reset()` bei date und file; Flag an und aus im Pixel-Gate.\n\n### G4 – Auswahl-Controls: checkbox → radio (#9576, #9583)\n\nNur diese beiden nutzen den FieldControl-Stack statt InputContainer und teilen `InputCheckboxRadioController`.\n\n- G4 besteht aus fünf PRs:\n\n ```\n G3 ─► G4.0 Testlücken ─► G4.1 options ─► G4.2 FieldControl-/Checkbox-/Radio-FCs + fieldset ─► G4.3 checkbox ─► G4.4 radio\n ```\n\n - G4.0: Snapshots für checkbox (ohne Namen, `_required`, `_value` als String und Objekt, eigene `_icons`, `_labelAlign: 'left'` mit `_hideLabel`, indeterminate als Switch) und radio (ausgewählte Option, ohne Namen, Gruppe `_disabled`, `_required`, `_hideLabel`, Option mit `hint`, Objektwert, Label als Zahl). e2e für die Pfeiltasten-Navigation von radio, `focus()`/`click()` sowie `getValue()`, den `onInput`-Wert `null` beim Abwählen (für `table-settings`) und das Löschen von `_indeterminate` bei checkbox. Befunde, die die Migration 1:1 übernimmt: #11114.\n - G4.1: `options`-Prop und `fillKeyOptionMap` ziehen aus `input-radio/controller.ts` nach `form-field/options.ts`; G5 braucht beides ebenfalls.\n - G4.2: FieldControlFC, CheckboxFC und RadioFC, dazu der `fieldset`-Wurzelknoten: `BemRootNodeFC` und `FormFieldFC` bekommen `component: 'div' | 'fieldset'`, das Label eine `legend`-Variante. Die alten FCs werden Adapter, damit Hydrate-Snapshot und Pixel-Gate die neuen sofort prüfen.\n - G4.3 und G4.4: die beiden Felder erben direkt von `BaseFormFieldWebComponent`; `InputCheckboxRadioController` wird aufgelöst.\n\n- Risiken: Theme-SCSS der Checkbox (300 LOC, verschachtelte Varianten); `indeterminate` nur als Property; Formularwert bei checked/unchecked; Objektwerte, Tastatur- und Fokus-Navigation bei Radio.\n- Zusätzliche Abnahme: alle drei Checkbox-Varianten je Theme; Tastatur-E2E für Radio; `table-settings` pixelgleich.\n\n### G5 – Listen: select → combobox + single-select (#9594, #9569, #9595)\n\n- select: `BaseSelectWebComponent` für `kol-select` und das Übergangs-Tag `kol-select-wc` (DD16, Übergangsmuster `shadow: false`).\n- combobox und single-select: gemeinsame Basis oder ein `ListboxBehavior` für Tastatur, open/close und focusin/out. `@Listen`-Handler folgen der Event-Handler-Policy.\n- Risiken: Options-Sync des versteckten `<select multiple>`; `pagination` hängt am Light DOM von `kol-select-wc`; `:has()`-Selektoren im CustomSuggestions-SCSS.\n- Zusätzliche Abnahme: `pagination` pixelgleich; Tastatur- und Maus-E2E für combobox und single-select; bekannte Bugs bestehen nachweislich unverändert weiter oder wurden vorher gefixt.\n- Schnitt: G5.0 Testlücken → G5.1 select-Fundament (Props, Wert-Helfer, `SelectFC`, Legacy-FCs als Adapter) → G5.2 `kol-select` und `kol-select-wc` → G5.3 `BaseListboxWebComponent` und `kol-combobox` → G5.4 `kol-single-select`.\n- G5.0: Die select-Snapshots registrieren `kol-select` und `kol-select-wc`, damit sie das native Select enthalten. Neue e2e-Tests „Keyboard and mouse“ (combobox, single-select) und „Value“ (select). Befunde in #11124 und #10841.\n- `kol-select` rendert das FC direkt in seinem Shadow DOM, ohne das innere `kol-select-wc`. Diese DOM-Änderung wird in G5.2 einzeln abgenommen. Als eigenständiges Formularfeld setzt `kol-select` `_touched` und `_value` wie alle migrierten Felder; damit ist #11014 behoben. Nach G5.4 überträgt `kol-select` auch einen Einzelwert in `FormData` und in das `_syncValueBySelector`-Ziel (#11015): Ein `<select>`-Ziel erhält je Wert eine gewählte Option, ein anderes Ziel den Wert als String, mit `_multiple` als JSON.\n- `kol-single-select` liest `_value` und `_rows` ungeprüft wie zuvor: Jeder Wert von `_value`, auch `undefined`, ist die Auswahl, und `_rows` ist der CSS-Wert `--visible-options`. Der Abgleich des Eingabetexts mit den Labels beim Verlassen des Felds (#10501, #10617) bleibt bis zu einem eigenen Fix.\n\n### G6 – Rückbau\n\nGelöscht wird, sobald der letzte Import weg ist. Veröffentlichte Schema-Typen bleiben stehen: die `*Watches`-Typen und die `validate*`-Funktionen aus `schema/index.ts` sind Teil der veröffentlichten API (der `react-hook-form-adapter` importiert aus `dist/types/schema`), ihr Entfernen wäre ein eigener Breaking Change. In `functional-components/` bleiben `Heading` (alert, card, collapsible) und `ToastItem` (toaster).\n\n| Zeitpunkt | Was gelöscht wird |\n| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| nach G2 | erledigt in G2: `utils/counter-dom-updater.ts`, `InputPasswordController`, `InputTextEmailController`, `InputTextController`, `TextAreaStateWrapper`, alte `inputs/TextArea` |\n| nach G3a | erledigt in G3a: `InputNumberController`, `InputRangeController`, `InputIconController.isNumberString` |\n| nach G3b | erledigt in G3b: `InputDateController`, `InputIconController.validateNumber`/`parseToNumber` |\n| nach G3c | erledigt in G3c: `InputFileController` |\n| nach G4 | erledigt in G4: `InputCheckboxController`, `InputRadioController`/`InputCheckboxRadioController`, `Checkbox`-/`Radio`-/`FieldControlStateWrapper`, altes `FieldControl`, alte `inputs/Checkbox`/`inputs/Radio` |\n| nach G5 | erledigt in G5.2–G5.4: `SelectController`, `ComboboxController`, `SingleSelectController`, `SelectStateWrapper`, `NativeSelect`/`NativeOption(List)`, alte `CustomSuggestionsOption(sGroup)`; `kol-select-wc` in G7.7, nachdem `kol-pagination` das Select-Feld in G7.6 direkt rendert |\n| final | erledigt: `@deprecated/input/*` (`InputController`, `InputIconController`), `input-adapter-leanup/`, `functional-component-wrappers/` inkl. `getRenderStates`, die Adapter aus G1.3, die alten FormField-/Input-FCs in `functional-components/` (`FormField*`, `InputContainer`, `InputAdornment`, `IconButton`, `inputs/`, `Suggestions`, `Button`) |\n\n### G7 – Folge-Epics (außerhalb dieser Migration)\n\n- ✅ Die Übergangs-Tags (`kol-button-wc`, `kol-link-wc`, `kol-popover-button-wc`, `kol-select-wc`, `kol-dialog-wc`, `kol-alert-wc`, `kol-table-settings-wc`) sind durch die Functional Components und Items ersetzt und gelöscht (G7.1–G7.7, #11167, #11172, #11174–#11178). `kol-tooltip-wc` bleibt als einzige `-wc`-Komponente erhalten: weiterhin deprecated und ausdrücklich nur für DESYBRI.\n- Umstieg auf natives `formAssociated` mit ElementInternals (Verhaltensänderung, eventuell Breaking).\n- BEM-Bereinigung mit CLI-Tasks `ScssRename*` und Migrationsguide für Custom-Themes.\n\n## Offene Fragen für die Detailpläne\n\n1. ~~Sind die Shell-Adapter (alte FC → neue FC) als Zwischenschicht akzeptabel? (G1.3)~~ Entschieden: ja, siehe G1.3.\n2. ~~ID-Strategie: bisherige Erzeugung für Snapshot-Parität oder DD12 `createUniqueId`? (G1.4)~~ Kein Konflikt: Legacy nutzt bereits `createUniqueId` und `createRelatedUniqueId`. Die Basis-ID wird einmal pro Instanz als `@State()` erzeugt.\n3. ~~`_touched` als `@State` oder als Render-Prop? (G1.4)~~ Entschieden: Render-Prop. `_touched` bleibt `@Prop({ mutable: true, reflect: true })`, der Blur-Handler schreibt die Prop (Vorbild `_open` in `kol-details`).\n4. ~~`_on`: eine gemeinsame Callback-Prop oder typisiert pro Feld? (G1.1)~~ Entschieden: gemeinsam, alle Felder deklarieren `InputTypeOnDefault`.\n5. ~~SSR-Absturz von `attachInternals(undefined)` 1:1 übernehmen oder mit eigenem PR über einen Guard absichern? (G1.2)~~ Entschieden: 1:1 übernehmen, Fix als eigener PR nach G1.2 (#11034). Ursache ist nicht ein leeres `@Element()`: In mock-doc greift `instanceof Element` in `findHostWithShadowRoot` nicht. Betroffen sind im SSR die inneren `kol-button-wc`/`kol-popover-button-wc` und mit `serializeShadowRoot: 'scoped'` alle Legacy-Felder.\n6. ~~Verhaltensverträge in Jest oder in Playwright? (G0)~~ Entschieden: Playwright, siehe G0.\n7. ~~Namen der Input-Props für min/max/step. (G3)~~ Entschieden: `inputMinProp`, `inputMaxProp` und `stepProp`, analog zu `horizontalIconsProp` (G1.1).\n8. ~~`kol-select-wc` als Übergangs-Tag behalten oder `pagination` direkt auf das FC umstellen? (G5)~~ Entschieden: `kol-select` rendert das FC direkt. `kol-select-wc` bleibt als Übergangs-Tag für `pagination`, mit derselben Basis; die Ablösung braucht Theme-Änderungen (G7). Umgesetzt in G7.6 und G7.7.\n9. ~~combobox und single-select: gemeinsame DD16-Basis oder `ListboxBehavior`? (G5)~~ Entschieden: gemeinsame Basis `BaseListboxWebComponent extends BaseFormFieldWebComponent`, wie `BaseTextInputWebComponent` in G2.\n10. ~~#10501 und #10617 vor oder nach der Migration fixen? (G5)~~ Entschieden: nach der Migration, als eigener PR auf dem migrierten Code. Die Migration übernimmt das Verhalten 1:1.\n\n## Grundlagen\n\n- `packages/components/src/components/_skeleton/ARC42.md`\n- Skill `.claude/skills/migrate-to-skeleton/` (inkl. `reference/patterns.md`, `reference/pitfalls.md`)\n- Skill `.claude/skills/zero-visual-delta-handoff/`\n",
377
377
  "kind": "doc"
378
378
  },
379
379
  {
@@ -469,7 +469,7 @@
469
469
  "group": "docs",
470
470
  "name": "RENOVATE",
471
471
  "path": "docs/RENOVATE.md",
472
- "code": "# Renovate — Automated Dependency Updates\n\nThis document is the outcome of issue [#10270](https://github.com/public-ui/kolibri/issues/10270)\n(_renovate vs npm-check-updates vs dependabot_). It contains:\n\n1. a comparison of the three candidate tools,\n2. a description of the exemplary [`renovate.json`](../renovate.json) that ships with this repo,\n3. how to enable Renovate, and\n4. the migration checklist for retiring the current tooling.\n\n> **Status:** The `renovate.json` and a self-hosted runner workflow\n> ([`.github/workflows/renovate.yml`](../.github/workflows/renovate.yml)) are committed as an\n> **exemplary, ready-to-run setup**. Renovate stays **idle** until that workflow runs — either on its\n> nightly schedule (every 3 hours between 18:00 and 06:00 Europe/Berlin) or via the manual\n> **Run workflow** button (see [Enabling Renovate](#enabling-renovate)).\n> Until then the existing Dependabot + npm-check-updates automation stays in charge.\n\n---\n\n## 1. Tool comparison\n\nKoliBri is a **pnpm-workspace monorepo** with 30+ packages, several intentionally pinned major\nlines (Angular `v20`/`v21`, React 18/19, Stencil 4, ESLint 9) and four maintained branches\n(`develop`, `release/3`, `release/2`, `release/1`). That shapes the comparison:\n\n| Capability | **Renovate** | Dependabot | npm-check-updates (ncu) |\n| -------------------------------------------- | :----------------------: | :---------------------------------: | :-----------------------: |\n| pnpm-workspace aware | ✅ native | ⚠️ partial | ❌ manual (per-package) |\n| One PR grouping related packages | ✅ fully configurable | ⚠️ `groups:` only | ❌ none |\n| Hold a package on a specific major line | ✅ per-folder rules | ⚠️ `ignore` (global) | ⚠️ `-x` exclude (global) |\n| GitHub Actions updates | ✅ | ✅ | ❌ |\n| Lockfile-only refresh | ✅ `lockFileMaintenance` | ⚠️ limited | ❌ (needs `pnpm install`) |\n| Automerge (per update type) | ✅ | ⚠️ limited | ❌ |\n| Multi-base-branch (release/\\*) support | ✅ `baseBranchPatterns` | ✅ `target-branch` (one entry each) | ⚠️ matrix in a workflow |\n| Security / vulnerability remediation | ✅ OSV + GitHub alerts | ✅ GitHub alerts | ❌ |\n| Dependency Dashboard (single overview issue) | ✅ | ❌ | ❌ |\n| Schedule / batching | ✅ | ⚠️ basic | ⚠️ via cron workflow |\n| Self-hostable (no third-party app) | ✅ (official Action) | ✅ (GitHub-native) | ✅ (CLI) |\n| Config surface | medium | low | minimal |\n| Cost for open source | free | free | free |\n\n### Verdict\n\n- **Renovate — recommended.** It is the only option that models KoliBri's reality in _one_ config:\n group the Angular/React/Stencil families, hold each adapter folder on its pinned major, automerge\n the safe stuff (GitHub Actions, `@types/*`), and route everything risky to a review queue (the\n Dependency Dashboard). It also folds in what we currently split across **two** systems\n (Dependabot for Actions + a daily `ncu` workflow for npm).\n- **Dependabot — viable fallback.** Now supports `groups:`, but every package directory needs its\n own `updates:` entry (≈30 for this monorepo × 4 branches) and it cannot hold a dependency on a\n specific major _per folder_ — exactly what the `angular/v20|v21` and `react*` adapters need.\n- **npm-check-updates — not an automation tool.** It is a CLI that rewrites version ranges; it has\n no PR/grouping/scheduling of its own. We only use it _inside_ a hand-written workflow\n (`.github/workflows/auto-dependency-updater.yml`). Renovate makes that workflow redundant.\n\n---\n\n## 2. What the exemplary `renovate.json` does\n\nThe committed [`renovate.json`](../renovate.json) is tailored to this repo. Highlights:\n\n### Global behaviour\n\n- **`extends: [\"config:recommended\", \"security:openssf-scorecard\"]`** — sensible defaults plus\n OpenSSF Scorecard badges on PRs.\n- **Conventional Commits** — `chore(deps): …` titles so PRs pass `pr-title-validation.yml`.\n- **`labels: [\"dependencies\", \"renovate\", \"release:engineering\"]`** — the `release:*` label is\n **required** by `pr-release-label-validation.yml`; `release:engineering` files dependency PRs\n under _🔧 Engineering_ in the changelog (see `.github/release.yml`).\n- **Night-only cadence (18:00–06:00, every 3 hours)** — Renovate must not open PRs or automerge\n during working hours. Two settings enforce this together:\n - `renovate.json` → `\"schedule\": [\"after 6pm\", \"before 6am\"]` with `timezone: \"Europe/Berlin\"`.\n The window crosses midnight, so it is written as **two** entries (Renovate ORs them); a single\n `\"after 6pm before 6am\"` string would never match.\n - the workflow cron `0 16,19,22,1,4 * * *`. GitHub cron only understands UTC, so these hours are\n chosen to stay inside the local window all year: in CEST (UTC+2) they fire at 18/21/00/03/06\n Berlin time, in CET (UTC+1) at 17/20/23/02/05. The one run that falls outside the window\n (17:00 in winter, 06:00 in summer) starts Renovate but produces no PRs, because the\n `renovate.json` schedule is the authoritative gate.\n\n **Exception:** `vulnerabilityAlerts` keeps `\"schedule\": [\"at any time\"]`, so a manual\n **Run workflow** during the day still ships security fixes.\n\n `prConcurrentLimit: 5` / `prHourlyLimit: 5` cap the number of open PRs **per base branch** (so\n `develop` and each `release/*` branch have their own budget).\n\n- **Automerge all non-major updates (merge commit)** — the first package rule enables `automerge` for\n `patch`/`minor`/`digest`/`pin`/lockfile updates; `automergeStrategy: merge` is the only method the\n `Production branches` ruleset allows on `develop` (see [Troubleshooting](#troubleshooting-prs-stay-open-although-ci-is-green)). Known-risky rules below (Stencil, kern-ux, ESLint,\n TypeScript, typescript-eslint) override it back to manual. Majors always keep dashboard approval.\n Prerequisites: repo setting **Allow auto-merge** enabled, green pipelines enforced via required\n status checks on `develop`, and the runner GitHub App listed in the branch-protection bypass\n (see [Enabling Renovate](#enabling-renovate)).\n- **`baseBranchPatterns`** — runs on `develop` **and** `release/3|2|1`; the maintenance branches are\n **security-only** (all regular npm and GitHub Actions updates are disabled) so released majors stay\n stable. Security PRs still automerge when non-major; major security updates require dashboard\n approval.\n- **`lockFileMaintenance`** — weekly `pnpm-lock.yaml` refresh (replaces the manual\n `04 - Update pnpm Lock` workflow runs).\n- **`configMigration: true`** — Renovate keeps `renovate.json` itself up to date when built-in\n presets or options change.\n- **`dependencyDashboardOSVVulnerabilitySummary: \"all\"`** — adds an OSV vulnerability table\n directly to the Dependency Dashboard issue.\n- **`postUpdateOptions: [\"pnpmDedupe\"]`** — keeps the pnpm lockfile tidy.\n\n### Grouping & guard-rails (the important part for this monorepo)\n\n| Rule | Effect |\n| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **All majors** | Require manual approval via the Dependency Dashboard — KoliBri pins majors deliberately. |\n| **Angular `@angular/*`, `zone.js`, `ng-packagr`** | Major updates **disabled** entirely; within-major updates grouped per adapter folder (_Angular 20/21_). A new Angular major = a new adapter folder, never an auto-bump. |\n| **React `react`, `react-dom`, `@types/react*`** | Major updates **disabled**; within-major React updates of the `react*` adapters grouped and automerged once green. |\n| **Stencil `@stencil/*`, `@stencil-community/*`** | **All** updates require dashboard approval — every 4.39+ release currently breaks the Popover API, tooltips and visual tests (see [`UPGRADEABLE_DEPENDENCIES.md`](./UPGRADEABLE_DEPENDENCIES.md)). |\n| **`@kern-ux/*`** | Dashboard approval only — upgraded by hand together with theming work. |\n| **`@typescript-eslint/*`, ESLint core + plugins** | Minor/major require approval (9 → 10 is a breaking migration). |\n| **`jest*`, `typescript`** | Majors/non-patch require approval. |\n| **`github-actions`, `@types/*`** | Grouped; automerged like every other non-major update. |\n| **Stylelint, Playwright** | Grouped into single PRs. |\n\n> The pins above mirror exactly what the current `ncu:*` scripts exclude\n> (`@kern-ux/*`, `@stencil/*`, `@typescript-eslint/*`) and what `UPGRADEABLE_DEPENDENCIES.md`\n> documents as breaking — so behaviour is preserved, just expressed declaratively.\n\n### Validate the config locally\n\n```sh\nnpx --yes --package renovate renovate-config-validator renovate.json\n```\n\n---\n\n## 3. Enabling Renovate\n\nTwo ways to run it; **this repo is wired for Option A**.\n\n### Option A — Self-hosted via GitHub Actions (committed in this repo)\n\nThis repo ships [`.github/workflows/renovate.yml`](../.github/workflows/renovate.yml). It runs Renovate\n**every 3 hours during the night window 18:00–06:00 Europe/Berlin**\n(`0 16,19,22,1,4 * * *` UTC, see [Global behaviour](#global-behaviour)) **and** on demand via the **Run workflow** button\n(`workflow_dispatch`, with an optional `dry_run` preview). It authenticates through the existing GitHub\nApp (`APP_ID` / `PRIVATE_KEY` secrets, shared with _04 - Update pnpm Lock_).\n\nTo activate it:\n\n1. Ensure that GitHub App installation grants **contents: write**, **pull-requests: write**,\n **issues: write** (for the Dependency Dashboard), **workflows: write** (so the `github-actions`\n manager may update `.github/workflows/*`), **commit statuses: write**, **Dependabot alerts: read**\n and the mandatory **metadata: read**. Nothing beyond those seven is needed. Changing them at the\n App is only half of it: the installation has to accept the new set, otherwise the runner keeps\n working with the old one and the symptoms below stay. _Alternative:_ replace the app-token step\n with a `RENOVATE_TOKEN` PAT/fine-grained token carrying the same scopes.\n\n > Without **Dependabot alerts: read** every run logs\n > `Cannot access vulnerability alerts`, and `vulnerabilityAlerts` stays inactive; security PRs\n > then only come from `osvVulnerabilityAlerts`.\n\n2. Branch protection. What governs `develop` are **rulesets**, not a classic branch protection\n rule. Keep it that way: a classic rule for the same branch is evaluated on top of the rulesets,\n and its _bypass required pull requests_ list only waives the need to **open** a pull request. It\n does not waive the approval a merge needs, so the bot stays blocked while the list suggests\n otherwise.\n\n A bypass entry always waives **every rule of its ruleset**, never a single one. Hence the split:\n\n | Ruleset | Rules | Bypass list |\n | --------------------- | ---------------------------------------------------------------- | ------------- |\n | `Production branches` | `pull_request` (approvals, code-owner review, merge methods) | `publicuibot` |\n | `Required checks` | `required_status_checks`, restrict deletions, block force pushes | empty |\n\n The runner merges without a human review, but never past a red check. For everyone else nothing\n changes.\n\n Three details that cost an evening on 2026-09-09:\n\n - `allowed_merge_methods` of the `pull_request` rule is `[\"merge\"]`, which is why\n `automergeStrategy` is `merge`. On the repository itself both squash merges and merge commits\n are enabled; the ruleset is what narrows it down.\n - A required status check may never be skipped by a path filter, or the pull request waits\n forever for a status that nothing will post. `codeql.yml` therefore has no `paths-ignore`, and\n `visual-review.yml` answers a skipped CI run with `success` (mode `docs-only`, see\n `scripts/visual-review/resolve-context.mjs`). Anything added to the list has to obey the same\n rule. `check-results` is deliberately absent: a red pipeline stops Renovate, which refuses to\n merge a red branch, but it does not stop a human.\n - Every workflow behind a required check needs `synchronize` in its `pull_request` types.\n GitHub wants the check on the **current head commit**, so a workflow that only reacts to\n `opened`/`edited` leaves it unreported after any push and the merge is refused with\n `Required status check \"…\" is expected`. Renovate runs into this on every rebase; that is what\n `pr-title-validation.yml` was missing.\n - In the target patterns of a ruleset GitHub prefixes `refs/heads/` itself. Enter `release/**/*`,\n never `refs/heads/release/**/*`, otherwise the prefix doubles and the release branches silently\n lose their protection. `gh api repos/public-ui/kolibri/rules/branches/release/3` shows what is\n really in effect.\n\n Repository level, **Settings → General → Pull Requests**: `Allow auto-merge` stays on, Renovate\n uses `platformAutomerge`.\n\n3. Trigger the workflow once via **Run workflow** (optionally with `dry_run` enabled) to verify it, then\n let the nightly schedule take over.\n\n> **Tip:** The `dry_run` input maps to `RENOVATE_DRY_RUN=full`, so the first manual run previews every PR\n> Renovate _would_ open without creating anything.\n\n### Option B — Mend-hosted GitHub App (alternative, zero maintenance)\n\nIf you would rather not self-host, delete `.github/workflows/renovate.yml` and instead:\n\n1. Have an **org admin** install the [Renovate GitHub App](https://github.com/apps/renovate) on\n `public-ui` (or just on `public-ui/kolibri`).\n2. Renovate detects `renovate.json` and opens a Dependency Dashboard issue.\n3. Review the dashboard, then let it run on the weekly schedule.\n\n### Troubleshooting: PRs stay open although CI is green\n\nRenovate never explains a refused merge in the PR itself. The runner log does, but only at\n`LOG_LEVEL=debug`: start the workflow via **Run workflow** with `log_level: debug` (and `dry_run`\noff — a dry run logs `DRY-RUN: Would merge PR` and never sends the request that carries the error).\n\nAt `info` level the only trace is one line per PR:\n\n```\nINFO: All merge attempts failed (repository=public-ui/kolibri, baseBranch=develop, branch=…)\n \"pr\": 10833\n```\n\nIt means Renovate did try. Automerge is configured correctly and the branch is green — GitHub\nrejected the `PUT /repos/:owner/:repo/pulls/:number/merge` call. The cause is therefore always a\nrepository setting or a ruleset, never `renovate.json`. Renovate walks three strategies in order\n(the configured `automergeStrategy` first, then merge commit, then rebase); the debug log holds one\n`Failed to … PR` block per attempt, each with the HTTP status and GitHub's own message:\n\n| Response | Meaning |\n| --------------------------------------------------------- | ------------------------------------------------------------------------- |\n| `405` `Squash merges are not allowed on this repository.` | The merge method is not allowed here — see `allowed_merge_methods` below. |\n| `405` `Rebase merges are not allowed on this repository.` | Same, for rebase. |\n| `405` `Repository rule violations found: …` | A ruleset blocks the bot; the text names the rule. |\n| `405` `… Required status check \"x\" is expected.` | Check `x` never ran on the head commit — see `synchronize` in §3. |\n| `403` `Resource not accessible by integration` | The App lacks **contents: write**. |\n\nWhich rules actually apply to a branch is readable without admin rights:\n\n```sh\ngh api repos/public-ui/kolibri/rules/branches/develop\n```\n\n> **The case of 2026-09-09.** Every green dependency PR sat blocked with `mergeable_state: blocked`\n> while the pipelines were green and no review existed. All three attempts failed with `405`. The\n> two rules behind it both lived in one ruleset that carried everything at once:\n> `allowed_merge_methods: [\"merge\"]` rejected squash and rebase, and `require_last_push_approval`\n> rejected the merge commit, a rule the bot can never satisfy on its own branches because it is\n> always the last pusher. The App was listed in the bypass list of the **classic** branch\n> protection rule, which looks like the fix and is not one: that list only waives the need to open\n> a pull request.\n>\n> What resolved it: the rules were split into the two rulesets described in\n> [§3](#option-a--self-hosted-via-github-actions-committed-in-this-repo), the App went into the\n> bypass list of the one holding the `pull_request` rule, the classic rule was deleted, and\n> `automergeStrategy` moved from `squash` to `merge`. A bypass entry is worth nothing until it sits\n> on the ruleset that actually carries the failing rule, and `bypass_actors` is only readable with\n> admin rights, so the runner log stays the honest source.\n\nTwo more silent failures show up as warnings rather than as a blocked PR:\n\n- `Cannot access vulnerability alerts` — **Dependabot alerts: read** is missing, `vulnerabilityAlerts`\n stays inactive and security PRs come from `osvVulnerabilityAlerts` only.\n- `Could not ensure issue … integration-unauthorized` — the Dependency Dashboard issue is not being\n updated any more. Check **Issues: write** for the App, and whether the issue itself is locked.\n\n---\n\n## 4. Migration checklist (do this only after Renovate is verified)\n\nOnce Renovate runs green for a cycle, retire the overlapping automation to avoid **duplicate PRs**.\nStatus as of the Renovate activation for `develop`:\n\n- [x] Removed `.github/dependabot.yml` (Renovate now manages GitHub Actions — see the\n `github-actions` group).\n- [x] Removed `.github/workflows/auto-dependency-updater.yml` (the daily `ncu` PR job).\n- [x] Dropped the `ncu:*` / `update` scripts and the `npm-check-updates` devDependency from the root\n `package.json`, plus `.ncurc.json`.\n- [ ] Keep `04 - Update pnpm Lock` if you still want a manual lockfile-refresh button; otherwise\n Renovate's `lockFileMaintenance` covers it.\n\nUntil every box is ticked, **leave the existing tooling in place** — Renovate only acts once the\nworkflow (or app) from [§3](#3-enabling-renovate) actually runs, so there is no conflict in the meantime.\n\n---\n\n## 🇩🇪 Zusammenfassung\n\n**Empfehlung: Renovate.** Es ist die einzige Lösung, die das KoliBri-Monorepo in _einer_ Konfiguration\nabbildet — verwandte Pakete gruppieren (z. B. alle `@angular/*`), jeden Adapter-Ordner auf seiner\nfixierten Major-Version halten (`angular/v20|v21`, `react*`), sichere Updates automatisch mergen\n(GitHub Actions, `@types/*`) und alles Riskante (Stencil, kern-ux, ESLint-/Angular-Majors) über das\n**Dependency Dashboard** zur manuellen Freigabe leiten.\n\n- **Dependabot** kann zwar gruppieren, braucht aber pro Paketverzeichnis einen eigenen Eintrag\n (≈30 × 4 Branches) und kann ein Paket nicht _pro Ordner_ auf einer Major-Version halten.\n- **npm-check-updates** ist nur ein CLI ohne eigene Automatisierung (läuft heute im Workflow\n `auto-dependency-updater.yml`).\n\nNeu hinzugekommen: `automergeStrategy: merge` folgt der Merge-Commit-Konvention des Repos,\nund die `release/*`-Branches werden auf **Security-only** umgestellt — reguläre npm- und\nGitHub-Actions-Updates werden dort komplett deaktiviert, während Vulnerability-Alert-PRs\nweiterhin (bei Nicht-Major) automatisch mergen.\n\nDie fertige [`renovate.json`](../renovate.json) liegt im Repo-Root (geprüft mit dem offiziellen\n`renovate-config-validator`), und der self-hosted Runner-Workflow\n[`.github/workflows/renovate.yml`](../.github/workflows/renovate.yml) ist ebenfalls committet.\n**Renovate läuft**, sobald der Workflow startet — nachts alle 3 Stunden zwischen 18:00 und 06:00 Uhr\n(Europe/Berlin) per Zeitplan oder jederzeit manuell über den **Run workflow**-Button (Option A); alternativ kann ein Org-Admin die\n[Renovate-GitHub-App](https://github.com/apps/renovate) installieren (Option B). Bis dahin bleibt die\nbestehende Dependabot-/ncu-Automatisierung zuständig; danach greift die Migrations-Checkliste oben.\n",
472
+ "code": "# Renovate — Automated Dependency Updates\n\nThis document is the outcome of issue [#10270](https://github.com/public-ui/kolibri/issues/10270)\n(_renovate vs npm-check-updates vs dependabot_). It contains:\n\n1. a comparison of the three candidate tools,\n2. a description of the exemplary [`renovate.json`](../renovate.json) that ships with this repo,\n3. how to enable Renovate, and\n4. the migration checklist for retiring the current tooling.\n\n> **Status:** The `renovate.json` and a self-hosted runner workflow\n> ([`.github/workflows/renovate.yml`](../.github/workflows/renovate.yml)) are committed as an\n> **exemplary, ready-to-run setup**. Renovate stays **idle** until that workflow runs — either on its\n> nightly schedule (every 3 hours between 18:00 and 06:00 Europe/Berlin) or via the manual\n> **Run workflow** button (see [Enabling Renovate](#enabling-renovate)).\n> Until then the existing Dependabot + npm-check-updates automation stays in charge.\n\n---\n\n## 1. Tool comparison\n\nKoliBri is a **pnpm-workspace monorepo** with 30+ packages, several intentionally pinned major\nlines (Angular `v20`/`v21`, React 18/19, Stencil 4, ESLint 9) and four maintained branches\n(`develop`, `release/3`, `release/2`, `release/1`). That shapes the comparison:\n\n| Capability | **Renovate** | Dependabot | npm-check-updates (ncu) |\n| -------------------------------------------- | :----------------------: | :---------------------------------: | :-----------------------: |\n| pnpm-workspace aware | ✅ native | ⚠️ partial | ❌ manual (per-package) |\n| One PR grouping related packages | ✅ fully configurable | ⚠️ `groups:` only | ❌ none |\n| Hold a package on a specific major line | ✅ per-folder rules | ⚠️ `ignore` (global) | ⚠️ `-x` exclude (global) |\n| GitHub Actions updates | ✅ | ✅ | ❌ |\n| Lockfile-only refresh | ✅ `lockFileMaintenance` | ⚠️ limited | ❌ (needs `pnpm install`) |\n| Automerge (per update type) | ✅ | ⚠️ limited | ❌ |\n| Multi-base-branch (release/\\*) support | ✅ `baseBranchPatterns` | ✅ `target-branch` (one entry each) | ⚠️ matrix in a workflow |\n| Security / vulnerability remediation | ✅ OSV + GitHub alerts | ✅ GitHub alerts | ❌ |\n| Dependency Dashboard (single overview issue) | ✅ | ❌ | ❌ |\n| Schedule / batching | ✅ | ⚠️ basic | ⚠️ via cron workflow |\n| Self-hostable (no third-party app) | ✅ (official Action) | ✅ (GitHub-native) | ✅ (CLI) |\n| Config surface | medium | low | minimal |\n| Cost for open source | free | free | free |\n\n### Verdict\n\n- **Renovate — recommended.** It is the only option that models KoliBri's reality in _one_ config:\n group the Angular/React/Stencil families, hold each adapter folder on its pinned major, automerge\n the safe stuff (GitHub Actions, `@types/*`), and route everything risky to a review queue (the\n Dependency Dashboard). It also folds in what we currently split across **two** systems\n (Dependabot for Actions + a daily `ncu` workflow for npm).\n- **Dependabot — viable fallback.** Now supports `groups:`, but every package directory needs its\n own `updates:` entry (≈30 for this monorepo × 4 branches) and it cannot hold a dependency on a\n specific major _per folder_ — exactly what the `angular/v20|v21` and `react*` adapters need.\n- **npm-check-updates — not an automation tool.** It is a CLI that rewrites version ranges; it has\n no PR/grouping/scheduling of its own. We only use it _inside_ a hand-written workflow\n (`.github/workflows/auto-dependency-updater.yml`). Renovate makes that workflow redundant.\n\n---\n\n## 2. What the exemplary `renovate.json` does\n\nThe committed [`renovate.json`](../renovate.json) is tailored to this repo. Highlights:\n\n### Global behaviour\n\n- **`extends: [\"config:recommended\", \"security:openssf-scorecard\"]`** — sensible defaults plus\n OpenSSF Scorecard badges on PRs.\n- **Conventional Commits** — `chore(deps): …` titles so PRs pass `pr-title-validation.yml`.\n- **`labels: [\"dependencies\", \"renovate\", \"release:engineering\"]`** — the `release:*` label is\n **required** by `pr-release-label-validation.yml`; `release:engineering` files dependency PRs\n under _🔧 Engineering_ in the changelog (see `.github/release.yml`).\n- **Night-only cadence (18:00–06:00, every 3 hours)** — Renovate must not open PRs or automerge\n during working hours. Two settings enforce this together:\n - `renovate.json` → `\"schedule\": [\"after 6pm\", \"before 6am\"]` with `timezone: \"Europe/Berlin\"`.\n The window crosses midnight, so it is written as **two** entries (Renovate ORs them); a single\n `\"after 6pm before 6am\"` string would never match.\n - the workflow cron `0 16,19,22,1,4 * * *`. GitHub cron only understands UTC, so these hours are\n chosen to stay inside the local window all year: in CEST (UTC+2) they fire at 18/21/00/03/06\n Berlin time, in CET (UTC+1) at 17/20/23/02/05. The one run that falls outside the window\n (17:00 in winter, 06:00 in summer) starts Renovate but produces no PRs, because the\n `renovate.json` schedule is the authoritative gate.\n\n **Exception:** `vulnerabilityAlerts` keeps `\"schedule\": [\"at any time\"]`, so a manual\n **Run workflow** during the day still ships security fixes.\n\n `prConcurrentLimit: 5` / `prHourlyLimit: 5` cap the number of open PRs **per base branch** (so\n `develop` and each `release/*` branch have their own budget).\n\n- **Automerge all non-major updates (merge commit)** — the first package rule enables `automerge` for\n `patch`/`minor`/`digest`/`pin`/lockfile updates; `automergeStrategy: merge` is the only method the\n `Production branches` ruleset allows on `develop` (see [Troubleshooting](#troubleshooting-prs-stay-open-although-ci-is-green)). Known-risky rules below (Stencil, kern-ux, ESLint,\n TypeScript, typescript-eslint) override it back to manual. Majors always keep dashboard approval.\n Prerequisites: repo setting **Allow auto-merge** enabled, green pipelines enforced via required\n status checks on `develop`, and the runner GitHub App listed in the branch-protection bypass\n (see [Enabling Renovate](#enabling-renovate)).\n- **`rebaseWhen: conflicted`** — Renovate rebases a branch only when it conflicts with `develop`, not\n whenever `develop` has moved on. The `Required checks` rules do not demand an up-to-date branch\n (`strict_required_status_checks_policy: false`), and a rebase restarts the CI for about 40 minutes.\n With `behind-base-branch`, every merge into `develop` (by people or by the runner itself, which\n merges one pull request per run) pushed all other green dependency pull requests behind and into a new\n rebase, so they rarely met a run in which they were both green and up to date.\n- **`baseBranchPatterns`** — runs on `develop` **and** `release/3|2|1`; the maintenance branches are\n **security-only** (all regular npm and GitHub Actions updates are disabled) so released majors stay\n stable. Security PRs still automerge when non-major; major security updates require dashboard\n approval.\n- **`lockFileMaintenance`** — weekly `pnpm-lock.yaml` refresh (replaces the manual\n `04 - Update pnpm Lock` workflow runs).\n- **`configMigration: true`** — Renovate keeps `renovate.json` itself up to date when built-in\n presets or options change.\n- **`dependencyDashboardOSVVulnerabilitySummary: \"all\"`** — adds an OSV vulnerability table\n directly to the Dependency Dashboard issue.\n- **`postUpdateOptions: [\"pnpmDedupe\"]`** — keeps the pnpm lockfile tidy.\n\n### Grouping & guard-rails (the important part for this monorepo)\n\n| Rule | Effect |\n| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **All majors** | Require manual approval via the Dependency Dashboard — KoliBri pins majors deliberately. |\n| **Angular `@angular/*`, `zone.js`, `ng-packagr`** | Major updates **disabled** entirely; within-major updates grouped per adapter folder (_Angular 20/21_). A new Angular major = a new adapter folder, never an auto-bump. |\n| **React `react`, `react-dom`, `@types/react*`** | Major updates **disabled**; within-major React updates of the `react*` adapters grouped and automerged once green. |\n| **Stencil `@stencil/*`, `@stencil-community/*`** | **All** updates require dashboard approval — every 4.39+ release currently breaks the Popover API, tooltips and visual tests (see [`UPGRADEABLE_DEPENDENCIES.md`](./UPGRADEABLE_DEPENDENCIES.md)). |\n| **`@kern-ux/*`** | Dashboard approval only — upgraded by hand together with theming work. |\n| **`@typescript-eslint/*`, ESLint core + plugins** | Minor/major require approval (9 → 10 is a breaking migration). |\n| **`jest*`, `typescript`** | Majors/non-patch require approval. |\n| **`github-actions`, `@types/*`** | Grouped; automerged like every other non-major update. |\n| **Stylelint, Playwright** | Grouped into single PRs. |\n\n> The pins above mirror exactly what the current `ncu:*` scripts exclude\n> (`@kern-ux/*`, `@stencil/*`, `@typescript-eslint/*`) and what `UPGRADEABLE_DEPENDENCIES.md`\n> documents as breaking — so behaviour is preserved, just expressed declaratively.\n\n### Validate the config locally\n\n```sh\nnpx --yes --package renovate renovate-config-validator renovate.json\n```\n\n---\n\n## 3. Enabling Renovate\n\nTwo ways to run it; **this repo is wired for Option A**.\n\n### Option A — Self-hosted via GitHub Actions (committed in this repo)\n\nThis repo ships [`.github/workflows/renovate.yml`](../.github/workflows/renovate.yml). It runs Renovate\n**every 3 hours during the night window 18:00–06:00 Europe/Berlin**\n(`0 16,19,22,1,4 * * *` UTC, see [Global behaviour](#global-behaviour)) **and** on demand via the **Run workflow** button\n(`workflow_dispatch`, with an optional `dry_run` preview). It authenticates through the existing GitHub\nApp (`APP_ID` / `PRIVATE_KEY` secrets, shared with _04 - Update pnpm Lock_).\n\nTo activate it:\n\n1. Ensure that GitHub App installation grants **contents: write**, **pull-requests: write**,\n **issues: write** (for the Dependency Dashboard), **workflows: write** (so the `github-actions`\n manager may update `.github/workflows/*`), **commit statuses: write**, **Dependabot alerts: read**\n and the mandatory **metadata: read**. Nothing beyond those seven is needed. Changing them at the\n App is only half of it: the installation has to accept the new set, otherwise the runner keeps\n working with the old one and the symptoms below stay. _Alternative:_ replace the app-token step\n with a `RENOVATE_TOKEN` PAT/fine-grained token carrying the same scopes.\n\n > Without **Dependabot alerts: read** every run logs\n > `Cannot access vulnerability alerts`, and `vulnerabilityAlerts` stays inactive; security PRs\n > then only come from `osvVulnerabilityAlerts`.\n\n2. Branch protection. What governs `develop` are **rulesets**, not a classic branch protection\n rule. Keep it that way: a classic rule for the same branch is evaluated on top of the rulesets,\n and its _bypass required pull requests_ list only waives the need to **open** a pull request. It\n does not waive the approval a merge needs, so the bot stays blocked while the list suggests\n otherwise.\n\n A bypass entry always waives **every rule of its ruleset**, never a single one. Hence the split:\n\n | Ruleset | Rules | Bypass list |\n | --------------------- | ---------------------------------------------------------------- | ------------- |\n | `Production branches` | `pull_request` (approvals, code-owner review, merge methods) | `publicuibot` |\n | `Required checks` | `required_status_checks`, restrict deletions, block force pushes | empty |\n\n The runner merges without a human review, but never past a red check. For everyone else nothing\n changes.\n\n Three details that cost an evening on 2026-09-09:\n\n - `allowed_merge_methods` of the `pull_request` rule is `[\"merge\"]`, which is why\n `automergeStrategy` is `merge`. On the repository itself both squash merges and merge commits\n are enabled; the ruleset is what narrows it down.\n - A required status check may never be skipped by a path filter, or the pull request waits\n forever for a status that nothing will post. `codeql.yml` therefore has no `paths-ignore`, and\n `visual-review.yml` answers a skipped CI run with `success` (mode `docs-only`, see\n `scripts/visual-review/resolve-context.mjs`). Anything added to the list has to obey the same\n rule. `check-results` is deliberately absent: a red pipeline stops Renovate, which refuses to\n merge a red branch, but it does not stop a human.\n - Every workflow behind a required check needs `synchronize` in its `pull_request` types.\n GitHub wants the check on the **current head commit**, so a workflow that only reacts to\n `opened`/`edited` leaves it unreported after any push and the merge is refused with\n `Required status check \"…\" is expected`. Renovate runs into this on every rebase; that is what\n `pr-title-validation.yml` was missing.\n - A workflow behind a required check that filters `pull_request` by base branch also needs\n `edited`. A stacked pull request opened against a feature branch never matches the filter, and\n when GitHub retargets it to `develop` after the pull request below it is merged, it sends only\n `edited`. Without it the check stays at `Expected — Waiting for status to be reported` until the\n next push. `codeql.yml` therefore reacts to `edited` and runs only when the base changed; a\n title or body edit gets a concurrency group of its own, so it cannot cancel a running analysis.\n - In the target patterns of a ruleset GitHub prefixes `refs/heads/` itself. Enter `release/**/*`,\n never `refs/heads/release/**/*`, otherwise the prefix doubles and the release branches silently\n lose their protection. `gh api repos/public-ui/kolibri/rules/branches/release/3` shows what is\n really in effect.\n\n Repository level, **Settings → General → Pull Requests**: `Allow auto-merge` stays on, Renovate\n uses `platformAutomerge`.\n\n3. Trigger the workflow once via **Run workflow** (optionally with `dry_run` enabled) to verify it, then\n let the nightly schedule take over.\n\n> **Tip:** The `dry_run` input maps to `RENOVATE_DRY_RUN=full`, so the first manual run previews every PR\n> Renovate _would_ open without creating anything.\n\n### Option B — Mend-hosted GitHub App (alternative, zero maintenance)\n\nIf you would rather not self-host, delete `.github/workflows/renovate.yml` and instead:\n\n1. Have an **org admin** install the [Renovate GitHub App](https://github.com/apps/renovate) on\n `public-ui` (or just on `public-ui/kolibri`).\n2. Renovate detects `renovate.json` and opens a Dependency Dashboard issue.\n3. Review the dashboard, then let it run on the weekly schedule.\n\n### Troubleshooting: PRs stay open although CI is green\n\nRenovate never explains a refused merge in the PR itself. The runner log does, but only at\n`LOG_LEVEL=debug`: start the workflow via **Run workflow** with `log_level: debug` (and `dry_run`\noff — a dry run logs `DRY-RUN: Would merge PR` and never sends the request that carries the error).\n\nAt `info` level the only trace is one line per PR:\n\n```\nINFO: All merge attempts failed (repository=public-ui/kolibri, baseBranch=develop, branch=…)\n \"pr\": 10833\n```\n\nIt means Renovate did try. Automerge is configured correctly and the branch is green — GitHub\nrejected the `PUT /repos/:owner/:repo/pulls/:number/merge` call. The cause is therefore always a\nrepository setting or a ruleset, never `renovate.json`. Renovate walks three strategies in order\n(the configured `automergeStrategy` first, then merge commit, then rebase); the debug log holds one\n`Failed to … PR` block per attempt, each with the HTTP status and GitHub's own message:\n\n| Response | Meaning |\n| --------------------------------------------------------- | ------------------------------------------------------------------------- |\n| `405` `Squash merges are not allowed on this repository.` | The merge method is not allowed here — see `allowed_merge_methods` below. |\n| `405` `Rebase merges are not allowed on this repository.` | Same, for rebase. |\n| `405` `Repository rule violations found: …` | A ruleset blocks the bot; the text names the rule. |\n| `405` `… Required status check \"x\" is expected.` | Check `x` never ran on the head commit — see `synchronize` in §3. |\n| `403` `Resource not accessible by integration` | The App lacks **contents: write**. |\n\nWhich rules actually apply to a branch is readable without admin rights:\n\n```sh\ngh api repos/public-ui/kolibri/rules/branches/develop\n```\n\n> **The case of 2026-09-09.** Every green dependency PR sat blocked with `mergeable_state: blocked`\n> while the pipelines were green and no review existed. All three attempts failed with `405`. The\n> two rules behind it both lived in one ruleset that carried everything at once:\n> `allowed_merge_methods: [\"merge\"]` rejected squash and rebase, and `require_last_push_approval`\n> rejected the merge commit, a rule the bot can never satisfy on its own branches because it is\n> always the last pusher. The App was listed in the bypass list of the **classic** branch\n> protection rule, which looks like the fix and is not one: that list only waives the need to open\n> a pull request.\n>\n> What resolved it: the rules were split into the two rulesets described in\n> [§3](#option-a--self-hosted-via-github-actions-committed-in-this-repo), the App went into the\n> bypass list of the one holding the `pull_request` rule, the classic rule was deleted, and\n> `automergeStrategy` moved from `squash` to `merge`. A bypass entry is worth nothing until it sits\n> on the ruleset that actually carries the failing rule, and `bypass_actors` is only readable with\n> admin rights, so the runner log stays the honest source.\n\nTwo more silent failures show up as warnings rather than as a blocked PR:\n\n- `Cannot access vulnerability alerts` — **Dependabot alerts: read** is missing, `vulnerabilityAlerts`\n stays inactive and security PRs come from `osvVulnerabilityAlerts` only.\n- `Could not ensure issue … integration-unauthorized` — the Dependency Dashboard issue is not being\n updated any more. Check **Issues: write** for the App, and whether the issue itself is locked.\n\n---\n\n## 4. Migration checklist (do this only after Renovate is verified)\n\nOnce Renovate runs green for a cycle, retire the overlapping automation to avoid **duplicate PRs**.\nStatus as of the Renovate activation for `develop`:\n\n- [x] Removed `.github/dependabot.yml` (Renovate now manages GitHub Actions — see the\n `github-actions` group).\n- [x] Removed `.github/workflows/auto-dependency-updater.yml` (the daily `ncu` PR job).\n- [x] Dropped the `ncu:*` / `update` scripts and the `npm-check-updates` devDependency from the root\n `package.json`, plus `.ncurc.json`.\n- [ ] Keep `04 - Update pnpm Lock` if you still want a manual lockfile-refresh button; otherwise\n Renovate's `lockFileMaintenance` covers it.\n\nUntil every box is ticked, **leave the existing tooling in place** — Renovate only acts once the\nworkflow (or app) from [§3](#3-enabling-renovate) actually runs, so there is no conflict in the meantime.\n\n---\n\n## 🇩🇪 Zusammenfassung\n\n**Empfehlung: Renovate.** Es ist die einzige Lösung, die das KoliBri-Monorepo in _einer_ Konfiguration\nabbildet — verwandte Pakete gruppieren (z. B. alle `@angular/*`), jeden Adapter-Ordner auf seiner\nfixierten Major-Version halten (`angular/v20|v21`, `react*`), sichere Updates automatisch mergen\n(GitHub Actions, `@types/*`) und alles Riskante (Stencil, kern-ux, ESLint-/Angular-Majors) über das\n**Dependency Dashboard** zur manuellen Freigabe leiten.\n\n- **Dependabot** kann zwar gruppieren, braucht aber pro Paketverzeichnis einen eigenen Eintrag\n (≈30 × 4 Branches) und kann ein Paket nicht _pro Ordner_ auf einer Major-Version halten.\n- **npm-check-updates** ist nur ein CLI ohne eigene Automatisierung (läuft heute im Workflow\n `auto-dependency-updater.yml`).\n\nNeu hinzugekommen: `automergeStrategy: merge` folgt der Merge-Commit-Konvention des Repos,\nund die `release/*`-Branches werden auf **Security-only** umgestellt — reguläre npm- und\nGitHub-Actions-Updates werden dort komplett deaktiviert, während Vulnerability-Alert-PRs\nweiterhin (bei Nicht-Major) automatisch mergen.\n\nDie fertige [`renovate.json`](../renovate.json) liegt im Repo-Root (geprüft mit dem offiziellen\n`renovate-config-validator`), und der self-hosted Runner-Workflow\n[`.github/workflows/renovate.yml`](../.github/workflows/renovate.yml) ist ebenfalls committet.\n**Renovate läuft**, sobald der Workflow startet — nachts alle 3 Stunden zwischen 18:00 und 06:00 Uhr\n(Europe/Berlin) per Zeitplan oder jederzeit manuell über den **Run workflow**-Button (Option A); alternativ kann ein Org-Admin die\n[Renovate-GitHub-App](https://github.com/apps/renovate) installieren (Option B). Bis dahin bleibt die\nbestehende Dependabot-/ncu-Automatisierung zuständig; danach greift die Migrations-Checkliste oben.\n",
473
473
  "kind": "doc"
474
474
  },
475
475
  {
@@ -648,14 +648,6 @@
648
648
  "code": "import React from 'react';\n\nimport { KolButtonLink } from '@public-ui/react-v19';\n\nimport type { FC } from 'react';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\nexport const ButtonLinkIcons: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>This sample shows KolButtonLink with icons in different locations.</p>\n\t\t</SampleDescription>\n\n\t\t<SampleBlock id=\"left\" fitContent>\n\t\t\t<KolButtonLink _icons=\"kolicon-house\" _label=\"I am a link with an icon on the left\" />\n\t\t</SampleBlock>\n\t\t<SampleBlock id=\"right\" fitContent>\n\t\t\t<KolButtonLink\n\t\t\t\t_icons={{\n\t\t\t\t\tright: 'kolicon-house',\n\t\t\t\t}}\n\t\t\t\t_label=\"I am a link with an icon on the right\"\n\t\t\t/>\n\t\t</SampleBlock>\n\t\t<SampleBlock id=\"top\" fitContent>\n\t\t\t<KolButtonLink\n\t\t\t\t_icons={{\n\t\t\t\t\ttop: 'kolicon-house',\n\t\t\t\t}}\n\t\t\t\t_label=\"I am a link with an icon at the top\"\n\t\t\t/>\n\t\t</SampleBlock>\n\t\t<SampleBlock id=\"bottom\" fitContent>\n\t\t\t<KolButtonLink\n\t\t\t\t_icons={{\n\t\t\t\t\tbottom: 'kolicon-house',\n\t\t\t\t}}\n\t\t\t\t_label=\"I am a link with icon below\"\n\t\t\t/>\n\t\t</SampleBlock>\n\t\t<SampleBlock id=\"all\" fitContent>\n\t\t\t<KolButtonLink\n\t\t\t\t_icons={{\n\t\t\t\t\ttop: 'kolicon-house',\n\t\t\t\t\tright: 'kolicon-house',\n\t\t\t\t\tbottom: 'kolicon-house',\n\t\t\t\t\tleft: 'kolicon-house',\n\t\t\t\t}}\n\t\t\t\t_label=\"I am a link with all icons\"\n\t\t\t/>\n\t\t</SampleBlock>\n\t\t<SampleBlock id=\"hide-label\" fitContent>\n\t\t\t<KolButtonLink _icons=\"kolicon-house\" _hideLabel _label=\"I am a link with icon only\" />\n\t\t</SampleBlock>\n\t</>\n);\n",
649
649
  "kind": "sample"
650
650
  },
651
- {
652
- "id": "sample/button-link/image",
653
- "group": "button-link",
654
- "name": "image",
655
- "path": "packages/samples/react/src/components/button-link/image.tsx",
656
- "code": "import React from 'react';\n\nimport { KolButtonLink, KolImage } from '@public-ui/react-v19';\n\nimport type { FC } from 'react';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\nexport const ButtonLinkImage: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>This sample shows KolButtonLink with slot-content instead of a label. The sample uses an image as content.</p>\n\t\t</SampleDescription>\n\n\t\t<SampleBlock id=\"image\">\n\t\t\t<KolButtonLink _label=\"I am a link that is rendered as text\" />\n\t\t\t<KolButtonLink _label=\"\">\n\t\t\t\t<KolImage\n\t\t\t\t\t_alt=\"KoliBri design system illustration showing three people working on laptops surrounded by UI components, charts, and the KoliBri hummingbird logo\"\n\t\t\t\t\t_src=\"sample-image.png\"\n\t\t\t\t\tclassName=\"w-image\"\n\t\t\t\t\tslot=\"expert\"\n\t\t\t\t/>\n\t\t\t</KolButtonLink>\n\t\t</SampleBlock>\n\t</>\n);\n",
657
- "kind": "sample"
658
- },
659
651
  {
660
652
  "id": "sample/button/access-key",
661
653
  "group": "button",
@@ -1733,7 +1725,7 @@
1733
1725
  "group": "table",
1734
1726
  "name": "action-and-render",
1735
1727
  "path": "packages/samples/react/src/components/table/action-render.tsx",
1736
- "code": "import type { KoliBriTableCell, KoliBriTableHeaderCellWithLogic } from '@public-ui/components';\nimport { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\ntype ProjectTask = {\n\tid: string;\n\tproject: string;\n\towner: string;\n\treact: string;\n};\n\nconst HEADERS: { horizontal: KoliBriTableHeaderCellWithLogic[][] } = {\n\thorizontal: [\n\t\t[\n\t\t\t{ key: 'project', label: 'Project' },\n\t\t\t{ key: 'owner', label: 'Owner', width: 140 },\n\t\t\t{\n\t\t\t\tlabel: 'ID',\n\t\t\t\tkey: 'id',\n\t\t\t\twidth: 100,\n\n\t\t\t\trender: (_el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\treturn `Index: ${label}`;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'actions',\n\t\t\t\tlabel: 'Actions',\n\t\t\t\twidth: 100,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst simpleRow = row as ProjectTask;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Details',\n\t\t\t\t\t\t\t_icons: 'kolicon-eye',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => alert(`Details: ${simpleRow.id} - ${simpleRow.project}`),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nconst DATA: ProjectTask[] = [\n\t{\n\t\tid: 'T-01',\n\t\tproject: 'Onboarding checklist',\n\t\towner: 'Alex Rivera',\n\t\treact: 'test',\n\t},\n\t{\n\t\tid: 'T-02',\n\t\tproject: 'Accessibility audit',\n\t\towner: 'Jamie Chen',\n\t\treact: 'test',\n\t},\n];\n\nexport const TableActionAndRenderColumns: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>\n\t\t\t\tSimple example using the refactored action column: Actions are defined once in the column header definition using a factory function. Two rows with\n\t\t\t\tinline action buttons demonstrate clean separation between data and UI behavior.\n\t\t\t</p>\n\t\t</SampleDescription>\n\n\t\t<section className=\"w-full\">\n\t\t\t<KolTableStateful _label=\"Tasks with action buttons\" _headers={HEADERS} _data={DATA} className=\"block\" _hasSettingsMenu />\n\t\t</section>\n\t</>\n);\n",
1728
+ "code": "import type { KoliBriTableCell, KoliBriTableHeaderCellWithLogic } from '@public-ui/components';\nimport { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\ntype ProjectTask = {\n\tid: string;\n\tproject: string;\n\towner: string;\n\treact: string;\n};\n\nconst HEADERS: { horizontal: KoliBriTableHeaderCellWithLogic[][] } = {\n\thorizontal: [\n\t\t[\n\t\t\t{ key: 'project', label: 'Project', width: 200 },\n\t\t\t{ key: 'owner', label: 'Owner', width: 140 },\n\t\t\t{\n\t\t\t\tlabel: 'ID',\n\t\t\t\tkey: 'id',\n\t\t\t\twidth: 100,\n\n\t\t\t\trender: (_el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\treturn `Index: ${label}`;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'actions',\n\t\t\t\tlabel: 'Actions',\n\t\t\t\twidth: 100,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst simpleRow = row as ProjectTask;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Details',\n\t\t\t\t\t\t\t_icons: 'kolicon-eye',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => alert(`Details: ${simpleRow.id} - ${simpleRow.project}`),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nconst DATA: ProjectTask[] = [\n\t{\n\t\tid: 'T-01',\n\t\tproject: 'Onboarding checklist',\n\t\towner: 'Alex Rivera',\n\t\treact: 'test',\n\t},\n\t{\n\t\tid: 'T-02',\n\t\tproject: 'Accessibility audit',\n\t\towner: 'Jamie Chen',\n\t\treact: 'test',\n\t},\n];\n\nexport const TableActionAndRenderColumns: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>\n\t\t\t\tSimple example using the refactored action column: Actions are defined once in the column header definition using a factory function. Two rows with\n\t\t\t\tinline action buttons demonstrate clean separation between data and UI behavior.\n\t\t\t</p>\n\t\t</SampleDescription>\n\n\t\t<section className=\"w-full\">\n\t\t\t<KolTableStateful _label=\"Tasks with action buttons\" _headers={HEADERS} _data={DATA} className=\"block\" _hasSettingsMenu />\n\t\t</section>\n\t</>\n);\n",
1737
1729
  "kind": "sample"
1738
1730
  },
1739
1731
  {
@@ -1741,7 +1733,7 @@
1741
1733
  "group": "table",
1742
1734
  "name": "action-columns",
1743
1735
  "path": "packages/samples/react/src/components/table/action-columns.tsx",
1744
- "code": "import type { KoliBriTableHeaderCellWithLogic } from '@public-ui/components';\nimport { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\ntype ProjectTask = {\n\tid: string;\n\tproject: string;\n\towner: string;\n};\n\nconst HEADERS: { horizontal: KoliBriTableHeaderCellWithLogic[][] } = {\n\thorizontal: [\n\t\t[\n\t\t\t{ key: 'id', label: 'ID', width: 80 },\n\t\t\t{ key: 'project', label: 'Project' },\n\t\t\t{ key: 'owner', label: 'Owner', width: 140 },\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'actions',\n\t\t\t\tlabel: 'Actions',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\twidth: 250,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst simpleRow = row as ProjectTask;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Details',\n\t\t\t\t\t\t\t_icons: 'kolicon-eye',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => alert(`Details: ${simpleRow.id} - ${simpleRow.project}`),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Start',\n\t\t\t\t\t\t\t_icons: 'kolicon-chevron-right',\n\t\t\t\t\t\t\t_variant: 'secondary',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => alert(`Start task ${simpleRow.id}`),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'link',\n\t\t\t\t\t\t\t_label: 'View',\n\t\t\t\t\t\t\t_href: `#/back-page?taskId=${simpleRow.id}`,\n\t\t\t\t\t\t\t_target: '_blank',\n\t\t\t\t\t\t\t_icons: 'kolicon-external-link',\n\t\t\t\t\t\t\t_hideLabel: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'externalActions',\n\t\t\t\tlabel: 'External Actions',\n\t\t\t\ttextAlign: 'right',\n\t\t\t\twidth: 280,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst simpleRow = row as ProjectTask;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'link',\n\t\t\t\t\t\t\t_label: 'View on Dashboard',\n\t\t\t\t\t\t\t_href: `#/back-page?taskId=${simpleRow.id}`,\n\t\t\t\t\t\t\t_target: '_blank',\n\t\t\t\t\t\t\t_icons: 'kolicon-external-link',\n\t\t\t\t\t\t\t_hideLabel: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nconst DATA: ProjectTask[] = [\n\t{\n\t\tid: 'T-01',\n\t\tproject: 'Onboarding checklist',\n\t\towner: 'Alex Rivera',\n\t},\n\t{\n\t\tid: 'T-02',\n\t\tproject: 'Accessibility audit',\n\t\towner: 'Jamie Chen',\n\t},\n];\n\nexport const TableActionColumns: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>\n\t\t\t\tSimple example using the refactored action column: Actions are defined once in the column header definition using a factory function. Two rows with\n\t\t\t\tinline action buttons demonstrate clean separation between data and UI behavior.\n\t\t\t</p>\n\t\t</SampleDescription>\n\n\t\t<SampleBlock id=\"action-columns\" className=\"w-full\">\n\t\t\t<KolTableStateful _label=\"Tasks with action buttons\" _headers={HEADERS} _data={DATA} className=\"block\" />\n\t\t</SampleBlock>\n\t</>\n);\n",
1736
+ "code": "import type { KoliBriTableHeaderCellWithLogic } from '@public-ui/components';\nimport { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\ntype ProjectTask = {\n\tid: string;\n\tproject: string;\n\towner: string;\n};\n\nconst HEADERS: { horizontal: KoliBriTableHeaderCellWithLogic[][] } = {\n\thorizontal: [\n\t\t[\n\t\t\t{ key: 'id', label: 'ID', width: 80 },\n\t\t\t{ key: 'project', label: 'Project', width: 200 },\n\t\t\t{ key: 'owner', label: 'Owner', width: 140 },\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'actions',\n\t\t\t\tlabel: 'Actions',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\twidth: 250,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst simpleRow = row as ProjectTask;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Details',\n\t\t\t\t\t\t\t_icons: 'kolicon-eye',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => alert(`Details: ${simpleRow.id} - ${simpleRow.project}`),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Start',\n\t\t\t\t\t\t\t_icons: 'kolicon-chevron-right',\n\t\t\t\t\t\t\t_variant: 'secondary',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => alert(`Start task ${simpleRow.id}`),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'link',\n\t\t\t\t\t\t\t_label: 'View',\n\t\t\t\t\t\t\t_href: `#/back-page?taskId=${simpleRow.id}`,\n\t\t\t\t\t\t\t_target: '_blank',\n\t\t\t\t\t\t\t_icons: 'kolicon-external-link',\n\t\t\t\t\t\t\t_hideLabel: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'externalActions',\n\t\t\t\tlabel: 'External Actions',\n\t\t\t\ttextAlign: 'right',\n\t\t\t\twidth: 280,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst simpleRow = row as ProjectTask;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'link',\n\t\t\t\t\t\t\t_label: 'View on Dashboard',\n\t\t\t\t\t\t\t_href: `#/back-page?taskId=${simpleRow.id}`,\n\t\t\t\t\t\t\t_target: '_blank',\n\t\t\t\t\t\t\t_icons: 'kolicon-external-link',\n\t\t\t\t\t\t\t_hideLabel: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nconst DATA: ProjectTask[] = [\n\t{\n\t\tid: 'T-01',\n\t\tproject: 'Onboarding checklist',\n\t\towner: 'Alex Rivera',\n\t},\n\t{\n\t\tid: 'T-02',\n\t\tproject: 'Accessibility audit',\n\t\towner: 'Jamie Chen',\n\t},\n];\n\nexport const TableActionColumns: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>\n\t\t\t\tSimple example using the refactored action column: Actions are defined once in the column header definition using a factory function. Two rows with\n\t\t\t\tinline action buttons demonstrate clean separation between data and UI behavior.\n\t\t\t</p>\n\t\t</SampleDescription>\n\n\t\t<SampleBlock id=\"action-columns\" className=\"w-full\">\n\t\t\t<KolTableStateful _label=\"Tasks with action buttons\" _headers={HEADERS} _data={DATA} className=\"block\" />\n\t\t</SampleBlock>\n\t</>\n);\n",
1745
1737
  "kind": "sample"
1746
1738
  },
1747
1739
  {
@@ -1749,7 +1741,7 @@
1749
1741
  "group": "table",
1750
1742
  "name": "action-columns-performance",
1751
1743
  "path": "packages/samples/react/src/components/table/action-columns-performance.tsx",
1752
- "code": "import type { KoliBriTableDataType, KoliBriTableHeaderCellWithLogic } from '@public-ui/components';\nimport { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\nconst ROWS_COUNT = 50;\n\ntype DataRow = {\n\tid: number;\n\tname: string;\n\temail: string;\n\tstatus: string;\n};\n\nconst FIRST_NAMES = [\n\t'Max',\n\t'Erika',\n\t'John',\n\t'Jane',\n\t'Peter',\n\t'Anna',\n\t'Michael',\n\t'Sarah',\n\t'Thomas',\n\t'Lisa',\n\t'Daniel',\n\t'Maria',\n\t'Christian',\n\t'Laura',\n\t'Stefan',\n\t'Julia',\n\t'Andreas',\n\t'Sophie',\n\t'Markus',\n\t'Emma',\n];\nconst LAST_NAMES = [\n\t'Mustermann',\n\t'Musterfrau',\n\t'Doe',\n\t'Smith',\n\t'Schmidt',\n\t'Müller',\n\t'Weber',\n\t'Meyer',\n\t'Wagner',\n\t'Becker',\n\t'Fischer',\n\t'Schneider',\n\t'Hoffmann',\n\t'Koch',\n\t'Bauer',\n\t'Richter',\n\t'Klein',\n\t'Wolf',\n\t'Schröder',\n\t'Neumann',\n];\nconst STATUSES = ['active', 'inactive', 'pending'];\n\nconst generateData = (count: number): DataRow[] => {\n\treturn Array.from({ length: count }, (_, i) => {\n\t\tconst firstName = FIRST_NAMES[i % FIRST_NAMES.length];\n\t\tconst lastName = LAST_NAMES[Math.floor(i / FIRST_NAMES.length) % LAST_NAMES.length];\n\t\tconst status = STATUSES[i % STATUSES.length];\n\t\tconst id = i + 1;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tname: `${firstName} ${lastName}`,\n\t\t\temail: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${id > 20 ? id : ''}@example.com`,\n\t\t\tstatus,\n\t\t};\n\t});\n};\n\nconst DATA: DataRow[] = generateData(ROWS_COUNT);\n\nconst HEADERS: { horizontal: KoliBriTableHeaderCellWithLogic[][] } = {\n\thorizontal: [\n\t\t[\n\t\t\t{\n\t\t\t\tkey: 'id',\n\t\t\t\tlabel: 'ID',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\twidth: 60,\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => (data0 as unknown as DataRow).id - (data1 as unknown as DataRow).id,\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'name',\n\t\t\t\tlabel: 'Name',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t\t\t(data0 as unknown as DataRow).name.localeCompare((data1 as unknown as DataRow).name, 'de'),\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'email',\n\t\t\t\tlabel: 'E-Mail',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t\t\t(data0 as unknown as DataRow).email.localeCompare((data1 as unknown as DataRow).email, 'de'),\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'status',\n\t\t\t\tlabel: 'Status',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 100,\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t\t\t(data0 as unknown as DataRow).status.localeCompare((data1 as unknown as DataRow).status, 'de'),\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'actions',\n\t\t\t\tlabel: 'Aktionen',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 180,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst dataRow = row as unknown as DataRow;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Bearbeiten',\n\t\t\t\t\t\t\t_icons: 'kolicon-eye',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_tooltipAlign: 'top',\n\t\t\t\t\t\t\t_variant: 'secondary',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\t\tconsole.log('Edit clicked for:', dataRow);\n\t\t\t\t\t\t\t\t\talert(`Bearbeiten: ${dataRow.name}`);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Löschen',\n\t\t\t\t\t\t\t_icons: 'kolicon-alert-error',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_tooltipAlign: 'top',\n\t\t\t\t\t\t\t_variant: 'danger',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\t\tconsole.log('Delete clicked for:', dataRow);\n\t\t\t\t\t\t\t\t\talert(`Löschen: ${dataRow.name}`);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Details anzeigen',\n\t\t\t\t\t\t\t_icons: 'kolicon-alert-info',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_tooltipAlign: 'top',\n\t\t\t\t\t\t\t_variant: 'normal',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\t\tconsole.log('Details clicked for:', dataRow);\n\t\t\t\t\t\t\t\t\talert(`Details: ${JSON.stringify(dataRow, null, 2)}`);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nexport const TableActionColumnPerformance: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>\n\t\t\t\tPerformance demo: {ROWS_COUNT} rows with action buttons defined once in the column header using the refactored approach. The factory function generates\n\t\t\t\tactions for each row on demand, eliminating redundant data and improving maintainability.\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\tActions stay type-safe with <code>ActionColumnPropType</code> (ButtonProps or LinkProps), and no custom render functions are needed.\n\t\t\t</p>\n\t\t</SampleDescription>\n\n\t\t<section className=\"w-full\">\n\t\t\t<KolTableStateful _label=\"Benutzerverwaltung mit Action-Spalte\" _headers={HEADERS} _data={DATA} className=\"block\" />\n\t\t</section>\n\t</>\n);\n",
1744
+ "code": "import type { KoliBriTableDataType, KoliBriTableHeaderCellWithLogic } from '@public-ui/components';\nimport { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\nconst ROWS_COUNT = 50;\n\ntype DataRow = {\n\tid: number;\n\tname: string;\n\temail: string;\n\tstatus: string;\n};\n\nconst FIRST_NAMES = [\n\t'Max',\n\t'Erika',\n\t'John',\n\t'Jane',\n\t'Peter',\n\t'Anna',\n\t'Michael',\n\t'Sarah',\n\t'Thomas',\n\t'Lisa',\n\t'Daniel',\n\t'Maria',\n\t'Christian',\n\t'Laura',\n\t'Stefan',\n\t'Julia',\n\t'Andreas',\n\t'Sophie',\n\t'Markus',\n\t'Emma',\n];\nconst LAST_NAMES = [\n\t'Mustermann',\n\t'Musterfrau',\n\t'Doe',\n\t'Smith',\n\t'Schmidt',\n\t'Müller',\n\t'Weber',\n\t'Meyer',\n\t'Wagner',\n\t'Becker',\n\t'Fischer',\n\t'Schneider',\n\t'Hoffmann',\n\t'Koch',\n\t'Bauer',\n\t'Richter',\n\t'Klein',\n\t'Wolf',\n\t'Schröder',\n\t'Neumann',\n];\nconst STATUSES = ['active', 'inactive', 'pending'];\n\nconst generateData = (count: number): DataRow[] => {\n\treturn Array.from({ length: count }, (_, i) => {\n\t\tconst firstName = FIRST_NAMES[i % FIRST_NAMES.length];\n\t\tconst lastName = LAST_NAMES[Math.floor(i / FIRST_NAMES.length) % LAST_NAMES.length];\n\t\tconst status = STATUSES[i % STATUSES.length];\n\t\tconst id = i + 1;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tname: `${firstName} ${lastName}`,\n\t\t\temail: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${id > 20 ? id : ''}@example.com`,\n\t\t\tstatus,\n\t\t};\n\t});\n};\n\nconst DATA: DataRow[] = generateData(ROWS_COUNT);\n\nconst HEADERS: { horizontal: KoliBriTableHeaderCellWithLogic[][] } = {\n\thorizontal: [\n\t\t[\n\t\t\t{\n\t\t\t\tkey: 'id',\n\t\t\t\tlabel: 'ID',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\twidth: 60,\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => (data0 as unknown as DataRow).id - (data1 as unknown as DataRow).id,\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'name',\n\t\t\t\tlabel: 'Name',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t\t\t(data0 as unknown as DataRow).name.localeCompare((data1 as unknown as DataRow).name, 'de'),\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t\twidth: 250,\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'email',\n\t\t\t\tlabel: 'E-Mail',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t\t\t(data0 as unknown as DataRow).email.localeCompare((data1 as unknown as DataRow).email, 'de'),\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t\twidth: 350,\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'status',\n\t\t\t\tlabel: 'Status',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 100,\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t\t\t(data0 as unknown as DataRow).status.localeCompare((data1 as unknown as DataRow).status, 'de'),\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: 'action',\n\t\t\t\tkey: 'actions',\n\t\t\t\tlabel: 'Aktionen',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 180,\n\t\t\t\tactions: (row) => {\n\t\t\t\t\tconst dataRow = row as unknown as DataRow;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Bearbeiten',\n\t\t\t\t\t\t\t_icons: 'kolicon-eye',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_tooltipAlign: 'top',\n\t\t\t\t\t\t\t_variant: 'secondary',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\t\tconsole.log('Edit clicked for:', dataRow);\n\t\t\t\t\t\t\t\t\talert(`Bearbeiten: ${dataRow.name}`);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Löschen',\n\t\t\t\t\t\t\t_icons: 'kolicon-alert-error',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_tooltipAlign: 'top',\n\t\t\t\t\t\t\t_variant: 'danger',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\t\tconsole.log('Delete clicked for:', dataRow);\n\t\t\t\t\t\t\t\t\talert(`Löschen: ${dataRow.name}`);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t_label: 'Details anzeigen',\n\t\t\t\t\t\t\t_icons: 'kolicon-alert-info',\n\t\t\t\t\t\t\t_hideLabel: true,\n\t\t\t\t\t\t\t_tooltipAlign: 'top',\n\t\t\t\t\t\t\t_variant: 'normal',\n\t\t\t\t\t\t\t_on: {\n\t\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\t\tconsole.log('Details clicked for:', dataRow);\n\t\t\t\t\t\t\t\t\talert(`Details: ${JSON.stringify(dataRow, null, 2)}`);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nexport const TableActionColumnPerformance: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>\n\t\t\t\tPerformance demo: {ROWS_COUNT} rows with action buttons defined once in the column header using the refactored approach. The factory function generates\n\t\t\t\tactions for each row on demand, eliminating redundant data and improving maintainability.\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\tActions stay type-safe with <code>ActionColumnPropType</code> (ButtonProps or LinkProps), and no custom render functions are needed.\n\t\t\t</p>\n\t\t</SampleDescription>\n\n\t\t<section className=\"w-full\">\n\t\t\t<KolTableStateful _label=\"Benutzerverwaltung mit Action-Spalte\" _headers={HEADERS} _data={DATA} className=\"block\" />\n\t\t</section>\n\t</>\n);\n",
1753
1745
  "kind": "sample"
1754
1746
  },
1755
1747
  {
@@ -1829,7 +1821,7 @@
1829
1821
  "group": "table",
1830
1822
  "name": "multi-sort",
1831
1823
  "path": "packages/samples/react/src/components/table/multi-sort.tsx",
1832
- "code": "import type { FC } from 'react';\nimport React, { useState } from 'react';\n\nimport type { KoliBriTableDataType, KoliBriTableHeaderCellWithLogic, KoliBriTableHeaders } from '@public-ui/components';\nimport { KolButtonLink, KolTableStateful } from '@public-ui/react-v19';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\nconst DATE_FORMATTER = Intl.DateTimeFormat('de-DE', {\n\tday: '2-digit',\n\tmonth: '2-digit',\n\tyear: 'numeric',\n});\n\ntype BacklogEntry = {\n\tdate: Date;\n\tassignee: string;\n\tdepartment: string;\n\tpriority: 'High' | 'Medium' | 'Low';\n\tstatus: 'Blocked' | 'In progress' | 'Ready';\n\topenTickets: number;\n};\n\nconst ASSIGNEES = ['Devon Chen', 'Fatima Alvi', 'Leon Köhler', 'Mila Schmidt', 'Sven Lindholm'];\nconst DEPARTMENTS = ['Customer Service', 'Digital Services', 'Infrastructure', 'Municipal Office'];\nconst PRIORITY_SEQUENCE: BacklogEntry['priority'][] = ['High', 'Medium', 'Low'];\nconst STATUS_SEQUENCE: BacklogEntry['status'][] = ['Blocked', 'In progress', 'Ready'];\n\nconst PRIORITY_ORDER = PRIORITY_SEQUENCE.reduce<Record<BacklogEntry['priority'], number>>(\n\t(order, priority, index) => {\n\t\torder[priority] = index;\n\t\treturn order;\n\t},\n\t{} as Record<BacklogEntry['priority'], number>,\n);\n\nconst STATUS_ORDER = STATUS_SEQUENCE.reduce<Record<BacklogEntry['status'], number>>(\n\t(order, status, index) => {\n\t\torder[status] = index;\n\t\treturn order;\n\t},\n\t{} as Record<BacklogEntry['status'], number>,\n);\n\nconst BACKLOG_DATA: BacklogEntry[] = Array.from({ length: 15 }).map((_, index) => ({\n\tdate: new Date(Date.now() - index * 1000 * 60 * 60 * 24),\n\tassignee: ASSIGNEES[index % ASSIGNEES.length],\n\tdepartment: DEPARTMENTS[index % DEPARTMENTS.length],\n\tpriority: PRIORITY_SEQUENCE[index % PRIORITY_SEQUENCE.length],\n\tstatus: STATUS_SEQUENCE[index % STATUS_SEQUENCE.length],\n\topenTickets: (index * 3) % 11,\n}));\n\nconst TABLE_HEADER_CELLS: KoliBriTableHeaderCellWithLogic[] = [\n\t{\n\t\tlabel: 'Assignee',\n\t\tkey: 'assignee',\n\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t(data0 as BacklogEntry).assignee.localeCompare((data1 as BacklogEntry).assignee, 'de'),\n\t\tsortDirection: 'ASC',\n\t},\n\t{\n\t\tlabel: 'Department',\n\t\tkey: 'department',\n\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t(data0 as BacklogEntry).department.localeCompare((data1 as BacklogEntry).department, 'de'),\n\t},\n\t{\n\t\tlabel: 'Priority',\n\t\tkey: 'priority',\n\n\t\ttextAlign: 'center',\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\tPRIORITY_ORDER[(data0 as BacklogEntry).priority] - PRIORITY_ORDER[(data1 as BacklogEntry).priority],\n\t\tsortDirection: 'DESC',\n\t},\n\t{\n\t\tlabel: 'Status',\n\t\tkey: 'status',\n\n\t\ttextAlign: 'center',\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\tSTATUS_ORDER[(data0 as BacklogEntry).status] - STATUS_ORDER[(data1 as BacklogEntry).status],\n\t},\n\t{\n\t\tlabel: 'Open tickets',\n\t\tkey: 'openTickets',\n\n\t\ttextAlign: 'right',\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => (data0 as BacklogEntry).openTickets - (data1 as BacklogEntry).openTickets,\n\t},\n\t{\n\t\tlabel: 'Last updated',\n\t\tkey: 'date',\n\n\t\ttextAlign: 'center',\n\t\trender: (_el, _cell, tuple) => DATE_FORMATTER.format((tuple as BacklogEntry).date),\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => {\n\t\t\tif ((data0 as BacklogEntry).date < (data1 as BacklogEntry).date) return -1;\n\t\t\telse if ((data1 as BacklogEntry).date < (data0 as BacklogEntry).date) return 1;\n\t\t\telse return 0;\n\t\t},\n\t},\n];\n\nconst HEADERS_HORIZONTAL: KoliBriTableHeaders = {\n\thorizontal: [TABLE_HEADER_CELLS],\n};\n\nconst HEADERS_VERTICAL: KoliBriTableHeaders = {\n\tvertical: [TABLE_HEADER_CELLS],\n};\n\nexport const MultiSortTable: FC = () => {\n\tconst [verticallHeader, setVerticalHeader] = useState(HEADERS_VERTICAL);\n\tconst [horizontalHeader, setHorizontalHeader] = useState(HEADERS_HORIZONTAL);\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>\n\t\t\t\t\tMulti-sort allows layering multiple column sorts at once. Compare the compact examples with the project backlog showcase to see how the sort order\n\t\t\t\t\tindicator helps track complex prioritisation.\n\t\t\t\t</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<section className=\"w-full grid gap-6\">\n\t\t\t\t<SampleBlock id=\"vertical\" heading=\"Vertical\">\n\t\t\t\t\t<KolButtonLink _label=\"Reset Table\" _on={{ onClick: () => setVerticalHeader({ vertical: [[...TABLE_HEADER_CELLS]] }) }}></KolButtonLink>\n\t\t\t\t\t<KolTableStateful\n\t\t\t\t\t\t_label=\"Sort Table with Order and Date\"\n\t\t\t\t\t\t_data={BACKLOG_DATA.slice(0, 10)}\n\t\t\t\t\t\t_headers={verticallHeader}\n\t\t\t\t\t\tclassName=\"block\"\n\t\t\t\t\t\t_allowMultiSort={true}\n\t\t\t\t\t/>\n\t\t\t\t</SampleBlock>\n\t\t\t\t<SampleBlock id=\"horizontal\" heading=\"Horizontal\">\n\t\t\t\t\t<KolButtonLink _label=\"Reset Table\" _on={{ onClick: () => setHorizontalHeader({ horizontal: [[...TABLE_HEADER_CELLS]] }) }}></KolButtonLink>\n\t\t\t\t\t<KolTableStateful _label=\"Sort Table with Order and Date\" _data={BACKLOG_DATA} _headers={horizontalHeader} className=\"block\" _allowMultiSort={true} />\n\t\t\t\t</SampleBlock>\n\t\t\t</section>\n\t\t</>\n\t);\n};\n",
1824
+ "code": "import type { FC } from 'react';\nimport React, { useState } from 'react';\n\nimport type { KoliBriTableDataType, KoliBriTableHeaderCellWithLogic, KoliBriTableHeaders } from '@public-ui/components';\nimport { KolButtonLink, KolTableStateful } from '@public-ui/react-v19';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\nconst DATE_FORMATTER = Intl.DateTimeFormat('de-DE', {\n\tday: '2-digit',\n\tmonth: '2-digit',\n\tyear: 'numeric',\n});\n\ntype BacklogEntry = {\n\tdate: Date;\n\tassignee: string;\n\tdepartment: string;\n\tpriority: 'High' | 'Medium' | 'Low';\n\tstatus: 'Blocked' | 'In progress' | 'Ready';\n\topenTickets: number;\n};\n\nconst ASSIGNEES = ['Devon Chen', 'Fatima Alvi', 'Leon Köhler', 'Mila Schmidt', 'Sven Lindholm'];\nconst DEPARTMENTS = ['Customer Service', 'Digital Services', 'Infrastructure', 'Municipal Office'];\nconst PRIORITY_SEQUENCE: BacklogEntry['priority'][] = ['High', 'Medium', 'Low'];\nconst STATUS_SEQUENCE: BacklogEntry['status'][] = ['Blocked', 'In progress', 'Ready'];\n\nconst PRIORITY_ORDER = PRIORITY_SEQUENCE.reduce<Record<BacklogEntry['priority'], number>>(\n\t(order, priority, index) => {\n\t\torder[priority] = index;\n\t\treturn order;\n\t},\n\t{} as Record<BacklogEntry['priority'], number>,\n);\n\nconst STATUS_ORDER = STATUS_SEQUENCE.reduce<Record<BacklogEntry['status'], number>>(\n\t(order, status, index) => {\n\t\torder[status] = index;\n\t\treturn order;\n\t},\n\t{} as Record<BacklogEntry['status'], number>,\n);\n\nconst BACKLOG_DATA: BacklogEntry[] = Array.from({ length: 15 }).map((_, index) => ({\n\tdate: new Date(Date.now() - index * 1000 * 60 * 60 * 24),\n\tassignee: ASSIGNEES[index % ASSIGNEES.length],\n\tdepartment: DEPARTMENTS[index % DEPARTMENTS.length],\n\tpriority: PRIORITY_SEQUENCE[index % PRIORITY_SEQUENCE.length],\n\tstatus: STATUS_SEQUENCE[index % STATUS_SEQUENCE.length],\n\topenTickets: (index * 3) % 11,\n}));\n\nconst TABLE_HEADER_CELLS: KoliBriTableHeaderCellWithLogic[] = [\n\t{\n\t\tlabel: 'Assignee',\n\t\tkey: 'assignee',\n\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t(data0 as BacklogEntry).assignee.localeCompare((data1 as BacklogEntry).assignee, 'de'),\n\t\tsortDirection: 'ASC',\n\t\twidth: 200,\n\t},\n\t{\n\t\tlabel: 'Department',\n\t\tkey: 'department',\n\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\t(data0 as BacklogEntry).department.localeCompare((data1 as BacklogEntry).department, 'de'),\n\t\twidth: 200,\n\t},\n\t{\n\t\tlabel: 'Priority',\n\t\tkey: 'priority',\n\n\t\ttextAlign: 'center',\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\tPRIORITY_ORDER[(data0 as BacklogEntry).priority] - PRIORITY_ORDER[(data1 as BacklogEntry).priority],\n\t\tsortDirection: 'DESC',\n\t\twidth: 150,\n\t},\n\t{\n\t\tlabel: 'Status',\n\t\tkey: 'status',\n\n\t\ttextAlign: 'center',\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) =>\n\t\t\tSTATUS_ORDER[(data0 as BacklogEntry).status] - STATUS_ORDER[(data1 as BacklogEntry).status],\n\t\twidth: 150,\n\t},\n\t{\n\t\tlabel: 'Open tickets',\n\t\tkey: 'openTickets',\n\n\t\ttextAlign: 'right',\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => (data0 as BacklogEntry).openTickets - (data1 as BacklogEntry).openTickets,\n\t\twidth: 100,\n\t},\n\t{\n\t\tlabel: 'Last updated',\n\t\tkey: 'date',\n\n\t\ttextAlign: 'center',\n\t\trender: (_el, _cell, tuple) => DATE_FORMATTER.format((tuple as BacklogEntry).date),\n\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => {\n\t\t\tif ((data0 as BacklogEntry).date < (data1 as BacklogEntry).date) return -1;\n\t\t\telse if ((data1 as BacklogEntry).date < (data0 as BacklogEntry).date) return 1;\n\t\t\telse return 0;\n\t\t},\n\t\twidth: 150,\n\t},\n];\n\nconst HEADERS_HORIZONTAL: KoliBriTableHeaders = {\n\thorizontal: [TABLE_HEADER_CELLS],\n};\n\nconst HEADERS_VERTICAL: KoliBriTableHeaders = {\n\tvertical: [TABLE_HEADER_CELLS],\n};\n\nexport const MultiSortTable: FC = () => {\n\tconst [verticallHeader, setVerticalHeader] = useState(HEADERS_VERTICAL);\n\tconst [horizontalHeader, setHorizontalHeader] = useState(HEADERS_HORIZONTAL);\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>\n\t\t\t\t\tMulti-sort allows layering multiple column sorts at once. Compare the compact examples with the project backlog showcase to see how the sort order\n\t\t\t\t\tindicator helps track complex prioritisation.\n\t\t\t\t</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<section className=\"w-full grid gap-6\">\n\t\t\t\t<SampleBlock id=\"vertical\" heading=\"Vertical\">\n\t\t\t\t\t<KolButtonLink _label=\"Reset Table\" _on={{ onClick: () => setVerticalHeader({ vertical: [[...TABLE_HEADER_CELLS]] }) }}></KolButtonLink>\n\t\t\t\t\t<KolTableStateful\n\t\t\t\t\t\t_label=\"Sort Table with Order and Date\"\n\t\t\t\t\t\t_data={BACKLOG_DATA.slice(0, 10)}\n\t\t\t\t\t\t_headers={verticallHeader}\n\t\t\t\t\t\tclassName=\"block\"\n\t\t\t\t\t\t_allowMultiSort={true}\n\t\t\t\t\t/>\n\t\t\t\t</SampleBlock>\n\t\t\t\t<SampleBlock id=\"horizontal\" heading=\"Horizontal\">\n\t\t\t\t\t<KolButtonLink _label=\"Reset Table\" _on={{ onClick: () => setHorizontalHeader({ horizontal: [[...TABLE_HEADER_CELLS]] }) }}></KolButtonLink>\n\t\t\t\t\t<KolTableStateful _label=\"Sort Table with Order and Date\" _data={BACKLOG_DATA} _headers={horizontalHeader} className=\"block\" _allowMultiSort={true} />\n\t\t\t\t</SampleBlock>\n\t\t\t</section>\n\t\t</>\n\t);\n};\n",
1833
1825
  "kind": "sample"
1834
1826
  },
1835
1827
  {
@@ -1853,7 +1845,7 @@
1853
1845
  "group": "table",
1854
1846
  "name": "predefined-settings",
1855
1847
  "path": "packages/samples/react/src/components/table/predefined-settings.tsx",
1856
- "code": "import { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\nconst DATA = [{ columnA: 'Column A', columnB: 'Column B', columnC: 'Column C' }];\n\nexport const PredefinedSettings: FC = () => {\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This example shows the table with predefined settings.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<KolTableStateful\n\t\t\t\t_label=\"Table for demonstration purposes with predefined settings\"\n\t\t\t\t_hasSettingsMenu\n\t\t\t\t_headers={{\n\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{ key: 'columnC', visible: true, label: 'Column C', width: 450 },\n\t\t\t\t\t\t\t{ key: 'columnB', visible: true, label: 'Column B', width: 200 },\n\t\t\t\t\t\t\t{ key: 'columnA', visible: false, label: 'Column A', width: 160 },\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t}}\n\t\t\t\t_data={DATA}\n\t\t\t\tclassName=\"block\"\n\t\t\t\tstyle={{ maxWidth: '600px' }}\n\t\t\t/>\n\t\t</>\n\t);\n};\n",
1848
+ "code": "import { KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\nconst DATA = [{ columnA: 'Column A', columnB: 'Column B', columnC: 'Column C' }];\n\nexport const PredefinedSettings: FC = () => {\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This example shows the table with predefined settings.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<KolTableStateful\n\t\t\t\t_label=\"Table for demonstration purposes with predefined settings\"\n\t\t\t\t_hasSettingsMenu\n\t\t\t\t_headers={{\n\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{ key: 'columnC', visible: true, label: 'Column C', width: 450 },\n\t\t\t\t\t\t\t{ key: 'columnB', visible: true, label: 'Column B', width: 200 },\n\t\t\t\t\t\t\t{ key: 'columnA', visible: false, label: 'Column A', width: 160 },\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t}}\n\t\t\t\t_data={DATA}\n\t\t\t\tclassName=\"block\"\n\t\t\t/>\n\t\t</>\n\t);\n};\n",
1857
1849
  "kind": "sample"
1858
1850
  },
1859
1851
  {
@@ -1861,7 +1853,7 @@
1861
1853
  "group": "table",
1862
1854
  "name": "render-cell",
1863
1855
  "path": "packages/samples/react/src/components/table/render-cell.tsx",
1864
- "code": "import type { FC } from 'react';\nimport React from 'react';\n\nimport { createReactRenderElement, KolButton, KolInputText, KolTableStateful } from '@public-ui/react-v19';\n\nimport { getRoot } from '../../shares/react-roots';\nimport { SampleDescription } from '../SampleDescription';\nimport { DATE_FORMATTER } from './formatter';\n\nimport type { IconsPropType, KoliBriTableCell, KoliBriTableHeaders } from '@public-ui/components';\nimport { useAlert } from '../../hooks/useAlert';\n\ntype Data = {\n\torder: number;\n\tdate: Date;\n\tshipped: boolean;\n};\n\n/**\n * Generates n data entries with random dates and shipping status\n * @param n - Number of data entries to generate\n * @returns Array of Data objects\n */\nfunction generateDataEntries(n: number): Data[] {\n\tconst entries: Data[] = [];\n\tconst startDate = new Date('1970-01-01').getTime();\n\tconst endDate = new Date('2025-12-31').getTime();\n\n\tfor (let i = 0; i < n; i++) {\n\t\tconst randomTimestamp = startDate + Math.random() * (endDate - startDate);\n\t\tentries.push({\n\t\t\torder: i,\n\t\t\tshipped: Math.random() > 0.5,\n\t\t\tdate: new Date(randomTimestamp),\n\t\t});\n\t}\n\n\treturn entries;\n}\n\nconst DATA: Data[] = generateDataEntries(100);\n\nfunction KolButtonWrapper({ label, icons }: { label: string; icons: IconsPropType }) {\n\tconst { dummyClickEventHandler } = useAlert();\n\n\tconst dummyEventHandler = {\n\t\tonClick: dummyClickEventHandler,\n\t};\n\n\treturn <KolButton _label={label} _icons={icons} _on={dummyEventHandler} />;\n}\n\nconst HEADERS: KoliBriTableHeaders = {\n\thorizontal: [\n\t\t[\n\t\t\t{\n\t\t\t\tlabel: '#',\n\t\t\t\tkey: 'order',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 100,\n\n\t\t\t\t/* Example 1: Use render return value to format data */\n\t\t\t\trender: (_el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\treturn `Index: ${label}`;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Status',\n\t\t\t\tkey: 'shipped',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 100,\n\n\t\t\t\t/* Example 2: Simple render function using textContent */\n\t\t\t\trender: (el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\tconst element = el as HTMLElement;\n\t\t\t\t\tif (label) {\n\t\t\t\t\t\telement.textContent = `Order has been dispatched 🚚`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\telement.textContent = `Order pending 📦`;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Date (string)',\n\t\t\t\tkey: 'date',\n\t\t\t\twidth: 200,\n\t\t\t\ttextAlign: 'center',\n\n\t\t\t\t/* Example 3: Render function using innerHTML. ⚠️Make sure to sanitize data to avoid XSS. */\n\t\t\t\trender: (el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\t(el as HTMLElement).innerHTML = `<strong>${DATE_FORMATTER.format(label as unknown as Date)}</strong>`;\n\t\t\t\t},\n\t\t\t\tcompareFn: (data0, data1) => (data0 as Data).date.getTime() - (data1 as Data).date.getTime(),\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Action (react)',\n\t\t\t\tkey: 'action',\n\t\t\t\twidth: 200,\n\n\t\t\t\t/* Example 4: Render function using React */\n\t\t\t\trender: (el) => {\n\t\t\t\t\tgetRoot(createReactRenderElement(el)).render(\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: `grid`,\n\t\t\t\t\t\t\t\tgridAutoFlow: `column`,\n\t\t\t\t\t\t\t\talignItems: `end`,\n\t\t\t\t\t\t\t\tgap: `1rem`,\n\t\t\t\t\t\t\t\tmaxWidth: `400px`,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<KolInputText _label=\"Input\" />\n\t\t\t\t\t\t\t<KolButtonWrapper label=\"Save\" icons={{ left: 'kolicon-check' }} />\n\t\t\t\t\t\t</div>,\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nexport const TableRenderCell: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>This sample shows KolTableStateful using React render functions for the cell contents.</p>\n\t\t</SampleDescription>\n\n\t\t<KolTableStateful _label=\"Sort by date column\" _data={DATA} _headers={HEADERS} className=\"w-full\" _hasSettingsMenu />\n\t</>\n);\n",
1856
+ "code": "import type { FC } from 'react';\nimport React from 'react';\n\nimport { createReactRenderElement, KolButton, KolInputText, KolTableStateful } from '@public-ui/react-v19';\n\nimport { getRoot } from '../../shares/react-roots';\nimport { SampleDescription } from '../SampleDescription';\nimport { DATE_FORMATTER } from './formatter';\n\nimport type { IconsPropType, KoliBriTableCell, KoliBriTableHeaders } from '@public-ui/components';\nimport { useAlert } from '../../hooks/useAlert';\n\ntype Data = {\n\torder: number;\n\tdate: Date;\n\tshipped: boolean;\n};\n\n/**\n * Generates n data entries with random dates and shipping status\n * @param n - Number of data entries to generate\n * @returns Array of Data objects\n */\nfunction generateDataEntries(n: number): Data[] {\n\tconst entries: Data[] = [];\n\tconst startDate = new Date('1970-01-01').getTime();\n\tconst endDate = new Date('2025-12-31').getTime();\n\n\tfor (let i = 0; i < n; i++) {\n\t\tconst randomTimestamp = startDate + Math.random() * (endDate - startDate);\n\t\tentries.push({\n\t\t\torder: i,\n\t\t\tshipped: Math.random() > 0.5,\n\t\t\tdate: new Date(randomTimestamp),\n\t\t});\n\t}\n\n\treturn entries;\n}\n\nconst DATA: Data[] = generateDataEntries(100);\n\nfunction KolButtonWrapper({ label, icons }: { label: string; icons: IconsPropType }) {\n\tconst { dummyClickEventHandler } = useAlert();\n\n\tconst dummyEventHandler = {\n\t\tonClick: dummyClickEventHandler,\n\t};\n\n\treturn <KolButton _label={label} _icons={icons} _on={dummyEventHandler} />;\n}\n\nconst HEADERS: KoliBriTableHeaders = {\n\thorizontal: [\n\t\t[\n\t\t\t{\n\t\t\t\tlabel: '#',\n\t\t\t\tkey: 'order',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 100,\n\n\t\t\t\t/* Example 1: Use render return value to format data */\n\t\t\t\trender: (_el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\treturn `Index: ${label}`;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Status',\n\t\t\t\tkey: 'shipped',\n\t\t\t\ttextAlign: 'center',\n\t\t\t\twidth: 100,\n\n\t\t\t\t/* Example 2: Simple render function using textContent */\n\t\t\t\trender: (el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\tconst element = el as HTMLElement;\n\t\t\t\t\tif (label) {\n\t\t\t\t\t\telement.textContent = `Order has been dispatched 🚚`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\telement.textContent = `Order pending 📦`;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Date (string)',\n\t\t\t\tkey: 'date',\n\t\t\t\twidth: 150,\n\t\t\t\ttextAlign: 'center',\n\n\t\t\t\t/* Example 3: Render function using innerHTML. ⚠️Make sure to sanitize data to avoid XSS. */\n\t\t\t\trender: (el, cell: KoliBriTableCell) => {\n\t\t\t\t\tconst { label } = cell as { label: string };\n\t\t\t\t\t(el as HTMLElement).innerHTML = `<strong>${DATE_FORMATTER.format(label as unknown as Date)}</strong>`;\n\t\t\t\t},\n\t\t\t\tcompareFn: (data0, data1) => (data0 as Data).date.getTime() - (data1 as Data).date.getTime(),\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Action (react)',\n\t\t\t\tkey: 'action',\n\t\t\t\twidth: 250,\n\n\t\t\t\t/* Example 4: Render function using React */\n\t\t\t\trender: (el) => {\n\t\t\t\t\tgetRoot(createReactRenderElement(el)).render(\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: `grid`,\n\t\t\t\t\t\t\t\tgridAutoFlow: `column`,\n\t\t\t\t\t\t\t\talignItems: `end`,\n\t\t\t\t\t\t\t\tgap: `4px`,\n\t\t\t\t\t\t\t\twidth: `190px`,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<KolInputText _label=\"Input\" />\n\t\t\t\t\t\t\t<KolButtonWrapper label=\"Save\" icons={{ left: 'kolicon-check' }} />\n\t\t\t\t\t\t</div>,\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t],\n};\n\nexport const TableRenderCell: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>This sample shows KolTableStateful using React render functions for the cell contents.</p>\n\t\t</SampleDescription>\n\n\t\t<KolTableStateful _label=\"Sort by date column\" _data={DATA} _headers={HEADERS} className=\"w-full\" _hasSettingsMenu />\n\t</>\n);\n",
1865
1857
  "kind": "sample"
1866
1858
  },
1867
1859
  {
@@ -1917,7 +1909,7 @@
1917
1909
  "group": "table",
1918
1910
  "name": "stateful-settings-persistence",
1919
1911
  "path": "packages/samples/react/src/components/table/stateful-settings-persistence.tsx",
1920
- "code": "import type { KoliBriTableDataType, KoliBriTableHeaders } from '@public-ui/components';\nimport { KolButton, KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React, { useState } from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\ntype UserRow = {\n\tid: string;\n\tname: string;\n\tteam: string;\n\temail: string;\n\tstatus: string;\n};\n\nconst DATA: UserRow[] = [\n\t{ id: 'U-001', name: 'Andrea Schmidt', team: 'Design', email: 'andrea@example.org', status: 'Active' },\n\t{ id: 'U-002', name: 'Boris Klein', team: 'Engineering', email: 'boris@example.org', status: 'Active' },\n\t{ id: 'U-003', name: 'Chiara Russo', team: 'Support', email: 'chiara@example.org', status: 'On leave' },\n\t{ id: 'U-004', name: 'Dmitri Volkov', team: 'Engineering', email: 'dmitri@example.org', status: 'Active' },\n\t{ id: 'U-005', name: 'Elena Costa', team: 'Design', email: 'elena@example.org', status: 'On leave' },\n\t{ id: 'U-006', name: 'Farid Haddad', team: 'Support', email: 'farid@example.org', status: 'Active' },\n];\n\nconst compareByKey =\n\t(key: keyof UserRow) =>\n\t(data0: KoliBriTableDataType, data1: KoliBriTableDataType, direction = 'ASC') => {\n\t\tconst result = String((data0 as UserRow)[key]).localeCompare(String((data1 as UserRow)[key]));\n\t\treturn direction === 'DESC' ? -result : result;\n\t};\n\n// Intentionally created inline on every render so the `_headers` prop receives a fresh object\n// reference each time. This mirrors the common React pattern of non-memoized headers and verifies\n// that applied settings survive parent re-renders as long as the column keys stay the same (#10344).\nconst buildHeaders = (): KoliBriTableHeaders => ({\n\thorizontal: [\n\t\t[\n\t\t\t{ key: 'id', label: 'ID', visible: true, width: 120 },\n\t\t\t{ key: 'name', label: 'Name', visible: true, width: 240, compareFn: compareByKey('name') },\n\t\t\t{ key: 'team', label: 'Team', visible: true, width: 200, compareFn: compareByKey('team') },\n\t\t\t{ key: 'email', label: 'E-Mail', visible: true, width: 280 },\n\t\t\t{ key: 'status', label: 'Status', visible: true, width: 160, compareFn: compareByKey('status') },\n\t\t],\n\t],\n});\n\nexport const TableStatefulSettingsPersistence: FC = () => {\n\tconst [renderCount, setRenderCount] = useState(0);\n\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>\n\t\t\t\t\tThis example demonstrates that column settings applied via the settings menu are remembered. Open the settings menu and change a column&apos;s width,\n\t\t\t\t\tvisibility or order and apply. Then sort a column, switch the pagination page, select a row, or use the &quot;Force parent re-render&quot; button\n\t\t\t\t\t&ndash; the customized columns must stay as you configured them and must no longer reset to their defaults (issue #10344).\n\t\t\t\t</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<KolButton _label={`Force parent re-render (${renderCount})`} _on={{ onClick: () => setRenderCount((count) => count + 1) }} className=\"block\" />\n\n\t\t\t<KolTableStateful\n\t\t\t\t_label=\"Table that remembers settings across sorting, pagination and selection\"\n\t\t\t\t_hasSettingsMenu\n\t\t\t\t_pagination={{ _page: 1, _pageSize: 3 }}\n\t\t\t\t_selection={{\n\t\t\t\t\tlabel: (row) => `Select ${(row as UserRow).name}`,\n\t\t\t\t\tkeyPropertyName: 'id',\n\t\t\t\t\tselectedKeys: [],\n\t\t\t\t}}\n\t\t\t\t_headers={buildHeaders()}\n\t\t\t\t_data={DATA}\n\t\t\t\tclassName=\"block\"\n\t\t\t\tstyle={{ maxWidth: '900px' }}\n\t\t\t/>\n\t\t</>\n\t);\n};\n",
1912
+ "code": "import type { KoliBriTableDataType, KoliBriTableHeaders } from '@public-ui/components';\nimport { KolButton, KolTableStateful } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React, { useState } from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\ntype UserRow = {\n\tid: string;\n\tname: string;\n\tteam: string;\n\temail: string;\n\tstatus: string;\n};\n\nconst DATA: UserRow[] = [\n\t{ id: 'U-001', name: 'Andrea Schmidt', team: 'Design', email: 'andrea@example.org', status: 'Active' },\n\t{ id: 'U-002', name: 'Boris Klein', team: 'Engineering', email: 'boris@example.org', status: 'Active' },\n\t{ id: 'U-003', name: 'Chiara Russo', team: 'Support', email: 'chiara@example.org', status: 'On leave' },\n\t{ id: 'U-004', name: 'Dmitri Volkov', team: 'Engineering', email: 'dmitri@example.org', status: 'Active' },\n\t{ id: 'U-005', name: 'Elena Costa', team: 'Design', email: 'elena@example.org', status: 'On leave' },\n\t{ id: 'U-006', name: 'Farid Haddad', team: 'Support', email: 'farid@example.org', status: 'Active' },\n];\n\nconst compareByKey =\n\t(key: keyof UserRow) =>\n\t(data0: KoliBriTableDataType, data1: KoliBriTableDataType, direction = 'ASC') => {\n\t\tconst result = String((data0 as UserRow)[key]).localeCompare(String((data1 as UserRow)[key]));\n\t\treturn direction === 'DESC' ? -result : result;\n\t};\n\n// Intentionally created inline on every render so the `_headers` prop receives a fresh object\n// reference each time. This mirrors the common React pattern of non-memoized headers and verifies\n// that applied settings survive parent re-renders as long as the column keys stay the same (#10344).\nconst buildHeaders = (): KoliBriTableHeaders => ({\n\thorizontal: [\n\t\t[\n\t\t\t{ key: 'id', label: 'ID', visible: true, width: 120 },\n\t\t\t{ key: 'name', label: 'Name', visible: true, width: 240, compareFn: compareByKey('name') },\n\t\t\t{ key: 'team', label: 'Team', visible: true, width: 200, compareFn: compareByKey('team') },\n\t\t\t{ key: 'email', label: 'E-Mail', visible: true, width: 280 },\n\t\t\t{ key: 'status', label: 'Status', visible: true, width: 160, compareFn: compareByKey('status') },\n\t\t],\n\t],\n});\n\nexport const TableStatefulSettingsPersistence: FC = () => {\n\tconst [renderCount, setRenderCount] = useState(0);\n\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>\n\t\t\t\t\tThis example demonstrates that column settings applied via the settings menu are remembered. Open the settings menu and change a column&apos;s width,\n\t\t\t\t\tvisibility or order and apply. Then sort a column, switch the pagination page, select a row, or use the &quot;Force parent re-render&quot; button\n\t\t\t\t\t&ndash; the customized columns must stay as you configured them and must no longer reset to their defaults (issue #10344).\n\t\t\t\t</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<KolButton _label={`Force parent re-render (${renderCount})`} _on={{ onClick: () => setRenderCount((count) => count + 1) }} className=\"block\" />\n\n\t\t\t<KolTableStateful\n\t\t\t\t_label=\"Table that remembers settings across sorting, pagination and selection\"\n\t\t\t\t_hasSettingsMenu\n\t\t\t\t_pagination={{ _page: 1, _pageSize: 3 }}\n\t\t\t\t_selection={{\n\t\t\t\t\tlabel: (row) => `Select ${(row as UserRow).name}`,\n\t\t\t\t\tkeyPropertyName: 'id',\n\t\t\t\t\tselectedKeys: [],\n\t\t\t\t}}\n\t\t\t\t_headers={buildHeaders()}\n\t\t\t\t_data={DATA}\n\t\t\t\tclassName=\"block\"\n\t\t\t/>\n\t\t</>\n\t);\n};\n",
1921
1913
  "kind": "sample"
1922
1914
  },
1923
1915
  {
@@ -1941,7 +1933,7 @@
1941
1933
  "group": "table",
1942
1934
  "name": "stateless",
1943
1935
  "path": "packages/samples/react/src/components/table/stateless.tsx",
1944
- "code": "import { KolTableStateless } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\nconst DATA = [{ left: 'Left Example', center: 'Center Example', right: 'Right Example' }, {}, {}, {}];\n\nexport const TableStateless: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>This sample shows how KolTableStateless can be used directly, with the KolTableStateful wrapper.</p>\n\t\t</SampleDescription>\n\n\t\t<SampleBlock id=\"stateless\" className=\"w-full\" fitContent>\n\t\t\t<KolTableStateless\n\t\t\t\t_label=\"Table for demonstration purposes\"\n\t\t\t\t_headers={{\n\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{ key: 'left', label: 'left', textAlign: 'left', sortDirection: 'ASC' },\n\t\t\t\t\t\t\t{ key: 'center', label: 'center', textAlign: 'center', sortDirection: 'DESC' },\n\t\t\t\t\t\t\t{ key: 'right', label: 'right', textAlign: 'right', sortDirection: 'NOS' },\n\t\t\t\t\t\t\t{ key: 'nosort', label: 'no sort option' },\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t\tvertical: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{ key: 'vertical-left', label: 'left', textAlign: 'left', sortDirection: 'ASC' },\n\t\t\t\t\t\t\t{ key: 'vertical-center', label: 'center', textAlign: 'center', sortDirection: 'DESC' },\n\t\t\t\t\t\t\t{ key: 'vertical-right', label: 'right', textAlign: 'right', sortDirection: 'NOS' },\n\t\t\t\t\t\t\t{ key: 'vertical-nosort', label: 'no sort option' },\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t}}\n\t\t\t\t_data={DATA}\n\t\t\t\tclassName=\"block\"\n\t\t\t\tstyle={{ maxWidth: '600px' }}\n\t\t\t\t_on={{\n\t\t\t\t\tonSort: (_event, payload) => {\n\t\t\t\t\t\tconsole.log(payload);\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t/>\n\t\t</SampleBlock>\n\t</>\n);\n",
1936
+ "code": "import { KolTableStateless } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\nconst DATA = [{ left: 'Left Example', center: 'Center Example', right: 'Right Example' }, {}, {}, {}];\n\nexport const TableStateless: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>This sample shows how KolTableStateless can be used directly, with the KolTableStateful wrapper.</p>\n\t\t</SampleDescription>\n\n\t\t<SampleBlock id=\"stateless\" className=\"w-full\" fitContent>\n\t\t\t<KolTableStateless\n\t\t\t\t_label=\"Table for demonstration purposes\"\n\t\t\t\t_headers={{\n\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{ key: 'left', label: 'left', textAlign: 'left', sortDirection: 'ASC', width: 100 },\n\t\t\t\t\t\t\t{ key: 'center', label: 'center', textAlign: 'center', sortDirection: 'DESC', width: 100 },\n\t\t\t\t\t\t\t{ key: 'right', label: 'right', textAlign: 'right', sortDirection: 'NOS', width: 100 },\n\t\t\t\t\t\t\t{ key: 'nosort', label: 'no sort option', width: 150 },\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t\tvertical: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{ key: 'vertical-left', label: 'left', textAlign: 'left', sortDirection: 'ASC' },\n\t\t\t\t\t\t\t{ key: 'vertical-center', label: 'center', textAlign: 'center', sortDirection: 'DESC' },\n\t\t\t\t\t\t\t{ key: 'vertical-right', label: 'right', textAlign: 'right', sortDirection: 'NOS' },\n\t\t\t\t\t\t\t{ key: 'vertical-nosort', label: 'no sort option' },\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t}}\n\t\t\t\t_data={DATA}\n\t\t\t\tclassName=\"block\"\n\t\t\t\tstyle={{ maxWidth: '600px' }}\n\t\t\t\t_on={{\n\t\t\t\t\tonSort: (_event, payload) => {\n\t\t\t\t\t\tconsole.log(payload);\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t/>\n\t\t</SampleBlock>\n\t</>\n);\n",
1945
1937
  "kind": "sample"
1946
1938
  },
1947
1939
  {
@@ -1957,7 +1949,7 @@
1957
1949
  "group": "table",
1958
1950
  "name": "stateless-with-selection",
1959
1951
  "path": "packages/samples/react/src/components/table/stateless-with-selection.tsx",
1960
- "code": "import type { KoliBriTableCell, KoliBriTableSelection, KoliBriTableSelectionKeys } from '@public-ui/components';\nimport { createReactRenderElement, KolButton, KolTableStateless } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React, { useEffect, useRef, useState } from 'react';\nimport { useAlert } from '../../hooks/useAlert';\nimport { getRoot } from '../../shares/react-roots';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\ntype SelectionValue = string | number;\ntype KolTableStatelessElement = {\n\taddEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n\tremoveEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n};\n\nconst DATA = [\n\t{ id: '1001', name: 'Foo Bar', internalIdentifier: `AAA1001` },\n\t{ id: '1002', name: 'Foo Baz', internalIdentifier: `AAA1002` },\n\t{ id: '1003', name: 'This row is always unchecked', internalIdentifier: `AAA1003` },\n\t{ id: '1004', name: 'This row is always checked', internalIdentifier: `AAA1004` },\n];\ntype Data = (typeof DATA)[0];\n\nfunction KolButtonWrapper({ label }: { label: string }) {\n\tconst { dummyClickEventHandler } = useAlert();\n\n\tconst dummyEventHandler = {\n\t\tonClick: dummyClickEventHandler,\n\t};\n\n\treturn <KolButton _label={label} _on={dummyEventHandler} />;\n}\n\nexport const TableStatelessWithSelection: FC = () => {\n\tconst [selectedKeys, setSelectedKeys] = useState<KoliBriTableSelectionKeys>(['AAA1002', 'AAA1004']);\n\n\tconst selection: KoliBriTableSelection = {\n\t\tlabel: (row) => `Selection for ${(row as Data).name}`,\n\t\tselectedKeys,\n\t\tkeyPropertyName: 'internalIdentifier',\n\t\tdisabledKeys: ['AAA1003', 'AAA1004'],\n\t};\n\n\tconst kolTableStatelessRef = useRef<HTMLKolTableStatelessElement>(null);\n\n\tconst handleSelectionChangeEvent = ({ detail: selection }: CustomEvent<SelectionValue[]>) => {\n\t\tconsole.log('Selection change via event', selection);\n\t};\n\tconst handleSelectionChangeCallback = (_event: Event, selection: SelectionValue[]) => {\n\t\tconsole.log('Selection change via callback', selection);\n\t\tsetSelectedKeys(selection);\n\t};\n\n\tuseEffect(() => {\n\t\tconst tableElement = kolTableStatelessRef.current as unknown as KolTableStatelessElement | null;\n\t\tconst selectionChangeEvent = 'kolSelectionChange';\n\t\ttableElement?.addEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\n\t\treturn () => {\n\t\t\ttableElement?.removeEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\t\t};\n\t}, [kolTableStatelessRef]);\n\n\tconst renderButton = (element: HTMLElement, cell: KoliBriTableCell) => {\n\t\tconst data = (cell as { data?: Data }).data;\n\t\tconst id = data?.id;\n\t\tgetRoot(createReactRenderElement(element)).render(<KolButtonWrapper label={`Click ${id}`} />);\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This sample shows KolTableStateless with checkboxes for selection enabled.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<SampleBlock id=\"selection\" className=\"w-full\">\n\t\t\t\t<KolTableStateless\n\t\t\t\t\t_label=\"Table with selection checkboxes\"\n\t\t\t\t\t_headers={{\n\t\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t{ key: 'id', label: '#ID', textAlign: 'left' },\n\t\t\t\t\t\t\t\t{ key: 'name', label: 'Name', textAlign: 'left' },\n\t\t\t\t\t\t\t\t{ key: 'action', label: 'Action', textAlign: 'left', render: renderButton },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t}}\n\t\t\t\t\t_data={DATA}\n\t\t\t\t\t_selection={selection}\n\t\t\t\t\t_on={{ onSelectionChange: handleSelectionChangeCallback }}\n\t\t\t\t\tclassName=\"block\"\n\t\t\t\t\tstyle={{ maxWidth: '600px' }}\n\t\t\t\t\tref={kolTableStatelessRef}\n\t\t\t\t/>\n\t\t\t</SampleBlock>\n\t\t</>\n\t);\n};\n",
1952
+ "code": "import type { KoliBriTableCell, KoliBriTableSelection, KoliBriTableSelectionKeys } from '@public-ui/components';\nimport { createReactRenderElement, KolButton, KolTableStateless } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React, { useEffect, useRef, useState } from 'react';\nimport { useAlert } from '../../hooks/useAlert';\nimport { getRoot } from '../../shares/react-roots';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\ntype SelectionValue = string | number;\ntype KolTableStatelessElement = {\n\taddEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n\tremoveEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n};\n\nconst DATA = [\n\t{ id: '1001', name: 'Foo Bar', internalIdentifier: `AAA1001` },\n\t{ id: '1002', name: 'Foo Baz', internalIdentifier: `AAA1002` },\n\t{ id: '1003', name: 'This row is always unchecked', internalIdentifier: `AAA1003` },\n\t{ id: '1004', name: 'This row is always checked', internalIdentifier: `AAA1004` },\n];\ntype Data = (typeof DATA)[0];\n\nfunction KolButtonWrapper({ label }: { label: string }) {\n\tconst { dummyClickEventHandler } = useAlert();\n\n\tconst dummyEventHandler = {\n\t\tonClick: dummyClickEventHandler,\n\t};\n\n\treturn <KolButton _label={label} _on={dummyEventHandler} />;\n}\n\nexport const TableStatelessWithSelection: FC = () => {\n\tconst [selectedKeys, setSelectedKeys] = useState<KoliBriTableSelectionKeys>(['AAA1002', 'AAA1004']);\n\n\tconst selection: KoliBriTableSelection = {\n\t\tlabel: (row) => `Selection for ${(row as Data).name}`,\n\t\tselectedKeys,\n\t\tkeyPropertyName: 'internalIdentifier',\n\t\tdisabledKeys: ['AAA1003', 'AAA1004'],\n\t};\n\n\tconst kolTableStatelessRef = useRef<HTMLKolTableStatelessElement>(null);\n\n\tconst handleSelectionChangeEvent = ({ detail: selection }: CustomEvent<SelectionValue[]>) => {\n\t\tconsole.log('Selection change via event', selection);\n\t};\n\tconst handleSelectionChangeCallback = (_event: Event, selection: SelectionValue[]) => {\n\t\tconsole.log('Selection change via callback', selection);\n\t\tsetSelectedKeys(selection);\n\t};\n\n\tuseEffect(() => {\n\t\tconst tableElement = kolTableStatelessRef.current as unknown as KolTableStatelessElement | null;\n\t\tconst selectionChangeEvent = 'kolSelectionChange';\n\t\ttableElement?.addEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\n\t\treturn () => {\n\t\t\ttableElement?.removeEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\t\t};\n\t}, [kolTableStatelessRef]);\n\n\tconst renderButton = (element: HTMLElement, cell: KoliBriTableCell) => {\n\t\tconst data = (cell as { data?: Data }).data;\n\t\tconst id = data?.id;\n\t\tgetRoot(createReactRenderElement(element)).render(<KolButtonWrapper label={`Click ${id}`} />);\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This sample shows KolTableStateless with checkboxes for selection enabled.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<SampleBlock id=\"selection\" className=\"w-full\">\n\t\t\t\t<KolTableStateless\n\t\t\t\t\t_label=\"Table with selection checkboxes\"\n\t\t\t\t\t_headers={{\n\t\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t{ key: 'id', label: '#ID', textAlign: 'left', width: 70 },\n\t\t\t\t\t\t\t\t{ key: 'name', label: 'Name', textAlign: 'left', width: 200 },\n\t\t\t\t\t\t\t\t{ key: 'action', label: 'Action', textAlign: 'left', render: renderButton, width: 100 },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t}}\n\t\t\t\t\t_data={DATA}\n\t\t\t\t\t_selection={selection}\n\t\t\t\t\t_on={{ onSelectionChange: handleSelectionChangeCallback }}\n\t\t\t\t\tclassName=\"block\"\n\t\t\t\t\tref={kolTableStatelessRef}\n\t\t\t\t/>\n\t\t\t</SampleBlock>\n\t\t</>\n\t);\n};\n",
1961
1953
  "kind": "sample"
1962
1954
  },
1963
1955
  {
@@ -1973,7 +1965,7 @@
1973
1965
  "group": "table",
1974
1966
  "name": "stateless-with-single-selection",
1975
1967
  "path": "packages/samples/react/src/components/table/stateless-with-single-selection.tsx",
1976
- "code": "import type { KoliBriTableCell, KoliBriTableSelection, KoliBriTableSelectionKeys } from '@public-ui/components';\nimport { createReactRenderElement, KolButton, KolTableStateless } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { useAlert } from '../../hooks/useAlert';\nimport { getRoot } from '../../shares/react-roots';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\ntype SelectionValue = string | number;\ntype KolTableStatelessElement = {\n\taddEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n\tremoveEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n};\n\nconst DATA = [\n\t{ id: '1001', name: 'Foo Bar', internalIdentifier: `AAA1001` },\n\t{ id: '1002', name: 'Foo Baz', internalIdentifier: `AAA1002` },\n\t{ id: '1003', name: 'Foo Disabled', internalIdentifier: `AAA1003` },\n];\ntype Data = (typeof DATA)[0];\n\nfunction KolButtonWrapper({ label }: { label: string }) {\n\tconst { dummyClickEventHandler } = useAlert();\n\n\tconst dummyEventHandler = {\n\t\tonClick: dummyClickEventHandler,\n\t};\n\n\treturn <KolButton _label={label} _on={dummyEventHandler} />;\n}\n\nexport const TableStatelessWithSingleSelection: FC = () => {\n\tconst [selectedKeys, setSelectedKeys] = useState<KoliBriTableSelectionKeys>(['1002']);\n\n\tconst selection: KoliBriTableSelection = {\n\t\tlabel: (row) => `Selection for ${(row as Data).name}`,\n\t\tmultiple: false,\n\t\tselectedKeys,\n\t\tdisabledKeys: ['AAA1003'],\n\t\tkeyPropertyName: 'internalIdentifier',\n\t};\n\n\tconst kolTableStatelessRef = useRef<HTMLKolTableStatelessElement>(null);\n\n\tconst handleSelectionChangeEvent = useCallback(({ detail: selection }: CustomEvent<SelectionValue[]>) => {\n\t\tconsole.log('Selection change via event', selection);\n\t}, []);\n\tconst handleSelectionChangeCallback = useCallback((_event: Event, selection: SelectionValue[]) => {\n\t\tconsole.log('Selection change via callback', selection);\n\t\tsetSelectedKeys(selection);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tconst tableElement = kolTableStatelessRef.current as unknown as KolTableStatelessElement | null;\n\t\tconst selectionChangeEvent = 'kolSelectionChange';\n\t\ttableElement?.addEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\n\t\treturn () => {\n\t\t\ttableElement?.removeEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\t\t};\n\t}, [kolTableStatelessRef, handleSelectionChangeEvent]);\n\n\tconst renderButton = useCallback((element: HTMLElement, cell: KoliBriTableCell) => {\n\t\tconst data = (cell as { data?: Data }).data;\n\t\tconst id = data?.id;\n\t\tgetRoot(createReactRenderElement(element)).render(<KolButtonWrapper label={`Click ${id}`} />);\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This sample shows KolTableStateless with checkboxes for selection enabled.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<SampleBlock id=\"single-selection\" className=\"w-full\" fitContent>\n\t\t\t\t<KolTableStateless\n\t\t\t\t\t_label=\"Table with selection checkboxes\"\n\t\t\t\t\t_headers={{\n\t\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t{ key: 'id', label: '#ID', textAlign: 'left' },\n\t\t\t\t\t\t\t\t{ key: 'name', label: 'Name', textAlign: 'left' },\n\t\t\t\t\t\t\t\t{ key: 'action', label: 'Action', textAlign: 'left', render: renderButton },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t}}\n\t\t\t\t\t_data={DATA}\n\t\t\t\t\t_selection={selection}\n\t\t\t\t\t_on={{ onSelectionChange: handleSelectionChangeCallback }}\n\t\t\t\t\tclassName=\"block\"\n\t\t\t\t\tstyle={{ maxWidth: '600px' }}\n\t\t\t\t\tref={kolTableStatelessRef}\n\t\t\t\t/>\n\t\t\t</SampleBlock>\n\t\t</>\n\t);\n};\n",
1968
+ "code": "import type { KoliBriTableCell, KoliBriTableSelection, KoliBriTableSelectionKeys } from '@public-ui/components';\nimport { createReactRenderElement, KolButton, KolTableStateless } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { useAlert } from '../../hooks/useAlert';\nimport { getRoot } from '../../shares/react-roots';\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\n\ntype SelectionValue = string | number;\ntype KolTableStatelessElement = {\n\taddEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n\tremoveEventListener: (type: string, listener: (event: CustomEvent<SelectionValue[]>) => void) => void;\n};\n\nconst DATA = [\n\t{ id: '1001', name: 'Foo Bar', internalIdentifier: `AAA1001` },\n\t{ id: '1002', name: 'Foo Baz', internalIdentifier: `AAA1002` },\n\t{ id: '1003', name: 'Foo Disabled', internalIdentifier: `AAA1003` },\n];\ntype Data = (typeof DATA)[0];\n\nfunction KolButtonWrapper({ label }: { label: string }) {\n\tconst { dummyClickEventHandler } = useAlert();\n\n\tconst dummyEventHandler = {\n\t\tonClick: dummyClickEventHandler,\n\t};\n\n\treturn <KolButton _label={label} _on={dummyEventHandler} />;\n}\n\nexport const TableStatelessWithSingleSelection: FC = () => {\n\tconst [selectedKeys, setSelectedKeys] = useState<KoliBriTableSelectionKeys>(['1002']);\n\n\tconst selection: KoliBriTableSelection = {\n\t\tlabel: (row) => `Selection for ${(row as Data).name}`,\n\t\tmultiple: false,\n\t\tselectedKeys,\n\t\tdisabledKeys: ['AAA1003'],\n\t\tkeyPropertyName: 'internalIdentifier',\n\t};\n\n\tconst kolTableStatelessRef = useRef<HTMLKolTableStatelessElement>(null);\n\n\tconst handleSelectionChangeEvent = useCallback(({ detail: selection }: CustomEvent<SelectionValue[]>) => {\n\t\tconsole.log('Selection change via event', selection);\n\t}, []);\n\tconst handleSelectionChangeCallback = useCallback((_event: Event, selection: SelectionValue[]) => {\n\t\tconsole.log('Selection change via callback', selection);\n\t\tsetSelectedKeys(selection);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tconst tableElement = kolTableStatelessRef.current as unknown as KolTableStatelessElement | null;\n\t\tconst selectionChangeEvent = 'kolSelectionChange';\n\t\ttableElement?.addEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\n\t\treturn () => {\n\t\t\ttableElement?.removeEventListener(selectionChangeEvent, handleSelectionChangeEvent);\n\t\t};\n\t}, [kolTableStatelessRef, handleSelectionChangeEvent]);\n\n\tconst renderButton = useCallback((element: HTMLElement, cell: KoliBriTableCell) => {\n\t\tconst data = (cell as { data?: Data }).data;\n\t\tconst id = data?.id;\n\t\tgetRoot(createReactRenderElement(element)).render(<KolButtonWrapper label={`Click ${id}`} />);\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This sample shows KolTableStateless with checkboxes for selection enabled.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<SampleBlock id=\"single-selection\" className=\"w-full\" fitContent>\n\t\t\t\t<KolTableStateless\n\t\t\t\t\t_label=\"Table with selection checkboxes\"\n\t\t\t\t\t_headers={{\n\t\t\t\t\t\thorizontal: [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t{ key: 'id', label: '#ID', textAlign: 'left', width: 100 },\n\t\t\t\t\t\t\t\t{ key: 'name', label: 'Name', textAlign: 'left', width: 200 },\n\t\t\t\t\t\t\t\t{ key: 'action', label: 'Action', textAlign: 'left', render: renderButton, width: 200 },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t}}\n\t\t\t\t\t_data={DATA}\n\t\t\t\t\t_selection={selection}\n\t\t\t\t\t_on={{ onSelectionChange: handleSelectionChangeCallback }}\n\t\t\t\t\tclassName=\"block\"\n\t\t\t\t\tref={kolTableStatelessRef}\n\t\t\t\t/>\n\t\t\t</SampleBlock>\n\t\t</>\n\t);\n};\n",
1977
1969
  "kind": "sample"
1978
1970
  },
1979
1971
  {
@@ -1997,7 +1989,7 @@
1997
1989
  "group": "table",
1998
1990
  "name": "variant",
1999
1991
  "path": "packages/samples/react/src/components/table/variant.tsx",
2000
- "code": "import type { FC } from 'react';\nimport React, { useMemo } from 'react';\n\nimport { KolTableStateful } from '@public-ui/react-v19';\n\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\nimport { DATE_FORMATTER } from './formatter';\nimport type { Data } from './test-data';\nimport { DATA } from './test-data';\n\nimport type { KoliBriTableHeaders } from '@public-ui/components';\nimport { useSearchParams } from 'react-router';\nimport { fetchVariantData } from '../../shares/fetchVariantData';\nimport { getTheme } from '../../shares/store';\n\nconst HEADERS: KoliBriTableHeaders = {\n\thorizontal: [\n\t\t[\n\t\t\t{ label: 'Order', key: 'order', width: 160 },\n\t\t\t{ label: 'Date', key: 'date', width: 160, render: (_el, _cell, tupel) => DATE_FORMATTER.format((tupel as unknown as Data).date) },\n\t\t],\n\t],\n};\n\nexport const TableVariant: FC = () => {\n\tconst [searchParams] = useSearchParams();\n\tconst theme = searchParams.get('theme') ?? getTheme();\n\tconst tableVariants = useMemo(() => (theme ? fetchVariantData(theme, 'tableVariants') : []), [theme]);\n\n\treturn (\n\t\t<div className=\"w-full grid gap-14\">\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This sample shows KolTableStateful with different variants controlled by the theme.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<SampleBlock id=\"variants\" className=\"w-full flex flex-col gap-14\">\n\t\t\t\t{!Array.isArray(tableVariants) || tableVariants.length === 0 ? (\n\t\t\t\t\t<p>This theme has no variants for tables.</p>\n\t\t\t\t) : (\n\t\t\t\t\ttableVariants.map((element) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<KolTableStateful\n\t\t\t\t\t\t\t\t_label={'Table with variant: ' + element}\n\t\t\t\t\t\t\t\t_data={DATA}\n\t\t\t\t\t\t\t\t_headers={HEADERS}\n\t\t\t\t\t\t\t\t_pagination={{\n\t\t\t\t\t\t\t\t\t_page: 1,\n\t\t\t\t\t\t\t\t\t_hasButtons: {\n\t\t\t\t\t\t\t\t\t\tfirst: false,\n\t\t\t\t\t\t\t\t\t\tnext: true,\n\t\t\t\t\t\t\t\t\t\tlast: false,\n\t\t\t\t\t\t\t\t\t\tprevious: true,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t_paginationPosition=\"bottom\"\n\t\t\t\t\t\t\t\t_variant={element}\n\t\t\t\t\t\t\t></KolTableStateful>\n\t\t\t\t\t\t);\n\t\t\t\t\t})\n\t\t\t\t)}\n\t\t\t</SampleBlock>\n\t\t</div>\n\t);\n};\n",
1992
+ "code": "import type { FC } from 'react';\nimport React, { useMemo } from 'react';\n\nimport { KolTableStateful } from '@public-ui/react-v19';\n\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\nimport { DATE_FORMATTER } from './formatter';\nimport type { Data } from './test-data';\nimport { DATA } from './test-data';\n\nimport type { KoliBriTableHeaders } from '@public-ui/components';\nimport { useSearchParams } from 'react-router';\nimport { fetchVariantData } from '../../shares/fetchVariantData';\nimport { getTheme } from '../../shares/store';\n\nconst HEADERS: KoliBriTableHeaders = {\n\thorizontal: [\n\t\t[\n\t\t\t{ label: 'Order', key: 'order', width: 160 },\n\t\t\t{ label: 'Date', key: 'date', width: 160, render: (_el, _cell, tupel) => DATE_FORMATTER.format((tupel as unknown as Data).date) },\n\t\t],\n\t],\n};\n\nexport const TableVariant: FC = () => {\n\tconst [searchParams] = useSearchParams();\n\tconst theme = searchParams.get('theme') ?? getTheme();\n\tconst tableVariants = useMemo(() => (theme ? fetchVariantData(theme, 'tableVariants') : []), [theme]);\n\n\treturn (\n\t\t<div className=\"w-full grid gap-14\">\n\t\t\t<SampleDescription>\n\t\t\t\t<p>This sample shows KolTableStateful with different variants controlled by the theme.</p>\n\t\t\t</SampleDescription>\n\n\t\t\t{!Array.isArray(tableVariants) || tableVariants.length === 0 ? (\n\t\t\t\t<SampleBlock id=\"variant-no\">\n\t\t\t\t\t<p>This theme has no variants for tables.</p>\n\t\t\t\t</SampleBlock>\n\t\t\t) : (\n\t\t\t\ttableVariants.map((element, index) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<SampleBlock id={'variant-' + index} className=\"w-full flex flex-col gap-14\">\n\t\t\t\t\t\t\t<KolTableStateful\n\t\t\t\t\t\t\t\t_label={'Table with variant: ' + element}\n\t\t\t\t\t\t\t\t_data={DATA}\n\t\t\t\t\t\t\t\t_headers={HEADERS}\n\t\t\t\t\t\t\t\t_pagination={{\n\t\t\t\t\t\t\t\t\t_page: 1,\n\t\t\t\t\t\t\t\t\t_hasButtons: {\n\t\t\t\t\t\t\t\t\t\tfirst: false,\n\t\t\t\t\t\t\t\t\t\tnext: true,\n\t\t\t\t\t\t\t\t\t\tlast: false,\n\t\t\t\t\t\t\t\t\t\tprevious: true,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t_paginationPosition=\"bottom\"\n\t\t\t\t\t\t\t\t_variant={element}\n\t\t\t\t\t\t\t></KolTableStateful>\n\t\t\t\t\t\t</SampleBlock>\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t)}\n\t\t</div>\n\t);\n};\n",
2001
1993
  "kind": "sample"
2002
1994
  },
2003
1995
  {
@@ -2016,6 +2008,14 @@
2016
2008
  "code": "import type { FC } from 'react';\nimport React from 'react';\n\nimport { KolTableStateful } from '@public-ui/react-v19';\n\nimport { SampleBlock } from '../SampleBlock';\nimport { SampleDescription } from '../SampleDescription';\nimport { DATE_FORMATTER } from './formatter';\nimport type { Data } from './test-data';\nimport { DATA } from './test-data';\n\nimport type { KoliBriTableHeaders, KoliBriTablePaginationProps } from '@public-ui/components';\n\nconst HEADERS: KoliBriTableHeaders = {\n\thorizontal: [\n\t\t[\n\t\t\t{ label: 'Order', key: 'order', width: 160 },\n\t\t\t{ label: 'Date', key: 'date', width: 160, render: (_el, _cell, tupel) => DATE_FORMATTER.format((tupel as unknown as Data).date) },\n\t\t],\n\t],\n};\nconst PAGINATION: KoliBriTablePaginationProps = { _page: 2 };\n\nexport const TableWithPagination: FC = () => (\n\t<>\n\t\t<SampleDescription>\n\t\t\t<p>This sample shows how KolTableStateful can be navigated using a pagination.</p>\n\t\t</SampleDescription>\n\t\t<SampleBlock id=\"with-pagination\" className=\"w-full\">\n\t\t\t<KolTableStateful _label=\"Table description\" _data={DATA} _headers={HEADERS} _pagination={PAGINATION}></KolTableStateful>\n\t\t</SampleBlock>\n\t</>\n);\n",
2017
2009
  "kind": "sample"
2018
2010
  },
2011
+ {
2012
+ "id": "sample/table/with-toolbar",
2013
+ "group": "table",
2014
+ "name": "with-toolbar",
2015
+ "path": "packages/samples/react/src/components/table/with-toolbar.tsx",
2016
+ "code": "import type { KoliBriTableDataType, KoliBriTableHeaderCellWithLogic, KoliBriTableSelection, ToolbarItemsPropType } from '@public-ui/components';\nimport { KolButton, KolDrawer, KolInputCheckbox, KolInputText, KolTableStateful, KolToolbar } from '@public-ui/react-v19';\nimport type { FC } from 'react';\nimport React, { useState } from 'react';\nimport { SampleDescription } from '../SampleDescription';\n\ntype DataRow = {\n\tid: number;\n\tname: string;\n\temail: string;\n};\n\nconst DATA: DataRow[] = [\n\t{ id: 1001, name: 'Max Mustermann', email: 'max.mustermann@example.com' },\n\t{ id: 1002, name: 'Erika Musterfrau', email: 'erika.musterfrau@example.com' },\n\t{ id: 1003, name: 'John Doe', email: 'john.doe@example.com' },\n\t{ id: 1004, name: 'Jane Doe', email: 'jane.doe@example.com' },\n\t{ id: 1005, name: 'Peter Smith', email: 'peter.smith@example.com' },\n\t{ id: 1006, name: 'Anna Smith', email: 'anna.smith@example.com' },\n\t{ id: 1007, name: 'Michael Schmidt', email: 'michael.schmidt@example.com' },\n\t{ id: 1008, name: 'Sarah Schmidt', email: 'sarah.schmidt@example.com' },\n\t{ id: 1009, name: 'Thomas Müller', email: 'thomas.mueller@example.com' },\n\t{ id: 1010, name: 'Lisa Müller', email: 'lisa.mueller@example.com' },\n];\n\nconst HEADERS: { horizontal: KoliBriTableHeaderCellWithLogic[][] } = {\n\thorizontal: [\n\t\t[\n\t\t\t{\n\t\t\t\tkey: 'id',\n\t\t\t\tlabel: 'ID',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\twidth: 80,\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => (data0 as DataRow).id - (data1 as DataRow).id,\n\t\t\t\tsortDirection: 'ASC',\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'name',\n\t\t\t\tlabel: 'Name',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => (data0 as DataRow).name.localeCompare((data1 as DataRow).name, 'en'),\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: 'email',\n\t\t\t\tlabel: 'Email',\n\t\t\t\ttextAlign: 'left',\n\t\t\t\tcompareFn: (data0: KoliBriTableDataType, data1: KoliBriTableDataType) => (data0 as DataRow).email.localeCompare((data1 as DataRow).email, 'en'),\n\t\t\t},\n\t\t],\n\t],\n};\n\nconst FIRST_NAMES = ['Ada', 'Bruno', 'Clara', 'Finn', 'Ida', 'Jonas', 'Lena', 'Nele', 'Otto', 'Sina'];\nconst LAST_NAMES = ['Bauer', 'Fuchs', 'Hirsch', 'Kraft', 'Lindner', 'Moor', 'Reuter', 'Schubert', 'Wagner', 'Winter'];\n\nfunction pick<T>(values: T[]): T {\n\treturn values[Math.floor(Math.random() * values.length)];\n}\n\nfunction nextId(rows: DataRow[]): number {\n\treturn rows.reduce((max, row) => Math.max(max, row.id), 1000) + 1;\n}\n\nfunction createDummyRow(id: number): DataRow {\n\tconst firstName = pick(FIRST_NAMES);\n\tconst lastName = pick(LAST_NAMES);\n\treturn {\n\t\tid,\n\t\tname: `${firstName} ${lastName}`,\n\t\temail: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`,\n\t};\n}\n\nexport const TableWithToolbar: FC = () => {\n\tconst [dataRows, setDataRows] = useState<DataRow[]>(DATA);\n\t/** Keys (`DataRow.id`) of all selected rows. */\n\tconst [selectedKeys, setSelectedKeys] = useState<number[]>([]);\n\tconst [isMultiple, setIsMultiple] = useState(false);\n\t/** The form is open while a draft exists; a draft whose ID already exists edits that row. */\n\tconst [draft, setDraft] = useState<DataRow | null>(null);\n\n\tconst isEditing = draft !== null && dataRows.some((row) => row.id === draft.id);\n\n\tconst selection: KoliBriTableSelection = {\n\t\tkeyPropertyName: 'id',\n\t\tlabel: (row) => `Select row ${(row as DataRow).name}`,\n\t\tmultiple: isMultiple,\n\t\tselectedKeys,\n\t};\n\n\tconst handleSelectionChange = (_event: Event, selectedRows: KoliBriTableDataType[] | null) => {\n\t\tsetSelectedKeys((selectedRows ?? []).map((row) => (row as DataRow).id));\n\t};\n\n\tconst handleSelectionModeSwitch = (_event: Event, value: unknown) => {\n\t\tsetIsMultiple(value === true);\n\t\t/* A multiple selection does not fit the single selection mode. */\n\t\tif (value !== true && selectedKeys.length > 1) {\n\t\t\tsetSelectedKeys([]);\n\t\t}\n\t};\n\n\tconst openAddDrawer = () => {\n\t\tsetDraft(createDummyRow(nextId(dataRows)));\n\t};\n\n\tconst editSelectedRow = () => {\n\t\tconst selectedRow = dataRows.find((row) => row.id === selectedKeys[0]);\n\t\tif (selectedRow !== undefined) {\n\t\t\tsetDraft({ ...selectedRow });\n\t\t}\n\t};\n\n\tconst closeDrawer = () => {\n\t\tsetDraft(null);\n\t};\n\n\tconst saveDraft = () => {\n\t\tif (draft !== null) {\n\t\t\tsetDataRows((rows) => (isEditing ? rows.map((row) => (row.id === draft.id ? draft : row)) : [...rows, draft]));\n\t\t}\n\t\tsetDraft(null);\n\t};\n\n\tconst deleteSelectedRows = () => {\n\t\tif (selectedKeys.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tsetDataRows(dataRows.filter((row) => !selectedKeys.includes(row.id)));\n\t\tsetSelectedKeys([]);\n\t};\n\n\tconst toolbarItems: ToolbarItemsPropType = [\n\t\t{\n\t\t\ttype: 'button',\n\t\t\t_label: 'Add',\n\t\t\t_icons: { left: { icon: 'kolicon-plus' } },\n\t\t\t_variant: 'primary',\n\t\t\t_on: { onClick: openAddDrawer },\n\t\t},\n\t\t{\n\t\t\ttype: 'button',\n\t\t\t_label: 'Edit',\n\t\t\t_variant: 'secondary',\n\t\t\t_disabled: selectedKeys.length !== 1,\n\t\t\t_on: { onClick: editSelectedRow },\n\t\t},\n\t\t{\n\t\t\ttype: 'button',\n\t\t\t_label: 'Delete',\n\t\t\t_icons: { left: { icon: 'kolicon-cross' } },\n\t\t\t_variant: 'danger',\n\t\t\t_disabled: selectedKeys.length === 0,\n\t\t\t_on: { onClick: deleteSelectedRows },\n\t\t},\n\t];\n\n\treturn (\n\t\t<>\n\t\t\t<SampleDescription>\n\t\t\t\t<p>\n\t\t\t\t\tThis sample combines a toolbar with a table: a switch toggles between single and multiple selection. \"Add\" generates a new dummy record in a form and\n\t\t\t\t\tinserts it via \"Create\". \"Edit\" opens the selected row in the same form when exactly one row is selected. \"Delete\" removes all selected rows and is\n\t\t\t\t\tonly enabled with a selection.\n\t\t\t\t</p>\n\t\t\t</SampleDescription>\n\n\t\t\t<section className=\"w-full\">\n\t\t\t\t<KolToolbar _label=\"Table actions\" _items={toolbarItems} />\n\n\t\t\t\t<KolInputCheckbox\n\t\t\t\t\tclassName=\"block w-fit py-2\"\n\t\t\t\t\t_label=\"Multiple selection\"\n\t\t\t\t\t_variant=\"switch\"\n\t\t\t\t\t_checked={isMultiple}\n\t\t\t\t\t_value={true}\n\t\t\t\t\t_on={{ onInput: handleSelectionModeSwitch }}\n\t\t\t\t/>\n\n\t\t\t\t<KolDrawer _label={isEditing ? 'Edit entry' : 'Add entry'} _align=\"right\" _level={2} _open={draft !== null} _hasCloser _on={{ onClose: closeDrawer }}>\n\t\t\t\t\t{draft !== null && (\n\t\t\t\t\t\t<div className=\"flex flex-col gap-4 py-4\">\n\t\t\t\t\t\t\t<KolInputText _label=\"Name\" _value={draft.name} _on={{ onInput: (_event, value) => setDraft({ ...draft, name: String(value) }) }} />\n\t\t\t\t\t\t\t<KolInputText _label=\"Email\" _value={draft.email} _on={{ onInput: (_event, value) => setDraft({ ...draft, email: String(value) }) }} />\n\t\t\t\t\t\t\t<div className=\"flex flex-wrap gap-2\">\n\t\t\t\t\t\t\t\t<KolButton _label={isEditing ? 'Save' : 'Create'} _variant=\"primary\" _on={{ onClick: saveDraft }} />\n\t\t\t\t\t\t\t\t<KolButton _label=\"Cancel\" _variant=\"secondary\" _on={{ onClick: closeDrawer }} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</KolDrawer>\n\n\t\t\t\t<KolTableStateful\n\t\t\t\t\t_label=\"User management\"\n\t\t\t\t\t_headers={HEADERS}\n\t\t\t\t\t_data={dataRows}\n\t\t\t\t\tclassName=\"block\"\n\t\t\t\t\t_selection={selection}\n\t\t\t\t\t_on={{ onSelectionChange: handleSelectionChange }}\n\t\t\t\t/>\n\t\t\t</section>\n\t\t</>\n\t);\n};\n",
2017
+ "kind": "sample"
2018
+ },
2019
2019
  {
2020
2020
  "id": "sample/tabs/align",
2021
2021
  "group": "tabs",
@@ -2541,7 +2541,7 @@
2541
2541
  "group": "spec",
2542
2542
  "name": "input-range",
2543
2543
  "path": "packages/tools/mcp/node_modules/@public-ui/components/doc/input-range.md",
2544
- "code": "# kol-input-range\n\n\n\n<!-- Auto Generated Below -->\n\n\n## Overview\n\nThe **Range** input type creates a slider control for selecting a numeric value within a defined range. Use the `_min`, `_max`, and `_step` properties to configure the range and step size.\n\n## Properties\n\n| Property | Attribute | Description | Type | Default |\n| --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------- |\n| `_accessKey` | `_access-key` | Defines the key combination that can be used to trigger or focus the component's interactive element. | `string \\| undefined` | `undefined` |\n| `_ariaDetails` | `_aria-details` | References an external element by ID that provides accessible details for this input. Uses ElementInternals.ariaDetailsElements to cross the Shadow DOM boundary. Supported by desktop screen readers (NVDA, JAWS with Chrome/Firefox). Not yet supported by mobile screen readers (TalkBack, VoiceOver iOS). | `string \\| undefined` | `undefined` |\n| `_autoComplete` | `_auto-complete` | Defines whether the input can be auto-completed. | `string \\| undefined` | `'off'` |\n| `_disabled` | `_disabled` | Makes the element not focusable and ignore all events. | `boolean \\| undefined` | `false` |\n| `_hideLabel` | `_hide-label` | Hides the caption by default and displays the caption text with a tooltip when the interactive element is focused or the mouse is over it. | `boolean \\| undefined` | `false` |\n| `_hideMsg` | `_hide-msg` | Hides the error message but leaves it in the DOM for the input's aria-describedby. | `boolean \\| undefined` | `false` |\n| `_hint` | `_hint` | Defines the hint text. | `string \\| undefined` | `''` |\n| `_icons` | `_icons` | Defines the icon classnames. | `string \\| undefined \\| { right?: IconOrIconClass \\| undefined; left?: IconOrIconClass \\| undefined; }` | `undefined` |\n| `_infoPopover` | -- | Defines the informational popover after the label. | `Omit<PopoverButtonProps, \"_hideLabel\" \\| \"_icons\" \\| \"_inline\"> & { _content: string; _icons: IconsPropType; } \\| undefined` | `undefined` |\n| `_label` _(required)_ | `_label` | Defines the visible or semantic label of the component (e.g. aria-label, label, headline, caption, summary, etc.). Set to `false` to enable the expert slot. | `string` | `undefined` |\n| `_max` | `_max` | Defines the maximum value of the element. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `100` |\n| `_min` | `_min` | Defines the smallest possible input value. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `0` |\n| `_msg` | `_msg` | Defines the properties for a message rendered as Alert component. | `Omit<AlertProps, \"_on\" \\| \"_variant\" \\| \"_hasCloser\" \\| \"_level\" \\| \"_label\"> & { _description: string; } \\| string \\| undefined` | `undefined` |\n| `_name` | `_name` | Defines the technical name of an input field. | `string \\| undefined` | `undefined` |\n| `_on` | -- | Gibt die EventCallback-Funktionen für das Input-Event an. | `InputTypeOnBlur & InputTypeOnClick & InputTypeOnChange & InputTypeOnFocus & InputTypeOnInput & InputTypeOnKeyDown \\| undefined` | `undefined` |\n| `_shortKey` | `_short-key` | Adds a visual shortcut hint after the label and instructs the screen reader to read the shortcut aloud. | `string \\| undefined` | `undefined` |\n| `_step` | `_step` | Defines the step size for value changes. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `undefined` |\n| `_suggestions` | `_suggestions` | Suggestions to provide for the input. | `W3CInputValue[] \\| string \\| undefined` | `undefined` |\n| `_tooltipAlign` | `_tooltip-align` | Defines where to show the Tooltip preferably: top, right, bottom or left. | `\"bottom\" \\| \"left\" \\| \"right\" \\| \"top\" \\| undefined` | `'top'` |\n| `_touched` | `_touched` | Shows if the input was touched by a user. | `boolean \\| undefined` | `false` |\n| `_value` | `_value` | Defines the value of the element. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `undefined` |\n| `_variant` | `_variant` | Defines which variant should be used for presentation. | `string \\| string[] \\| undefined` | `undefined` |\n\n\n## Methods\n\n### `click() => Promise<void>`\n\nClicks the primary interactive element inside this component.\n\n#### Returns\n\nType: `Promise<void>`\n\n\n\n### `focus(options?: KolFocusOptions) => Promise<void>`\n\nSets focus on the internal element.\n\n#### Parameters\n\n| Name | Type | Description |\n| --------- | ------------------------------ | ----------- |\n| `options` | `KolFocusOptions \\| undefined` | |\n\n#### Returns\n\nType: `Promise<void>`\n\n\n\n### `getValue() => Promise<number | NumberString | undefined>`\n\nReturns the current value.\n\n#### Returns\n\nType: `Promise<number | NumberString | undefined>`\n\n\n\n\n## Slots\n\n| Slot | Description |\n| ---------- | -------------------------------------------------------------------------------------------------------- |\n| | The label of the input field. |\n| `\"expert\"` | Custom label content, e.g. for rich text or icons. https://public-ui.github.io/docs/concepts/expert-slot |\n\n\n----------------------------------------------\n\n\n",
2544
+ "code": "# kol-input-range\n\n\n\n<!-- Auto Generated Below -->\n\n\n## Overview\n\nThe **Range** input type creates a slider control for selecting a numeric value within a defined range. Use the `_min`, `_max`, and `_step` properties to configure the range and step size.\n\nLike a native range input, the field always has a value: without `_value` it starts at the middle of the range and reports it as its value and form value.\n\n## Properties\n\n| Property | Attribute | Description | Type | Default |\n| --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------- |\n| `_accessKey` | `_access-key` | Defines the key combination that can be used to trigger or focus the component's interactive element. | `string \\| undefined` | `undefined` |\n| `_ariaDetails` | `_aria-details` | References an external element by ID that provides accessible details for this input. Uses ElementInternals.ariaDetailsElements to cross the Shadow DOM boundary. Supported by desktop screen readers (NVDA, JAWS with Chrome/Firefox). Not yet supported by mobile screen readers (TalkBack, VoiceOver iOS). | `string \\| undefined` | `undefined` |\n| `_autoComplete` | `_auto-complete` | Defines whether the input can be auto-completed. | `string \\| undefined` | `'off'` |\n| `_disabled` | `_disabled` | Makes the element not focusable and ignore all events. | `boolean \\| undefined` | `false` |\n| `_hideLabel` | `_hide-label` | Hides the caption by default and displays the caption text with a tooltip when the interactive element is focused or the mouse is over it. | `boolean \\| undefined` | `false` |\n| `_hideMsg` | `_hide-msg` | Hides the error message but leaves it in the DOM for the input's aria-describedby. | `boolean \\| undefined` | `false` |\n| `_hint` | `_hint` | Defines the hint text. | `string \\| undefined` | `''` |\n| `_icons` | `_icons` | Defines the icon classnames. | `string \\| undefined \\| { right?: IconOrIconClass \\| undefined; left?: IconOrIconClass \\| undefined; }` | `undefined` |\n| `_infoPopover` | -- | Defines the informational popover after the label. | `Omit<PopoverButtonProps, \"_hideLabel\" \\| \"_icons\" \\| \"_inline\"> & { _content: string; _icons: IconsPropType; } \\| undefined` | `undefined` |\n| `_label` _(required)_ | `_label` | Defines the visible or semantic label of the component (e.g. aria-label, label, headline, caption, summary, etc.). Set to `false` to enable the expert slot. | `string` | `undefined` |\n| `_max` | `_max` | Defines the maximum value of the element. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `100` |\n| `_min` | `_min` | Defines the smallest possible input value. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `0` |\n| `_msg` | `_msg` | Defines the properties for a message rendered as Alert component. | `Omit<AlertProps, \"_on\" \\| \"_variant\" \\| \"_hasCloser\" \\| \"_level\" \\| \"_label\"> & { _description: string; } \\| string \\| undefined` | `undefined` |\n| `_name` | `_name` | Defines the technical name of an input field. | `string \\| undefined` | `undefined` |\n| `_on` | -- | Gibt die EventCallback-Funktionen für das Input-Event an. | `InputTypeOnBlur & InputTypeOnClick & InputTypeOnChange & InputTypeOnFocus & InputTypeOnInput & InputTypeOnKeyDown \\| undefined` | `undefined` |\n| `_shortKey` | `_short-key` | Adds a visual shortcut hint after the label and instructs the screen reader to read the shortcut aloud. | `string \\| undefined` | `undefined` |\n| `_step` | `_step` | Defines the step size for value changes. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `undefined` |\n| `_suggestions` | `_suggestions` | Suggestions to provide for the input. | `W3CInputValue[] \\| string \\| undefined` | `undefined` |\n| `_tooltipAlign` | `_tooltip-align` | Defines where to show the Tooltip preferably: top, right, bottom or left. | `\"bottom\" \\| \"left\" \\| \"right\" \\| \"top\" \\| undefined` | `'top'` |\n| `_touched` | `_touched` | Shows if the input was touched by a user. | `boolean \\| undefined` | `false` |\n| `_value` | `_value` | Defines the value of the element. | `` `${number}.${number}` \\| `${number}` \\| number \\| undefined `` | `undefined` |\n| `_variant` | `_variant` | Defines which variant should be used for presentation. | `string \\| string[] \\| undefined` | `undefined` |\n\n\n## Methods\n\n### `click() => Promise<void>`\n\nClicks the primary interactive element inside this component.\n\n#### Returns\n\nType: `Promise<void>`\n\n\n\n### `focus(options?: KolFocusOptions) => Promise<void>`\n\nSets focus on the internal element.\n\n#### Parameters\n\n| Name | Type | Description |\n| --------- | ------------------------------ | ----------- |\n| `options` | `KolFocusOptions \\| undefined` | |\n\n#### Returns\n\nType: `Promise<void>`\n\n\n\n### `getValue() => Promise<number | NumberString | undefined>`\n\nReturns the current value.\n\n#### Returns\n\nType: `Promise<number | NumberString | undefined>`\n\n\n\n\n## Slots\n\n| Slot | Description |\n| ---------- | -------------------------------------------------------------------------------------------------------- |\n| | The label of the input field. |\n| `\"expert\"` | Custom label content, e.g. for rich text or icons. https://public-ui.github.io/docs/concepts/expert-slot |\n\n\n----------------------------------------------\n\n\n",
2545
2545
  "kind": "spec"
2546
2546
  },
2547
2547
  {