@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/README.md CHANGED
@@ -1,467 +1,503 @@
1
- ![NPM Version](https://img.shields.io/npm/v/@guildofgleks/ui?color=red)
2
- ![Node Version](https://img.shields.io/node/v/@guildofgleks/ui)
3
- ![Angular 21](https://img.shields.io/badge/Angular-21%2B-dd0031?logo=angular)
4
- ![NPM Downloads](https://img.shields.io/npm/dm/@guildofgleks/ui)
5
- ![License](https://img.shields.io/npm/l/@guildofgleks/ui)
6
-
7
- # @guildofgleks/ui
8
-
9
- An Angular 21 and 22 component library with **no CDK and no Material**. 29 components, 5
10
- directives and 3 services, all standalone, all signal-based, themed entirely through CSS custom
11
- properties.
12
-
13
- ```bash
14
- npm install @guildofgleks/ui
15
- ```
16
-
17
- ## Why this one
18
-
19
- - **Small dependency footprint.** Peers are `@angular/core`, `@angular/common`, `@angular/forms`
20
- and `@angular/platform-browser`. No router, no CDK, no animations package. `tslib` is the only
21
- runtime dependency.
22
- - **Signals throughout.** `input()` / `output()` / `model()`, `OnPush` everywhere, no NgModules.
23
- - **Themeable without a build step.** Every value a component paints with is a `--gog-*` custom
24
- property. Swap a palette, restyle one component, or override a single instance — no Sass
25
- variables, no JS theme object.
26
- - **Reactive Forms native.** Every form control is a `ControlValueAccessor` built and tested
27
- against `[formControl]` / `formControlName`.
28
- - **Your data, your shapes.** Dropdowns take your objects with accessor paths
29
- (`optionLabel="profile.fullName"`), not a mandated `{ id, name }` DTO.
30
- - **Accessible by default.** Keyboard navigation, ARIA wiring and generated label associations
31
- come with the components rather than with extra attributes.
32
- - **Right-to-left included.** `dir="rtl"` on `<html>` or on any wrapper mirrors every component,
33
- portaled overlays included. Nothing to configure per component, no second stylesheet.
34
-
35
- ## Setup
36
-
37
- Install it with whichever package manager you use — or with `ng add`, which installs it and does
38
- step 1 for you:
39
-
40
- ```bash
41
- npm install @guildofgleks/ui
42
- # or
43
- yarn add @guildofgleks/ui
44
- # or — also does step 1 below
45
- ng add @guildofgleks/ui
46
- ```
47
-
48
- Steps 2 and 3 are yours either way: a schematic can't know where in your app you want components
49
- or dialog and toast hosts.
50
-
51
- **1. Add the stylesheet.** It carries the baseline theme and the utility classes the components
52
- use — without it they render unstyled.
53
-
54
- ```jsonc
55
- // angular.json → projects.<app>.architect.build.options
56
- "styles": [
57
- "node_modules/@guildofgleks/ui/styles/index.css",
58
- "src/styles.scss" // yours, after the baseline so it wins
59
- ]
60
- ```
61
-
62
- It brings its own `box-sizing: border-box`, scoped to the elements the library renders, so the
63
- components size correctly whether or not your app has a global reset — since 21.6.0. Your own
64
- reset is untouched either way, and a single class of specificity means your own styles still win.
65
-
66
- **2. Import components where you use them** — each is standalone:
67
-
68
- ```ts
69
- import { ButtonComponent, SelectComponent } from '@guildofgleks/ui';
70
-
71
- @Component({
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
- Outputs are prefixed `gog` (`gogClick`, `gogToggle`) so they never collide with native DOM
82
- events. Inputs keep their natural names.
83
-
84
- > **Don't write `(click)` on `gog-button`.** Its click handler is bound on the `<button>` inside
85
- > its own template, not on the host — a native click still bubbles up through the host element, so
86
- > a `(click)` listener there fires on every press, silently bypassing `debounce`'s throttling.
87
- > `(gogClick)` is the one that only emits once the debounce window has passed; use it instead. This
88
- > is specific to the `gog-button` component — `[gogButton]` on your own `<a>`/`<button>` has no
89
- > debounce to bypass, so your own `(click)` on it works exactly as written.
90
-
91
- **3. If you use dialogs or toasts, place their hosts once.** `DialogService.open()` and
92
- `ToastService.show()` update state but render nothing without them:
93
-
94
- ```ts
95
- @Component({
96
- selector: 'app-root',
97
- imports: [DialogComponent, ToastContainerComponent],
98
- template: `
99
- <router-outlet />
100
- <gog-dialog />
101
- <gog-toast-container />
102
- `,
103
- })
104
- export class App {}
105
- ```
106
-
107
- One `<gog-dialog />` hosts every dialog (they stack); one `<gog-toast-container />` hosts all
108
- four toast corners.
109
-
110
- ## Right-to-left
111
-
112
- **RTL is supported.** Set `dir="rtl"` on `<html>` (or on any subtree) and every component
113
- mirrors: stylesheets use logical properties, portaled panels and tooltip bubbles copy a scoped
114
- `dir` onto themselves, a tooltip's `position="auto"` prefers the mirrored horizontal side, and
115
- the calendar's month arrows turn around.
116
-
117
- Two things stay physical on purpose, because they are physical words in the API: a tooltip's
118
- explicit `position="left"`/`"right"`, and a toast's `top-left`/`top-right`/`bottom-left`/
119
- `bottom-right` corner. `"auto"` is the direction-aware tooltip placement; pick the corner you
120
- want for a toast.
121
-
122
- ## Theming
123
-
124
- Every value the components paint with lives in `styles/theme.css`, in three layers:
125
-
126
- **Foundation** — palette, type scale, spacing, motion, and a small _character_ layer: corner
127
- rounding (`--gog-radius`), border weight (`--gog-control-border-*` for form fields,
128
- `--gog-panel-border-*` for raised surfaces, `--gog-border-*` for everything smaller and inline),
129
- and emphasis casing/tracking (`--gog-text-transform`, `--gog-letter-spacing`). Override these to
130
- restyle everything at once; component tokens derive from them, so a palette or character change
131
- carries through on its own, with nothing to re-list per component.
132
-
133
- **Component** — `--gog-<component>-*`, one block per component, named after the component you
134
- write in markup (`gog-button` → `--gog-button-*`), to restyle a single component app-wide:
135
-
136
- ```css
137
- :root[data-theme='mine'] {
138
- --gog-button-font-family: var(--gog-font-body);
139
- --gog-button-ghost-hover-bg: color-mix(in srgb, var(--gog-accent-color) 20%, transparent);
140
- --gog-table-hover-bg: var(--gog-hover-color);
141
- }
142
- ```
143
-
144
- **Instance** — a small set left deliberately undeclared, so setting one anywhere beats the
145
- variant and size classes:
146
-
147
- ```css
148
- .my-form gog-button {
149
- --gog-button-bg: rebeccapurple; /* wins over .gog-btn--primary */
150
- }
151
- ```
152
-
153
- > **Renamed in 21.5.0, removed in 21.7.0.** Three prefixes were abbreviated and are now spelled
154
- > out: `--gog-btn-*` → `--gog-button-*`, `--gog-confirm-*` → `--gog-confirmation-dialog-*`, and
155
- > `--gog-ms-*` → `--gog-multiselect-*` (that one since 21.3.0). **The old spellings no longer
156
- > resolve.** If you set one of them, rename it — a `var()` reference to a name nothing declares
157
- > doesn't fail your build, it just silently stops matching anything.
158
- >
159
- > One prefix that looks abbreviated and is not: **`--gog-input-*`**. It names the shared
160
- > text-field block that both `gog-inputfield` and `gog-textarea` render (`.gog-input__field`), not
161
- > the `gog-inputfield` component — the two are meant to restyle together from one token set, so
162
- > there is no `--gog-inputfield-*` and there will not be one.
163
- >
164
- > And one prefix that means two things on purpose: **`--gog-panel-*`**. Four of them —
165
- > `--gog-panel-radius`, `--gog-panel-shadow`, `--gog-panel-border-width`,
166
- > `--gog-panel-border-style` — are the _foundation_ surface tier that dialogs, dropdown panels and
167
- > tooltips read, and the `gog-panel` component reads them too rather than owning a fourth copy of
168
- > "what a raised surface looks like here". Change one and every raised surface follows, which is
169
- > the intent; the rest of `--gog-panel-*` belongs to the component alone.
170
-
171
- **A status colour is three tokens, not one.** `--gog-danger-color` and its three siblings are
172
- fills, and a fill needs a label that reads on it and a direction to deepen in so each also has
173
- `--gog-<status>-text-color` (the label; defaults to the accent's, state it only when your hue
174
- disagrees) and `--gog-<status>-shade` (which way hover and press move; defaults to the page's ink,
175
- and should be the opposite when your label *is* the ink). Setting a status colour alone and
176
- leaving those at their defaults is how a bright amber ends up under white text: it measured 1.97:1
177
- in one of this package's own presets before 21.9.0. `gogBadge` and `gog-button`'s `severity` read
178
- the label; the button also reads the shade. `gog-tag` derives its own pair by mixing and
179
- `gog-progressbar` paints no label on its bar, so neither needs them.
180
-
181
- Every group and token name is in **[`TOKENS.md`](./TOKENS.md)**, generated from `theme.css` so it
182
- cannot drift, and available at runtime as `GOG_TOKEN_GROUPS`.
183
-
184
- ### Light, dark and your own
185
-
186
- The active theme is a `data-theme` attribute on `:root`, managed by `ThemeService`:
187
-
188
- ```ts
189
- private readonly theme = inject(ThemeService);
190
- this.theme.toggleTheme(); // light ⇄ dark
191
- this.theme.setTheme('one-dark'); // any preset you imported, or any name you declared in CSS
192
- ```
193
-
194
- Out of the box it adopts whatever `data-theme` is already on the document, or `light`.
195
- Persisting the choice and following the OS setting are opt-in:
196
-
197
- ```ts
198
- provideGogConfig({
199
- theme: { storageKey: 'app-theme', followSystem: true, darkTheme: 'one-dark' },
200
- });
201
- ```
202
-
203
- A theme only declares what it changes the derived layer re-resolves against whatever is in
204
- scope, so a theme restyles every component without listing any of them. **Eleven declarations
205
- are a whole visual identity**, not a recolour:
206
-
207
- ```css
208
- [data-theme='compact'] {
209
- /* Palette: every colour in the library re-derives from these. */
210
- --gog-background-color: #f4f6f8;
211
- --gog-surface-color: #ffffff;
212
- --gog-text-color: #1e293b;
213
- --gog-accent-color: #4f46e5;
214
-
215
- /* Character: every corner, border, label and gap in the library re-derives from these. */
216
- --gog-radius: 2px;
217
- --gog-density: 0.85; /* one number = every padding and gap at once */
218
- --gog-control-border-width: 1px;
219
- --gog-text-transform: none;
220
- --gog-letter-spacing: normal;
221
- }
222
- ```
223
-
224
- The palette half has always worked this way. The **character layer** (`--gog-radius`,
225
- `--gog-density`, the border and casing tokens — all since 21.7.0) is the other half: it is what
226
- lets a theme change the library's _shape_ — square or generous corners, thin borders,
227
- sentence-case labels, tight or roomy spacing still without naming a single component.
228
-
229
- Nine presets ship alongside the built-in `light` and `dark`, each at `styles/presets/<name>.css`
230
- and activated by `data-theme="<name>"`. All nine set palette **and** character:
231
-
232
- | Preset | The identity |
233
- | ----------------------- | ----------------------------------------------------------------------- |
234
- | `slate` | soft modern — 12px corners, hairline borders, roomier than the default |
235
- | `one-dark`, `one-light` | editor chrome — 4px corners, compact, sentence case; one UI, two tones |
236
- | `material`, `primeng` | Material Design 3 and PrimeNG Aura, including their shape and density |
237
- | `ledger` | administrative software — square corners, hard offset shadow, no motion |
238
- | `terminal` | green phosphor — monospaced throughout, square, no motion |
239
- | `bevel` | the early-web desktopraised buttons, sunken fields, grey and navy |
240
- | `parchment` | ink on laid paper old-style serif, oxblood accent, roomy |
241
-
242
- **No preset downloads a font.** Each sets a stack that resolves to a real system face — the
243
- platform's own monospace for `terminal`, Tahoma/Verdana for `bevel`, Iowan Old Style/Palatino for
244
- `parchment` — so importing a preset never adds a network request. Where a webfont makes a visible
245
- difference, it lives in a separate opt-in file you import _after_ the preset:
246
-
247
- ```css
248
- @import '@guildofgleks/ui/styles/presets/parchment.css';
249
- @import '@guildofgleks/ui/styles/presets/parchment.fonts.css'; /* optional: EB Garamond */
250
- ```
251
-
252
- `terminal.fonts.css` (IBM Plex Mono) is the other one. `material` and `primeng` additionally set
253
- a few things the character layer has no vocabulary for (a pill button, a table's header font), and
254
- `bevel` sets one (a button's bevel must disagree with a field's). `AGENTS.md` has the per-preset
255
- detail and the full token list.
256
-
257
- Fonts are left alone on purpose (system stacks, no webfont download). Add
258
- `@guildofgleks/ui/styles/fonts.css` for the showcase's typography.
259
-
260
- ### Making it fluid one `clamp()`, not thirty
261
-
262
- **Component sizing ships no `clamp()`, no `vw` and no breakpoints, and that is a decision rather
263
- than an omission.** A component does not know how wide the screen is; it knows how wide its
264
- container is, and `size` is your input, not something a stylesheet should override at 400px. So
265
- fluid sizing is the app's to declare — and because everything here derives from a few foundation
266
- tokens, it is one declaration rather than one per component.
267
-
268
- **Chrome that floats over the viewport is the deliberate exception, and it is a narrower thing than
269
- fluid sizing.** `--gog-tooltip-max-width`, `--gog-menu-max-width` and `--gog-toast-max-width` each
270
- read `min(<cap>, calc(100vw - <margin> * 2))` an overlay positioned against the screen rather
271
- than a container, where "no wider than the screen" is what the component is for, not a style choice
272
- a consumer makes. It does not grow anything: it only ever narrows a cap that would otherwise
273
- overflow a small screen. It is not the recipe below, and reading one as an example of the other is
274
- the mistake to avoid.
275
-
276
- Viewport units appear in four other places, all older than that rule and all the same shape — a
277
- ceiling rather than a curve: a dialog panel defaults to `90vw` and `--gog-dialog-max-height` to
278
- `90vh`, `gog-menu` falls back to `100vh` when it cannot measure the room below its trigger, and
279
- `gog-table`'s `maxHeight` takes any CSS length you give it, `'60vh'` included. `gog-confirmation-dialog`
280
- has **no** viewport clamp of its own precisely because the panel it renders inside already carries
281
- one.
282
-
283
- Interpolate as a straight line between two viewports. Between `(W_min, V_min)` and
284
- `(W_max, V_max)`:
285
-
286
- ```
287
- slope m = (V_max − V_min) / (W_max − W_min) × 100 → the vw coefficient
288
- intercept b = (W_min·V_max W_max·V_min) / (W_min W_max) the constant
289
- size = clamp(V_min, b + m·vw, V_max)
290
- ```
291
-
292
- **The type scale is in `rem`, so the root font size is the one knob that moves all of it.** For 15px
293
- at a 360px viewport growing to 17px at 1440px `m = 0.185`, `b = 14.33px` — write the constant in
294
- `rem` rather than `px`:
295
-
296
- ```css
297
- html {
298
- /* 15px at 360px wide, 17px at 1440px. 0.8958rem is the 14.33px intercept. */
299
- font-size: clamp(0.9375rem, 0.8958rem + 0.185vw, 1.0625rem);
300
- }
301
- ```
302
-
303
- **Keep the intercept in `rem`, not `px`.** A viewport-only font size ignores the reader's own
304
- browser text-size setting, which fails WCAG 1.4.4; with a `rem` term in the expression, their
305
- preference still scales the result. Every `--gog-text-*` follows, and so does `--gog-icon-size`,
306
- which is `1.2em`.
307
-
308
- Spacing does not follow, deliberately: `--gog-space-*` is authored in `px` times `--gog-density` so
309
- that one number is the whole spacing system, and a unitless multiplier cannot carry a `vw` term
310
- (`calc()` will not add a number to a length). If you want gaps to grow with the type too, restate
311
- the ten steps against the root font size once — the derived layer re-resolves and every component
312
- follows:
313
-
314
- ```css
315
- :root {
316
- --gog-space-4: calc(0.25rem * var(--gog-density));
317
- --gog-space-8: calc(0.5rem * var(--gog-density));
318
- /* …12, 16, 20, 24, 28, 32, 40, 48, each Npx as N/16 rem */
319
- }
320
- ```
321
-
322
- `--gog-density` on its own remains the simpler answer for "roomier" versus "compact", and it needs
323
- no arithmetic at all.
324
-
325
- ## App-wide configuration
326
-
327
- Anything visual is a token. Everything else — the settings you would otherwise repeat on every
328
- instance goes through one provider:
329
-
330
- ```ts
331
- provideGogConfig({
332
- control: { size: 'sm', errorDisplay: 'auto', clearable: true },
333
- dropdown: { appendToBody: true },
334
- datepicker: { locale: 'de-DE', format: 'dd.MM.yyyy' },
335
- ripple: { enabled: true }, // press feedback on every interactive surface at once
336
- labels: { clear: 'Löschen', selectAll: 'Alle auswählen' }, // translate the library once
337
- });
338
- ```
339
-
340
- Keys: `control`, `dropdown`, `floatLabel`, `datepicker`, `autocomplete`, `inputfield`, `textarea`,
341
- `tooltip`, `scroll`, `button`, `ripple`, `spinner`, `paginator`, `toast`, `theme`, `labels`. An
342
- instance's own input always wins, and providing the config again lower in the injector tree layers
343
- onto the parent rather than replacing it.
344
-
345
- `spinner` is the one key that takes a **component** rather than a value:
346
-
347
- ```ts
348
- provideGogConfig({ spinner: { component: HouseLoader } });
349
- ```
350
-
351
- That renders your loader wherever the library draws a spinner — including the two places no input
352
- could reach, since `gog-button` and `gog-autocomplete` render their own and expose nothing for it.
353
- It keeps the library's sizing, overlay behaviour, `role="status"` and accessible name; only the
354
- visual is yours.
355
-
356
- `ripple` is the one visual default that is not a token, and the exception is deliberate:
357
- `--gog-ripple-opacity: 0` would hide the wash but still pay for the DOM node, the listeners and
358
- the animation frames, so a real off has to reach the TypeScript. It is **off by default**, and
359
- every rippling component takes a `ripple` input that beats it in both directions.
360
-
361
- **Under `prefers-reduced-motion: reduce` the ripple does not appear at all** — suppressed
362
- outright rather than shortened, in CSS and in the controller, so no node is created and no
363
- listener attached. If you turned it on and see nothing, check that setting before checking your
364
- config.
365
-
366
- That is the one place motion is removed entirely, and it is deliberate: a ripple is decoration,
367
- so losing it costs a user nothing. Everywhere else the rule is the opposite — **reduced motion
368
- drops the animation, never the information.** A chevron still turns to show a panel is open, a
369
- toggle still moves; only the tween between the two states goes away.
370
-
371
- Icons work the same way 41 Lucide glyphs ship with the package, and your own register by name:
372
-
373
- ```ts
374
- provideGogIcons({ cart: '<svg viewBox="0 0 24 24">…</svg>' });
375
- ```
376
-
377
- ```html
378
- <gog-icon name="cart" /> <gog-tag iconName="cart">In basket</gog-tag>
379
- ```
380
-
381
- ## Overlays and the viewport
382
-
383
- Three things this library renders cover the **viewport** with `position: fixed`:
384
- `<gog-dialog />`'s backdrop, `<gog-toast-container />`, and `<gog-spinner [overlay]="true" />`.
385
-
386
- That is true only while nothing above them establishes a containing block. `contain`,
387
- `transform`, `filter`, `backdrop-filter` and `will-change` on **any** ancestor silently retarget
388
- a fixed element to that ancestor's box a CSS rule with no error and no warning, and the usual
389
- first sighting is "my modal only dims half the page".
390
-
391
- It is not hypothetical here: **`gog-scroll` sets `contain: layout style`**, so a dialog opened
392
- from inside a scroller dims the scroller, and a toast container nested in one corners its toasts
393
- against the scroller. Two rules keep it simple:
394
-
395
- - **Place the dialog and toast outlets in your root component**, not inside the section that
396
- happens to use them. They are singletons anyway — one of each renders everything.
397
- - **A spinner overlay covers whatever contains it**, which is often what you want inside a card.
398
- For a genuinely full-screen one, render it at the root too.
399
-
400
- The dropdown panels (`gog-select`, `gog-multiselect`, `gog-autocomplete`, `gog-datepicker`) and
401
- `gog-menu` avoid the whole question by rendering into `<body>` — `appendToBody` for the
402
- dropdowns, always for the menu.
403
-
404
- ## Components
405
-
406
- | Group | Components |
407
- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
408
- | Form controls | `gog-inputfield`, `gog-textarea`, `gog-select`, `gog-multiselect`, `gog-autocomplete`, `gog-checkbox`, `gog-radio-group`, `gog-toggle`, `gog-slider`, `gog-datepicker`, `gog-calendar`, `gog-button-toggle-group` |
409
- | Actions | `gog-button`, `gog-chip` |
410
- | Data | `gog-table` (+ `gog-column`), `gog-paginator`, `gog-tag` |
411
- | Layout & disclosure | `gog-accordion`, `gog-tabs` (+ `gog-tab`), `gog-collapsible`, `gog-card`, `gog-panel`, `gog-divider`, `gog-scroll` |
412
- | Overlays | `gog-dialog`, `gog-confirmation-dialog`, `gog-toast` (+ `gog-toast-container`), `gog-menu` (+ `gogMenuTrigger` / `gogMenuItem`) |
413
- | Feedback | `gog-spinner`, `gog-spinner-overlay`, `gog-progressbar`, `gog-skeleton` |
414
- | Content | `gog-icon` |
415
-
416
- **Directives:** `gogButton` (a link that looks like a button), `gogTooltip`, `gogBadge`,
417
- `gogRipple` (a press wash on any element), `gogCollapsibleTrigger`, `gogCollapsibleContent`,
418
- `gogCardLink` (a link the whole card activates).
419
- **Services:** `DialogService`, `ToastService`, `ThemeService`.
420
-
421
- Seventeen more directives go on markup you own rather than configuring a component through an
422
- input slots like `gogColumnBody`, `gogInputAddonStart` and `gogDropdownOption`, and the menu's
423
- `gogMenuTrigger` / `gogMenuItem`.
424
-
425
- A few things worth knowing before you reach for a workaround:
426
-
427
- - **`gog-table` works two ways.** By default it owns the data and sorts and pages in memory. With
428
- `[lazy]="true"` it hands both to the server: `value` is the current page, `totalRecords` drives
429
- the paginator, and `gogSortChange` / `gogPageChange` are your refetch signals. Row selection is
430
- `selectionMode` + `[(selection)]`; set `dataKey` or a refetch drops it.
431
- - **`gog-button` cannot be a link** it renders its own `<button>`. Use `[gogButton]` on your own
432
- `<a>` instead; nothing is brokered through inputs, so `routerLink`, `href` and `target` keep
433
- working. That is also why this package needs no `@angular/router`.
434
- - **`gog-inputfield` and `gog-textarea` forward the native attribute space** they wrap —
435
- `readonly`, `maxlength`, `pattern`, `inputMode`, `spellcheck` and the text-field `type` values.
436
- They also generate their own `id`, so labels and error messages are wired up without `inputId`.
437
- - **`gog-collapsible` is headless** no markup of its own. Project any element as the trigger and
438
- any element as the panel.
439
- - **A clickable `gog-card` has no `interactive` input.** Put `gogCardLink` on the `<a>` the card is
440
- about — usually the one in its heading — and the whole surface activates that link, keyboard,
441
- middle-click and `routerLink` included. Same reasoning as `gog-button` above: the element stays
442
- yours. Anything else focusable in the card keeps receiving its own clicks.
443
- - **`gog-panel` shares the `--gog-panel-*` prefix with the foundation surface tier.**
444
- `--gog-panel-radius`, `--gog-panel-shadow` and the border pair are the tokens dialogs and
445
- dropdown panels already read, so a theme's idea of a raised surface reaches the component for
446
- free. Its own family (background, padding, heading, toggle, footer) sits alongside them.
447
- - **`[(ngModel)]` is untested.** The library never imports `FormsModule`; use Reactive Forms.
448
-
449
- ## Documentation
450
-
451
- | | |
452
- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
453
- | **[`AGENTS.md`](./AGENTS.md)** | the full API reference — every input, output, slot, type and default, per component. Ships in this package. |
454
- | **[`TOKENS.md`](./TOKENS.md)** | every `--gog-*` token, generated from `theme.css` |
455
- | [CHANGELOG](https://github.com/GuildOfGleks/gleks_web_ui/blob/master/projects/gleks/ui/CHANGELOG.md) | release history |
456
-
457
- `AGENTS.md` is written for an AI coding assistant working in your project, but it is the most
458
- complete API reference either way point your assistant at it and it will stop guessing.
459
-
460
- ## License
461
-
462
- Apache-2.0 © Roman Malitskyi
463
-
464
- Built-in icons are [Lucide](https://lucide.dev) glyphs, inlined so the package keeps zero runtime
465
- dependencies. Lucide is ISC licensed; portions are held by Cole Bemis 2013–2022 as part of
466
- Feather (MIT), all others by Lucide Contributors 2022 — full notice in
467
- `src/lib/shared/icons.ts`.
1
+ ![NPM Version](https://img.shields.io/npm/v/@guildofgleks/ui?color=red)
2
+ ![Node Version](https://img.shields.io/node/v/@guildofgleks/ui)
3
+ ![Angular 21](https://img.shields.io/badge/Angular-21%2B-dd0031?logo=angular)
4
+ ![NPM Downloads](https://img.shields.io/npm/dm/@guildofgleks/ui)
5
+ ![License](https://img.shields.io/npm/l/@guildofgleks/ui)
6
+
7
+ # @guildofgleks/ui
8
+
9
+ An Angular 21 and 22 component library with **no CDK and no Material**. 29 components, 5
10
+ directives and 3 services, all standalone, all signal-based, themed entirely through CSS custom
11
+ properties.
12
+
13
+ ```bash
14
+ npm install @guildofgleks/ui
15
+ ```
16
+
17
+ ## Why this one
18
+
19
+ - **Small dependency footprint.** Peers are `@angular/core`, `@angular/common`, `@angular/forms`
20
+ and `@angular/platform-browser`. No router, no CDK, no animations package. `tslib` is the only
21
+ runtime dependency.
22
+ - **Signals throughout.** `input()` / `output()` / `model()`, `OnPush` everywhere, no NgModules.
23
+ - **Themeable without a build step.** Every value a component paints with is a `--gog-*` custom
24
+ property. Swap a palette, restyle one component, or override a single instance — no Sass
25
+ variables, no JS theme object.
26
+ - **Reactive Forms native.** Every form control is a `ControlValueAccessor` built and tested
27
+ against `[formControl]` / `formControlName`.
28
+ - **Your data, your shapes.** Dropdowns take your objects with accessor paths
29
+ (`optionLabel="profile.fullName"`), not a mandated `{ id, name }` DTO.
30
+ - **Accessible by default.** Keyboard navigation, ARIA wiring and generated label associations
31
+ come with the components rather than with extra attributes.
32
+ - **Right-to-left included.** `dir="rtl"` on `<html>` or on any wrapper mirrors every component,
33
+ portaled overlays included. Nothing to configure per component, no second stylesheet.
34
+
35
+ ## Setup
36
+
37
+ Install it with whichever package manager you use — or with `ng add`, which installs it and does
38
+ step 1 for you:
39
+
40
+ ```bash
41
+ npm install @guildofgleks/ui
42
+ # or
43
+ yarn add @guildofgleks/ui
44
+ # or — also does step 1 below
45
+ ng add @guildofgleks/ui
46
+ ```
47
+
48
+ Steps 2 and 3 are yours either way: a schematic can't know where in your app you want components
49
+ or dialog and toast hosts.
50
+
51
+ **1. Add the stylesheet.** It carries the baseline theme and the utility classes the components
52
+ use — without it they render unstyled.
53
+
54
+ ```jsonc
55
+ // angular.json → projects.<app>.architect.build.options
56
+ "styles": [
57
+ "node_modules/@guildofgleks/ui/styles/index.css",
58
+ "src/styles.scss" // yours, after the baseline so it wins
59
+ ]
60
+ ```
61
+
62
+ It brings its own `box-sizing: border-box`, scoped to the elements the library renders, so the
63
+ components size correctly whether or not your app has a global reset — since 21.6.0. Your own
64
+ reset is untouched either way, and a single class of specificity means your own styles still win.
65
+
66
+ **2. Import components where you use them** — each is standalone:
67
+
68
+ ```ts
69
+ import { ButtonComponent, SelectComponent } from '@guildofgleks/ui';
70
+
71
+ @Component({
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
+ Outputs are prefixed `gog` (`gogClick`, `gogToggle`) so they never collide with native DOM
82
+ events. Inputs keep their natural names.
83
+
84
+ > **Don't write `(click)` on `gog-button`.** Its click handler is bound on the `<button>` inside
85
+ > its own template, not on the host — a native click still bubbles up through the host element, so
86
+ > a `(click)` listener there fires on every press, silently bypassing `debounce`'s throttling.
87
+ > `(gogClick)` is the one that only emits once the debounce window has passed; use it instead. This
88
+ > is specific to the `gog-button` component — `[gogButton]` on your own `<a>`/`<button>` has no
89
+ > debounce to bypass, so your own `(click)` on it works exactly as written.
90
+
91
+ **3. If you use dialogs or toasts, place their hosts once.** `DialogService.open()` and
92
+ `ToastService.show()` update state but render nothing without them:
93
+
94
+ ```ts
95
+ @Component({
96
+ selector: 'app-root',
97
+ imports: [DialogComponent, ToastContainerComponent],
98
+ template: `
99
+ <router-outlet />
100
+ <gog-dialog />
101
+ <gog-toast-container />
102
+ `,
103
+ })
104
+ export class App {}
105
+ ```
106
+
107
+ One `<gog-dialog />` hosts every dialog (they stack); one `<gog-toast-container />` hosts all
108
+ four toast corners.
109
+
110
+ ## Right-to-left
111
+
112
+ **RTL is supported.** Set `dir="rtl"` on `<html>` (or on any subtree) and every component
113
+ mirrors: stylesheets use logical properties, portaled panels and tooltip bubbles copy a scoped
114
+ `dir` onto themselves, a tooltip's `position="auto"` prefers the mirrored horizontal side, and
115
+ the calendar's month arrows turn around.
116
+
117
+ Two things stay physical on purpose, because they are physical words in the API: a tooltip's
118
+ explicit `position="left"`/`"right"`, and a toast's `top-left`/`top-right`/`bottom-left`/
119
+ `bottom-right` corner. `"auto"` is the direction-aware tooltip placement; pick the corner you
120
+ want for a toast.
121
+
122
+ ## Theming
123
+
124
+ Every value the components paint with lives in `styles/theme.css`, in three layers:
125
+
126
+ **Foundation** — palette, type scale, spacing, motion, and a small _character_ layer: corner
127
+ rounding (`--gog-radius`), border weight (`--gog-control-border-*` for form fields,
128
+ `--gog-panel-border-*` for raised surfaces, `--gog-border-*` for everything smaller and inline),
129
+ and emphasis casing/tracking (`--gog-text-transform`, `--gog-letter-spacing`). Override these to
130
+ restyle everything at once; component tokens derive from them, so a palette or character change
131
+ carries through on its own, with nothing to re-list per component.
132
+
133
+ **Component** — `--gog-<component>-*`, one block per component, named after the component you
134
+ write in markup (`gog-button` → `--gog-button-*`), to restyle a single component app-wide:
135
+
136
+ ```css
137
+ :root[data-theme='mine'] {
138
+ --gog-button-font-family: var(--gog-font-body);
139
+ --gog-button-ghost-hover-bg: color-mix(in srgb, var(--gog-accent-color) 20%, transparent);
140
+ --gog-table-hover-bg: var(--gog-hover-color);
141
+ }
142
+ ```
143
+
144
+ **Instance** — a small set left deliberately undeclared, so setting one anywhere beats the
145
+ variant and size classes:
146
+
147
+ ```css
148
+ .my-form gog-button {
149
+ --gog-button-bg: rebeccapurple; /* wins over .gog-btn--primary */
150
+ }
151
+ ```
152
+
153
+ > **Renamed in 21.5.0, removed in 21.7.0.** Three prefixes were abbreviated and are now spelled
154
+ > out: `--gog-btn-*` → `--gog-button-*`, `--gog-confirm-*` → `--gog-confirmation-dialog-*`, and
155
+ > `--gog-ms-*` → `--gog-multiselect-*` (that one since 21.3.0). **The old spellings no longer
156
+ > resolve.** If you set one of them, rename it — a `var()` reference to a name nothing declares
157
+ > doesn't fail your build, it just silently stops matching anything.
158
+ >
159
+ > One prefix that looks abbreviated and is not: **`--gog-input-*`**. It names the shared
160
+ > text-field block that both `gog-inputfield` and `gog-textarea` render (`.gog-input__field`), not
161
+ > the `gog-inputfield` component — the two are meant to restyle together from one token set, so
162
+ > there is no `--gog-inputfield-*` and there will not be one.
163
+ >
164
+ > And one prefix that means two things on purpose: **`--gog-panel-*`**. Four of them —
165
+ > `--gog-panel-radius`, `--gog-panel-shadow`, `--gog-panel-border-width`,
166
+ > `--gog-panel-border-style` — are the _foundation_ surface tier that dialogs, dropdown panels and
167
+ > tooltips read, and the `gog-panel` component reads them too rather than owning a fourth copy of
168
+ > "what a raised surface looks like here". Change one and every raised surface follows, which is
169
+ > the intent; the rest of `--gog-panel-*` belongs to the component alone.
170
+
171
+ **Shadows come off a ladder, not out of a stylesheet.** Since 21.12.0 every raised surface reads
172
+ one of six heights `--gog-elevation-0` through `-5`, with Z doubling: 0, 1, 2, 4, 8, 16. Step 3
173
+ is anything anchored to a control (a dropdown panel, a tooltip, a menu), step 4 a toast, step 5 a
174
+ modal dialog. The steps are generated, so you never write one; a theme turns ten knobs and all six
175
+ follow:
176
+
177
+ ```css
178
+ :root[data-theme='mine'] {
179
+ --gog-elevation-ink: 15 23 42; /* the shadow's colour, unpacked for rgb(… / α) */
180
+ --gog-elevation-key-alpha: 0.12; /* the light that moves with height */
181
+ --gog-elevation-ambient-alpha: 0.06; /* the contact shadow, which does not */
182
+ --gog-elevation-contact-blur: 3px;
183
+ --gog-elevation-key-x: 0; /* per unit of Z — the three that carry the style */
184
+ --gog-elevation-key-y: 1;
185
+ --gog-elevation-key-blur: 3;
186
+ --gog-elevation-ring-width: 1px; /* a hairline contour; 0px for none */
187
+ --gog-elevation-highlight-ink: 255 255 255; /* a top-edge catch light, for dark grounds */
188
+ --gog-elevation-highlight-alpha: 0;
189
+ }
190
+ ```
191
+
192
+ The three per-Z multipliers are the whole style axis. Leave them alone for a soft drop shadow; set
193
+ `key-x` and `key-y` to a fraction and `key-blur` to `0` for a hard offset (that is what `bevel` and
194
+ `ledger` do); set `key-y` to `0` and keep the blur and the key light becomes a glow (`terminal`).
195
+ **Declare all ten or none** a custom property inherits, so a theme that states six of them picks
196
+ the other four up from whatever encloses it, which is how a light subtree inside a dark page ends
197
+ up with dark-weight shadows. `--gog-panel-shadow`, `--gog-dialog-shadow` and the rest are still the
198
+ names you override for a single surface; what changed is that their default is a step.
199
+
200
+ **A control's edge and a divider are different tokens.** `--gog-border-color` is decoration —
201
+ dividers, table rules, panel outlines — and every theme keeps it faint on purpose.
202
+ `--gog-control-boundary-color` is the edge that says _this is a control_, and WCAG SC 1.4.11 wants
203
+ it at 3:1 against whatever it sits on. If you build a theme, set both: a palette that gives them
204
+ one value either shouts its dividers or hides its controls. `npm run suggest:color -- <ink>
205
+ <ground> 3` will tell you the nearest passing value for any colour you would rather keep.
206
+
207
+ **A status colour is three tokens, not one.** `--gog-danger-color` and its three siblings are
208
+ fills, and a fill needs a label that reads on it and a direction to deepen in — so each also has
209
+ `--gog-<status>-text-color` (the label; defaults to the accent's, state it only when your hue
210
+ disagrees) and `--gog-<status>-shade` (which way hover and press move; defaults to the page's ink,
211
+ and should be the opposite when your label _is_ the ink). Setting a status colour alone and
212
+ leaving those at their defaults is how a bright amber ends up under white text: it measured 1.97:1
213
+ in one of this package's own presets before 21.9.0. `gogBadge` and `gog-button`'s `severity` read
214
+ the label; the button also reads the shade. `gog-tag` derives its own pair by mixing and
215
+ `gog-progressbar` paints no label on its bar, so neither needs them.
216
+
217
+ Every group and token name is in **[`TOKENS.md`](./TOKENS.md)**, generated from `theme.css` so it
218
+ cannot drift, and available at runtime as `GOG_TOKEN_GROUPS`.
219
+
220
+ ### Light, dark and your own
221
+
222
+ The active theme is a `data-theme` attribute on `:root`, managed by `ThemeService`:
223
+
224
+ ```ts
225
+ private readonly theme = inject(ThemeService);
226
+ this.theme.toggleTheme(); // light dark
227
+ this.theme.setTheme('one-dark'); // any preset you imported, or any name you declared in CSS
228
+ ```
229
+
230
+ Out of the box it adopts whatever `data-theme` is already on the document, or `light`.
231
+ Persisting the choice and following the OS setting are opt-in:
232
+
233
+ ```ts
234
+ provideGogConfig({
235
+ theme: { storageKey: 'app-theme', followSystem: true, darkTheme: 'one-dark' },
236
+ });
237
+ ```
238
+
239
+ A theme only declares what it changes the derived layer re-resolves against whatever is in
240
+ scope, so a theme restyles every component without listing any of them. **Eleven declarations
241
+ are a whole visual identity**, not a recolour:
242
+
243
+ ```css
244
+ [data-theme='compact'] {
245
+ /* Palette: every colour in the library re-derives from these. */
246
+ --gog-background-color: #f4f6f8;
247
+ --gog-surface-color: #ffffff;
248
+ --gog-text-color: #1e293b;
249
+ --gog-accent-color: #4f46e5;
250
+
251
+ /* Character: every corner, border, label and gap in the library re-derives from these. */
252
+ --gog-radius: 2px;
253
+ --gog-density: 0.85; /* one number = every padding and gap at once */
254
+ --gog-control-border-width: 1px;
255
+ --gog-text-transform: none;
256
+ --gog-letter-spacing: normal;
257
+ }
258
+ ```
259
+
260
+ The palette half has always worked this way. The **character layer** (`--gog-radius`,
261
+ `--gog-density`, the border and casing tokens — all since 21.7.0) is the other half: it is what
262
+ lets a theme change the library's _shape_ square or generous corners, thin borders,
263
+ sentence-case labels, tight or roomy spacing still without naming a single component.
264
+
265
+ Nine presets ship alongside the built-in `light` and `dark`, each at `styles/presets/<name>.css`
266
+ and activated by `data-theme="<name>"`. All nine set palette **and** character:
267
+
268
+ | Preset | The identity |
269
+ | ----------------------- | ----------------------------------------------------------------------- |
270
+ | `slate` | soft modern 12px corners, hairline borders, roomier than the default |
271
+ | `one-dark`, `one-light` | editor chrome 4px corners, compact, sentence case; one UI, two tones |
272
+ | `material`, `primeng` | Material Design 3 and PrimeNG Aura, including their shape and density |
273
+ | `ledger` | administrative software square corners, hard offset shadow, no motion |
274
+ | `terminal` | green phosphor — monospaced throughout, square, no motion |
275
+ | `bevel` | the early-web desktop — raised buttons, sunken fields, grey and navy |
276
+ | `parchment` | ink on laid paper old-style serif, oxblood accent, roomy |
277
+
278
+ **No preset downloads a font.** Each sets a stack that resolves to a real system face — the
279
+ platform's own monospace for `terminal`, Tahoma/Verdana for `bevel`, Iowan Old Style/Palatino for
280
+ `parchment` so importing a preset never adds a network request. Where a webfont makes a visible
281
+ difference, it lives in a separate opt-in file you import _after_ the preset:
282
+
283
+ ```css
284
+ @import '@guildofgleks/ui/styles/presets/parchment.css';
285
+ @import '@guildofgleks/ui/styles/presets/parchment.fonts.css'; /* optional: EB Garamond */
286
+ ```
287
+
288
+ `terminal.fonts.css` (IBM Plex Mono) is the other one. `material` and `primeng` additionally set
289
+ a few things the character layer has no vocabulary for (a pill button, a table's header font), and
290
+ `bevel` sets one (a button's bevel must disagree with a field's). `AGENTS.md` has the per-preset
291
+ detail and the full token list.
292
+
293
+ Fonts are left alone on purpose (system stacks, no webfont download). Add
294
+ `@guildofgleks/ui/styles/fonts.css` for the showcase's typography.
295
+
296
+ ### Making it fluid — one `clamp()`, not thirty
297
+
298
+ **Component sizing ships no `clamp()`, no `vw` and no breakpoints, and that is a decision rather
299
+ than an omission.** A component does not know how wide the screen is; it knows how wide its
300
+ container is, and `size` is your input, not something a stylesheet should override at 400px. So
301
+ fluid sizing is the app's to declare — and because everything here derives from a few foundation
302
+ tokens, it is one declaration rather than one per component.
303
+
304
+ **Chrome that floats over the viewport is the deliberate exception, and it is a narrower thing than
305
+ fluid sizing.** `--gog-tooltip-max-width`, `--gog-menu-max-width` and `--gog-toast-max-width` each
306
+ read `min(<cap>, calc(100vw - <margin> * 2))` — an overlay positioned against the screen rather
307
+ than a container, where "no wider than the screen" is what the component is for, not a style choice
308
+ a consumer makes. It does not grow anything: it only ever narrows a cap that would otherwise
309
+ overflow a small screen. It is not the recipe below, and reading one as an example of the other is
310
+ the mistake to avoid.
311
+
312
+ Viewport units appear in four other places, all older than that rule and all the same shape — a
313
+ ceiling rather than a curve: a dialog panel defaults to `90vw` and `--gog-dialog-max-height` to
314
+ `90vh`, `gog-menu` falls back to `100vh` when it cannot measure the room below its trigger, and
315
+ `gog-table`'s `maxHeight` takes any CSS length you give it, `'60vh'` included. `gog-confirmation-dialog`
316
+ has **no** viewport clamp of its own precisely because the panel it renders inside already carries
317
+ one.
318
+
319
+ Interpolate as a straight line between two viewports. Between `(W_min, V_min)` and
320
+ `(W_max, V_max)`:
321
+
322
+ ```
323
+ slope m = (V_max − V_min) / (W_max − W_min) × 100 → the vw coefficient
324
+ intercept b = (W_min·V_max − W_max·V_min) / (W_min − W_max) → the constant
325
+ size = clamp(V_min, b + m·vw, V_max)
326
+ ```
327
+
328
+ **The type scale is in `rem`, so the root font size is the one knob that moves all of it.** For 15px
329
+ at a 360px viewport growing to 17px at 1440px — `m = 0.185`, `b = 14.33px` — write the constant in
330
+ `rem` rather than `px`:
331
+
332
+ ```css
333
+ html {
334
+ /* 15px at 360px wide, 17px at 1440px. 0.8958rem is the 14.33px intercept. */
335
+ font-size: clamp(0.9375rem, 0.8958rem + 0.185vw, 1.0625rem);
336
+ }
337
+ ```
338
+
339
+ **Keep the intercept in `rem`, not `px`.** A viewport-only font size ignores the reader's own
340
+ browser text-size setting, which fails WCAG 1.4.4; with a `rem` term in the expression, their
341
+ preference still scales the result. Every `--gog-text-*` follows, and so does `--gog-icon-size`,
342
+ which is `1.2em`.
343
+
344
+ Spacing does not follow, deliberately: `--gog-space-*` is authored in `px` times `--gog-density` so
345
+ that one number is the whole spacing system, and a unitless multiplier cannot carry a `vw` term
346
+ (`calc()` will not add a number to a length). If you want gaps to grow with the type too, restate
347
+ the ten steps against the root font size once — the derived layer re-resolves and every component
348
+ follows:
349
+
350
+ ```css
351
+ :root {
352
+ --gog-space-4: calc(0.25rem * var(--gog-density));
353
+ --gog-space-8: calc(0.5rem * var(--gog-density));
354
+ /* …12, 16, 20, 24, 28, 32, 40, 48, each Npx as N/16 rem */
355
+ }
356
+ ```
357
+
358
+ `--gog-density` on its own remains the simpler answer for "roomier" versus "compact", and it needs
359
+ no arithmetic at all.
360
+
361
+ ## App-wide configuration
362
+
363
+ Anything visual is a token. Everything else the settings you would otherwise repeat on every
364
+ instance — goes through one provider:
365
+
366
+ ```ts
367
+ provideGogConfig({
368
+ control: { size: 'sm', errorDisplay: 'auto', clearable: true },
369
+ dropdown: { appendToBody: true },
370
+ datepicker: { locale: 'de-DE', format: 'dd.MM.yyyy' },
371
+ ripple: { enabled: true }, // press feedback on every interactive surface at once
372
+ labels: { clear: 'Löschen', selectAll: 'Alle auswählen' }, // translate the library once
373
+ });
374
+ ```
375
+
376
+ Keys: `control`, `dropdown`, `floatLabel`, `datepicker`, `autocomplete`, `inputfield`, `textarea`,
377
+ `tooltip`, `scroll`, `button`, `ripple`, `spinner`, `paginator`, `toast`, `theme`, `labels`. An
378
+ instance's own input always wins, and providing the config again lower in the injector tree layers
379
+ onto the parent rather than replacing it.
380
+
381
+ `spinner` is the one key that takes a **component** rather than a value:
382
+
383
+ ```ts
384
+ provideGogConfig({ spinner: { component: HouseLoader } });
385
+ ```
386
+
387
+ That renders your loader wherever the library draws a spinner — including the two places no input
388
+ could reach, since `gog-button` and `gog-autocomplete` render their own and expose nothing for it.
389
+ It keeps the library's sizing, overlay behaviour, `role="status"` and accessible name; only the
390
+ visual is yours.
391
+
392
+ `ripple` is the one visual default that is not a token, and the exception is deliberate:
393
+ `--gog-ripple-opacity: 0` would hide the wash but still pay for the DOM node, the listeners and
394
+ the animation frames, so a real off has to reach the TypeScript. It is **off by default**, and
395
+ every rippling component takes a `ripple` input that beats it in both directions.
396
+
397
+ **Under `prefers-reduced-motion: reduce` the ripple does not appear at all** suppressed
398
+ outright rather than shortened, in CSS and in the controller, so no node is created and no
399
+ listener attached. If you turned it on and see nothing, check that setting before checking your
400
+ config.
401
+
402
+ That is the one place motion is removed entirely, and it is deliberate: a ripple is decoration,
403
+ so losing it costs a user nothing. Everywhere else the rule is the opposite — **reduced motion
404
+ drops the animation, never the information.** A chevron still turns to show a panel is open, a
405
+ toggle still moves; only the tween between the two states goes away.
406
+
407
+ Icons work the same way — 41 Lucide glyphs ship with the package, and your own register by name:
408
+
409
+ ```ts
410
+ provideGogIcons({ cart: '<svg viewBox="0 0 24 24">…</svg>' });
411
+ ```
412
+
413
+ ```html
414
+ <gog-icon name="cart" /> <gog-tag iconName="cart">In basket</gog-tag>
415
+ ```
416
+
417
+ ## Overlays and the viewport
418
+
419
+ Three things this library renders cover the **viewport** with `position: fixed`:
420
+ `<gog-dialog />`'s backdrop, `<gog-toast-container />`, and `<gog-spinner [overlay]="true" />`.
421
+
422
+ That is true only while nothing above them establishes a containing block. `contain`,
423
+ `transform`, `filter`, `backdrop-filter` and `will-change` on **any** ancestor silently retarget
424
+ a fixed element to that ancestor's box — a CSS rule with no error and no warning, and the usual
425
+ first sighting is "my modal only dims half the page".
426
+
427
+ It is not hypothetical here: **`gog-scroll` sets `contain: layout style`**, so a dialog opened
428
+ from inside a scroller dims the scroller, and a toast container nested in one corners its toasts
429
+ against the scroller. Two rules keep it simple:
430
+
431
+ - **Place the dialog and toast outlets in your root component**, not inside the section that
432
+ happens to use them. They are singletons anyway one of each renders everything.
433
+ - **A spinner overlay covers whatever contains it**, which is often what you want inside a card.
434
+ For a genuinely full-screen one, render it at the root too.
435
+
436
+ The dropdown panels (`gog-select`, `gog-multiselect`, `gog-autocomplete`, `gog-datepicker`) and
437
+ `gog-menu` avoid the whole question by rendering into `<body>` `appendToBody` for the
438
+ dropdowns, always for the menu.
439
+
440
+ ## Components
441
+
442
+ | Group | Components |
443
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
444
+ | Form controls | `gog-inputfield`, `gog-textarea`, `gog-select`, `gog-multiselect`, `gog-autocomplete`, `gog-checkbox`, `gog-radio-group`, `gog-toggle`, `gog-slider`, `gog-datepicker`, `gog-calendar`, `gog-button-toggle-group` |
445
+ | Actions | `gog-button`, `gog-chip` |
446
+ | Data | `gog-table` (+ `gog-column`), `gog-paginator`, `gog-tag` |
447
+ | Layout & disclosure | `gog-accordion`, `gog-tabs` (+ `gog-tab`), `gog-collapsible`, `gog-card`, `gog-panel`, `gog-divider`, `gog-scroll` |
448
+ | Overlays | `gog-dialog`, `gog-confirmation-dialog`, `gog-toast` (+ `gog-toast-container`), `gog-menu` (+ `gogMenuTrigger` / `gogMenuItem`) |
449
+ | Feedback | `gog-spinner`, `gog-spinner-overlay`, `gog-progressbar`, `gog-skeleton` |
450
+ | Content | `gog-icon` |
451
+
452
+ **Directives:** `gogButton` (a link that looks like a button), `gogTooltip`, `gogBadge`,
453
+ `gogRipple` (a press wash on any element), `gogCollapsibleTrigger`, `gogCollapsibleContent`,
454
+ `gogCardLink` (a link the whole card activates).
455
+ **Services:** `DialogService`, `ToastService`, `ThemeService`.
456
+
457
+ Seventeen more directives go on markup you own rather than configuring a component through an
458
+ inputslots like `gogColumnBody`, `gogInputAddonStart` and `gogDropdownOption`, and the menu's
459
+ `gogMenuTrigger` / `gogMenuItem`.
460
+
461
+ A few things worth knowing before you reach for a workaround:
462
+
463
+ - **`gog-table` works two ways.** By default it owns the data and sorts and pages in memory. With
464
+ `[lazy]="true"` it hands both to the server: `value` is the current page, `totalRecords` drives
465
+ the paginator, and `gogSortChange` / `gogPageChange` are your refetch signals. Row selection is
466
+ `selectionMode` + `[(selection)]`; set `dataKey` or a refetch drops it.
467
+ - **`gog-button` cannot be a link** — it renders its own `<button>`. Use `[gogButton]` on your own
468
+ `<a>` instead; nothing is brokered through inputs, so `routerLink`, `href` and `target` keep
469
+ working. That is also why this package needs no `@angular/router`.
470
+ - **`gog-inputfield` and `gog-textarea` forward the native attribute space** they wrap —
471
+ `readonly`, `maxlength`, `pattern`, `inputMode`, `spellcheck` and the text-field `type` values.
472
+ They also generate their own `id`, so labels and error messages are wired up without `inputId`.
473
+ - **`gog-collapsible` is headless** — no markup of its own. Project any element as the trigger and
474
+ any element as the panel.
475
+ - **A clickable `gog-card` has no `interactive` input.** Put `gogCardLink` on the `<a>` the card is
476
+ about — usually the one in its heading — and the whole surface activates that link, keyboard,
477
+ middle-click and `routerLink` included. Same reasoning as `gog-button` above: the element stays
478
+ yours. Anything else focusable in the card keeps receiving its own clicks.
479
+ - **`gog-panel` shares the `--gog-panel-*` prefix with the foundation surface tier.**
480
+ `--gog-panel-radius`, `--gog-panel-shadow` and the border pair are the tokens dialogs and
481
+ dropdown panels already read, so a theme's idea of a raised surface reaches the component for
482
+ free. Its own family (background, padding, heading, toggle, footer) sits alongside them.
483
+ - **`[(ngModel)]` is untested.** The library never imports `FormsModule`; use Reactive Forms.
484
+
485
+ ## Documentation
486
+
487
+ | | |
488
+ | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
489
+ | **[`AGENTS.md`](./AGENTS.md)** | the full API reference — every input, output, slot, type and default, per component. Ships in this package. |
490
+ | **[`TOKENS.md`](./TOKENS.md)** | every `--gog-*` token, generated from `theme.css` |
491
+ | [CHANGELOG](https://github.com/GuildOfGleks/gleks_web_ui/blob/master/projects/gleks/ui/CHANGELOG.md) | release history |
492
+
493
+ `AGENTS.md` is written for an AI coding assistant working in your project, but it is the most
494
+ complete API reference either way — point your assistant at it and it will stop guessing.
495
+
496
+ ## License
497
+
498
+ Apache-2.0 © Roman Malitskyi
499
+
500
+ Built-in icons are [Lucide](https://lucide.dev) glyphs, inlined so the package keeps zero runtime
501
+ dependencies. Lucide is ISC licensed; portions are held by Cole Bemis 2013–2022 as part of
502
+ Feather (MIT), all others by Lucide Contributors 2022 — full notice in
503
+ `src/lib/shared/icons.ts`.