diff --git a/angular.json b/angular.json index b17adcee0..4e95756a3 100644 --- a/angular.json +++ b/angular.json @@ -534,6 +534,36 @@ } } }, + "dev-flag": { + "projectType": "application", + "root": "packages/components-dev/flag", + "sourceRoot": "packages/components-dev/flag", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": { + "base": "dist/components-dev/flag" + }, + "tsConfig": "packages/components-dev/flag/tsconfig.app.json", + "index": "packages/components-dev/index.html", + "styles": ["packages/components-dev/main.scss"], + "polyfills": ["zone.js"], + "extractLicenses": false, + "sourceMap": true, + "optimization": false, + "namedChunks": true, + "browser": "packages/components-dev/flag/main.ts" + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "buildTarget": "dev-flag:build" + } + } + } + }, "dev-breadcrumbs": { "projectType": "application", "root": "packages/components-dev/breadcrumbs", diff --git a/apps/docs/src/app/components/stackblitz/stackblitz-writer.ts b/apps/docs/src/app/components/stackblitz/stackblitz-writer.ts index 2468e43cd..b10f44bd6 100644 --- a/apps/docs/src/app/components/stackblitz/stackblitz-writer.ts +++ b/apps/docs/src/app/components/stackblitz/stackblitz-writer.ts @@ -22,6 +22,9 @@ const TEMPLATE_PATH = 'assets/stackblitz/'; * These are only included when the example actually uses them. */ const OPTIONAL_PACKAGE_JSON_DEPENDENCIES = { + 'country-flag-icons': { + 'country-flag-icons': '^1.6.20' + }, '@koobiq/components/markdown': { marked: '^17.0.3' }, diff --git a/apps/docs/src/app/structure.ts b/apps/docs/src/app/structure.ts index aaa5667f8..0dc8a2d2b 100644 --- a/apps/docs/src/app/structure.ts +++ b/apps/docs/src/app/structure.ts @@ -41,6 +41,7 @@ export enum DocsStructureItemId { Input = 'input', FileUpload = 'file-upload', FilterBar = 'filter-bar', + Flag = 'flag', FormField = 'form-field', Highlight = 'highlight', LayoutFlex = 'layout-flex', @@ -594,6 +595,18 @@ const structure: DocsStructure = makeStructure({ apiId: 'filter-bar', hasExamples: true }, + { + id: DocsStructureItemId.Flag, + name: { + ru: 'Flag', + en: 'Flag' + }, + svgPreview: '', + hasApi: true, + apiId: 'flag', + hasExamples: false, + isNew: expiresAt('2026-10-07') + }, { id: DocsStructureItemId.FormField, name: { diff --git a/package.json b/package.json index d927ccb8e..c5ef7b38c 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "conventional-changelog": "^3.1.10", "conventional-changelog-angular": "5.0.13", "conventional-changelog-writer": "5.0.1", + "country-flag-icons": "^1.6.20", "cspell": "^8.19.4", "dotenv": "^16.6.1", "eslint": "^9.0.0", @@ -197,6 +198,7 @@ "dev:autocomplete": "ng serve dev-autocomplete --port 3003", "dev:badge": "ng serve dev-badge --port 3003", "dev:breadcrumbs": "ng serve dev-breadcrumbs --port 3003", + "dev:flag": "ng serve dev-flag --port 3003", "dev:button": "ng serve dev-button --port 3003", "dev:button-toggle": "ng serve dev-button-toggle --port 3003", "dev:checkbox": "ng serve dev-checkbox --port 3003", diff --git a/packages/components-dev/flag/main.ts b/packages/components-dev/flag/main.ts new file mode 100644 index 000000000..2bbc80baf --- /dev/null +++ b/packages/components-dev/flag/main.ts @@ -0,0 +1,9 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { DevApp } from './module'; + +bootstrapApplication(DevApp, { + providers: [ + provideAnimations() + ] +}).catch((error) => console.error(error)); diff --git a/packages/components-dev/flag/module.ts b/packages/components-dev/flag/module.ts new file mode 100644 index 000000000..8e6c562a9 --- /dev/null +++ b/packages/components-dev/flag/module.ts @@ -0,0 +1,32 @@ +import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; +import { FlagExamplesModule } from '../../docs-examples/components/flag'; +import { DevThemeToggle } from '../theme-toggle'; + +@Component({ + selector: 'dev-examples', + imports: [FlagExamplesModule], + template: ` + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DevDocsExamples {} + +@Component({ + selector: 'dev-app', + imports: [DevDocsExamples, DevThemeToggle], + template: ` + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class DevApp {} diff --git a/packages/components-dev/flag/tsconfig.app.json b/packages/components-dev/flag/tsconfig.app.json new file mode 100644 index 000000000..81fb90e7c --- /dev/null +++ b/packages/components-dev/flag/tsconfig.app.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*.d.ts"], + "files": ["main.ts"] +} diff --git a/packages/components/flag/__screenshots__/01-dark.png b/packages/components/flag/__screenshots__/01-dark.png new file mode 100644 index 000000000..ea97b32ea Binary files /dev/null and b/packages/components/flag/__screenshots__/01-dark.png differ diff --git a/packages/components/flag/__screenshots__/01-light.png b/packages/components/flag/__screenshots__/01-light.png new file mode 100644 index 000000000..ff78991fb Binary files /dev/null and b/packages/components/flag/__screenshots__/01-light.png differ diff --git a/packages/components/flag/e2e.playwright-spec.ts b/packages/components/flag/e2e.playwright-spec.ts new file mode 100644 index 000000000..93517a880 --- /dev/null +++ b/packages/components/flag/e2e.playwright-spec.ts @@ -0,0 +1,15 @@ +import { expect, Page, test } from '@playwright/test'; +import { e2eEnableDarkTheme } from 'packages/e2e/utils'; + +test.describe('KbqFlagModule', () => { + test.describe('E2eFlagStyles', () => { + const getComponent = (page: Page) => page.getByTestId('e2eFlagStyles'); + + test('styles', async ({ page }) => { + await page.goto('/E2eFlagStyles'); + await expect(getComponent(page)).toHaveScreenshot('01-light.png'); + await e2eEnableDarkTheme(page); + await expect(getComponent(page)).toHaveScreenshot('01-dark.png'); + }); + }); +}); diff --git a/packages/components/flag/e2e.ts b/packages/components/flag/e2e.ts new file mode 100644 index 000000000..56fc9636b --- /dev/null +++ b/packages/components/flag/e2e.ts @@ -0,0 +1,54 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { KbqFlagModule, KbqFlagShape } from '@koobiq/components/flag'; + +// A minimal inline SVG (Norway-like) is enough to exercise shape/shadow decoration without +// depending on a flag data package in the e2e app. +const sampleFlag = ` + + + + + +`; + +@Component({ + selector: 'e2e-flag-styles', + imports: [KbqFlagModule], + template: ` +
+ @for (shape of shapes; track shape) { + + } +
+
+ + + +
+ `, + styles: ` + :host { + display: inline-flex; + flex-direction: column; + gap: var(--kbq-size-l); + padding: var(--kbq-size-l); + font-size: 48px; + } + + :host > div { + display: flex; + align-items: center; + gap: var(--kbq-size-l); + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'data-testid': 'e2eFlagStyles' + } +}) +export class E2eFlagStyles { + private readonly sanitizer = inject(DomSanitizer); + protected readonly flag = this.sanitizer.bypassSecurityTrustHtml(sampleFlag); + protected readonly shapes: KbqFlagShape[] = ['rectangle', 'square', 'circle']; +} diff --git a/packages/components/flag/flag.en.md b/packages/components/flag/flag.en.md new file mode 100644 index 000000000..833430192 --- /dev/null +++ b/packages/components/flag/flag.en.md @@ -0,0 +1,107 @@ +
+
Note
+
+ +Use the [country-flag-icons](https://www.npmjs.com/package/country-flag-icons) package to show a small country flag in your product: + +```bash +npm install country-flag-icons +``` + +
+
+ +- **Full ISO 3166-1 coverage**. Every existing country is identified by a two-letter code (RU, DE, FR). +- **Flags are redrawn for small sizes**. Most sets take detailed SVGs from Wikimedia Commons, and at interface sizes (16—24 px) the small coats of arms, inscriptions and emblems on them become illegible. In country-flag-icons the details are simplified, so flags stay recognizable even at icon size. +- Actively maintained, MIT licensed. + +## Aspect ratio + +By default flags use a 3:2 ratio. This is the primary format — use it in most scenarios: inline with text, lists, selects. Flags with a different ratio (e.g. 4:3) or from other sources are supported by overriding the `--kbq-flag-aspect-ratio` CSS variable. + + + +Square flags live in a separate subpackage and are needed where the 3:2 format does not fit: avatar selectors, compact circles, dense grids. + +Most 1:1 versions are a center crop of the 3:2 flag. For flags that lose meaning when cropped this way, the package ships dedicated square variants (e.g. EU and KR) — they are substituted automatically from the same subpackage. + + + +## Inset shadow: separating from the background + +Flags get a thin inset shadow (an inset outline) along the edge. Without it a flag blends into the background: + +- lots of white: the flag blends into the light page background. +- some flags contain black or dark colors: they blend into the background in the dark theme. + +That is why the shadow color depends on the theme: in the light theme the shadow is dark, in the dark theme it lightens or is removed. The shadow is implemented as an inset box-shadow on a pseudo-element over the flag, and its color comes from a CSS variable that switches together with the theme. + + + +## A flag is not a language + +A flag denotes a country or region, not a language. Do not use a flag to pick an interface language: one language is spoken in several countries (Spanish is not only Spain), and one country may have several languages. + + + +## When the flag is missing + +First check availability via `hasFlag(code)` and do not render an empty gap: + +```ts +import { hasFlag } from 'country-flag-icons'; + +hasFlag('RU'); // true +hasFlag('ZZ'); // false +``` + +Then it depends on the situation. + +**Unknown or invalid code.** Show a neutral placeholder (a gray rectangle, square, circle or a globe icon). Empty space is acceptable only if it does not break the layout. + + + +**You need a non-country flag** (organization, historical or disputed territory, federal subject). Do not add it to the country-flag-icons package. Add it to your internal set so that the way it is used does not change. + +**Supranational flags.** The EU flag is available as a separate 1:1 version; for the rest (ASEAN, UN, etc.) create your own images in the project. + +**It is a real country, but the flag really is missing.** This is unlikely — the ISO 3166-1 standard is fully covered — but first update the package version. If the flag is outdated or missing, open an issue or PR in the package repository, and temporarily use your own image in your project. + +**You need to show the flag at a large size.** In the package the flags are redrawn and simplified for small sizes. To show them at a large size you need flags in their original detail — use a third-party package. In most sources flags are provided without redrawing. + +## Accessibility + +If the flag carries meaning (for example, it is the only indicator of the selected country), pass a text caption: + +```html + +``` + +If there is already adjacent text (inline, option, block), the flag is decorative — mark it `decorative` so it is hidden from screen readers and does not duplicate the caption. + +```html + +Germany +``` + +In short: a flag must always have a text alternative — either visible text next to it (then the flag is `decorative`), or a `label` caption for the screen reader (when there is no visible text). Only the third case is not allowed — when there is neither. + +## Examples + +### With shadow and volume + +A gradient imitating folds is layered over the image; the flag is rounded and casts a shadow. + + + +### Circular flag + +Use the 1:1 aspect ratio version. + + + +### Integer sizes + +Choose a flag size whose dimensions are multiples of a pixel — otherwise the image edges will be blurry. + + diff --git a/packages/components/flag/flag.ru.md b/packages/components/flag/flag.ru.md new file mode 100644 index 000000000..474e990f0 --- /dev/null +++ b/packages/components/flag/flag.ru.md @@ -0,0 +1,106 @@ +Компонент `kbq-flag` показывает флаг страны. Он не содержит изображений, а декорирует переданное содержимое: управляет формой и тенью, обеспечивает доступность. Поэтому флаг выглядит одинаково в продукте независимо от источника изображения. + +Для работы с `kbq-flag` рекомендуется пакет [country-flag-icons](https://www.npmjs.com/package/country-flag-icons): + +```bash +npm install country-flag-icons +``` + +Особенности пакета: + +- **Полное покрытие ISO 3166-1**. Все страны обозначаются двухбуквенным кодом (RU, DE, FR). +- **Флаги перерисованы под мелкий размер**. Большинство наборов используют детализированные SVG с Викисклада. В интерфейсных размерах (16—24 px) мелкие гербы, надписи и эмблемы на них становятся неразборчивыми. В country-flag-icons детали упрощены, поэтому флаги остаются узнаваемыми даже в размере иконки. +- Активная поддержка и лицензия MIT. + +## Форма + +### Прямоугольник + +Это основной формат — используйте его в большинстве сценариев: инлайн с текстом, списки, селекты. Флаги с другим соотношением (например, 4:3) поддерживаются через переопределение CSS-переменной `--kbq-flag-aspect-ratio`. + + + +### Квадрат + +Для квадратной формы берите изображения из директории `1x1/` пакета. Не обрезайте прямоугольный флаг самостоятельно — простое кадрирование по центру может убрать значимые элементы герба или флага. В пакете уже подготовлены версии 1:1 для каждой страны, а часть флагов — например, EU и KR — перерисована заново, чтобы не терять смысл при таком формате. + + + +### Круг + +Используйте изображение с соотношением сторон 1:1. + + + +## Внутренняя тень: отделение от фона + +Флагам добавляется тонкая внутренняя тень (inset-обводка) по краю. Без нее флаг сливается с фоном: + +- много белого цвета: флаг сливается со светлым фоном страницы. +- в части флагов есть черный или темный цвет: он сливается с фоном в темной теме. + +Поэтому цвет тени зависит от темы: в светлой теме тень темная, в темной — светлеет. + + + +## Флаг — это не язык + +Флаг обозначает страну или регион, а не язык. Не используйте флаг для выбора языка интерфейса: один язык бывает в нескольких странах (испанский — не только Испания), а в одной стране бывает несколько языков. + + + +## Если в пакете нет нужного флага + +Сначала проверьте наличие флага через `hasFlag(code)` и не показывайте пустоту: + +```ts +import { hasFlag } from 'country-flag-icons'; + +hasFlag('RU'); // true +hasFlag('ZZ'); // false +``` + +Дальше — по ситуации. + +**Неизвестный или некорректный код.** Показывайте нейтральную заглушку (серый прямоугольник, квадрат, круг или иконку глобуса). Пустота допустима, только если это не ломает верстку. + + + +**Нужен флаг не-страны** (организация, историческая или спорная территория, субъект федерации). Пакет — сторонний продукт, ограниченный стандартом ISO 3166-1: такие флаги в него не попадают. Заведите их во внутреннем наборе проекта, чтобы способ использования `kbq-flag` не менялся. + +**Наднациональные флаги.** Флаг ЕС доступен как отдельная версия 1:1; для остальных (ASEAN, ООН и т. п.) создавайте в проекте собственные изображения. + +**Это реальная страна, но флага нет.** Случай маловероятный — стандарт ISO 3166-1 покрыт полностью, — но сначала обновите версию пакета. Если флаг устарел или отсутствует, заведите задачу или PR в репозитории пакета, а в своем проекте временно используйте собственное изображение. + +**Нужно отобразить флаг в большом размере.** В пакете флаги перерисованы и упрощены для мелкого размера. Для показа в крупном размере используйте сторонний пакет с оригинальной детализацией — большинство таких источников её не упрощают. + +## Доступность + +Если флаг несет смысл (например, это единственный индикатор выбранной страны) — передавайте текстовую подпись: + +```html + +``` + +Если рядом уже есть текст (инлайн, опция, блок) — флаг декоративный, помечайте его decorative, чтобы он скрывался от экранных дикторов и не дублировал подпись. + +```html + +Германия +``` + +Итого: у флага должна быть текстовая альтернатива — либо видимый текст рядом (тогда флаг decorative), либо подпись label для экранного диктора (когда видимого текста нет). Недопустим только третий случай — когда нет ни того, ни другого. + +## Примеры + +### С тенью и объёмом + +Компонент поддерживает стилизацию через CSS. Например, можно добавить скругление, тень и градиент, имитирующий складки. + + + +### Целочисленные размеры + +Выбирайте размер флага так, чтобы его габариты были кратны пикселю — иначе границы изображения будут размытыми. + + diff --git a/packages/components/flag/flag.scss b/packages/components/flag/flag.scss new file mode 100644 index 000000000..f65dfbdf0 --- /dev/null +++ b/packages/components/flag/flag.scss @@ -0,0 +1,53 @@ +.kbq-flag { + // Theme tokens. The faded contrast line resolves to a dark hairline in the light theme and a + // light one in the dark theme, so the flag stays separated from the background in both. + --kbq-flag-shadow-color: var(--kbq-line-contrast-fade); + --kbq-flag-empty-background: var(--kbq-states-background-disabled); + --kbq-flag-border-radius: 0; + --kbq-flag-aspect-ratio: 3 / 2; + + display: inline-block; + position: relative; + box-sizing: border-box; + // Sizing follows the surrounding text; 3:2 gives width = 1.5 × line height. Override via + // font-size / width / height to resize. + height: 1em; + aspect-ratio: var(--kbq-flag-aspect-ratio); + vertical-align: middle; + overflow: hidden; + border-radius: var(--kbq-flag-border-radius); + + // Size the flag image whether it is projected directly or bound via [innerHTML] + // (the injected svg may sit inside a wrapper element). + svg, + img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + } + + // Inset hairline drawn on top of the flag, gated by the shadow input. + &.kbq-flag_shadow-inset::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + box-shadow: inset 0 0 0 1px var(--kbq-flag-shadow-color); + pointer-events: none; + } + + &.kbq-flag_square, + &.kbq-flag_circle { + --kbq-flag-aspect-ratio: 1 / 1; + } + + &.kbq-flag_circle { + border-radius: 50%; + } + + // Neutral placeholder when no flag is projected (unknown / invalid country code). + &.kbq-flag_empty { + background: var(--kbq-flag-empty-background); + } +} diff --git a/packages/components/flag/flag.spec.ts b/packages/components/flag/flag.spec.ts new file mode 100644 index 000000000..fa624c2ba --- /dev/null +++ b/packages/components/flag/flag.spec.ts @@ -0,0 +1,157 @@ +import { Component, inject, Type } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By, DomSanitizer } from '@angular/platform-browser'; +import { KbqFlag } from './flag'; + +const createComponent = (component: Type): ComponentFixture => { + TestBed.configureTestingModule({ imports: [component] }).compileComponents(); + const fixture = TestBed.createComponent(component); + + fixture.autoDetectChanges(); + + return fixture; +}; + +const getFlag = (fixture: ComponentFixture): HTMLElement => + fixture.debugElement.query(By.directive(KbqFlag)).nativeElement; + +describe(KbqFlag.name, () => { + it('should project the provided flag content', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.classList).toContain('kbq-flag'); + expect(flag.querySelector('img')).toBeTruthy(); + }); + + it('should render an inline svg projected via innerHTML as a direct child', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent { + private readonly sanitizer = inject(DomSanitizer); + readonly svg = this.sanitizer.bypassSecurityTrustHtml(''); + } + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.querySelector(':scope > svg')).toBeTruthy(); + }); + + it('should apply the inset shadow by default', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.classList).toContain('kbq-flag_shadow-inset'); + }); + + it('should not apply the inset shadow when shadow is "none"', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.classList).not.toContain('kbq-flag_shadow-inset'); + }); + + it('should apply the shape modifier class', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.classList).toContain('kbq-flag_circle'); + expect(flag.classList).not.toContain('kbq-flag_square'); + }); + + it('should apply the empty placeholder class', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.classList).toContain('kbq-flag_empty'); + }); + + describe('accessibility', () => { + it('should expose role="img" and aria-label when labelled', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.getAttribute('role')).toBe('img'); + expect(flag.getAttribute('aria-label')).toBe('Germany'); + expect(flag.hasAttribute('aria-hidden')).toBe(false); + }); + + it('should hide the flag from assistive tech when decorative', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.getAttribute('aria-hidden')).toBe('true'); + expect(flag.hasAttribute('role')).toBe(false); + expect(flag.hasAttribute('aria-label')).toBe(false); + }); + + it('should not set role/aria when neither labelled nor decorative', () => { + @Component({ + imports: [KbqFlag], + template: ` + + ` + }) + class TestComponent {} + + const flag = getFlag(createComponent(TestComponent)); + + expect(flag.hasAttribute('role')).toBe(false); + expect(flag.hasAttribute('aria-label')).toBe(false); + expect(flag.hasAttribute('aria-hidden')).toBe(false); + }); + }); +}); diff --git a/packages/components/flag/flag.ts b/packages/components/flag/flag.ts new file mode 100644 index 000000000..fb8dc3462 --- /dev/null +++ b/packages/components/flag/flag.ts @@ -0,0 +1,47 @@ +import { booleanAttribute, ChangeDetectionStrategy, Component, input, ViewEncapsulation } from '@angular/core'; +import { KbqFlagShadow, KbqFlagShape } from './flag.types'; + +/** + * Presentational wrapper for a consumer-provided country flag (inline `` or ``). + * Decorates it with shape and shadow and applies the accessibility contract. + * + * ```html + * Albania + * ``` + */ +@Component({ + selector: 'kbq-flag', + template: '', + styleUrl: 'flag.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + class: 'kbq-flag', + '[class.kbq-flag_square]': "shape() === 'square'", + '[class.kbq-flag_circle]': "shape() === 'circle'", + '[class.kbq-flag_shadow-inset]': "shadow() === 'inset'", + '[class.kbq-flag_empty]': 'empty()', + '[attr.role]': "decorative() ? null : (label() ? 'img' : null)", + '[attr.aria-label]': 'decorative() ? null : (label() || null)', + '[attr.aria-hidden]': "decorative() ? 'true' : null" + }, + exportAs: 'kbqFlag' +}) +export class KbqFlag { + /** + * Shape of the flag. `square`/`circle` expect a 1:1 (square) flag source to be projected. + * @default rectangle + */ + readonly shape = input('rectangle'); + /** + * Inset hairline that keeps the flag distinct from the background. Its color adapts to the theme. + * @default inset + */ + readonly shadow = input('inset'); + /** Renders a neutral placeholder — use when no flag can be shown (unknown / invalid country code). */ + readonly empty = input(false, { transform: booleanAttribute }); + /** Marks the flag as decorative — hidden from screen readers (use when adjacent text is present). */ + readonly decorative = input(false, { transform: booleanAttribute }); + /** Accessible name for the flag. Use when the flag carries meaning and has no adjacent text. */ + readonly label = input(); +} diff --git a/packages/components/flag/flag.types.ts b/packages/components/flag/flag.types.ts new file mode 100644 index 000000000..e9c6d2636 --- /dev/null +++ b/packages/components/flag/flag.types.ts @@ -0,0 +1,5 @@ +/** Shape of the flag. `rectangle` keeps the default 3:2 ratio, `square`/`circle` expect a 1:1 source. */ +export type KbqFlagShape = 'rectangle' | 'square' | 'circle'; + +/** Inset shadow (hairline) that separates the flag from the background. */ +export type KbqFlagShadow = 'inset' | 'none'; diff --git a/packages/components/flag/index.ts b/packages/components/flag/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/packages/components/flag/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/packages/components/flag/module.ts b/packages/components/flag/module.ts new file mode 100644 index 000000000..772d7fe8a --- /dev/null +++ b/packages/components/flag/module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core'; +import { KbqFlag } from './flag'; + +@NgModule({ + imports: [KbqFlag], + exports: [KbqFlag] +}) +export class KbqFlagModule {} diff --git a/packages/components/flag/ng-package.json b/packages/components/flag/ng-package.json new file mode 100644 index 000000000..bebf62dcb --- /dev/null +++ b/packages/components/flag/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/packages/components/flag/public-api.ts b/packages/components/flag/public-api.ts new file mode 100644 index 000000000..c204e0c64 --- /dev/null +++ b/packages/components/flag/public-api.ts @@ -0,0 +1,3 @@ +export * from './flag'; +export * from './flag.types'; +export * from './module'; diff --git a/packages/docs-examples/components/flag/flag-aspect-ratio/flag-aspect-ratio-example.ts b/packages/docs-examples/components/flag/flag-aspect-ratio/flag-aspect-ratio-example.ts new file mode 100644 index 000000000..cbf96dab4 --- /dev/null +++ b/packages/docs-examples/components/flag/flag-aspect-ratio/flag-aspect-ratio-example.ts @@ -0,0 +1,90 @@ +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { KbqFlag } from '@koobiq/components/flag'; +import { KbqFormFieldModule } from '@koobiq/components/form-field'; +import { KbqSelectModule } from '@koobiq/components/select'; +import { countries } from 'country-flag-icons'; +import * as flags3x2 from 'country-flag-icons/string/3x2'; + +const regionNames = new Intl.DisplayNames(['en'], { type: 'region' }); + +const getCountryName = (code: string): string => { + try { + return regionNames.of(code) ?? code; + } catch { + // Subdivision / special codes (e.g. GB-ENG, EU) aren't valid ISO regions for Intl.DisplayNames. + return code; + } +}; + +type Country = { code: string; name: string; svg: SafeHtml }; + +/** + * @title Flags in a select + */ +@Component({ + selector: 'flag-aspect-ratio-example', + imports: [ReactiveFormsModule, ScrollingModule, KbqFormFieldModule, KbqSelectModule, KbqFlag], + template: ` + + + + @for (country of countries; track country) { + + + {{ country.name }} + + } + + + `, + styles: ` + .kbq-form-field { + width: 320px; + --kbq-form-field-size-addon-width: 18px; + } + + /* Match the prefix flag to the flag shown inside the options (21×14). */ + .example-flag-prefix, + .example-flag-option { + height: 12px; + } + + .example-flag-option { + margin-right: var(--kbq-size-xs); + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-row layout-align-center-center layout-padding-l' + } +}) +export class FlagAspectRatioExample { + private readonly sanitizer = inject(DomSanitizer); + + // Every flag exported from country-flag-icons, resolved to a country name for display. + // Subdivision codes are exported with underscores (GB-ENG → GB_ENG). + protected readonly countries: Country[] = countries + .map((code) => ({ + code, + name: getCountryName(code), + svg: this.sanitizer.bypassSecurityTrustHtml(flags3x2[code.replace(/-/g, '_')]) + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + protected readonly control = new FormControl(this.countries[0] ?? null); + + protected readonly compareWith = (a: Country | null, b: Country | null) => a?.code === b?.code; +} diff --git a/packages/docs-examples/components/flag/flag-circle/flag-circle-example.ts b/packages/docs-examples/components/flag/flag-circle/flag-circle-example.ts new file mode 100644 index 000000000..0bad9ef0f --- /dev/null +++ b/packages/docs-examples/components/flag/flag-circle/flag-circle-example.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { KbqFlag } from '@koobiq/components/flag'; +import * as flags1x1 from 'country-flag-icons/string/1x1'; + +/** + * @title Circular flags + */ +@Component({ + selector: 'flag-circle-example', + imports: [KbqFlag], + template: ` + @for (country of countries; track country.code) { + + } + `, + styles: ` + :host { + font-size: 32px; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-row layout-align-center-center layout-gap-m' + } +}) +export class FlagCircleExample { + private readonly sanitizer = inject(DomSanitizer); + protected readonly countries = [ + { code: 'AO', name: 'Angola' }, + { code: 'MY', name: 'Malaysia' }, + { code: 'UY', name: 'Uruguay' } + ].map((country) => ({ ...country, svg: this.sanitizer.bypassSecurityTrustHtml(flags1x1[country.code]) })); +} diff --git a/packages/docs-examples/components/flag/flag-fallback/flag-fallback-example.ts b/packages/docs-examples/components/flag/flag-fallback/flag-fallback-example.ts new file mode 100644 index 000000000..6bbb9533e --- /dev/null +++ b/packages/docs-examples/components/flag/flag-fallback/flag-fallback-example.ts @@ -0,0 +1,58 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { KbqFlag, KbqFlagShadow } from '@koobiq/components/flag'; +import { hasFlag } from 'country-flag-icons'; +import * as flags3x2 from 'country-flag-icons/string/3x2'; + +type FallbackCountry = { code: string; name: string; custom?: string; shadow?: KbqFlagShadow }; + +/** + * @title Fallback for a missing flag + */ +@Component({ + selector: 'flag-fallback-example', + imports: [KbqFlag], + template: ` + @for (country of countries; track country.name) { + + + @if (country.svg) { + + } @else if (country.custom) { + + {{ country.custom }} + + } + + {{ country.name }} + + } + `, + styles: ` + .kbq-flag { + height: 12px; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-row layout-align-center-center layout-gap-l' + } +}) +export class FlagFallbackExample { + private readonly sanitizer = inject(DomSanitizer); + + // "GND", "KRK" and "WA" are not real ISO codes. `hasFlag` guards the lookup — Gondor projects a + // custom glyph, the rest render a neutral placeholder. + protected readonly countries = ( + [ + { code: 'AL', name: 'Albania' }, + { code: 'GND', name: 'Gondor', custom: '🏇', shadow: 'none' }, + { code: 'KRK', name: 'Krakozhia' }, + { code: 'PE', name: 'Peru' }, + { code: 'WA', name: 'Wakanda' } + ] satisfies FallbackCountry[] + ).map((country) => ({ + ...country, + svg: hasFlag(country.code) ? this.sanitizer.bypassSecurityTrustHtml(flags3x2[country.code]) : null + })); +} diff --git a/packages/docs-examples/components/flag/flag-language/flag-language-example.ts b/packages/docs-examples/components/flag/flag-language/flag-language-example.ts new file mode 100644 index 000000000..ee559f6dc --- /dev/null +++ b/packages/docs-examples/components/flag/flag-language/flag-language-example.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { KbqFormFieldModule } from '@koobiq/components/form-field'; +import { KbqIconModule } from '@koobiq/components/icon'; +import { KbqSelectModule } from '@koobiq/components/select'; + +/** + * @title Flag is not a language + */ +@Component({ + selector: 'flag-language-example', + imports: [ReactiveFormsModule, KbqFormFieldModule, KbqSelectModule, KbqIconModule], + template: ` + + + + @for (language of languages; track language) { + {{ language }} + } + + + `, + styles: ` + .kbq-form-field { + width: 320px; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-row layout-align-center-center layout-padding-l' + } +}) +export class FlagLanguageExample { + // Languages are picked with a neutral icon, not a flag: one language spans several countries. + protected readonly languages = ['English (UK)', 'English (US)', 'Español', 'Français', 'Русский', 'हिन्दी', '中文']; + + protected readonly control = new FormControl(this.languages[0]); +} diff --git a/packages/docs-examples/components/flag/flag-overview/flag-overview-example.ts b/packages/docs-examples/components/flag/flag-overview/flag-overview-example.ts new file mode 100644 index 000000000..789b1d6d0 --- /dev/null +++ b/packages/docs-examples/components/flag/flag-overview/flag-overview-example.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { KbqFlag } from '@koobiq/components/flag'; +import * as flags3x2 from 'country-flag-icons/string/3x2'; + +/** + * @title Flag overview + */ +@Component({ + selector: 'flag-overview-example', + imports: [KbqFlag], + template: ` +
+ @for (country of countries; track country.name) { + + } +
+ `, + styles: ` + .example-flag-grid { + font-size: 32px; + max-width: 320px; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-column layout-align-center-center layout-gap-m' + } +}) +export class FlagOverviewExample { + private readonly sanitizer = inject(DomSanitizer); + + // A mix of flags, including several that are prone to blending into light or dark backgrounds. + // Subdivision codes are exported with underscores (GB-ENG → GB_ENG). + protected readonly countries = [ + { code: 'BR', name: 'Brazil' }, + { code: 'BG', name: 'Bulgaria' }, + { code: 'ID', name: 'Indonesia' }, + { code: 'MF', name: 'Saint Martin' }, + { code: 'UY', name: 'Uruguay' }, + { code: 'YT', name: 'Mayotte' }, + { code: 'PK', name: 'Pakistan' }, + { code: 'IL', name: 'Israel' }, + { code: 'GB_ENG', name: 'England' }, + { code: 'FI', name: 'Finland' }, + { code: 'BE', name: 'Belgium' }, + { code: 'CW', name: 'Curaçao' }, + { code: 'KR', name: 'South Korea' } + ].map((country) => ({ ...country, svg: this.sanitizer.bypassSecurityTrustHtml(flags3x2[country.code]) })); +} diff --git a/packages/docs-examples/components/flag/flag-sizes/flag-sizes-example.ts b/packages/docs-examples/components/flag/flag-sizes/flag-sizes-example.ts new file mode 100644 index 000000000..c9ca4ceab --- /dev/null +++ b/packages/docs-examples/components/flag/flag-sizes/flag-sizes-example.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { KbqFlag } from '@koobiq/components/flag'; +import { AD } from 'country-flag-icons/string/3x2'; + +type FlagSizeItem = { + width: number; + height: number; + /** Typography level (see `_typography.scss`) the size is meant to sit next to. */ + typography: string; + label: string; +}; + +/** + * @title Integer flag sizes + */ +@Component({ + selector: 'flag-sizes-example', + imports: [KbqFlag], + template: ` + @for (item of sizes; track $index) { +
+ + {{ item.label }} {{ item.width }}×{{ item.height }} +
+ } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-column layout-align-center-center layout-gap-m' + } +}) +export class FlagSizesExample { + protected readonly flag = inject(DomSanitizer).bypassSecurityTrustHtml(AD); + + // Dimensions are multiples of a pixel so the flag edges stay crisp, and match the typography + // level each size is meant to sit next to. + protected readonly sizes: FlagSizeItem[] = [ + { width: 24, height: 16, typography: 'kbq-subheading', label: 'Subheading' }, + { width: 21, height: 14, typography: 'kbq-subheading', label: 'Subheading' }, + { width: 18, height: 12, typography: 'kbq-text-normal', label: 'Text Normal' }, + { width: 15, height: 10, typography: 'kbq-text-normal', label: 'Text Normal' }, + { width: 15, height: 10, typography: 'kbq-text-compact', label: 'Text Compact' }, + { width: 12, height: 8, typography: 'kbq-text-compact', label: 'Text Compact' } + ]; +} diff --git a/packages/docs-examples/components/flag/flag-square/flag-square-example.ts b/packages/docs-examples/components/flag/flag-square/flag-square-example.ts new file mode 100644 index 000000000..8c656e86c --- /dev/null +++ b/packages/docs-examples/components/flag/flag-square/flag-square-example.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { KbqFlag } from '@koobiq/components/flag'; +import * as flags1x1 from 'country-flag-icons/string/1x1'; + +/** + * @title Square flags + */ +@Component({ + selector: 'flag-square-example', + imports: [KbqFlag], + template: ` + @for (country of countries; track country.code) { + + } + `, + styles: ` + :host { + font-size: 64px; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-row layout-align-center-center layout-gap-l' + } +}) +export class FlagSquareExample { + private readonly sanitizer = inject(DomSanitizer); + protected readonly countries = [ + { code: 'EU', name: 'European Union' }, + { code: 'NO', name: 'Norway' }, + { code: 'KR', name: 'South Korea' }, + { code: 'RU', name: 'Russia' } + ].map((country) => ({ ...country, svg: this.sanitizer.bypassSecurityTrustHtml(flags1x1[country.code]) })); +} diff --git a/packages/docs-examples/components/flag/flag-stylized/flag-stylized-example.ts b/packages/docs-examples/components/flag/flag-stylized/flag-stylized-example.ts new file mode 100644 index 000000000..9eb30eb0a --- /dev/null +++ b/packages/docs-examples/components/flag/flag-stylized/flag-stylized-example.ts @@ -0,0 +1,54 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { KbqFlag } from '@koobiq/components/flag'; +import { BR } from 'country-flag-icons/string/3x2'; + +/** + * @title Stylized flag + */ +@Component({ + selector: 'flag-stylized-example', + imports: [KbqFlag], + template: ` + + `, + styles: ` + .example-stylized-flag { + /* Rounded corners via the component's overridable radius token. */ + --kbq-flag-border-radius: var(--kbq-size-xs); + + font-size: 64px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } + + /* Gradient imitating folds, blended over the flag. */ + .example-stylized-flag::before { + content: ''; + position: absolute; + inset: 0; + z-index: 1; + border-radius: inherit; + background: linear-gradient( + 240.64deg, + rgba(255, 255, 255, 0.3) 0%, + rgba(0, 0, 0, 0.27) 26.27%, + rgba(255, 255, 255, 0.26) 37%, + rgba(0, 0, 0, 0.55) 48.7%, + rgba(0, 0, 0, 0.24) 59.44%, + rgba(255, 255, 255, 0.3) 73.64%, + rgba(39, 39, 39, 0.22) 90.15%, + rgba(0, 0, 0, 0.2) 100% + ); + mix-blend-mode: overlay; + pointer-events: none; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'layout-row layout-align-center-center layout-padding-xxl' + } +}) +export class FlagStylizedExample { + private readonly sanitizer = inject(DomSanitizer); + protected readonly BR = this.sanitizer.bypassSecurityTrustHtml(BR); +} diff --git a/packages/docs-examples/components/flag/index.ts b/packages/docs-examples/components/flag/index.ts new file mode 100644 index 000000000..2813cad71 --- /dev/null +++ b/packages/docs-examples/components/flag/index.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { FlagAspectRatioExample } from './flag-aspect-ratio/flag-aspect-ratio-example'; +import { FlagCircleExample } from './flag-circle/flag-circle-example'; +import { FlagFallbackExample } from './flag-fallback/flag-fallback-example'; +import { FlagLanguageExample } from './flag-language/flag-language-example'; +import { FlagOverviewExample } from './flag-overview/flag-overview-example'; +import { FlagSizesExample } from './flag-sizes/flag-sizes-example'; +import { FlagSquareExample } from './flag-square/flag-square-example'; +import { FlagStylizedExample } from './flag-stylized/flag-stylized-example'; + +export { + FlagAspectRatioExample, + FlagCircleExample, + FlagFallbackExample, + FlagLanguageExample, + FlagOverviewExample, + FlagSizesExample, + FlagSquareExample, + FlagStylizedExample +}; + +const EXAMPLES = [ + FlagOverviewExample, + FlagAspectRatioExample, + FlagSquareExample, + FlagLanguageExample, + FlagFallbackExample, + FlagStylizedExample, + FlagCircleExample, + FlagSizesExample +]; + +@NgModule({ + imports: EXAMPLES, + exports: EXAMPLES +}) +export class FlagExamplesModule {} diff --git a/packages/docs-examples/components/flag/ng-package.json b/packages/docs-examples/components/flag/ng-package.json new file mode 100644 index 000000000..bebf62dcb --- /dev/null +++ b/packages/docs-examples/components/flag/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/packages/docs-examples/example-module.ts b/packages/docs-examples/example-module.ts index 8cabc3bc3..aa3b7bd61 100644 --- a/packages/docs-examples/example-module.ts +++ b/packages/docs-examples/example-module.ts @@ -2205,6 +2205,102 @@ export const EXAMPLE_COMPONENTS: {[id: string]: LiveExample} = { "primaryFile": "filter-bar-uniq-pipes-example.ts", "importPath": "components/filter-bar" }, + "flag-aspect-ratio": { + "packagePath": "components/flag/flag-aspect-ratio", + "title": "Flags in a select", + "componentName": "FlagAspectRatioExample", + "files": [ + "flag-aspect-ratio-example.ts" + ], + "selector": "flag-aspect-ratio-example", + "additionalComponents": [], + "primaryFile": "flag-aspect-ratio-example.ts", + "importPath": "components/flag" + }, + "flag-circle": { + "packagePath": "components/flag/flag-circle", + "title": "Circular flags", + "componentName": "FlagCircleExample", + "files": [ + "flag-circle-example.ts" + ], + "selector": "flag-circle-example", + "additionalComponents": [], + "primaryFile": "flag-circle-example.ts", + "importPath": "components/flag" + }, + "flag-fallback": { + "packagePath": "components/flag/flag-fallback", + "title": "Fallback for a missing flag", + "componentName": "FlagFallbackExample", + "files": [ + "flag-fallback-example.ts" + ], + "selector": "flag-fallback-example", + "additionalComponents": [], + "primaryFile": "flag-fallback-example.ts", + "importPath": "components/flag" + }, + "flag-language": { + "packagePath": "components/flag/flag-language", + "title": "Flag is not a language", + "componentName": "FlagLanguageExample", + "files": [ + "flag-language-example.ts" + ], + "selector": "flag-language-example", + "additionalComponents": [], + "primaryFile": "flag-language-example.ts", + "importPath": "components/flag" + }, + "flag-overview": { + "packagePath": "components/flag/flag-overview", + "title": "Flag overview", + "componentName": "FlagOverviewExample", + "files": [ + "flag-overview-example.ts" + ], + "selector": "flag-overview-example", + "additionalComponents": [], + "primaryFile": "flag-overview-example.ts", + "importPath": "components/flag" + }, + "flag-sizes": { + "packagePath": "components/flag/flag-sizes", + "title": "Integer flag sizes", + "componentName": "FlagSizesExample", + "files": [ + "flag-sizes-example.ts" + ], + "selector": "flag-sizes-example", + "additionalComponents": [], + "primaryFile": "flag-sizes-example.ts", + "importPath": "components/flag" + }, + "flag-square": { + "packagePath": "components/flag/flag-square", + "title": "Square flags", + "componentName": "FlagSquareExample", + "files": [ + "flag-square-example.ts" + ], + "selector": "flag-square-example", + "additionalComponents": [], + "primaryFile": "flag-square-example.ts", + "importPath": "components/flag" + }, + "flag-stylized": { + "packagePath": "components/flag/flag-stylized", + "title": "Stylized flag", + "componentName": "FlagStylizedExample", + "files": [ + "flag-stylized-example.ts" + ], + "selector": "flag-stylized-example", + "additionalComponents": [], + "primaryFile": "flag-stylized-example.ts", + "importPath": "components/flag" + }, "form-field-horizontal": { "packagePath": "components/form-field/form-field-horizontal", "title": "Form field horizontal", @@ -6876,6 +6972,22 @@ return import('@koobiq/docs-examples/components/filter-bar'); return import('@koobiq/docs-examples/components/filter-bar'); case 'filter-bar-uniq-pipes': return import('@koobiq/docs-examples/components/filter-bar'); + case 'flag-aspect-ratio': +return import('@koobiq/docs-examples/components/flag'); + case 'flag-circle': +return import('@koobiq/docs-examples/components/flag'); + case 'flag-fallback': +return import('@koobiq/docs-examples/components/flag'); + case 'flag-language': +return import('@koobiq/docs-examples/components/flag'); + case 'flag-overview': +return import('@koobiq/docs-examples/components/flag'); + case 'flag-sizes': +return import('@koobiq/docs-examples/components/flag'); + case 'flag-square': +return import('@koobiq/docs-examples/components/flag'); + case 'flag-stylized': +return import('@koobiq/docs-examples/components/flag'); case 'form-field-horizontal': return import('@koobiq/docs-examples/components/form-field'); case 'form-field-overview': diff --git a/packages/e2e/routes.ts b/packages/e2e/routes.ts index 82e5083d4..5e7d0cff3 100644 --- a/packages/e2e/routes.ts +++ b/packages/e2e/routes.ts @@ -31,6 +31,7 @@ import { import { E2eEmptyStateStateAndStyle } from '../components/empty-state/e2e'; import { E2eFileUploadDropzone, E2eFileUploadStateAndStyle } from '../components/file-upload/e2e'; import { E2eFilterBarStates } from '../components/filter-bar/e2e'; +import { E2eFlagStyles } from '../components/flag/e2e'; import { E2eFormFieldGroup, E2eFormFieldset } from '../components/form-field/e2e'; import { E2eIconStateAndStyle, E2eIconSvg } from '../components/icon/e2e'; import { @@ -156,6 +157,7 @@ const components = [ E2eAppSwitcherWithSitesStates, E2eTagAutocompleteStates, E2eFilterBarStates, + E2eFlagStyles, E2eTimepickerStates, E2eIconStateAndStyle, E2eIconSvg, diff --git a/tools/api-extractor/config.json b/tools/api-extractor/config.json index 2b7018eb4..49795e533 100644 --- a/tools/api-extractor/config.json +++ b/tools/api-extractor/config.json @@ -22,6 +22,7 @@ "empty-state", "file-upload", "filter-bar", + "flag", "form-field", "icon", "inline-edit", diff --git a/tools/cspell-locales/ru.json b/tools/cspell-locales/ru.json index a8afe1394..3c4c4d55e 100644 --- a/tools/cspell-locales/ru.json +++ b/tools/cspell-locales/ru.json @@ -37,6 +37,7 @@ "версионирования", "виджете", "виджетом", + "викисклада", "гггг", "гига", "грид", diff --git a/tools/public_api_guard/components/flag.api.md b/tools/public_api_guard/components/flag.api.md new file mode 100644 index 000000000..1b55a5895 --- /dev/null +++ b/tools/public_api_guard/components/flag.api.md @@ -0,0 +1,40 @@ +## API Report File for "koobiq" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as _angular_core from '@angular/core'; + +// @public +export class KbqFlag { + readonly decorative: _angular_core.InputSignalWithTransform; + readonly empty: _angular_core.InputSignalWithTransform; + readonly label: _angular_core.InputSignal; + readonly shadow: _angular_core.InputSignal; + readonly shape: _angular_core.InputSignal; + // (undocumented) + static ɵcmp: _angular_core.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: _angular_core.ɵɵFactoryDeclaration; +} + +// @public (undocumented) +export class KbqFlagModule { + // (undocumented) + static ɵfac: _angular_core.ɵɵFactoryDeclaration; + // (undocumented) + static ɵinj: _angular_core.ɵɵInjectorDeclaration; + // (undocumented) + static ɵmod: _angular_core.ɵɵNgModuleDeclaration; +} + +// @public +export type KbqFlagShadow = 'inset' | 'none'; + +// @public +export type KbqFlagShape = 'rectangle' | 'square' | 'circle'; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/tsconfig.json b/tsconfig.json index d1a044533..a34e3c372 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,6 +48,7 @@ "@koobiq/components/form-field": ["packages/components/form-field/index.ts"], "@koobiq/components/file-upload": ["packages/components/file-upload/index.ts"], "@koobiq/components/filter-bar": ["packages/components/filter-bar/index.ts"], + "@koobiq/components/flag": ["packages/components/flag/index.ts"], "@koobiq/components/icon": ["packages/components/icon/index.ts"], "@koobiq/components/inline-edit": ["packages/components/inline-edit/index.ts"], "@koobiq/components/input": ["packages/components/input/index.ts"], diff --git a/yarn.lock b/yarn.lock index b24a2ce57..baadd091d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12454,6 +12454,13 @@ __metadata: languageName: node linkType: hard +"country-flag-icons@npm:^1.6.20": + version: 1.6.20 + resolution: "country-flag-icons@npm:1.6.20" + checksum: 10c0/e270cbf3f31d8f57a7f6da56c15733897f4dcc9c7fd50f88c7d295d16cb9fd97dcae96c054b2d840916bec3f27472335201938dc569961d935da4c4d341f9d32 + languageName: node + linkType: hard + "crc-32@npm:^1.2.0": version: 1.2.2 resolution: "crc-32@npm:1.2.2" @@ -18432,6 +18439,7 @@ __metadata: conventional-changelog: "npm:^3.1.10" conventional-changelog-angular: "npm:5.0.13" conventional-changelog-writer: "npm:5.0.1" + country-flag-icons: "npm:^1.6.20" cspell: "npm:^8.19.4" dotenv: "npm:^16.6.1" eslint: "npm:^9.0.0"