@akcelik/strct 0.2.0 → 0.3.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
@@ -19,7 +19,13 @@ npm install @akcelik/strct
19
19
  ```
20
20
 
21
21
  `@angular/core`, `@angular/common`, `@angular/forms` and
22
- `@angular/platform-browser` are peer dependencies.
22
+ `@angular/platform-browser` are peer dependencies (Angular 21.2+).
23
+
24
+ > **Consuming a local build?** Install the packed tarball
25
+ > (`ng build strct && npm pack dist/strct`, then `npm i ../akcelik-strct-x.y.z.tgz`)
26
+ > rather than a `file:` symlink — a symlinked dependency resolves its own copy of
27
+ > `@angular/core`, which can trip an `InputSignal` brand mismatch (TS2551) across
28
+ > Angular patch versions.
23
29
 
24
30
  ## Theme setup
25
31
 
@@ -31,6 +37,10 @@ form-control styles):
31
37
  @use '@akcelik/strct/styles/theme';
32
38
  ```
33
39
 
40
+ The theme **self-hosts its fonts** (DM Sans + JetBrains Mono, OFL) — they ship as
41
+ `woff2` under `styles/fonts/` and are referenced by `@font-face`, so there is no
42
+ external request and nothing else to load.
43
+
34
44
  Set the scheme on the document root (or let `StrctThemeService` manage it):
35
45
 
36
46
  ```html
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, signal, computed, Injectable, input, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, afterNextRender, Directive, booleanAttribute, output, HostListener, model, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, TemplateRef, contentChild, Renderer2 } from '@angular/core';
2
+ import { inject, DOCUMENT, signal, computed, Injectable, input, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, afterNextRender, Directive, booleanAttribute, output, HostListener, model, contentChildren, effect, DestroyRef, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, TemplateRef, contentChild, Renderer2 } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import { DOCUMENT as DOCUMENT$1, NgTemplateOutlet } from '@angular/common';
5
5
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
@@ -124,6 +124,9 @@ const STRCT_ICONS = {
124
124
  code: '<path d="M5.5 5L2.5 8l3 3M10.5 5l3 3-3 3M9 3.5l-2 9"/>',
125
125
  book: '<path d="M3 3.2h6a1.5 1.5 0 0 1 1.5 1.5v8.1H4.5A1.5 1.5 0 0 1 3 11.3z"/><path d="M13 3.2H9.5A1.5 1.5 0 0 0 8 4.7v8.1h5z"/>',
126
126
  terminal: '<rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M4.8 6.5L7 8.2 4.8 9.9M8.2 10.2h3"/>',
127
+ folder: '<path d="M2 4.6a1 1 0 0 1 1-1h3.1l1.3 1.6H13a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z"/>',
128
+ template: '<path d="M4.3 2.5h4.6l3.1 3.1V13a.5.5 0 0 1-.5.5H4.3a.5.5 0 0 1-.5-.5V3a.5.5 0 0 1 .5-.5z"/><path d="M8.7 2.6v3.1h3.1"/><path d="M5.8 9h4.2M5.8 11h2.6"/>',
129
+ tag: '<path d="M2.6 7.7V3.2a.6.6 0 0 1 .6-.6h4.5l5.6 5.6a1 1 0 0 1 0 1.4l-3.5 3.5a1 1 0 0 1-1.4 0z"/><circle cx="5.4" cy="5.4" r=".9" fill="currentColor" stroke="none"/>',
127
130
  // ── Datacenter / infrastructure ─────────────────────────────
128
131
  datacenter: '<rect x="2.5" y="2.5" width="11" height="11" rx="1"/><path d="M5 5h6M5 7.3h6M5 9.6h3.5"/><circle cx="11" cy="9.7" r=".5" fill="currentColor" stroke="none"/>',
129
132
  rack: '<rect x="3.5" y="2" width="9" height="12" rx="1"/><path d="M3.5 5.2h9M3.5 8.4h9M3.5 11.6h9"/><circle cx="5.4" cy="3.6" r=".4" fill="currentColor" stroke="none"/><circle cx="5.4" cy="6.8" r=".4" fill="currentColor" stroke="none"/><circle cx="5.4" cy="10" r=".4" fill="currentColor" stroke="none"/>',
@@ -144,6 +147,10 @@ const STRCT_ICONS = {
144
147
  hba: '<rect x="2.3" y="3.4" width="11.4" height="6.4" rx="1"/><circle cx="5.1" cy="6.6" r="1.2"/><circle cx="8.1" cy="6.6" r="1.2"/><path d="M10.6 5.6h1.6M10.6 7.6h1.6M5 9.8v2.6M11 9.8v2.6"/>',
145
148
  // RJ45 ethernet port (switch / server) — keyed jack with contact pins.
146
149
  ethernet: '<rect x="2.5" y="4" width="11" height="7.4" rx="1"/><path d="M6 4V2.6h4V4"/><path d="M5.3 7.6v2M7.1 7.6v2M8.9 7.6v2M10.7 7.6v2"/>',
150
+ // Resource pool — a proportional allocation (pie with two radii).
151
+ resourcePool: '<circle cx="8" cy="8" r="5.6"/><path d="M8 8V2.4M8 8l4.9 2.7"/>',
152
+ // Port group — a grouped set of switch ports under a shared rail.
153
+ portGroup: '<path d="M1.8 4.4h12.4"/><rect x="2.2" y="6.2" width="3.4" height="4" rx=".5"/><rect x="6.3" y="6.2" width="3.4" height="4" rx=".5"/><rect x="10.4" y="6.2" width="3.4" height="4" rx=".5"/><path d="M3.9 10.2v1.6M8 10.2v1.6M12.1 10.2v1.6"/>',
147
154
  // ── Accessibility (original glyphs for generic concepts) ─────
148
155
  universalAccess: '<circle cx="8" cy="8" r="6"/><circle cx="8" cy="4.9" r=".9" fill="currentColor" stroke="none"/><path d="M4.9 6.3c2 .8 4.2 .8 6.2 0M8 6.4v3.1M6.3 11.9 8 9.4l1.7 2.5"/>',
149
156
  wheelchair: '<circle cx="6.5" cy="3.1" r="1.3"/><path d="M6.5 4.5v3.3h3.1l1.7 3.4"/><circle cx="6.7" cy="11.3" r="2.6"/><path d="M9.3 11.3h2.1l-.5 1.6"/>',
@@ -181,7 +188,7 @@ const STRCT_ICON_GROUPS = [
181
188
  'hexagon', 'search', 'menu', 'ellipsis', 'dots', 'close', 'check', 'calendar',
182
189
  'eye', 'eyeOff', 'upload', 'download', 'sun', 'moon', 'bell', 'heart', 'layers',
183
190
  'grid', 'form', 'chart', 'bars', 'gauge', 'palette', 'sidebar', 'compass',
184
- 'copy', 'code', 'book', 'terminal',
191
+ 'copy', 'code', 'book', 'terminal', 'folder', 'template', 'tag',
185
192
  ],
186
193
  },
187
194
  {
@@ -201,6 +208,7 @@ const STRCT_ICON_GROUPS = [
201
208
  names: [
202
209
  'datacenter', 'rack', 'cluster', 'host', 'vm', 'switch', 'storage',
203
210
  'network', 'cpu', 'memory', 'disk', 'port', 'nic', 'hba', 'ethernet', 'power',
211
+ 'resourcePool', 'portGroup',
204
212
  ],
205
213
  },
206
214
  {
@@ -436,10 +444,16 @@ class StrctOverlay {
436
444
  let left;
437
445
  if (p === 'right') {
438
446
  left = a.right + gap;
447
+ // Flip to the left of the anchor if it would overflow the right edge.
448
+ if (left + w > vw - margin && a.left - gap - w > margin)
449
+ left = a.left - gap - w;
439
450
  top = a.top;
440
451
  }
441
452
  else if (p === 'left') {
442
453
  left = a.left - gap - w;
454
+ // Flip to the right of the anchor if it would overflow the left edge.
455
+ if (left < margin && a.right + gap + w < vw - margin)
456
+ left = a.right + gap;
443
457
  top = a.top;
444
458
  }
445
459
  else {
@@ -1295,6 +1309,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1295
1309
  }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }] } });
1296
1310
 
1297
1311
  let modalCounter = 0;
1312
+ // Body scroll-lock shared across any number of simultaneously open modals.
1313
+ let scrollLockCount = 0;
1314
+ let savedBodyOverflow = '';
1315
+ function lockBodyScroll(doc) {
1316
+ if (scrollLockCount === 0) {
1317
+ savedBodyOverflow = doc.body.style.overflow;
1318
+ doc.body.style.overflow = 'hidden';
1319
+ }
1320
+ scrollLockCount++;
1321
+ }
1322
+ function unlockBodyScroll(doc) {
1323
+ scrollLockCount = Math.max(0, scrollLockCount - 1);
1324
+ if (scrollLockCount === 0)
1325
+ doc.body.style.overflow = savedBodyOverflow;
1326
+ }
1298
1327
  /**
1299
1328
  * Overlay dialog with two-way `open`:
1300
1329
  * <strct-modal [(open)]="show" title="Confirm">
@@ -1317,16 +1346,32 @@ class StrctModal {
1317
1346
  titleId = `strct-modal-${++modalCounter}`;
1318
1347
  /** Element that had focus before the dialog opened, restored on close. */
1319
1348
  previousActive = null;
1349
+ /** Whether this instance currently holds a scroll lock. */
1350
+ locked = false;
1320
1351
  constructor() {
1321
1352
  effect(() => {
1322
- if (this.open()) {
1353
+ const open = this.open();
1354
+ if (open && !this.locked) {
1355
+ this.locked = true;
1356
+ lockBodyScroll(this.doc);
1323
1357
  this.previousActive = this.doc.activeElement;
1324
1358
  // Move focus into the dialog once it has rendered.
1325
1359
  setTimeout(() => this.focusInitial());
1326
1360
  }
1327
- else if (this.previousActive) {
1328
- this.previousActive.focus?.();
1329
- this.previousActive = null;
1361
+ else if (!open && this.locked) {
1362
+ this.locked = false;
1363
+ unlockBodyScroll(this.doc);
1364
+ if (this.previousActive) {
1365
+ this.previousActive.focus?.();
1366
+ this.previousActive = null;
1367
+ }
1368
+ }
1369
+ });
1370
+ // Release the lock if the modal is destroyed while still open.
1371
+ inject(DestroyRef).onDestroy(() => {
1372
+ if (this.locked) {
1373
+ this.locked = false;
1374
+ unlockBodyScroll(this.doc);
1330
1375
  }
1331
1376
  });
1332
1377
  }
@@ -1627,28 +1672,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1627
1672
  }] } });
1628
1673
 
1629
1674
  /**
1630
- * A nested fly-out inside a `strct-context-menu` or `strct-dropdown`. Reuse
1631
- * `strct-dropdown-item` for the nested entries.
1675
+ * A nested fly-out inside a `strct-context-menu` or `strct-dropdown`. Opens on
1676
+ * hover, click/tap, or the keyboard (Enter / Space / →), and flips to the left
1677
+ * near the right edge of the viewport. Reuse `strct-dropdown-item` for entries.
1632
1678
  * <strct-submenu label="Power">
1633
1679
  * <strct-dropdown-item>Power on</strct-dropdown-item>
1634
1680
  * <strct-dropdown-item>Power off</strct-dropdown-item>
1635
1681
  * </strct-submenu>
1636
1682
  */
1637
1683
  class StrctSubmenu {
1684
+ host = inject(ElementRef);
1638
1685
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1639
1686
  /** Optional leading icon; when omitted the icon column is still reserved so
1640
1687
  * the label stays aligned with sibling items that do have icons. */
1641
1688
  icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
1642
1689
  open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1690
+ /** Open to the left when the fly-out would overflow the right edge. */
1691
+ flip = signal(false, ...(ngDevMode ? [{ debugName: "flip" }] : /* istanbul ignore next */ []));
1692
+ setOpen(value) {
1693
+ if (value) {
1694
+ const rect = this.host.nativeElement.getBoundingClientRect();
1695
+ this.flip.set(rect.right + 190 > window.innerWidth);
1696
+ }
1697
+ this.open.set(value);
1698
+ }
1643
1699
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1644
1700
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctSubmenu, isStandalone: true, selector: "strct-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-submenu-host" }, ngImport: i0, template: `
1645
- <div class="strct-submenu" (mouseenter)="open.set(true)" (mouseleave)="open.set(false)">
1701
+ <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
1646
1702
  <div
1647
1703
  class="strct-submenu__trigger"
1648
1704
  role="menuitem"
1705
+ tabindex="0"
1649
1706
  aria-haspopup="menu"
1650
1707
  [attr.aria-expanded]="open()"
1651
- (click)="$event.stopPropagation()"
1708
+ (click)="$event.stopPropagation(); setOpen(!open())"
1709
+ (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1710
+ (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1711
+ (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1712
+ (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
1713
+ (keydown.escape)="$event.stopPropagation(); open.set(false)"
1652
1714
  >
1653
1715
  @if (icon()) {
1654
1716
  <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
@@ -1659,21 +1721,29 @@ class StrctSubmenu {
1659
1721
  <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1660
1722
  </div>
1661
1723
  @if (open()) {
1662
- <div class="strct-submenu__panel" role="menu"><ng-content /></div>
1724
+ <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
1725
+ <ng-content />
1726
+ </div>
1663
1727
  }
1664
1728
  </div>
1665
- `, isInline: true, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1729
+ `, isInline: true, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1666
1730
  }
1667
1731
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, decorators: [{
1668
1732
  type: Component,
1669
1733
  args: [{ selector: 'strct-submenu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1670
- <div class="strct-submenu" (mouseenter)="open.set(true)" (mouseleave)="open.set(false)">
1734
+ <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
1671
1735
  <div
1672
1736
  class="strct-submenu__trigger"
1673
1737
  role="menuitem"
1738
+ tabindex="0"
1674
1739
  aria-haspopup="menu"
1675
1740
  [attr.aria-expanded]="open()"
1676
- (click)="$event.stopPropagation()"
1741
+ (click)="$event.stopPropagation(); setOpen(!open())"
1742
+ (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1743
+ (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1744
+ (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1745
+ (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
1746
+ (keydown.escape)="$event.stopPropagation(); open.set(false)"
1677
1747
  >
1678
1748
  @if (icon()) {
1679
1749
  <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
@@ -1684,10 +1754,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1684
1754
  <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1685
1755
  </div>
1686
1756
  @if (open()) {
1687
- <div class="strct-submenu__panel" role="menu"><ng-content /></div>
1757
+ <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
1758
+ <ng-content />
1759
+ </div>
1688
1760
  }
1689
1761
  </div>
1690
- `, host: { class: 'strct-submenu-host' }, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"] }]
1762
+ `, host: { class: 'strct-submenu-host' }, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"] }]
1691
1763
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }] } });
1692
1764
 
1693
1765
  /**
@@ -2329,6 +2401,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
2329
2401
  `, host: { class: 'strct-pg', role: 'navigation', 'aria-label': 'Pagination' }, styles: [".strct-pg{display:inline-flex;align-items:center;gap:4px}.strct-pg__btn{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:30px;padding:0 7px;border-radius:6px;font-family:var(--font);font-size:13px;cursor:pointer;color:var(--t1);background:transparent;border:1px solid transparent;transition:background .14s ease,border-color .14s ease,color .14s ease}.strct-pg__btn:hover{background:var(--bg-3)}.strct-pg__btn--active{color:var(--acc);border-color:var(--acc30);background:var(--acc-m)}.strct-pg__btn:disabled{color:var(--t4);cursor:not-allowed;background:transparent}.strct-pg__nav{color:var(--t2)}.strct-pg__dots{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:30px;color:var(--t3)}\n"] }]
2330
2402
  }], propDecorators: { total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }, { type: i0.Output, args: ["pageChange"] }] } });
2331
2403
 
2404
+ let fieldCounter = 0;
2405
+ /**
2406
+ * Form-field wrapper: a label (with optional required marker), the projected
2407
+ * control, and a hint or error message. It auto-links the control via
2408
+ * `aria-describedby` and sets `aria-invalid` when an error is present.
2409
+ *
2410
+ * <strct-field label="Email" required hint="We never share it." [error]="emailError()">
2411
+ * <input strctInput type="email" [(ngModel)]="email" />
2412
+ * </strct-field>
2413
+ */
2414
+ class StrctField {
2415
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2416
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2417
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
2418
+ /** Error message (string or first-of array); falsy clears the error state. */
2419
+ error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
2420
+ host = inject(ElementRef);
2421
+ n = ++fieldCounter;
2422
+ hintId = `strct-field-hint-${this.n}`;
2423
+ errorId = `strct-field-err-${this.n}`;
2424
+ controlId = signal('', ...(ngDevMode ? [{ debugName: "controlId" }] : /* istanbul ignore next */ []));
2425
+ errorText = computed(() => {
2426
+ const e = this.error();
2427
+ return (Array.isArray(e) ? e[0] : e) ?? '';
2428
+ }, ...(ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []));
2429
+ constructor() {
2430
+ afterNextRender(() => this.link());
2431
+ // Keep aria in sync as the error / hint change.
2432
+ effect(() => {
2433
+ this.errorText();
2434
+ this.hint();
2435
+ this.applyAria();
2436
+ });
2437
+ }
2438
+ control() {
2439
+ return this.host.nativeElement.querySelector('input, select, textarea, [strctInput], [strctField]');
2440
+ }
2441
+ link() {
2442
+ const el = this.control();
2443
+ if (!el)
2444
+ return;
2445
+ if (!el.id)
2446
+ el.id = `strct-field-ctrl-${this.n}`;
2447
+ this.controlId.set(el.id);
2448
+ this.applyAria();
2449
+ }
2450
+ applyAria() {
2451
+ const el = this.control();
2452
+ if (!el)
2453
+ return;
2454
+ const describedBy = this.errorText() ? this.errorId : this.hint() ? this.hintId : '';
2455
+ if (describedBy)
2456
+ el.setAttribute('aria-describedby', describedBy);
2457
+ else
2458
+ el.removeAttribute('aria-describedby');
2459
+ if (this.errorText())
2460
+ el.setAttribute('aria-invalid', 'true');
2461
+ else
2462
+ el.removeAttribute('aria-invalid');
2463
+ }
2464
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctField, deps: [], target: i0.ɵɵFactoryTarget.Component });
2465
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctField, isStandalone: true, selector: "strct-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-field--invalid": "!!errorText()" }, classAttribute: "strct-field" }, ngImport: i0, template: `
2466
+ @if (label()) {
2467
+ <label class="strct-field__label" [attr.for]="controlId() || null">
2468
+ {{ label() }}@if (required()) {<span class="strct-field__req" aria-hidden="true">*</span>}
2469
+ </label>
2470
+ }
2471
+ <div class="strct-field__control"><ng-content /></div>
2472
+ @if (errorText()) {
2473
+ <div class="strct-field__msg strct-field__msg--error" [id]="errorId" role="alert">
2474
+ {{ errorText() }}
2475
+ </div>
2476
+ } @else if (hint()) {
2477
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
2478
+ }
2479
+ `, isInline: true, styles: [".strct-field{display:flex;flex-direction:column;gap:6px}.strct-field__label{font-size:12px;font-weight:600;color:var(--t2)}.strct-field__req{color:var(--crt);margin-left:2px}.strct-field__control{display:flex;flex-direction:column}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint{color:var(--t3)}.strct-field__msg--error{color:var(--crt)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2480
+ }
2481
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctField, decorators: [{
2482
+ type: Component,
2483
+ args: [{ selector: 'strct-field', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2484
+ @if (label()) {
2485
+ <label class="strct-field__label" [attr.for]="controlId() || null">
2486
+ {{ label() }}@if (required()) {<span class="strct-field__req" aria-hidden="true">*</span>}
2487
+ </label>
2488
+ }
2489
+ <div class="strct-field__control"><ng-content /></div>
2490
+ @if (errorText()) {
2491
+ <div class="strct-field__msg strct-field__msg--error" [id]="errorId" role="alert">
2492
+ {{ errorText() }}
2493
+ </div>
2494
+ } @else if (hint()) {
2495
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
2496
+ }
2497
+ `, host: {
2498
+ class: 'strct-field',
2499
+ '[class.strct-field--invalid]': '!!errorText()',
2500
+ }, styles: [".strct-field{display:flex;flex-direction:column;gap:6px}.strct-field__label{font-size:12px;font-weight:600;color:var(--t2)}.strct-field__req{color:var(--crt);margin-left:2px}.strct-field__control{display:flex;flex-direction:column}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint{color:var(--t3)}.strct-field__msg--error{color:var(--crt)}\n"] }]
2501
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }] } });
2502
+
2332
2503
  /**
2333
2504
  * Applies the shared `.strct-control` look to a native input / textarea / select.
2334
2505
  * <input strctInput placeholder="Name" />
@@ -5930,5 +6101,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
5930
6101
  * Generated bundle index. Do not edit.
5931
6102
  */
5932
6103
 
5933
- export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctChart, StrctCheckbox, StrctChips, StrctColorPicker, StrctCombobox, StrctContextMenu, StrctContextMenuTrigger, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDivider, StrctDonut, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctFile, StrctFooter, StrctGauge, StrctHeader, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKnob, StrctLogin, StrctMenuPanel, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRange, StrctRating, StrctRowDetailDef, StrctShell, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWizard, registerStrctIcon };
6104
+ export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctChart, StrctCheckbox, StrctChips, StrctColorPicker, StrctCombobox, StrctContextMenu, StrctContextMenuTrigger, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDivider, StrctDonut, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctField, StrctFile, StrctFooter, StrctGauge, StrctHeader, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKnob, StrctLogin, StrctMenuPanel, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRange, StrctRating, StrctRowDetailDef, StrctShell, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWizard, registerStrctIcon };
5934
6105
  //# sourceMappingURL=akcelik-strct.mjs.map