From 987160823a0b6c1dd5dff499155aa1955d5d54cd Mon Sep 17 00:00:00 2001 From: Nikita Guryev Date: Wed, 30 Jul 2025 18:09:39 +0300 Subject: [PATCH 01/15] feat(username): init component (#DS-3296) --- .github/CODEOWNERS | 1 + angular.json | 30 +++++++ .../src/app/services/documentation-items.ts | 10 +++ apps/docs/src/sitemap.xml | 12 +++ package.json | 1 + packages/components-dev/username/main.ts | 9 +++ packages/components-dev/username/module.ts | 23 ++++++ .../components-dev/username/template.html | 1 + .../components-dev/username/tsconfig.app.json | 5 ++ packages/components/username/constants.ts | 18 +++++ packages/components/username/index.ts | 5 ++ packages/components/username/module.ts | 11 +++ packages/components/username/ng-package.json | 5 ++ packages/components/username/types.ts | 44 ++++++++++ packages/components/username/username.en.md | 1 + packages/components/username/username.pipe.ts | 49 +++++++++++ packages/components/username/username.ru.md | 5 ++ packages/components/username/username.spec.ts | 14 ++++ packages/components/username/username.ts | 22 +++++ .../components/username/index.ts | 14 ++++ .../components/username/ng-package.json | 5 ++ .../username-overview-example.ts | 81 +++++++++++++++++++ packages/docs-examples/example-module.ts | 2 +- tools/api-extractor/config.json | 3 +- tools/generate-sitemap.ts | 3 + tsconfig.json | 1 + 26 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 packages/components-dev/username/main.ts create mode 100644 packages/components-dev/username/module.ts create mode 100644 packages/components-dev/username/template.html create mode 100644 packages/components-dev/username/tsconfig.app.json create mode 100644 packages/components/username/constants.ts create mode 100644 packages/components/username/index.ts create mode 100644 packages/components/username/module.ts create mode 100644 packages/components/username/ng-package.json create mode 100644 packages/components/username/types.ts create mode 100644 packages/components/username/username.en.md create mode 100644 packages/components/username/username.pipe.ts create mode 100644 packages/components/username/username.ru.md create mode 100644 packages/components/username/username.spec.ts create mode 100644 packages/components/username/username.ts create mode 100644 packages/docs-examples/components/username/index.ts create mode 100644 packages/docs-examples/components/username/ng-package.json create mode 100644 packages/docs-examples/components/username/username-overview/username-overview-example.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e426c11d7..74b9f737a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -34,6 +34,7 @@ /packages/components/timezone/* @NikGurev /packages/components/toggle/* @NikGurev /packages/components/top-bar/* @NikGurev +/packages/components/username/* @NikGurev /packages/components/actions-panel/* @artembelik /packages/components/autocomplete/* @artembelik diff --git a/angular.json b/angular.json index 37cf7687f..9a09a8a0e 100644 --- a/angular.json +++ b/angular.json @@ -2368,6 +2368,36 @@ } } }, + "dev-username": { + "projectType": "application", + "root": "packages/components-dev/username", + "sourceRoot": "packages/components-dev/username", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": { + "base": "dist/components-dev/username" + }, + "tsConfig": "packages/components-dev/username/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/username/main.ts" + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "buildTarget": "dev-username:build" + } + } + } + }, "dev-validation": { "projectType": "application", "root": "packages/components-dev/validation", diff --git a/apps/docs/src/app/services/documentation-items.ts b/apps/docs/src/app/services/documentation-items.ts index edaffe014..c504f71a1 100644 --- a/apps/docs/src/app/services/documentation-items.ts +++ b/apps/docs/src/app/services/documentation-items.ts @@ -793,6 +793,16 @@ const DOCS: { [key: string]: DocsDocCategory[] } = { hasApi: true, apiId: 'tree-select', hasExamples: true + }, + { + id: 'username', + name: { + ru: 'Username', + en: 'Username' + }, + hasApi: true, + apiId: 'username', + hasExamples: false } ] } diff --git a/apps/docs/src/sitemap.xml b/apps/docs/src/sitemap.xml index 581d49856..baba59d49 100644 --- a/apps/docs/src/sitemap.xml +++ b/apps/docs/src/sitemap.xml @@ -720,6 +720,18 @@ https://koobiq.io/ru/components/tree-select/api + + https://koobiq.io/en/components/username/overview + + + https://koobiq.io/ru/components/username/overview + + + https://koobiq.io/en/components/username/api + + + https://koobiq.io/ru/components/username/api + https://koobiq.io/en/other/date-formatter/overview diff --git a/package.json b/package.json index e9925d44d..5e46c5a99 100644 --- a/package.json +++ b/package.json @@ -264,6 +264,7 @@ "dev:tree": "ng serve dev-tree --port 3003", "dev:tree-select": "ng serve dev-tree-select --port 3003", "dev:typography": "ng serve dev-typography --port 3003", + "dev:username": "ng serve dev-username --port 3003", "dev:validation": "ng serve dev-validation --port 3003", "dev:z-index": "ng serve dev-z-index --port 3003", "-----UNIT_TESTS-----": "-------------------------------------------------------------------------------------", diff --git a/packages/components-dev/username/main.ts b/packages/components-dev/username/main.ts new file mode 100644 index 000000000..2bbc80baf --- /dev/null +++ b/packages/components-dev/username/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/username/module.ts b/packages/components-dev/username/module.ts new file mode 100644 index 000000000..711bae490 --- /dev/null +++ b/packages/components-dev/username/module.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; +import { UsernameExamplesModule } from '../../docs-examples/components/username'; + +@Component({ + selector: 'dev-examples', + standalone: true, + imports: [UsernameExamplesModule], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +class DevExamples {} + +@Component({ + selector: 'dev-app', + standalone: true, + imports: [DevExamples], + templateUrl: './template.html', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DevApp {} diff --git a/packages/components-dev/username/template.html b/packages/components-dev/username/template.html new file mode 100644 index 000000000..c6a5b7b43 --- /dev/null +++ b/packages/components-dev/username/template.html @@ -0,0 +1 @@ + diff --git a/packages/components-dev/username/tsconfig.app.json b/packages/components-dev/username/tsconfig.app.json new file mode 100644 index 000000000..81fb90e7c --- /dev/null +++ b/packages/components-dev/username/tsconfig.app.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*.d.ts"], + "files": ["main.ts"] +} diff --git a/packages/components/username/constants.ts b/packages/components/username/constants.ts new file mode 100644 index 000000000..51092d722 --- /dev/null +++ b/packages/components/username/constants.ts @@ -0,0 +1,18 @@ +import { InjectionToken } from '@angular/core'; +import { KbqFormatKeyToProfileMapping } from './types'; + +/** Default name format: Last name full, first and middle as initials. */ +export const kbqDefaultFullNameFormat = 'L f. m.'; + +/** + * Throws an error when no profile field mapping is provided to the username pipe. + * @docs-private + */ +export function throwKbqMappingMissingError() { + throw new Error('KbqUsernamePipe: profile field mapping is required but was not provided.'); +} + +/** + * Injection token for providing a global username format-to-profile mapping. + */ +export const KBQ_PROFILE_MAPPING = new InjectionToken('KBQ_PROFILE_MAPPING'); diff --git a/packages/components/username/index.ts b/packages/components/username/index.ts new file mode 100644 index 000000000..245d33d7e --- /dev/null +++ b/packages/components/username/index.ts @@ -0,0 +1,5 @@ +export * from './constants'; +export * from './module'; +export * from './types'; +export * from './username'; +export * from './username.pipe'; diff --git a/packages/components/username/module.ts b/packages/components/username/module.ts new file mode 100644 index 000000000..f427a40de --- /dev/null +++ b/packages/components/username/module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { KbqUsername } from './username'; +import { KbqUsernamePipe } from './username.pipe'; + +const COMPONENTS = [KbqUsername, KbqUsernamePipe]; + +@NgModule({ + imports: COMPONENTS, + exports: COMPONENTS +}) +export class KbqUsernameModule {} diff --git a/packages/components/username/ng-package.json b/packages/components/username/ng-package.json new file mode 100644 index 000000000..bebf62dcb --- /dev/null +++ b/packages/components/username/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/packages/components/username/types.ts b/packages/components/username/types.ts new file mode 100644 index 000000000..235cfbf71 --- /dev/null +++ b/packages/components/username/types.ts @@ -0,0 +1,44 @@ +/** + * Keys for formatting username parts in short (initial) or full form. + */ +export enum KbqUsernameFormatKey { + /** + * Short form of the first name (e.g., "John" → "J") + */ + FirstNameShort = 'f', + + /** + * Full form of the first name (e.g., "John") + */ + FirstNameFull = 'F', + + /** + * Short form of the middle name (e.g., "Henry" → "H") + */ + MiddleNameShort = 'm', + + /** + * Full form of the middle name (e.g., "Henry") + */ + MiddleNameFull = 'M', + + /** + * Short form of the last name (e.g., "Doe" → "D") + */ + LastNameShort = 'l', + + /** + * Full form of the last name (e.g., "Doe") + */ + LastNameFull = 'L' +} + +/** + * Maps each format key to a property name in the user profile object. + * Allows flexible formatting regardless of profile field names. + */ +export type KbqFormatKeyToProfileMapping = { + [key in KbqUsernameFormatKey]: keyof T; +}; + +export type KbqUsernameMode = 'stacked' | 'inline' | 'text'; diff --git a/packages/components/username/username.en.md b/packages/components/username/username.en.md new file mode 100644 index 000000000..acc947f57 --- /dev/null +++ b/packages/components/username/username.en.md @@ -0,0 +1 @@ + diff --git a/packages/components/username/username.pipe.ts b/packages/components/username/username.pipe.ts new file mode 100644 index 000000000..42579acdf --- /dev/null +++ b/packages/components/username/username.pipe.ts @@ -0,0 +1,49 @@ +import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'; +import { KBQ_PROFILE_MAPPING, kbqDefaultFullNameFormat, throwKbqMappingMissingError } from './constants'; +import { KbqFormatKeyToProfileMapping, KbqUsernameFormatKey } from './types'; + +/** + * Pipe to format a user profile into a name string using a format pattern and field mapping. + * Lowercase keys output initials; uppercase keys show full values. + */ +@Injectable({ providedIn: 'root' }) +@Pipe({ + name: 'kbqUsername', + pure: true, + standalone: true +}) +export class KbqUsernamePipe implements PipeTransform { + private readonly mapping = inject(KBQ_PROFILE_MAPPING, { optional: true }); + + /** Builds a formatted name string from the user profile using the provided format and mapping. */ + transform(profile: T, format = kbqDefaultFullNameFormat, argMapping?: KbqFormatKeyToProfileMapping): string { + const resolvedMapping = argMapping || this.mapping; + + if (!resolvedMapping) { + throwKbqMappingMissingError(); + } + + if (!profile || typeof profile !== 'object') return ''; + + let result = ''; + + const formatUnits = format.split('') as KbqUsernameFormatKey[]; + + formatUnits.forEach((letter: KbqUsernameFormatKey) => { + const field = resolvedMapping![letter]; + + if (!field) { + result += letter; + + return; + } + + const isShort = letter === letter.toLowerCase(); + const fieldValue: string = profile[field] || ''; + + result += fieldValue && isShort ? fieldValue[0] : fieldValue; + }); + + return result.trim(); + } +} diff --git a/packages/components/username/username.ru.md b/packages/components/username/username.ru.md new file mode 100644 index 000000000..7e2af4784 --- /dev/null +++ b/packages/components/username/username.ru.md @@ -0,0 +1,5 @@ +Паттерн применяется, когда интерфейс ссылается на внутреннего пользователя. + +Под отображением имени пользователя понимается набор атрибутов внутреннего пользователя системы, которые помогают идентифицировать пользователя в интерфейсе. + + diff --git a/packages/components/username/username.spec.ts b/packages/components/username/username.spec.ts new file mode 100644 index 000000000..4f7492b71 --- /dev/null +++ b/packages/components/username/username.spec.ts @@ -0,0 +1,14 @@ +import { KbqUsernamePipe } from './username.pipe'; + +describe(KbqUsernamePipe.name, () => { + let pipe: KbqUsernamePipe; + + beforeEach(() => { + pipe = new KbqUsernamePipe(); + }); + + it('create an instance', () => { + pipe = new KbqUsernamePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/packages/components/username/username.ts b/packages/components/username/username.ts new file mode 100644 index 000000000..a2729c537 --- /dev/null +++ b/packages/components/username/username.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { KbqUsernameMode } from './types'; + +@Component({ + selector: 'kbq-username', + standalone: true, + exportAs: 'kbqUsername', + imports: [], + template: ` + + + + `, + host: { + class: 'kbq-username' + }, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class KbqUsername { + profile = input.required(); + mode = input('inline'); +} diff --git a/packages/docs-examples/components/username/index.ts b/packages/docs-examples/components/username/index.ts new file mode 100644 index 000000000..77e8df3cd --- /dev/null +++ b/packages/docs-examples/components/username/index.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { UsernameOverviewExample } from './username-overview/username-overview-example'; + +export { UsernameOverviewExample }; + +const EXAMPLES = [ + UsernameOverviewExample +]; + +@NgModule({ + imports: EXAMPLES, + exports: EXAMPLES +}) +export class UsernameExamplesModule {} diff --git a/packages/docs-examples/components/username/ng-package.json b/packages/docs-examples/components/username/ng-package.json new file mode 100644 index 000000000..bebf62dcb --- /dev/null +++ b/packages/docs-examples/components/username/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/packages/docs-examples/components/username/username-overview/username-overview-example.ts b/packages/docs-examples/components/username/username-overview/username-overview-example.ts new file mode 100644 index 000000000..7c373bbec --- /dev/null +++ b/packages/docs-examples/components/username/username-overview/username-overview-example.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { KbqFormField } from '@koobiq/components/form-field'; +import { KbqLinkModule } from '@koobiq/components/link'; +import { KbqTextareaModule } from '@koobiq/components/textarea'; +import { + KBQ_PROFILE_MAPPING, + KbqFormatKeyToProfileMapping, + KbqUsernameFormatKey, + KbqUsernameModule +} from '@koobiq/components/username'; + +type ExampleUser = { + firstName?: string; + lastName?: string; + middleName?: string; +}; + +const mapping: KbqFormatKeyToProfileMapping = { + [KbqUsernameFormatKey.FirstNameShort]: 'firstName', + [KbqUsernameFormatKey.FirstNameFull]: 'firstName', + + [KbqUsernameFormatKey.MiddleNameShort]: 'middleName', + [KbqUsernameFormatKey.MiddleNameFull]: 'middleName', + + [KbqUsernameFormatKey.LastNameShort]: 'lastName', + [KbqUsernameFormatKey.LastNameFull]: 'lastName' +}; + +/** + * @title Username overview + */ +@Component({ + selector: 'username-overview-example', + standalone: true, + imports: [KbqUsernameModule, KbqTextareaModule, FormsModule, KbqFormField, KbqLinkModule], + template: ` +
+
{{ profile | kbqUsername: 'L f. m.' }}
+
{{ profile | kbqUsername: 'L f. m.' }}
+ + + +
+ + {{ profile | kbqUsername }} + +
+ +
+ + {{ profile | kbqUsername }} + +
+ +
+ {{ profile | kbqUsername: format }} +
+
+ + + + + `, + styles: ` + .example-error-text { + color: var(--kbq-foreground-error); + } + `, + providers: [ + { provide: KBQ_PROFILE_MAPPING, useValue: mapping }], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class UsernameOverviewExample { + profile: ExampleUser = { firstName: 'First name', lastName: 'LastName', middleName: 'MiddleName' }; + format = 'L f. m.'; +} diff --git a/packages/docs-examples/example-module.ts b/packages/docs-examples/example-module.ts index e2d4db2ec..60b2ae173 100644 --- a/packages/docs-examples/example-module.ts +++ b/packages/docs-examples/example-module.ts @@ -5411,4 +5411,4 @@ return import('@koobiq/docs-examples/components/validation'); default: return undefined; } -} \ No newline at end of file +} diff --git a/tools/api-extractor/config.json b/tools/api-extractor/config.json index 6700ceb7c..78ea802d3 100644 --- a/tools/api-extractor/config.json +++ b/tools/api-extractor/config.json @@ -52,7 +52,8 @@ "toggle", "tooltip", "tree", - "tree-select" + "tree-select", + "username" ], "components-experimental": [ "form-field" diff --git a/tools/generate-sitemap.ts b/tools/generate-sitemap.ts index 046fdb0eb..802196e51 100644 --- a/tools/generate-sitemap.ts +++ b/tools/generate-sitemap.ts @@ -186,6 +186,9 @@ const paths = [ 'components/tree-select/overview', 'components/tree-select/api', + 'components/username/overview', + 'components/username/api', + // ---------------------- Other ---------------------- 'other/date-formatter/overview', 'other/date-formatter/api', diff --git a/tsconfig.json b/tsconfig.json index 16111876b..5d93d9043 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -82,6 +82,7 @@ "@koobiq/components/top-bar": ["packages/components/top-bar/index.ts"], "@koobiq/components/tree": ["packages/components/tree/index.ts"], "@koobiq/components/tree-select": ["packages/components/tree-select/index.ts"], + "@koobiq/components/username": ["packages/components/username/index.ts"], "@koobiq/components/vertical-navbar": ["packages/components/vertical-navbar/index.ts"], "@koobiq/angular-luxon-adapter": ["packages/angular-luxon-adapter/index.ts"], "@koobiq/angular-moment-adapter": ["packages/angular-moment-adapter/index.ts"], From 013158e9302fd89cdd25dc7bb8fef1ebed4f05bc Mon Sep 17 00:00:00 2001 From: Nikita Guryev Date: Thu, 31 Jul 2025 18:10:48 +0300 Subject: [PATCH 02/15] chore: added default view , refactor & examples --- packages/components-dev/username/module.ts | 15 +++ .../__snapshots__/username.spec.ts.snap | 9 ++ packages/components/username/constants.ts | 25 +++- packages/components/username/module.ts | 4 +- packages/components/username/types.ts | 22 +++- packages/components/username/username.html | 31 +++++ packages/components/username/username.pipe.ts | 58 ++++++++- packages/components/username/username.ru.md | 2 + packages/components/username/username.scss | 98 ++++++++++++++ packages/components/username/username.spec.ts | 56 ++++++-- packages/components/username/username.ts | 68 +++++++++- .../components/username/index.ts | 6 +- .../username-overview-example.ts | 77 ++--------- .../username-playground-example.ts | 83 ++++++++++++ .../components/username.api.md | 120 ++++++++++++++++++ 15 files changed, 575 insertions(+), 99 deletions(-) create mode 100644 packages/components/username/__snapshots__/username.spec.ts.snap create mode 100644 packages/components/username/username.html create mode 100644 packages/components/username/username.scss create mode 100644 packages/docs-examples/components/username/username-playground/username-playground-example.ts create mode 100644 tools/public_api_guard/components/username.api.md diff --git a/packages/components-dev/username/module.ts b/packages/components-dev/username/module.ts index 711bae490..311687e5e 100644 --- a/packages/components-dev/username/module.ts +++ b/packages/components-dev/username/module.ts @@ -7,6 +7,21 @@ import { UsernameExamplesModule } from '../../docs-examples/components/username' imports: [UsernameExamplesModule], template: ` + + `, + styles: ` + :host { + display: flex; + gap: var(--kbq-size-l); + flex-wrap: wrap; + } + :host > * { + border-radius: var(--kbq-size-border-radius); + border: 1px solid var(--kbq-line-contrast-less); + margin-bottom: var(--kbq-size-l); + padding: var(--kbq-size-m); + flex: 1 0 auto; + } `, changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/packages/components/username/__snapshots__/username.spec.ts.snap b/packages/components/username/__snapshots__/username.spec.ts.snap new file mode 100644 index 000000000..da68f32b9 --- /dev/null +++ b/packages/components/username/__snapshots__/username.spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KbqUsername should use default input values 1`] = ` +{ + "kbq-username": true, + "kbq-username_default": true, + "kbq-username_inline": true, +} +`; diff --git a/packages/components/username/constants.ts b/packages/components/username/constants.ts index 51092d722..facea1444 100644 --- a/packages/components/username/constants.ts +++ b/packages/components/username/constants.ts @@ -1,18 +1,33 @@ import { InjectionToken } from '@angular/core'; -import { KbqFormatKeyToProfileMapping } from './types'; +import { KbqFormatKeyToProfileMapping, KbqUsernameFormatKey } from './types'; /** Default name format: Last name full, first and middle as initials. */ -export const kbqDefaultFullNameFormat = 'L f. m.'; +export const kbqDefaultFullNameFormatCustom = 'L f. m.'; +export const kbqDefaultFullNameFormat = 'lf.m.'; /** * Throws an error when no profile field mapping is provided to the username pipe. * @docs-private */ -export function throwKbqMappingMissingError() { - throw new Error('KbqUsernamePipe: profile field mapping is required but was not provided.'); +export function KbqMappingMissingError() { + return new Error('KbqUsernamePipe: profile field mapping is required but was not provided.'); } /** * Injection token for providing a global username format-to-profile mapping. */ -export const KBQ_PROFILE_MAPPING = new InjectionToken('KBQ_PROFILE_MAPPING'); +export const KBQ_PROFILE_MAPPING = new InjectionToken('KBQ_PROFILE_MAPPING', { + factory: () => + ({ + [KbqUsernameFormatKey.FirstNameShort]: 'firstName', + [KbqUsernameFormatKey.FirstNameFull]: undefined, + + [KbqUsernameFormatKey.MiddleNameShort]: 'middleName', + [KbqUsernameFormatKey.MiddleNameFull]: undefined, + + [KbqUsernameFormatKey.LastNameShort]: 'lastName', + [KbqUsernameFormatKey.LastNameFull]: undefined, + + [KbqUsernameFormatKey.Dot]: undefined + }) satisfies KbqFormatKeyToProfileMapping<{ firstName: string; middleName: string; lastName: string }> +}); diff --git a/packages/components/username/module.ts b/packages/components/username/module.ts index f427a40de..2910e4fc8 100644 --- a/packages/components/username/module.ts +++ b/packages/components/username/module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { KbqUsername } from './username'; -import { KbqUsernamePipe } from './username.pipe'; +import { KbqUsernameCustomPipe, KbqUsernamePipe } from './username.pipe'; -const COMPONENTS = [KbqUsername, KbqUsernamePipe]; +const COMPONENTS = [KbqUsername, KbqUsernameCustomPipe, KbqUsernamePipe]; @NgModule({ imports: COMPONENTS, diff --git a/packages/components/username/types.ts b/packages/components/username/types.ts index 235cfbf71..047d41d20 100644 --- a/packages/components/username/types.ts +++ b/packages/components/username/types.ts @@ -30,7 +30,8 @@ export enum KbqUsernameFormatKey { /** * Full form of the last name (e.g., "Doe") */ - LastNameFull = 'L' + LastNameFull = 'L', + Dot = '.' } /** @@ -38,7 +39,24 @@ export enum KbqUsernameFormatKey { * Allows flexible formatting regardless of profile field names. */ export type KbqFormatKeyToProfileMapping = { - [key in KbqUsernameFormatKey]: keyof T; + [key in KbqUsernameFormatKey]: keyof T | undefined; }; +/** + * Layout mode for displaying a username and applying text-ellipsis. + * + * - `stacked`: Elements shown vertically. + * - `inline`: Elements shown in one line. Text ellipsis is applied to both parts. + * - `text`: Plain text, no layout styling. No text-ellipsis. + */ export type KbqUsernameMode = 'stacked' | 'inline' | 'text'; + +/** + * Visual style of the username. + * + * - `default`: standard styling with primary and secondary colors. + * - `error`: error colors (e.g., red). + * - `simple`: minimal style with contrast color. + * - `none`: inherits parent styles, no theming. For exampe, useful when using inside links. + */ +export type KbqUsernameStyle = 'default' | 'error' | 'simple' | 'none'; diff --git a/packages/components/username/username.html b/packages/components/username/username.html new file mode 100644 index 000000000..2dbb873f9 --- /dev/null +++ b/packages/components/username/username.html @@ -0,0 +1,31 @@ +@let profile = userInfo(); +@let fullName = profile | kbqUsername: fullNameFormat(); + +@if (!isCompact()) { + @if (hasFullName()) { + {{ fullName }} + } + + @if (profile?.login) { + + } +} @else { + + @if (hasFullName()) { + {{ fullName }} + } + @if (!hasFullName() && profile?.login) { + {{ profile?.login }} + } + + @if (profile?.site) { +  ({{ profile?.site }}) + } + +} diff --git a/packages/components/username/username.pipe.ts b/packages/components/username/username.pipe.ts index 42579acdf..606aff9ed 100644 --- a/packages/components/username/username.pipe.ts +++ b/packages/components/username/username.pipe.ts @@ -1,26 +1,72 @@ import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'; -import { KBQ_PROFILE_MAPPING, kbqDefaultFullNameFormat, throwKbqMappingMissingError } from './constants'; +import { + KBQ_PROFILE_MAPPING, + kbqDefaultFullNameFormat, + kbqDefaultFullNameFormatCustom, + KbqMappingMissingError +} from './constants'; import { KbqFormatKeyToProfileMapping, KbqUsernameFormatKey } from './types'; +@Injectable({ providedIn: 'root' }) +@Pipe({ + name: 'kbqUsername', + pure: true, + standalone: true +}) +export class KbqUsernamePipe implements PipeTransform { + private readonly mapping = inject(KBQ_PROFILE_MAPPING, { optional: true }); + + /** Builds a formatted name string from the user profile using the provided format and mapping. */ + transform(profile: T, format = kbqDefaultFullNameFormat, argMapping?: KbqFormatKeyToProfileMapping): string { + const resolvedMapping = argMapping || this.mapping; + + if (!resolvedMapping) { + throw KbqMappingMissingError(); + } + + if (!profile || typeof profile !== 'object') return ''; + + let result = ''; + + const formatUnits = format.split('') as KbqUsernameFormatKey[]; + + formatUnits.forEach((letter: KbqUsernameFormatKey, index: number, array) => { + if (letter !== KbqUsernameFormatKey.Dot) { + const field = resolvedMapping[letter] as keyof T; + const fieldValue = profile[field]; + + if (fieldValue) { + const isShort = array[index + 1] === KbqUsernameFormatKey.Dot; + const resolvedFieldValue = isShort ? `${fieldValue[0]}${KbqUsernameFormatKey.Dot}` : fieldValue; + + result += ` ${resolvedFieldValue}`; + } + } + }); + + return result.trim(); + } +} + /** * Pipe to format a user profile into a name string using a format pattern and field mapping. * Lowercase keys output initials; uppercase keys show full values. */ @Injectable({ providedIn: 'root' }) @Pipe({ - name: 'kbqUsername', + name: 'kbqUsernameCustom', pure: true, standalone: true }) -export class KbqUsernamePipe implements PipeTransform { +export class KbqUsernameCustomPipe implements PipeTransform { private readonly mapping = inject(KBQ_PROFILE_MAPPING, { optional: true }); /** Builds a formatted name string from the user profile using the provided format and mapping. */ - transform(profile: T, format = kbqDefaultFullNameFormat, argMapping?: KbqFormatKeyToProfileMapping): string { + transform(profile: T, format = kbqDefaultFullNameFormatCustom, argMapping?: KbqFormatKeyToProfileMapping): string { const resolvedMapping = argMapping || this.mapping; if (!resolvedMapping) { - throwKbqMappingMissingError(); + throw KbqMappingMissingError(); } if (!profile || typeof profile !== 'object') return ''; @@ -30,7 +76,7 @@ export class KbqUsernamePipe implements PipeTransform { const formatUnits = format.split('') as KbqUsernameFormatKey[]; formatUnits.forEach((letter: KbqUsernameFormatKey) => { - const field = resolvedMapping![letter]; + const field = resolvedMapping[letter]; if (!field) { result += letter; diff --git a/packages/components/username/username.ru.md b/packages/components/username/username.ru.md index 7e2af4784..3f1f3140f 100644 --- a/packages/components/username/username.ru.md +++ b/packages/components/username/username.ru.md @@ -3,3 +3,5 @@ Под отображением имени пользователя понимается набор атрибутов внутреннего пользователя системы, которые помогают идентифицировать пользователя в интерфейсе. + + diff --git a/packages/components/username/username.scss b/packages/components/username/username.scss new file mode 100644 index 000000000..aa30871ba --- /dev/null +++ b/packages/components/username/username.scss @@ -0,0 +1,98 @@ +@use '../core/styles/common'; +@use '../core/styles/common/tokens'; + +:where(.kbq-username) { + --kbq-username-primary-color: var(--kbq-foreground-contrast); + --kbq-username-secondary-color: var(--kbq-foreground-contrast-secondary); + --kbq-username-vertical-gap: var(--kbq-size-xxs); + --kbq-username-horizontal-gap: var(--kbq-size-xxs); +} + +@mixin _kbq-username-theme() { + & { + color: var(--kbq-username-primary-color); + + .kbq-username__site, + .kbq-username__name + .kbq-username__login { + color: var(--kbq-username-secondary-color); + } + } + + &.kbq-username_error { + --kbq-username-primary-color: var(--kbq-foreground-error); + --kbq-username-secondary-color: var(--kbq-foreground-error); + } + + &.kbq-username_simple { + --kbq-username-primary-color: var(--kbq-foreground-contrast); + --kbq-username-secondary-color: var(--kbq-foreground-contrast); + } +} + +@mixin _kbq-username-typography() { + & { + @include tokens.kbq-typography-level-to-styles_css-variables(typography, text-normal); + } + + &.kbq-username_simple { + .kbq-username__name { + @include tokens.kbq-typography-level-to-styles_css-variables(typography, text-normal-strong); + } + } +} + +.kbq-username { + @include _kbq-username-theme(); + @include _kbq-username-typography(); + + &.kbq-username_stacked { + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + align-items: normal; + + gap: var(--kbq-username-vertical-gap); + } + + &.kbq-username_inline:not(.kbq-username_none) { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: baseline; + + .kbq-username__name + .kbq-username__login { + @include common.rtl-prop(margin-left, margin-right, var(--kbq-username-horizontal-gap), 0); + } + } + + &.kbq-username_text { + display: inline; + } + + &.kbq-username_stacked, + &.kbq-username_inline { + .kbq-username__name, + .kbq-username__login { + @include common.kbq-truncate-line(); + } + } + + &.kbq-username_none { + display: inline; + color: inherit; + + .kbq-username__login, + .kbq-username__site { + color: inherit; + } + } +} + +.kbq-link .kbq-username { + color: inherit; + + .kbq-username__login, + .kbq-username__site { + color: inherit; + } +} diff --git a/packages/components/username/username.spec.ts b/packages/components/username/username.spec.ts index 4f7492b71..4828d1b46 100644 --- a/packages/components/username/username.spec.ts +++ b/packages/components/username/username.spec.ts @@ -1,14 +1,52 @@ -import { KbqUsernamePipe } from './username.pipe'; +import { ChangeDetectionStrategy, Component, Type } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { KbqUsernameMode, KbqUsernameStyle } from './types'; +import { KbqUsername } from './username'; -describe(KbqUsernamePipe.name, () => { - let pipe: KbqUsernamePipe; +const createComponent = (component: Type, providers: any[] = []): ComponentFixture => { + TestBed.configureTestingModule({ imports: [component], providers }).compileComponents(); + const fixture = TestBed.createComponent(component); - beforeEach(() => { - pipe = new KbqUsernamePipe(); - }); + fixture.autoDetectChanges(); + + return fixture; +}; - it('create an instance', () => { - pipe = new KbqUsernamePipe(); - expect(pipe).toBeTruthy(); +describe(KbqUsername.name, () => { + it('should use default input values', () => { + const { debugElement } = createComponent(TestComponent); + + expect(debugElement.query(By.directive(KbqUsername)).classes).toMatchSnapshot(); }); }); + +@Component({ + selector: 'test-component', + standalone: true, + imports: [ + KbqUsername + ], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TestComponent { + userInfo: any = { + firstName: 'firstName', + middleName: 'MiddleName', + lastName: 'LastName', + login: 'login' + }; + selectedMode: KbqUsernameMode = 'inline'; + selectedType: KbqUsernameStyle = 'default'; + isCompact = false; + fullNameFormat = 'f.m.l'; +} diff --git a/packages/components/username/username.ts b/packages/components/username/username.ts index a2729c537..774bf245c 100644 --- a/packages/components/username/username.ts +++ b/packages/components/username/username.ts @@ -1,13 +1,70 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import { KbqUsernameMode } from './types'; +import { + booleanAttribute, + ChangeDetectionStrategy, + Component, + computed, + input, + ViewEncapsulation +} from '@angular/core'; +import { KbqTitleModule } from '@koobiq/components/title'; +import { kbqDefaultFullNameFormat } from './constants'; +import { KbqUsernameMode, KbqUsernameStyle } from './types'; +import { KbqUsernamePipe } from './username.pipe'; + +const cssBase = 'kbq-username'; + +type Profile = { + firstName?: string; + lastName?: string; + middleName?: string; + login?: string; + site?: string; +}; @Component({ selector: 'kbq-username', standalone: true, exportAs: 'kbqUsername', + imports: [ + KbqUsernamePipe, + KbqTitleModule + ], + templateUrl: './username.html', + styleUrls: ['./username.scss'], + host: { + class: cssBase, + '[class]': 'class()' + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class KbqUsername { + readonly userInfo = input(); + readonly mode = input('inline'); + readonly isCompact = input(false, { transform: booleanAttribute }); + readonly fullNameFormat = input(kbqDefaultFullNameFormat); + readonly type = input('default'); + + protected readonly hasFullName = computed(() => { + const profile = this.userInfo(); + + if (!profile) return false; + + return profile?.lastName && profile?.firstName; + }); + + protected readonly class = computed(() => { + return [this.type(), this.mode()].map((modificator) => `${cssBase}_${modificator}`).join(' '); + }); +} + +@Component({ + selector: 'kbq-username-custom', + standalone: true, + exportAs: 'kbqUsernameCustom', imports: [], template: ` - + `, @@ -16,7 +73,6 @@ import { KbqUsernameMode } from './types'; }, changeDetection: ChangeDetectionStrategy.OnPush }) -export class KbqUsername { - profile = input.required(); - mode = input('inline'); +export class KbqUsernameCustom { + protected readonly mode = input('inline'); } diff --git a/packages/docs-examples/components/username/index.ts b/packages/docs-examples/components/username/index.ts index 77e8df3cd..ec9d8eb4c 100644 --- a/packages/docs-examples/components/username/index.ts +++ b/packages/docs-examples/components/username/index.ts @@ -1,10 +1,12 @@ import { NgModule } from '@angular/core'; import { UsernameOverviewExample } from './username-overview/username-overview-example'; +import { UsernamePlaygroundExample } from './username-playground/username-playground-example'; -export { UsernameOverviewExample }; +export { UsernameOverviewExample, UsernamePlaygroundExample }; const EXAMPLES = [ - UsernameOverviewExample + UsernameOverviewExample, + UsernamePlaygroundExample ]; @NgModule({ diff --git a/packages/docs-examples/components/username/username-overview/username-overview-example.ts b/packages/docs-examples/components/username/username-overview/username-overview-example.ts index 7c373bbec..5136db9d1 100644 --- a/packages/docs-examples/components/username/username-overview/username-overview-example.ts +++ b/packages/docs-examples/components/username/username-overview/username-overview-example.ts @@ -1,31 +1,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { KbqFormField } from '@koobiq/components/form-field'; -import { KbqLinkModule } from '@koobiq/components/link'; -import { KbqTextareaModule } from '@koobiq/components/textarea'; -import { - KBQ_PROFILE_MAPPING, - KbqFormatKeyToProfileMapping, - KbqUsernameFormatKey, - KbqUsernameModule -} from '@koobiq/components/username'; - -type ExampleUser = { - firstName?: string; - lastName?: string; - middleName?: string; -}; - -const mapping: KbqFormatKeyToProfileMapping = { - [KbqUsernameFormatKey.FirstNameShort]: 'firstName', - [KbqUsernameFormatKey.FirstNameFull]: 'firstName', - - [KbqUsernameFormatKey.MiddleNameShort]: 'middleName', - [KbqUsernameFormatKey.MiddleNameFull]: 'middleName', - - [KbqUsernameFormatKey.LastNameShort]: 'lastName', - [KbqUsernameFormatKey.LastNameFull]: 'lastName' -}; +import { KbqUsername } from '@koobiq/components/username'; /** * @title Username overview @@ -33,49 +7,18 @@ const mapping: KbqFormatKeyToProfileMapping = { @Component({ selector: 'username-overview-example', standalone: true, - imports: [KbqUsernameModule, KbqTextareaModule, FormsModule, KbqFormField, KbqLinkModule], + imports: [KbqUsername], template: ` -
-
{{ profile | kbqUsername: 'L f. m.' }}
-
{{ profile | kbqUsername: 'L f. m.' }}
- - - -
- - {{ profile | kbqUsername }} - -
- -
- - {{ profile | kbqUsername }} - -
- -
- {{ profile | kbqUsername: format }} -
-
- - - - - `, - styles: ` - .example-error-text { - color: var(--kbq-foreground-error); - } + `, - providers: [ - { provide: KBQ_PROFILE_MAPPING, useValue: mapping }], changeDetection: ChangeDetectionStrategy.OnPush }) export class UsernameOverviewExample { - profile: ExampleUser = { firstName: 'First name', lastName: 'LastName', middleName: 'MiddleName' }; - format = 'L f. m.'; + userInfo = { + firstName: 'Maxwell', + middleName: 'Alan', + lastName: 'Root', + login: 'mroot', + site: 'corp' + }; } diff --git a/packages/docs-examples/components/username/username-playground/username-playground-example.ts b/packages/docs-examples/components/username/username-playground/username-playground-example.ts new file mode 100644 index 000000000..0dab32cf6 --- /dev/null +++ b/packages/docs-examples/components/username/username-playground/username-playground-example.ts @@ -0,0 +1,83 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { KbqCheckboxModule } from '@koobiq/components/checkbox'; +import { KbqFormFieldModule } from '@koobiq/components/form-field'; +import { KbqLinkModule } from '@koobiq/components/link'; +import { KbqRadioModule } from '@koobiq/components/radio'; +import { KbqTextareaModule } from '@koobiq/components/textarea'; +import { KbqUsernameMode, KbqUsernameModule, KbqUsernameStyle } from '@koobiq/components/username'; + +/** + * @title Username overview + */ +@Component({ + selector: 'username-playground-example', + standalone: true, + imports: [ + FormsModule, + KbqUsernameModule, + KbqTextareaModule, + KbqFormFieldModule, + KbqLinkModule, + KbqCheckboxModule, + KbqRadioModule + ], + template: ` +
+ +
+
+ + Name format + + + + isCompact + + @for (usernameMode of modes; track usernameMode) { + + {{ usernameMode }} + + } + + + @for (usernameType of types; track usernameType) { + + {{ usernameType }} + + } + +
+ `, + styles: ` + .example-result { + padding: var(--kbq-size-m); + margin-bottom: var(--kbq-size-m); + background: var(--kbq-background-theme-less); + border-radius: var(--kbq-size-border-radius); + } + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class UsernamePlaygroundExample { + userInfo = { + firstName: 'Maxwell', + middleName: 'Alan', + lastName: 'Root', + login: 'mroot', + site: 'corp' + }; + selectedMode: KbqUsernameMode = 'inline'; + selectedType: KbqUsernameStyle = 'default'; + isCompact = false; + fullNameFormat = 'f.m.l'; + + modes: KbqUsernameMode[] = ['inline', 'stacked', 'text']; + types: KbqUsernameStyle[] = ['default', 'error', 'simple', 'none']; +} diff --git a/tools/public_api_guard/components/username.api.md b/tools/public_api_guard/components/username.api.md new file mode 100644 index 000000000..94dbd5b7e --- /dev/null +++ b/tools/public_api_guard/components/username.api.md @@ -0,0 +1,120 @@ +## 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 i0 from '@angular/core'; +import { InjectionToken } from '@angular/core'; +import { InputSignal } from '@angular/core'; +import { InputSignalWithTransform } from '@angular/core'; +import { PipeTransform } from '@angular/core'; +import { Signal } from '@angular/core'; + +// @public +export const KBQ_PROFILE_MAPPING: InjectionToken; + +// @public (undocumented) +export const kbqDefaultFullNameFormat = "lf.m."; + +// @public +export const kbqDefaultFullNameFormatCustom = "L f. m."; + +// @public +export type KbqFormatKeyToProfileMapping = { + [key in KbqUsernameFormatKey]: keyof T | undefined; +}; + +// @public +export function KbqMappingMissingError(): Error; + +// @public (undocumented) +export class KbqUsername { + // (undocumented) + protected readonly class: Signal; + // (undocumented) + readonly fullNameFormat: InputSignal; + // (undocumented) + protected readonly hasFullName: Signal; + // (undocumented) + readonly isCompact: InputSignalWithTransform; + // (undocumented) + readonly mode: InputSignal; + // (undocumented) + readonly type: InputSignal; + // Warning: (ae-forgotten-export) The symbol "Profile" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly userInfo: InputSignal; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public (undocumented) +export class KbqUsernameCustom { + // (undocumented) + protected readonly mode: InputSignal; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export class KbqUsernameCustomPipe implements PipeTransform { + transform(profile: T, format?: string, argMapping?: KbqFormatKeyToProfileMapping): string; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration, never>; + // (undocumented) + static ɵpipe: i0.ɵɵPipeDeclaration, "kbqUsernameCustom", true>; + // (undocumented) + static ɵprov: i0.ɵɵInjectableDeclaration>; +} + +// @public +export enum KbqUsernameFormatKey { + // (undocumented) + Dot = ".", + FirstNameFull = "F", + FirstNameShort = "f", + LastNameFull = "L", + LastNameShort = "l", + MiddleNameFull = "M", + MiddleNameShort = "m" +} + +// @public +export type KbqUsernameMode = 'stacked' | 'inline' | 'text'; + +// @public (undocumented) +export class KbqUsernameModule { + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; + // (undocumented) + static ɵinj: i0.ɵɵInjectorDeclaration; + // Warning: (ae-forgotten-export) The symbol "i1" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "i2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static ɵmod: i0.ɵɵNgModuleDeclaration; +} + +// @public (undocumented) +export class KbqUsernamePipe implements PipeTransform { + transform(profile: T, format?: string, argMapping?: KbqFormatKeyToProfileMapping): string; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration, never>; + // (undocumented) + static ɵpipe: i0.ɵɵPipeDeclaration, "kbqUsername", true>; + // (undocumented) + static ɵprov: i0.ɵɵInjectableDeclaration>; +} + +// @public +export type KbqUsernameStyle = 'default' | 'error' | 'simple' | 'none'; + +// (No @packageDocumentation comment for this package) + +``` From af1bcb32993098d109a4fa639552a342a18c67f6 Mon Sep 17 00:00:00 2001 From: Nikita Guryev Date: Fri, 1 Aug 2025 14:47:38 +0300 Subject: [PATCH 03/15] chore: added customization, replaced css classes with directives, renamed types --- packages/components/username/module.ts | 11 +- packages/components/username/types.ts | 6 +- .../components/username/username-tokens.scss | 6 + packages/components/username/username.en.md | 16 +++ packages/components/username/username.html | 50 ++++---- packages/components/username/username.ru.md | 10 ++ packages/components/username/username.scss | 35 +++--- packages/components/username/username.ts | 90 +++++++++++---- .../username-custom-example.ts | 107 ++++++++++++++++++ .../username-playground-example.ts | 11 +- .../components/username.api.md | 55 +++++---- 11 files changed, 299 insertions(+), 98 deletions(-) create mode 100644 packages/components/username/username-tokens.scss create mode 100644 packages/docs-examples/components/username/username-custom/username-custom-example.ts diff --git a/packages/components/username/module.ts b/packages/components/username/module.ts index 2910e4fc8..5a5693fcc 100644 --- a/packages/components/username/module.ts +++ b/packages/components/username/module.ts @@ -1,8 +1,15 @@ import { NgModule } from '@angular/core'; -import { KbqUsername } from './username'; +import { KbqUsername, KbqUsernameCustomView, KbqUsernamePrimary, KbqUsernameSecondary } from './username'; import { KbqUsernameCustomPipe, KbqUsernamePipe } from './username.pipe'; -const COMPONENTS = [KbqUsername, KbqUsernameCustomPipe, KbqUsernamePipe]; +const COMPONENTS = [ + KbqUsername, + KbqUsernameCustomView, + KbqUsernamePrimary, + KbqUsernameSecondary, + KbqUsernameCustomPipe, + KbqUsernamePipe +]; @NgModule({ imports: COMPONENTS, diff --git a/packages/components/username/types.ts b/packages/components/username/types.ts index 047d41d20..b9f3bab66 100644 --- a/packages/components/username/types.ts +++ b/packages/components/username/types.ts @@ -56,7 +56,7 @@ export type KbqUsernameMode = 'stacked' | 'inline' | 'text'; * * - `default`: standard styling with primary and secondary colors. * - `error`: error colors (e.g., red). - * - `simple`: minimal style with contrast color. - * - `none`: inherits parent styles, no theming. For exampe, useful when using inside links. + * - `accented`: no color theming; emphasizes via typography only. + * - `inherit`: inherits parent styles, no theming. For example, useful when using inside links. */ -export type KbqUsernameStyle = 'default' | 'error' | 'simple' | 'none'; +export type KbqUsernameStyle = 'default' | 'error' | 'accented' | 'inherit'; diff --git a/packages/components/username/username-tokens.scss b/packages/components/username/username-tokens.scss new file mode 100644 index 000000000..3ff6c44bb --- /dev/null +++ b/packages/components/username/username-tokens.scss @@ -0,0 +1,6 @@ +:where(.kbq-username) { + --kbq-username-primary-color: var(--kbq-foreground-contrast); + --kbq-username-secondary-color: var(--kbq-foreground-contrast-secondary); + --kbq-username-vertical-gap: var(--kbq-size-xxs); + --kbq-username-horizontal-gap: var(--kbq-size-xxs); +} diff --git a/packages/components/username/username.en.md b/packages/components/username/username.en.md index acc947f57..4e2c5153e 100644 --- a/packages/components/username/username.en.md +++ b/packages/components/username/username.en.md @@ -1 +1,17 @@ +The pattern is used when the interface refers to an internal user. + +Displaying the username means showing a set of attributes of the system’s internal user that help identify the user within the interface. + + +### Settings demonstration + + + +### Custom template + +If flexible data display is needed and the standard template doesn’t fit, use the `custom` mode with the `KbqUsernameCustomView` directive. +It supports the same modes and styles while maintaining consistency with the design system. +To format the full name, you can use the `kbqUsernameCustom` pipe with format settings and a mapping object (which links format elements to the corresponding user properties, defining which data to display). + + diff --git a/packages/components/username/username.html b/packages/components/username/username.html index 2dbb873f9..366d91304 100644 --- a/packages/components/username/username.html +++ b/packages/components/username/username.html @@ -1,31 +1,35 @@ -@let profile = userInfo(); -@let fullName = profile | kbqUsername: fullNameFormat(); + -@if (!isCompact()) { - @if (hasFullName()) { - {{ fullName }} - } +@if (!customView()) { + @let profile = userInfo(); + @let fullName = profile | kbqUsername: fullNameFormat(); + + @if (!isCompact()) { + @if (hasFullName()) { + {{ fullName }} + } - @if (profile?.login) { -