diff --git a/docs/guides/migration.en.md b/docs/guides/migration.en.md
index c284812bbc..ade6f98908 100644
--- a/docs/guides/migration.en.md
+++ b/docs/guides/migration.en.md
@@ -17,6 +17,7 @@ New versions include improvements but also contain **breaking changes**; they mu
11. **20.3.0**: the button-toggle review — ARIA semantics, keyboard navigation and signal inputs.
12. **20.3.0**: the form-field review — signals, accessibility and the removal of `mixinColor`.
13. **20.3.0**: the theme service review — signals, `auto` mode and built-in persistence.
+14. **20.3.0**: explicit prefix and suffix slots for tag content.
### 1. Upgrade to 18.5.3
@@ -766,6 +767,74 @@ themeService.currentTheme(); // read directly, or wrap with toObservable() if yo
**Custom themes and DI-based setup.** `setThemes()` still accepts any array of `{ name, className, colorScheme? }` objects — `colorScheme` (`'light' | 'dark'`) is optional: when set, it's each theme's own polarity, independent of its `name`, and is what `colorScheme()` (and `toggle()`) key off; when omitted, `colorScheme()` falls back to the OS preference for that theme. New: `kbqThemeProvider({ themes, mode, storageKey, autoLight, autoDark })` configures the service through DI instead of calling `setThemes()`/`setTheme()` imperatively. The active theme is always applied as a CSS class on `
` — the design tokens' `.kbq-light`/`.kbq-dark` styles depend on it, so there's no attribute-based alternative. `auto` resolves to the theme named `autoLight`/`autoDark` (`'light'`/`'dark'` by default) — set these if your custom theme set doesn't use those names, otherwise `auto` won't match any registered theme.
+### 14. Explicit tag content slots (20.3.0)
+
+Until 20.3.0 every directly projected element with `kbq-icon` was placed before the tag text, regardless of its position in the template. This implicit rule made icon placement depend on the component's projection selector and made the markup easy to break. Tag content now has explicit `kbqTagPrefix` and `kbqTagSuffix` slots:
+
+```html
+
+
+ Tag
+
+
+```
+
+`kbqTagRemove` and `kbqTagEditSubmit` are suffix controls already: `KbqTagSuffix` is attached to them through `hostDirectives`. Do not add `kbqTagSuffix` to the same element explicitly, because that applies the directive twice.
+
+#### Running the migration
+
+The `tag-slots` schematic runs automatically:
+
+```bash
+ng update @koobiq/components@20
+```
+
+Or manually — for example, if you have already upgraded to 20.3.0:
+
+```bash
+ng g @koobiq/components:tag-slots --project
+```
+
+To preview the changes without writing them, use `--fix=false`:
+
+```bash
+ng g @koobiq/components:tag-slots --project --fix=false
+```
+
+#### What is fixed automatically
+
+The schematic adds `kbqTagPrefix` to every legacy, directly projected `kbq-icon` that is not already assigned to a slot and is not a remove or edit-submit control:
+
+```html
+
+
+ Tag
+
+
+
+
+
+
+ Tag
+
+
+
+```
+
+Source order is not used to infer a suffix: under the old projection rule all such icons were prefixes. Existing `kbqTagPrefix`, `kbqTagSuffix`, `kbqTagRemove` and `kbqTagEditSubmit` attributes are left unchanged, so the migration is idempotent.
+
+#### What you need to fix manually
+
+**Intentional trailing content.** Add `kbqTagSuffix` yourself when an icon or another element should follow the label. The schematic cannot infer a new visual intent from markup whose old rendering always placed `kbq-icon` before the label.
+
+**Content outside the legacy icon selector.** Elements with only `kbq-icon-button` or `kbq-icon-item`, nested consumer wrappers and nodes with `ngProjectAs` are left unchanged because they were not directly matched by the old `kbq-icon` slot. Review them only if you want to move them to one of the new slots.
+
+**Standalone imports.** `KbqTagsModule` exports both slot directives. If a standalone component imports `KbqTag` directly instead of the module, also import `KbqTagPrefix` and/or `KbqTagSuffix` when using them; otherwise their host classes and slot spacing are not applied.
+
+
+
+**Deprecated imperative placement and styles.** Replace calls to `addClassModificatorForIcons()` with explicit slot directives, and migrate custom selectors from `.kbq-icon_left` to `.kbq-tag-prefix`. The method and the old selector are deprecated and will be removed in the next major version.
+
### After the migration
The migration is regex-based and does not rewrite aliased imports, local variables, or re-exports — **review the diff before committing**, rebuild the project and run your tests. The full list of breaking changes is on the [Angular 20 breaking changes](https://github.com/koobiq/angular-components/blob/main/docs/guides/angular-20-breaking-changes.en.md) page.
diff --git a/docs/guides/migration.ru.md b/docs/guides/migration.ru.md
index 565f12e210..fe6294451f 100644
--- a/docs/guides/migration.ru.md
+++ b/docs/guides/migration.ru.md
@@ -17,6 +17,7 @@
11. **20.3.0**: ревью группы кнопок — ARIA-семантика, навигация с клавиатуры и сигнальные входы.
12. **20.3.0**: ревью поля формы — сигналы, доступность и удаление `mixinColor`.
13. **20.3.0**: ревью сервиса темизации — сигналы, режим `auto` и сохранение выбора из коробки.
+14. **20.3.0**: явные prefix- и suffix-слоты для содержимого тегов.
### 1. Обновление до 18.5.3
@@ -766,6 +767,74 @@ themeService.currentTheme(); // читайте напрямую, либо обе
**Кастомные темы и настройка через DI.** `setThemes()` по-прежнему принимает любой массив объектов `{ name, className, colorScheme? }` — `colorScheme` (`'light' | 'dark'`) необязателен: если задан, это собственная «полярность» темы, независимая от её `name`, и именно на неё опирается `colorScheme()` (а также `toggle()`); если не задан, `colorScheme()` для этой темы откатывается на предпочтение ОС. Новое: `kbqThemeProvider({ themes, mode, storageKey, autoLight, autoDark })` настраивает сервис через DI вместо императивных вызовов `setThemes()`/`setTheme()`. Активная тема всегда применяется как CSS-класс на `` — от этого зависят стили `.kbq-light`/`.kbq-dark` дизайн-токенов, поэтому альтернативы через атрибут нет. `auto` разрешается в тему с именем `autoLight`/`autoDark` (по умолчанию `'light'`/`'dark'`) — задайте их, если ваш набор кастомных тем использует другие имена, иначе `auto` не совпадёт ни с одной зарегистрированной темой.
+### 14. Явные слоты содержимого тегов (20.3.0)
+
+До 20.3.0 каждый непосредственно спроецированный элемент с `kbq-icon` размещался перед текстом тега независимо от своего положения в шаблоне. Это неявное правило привязывало расположение иконок к селектору проекции компонента, поэтому разметку было легко сломать. Теперь у содержимого тега есть явные слоты `kbqTagPrefix` и `kbqTagSuffix`:
+
+```html
+
+
+ Тег
+
+
+```
+
+`kbqTagRemove` и `kbqTagEditSubmit` уже являются suffix-контролами: `KbqTagSuffix` подключается к ним через `hostDirectives`. Не добавляйте `kbqTagSuffix` на тот же элемент явно, иначе директива будет применена дважды.
+
+#### Запуск миграции
+
+Схематик `tag-slots` запускается автоматически:
+
+```bash
+ng update @koobiq/components@20
+```
+
+Или вручную — например, если вы уже обновились до 20.3.0:
+
+```bash
+ng g @koobiq/components:tag-slots --project
+```
+
+Посмотреть изменения без записи можно с помощью `--fix=false`:
+
+```bash
+ng g @koobiq/components:tag-slots --project --fix=false
+```
+
+#### Что исправляется автоматически
+
+Схематик добавляет `kbqTagPrefix` каждой непосредственно спроецированной legacy-иконке с `kbq-icon`, которая ещё не помещена в слот и не является контролом удаления или подтверждения редактирования:
+
+```html
+
+
+ Тег
+
+
+
+
+
+
+ Тег
+
+
+
+```
+
+Положение в исходном коде не используется для определения suffix-слота: по старому правилу проекции все такие иконки были префиксами. Существующие атрибуты `kbqTagPrefix`, `kbqTagSuffix`, `kbqTagRemove` и `kbqTagEditSubmit` не изменяются, поэтому миграция идемпотентна.
+
+#### Что нужно исправить вручную
+
+**Намеренно замыкающий контент.** Добавьте `kbqTagSuffix` самостоятельно, если иконка или другой элемент должны располагаться после текста. Схематик не может определить новое визуальное намерение по разметке, в которой старое правило всегда размещало `kbq-icon` перед текстом.
+
+**Контент вне старого селектора иконок.** Элементы только с `kbq-icon-button` или `kbq-icon-item`, вложенные пользовательские обёртки и узлы с `ngProjectAs` остаются без изменений, поскольку старый слот `kbq-icon` не находил их напрямую. Проверьте их только в том случае, если хотите перенести их в один из новых слотов.
+
+**Standalone-импорты.** `KbqTagsModule` экспортирует обе slot-директивы. Если standalone-компонент импортирует `KbqTag` напрямую вместо модуля, при использовании слотов также импортируйте `KbqTagPrefix` и/или `KbqTagSuffix` — иначе их host-классы и отступы слотов не применятся.
+
+
+
+**Устаревшее императивное расположение и стили.** Замените вызовы `addClassModificatorForIcons()` явными slot-директивами, а пользовательские селекторы `.kbq-icon_left` — на `.kbq-tag-prefix`. Метод и старый селектор устарели и будут удалены в следующей мажорной версии.
+
### После миграции
Миграция работает на регулярных выражениях и не переписывает алиасные импорты, локальные переменные и ре-экспорты — **проверьте диф перед коммитом**, пересоберите проект и прогоните тесты. Полный список ломающих изменений — на странице [Ломающие изменения — Angular 20](https://github.com/koobiq/angular-components/blob/main/docs/guides/angular-20-breaking-changes.ru.md).
diff --git a/packages/components-dev/all/template.html b/packages/components-dev/all/template.html
index 0e53f27d31..a300c7bbaf 100644
--- a/packages/components-dev/all/template.html
+++ b/packages/components-dev/all/template.html
@@ -886,19 +886,19 @@
`,
styleUrls: ['./tag.scss'],
@@ -218,10 +237,7 @@ export class KbqTagEditInput {
hostDirectives: [CdkDrag],
exportAs: 'kbqTag'
})
-export class KbqTag
- extends KbqColorDirective
- implements IFocusableOption, OnDestroy, KbqTitleTextRef, AfterContentInit, AfterViewInit
-{
+export class KbqTag extends KbqColorDirective implements IFocusableOption, OnDestroy, KbqTitleTextRef, AfterViewInit {
changeDetectorRef = inject(ChangeDetectorRef);
private readonly focusMonitor = inject(FocusMonitor);
@@ -425,10 +441,6 @@ export class KbqTag
this.setupDragInitialProperties();
}
- ngAfterContentInit() {
- this.addClassModificatorForIcons();
- }
-
ngAfterViewInit(): void {
this.setupFocusMonitor();
}
@@ -439,8 +451,13 @@ export class KbqTag
this.destroyed.emit({ tag: this });
}
- /** @docs-private */
- addClassModificatorForIcons() {
+ /**
+ * Adds the legacy positional classes inferred from projected icons.
+ *
+ * @deprecated Use `KbqTagPrefix` and `KbqTagSuffix` to declare the position explicitly.
+ * @docs-private
+ */
+ addClassModificatorForIcons(): void {
const icons = this.contentChildren().map((item) => item.elementRef.nativeElement);
if (icons.length === 1) {
@@ -738,7 +755,8 @@ export class KbqTag
'[attr.tabindex]': '-1',
'(click)': 'handleClick($event)',
'(focus)': 'focus($event)'
- }
+ },
+ hostDirectives: [KbqTagSuffix]
})
export class KbqTagRemove {
protected parentTag = inject(KbqTag);
diff --git a/packages/components/tags/tag.en.md b/packages/components/tags/tag.en.md
index ae1c54691f..655f4f956e 100644
--- a/packages/components/tags/tag.en.md
+++ b/packages/components/tags/tag.en.md
@@ -12,6 +12,8 @@ Tags are used within the [`Tag list`](/en/components/tag-list) component in inpu
### Icon
+Use `kbqTagPrefix` and `kbqTagSuffix` to explicitly place content before or after the tag text. `kbqTagRemove` and `kbqTagEditSubmit` are suffix controls already; do not combine them with `kbqTagSuffix`, because it is supplied through their host directives.
+
### “Remove” Button
diff --git a/packages/components/tags/tag.module.ts b/packages/components/tags/tag.module.ts
index 0a53ed6cad..9d3abe4044 100644
--- a/packages/components/tags/tag.module.ts
+++ b/packages/components/tags/tag.module.ts
@@ -9,13 +9,17 @@ import {
KbqTagAvatar,
KbqTagEditInput,
KbqTagEditSubmit,
+ KbqTagPrefix,
KbqTagRemove,
+ KbqTagSuffix,
KbqTagTrailingIcon
} from './tag.component';
const COMPONENTS = [
KbqTagEditSubmit,
- KbqTagEditInput
+ KbqTagEditInput,
+ KbqTagPrefix,
+ KbqTagSuffix
];
@NgModule({
diff --git a/packages/components/tags/tag.ru.md b/packages/components/tags/tag.ru.md
index 73151d3455..9c7030af02 100644
--- a/packages/components/tags/tag.ru.md
+++ b/packages/components/tags/tag.ru.md
@@ -12,6 +12,8 @@
### Иконка
+Используйте `kbqTagPrefix` и `kbqTagSuffix`, чтобы явно расположить контент до или после текста тега. `kbqTagRemove` и `kbqTagEditSubmit` уже являются suffix-контролами: не комбинируйте их с `kbqTagSuffix`, поскольку эта директива подключается через их `hostDirectives`.
+
### Длинный текст
diff --git a/packages/components/tags/tag.scss b/packages/components/tags/tag.scss
index afec1b1d08..7b3101d481 100644
--- a/packages/components/tags/tag.scss
+++ b/packages/components/tags/tag.scss
@@ -48,6 +48,20 @@
margin: 0 var(--kbq-tag-size-content-gap-horizontal);
}
+ & .kbq-tag-prefix {
+ flex-shrink: 0;
+ margin-inline-start: var(--kbq-tag-size-icon-margin-left);
+ }
+
+ & .kbq-tag-suffix {
+ flex-shrink: 0;
+ }
+
+ & .kbq-tag-suffix:not(.kbq-tag-remove, .kbq-tag-edit-submit) {
+ margin-inline-end: var(--kbq-tag-size-close-button-margin-right);
+ }
+
+ // @deprecated Use `.kbq-tag-prefix` (`kbqTagPrefix`) instead. Will be removed in the next major version.
& .kbq-icon_left {
margin-left: var(--kbq-tag-size-icon-margin-left);
}
diff --git a/packages/docs-examples/components/select/select-custom-tag-content/select-custom-tag-content-example.ts b/packages/docs-examples/components/select/select-custom-tag-content/select-custom-tag-content-example.ts
index 6e27fb7f1a..b52506d47b 100644
--- a/packages/docs-examples/components/select/select-custom-tag-content/select-custom-tag-content-example.ts
+++ b/packages/docs-examples/components/select/select-custom-tag-content/select-custom-tag-content-example.ts
@@ -23,7 +23,7 @@ import { KbqTagsModule } from '@koobiq/components/tags';
[disabled]="option.disabled || select.disabled"
[color]="severities[option.value].color"
>
-
+
{{ option.viewValue }}
@if (!option.disabled && !select.disabled) {
diff --git a/packages/docs-examples/components/tags/tag-fill-and-style/tag-fill-and-style-example.ts b/packages/docs-examples/components/tags/tag-fill-and-style/tag-fill-and-style-example.ts
index 1b52c86a73..2adff74b2c 100644
--- a/packages/docs-examples/components/tags/tag-fill-and-style/tag-fill-and-style-example.ts
+++ b/packages/docs-examples/components/tags/tag-fill-and-style/tag-fill-and-style-example.ts
@@ -14,22 +14,22 @@ import { KbqTagsModule } from '@koobiq/components/tags';
],
template: `
-
+
Contrast
-
+
Theme
-
+
Error
-
+
Warning
diff --git a/packages/docs-examples/components/tags/tag-with-icon/tag-with-icon-example.ts b/packages/docs-examples/components/tags/tag-with-icon/tag-with-icon-example.ts
index d56828e8b5..c4d2062093 100644
--- a/packages/docs-examples/components/tags/tag-with-icon/tag-with-icon-example.ts
+++ b/packages/docs-examples/components/tags/tag-with-icon/tag-with-icon-example.ts
@@ -14,8 +14,9 @@ import { KbqTagsModule } from '@koobiq/components/tags';
],
template: `
-
+
Tag
+
`,
changeDetection: ChangeDetectionStrategy.OnPush,
diff --git a/packages/schematics/src/collection.json b/packages/schematics/src/collection.json
index 37ff7cdaab..407fc37a56 100644
--- a/packages/schematics/src/collection.json
+++ b/packages/schematics/src/collection.json
@@ -82,6 +82,11 @@
"factory": "./migrations/button-truncation/index",
"schema": "./migrations/button-truncation/schema.json"
},
+ "tag-slots": {
+ "description": "Marks legacy implicit kbq-icon content in tags with kbqTagPrefix so tags can use explicit content slots",
+ "factory": "./migrations/tag-slots/index",
+ "schema": "./migrations/tag-slots/schema.json"
+ },
"button-supported-colors": {
"description": "Removes button colors outside theme/theme-fade/contrast/contrast-fade from templates and warns about the ones it cannot resolve, plus the transparent default color change",
"factory": "./migrations/button-supported-colors/index",
diff --git a/packages/schematics/src/migrations.json b/packages/schematics/src/migrations.json
index 29a7405feb..12f86ab295 100644
--- a/packages/schematics/src/migrations.json
+++ b/packages/schematics/src/migrations.json
@@ -56,6 +56,11 @@
"description": "Adds the packages that became mandatory peerDependencies of @koobiq/components — @angular/animations (at the range the project uses for @angular/core, because every animations release pins core exactly), overlayscrollbars and @koobiq/date-adapter — and schedules an install. npm adds a newly mandatory peer on upgrade by itself, but Yarn does not and pnpm only does with auto-install-peers, so those projects would otherwise upgrade into 'Cannot find module' at build time. Warns when @koobiq/date-adapter is already present, since an existing entry is left untouched and anything below 3.4.0 lacks the addCalendarUnits()/startOf() the date components call.",
"factory": "./migrations/mandatory-peer-dependencies/index"
},
+ "tag-slots": {
+ "version": "20.3.0-0",
+ "description": "Migrates KbqTag consumers from the legacy implicit icon placement to explicit content slots. Adds kbqTagPrefix to every directly projected kbq-icon that is not already a prefix/suffix, remove control, or edit-submit control. The migration preserves current rendering even when the icon is written after the label, because the old component always projected such icons before the label; kbq-icon-button and kbq-icon-item without kbq-icon remain unchanged because they were not part of the legacy slot.",
+ "factory": "./migrations/tag-slots/index"
+ },
"button-supported-colors": {
"version": "20.3.0-0",
"description": "Migrates consumers to the narrowed button color set. `color` on KbqButton, KbqButtonGroupRoot and KbqSplitButton (and `kbqOkType` on KbqModalComponent / ModalOptions) now accepts theme, theme-fade, contrast and contrast-fade only — every other value matched no rule in kbq-button-theme() and rendered the button as a native one. Removes such a color written as a literal from templates, which is appearance-preserving now that each style falls back to its own default color, and warns about the ones it cannot resolve (enum members, programmatic assignment, members still typed KbqComponentColors / ThemePalette). Also flags the silent part: a transparent button with no explicit color now defaults to contrast instead of contrast-fade, so a `.kbq-button_transparent.kbq-contrast-fade` override stops matching.",
diff --git a/packages/schematics/src/migrations/tag-slots/README.md b/packages/schematics/src/migrations/tag-slots/README.md
new file mode 100644
index 0000000000..2ce1010f09
--- /dev/null
+++ b/packages/schematics/src/migrations/tag-slots/README.md
@@ -0,0 +1,41 @@
+### Tag slots schematic
+
+This schematic migrates legacy implicit icons in `KbqTag` to the explicit `kbqTagPrefix` slot. The old tag
+template projected every element with `kbq-icon` before the label regardless of its source position, so every
+matching element receives `kbqTagPrefix`; the migration never guesses a suffix from source order. Elements
+with only `kbq-icon-button` or `kbq-icon-item` were not part of that legacy slot and remain unchanged.
+
+`kbqTagRemove`, `kbqTagEditSubmit`, and icons already marked with `kbqTagPrefix` or `kbqTagSuffix` are left
+unchanged.
+
+Usage for Angular CLI:
+
+```shell
+ng g @koobiq/components:tag-slots --project
+```
+
+Run without writing first:
+
+```shell
+ng g @koobiq/components:tag-slots --project --fix=false
+```
+
+#### Before
+
+```html
+
+ Tag
+
+
+
+```
+
+#### After
+
+```html
+
+ Tag
+
+
+
+```
diff --git a/packages/schematics/src/migrations/tag-slots/index.spec.ts b/packages/schematics/src/migrations/tag-slots/index.spec.ts
new file mode 100644
index 0000000000..935d5716a4
--- /dev/null
+++ b/packages/schematics/src/migrations/tag-slots/index.spec.ts
@@ -0,0 +1,158 @@
+import { workspaces } from '@angular-devkit/core';
+import { Tree } from '@angular-devkit/schematics';
+import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
+import { getWorkspace } from '@schematics/angular/utility/workspace';
+import * as path from 'path';
+import { createTestApp } from '../../utils/testing';
+import { Schema } from './schema';
+
+const collectionPath = path.join(__dirname, '../../collection.json');
+const SCHEMATIC_NAME = 'tag-slots';
+
+const getProjectContentPaths = (project: workspaces.ProjectDefinition, tree: Tree | UnitTestTree) => {
+ const root = `/${project.root}/src/app`;
+
+ return {
+ templatePath: tree.exists(`${root}/app.html`) ? `${root}/app.html` : `${root}/app.component.html`,
+ tsPath: tree.exists(`${root}/app.ts`) ? `${root}/app.ts` : `${root}/app.component.ts`
+ };
+};
+
+describe(SCHEMATIC_NAME, () => {
+ let runner: SchematicTestRunner;
+ let appTree: Tree;
+ let projects: workspaces.ProjectDefinitionCollection;
+ let projectKey: string;
+
+ const migrateTemplate = async (template: string, options: Partial = {}) => {
+ const { templatePath } = getProjectContentPaths(projects.get(projectKey)!, appTree);
+
+ appTree.overwrite(templatePath, template);
+
+ const updatedTree = await runner.runSchematic(
+ SCHEMATIC_NAME,
+ { project: projectKey, fix: true, ...options } satisfies Schema,
+ appTree
+ );
+
+ return updatedTree.read(templatePath)!.toString();
+ };
+
+ beforeEach(async () => {
+ runner = new SchematicTestRunner('schematics', collectionPath);
+ appTree = await createTestApp(runner, { style: 'scss' });
+
+ const workspace = await getWorkspace(appTree);
+
+ projects = workspace.projects as unknown as workspaces.ProjectDefinitionCollection;
+ [projectKey] = projects.keys();
+ });
+
+ it('should run for a specified project', async () => {
+ await runner.runSchematic(SCHEMATIC_NAME, { project: projectKey, fix: true } satisfies Schema, appTree);
+ });
+
+ it('should mark every legacy tag icon as prefix regardless of source order', async () => {
+ const result = await migrateTemplate(
+ 'Tag'
+ );
+
+ expect(result).toContain('');
+ expect(result).toContain('');
+ expect(result).not.toContain('kbqTagSuffix kbq-icon');
+ });
+
+ it.each([
+ 'Tag',
+ '