Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
Expand Down
13 changes: 13 additions & 0 deletions apps/docs/src/app/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -594,6 +595,18 @@ const structure: DocsStructure = makeStructure({
apiId: 'filter-bar',
hasExamples: true
},
{
id: DocsStructureItemId.Flag,
name: {
ru: 'Flag',
en: 'Flag'
},
svgPreview: '',
Comment thread
NikGurev marked this conversation as resolved.
hasApi: true,
apiId: 'flag',
hasExamples: false,
isNew: expiresAt('2026-10-07')
Comment thread
NikGurev marked this conversation as resolved.
},
{
id: DocsStructureItemId.FormField,
name: {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
NikGurev marked this conversation as resolved.
"cspell": "^8.19.4",
Comment thread
NikGurev marked this conversation as resolved.
"dotenv": "^16.6.1",
"eslint": "^9.0.0",
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions packages/components-dev/flag/main.ts
Original file line number Diff line number Diff line change
@@ -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));
32 changes: 32 additions & 0 deletions packages/components-dev/flag/module.ts
Original file line number Diff line number Diff line change
@@ -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: `
<flag-aspect-ratio-example />
<flag-square-example />
<flag-overview-example />
<flag-language-example />
<flag-fallback-example />
<flag-stylized-example />
<flag-circle-example />
<flag-sizes-example />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DevDocsExamples {}

@Component({
selector: 'dev-app',
imports: [DevDocsExamples, DevThemeToggle],
template: `
<dev-theme-toggle />
<dev-examples />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class DevApp {}
5 changes: 5 additions & 0 deletions packages/components-dev/flag/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.d.ts"],
"files": ["main.ts"]
}
Comment thread
NikGurev marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions packages/components/flag/e2e.playwright-spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
54 changes: 54 additions & 0 deletions packages/components/flag/e2e.ts
Original file line number Diff line number Diff line change
@@ -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 = `<svg viewBox="0 0 22 16" xmlns="http://www.w3.org/2000/svg">
<rect width="22" height="16" fill="#ef2b2d" />
<rect x="6" width="4" height="16" fill="#fff" />
<rect y="6" width="22" height="4" fill="#fff" />
<rect x="7" width="2" height="16" fill="#002868" />
<rect y="7" width="22" height="2" fill="#002868" />
</svg>`;

@Component({
selector: 'e2e-flag-styles',
imports: [KbqFlagModule],
template: `
<div>
@for (shape of shapes; track shape) {
<kbq-flag [shape]="shape" [innerHTML]="flag" />
}
</div>
<div>
<kbq-flag shadow="none" [innerHTML]="flag" />
<kbq-flag shadow="inset" [innerHTML]="flag" />
<kbq-flag empty />
</div>
`,
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'];
}
107 changes: 107 additions & 0 deletions packages/components/flag/flag.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<div class="kbq-callout kbq-callout_theme">
<div class="kbq-callout__header">Note</div>
<div class="kbq-callout__content kbq-docs-element-last-child-margin-bottom-0">

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
```

</div>
</div>

- **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.

<!-- example(flag-aspect-ratio) -->

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.

<!-- example(flag-square) -->

## 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.

<!-- example(flag-overview) -->

## 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.

<!-- example(flag-language) -->

## 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.

<!-- example(flag-fallback) -->

**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
<kbq-flag label="Germany"><img src="…/DE.svg" alt="" /></kbq-flag>
```

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
<kbq-flag decorative><img src="…/DE.svg" alt="" /></kbq-flag>
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.

<!-- example(flag-stylized) -->

### Circular flag

Use the 1:1 aspect ratio version.

<!-- example(flag-circle) -->

### Integer sizes

Choose a flag size whose dimensions are multiples of a pixel — otherwise the image edges will be blurry.

<!-- example(flag-sizes) -->
Loading