@guildofgleks/ui 21.4.0 → 21.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,184 +4,126 @@
4
4
  ![NPM Downloads](https://img.shields.io/npm/dm/@guildofgleks/ui)
5
5
  ![License](https://img.shields.io/npm/l/@guildofgleks/ui)
6
6
 
7
- A lightweight Angular 21 component library. No CDK, no Material — the only runtime
8
- dependencies are `@angular/core`, `@angular/common` and `@angular/forms`.
7
+ # @guildofgleks/ui
9
8
 
10
- **Tags:** angular, angular21, components, ui-library, design-system, signals, standalone, accessible
11
-
12
- ## Features
13
-
14
- - 27 standalone components and 3 directives — see [Components](#components) for the full
15
- list — plus `DialogService`, `ToastService` and `ThemeService`.
16
- - Signal-based API throughout: `input()` / `output()` / `model()`, `OnPush` change
17
- detection, no NgModules.
18
- - No CDK, no Material — a small dependency footprint on top of `@angular/core`,
19
- `@angular/common` and `@angular/forms`.
20
- - Full theming through CSS custom properties: restyle any component, swap the whole
21
- palette, or ship light/dark and custom themes at runtime via a `data-theme` attribute.
22
- - `ControlValueAccessor` on every form control (checkbox, inputfield, select,
23
- multiselect, slider, textarea) — built and tested against Reactive Forms (`formControl` /
24
- `formControlName`). The library does not use `ngModel`/`FormsModule` anywhere itself,
25
- and template-driven usage via `[(ngModel)]` is untested — CVA makes it likely to work,
26
- but it isn't a supported or verified path.
27
- - Accessible by default: keyboard navigation, ARIA attributes, WCAG AA contrast.
28
-
29
- ## Install
30
-
31
- npm:
9
+ An Angular 21 component library with **no CDK and no Material**. 27 components, 5 directives and
10
+ 3 services, all standalone, all signal-based, themed entirely through CSS custom properties.
32
11
 
33
12
  ```bash
34
13
  npm install @guildofgleks/ui
35
14
  ```
36
15
 
37
- yarn:
16
+ ## Why this one
38
17
 
39
- ```bash
40
- yarn add @guildofgleks/ui
41
- ```
18
+ - **Small dependency footprint.** Peers are `@angular/core`, `@angular/common`, `@angular/forms`
19
+ and `@angular/platform-browser`. No router, no CDK, no animations package. `tslib` is the only
20
+ runtime dependency.
21
+ - **Signals throughout.** `input()` / `output()` / `model()`, `OnPush` everywhere, no NgModules.
22
+ - **Themeable without a build step.** Every value a component paints with is a `--gog-*` custom
23
+ property. Swap a palette, restyle one component, or override a single instance — no Sass
24
+ variables, no JS theme object.
25
+ - **Reactive Forms native.** Every form control is a `ControlValueAccessor` built and tested
26
+ against `[formControl]` / `formControlName`.
27
+ - **Your data, your shapes.** Dropdowns take your objects with accessor paths
28
+ (`optionLabel="profile.fullName"`), not a mandated `{ id, name }` DTO.
29
+ - **Accessible by default.** Keyboard navigation, ARIA wiring and generated label associations
30
+ come with the components rather than with extra attributes.
42
31
 
43
32
  ## Setup
44
33
 
45
- Add the stylesheet once. It carries the baseline theme (every token the components
46
- read) plus the utility classes the component templates use, so without it components
47
- render unstyled.
34
+ **1. Add the stylesheet.** It carries the baseline theme and the utility classes the components
35
+ use without it they render unstyled.
48
36
 
49
37
  ```jsonc
50
38
  // angular.json → projects.<app>.architect.build.options
51
39
  "styles": [
52
40
  "node_modules/@guildofgleks/ui/styles/index.css",
53
- "src/styles.scss" // your own styles, after the baseline so they win
41
+ "src/styles.scss" // yours, after the baseline so it wins
54
42
  ]
55
43
  ```
56
44
 
57
- > Up to 21.3.1 the same files shipped under `@guildofgleks/ui/src/styles/…`. That path still
58
- > works and will keep working until 21.5.0 new setups should use the shorter one above.
45
+ > Up to 21.3.1 these files shipped under `@guildofgleks/ui/src/styles/…`. That path keeps working
46
+ > until 21.5.0; new setups should use the shorter one.
59
47
 
60
- Then import a component where you need it:
48
+ **2. Import components where you use them** — each is standalone:
61
49
 
62
50
  ```ts
63
- import { Component } from '@angular/core';
64
51
  import { ButtonComponent, SelectComponent } from '@guildofgleks/ui';
65
52
 
66
53
  @Component({
67
- selector: 'app-example',
68
54
  imports: [ButtonComponent, SelectComponent],
69
55
  template: `
70
56
  <gog-select label="Region" [options]="regions" [(value)]="region" />
71
57
  <gog-button (gogClick)="save()">Save</gog-button>
72
58
  `,
73
59
  })
74
- export class ExampleComponent {
75
- /* … */
76
- }
60
+ export class ExampleComponent {}
77
61
  ```
78
62
 
79
- Outputs are prefixed with `gog` (`gogClick`, `gogToggle`) so they never collide with
80
- native DOM events. Inputs keep their natural names.
81
-
82
- ### Services need a host element
63
+ Outputs are prefixed `gog` (`gogClick`, `gogToggle`) so they never collide with native DOM
64
+ events. Inputs keep their natural names.
83
65
 
84
- `DialogService` and `ToastService` render into a host component you place once, rather than
85
- creating a detached portal of their own. **Without the host in a template, `open()` and
86
- `show()` update state and nothing appears on screen.** Put both in your root template — they
87
- render nothing until something is actually open:
66
+ **3. If you use dialogs or toasts, place their hosts once.** `DialogService.open()` and
67
+ `ToastService.show()` update state but render nothing without them:
88
68
 
89
69
  ```ts
90
- import { Component, inject } from '@angular/core';
91
- import { DialogComponent, ToastContainerComponent, ToastService } from '@guildofgleks/ui';
92
-
93
70
  @Component({
94
71
  selector: 'app-root',
95
72
  imports: [DialogComponent, ToastContainerComponent],
96
73
  template: `
97
74
  <router-outlet />
98
-
99
75
  <gog-dialog />
100
76
  <gog-toast-container />
101
77
  `,
102
78
  })
103
- export class App {
104
- private readonly toasts = inject(ToastService);
105
- notify = () => this.toasts.show({ message: 'Saved', type: 'success' });
106
- }
79
+ export class App {}
107
80
  ```
108
81
 
109
- `<gog-dialog />` hosts every dialog opened through `DialogService` (they stack, so one host is
110
- enough for the whole app). `<gog-toast-container />` hosts all four toast positions at once and
111
- takes an optional `maxVisiblePerPosition` (default `5`).
82
+ One `<gog-dialog />` hosts every dialog (they stack); one `<gog-toast-container />` hosts all
83
+ four toast corners.
112
84
 
113
85
  ## Theming
114
86
 
115
- Everything is themed through CSS custom properties, and **every value the components
116
- paint with lives in `styles/theme.css`** — no component stylesheet holds a colour, font,
117
- border, radius, shadow, spacing or duration of its own. There are three layers:
118
-
119
- **1. Foundation tokens** — the palette, type scale, spacing, motion and control metrics.
120
- Override these to restyle everything at once; the component tokens all derive from them,
121
- so a palette swap carries through without touching anything else.
87
+ Every value the components paint with lives in `styles/theme.css`, in three layers:
122
88
 
123
- Every group and every token name is listed in [`TOKENS.md`](./TOKENS.md) generated from
124
- `theme.css`, so it cannot drift from what the components actually read. The same catalogue is
125
- available at runtime as `GOG_TOKEN_GROUPS`, typed by `GogTokenName`.
89
+ **Foundation** palette, type scale, spacing, motion. Override these to restyle everything at
90
+ once; component tokens derive from them, so a palette swap carries through on its own.
126
91
 
127
- **2. Component tokens** — `--gog-<block>-*`, one block per component in `theme.css`, for
128
- restyling a single component theme-wide. They cover every painted property, including
129
- per-variant and per-size values (`--gog-btn-ghost-hover-bg`, `--gog-tag-lg-padding-block`,
130
- `--gog-accordion-sm-chevron-size`, `--gog-skeleton-shine`, `--gog-paginator-gap`, …), plus
131
- each component's font family, so a theme can decide that e.g. buttons use the body face:
92
+ **Component** — `--gog-<block>-*`, one block per component, to restyle a single component
93
+ app-wide:
132
94
 
133
95
  ```css
134
96
  :root[data-theme='mine'] {
135
97
  --gog-btn-font-family: var(--gog-font-body);
136
- --gog-btn-font-weight: 600;
137
98
  --gog-btn-ghost-hover-bg: color-mix(in srgb, var(--gog-accent-color) 20%, transparent);
138
99
  --gog-table-hover-bg: var(--gog-hover-color);
139
100
  }
140
101
  ```
141
102
 
142
- **3. Instance tokens** — a small set left deliberately _undeclared_ so that setting them
143
- anywhere always wins over the variant/size classes. This is the per-instance escape
144
- hatch:
103
+ **Instance** — a small set left deliberately undeclared, so setting one anywhere beats the
104
+ variant and size classes:
145
105
 
146
106
  ```css
147
107
  .my-form gog-button {
148
- --gog-btn-bg: rebeccapurple; /* beats .gog-btn--primary, unlike a declared token */
108
+ --gog-btn-bg: rebeccapurple; /* wins over .gog-btn--primary */
149
109
  }
150
110
  ```
151
111
 
152
- They are `--gog-btn-{bg,color,border,shadow,hover-bg,hover-color,hover-shadow,padding,font-size,spinner-color}`,
153
- `--gog-tag-{accent,bg,border,color,font-size,gap,padding-block,padding-inline,icon-size}`,
154
- `--gog-chip-{font-size,gap,padding-block,padding-inline,avatar-size,icon-size,remove-size}`,
155
- the per-size field hooks (`--gog-input-padding-y`, `--gog-select-control-font`,
156
- `--gog-ms-font-size`, `--gog-table-td-padding-v`, `--gog-accordion-padding-y`, …),
157
- `--gog-spinner-color`, and `--gog-{input,select,ms}-float-label-on-bg` (the patch masking the
158
- border behind an `'on'` label).
112
+ Every group and token name is in **[`TOKENS.md`](./TOKENS.md)**, generated from `theme.css` so it
113
+ cannot drift, and available at runtime as `GOG_TOKEN_GROUPS`.
159
114
 
160
- The full list is the `INSTANCE_TOKENS` set in `scripts/check-tokens.mjs`, which is verified
161
- against the stylesheets on every CI run — an instance token that gets declared anywhere, or
162
- stops being read, fails the build. That check also enforces the other half of the contract:
163
- **no component stylesheet carries a default in a `var()` fallback**, so every value a
164
- component paints with really is discoverable in `theme.css`.
115
+ ### Light, dark and your own
165
116
 
166
- The float label's _geometry_ is not instance-layer it is themeable per component
167
- (`--gog-{input,select,ms}-float-label-{reserve,in-top,over-gap,over-reserve}`), and all three
168
- derive from the shared `--gog-field-float-label-*` scale above, so one declaration retunes
169
- every field at once while a single control can still be overridden on its own.
170
-
171
- ### Light and dark
172
-
173
- The theme is selected with a `data-theme` attribute on `:root`, and `ThemeService`
174
- manages it:
117
+ The active theme is a `data-theme` attribute on `:root`, managed by `ThemeService`:
175
118
 
176
119
  ```ts
177
120
  private readonly theme = inject(ThemeService);
178
- this.theme.toggleTheme(); // light ⇄ dark
179
- this.theme.setTheme('cyberpunk'); // any custom theme name
180
- this.theme.theme(); // read-only signal — go through the two methods above
121
+ this.theme.toggleTheme(); // light ⇄ dark
122
+ this.theme.setTheme('cyberpunk'); // any name you declared in CSS
181
123
  ```
182
124
 
183
- Out of the box it adopts whatever `data-theme` is already on the document, or `'light'`.
184
- Persisting the choice and following the OS setting are opt-in, through `GOG_CONFIG.theme`:
125
+ Out of the box it adopts whatever `data-theme` is already on the document, or `light`.
126
+ Persisting the choice and following the OS setting are opt-in:
185
127
 
186
128
  ```ts
187
129
  provideGogConfig({
@@ -189,146 +131,47 @@ provideGogConfig({
189
131
  });
190
132
  ```
191
133
 
192
- To add a theme, copy a palette block from `styles/theme.css` and change the attribute
193
- value. A theme only needs to declare what it actually changes the component tokens are
194
- re-derived from whatever palette is in scope, so a swapped `--gog-accent-color` reaches every
195
- component without listing any of them:
196
-
197
- ```css
198
- :root[data-theme='cyberpunk'],
199
- [data-theme='cyberpunk'] {
200
- color-scheme: dark;
201
- --gog-background-color: #050816;
202
- --gog-accent-color: #ff4edb;
203
- --gog-radius: 22px;
204
- }
205
- ```
206
-
207
- The second selector is what lets a theme apply to a _subtree_ rather than the whole page
208
- — put `data-theme="cyberpunk"` on any element and everything inside it re-derives from
209
- that palette, so one page can show several themes side by side (see the showcase's Theme
210
- lab). That works because `theme.css` re-declares its derived layer on `:root, [data-theme]`:
211
- a custom property's `var()` references are substituted where the property is _declared_,
212
- so a derived token declared only on `:root` would freeze to the root palette.
213
-
214
- Corollary worth knowing when writing your own themes: **anything you declare that reads
215
- another token must sit on the theme scope itself**, not on `:root`, or it will not follow
216
- a nested theme.
217
-
218
- ### Presets
219
-
220
- `theme.css` ships `light` and `dark`. `slate` is a third, importable separately — a
221
- palette-only theme in a cool/indigo register:
134
+ A theme only declares what it changes the derived layer re-resolves against whatever palette is
135
+ in scope, so a new palette restyles every component without listing any of them. The `slate`
136
+ preset is the worked example, palette-only:
222
137
 
223
138
  ```css
224
139
  @import '@guildofgleks/ui/styles/index.css';
225
140
  @import '@guildofgleks/ui/styles/presets/slate.css';
226
141
  ```
227
142
 
228
- ```html
229
- <html data-theme="slate"></html>
230
- ```
231
-
232
- It is worth reading as the worked example of the contract above: it declares **only palette
233
- tokens** — no `--gog-btn-*`, no `--gog-table-*` — and still restyles every component, because
234
- the derived layer re-resolves against whatever palette is in scope. If a theme of yours needs to
235
- list component tokens, that usually means the derived layer is missing something rather than
236
- that the theme needs to be longer.
237
-
238
- ### Optional font preset
143
+ Fonts are left alone on purpose (system stacks, no webfont download). Add
144
+ `@guildofgleks/ui/styles/fonts.css` for the showcase's typography.
239
145
 
240
- `styles/index.css` intentionally leaves fonts alone — generic system stacks, no webfont
241
- download. For the showcase's typography (Cinzel / Inter / JetBrains Mono, pulled from
242
- Google Fonts) add `@guildofgleks/ui/styles/fonts.css` as well.
146
+ ## App-wide configuration
243
147
 
244
- ## Global configuration
245
-
246
- Anything visual is a CSS token (above). For the rest — the handful of settings an app would
247
- otherwise repeat on every instance — there is one provider, not an injection token per
248
- component per setting:
148
+ Anything visual is a token. Everything else — the settings you would otherwise repeat on every
149
+ instance — goes through one provider:
249
150
 
250
151
  ```ts
251
- // app.config.ts
252
- import { provideGogConfig } from '@guildofgleks/ui';
253
-
254
- providers: [
255
- provideGogConfig({
256
- control: { size: 'sm', errorDisplay: 'auto', clearable: true },
257
- dropdown: { appendToBody: true },
258
- datepicker: { locale: 'de-DE', format: 'dd.MM.yyyy' },
259
- button: { debounce: 500 },
260
- }),
261
- ];
152
+ provideGogConfig({
153
+ control: { size: 'sm', errorDisplay: 'auto', clearable: true },
154
+ dropdown: { appendToBody: true },
155
+ datepicker: { locale: 'de-DE', format: 'dd.MM.yyyy' },
156
+ labels: { clear: 'Очистить', selectAll: 'Выбрать все' }, // translate the library once
157
+ });
262
158
  ```
263
159
 
264
- Keys: `control`, `dropdown`, `floatLabel`, `datepicker`, `autocomplete`, `inputfield`,
265
- `textarea`, `tooltip`, `scroll`, `button`, `toast`, `theme`, `labels`. Every field is optional,
266
- and an instance's own input always wins over the configured value. Providing it again lower in
267
- the injector tree (a route, a component) **layers onto** the parent's config one level deep
268
- rather than replacing it, so a route can override one field and inherit the rest.
269
-
270
- ### Your own icons
160
+ Keys: `control`, `dropdown`, `floatLabel`, `datepicker`, `autocomplete`, `inputfield`, `textarea`,
161
+ `tooltip`, `scroll`, `button`, `paginator`, `toast`, `theme`, `labels`. An instance's own input
162
+ always wins, and providing the config again lower in the injector tree layers onto the parent
163
+ rather than replacing it.
271
164
 
272
- `gog-icon` ships 41 [Lucide](https://lucide.dev) glyphs arrows and chevrons, check/close,
273
- the form-state set (success, error, warning, info), and the common actions (search, plus, minus,
274
- trash, pencil, download, upload, refresh, filter, external-link, copy), plus `menu`,
275
- `more-horizontal`/`more-vertical`, `user`, `settings`, `lock`, `mail`, `calendar`, `clock`, and
276
- `star`/`star-filled` for a rating or favourite toggle.
277
-
278
- Register your own by name and they behave like built-ins — including in every component that
279
- takes an icon *name* rather than a template:
165
+ Icons work the same way — 41 Lucide glyphs ship with the package, and your own register by name:
280
166
 
281
167
  ```ts
282
- // app.config.ts
283
- import { provideGogIcons } from '@guildofgleks/ui';
284
-
285
- providers: [
286
- provideGogIcons({
287
- cart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">…</svg>',
288
- }),
289
- ];
168
+ provideGogIcons({ cart: '<svg viewBox="0 0 24 24">…</svg>' });
290
169
  ```
291
170
 
292
171
  ```html
293
- <gog-icon name="cart" />
294
- <gog-tag iconName="cart">In basket</gog-tag>
295
- ```
296
-
297
- A registered name overrides a built-in of the same name, so you can swap the library's whole
298
- look — its checkmark, its chevrons — without touching a component. Nested calls layer onto the
299
- parent set. An unknown name renders nothing and warns in dev mode rather than throwing.
300
-
301
- Write the SVG with a `viewBox` and `currentColor`, and no width/height: sizing and stroke width
302
- come from the `--gog-icon-*` tokens, which is what makes a registered icon scale and colour like
303
- a built-in. The markup is inserted with `bypassSecurityTrustHtml` (Angular's sanitizer strips
304
- SVG outright), so register only static markup you authored — never a string built from user
305
- input.
306
-
307
- ### Translating the library
308
-
309
- `labels` carries every fixed string the components render themselves — the ones you never write
310
- markup for. Set them once instead of on every control:
311
-
312
- ```ts
313
- provideGogConfig({
314
- labels: {
315
- clear: 'Очистить',
316
- clearSelection: 'Очистить выбор',
317
- selectAll: 'Выбрать все',
318
- closeDialog: 'Закрыть',
319
- previousPage: 'Предыдущая страница',
320
- today: 'Сегодня',
321
- // interpolating labels take a function, since word order and agreement vary by language
322
- page: (page, isCurrent) =>
323
- isCurrent ? `Страница ${page}, текущая` : `Перейти на страницу ${page}`,
324
- },
325
- });
172
+ <gog-icon name="cart" /> <gog-tag iconName="cart">In basket</gog-tag>
326
173
  ```
327
174
 
328
- The full key list is on `GogGlobalConfig['labels']`. Strings that describe *one* control rather
329
- than library chrome — a field's `label`, a button's `ariaLabel` — are deliberately not in here;
330
- those stay per instance.
331
-
332
175
  ## Components
333
176
 
334
177
  | Group | Components |
@@ -341,150 +184,45 @@ those stay per instance.
341
184
  | Feedback | `gog-spinner`, `gog-spinner-overlay`, `gog-progressbar`, `gog-skeleton` |
342
185
  | Content | `gog-icon` |
343
186
 
344
- Directives: `gogButton`, `gogTooltip`, `gogBadge`, `gogCollapsibleTrigger`, `gogCollapsibleContent`.
345
-
346
- Slot directives, for replacing a component's markup rather than configuring it:
347
- `gogAccordionHeader`, `gogAccordionContent`, `gogAccordionChevron`, `gogButtonToggleOption`,
348
- `gogCheckboxIcon`, `gogColumnBody`, `gogColumnHeader`, `gogDropdownOption`,
349
- `gogDropdownChevron`, `gogInputAddonStart`, `gogInputAddonEnd`, `gogMultiselectClearIcon`,
350
- `gogTabHeader`, `gogTabContent`, `gogTagIcon`.
351
-
352
- `gog-dialog` and `gog-toast-container` are the host elements the two services render into —
353
- see [Services need a host element](#services-need-a-host-element).
354
-
355
- Services: `DialogService`, `ToastService`, `ThemeService`.
356
-
357
- `gog-collapsible` is a headless expand/collapse primitive — no owned markup, no portal.
358
- Project any element as the trigger via `gogCollapsibleTrigger` and any element as the
359
- panel via `gogCollapsibleContent`; `[(open)]` is two-way bindable. Useful for anything
360
- that needs an inline expanding region without the overlay behavior of `gog-select`/
361
- `gog-multiselect` — e.g. a collapsible group of links in a nav sidebar.
362
-
363
- ### Options come from your own objects
364
-
365
- `gog-select` and `gog-multiselect` do not require a `{ id, name }` shape. `optionLabel`,
366
- `optionValue` and `optionDisabled` each take a property path — dot-paths included — or a
367
- function:
368
-
369
- ```html
370
- <gog-select
371
- [options]="members"
372
- optionLabel="profile.fullName"
373
- optionValue="uuid"
374
- optionDisabled="suspended"
375
- [(value)]="memberId"
376
- />
377
- ```
378
-
379
- Set `[optionValue]="null"` and the control emits the option object itself, so a selection round-trips
380
- without a lookup table:
381
-
382
- ```html
383
- <gog-select [options]="members" [optionLabel]="nameOf" [optionValue]="null" [(value)]="member">
384
- <ng-template gogDropdownOption let-m let-label="label">
385
- {{ label }} — {{ $any(m).profile.role }}
386
- </ng-template>
387
- </gog-select>
388
- ```
389
-
390
- The defaults are `'name'` / `'id'` / `'disabled'`, which is what `GogDropdownOption` describes —
391
- so code written against that shape keeps working unchanged.
392
-
393
- `gog-table` works two ways. By default it owns the data: give it `value` and it sorts and pages
394
- in memory. With `[lazy]="true"` it hands both to the server — `value` is the current page,
395
- already sorted, `totalRecords` says how many rows exist in total, and `gogSortChange` /
396
- `gogPageChange` are the refetch signals. Row selection is `selectionMode` plus a two-way
397
- `[(selection)]`; set `dataKey` so rows are matched by id rather than object identity, or a
398
- refetch will drop the selection.
187
+ **Directives:** `gogButton` (a link that looks like a button), `gogTooltip`, `gogBadge`,
188
+ `gogCollapsibleTrigger`, `gogCollapsibleContent`.
189
+ **Services:** `DialogService`, `ToastService`, `ThemeService`.
399
190
 
400
- ```html
401
- <gog-table
402
- [value]="page()"
403
- [lazy]="true"
404
- [totalRecords]="total()"
405
- [pageSize]="20"
406
- [loading]="loading()"
407
- selectionMode="multiple"
408
- [(selection)]="selected"
409
- dataKey="id"
410
- (gogSortChange)="sort.set($event); reload()"
411
- (gogPageChange)="pageNumber.set($event); reload()"
412
- >
413
- <gog-column field="name" header="Name" [sortable]="true" />
414
- <gog-column field="email" header="Email" />
415
- </gog-table>
416
- ```
191
+ Fifteen more slot directives replace a component's markup rather than configuring it —
192
+ `gogColumnBody`, `gogInputAddonStart`, `gogDropdownOption` and friends.
417
193
 
418
- `gog-paginator` can offer a rows-per-page select off by default, on per instance with
419
- `showPageSizeSelect` or app-wide through `GOG_CONFIG.paginator`. Give it `totalRecords` rather
420
- than `totalPages` and it works out the page count from `pageSize` itself:
421
-
422
- ```html
423
- <gog-paginator [(page)]="page" [(pageSize)]="size" [totalRecords]="items().length" [showPageSizeSelect]="true" />
424
- ```
194
+ A few things worth knowing before you reach for a workaround:
425
195
 
426
- `pageSize` is a two-way `model` on both `gog-paginator` and `gog-table`, which is what connects
427
- the two without a signal in between: the table binds its own model to the paginator's, so a size
428
- picked in the select lands directly on whatever you bound with `[(pageSize)]`.
196
+ - **`gog-table` works two ways.** By default it owns the data and sorts and pages in memory. With
197
+ `[lazy]="true"` it hands both to the server: `value` is the current page, `totalRecords` drives
198
+ the paginator, and `gogSortChange` / `gogPageChange` are your refetch signals. Row selection is
199
+ `selectionMode` + `[(selection)]`; set `dataKey` or a refetch drops it.
200
+ - **`gog-button` cannot be a link** — it renders its own `<button>`. Use `[gogButton]` on your own
201
+ `<a>` instead; nothing is brokered through inputs, so `routerLink`, `href` and `target` keep
202
+ working. That is also why this package needs no `@angular/router`.
203
+ - **`gog-inputfield` and `gog-textarea` forward the native attribute space** they wrap —
204
+ `readonly`, `maxlength`, `pattern`, `inputMode`, `spellcheck` and the text-field `type` values.
205
+ They also generate their own `id`, so labels and error messages are wired up without `inputId`.
206
+ - **`gog-collapsible` is headless** — no markup of its own. Project any element as the trigger and
207
+ any element as the panel.
208
+ - **`[(ngModel)]` is untested.** The library never imports `FormsModule`; use Reactive Forms.
429
209
 
430
- `gog-button` renders its own `<button>`, so it can never *be* a link. For a call to action that
431
- navigates, use the `gogButton` directive on your own element instead — it applies the same look
432
- and nothing is brokered through an input, so `routerLink`, `href`, `target` and any directive of
433
- your own keep working:
210
+ ## Documentation
434
211
 
435
- ```html
436
- <a gogButton routerLink="/pricing">See pricing</a>
437
- <a gogButton variant="ghost" href="https://example.com" target="_blank" rel="noreferrer">Docs</a>
438
- ```
212
+ | | |
213
+ | --- | --- |
214
+ | **[`AGENTS.md`](./AGENTS.md)** | the full API reference — every input, output, slot, type and default, per component. Ships in this package. |
215
+ | **[`TOKENS.md`](./TOKENS.md)** | every `--gog-*` token, generated from `theme.css` |
216
+ | [CHANGELOG](https://github.com/GuildOfGleks/gleks_web_ui/blob/master/projects/gleks/ui/CHANGELOG.md) | release history |
439
217
 
440
- It is also why the package still needs no `@angular/router`. Keep `gog-button` for buttons that
441
- act on the page`loading`, `debounce` and `gogClick` are its, and a bare element cannot offer
442
- them.
443
-
444
- `gogTooltip` is a hover/focus tooltip directive, not a component — drop it on any element,
445
- a `gog-*` component's own host tag or a plain native one
446
- (`<button gogTooltip="Save changes">`, `<gog-chip [gogTooltip]="hint">`). Content is a
447
- string or a `TemplateRef`; `gogTooltipPosition` (`'auto'` default, or an explicit side),
448
- `gogTooltipShowDelay` (`300`ms default), `gogTooltipHideDelay` (`100`ms default) and
449
- `gogTooltipDisabled` inputs, the first three also configurable app-wide via
450
- `GOG_CONFIG.tooltip`.
451
-
452
- `gog-inputfield` and `gog-textarea` forward the native attribute space of the element they
453
- wrap, so nothing is out of reach because it is hidden inside a component: `readonly`,
454
- `maxlength`, `minlength`, `spellcheck`, plus `pattern` and `inputMode` on the input, and the
455
- `type` values that render as a text field (`text`, `password`, `email`, `number`, `search`,
456
- `tel`, `url`, `date`, `time`, `datetime-local` — see `GogInputType`). `readonly` keeps the
457
- value focusable and submitted but blocks edits, so the clear button and a number field's
458
- stepper both stand down while it is on. `autofocus` is deliberately **not** forwarded: moving
459
- focus without the user asking is disorienting for keyboard and screen-reader users, and an app
460
- that genuinely needs it can focus the element itself.
461
-
462
- Every field also generates its own `id` when you don't pass `inputId`, so the label is always
463
- associated with the control and the error message is always reachable through
464
- `aria-describedby`. Pass `inputId` only when something outside the component has to reference
465
- the field by a known id.
466
-
467
- `gog-checkbox`, `gog-inputfield`, `gog-select`, `gog-multiselect`, `gog-slider` and
468
- `gog-textarea` implement `ControlValueAccessor` and are built for Reactive Forms — use
469
- `[formControl]` or `formControlName`. The library itself never imports `FormsModule` or
470
- uses `ngModel`; `[(ngModel)]` is not tested against these components and isn't a
471
- supported usage path. With a form control attached, the error message appears once the
472
- control is both touched and invalid; without one it shows for as long as `errorMessage`
473
- is non-empty, and the consumer decides when to clear it.
474
-
475
- `gog-inputfield`, `gog-select`, `gog-multiselect` and `gog-textarea` all accept a
476
- `floatLabel` input — `'in'` (floats up but stays inside the border), `'on'` (floats to sit
477
- centered on the top border line) or `'over'` (floats fully above the field, outside the
478
- border) — plus `floatLabelShowPlaceholder` to reveal the field's own `placeholder` once the
479
- label has floated out of the way (it stays hidden the whole time otherwise, since the
480
- resting label already sits where it would). Both default to off/`false` and are settable
481
- per instance or app-wide via `GOG_CONFIG.floatLabel`.
218
+ `AGENTS.md` is written for an AI coding assistant working in your project, but it is the most
219
+ complete API reference either way point your assistant at it and it will stop guessing.
482
220
 
483
221
  ## License
484
222
 
485
223
  Apache-2.0 © Roman Malitskyi
486
224
 
487
- The built-in icons are [Lucide](https://lucide.dev) glyphs, inlined so the package keeps zero
488
- runtime dependencies. Lucide is ISC licensed; portions are held by Cole Bemis 2013–2022 as part
489
- of Feather (MIT), all others by Lucide Contributors 2022. The full notice is at the top of
490
- `src/lib/shared/icons.ts` in the source.
225
+ Built-in icons are [Lucide](https://lucide.dev) glyphs, inlined so the package keeps zero runtime
226
+ dependencies. Lucide is ISC licensed; portions are held by Cole Bemis 2013–2022 as part of
227
+ Feather (MIT), all others by Lucide Contributors 2022 full notice in
228
+ `src/lib/shared/icons.ts`.
@@ -836,6 +836,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
836
836
  }]
837
837
  }] });
838
838
 
839
+ /**
840
+ * Which `data-theme` an overlay rendered into `<body>` has to carry, if any.
841
+ *
842
+ * A panel or tooltip bubble is appended to `<body>`, which puts it outside whatever subtree it
843
+ * was opened from. `data-theme` can be scoped to any subtree — several themes rendering side by
844
+ * side is a documented use — so an overlay opened inside one of those has to be told which theme
845
+ * it belongs to, or it silently picks up the document's instead.
846
+ *
847
+ * **But only when the theme really is scoped.** When the nearest themed ancestor is the document
848
+ * element, copying the attribute is not merely redundant, it is actively wrong: the overlay
849
+ * already inherits everything from `<html>` through `<body>`, and re-stating `data-theme` on it
850
+ * makes it match `theme.css`'s derived layer (`:root, [data-theme]`) *locally*. That re-declares
851
+ * every component token on the overlay itself, resolved against the plain preset palette — which
852
+ * discards anything set on `<html>` that is not part of that preset. Custom properties written
853
+ * inline on `:root` are the case that bites: a live theme editor sets them there, the page
854
+ * follows, and every portal keeps rendering the un-edited theme.
855
+ *
856
+ * So: return the theme only for a genuinely scoped ancestor, and let inheritance do the work
857
+ * otherwise.
858
+ *
859
+ * Known limitation, currently unreachable: a *scoped* theme that is itself being edited through
860
+ * inline custom properties would still lose those on the overlay, since only the attribute is
861
+ * carried across. Copying resolved values instead would mean reading ~1200 properties on every
862
+ * open, which is not worth it for a case nothing does yet.
863
+ */
864
+ function scopedOverlayTheme(themeSource, documentElement) {
865
+ const themedAncestor = themeSource?.closest('[data-theme]');
866
+ if (!themedAncestor || themedAncestor === documentElement)
867
+ return null;
868
+ return themedAncestor.getAttribute('data-theme');
869
+ }
870
+
839
871
  /**
840
872
  * Renders a template into `document.body` while keeping it wired to the declaring
841
873
  * component: the view is stamped with that component's style encapsulation and reads
@@ -866,11 +898,9 @@ class GogDropdownOverlay {
866
898
  this.detach();
867
899
  this.hostEl = this.document.createElement('div');
868
900
  this.hostEl.classList.add('gog-overlay-host');
869
- // `data-theme` can be scoped to any subtree, not just `:root` (see the themes
870
- // showcase page), so the host has to copy it from wherever the trigger actually
871
- // sits rather than assume the document-wide theme applies.
872
- const themedAncestor = themeSource?.closest('[data-theme]');
873
- const theme = themedAncestor?.getAttribute('data-theme');
901
+ // Only for a genuinely scoped theme see `scopedOverlayTheme` for why copying it when the
902
+ // theme sits on `<html>` breaks live-edited themes.
903
+ const theme = scopedOverlayTheme(themeSource, this.document.documentElement);
874
904
  if (theme) {
875
905
  this.hostEl.setAttribute('data-theme', theme);
876
906
  }
@@ -6003,11 +6033,8 @@ class GogTooltipOverlay {
6003
6033
  this.detach();
6004
6034
  this.componentRef = this.viewContainerRef.createComponent(GogTooltipBubbleComponent);
6005
6035
  const el = this.componentRef.location.nativeElement;
6006
- // See GogDropdownOverlay.attach data-theme can be scoped to any subtree, not just
6007
- // :root, so it has to be copied from the trigger's nearest themed ancestor rather than
6008
- // assumed from the document.
6009
- const themedAncestor = themeSource?.closest('[data-theme]');
6010
- const theme = themedAncestor?.getAttribute('data-theme');
6036
+ // Only for a genuinely scoped theme see `scopedOverlayTheme`.
6037
+ const theme = scopedOverlayTheme(themeSource, this.document.documentElement);
6011
6038
  if (theme) {
6012
6039
  el.setAttribute('data-theme', theme);
6013
6040
  }