@guildofgleks/ui 21.11.0 → 21.12.0

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 CHANGED
@@ -1,1964 +1,1982 @@
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.9.0`** (in progress — the
9
- released version is 21.8.0; see `CHANGELOG.md` for what 21.9.0 adds). 21.7.0 removed the three
10
- abbreviated token prefixes and 21.5.0 removed a batch of deprecated API — see **Removed in 21.7.0**
11
- and **Removed in 21.5.0** near the end of this file, which exist so code written against an older
12
- version can be migrated — and `CHANGELOG.md` has the rest. `README.md` covers the same ground at a
13
- higher level — install, setup, theming, global configuration — and is accurate; this file goes
14
- further, into per-component input tables, and is the one to trust for exact names, types and
15
- defaults.
16
-
17
- > **Maintainers:** this file ships inside the npm package and is the API reference an agent reads
18
- > while writing code against it, so a stale table here becomes wrong code in someone else's app —
19
- > silently, because nothing fails a build. **Any change to an input, output, slot, type, service
20
- > method or default updates this file in the same change**, and moves the version marker in the
21
- > paragraph above. See `.github/instructions/gleks-ui-library.instructions.md`, definition of
22
- > done, step 9.
23
-
24
- ## Quick facts
25
-
26
- - Angular **v21+** only (`peerDependencies` require `^21.2.0` for `@angular/core`,
27
- `@angular/common`, `@angular/forms`, `@angular/platform-browser`). No support for older
28
- Angular.
29
- - No Angular CDK, no Material. Only runtime dependency is `tslib`.
30
- - Every component is **standalone**, `ChangeDetectionStrategy.OnPush`, and built with signals —
31
- `input()` / `output()` / `model()`, never `@Input()`/`@Output()` decorators, never `ngClass`/
32
- `ngStyle`.
33
- - **Reactive Forms only.** Every form control implements `ControlValueAccessor` and is built
34
- and tested against `[formControl]` / `formControlName`. The library never imports
35
- `FormsModule` and `[(ngModel)]` is untested — don't suggest it.
36
- - Theming is 100% CSS custom properties (`--gog-*`) — no Sass config, no JS theme objects, no
37
- build step to restyle anything.
38
- - Tree-shakeable: `"sideEffects": false` and every component is a separate standalone import, so
39
- importing `ButtonComponent` alone does not pull in the rest of the library.
40
- - SSR-safe: anything touching `window`/`document` is guarded with `isPlatformBrowser`/
41
- `afterNextRender`.
42
-
43
- ## Install & setup
44
-
45
- ```bash
46
- npm install @guildofgleks/ui
47
- # or
48
- yarn add @guildofgleks/ui
49
- # or — installs it and adds the stylesheet below to angular.json automatically
50
- ng add @guildofgleks/ui
51
- ```
52
-
53
- Add the baseline stylesheet once — it carries every token the components read plus their
54
- utility classes, so without it components render unstyled:
55
-
56
- ```jsonc
57
- // angular.json → projects.<app>.architect.build.options
58
- "styles": [
59
- "node_modules/@guildofgleks/ui/styles/index.css",
60
- "src/styles.scss", // your own styles, after the baseline so they win
61
- ],
62
- ```
63
-
64
- Import components where you use them — every one is standalone:
65
-
66
- ```ts
67
- import { Component } from '@angular/core';
68
- import { ButtonComponent, SelectComponent } from '@guildofgleks/ui';
69
-
70
- @Component({
71
- selector: 'app-example',
72
- imports: [ButtonComponent, SelectComponent],
73
- template: `
74
- <gog-select label="Region" [options]="regions" [(value)]="region" />
75
- <gog-button (gogClick)="save()">Save</gog-button>
76
- `,
77
- })
78
- export class ExampleComponent {}
79
- ```
80
-
81
- ## Core conventions (read once, applies everywhere)
82
-
83
- These hold for essentially every component in the library. Knowing them means you can guess a
84
- new component's API correctly instead of guessing wrong and hallucinating an input that doesn't
85
- exist.
86
-
87
- - **Selector prefix `gog-`** for components (`gog-button`, `gog-select`, …), attribute selectors
88
- for directives (`gogTooltip`, `[gogBadge]`).
89
- - **Outputs are prefixed `gog`** so they never collide with native DOM events —
90
- `gogClick`, `gogToggle`, `gogSearch`, `gogTabChange`, `gogRemove`, `gogScroll`, `gogLoadMore`,
91
- `gogDateSelect`. **Inputs keep their natural name** (`variant`, `size`, `disabled`).
92
- - **Two-way binding via `model()`.** Wherever a component holds a value the consumer drives, it's
93
- a `model()` input — bind with `[(value)]="signal"` / `[(checked)]="signal"` /
94
- `[(open)]="signal"` etc., or split into `[value]` + `(valueChange)`.
95
- - **Every input has a zero-config default.** Nothing requires configuration to render something
96
- reasonable.
97
- - **`size` is `GogSize = 'xsm' | 'sm' | 'md' | 'lg' | 'slg'`**, shared by every sized component.
98
- Default is `'md'` almost everywhere — exceptions: `gog-accordion` and `gog-table` default to
99
- `'lg'` (their `size` means row/section density, not form-control size), `gog-paginator`
100
- defaults to `'sm'`.
101
- - **`variant` is `GogVariant = 'primary' | 'secondary' | 'outline' | 'ghost'`** on `gog-button`.
102
- Status-colored components (`gog-tag`, `gog-badge`) use a different, four-value
103
- `GogTagVariant = 'success' | 'danger' | 'warning' | 'info'` instead — don't confuse the two.
104
- - **`errorDisplay: GogErrorDisplay = 'auto' | 'manual'`** (default `'manual'`) on every control
105
- that shows a validation message (inputfield, textarea, select, multiselect, autocomplete,
106
- radio-group, slider, datepicker). `'manual'`: the field shows `errorMessage` whenever it's
107
- non-empty — you own the timing (`errorMessage="control.invalid && control.touched ? 'Required' : ''"`).
108
- `'auto'`: shown once the attached `[formControl]`/`formControlName` is touched _and_ invalid —
109
- you only supply the message text. `'auto'` silently behaves like `'manual'` if there's no real
110
- form control attached.
111
- - **`inputId` is optional everywhere.** Every form control renders a real `id` — its own if you
112
- pass one, a generated one otherwise — so the `<label for>` and the error message's
113
- `aria-describedby` are always wired up. Pass `inputId` only when something outside the
114
- component needs to reference the field by a known id; never pass one just to get a label.
115
- - **User-visible chrome strings come from `GOG_CONFIG.labels`**, not from an input per string —
116
- "Clear", "Close dialog", "Go to page 4" and the rest. Per-instance label inputs exist where a
117
- single control realistically differs and win over the config. See
118
- [`labels`](#labels--translating-the-library).
119
- - **`floatLabel: GogFloatLabelVariant = 'none' | 'in' | 'on' | 'over'`** (default `'none'`) on
120
- the six field controls: inputfield, textarea, select, multiselect, autocomplete, datepicker.
121
- `'in'` floats up but stays inside the border, `'on'` floats to sit centered on the top border
122
- line, `'over'` floats fully above the field. Pair with `floatLabelShowPlaceholder` (default
123
- `false`) to reveal the field's own `placeholder` once the label has floated clear.
124
- - **`clearable`** (default varies) on inputfield, textarea, select, multiselect, autocomplete,
125
- datepicker — shows a clear (×) button once the field has content. Off by default everywhere
126
- except `gog-multiselect`, which had one before the input existed.
127
- - **Generic option accessors, not a fixed DTO.** Any collection-driven control (`gog-select`,
128
- `gog-multiselect`, `gog-autocomplete`, `gog-button-toggle-group`) takes **your own object
129
- shape** through `optionLabel` / `optionValue` / `optionDisabled` — each is a property path
130
- (`'name'`, dot-paths like `'profile.title'` work) **or** a function
131
- `(option: T) => TResult`. Defaults are `'name'` / `'id'` / `'disabled'`. Set
132
- `[optionValue]="null"` to emit **the option object itself** instead of a plucked id — the
133
- control then round-trips your own object with no lookup table needed:
134
- ```html
135
- <gog-select [options]="members" [optionLabel]="nameOf" [optionValue]="null" [(value)]="member" />
136
- ```
137
- - **Global defaults via `GOG_CONFIG` / `provideGogConfig(...)`** — see its own section below.
138
- Precedence is always: the instance's own input (if set) → `GOG_CONFIG` → the component's
139
- built-in default.
140
- - **Don't bind both a `model()` and a form directive on the same instance.** Every CVA control
141
- (checkbox, toggle, radio-group, inputfield, textarea, select, multiselect, autocomplete,
142
- slider, datepicker) exposes its value as both a two-way `model()` (`[(checked)]`, `[(value)]`)
143
- and, separately, `ControlValueAccessor` for `[formControl]`/`formControlName`. Pick one per
144
- instance — wiring both gives the value two competing sources of truth.
145
- - **The custom-content slot pattern.** Wherever a component needs custom markup for a specific
146
- part of itself, it's an attribute directive read with `contentChild()`, given a **typed**
147
- context via `let-` variables — never a plain `TemplateRef` input, never a string-keyed lookup.
148
- Recognize the shape:
149
- ```html
150
- <gog-accordion [items]="items">
151
- <ng-template gogAccordionHeader let-item let-open="open">{{ item.title }}</ng-template>
152
- </gog-accordion>
153
- ```
154
- See the per-component tables below for which slot directives exist on which component.
155
- - **Legacy `TemplateRef` inputs and string-keyed lookups still exist on a few components and
156
- still work, but are `@deprecated` — do not use them in new code.** See
157
- [Deprecated patterns — do not use in new code](#deprecated-patterns--do-not-use-in-new-code).
158
- - **Accessibility is built in**, not optional: keyboard navigation (roving tabindex, arrow keys,
159
- Home/End), ARIA roles/states, `:focus-visible` styling, `prefers-reduced-motion` handling, and
160
- WCAG AA contrast are already implemented — you don't need to add any of this yourself, just
161
- supply `ariaLabel`/`label` inputs where a component has no visible text of its own (icon-only
162
- buttons, `gog-progressbar`, `gog-scroll`).
163
- - **`aria-label` on the host tag does nothing.** Several components (`gog-button` chief among
164
- them) render their real interactive element (a `<button>`) _inside_ the component's own host
165
- tag. An `aria-label` attribute placed directly on `<gog-button>` in a template lands on the
166
- custom element wrapper, not on the inner `<button>`, so assistive tech never sees it — always
167
- use the component's own `ariaLabel` input instead.
168
-
169
- ## Theming
170
-
171
- Full model is in `README.md`'s Theming section; short version:
172
-
173
- - Every visual value (color, spacing, radius, shadow, duration) is a `--gog-*` CSS custom
174
- property, layered **foundation** (`--gog-accent-color`, `--gog-space-md`, …, restyles
175
- everything) → **component** (`--gog-button-primary-bg`, …, one block per component, named after
176
- the component's own element) → **instance** (`--gog-button-bg`, …, deliberately undeclared
177
- escape hatch for one element).
178
- - **Foundation includes a small character layer** (since 21.7.0, `docs/themes.md` iteration 1):
179
- `--gog-radius` (corner rounding), `--gog-control-border-*`/`--gog-panel-border-*`/`--gog-border-*`
180
- (border weight form fields, raised surfaces, everything smaller and inline, respectively),
181
- `--gog-text-transform`/`--gog-letter-spacing` (emphasis casing/tracking). Component tokens in
182
- the categories these cover derive from them by default; setting one in a `[data-theme]` block
183
- restyles every component that reads it, with nothing to re-list per component.
184
- - **The type scale is `--gog-text-xs | sm | md | lg | slg | xl | 2xl | 3xl`.** `slg` (1.25rem)
185
- fills the gap between `lg` and `xl` and is named for the control size that needed it. Every
186
- component font size that is one of these reads the token, so retuning the scale retunes the
187
- library; the handful that do not are off-scale on purpose (an 11px chip, the accordion
188
- chevron's px ramp, the toggle's own micro-ramp).
189
-
190
- - **Weight is `--gog-font-weight-medium | semibold | bold | heavy`** (500/600/700/900). Every
191
- component weight reads one of them, so a lighter or heavier house style is four declarations.
192
-
193
- - **`--gog-z-base` moves the whole stacking order.** Badge `+1`, toast `+100`, dropdowns, dialogs
194
- and menus `+300`, tooltip `+400`, the blocking spinner overlay `+8000`. Set the base to lift
195
- the library above your own chrome without disturbing its internal order.
196
-
197
- - **`--gog-density` is the character layer for spacing** (since 21.7.0, `docs/themes.md`
198
- iteration 6). It multiplies the ten-step scale `--gog-space-4` `--gog-space-48`, named
199
- for their pixel value at density 1, and every padding and gap in the library derives from a
200
- step. `--gog-density: 0.9` in a `[data-theme]` block makes the whole library tighter; nothing
201
- else needs to be named. `--gog-space-xs|sm|md|lg|2xl` are aliases for steps 4/8/16/24/48 and
202
- still work. **Every step is a multiple of 4** (since 21.11.0): the five 2px-granular steps came
203
- out once the last of their 102 readers moved, so "on the grid" is a fact about the scale rather
204
- than a habit. Three lengths stay off it on purpose and say so in their own comments — a toggle
205
- thumb's inset, a scrollbar thumb's, and the resize grip's hairline gap — because a length inside
206
- a single painted mark defines that mark's shape rather than spacing two things apart.
207
- Icon offsets, dropdown panel gaps, error-line offsets and the badge's overhang
208
- follow density; the glyph box, the focus-ring offset, the float-label reserve and the
209
- scrollbar/toggle thumb insets deliberately do not — those are legibility or geometry fitted to
210
- a fixed-width track, not spacing. Since 21.9.0 the split is enforced rather than trusted:
211
- `check-tokens` rule H fails the build on a length token that restates a scale step's value as
212
- a bare literal, with the three exceptions named in the script.
213
- - **Component prefixes are spelled out** since 21.5.0: `--gog-button-*`, `--gog-multiselect-*`,
214
- `--gog-confirmation-dialog-*`. The abbreviated `--gog-btn-*`, `--gog-ms-*` and `--gog-confirm-*`
215
- were removed in 21.7.0 if you're reading a codebase or an example that still uses one, rename
216
- it; it no longer resolves. The exception is `--gog-input-*`, which is not an abbreviation: it is
217
- the shared text-field block that `gog-inputfield` and `gog-textarea` both render, and it keeps
218
- that name.
219
- - **The package does not need the app's `box-sizing` reset** (since 21.6.0): `utilities.css`
220
- sets `border-box` on every element carrying a `gog-*` class, including the ones the library
221
- puts on a consumer's own element. Do not add a reset "so the components line up" — they
222
- already do, and a `* { box-sizing: content-box }` in an app is the only thing that undoes it.
223
- - Theme switch is a `data-theme` attribute, usually on `<html>`, toggled through the
224
- `ThemeService` (`inject(ThemeService).setTheme('dark')` / `.toggleTheme()` / `.theme` signal).
225
- Ships `light` and `dark`, plus nine importable presets at
226
- `@guildofgleks/ui/styles/presets/<name>.css`. **All nine set palette and character** (since
227
- 21.7.0 before it, three were palette-only, which made them recoloured defaults):
228
-
229
- | Preset | Radius | Density | Identity |
230
- | ---------------------- | ------ | ------- | ------------------------------------------------ |
231
- | `slate` | 12px | 1.05 | soft modern — hairline borders, roomy |
232
- | `one-dark`/`one-light` | 4px | 0.9 | editor chrome; identical character, two tones |
233
- | `material` | 4px | 1.1 | Material Design 3, pill buttons |
234
- | `primeng` | 6px | 0.95 | PrimeNG Aura |
235
- | `ledger` | 0 | 0.9 | administrative hard offset shadow, no motion |
236
- | `terminal` | 0 | 0.85 | green phosphor, monospaced throughout, no motion |
237
- | `bevel` | 0 | 0.9 | early-web desktop `outset`/`inset` borders |
238
- | `parchment` | 0 | 1.1 | ink on paper old-style serif, oxblood |
239
-
240
- `material`, `primeng` and `bevel` also set a few genuinely per-component things the character
241
- layer has no vocabulary for (a pill button, a table's header font, a button bevel that has to
242
- disagree with a field's); see their own file headers.
243
-
244
- - **A preset never makes a network request.** Each sets a font _stack_ resolving to a real system
245
- face. Where a webfont is worth offering, it is a separate opt-in file — `terminal.fonts.css`
246
- (IBM Plex Mono), `parchment.fonts.css` (EB Garamond) imported **after** the preset, since it
247
- re-points the same tokens and later wins. Do not add an `@import url(…)` to a preset itself; put
248
- it in a companion file, or the import becomes a download nobody asked for.
249
- - Restyle one instance without touching a theme: `<gog-button style="--gog-button-bg: #ff4edb">`.
250
- - Build a custom theme by declaring a palette **and a character** against a new `data-theme`
251
- value (see `README.md`'s Theming section for the full worked example) component tokens
252
- re-derive automatically, you don't restate them.
253
-
254
- ## Right-to-left
255
-
256
- Supported since 21.5.0. `dir="rtl"` on `<html>` or on any wrapper mirrors every component —
257
- you write nothing per component. Portaled overlays (select/multiselect panels, tooltip bubbles)
258
- copy a _scoped_ `dir` onto themselves, so an RTL region inside an LTR page works too.
259
-
260
- Physical by design, in both directions: `gogTooltip [position]="'left' | 'right'"` and
261
- `ToastConfig.position` (`'top-right'`, …). Use the tooltip's `'auto'` for direction-aware
262
- placement; a toast corner is a deliberate choice, so it is not mirrored.
263
-
264
- ## Global configuration — `GOG_CONFIG` / `provideGogConfig(...)`
265
-
266
- For the handful of inputs an app typically wants to set once (a size for every form control, a
267
- locale for every datepicker) rather than repeat on every instance:
268
-
269
- ```ts
270
- import { provideGogConfig } from '@guildofgleks/ui';
271
-
272
- bootstrapApplication(App, {
273
- providers: [
274
- provideGogConfig({
275
- control: { size: 'sm', errorDisplay: 'auto', clearable: true },
276
- dropdown: { appendToBody: true, filter: true },
277
- datepicker: { locale: 'de-DE', firstDayOfWeek: 1, format: 'dd.MM.yyyy' },
278
- toast: { position: 'top-right', duration: 4000 },
279
- }),
280
- ],
281
- });
282
- ```
283
-
284
- Precedence, always: **instance input → `GOG_CONFIG` → component's built-in default.** A nested
285
- `provideGogConfig(...)` (in a route's or component's own `providers`) **layers onto the
286
- parent's config**, one level deep per key — it does not replace it.
287
-
288
- | Key | Fields | Applies to |
289
- | -------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
290
- | `control` | `size`, `errorDisplay`, `clearable` | `size`: button, `[gogButton]`, button-toggle-group, checkbox, toggle, radio-group, inputfield, textarea, select, multiselect, autocomplete, datepicker. `errorDisplay`: inputfield, textarea, select, multiselect, autocomplete, datepicker, radio-group, slider. `clearable`: inputfield, textarea, select, multiselect, autocomplete, datepicker. Not table/accordion/paginator (density, not form size), not spinner/skeleton/tag/chip. |
291
- | `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). |
292
- | `floatLabel` | `variant`, `showPlaceholder` | inputfield, textarea, select, multiselect, autocomplete, datepicker. |
293
- | `datepicker` | `locale`, `firstDayOfWeek`, `format` | `gog-datepicker`, `gog-calendar`. |
294
- | `autocomplete` | `searchDebounce`, `minLength`, `openOnFocus` | `gog-autocomplete`. |
295
- | `tooltip` | `position`, `showDelay`, `hideDelay` | the `gogTooltip` directive. |
296
- | `spinner` | `component`, `variant` | every spinner the library draws — `gog-spinner`, `gog-spinner-overlay`, and the ones inside `gog-button`, `gog-autocomplete` and `gog-table`, which have no input of their own. `component` takes **your** component and renders it in place of the built-in look. The overlay honoured neither key until 21.10.0, and `gog-table` was simply never listed. |
297
- | `scroll` | `autoHide`, `hideDelay`, `size`, `overscrollBehavior`, `showTrack`, `horizontalWheel` | `gog-scroll` (and every component that uses one internally). |
298
- | `button` | `debounce` | `gog-button`. |
299
- | `ripple` | `enabled` | the press ripple on `gog-button`, `[gogButton]`, `gog-button-toggle-group`, `gog-chip`, `gog-tabs`, `gog-accordion`, `gogCollapsibleTrigger`, `gogMenuItem` and the `gog-select`/`gog-multiselect`/`gog-autocomplete` options. **Off by default.** Each of those takes a `ripple` input that wins over it. Not the `gogRipple` directive — writing that attribute is already the per-element decision. |
300
- | `inputfield` | `showSpinButtons` | `gog-inputfield`. |
301
- | `textarea` | `resize` | `gog-textarea`. |
302
- | `paginator` | `showPageSizeSelect`, `pageSizeOptions` | `gog-paginator`, and through it `gog-table`'s built-in pagination. |
303
- | `toast` | `position`, `duration` | `ToastService`. |
304
- | `theme` | `storageKey`, `defaultTheme`, `followSystem`, `lightTheme`, `darkTheme` | `ThemeService`. All off/neutral by defaultsee below. |
305
- | `labels` | every fixed string the library renders — see below | inputfield, textarea, select, multiselect, autocomplete, datepicker, calendar, paginator, table, `DialogService`, `ToastService`. |
306
-
307
- Anything visual does **not** belong here — override the `--gog-*` token instead.
308
-
309
- ### `labels` — translating the library
310
-
311
- Every string a component renders that the consumer never writes markup for. An app that isn't
312
- in English sets these once rather than on every control:
313
-
314
- ```ts
315
- provideGogConfig({
316
- labels: {
317
- clear: 'Löschen', // inputfield / textarea clear button
318
- clearSelection: 'Auswahl löschen', // select / multiselect / autocomplete
319
- clearDate: 'Datum löschen', // datepicker
320
- selectAll: 'Alle auswählen', // multiselect panel
321
- clearAll: 'Alle löschen', // multiselect panel
322
- increment: 'Erhöhen', // number spin buttons
323
- decrement: 'Verringern',
324
- showPassword: 'Passwort anzeigen',
325
- hidePassword: 'Passwort verbergen',
326
- closeDialog: 'Schließen',
327
- closeToast: 'Schließen',
328
- pagination: 'Seitennavigation',
329
- previousPage: 'Vorherige Seite',
330
- nextPage: 'Nächste Seite',
331
- openCalendar: 'Kalender öffnen',
332
- togglePanel: 'Bereich umschalten', // gog-panel's toggle, only when it has no heading
333
- rowsPerPage: 'Zeilen pro Seite', // gog-paginator's size select
334
- total: 'Gesamt', // gog-table's row-count label
335
- tablePagination: 'Tabellennavigation',
336
- selectRow: 'Zeile auswählen',
337
- selectAllRows: 'Alle Zeilen auswählen',
338
- today: 'Heute',
339
- thisMonth: 'Aktueller Monat',
340
- previousMonth: 'Vorheriger Monat',
341
- nextMonth: 'Nächster Monat',
342
- previousYear: 'Vorheriges Jahr',
343
- nextYear: 'Nächstes Jahr',
344
- hours: 'Stunden',
345
- minutes: 'Minuten',
346
- seconds: 'Sekunden',
347
- // The one non-string field: it interpolates the page number, and word order and
348
- // agreement around a number vary by language, so it takes a formatter.
349
- page: (page, isCurrent) => (isCurrent ? `Seite ${page}, aktuell` : `Zu Seite ${page} wechseln`),
350
- },
351
- });
352
- ```
353
-
354
- Strings that describe **one** control rather than library chrome — `gog-checkbox`'s `ariaLabel`,
355
- `gog-button`'s `ariaLabel`, any field's `label`/`placeholder` — are deliberately **not** here.
356
- Those stay per instance. Where a per-instance label input exists (`clearAriaLabel`, `todayLabel`,
357
- …) it still wins over the configured value.
358
-
359
- ## Services
360
-
361
- ### `ThemeService`
362
-
363
- ```ts
364
- private readonly theme = inject(ThemeService);
365
- this.theme.theme(); // Signal<string>, READ-ONLY — current data-theme
366
- this.theme.setTheme('dark'); // any theme name, including a custom one you declared in CSS
367
- this.theme.toggleTheme(); // flips between the configured light and dark names
368
- ```
369
-
370
- `theme` is read-only on purpose: writing to it would move the signal without touching the
371
- `data-theme` attribute the styles actually read. Never suggest `theme.set(...)` — it does not
372
- exist.
373
-
374
- Zero-config behaviour: adopt whatever `data-theme` is already on `<html>`, else `'light'`.
375
- Persistence and following the OS setting are **opt-in**, so upgrading cannot change which theme
376
- an existing app opens in:
377
-
378
- ```ts
379
- provideGogConfig({
380
- theme: {
381
- storageKey: 'app-theme', // persist the choice in localStorage; unset = no persistence
382
- followSystem: true, // open in the OS prefers-color-scheme, and keep following it
383
- // until the app calls setTheme/toggleTheme
384
- lightTheme: 'light', // the two names followSystem maps to and toggleTheme alternates
385
- darkTheme: 'one-dark', // between
386
- defaultTheme: 'light', // used when nothing else decides
387
- },
388
- });
389
- ```
390
-
391
- Resolution order at startup: existing `data-theme` on the document → persisted value →
392
- OS setting (if `followSystem`) → `defaultTheme` → `'light'`.
393
-
394
- ### `ToastService`
395
-
396
- Root-provided singleton. Requires a `<gog-toast-container />` placed once in your app (see
397
- [gog-toast](#gog-toast--gog-toast-container) below it is **not** wired up automatically).
398
-
399
- ```ts
400
- private readonly toast = inject(ToastService);
401
-
402
- this.toast.success('Saved');
403
- this.toast.error('Could not save', {
404
- isSticky: true,
405
- actions: [{ label: 'Retry', onClick: () => this.save() }],
406
- });
407
- // also: .warning(msg, config?), .info(msg, config?), .show(config), .dismiss(id), .dismissAll()
408
- ```
409
-
410
- `ToastConfig`: `{ message, type?, iconName?, iconTemplate?, actions?, dedupeKey?, isSticky?, duration?, position? }`.
411
- Repeated calls with the same (explicit or inferred) `dedupeKey` replace the existing toast in
412
- place instead of stacking a duplicate.
413
-
414
- ### `DialogService`
415
-
416
- Root-provided singleton, imperative dynamic-component dialogs. Requires a `<gog-dialog />`
417
- placed once in your app (see [gog-dialog](#gog-dialog) below — also **not** automatic).
418
-
419
- ```ts
420
- private readonly dialogService = inject(DialogService);
421
-
422
- async confirmDelete(): Promise<void> {
423
- const handle = this.dialogService.open<boolean>({
424
- component: ConfirmationDialogComponent, // or your own component
425
- title: 'Delete this item?',
426
- role: 'alertdialog',
427
- data: { message: 'This cannot be undone.' },
428
- });
429
- const confirmed = await handle.afterClosed; // boolean | undefined
430
- }
431
- ```
432
-
433
- `DialogConfig<TData>`: `{ title?, component, data?: TData, modal? (default true), closable?, draggable?, closeIconName?, closeIconTemplate?, width?, maxWidth?, role? ('dialog' default | 'alertdialog'), zIndex? }`.
434
- `open<TResult, TData>()` returns `{ close(result?), afterClosed: Promise<TResult | undefined> }`. Also:
435
- `closeAll(result?)`, `updatePosition(id, offsetX, offsetY)` (for `draggable` dialogs).
436
-
437
- **`open<TResult, TData>()` type-checks `data` against `TData` when you supply both type
438
- arguments** supplying only `TResult` (the common case above) leaves `TData` as `unknown`,
439
- exactly as before:
440
-
441
- ```ts
442
- interface EditUserData {
443
- userId: string;
444
- }
445
-
446
- const handle = this.dialogService.open<{ saved: boolean }, EditUserData>({
447
- component: EditDialogComponent,
448
- data: { userId: user.id }, // checked against EditUserData here
449
- });
450
- ```
451
-
452
- This checks only the call site. `EditDialogComponent` still reads its data via `inject(DIALOG_DATA)`
453
- — an `InjectionToken<unknown>` shared by every dialog, so it still needs its own cast
454
- (`inject<EditUserData>(DIALOG_DATA)`, shown below). Angular's DI has no way to carry a
455
- per-call-site type through one shared token, so the receiving half of the round trip is still on
456
- trust — this closes only the half that can be closed.
457
-
458
- The library ships a ready-made `ConfirmationDialogComponent` for yes/no prompts — pass it as
459
- `component` with `data: { title, description, confirmText, cancelText }`; it resolves the
460
- dialog's result to `true`/`false`.
461
-
462
- **Wiring a custom component into a dialog** — it reads its data via `DIALOG_DATA` and closes
463
- itself via `DIALOG_REF`:
464
-
465
- ```ts
466
- import { Component, inject } from '@angular/core';
467
- import { DIALOG_DATA, DIALOG_REF } from '@guildofgleks/ui';
468
-
469
- @Component({ selector: 'app-edit-dialog', template: `…` })
470
- export class EditDialogComponent {
471
- protected readonly data = inject<{ userId: string }>(DIALOG_DATA);
472
- private readonly ref = inject(DIALOG_REF);
473
-
474
- save(): void {
475
- this.ref.close({ saved: true });
476
- }
477
- }
478
- ```
479
-
480
- ---
481
-
482
- ## Component reference
483
-
484
- Every component below is exported from `@guildofgleks/ui`'s root — `import { X } from '@guildofgleks/ui'`.
485
- "CVA" = implements `ControlValueAccessor` (works with `[formControl]`/`formControlName`).
486
-
487
- ### Buttons & choices
488
-
489
- #### `gog-button`
490
-
491
- | Input | Type | Default | Notes |
492
- | ----------- | --------------------------------- | ----------- | ----------------------------------------------------- |
493
- | `variant` | `GogVariant` | `'primary'` | |
494
- | `severity` | `GogSeverity` | `'accent'` | what the action means; orthogonal to `variant` — see below |
495
- | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
496
- | `disabled` | `boolean` | `false` | |
497
- | `fullWidth` | `boolean` | `false` | |
498
- | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | |
499
- | `loading` | `boolean` | `false` | shows an inline `gog-spinner`, blocks clicks |
500
- | `debounce` | `number \| undefined` | `300` | ms; via `GOG_CONFIG.button.debounce` — see note below |
501
- | `ariaLabel` | `string \| null` | `null` | **use this, not a raw `aria-label` attribute** |
502
- | `ariaPressed` | `boolean \| 'mixed' \| null` | `null` | toggle button; `false` renders `aria-pressed="false"` |
503
- | `ariaExpanded` | `boolean \| null` | `null` | disclosure / popup trigger |
504
- | `ariaControls` | `string \| null` | `null` | id of the controlled element; pairs with `ariaExpanded` |
505
- | `ariaHasPopup` | `GogAriaHasPopup \| null` | `null` | `boolean \| 'menu' \| 'listbox' \| 'tree' \| 'grid' \| 'dialog'` |
506
- | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
507
-
508
- Outputs: `gogClick: MouseEvent`.
509
-
510
- **`severity` says what the action means; `variant` says how loudly it is drawn** (21.9.0). The
511
- two are orthogonal, so this is not a fifth variant — it re-points the colours all four are built
512
- from, and every combination is real: `variant="ghost" severity="danger"` is a quiet delete,
513
- `variant="primary" severity="danger"` a loud one. `'accent'` is the default and the absence of a
514
- claim, so nothing has to opt out of a severity it does not have. `GogSeverity` is shared with
515
- `gog-progressbar`, whose `GogProgressbarVariant` is now an alias of it.
516
-
517
- ```html
518
- <gog-button severity="danger" (gogClick)="deleteAccount()">Delete account</gog-button>
519
- <gog-button variant="outline" severity="warning">Discard draft</gog-button>
520
- <a gogButton severity="success" routerLink="/done">Finish</a>
521
- ```
522
-
523
- Two colour rules are worth knowing before you override anything. A **filled** severity button's
524
- label is `--gog-<status>-text-color`, which each theme states for its own hue `material` and
525
- `primeng` put near-black on their bright ones, the rest white and hover and press deepen the
526
- fill *away* from that label (`--gog-<status>-shade`), so a state always makes the label easier to
527
- read rather than harder. A **transparent** one's label is `--gog-button-<status>-ink`: the status
528
- hue mixed halfway toward the page's ink, because the raw hue is legible body text in only five of
529
- the eleven shipped themes. Override `--gog-button-<status>-ink` if your own theme wants more
530
- colour there, and check it: all four severities across all four variants and all their states are
531
- gated by `npm run check:contrast`.
532
-
533
- **Every ARIA attribute this button needs has an input, and a raw attribute is not a
534
- substitute.** `<gog-button [attr.aria-pressed]="on()">` compiles, throws nothing, and does
535
- nothing: the attribute lands on the `<gog-button>` custom element, which has no role, while the
536
- real `<button>` inside stays unmarked. The failure is invisible the control looks right and is
537
- simply not a toggle to a screen reader. Use `[ariaPressed]`, `[ariaExpanded]`, `[ariaControls]`,
538
- `[ariaHasPopup]` and `ariaLabel`.
539
-
540
- `false` is not the same as unset. `null` omits the attribute; `false` renders
541
- `aria-pressed="false"` / `aria-expanded="false"`, which is what an off toggle or a closed
542
- disclosure has to say a button with no `aria-pressed` at all is not a toggle button.
543
-
544
- **A toggle button now looks toggled** (21.9.0). `aria-pressed="true"` (or `"mixed"`) draws an
545
- inset ring — `--gog-button-<variant>-toggled-shadow`, overridable per instance with
546
- `--gog-button-toggled-shadow`. A ring rather than a fill because hover and press already own the
547
- background: the state has to survive both, and until 21.9.0 it did not exist at all, so a button
548
- could announce itself as on to a screen reader and look identical to an off one. `[gogButton]`
549
- gets the same look from the attribute you write on your own element.
550
-
551
- **A `disabled` toggle keeps the ring** (21.10.0), dimmed by `--gog-button-disabled-opacity` like
552
- the rest of the button. `disabled` on a real `<button>` does not remove `aria-pressed`, so "on,
553
- and unavailable" is announced either way and has to be visible; the rule had excluded
554
- `:disabled` until then, copied from the hover and press rules where the guard belongs.
555
- `gog-chip`'s `selected` ring has always behaved this way, and the two are now the same.
556
-
557
- **`[gogButton]` needs none of these inputs.** It styles an element you own, so write the ARIA
558
- attributes on your own `<button>`/`<a>` directly. Same for `[gogMenuTrigger]`, which sets
559
- `aria-haspopup`/`aria-expanded`/`aria-controls` on its host put it on your own `<button
560
- gogButton>`, as its own example shows, not on a `<gog-button>`.
561
-
562
- ```html
563
- <gog-button [ariaPressed]="mirrored()" (gogClick)="toggleMirror()">Mirror</gog-button>
564
-
565
- <gog-button [ariaExpanded]="open()" ariaControls="filters" ariaHasPopup="dialog"
566
- (gogClick)="open.set(!open())">Filters</gog-button>
567
- ```
568
-
569
- **The press is a colour, not only a movement.** `:active` deepens the button's background (and
570
- the label where the fill demands it) as well as scaling it by `--gog-button-active-scale`. Under
571
- `prefers-reduced-motion: reduce` the scale is dropped and the colour stays, so the press is still
572
- visible to a reader who has switched animations off before 21.9.0 that reader got no feedback at
573
- all, since the ripple is off by default and is itself suppressed under reduced motion. Override
574
- per instance with `--gog-button-press-bg` / `--gog-button-press-color`, or per theme with
575
- `--gog-button-<variant>-active-bg`.
576
-
577
- Every other pressable surface in the library does the same thing since 21.9.0 — menu items,
578
- chips, tab and accordion headers, button-toggle options and the three dropdowns' option rows —
579
- each through its own `--gog-<block>-press-bg`. `gogCollapsibleTrigger` is the exception: the
580
- library paints nothing on that element in any state, because it is yours.
581
-
582
- **`debounce` is a spam guard, not a delay before the first click.** The first click in a window
583
- fires immediately (leading edge); further clicks within `debounce` ms are silently dropped.
584
-
585
- **Use `(gogClick)`, never `(click)`, on `gog-button`.** The click handler that drives `debounce`
586
- and emits `gogClick` is bound on the `<button>` inside the component's own template, not on the
587
- host a native click still bubbles up through `<gog-button>`, so a `(click)` listener written
588
- there fires on every press, silently bypassing the debounce entirely. This is specific to the
589
- component: `[gogButton]` on your own `<a>`/`<button>` has no debounce to bypass, so `(click)` on
590
- it works exactly as written.
591
-
592
- ```html
593
- <gog-button variant="primary" [loading]="saving()" (gogClick)="save()">Save</gog-button>
594
- <gog-button variant="ghost" ariaLabel="Close" (gogClick)="close()"
595
- ><gog-icon name="close"
596
- /></gog-button>
597
- ```
598
-
599
- #### `gog-button-toggle-group`
600
-
601
- A row of buttons, single- or multi-select, built from your own option objects.
602
-
603
- | Input | Type | Default | Notes |
604
- | ------------------------------------ | ------------------------------------------ | ---------------------- | --------------------------------------------- |
605
- | `options` | `TOption[]` | `[]` | |
606
- | `optionLabel` | accessor | `'name'` | |
607
- | `optionValue` | accessor \| `null` | `'id'` | `null` emits the option object |
608
- | `optionDisabled` | accessor | `'disabled'` | |
609
- | `optionIcon` | accessor → `GogIconName \| null` \| `null` | `null` | optional leading icon per option |
610
- | `multiple` | `boolean` | `false` | changes ARIA role entirely — see note |
611
- | `appearance` | `'joined' \| 'separated'` | `'joined'` | |
612
- | `orientation` | `GogOrientation` | `'horizontal'` | |
613
- | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
614
- | `disabled`, `fullWidth`, `ariaLabel` | | `false`, `false`, `''` | |
615
- | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
616
-
617
- Model: `value: TValue | TValue[] | null` (single value, or array in `multiple` mode). CVA: yes.
618
- Slot: `<ng-template gogButtonToggleOption let-opt let-selected="selected">` for custom button
619
- markup. **Single mode is a radio group** (`role="radiogroup"`, arrows move _and_ select);
620
- **multiple mode is a toolbar of independent toggles** (`role="group"`, arrows only move, Space
621
- toggles) this is a real ARIA distinction, not cosmetic.
622
-
623
- ```html
624
- <gog-button-toggle-group [options]="alignments" [(value)]="align" />
625
- <gog-button-toggle-group [options]="tools" [multiple]="true" [(value)]="activeTools" />
626
- ```
627
-
628
- ### Form fields
629
-
630
- #### `gog-inputfield`
631
-
632
- | Input | Type | Default | Notes |
633
- | ----------------------------------------- | ---------------------- | ----------------------------------- | -------------------------------------------------------------------------------------- |
634
- | `label`, `placeholder` | `string` | `''` | |
635
- | `type` | `GogInputType` | `'text'` | `text`/`password`/`email`/`number`/`search`/`tel`/`url`/`date`/`time`/`datetime-local` |
636
- | `readonly` | `boolean` | `false` | value stays focusable and submitted, edits blocked; hides the clear button and stepper |
637
- | `maxlength`, `minlength` | `number \| null` | `null` | native attributes |
638
- | `pattern` | `string` | `''` | native attribute, regex source |
639
- | `inputMode` | `GogInputMode \| null` | `null` | on-screen keyboard hint (`numeric`, `tel`, …) |
640
- | `spellcheck` | `boolean \| null` | `null` | unset = browser default |
641
- | `inputId` | `string` | `''` → generated | a real id is always rendered; pass one only to reference the field externally |
642
- | `min`, `max`, `step` | `number \| null` | `null` | `type="number"` only |
643
- | `showSpinButtons` | `boolean \| undefined` | `true` | own +/- glyphs on `type="number"`; via `GOG_CONFIG.inputfield.showSpinButtons` |
644
- | `errorMessage`, `errorDisplay` | | `''`, `'manual'` | see conventions |
645
- | `disabled`, `size`, `fullWidth` | | `false`, `'md'`, `true` | |
646
- | `iconStart` / `iconEnd` | `GogIconName \| ''` | `''` | bare leading/trailing icon |
647
- | `clearable`, `clearAriaLabel` | | `false`, `'Clear'` | on `type="number"` the clear button renders alongside the stepper |
648
- | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
649
- | `showPasswordLabel` / `hidePasswordLabel` | `string \| undefined` | `'Show password'`/`'Hide password'` | `type="password"` reveal toggle aria-labels; via `GOG_CONFIG.labels` |
650
- | `incrementLabel` / `decrementLabel` | `string \| undefined` | `'Increment'`/`'Decrement'` | spin button aria-labels; via `GOG_CONFIG.labels` |
651
-
652
- Model: `value: string` (always a string, even for `type="number"` — the _form control_ value is
653
- `number | null`, but the `[(value)]` model mirrors the raw text). CVA: yes.
654
-
655
- Slots: project `<span gogInputAddonStart>`/`<span gogInputAddonEnd>` (or a `<button>`) for
656
- custom leading/trailing markup a normal DOM element with its own `aria-label`, click handler
657
- and disabled state, not a component-managed slot. This is the **current, non-deprecated**
658
- replacement for the old icon-template/icon-fn/icon-label input quartet see
659
- [Deprecated patterns](#deprecated-patterns--do-not-use-in-new-code).
660
-
661
- ```html
662
- <gog-inputfield
663
- label="Email"
664
- type="email"
665
- formControlName="email"
666
- errorDisplay="auto"
667
- errorMessage="Enter a valid email"
668
- [clearable]="true"
669
- />
670
-
671
- <gog-inputfield label="Amount" [fullWidth]="false">
672
- <span gogInputAddonStart>€</span>
673
- </gog-inputfield>
674
- ```
675
-
676
- #### `gog-textarea`
677
-
678
- | Input | Type | Default |
679
- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ---------------------------------------------- |
680
- | `label`, `placeholder` | `string` | `''` |
681
- | `rows` | `number` | `4` |
682
- | `readonly` | `boolean` | `false` |
683
- | `maxlength`, `minlength` | `number \| null` | `null` |
684
- | `spellcheck` | `boolean \| null` | `null` |
685
- | `inputId` | `string` | `''` → generated, same as inputfield |
686
- | `resize` | `GogTextareaResize \| undefined` (`'vertical'\|'horizontal'\|'both'\|'none'`) | `'vertical'`; via `GOG_CONFIG.textarea.resize` |
687
- | `errorMessage`, `errorDisplay`, `disabled`, `size`, `fullWidth` | | same shape as inputfield |
688
- | `clearable`, `clearAriaLabel`, `floatLabel`, `floatLabelShowPlaceholder` | | same shape as inputfield |
689
-
690
- Model: `value: string`. CVA: yes.
691
-
692
- ```html
693
- <gog-textarea label="Notes" formControlName="notes" [rows]="6" resize="vertical" />
694
- ```
695
-
696
- #### `gog-select`
697
-
698
- Extends the shared listbox behaviour (`GogDropdownBase`) that also backs `gog-multiselect` and
699
- partly `gog-autocomplete` placement, the append-to-body overlay, click-outside, keyboard nav,
700
- and CVA all come from there. Full shared input surface (documented once, applies to both select
701
- and multiselect unless noted otherwise):
702
-
703
- | Input | Type | Default | Notes |
704
- | ------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
705
- | `label`, `ariaLabel`, `placeholder` | `string` | `''`, `''`, `'Select...'` | |
706
- | `options` | `TOption[]` | `[]` | your own objects |
707
- | `optionLabel` | accessor | `'name'` | path or fn |
708
- | `optionValue` | accessor \| `null` | `'id'` | `null` = emit the option object |
709
- | `optionDisabled` | accessor | `'disabled'` | |
710
- | `clearable`, `clearAriaLabel` | | `false` (select) / `true` (multiselect), `'Clear selection'` | |
711
- | `minWidth` | `string \| null` | `null` | only with `[fullWidth]="false"` |
712
- | `filter` | `boolean \| undefined` | `false` | search box in the panel; via `GOG_CONFIG.dropdown.filter` |
713
- | `filterPlaceholder`, `filterEmptyMessage` | `string` | `'Search...'`, `'No matches'` | |
714
- | `filterPosition` | `'top' \| 'bottom' \| undefined` | `'top'` | via `GOG_CONFIG.dropdown.filterPosition` |
715
- | `filterMatch` | `((option, query) => boolean) \| null` | `null` | custom matcher, else case-insensitive substring on the resolved label |
716
- | `errorMessage`, `errorDisplay` | | `''`, `'manual'` | |
717
- | `size` | `GogSize \| undefined` | `'md'` | |
718
- | `dropdownDirection` | `'auto' \| 'up' \| 'down' \| undefined` | `'auto'` | |
719
- | `dropdownZIndex`, `dropdownWidth`, `dropdownMaxHeight` | | `null` | only meaningful with `appendToBody` |
720
- | `appendToBody` | `boolean \| undefined` | `false` | renders the panel into `<body>` — needed inside a scroll/overflow-clipped container |
721
- | `disabled`, `fullWidth` | | `false`, `true` | |
722
- | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
723
- | `inputId` (select/autocomplete only) | `string` | `''` | |
724
- | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
725
-
726
- `gog-select`-specific: `value: model<TValue>(null)`.
727
- `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`).
728
-
729
- CVA: yes, both. Slots (shared): `<ng-template gogDropdownChevron>` (custom chevron markup),
730
- `<ng-template gogDropdownOption let-opt let-selected="selected" let-label="label">` (custom
731
- option row). Multiselect adds `<ng-template gogMultiselectClearIcon>`.
732
-
733
- **Turn `filter` on past about seven options or order them instead.** Choice time grows with the
734
- log of the count (`T = b · log₂(n + 1)`), so beyond roughly seven a panel stops being scanned and
735
- starts being read. The escape is not always the filter box: the law governs _unordered_ choices,
736
- and a list the reader can predict alphabetical countries, ascending amounts, a familiar fixed
737
- sequence is one they search rather than choose from, so ordering it well is worth as much as
738
- filtering it. Both, for a long list of neither. `GOG_CONFIG.dropdown.filter` sets this once for
739
- the app rather than per dropdown, which is usually the right place for it.
740
-
741
- ```html
742
- <gog-select
743
- label="Region"
744
- [options]="regions"
745
- optionLabel="title"
746
- [(value)]="regionId"
747
- [filter]="true"
748
- />
749
-
750
- <gog-multiselect
751
- label="Tags"
752
- [options]="tags"
753
- [(value)]="selectedTagIds"
754
- [showControls]="true"
755
- formControlName="tags"
756
- errorDisplay="auto"
757
- />
758
- ```
759
-
760
- #### `gog-autocomplete`
761
-
762
- Shares `GogDropdownBase` too, but the trigger is a real `<input>` (combobox pattern,
763
- `aria-activedescendant`), not a listbox button — so it does **not** reuse the base's built-in
764
- panel-filter box; it filters/searches off what's typed in the field itself.
765
-
766
- | Input | Type | Default | Notes |
767
- | ------------------------------------------------------------------------------------------------------ | ---------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
768
- | _(all the shared `GogDropdownBase` inputs above except `filter`/`filterPlaceholder`/`filterPosition`)_ | | | |
769
- | `filterLocal` | `boolean` | `true` | narrow `options` client-side as you type; turn **off** when `gogSearch` already returns a filtered server list (avoids double-filtering) |
770
- | `minLength` | `number \| undefined` | `1` | via `GOG_CONFIG.autocomplete.minLength` |
771
- | `openOnFocus` | `boolean \| undefined` | `true` | via `GOG_CONFIG.autocomplete.openOnFocus` |
772
- | `searchDebounce` | `number \| undefined` | `300` | ms before `gogSearch` fires; via `GOG_CONFIG.autocomplete.searchDebounce` |
773
- | `loading` | `boolean` | `false` | shows a spinner in the trailing slot |
774
- | `emptyMessage` | `string` | `'No matches'` | |
775
- | `forceSelection` | `boolean` | `true` | see note below |
776
- | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
777
-
778
- Outputs: `gogSearch: string` (debounced query — wire your server lookup here),
779
- `gogLoadMore: void` (panel scrolled to the end — fetch the next page).
780
-
781
- Model: `value: TValue | null`. CVA: yes.
782
-
783
- **`forceSelection` matters.** On (default): the field always ends up reflecting a real
784
- selection free-typed text that matches nothing snaps back on blur/Escape. Off: what the user
785
- typed is itself meaningful (a create-as-you-type flow) read the typed text from `gogSearch`,
786
- not from `value`, since `value` clears the moment the text stops matching the selection.
787
-
788
- ```html
789
- <gog-autocomplete
790
- [options]="users"
791
- optionLabel="profile.fullName"
792
- [optionValue]="null"
793
- [(value)]="user"
794
- [loading]="searching()"
795
- (gogSearch)="search($event)"
796
- />
797
- ```
798
-
799
- #### `gog-checkbox`
800
-
801
- | Input | Type | Default |
802
- | ---------------------------------------- | ---------------------- | ------- |
803
- | `label`, `ariaLabel` | `string` | `''` |
804
- | `size` | `GogSize \| undefined` | `'md'` |
805
- | `indeterminate`, `disabled`, `fullWidth` | `boolean` | `false` |
806
-
807
- Model: `checked: boolean`. CVA: yes. Slot: `<ng-template gogCheckboxIcon>` for a custom tick
808
- icon.
809
-
810
- ```html
811
- <gog-checkbox label="I agree to the terms" formControlName="agree" />
812
- ```
813
-
814
- #### `gog-toggle`
815
-
816
- An on/off switch (`role="switch"`) — semantically different from a checkbox ("is this setting
817
- on", not "is this one of the things you selected").
818
-
819
- | Input | Type | Default | Notes |
820
- | ----------------------- | ---------------------- | ------- | ------------------------------------- |
821
- | `label`, `ariaLabel` | `string` | `''` | |
822
- | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
823
- | `disabled`, `fullWidth` | `boolean` | `false` | |
824
- | `labelPosition` | `'start' \| 'end'` | `'end'` | |
825
- | `onLabel`, `offLabel` | `string` | `''` | text rendered inside the track itself |
826
-
827
- Model: `checked: boolean`. CVA: yes.
828
-
829
- ```html
830
- <gog-toggle label="Notifications" formControlName="notificationsOn" onLabel="ON" offLabel="OFF" />
831
- ```
832
-
833
- #### `gog-radio-group`
834
-
835
- | Input | Type | Default |
836
- | ------------------------------ | ----------------------------------------------- | ---------------- |
837
- | `options` | `GogRadioOption[]` (`{ id, label, disabled? }`) | `[]` |
838
- | `label`, `ariaLabel`, `name` | `string` | `''` |
839
- | `size` | `GogSize \| undefined` | `'md'` |
840
- | `disabled`, `fullWidth` | `boolean` | `false` |
841
- | `orientation` | `GogOrientation` | `'vertical'` |
842
- | `errorMessage`, `errorDisplay` | | `''`, `'manual'` |
843
-
844
- Model: `value: string | number | null`. CVA: yes. Fixed `{ id, label, disabled? }` shape (not
845
- a generic accessor, unlike select/multiselect/button-toggle).
846
-
847
- ```html
848
- <gog-radio-group
849
- [options]="[{id:'m',label:'Male'},{id:'f',label:'Female'}]"
850
- formControlName="gender"
851
- />
852
- ```
853
-
854
- #### `gog-slider`
855
-
856
- | Input | Type | Default |
857
- | -------------------------------- | ---------------------- | ---------------------------------------------- |
858
- | `label`, `ariaLabel` | `string` | `''` |
859
- | `min`, `max`, `step` | `number` | `0`, `100`, `1` |
860
- | `showValue`, `showThumb` | `boolean` | `true` |
861
- | `errorMessage`, `errorDisplay` | | `''`, `'manual'` |
862
- | `disabled` | `boolean` | `false` |
863
- | `fullWidth` | `boolean` | `true` (ignored when `orientation="vertical"`) |
864
- | `orientation` | `GogSliderOrientation` | `'horizontal'` |
865
- | `range` | `boolean` | `false` — two thumbs; see below |
866
- | `startDisabled`, `endDisabled` | `boolean` | `false` — `range` only |
867
- | `startAriaLabel`, `endAriaLabel` | `string` | `'Minimum'` / `'Maximum'`, prefixed by `label` |
868
-
869
- Models: `value: number`, and `rangeValue: GogSliderRange` (`{ start: number; end: number }`).
870
- CVA: yes. Backed by a real `<input type="range">` (rotated via `writing-mode` for vertical), so
871
- dragging/touch/keyboard all come from the platform.
872
-
873
- ```html
874
- <gog-slider label="Volume" [min]="0" [max]="100" formControlName="volume" />
875
- ```
876
-
877
- **Range mode.** `[range]="true"` puts a second thumb on the track and switches which model is
878
- live: bind `[(rangeValue)]` instead of `[(value)]`. The two are **mutually exclusive** — `value`
879
- (and a form control's `writeValue`) is ignored while `range` is on, and vice versa.
880
-
881
- ```html
882
- <gog-slider label="Price" [range]="true" [(rangeValue)]="price" startAriaLabel="Lowest" />
883
- ```
884
-
885
- Each thumb needs its own accessible name, because one `<label>` cannot be associated with two
886
- inputs through `for`; unset, they fall back to `'Minimum'`/`'Maximum'` prefixed with `label`
887
- (`'Price Minimum'`). `startDisabled`/`endDisabled` pin one end while the other stays movable —
888
- they are ORed with `disabled` rather than overriding it, and unlike it they do not dim the whole
889
- control or cut pointer events over the track, which would take the still-enabled thumb with them.
890
-
891
- #### `gog-datepicker` / `gog-calendar`
892
-
893
- `gog-datepicker` is a field + panel; `gog-calendar` is the month grid alone (what `inline` mode
894
- renders). Native `Date` only — no date library, no adapter.
895
-
896
- | Input | Type | Default | Notes |
897
- | ----------------------------------------------------- | -------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------- |
898
- | `inputId`, `label`, `ariaLabel`, `placeholder` | `string` | `''` | |
899
- | `selectionMode` | `GogDateSelectionMode` (`'single'\|'range'`) | `'single'` | |
900
- | `min`, `max` | `Date \| null` | `null` | |
901
- | `disabledDates` | `((date: Date) => boolean) \| null` | `null` | predicate, not a list |
902
- | `defaultMonth` | `Date \| null` | `null` | which month opens when nothing is selected |
903
- | `numberOfMonths` | `number` | `1` | `2` is what makes a range picker usable |
904
- | `showTime`, `hourFormat`, `minuteStep`, `showSeconds` | | `false`, `'24'`, `1`, `false` | |
905
- | `showTodayButton` | `boolean` | `true` | **selects** today |
906
- | `showThisMonthButton` | `boolean` | `false` | only moves the _view_, leaves selection alone |
907
- | `format` | `string \| null` | `null` | display/parse pattern (`'dd.MM.yyyy'`); derived from `showTime` when unset |
908
- | `locale` | `string \| undefined` | `'en-US'` | via `GOG_CONFIG.datepicker.locale` |
909
- | `firstDayOfWeek` | `number \| undefined` | locale's own | via `GOG_CONFIG.datepicker.firstDayOfWeek` |
910
- | `allowTextInput` | `boolean` | `true` | typed text parsed against `format`; unparseable drafts don't clear the value |
911
- | `inline` | `boolean` | `false` | renders the calendar with no field/panel |
912
- | `disabled`, `fullWidth` | | `false`, `true` | |
913
- | `clearable`, `clearAriaLabel` | | `false`, `'Clear date'` | |
914
- | `errorMessage`, `errorDisplay`, `size` | | `''`, `'manual'`, `'md'` | |
915
- | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
916
- | `appendToBody`, `dropdownDirection`, `dropdownZIndex` | | `false`, `'auto'`, `null` | |
917
-
918
- Model: `value: Date | GogDateRange | null` (`GogDateRange = { start: Date | null; end: Date | null }`).
919
- CVA: yes.
920
-
921
- `gog-calendar` (usable standalone) takes most of the same date/range/time inputs directly, plus
922
- `gogDateSelect: output<GogDatepickerValue>()` fired only on a _complete_ selection. It resolves
923
- `locale` and `firstDayOfWeek` from `GOG_CONFIG.datepicker` itself, so a standalone calendar
924
- honours an app-wide locale without being handed one; its navigation, shortcut and time labels
925
- (`todayLabel`, `thisMonthLabel`, `previousMonthLabel`, `nextMonthLabel`, `previousYearLabel`,
926
- `nextYearLabel`, `hoursLabel`, `minutesLabel`, `secondsLabel`) resolve through
927
- `GOG_CONFIG.labels` the same way.
928
-
929
- Also exported for direct reuse: `formatDate(date, pattern)`, `parseDate(text, pattern)`, and a
930
- family of date-math helpers (`addDays`, `addMonths`, `isSameDay`, `isWithinBounds`, …) from
931
- `date-utils`.
932
-
933
- **Sizing.** `gog-calendar` caps itself at its own month grid — you do not need to give it a
934
- width. `--gog-calendar-max-width` (default `max-content`) is the cap, and it covers the size
935
- variants, `numberOfMonths`, `showTime` and wider locales on its own; set it to `100%` for a
936
- calendar that fills its container. This is also what sizes `inline` mode, because `inline` is
937
- `gog-calendar` with a border and nothing else. The dropdown panel is separate:
938
- `--gog-datepicker-panel-width`, also `max-content`.
939
-
940
- ```html
941
- <gog-datepicker label="Birth date" [(value)]="birthDate" [max]="today" />
942
- <gog-datepicker selectionMode="range" [(value)]="stayRange" [numberOfMonths]="2" />
943
- ```
944
-
945
- ### Display, feedback & status
946
-
947
- #### `gog-icon`
948
-
949
- | Input | Type | Default |
950
- | ------------ | --------------------- | -------------------------------------------------- |
951
- | `name` | `GogIconName` | `'close'` |
952
- | `template` | `TemplateRef \| null` | `null` custom markup instead of the built-in SVG |
953
- | `title` | `string` | `''` |
954
- | `ariaHidden` | `boolean` | `true` |
955
-
956
- The package ships **41** glyphs (`GogBuiltinIconName`), all from [Lucide](https://lucide.dev)
957
- and inlined so the package keeps zero runtime dependencies:
958
-
959
- | Group | Names |
960
- | ----------------- | ------------------------------------------------------------------------------------------------------ |
961
- | Chevrons & arrows | `chevron-up`, `chevron-down`, `chevron-left`, `chevron-right`, `arrow-left`, `arrow-right` |
962
- | Confirm & dismiss | `check`, `close`, `checkbox`, `checkbox-checked` |
963
- | Status | `success`, `error`, `warning`, `info` |
964
- | Sorting | `sort`, `sort-up`, `sort-down`, `filter` |
965
- | Actions | `search`, `plus`, `minus`, `trash`, `pencil`, `copy`, `download`, `upload`, `refresh`, `external-link` |
966
- | Chrome | `menu`, `more-horizontal`, `more-vertical`, `settings` |
967
- | Objects & state | `user`, `lock`, `mail`, `calendar`, `clock`, `eye`, `eye-off`, `star`, `star-filled` |
968
-
969
- `star` / `star-filled` is the one outline/filled pair, for a rating or favourite **toggle** —
970
- the same reason `checkbox` / `checkbox-checked` exists. The set is otherwise outline-only on
971
- purpose; a blanket solid duplicate of every glyph would double the payload for a distinction
972
- almost nothing needs. If you want a filled variant of something else, register it with
973
- `provideGogIcons`.
974
-
975
- `Object.keys(ICON_DEFS)` is the runtime list, if you need to enumerate them (an icon picker, a
976
- gallery). Do not hand-copy the names into an array — that is what goes stale.
977
-
978
- ```html
979
- <gog-icon name="calendar" />
980
- ```
981
-
982
- ##### Registering your own icons `provideGogIcons(...)`
983
-
984
- `name` is typed `GogIconName = GogBuiltinIconName | (string & {})`: the built-ins autocomplete,
985
- and any name you register is accepted. **This is the supported way to use your own icon set** —
986
- prefer it over the `template` input, which costs an `<ng-template>` at every use site and is for
987
- one-offs.
988
-
989
- ```ts
990
- // app.config.ts
991
- import { provideGogIcons } from '@guildofgleks/ui';
992
-
993
- providers: [
994
- provideGogIcons({
995
- cart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">…</svg>',
996
- rocket: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">…</svg>',
997
- }),
998
- ];
999
- ```
1000
-
1001
- ```html
1002
- <gog-icon name="cart" />
1003
- <gog-tag iconName="cart">In basket</gog-tag>
1004
- <!-- works anywhere an icon *name* is taken -->
1005
- ```
1006
-
1007
- - **Registered names win over built-ins of the same name** — that is how you replace the
1008
- library's checkmark or chevrons across every component at once, without touching any of them.
1009
- - **Nested `provideGogIcons(...)` layers onto the parent set** rather than replacing it, the same
1010
- as `provideGogConfig`: a lazy route can register only what it uses.
1011
- - **An unknown name renders nothing and warns in dev mode; it never throws.** An icon is
1012
- decoration — failing the render over a typo would be the worse outcome.
1013
- - **`GOG_ICONS`** is the `InjectionToken<Readonly<Record<string, string>>>` behind it, exported
1014
- for the one case `provideGogIcons` does not cover: reading the registered set back
1015
- (`inject(GOG_ICONS)`) to enumerate it in an icon picker. Provide it through
1016
- `provideGogIcons(...)` rather than directly — the helper is what layers a child injector's
1017
- icons onto the parent's instead of replacing them.
1018
- - **Write the SVG for inheritance:** a `viewBox`, `stroke="currentColor"` (or `fill`), and no
1019
- width/height — `gog-icon` drives size and stroke width from the `--gog-icon-*` tokens, so a
1020
- registered icon scales and colours like a built-in.
1021
- - **Security:** the markup is inserted with `bypassSecurityTrustHtml` (Angular's HTML sanitizer
1022
- strips SVG, so there is no alternative). That is fine for static icon markup you authored;
1023
- **never** build a registered icon string from user input or fetch it at runtime unsanitized.
1024
-
1025
- #### `[gogButton]` the link-flavoured button
1026
-
1027
- `gog-button` renders its own `<button>`, so it can never _be_ a link. `[gogButton]` inverts that:
1028
- the element stays yours and the directive only gives it the look.
1029
-
1030
- ```html
1031
- <a gogButton routerLink="/pricing">See pricing</a>
1032
- <a gogButton variant="ghost" href="https://example.com" target="_blank" rel="noreferrer">Docs</a>
1033
- <button gogButton variant="outline" size="sm" type="submit">Save</button>
1034
- <a gogButton fullWidth routerLink="/checkout">Checkout</a>
1035
- ```
1036
-
1037
- | Input | Type | Default |
1038
- | ----------- | ------------------------ | ---------------------------------------- |
1039
- | `variant` | `GogVariant` | `'primary'` |
1040
- | `severity` | `GogSeverity` | `'accent'`; same as `gog-button` |
1041
- | `size` | `GogSize \| undefined` | `'md'`; via `GOG_CONFIG.control.size` |
1042
- | `fullWidth` | `boolean` (bare attr ok) | `false` |
1043
- | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1044
-
1045
- Selector is `a[gogButton], button[gogButton]` deliberately not a bare `[gogButton]`, because on
1046
- a `<div>` the result looks like a button and is invisible to the keyboard and to assistive tech.
1047
-
1048
- **Which to reach for.** `gog-button` for a button that acts on the page: it owns `loading` (a
1049
- centred spinner it projects), `debounce` click throttling and the `gogClick` output, none of which
1050
- a bare element can provide. `[gogButton]` when the element must be a link, or when you need to
1051
- keep directives of your own on it — `routerLink`, `href`, `target`, `download`, `type="submit"`
1052
- and anything else keep working because they were never brokered through an input in the first
1053
- place. That is also why the library still has no `@angular/router` dependency.
1054
-
1055
- Two things it deliberately does not do: no `disabled` on an `<a>` (there is no such thing — drop
1056
- the `href` or render a real `<button>`), and no loading state (the spinner is a projected child a
1057
- directive cannot add without taking over the element's content).
1058
-
1059
- #### `[gogBadge]` directive, not a component
1060
-
1061
- Decorates an existing element (a button, an icon, an avatar) with a count/status dot — it never
1062
- wraps its host.
1063
-
1064
- | Input | Type | Default |
1065
- | ---------------- | --------------------------------------------------------------------------- | -------------------------------- |
1066
- | `gogBadge` | `string \| number \| null` | `null` — the content |
1067
- | `badgePosition` | `GogBadgePosition` (`'top-end'\|'top-start'\|'bottom-end'\|'bottom-start'`) | `'top-end'` |
1068
- | `badgeVariant` | `GogTagVariant` | `'danger'` |
1069
- | `badgeDot` | `boolean` | `false` — bare dot, no text |
1070
- | `badgeMax` | `number` | `99` beyond this, renders `N+` |
1071
- | `badgeHidden` | `boolean` | `false` |
1072
- | `badgeAriaLabel` | `string` | `''` |
1073
-
1074
- Renders **nothing** when the value is `0`, `null` or empty and `badgeDot` is off "0" badges
1075
- are impossible by design.
1076
-
1077
- ```html
1078
- <gog-button gogBadge="12" badgeAriaLabel="12 unread">Inbox</gog-button>
1079
- <gog-icon name="info" gogBadge badgeDot />
1080
- ```
1081
-
1082
- #### `gog-chip`
1083
-
1084
- | Input | Type | Default |
1085
- | ------------------------------ | ----------------------------------- | ---------------------------------------- |
1086
- | `size` | `GogSize` | `'md'` |
1087
- | `shape` | `GogTagShape` (`'rounded'\|'pill'`) | `'rounded'` |
1088
- | `disabled`, `clickable` | `boolean` | `false`, `true` |
1089
- | `selected` | `boolean \| null` (two-way) | `null` — see below |
1090
- | `removable` | `boolean` | `false` |
1091
- | `fullWidth` | `boolean` | `false` |
1092
- | `ariaLabel`, `removeAriaLabel` | `string` | `''`, `'Remove chip'` |
1093
- | `avatarUrl`, `avatarAlt` | `string \| null` / `string` | `null`, `''` |
1094
- | `iconName` | `GogIconName \| null` | `null` |
1095
- | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1096
-
1097
- Outputs: `gogClick: MouseEvent | KeyboardEvent`, `gogRemove: void`.
1098
-
1099
- ```html
1100
- <gog-chip [avatarUrl]="user.photo" [removable]="true" (gogRemove)="removeUser(user)"
1101
- >{{ user.name }}</gog-chip
1102
- >
1103
- ```
1104
-
1105
- **`selected` makes it a filter chip** (21.9.0) a chip you toggle on and off rather than press.
1106
- It is tri-state, and `null` is the default so nothing about an existing chip changes: no
1107
- `aria-pressed`, no selected look, activation only emits `gogClick`. Set it to `false` and the chip
1108
- is a toggle that is off (`aria-pressed="false"` — a chip with no `aria-pressed` at all is not a
1109
- toggle to a screen reader, so "off" has to be stated); `true` and it is on, which draws an inset
1110
- ring from `--gog-chip-selected-shadow`. A ring rather than a fill because `:hover` and `:active`
1111
- already own the chip's background and the selection has to survive both.
1112
-
1113
- It is a two-way `model`, so the chip flips it on click, Enter and Space — a row of filters needs
1114
- no click handler:
1115
-
1116
- ```html
1117
- @for (f of filters; track f.label) {
1118
- <gog-chip [(selected)]="f.on">{{ f.label }}</gog-chip>
1119
- }
1120
- ```
1121
-
1122
- `gogClick` still fires, **after** the flip, so a handler reading `selected()` sees the new value.
1123
- Drive the state from that handler instead and you want a one-way `[selected]`, or the two writes
1124
- cancel out. A `disabled` chip keeps the ring but drops `aria-pressed`, which needs the
1125
- `role="button"` a disabled chip does not carry "selected, and currently unavailable" is a real
1126
- state and hiding it would leave it announced and invisible.
1127
-
1128
- #### `gog-tag`
1129
-
1130
- | Input | Type | Default |
1131
- | ----------- | --------------------- | ----------- |
1132
- | `variant` | `GogTagVariant` | `'info'` |
1133
- | `size` | `GogSize` | `'md'` |
1134
- | `shape` | `GogTagShape` | `'rounded'` |
1135
- | `iconName` | `GogIconName \| null` | `null` |
1136
- | `fullWidth` | `boolean` | `false` |
1137
-
1138
- Slot: `<ng-template gogTagIcon>` for custom icon markup.
1139
-
1140
- ```html
1141
- <gog-tag variant="success">Active</gog-tag>
1142
- ```
1143
-
1144
- #### `gog-spinner` / `gog-spinner-overlay`
1145
-
1146
- | Input | Type | Default |
1147
- | -------------------------------- | ------------------------------------------------- | ------------------------------------------- |
1148
- | `size` | `GogSize` | `'md'` |
1149
- | `variant` | `GogSpinnerVariant` (`'runic'\|'ring'\|'custom'`) | unset — see below |
1150
- | `ariaLabel` | `string` | `'Loading'` |
1151
- | `overlay` (spinner only) | `boolean` | `false` |
1152
- | `loading` (spinner-overlay only) | `boolean` | `false` — toggles the overlay + `aria-busy` |
1153
-
1154
- `variant="custom"` renders your own projected markup, still inheriting the size wrapper and
1155
- `--gog-spinner-color` theming.
1156
-
1157
- **To replace the spinner everywhere at once, pass a component to `GOG_CONFIG`** — including the
1158
- three places you cannot reach with an input: `gog-button`'s and `gog-autocomplete`'s loading
1159
- states, and the spinner `gog-table` draws in place of its rows.
1160
-
1161
- ```ts
1162
- provideGogConfig({ spinner: { component: HouseLoaderComponent } });
1163
- ```
1164
-
1165
- It renders inside the same size wrapper as the built-ins, so it keeps the sizing, the overlay
1166
- behaviour, `role="status"` and the accessible name — only the visual is yours. An instance's own
1167
- `variant` still wins over it, so `<gog-spinner variant="ring">` is a ring in an app that has set
1168
- a component: a default does not overrule something asked for explicitly.
1169
-
1170
- **Neither component's `variant` has a default value**, and on `gog-spinner-overlay` that is the
1171
- whole of the 21.10.0 fix: the overlay forwards its `variant` to the spinner it wraps, so a default
1172
- there would have been an instance overruling the config on every overlay ever rendered — which is
1173
- exactly what happened before, leaving the one spinner that covers a whole region on the built-in
1174
- look while every other spinner in the app was the house one. `size` and `ariaLabel` keep their
1175
- defaults: neither has a config key to fall through to.
1176
-
1177
- ```html
1178
- <gog-spinner-overlay [loading]="isLoading()">
1179
- <app-content-that-loads />
1180
- </gog-spinner-overlay>
1181
- ```
1182
-
1183
- #### `gog-skeleton`
1184
-
1185
- | Input | Type | Default |
1186
- | ----------------- | -------------------------------------------------- | ---------------------------------------------------- |
1187
- | `shape` | `GogSkeletonShape` (`'text'\|'circle'\|'rect'`) | `'text'` |
1188
- | `size` | `GogSize` | `'md'` |
1189
- | `animation` | `GogSkeletonAnimation` (`'pulse'\|'wave'\|'none'`) | `'pulse'` |
1190
- | `width`, `height` | `string \| null` | `null` |
1191
- | `lines` | `number` | `1` `shape="text"` only, last line renders shorter |
1192
- | `rounded` | `boolean` | `true` |
1193
- | `ariaLabel` | `string \| null` | `null` decorative (no `role`) unless set |
1194
-
1195
- ```html
1196
- <gog-skeleton shape="text" [lines]="3" /> <gog-skeleton shape="circle" width="48px" />
1197
- ```
1198
-
1199
- #### `gog-progressbar`
1200
-
1201
- | Input | Type | Default |
1202
- | ----------------- | ---------------------------------------------------------------------------- | --------------- |
1203
- | `value`, `buffer` | `number` (0–100, clamped) | `0` |
1204
- | `mode` | `GogProgressbarMode` (`'determinate'\|'indeterminate'\|'buffer'`) | `'determinate'` |
1205
- | `variant` | `GogProgressbarVariant` (`'accent'\|'success'\|'danger'\|'warning'\|'info'`) | `'accent'` |
1206
- | `size` | `GogSize` | `'md'` |
1207
- | `showValue` | `boolean` | `false` |
1208
- | `ariaLabel` | `string` | `''` |
1209
-
1210
- ```html
1211
- <gog-progressbar mode="indeterminate" ariaLabel="Loading" />
1212
- <gog-progressbar mode="buffer" [value]="42" [buffer]="70" />
1213
- ```
1214
-
1215
- **The fill's end is marked by two hairlines** (21.10.0), `--gog-progressbar-edge-color` over
1216
- `--gog-progressbar-edge-backing-color`, each `--gog-progressbar-edge-width` wide. That boundary is
1217
- the value — `showValue` is off by default — and the fill and the track cannot carry it themselves:
1218
- in every shipped theme the five fills straddle mid-luminance, so no one track colour clears WCAG
1219
- 1.4.11's 3:1 against all of them. Two tones always do, and `check:contrast` gates the pair. Retint
1220
- them per theme if you like; keep them a *pair* whose tones sit on opposite sides of the middle, or
1221
- the marker disappears on whichever fill it happens to match.
1222
-
1223
- #### `gog-divider`
1224
-
1225
- | Input | Type | Default |
1226
- | ------------- | --------------------------------------------------- | -------------- |
1227
- | `orientation` | `GogOrientation` | `'horizontal'` |
1228
- | `variant` | `GogDividerVariant` (`'solid'\|'dashed'\|'dotted'`) | `'solid'` |
1229
- | `inset` | `boolean` | `false` |
1230
-
1231
- Label is projected content, not an input — put an icon or a `gog-tag` inside it if needed.
1232
-
1233
- ```html
1234
- <gog-divider>OR</gog-divider>
1235
- ```
1236
-
1237
- #### `gogRipple` directive, not a component
1238
-
1239
- A pointer-position wash that grows from where you pressed and fades when you let go. Drop it on
1240
- any element you already have — it adds no wrapper and changes no layout.
1241
-
1242
- | Input | Type | Default |
1243
- | ---------------- | --------- | ----------------------------------------------------------- |
1244
- | `rippleDisabled` | `boolean` | `false` |
1245
- | `rippleCentred` | `boolean` | `false` — start from the middle instead of from the pointer |
1246
-
1247
- ```html
1248
- <button gogRipple>Press me</button>
1249
- <div gogRipple rippleCentred class="tile">A tile</div>
1250
- ```
1251
-
1252
- Four things suppress it, none of which you have to wire up: `rippleDisabled`, a host carrying
1253
- `disabled`, a host carrying `aria-disabled="true"`, and `prefers-reduced-motion: reduce` — the
1254
- last one **suppressed outright, not shortened**. Keyboard activation (`Enter`/`Space`) is always
1255
- centred, because a key press carries no coordinates.
1256
-
1257
- **Put it on the element that paints the surface.** The wash lives in its own layer that clips
1258
- itself the host is never given `overflow: hidden`, so a `gogBadge` on the same element is not
1259
- clipped — and that layer takes its corner radius from its host with `border-radius: inherit`. On a
1260
- wrapper whose _child_ paints the rounded background, the layer inherits the wrapper's radius (very
1261
- often `0`) and the wash squares off at the corners.
1262
-
1263
- Tokens: `--gog-ripple-color` (`currentColor`, so the wash reads as the surface's own foreground on
1264
- a filled surface and a ghost one alike), `--gog-ripple-opacity`, `--gog-ripple-enter-duration`,
1265
- `--gog-ripple-exit-duration`, `--gog-ripple-easing`. All five are ordinary inherited custom
1266
- properties, so setting one anywhere above the host is the per-instance override.
1267
-
1268
- #### Turning the ripple on for the library's own components
1269
-
1270
- You do **not** add `gogRipple` to a `gog-*` component: each one already owns the element that
1271
- paints its surface, so it wires its own. What you do is switch it on, once:
1272
-
1273
- ```ts
1274
- provideGogConfig({ ripple: { enabled: true } });
1275
- ```
1276
-
1277
- That covers `gog-button`, `[gogButton]`, `gog-button-toggle-group`, `gog-chip`, `gog-tabs`
1278
- headers, `gog-accordion` headers, `gogCollapsibleTrigger`, `gogMenuItem`, and the options inside
1279
- `gog-select` / `gog-multiselect` / `gog-autocomplete`. `gog-paginator` follows because its page
1280
- buttons are `gog-button`s.
1281
-
1282
- **Off by default**, so adding the ripple to the library changed the look of nothing. Every one of
1283
- those takes a `ripple` input that beats the config in both directions: `[ripple]="false"` opts one
1284
- control out of an app-wide on, `[ripple]="true"` opts one in without switching the app over.
1285
-
1286
- Not covered, and deliberately: `gog-table` rows and `gogCardLink`. A row and a card are hundreds
1287
- of pixels wide, so the wave has to travel the whole surface and reads as a flash rather than as
1288
- feedback at the point you pressed and a table renders one directive per row, with no
1289
- virtualization in this library yet. Put `gogRipple` on them yourself if you disagree.
1290
-
1291
- A chip that is not `clickable`, or is `disabled`, never ripples whatever the config says: a label
1292
- answering a press is a promise it cannot keep.
1293
-
1294
- #### `gogTooltip` — directive, not a component
1295
-
1296
- Drop on any element — a `gog-*` component's host tag or a plain native one.
1297
-
1298
- | Input | Type | Default |
1299
- | --------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ |
1300
- | `gogTooltip` | `string \| TemplateRef \| null` | `null` content |
1301
- | `gogTooltipPosition` | `GogTooltipPosition` (`'auto'\|'top'\|'bottom'\|'left'\|'right'`) | `'auto'`; via `GOG_CONFIG.tooltip.position` |
1302
- | `gogTooltipShowDelay` | `number \| undefined` | `300`; via `GOG_CONFIG.tooltip.showDelay` |
1303
- | `gogTooltipHideDelay` | `number \| undefined` | `100`; via `GOG_CONFIG.tooltip.hideDelay` |
1304
- | `gogTooltipDisabled` | `boolean` | `false` |
1305
- | `gogTooltipClass` | `string` | `''` class on the bubble itself, since it's portaled to `<body>` |
1306
-
1307
- ```html
1308
- <button gogTooltip="Save changes">💾</button> <gog-chip [gogTooltip]="hintTemplate">Beta</gog-chip>
1309
- ```
1310
-
1311
- ### Layout & navigation
1312
-
1313
- #### `gog-accordion`
1314
-
1315
- | Input | Type | Default |
1316
- | --------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
1317
- | `items` | `GogAccordionItem[]` (`{ id, title, disabled?, [key: string]: unknown }`) | `[]` |
1318
- | `size` | `GogSize` | `'lg'` (not `'md'` see conventions) |
1319
- | `expandFirst`, `multi`, `loading` | `boolean` | `false` |
1320
- | `skeletonCount` | `number` | `3` rows shown while `loading` and `items` is still empty |
1321
- | `showChevron` | `boolean` | `true` |
1322
- | `headingLevel` | `2\|3\|4\|5\|6 \| undefined` | `undefined` — wraps headers in `role="heading"` when set |
1323
- | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1324
-
1325
- Model: `openIds: ReadonlySet<string | number>`. Output: `gogToggle: { item, open }`.
1326
-
1327
- Slots: `<ng-template gogAccordionHeader let-item let-open="open">`,
1328
- `<ng-template gogAccordionContent let-item>`, `<ng-template gogAccordionChevron let-item let-open="open">`.
1329
- This is the library's canonical example of the slot pattern — copy its shape for anything similar.
1330
-
1331
- ```html
1332
- <gog-accordion [items]="faqItems" [multi]="true">
1333
- <ng-template gogAccordionContent let-item>{{ item.answer }}</ng-template>
1334
- </gog-accordion>
1335
- ```
1336
-
1337
- #### `gog-collapsible` + `gogCollapsibleTrigger` / `gogCollapsibleContent`
1338
-
1339
- **Headless primitive** owns no markup at all, just open/close state plus two attribute
1340
- directives you place on your own elements. Use this when `gog-accordion`'s opinionated markup
1341
- doesn't fit (e.g. a sidebar nav group).
1342
-
1343
- | Input (on `gog-collapsible`) | Type | Default |
1344
- | ---------------------------- | --------- | ---------------------------------------------------------- |
1345
- | `disabled` | `boolean` | `false` |
1346
- | `collapseOnFocusOut` | `boolean` | `false` close once focus leaves both trigger and content |
1347
-
1348
- Model: `open: boolean`.
1349
-
1350
- ```html
1351
- <gog-collapsible [(open)]="isOpen">
1352
- <button gogCollapsibleTrigger>Advanced options</button>
1353
- <div gogCollapsibleContent>
1354
- <!-- any markup -->
1355
- </div>
1356
- </gog-collapsible>
1357
- ```
1358
-
1359
- **The trigger can be any element.** On a `<button>` or `<a href>` the directive adds only the
1360
- ARIA wiring, because the browser already handles focus and keys. On anything else — a `<div>`, a
1361
- `<span>` it also supplies `role="button"`, `tabindex="0"` and Enter/Space, so the control it
1362
- announces is one a keyboard can actually reach. If you set `role` or `tabindex` yourself, the
1363
- directive leaves both alone: you have said what the element is.
1364
-
1365
- `gogCollapsibleTrigger` takes a **`ripple`** input of its own (`boolean | undefined`, `false`, via
1366
- `GOG_CONFIG.ripple.enabled`) — the trigger is your element, but the directive owns the ripple so
1367
- you do not have to add `gogRipple` beside it.
1368
-
1369
- An open panel is as tall as its content — `--gog-collapsible-max-height` defaults to
1370
- `max-content`. Set it to a length on an instance to cap one deliberately; the panel is
1371
- `overflow: hidden`, so a cap **clips** rather than scrolls. (Before 21.4.4 that default was
1372
- `480px`, which clipped taller panels silently.)
1373
-
1374
- #### `gog-tabs` + `gog-tab`
1375
-
1376
- | Input (on `gog-tabs`) | Type | Default |
1377
- | ------------------------ | ------------------------------------------------------ | ---------------------------------------------------- |
1378
- | `align` | `GogTabsAlign` (`'start'\|'center'\|'end'\|'stretch'`) | `'start'` |
1379
- | `orientation` | `GogOrientation` | `'horizontal'` |
1380
- | `size` | `GogSize` | `'md'` |
1381
- | `fullWidth`, `ariaLabel` | | `false`, `''` |
1382
- | `scrollActiveIntoView` | `boolean` | `true` |
1383
- | `showScrollTrack` | `boolean \| undefined` | follows `scrollActiveIntoView` (hidden when it's on) |
1384
- | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1385
-
1386
- Model: `activeIndex: number`. Output: `gogTabChange: number`.
1387
-
1388
- | Input (on `gog-tab`) | Type | Default |
1389
- | -------------------- | --------------------- | ------- |
1390
- | `label` | `string` | `''` |
1391
- | `iconName` | `GogIconName \| null` | `null` |
1392
- | `disabled` | `boolean` | `false` |
1393
-
1394
- Slots: `<ng-template gogTabHeader let-tab let-active="active">` on `gog-tabs` for custom header
1395
- markup; `<ng-template gogTabContent>` **inside** a `gog-tab` to make that tab's content **lazy**
1396
- (built on first activation, then kept alive) instead of the default (rendered immediately,
1397
- hidden via `[hidden]` while inactive — preserves scroll/input state).
1398
-
1399
- ```html
1400
- <gog-tabs [(activeIndex)]="tabIndex">
1401
- <gog-tab label="Profile"><app-profile /></gog-tab>
1402
- <gog-tab label="Report" iconName="info">
1403
- <ng-template gogTabContent><app-expensive-report /></ng-template>
1404
- </gog-tab>
1405
- </gog-tabs>
1406
- ```
1407
-
1408
- #### `gog-card` + `gogCardHeader` / `gogCardMedia` / `gogCardFooter` / `gogCardLink`
1409
-
1410
- A surface for one self-contained thing — a product tile, a summary, a search result.
1411
-
1412
- | Input | Type | Default |
1413
- | --------------- | -------------------------------------------------------- | --------------------------------------- |
1414
- | `variant` | `GogSurfaceVariant` (`'outlined'\|'elevated'\|'filled'`) | `'outlined'` |
1415
- | `size` | `GogSize` | `'md'`drives padding and the row gap |
1416
- | `disabled` | `boolean` (bare attribute works) | `false` |
1417
- | `loading` | `boolean` (bare attribute works) | `false` |
1418
- | `skeletonLines` | `number` | `2` — body lines shown while `loading` |
1419
-
1420
- No outputs. Slots, all **attribute** directives on your own elements (not `ng-template`):
1421
- `gogCardHeader`, `gogCardMedia`, `gogCardFooter`, `gogCardLink`. Layout order is fixed by the
1422
- component — media, heading, body (the default slot), footer — not by the order you write them.
1423
-
1424
- ```html
1425
- <gog-card>
1426
- <img gogCardMedia [src]="person.photo" alt="" />
1427
- <h3 gogCardHeader><a gogCardLink [routerLink]="['/people', person.id]">{{ person.name }}</a></h3>
1428
- <p>{{ person.role }}</p>
1429
- <div gogCardFooter>
1430
- <gog-button size="xsm" (gogClick)="shortlist(person)">Shortlist</gog-button>
1431
- </div>
1432
- </gog-card>
1433
- ```
1434
-
1435
- - **`gogCardHeader` names the card.** The card reads that element's `id` (minting one if it has
1436
- none) and points its own `aria-labelledby` at it, with `role="group"`. A card with no header
1437
- gets neither — an unnamed group is noise, not structure. The heading level is yours; the visual
1438
- size comes from `--gog-card-heading-font-size` regardless of it.
1439
- - **There is no `interactive` input, and no `gogClick` output.** A card becomes interactive by
1440
- _containing_ a `gogCardLink`, which stretches that link's hit area over the whole surface. The
1441
- link stays yours: `routerLink`, `href`, `target`, middle-click, "open in new tab" and Enter all
1442
- behave normally, and the focus ring is drawn around the card. `gogCardLink` only applies to
1443
- `<a>` and `<button>` — on a `<div>` it does nothing, deliberately.
1444
- - **Other controls inside an interactive card still get their own clicks.** A footer button, a
1445
- checkbox, a second link: each sits above the stretched hit area automatically.
1446
- - Two costs of the pattern, inherent to it: text in the card cannot be selected by dragging, and
1447
- a second link is reachable by keyboard but not by clicking the surface around it.
1448
- - **`loading`** replaces the content with a title bar plus `skeletonLines` text lines and sets
1449
- `aria-busy`; **`disabled`** dims the card, sets `aria-disabled`, and takes the card link out of
1450
- the tab order. Both make the link non-clickable. For a _refresh_ of a card that already has
1451
- content, project a `gog-spinner-overlay` instead — `loading` is the first-paint treatment.
1452
- - `gogCardMedia` runs full-bleed to the card's edges, and rounds into its top corners when it is
1453
- the first element in the card.
1454
-
1455
- #### `gog-panel` + `gogPanelHeader` / `gogPanelFooter`
1456
-
1457
- A titled region of a page a settings section, a dashboard area, a form group.
1458
-
1459
- | Input | Type | Default |
1460
- | --------------- | -------------------------------- | ------------ |
1461
- | `variant` | `GogSurfaceVariant` | `'elevated'` |
1462
- | `size` | `GogSize` | `'lg'` |
1463
- | `collapsible` | `boolean` (bare attribute works) | `false` |
1464
- | `disabled` | `boolean` (bare attribute works) | `false` |
1465
- | `loading` | `boolean` (bare attribute works) | `false` |
1466
- | `skeletonLines` | `number` | `3` |
1467
-
1468
- Model: `open: boolean` (default `true`, ignored while `collapsible` is off). No outputs beyond
1469
- `openChange`. Slots: `gogPanelHeader`, `gogPanelFooter` — attribute directives on your elements.
1470
-
1471
- ```html
1472
- <gog-panel [collapsible]="true" [(open)]="notificationsOpen">
1473
- <h2 gogPanelHeader>Notifications</h2>
1474
- <gog-checkbox label="Email digest" [(checked)]="emailDigest" />
1475
- <div gogPanelFooter><gog-button size="xsm">Save</gog-button></div>
1476
- </gog-panel>
1477
- ```
1478
-
1479
- - **It is a landmark.** With a `gogPanelHeader` it renders `role="region"` named by that heading —
1480
- which is why the panel gets one and `gog-card` gets `role="group"`: a handful of named regions
1481
- is how a page is navigated, a landmark per card would bury that list.
1482
- - **Collapsing composes `gog-collapsible`**, so the state, the id wiring and the animation are the
1483
- library's existing ones. The heading stays a heading: the toggle is a separate `<button>` named
1484
- by it through `aria-labelledby`, with its hit area stretched across the header row so clicking
1485
- the title works for the pointer. Without a header the toggle falls back to
1486
- `GOG_CONFIG.labels.togglePanel` (default `'Toggle section'`).
1487
- - **A non-collapsible panel does not clip.** It undoes the collapse geometry it inherits,
1488
- `overflow` included, so a dropdown or menu opened inside it escapes the panel's box. A
1489
- _collapsible_ one does clip while animating, exactly like `gog-collapsible` — prefer
1490
- `[appendToBody]` for an overlay inside one.
1491
- - **`loading` keeps the heading and the footer** and replaces only the body: a page section is
1492
- titled before its content arrives, and blanking the title would move the layout twice.
1493
- - **The surface is never itself a link** — there is no `gogPanelLink`. Controls live inside a
1494
- panel, and a region that is a link cannot hold them. Use `gog-card` for that.
1495
-
1496
- #### `gog-paginator`
1497
-
1498
- | Input | Type | Default |
1499
- | ------------------------------- | ------------------------------------------------ | -------------------------------------------------- |
1500
- | `fullWidth`, `totalPages` | `boolean`, `number` | `true`, `1` |
1501
- | `rangeMode` | `GogPaginatorRangeMode` (`'window'\|'ellipsis'`) | `'window'` see note |
1502
- | `visiblePages` | `number` | `5` `'window'` mode only |
1503
- | `showFirstPage`, `showLastPage` | `boolean` | `false` `'window'` mode only |
1504
- | `siblingCount` | `number` | `2` — `'ellipsis'` mode only |
1505
- | `size` | `GogSize` | `'sm'` |
1506
- | `disabled`, `ariaLabel` | | `false`, `'Pagination'` |
1507
- | `totalRecords` | `number \| null` | `null` — see below |
1508
- | `pageSize` | `model<number>` | `10` two-way bindable |
1509
- | `showPageSizeSelect` | `boolean \| undefined` | `false`; via `GOG_CONFIG.paginator` |
1510
- | `pageSizeOptions` | `number[] \| undefined` | `[10, 20, 30, 40, 50]`; via `GOG_CONFIG.paginator` |
1511
-
1512
- The step buttons (`'Previous page'`/`'Next page'`) and the per-page names are configured, not
1513
- input-driven: `GOG_CONFIG.labels.previousPage`/`nextPage`, and `labels.page`, a
1514
- `(page: number, isCurrent: boolean) => string` formatter defaulting to
1515
- `` `Page ${page}, current page` `` / `` `Go to page ${page}` ``.
1516
-
1517
- Models: `page: number` (1-based, self-clamps) and `pageSize: number`.
1518
-
1519
- **Give it `totalRecords` instead of `totalPages` when you know the row count** it then derives
1520
- the page count from `pageSize` itself, which is what removes the
1521
- `computed(() => Math.ceil(total / size))` a consumer would otherwise have to write _and_ keep in
1522
- sync with the rows-per-page select:
1523
-
1524
- ```html
1525
- <gog-paginator
1526
- [(page)]="page"
1527
- [(pageSize)]="size"
1528
- [totalRecords]="items().length"
1529
- [showPageSizeSelect]="true"
1530
- />
1531
- ```
1532
-
1533
- `totalPages` still works and is the right input when the server tells you a page count directly;
1534
- `totalRecords` wins when both are set. Changing the page size always returns to page 1 — "page 5"
1535
- of 10-row pages is not "page 5" of 50-row ones, so clamping alone would leave the user somewhere
1536
- they never asked to be.
1537
-
1538
- `'window'`: a fixed number of page buttons that slides to keep the current page centered.
1539
- `'ellipsis'`: first/last pinned, `siblingCount` around the current page, "…" fills the gap
1540
- (what `gog-table`'s built-in pagination uses).
1541
-
1542
- ```html
1543
- <gog-paginator [(page)]="page" [totalPages]="totalPages" />
1544
- ```
1545
-
1546
- #### `gog-table<T>`
1547
-
1548
- | Input | Type | Default |
1549
- | ----------------------------- | ----------------------------- | ----------------------------------- |
1550
- | `value` | `T[]` | `[]` |
1551
- | `fullWidth` | `boolean` | `true` |
1552
- | `pageSize` | `model<number>` | `0` (no pagination)two-way |
1553
- | `showPageSizeSelect` | `boolean \| undefined` | `false`; forwarded to the paginator |
1554
- | `pageSizeOptions` | `number[] \| undefined` | `[10, 20, 30, 40, 50]`; forwarded |
1555
- | `showRowNumbers`, `showTotal` | `boolean` | `true`, `false` |
1556
- | `emptyPlaceholder` | `string` | `'-'` |
1557
- | `paginatorPosition` | `'left'\|'center'\|'right'` | `'center'` |
1558
- | `totalPosition` | `'left'\|'right'\|'opposite'` | `'opposite'` |
1559
- | `loading` | `boolean` | `false` |
1560
- | `showColumnBorders` | `boolean` | `false` |
1561
- | `stickyHeader` | `boolean` | `false` — pair with `maxHeight` |
1562
- | `maxHeight` | `string \| null` | `null` — any CSS length |
1563
- | `size` | `GogSize` | `'lg'` (row density — not `'md'`) |
1564
- | `lazy` | `boolean` | `false` — see below |
1565
- | `totalRecords` | `number \| null` | `null` — `lazy` only |
1566
- | `selectionMode` | `GogTableSelectionMode` | `'none'` |
1567
- | `selection` | `model<T[]>` | `[]` two-way bindable |
1568
- | `dataKey` | `string` | `''` — row identity field |
1569
- | `showSelectionColumn` | `boolean` | `true` (once selection is on) |
1570
- | `interactiveRows` | `boolean` | `false` |
1571
-
1572
- Outputs: `gogSortChange: GogTableSortEvent` (`{ field, direction }`, `{ field: '', direction:
1573
- null }` when the third click clears it), `gogPageChange: number` (1-based; **does not fire** on
1574
- first render, nor for the page reset a new sort causes — that reset belongs to the sort),
1575
- `gogRowClick: GogTableRowClickEvent<T>` (`{ row, index, originalEvent }`).
1576
-
1577
- **`fullWidth` also picks the layout algorithm.** Left at its default the table is `100%` wide with
1578
- `table-layout: fixed`; since 21.6.0 `[fullWidth]="false"` makes it `fit-content` with
1579
- `table-layout: auto`, so the columns are measured against their content instead of splitting the
1580
- total evenly. Before 21.6.0 that split clipped the widest header, and a `width` on the column was
1581
- the workaround under auto layout a stated `width` is a suggestion weighed against content
1582
- rather than a hard split, so those can usually go.
1583
-
1584
- **`stickyHeader` needs `maxHeight`** (both since 21.6.0 for the pairing). A sticky element
1585
- resolves against its nearest scroll container, and the table wraps itself in a `gog-scroll`;
1586
- once that scroller moves on either axis it is a scroll container on _both_, because CSS coerces
1587
- `overflow-y: visible` to `auto` beside a scrolling `overflow-x` (and `clip` to `hidden`). So the
1588
- header can only ever stick to something inside the table — and without `maxHeight` that viewport
1589
- is exactly as tall as its content and never scrolls, so there is nothing to stick to.
1590
-
1591
- ```html
1592
- <gog-table [value]="rows" maxHeight="260px" [stickyHeader]="true">…</gog-table>
1593
- ```
1594
-
1595
- `maxHeight` takes any CSS length and is what makes the table own its vertical scrolling. Left
1596
- `null`, the table grows to its content and an ancestor scrolls it the header then follows that
1597
- ancestor's scroll like everything else, which is the pre-21.6.0 behaviour and is fine as long as
1598
- you are not asking for a sticky header.
1599
-
1600
- Columns are declared as **projected `gog-column` children**, not an input array:
1601
-
1602
- ```html
1603
- <gog-table [value]="rows">
1604
- <gog-column field="name" header="Name" sortable="true" />
1605
- <gog-column field="email" header="Email" />
1606
- <gog-column field="status" header="Status">
1607
- <ng-template gogColumnBody let-row let-value="value">
1608
- <gog-tag [variant]="row.active ? 'success' : 'danger'">{{ value }}</gog-tag>
1609
- </ng-template>
1610
- </gog-column>
1611
- </gog-table>
1612
- ```
1613
-
1614
- ##### Server-driven tables`lazy`
1615
-
1616
- By default the table owns the whole data set: it sorts `value` and slices the page itself. With
1617
- `[lazy]="true"` it does neither — `value` **is** the current page, already sorted, and the table
1618
- renders it untouched. Supply `totalRecords` (without it the table cannot know how many pages
1619
- exist, so pagination stays hidden and it warns in dev), then refetch from the two outputs:
1620
-
1621
- ```html
1622
- <gog-table
1623
- [value]="page()"
1624
- [lazy]="true"
1625
- [totalRecords]="total()"
1626
- [pageSize]="20"
1627
- [loading]="loading()"
1628
- dataKey="id"
1629
- (gogSortChange)="sort.set($event); reload()"
1630
- (gogPageChange)="pageNumber.set($event); reload()"
1631
- ></gog-table>
1632
- ```
1633
-
1634
- Row numbers still count from the current page (`(page - 1) * pageSize + i + 1`), and `showTotal`
1635
- reports `totalRecords` rather than `value.length`. **Do not** sort or slice `value` yourself in
1636
- addition that is what the flag turns off.
1637
-
1638
- ##### Rows per page
1639
-
1640
- `pageSize` is a **`model`**, not an input: `[pageSize]="20"` works exactly as before, and
1641
- `[(pageSize)]="size"` becomes possible. That is what makes the rows-per-page select work with no
1642
- wiring — the table binds its own model straight to the paginator's, the select writes back
1643
- through it, and there is no intermediate signal to keep in sync in either direction.
1644
-
1645
- ```html
1646
- <!-- off by default; turn it on per table, or app-wide via GOG_CONFIG.paginator -->
1647
- <gog-table
1648
- [value]="rows"
1649
- [(pageSize)]="size"
1650
- [showPageSizeSelect]="true"
1651
- [pageSizeOptions]="[5, 10, 20]"
1652
- ></gog-table>
1653
- ```
1654
-
1655
- Changing the size returns to page 1 and does **not** emit `gogPageChange` — the consumer already
1656
- knows from `pageSizeChange`, and firing both would make a lazy table fetch twice. In `lazy` mode
1657
- `pageSizeChange` is the refetch signal; bind `[pageSize]` + `(pageSizeChange)` rather than the
1658
- banana-box if you need to act on it.
1659
-
1660
- The footer stays visible at a single page whenever the select is on hiding it would strand the
1661
- user on whatever size produced that one page, with no control left to pick a smaller one.
1662
-
1663
- ##### Selection
1664
-
1665
- `selectionMode` turns it on; `[(selection)]` is always a `T[]`, including in `'single'` mode
1666
- where it holds zero or one row — one shape rather than a union to narrow on every read.
1667
-
1668
- ```html
1669
- <gog-table
1670
- [value]="rows"
1671
- selectionMode="multiple"
1672
- [(selection)]="selected"
1673
- dataKey="id"
1674
- ></gog-table>
1675
- ```
1676
-
1677
- - **Set `dataKey`.** Without it rows are matched by object identity, so any refetch that produces
1678
- new objects silently drops the selection. It is also the `@for` track key, which is what lets
1679
- the DOM survive a refetch instead of being rebuilt.
1680
- - The checkbox column renders automatically (`showSelectionColumn` to turn it off, e.g. for a
1681
- table that selects by row click — pair that with `interactiveRows`).
1682
- - The header select-all appears only in `'multiple'` mode and covers **the current page**, never
1683
- the whole data set: in `lazy` mode the table has never seen the other pages, and a control that
1684
- behaved differently between the two modes would be worse than either.
1685
-
1686
- ##### Clickable rows
1687
-
1688
- `gogRowClick` fires on a click regardless, but a `<tr>` is not focusable, so on its own that is a
1689
- mouse-only affordance. `interactiveRows` makes rows focusable and styles them as clickable, and
1690
- Enter/Space then activate the focused row. If the action is really "open this one thing", a link
1691
- or button inside a cell is better than a whole-row target.
1692
-
1693
- `gog-column` inputs: `field` (required, dot-paths ok), `header`, `sortable` (default `false`),
1694
- `width`/`minWidth`/`maxWidth`, `comparator` (custom `(a, b) => number`, defaults to a
1695
- locale-aware collator for strings). Slots inside a column: `<ng-template gogColumnBody let-row let-value="value" let-index="index">`,
1696
- `<ng-template gogColumnHeader let-header let-field="field">`.
1697
-
1698
- **Sorting, empty/loading states and pagination are all built in** sortable columns toggle
1699
- asc desc unsorted on click, `loading` shows a spinner in place of rows, an empty `value`
1700
- shows `emptyPlaceholder`, and `pageSize > 0` turns on the internal paginator automatically. You
1701
- don't need to hand-roll any of this.
1702
-
1703
- There is **no typed row-selection API** in the current version — if you need it, track
1704
- selection yourself (e.g. a `Set` keyed by row id) and render a `gogColumnBody` checkbox column.
1705
-
1706
- #### `gog-scroll`
1707
-
1708
- Drop-in replacement for `overflow: auto` content still scrolls natively (wheel, touch,
1709
- keyboard); only the browser's own scrollbar chrome is replaced with a themeable overlay thumb.
1710
- Used internally by several other components (`gog-dialog`'s body, `gog-select`'s panel,
1711
- `gog-tabs`' header row) and equally usable directly in your own markup for any scrollable
1712
- region the library's official recommendation over a raw `overflow-x`/`overflow-y`.
1713
-
1714
- | Input | Type | Default |
1715
- | -------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
1716
- | `axis` | `GogScrollAxis` (`'vertical'\|'horizontal'\|'both'`) | `'vertical'` |
1717
- | `size` | `GogScrollSize \| undefined` (`'normal'\|'thin'`) | `'normal'`; via `GOG_CONFIG.scroll.size` |
1718
- | `autoHide` | `boolean \| undefined` | `true`; via `GOG_CONFIG.scroll.autoHide` |
1719
- | `hideDelay` | `number \| undefined` | `800`; via `GOG_CONFIG.scroll.hideDelay` |
1720
- | `reachThreshold` | `number` | `0` |
1721
- | `focusable` | `boolean` | `true` turn off when the parent already owns focus (a dialog with its own focus trap) |
1722
- | `ariaLabel` | `string` | `''` |
1723
- | `overscrollBehavior` | `GogScrollOverscrollBehavior \| undefined` (`'auto'\|'contain'\|'none'`) | `'auto'`; via `GOG_CONFIG.scroll.overscrollBehavior` |
1724
- | `showTrack` | `boolean \| undefined` | `true`; via `GOG_CONFIG.scroll.showTrack` |
1725
- | `horizontalWheel` | `boolean \| undefined` | `false`; via `GOG_CONFIG.scroll.horizontalWheel` |
1726
-
1727
- **`horizontalWheel` turns a vertical wheel into horizontal scrolling** (21.9.0), for the case a
1728
- consumer hits first: hover a horizontal-only row, turn the wheel, and the *page* moves. That is
1729
- the browser's own behaviour and the component deliberately did nothing about it until now.
1730
-
1731
- It is off by default because it changes what an existing instance does with a gesture it
1732
- currently passes on; `provideGogConfig({ scroll: { horizontalWheel: true } })` turns it on
1733
- app-wide. It only acts when the viewport cannot scroll vertically (checked against live
1734
- geometry, so `axis="both"` scrolls down while there is down to go), the event carries no
1735
- horizontal delta of its own (a trackpad swipe and `Shift`+wheel already work), `ctrlKey` is
1736
- clear (pinch-zoom), and there is room left in the direction of the turn. **That last condition is
1737
- the point:** at the content's end the event is left alone and the page picks it up, so the wheel
1738
- never goes dead over a scrolled-to-the-end region. `overscrollBehavior: 'contain'` still
1739
- containsthat boundary is the browser's and this never reaches past it.
1740
-
1741
- Outputs: `gogScroll: GogScrollMetrics`, `gogReachStart`/`gogReachEnd: 'vertical'|'horizontal'`.
1742
- Methods (via template ref): `scrollTo(options)`, `scrollToTop()`, `scrollToBottom()`,
1743
- `scrollToLeft()`, `scrollToRight()`.
1744
-
1745
- ```html
1746
- <gog-scroll size="thin" [focusable]="false" overscrollBehavior="contain" style="max-height: 320px">
1747
- <!-- content that might overflow -->
1748
- </gog-scroll>
1749
- ```
1750
-
1751
- ### Overlays
1752
-
1753
- **Overlays and the viewport the caveat that bites once per project.** `gog-dialog`'s backdrop,
1754
- `gog-toast-container` and `gog-spinner [overlay]` are `position: fixed`, which covers the viewport
1755
- only while no ancestor establishes a containing block. `contain`, `transform`, `filter`,
1756
- `backdrop-filter` or `will-change` anywhere above retargets them to that element's box — and
1757
- **`gog-scroll` sets `contain: layout style`**, so a dialog opened inside a scroller dims the
1758
- scroller rather than the page. Place the dialog and toast outlets in the root component. The
1759
- dropdown panels and `gog-menu` sidestep it by rendering into `<body>`.
1760
-
1761
- #### `gog-menu` + `gogMenuTrigger` / `gogMenuItem`
1762
-
1763
- A command menu. The trigger is a directive on **your own button** — usually the icon button you
1764
- already styled and the items are your own buttons too, so an item can hold an icon, a label and
1765
- a shortcut hint without an input per piece:
1766
-
1767
- ```html
1768
- <button gogButton variant="ghost" [gogMenuTrigger]="rowMenu" aria-label="Row actions">
1769
- <gog-icon name="more-vertical" />
1770
- </button>
1771
-
1772
- <gog-menu #rowMenu ariaLabel="Row actions">
1773
- <button gogMenuItem (click)="edit(row)"><gog-icon name="check" /> Edit</button>
1774
- <button gogMenuItem disabled>Transfer ownership</button>
1775
- <button gogMenuItem (click)="remove(row)"><gog-icon name="close" /> Remove</button>
1776
- </gog-menu>
1777
- ```
1778
-
1779
- | Input | Type | Default | Notes |
1780
- | ----------- | -------------------------- | -------- | ---------------------------------------------------------------------------- |
1781
- | `direction` | `'auto' \| 'up' \| 'down'` | `'auto'` | `'auto'` drops down whenever the panel fits and flips up only when it cannot |
1782
- | `ariaLabel` | `string` | `''` | Names the panel itself |
1783
-
1784
- **There is no `appendToBody`.** The panel always renders into `<body>` and is placed from the
1785
- trigger's measured rect, so a menu inside `gog-scroll`, `gog-table` or any `overflow: hidden`
1786
- ancestor is not clipped and needs no configuration. It also takes the `--gog-dropdown-z` its
1787
- trigger inherits, so a menu opened inside a `gog-dialog` stacks above the dialog.
1788
-
1789
- `gogMenuItem` takes a **`ripple`** input (`boolean | undefined`, `false`, via
1790
- `GOG_CONFIG.ripple.enabled`). The item is your own `<button>`, but the directive owns the ripple,
1791
- so there is no `gogRipple` to add.
1792
-
1793
- Output: `gogClosed` fires after every close, whatever caused it.
1794
-
1795
- Public methods, for driving it yourself: `open(trigger, 'first' | 'last')`, `close(restoreFocus?)`,
1796
- `toggle(trigger)`, and the `isOpen` signal.
1797
-
1798
- **Keyboard**, the WAI-ARIA menu button pattern: Enter/Space/ArrowDown open with the first item
1799
- focused, ArrowUp opens with the last, arrows and Home/End move between items and step over
1800
- disabled ones, Escape closes and returns focus to the trigger, Tab closes and lets focus move on.
1801
- A press outside closes without pulling focus back.
1802
-
1803
- **Disabling an item** is the native `disabled` attribute on your own button — static or bound,
1804
- there is no input for it:
1805
-
1806
- ```html
1807
- <button gogMenuItem disabled>Transfer ownership</button>
1808
- <button gogMenuItem [disabled]="isLocked()" (click)="edit()">Edit</button>
1809
- ```
1810
-
1811
- A disabled item stays in the list rather than disappearing (removing it would shift the others
1812
- under the pointer), the arrow keys step over it, and clicking it does nothing.
1813
-
1814
- **A long menu scrolls itself**, using `gog-scroll` — the same thin, auto-hiding scroller as
1815
- everywhere else in the package, with `overscrollBehavior="contain"` so a wheel at the end of the
1816
- list does not scroll the page behind it. Arrowing past the last visible item scrolls it into view.
1817
-
1818
- The panel's height is the smallest of three: its own content, `--gog-menu-max-height` (320px by
1819
- default), and the room between the trigger and the viewport edge. Lower the token to make a menu
1820
- scroll sooner. **In 21.5.0 the token did nothing** — the measured room was written onto the panel
1821
- as an inline `max-height`, which beat it; fixed in 21.5.1.
1822
-
1823
- A closed menu renders nothing at all, so its commands are not in the accessibility tree until it
1824
- opens.
1825
-
1826
- #### `gog-dialog`
1827
-
1828
- A **single** `<gog-dialog />` renders **every** dialog `DialogService.open(...)` creates —
1829
- place it once, typically in your root app component's template, not per-page and not per-dialog
1830
- call:
1831
-
1832
- ```html
1833
- <!-- app.html -->
1834
- <router-outlet />
1835
- <gog-dialog />
1836
- ```
1837
-
1838
- It has no inputs of its owneverything is driven through `DialogService` (see
1839
- [Services](#services) above). Supports nesting, dragging (when `draggable !== false` and the
1840
- dialog has a title or close button), a focus trap for modal dialogs, `Escape` to close (when
1841
- `closable !== false`), and click-outside-to-close on the backdrop.
1842
-
1843
- #### `gog-toast` / `gog-toast-container`
1844
-
1845
- Same pattern — place **one** `<gog-toast-container />`, typically in the root component:
1846
-
1847
- ```html
1848
- <gog-toast-container [maxVisiblePerPosition]="5" />
1849
- ```
1850
-
1851
- `maxVisiblePerPosition` (default `5`) caps how many toasts stack at once per corner; the rest
1852
- queue. Individual `gog-toast` instances are rendered internally by the container from
1853
- `ToastService.toasts()` — you don't place these yourself. Toasts auto-dismiss after their
1854
- `duration` unless `isSticky`; hovering pauses the countdown (front-of-stack toast only).
1855
-
1856
- Announcements come from two permanently-mounted, visually-hidden live regions the container
1857
- owns polite, and assertive for `error`/`warning`. The toasts themselves carry no
1858
- `role`/`aria-live`: a live region created in the same tick as its text is routinely skipped by
1859
- screen readers, and a second region would announce everything twice. Don't add either back.
1860
-
1861
- ---
1862
-
1863
- ## Reading the deprecations at runtime `GOG_DEPRECATIONS`
1864
-
1865
- Everything the package currently deprecates, as data:
1866
-
1867
- ```ts
1868
- import { GOG_DEPRECATIONS, type GogDeprecation } from '@guildofgleks/ui';
1869
-
1870
- GOG_DEPRECATIONS; // []
1871
- ```
1872
-
1873
- `kind` is `'symbol'` for an export or input and `'token'` for a `--gog-*` custom property. The
1874
- list is generated from the library's source — tags for symbols, stylesheets for tokens so it
1875
- matches what actually still resolves in the version you installed.
1876
-
1877
- **As of 21.7.0 the list is empty on both halves.** Nothing in the TypeScript API is deprecated, and
1878
- the three abbreviated token prefixes that used to fill the token half are gone rather than
1879
- deprecated — see the removal table below. An empty list here means exactly that: nothing to
1880
- migrate away from right now.
1881
-
1882
- ## Removed in 21.7.0
1883
-
1884
- **Nothing in this table exists any more.** Three CSS custom-property prefixes, abbreviations of a
1885
- component's own name, are gone — each was honoured only as a fallback the spelled-out token wrapped
1886
- (`--gog-button-x: var(--gog-btn-x, value)`), never declared on its own.
1887
-
1888
- | Removed | Replacement |
1889
- | ----------------- | ----------------------------- |
1890
- | `--gog-btn-*` | `--gog-button-*` |
1891
- | `--gog-ms-*` | `--gog-multiselect-*` |
1892
- | `--gog-confirm-*` | `--gog-confirmation-dialog-*` |
1893
-
1894
- A consumer's CSS that still sets one of the left-hand names doesn't fail their build — an
1895
- unresolved `var()` just stops matching anything, silently. If a themed surface stopped picking up
1896
- an override after upgrading to 21.7.0, this table is the first thing to check.
1897
-
1898
- ## Removed in 21.5.0
1899
-
1900
- **Nothing in this table exists any more.** It is here so that code written against 21.4.x — or
1901
- generated from a stale copy of this file — can be migrated: each row names what a call site must
1902
- become. If you are writing new code, ignore this section entirely and use the right-hand column,
1903
- which is documented in full above.
1904
-
1905
- | Removed | Replacement |
1906
- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
1907
- | `gog-select`/`gog-multiselect` `chevronTemplate` input | `<ng-template gogDropdownChevron>` |
1908
- | `gog-checkbox` `checkIconTemplate` input | `<ng-template gogCheckboxIcon>` |
1909
- | `gog-tag` `iconTemplate` input | `<ng-template gogTagIcon>` |
1910
- | `gog-multiselect` `clearIconTemplate` input | `<ng-template gogMultiselectClearIcon>` |
1911
- | `gog-inputfield` `iconStartTemplate`/`iconEndTemplate`/`iconStartFn`/`iconEndFn`/`iconStartLabel`/`iconEndLabel` | `<span gogInputAddonStart>`/`<span gogInputAddonEnd>` (or a `<button>` with its own handler) |
1912
- | `gog-table`'s `[template]` attribute (`<ng-template template="field" type="body">`) | `<ng-template gogColumnBody>` / `<ng-template gogColumnHeader>` declared **inside** the matching `<gog-column>` |
1913
- | `<column>` selector / `Column` export | `<gog-column>` / `GogColumn` |
1914
- | `GogSelectOption` / `GogMultiselectOption` types | `GogDropdownOption` (the same type they were aliases of it) |
1915
- | `@guildofgleks/ui/src/styles/…` asset path | `@guildofgleks/ui/styles/…` |
1916
-
1917
- The general rule they all followed: a `TemplateRef` **input** or a string-keyed lookup was the old
1918
- shape; a **projected content directive with a typed context**, declared where it's used, is the
1919
- current one. If you're about to write `fooTemplate` next to an existing `foo` input, or key
1920
- something off a string that has to match another string elsewhere, that's this exact
1921
- anti-pattern reach for a slot directive instead.
1922
-
1923
- ## Full type reference
1924
-
1925
- Shared enum-like types (`import type { ... } from '@guildofgleks/ui'`):
1926
-
1927
- | Type | Values |
1928
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1929
- | `GogSize` | `'xsm' \| 'sm' \| 'md' \| 'lg' \| 'slg'` |
1930
- | `GogVariant` | `'primary' \| 'secondary' \| 'outline' \| 'ghost'` |
1931
- | `GogSurfaceVariant` | `'outlined' \| 'elevated' \| 'filled'` `gog-card` and `gog-panel` |
1932
- | `GogAriaHasPopup` | `boolean \| 'menu' \| 'listbox' \| 'tree' \| 'grid' \| 'dialog'` — `gog-button`'s `ariaHasPopup` |
1933
- | `GogTagVariant` | `'success' \| 'danger' \| 'warning' \| 'info'` |
1934
- | `GogOrientation` | `'horizontal' \| 'vertical'` |
1935
- | `GogTagShape` | `'rounded' \| 'pill'` |
1936
- | `GogSpinnerVariant` | `'runic' \| 'ring' \| 'custom'` |
1937
- | `GogSkeletonShape` | `'text' \| 'circle' \| 'rect'` |
1938
- | `GogSkeletonAnimation` | `'pulse' \| 'wave' \| 'none'` |
1939
- | `GogPaginatorRangeMode` | `'window' \| 'ellipsis'` |
1940
- | `GogScrollAxis` | `'vertical' \| 'horizontal' \| 'both'` |
1941
- | `GogScrollSize` | `'normal' \| 'thin'` |
1942
- | `GogScrollOverscrollBehavior` | `'auto' \| 'contain' \| 'none'` |
1943
- | `GogTooltipPosition` | `'auto' \| 'top' \| 'bottom' \| 'left' \| 'right'` |
1944
- | `GogFloatLabelVariant` | `'none' \| 'in' \| 'on' \| 'over'` |
1945
- | `GogDropdownFilterPosition` | `'top' \| 'bottom'` |
1946
- | `GogDividerVariant` | `'solid' \| 'dashed' \| 'dotted'` |
1947
- | `GogBadgePosition` | `'top-end' \| 'top-start' \| 'bottom-end' \| 'bottom-start'` |
1948
- | `GogProgressbarMode` | `'determinate' \| 'indeterminate' \| 'buffer'` |
1949
- | `GogProgressbarVariant` | `'accent' \| 'success' \| 'danger' \| 'warning' \| 'info'` |
1950
- | `GogButtonToggleAppearance` | `'joined' \| 'separated'` |
1951
- | `GogTabsAlign` | `'start' \| 'center' \| 'end' \| 'stretch'` |
1952
- | `GogDateSelectionMode` | `'single' \| 'range'` |
1953
- | `GogHourFormat` | `'12' \| '24'` |
1954
- | `GogTextareaResize` | `'vertical' \| 'horizontal' \| 'both' \| 'none'` |
1955
- | `GogInputType` | `'text' \| 'password' \| 'email' \| 'number' \| 'search' \| 'tel' \| 'url' \| 'date' \| 'time' \| 'datetime-local'` |
1956
- | `GogInputMode` | `'none' \| 'text' \| 'decimal' \| 'numeric' \| 'tel' \| 'search' \| 'email' \| 'url'` |
1957
- | `GogTableSelectionMode` | `'none' \| 'single' \| 'multiple'` |
1958
- | `GogTableSortEvent` | `{ field: string; direction: SortDirection }` |
1959
- | `GogTableRowClickEvent<T>` | `{ row: T; index: number; originalEvent: MouseEvent \| KeyboardEvent }` |
1960
- | `GogErrorDisplay` | `'auto' \| 'manual'` |
1961
- | `GogDropdownDirection` | `'auto' \| 'up' \| 'down'` |
1962
- | `GogTooltipSide` | `'top' \| 'bottom' \| 'left' \| 'right'` (resolved form of `GogTooltipPosition`, no `'auto'`) |
1963
- | `GogBuiltinIconName` | the 20 glyphs the package ships — see [`gog-icon`](#gog-icon) |
1964
- | `GogIconName` | `GogBuiltinIconName \| (string & {})` — built-ins plus anything registered via `provideGogIcons` |
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.9.0`** (in progress — the
9
+ released version is 21.8.0; see `CHANGELOG.md` for what 21.9.0 adds). 21.7.0 removed the three
10
+ abbreviated token prefixes and 21.5.0 removed a batch of deprecated API — see **Removed in 21.7.0**
11
+ and **Removed in 21.5.0** near the end of this file, which exist so code written against an older
12
+ version can be migrated — and `CHANGELOG.md` has the rest. `README.md` covers the same ground at a
13
+ higher level — install, setup, theming, global configuration — and is accurate; this file goes
14
+ further, into per-component input tables, and is the one to trust for exact names, types and
15
+ defaults.
16
+
17
+ > **Maintainers:** this file ships inside the npm package and is the API reference an agent reads
18
+ > while writing code against it, so a stale table here becomes wrong code in someone else's app —
19
+ > silently, because nothing fails a build. **Any change to an input, output, slot, type, service
20
+ > method or default updates this file in the same change**, and moves the version marker in the
21
+ > paragraph above. See `.github/instructions/gleks-ui-library.instructions.md`, definition of
22
+ > done, step 9.
23
+
24
+ ## Quick facts
25
+
26
+ - Angular **v21+** only (`peerDependencies` require `^21.2.0` for `@angular/core`,
27
+ `@angular/common`, `@angular/forms`, `@angular/platform-browser`). No support for older
28
+ Angular.
29
+ - No Angular CDK, no Material. Only runtime dependency is `tslib`.
30
+ - Every component is **standalone**, `ChangeDetectionStrategy.OnPush`, and built with signals —
31
+ `input()` / `output()` / `model()`, never `@Input()`/`@Output()` decorators, never `ngClass`/
32
+ `ngStyle`.
33
+ - **Reactive Forms only.** Every form control implements `ControlValueAccessor` and is built
34
+ and tested against `[formControl]` / `formControlName`. The library never imports
35
+ `FormsModule` and `[(ngModel)]` is untested — don't suggest it.
36
+ - Theming is 100% CSS custom properties (`--gog-*`) — no Sass config, no JS theme objects, no
37
+ build step to restyle anything.
38
+ - Tree-shakeable: `"sideEffects": false` and every component is a separate standalone import, so
39
+ importing `ButtonComponent` alone does not pull in the rest of the library.
40
+ - SSR-safe: anything touching `window`/`document` is guarded with `isPlatformBrowser`/
41
+ `afterNextRender`.
42
+
43
+ ## Install & setup
44
+
45
+ ```bash
46
+ npm install @guildofgleks/ui
47
+ # or
48
+ yarn add @guildofgleks/ui
49
+ # or — installs it and adds the stylesheet below to angular.json automatically
50
+ ng add @guildofgleks/ui
51
+ ```
52
+
53
+ Add the baseline stylesheet once — it carries every token the components read plus their
54
+ utility classes, so without it components render unstyled:
55
+
56
+ ```jsonc
57
+ // angular.json → projects.<app>.architect.build.options
58
+ "styles": [
59
+ "node_modules/@guildofgleks/ui/styles/index.css",
60
+ "src/styles.scss", // your own styles, after the baseline so they win
61
+ ],
62
+ ```
63
+
64
+ Import components where you use them — every one is standalone:
65
+
66
+ ```ts
67
+ import { Component } from '@angular/core';
68
+ import { ButtonComponent, SelectComponent } from '@guildofgleks/ui';
69
+
70
+ @Component({
71
+ selector: 'app-example',
72
+ imports: [ButtonComponent, SelectComponent],
73
+ template: `
74
+ <gog-select label="Region" [options]="regions" [(value)]="region" />
75
+ <gog-button (gogClick)="save()">Save</gog-button>
76
+ `,
77
+ })
78
+ export class ExampleComponent {}
79
+ ```
80
+
81
+ ## Core conventions (read once, applies everywhere)
82
+
83
+ These hold for essentially every component in the library. Knowing them means you can guess a
84
+ new component's API correctly instead of guessing wrong and hallucinating an input that doesn't
85
+ exist.
86
+
87
+ - **Selector prefix `gog-`** for components (`gog-button`, `gog-select`, …), attribute selectors
88
+ for directives (`gogTooltip`, `[gogBadge]`).
89
+ - **Outputs are prefixed `gog`** so they never collide with native DOM events —
90
+ `gogClick`, `gogToggle`, `gogSearch`, `gogTabChange`, `gogRemove`, `gogScroll`, `gogLoadMore`,
91
+ `gogDateSelect`. **Inputs keep their natural name** (`variant`, `size`, `disabled`).
92
+ - **Two-way binding via `model()`.** Wherever a component holds a value the consumer drives, it's
93
+ a `model()` input — bind with `[(value)]="signal"` / `[(checked)]="signal"` /
94
+ `[(open)]="signal"` etc., or split into `[value]` + `(valueChange)`.
95
+ - **Every input has a zero-config default.** Nothing requires configuration to render something
96
+ reasonable.
97
+ - **`size` is `GogSize = 'xsm' | 'sm' | 'md' | 'lg' | 'slg'`**, shared by every sized component.
98
+ Default is `'md'` almost everywhere — exceptions: `gog-accordion` and `gog-table` default to
99
+ `'lg'` (their `size` means row/section density, not form-control size), `gog-paginator`
100
+ defaults to `'sm'`.
101
+ - **`variant` is `GogVariant = 'primary' | 'secondary' | 'outline' | 'ghost'`** on `gog-button`.
102
+ Status-colored components (`gog-tag`, `gog-badge`) use a different, four-value
103
+ `GogTagVariant = 'success' | 'danger' | 'warning' | 'info'` instead — don't confuse the two.
104
+ - **`errorDisplay: GogErrorDisplay = 'auto' | 'manual'`** (default `'manual'`) on every control
105
+ that shows a validation message (inputfield, textarea, select, multiselect, autocomplete,
106
+ radio-group, slider, datepicker). `'manual'`: the field shows `errorMessage` whenever it's
107
+ non-empty — you own the timing (`errorMessage="control.invalid && control.touched ? 'Required' : ''"`).
108
+ `'auto'`: shown once the attached `[formControl]`/`formControlName` is touched _and_ invalid —
109
+ you only supply the message text. `'auto'` silently behaves like `'manual'` if there's no real
110
+ form control attached.
111
+ - **`inputId` is optional everywhere.** Every form control renders a real `id` — its own if you
112
+ pass one, a generated one otherwise — so the `<label for>` and the error message's
113
+ `aria-describedby` are always wired up. Pass `inputId` only when something outside the
114
+ component needs to reference the field by a known id; never pass one just to get a label.
115
+ - **User-visible chrome strings come from `GOG_CONFIG.labels`**, not from an input per string —
116
+ "Clear", "Close dialog", "Go to page 4" and the rest. Per-instance label inputs exist where a
117
+ single control realistically differs and win over the config. See
118
+ [`labels`](#labels--translating-the-library).
119
+ - **`floatLabel: GogFloatLabelVariant = 'none' | 'in' | 'on' | 'over'`** (default `'none'`) on
120
+ the six field controls: inputfield, textarea, select, multiselect, autocomplete, datepicker.
121
+ `'in'` floats up but stays inside the border, `'on'` floats to sit centered on the top border
122
+ line, `'over'` floats fully above the field. Pair with `floatLabelShowPlaceholder` (default
123
+ `false`) to reveal the field's own `placeholder` once the label has floated clear.
124
+ - **`clearable`** (default varies) on inputfield, textarea, select, multiselect, autocomplete,
125
+ datepicker — shows a clear (×) button once the field has content. Off by default everywhere
126
+ except `gog-multiselect`, which had one before the input existed.
127
+ - **Generic option accessors, not a fixed DTO.** Any collection-driven control (`gog-select`,
128
+ `gog-multiselect`, `gog-autocomplete`, `gog-button-toggle-group`) takes **your own object
129
+ shape** through `optionLabel` / `optionValue` / `optionDisabled` — each is a property path
130
+ (`'name'`, dot-paths like `'profile.title'` work) **or** a function
131
+ `(option: T) => TResult`. Defaults are `'name'` / `'id'` / `'disabled'`. Set
132
+ `[optionValue]="null"` to emit **the option object itself** instead of a plucked id — the
133
+ control then round-trips your own object with no lookup table needed:
134
+ ```html
135
+ <gog-select [options]="members" [optionLabel]="nameOf" [optionValue]="null" [(value)]="member" />
136
+ ```
137
+ - **Global defaults via `GOG_CONFIG` / `provideGogConfig(...)`** — see its own section below.
138
+ Precedence is always: the instance's own input (if set) → `GOG_CONFIG` → the component's
139
+ built-in default.
140
+ - **Don't bind both a `model()` and a form directive on the same instance.** Every CVA control
141
+ (checkbox, toggle, radio-group, inputfield, textarea, select, multiselect, autocomplete,
142
+ slider, datepicker) exposes its value as both a two-way `model()` (`[(checked)]`, `[(value)]`)
143
+ and, separately, `ControlValueAccessor` for `[formControl]`/`formControlName`. Pick one per
144
+ instance — wiring both gives the value two competing sources of truth.
145
+ - **The custom-content slot pattern.** Wherever a component needs custom markup for a specific
146
+ part of itself, it's an attribute directive read with `contentChild()`, given a **typed**
147
+ context via `let-` variables — never a plain `TemplateRef` input, never a string-keyed lookup.
148
+ Recognize the shape:
149
+ ```html
150
+ <gog-accordion [items]="items">
151
+ <ng-template gogAccordionHeader let-item let-open="open">{{ item.title }}</ng-template>
152
+ </gog-accordion>
153
+ ```
154
+ See the per-component tables below for which slot directives exist on which component.
155
+ - **Legacy `TemplateRef` inputs and string-keyed lookups still exist on a few components and
156
+ still work, but are `@deprecated` — do not use them in new code.** See
157
+ [Deprecated patterns — do not use in new code](#deprecated-patterns--do-not-use-in-new-code).
158
+ - **Accessibility is built in**, not optional: keyboard navigation (roving tabindex, arrow keys,
159
+ Home/End), ARIA roles/states, `:focus-visible` styling, `prefers-reduced-motion` handling, and
160
+ WCAG AA contrast are already implemented — you don't need to add any of this yourself, just
161
+ supply `ariaLabel`/`label` inputs where a component has no visible text of its own (icon-only
162
+ buttons, `gog-progressbar`, `gog-scroll`).
163
+ - **`aria-label` on the host tag does nothing.** Several components (`gog-button` chief among
164
+ them) render their real interactive element (a `<button>`) _inside_ the component's own host
165
+ tag. An `aria-label` attribute placed directly on `<gog-button>` in a template lands on the
166
+ custom element wrapper, not on the inner `<button>`, so assistive tech never sees it — always
167
+ use the component's own `ariaLabel` input instead.
168
+
169
+ ## Theming
170
+
171
+ Full model is in `README.md`'s Theming section; short version:
172
+
173
+ - Every visual value (color, spacing, radius, shadow, duration) is a `--gog-*` CSS custom
174
+ property, layered **foundation** (`--gog-accent-color`, `--gog-space-md`, …, restyles
175
+ everything) → **component** (`--gog-button-primary-bg`, …, one block per component, named after
176
+ the component's own element) → **instance** (`--gog-button-bg`, …, deliberately undeclared
177
+ escape hatch for one element).
178
+ - **`--gog-control-boundary-color` is the edge that identifies a control** (since 21.12.0), and
179
+ it is not `--gog-border-color`, which is the decorative hairline for dividers, table rules and
180
+ panel outlines. `gog-chip`, `gog-toggle` and `gog-button-toggle` read it. A theme sets both.
181
+ - **Shadows are an elevation ladder** (since 21.12.0): `--gog-elevation-0` `-5`, Z doubling
182
+ 0/1/2/4/8/16. Step 1 is a thumb riding on a control, 2 an `elevated` card or panel, 3 anything
183
+ anchored to a control (dropdown panel, tooltip, menu), 4 a toast, 5 a modal dialog. The steps are
184
+ generated from ten per-theme knobs (`--gog-elevation-ink`, the two alphas, `-contact-blur`, the
185
+ three per-Z multipliers `-key-x`/`-key-y`/`-key-blur` that carry the style, `-ring-width`, and
186
+ the two `-highlight-*`). **A theme declares all ten or none** — they inherit, so a partial set
187
+ borrows the enclosing theme's weight. `--gog-panel-shadow`, `--gog-dialog-shadow`,
188
+ `--gog-toast-shadow`, `--gog-menu-shadow`, `--gog-toggle-thumb-shadow` and the `*-elevated-shadow`
189
+ pair are still the names to override for one surface; their default is now a step. Never
190
+ hand-write a shadow in a theme block `npm run check:elevation` fails on it.
191
+ - **Foundation includes a small character layer** (since 21.7.0, `docs/themes.md` iteration 1):
192
+ `--gog-radius` (corner rounding), `--gog-control-border-*`/`--gog-panel-border-*`/`--gog-border-*`
193
+ (border weight form fields, raised surfaces, everything smaller and inline, respectively),
194
+ `--gog-text-transform`/`--gog-letter-spacing` (emphasis casing/tracking). Component tokens in
195
+ the categories these cover derive from them by default; setting one in a `[data-theme]` block
196
+ restyles every component that reads it, with nothing to re-list per component.
197
+ - **The type scale is `--gog-text-xs | sm | md | lg | slg | xl | 2xl | 3xl`.** `slg` (1.25rem)
198
+ fills the gap between `lg` and `xl` and is named for the control size that needed it. Every
199
+ component font size that is one of these reads the token, so retuning the scale retunes the
200
+ library; the handful that do not are off-scale on purpose (an 11px chip, the accordion
201
+ chevron's px ramp, the toggle's own micro-ramp).
202
+
203
+ - **Weight is `--gog-font-weight-medium | semibold | bold | heavy`** (500/600/700/900). Every
204
+ component weight reads one of them, so a lighter or heavier house style is four declarations.
205
+
206
+ - **`--gog-z-base` moves the whole stacking order.** Badge `+1`, toast `+100`, dropdowns, dialogs
207
+ and menus `+300`, tooltip `+400`, the blocking spinner overlay `+8000`. Set the base to lift
208
+ the library above your own chrome without disturbing its internal order.
209
+
210
+ - **`--gog-density` is the character layer for spacing** (since 21.7.0, `docs/themes.md`
211
+ iteration 6). It multiplies the ten-step scale `--gog-space-4` `--gog-space-48`, named
212
+ for their pixel value at density 1, and every padding and gap in the library derives from a
213
+ step. `--gog-density: 0.9` in a `[data-theme]` block makes the whole library tighter; nothing
214
+ else needs to be named. `--gog-space-xs|sm|md|lg|2xl` are aliases for steps 4/8/16/24/48 and
215
+ still work. **Every step is a multiple of 4** (since 21.11.0): the five 2px-granular steps came
216
+ out once the last of their 102 readers moved, so "on the grid" is a fact about the scale rather
217
+ than a habit. Three lengths stay off it on purpose and say so in their own comments — a toggle
218
+ thumb's inset, a scrollbar thumb's, and the resize grip's hairline gap — because a length inside
219
+ a single painted mark defines that mark's shape rather than spacing two things apart.
220
+ Icon offsets, dropdown panel gaps, error-line offsets and the badge's overhang
221
+ follow density; the glyph box, the focus-ring offset, the float-label reserve and the
222
+ scrollbar/toggle thumb insets deliberately do not those are legibility or geometry fitted to
223
+ a fixed-width track, not spacing. Since 21.9.0 the split is enforced rather than trusted:
224
+ `check-tokens` rule H fails the build on a length token that restates a scale step's value as
225
+ a bare literal, with the three exceptions named in the script.
226
+ - **Component prefixes are spelled out** since 21.5.0: `--gog-button-*`, `--gog-multiselect-*`,
227
+ `--gog-confirmation-dialog-*`. The abbreviated `--gog-btn-*`, `--gog-ms-*` and `--gog-confirm-*`
228
+ were removed in 21.7.0 — if you're reading a codebase or an example that still uses one, rename
229
+ it; it no longer resolves. The exception is `--gog-input-*`, which is not an abbreviation: it is
230
+ the shared text-field block that `gog-inputfield` and `gog-textarea` both render, and it keeps
231
+ that name.
232
+ - **The package does not need the app's `box-sizing` reset** (since 21.6.0): `utilities.css`
233
+ sets `border-box` on every element carrying a `gog-*` class, including the ones the library
234
+ puts on a consumer's own element. Do not add a reset "so the components line up" — they
235
+ already do, and a `* { box-sizing: content-box }` in an app is the only thing that undoes it.
236
+ - Theme switch is a `data-theme` attribute, usually on `<html>`, toggled through the
237
+ `ThemeService` (`inject(ThemeService).setTheme('dark')` / `.toggleTheme()` / `.theme` signal).
238
+ Ships `light` and `dark`, plus nine importable presets at
239
+ `@guildofgleks/ui/styles/presets/<name>.css`. **All nine set palette and character** (since
240
+ 21.7.0 before it, three were palette-only, which made them recoloured defaults):
241
+
242
+ | Preset | Radius | Density | Identity |
243
+ | ---------------------- | ------ | ------- | ------------------------------------------------ |
244
+ | `slate` | 12px | 1.05 | soft modern hairline borders, roomy |
245
+ | `one-dark`/`one-light` | 4px | 0.9 | editor chrome; identical character, two tones |
246
+ | `material` | 4px | 1.1 | Material Design 3, pill buttons |
247
+ | `primeng` | 6px | 0.95 | PrimeNG Aura |
248
+ | `ledger` | 0 | 0.9 | administrative hard offset shadow, no motion |
249
+ | `terminal` | 0 | 0.85 | green phosphor, monospaced throughout, no motion |
250
+ | `bevel` | 0 | 0.9 | early-web desktop `outset`/`inset` borders |
251
+ | `parchment` | 0 | 1.1 | ink on paper old-style serif, oxblood |
252
+
253
+ `material`, `primeng` and `bevel` also set a few genuinely per-component things the character
254
+ layer has no vocabulary for (a pill button, a table's header font, a button bevel that has to
255
+ disagree with a field's); see their own file headers.
256
+
257
+ - **A preset never makes a network request.** Each sets a font _stack_ resolving to a real system
258
+ face. Where a webfont is worth offering, it is a separate opt-in file `terminal.fonts.css`
259
+ (IBM Plex Mono), `parchment.fonts.css` (EB Garamond) — imported **after** the preset, since it
260
+ re-points the same tokens and later wins. Do not add an `@import url(…)` to a preset itself; put
261
+ it in a companion file, or the import becomes a download nobody asked for.
262
+ - Restyle one instance without touching a theme: `<gog-button style="--gog-button-bg: #ff4edb">`.
263
+ - Build a custom theme by declaring a palette **and a character** against a new `data-theme`
264
+ value (see `README.md`'s Theming section for the full worked example) — component tokens
265
+ re-derive automatically, you don't restate them.
266
+
267
+ ## Right-to-left
268
+
269
+ Supported since 21.5.0. `dir="rtl"` on `<html>` or on any wrapper mirrors every component —
270
+ you write nothing per component. Portaled overlays (select/multiselect panels, tooltip bubbles)
271
+ copy a _scoped_ `dir` onto themselves, so an RTL region inside an LTR page works too.
272
+
273
+ Physical by design, in both directions: `gogTooltip [position]="'left' | 'right'"` and
274
+ `ToastConfig.position` (`'top-right'`, …). Use the tooltip's `'auto'` for direction-aware
275
+ placement; a toast corner is a deliberate choice, so it is not mirrored.
276
+
277
+ ## Global configuration `GOG_CONFIG` / `provideGogConfig(...)`
278
+
279
+ For the handful of inputs an app typically wants to set once (a size for every form control, a
280
+ locale for every datepicker) rather than repeat on every instance:
281
+
282
+ ```ts
283
+ import { provideGogConfig } from '@guildofgleks/ui';
284
+
285
+ bootstrapApplication(App, {
286
+ providers: [
287
+ provideGogConfig({
288
+ control: { size: 'sm', errorDisplay: 'auto', clearable: true },
289
+ dropdown: { appendToBody: true, filter: true },
290
+ datepicker: { locale: 'de-DE', firstDayOfWeek: 1, format: 'dd.MM.yyyy' },
291
+ toast: { position: 'top-right', duration: 4000 },
292
+ }),
293
+ ],
294
+ });
295
+ ```
296
+
297
+ Precedence, always: **instance input `GOG_CONFIG` component's built-in default.** A nested
298
+ `provideGogConfig(...)` (in a route's or component's own `providers`) **layers onto the
299
+ parent's config**, one level deep per key it does not replace it.
300
+
301
+ | Key | Fields | Applies to |
302
+ | -------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
303
+ | `control` | `size`, `errorDisplay`, `clearable` | `size`: button, `[gogButton]`, button-toggle-group, checkbox, toggle, radio-group, inputfield, textarea, select, multiselect, autocomplete, datepicker. `errorDisplay`: inputfield, textarea, select, multiselect, autocomplete, datepicker, radio-group, slider. `clearable`: inputfield, textarea, select, multiselect, autocomplete, datepicker. Not table/accordion/paginator (density, not form size), not spinner/skeleton/tag/chip. |
304
+ | `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). |
305
+ | `floatLabel` | `variant`, `showPlaceholder` | inputfield, textarea, select, multiselect, autocomplete, datepicker. |
306
+ | `datepicker` | `locale`, `firstDayOfWeek`, `format` | `gog-datepicker`, `gog-calendar`. |
307
+ | `autocomplete` | `searchDebounce`, `minLength`, `openOnFocus` | `gog-autocomplete`. |
308
+ | `tooltip` | `position`, `showDelay`, `hideDelay` | the `gogTooltip` directive. |
309
+ | `spinner` | `component`, `variant` | every spinner the library draws `gog-spinner`, `gog-spinner-overlay`, and the ones inside `gog-button`, `gog-autocomplete` and `gog-table`, which have no input of their own. `component` takes **your** component and renders it in place of the built-in look. The overlay honoured neither key until 21.10.0, and `gog-table` was simply never listed. |
310
+ | `scroll` | `autoHide`, `hideDelay`, `size`, `overscrollBehavior`, `showTrack`, `horizontalWheel` | `gog-scroll` (and every component that uses one internally). |
311
+ | `button` | `debounce` | `gog-button`. |
312
+ | `ripple` | `enabled` | the press ripple on `gog-button`, `[gogButton]`, `gog-button-toggle-group`, `gog-chip`, `gog-tabs`, `gog-accordion`, `gogCollapsibleTrigger`, `gogMenuItem` and the `gog-select`/`gog-multiselect`/`gog-autocomplete` options. **Off by default.** Each of those takes a `ripple` input that wins over it. Not the `gogRipple` directive — writing that attribute is already the per-element decision. |
313
+ | `inputfield` | `showSpinButtons` | `gog-inputfield`. |
314
+ | `textarea` | `resize` | `gog-textarea`. |
315
+ | `paginator` | `showPageSizeSelect`, `pageSizeOptions` | `gog-paginator`, and through it `gog-table`'s built-in pagination. |
316
+ | `toast` | `position`, `duration` | `ToastService`. |
317
+ | `theme` | `storageKey`, `defaultTheme`, `followSystem`, `lightTheme`, `darkTheme` | `ThemeService`. All off/neutral by default — see below. |
318
+ | `labels` | every fixed string the library renders — see below | inputfield, textarea, select, multiselect, autocomplete, datepicker, calendar, paginator, table, `DialogService`, `ToastService`. |
319
+
320
+ Anything visual does **not** belong here — override the `--gog-*` token instead.
321
+
322
+ ### `labels` translating the library
323
+
324
+ Every string a component renders that the consumer never writes markup for. An app that isn't
325
+ in English sets these once rather than on every control:
326
+
327
+ ```ts
328
+ provideGogConfig({
329
+ labels: {
330
+ clear: 'Löschen', // inputfield / textarea clear button
331
+ clearSelection: 'Auswahl löschen', // select / multiselect / autocomplete
332
+ clearDate: 'Datum löschen', // datepicker
333
+ selectAll: 'Alle auswählen', // multiselect panel
334
+ clearAll: 'Alle löschen', // multiselect panel
335
+ increment: 'Erhöhen', // number spin buttons
336
+ decrement: 'Verringern',
337
+ showPassword: 'Passwort anzeigen',
338
+ hidePassword: 'Passwort verbergen',
339
+ closeDialog: 'Schließen',
340
+ closeToast: 'Schließen',
341
+ pagination: 'Seitennavigation',
342
+ previousPage: 'Vorherige Seite',
343
+ nextPage: 'Nächste Seite',
344
+ openCalendar: 'Kalender öffnen',
345
+ togglePanel: 'Bereich umschalten', // gog-panel's toggle, only when it has no heading
346
+ rowsPerPage: 'Zeilen pro Seite', // gog-paginator's size select
347
+ total: 'Gesamt', // gog-table's row-count label
348
+ tablePagination: 'Tabellennavigation',
349
+ selectRow: 'Zeile auswählen',
350
+ selectAllRows: 'Alle Zeilen auswählen',
351
+ today: 'Heute',
352
+ thisMonth: 'Aktueller Monat',
353
+ previousMonth: 'Vorheriger Monat',
354
+ nextMonth: 'Nächster Monat',
355
+ previousYear: 'Vorheriges Jahr',
356
+ nextYear: 'Nächstes Jahr',
357
+ hours: 'Stunden',
358
+ minutes: 'Minuten',
359
+ seconds: 'Sekunden',
360
+ // The one non-string field: it interpolates the page number, and word order and
361
+ // agreement around a number vary by language, so it takes a formatter.
362
+ page: (page, isCurrent) => (isCurrent ? `Seite ${page}, aktuell` : `Zu Seite ${page} wechseln`),
363
+ },
364
+ });
365
+ ```
366
+
367
+ Strings that describe **one** control rather than library chrome — `gog-checkbox`'s `ariaLabel`,
368
+ `gog-button`'s `ariaLabel`, any field's `label`/`placeholder` — are deliberately **not** here.
369
+ Those stay per instance. Where a per-instance label input exists (`clearAriaLabel`, `todayLabel`,
370
+ …) it still wins over the configured value.
371
+
372
+ ## Services
373
+
374
+ ### `ThemeService`
375
+
376
+ ```ts
377
+ private readonly theme = inject(ThemeService);
378
+ this.theme.theme(); // Signal<string>, READ-ONLY — current data-theme
379
+ this.theme.setTheme('dark'); // any theme name, including a custom one you declared in CSS
380
+ this.theme.toggleTheme(); // flips between the configured light and dark names
381
+ ```
382
+
383
+ `theme` is read-only on purpose: writing to it would move the signal without touching the
384
+ `data-theme` attribute the styles actually read. Never suggest `theme.set(...)` it does not
385
+ exist.
386
+
387
+ Zero-config behaviour: adopt whatever `data-theme` is already on `<html>`, else `'light'`.
388
+ Persistence and following the OS setting are **opt-in**, so upgrading cannot change which theme
389
+ an existing app opens in:
390
+
391
+ ```ts
392
+ provideGogConfig({
393
+ theme: {
394
+ storageKey: 'app-theme', // persist the choice in localStorage; unset = no persistence
395
+ followSystem: true, // open in the OS prefers-color-scheme, and keep following it
396
+ // until the app calls setTheme/toggleTheme
397
+ lightTheme: 'light', // the two names followSystem maps to and toggleTheme alternates
398
+ darkTheme: 'one-dark', // between
399
+ defaultTheme: 'light', // used when nothing else decides
400
+ },
401
+ });
402
+ ```
403
+
404
+ Resolution order at startup: existing `data-theme` on the document → persisted value →
405
+ OS setting (if `followSystem`) `defaultTheme` `'light'`.
406
+
407
+ ### `ToastService`
408
+
409
+ Root-provided singleton. Requires a `<gog-toast-container />` placed once in your app (see
410
+ [gog-toast](#gog-toast--gog-toast-container) below it is **not** wired up automatically).
411
+
412
+ ```ts
413
+ private readonly toast = inject(ToastService);
414
+
415
+ this.toast.success('Saved');
416
+ this.toast.error('Could not save', {
417
+ isSticky: true,
418
+ actions: [{ label: 'Retry', onClick: () => this.save() }],
419
+ });
420
+ // also: .warning(msg, config?), .info(msg, config?), .show(config), .dismiss(id), .dismissAll()
421
+ ```
422
+
423
+ `ToastConfig`: `{ message, type?, iconName?, iconTemplate?, actions?, dedupeKey?, isSticky?, duration?, position? }`.
424
+ Repeated calls with the same (explicit or inferred) `dedupeKey` replace the existing toast in
425
+ place instead of stacking a duplicate.
426
+
427
+ ### `DialogService`
428
+
429
+ Root-provided singleton, imperative dynamic-component dialogs. Requires a `<gog-dialog />`
430
+ placed once in your app (see [gog-dialog](#gog-dialog) below — also **not** automatic).
431
+
432
+ ```ts
433
+ private readonly dialogService = inject(DialogService);
434
+
435
+ async confirmDelete(): Promise<void> {
436
+ const handle = this.dialogService.open<boolean>({
437
+ component: ConfirmationDialogComponent, // or your own component
438
+ title: 'Delete this item?',
439
+ role: 'alertdialog',
440
+ data: { message: 'This cannot be undone.' },
441
+ });
442
+ const confirmed = await handle.afterClosed; // boolean | undefined
443
+ }
444
+ ```
445
+
446
+ `DialogConfig<TData>`: `{ title?, component, data?: TData, modal? (default true), closable?, draggable?, closeIconName?, closeIconTemplate?, width?, maxWidth?, role? ('dialog' default | 'alertdialog'), zIndex? }`.
447
+ `open<TResult, TData>()` returns `{ close(result?), afterClosed: Promise<TResult | undefined> }`. Also:
448
+ `closeAll(result?)`, `updatePosition(id, offsetX, offsetY)` (for `draggable` dialogs).
449
+
450
+ **`open<TResult, TData>()` type-checks `data` against `TData` when you supply both type
451
+ arguments** — supplying only `TResult` (the common case above) leaves `TData` as `unknown`,
452
+ exactly as before:
453
+
454
+ ```ts
455
+ interface EditUserData {
456
+ userId: string;
457
+ }
458
+
459
+ const handle = this.dialogService.open<{ saved: boolean }, EditUserData>({
460
+ component: EditDialogComponent,
461
+ data: { userId: user.id }, // checked against EditUserData here
462
+ });
463
+ ```
464
+
465
+ This checks only the call site. `EditDialogComponent` still reads its data via `inject(DIALOG_DATA)`
466
+ an `InjectionToken<unknown>` shared by every dialog, so it still needs its own cast
467
+ (`inject<EditUserData>(DIALOG_DATA)`, shown below). Angular's DI has no way to carry a
468
+ per-call-site type through one shared token, so the receiving half of the round trip is still on
469
+ trust this closes only the half that can be closed.
470
+
471
+ The library ships a ready-made `ConfirmationDialogComponent` for yes/no prompts — pass it as
472
+ `component` with `data: { title, description, confirmText, cancelText }`; it resolves the
473
+ dialog's result to `true`/`false`.
474
+
475
+ **Wiring a custom component into a dialog** — it reads its data via `DIALOG_DATA` and closes
476
+ itself via `DIALOG_REF`:
477
+
478
+ ```ts
479
+ import { Component, inject } from '@angular/core';
480
+ import { DIALOG_DATA, DIALOG_REF } from '@guildofgleks/ui';
481
+
482
+ @Component({ selector: 'app-edit-dialog', template: `…` })
483
+ export class EditDialogComponent {
484
+ protected readonly data = inject<{ userId: string }>(DIALOG_DATA);
485
+ private readonly ref = inject(DIALOG_REF);
486
+
487
+ save(): void {
488
+ this.ref.close({ saved: true });
489
+ }
490
+ }
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Component reference
496
+
497
+ Every component below is exported from `@guildofgleks/ui`'s root `import { X } from '@guildofgleks/ui'`.
498
+ "CVA" = implements `ControlValueAccessor` (works with `[formControl]`/`formControlName`).
499
+
500
+ ### Buttons & choices
501
+
502
+ #### `gog-button`
503
+
504
+ | Input | Type | Default | Notes |
505
+ | -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------- |
506
+ | `variant` | `GogVariant` | `'primary'` | |
507
+ | `severity` | `GogSeverity` | `'accent'` | what the action means; orthogonal to `variant` — see below |
508
+ | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
509
+ | `disabled` | `boolean` | `false` | |
510
+ | `fullWidth` | `boolean` | `false` | |
511
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | |
512
+ | `loading` | `boolean` | `false` | shows an inline `gog-spinner`, blocks clicks |
513
+ | `debounce` | `number \| undefined` | `300` | ms; via `GOG_CONFIG.button.debounce` see note below |
514
+ | `ariaLabel` | `string \| null` | `null` | **use this, not a raw `aria-label` attribute** |
515
+ | `ariaPressed` | `boolean \| 'mixed' \| null` | `null` | toggle button; `false` renders `aria-pressed="false"` |
516
+ | `ariaExpanded` | `boolean \| null` | `null` | disclosure / popup trigger |
517
+ | `ariaControls` | `string \| null` | `null` | id of the controlled element; pairs with `ariaExpanded` |
518
+ | `ariaHasPopup` | `GogAriaHasPopup \| null` | `null` | `boolean \| 'menu' \| 'listbox' \| 'tree' \| 'grid' \| 'dialog'` |
519
+ | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
520
+
521
+ Outputs: `gogClick: MouseEvent`.
522
+
523
+ **`severity` says what the action means; `variant` says how loudly it is drawn** (21.9.0). The
524
+ two are orthogonal, so this is not a fifth variant it re-points the colours all four are built
525
+ from, and every combination is real: `variant="ghost" severity="danger"` is a quiet delete,
526
+ `variant="primary" severity="danger"` a loud one. `'accent'` is the default and the absence of a
527
+ claim, so nothing has to opt out of a severity it does not have. `GogSeverity` is shared with
528
+ `gog-progressbar`, whose `GogProgressbarVariant` is now an alias of it.
529
+
530
+ ```html
531
+ <gog-button severity="danger" (gogClick)="deleteAccount()">Delete account</gog-button>
532
+ <gog-button variant="outline" severity="warning">Discard draft</gog-button>
533
+ <a gogButton severity="success" routerLink="/done">Finish</a>
534
+ ```
535
+
536
+ Two colour rules are worth knowing before you override anything. A **filled** severity button's
537
+ label is `--gog-<status>-text-color`, which each theme states for its own hue — `material` and
538
+ `primeng` put near-black on their bright ones, the rest white — and hover and press deepen the
539
+ fill _away_ from that label (`--gog-<status>-shade`), so a state always makes the label easier to
540
+ read rather than harder. A **transparent** one's label is `--gog-button-<status>-ink`: the status
541
+ hue mixed halfway toward the page's ink, because the raw hue is legible body text in only five of
542
+ the eleven shipped themes. Override `--gog-button-<status>-ink` if your own theme wants more
543
+ colour there, and check it: all four severities across all four variants and all their states are
544
+ gated by `npm run check:contrast`.
545
+
546
+ **Every ARIA attribute this button needs has an input, and a raw attribute is not a
547
+ substitute.** `<gog-button [attr.aria-pressed]="on()">` compiles, throws nothing, and does
548
+ nothing: the attribute lands on the `<gog-button>` custom element, which has no role, while the
549
+ real `<button>` inside stays unmarked. The failure is invisible the control looks right and is
550
+ simply not a toggle to a screen reader. Use `[ariaPressed]`, `[ariaExpanded]`, `[ariaControls]`,
551
+ `[ariaHasPopup]` and `ariaLabel`.
552
+
553
+ `false` is not the same as unset. `null` omits the attribute; `false` renders
554
+ `aria-pressed="false"` / `aria-expanded="false"`, which is what an off toggle or a closed
555
+ disclosure has to say a button with no `aria-pressed` at all is not a toggle button.
556
+
557
+ **A toggle button now looks toggled** (21.9.0). `aria-pressed="true"` (or `"mixed"`) draws an
558
+ inset ring `--gog-button-<variant>-toggled-shadow`, overridable per instance with
559
+ `--gog-button-toggled-shadow`. A ring rather than a fill because hover and press already own the
560
+ background: the state has to survive both, and until 21.9.0 it did not exist at all, so a button
561
+ could announce itself as on to a screen reader and look identical to an off one. `[gogButton]`
562
+ gets the same look from the attribute you write on your own element.
563
+
564
+ **A `disabled` toggle keeps the ring** (21.10.0), dimmed by `--gog-button-disabled-opacity` like
565
+ the rest of the button. `disabled` on a real `<button>` does not remove `aria-pressed`, so "on,
566
+ and unavailable" is announced either way and has to be visible; the rule had excluded
567
+ `:disabled` until then, copied from the hover and press rules where the guard belongs.
568
+ `gog-chip`'s `selected` ring has always behaved this way, and the two are now the same.
569
+
570
+ **`[gogButton]` needs none of these inputs.** It styles an element you own, so write the ARIA
571
+ attributes on your own `<button>`/`<a>` directly. Same for `[gogMenuTrigger]`, which sets
572
+ `aria-haspopup`/`aria-expanded`/`aria-controls` on its hostput it on your own `<button
573
+ gogButton>`, as its own example shows, not on a `<gog-button>`.
574
+
575
+ ```html
576
+ <gog-button [ariaPressed]="mirrored()" (gogClick)="toggleMirror()">Mirror</gog-button>
577
+
578
+ <gog-button
579
+ [ariaExpanded]="open()"
580
+ ariaControls="filters"
581
+ ariaHasPopup="dialog"
582
+ (gogClick)="open.set(!open())"
583
+ >Filters</gog-button
584
+ >
585
+ ```
586
+
587
+ **The press is a colour, not only a movement.** `:active` deepens the button's background (and
588
+ the label where the fill demands it) as well as scaling it by `--gog-button-active-scale`. Under
589
+ `prefers-reduced-motion: reduce` the scale is dropped and the colour stays, so the press is still
590
+ visible to a reader who has switched animations off — before 21.9.0 that reader got no feedback at
591
+ all, since the ripple is off by default and is itself suppressed under reduced motion. Override
592
+ per instance with `--gog-button-press-bg` / `--gog-button-press-color`, or per theme with
593
+ `--gog-button-<variant>-active-bg`.
594
+
595
+ Every other pressable surface in the library does the same thing since 21.9.0 — menu items,
596
+ chips, tab and accordion headers, button-toggle options and the three dropdowns' option rows —
597
+ each through its own `--gog-<block>-press-bg`. `gogCollapsibleTrigger` is the exception: the
598
+ library paints nothing on that element in any state, because it is yours.
599
+
600
+ **`debounce` is a spam guard, not a delay before the first click.** The first click in a window
601
+ fires immediately (leading edge); further clicks within `debounce` ms are silently dropped.
602
+
603
+ **Use `(gogClick)`, never `(click)`, on `gog-button`.** The click handler that drives `debounce`
604
+ and emits `gogClick` is bound on the `<button>` inside the component's own template, not on the
605
+ host a native click still bubbles up through `<gog-button>`, so a `(click)` listener written
606
+ there fires on every press, silently bypassing the debounce entirely. This is specific to the
607
+ component: `[gogButton]` on your own `<a>`/`<button>` has no debounce to bypass, so `(click)` on
608
+ it works exactly as written.
609
+
610
+ ```html
611
+ <gog-button variant="primary" [loading]="saving()" (gogClick)="save()">Save</gog-button>
612
+ <gog-button variant="ghost" ariaLabel="Close" (gogClick)="close()"
613
+ ><gog-icon name="close"
614
+ /></gog-button>
615
+ ```
616
+
617
+ #### `gog-button-toggle-group`
618
+
619
+ A row of buttons, single- or multi-select, built from your own option objects.
620
+
621
+ | Input | Type | Default | Notes |
622
+ | ------------------------------------ | ------------------------------------------ | ---------------------- | --------------------------------------------- |
623
+ | `options` | `TOption[]` | `[]` | |
624
+ | `optionLabel` | accessor | `'name'` | |
625
+ | `optionValue` | accessor \| `null` | `'id'` | `null` emits the option object |
626
+ | `optionDisabled` | accessor | `'disabled'` | |
627
+ | `optionIcon` | accessor → `GogIconName \| null` \| `null` | `null` | optional leading icon per option |
628
+ | `multiple` | `boolean` | `false` | changes ARIA role entirely — see note |
629
+ | `appearance` | `'joined' \| 'separated'` | `'joined'` | |
630
+ | `orientation` | `GogOrientation` | `'horizontal'` | |
631
+ | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
632
+ | `disabled`, `fullWidth`, `ariaLabel` | | `false`, `false`, `''` | |
633
+ | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
634
+
635
+ Model: `value: TValue | TValue[] | null` (single value, or array in `multiple` mode). CVA: yes.
636
+ Slot: `<ng-template gogButtonToggleOption let-opt let-selected="selected">` for custom button
637
+ markup. **Single mode is a radio group** (`role="radiogroup"`, arrows move _and_ select);
638
+ **multiple mode is a toolbar of independent toggles** (`role="group"`, arrows only move, Space
639
+ toggles) this is a real ARIA distinction, not cosmetic.
640
+
641
+ ```html
642
+ <gog-button-toggle-group [options]="alignments" [(value)]="align" />
643
+ <gog-button-toggle-group [options]="tools" [multiple]="true" [(value)]="activeTools" />
644
+ ```
645
+
646
+ ### Form fields
647
+
648
+ #### `gog-inputfield`
649
+
650
+ | Input | Type | Default | Notes |
651
+ | ----------------------------------------- | ---------------------- | ----------------------------------- | -------------------------------------------------------------------------------------- |
652
+ | `label`, `placeholder` | `string` | `''` | |
653
+ | `type` | `GogInputType` | `'text'` | `text`/`password`/`email`/`number`/`search`/`tel`/`url`/`date`/`time`/`datetime-local` |
654
+ | `readonly` | `boolean` | `false` | value stays focusable and submitted, edits blocked; hides the clear button and stepper |
655
+ | `maxlength`, `minlength` | `number \| null` | `null` | native attributes |
656
+ | `pattern` | `string` | `''` | native attribute, regex source |
657
+ | `inputMode` | `GogInputMode \| null` | `null` | on-screen keyboard hint (`numeric`, `tel`, …) |
658
+ | `spellcheck` | `boolean \| null` | `null` | unset = browser default |
659
+ | `inputId` | `string` | `''` → generated | a real id is always rendered; pass one only to reference the field externally |
660
+ | `min`, `max`, `step` | `number \| null` | `null` | `type="number"` only |
661
+ | `showSpinButtons` | `boolean \| undefined` | `true` | own +/- glyphs on `type="number"`; via `GOG_CONFIG.inputfield.showSpinButtons` |
662
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` | see conventions |
663
+ | `disabled`, `size`, `fullWidth` | | `false`, `'md'`, `true` | |
664
+ | `iconStart` / `iconEnd` | `GogIconName \| ''` | `''` | bare leading/trailing icon |
665
+ | `clearable`, `clearAriaLabel` | | `false`, `'Clear'` | on `type="number"` the clear button renders alongside the stepper |
666
+ | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
667
+ | `showPasswordLabel` / `hidePasswordLabel` | `string \| undefined` | `'Show password'`/`'Hide password'` | `type="password"` reveal toggle aria-labels; via `GOG_CONFIG.labels` |
668
+ | `incrementLabel` / `decrementLabel` | `string \| undefined` | `'Increment'`/`'Decrement'` | spin button aria-labels; via `GOG_CONFIG.labels` |
669
+
670
+ Model: `value: string` (always a string, even for `type="number"` — the _form control_ value is
671
+ `number | null`, but the `[(value)]` model mirrors the raw text). CVA: yes.
672
+
673
+ Slots: project `<span gogInputAddonStart>`/`<span gogInputAddonEnd>` (or a `<button>`) for
674
+ custom leading/trailing markup — a normal DOM element with its own `aria-label`, click handler
675
+ and disabled state, not a component-managed slot. This is the **current, non-deprecated**
676
+ replacement for the old icon-template/icon-fn/icon-label input quartet — see
677
+ [Deprecated patterns](#deprecated-patterns--do-not-use-in-new-code).
678
+
679
+ ```html
680
+ <gog-inputfield
681
+ label="Email"
682
+ type="email"
683
+ formControlName="email"
684
+ errorDisplay="auto"
685
+ errorMessage="Enter a valid email"
686
+ [clearable]="true"
687
+ />
688
+
689
+ <gog-inputfield label="Amount" [fullWidth]="false">
690
+ <span gogInputAddonStart>€</span>
691
+ </gog-inputfield>
692
+ ```
693
+
694
+ #### `gog-textarea`
695
+
696
+ | Input | Type | Default |
697
+ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ---------------------------------------------- |
698
+ | `label`, `placeholder` | `string` | `''` |
699
+ | `rows` | `number` | `4` |
700
+ | `readonly` | `boolean` | `false` |
701
+ | `maxlength`, `minlength` | `number \| null` | `null` |
702
+ | `spellcheck` | `boolean \| null` | `null` |
703
+ | `inputId` | `string` | `''` → generated, same as inputfield |
704
+ | `resize` | `GogTextareaResize \| undefined` (`'vertical'\|'horizontal'\|'both'\|'none'`) | `'vertical'`; via `GOG_CONFIG.textarea.resize` |
705
+ | `errorMessage`, `errorDisplay`, `disabled`, `size`, `fullWidth` | | same shape as inputfield |
706
+ | `clearable`, `clearAriaLabel`, `floatLabel`, `floatLabelShowPlaceholder` | | same shape as inputfield |
707
+
708
+ Model: `value: string`. CVA: yes.
709
+
710
+ ```html
711
+ <gog-textarea label="Notes" formControlName="notes" [rows]="6" resize="vertical" />
712
+ ```
713
+
714
+ #### `gog-select`
715
+
716
+ Extends the shared listbox behaviour (`GogDropdownBase`) that also backs `gog-multiselect` and
717
+ partly `gog-autocomplete` placement, the append-to-body overlay, click-outside, keyboard nav,
718
+ and CVA all come from there. Full shared input surface (documented once, applies to both select
719
+ and multiselect unless noted otherwise):
720
+
721
+ | Input | Type | Default | Notes |
722
+ | ------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
723
+ | `label`, `ariaLabel`, `placeholder` | `string` | `''`, `''`, `'Select...'` | |
724
+ | `options` | `TOption[]` | `[]` | your own objects |
725
+ | `optionLabel` | accessor | `'name'` | path or fn |
726
+ | `optionValue` | accessor \| `null` | `'id'` | `null` = emit the option object |
727
+ | `optionDisabled` | accessor | `'disabled'` | |
728
+ | `clearable`, `clearAriaLabel` | | `false` (select) / `true` (multiselect), `'Clear selection'` | |
729
+ | `minWidth` | `string \| null` | `null` | only with `[fullWidth]="false"` |
730
+ | `filter` | `boolean \| undefined` | `false` | search box in the panel; via `GOG_CONFIG.dropdown.filter` |
731
+ | `filterPlaceholder`, `filterEmptyMessage` | `string` | `'Search...'`, `'No matches'` | |
732
+ | `filterPosition` | `'top' \| 'bottom' \| undefined` | `'top'` | via `GOG_CONFIG.dropdown.filterPosition` |
733
+ | `filterMatch` | `((option, query) => boolean) \| null` | `null` | custom matcher, else case-insensitive substring on the resolved label |
734
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` | |
735
+ | `size` | `GogSize \| undefined` | `'md'` | |
736
+ | `dropdownDirection` | `'auto' \| 'up' \| 'down' \| undefined` | `'auto'` | |
737
+ | `dropdownZIndex`, `dropdownWidth`, `dropdownMaxHeight` | | `null` | only meaningful with `appendToBody` |
738
+ | `appendToBody` | `boolean \| undefined` | `false` | renders the panel into `<body>` needed inside a scroll/overflow-clipped container |
739
+ | `disabled`, `fullWidth` | | `false`, `true` | |
740
+ | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
741
+ | `inputId` (select/autocomplete only) | `string` | `''` | |
742
+ | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
743
+
744
+ `gog-select`-specific: `value: model<TValue>(null)`.
745
+ `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`).
746
+
747
+ CVA: yes, both. Slots (shared): `<ng-template gogDropdownChevron>` (custom chevron markup),
748
+ `<ng-template gogDropdownOption let-opt let-selected="selected" let-label="label">` (custom
749
+ option row). Multiselect adds `<ng-template gogMultiselectClearIcon>`.
750
+
751
+ **Turn `filter` on past about seven options — or order them instead.** Choice time grows with the
752
+ log of the count (`T = b · log₂(n + 1)`), so beyond roughly seven a panel stops being scanned and
753
+ starts being read. The escape is not always the filter box: the law governs _unordered_ choices,
754
+ and a list the reader can predict — alphabetical countries, ascending amounts, a familiar fixed
755
+ sequence — is one they search rather than choose from, so ordering it well is worth as much as
756
+ filtering it. Both, for a long list of neither. `GOG_CONFIG.dropdown.filter` sets this once for
757
+ the app rather than per dropdown, which is usually the right place for it.
758
+
759
+ ```html
760
+ <gog-select
761
+ label="Region"
762
+ [options]="regions"
763
+ optionLabel="title"
764
+ [(value)]="regionId"
765
+ [filter]="true"
766
+ />
767
+
768
+ <gog-multiselect
769
+ label="Tags"
770
+ [options]="tags"
771
+ [(value)]="selectedTagIds"
772
+ [showControls]="true"
773
+ formControlName="tags"
774
+ errorDisplay="auto"
775
+ />
776
+ ```
777
+
778
+ #### `gog-autocomplete`
779
+
780
+ Shares `GogDropdownBase` too, but the trigger is a real `<input>` (combobox pattern,
781
+ `aria-activedescendant`), not a listbox button — so it does **not** reuse the base's built-in
782
+ panel-filter box; it filters/searches off what's typed in the field itself.
783
+
784
+ | Input | Type | Default | Notes |
785
+ | ------------------------------------------------------------------------------------------------------ | ---------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
786
+ | _(all the shared `GogDropdownBase` inputs above except `filter`/`filterPlaceholder`/`filterPosition`)_ | | | |
787
+ | `filterLocal` | `boolean` | `true` | narrow `options` client-side as you type; turn **off** when `gogSearch` already returns a filtered server list (avoids double-filtering) |
788
+ | `minLength` | `number \| undefined` | `1` | via `GOG_CONFIG.autocomplete.minLength` |
789
+ | `openOnFocus` | `boolean \| undefined` | `true` | via `GOG_CONFIG.autocomplete.openOnFocus` |
790
+ | `searchDebounce` | `number \| undefined` | `300` | ms before `gogSearch` fires; via `GOG_CONFIG.autocomplete.searchDebounce` |
791
+ | `loading` | `boolean` | `false` | shows a spinner in the trailing slot |
792
+ | `emptyMessage` | `string` | `'No matches'` | |
793
+ | `forceSelection` | `boolean` | `true` | see note below |
794
+ | `ripple` | `boolean \| undefined` | `false` | press ripple; via `GOG_CONFIG.ripple.enabled` |
795
+
796
+ Outputs: `gogSearch: string` (debounced query — wire your server lookup here),
797
+ `gogLoadMore: void` (panel scrolled to the end — fetch the next page).
798
+
799
+ Model: `value: TValue | null`. CVA: yes.
800
+
801
+ **`forceSelection` matters.** On (default): the field always ends up reflecting a real
802
+ selection free-typed text that matches nothing snaps back on blur/Escape. Off: what the user
803
+ typed is itself meaningful (a create-as-you-type flow) — read the typed text from `gogSearch`,
804
+ not from `value`, since `value` clears the moment the text stops matching the selection.
805
+
806
+ ```html
807
+ <gog-autocomplete
808
+ [options]="users"
809
+ optionLabel="profile.fullName"
810
+ [optionValue]="null"
811
+ [(value)]="user"
812
+ [loading]="searching()"
813
+ (gogSearch)="search($event)"
814
+ />
815
+ ```
816
+
817
+ #### `gog-checkbox`
818
+
819
+ | Input | Type | Default |
820
+ | ---------------------------------------- | ---------------------- | ------- |
821
+ | `label`, `ariaLabel` | `string` | `''` |
822
+ | `size` | `GogSize \| undefined` | `'md'` |
823
+ | `indeterminate`, `disabled`, `fullWidth` | `boolean` | `false` |
824
+
825
+ Model: `checked: boolean`. CVA: yes. Slot: `<ng-template gogCheckboxIcon>` for a custom tick
826
+ icon.
827
+
828
+ ```html
829
+ <gog-checkbox label="I agree to the terms" formControlName="agree" />
830
+ ```
831
+
832
+ #### `gog-toggle`
833
+
834
+ An on/off switch (`role="switch"`) — semantically different from a checkbox ("is this setting
835
+ on", not "is this one of the things you selected").
836
+
837
+ | Input | Type | Default | Notes |
838
+ | ----------------------- | ---------------------- | ------- | ------------------------------------- |
839
+ | `label`, `ariaLabel` | `string` | `''` | |
840
+ | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
841
+ | `disabled`, `fullWidth` | `boolean` | `false` | |
842
+ | `labelPosition` | `'start' \| 'end'` | `'end'` | |
843
+ | `onLabel`, `offLabel` | `string` | `''` | text rendered inside the track itself |
844
+
845
+ Model: `checked: boolean`. CVA: yes.
846
+
847
+ ```html
848
+ <gog-toggle label="Notifications" formControlName="notificationsOn" onLabel="ON" offLabel="OFF" />
849
+ ```
850
+
851
+ #### `gog-radio-group`
852
+
853
+ | Input | Type | Default |
854
+ | ------------------------------ | ----------------------------------------------- | ---------------- |
855
+ | `options` | `GogRadioOption[]` (`{ id, label, disabled? }`) | `[]` |
856
+ | `label`, `ariaLabel`, `name` | `string` | `''` |
857
+ | `size` | `GogSize \| undefined` | `'md'` |
858
+ | `disabled`, `fullWidth` | `boolean` | `false` |
859
+ | `orientation` | `GogOrientation` | `'vertical'` |
860
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` |
861
+
862
+ Model: `value: string | number | null`. CVA: yes. Fixed `{ id, label, disabled? }` shape (not
863
+ a generic accessor, unlike select/multiselect/button-toggle).
864
+
865
+ ```html
866
+ <gog-radio-group
867
+ [options]="[{id:'m',label:'Male'},{id:'f',label:'Female'}]"
868
+ formControlName="gender"
869
+ />
870
+ ```
871
+
872
+ #### `gog-slider`
873
+
874
+ | Input | Type | Default |
875
+ | -------------------------------- | ---------------------- | ---------------------------------------------- |
876
+ | `label`, `ariaLabel` | `string` | `''` |
877
+ | `min`, `max`, `step` | `number` | `0`, `100`, `1` |
878
+ | `showValue`, `showThumb` | `boolean` | `true` |
879
+ | `errorMessage`, `errorDisplay` | | `''`, `'manual'` |
880
+ | `disabled` | `boolean` | `false` |
881
+ | `fullWidth` | `boolean` | `true` (ignored when `orientation="vertical"`) |
882
+ | `orientation` | `GogSliderOrientation` | `'horizontal'` |
883
+ | `range` | `boolean` | `false` — two thumbs; see below |
884
+ | `startDisabled`, `endDisabled` | `boolean` | `false` — `range` only |
885
+ | `startAriaLabel`, `endAriaLabel` | `string` | `'Minimum'` / `'Maximum'`, prefixed by `label` |
886
+
887
+ Models: `value: number`, and `rangeValue: GogSliderRange` (`{ start: number; end: number }`).
888
+ CVA: yes. Backed by a real `<input type="range">` (rotated via `writing-mode` for vertical), so
889
+ dragging/touch/keyboard all come from the platform.
890
+
891
+ ```html
892
+ <gog-slider label="Volume" [min]="0" [max]="100" formControlName="volume" />
893
+ ```
894
+
895
+ **Range mode.** `[range]="true"` puts a second thumb on the track and switches which model is
896
+ live: bind `[(rangeValue)]` instead of `[(value)]`. The two are **mutually exclusive** — `value`
897
+ (and a form control's `writeValue`) is ignored while `range` is on, and vice versa.
898
+
899
+ ```html
900
+ <gog-slider label="Price" [range]="true" [(rangeValue)]="price" startAriaLabel="Lowest" />
901
+ ```
902
+
903
+ Each thumb needs its own accessible name, because one `<label>` cannot be associated with two
904
+ inputs through `for`; unset, they fall back to `'Minimum'`/`'Maximum'` prefixed with `label`
905
+ (`'Price Minimum'`). `startDisabled`/`endDisabled` pin one end while the other stays movable —
906
+ they are ORed with `disabled` rather than overriding it, and unlike it they do not dim the whole
907
+ control or cut pointer events over the track, which would take the still-enabled thumb with them.
908
+
909
+ #### `gog-datepicker` / `gog-calendar`
910
+
911
+ `gog-datepicker` is a field + panel; `gog-calendar` is the month grid alone (what `inline` mode
912
+ renders). Native `Date` only — no date library, no adapter.
913
+
914
+ | Input | Type | Default | Notes |
915
+ | ----------------------------------------------------- | -------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------- |
916
+ | `inputId`, `label`, `ariaLabel`, `placeholder` | `string` | `''` | |
917
+ | `selectionMode` | `GogDateSelectionMode` (`'single'\|'range'`) | `'single'` | |
918
+ | `min`, `max` | `Date \| null` | `null` | |
919
+ | `disabledDates` | `((date: Date) => boolean) \| null` | `null` | predicate, not a list |
920
+ | `defaultMonth` | `Date \| null` | `null` | which month opens when nothing is selected |
921
+ | `numberOfMonths` | `number` | `1` | `2` is what makes a range picker usable |
922
+ | `showTime`, `hourFormat`, `minuteStep`, `showSeconds` | | `false`, `'24'`, `1`, `false` | |
923
+ | `showTodayButton` | `boolean` | `true` | **selects** today |
924
+ | `showThisMonthButton` | `boolean` | `false` | only moves the _view_, leaves selection alone |
925
+ | `format` | `string \| null` | `null` | display/parse pattern (`'dd.MM.yyyy'`); derived from `showTime` when unset |
926
+ | `locale` | `string \| undefined` | `'en-US'` | via `GOG_CONFIG.datepicker.locale` |
927
+ | `firstDayOfWeek` | `number \| undefined` | locale's own | via `GOG_CONFIG.datepicker.firstDayOfWeek` |
928
+ | `allowTextInput` | `boolean` | `true` | typed text parsed against `format`; unparseable drafts don't clear the value |
929
+ | `inline` | `boolean` | `false` | renders the calendar with no field/panel |
930
+ | `disabled`, `fullWidth` | | `false`, `true` | |
931
+ | `clearable`, `clearAriaLabel` | | `false`, `'Clear date'` | |
932
+ | `errorMessage`, `errorDisplay`, `size` | | `''`, `'manual'`, `'md'` | |
933
+ | `floatLabel`, `floatLabelShowPlaceholder` | | `'none'`, `false` | |
934
+ | `appendToBody`, `dropdownDirection`, `dropdownZIndex` | | `false`, `'auto'`, `null` | |
935
+
936
+ Model: `value: Date | GogDateRange | null` (`GogDateRange = { start: Date | null; end: Date | null }`).
937
+ CVA: yes.
938
+
939
+ `gog-calendar` (usable standalone) takes most of the same date/range/time inputs directly, plus
940
+ `gogDateSelect: output<GogDatepickerValue>()` fired only on a _complete_ selection. It resolves
941
+ `locale` and `firstDayOfWeek` from `GOG_CONFIG.datepicker` itself, so a standalone calendar
942
+ honours an app-wide locale without being handed one; its navigation, shortcut and time labels
943
+ (`todayLabel`, `thisMonthLabel`, `previousMonthLabel`, `nextMonthLabel`, `previousYearLabel`,
944
+ `nextYearLabel`, `hoursLabel`, `minutesLabel`, `secondsLabel`) resolve through
945
+ `GOG_CONFIG.labels` the same way.
946
+
947
+ Also exported for direct reuse: `formatDate(date, pattern)`, `parseDate(text, pattern)`, and a
948
+ family of date-math helpers (`addDays`, `addMonths`, `isSameDay`, `isWithinBounds`, …) from
949
+ `date-utils`.
950
+
951
+ **Sizing.** `gog-calendar` caps itself at its own month grid — you do not need to give it a
952
+ width. `--gog-calendar-max-width` (default `max-content`) is the cap, and it covers the size
953
+ variants, `numberOfMonths`, `showTime` and wider locales on its own; set it to `100%` for a
954
+ calendar that fills its container. This is also what sizes `inline` mode, because `inline` is
955
+ `gog-calendar` with a border and nothing else. The dropdown panel is separate:
956
+ `--gog-datepicker-panel-width`, also `max-content`.
957
+
958
+ ```html
959
+ <gog-datepicker label="Birth date" [(value)]="birthDate" [max]="today" />
960
+ <gog-datepicker selectionMode="range" [(value)]="stayRange" [numberOfMonths]="2" />
961
+ ```
962
+
963
+ ### Display, feedback & status
964
+
965
+ #### `gog-icon`
966
+
967
+ | Input | Type | Default |
968
+ | ------------ | --------------------- | -------------------------------------------------- |
969
+ | `name` | `GogIconName` | `'close'` |
970
+ | `template` | `TemplateRef \| null` | `null` custom markup instead of the built-in SVG |
971
+ | `title` | `string` | `''` |
972
+ | `ariaHidden` | `boolean` | `true` |
973
+
974
+ The package ships **41** glyphs (`GogBuiltinIconName`), all from [Lucide](https://lucide.dev)
975
+ and inlined so the package keeps zero runtime dependencies:
976
+
977
+ | Group | Names |
978
+ | ----------------- | ------------------------------------------------------------------------------------------------------ |
979
+ | Chevrons & arrows | `chevron-up`, `chevron-down`, `chevron-left`, `chevron-right`, `arrow-left`, `arrow-right` |
980
+ | Confirm & dismiss | `check`, `close`, `checkbox`, `checkbox-checked` |
981
+ | Status | `success`, `error`, `warning`, `info` |
982
+ | Sorting | `sort`, `sort-up`, `sort-down`, `filter` |
983
+ | Actions | `search`, `plus`, `minus`, `trash`, `pencil`, `copy`, `download`, `upload`, `refresh`, `external-link` |
984
+ | Chrome | `menu`, `more-horizontal`, `more-vertical`, `settings` |
985
+ | Objects & state | `user`, `lock`, `mail`, `calendar`, `clock`, `eye`, `eye-off`, `star`, `star-filled` |
986
+
987
+ `star` / `star-filled` is the one outline/filled pair, for a rating or favourite **toggle** —
988
+ the same reason `checkbox` / `checkbox-checked` exists. The set is otherwise outline-only on
989
+ purpose; a blanket solid duplicate of every glyph would double the payload for a distinction
990
+ almost nothing needs. If you want a filled variant of something else, register it with
991
+ `provideGogIcons`.
992
+
993
+ `Object.keys(ICON_DEFS)` is the runtime list, if you need to enumerate them (an icon picker, a
994
+ gallery). Do not hand-copy the names into an array — that is what goes stale.
995
+
996
+ ```html
997
+ <gog-icon name="calendar" />
998
+ ```
999
+
1000
+ ##### Registering your own icons — `provideGogIcons(...)`
1001
+
1002
+ `name` is typed `GogIconName = GogBuiltinIconName | (string & {})`: the built-ins autocomplete,
1003
+ and any name you register is accepted. **This is the supported way to use your own icon set** —
1004
+ prefer it over the `template` input, which costs an `<ng-template>` at every use site and is for
1005
+ one-offs.
1006
+
1007
+ ```ts
1008
+ // app.config.ts
1009
+ import { provideGogIcons } from '@guildofgleks/ui';
1010
+
1011
+ providers: [
1012
+ provideGogIcons({
1013
+ cart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">…</svg>',
1014
+ rocket: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">…</svg>',
1015
+ }),
1016
+ ];
1017
+ ```
1018
+
1019
+ ```html
1020
+ <gog-icon name="cart" />
1021
+ <gog-tag iconName="cart">In basket</gog-tag>
1022
+ <!-- works anywhere an icon *name* is taken -->
1023
+ ```
1024
+
1025
+ - **Registered names win over built-ins of the same name** — that is how you replace the
1026
+ library's checkmark or chevrons across every component at once, without touching any of them.
1027
+ - **Nested `provideGogIcons(...)` layers onto the parent set** rather than replacing it, the same
1028
+ as `provideGogConfig`: a lazy route can register only what it uses.
1029
+ - **An unknown name renders nothing and warns in dev mode; it never throws.** An icon is
1030
+ decoration — failing the render over a typo would be the worse outcome.
1031
+ - **`GOG_ICONS`** is the `InjectionToken<Readonly<Record<string, string>>>` behind it, exported
1032
+ for the one case `provideGogIcons` does not cover: reading the registered set back
1033
+ (`inject(GOG_ICONS)`) to enumerate it in an icon picker. Provide it through
1034
+ `provideGogIcons(...)` rather than directly — the helper is what layers a child injector's
1035
+ icons onto the parent's instead of replacing them.
1036
+ - **Write the SVG for inheritance:** a `viewBox`, `stroke="currentColor"` (or `fill`), and no
1037
+ width/height `gog-icon` drives size and stroke width from the `--gog-icon-*` tokens, so a
1038
+ registered icon scales and colours like a built-in.
1039
+ - **Security:** the markup is inserted with `bypassSecurityTrustHtml` (Angular's HTML sanitizer
1040
+ strips SVG, so there is no alternative). That is fine for static icon markup you authored;
1041
+ **never** build a registered icon string from user input or fetch it at runtime unsanitized.
1042
+
1043
+ #### `[gogButton]` the link-flavoured button
1044
+
1045
+ `gog-button` renders its own `<button>`, so it can never _be_ a link. `[gogButton]` inverts that:
1046
+ the element stays yours and the directive only gives it the look.
1047
+
1048
+ ```html
1049
+ <a gogButton routerLink="/pricing">See pricing</a>
1050
+ <a gogButton variant="ghost" href="https://example.com" target="_blank" rel="noreferrer">Docs</a>
1051
+ <button gogButton variant="outline" size="sm" type="submit">Save</button>
1052
+ <a gogButton fullWidth routerLink="/checkout">Checkout</a>
1053
+ ```
1054
+
1055
+ | Input | Type | Default |
1056
+ | ----------- | ------------------------ | ---------------------------------------- |
1057
+ | `variant` | `GogVariant` | `'primary'` |
1058
+ | `severity` | `GogSeverity` | `'accent'`; same as `gog-button` |
1059
+ | `size` | `GogSize \| undefined` | `'md'`; via `GOG_CONFIG.control.size` |
1060
+ | `fullWidth` | `boolean` (bare attr ok) | `false` |
1061
+ | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1062
+
1063
+ Selector is `a[gogButton], button[gogButton]` — deliberately not a bare `[gogButton]`, because on
1064
+ a `<div>` the result looks like a button and is invisible to the keyboard and to assistive tech.
1065
+
1066
+ **Which to reach for.** `gog-button` for a button that acts on the page: it owns `loading` (a
1067
+ centred spinner it projects), `debounce` click throttling and the `gogClick` output, none of which
1068
+ a bare element can provide. `[gogButton]` when the element must be a link, or when you need to
1069
+ keep directives of your own on it — `routerLink`, `href`, `target`, `download`, `type="submit"`
1070
+ and anything else keep working because they were never brokered through an input in the first
1071
+ place. That is also why the library still has no `@angular/router` dependency.
1072
+
1073
+ Two things it deliberately does not do: no `disabled` on an `<a>` (there is no such thing — drop
1074
+ the `href` or render a real `<button>`), and no loading state (the spinner is a projected child a
1075
+ directive cannot add without taking over the element's content).
1076
+
1077
+ #### `[gogBadge]` — directive, not a component
1078
+
1079
+ Decorates an existing element (a button, an icon, an avatar) with a count/status dot — it never
1080
+ wraps its host.
1081
+
1082
+ | Input | Type | Default |
1083
+ | ---------------- | --------------------------------------------------------------------------- | -------------------------------- |
1084
+ | `gogBadge` | `string \| number \| null` | `null` — the content |
1085
+ | `badgePosition` | `GogBadgePosition` (`'top-end'\|'top-start'\|'bottom-end'\|'bottom-start'`) | `'top-end'` |
1086
+ | `badgeVariant` | `GogTagVariant` | `'danger'` |
1087
+ | `badgeDot` | `boolean` | `false` bare dot, no text |
1088
+ | `badgeMax` | `number` | `99` — beyond this, renders `N+` |
1089
+ | `badgeHidden` | `boolean` | `false` |
1090
+ | `badgeAriaLabel` | `string` | `''` |
1091
+
1092
+ Renders **nothing** when the value is `0`, `null` or empty and `badgeDot` is off — "0" badges
1093
+ are impossible by design.
1094
+
1095
+ ```html
1096
+ <gog-button gogBadge="12" badgeAriaLabel="12 unread">Inbox</gog-button>
1097
+ <gog-icon name="info" gogBadge badgeDot />
1098
+ ```
1099
+
1100
+ #### `gog-chip`
1101
+
1102
+ | Input | Type | Default |
1103
+ | ------------------------------ | ----------------------------------- | ---------------------------------------- |
1104
+ | `size` | `GogSize` | `'md'` |
1105
+ | `shape` | `GogTagShape` (`'rounded'\|'pill'`) | `'rounded'` |
1106
+ | `disabled`, `clickable` | `boolean` | `false`, `true` |
1107
+ | `selected` | `boolean \| null` (two-way) | `null` see below |
1108
+ | `removable` | `boolean` | `false` |
1109
+ | `fullWidth` | `boolean` | `false` |
1110
+ | `ariaLabel`, `removeAriaLabel` | `string` | `''`, `'Remove chip'` |
1111
+ | `avatarUrl`, `avatarAlt` | `string \| null` / `string` | `null`, `''` |
1112
+ | `iconName` | `GogIconName \| null` | `null` |
1113
+ | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1114
+
1115
+ Outputs: `gogClick: MouseEvent | KeyboardEvent`, `gogRemove: void`.
1116
+
1117
+ ```html
1118
+ <gog-chip [avatarUrl]="user.photo" [removable]="true" (gogRemove)="removeUser(user)"
1119
+ >{{ user.name }}</gog-chip
1120
+ >
1121
+ ```
1122
+
1123
+ **`selected` makes it a filter chip** (21.9.0) a chip you toggle on and off rather than press.
1124
+ It is tri-state, and `null` is the default so nothing about an existing chip changes: no
1125
+ `aria-pressed`, no selected look, activation only emits `gogClick`. Set it to `false` and the chip
1126
+ is a toggle that is off (`aria-pressed="false"` a chip with no `aria-pressed` at all is not a
1127
+ toggle to a screen reader, so "off" has to be stated); `true` and it is on, which draws an inset
1128
+ ring from `--gog-chip-selected-shadow`. A ring rather than a fill because `:hover` and `:active`
1129
+ already own the chip's background and the selection has to survive both.
1130
+
1131
+ It is a two-way `model`, so the chip flips it on click, Enter and Space — a row of filters needs
1132
+ no click handler:
1133
+
1134
+ ```html
1135
+ @for (f of filters; track f.label) {
1136
+ <gog-chip [(selected)]="f.on">{{ f.label }}</gog-chip>
1137
+ }
1138
+ ```
1139
+
1140
+ `gogClick` still fires, **after** the flip, so a handler reading `selected()` sees the new value.
1141
+ Drive the state from that handler instead and you want a one-way `[selected]`, or the two writes
1142
+ cancel out. A `disabled` chip keeps the ring but drops `aria-pressed`, which needs the
1143
+ `role="button"` a disabled chip does not carry — "selected, and currently unavailable" is a real
1144
+ state and hiding it would leave it announced and invisible.
1145
+
1146
+ #### `gog-tag`
1147
+
1148
+ | Input | Type | Default |
1149
+ | ----------- | --------------------- | ----------- |
1150
+ | `variant` | `GogTagVariant` | `'info'` |
1151
+ | `size` | `GogSize` | `'md'` |
1152
+ | `shape` | `GogTagShape` | `'rounded'` |
1153
+ | `iconName` | `GogIconName \| null` | `null` |
1154
+ | `fullWidth` | `boolean` | `false` |
1155
+
1156
+ Slot: `<ng-template gogTagIcon>` for custom icon markup.
1157
+
1158
+ ```html
1159
+ <gog-tag variant="success">Active</gog-tag>
1160
+ ```
1161
+
1162
+ #### `gog-spinner` / `gog-spinner-overlay`
1163
+
1164
+ | Input | Type | Default |
1165
+ | -------------------------------- | ------------------------------------------------- | ------------------------------------------- |
1166
+ | `size` | `GogSize` | `'md'` |
1167
+ | `variant` | `GogSpinnerVariant` (`'runic'\|'ring'\|'custom'`) | unset see below |
1168
+ | `ariaLabel` | `string` | `'Loading'` |
1169
+ | `overlay` (spinner only) | `boolean` | `false` |
1170
+ | `loading` (spinner-overlay only) | `boolean` | `false` toggles the overlay + `aria-busy` |
1171
+
1172
+ `variant="custom"` renders your own projected markup, still inheriting the size wrapper and
1173
+ `--gog-spinner-color` theming.
1174
+
1175
+ **To replace the spinner everywhere at once, pass a component to `GOG_CONFIG`** including the
1176
+ three places you cannot reach with an input: `gog-button`'s and `gog-autocomplete`'s loading
1177
+ states, and the spinner `gog-table` draws in place of its rows.
1178
+
1179
+ ```ts
1180
+ provideGogConfig({ spinner: { component: HouseLoaderComponent } });
1181
+ ```
1182
+
1183
+ It renders inside the same size wrapper as the built-ins, so it keeps the sizing, the overlay
1184
+ behaviour, `role="status"` and the accessible name — only the visual is yours. An instance's own
1185
+ `variant` still wins over it, so `<gog-spinner variant="ring">` is a ring in an app that has set
1186
+ a component: a default does not overrule something asked for explicitly.
1187
+
1188
+ **Neither component's `variant` has a default value**, and on `gog-spinner-overlay` that is the
1189
+ whole of the 21.10.0 fix: the overlay forwards its `variant` to the spinner it wraps, so a default
1190
+ there would have been an instance overruling the config on every overlay ever rendered — which is
1191
+ exactly what happened before, leaving the one spinner that covers a whole region on the built-in
1192
+ look while every other spinner in the app was the house one. `size` and `ariaLabel` keep their
1193
+ defaults: neither has a config key to fall through to.
1194
+
1195
+ ```html
1196
+ <gog-spinner-overlay [loading]="isLoading()">
1197
+ <app-content-that-loads />
1198
+ </gog-spinner-overlay>
1199
+ ```
1200
+
1201
+ #### `gog-skeleton`
1202
+
1203
+ | Input | Type | Default |
1204
+ | ----------------- | -------------------------------------------------- | ---------------------------------------------------- |
1205
+ | `shape` | `GogSkeletonShape` (`'text'\|'circle'\|'rect'`) | `'text'` |
1206
+ | `size` | `GogSize` | `'md'` |
1207
+ | `animation` | `GogSkeletonAnimation` (`'pulse'\|'wave'\|'none'`) | `'pulse'` |
1208
+ | `width`, `height` | `string \| null` | `null` |
1209
+ | `lines` | `number` | `1` — `shape="text"` only, last line renders shorter |
1210
+ | `rounded` | `boolean` | `true` |
1211
+ | `ariaLabel` | `string \| null` | `null` — decorative (no `role`) unless set |
1212
+
1213
+ ```html
1214
+ <gog-skeleton shape="text" [lines]="3" /> <gog-skeleton shape="circle" width="48px" />
1215
+ ```
1216
+
1217
+ #### `gog-progressbar`
1218
+
1219
+ | Input | Type | Default |
1220
+ | ----------------- | ---------------------------------------------------------------------------- | --------------- |
1221
+ | `value`, `buffer` | `number` (0–100, clamped) | `0` |
1222
+ | `mode` | `GogProgressbarMode` (`'determinate'\|'indeterminate'\|'buffer'`) | `'determinate'` |
1223
+ | `variant` | `GogProgressbarVariant` (`'accent'\|'success'\|'danger'\|'warning'\|'info'`) | `'accent'` |
1224
+ | `size` | `GogSize` | `'md'` |
1225
+ | `showValue` | `boolean` | `false` |
1226
+ | `ariaLabel` | `string` | `''` |
1227
+
1228
+ ```html
1229
+ <gog-progressbar mode="indeterminate" ariaLabel="Loading" />
1230
+ <gog-progressbar mode="buffer" [value]="42" [buffer]="70" />
1231
+ ```
1232
+
1233
+ **The fill's end is marked by two hairlines** (21.10.0), `--gog-progressbar-edge-color` over
1234
+ `--gog-progressbar-edge-backing-color`, each `--gog-progressbar-edge-width` wide. That boundary is
1235
+ the value — `showValue` is off by default — and the fill and the track cannot carry it themselves:
1236
+ in every shipped theme the five fills straddle mid-luminance, so no one track colour clears WCAG
1237
+ 1.4.11's 3:1 against all of them. Two tones always do, and `check:contrast` gates the pair. Retint
1238
+ them per theme if you like; keep them a _pair_ whose tones sit on opposite sides of the middle, or
1239
+ the marker disappears on whichever fill it happens to match.
1240
+
1241
+ #### `gog-divider`
1242
+
1243
+ | Input | Type | Default |
1244
+ | ------------- | --------------------------------------------------- | -------------- |
1245
+ | `orientation` | `GogOrientation` | `'horizontal'` |
1246
+ | `variant` | `GogDividerVariant` (`'solid'\|'dashed'\|'dotted'`) | `'solid'` |
1247
+ | `inset` | `boolean` | `false` |
1248
+
1249
+ Label is projected content, not an input — put an icon or a `gog-tag` inside it if needed.
1250
+
1251
+ ```html
1252
+ <gog-divider>OR</gog-divider>
1253
+ ```
1254
+
1255
+ #### `gogRipple` directive, not a component
1256
+
1257
+ A pointer-position wash that grows from where you pressed and fades when you let go. Drop it on
1258
+ any element you already have it adds no wrapper and changes no layout.
1259
+
1260
+ | Input | Type | Default |
1261
+ | ---------------- | --------- | ----------------------------------------------------------- |
1262
+ | `rippleDisabled` | `boolean` | `false` |
1263
+ | `rippleCentred` | `boolean` | `false` start from the middle instead of from the pointer |
1264
+
1265
+ ```html
1266
+ <button gogRipple>Press me</button>
1267
+ <div gogRipple rippleCentred class="tile">A tile</div>
1268
+ ```
1269
+
1270
+ Four things suppress it, none of which you have to wire up: `rippleDisabled`, a host carrying
1271
+ `disabled`, a host carrying `aria-disabled="true"`, and `prefers-reduced-motion: reduce` the
1272
+ last one **suppressed outright, not shortened**. Keyboard activation (`Enter`/`Space`) is always
1273
+ centred, because a key press carries no coordinates.
1274
+
1275
+ **Put it on the element that paints the surface.** The wash lives in its own layer that clips
1276
+ itself — the host is never given `overflow: hidden`, so a `gogBadge` on the same element is not
1277
+ clipped and that layer takes its corner radius from its host with `border-radius: inherit`. On a
1278
+ wrapper whose _child_ paints the rounded background, the layer inherits the wrapper's radius (very
1279
+ often `0`) and the wash squares off at the corners.
1280
+
1281
+ Tokens: `--gog-ripple-color` (`currentColor`, so the wash reads as the surface's own foreground on
1282
+ a filled surface and a ghost one alike), `--gog-ripple-opacity`, `--gog-ripple-enter-duration`,
1283
+ `--gog-ripple-exit-duration`, `--gog-ripple-easing`. All five are ordinary inherited custom
1284
+ properties, so setting one anywhere above the host is the per-instance override.
1285
+
1286
+ #### Turning the ripple on for the library's own components
1287
+
1288
+ You do **not** add `gogRipple` to a `gog-*` component: each one already owns the element that
1289
+ paints its surface, so it wires its own. What you do is switch it on, once:
1290
+
1291
+ ```ts
1292
+ provideGogConfig({ ripple: { enabled: true } });
1293
+ ```
1294
+
1295
+ That covers `gog-button`, `[gogButton]`, `gog-button-toggle-group`, `gog-chip`, `gog-tabs`
1296
+ headers, `gog-accordion` headers, `gogCollapsibleTrigger`, `gogMenuItem`, and the options inside
1297
+ `gog-select` / `gog-multiselect` / `gog-autocomplete`. `gog-paginator` follows because its page
1298
+ buttons are `gog-button`s.
1299
+
1300
+ **Off by default**, so adding the ripple to the library changed the look of nothing. Every one of
1301
+ those takes a `ripple` input that beats the config in both directions: `[ripple]="false"` opts one
1302
+ control out of an app-wide on, `[ripple]="true"` opts one in without switching the app over.
1303
+
1304
+ Not covered, and deliberately: `gog-table` rows and `gogCardLink`. A row and a card are hundreds
1305
+ of pixels wide, so the wave has to travel the whole surface and reads as a flash rather than as
1306
+ feedback at the point you pressed — and a table renders one directive per row, with no
1307
+ virtualization in this library yet. Put `gogRipple` on them yourself if you disagree.
1308
+
1309
+ A chip that is not `clickable`, or is `disabled`, never ripples whatever the config says: a label
1310
+ answering a press is a promise it cannot keep.
1311
+
1312
+ #### `gogTooltip` — directive, not a component
1313
+
1314
+ Drop on any element — a `gog-*` component's host tag or a plain native one.
1315
+
1316
+ | Input | Type | Default |
1317
+ | --------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ |
1318
+ | `gogTooltip` | `string \| TemplateRef \| null` | `null` — content |
1319
+ | `gogTooltipPosition` | `GogTooltipPosition` (`'auto'\|'top'\|'bottom'\|'left'\|'right'`) | `'auto'`; via `GOG_CONFIG.tooltip.position` |
1320
+ | `gogTooltipShowDelay` | `number \| undefined` | `300`; via `GOG_CONFIG.tooltip.showDelay` |
1321
+ | `gogTooltipHideDelay` | `number \| undefined` | `100`; via `GOG_CONFIG.tooltip.hideDelay` |
1322
+ | `gogTooltipDisabled` | `boolean` | `false` |
1323
+ | `gogTooltipClass` | `string` | `''` class on the bubble itself, since it's portaled to `<body>` |
1324
+
1325
+ ```html
1326
+ <button gogTooltip="Save changes">💾</button> <gog-chip [gogTooltip]="hintTemplate">Beta</gog-chip>
1327
+ ```
1328
+
1329
+ ### Layout & navigation
1330
+
1331
+ #### `gog-accordion`
1332
+
1333
+ | Input | Type | Default |
1334
+ | --------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
1335
+ | `items` | `GogAccordionItem[]` (`{ id, title, disabled?, [key: string]: unknown }`) | `[]` |
1336
+ | `size` | `GogSize` | `'lg'` (not `'md'` — see conventions) |
1337
+ | `expandFirst`, `multi`, `loading` | `boolean` | `false` |
1338
+ | `skeletonCount` | `number` | `3` — rows shown while `loading` and `items` is still empty |
1339
+ | `showChevron` | `boolean` | `true` |
1340
+ | `headingLevel` | `2\|3\|4\|5\|6 \| undefined` | `undefined` wraps headers in `role="heading"` when set |
1341
+ | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1342
+
1343
+ Model: `openIds: ReadonlySet<string | number>`. Output: `gogToggle: { item, open }`.
1344
+
1345
+ Slots: `<ng-template gogAccordionHeader let-item let-open="open">`,
1346
+ `<ng-template gogAccordionContent let-item>`, `<ng-template gogAccordionChevron let-item let-open="open">`.
1347
+ This is the library's canonical example of the slot pattern — copy its shape for anything similar.
1348
+
1349
+ ```html
1350
+ <gog-accordion [items]="faqItems" [multi]="true">
1351
+ <ng-template gogAccordionContent let-item>{{ item.answer }}</ng-template>
1352
+ </gog-accordion>
1353
+ ```
1354
+
1355
+ #### `gog-collapsible` + `gogCollapsibleTrigger` / `gogCollapsibleContent`
1356
+
1357
+ **Headless primitive** — owns no markup at all, just open/close state plus two attribute
1358
+ directives you place on your own elements. Use this when `gog-accordion`'s opinionated markup
1359
+ doesn't fit (e.g. a sidebar nav group).
1360
+
1361
+ | Input (on `gog-collapsible`) | Type | Default |
1362
+ | ---------------------------- | --------- | ---------------------------------------------------------- |
1363
+ | `disabled` | `boolean` | `false` |
1364
+ | `collapseOnFocusOut` | `boolean` | `false` — close once focus leaves both trigger and content |
1365
+
1366
+ Model: `open: boolean`.
1367
+
1368
+ ```html
1369
+ <gog-collapsible [(open)]="isOpen">
1370
+ <button gogCollapsibleTrigger>Advanced options</button>
1371
+ <div gogCollapsibleContent>
1372
+ <!-- any markup -->
1373
+ </div>
1374
+ </gog-collapsible>
1375
+ ```
1376
+
1377
+ **The trigger can be any element.** On a `<button>` or `<a href>` the directive adds only the
1378
+ ARIA wiring, because the browser already handles focus and keys. On anything else — a `<div>`, a
1379
+ `<span>` — it also supplies `role="button"`, `tabindex="0"` and Enter/Space, so the control it
1380
+ announces is one a keyboard can actually reach. If you set `role` or `tabindex` yourself, the
1381
+ directive leaves both alone: you have said what the element is.
1382
+
1383
+ `gogCollapsibleTrigger` takes a **`ripple`** input of its own (`boolean | undefined`, `false`, via
1384
+ `GOG_CONFIG.ripple.enabled`) the trigger is your element, but the directive owns the ripple so
1385
+ you do not have to add `gogRipple` beside it.
1386
+
1387
+ An open panel is as tall as its content — `--gog-collapsible-max-height` defaults to
1388
+ `max-content`. Set it to a length on an instance to cap one deliberately; the panel is
1389
+ `overflow: hidden`, so a cap **clips** rather than scrolls. (Before 21.4.4 that default was
1390
+ `480px`, which clipped taller panels silently.)
1391
+
1392
+ #### `gog-tabs` + `gog-tab`
1393
+
1394
+ | Input (on `gog-tabs`) | Type | Default |
1395
+ | ------------------------ | ------------------------------------------------------ | ---------------------------------------------------- |
1396
+ | `align` | `GogTabsAlign` (`'start'\|'center'\|'end'\|'stretch'`) | `'start'` |
1397
+ | `orientation` | `GogOrientation` | `'horizontal'` |
1398
+ | `size` | `GogSize` | `'md'` |
1399
+ | `fullWidth`, `ariaLabel` | | `false`, `''` |
1400
+ | `scrollActiveIntoView` | `boolean` | `true` |
1401
+ | `showScrollTrack` | `boolean \| undefined` | follows `scrollActiveIntoView` (hidden when it's on) |
1402
+ | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
1403
+
1404
+ Model: `activeIndex: number`. Output: `gogTabChange: number`.
1405
+
1406
+ | Input (on `gog-tab`) | Type | Default |
1407
+ | -------------------- | --------------------- | ------- |
1408
+ | `label` | `string` | `''` |
1409
+ | `iconName` | `GogIconName \| null` | `null` |
1410
+ | `disabled` | `boolean` | `false` |
1411
+
1412
+ Slots: `<ng-template gogTabHeader let-tab let-active="active">` on `gog-tabs` for custom header
1413
+ markup; `<ng-template gogTabContent>` **inside** a `gog-tab` to make that tab's content **lazy**
1414
+ (built on first activation, then kept alive) instead of the default (rendered immediately,
1415
+ hidden via `[hidden]` while inactive preserves scroll/input state).
1416
+
1417
+ ```html
1418
+ <gog-tabs [(activeIndex)]="tabIndex">
1419
+ <gog-tab label="Profile"><app-profile /></gog-tab>
1420
+ <gog-tab label="Report" iconName="info">
1421
+ <ng-template gogTabContent><app-expensive-report /></ng-template>
1422
+ </gog-tab>
1423
+ </gog-tabs>
1424
+ ```
1425
+
1426
+ #### `gog-card` + `gogCardHeader` / `gogCardMedia` / `gogCardFooter` / `gogCardLink`
1427
+
1428
+ A surface for one self-contained thing — a product tile, a summary, a search result.
1429
+
1430
+ | Input | Type | Default |
1431
+ | --------------- | -------------------------------------------------------- | --------------------------------------- |
1432
+ | `variant` | `GogSurfaceVariant` (`'outlined'\|'elevated'\|'filled'`) | `'outlined'` |
1433
+ | `size` | `GogSize` | `'md'` — drives padding and the row gap |
1434
+ | `disabled` | `boolean` (bare attribute works) | `false` |
1435
+ | `loading` | `boolean` (bare attribute works) | `false` |
1436
+ | `skeletonLines` | `number` | `2` body lines shown while `loading` |
1437
+
1438
+ No outputs. Slots, all **attribute** directives on your own elements (not `ng-template`):
1439
+ `gogCardHeader`, `gogCardMedia`, `gogCardFooter`, `gogCardLink`. Layout order is fixed by the
1440
+ component media, heading, body (the default slot), footer not by the order you write them.
1441
+
1442
+ ```html
1443
+ <gog-card>
1444
+ <img gogCardMedia [src]="person.photo" alt="" />
1445
+ <h3 gogCardHeader><a gogCardLink [routerLink]="['/people', person.id]">{{ person.name }}</a></h3>
1446
+ <p>{{ person.role }}</p>
1447
+ <div gogCardFooter>
1448
+ <gog-button size="xsm" (gogClick)="shortlist(person)">Shortlist</gog-button>
1449
+ </div>
1450
+ </gog-card>
1451
+ ```
1452
+
1453
+ - **`gogCardHeader` names the card.** The card reads that element's `id` (minting one if it has
1454
+ none) and points its own `aria-labelledby` at it, with `role="group"`. A card with no header
1455
+ gets neither an unnamed group is noise, not structure. The heading level is yours; the visual
1456
+ size comes from `--gog-card-heading-font-size` regardless of it.
1457
+ - **There is no `interactive` input, and no `gogClick` output.** A card becomes interactive by
1458
+ _containing_ a `gogCardLink`, which stretches that link's hit area over the whole surface. The
1459
+ link stays yours: `routerLink`, `href`, `target`, middle-click, "open in new tab" and Enter all
1460
+ behave normally, and the focus ring is drawn around the card. `gogCardLink` only applies to
1461
+ `<a>` and `<button>` on a `<div>` it does nothing, deliberately.
1462
+ - **Other controls inside an interactive card still get their own clicks.** A footer button, a
1463
+ checkbox, a second link: each sits above the stretched hit area automatically.
1464
+ - Two costs of the pattern, inherent to it: text in the card cannot be selected by dragging, and
1465
+ a second link is reachable by keyboard but not by clicking the surface around it.
1466
+ - **`loading`** replaces the content with a title bar plus `skeletonLines` text lines and sets
1467
+ `aria-busy`; **`disabled`** dims the card, sets `aria-disabled`, and takes the card link out of
1468
+ the tab order. Both make the link non-clickable. For a _refresh_ of a card that already has
1469
+ content, project a `gog-spinner-overlay` instead `loading` is the first-paint treatment.
1470
+ - `gogCardMedia` runs full-bleed to the card's edges, and rounds into its top corners when it is
1471
+ the first element in the card.
1472
+
1473
+ #### `gog-panel` + `gogPanelHeader` / `gogPanelFooter`
1474
+
1475
+ A titled region of a page — a settings section, a dashboard area, a form group.
1476
+
1477
+ | Input | Type | Default |
1478
+ | --------------- | -------------------------------- | ------------ |
1479
+ | `variant` | `GogSurfaceVariant` | `'elevated'` |
1480
+ | `size` | `GogSize` | `'lg'` |
1481
+ | `collapsible` | `boolean` (bare attribute works) | `false` |
1482
+ | `disabled` | `boolean` (bare attribute works) | `false` |
1483
+ | `loading` | `boolean` (bare attribute works) | `false` |
1484
+ | `skeletonLines` | `number` | `3` |
1485
+
1486
+ Model: `open: boolean` (default `true`, ignored while `collapsible` is off). No outputs beyond
1487
+ `openChange`. Slots: `gogPanelHeader`, `gogPanelFooter` attribute directives on your elements.
1488
+
1489
+ ```html
1490
+ <gog-panel [collapsible]="true" [(open)]="notificationsOpen">
1491
+ <h2 gogPanelHeader>Notifications</h2>
1492
+ <gog-checkbox label="Email digest" [(checked)]="emailDigest" />
1493
+ <div gogPanelFooter><gog-button size="xsm">Save</gog-button></div>
1494
+ </gog-panel>
1495
+ ```
1496
+
1497
+ - **It is a landmark.** With a `gogPanelHeader` it renders `role="region"` named by that heading —
1498
+ which is why the panel gets one and `gog-card` gets `role="group"`: a handful of named regions
1499
+ is how a page is navigated, a landmark per card would bury that list.
1500
+ - **Collapsing composes `gog-collapsible`**, so the state, the id wiring and the animation are the
1501
+ library's existing ones. The heading stays a heading: the toggle is a separate `<button>` named
1502
+ by it through `aria-labelledby`, with its hit area stretched across the header row so clicking
1503
+ the title works for the pointer. Without a header the toggle falls back to
1504
+ `GOG_CONFIG.labels.togglePanel` (default `'Toggle section'`).
1505
+ - **A non-collapsible panel does not clip.** It undoes the collapse geometry it inherits,
1506
+ `overflow` included, so a dropdown or menu opened inside it escapes the panel's box. A
1507
+ _collapsible_ one does clip while animating, exactly like `gog-collapsible` — prefer
1508
+ `[appendToBody]` for an overlay inside one.
1509
+ - **`loading` keeps the heading and the footer** and replaces only the body: a page section is
1510
+ titled before its content arrives, and blanking the title would move the layout twice.
1511
+ - **The surface is never itself a link** — there is no `gogPanelLink`. Controls live inside a
1512
+ panel, and a region that is a link cannot hold them. Use `gog-card` for that.
1513
+
1514
+ #### `gog-paginator`
1515
+
1516
+ | Input | Type | Default |
1517
+ | ------------------------------- | ------------------------------------------------ | -------------------------------------------------- |
1518
+ | `fullWidth`, `totalPages` | `boolean`, `number` | `true`, `1` |
1519
+ | `rangeMode` | `GogPaginatorRangeMode` (`'window'\|'ellipsis'`) | `'window'` — see note |
1520
+ | `visiblePages` | `number` | `5` `'window'` mode only |
1521
+ | `showFirstPage`, `showLastPage` | `boolean` | `false` `'window'` mode only |
1522
+ | `siblingCount` | `number` | `2` — `'ellipsis'` mode only |
1523
+ | `size` | `GogSize` | `'sm'` |
1524
+ | `disabled`, `ariaLabel` | | `false`, `'Pagination'` |
1525
+ | `totalRecords` | `number \| null` | `null` — see below |
1526
+ | `pageSize` | `model<number>` | `10` — two-way bindable |
1527
+ | `showPageSizeSelect` | `boolean \| undefined` | `false`; via `GOG_CONFIG.paginator` |
1528
+ | `pageSizeOptions` | `number[] \| undefined` | `[10, 20, 30, 40, 50]`; via `GOG_CONFIG.paginator` |
1529
+
1530
+ The step buttons (`'Previous page'`/`'Next page'`) and the per-page names are configured, not
1531
+ input-driven: `GOG_CONFIG.labels.previousPage`/`nextPage`, and `labels.page`, a
1532
+ `(page: number, isCurrent: boolean) => string` formatter defaulting to
1533
+ `` `Page ${page}, current page` `` / `` `Go to page ${page}` ``.
1534
+
1535
+ Models: `page: number` (1-based, self-clamps) and `pageSize: number`.
1536
+
1537
+ **Give it `totalRecords` instead of `totalPages` when you know the row count** — it then derives
1538
+ the page count from `pageSize` itself, which is what removes the
1539
+ `computed(() => Math.ceil(total / size))` a consumer would otherwise have to write _and_ keep in
1540
+ sync with the rows-per-page select:
1541
+
1542
+ ```html
1543
+ <gog-paginator
1544
+ [(page)]="page"
1545
+ [(pageSize)]="size"
1546
+ [totalRecords]="items().length"
1547
+ [showPageSizeSelect]="true"
1548
+ />
1549
+ ```
1550
+
1551
+ `totalPages` still works and is the right input when the server tells you a page count directly;
1552
+ `totalRecords` wins when both are set. Changing the page size always returns to page 1 "page 5"
1553
+ of 10-row pages is not "page 5" of 50-row ones, so clamping alone would leave the user somewhere
1554
+ they never asked to be.
1555
+
1556
+ `'window'`: a fixed number of page buttons that slides to keep the current page centered.
1557
+ `'ellipsis'`: first/last pinned, `siblingCount` around the current page, "…" fills the gap
1558
+ (what `gog-table`'s built-in pagination uses).
1559
+
1560
+ ```html
1561
+ <gog-paginator [(page)]="page" [totalPages]="totalPages" />
1562
+ ```
1563
+
1564
+ #### `gog-table<T>`
1565
+
1566
+ | Input | Type | Default |
1567
+ | ----------------------------- | ----------------------------- | ----------------------------------- |
1568
+ | `value` | `T[]` | `[]` |
1569
+ | `fullWidth` | `boolean` | `true` |
1570
+ | `pageSize` | `model<number>` | `0` (no pagination) — two-way |
1571
+ | `showPageSizeSelect` | `boolean \| undefined` | `false`; forwarded to the paginator |
1572
+ | `pageSizeOptions` | `number[] \| undefined` | `[10, 20, 30, 40, 50]`; forwarded |
1573
+ | `showRowNumbers`, `showTotal` | `boolean` | `true`, `false` |
1574
+ | `emptyPlaceholder` | `string` | `'-'` |
1575
+ | `paginatorPosition` | `'left'\|'center'\|'right'` | `'center'` |
1576
+ | `totalPosition` | `'left'\|'right'\|'opposite'` | `'opposite'` |
1577
+ | `loading` | `boolean` | `false` |
1578
+ | `showColumnBorders` | `boolean` | `false` |
1579
+ | `stickyHeader` | `boolean` | `false` pair with `maxHeight` |
1580
+ | `maxHeight` | `string \| null` | `null` any CSS length |
1581
+ | `size` | `GogSize` | `'lg'` (row density not `'md'`) |
1582
+ | `lazy` | `boolean` | `false` see below |
1583
+ | `totalRecords` | `number \| null` | `null` — `lazy` only |
1584
+ | `selectionMode` | `GogTableSelectionMode` | `'none'` |
1585
+ | `selection` | `model<T[]>` | `[]` two-way bindable |
1586
+ | `dataKey` | `string` | `''` row identity field |
1587
+ | `showSelectionColumn` | `boolean` | `true` (once selection is on) |
1588
+ | `interactiveRows` | `boolean` | `false` |
1589
+
1590
+ Outputs: `gogSortChange: GogTableSortEvent` (`{ field, direction }`, `{ field: '', direction:
1591
+ null }` when the third click clears it), `gogPageChange: number` (1-based; **does not fire** on
1592
+ first render, nor for the page reset a new sort causes — that reset belongs to the sort),
1593
+ `gogRowClick: GogTableRowClickEvent<T>` (`{ row, index, originalEvent }`).
1594
+
1595
+ **`fullWidth` also picks the layout algorithm.** Left at its default the table is `100%` wide with
1596
+ `table-layout: fixed`; since 21.6.0 `[fullWidth]="false"` makes it `fit-content` with
1597
+ `table-layout: auto`, so the columns are measured against their content instead of splitting the
1598
+ total evenly. Before 21.6.0 that split clipped the widest header, and a `width` on the column was
1599
+ the workaround — under auto layout a stated `width` is a suggestion weighed against content
1600
+ rather than a hard split, so those can usually go.
1601
+
1602
+ **`stickyHeader` needs `maxHeight`** (both since 21.6.0 for the pairing). A sticky element
1603
+ resolves against its nearest scroll container, and the table wraps itself in a `gog-scroll`;
1604
+ once that scroller moves on either axis it is a scroll container on _both_, because CSS coerces
1605
+ `overflow-y: visible` to `auto` beside a scrolling `overflow-x` (and `clip` to `hidden`). So the
1606
+ header can only ever stick to something inside the table — and without `maxHeight` that viewport
1607
+ is exactly as tall as its content and never scrolls, so there is nothing to stick to.
1608
+
1609
+ ```html
1610
+ <gog-table [value]="rows" maxHeight="260px" [stickyHeader]="true">…</gog-table>
1611
+ ```
1612
+
1613
+ `maxHeight` takes any CSS length and is what makes the table own its vertical scrolling. Left
1614
+ `null`, the table grows to its content and an ancestor scrolls it the header then follows that
1615
+ ancestor's scroll like everything else, which is the pre-21.6.0 behaviour and is fine as long as
1616
+ you are not asking for a sticky header.
1617
+
1618
+ Columns are declared as **projected `gog-column` children**, not an input array:
1619
+
1620
+ ```html
1621
+ <gog-table [value]="rows">
1622
+ <gog-column field="name" header="Name" sortable="true" />
1623
+ <gog-column field="email" header="Email" />
1624
+ <gog-column field="status" header="Status">
1625
+ <ng-template gogColumnBody let-row let-value="value">
1626
+ <gog-tag [variant]="row.active ? 'success' : 'danger'">{{ value }}</gog-tag>
1627
+ </ng-template>
1628
+ </gog-column>
1629
+ </gog-table>
1630
+ ```
1631
+
1632
+ ##### Server-driven tables — `lazy`
1633
+
1634
+ By default the table owns the whole data set: it sorts `value` and slices the page itself. With
1635
+ `[lazy]="true"` it does neither — `value` **is** the current page, already sorted, and the table
1636
+ renders it untouched. Supply `totalRecords` (without it the table cannot know how many pages
1637
+ exist, so pagination stays hidden and it warns in dev), then refetch from the two outputs:
1638
+
1639
+ ```html
1640
+ <gog-table
1641
+ [value]="page()"
1642
+ [lazy]="true"
1643
+ [totalRecords]="total()"
1644
+ [pageSize]="20"
1645
+ [loading]="loading()"
1646
+ dataKey="id"
1647
+ (gogSortChange)="sort.set($event); reload()"
1648
+ (gogPageChange)="pageNumber.set($event); reload()"
1649
+ ></gog-table>
1650
+ ```
1651
+
1652
+ Row numbers still count from the current page (`(page - 1) * pageSize + i + 1`), and `showTotal`
1653
+ reports `totalRecords` rather than `value.length`. **Do not** sort or slice `value` yourself in
1654
+ addition — that is what the flag turns off.
1655
+
1656
+ ##### Rows per page
1657
+
1658
+ `pageSize` is a **`model`**, not an input: `[pageSize]="20"` works exactly as before, and
1659
+ `[(pageSize)]="size"` becomes possible. That is what makes the rows-per-page select work with no
1660
+ wiring the table binds its own model straight to the paginator's, the select writes back
1661
+ through it, and there is no intermediate signal to keep in sync in either direction.
1662
+
1663
+ ```html
1664
+ <!-- off by default; turn it on per table, or app-wide via GOG_CONFIG.paginator -->
1665
+ <gog-table
1666
+ [value]="rows"
1667
+ [(pageSize)]="size"
1668
+ [showPageSizeSelect]="true"
1669
+ [pageSizeOptions]="[5, 10, 20]"
1670
+ ></gog-table>
1671
+ ```
1672
+
1673
+ Changing the size returns to page 1 and does **not** emit `gogPageChange` — the consumer already
1674
+ knows from `pageSizeChange`, and firing both would make a lazy table fetch twice. In `lazy` mode
1675
+ `pageSizeChange` is the refetch signal; bind `[pageSize]` + `(pageSizeChange)` rather than the
1676
+ banana-box if you need to act on it.
1677
+
1678
+ The footer stays visible at a single page whenever the select is on hiding it would strand the
1679
+ user on whatever size produced that one page, with no control left to pick a smaller one.
1680
+
1681
+ ##### Selection
1682
+
1683
+ `selectionMode` turns it on; `[(selection)]` is always a `T[]`, including in `'single'` mode
1684
+ where it holds zero or one row one shape rather than a union to narrow on every read.
1685
+
1686
+ ```html
1687
+ <gog-table
1688
+ [value]="rows"
1689
+ selectionMode="multiple"
1690
+ [(selection)]="selected"
1691
+ dataKey="id"
1692
+ ></gog-table>
1693
+ ```
1694
+
1695
+ - **Set `dataKey`.** Without it rows are matched by object identity, so any refetch that produces
1696
+ new objects silently drops the selection. It is also the `@for` track key, which is what lets
1697
+ the DOM survive a refetch instead of being rebuilt.
1698
+ - The checkbox column renders automatically (`showSelectionColumn` to turn it off, e.g. for a
1699
+ table that selects by row click pair that with `interactiveRows`).
1700
+ - The header select-all appears only in `'multiple'` mode and covers **the current page**, never
1701
+ the whole data set: in `lazy` mode the table has never seen the other pages, and a control that
1702
+ behaved differently between the two modes would be worse than either.
1703
+
1704
+ ##### Clickable rows
1705
+
1706
+ `gogRowClick` fires on a click regardless, but a `<tr>` is not focusable, so on its own that is a
1707
+ mouse-only affordance. `interactiveRows` makes rows focusable and styles them as clickable, and
1708
+ Enter/Space then activate the focused row. If the action is really "open this one thing", a link
1709
+ or button inside a cell is better than a whole-row target.
1710
+
1711
+ `gog-column` inputs: `field` (required, dot-paths ok), `header`, `sortable` (default `false`),
1712
+ `width`/`minWidth`/`maxWidth`, `comparator` (custom `(a, b) => number`, defaults to a
1713
+ locale-aware collator for strings). Slots inside a column: `<ng-template gogColumnBody let-row let-value="value" let-index="index">`,
1714
+ `<ng-template gogColumnHeader let-header let-field="field">`.
1715
+
1716
+ **Sorting, empty/loading states and pagination are all built in** — sortable columns toggle
1717
+ asc desc unsorted on click, `loading` shows a spinner in place of rows, an empty `value`
1718
+ shows `emptyPlaceholder`, and `pageSize > 0` turns on the internal paginator automatically. You
1719
+ don't need to hand-roll any of this.
1720
+
1721
+ There is **no typed row-selection API** in the current version if you need it, track
1722
+ selection yourself (e.g. a `Set` keyed by row id) and render a `gogColumnBody` checkbox column.
1723
+
1724
+ #### `gog-scroll`
1725
+
1726
+ Drop-in replacement for `overflow: auto` — content still scrolls natively (wheel, touch,
1727
+ keyboard); only the browser's own scrollbar chrome is replaced with a themeable overlay thumb.
1728
+ Used internally by several other components (`gog-dialog`'s body, `gog-select`'s panel,
1729
+ `gog-tabs`' header row) and equally usable directly in your own markup for any scrollable
1730
+ region — the library's official recommendation over a raw `overflow-x`/`overflow-y`.
1731
+
1732
+ | Input | Type | Default |
1733
+ | -------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
1734
+ | `axis` | `GogScrollAxis` (`'vertical'\|'horizontal'\|'both'`) | `'vertical'` |
1735
+ | `size` | `GogScrollSize \| undefined` (`'normal'\|'thin'`) | `'normal'`; via `GOG_CONFIG.scroll.size` |
1736
+ | `autoHide` | `boolean \| undefined` | `true`; via `GOG_CONFIG.scroll.autoHide` |
1737
+ | `hideDelay` | `number \| undefined` | `800`; via `GOG_CONFIG.scroll.hideDelay` |
1738
+ | `reachThreshold` | `number` | `0` |
1739
+ | `focusable` | `boolean` | `true` turn off when the parent already owns focus (a dialog with its own focus trap) |
1740
+ | `ariaLabel` | `string` | `''` |
1741
+ | `overscrollBehavior` | `GogScrollOverscrollBehavior \| undefined` (`'auto'\|'contain'\|'none'`) | `'auto'`; via `GOG_CONFIG.scroll.overscrollBehavior` |
1742
+ | `showTrack` | `boolean \| undefined` | `true`; via `GOG_CONFIG.scroll.showTrack` |
1743
+ | `horizontalWheel` | `boolean \| undefined` | `false`; via `GOG_CONFIG.scroll.horizontalWheel` |
1744
+
1745
+ **`horizontalWheel` turns a vertical wheel into horizontal scrolling** (21.9.0), for the case a
1746
+ consumer hits first: hover a horizontal-only row, turn the wheel, and the _page_ moves. That is
1747
+ the browser's own behaviour and the component deliberately did nothing about it until now.
1748
+
1749
+ It is off by default because it changes what an existing instance does with a gesture it
1750
+ currently passes on; `provideGogConfig({ scroll: { horizontalWheel: true } })` turns it on
1751
+ app-wide. It only acts when the viewport cannot scroll vertically (checked against live
1752
+ geometry, so `axis="both"` scrolls down while there is down to go), the event carries no
1753
+ horizontal delta of its own (a trackpad swipe and `Shift`+wheel already work), `ctrlKey` is
1754
+ clear (pinch-zoom), and there is room left in the direction of the turn. **That last condition is
1755
+ the point:** at the content's end the event is left alone and the page picks it up, so the wheel
1756
+ never goes dead over a scrolled-to-the-end region. `overscrollBehavior: 'contain'` still
1757
+ contains that boundary is the browser's and this never reaches past it.
1758
+
1759
+ Outputs: `gogScroll: GogScrollMetrics`, `gogReachStart`/`gogReachEnd: 'vertical'|'horizontal'`.
1760
+ Methods (via template ref): `scrollTo(options)`, `scrollToTop()`, `scrollToBottom()`,
1761
+ `scrollToLeft()`, `scrollToRight()`.
1762
+
1763
+ ```html
1764
+ <gog-scroll size="thin" [focusable]="false" overscrollBehavior="contain" style="max-height: 320px">
1765
+ <!-- content that might overflow -->
1766
+ </gog-scroll>
1767
+ ```
1768
+
1769
+ ### Overlays
1770
+
1771
+ **Overlays and the viewport — the caveat that bites once per project.** `gog-dialog`'s backdrop,
1772
+ `gog-toast-container` and `gog-spinner [overlay]` are `position: fixed`, which covers the viewport
1773
+ only while no ancestor establishes a containing block. `contain`, `transform`, `filter`,
1774
+ `backdrop-filter` or `will-change` anywhere above retargets them to that element's box — and
1775
+ **`gog-scroll` sets `contain: layout style`**, so a dialog opened inside a scroller dims the
1776
+ scroller rather than the page. Place the dialog and toast outlets in the root component. The
1777
+ dropdown panels and `gog-menu` sidestep it by rendering into `<body>`.
1778
+
1779
+ #### `gog-menu` + `gogMenuTrigger` / `gogMenuItem`
1780
+
1781
+ A command menu. The trigger is a directive on **your own button** usually the icon button you
1782
+ already styled and the items are your own buttons too, so an item can hold an icon, a label and
1783
+ a shortcut hint without an input per piece:
1784
+
1785
+ ```html
1786
+ <button gogButton variant="ghost" [gogMenuTrigger]="rowMenu" aria-label="Row actions">
1787
+ <gog-icon name="more-vertical" />
1788
+ </button>
1789
+
1790
+ <gog-menu #rowMenu ariaLabel="Row actions">
1791
+ <button gogMenuItem (click)="edit(row)"><gog-icon name="check" /> Edit</button>
1792
+ <button gogMenuItem disabled>Transfer ownership</button>
1793
+ <button gogMenuItem (click)="remove(row)"><gog-icon name="close" /> Remove</button>
1794
+ </gog-menu>
1795
+ ```
1796
+
1797
+ | Input | Type | Default | Notes |
1798
+ | ----------- | -------------------------- | -------- | ---------------------------------------------------------------------------- |
1799
+ | `direction` | `'auto' \| 'up' \| 'down'` | `'auto'` | `'auto'` drops down whenever the panel fits and flips up only when it cannot |
1800
+ | `ariaLabel` | `string` | `''` | Names the panel itself |
1801
+
1802
+ **There is no `appendToBody`.** The panel always renders into `<body>` and is placed from the
1803
+ trigger's measured rect, so a menu inside `gog-scroll`, `gog-table` or any `overflow: hidden`
1804
+ ancestor is not clipped and needs no configuration. It also takes the `--gog-dropdown-z` its
1805
+ trigger inherits, so a menu opened inside a `gog-dialog` stacks above the dialog.
1806
+
1807
+ `gogMenuItem` takes a **`ripple`** input (`boolean | undefined`, `false`, via
1808
+ `GOG_CONFIG.ripple.enabled`). The item is your own `<button>`, but the directive owns the ripple,
1809
+ so there is no `gogRipple` to add.
1810
+
1811
+ Output: `gogClosed` fires after every close, whatever caused it.
1812
+
1813
+ Public methods, for driving it yourself: `open(trigger, 'first' | 'last')`, `close(restoreFocus?)`,
1814
+ `toggle(trigger)`, and the `isOpen` signal.
1815
+
1816
+ **Keyboard**, the WAI-ARIA menu button pattern: Enter/Space/ArrowDown open with the first item
1817
+ focused, ArrowUp opens with the last, arrows and Home/End move between items and step over
1818
+ disabled ones, Escape closes and returns focus to the trigger, Tab closes and lets focus move on.
1819
+ A press outside closes without pulling focus back.
1820
+
1821
+ **Disabling an item** is the native `disabled` attribute on your own button — static or bound,
1822
+ there is no input for it:
1823
+
1824
+ ```html
1825
+ <button gogMenuItem disabled>Transfer ownership</button>
1826
+ <button gogMenuItem [disabled]="isLocked()" (click)="edit()">Edit</button>
1827
+ ```
1828
+
1829
+ A disabled item stays in the list rather than disappearing (removing it would shift the others
1830
+ under the pointer), the arrow keys step over it, and clicking it does nothing.
1831
+
1832
+ **A long menu scrolls itself**, using `gog-scroll` — the same thin, auto-hiding scroller as
1833
+ everywhere else in the package, with `overscrollBehavior="contain"` so a wheel at the end of the
1834
+ list does not scroll the page behind it. Arrowing past the last visible item scrolls it into view.
1835
+
1836
+ The panel's height is the smallest of three: its own content, `--gog-menu-max-height` (320px by
1837
+ default), and the room between the trigger and the viewport edge. Lower the token to make a menu
1838
+ scroll sooner. **In 21.5.0 the token did nothing** the measured room was written onto the panel
1839
+ as an inline `max-height`, which beat it; fixed in 21.5.1.
1840
+
1841
+ A closed menu renders nothing at all, so its commands are not in the accessibility tree until it
1842
+ opens.
1843
+
1844
+ #### `gog-dialog`
1845
+
1846
+ A **single** `<gog-dialog />` renders **every** dialog `DialogService.open(...)` creates —
1847
+ place it once, typically in your root app component's template, not per-page and not per-dialog
1848
+ call:
1849
+
1850
+ ```html
1851
+ <!-- app.html -->
1852
+ <router-outlet />
1853
+ <gog-dialog />
1854
+ ```
1855
+
1856
+ It has no inputs of its own everything is driven through `DialogService` (see
1857
+ [Services](#services) above). Supports nesting, dragging (when `draggable !== false` and the
1858
+ dialog has a title or close button), a focus trap for modal dialogs, `Escape` to close (when
1859
+ `closable !== false`), and click-outside-to-close on the backdrop.
1860
+
1861
+ #### `gog-toast` / `gog-toast-container`
1862
+
1863
+ Same pattern place **one** `<gog-toast-container />`, typically in the root component:
1864
+
1865
+ ```html
1866
+ <gog-toast-container [maxVisiblePerPosition]="5" />
1867
+ ```
1868
+
1869
+ `maxVisiblePerPosition` (default `5`) caps how many toasts stack at once per corner; the rest
1870
+ queue. Individual `gog-toast` instances are rendered internally by the container from
1871
+ `ToastService.toasts()` — you don't place these yourself. Toasts auto-dismiss after their
1872
+ `duration` unless `isSticky`; hovering pauses the countdown (front-of-stack toast only).
1873
+
1874
+ Announcements come from two permanently-mounted, visually-hidden live regions the container
1875
+ owns polite, and assertive for `error`/`warning`. The toasts themselves carry no
1876
+ `role`/`aria-live`: a live region created in the same tick as its text is routinely skipped by
1877
+ screen readers, and a second region would announce everything twice. Don't add either back.
1878
+
1879
+ ---
1880
+
1881
+ ## Reading the deprecations at runtime — `GOG_DEPRECATIONS`
1882
+
1883
+ Everything the package currently deprecates, as data:
1884
+
1885
+ ```ts
1886
+ import { GOG_DEPRECATIONS, type GogDeprecation } from '@guildofgleks/ui';
1887
+
1888
+ GOG_DEPRECATIONS; // []
1889
+ ```
1890
+
1891
+ `kind` is `'symbol'` for an export or input and `'token'` for a `--gog-*` custom property. The
1892
+ list is generated from the library's source — tags for symbols, stylesheets for tokens — so it
1893
+ matches what actually still resolves in the version you installed.
1894
+
1895
+ **As of 21.7.0 the list is empty on both halves.** Nothing in the TypeScript API is deprecated, and
1896
+ the three abbreviated token prefixes that used to fill the token half are gone rather than
1897
+ deprecated — see the removal table below. An empty list here means exactly that: nothing to
1898
+ migrate away from right now.
1899
+
1900
+ ## Removed in 21.7.0
1901
+
1902
+ **Nothing in this table exists any more.** Three CSS custom-property prefixes, abbreviations of a
1903
+ component's own name, are gone — each was honoured only as a fallback the spelled-out token wrapped
1904
+ (`--gog-button-x: var(--gog-btn-x, value)`), never declared on its own.
1905
+
1906
+ | Removed | Replacement |
1907
+ | ----------------- | ----------------------------- |
1908
+ | `--gog-btn-*` | `--gog-button-*` |
1909
+ | `--gog-ms-*` | `--gog-multiselect-*` |
1910
+ | `--gog-confirm-*` | `--gog-confirmation-dialog-*` |
1911
+
1912
+ A consumer's CSS that still sets one of the left-hand names doesn't fail their build an
1913
+ unresolved `var()` just stops matching anything, silently. If a themed surface stopped picking up
1914
+ an override after upgrading to 21.7.0, this table is the first thing to check.
1915
+
1916
+ ## Removed in 21.5.0
1917
+
1918
+ **Nothing in this table exists any more.** It is here so that code written against 21.4.x — or
1919
+ generated from a stale copy of this file can be migrated: each row names what a call site must
1920
+ become. If you are writing new code, ignore this section entirely and use the right-hand column,
1921
+ which is documented in full above.
1922
+
1923
+ | Removed | Replacement |
1924
+ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
1925
+ | `gog-select`/`gog-multiselect` `chevronTemplate` input | `<ng-template gogDropdownChevron>` |
1926
+ | `gog-checkbox` `checkIconTemplate` input | `<ng-template gogCheckboxIcon>` |
1927
+ | `gog-tag` `iconTemplate` input | `<ng-template gogTagIcon>` |
1928
+ | `gog-multiselect` `clearIconTemplate` input | `<ng-template gogMultiselectClearIcon>` |
1929
+ | `gog-inputfield` `iconStartTemplate`/`iconEndTemplate`/`iconStartFn`/`iconEndFn`/`iconStartLabel`/`iconEndLabel` | `<span gogInputAddonStart>`/`<span gogInputAddonEnd>` (or a `<button>` with its own handler) |
1930
+ | `gog-table`'s `[template]` attribute (`<ng-template template="field" type="body">`) | `<ng-template gogColumnBody>` / `<ng-template gogColumnHeader>` declared **inside** the matching `<gog-column>` |
1931
+ | `<column>` selector / `Column` export | `<gog-column>` / `GogColumn` |
1932
+ | `GogSelectOption` / `GogMultiselectOption` types | `GogDropdownOption` (the same type they were aliases of it) |
1933
+ | `@guildofgleks/ui/src/styles/…` asset path | `@guildofgleks/ui/styles/…` |
1934
+
1935
+ The general rule they all followed: a `TemplateRef` **input** or a string-keyed lookup was the old
1936
+ shape; a **projected content directive with a typed context**, declared where it's used, is the
1937
+ current one. If you're about to write `fooTemplate` next to an existing `foo` input, or key
1938
+ something off a string that has to match another string elsewhere, that's this exact
1939
+ anti-pattern reach for a slot directive instead.
1940
+
1941
+ ## Full type reference
1942
+
1943
+ Shared enum-like types (`import type { ... } from '@guildofgleks/ui'`):
1944
+
1945
+ | Type | Values |
1946
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1947
+ | `GogSize` | `'xsm' \| 'sm' \| 'md' \| 'lg' \| 'slg'` |
1948
+ | `GogVariant` | `'primary' \| 'secondary' \| 'outline' \| 'ghost'` |
1949
+ | `GogSurfaceVariant` | `'outlined' \| 'elevated' \| 'filled'` `gog-card` and `gog-panel` |
1950
+ | `GogAriaHasPopup` | `boolean \| 'menu' \| 'listbox' \| 'tree' \| 'grid' \| 'dialog'` — `gog-button`'s `ariaHasPopup` |
1951
+ | `GogTagVariant` | `'success' \| 'danger' \| 'warning' \| 'info'` |
1952
+ | `GogOrientation` | `'horizontal' \| 'vertical'` |
1953
+ | `GogTagShape` | `'rounded' \| 'pill'` |
1954
+ | `GogSpinnerVariant` | `'runic' \| 'ring' \| 'custom'` |
1955
+ | `GogSkeletonShape` | `'text' \| 'circle' \| 'rect'` |
1956
+ | `GogSkeletonAnimation` | `'pulse' \| 'wave' \| 'none'` |
1957
+ | `GogPaginatorRangeMode` | `'window' \| 'ellipsis'` |
1958
+ | `GogScrollAxis` | `'vertical' \| 'horizontal' \| 'both'` |
1959
+ | `GogScrollSize` | `'normal' \| 'thin'` |
1960
+ | `GogScrollOverscrollBehavior` | `'auto' \| 'contain' \| 'none'` |
1961
+ | `GogTooltipPosition` | `'auto' \| 'top' \| 'bottom' \| 'left' \| 'right'` |
1962
+ | `GogFloatLabelVariant` | `'none' \| 'in' \| 'on' \| 'over'` |
1963
+ | `GogDropdownFilterPosition` | `'top' \| 'bottom'` |
1964
+ | `GogDividerVariant` | `'solid' \| 'dashed' \| 'dotted'` |
1965
+ | `GogBadgePosition` | `'top-end' \| 'top-start' \| 'bottom-end' \| 'bottom-start'` |
1966
+ | `GogProgressbarMode` | `'determinate' \| 'indeterminate' \| 'buffer'` |
1967
+ | `GogProgressbarVariant` | `'accent' \| 'success' \| 'danger' \| 'warning' \| 'info'` |
1968
+ | `GogButtonToggleAppearance` | `'joined' \| 'separated'` |
1969
+ | `GogTabsAlign` | `'start' \| 'center' \| 'end' \| 'stretch'` |
1970
+ | `GogDateSelectionMode` | `'single' \| 'range'` |
1971
+ | `GogHourFormat` | `'12' \| '24'` |
1972
+ | `GogTextareaResize` | `'vertical' \| 'horizontal' \| 'both' \| 'none'` |
1973
+ | `GogInputType` | `'text' \| 'password' \| 'email' \| 'number' \| 'search' \| 'tel' \| 'url' \| 'date' \| 'time' \| 'datetime-local'` |
1974
+ | `GogInputMode` | `'none' \| 'text' \| 'decimal' \| 'numeric' \| 'tel' \| 'search' \| 'email' \| 'url'` |
1975
+ | `GogTableSelectionMode` | `'none' \| 'single' \| 'multiple'` |
1976
+ | `GogTableSortEvent` | `{ field: string; direction: SortDirection }` |
1977
+ | `GogTableRowClickEvent<T>` | `{ row: T; index: number; originalEvent: MouseEvent \| KeyboardEvent }` |
1978
+ | `GogErrorDisplay` | `'auto' \| 'manual'` |
1979
+ | `GogDropdownDirection` | `'auto' \| 'up' \| 'down'` |
1980
+ | `GogTooltipSide` | `'top' \| 'bottom' \| 'left' \| 'right'` (resolved form of `GogTooltipPosition`, no `'auto'`) |
1981
+ | `GogBuiltinIconName` | the 20 glyphs the package ships — see [`gog-icon`](#gog-icon) |
1982
+ | `GogIconName` | `GogBuiltinIconName \| (string & {})` — built-ins plus anything registered via `provideGogIcons` |