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