@guildofgleks/ui 21.3.0 → 21.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,1149 @@
1
+ # @guildofgleks/ui — AI agent guide
2
+
3
+ This file is for an AI coding agent (Claude, Copilot, Cursor, etc.) helping a developer build
4
+ an app that **consumes** the published `@guildofgleks/ui` npm package. It is not about
5
+ authoring the library — if you are working inside the `gleks_web_ui` monorepo itself, read
6
+ `.github/instructions/*.md` instead.
7
+
8
+ Everything below reflects the library's actual source as of **`21.3.2`**. `README.md` covers the
9
+ same ground at a higher level — install, setup, theming, global configuration — and is accurate;
10
+ this file goes further, into per-component input tables, and is the one to trust for exact names,
11
+ types and defaults.
12
+
13
+ > **Maintainers:** this file ships inside the npm package and is the API reference an agent reads
14
+ > while writing code against it, so a stale table here becomes wrong code in someone else's app —
15
+ > silently, because nothing fails a build. **Any change to an input, output, slot, type, service
16
+ > method or default updates this file in the same change**, and moves the version marker in the
17
+ > paragraph above. See `.github/instructions/gleks-ui-library.instructions.md`, definition of
18
+ > done, step 9.
19
+
20
+ ## Quick facts
21
+
22
+ - Angular **v21+** only (`peerDependencies` require `^21.2.0` for `@angular/core`,
23
+ `@angular/common`, `@angular/forms`, `@angular/platform-browser`). No support for older
24
+ Angular.
25
+ - No Angular CDK, no Material. Only runtime dependency is `tslib`.
26
+ - Every component is **standalone**, `ChangeDetectionStrategy.OnPush`, and built with signals —
27
+ `input()` / `output()` / `model()`, never `@Input()`/`@Output()` decorators, never `ngClass`/
28
+ `ngStyle`.
29
+ - **Reactive Forms only.** Every form control implements `ControlValueAccessor` and is built
30
+ and tested against `[formControl]` / `formControlName`. The library never imports
31
+ `FormsModule` and `[(ngModel)]` is untested — don't suggest it.
32
+ - Theming is 100% CSS custom properties (`--gog-*`) — no Sass config, no JS theme objects, no
33
+ build step to restyle anything.
34
+ - Tree-shakeable: `"sideEffects": false` and every component is a separate standalone import, so
35
+ importing `ButtonComponent` alone does not pull in the rest of the library.
36
+ - SSR-safe: anything touching `window`/`document` is guarded with `isPlatformBrowser`/
37
+ `afterNextRender`.
38
+
39
+ ## Install & setup
40
+
41
+ ```bash
42
+ npm install @guildofgleks/ui
43
+ ```
44
+
45
+ Add the baseline stylesheet once — it carries every token the components read plus their
46
+ utility classes, so without it components render unstyled:
47
+
48
+ ```jsonc
49
+ // angular.json → projects.<app>.architect.build.options
50
+ "styles": [
51
+ "node_modules/@guildofgleks/ui/styles/index.css",
52
+ "src/styles.scss", // your own styles, after the baseline so they win
53
+ ],
54
+ ```
55
+
56
+ Import components where you use them — every one is standalone:
57
+
58
+ ```ts
59
+ import { Component } from '@angular/core';
60
+ import { ButtonComponent, SelectComponent } from '@guildofgleks/ui';
61
+
62
+ @Component({
63
+ selector: 'app-example',
64
+ imports: [ButtonComponent, SelectComponent],
65
+ template: `
66
+ <gog-select label="Region" [options]="regions" [(value)]="region" />
67
+ <gog-button (gogClick)="save()">Save</gog-button>
68
+ `,
69
+ })
70
+ export class ExampleComponent {}
71
+ ```
72
+
73
+ ## Core conventions (read once, applies everywhere)
74
+
75
+ These hold for essentially every component in the library. Knowing them means you can guess a
76
+ new component's API correctly instead of guessing wrong and hallucinating an input that doesn't
77
+ exist.
78
+
79
+ - **Selector prefix `gog-`** for components (`gog-button`, `gog-select`, …), attribute selectors
80
+ for directives (`gogTooltip`, `[gogBadge]`).
81
+ - **Outputs are prefixed `gog`** so they never collide with native DOM events —
82
+ `gogClick`, `gogToggle`, `gogSearch`, `gogTabChange`, `gogRemove`, `gogScroll`, `gogLoadMore`,
83
+ `gogDateSelect`. **Inputs keep their natural name** (`variant`, `size`, `disabled`).
84
+ - **Two-way binding via `model()`.** Wherever a component holds a value the consumer drives, it's
85
+ a `model()` input — bind with `[(value)]="signal"` / `[(checked)]="signal"` /
86
+ `[(open)]="signal"` etc., or split into `[value]` + `(valueChange)`.
87
+ - **Every input has a zero-config default.** Nothing requires configuration to render something
88
+ reasonable.
89
+ - **`size` is `GogSize = 'xsm' | 'sm' | 'md' | 'lg' | 'slg'`**, shared by every sized component.
90
+ Default is `'md'` almost everywhere — exceptions: `gog-accordion` and `gog-table` default to
91
+ `'lg'` (their `size` means row/section density, not form-control size), `gog-paginator`
92
+ defaults to `'sm'`.
93
+ - **`variant` is `GogVariant = 'primary' | 'secondary' | 'outline' | 'ghost'`** on `gog-button`.
94
+ Status-colored components (`gog-tag`, `gog-badge`) use a different, four-value
95
+ `GogTagVariant = 'success' | 'danger' | 'warning' | 'info'` instead — don't confuse the two.
96
+ - **`errorDisplay: GogErrorDisplay = 'auto' | 'manual'`** (default `'manual'`) on every control
97
+ that shows a validation message (inputfield, textarea, select, multiselect, autocomplete,
98
+ radio-group, slider, datepicker). `'manual'`: the field shows `errorMessage` whenever it's
99
+ non-empty — you own the timing (`errorMessage="control.invalid && control.touched ? 'Required' : ''"`).
100
+ `'auto'`: shown once the attached `[formControl]`/`formControlName` is touched _and_ invalid —
101
+ you only supply the message text. `'auto'` silently behaves like `'manual'` if there's no real
102
+ form control attached.
103
+ - **`inputId` is optional everywhere.** Every form control renders a real `id` — its own if you
104
+ pass one, a generated one otherwise — so the `<label for>` and the error message's
105
+ `aria-describedby` are always wired up. Pass `inputId` only when something outside the
106
+ component needs to reference the field by a known id; never pass one just to get a label.
107
+ - **User-visible chrome strings come from `GOG_CONFIG.labels`**, not from an input per string —
108
+ "Clear", "Close dialog", "Go to page 4" and the rest. Per-instance label inputs exist where a
109
+ single control realistically differs and win over the config. See
110
+ [`labels`](#labels--translating-the-library).
111
+ - **`floatLabel: GogFloatLabelVariant = 'none' | 'in' | 'on' | 'over'`** (default `'none'`) on
112
+ the six field controls: inputfield, textarea, select, multiselect, autocomplete, datepicker.
113
+ `'in'` floats up but stays inside the border, `'on'` floats to sit centered on the top border
114
+ line, `'over'` floats fully above the field. Pair with `floatLabelShowPlaceholder` (default
115
+ `false`) to reveal the field's own `placeholder` once the label has floated clear.
116
+ - **`clearable`** (default varies) on inputfield, textarea, select, multiselect, autocomplete,
117
+ datepicker — shows a clear (×) button once the field has content. Off by default everywhere
118
+ except `gog-multiselect`, which had one before the input existed.
119
+ - **Generic option accessors, not a fixed DTO.** Any collection-driven control (`gog-select`,
120
+ `gog-multiselect`, `gog-autocomplete`, `gog-button-toggle-group`) takes **your own object
121
+ shape** through `optionLabel` / `optionValue` / `optionDisabled` — each is a property path
122
+ (`'name'`, dot-paths like `'profile.title'` work) **or** a function
123
+ `(option: T) => TResult`. Defaults are `'name'` / `'id'` / `'disabled'`. Set
124
+ `[optionValue]="null"` to emit **the option object itself** instead of a plucked id — the
125
+ control then round-trips your own object with no lookup table needed:
126
+ ```html
127
+ <gog-select [options]="members" [optionLabel]="nameOf" [optionValue]="null" [(value)]="member" />
128
+ ```
129
+ - **Global defaults via `GOG_CONFIG` / `provideGogConfig(...)`** — see its own section below.
130
+ Precedence is always: the instance's own input (if set) → `GOG_CONFIG` → the component's
131
+ built-in default.
132
+ - **Don't bind both a `model()` and a form directive on the same instance.** Every CVA control
133
+ (checkbox, toggle, radio-group, inputfield, textarea, select, multiselect, autocomplete,
134
+ slider, datepicker) exposes its value as both a two-way `model()` (`[(checked)]`, `[(value)]`)
135
+ and, separately, `ControlValueAccessor` for `[formControl]`/`formControlName`. Pick one per
136
+ instance — wiring both gives the value two competing sources of truth.
137
+ - **The custom-content slot pattern.** Wherever a component needs custom markup for a specific
138
+ part of itself, it's an attribute directive read with `contentChild()`, given a **typed**
139
+ context via `let-` variables — never a plain `TemplateRef` input, never a string-keyed lookup.
140
+ Recognize the shape:
141
+ ```html
142
+ <gog-accordion [items]="items">
143
+ <ng-template gogAccordionHeader let-item let-open="open">{{ item.title }}</ng-template>
144
+ </gog-accordion>
145
+ ```
146
+ See the per-component tables below for which slot directives exist on which component.
147
+ - **Legacy `TemplateRef` inputs and string-keyed lookups still exist on a few components and
148
+ still work, but are `@deprecated` — do not use them in new code.** See
149
+ [Deprecated patterns — do not use in new code](#deprecated-patterns--do-not-use-in-new-code).
150
+ - **Accessibility is built in**, not optional: keyboard navigation (roving tabindex, arrow keys,
151
+ Home/End), ARIA roles/states, `:focus-visible` styling, `prefers-reduced-motion` handling, and
152
+ WCAG AA contrast are already implemented — you don't need to add any of this yourself, just
153
+ supply `ariaLabel`/`label` inputs where a component has no visible text of its own (icon-only
154
+ buttons, `gog-progressbar`, `gog-scroll`).
155
+ - **`aria-label` on the host tag does nothing.** Several components (`gog-button` chief among
156
+ them) render their real interactive element (a `<button>`) _inside_ the component's own host
157
+ tag. An `aria-label` attribute placed directly on `<gog-button>` in a template lands on the
158
+ custom element wrapper, not on the inner `<button>`, so assistive tech never sees it — always
159
+ use the component's own `ariaLabel` input instead.
160
+
161
+ ## Theming
162
+
163
+ Full model is in `README.md`'s Theming section and `theming.md`; short version:
164
+
165
+ - Every visual value (color, spacing, radius, shadow, duration) is a `--gog-*` CSS custom
166
+ property, layered **foundation** (`--gog-accent-color`, `--gog-space-md`, …, restyles
167
+ everything) → **component** (`--gog-btn-primary-bg`, …, one block per component) →
168
+ **instance** (`--gog-btn-bg`, …, deliberately undeclared escape hatch for one element).
169
+ - Theme switch is a `data-theme` attribute, usually on `<html>`, toggled through the
170
+ `ThemeService` (`inject(ThemeService).setTheme('dark')` / `.toggleTheme()` / `.theme` signal).
171
+ Ships `light` and `dark`. Three more importable presets: `slate`, `one-dark`, `one-light`
172
+ (`@guildofgleks/ui/styles/presets/<name>.css`).
173
+ - Restyle one instance without touching a theme: `<gog-button style="--gog-btn-bg: #ff4edb">`.
174
+ - Build a custom theme by declaring a palette against a new `data-theme` value (see
175
+ `theming.md` for the full worked example) — component tokens re-derive automatically, you
176
+ don't restate them.
177
+
178
+ ## Global configuration — `GOG_CONFIG` / `provideGogConfig(...)`
179
+
180
+ For the handful of inputs an app typically wants to set once (a size for every form control, a
181
+ locale for every datepicker) rather than repeat on every instance:
182
+
183
+ ```ts
184
+ import { provideGogConfig } from '@guildofgleks/ui';
185
+
186
+ bootstrapApplication(App, {
187
+ providers: [
188
+ provideGogConfig({
189
+ control: { size: 'sm', errorDisplay: 'auto', clearable: true },
190
+ dropdown: { appendToBody: true, filter: true },
191
+ datepicker: { locale: 'de-DE', firstDayOfWeek: 1, format: 'dd.MM.yyyy' },
192
+ toast: { position: 'top-right', duration: 4000 },
193
+ }),
194
+ ],
195
+ });
196
+ ```
197
+
198
+ Precedence, always: **instance input → `GOG_CONFIG` → component's built-in default.** A nested
199
+ `provideGogConfig(...)` (in a route's or component's own `providers`) **layers onto the
200
+ parent's config**, one level deep per key — it does not replace it.
201
+
202
+ | Key | Fields | Applies to |
203
+ | -------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
204
+ | `control` | `size`, `errorDisplay`, `clearable` | `size`: button, inputfield, textarea, select, multiselect, checkbox, radio-group, button-toggle-group, datepicker. `errorDisplay`: inputfield, textarea, select, multiselect, autocomplete, radio-group, slider, datepicker. `clearable`: inputfield, textarea, select, multiselect, autocomplete, datepicker. Not table/accordion/paginator (density, not form size), not spinner/skeleton/tag/chip/toggle. |
205
+ | `dropdown` | `appendToBody`, `direction`, `filter`, `filterPosition` | `gog-select`, `gog-multiselect`. `gog-datepicker`/`gog-autocomplete` honour `appendToBody`/`direction` too (autocomplete has no `filter` box — it filters via the trigger's own text). |
206
+ | `floatLabel` | `variant`, `showPlaceholder` | inputfield, textarea, select, multiselect, autocomplete, datepicker. |
207
+ | `datepicker` | `locale`, `firstDayOfWeek`, `format` | `gog-datepicker`, `gog-calendar`. |
208
+ | `autocomplete` | `searchDebounce`, `minLength`, `openOnFocus` | `gog-autocomplete`. |
209
+ | `tooltip` | `position`, `showDelay`, `hideDelay` | the `gogTooltip` directive. |
210
+ | `scroll` | `autoHide`, `hideDelay`, `size`, `overscrollBehavior`, `showTrack` | `gog-scroll` (and every component that uses one internally). |
211
+ | `button` | `debounce` | `gog-button`. |
212
+ | `inputfield` | `showSpinButtons` | `gog-inputfield`. |
213
+ | `textarea` | `resize` | `gog-textarea`. |
214
+ | `toast` | `position`, `duration` | `ToastService`. |
215
+ | `theme` | `storageKey`, `defaultTheme`, `followSystem`, `lightTheme`, `darkTheme` | `ThemeService`. All off/neutral by default — see below. |
216
+ | `labels` | every fixed string the library renders — see below | inputfield, textarea, select, multiselect, autocomplete, datepicker, calendar, paginator, `DialogService`, `ToastService`. |
217
+
218
+ Anything visual does **not** belong here — override the `--gog-*` token instead.
219
+
220
+ ### `labels` — translating the library
221
+
222
+ Every string a component renders that the consumer never writes markup for. An app that isn't
223
+ in English sets these once rather than on every control:
224
+
225
+ ```ts
226
+ provideGogConfig({
227
+ labels: {
228
+ clear: 'Очистить', // inputfield / textarea clear button
229
+ clearSelection: 'Очистить выбор', // select / multiselect / autocomplete
230
+ clearDate: 'Очистить дату', // datepicker
231
+ selectAll: 'Выбрать все', // multiselect panel
232
+ clearAll: 'Очистить', // multiselect panel
233
+ increment: 'Увеличить', // number spin buttons
234
+ decrement: 'Уменьшить',
235
+ showPassword: 'Показать пароль',
236
+ hidePassword: 'Скрыть пароль',
237
+ closeDialog: 'Закрыть',
238
+ closeToast: 'Закрыть',
239
+ pagination: 'Навигация по страницам',
240
+ previousPage: 'Предыдущая страница',
241
+ nextPage: 'Следующая страница',
242
+ openCalendar: 'Открыть календарь',
243
+ today: 'Сегодня',
244
+ thisMonth: 'Текущий месяц',
245
+ previousMonth: 'Предыдущий месяц',
246
+ nextMonth: 'Следующий месяц',
247
+ previousYear: 'Предыдущий год',
248
+ nextYear: 'Следующий год',
249
+ hours: 'Часы',
250
+ minutes: 'Минуты',
251
+ seconds: 'Секунды',
252
+ // The one non-string field: it interpolates the page number, and word order and
253
+ // agreement around a number vary by language, so it takes a formatter.
254
+ page: (page, isCurrent) =>
255
+ isCurrent ? `Страница ${page}, текущая` : `Перейти на страницу ${page}`,
256
+ },
257
+ });
258
+ ```
259
+
260
+ Strings that describe **one** control rather than library chrome — `gog-checkbox`'s `ariaLabel`,
261
+ `gog-button`'s `ariaLabel`, any field's `label`/`placeholder` — are deliberately **not** here.
262
+ Those stay per instance. Where a per-instance label input exists (`clearAriaLabel`, `todayLabel`,
263
+ …) it still wins over the configured value.
264
+
265
+ ## Services
266
+
267
+ ### `ThemeService`
268
+
269
+ ```ts
270
+ private readonly theme = inject(ThemeService);
271
+ this.theme.theme(); // Signal<string>, READ-ONLY — current data-theme
272
+ this.theme.setTheme('dark'); // any theme name, including a custom one you declared in CSS
273
+ this.theme.toggleTheme(); // flips between the configured light and dark names
274
+ ```
275
+
276
+ `theme` is read-only on purpose: writing to it would move the signal without touching the
277
+ `data-theme` attribute the styles actually read. Never suggest `theme.set(...)` — it does not
278
+ exist.
279
+
280
+ Zero-config behaviour: adopt whatever `data-theme` is already on `<html>`, else `'light'`.
281
+ Persistence and following the OS setting are **opt-in**, so upgrading cannot change which theme
282
+ an existing app opens in:
283
+
284
+ ```ts
285
+ provideGogConfig({
286
+ theme: {
287
+ storageKey: 'app-theme', // persist the choice in localStorage; unset = no persistence
288
+ followSystem: true, // open in the OS prefers-color-scheme, and keep following it
289
+ // until the app calls setTheme/toggleTheme
290
+ lightTheme: 'light', // the two names followSystem maps to and toggleTheme alternates
291
+ darkTheme: 'cyberpunk', // between
292
+ defaultTheme: 'light', // used when nothing else decides
293
+ },
294
+ });
295
+ ```
296
+
297
+ Resolution order at startup: existing `data-theme` on the document → persisted value →
298
+ OS setting (if `followSystem`) → `defaultTheme` → `'light'`.
299
+
300
+ ### `ToastService`
301
+
302
+ Root-provided singleton. Requires a `<gog-toast-container />` placed once in your app (see
303
+ [gog-toast](#gog-toast--gog-toast-container) below — it is **not** wired up automatically).
304
+
305
+ ```ts
306
+ private readonly toast = inject(ToastService);
307
+
308
+ this.toast.success('Saved');
309
+ this.toast.error('Could not save', {
310
+ isSticky: true,
311
+ actions: [{ label: 'Retry', onClick: () => this.save() }],
312
+ });
313
+ // also: .warning(msg, config?), .info(msg, config?), .show(config), .dismiss(id), .dismissAll()
314
+ ```
315
+
316
+ `ToastConfig`: `{ message, type?, iconName?, iconTemplate?, actions?, dedupeKey?, isSticky?, duration?, position? }`.
317
+ Repeated calls with the same (explicit or inferred) `dedupeKey` replace the existing toast in
318
+ place instead of stacking a duplicate.
319
+
320
+ ### `DialogService`
321
+
322
+ Root-provided singleton, imperative dynamic-component dialogs. Requires a `<gog-dialog />`
323
+ placed once in your app (see [gog-dialog](#gog-dialog) below — also **not** automatic).
324
+
325
+ ```ts
326
+ private readonly dialogService = inject(DialogService);
327
+
328
+ async confirmDelete(): Promise<void> {
329
+ const handle = this.dialogService.open<boolean>({
330
+ component: ConfirmationDialogComponent, // or your own component
331
+ title: 'Delete this item?',
332
+ role: 'alertdialog',
333
+ data: { message: 'This cannot be undone.' },
334
+ });
335
+ const confirmed = await handle.afterClosed; // boolean | undefined
336
+ }
337
+ ```
338
+
339
+ `DialogConfig`: `{ title?, component, data?, modal? (default true), closable?, draggable?, closeIconName?, closeIconTemplate?, width?, maxWidth?, role? ('dialog' default | 'alertdialog'), zIndex? }`.
340
+ `open()` returns `{ close(result?), afterClosed: Promise<TResult | undefined> }`. Also:
341
+ `closeAll(result?)`, `updatePosition(id, offsetX, offsetY)` (for `draggable` dialogs).
342
+
343
+ The library ships a ready-made `ConfirmationDialogComponent` for yes/no prompts — pass it as
344
+ `component` with `data: { title, description, confirmText, cancelText }`; it resolves the
345
+ dialog's result to `true`/`false`.
346
+
347
+ **Wiring a custom component into a dialog** — it reads its data via `DIALOG_DATA` and closes
348
+ itself via `DIALOG_REF`:
349
+
350
+ ```ts
351
+ import { Component, inject } from '@angular/core';
352
+ import { DIALOG_DATA, DIALOG_REF } from '@guildofgleks/ui';
353
+
354
+ @Component({ selector: 'app-edit-dialog', template: `…` })
355
+ export class EditDialogComponent {
356
+ protected readonly data = inject<{ userId: string }>(DIALOG_DATA);
357
+ private readonly ref = inject(DIALOG_REF);
358
+
359
+ save(): void {
360
+ this.ref.close({ saved: true });
361
+ }
362
+ }
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Component reference
368
+
369
+ Every component below is exported from `@guildofgleks/ui`'s root — `import { X } from '@guildofgleks/ui'`.
370
+ "CVA" = implements `ControlValueAccessor` (works with `[formControl]`/`formControlName`).
371
+
372
+ ### Buttons & choices
373
+
374
+ #### `gog-button`
375
+
376
+ | Input | Type | Default | Notes |
377
+ | ----------- | --------------------------------- | ----------- | ----------------------------------------------------- |
378
+ | `variant` | `GogVariant` | `'primary'` | |
379
+ | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
380
+ | `disabled` | `boolean` | `false` | |
381
+ | `fullWidth` | `boolean` | `false` | |
382
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | |
383
+ | `loading` | `boolean` | `false` | shows an inline `gog-spinner`, blocks clicks |
384
+ | `debounce` | `number \| undefined` | `300` | ms; via `GOG_CONFIG.button.debounce` — see note below |
385
+ | `ariaLabel` | `string \| null` | `null` | **use this, not a raw `aria-label` attribute** |
386
+
387
+ Outputs: `gogClick: MouseEvent`.
388
+
389
+ **`debounce` is a spam guard, not a delay before the first click.** The first click in a window
390
+ fires immediately (leading edge); further clicks within `debounce` ms are silently dropped.
391
+
392
+ ```html
393
+ <gog-button variant="primary" [loading]="saving()" (gogClick)="save()">Save</gog-button>
394
+ <gog-button variant="ghost" ariaLabel="Close" (gogClick)="close()"
395
+ ><gog-icon name="close"
396
+ /></gog-button>
397
+ ```
398
+
399
+ #### `gog-button-toggle-group`
400
+
401
+ A row of buttons, single- or multi-select, built from your own option objects.
402
+
403
+ | Input | Type | Default | Notes |
404
+ | ------------------------------------ | ------------------------------------------ | ---------------------- | ------------------------------------- |
405
+ | `options` | `TOption[]` | `[]` | |
406
+ | `optionLabel` | accessor | `'name'` | |
407
+ | `optionValue` | accessor \| `null` | `'id'` | `null` emits the option object |
408
+ | `optionDisabled` | accessor | `'disabled'` | |
409
+ | `optionIcon` | accessor → `GogIconName \| null` \| `null` | `null` | optional leading icon per option |
410
+ | `multiple` | `boolean` | `false` | changes ARIA role entirely — see note |
411
+ | `appearance` | `'joined' \| 'separated'` | `'joined'` | |
412
+ | `orientation` | `GogOrientation` | `'horizontal'` | |
413
+ | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
414
+ | `disabled`, `fullWidth`, `ariaLabel` | | `false`, `false`, `''` | |
415
+
416
+ Model: `value: TValue | TValue[] | null` (single value, or array in `multiple` mode). CVA: yes.
417
+ Slot: `<ng-template gogButtonToggleOption let-opt let-selected="selected">` for custom button
418
+ markup. **Single mode is a radio group** (`role="radiogroup"`, arrows move _and_ select);
419
+ **multiple mode is a toolbar of independent toggles** (`role="group"`, arrows only move, Space
420
+ toggles) — this is a real ARIA distinction, not cosmetic.
421
+
422
+ ```html
423
+ <gog-button-toggle-group [options]="alignments" [(value)]="align" />
424
+ <gog-button-toggle-group [options]="tools" [multiple]="true" [(value)]="activeTools" />
425
+ ```
426
+
427
+ ### Form fields
428
+
429
+ #### `gog-inputfield`
430
+
431
+ | Input | Type | Default | Notes |
432
+ | ----------------------------------------- | ------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------ |
433
+ | `label`, `placeholder` | `string` | `''` | |
434
+ | `type` | `GogInputType` | `'text'` | `text`/`password`/`email`/`number`/`search`/`tel`/`url`/`date`/`time`/`datetime-local` |
435
+ | `readonly` | `boolean` | `false` | value stays focusable and submitted, edits blocked; hides the clear button and stepper |
436
+ | `maxlength`, `minlength` | `number \| null` | `null` | native attributes |
437
+ | `pattern` | `string` | `''` | native attribute, regex source |
438
+ | `inputMode` | `GogInputMode \| null` | `null` | on-screen keyboard hint (`numeric`, `tel`, …) |
439
+ | `spellcheck` | `boolean \| null` | `null` | unset = browser default |
440
+ | `inputId` | `string` | `''` → generated | a real id is always rendered; pass one only to reference the field externally |
441
+ | `min`, `max`, `step` | `number \| null` | `null` | `type="number"` only |
442
+ | `showSpinButtons` | `boolean \| undefined` | `true` | own +/- glyphs on `type="number"`; via `GOG_CONFIG.inputfield.showSpinButtons` |
443
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` | see conventions |
444
+ | `disabled`, `size`, `fullWidth` | | `false`, `'md'`, `true` | |
445
+ | `iconStart` / `iconEnd` | `GogIconName \| ''` | `''` | bare leading/trailing icon |
446
+ | `clearable`, `clearAriaLabel` | | `false`, `'Clear'` | on `type="number"` the clear button renders alongside the stepper |
447
+ | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
448
+ | `showPasswordLabel` / `hidePasswordLabel` | `string \| undefined` | `'Show password'`/`'Hide password'` | `type="password"` reveal toggle aria-labels; via `GOG_CONFIG.labels` |
449
+ | `incrementLabel` / `decrementLabel` | `string \| undefined` | `'Increment'`/`'Decrement'` | spin button aria-labels; via `GOG_CONFIG.labels` |
450
+
451
+ Model: `value: string` (always a string, even for `type="number"` — the _form control_ value is
452
+ `number | null`, but the `[(value)]` model mirrors the raw text). CVA: yes.
453
+
454
+ Slots: project `<span gogInputAddonStart>`/`<span gogInputAddonEnd>` (or a `<button>`) for
455
+ custom leading/trailing markup — a normal DOM element with its own `aria-label`, click handler
456
+ and disabled state, not a component-managed slot. This is the **current, non-deprecated**
457
+ replacement for the old icon-template/icon-fn/icon-label input quartet — see
458
+ [Deprecated patterns](#deprecated-patterns--do-not-use-in-new-code).
459
+
460
+ ```html
461
+ <gog-inputfield
462
+ label="Email"
463
+ type="email"
464
+ formControlName="email"
465
+ errorDisplay="auto"
466
+ errorMessage="Enter a valid email"
467
+ [clearable]="true"
468
+ />
469
+
470
+ <gog-inputfield label="Amount" [fullWidth]="false">
471
+ <span gogInputAddonStart>€</span>
472
+ </gog-inputfield>
473
+ ```
474
+
475
+ #### `gog-textarea`
476
+
477
+ | Input | Type | Default |
478
+ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ---------------------------------------------- |
479
+ | `label`, `placeholder` | `string` | `''` |
480
+ | `rows` | `number` | `4` |
481
+ | `readonly` | `boolean` | `false` |
482
+ | `maxlength`, `minlength` | `number \| null` | `null` |
483
+ | `spellcheck` | `boolean \| null` | `null` |
484
+ | `inputId` | `string` | `''` → generated, same as inputfield |
485
+ | `resize` | `GogTextareaResize \| undefined` (`'vertical'\|'horizontal'\|'both'\|'none'`) | `'vertical'`; via `GOG_CONFIG.textarea.resize` |
486
+ | `errorMessage`, `errorDisplay`, `disabled`, `size`, `fullWidth` | | same shape as inputfield |
487
+ | `clearable`, `clearAriaLabel`, `floatLabel`, `floatLabelShowPlaceholder` | | same shape as inputfield |
488
+
489
+ Model: `value: string`. CVA: yes.
490
+
491
+ ```html
492
+ <gog-textarea label="Notes" formControlName="notes" [rows]="6" resize="vertical" />
493
+ ```
494
+
495
+ #### `gog-select`
496
+
497
+ Extends the shared listbox behaviour (`GogDropdownBase`) that also backs `gog-multiselect` and
498
+ partly `gog-autocomplete` — placement, the append-to-body overlay, click-outside, keyboard nav,
499
+ and CVA all come from there. Full shared input surface (documented once, applies to both select
500
+ and multiselect unless noted otherwise):
501
+
502
+ | Input | Type | Default | Notes |
503
+ | ------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
504
+ | `label`, `ariaLabel`, `placeholder` | `string` | `''`, `''`, `'Select...'` | |
505
+ | `options` | `TOption[]` | `[]` | your own objects |
506
+ | `optionLabel` | accessor | `'name'` | path or fn |
507
+ | `optionValue` | accessor \| `null` | `'id'` | `null` = emit the option object |
508
+ | `optionDisabled` | accessor | `'disabled'` | |
509
+ | `clearable`, `clearAriaLabel` | | `false` (select) / `true` (multiselect), `'Clear selection'` | |
510
+ | `minWidth` | `string \| null` | `null` | only with `[fullWidth]="false"` |
511
+ | `filter` | `boolean \| undefined` | `false` | search box in the panel; via `GOG_CONFIG.dropdown.filter` |
512
+ | `filterPlaceholder`, `filterEmptyMessage` | `string` | `'Search...'`, `'No matches'` | |
513
+ | `filterPosition` | `'top' \| 'bottom' \| undefined` | `'top'` | via `GOG_CONFIG.dropdown.filterPosition` |
514
+ | `filterMatch` | `((option, query) => boolean) \| null` | `null` | custom matcher, else case-insensitive substring on the resolved label |
515
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` | |
516
+ | `size` | `GogSize \| undefined` | `'md'` | |
517
+ | `dropdownDirection` | `'auto' \| 'up' \| 'down' \| undefined` | `'auto'` | |
518
+ | `dropdownZIndex`, `dropdownWidth`, `dropdownMaxHeight` | | `null` | only meaningful with `appendToBody` |
519
+ | `appendToBody` | `boolean \| undefined` | `false` | renders the panel into `<body>` — needed inside a scroll/overflow-clipped container |
520
+ | `disabled`, `fullWidth` | | `false`, `true` | |
521
+ | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
522
+ | `inputId` (select/autocomplete only) | `string` | `''` | |
523
+
524
+ `gog-select`-specific: `value: model<TValue>(null)`.
525
+ `gog-multiselect`-specific additions: `value: model<TValue[]>([])`, `showControls: boolean` (default `false`, a select-all/clear row), `controlsPosition: 'top'|'bottom'` (default `'top'`), and `selectAllLabel`/`clearAllLabel` for that row's two buttons (`'Select all'`/`'Clear'`, also via `GOG_CONFIG.labels`).
526
+
527
+ CVA: yes, both. Slots (shared): `<ng-template gogDropdownChevron>` (custom chevron markup),
528
+ `<ng-template gogDropdownOption let-opt let-selected="selected" let-label="label">` (custom
529
+ option row). Multiselect adds `<ng-template gogMultiselectClearIcon>`.
530
+
531
+ ```html
532
+ <gog-select
533
+ label="Region"
534
+ [options]="regions"
535
+ optionLabel="title"
536
+ [(value)]="regionId"
537
+ [filter]="true"
538
+ />
539
+
540
+ <gog-multiselect
541
+ label="Tags"
542
+ [options]="tags"
543
+ [(value)]="selectedTagIds"
544
+ [showControls]="true"
545
+ formControlName="tags"
546
+ errorDisplay="auto"
547
+ />
548
+ ```
549
+
550
+ #### `gog-autocomplete`
551
+
552
+ Shares `GogDropdownBase` too, but the trigger is a real `<input>` (combobox pattern,
553
+ `aria-activedescendant`), not a listbox button — so it does **not** reuse the base's built-in
554
+ panel-filter box; it filters/searches off what's typed in the field itself.
555
+
556
+ | Input | Type | Default | Notes |
557
+ | ------------------------------------------------------------------------------------------------------ | ---------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
558
+ | _(all the shared `GogDropdownBase` inputs above except `filter`/`filterPlaceholder`/`filterPosition`)_ | | | |
559
+ | `filterLocal` | `boolean` | `true` | narrow `options` client-side as you type; turn **off** when `gogSearch` already returns a filtered server list (avoids double-filtering) |
560
+ | `minLength` | `number \| undefined` | `1` | via `GOG_CONFIG.autocomplete.minLength` |
561
+ | `openOnFocus` | `boolean \| undefined` | `true` | via `GOG_CONFIG.autocomplete.openOnFocus` |
562
+ | `searchDebounce` | `number \| undefined` | `300` | ms before `gogSearch` fires; via `GOG_CONFIG.autocomplete.searchDebounce` |
563
+ | `loading` | `boolean` | `false` | shows a spinner in the trailing slot |
564
+ | `emptyMessage` | `string` | `'No matches'` | |
565
+ | `forceSelection` | `boolean` | `true` | see note below |
566
+
567
+ Outputs: `gogSearch: string` (debounced query — wire your server lookup here),
568
+ `gogLoadMore: void` (panel scrolled to the end — fetch the next page).
569
+
570
+ Model: `value: TValue | null`. CVA: yes.
571
+
572
+ **`forceSelection` matters.** On (default): the field always ends up reflecting a real
573
+ selection — free-typed text that matches nothing snaps back on blur/Escape. Off: what the user
574
+ typed is itself meaningful (a create-as-you-type flow) — read the typed text from `gogSearch`,
575
+ not from `value`, since `value` clears the moment the text stops matching the selection.
576
+
577
+ ```html
578
+ <gog-autocomplete
579
+ [options]="users"
580
+ optionLabel="profile.fullName"
581
+ [optionValue]="null"
582
+ [(value)]="user"
583
+ [loading]="searching()"
584
+ (gogSearch)="search($event)"
585
+ />
586
+ ```
587
+
588
+ #### `gog-checkbox`
589
+
590
+ | Input | Type | Default |
591
+ | ---------------------------------------- | ---------------------- | ------- |
592
+ | `label`, `ariaLabel` | `string` | `''` |
593
+ | `size` | `GogSize \| undefined` | `'md'` |
594
+ | `indeterminate`, `disabled`, `fullWidth` | `boolean` | `false` |
595
+
596
+ Model: `checked: boolean`. CVA: yes. Slot: `<ng-template gogCheckboxIcon>` for a custom tick
597
+ icon (replaces the deprecated `checkIconTemplate` input).
598
+
599
+ ```html
600
+ <gog-checkbox label="I agree to the terms" formControlName="agree" />
601
+ ```
602
+
603
+ #### `gog-toggle`
604
+
605
+ An on/off switch (`role="switch"`) — semantically different from a checkbox ("is this setting
606
+ on", not "is this one of the things you selected").
607
+
608
+ | Input | Type | Default |
609
+ | ----------------------- | ---------------------- | ------- |
610
+ | `label`, `ariaLabel` | `string` | `''` |
611
+ | `size` | `GogSize \| undefined` | `'md'` |
612
+ | `disabled`, `fullWidth` | `boolean` | `false` |
613
+ | `labelPosition` | `'start' \| 'end'` | `'end'` |
614
+ | `onLabel`, `offLabel` | `string` | `''` | text rendered inside the track itself |
615
+
616
+ Model: `checked: boolean`. CVA: yes.
617
+
618
+ ```html
619
+ <gog-toggle label="Notifications" formControlName="notificationsOn" onLabel="ON" offLabel="OFF" />
620
+ ```
621
+
622
+ #### `gog-radio-group`
623
+
624
+ | Input | Type | Default |
625
+ | ------------------------------ | ----------------------------------------------- | ---------------- |
626
+ | `options` | `GogRadioOption[]` (`{ id, label, disabled? }`) | `[]` |
627
+ | `label`, `ariaLabel`, `name` | `string` | `''` |
628
+ | `size` | `GogSize \| undefined` | `'md'` |
629
+ | `disabled`, `fullWidth` | `boolean` | `false` |
630
+ | `orientation` | `GogOrientation` | `'vertical'` |
631
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` |
632
+
633
+ Model: `value: string | number | null`. CVA: yes. Fixed `{ id, label, disabled? }` shape (not
634
+ a generic accessor, unlike select/multiselect/button-toggle).
635
+
636
+ ```html
637
+ <gog-radio-group
638
+ [options]="[{id:'m',label:'Male'},{id:'f',label:'Female'}]"
639
+ formControlName="gender"
640
+ />
641
+ ```
642
+
643
+ #### `gog-slider`
644
+
645
+ | Input | Type | Default |
646
+ | ------------------------------ | ---------------------- | ---------------------------------------------- |
647
+ | `label`, `ariaLabel` | `string` | `''` |
648
+ | `min`, `max`, `step` | `number` | `0`, `100`, `1` |
649
+ | `showValue`, `showThumb` | `boolean` | `true` |
650
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` |
651
+ | `disabled` | `boolean` | `false` |
652
+ | `fullWidth` | `boolean` | `true` (ignored when `orientation="vertical"`) |
653
+ | `orientation` | `GogSliderOrientation` | `'horizontal'` |
654
+
655
+ Model: `value: number`. CVA: yes. Backed by a real `<input type="range">` (rotated via
656
+ `writing-mode` for vertical), so dragging/touch/keyboard all come from the platform.
657
+
658
+ ```html
659
+ <gog-slider label="Volume" [min]="0" [max]="100" formControlName="volume" />
660
+ ```
661
+
662
+ #### `gog-datepicker` / `gog-calendar`
663
+
664
+ `gog-datepicker` is a field + panel; `gog-calendar` is the month grid alone (what `inline` mode
665
+ renders). Native `Date` only — no date library, no adapter.
666
+
667
+ | Input | Type | Default | Notes |
668
+ | ----------------------------------------------------- | -------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------- |
669
+ | `inputId`, `label`, `ariaLabel`, `placeholder` | `string` | `''` | |
670
+ | `selectionMode` | `GogDateSelectionMode` (`'single'\|'range'`) | `'single'` | |
671
+ | `min`, `max` | `Date \| null` | `null` | |
672
+ | `disabledDates` | `((date: Date) => boolean) \| null` | `null` | predicate, not a list |
673
+ | `defaultMonth` | `Date \| null` | `null` | which month opens when nothing is selected |
674
+ | `numberOfMonths` | `number` | `1` | `2` is what makes a range picker usable |
675
+ | `showTime`, `hourFormat`, `minuteStep`, `showSeconds` | | `false`, `'24'`, `1`, `false` | |
676
+ | `showTodayButton` | `boolean` | `true` | **selects** today |
677
+ | `showThisMonthButton` | `boolean` | `false` | only moves the _view_, leaves selection alone |
678
+ | `format` | `string \| null` | `null` | display/parse pattern (`'dd.MM.yyyy'`); derived from `showTime` when unset |
679
+ | `locale` | `string \| undefined` | `'en-US'` | via `GOG_CONFIG.datepicker.locale` |
680
+ | `firstDayOfWeek` | `number \| undefined` | locale's own | via `GOG_CONFIG.datepicker.firstDayOfWeek` |
681
+ | `allowTextInput` | `boolean` | `true` | typed text parsed against `format`; unparseable drafts don't clear the value |
682
+ | `inline` | `boolean` | `false` | renders the calendar with no field/panel |
683
+ | `disabled`, `fullWidth` | | `false`, `true` | |
684
+ | `clearable`, `clearAriaLabel` | | `false`, `'Clear date'` | |
685
+ | `errorMessage`, `errorDisplay`, `size` | | `''`, `'manual'`, `'md'` | |
686
+ | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
687
+ | `appendToBody`, `dropdownDirection`, `dropdownZIndex` | | `false`, `'auto'`, `null` | |
688
+
689
+ Model: `value: Date | GogDateRange | null` (`GogDateRange = { start: Date | null; end: Date | null }`).
690
+ CVA: yes.
691
+
692
+ `gog-calendar` (usable standalone) takes most of the same date/range/time inputs directly, plus
693
+ `gogDateSelect: output<GogDatepickerValue>()` fired only on a _complete_ selection. It resolves
694
+ `locale` and `firstDayOfWeek` from `GOG_CONFIG.datepicker` itself, so a standalone calendar
695
+ honours an app-wide locale without being handed one; its navigation, shortcut and time labels
696
+ (`todayLabel`, `thisMonthLabel`, `previousMonthLabel`, `nextMonthLabel`, `previousYearLabel`,
697
+ `nextYearLabel`, `hoursLabel`, `minutesLabel`, `secondsLabel`) resolve through
698
+ `GOG_CONFIG.labels` the same way.
699
+
700
+ Also exported for direct reuse: `formatDate(date, pattern)`, `parseDate(text, pattern)`, and a
701
+ family of date-math helpers (`addDays`, `addMonths`, `isSameDay`, `isWithinBounds`, …) from
702
+ `date-utils`.
703
+
704
+ ```html
705
+ <gog-datepicker label="Birth date" [(value)]="birthDate" [max]="today" />
706
+ <gog-datepicker selectionMode="range" [(value)]="stayRange" [numberOfMonths]="2" />
707
+ ```
708
+
709
+ ### Display, feedback & status
710
+
711
+ #### `gog-icon`
712
+
713
+ | Input | Type | Default |
714
+ | ------------ | --------------------- | -------------------------------------------------- |
715
+ | `name` | `GogIconName` | `'close'` |
716
+ | `template` | `TemplateRef \| null` | `null` — custom markup instead of the built-in SVG |
717
+ | `title` | `string` | `''` |
718
+ | `ariaHidden` | `boolean` | `true` |
719
+
720
+ `GogIconName` is a **closed set of exactly 20 names**: `check`, `close`, `chevron-up`,
721
+ `chevron-down`, `chevron-left`, `chevron-right`, `calendar`, `clock`, `sort`, `sort-up`,
722
+ `sort-down`, `success`, `error`, `warning`, `info`, `checkbox`, `checkbox-checked`, `eye`,
723
+ `eye-off`, `copy`. There is no way to pass an arbitrary SVG/URL as `name` — use the `template`
724
+ input (or project your own `<svg>`/icon component elsewhere) for anything outside this set.
725
+
726
+ ```html
727
+ <gog-icon name="calendar" />
728
+ ```
729
+
730
+ #### `[gogBadge]` — directive, not a component
731
+
732
+ Decorates an existing element (a button, an icon, an avatar) with a count/status dot — it never
733
+ wraps its host.
734
+
735
+ | Input | Type | Default |
736
+ | ---------------- | --------------------------------------------------------------------------- | -------------------------------- |
737
+ | `gogBadge` | `string \| number \| null` | `null` — the content |
738
+ | `badgePosition` | `GogBadgePosition` (`'top-end'\|'top-start'\|'bottom-end'\|'bottom-start'`) | `'top-end'` |
739
+ | `badgeVariant` | `GogTagVariant` | `'danger'` |
740
+ | `badgeDot` | `boolean` | `false` — bare dot, no text |
741
+ | `badgeMax` | `number` | `99` — beyond this, renders `N+` |
742
+ | `badgeHidden` | `boolean` | `false` |
743
+ | `badgeAriaLabel` | `string` | `''` |
744
+
745
+ Renders **nothing** when the value is `0`, `null` or empty and `badgeDot` is off — "0" badges
746
+ are impossible by design.
747
+
748
+ ```html
749
+ <gog-button gogBadge="12" badgeAriaLabel="12 unread">Inbox</gog-button>
750
+ <gog-icon name="info" gogBadge badgeDot />
751
+ ```
752
+
753
+ #### `gog-chip`
754
+
755
+ | Input | Type | Default |
756
+ | ------------------------------ | ----------------------------------- | --------------------- |
757
+ | `size` | `GogSize` | `'md'` |
758
+ | `shape` | `GogTagShape` (`'rounded'\|'pill'`) | `'rounded'` |
759
+ | `disabled`, `clickable` | `boolean` | `false`, `true` |
760
+ | `removable` | `boolean` | `false` |
761
+ | `fullWidth` | `boolean` | `false` |
762
+ | `ariaLabel`, `removeAriaLabel` | `string` | `''`, `'Remove chip'` |
763
+ | `avatarUrl`, `avatarAlt` | `string \| null` / `string` | `null`, `''` |
764
+ | `iconName` | `GogIconName \| null` | `null` |
765
+
766
+ Outputs: `gogClick: MouseEvent | KeyboardEvent`, `gogRemove: void`.
767
+
768
+ ```html
769
+ <gog-chip [avatarUrl]="user.photo" [removable]="true" (gogRemove)="removeUser(user)"
770
+ >{{ user.name }}</gog-chip
771
+ >
772
+ ```
773
+
774
+ #### `gog-tag`
775
+
776
+ | Input | Type | Default |
777
+ | ----------- | --------------------- | ----------- |
778
+ | `variant` | `GogTagVariant` | `'info'` |
779
+ | `size` | `GogSize` | `'md'` |
780
+ | `shape` | `GogTagShape` | `'rounded'` |
781
+ | `iconName` | `GogIconName \| null` | `null` |
782
+ | `fullWidth` | `boolean` | `false` |
783
+
784
+ Slot: `<ng-template gogTagIcon>` for custom icon markup (replaces the deprecated `iconTemplate`).
785
+
786
+ ```html
787
+ <gog-tag variant="success">Active</gog-tag>
788
+ ```
789
+
790
+ #### `gog-spinner` / `gog-spinner-overlay`
791
+
792
+ | Input | Type | Default |
793
+ | -------------------------------- | ------------------------------------------------- | ------------------------------------------- |
794
+ | `size` | `GogSize` | `'md'` |
795
+ | `variant` | `GogSpinnerVariant` (`'runic'\|'ring'\|'custom'`) | `'runic'` |
796
+ | `ariaLabel` | `string` | `'Loading'` |
797
+ | `overlay` (spinner only) | `boolean` | `false` |
798
+ | `loading` (spinner-overlay only) | `boolean` | `false` — toggles the overlay + `aria-busy` |
799
+
800
+ `variant="custom"` renders your own projected markup, still inheriting the size wrapper and
801
+ `--gog-spinner-color` theming.
802
+
803
+ ```html
804
+ <gog-spinner-overlay [loading]="isLoading()">
805
+ <app-content-that-loads />
806
+ </gog-spinner-overlay>
807
+ ```
808
+
809
+ #### `gog-skeleton`
810
+
811
+ | Input | Type | Default |
812
+ | ----------------- | -------------------------------------------------- | ---------------------------------------------------- |
813
+ | `shape` | `GogSkeletonShape` (`'text'\|'circle'\|'rect'`) | `'text'` |
814
+ | `size` | `GogSize` | `'md'` |
815
+ | `animation` | `GogSkeletonAnimation` (`'pulse'\|'wave'\|'none'`) | `'pulse'` |
816
+ | `width`, `height` | `string \| null` | `null` |
817
+ | `lines` | `number` | `1` — `shape="text"` only, last line renders shorter |
818
+ | `rounded` | `boolean` | `true` |
819
+ | `ariaLabel` | `string \| null` | `null` — decorative (no `role`) unless set |
820
+
821
+ ```html
822
+ <gog-skeleton shape="text" [lines]="3" /> <gog-skeleton shape="circle" width="48px" />
823
+ ```
824
+
825
+ #### `gog-progressbar`
826
+
827
+ | Input | Type | Default |
828
+ | ----------------- | ---------------------------------------------------------------------------- | --------------- |
829
+ | `value`, `buffer` | `number` (0–100, clamped) | `0` |
830
+ | `mode` | `GogProgressbarMode` (`'determinate'\|'indeterminate'\|'buffer'`) | `'determinate'` |
831
+ | `variant` | `GogProgressbarVariant` (`'accent'\|'success'\|'danger'\|'warning'\|'info'`) | `'accent'` |
832
+ | `size` | `GogSize` | `'md'` |
833
+ | `showValue` | `boolean` | `false` |
834
+ | `ariaLabel` | `string` | `''` |
835
+
836
+ ```html
837
+ <gog-progressbar mode="indeterminate" ariaLabel="Loading" />
838
+ <gog-progressbar mode="buffer" [value]="42" [buffer]="70" />
839
+ ```
840
+
841
+ #### `gog-divider`
842
+
843
+ | Input | Type | Default |
844
+ | ------------- | --------------------------------------------------- | -------------- |
845
+ | `orientation` | `GogOrientation` | `'horizontal'` |
846
+ | `variant` | `GogDividerVariant` (`'solid'\|'dashed'\|'dotted'`) | `'solid'` |
847
+ | `inset` | `boolean` | `false` |
848
+
849
+ Label is projected content, not an input — put an icon or a `gog-tag` inside it if needed.
850
+
851
+ ```html
852
+ <gog-divider>OR</gog-divider>
853
+ ```
854
+
855
+ #### `gogTooltip` — directive, not a component
856
+
857
+ Drop on any element — a `gog-*` component's host tag or a plain native one.
858
+
859
+ | Input | Type | Default |
860
+ | --------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ |
861
+ | `gogTooltip` | `string \| TemplateRef \| null` | `null` — content |
862
+ | `gogTooltipPosition` | `GogTooltipPosition` (`'auto'\|'top'\|'bottom'\|'left'\|'right'`) | `'auto'`; via `GOG_CONFIG.tooltip.position` |
863
+ | `gogTooltipShowDelay` | `number \| undefined` | `300`; via `GOG_CONFIG.tooltip.showDelay` |
864
+ | `gogTooltipHideDelay` | `number \| undefined` | `100`; via `GOG_CONFIG.tooltip.hideDelay` |
865
+ | `gogTooltipDisabled` | `boolean` | `false` |
866
+ | `gogTooltipClass` | `string` | `''` — class on the bubble itself, since it's portaled to `<body>` |
867
+
868
+ ```html
869
+ <button gogTooltip="Save changes">💾</button> <gog-chip [gogTooltip]="hintTemplate">Beta</gog-chip>
870
+ ```
871
+
872
+ ### Layout & navigation
873
+
874
+ #### `gog-accordion`
875
+
876
+ | Input | Type | Default |
877
+ | --------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
878
+ | `items` | `GogAccordionItem[]` (`{ id, title, disabled?, [key: string]: unknown }`) | `[]` |
879
+ | `size` | `GogSize` | `'lg'` (not `'md'` — see conventions) |
880
+ | `expandFirst`, `multi`, `loading` | `boolean` | `false` |
881
+ | `skeletonCount` | `number` | `3` — rows shown while `loading` and `items` is still empty |
882
+ | `showChevron` | `boolean` | `true` |
883
+ | `headingLevel` | `2\|3\|4\|5\|6 \| undefined` | `undefined` — wraps headers in `role="heading"` when set |
884
+
885
+ Model: `openIds: ReadonlySet<string | number>`. Output: `gogToggle: { item, open }`.
886
+
887
+ Slots: `<ng-template gogAccordionHeader let-item let-open="open">`,
888
+ `<ng-template gogAccordionContent let-item>`, `<ng-template gogAccordionChevron let-item let-open="open">`.
889
+ This is the library's canonical example of the slot pattern — copy its shape for anything similar.
890
+
891
+ ```html
892
+ <gog-accordion [items]="faqItems" [multi]="true">
893
+ <ng-template gogAccordionContent let-item>{{ item.answer }}</ng-template>
894
+ </gog-accordion>
895
+ ```
896
+
897
+ #### `gog-collapsible` + `gogCollapsibleTrigger` / `gogCollapsibleContent`
898
+
899
+ **Headless primitive** — owns no markup at all, just open/close state plus two attribute
900
+ directives you place on your own elements. Use this when `gog-accordion`'s opinionated markup
901
+ doesn't fit (e.g. a sidebar nav group).
902
+
903
+ | Input (on `gog-collapsible`) | Type | Default |
904
+ | ---------------------------- | --------- | ---------------------------------------------------------- |
905
+ | `disabled` | `boolean` | `false` |
906
+ | `collapseOnFocusOut` | `boolean` | `false` — close once focus leaves both trigger and content |
907
+
908
+ Model: `open: boolean`.
909
+
910
+ ```html
911
+ <gog-collapsible [(open)]="isOpen">
912
+ <button gogCollapsibleTrigger>Advanced options</button>
913
+ <div gogCollapsibleContent>
914
+ <!-- any markup -->
915
+ </div>
916
+ </gog-collapsible>
917
+ ```
918
+
919
+ #### `gog-tabs` + `gog-tab`
920
+
921
+ | Input (on `gog-tabs`) | Type | Default |
922
+ | ------------------------ | ------------------------------------------------------ | ---------------------------------------------------- |
923
+ | `align` | `GogTabsAlign` (`'start'\|'center'\|'end'\|'stretch'`) | `'start'` |
924
+ | `orientation` | `GogOrientation` | `'horizontal'` |
925
+ | `size` | `GogSize` | `'md'` |
926
+ | `fullWidth`, `ariaLabel` | | `false`, `''` |
927
+ | `scrollActiveIntoView` | `boolean` | `true` |
928
+ | `showScrollTrack` | `boolean \| undefined` | follows `scrollActiveIntoView` (hidden when it's on) |
929
+
930
+ Model: `activeIndex: number`. Output: `gogTabChange: number`.
931
+
932
+ | Input (on `gog-tab`) | Type | Default |
933
+ | -------------------- | --------------------- | ------- |
934
+ | `label` | `string` | `''` |
935
+ | `iconName` | `GogIconName \| null` | `null` |
936
+ | `disabled` | `boolean` | `false` |
937
+
938
+ Slots: `<ng-template gogTabHeader let-tab let-active="active">` on `gog-tabs` for custom header
939
+ markup; `<ng-template gogTabContent>` **inside** a `gog-tab` to make that tab's content **lazy**
940
+ (built on first activation, then kept alive) instead of the default (rendered immediately,
941
+ hidden via `[hidden]` while inactive — preserves scroll/input state).
942
+
943
+ ```html
944
+ <gog-tabs [(activeIndex)]="tabIndex">
945
+ <gog-tab label="Profile"><app-profile /></gog-tab>
946
+ <gog-tab label="Report" iconName="info">
947
+ <ng-template gogTabContent><app-expensive-report /></ng-template>
948
+ </gog-tab>
949
+ </gog-tabs>
950
+ ```
951
+
952
+ #### `gog-paginator`
953
+
954
+ | Input | Type | Default |
955
+ | ------------------------------- | ------------------------------------------------ | ------------------------------ |
956
+ | `fullWidth`, `totalPages` | `boolean`, `number` | `true`, `1` |
957
+ | `rangeMode` | `GogPaginatorRangeMode` (`'window'\|'ellipsis'`) | `'window'` — see note |
958
+ | `visiblePages` | `number` | `5` — `'window'` mode only |
959
+ | `showFirstPage`, `showLastPage` | `boolean` | `false` — `'window'` mode only |
960
+ | `siblingCount` | `number` | `2` — `'ellipsis'` mode only |
961
+ | `size` | `GogSize` | `'sm'` |
962
+ | `disabled`, `ariaLabel` | | `false`, `'Pagination'` |
963
+
964
+ The step buttons (`'Previous page'`/`'Next page'`) and the per-page names are configured, not
965
+ input-driven: `GOG_CONFIG.labels.previousPage`/`nextPage`, and `labels.page`, a
966
+ `(page: number, isCurrent: boolean) => string` formatter defaulting to
967
+ `` `Page ${page}, current page` `` / `` `Go to page ${page}` ``.
968
+
969
+ Model: `page: number` (1-based, self-clamps to `totalPages`).
970
+
971
+ `'window'`: a fixed number of page buttons that slides to keep the current page centered.
972
+ `'ellipsis'`: first/last pinned, `siblingCount` around the current page, "…" fills the gap
973
+ (what `gog-table`'s built-in pagination uses).
974
+
975
+ ```html
976
+ <gog-paginator [(page)]="page" [totalPages]="totalPages" />
977
+ ```
978
+
979
+ #### `gog-table<T>`
980
+
981
+ | Input | Type | Default |
982
+ | ----------------------------- | ----------------------------- | --------------------------------- |
983
+ | `value` | `T[]` | `[]` |
984
+ | `fullWidth` | `boolean` | `true` |
985
+ | `pageSize` | `number` | `0` (no pagination) |
986
+ | `showRowNumbers`, `showTotal` | `boolean` | `true`, `false` |
987
+ | `emptyPlaceholder` | `string` | `'-'` |
988
+ | `paginatorPosition` | `'left'\|'center'\|'right'` | `'center'` |
989
+ | `totalPosition` | `'left'\|'right'\|'opposite'` | `'opposite'` |
990
+ | `loading` | `boolean` | `false` |
991
+ | `showColumnBorders` | `boolean` | `false` |
992
+ | `stickyHeader` | `boolean` | `false` |
993
+ | `size` | `GogSize` | `'lg'` (row density — not `'md'`) |
994
+
995
+ Columns are declared as **projected `gog-column` children**, not an input array:
996
+
997
+ ```html
998
+ <gog-table [value]="rows">
999
+ <gog-column field="name" header="Name" sortable="true" />
1000
+ <gog-column field="email" header="Email" />
1001
+ <gog-column field="status" header="Status">
1002
+ <ng-template gogColumnBody let-row let-value="value">
1003
+ <gog-tag [variant]="row.active ? 'success' : 'danger'">{{ value }}</gog-tag>
1004
+ </ng-template>
1005
+ </gog-column>
1006
+ </gog-table>
1007
+ ```
1008
+
1009
+ `gog-column` inputs: `field` (required, dot-paths ok), `header`, `sortable` (default `false`),
1010
+ `width`/`minWidth`/`maxWidth`, `comparator` (custom `(a, b) => number`, defaults to a
1011
+ locale-aware collator for strings). Slots inside a column: `<ng-template gogColumnBody let-row let-value="value" let-index="index">`,
1012
+ `<ng-template gogColumnHeader let-header let-field="field">`.
1013
+
1014
+ **Sorting, empty/loading states and pagination are all built in** — sortable columns toggle
1015
+ asc → desc → unsorted on click, `loading` shows a spinner in place of rows, an empty `value`
1016
+ shows `emptyPlaceholder`, and `pageSize > 0` turns on the internal paginator automatically. You
1017
+ don't need to hand-roll any of this.
1018
+
1019
+ There is **no typed row-selection API** in the current version — if you need it, track
1020
+ selection yourself (e.g. a `Set` keyed by row id) and render a `gogColumnBody` checkbox column.
1021
+
1022
+ #### `gog-scroll`
1023
+
1024
+ Drop-in replacement for `overflow: auto` — content still scrolls natively (wheel, touch,
1025
+ keyboard); only the browser's own scrollbar chrome is replaced with a themeable overlay thumb.
1026
+ Used internally by several other components (`gog-dialog`'s body, `gog-select`'s panel,
1027
+ `gog-tabs`' header row) and equally usable directly in your own markup for any scrollable
1028
+ region — the library's official recommendation over a raw `overflow-x`/`overflow-y`.
1029
+
1030
+ | Input | Type | Default |
1031
+ | -------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
1032
+ | `axis` | `GogScrollAxis` (`'vertical'\|'horizontal'\|'both'`) | `'vertical'` |
1033
+ | `size` | `GogScrollSize \| undefined` (`'normal'\|'thin'`) | `'normal'`; via `GOG_CONFIG.scroll.size` |
1034
+ | `autoHide` | `boolean \| undefined` | `true`; via `GOG_CONFIG.scroll.autoHide` |
1035
+ | `hideDelay` | `number \| undefined` | `800`; via `GOG_CONFIG.scroll.hideDelay` |
1036
+ | `reachThreshold` | `number` | `0` |
1037
+ | `focusable` | `boolean` | `true` — turn off when the parent already owns focus (a dialog with its own focus trap) |
1038
+ | `ariaLabel` | `string` | `''` |
1039
+ | `overscrollBehavior` | `GogScrollOverscrollBehavior \| undefined` (`'auto'\|'contain'\|'none'`) | `'auto'`; via `GOG_CONFIG.scroll.overscrollBehavior` |
1040
+ | `showTrack` | `boolean \| undefined` | `true`; via `GOG_CONFIG.scroll.showTrack` |
1041
+
1042
+ Outputs: `gogScroll: GogScrollMetrics`, `gogReachStart`/`gogReachEnd: 'vertical'|'horizontal'`.
1043
+ Methods (via template ref): `scrollTo(options)`, `scrollToTop()`, `scrollToBottom()`,
1044
+ `scrollToLeft()`, `scrollToRight()`.
1045
+
1046
+ ```html
1047
+ <gog-scroll size="thin" [focusable]="false" overscrollBehavior="contain" style="max-height: 320px">
1048
+ <!-- content that might overflow -->
1049
+ </gog-scroll>
1050
+ ```
1051
+
1052
+ ### Overlays
1053
+
1054
+ #### `gog-dialog`
1055
+
1056
+ A **single** `<gog-dialog />` renders **every** dialog `DialogService.open(...)` creates —
1057
+ place it once, typically in your root app component's template, not per-page and not per-dialog
1058
+ call:
1059
+
1060
+ ```html
1061
+ <!-- app.html -->
1062
+ <router-outlet />
1063
+ <gog-dialog />
1064
+ ```
1065
+
1066
+ It has no inputs of its own — everything is driven through `DialogService` (see
1067
+ [Services](#services) above). Supports nesting, dragging (when `draggable !== false` and the
1068
+ dialog has a title or close button), a focus trap for modal dialogs, `Escape` to close (when
1069
+ `closable !== false`), and click-outside-to-close on the backdrop.
1070
+
1071
+ #### `gog-toast` / `gog-toast-container`
1072
+
1073
+ Same pattern — place **one** `<gog-toast-container />`, typically in the root component:
1074
+
1075
+ ```html
1076
+ <gog-toast-container [maxVisiblePerPosition]="5" />
1077
+ ```
1078
+
1079
+ `maxVisiblePerPosition` (default `5`) caps how many toasts stack at once per corner; the rest
1080
+ queue. Individual `gog-toast` instances are rendered internally by the container from
1081
+ `ToastService.toasts()` — you don't place these yourself. Toasts auto-dismiss after their
1082
+ `duration` unless `isSticky`; hovering pauses the countdown (front-of-stack toast only).
1083
+
1084
+ Announcements come from two permanently-mounted, visually-hidden live regions the container
1085
+ owns — polite, and assertive for `error`/`warning`. The toasts themselves carry no
1086
+ `role`/`aria-live`: a live region created in the same tick as its text is routinely skipped by
1087
+ screen readers, and a second region would announce everything twice. Don't add either back.
1088
+
1089
+ ---
1090
+
1091
+ ## Deprecated patterns — do not use in new code
1092
+
1093
+ These still work (nothing breaks if you use them), but are marked `@deprecated` and **will be
1094
+ removed** on the stated schedule. Don't generate new code using any of them — use the listed
1095
+ replacement instead.
1096
+
1097
+ | Deprecated | Removed in | Replacement |
1098
+ | ---------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------- |
1099
+ | `GogSelectOption`, `GogMultiselectOption` type aliases | `21.4.0` | `GogDropdownOption` |
1100
+ | `gog-select`/`gog-multiselect` `chevronTemplate` input | `21.5.0` | `<ng-template gogDropdownChevron>` |
1101
+ | `gog-checkbox` `checkIconTemplate` input | `21.5.0` | `<ng-template gogCheckboxIcon>` |
1102
+ | `gog-tag` `iconTemplate` input | `21.5.0` | `<ng-template gogTagIcon>` |
1103
+ | `gog-multiselect` `clearIconTemplate` input | `21.5.0` | `<ng-template gogMultiselectClearIcon>` |
1104
+ | `gog-inputfield` `iconStartTemplate`/`iconEndTemplate`/`iconStartFn`/`iconEndFn`/`iconStartLabel`/`iconEndLabel` | `21.5.0` | `<span gogInputAddonStart>`/`<span gogInputAddonEnd>` (or a `<button>` with its own handler) |
1105
+ | `gog-table`'s `[template]` attribute (`<ng-template template="field" type="body">`) | `21.5.0` | `<ng-template gogColumnBody>` / `<ng-template gogColumnHeader>` declared **inside** the matching `<gog-column>` |
1106
+ | `<column>` selector / `Column` export | `21.5.0` | `<gog-column>` / `GogColumn` |
1107
+
1108
+ The general rule they all follow: a `TemplateRef` **input** or a string-keyed lookup is the old
1109
+ shape; a **projected content directive with a typed context**, declared where it's used, is the
1110
+ current one. If you're about to write `fooTemplate` next to an existing `foo` input, or key
1111
+ something off a string that has to match another string elsewhere, that's this exact
1112
+ anti-pattern — reach for a slot directive instead.
1113
+
1114
+ ## Full type reference
1115
+
1116
+ Shared enum-like types (`import type { ... } from '@guildofgleks/ui'`):
1117
+
1118
+ | Type | Values |
1119
+ | ----------------------------- | --------------------------------------------------------------------------------------------- |
1120
+ | `GogSize` | `'xsm' \| 'sm' \| 'md' \| 'lg' \| 'slg'` |
1121
+ | `GogVariant` | `'primary' \| 'secondary' \| 'outline' \| 'ghost'` |
1122
+ | `GogTagVariant` | `'success' \| 'danger' \| 'warning' \| 'info'` |
1123
+ | `GogOrientation` | `'horizontal' \| 'vertical'` |
1124
+ | `GogTagShape` | `'rounded' \| 'pill'` |
1125
+ | `GogSpinnerVariant` | `'runic' \| 'ring' \| 'custom'` |
1126
+ | `GogSkeletonShape` | `'text' \| 'circle' \| 'rect'` |
1127
+ | `GogSkeletonAnimation` | `'pulse' \| 'wave' \| 'none'` |
1128
+ | `GogPaginatorRangeMode` | `'window' \| 'ellipsis'` |
1129
+ | `GogScrollAxis` | `'vertical' \| 'horizontal' \| 'both'` |
1130
+ | `GogScrollSize` | `'normal' \| 'thin'` |
1131
+ | `GogScrollOverscrollBehavior` | `'auto' \| 'contain' \| 'none'` |
1132
+ | `GogTooltipPosition` | `'auto' \| 'top' \| 'bottom' \| 'left' \| 'right'` |
1133
+ | `GogFloatLabelVariant` | `'none' \| 'in' \| 'on' \| 'over'` |
1134
+ | `GogDropdownFilterPosition` | `'top' \| 'bottom'` |
1135
+ | `GogDividerVariant` | `'solid' \| 'dashed' \| 'dotted'` |
1136
+ | `GogBadgePosition` | `'top-end' \| 'top-start' \| 'bottom-end' \| 'bottom-start'` |
1137
+ | `GogProgressbarMode` | `'determinate' \| 'indeterminate' \| 'buffer'` |
1138
+ | `GogProgressbarVariant` | `'accent' \| 'success' \| 'danger' \| 'warning' \| 'info'` |
1139
+ | `GogButtonToggleAppearance` | `'joined' \| 'separated'` |
1140
+ | `GogTabsAlign` | `'start' \| 'center' \| 'end' \| 'stretch'` |
1141
+ | `GogDateSelectionMode` | `'single' \| 'range'` |
1142
+ | `GogHourFormat` | `'12' \| '24'` |
1143
+ | `GogTextareaResize` | `'vertical' \| 'horizontal' \| 'both' \| 'none'` |
1144
+ | `GogInputType` | `'text' \| 'password' \| 'email' \| 'number' \| 'search' \| 'tel' \| 'url' \| 'date' \| 'time' \| 'datetime-local'` |
1145
+ | `GogInputMode` | `'none' \| 'text' \| 'decimal' \| 'numeric' \| 'tel' \| 'search' \| 'email' \| 'url'` |
1146
+ | `GogErrorDisplay` | `'auto' \| 'manual'` |
1147
+ | `GogDropdownDirection` | `'auto' \| 'up' \| 'down'` |
1148
+ | `GogTooltipSide` | `'top' \| 'bottom' \| 'left' \| 'right'` (resolved form of `GogTooltipPosition`, no `'auto'`) |
1149
+ | `GogIconName` | closed set of 20 — see [`gog-icon`](#gog-icon) |