@akcelik/strct 4.8.0 → 4.10.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.
@@ -7438,6 +7438,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
7438
7438
  }], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }], loadingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingLabel", required: false }] }] } });
7439
7439
 
7440
7440
  let fieldCounter = 0;
7441
+ /** Text or a control rendered inside the field's box, before the input. */
7442
+ class StrctFieldPrefix {
7443
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldPrefix, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7444
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctFieldPrefix, isStandalone: true, selector: "[strctFieldPrefix]", ngImport: i0 });
7445
+ }
7446
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldPrefix, decorators: [{
7447
+ type: Directive,
7448
+ args: [{ selector: '[strctFieldPrefix]' }]
7449
+ }] });
7450
+ /** Text or a control rendered inside the field's box, after the input. */
7451
+ class StrctFieldSuffix {
7452
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldSuffix, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7453
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctFieldSuffix, isStandalone: true, selector: "[strctFieldSuffix]", ngImport: i0 });
7454
+ }
7455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldSuffix, decorators: [{
7456
+ type: Directive,
7457
+ args: [{ selector: '[strctFieldSuffix]' }]
7458
+ }] });
7459
+ /**
7460
+ * A hint with markup in it, rendered where the string `hint` renders and
7461
+ * carrying the same id, so it is still the control's description.
7462
+ *
7463
+ * <ng-template strctFieldHint>Hosts with a switch named <strong>{{ n }}</strong> join it.</ng-template>
7464
+ */
7465
+ class StrctFieldHint {
7466
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldHint, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7467
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctFieldHint, isStandalone: true, selector: "ng-template[strctFieldHint]", ngImport: i0 });
7468
+ }
7469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldHint, decorators: [{
7470
+ type: Directive,
7471
+ args: [{ selector: 'ng-template[strctFieldHint]' }]
7472
+ }] });
7473
+ /**
7474
+ * Wrapper that opts a run of `layout="inline"` fields out of the hairlines
7475
+ * between them — for settings that belong together and should read as one
7476
+ * block.
7477
+ */
7478
+ class StrctFieldGroup {
7479
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7480
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctFieldGroup, isStandalone: true, selector: "[strctFieldGroup]", host: { classAttribute: "strct-field-group" }, ngImport: i0 });
7481
+ }
7482
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFieldGroup, decorators: [{
7483
+ type: Directive,
7484
+ args: [{ selector: '[strctFieldGroup]', host: { class: 'strct-field-group' } }]
7485
+ }] });
7441
7486
  /**
7442
7487
  * Form-field wrapper: a label (with optional required marker), the projected
7443
7488
  * control, and a hint or error message. It auto-links the control via
@@ -7459,6 +7504,13 @@ class StrctField {
7459
7504
  /** Error message (string or first-of array); falsy clears the error state. */
7460
7505
  error = input(null, /* @ts-ignore */
7461
7506
  ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
7507
+ /**
7508
+ * `'inline'` puts the label and its hint in a fixed-width column
7509
+ * (`--strct-field-label-w`, 220px) and the control beside them — how a long
7510
+ * settings form reads. Below 480px of field width it falls back to stacked.
7511
+ */
7512
+ layout = input('stacked', /* @ts-ignore */
7513
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
7462
7514
  /**
7463
7515
  * Async-validation state rendered as a trailing adornment (spinner / check /
7464
7516
  * warning) plus its message in the hint/error slot — so apps stop composing a
@@ -7470,8 +7522,15 @@ class StrctField {
7470
7522
  n = ++fieldCounter;
7471
7523
  hintId = `strct-field-hint-${this.n}`;
7472
7524
  errorId = `strct-field-err-${this.n}`;
7525
+ prefixId = `strct-field-pre-${this.n}`;
7526
+ suffixId = `strct-field-suf-${this.n}`;
7473
7527
  controlId = signal('', /* @ts-ignore */
7474
7528
  ...(ngDevMode ? [{ debugName: "controlId" }] : /* istanbul ignore next */ []));
7529
+ hintTpl = contentChild(StrctFieldHint, { ...(ngDevMode ? { debugName: "hintTpl" } : /* istanbul ignore next */ {}), read: TemplateRef });
7530
+ hasPrefix = contentChild(StrctFieldPrefix, /* @ts-ignore */
7531
+ ...(ngDevMode ? [{ debugName: "hasPrefix" }] : /* istanbul ignore next */ []));
7532
+ hasSuffix = contentChild(StrctFieldSuffix, /* @ts-ignore */
7533
+ ...(ngDevMode ? [{ debugName: "hasSuffix" }] : /* istanbul ignore next */ []));
7475
7534
  errorText = computed(() => {
7476
7535
  const e = this.error();
7477
7536
  return (Array.isArray(e) ? e[0] : e) ?? '';
@@ -7497,6 +7556,9 @@ class StrctField {
7497
7556
  effect(() => {
7498
7557
  this.errorText();
7499
7558
  this.hint();
7559
+ this.hintTpl();
7560
+ this.hasPrefix();
7561
+ this.hasSuffix();
7500
7562
  this.stateStatus();
7501
7563
  this.stateMessage();
7502
7564
  this.applyAria();
@@ -7514,15 +7576,37 @@ class StrctField {
7514
7576
  this.controlId.set(el.id);
7515
7577
  this.applyAria();
7516
7578
  }
7579
+ /**
7580
+ * A text addon belongs to the control's description — "Minimum memory, MB" —
7581
+ * while an addon holding its own control (a send button) is a tab stop of its
7582
+ * own and says nothing about the value.
7583
+ */
7584
+ addonDescription(sel, id) {
7585
+ const el = this.host.nativeElement.querySelector(sel);
7586
+ if (!el || !el.textContent?.trim())
7587
+ return '';
7588
+ if (el.matches('button, a, input, select, textarea'))
7589
+ return '';
7590
+ if (el.querySelector('button, a, input, select, textarea'))
7591
+ return '';
7592
+ return id;
7593
+ }
7517
7594
  applyAria() {
7518
7595
  const el = this.control();
7519
7596
  if (!el)
7520
7597
  return;
7521
- const describedBy = this.errorText()
7598
+ const message = this.errorText()
7522
7599
  ? this.errorId
7523
- : this.stateMessage() || this.hint()
7600
+ : this.stateMessage() || this.hintTpl() || this.hint()
7524
7601
  ? this.hintId
7525
7602
  : '';
7603
+ const describedBy = [
7604
+ this.addonDescription('[strctFieldPrefix]', this.prefixId),
7605
+ this.addonDescription('[strctFieldSuffix]', this.suffixId),
7606
+ message,
7607
+ ]
7608
+ .filter(Boolean)
7609
+ .join(' ');
7526
7610
  if (describedBy)
7527
7611
  el.setAttribute('aria-describedby', describedBy);
7528
7612
  else
@@ -7533,7 +7617,7 @@ class StrctField {
7533
7617
  el.removeAttribute('aria-invalid');
7534
7618
  }
7535
7619
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctField, deps: [], target: i0.ɵɵFactoryTarget.Component });
7536
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", 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 }, validationState: { classPropertyName: "validationState", publicName: "validationState", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-field--invalid": "isInvalid()", "class.strct-field--validating": "stateActive()" }, classAttribute: "strct-field" }, ngImport: i0, template: `
7620
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", 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 }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, validationState: { classPropertyName: "validationState", publicName: "validationState", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-field--invalid": "isInvalid()", "class.strct-field--validating": "stateActive()", "class.strct-field--inline": "layout() === 'inline'", "class.strct-field--prefix": "hasPrefix()", "class.strct-field--suffix": "hasSuffix()" }, classAttribute: "strct-field" }, queries: [{ propertyName: "hintTpl", first: true, predicate: StrctFieldHint, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "hasPrefix", first: true, predicate: StrctFieldPrefix, descendants: true, isSignal: true }, { propertyName: "hasSuffix", first: true, predicate: StrctFieldSuffix, descendants: true, isSignal: true }], ngImport: i0, template: `
7537
7621
  @if (label()) {
7538
7622
  <label class="strct-field__label" [attr.for]="controlId() || null">
7539
7623
  {{ label() }}
@@ -7543,6 +7627,15 @@ class StrctField {
7543
7627
  </label>
7544
7628
  }
7545
7629
  <div class="strct-field__control">
7630
+ <!-- The addon slots come first so their selectors win over the catch-all
7631
+ below (Angular matches in template order); CSS order puts them
7632
+ either side of the control. -->
7633
+ <span class="strct-field__addon strct-field__addon--prefix" [id]="prefixId">
7634
+ <ng-content select="[strctFieldPrefix]" />
7635
+ </span>
7636
+ <span class="strct-field__addon strct-field__addon--suffix" [id]="suffixId">
7637
+ <ng-content select="[strctFieldSuffix]" />
7638
+ </span>
7546
7639
  <ng-content />
7547
7640
  @if (stateActive()) {
7548
7641
  <span class="strct-field__adorn strct-field__adorn--{{ stateStatus() }}" aria-hidden="true">
@@ -7567,14 +7660,18 @@ class StrctField {
7567
7660
  >
7568
7661
  {{ stateMessage() }}
7569
7662
  </div>
7663
+ } @else if (hintTpl()) {
7664
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">
7665
+ <ng-container [ngTemplateOutlet]="hintTpl()!" />
7666
+ </div>
7570
7667
  } @else if (hint()) {
7571
7668
  <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
7572
7669
  }
7573
- `, 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(--critical);margin-inline-start:2px}.strct-field__control{position:relative;display:flex;flex-direction:column}.strct-field__adorn{position:absolute;top:50%;inset-inline-end:10px;transform:translateY(-50%);display:inline-flex;align-items:center;pointer-events:none}.strct-field__adorn--ok{color:var(--success)}.strct-field__adorn--warning{color:var(--warning)}.strct-field__adorn--error{color:var(--critical)}.strct-field--validating .strct-control{padding-inline-end:32px}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint,.strct-field__msg--checking{color:var(--t3)}.strct-field__msg--ok{color:var(--success)}.strct-field__msg--warning{color:var(--warning)}.strct-field__msg--error{color:var(--critical)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctSpinner, selector: "strct-spinner", inputs: ["size", "label", "caption"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7670
+ `, 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(--critical);margin-inline-start:2px}.strct-field__control{position:relative;display:flex;flex-direction:column}.strct-field__adorn{position:absolute;top:50%;inset-inline-end:10px;transform:translateY(-50%);display:inline-flex;align-items:center;pointer-events:none}.strct-field__adorn--ok{color:var(--success)}.strct-field__adorn--warning{color:var(--warning)}.strct-field__adorn--error{color:var(--critical)}.strct-field--validating .strct-control{padding-inline-end:32px}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint,.strct-field__msg--checking{color:var(--t3)}.strct-field__msg--ok{color:var(--success)}.strct-field__msg--warning{color:var(--warning)}.strct-field__msg--error{color:var(--critical)}.strct-field__addon{display:none;align-items:center;flex:none;font-size:13px;color:var(--t3)}.strct-field__addon--prefix{order:-1;padding-inline-start:var(--space-3)}.strct-field__addon--suffix{order:1;padding-inline-end:var(--space-3)}.strct-field--prefix .strct-field__addon--prefix,.strct-field--suffix .strct-field__addon--suffix{display:inline-flex}.strct-field__addon:has(.strct-btn){padding:3px}.strct-field--prefix .strct-field__control,.strct-field--suffix .strct-field__control{flex-direction:row;align-items:center;background:var(--bg-2);border:1px solid var(--b2);border-radius:var(--radius-md);transition:border-color .14s ease,box-shadow .14s ease,background .14s ease}.strct-field--prefix .strct-field__control:hover,.strct-field--suffix .strct-field__control:hover{border-color:var(--b3)}.strct-field--prefix .strct-field__control:focus-within,.strct-field--suffix .strct-field__control:focus-within{border-color:var(--acc50);box-shadow:0 0 0 3px var(--acc18);background:var(--bg-1)}.strct-field--invalid.strct-field--prefix .strct-field__control,.strct-field--invalid.strct-field--suffix .strct-field__control{border-color:var(--critical)}.strct-field--prefix .strct-control,.strct-field--suffix .strct-control{flex:1;min-width:0;border:0;background:transparent}.strct-field--prefix .strct-control:focus,.strct-field--prefix .strct-control:focus-visible,.strct-field--suffix .strct-control:focus,.strct-field--suffix .strct-control:focus-visible{box-shadow:none;background:transparent}.strct-field--prefix textarea.strct-control,.strct-field--suffix textarea.strct-control{min-height:0}.strct-field--inline{container-type:inline-size;display:grid;grid-template-columns:var(--strct-field-label-w, 220px) minmax(0,1fr);column-gap:var(--space-3);row-gap:4px;align-items:start}.strct-field--inline>.strct-field__label{grid-column:1;grid-row:1;padding-block-start:9px}.strct-field--inline>.strct-field__control{grid-column:2;grid-row:1}.strct-field--inline>.strct-field__msg{grid-column:2;grid-row:2}.strct-field--inline>.strct-field__msg--hint{grid-column:1;padding-block-end:var(--space-2)}.strct-field--inline+.strct-field--inline{border-block-start:1px solid var(--b1);margin-block-start:var(--space-3);padding-block-start:var(--space-3)}.strct-field-group .strct-field--inline+.strct-field--inline{border-block-start:0;padding-block-start:0}@container (max-width: 480px){.strct-field--inline>.strct-field__label,.strct-field--inline>.strct-field__control,.strct-field--inline>.strct-field__msg{grid-column:1 / -1;grid-row:auto}.strct-field--inline>.strct-field__label{padding-block-start:0}.strct-field--inline>.strct-field__msg--hint{padding-block-end:0}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctSpinner, selector: "strct-spinner", inputs: ["size", "label", "caption"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7574
7671
  }
7575
7672
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctField, decorators: [{
7576
7673
  type: Component,
7577
- args: [{ selector: 'strct-field', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctSpinner], template: `
7674
+ args: [{ selector: 'strct-field', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctSpinner, NgTemplateOutlet], template: `
7578
7675
  @if (label()) {
7579
7676
  <label class="strct-field__label" [attr.for]="controlId() || null">
7580
7677
  {{ label() }}
@@ -7584,6 +7681,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
7584
7681
  </label>
7585
7682
  }
7586
7683
  <div class="strct-field__control">
7684
+ <!-- The addon slots come first so their selectors win over the catch-all
7685
+ below (Angular matches in template order); CSS order puts them
7686
+ either side of the control. -->
7687
+ <span class="strct-field__addon strct-field__addon--prefix" [id]="prefixId">
7688
+ <ng-content select="[strctFieldPrefix]" />
7689
+ </span>
7690
+ <span class="strct-field__addon strct-field__addon--suffix" [id]="suffixId">
7691
+ <ng-content select="[strctFieldSuffix]" />
7692
+ </span>
7587
7693
  <ng-content />
7588
7694
  @if (stateActive()) {
7589
7695
  <span class="strct-field__adorn strct-field__adorn--{{ stateStatus() }}" aria-hidden="true">
@@ -7608,6 +7714,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
7608
7714
  >
7609
7715
  {{ stateMessage() }}
7610
7716
  </div>
7717
+ } @else if (hintTpl()) {
7718
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">
7719
+ <ng-container [ngTemplateOutlet]="hintTpl()!" />
7720
+ </div>
7611
7721
  } @else if (hint()) {
7612
7722
  <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
7613
7723
  }
@@ -7615,8 +7725,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
7615
7725
  class: 'strct-field',
7616
7726
  '[class.strct-field--invalid]': 'isInvalid()',
7617
7727
  '[class.strct-field--validating]': 'stateActive()',
7618
- }, 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(--critical);margin-inline-start:2px}.strct-field__control{position:relative;display:flex;flex-direction:column}.strct-field__adorn{position:absolute;top:50%;inset-inline-end:10px;transform:translateY(-50%);display:inline-flex;align-items:center;pointer-events:none}.strct-field__adorn--ok{color:var(--success)}.strct-field__adorn--warning{color:var(--warning)}.strct-field__adorn--error{color:var(--critical)}.strct-field--validating .strct-control{padding-inline-end:32px}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint,.strct-field__msg--checking{color:var(--t3)}.strct-field__msg--ok{color:var(--success)}.strct-field__msg--warning{color:var(--warning)}.strct-field__msg--error{color:var(--critical)}\n"] }]
7619
- }], 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 }] }], validationState: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationState", required: false }] }] } });
7728
+ '[class.strct-field--inline]': "layout() === 'inline'",
7729
+ '[class.strct-field--prefix]': 'hasPrefix()',
7730
+ '[class.strct-field--suffix]': 'hasSuffix()',
7731
+ }, 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(--critical);margin-inline-start:2px}.strct-field__control{position:relative;display:flex;flex-direction:column}.strct-field__adorn{position:absolute;top:50%;inset-inline-end:10px;transform:translateY(-50%);display:inline-flex;align-items:center;pointer-events:none}.strct-field__adorn--ok{color:var(--success)}.strct-field__adorn--warning{color:var(--warning)}.strct-field__adorn--error{color:var(--critical)}.strct-field--validating .strct-control{padding-inline-end:32px}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint,.strct-field__msg--checking{color:var(--t3)}.strct-field__msg--ok{color:var(--success)}.strct-field__msg--warning{color:var(--warning)}.strct-field__msg--error{color:var(--critical)}.strct-field__addon{display:none;align-items:center;flex:none;font-size:13px;color:var(--t3)}.strct-field__addon--prefix{order:-1;padding-inline-start:var(--space-3)}.strct-field__addon--suffix{order:1;padding-inline-end:var(--space-3)}.strct-field--prefix .strct-field__addon--prefix,.strct-field--suffix .strct-field__addon--suffix{display:inline-flex}.strct-field__addon:has(.strct-btn){padding:3px}.strct-field--prefix .strct-field__control,.strct-field--suffix .strct-field__control{flex-direction:row;align-items:center;background:var(--bg-2);border:1px solid var(--b2);border-radius:var(--radius-md);transition:border-color .14s ease,box-shadow .14s ease,background .14s ease}.strct-field--prefix .strct-field__control:hover,.strct-field--suffix .strct-field__control:hover{border-color:var(--b3)}.strct-field--prefix .strct-field__control:focus-within,.strct-field--suffix .strct-field__control:focus-within{border-color:var(--acc50);box-shadow:0 0 0 3px var(--acc18);background:var(--bg-1)}.strct-field--invalid.strct-field--prefix .strct-field__control,.strct-field--invalid.strct-field--suffix .strct-field__control{border-color:var(--critical)}.strct-field--prefix .strct-control,.strct-field--suffix .strct-control{flex:1;min-width:0;border:0;background:transparent}.strct-field--prefix .strct-control:focus,.strct-field--prefix .strct-control:focus-visible,.strct-field--suffix .strct-control:focus,.strct-field--suffix .strct-control:focus-visible{box-shadow:none;background:transparent}.strct-field--prefix textarea.strct-control,.strct-field--suffix textarea.strct-control{min-height:0}.strct-field--inline{container-type:inline-size;display:grid;grid-template-columns:var(--strct-field-label-w, 220px) minmax(0,1fr);column-gap:var(--space-3);row-gap:4px;align-items:start}.strct-field--inline>.strct-field__label{grid-column:1;grid-row:1;padding-block-start:9px}.strct-field--inline>.strct-field__control{grid-column:2;grid-row:1}.strct-field--inline>.strct-field__msg{grid-column:2;grid-row:2}.strct-field--inline>.strct-field__msg--hint{grid-column:1;padding-block-end:var(--space-2)}.strct-field--inline+.strct-field--inline{border-block-start:1px solid var(--b1);margin-block-start:var(--space-3);padding-block-start:var(--space-3)}.strct-field-group .strct-field--inline+.strct-field--inline{border-block-start:0;padding-block-start:0}@container (max-width: 480px){.strct-field--inline>.strct-field__label,.strct-field--inline>.strct-field__control,.strct-field--inline>.strct-field__msg{grid-column:1 / -1;grid-row:auto}.strct-field--inline>.strct-field__label{padding-block-start:0}.strct-field--inline>.strct-field__msg--hint{padding-block-end:0}}\n"] }]
7732
+ }], 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 }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], validationState: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationState", required: false }] }], hintTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctFieldHint), { ...{ read: TemplateRef }, isSignal: true }] }], hasPrefix: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctFieldPrefix), { isSignal: true }] }], hasSuffix: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctFieldSuffix), { isSignal: true }] }] } });
7620
7733
 
7621
7734
  /**
7622
7735
  * Applies the shared `.strct-control` look to a native input / textarea / select.
@@ -11740,178 +11853,452 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
11740
11853
  }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], hover: [{ type: i0.Input, args: [{ isSignal: true, alias: "hover", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], cellDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctCellDef), { isSignal: true }] }] } });
11741
11854
 
11742
11855
  /**
11743
- * Minimal, dependency-free XLSX writer for the datagrid's Excel export.
11856
+ * Numeric stepper input: − / + buttons flanking a text field. The value is
11857
+ * `number | null` (null = empty); typing is free-form (intermediate states
11858
+ * like "-" are kept), committed values clamp to `min`/`max` on blur, and the
11859
+ * buttons + keyboard always clamp immediately.
11744
11860
  *
11745
- * An .xlsx file is a ZIP of SpreadsheetML parts. This builds the five
11746
- * required parts with inline strings and zips them with STORED (uncompressed)
11747
- * entries — no runtime dependency, byte-accurate CRC32, opens in Excel,
11748
- * LibreOffice and Google Sheets.
11861
+ * <strct-number [min]="0" [max]="64" [step]="4" [(ngModel)]="vcpus" />
11862
+ *
11863
+ * Keyboard on the field: ArrowUp/Down ±step, PageUp/Down ±10×step,
11864
+ * Home/End jump to min/max. The field exposes `role="spinbutton"` with
11865
+ * aria-valuemin/max/now so assistive tech announces it as a number control.
11749
11866
  */
11750
- const CRC_TABLE = (() => {
11751
- const t = new Uint32Array(256);
11752
- for (let n = 0; n < 256; n++) {
11753
- let c = n;
11754
- for (let k = 0; k < 8; k++)
11755
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
11756
- t[n] = c >>> 0;
11867
+ class StrctNumber {
11868
+ /** Minimum allowed value; null = unbounded. */
11869
+ min = input(null, /* @ts-ignore */
11870
+ ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
11871
+ /** Maximum allowed value; null = unbounded. */
11872
+ max = input(null, /* @ts-ignore */
11873
+ ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
11874
+ /** Step increment for buttons and Arrow keys (PageUp/Down use 10×). */
11875
+ step = input(1, /* @ts-ignore */
11876
+ ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
11877
+ /** Placeholder text when empty. */
11878
+ placeholder = input('', /* @ts-ignore */
11879
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
11880
+ /** Accessible label of the + button (localizable). */
11881
+ incrementLabel = input('Increment', /* @ts-ignore */
11882
+ ...(ngDevMode ? [{ debugName: "incrementLabel" }] : /* istanbul ignore next */ []));
11883
+ /** Accessible label of the − button (localizable). */
11884
+ decrementLabel = input('Decrement', /* @ts-ignore */
11885
+ ...(ngDevMode ? [{ debugName: "decrementLabel" }] : /* istanbul ignore next */ []));
11886
+ /** Static disable; forms' setDisabledState also drives the disabled state. */
11887
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
11888
+ value = signal(null, /* @ts-ignore */
11889
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
11890
+ /** Raw field text — may hold an unparseable intermediate state while typing. */
11891
+ text = signal('', /* @ts-ignore */
11892
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
11893
+ /** Disabled state pushed by the forms API (setDisabledState). */
11894
+ cvaDisabled = signal(false, /* @ts-ignore */
11895
+ ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
11896
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
11897
+ ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
11898
+ /** At/below min (or min unset → never) — greys out the − button. */
11899
+ atMin = computed(() => {
11900
+ const v = this.value();
11901
+ const m = this.min();
11902
+ return v !== null && m !== null && v <= m;
11903
+ }, /* @ts-ignore */
11904
+ ...(ngDevMode ? [{ debugName: "atMin" }] : /* istanbul ignore next */ []));
11905
+ /** At/above max (or max unset → never) — greys out the + button. */
11906
+ atMax = computed(() => {
11907
+ const v = this.value();
11908
+ const m = this.max();
11909
+ return v !== null && m !== null && v >= m;
11910
+ }, /* @ts-ignore */
11911
+ ...(ngDevMode ? [{ debugName: "atMax" }] : /* istanbul ignore next */ []));
11912
+ onChange = () => { };
11913
+ onTouched = () => { };
11914
+ onInput(event) {
11915
+ const raw = event.target.value;
11916
+ this.text.set(raw);
11917
+ const trimmed = raw.trim();
11918
+ if (trimmed === '') {
11919
+ this.value.set(null);
11920
+ this.onChange(null);
11921
+ return;
11922
+ }
11923
+ const parsed = Number(trimmed);
11924
+ // Intermediate states ("-", "1.", "2e") don't parse yet — keep the text,
11925
+ // hold the last value; clamping happens on blur.
11926
+ if (Number.isFinite(parsed)) {
11927
+ this.value.set(parsed);
11928
+ this.onChange(parsed);
11929
+ }
11757
11930
  }
11758
- return t;
11759
- })();
11760
- function crc32(data) {
11761
- let c = 0xffffffff;
11762
- for (const byte of data)
11763
- c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
11764
- return (c ^ 0xffffffff) >>> 0;
11765
- }
11766
- const enc = new TextEncoder();
11767
- /** ZIP with STORED entries (no compression — spreadsheets are small). */
11768
- function zip(entries) {
11769
- const chunks = [];
11770
- const central = [];
11771
- let offset = 0;
11772
- const u16 = (n) => new Uint8Array([n & 0xff, (n >> 8) & 0xff]);
11773
- const u32 = (n) => new Uint8Array([n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]);
11774
- const cat = (...parts) => {
11775
- const total = parts.reduce((s, p) => s + p.length, 0);
11776
- const out = new Uint8Array(total);
11777
- let o = 0;
11778
- for (const p of parts) {
11779
- out.set(p, o);
11780
- o += p.length;
11931
+ onKeydown(event) {
11932
+ if (this.isDisabled())
11933
+ return;
11934
+ switch (event.key) {
11935
+ case 'ArrowUp':
11936
+ event.preventDefault();
11937
+ this.stepBy(1);
11938
+ break;
11939
+ case 'ArrowDown':
11940
+ event.preventDefault();
11941
+ this.stepBy(-1);
11942
+ break;
11943
+ case 'PageUp':
11944
+ event.preventDefault();
11945
+ this.stepBy(1, 10);
11946
+ break;
11947
+ case 'PageDown':
11948
+ event.preventDefault();
11949
+ this.stepBy(-1, 10);
11950
+ break;
11951
+ case 'Home': {
11952
+ const m = this.min();
11953
+ if (m === null)
11954
+ return;
11955
+ event.preventDefault();
11956
+ this.commitTo(m);
11957
+ break;
11958
+ }
11959
+ case 'End': {
11960
+ const m = this.max();
11961
+ if (m === null)
11962
+ return;
11963
+ event.preventDefault();
11964
+ this.commitTo(m);
11965
+ break;
11966
+ }
11781
11967
  }
11782
- return out;
11783
- };
11784
- for (const { name, data } of entries) {
11785
- const nameBytes = enc.encode(name);
11786
- const crc = crc32(data);
11787
- const local = cat(u32(0x04034b50), u16(20), // version needed
11788
- u16(0), // flags
11789
- u16(0), // method: stored
11790
- u16(0), u16(0), // mod time/date
11791
- u32(crc), u32(data.length), u32(data.length), u16(nameBytes.length), u16(0), nameBytes, data);
11792
- central.push(cat(u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0), u32(crc), u32(data.length), u32(data.length), u16(nameBytes.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(offset), nameBytes));
11793
- chunks.push(local);
11794
- offset += local.length;
11795
11968
  }
11796
- const centralStart = offset;
11797
- const centralBytes = cat(...central);
11798
- const end = cat(u32(0x06054b50), u16(0), u16(0), u16(entries.length), u16(entries.length), u32(centralBytes.length), u32(centralStart), u16(0));
11799
- return cat(...chunks, centralBytes, end);
11800
- }
11801
- const xml = (s) => String(s ?? '')
11802
- .replace(/&/g, '&amp;')
11803
- .replace(/</g, '&lt;')
11804
- .replace(/>/g, '&gt;')
11805
- .replace(/"/g, '&quot;');
11806
- /** A1-style column ref: 0 → A, 26 → AA … */
11807
- function colRef(i) {
11808
- let s = '';
11809
- for (let n = i; n >= 0; n = Math.floor(n / 26) - 1)
11810
- s = String.fromCharCode(65 + (n % 26)) + s;
11811
- return s;
11812
- }
11813
- /**
11814
- * Build a single-sheet workbook: `header` as a bold-free first row, then
11815
- * `rows`. Numbers stay numeric; everything else exports as inline strings.
11816
- */
11817
- function buildXlsx(header, rows, sheetName = 'Data') {
11818
- const rowXml = (cells, r) => `<row r="${r}">` +
11819
- cells
11820
- .map((v, c) => {
11821
- const ref = `${colRef(c)}${r}`;
11822
- if (typeof v === 'number' && Number.isFinite(v)) {
11823
- return `<c r="${ref}"><v>${v}</v></c>`;
11969
+ /** Commit the pending text on blur: parse, clamp, restore on garbage. */
11970
+ onBlur() {
11971
+ const trimmed = this.text().trim();
11972
+ if (trimmed === '') {
11973
+ if (this.value() !== null) {
11974
+ this.value.set(null);
11975
+ this.onChange(null);
11824
11976
  }
11825
- return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${xml(v)}</t></is></c>`;
11826
- })
11827
- .join('') +
11828
- '</row>';
11829
- const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11830
- `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>` +
11831
- rowXml(header, 1) +
11832
- rows.map((cells, i) => rowXml(cells, i + 2)).join('') +
11833
- `</sheetData></worksheet>`;
11834
- const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11835
- `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ` +
11836
- `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">` +
11837
- `<sheets><sheet name="${xml(sheetName)}" sheetId="1" r:id="rId1"/></sheets></workbook>`;
11838
- const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11839
- `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
11840
- `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>` +
11841
- `</Relationships>`;
11842
- const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11843
- `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
11844
- `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>` +
11845
- `</Relationships>`;
11846
- const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11847
- `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
11848
- `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
11849
- `<Default Extension="xml" ContentType="application/xml"/>` +
11850
- `<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` +
11851
- `<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
11852
- `</Types>`;
11853
- return zip([
11854
- { name: '[Content_Types].xml', data: enc.encode(contentTypes) },
11855
- { name: '_rels/.rels', data: enc.encode(rootRels) },
11856
- { name: 'xl/workbook.xml', data: enc.encode(workbook) },
11857
- { name: 'xl/_rels/workbook.xml.rels', data: enc.encode(workbookRels) },
11858
- { name: 'xl/worksheets/sheet1.xml', data: enc.encode(sheet) },
11859
- ]);
11977
+ this.text.set('');
11978
+ }
11979
+ else {
11980
+ const parsed = Number(trimmed);
11981
+ if (Number.isFinite(parsed)) {
11982
+ const clamped = this.clamp(parsed);
11983
+ this.value.set(clamped);
11984
+ if (clamped !== parsed)
11985
+ this.onChange(clamped);
11986
+ this.text.set(String(clamped));
11987
+ }
11988
+ else {
11989
+ // Unparseable — restore the last committed value.
11990
+ this.text.set(this.value() === null ? '' : String(this.value()));
11991
+ }
11992
+ }
11993
+ this.onTouched();
11994
+ }
11995
+ /** Button / keyboard step: an empty field first lands on min (or 0). */
11996
+ stepBy(dir, factor = 1) {
11997
+ if (this.isDisabled())
11998
+ return;
11999
+ const v = this.value();
12000
+ if (v === null) {
12001
+ this.commitTo(this.min() ?? 0);
12002
+ return;
12003
+ }
12004
+ this.commitTo(v + dir * factor * this.step());
12005
+ }
12006
+ commitTo(n) {
12007
+ const clamped = this.clamp(this.snap(n));
12008
+ this.value.set(clamped);
12009
+ this.text.set(String(clamped));
12010
+ this.onChange(clamped);
12011
+ this.onTouched();
12012
+ }
12013
+ clamp(n) {
12014
+ const lo = this.min();
12015
+ const hi = this.max();
12016
+ if (lo !== null && n < lo)
12017
+ return lo;
12018
+ if (hi !== null && n > hi)
12019
+ return hi;
12020
+ return n;
12021
+ }
12022
+ /** Trim float dust from step arithmetic (0.1 + 0.2), not a grid snap. */
12023
+ snap(n) {
12024
+ const step = this.step();
12025
+ if (!Number.isFinite(step) || step <= 0)
12026
+ return n;
12027
+ const decimals = (String(step).split('.')[1] ?? '').length;
12028
+ return Number(n.toFixed(Math.min(decimals + 2, 12)));
12029
+ }
12030
+ writeValue(value) {
12031
+ const parsed = value === null || value === undefined ? null : Number(value);
12032
+ const v = parsed !== null && Number.isFinite(parsed) ? this.clamp(parsed) : null;
12033
+ this.value.set(v);
12034
+ this.text.set(v === null ? '' : String(v));
12035
+ }
12036
+ registerOnChange(fn) {
12037
+ this.onChange = fn;
12038
+ }
12039
+ registerOnTouched(fn) {
12040
+ this.onTouched = fn;
12041
+ }
12042
+ setDisabledState(isDisabled) {
12043
+ this.cvaDisabled.set(isDisabled);
12044
+ }
12045
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctNumber, deps: [], target: i0.ɵɵFactoryTarget.Component });
12046
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctNumber, isStandalone: true, selector: "strct-number", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, incrementLabel: { classPropertyName: "incrementLabel", publicName: "incrementLabel", isSignal: true, isRequired: false, transformFunction: null }, decrementLabel: { classPropertyName: "decrementLabel", publicName: "decrementLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-num" }, providers: [
12047
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctNumber), multi: true },
12048
+ ], ngImport: i0, template: `
12049
+ <div class="strct-num__wrap">
12050
+ <button
12051
+ type="button"
12052
+ class="strct-num__btn"
12053
+ [attr.aria-label]="decrementLabel()"
12054
+ [disabled]="isDisabled() || atMin()"
12055
+ (click)="stepBy(-1)"
12056
+ >
12057
+ <strct-icon strictName="minus" [size]="12" [strokeWidth]="1.8" />
12058
+ </button>
12059
+ <input
12060
+ type="text"
12061
+ inputmode="decimal"
12062
+ class="strct-control strct-num__input"
12063
+ role="spinbutton"
12064
+ [attr.aria-valuemin]="min()"
12065
+ [attr.aria-valuemax]="max()"
12066
+ [attr.aria-valuenow]="value()"
12067
+ [placeholder]="placeholder()"
12068
+ [value]="text()"
12069
+ [disabled]="isDisabled()"
12070
+ (input)="onInput($event)"
12071
+ (keydown)="onKeydown($event)"
12072
+ (blur)="onBlur()"
12073
+ />
12074
+ <button
12075
+ type="button"
12076
+ class="strct-num__btn"
12077
+ [attr.aria-label]="incrementLabel()"
12078
+ [disabled]="isDisabled() || atMax()"
12079
+ (click)="stepBy(1)"
12080
+ >
12081
+ <strct-icon strictName="plus" [size]="12" [strokeWidth]="1.8" />
12082
+ </button>
12083
+ </div>
12084
+ `, isInline: true, styles: [".strct-num{display:block;width:100%}.strct-num__wrap{display:flex;align-items:stretch;gap:var(--space-1)}.strct-num__input{flex:1;min-width:0;text-align:center;font-variant-numeric:tabular-nums}.strct-num__btn{display:inline-flex;align-items:center;justify-content:center;flex:none;padding-inline:var(--space-2);color:var(--t2);background:var(--bg-2);border:1px solid var(--b2);border-radius:var(--radius-md);cursor:pointer;transition:border-color .14s ease,color .14s ease,background .14s ease}.strct-num__btn:hover:not(:disabled){color:var(--t1);border-color:var(--b3);background:var(--bg-3)}.strct-num__btn:disabled{opacity:var(--disabled-opacity);cursor:not-allowed}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11860
12085
  }
12086
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctNumber, decorators: [{
12087
+ type: Component,
12088
+ args: [{ selector: 'strct-number', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], providers: [
12089
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctNumber), multi: true },
12090
+ ], template: `
12091
+ <div class="strct-num__wrap">
12092
+ <button
12093
+ type="button"
12094
+ class="strct-num__btn"
12095
+ [attr.aria-label]="decrementLabel()"
12096
+ [disabled]="isDisabled() || atMin()"
12097
+ (click)="stepBy(-1)"
12098
+ >
12099
+ <strct-icon strictName="minus" [size]="12" [strokeWidth]="1.8" />
12100
+ </button>
12101
+ <input
12102
+ type="text"
12103
+ inputmode="decimal"
12104
+ class="strct-control strct-num__input"
12105
+ role="spinbutton"
12106
+ [attr.aria-valuemin]="min()"
12107
+ [attr.aria-valuemax]="max()"
12108
+ [attr.aria-valuenow]="value()"
12109
+ [placeholder]="placeholder()"
12110
+ [value]="text()"
12111
+ [disabled]="isDisabled()"
12112
+ (input)="onInput($event)"
12113
+ (keydown)="onKeydown($event)"
12114
+ (blur)="onBlur()"
12115
+ />
12116
+ <button
12117
+ type="button"
12118
+ class="strct-num__btn"
12119
+ [attr.aria-label]="incrementLabel()"
12120
+ [disabled]="isDisabled() || atMax()"
12121
+ (click)="stepBy(1)"
12122
+ >
12123
+ <strct-icon strictName="plus" [size]="12" [strokeWidth]="1.8" />
12124
+ </button>
12125
+ </div>
12126
+ `, host: { class: 'strct-num' }, styles: [".strct-num{display:block;width:100%}.strct-num__wrap{display:flex;align-items:stretch;gap:var(--space-1)}.strct-num__input{flex:1;min-width:0;text-align:center;font-variant-numeric:tabular-nums}.strct-num__btn{display:inline-flex;align-items:center;justify-content:center;flex:none;padding-inline:var(--space-2);color:var(--t2);background:var(--bg-2);border:1px solid var(--b2);border-radius:var(--radius-md);cursor:pointer;transition:border-color .14s ease,color .14s ease,background .14s ease}.strct-num__btn:hover:not(:disabled){color:var(--t1);border-color:var(--b3);background:var(--bg-3)}.strct-num__btn:disabled{opacity:var(--disabled-opacity);cursor:not-allowed}\n"] }]
12127
+ }], propDecorators: { min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], incrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementLabel", required: false }] }], decrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
11861
12128
 
11862
- const DG_LABELS = {
11863
- row: 'row',
11864
- rows: 'rows',
11865
- selected: 'selected',
11866
- selectAll: 'Select all rows on this page',
11867
- selectRow: 'Select row',
11868
- selectRowFor: (id) => `Select row ${id}`,
11869
- selectAllInGroup: (group) => `Select all in ${group}`,
11870
- openDetail: 'Open detail',
11871
- toggleDetail: 'Toggle detail',
11872
- closeDetail: 'Close detail',
11873
- rowActions: 'Row actions',
11874
- chooseColumns: 'Choose columns',
11875
- refresh: 'Refresh data',
11876
- actions: 'Actions',
11877
- filterColumn: 'Filter',
11878
- filterPlaceholder: 'Filter…',
11879
- clearFilter: 'Clear filter',
11880
- editCell: 'Edit',
11881
- toggleChildren: 'Toggle children',
11882
- quickFilter: 'Quick filter',
11883
- };
11884
- let datagridCounter = 0;
11885
- /** Utility-column widths used when sticky columns are active (px). */
11886
- const UTIL_W = { detail: 36, expand: 36, sel: 40 };
11887
- /** Fallback width for a sticky column without an explicit px width. */
11888
- const STICKY_FALLBACK_W = 120;
11889
12129
  /**
11890
- * Marks the expandable-row detail template. The row is the template's implicit
11891
- * context (`let-row`, or `let-row="row"`): `<ng-template strctRowDetail let-row> … {{ row['name'] }} … </ng-template>`.
12130
+ * Minimal, dependency-free XLSX writer for the datagrid's Excel export.
12131
+ *
12132
+ * An .xlsx file is a ZIP of SpreadsheetML parts. This builds the five
12133
+ * required parts with inline strings and zips them with STORED (uncompressed)
12134
+ * entries — no runtime dependency, byte-accurate CRC32, opens in Excel,
12135
+ * LibreOffice and Google Sheets.
11892
12136
  */
11893
- class StrctRowDetailDef {
11894
- template = inject(TemplateRef);
11895
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctRowDetailDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11896
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctRowDetailDef, isStandalone: true, selector: "[strctRowDetail]", ngImport: i0 });
12137
+ const CRC_TABLE = (() => {
12138
+ const t = new Uint32Array(256);
12139
+ for (let n = 0; n < 256; n++) {
12140
+ let c = n;
12141
+ for (let k = 0; k < 8; k++)
12142
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
12143
+ t[n] = c >>> 0;
12144
+ }
12145
+ return t;
12146
+ })();
12147
+ function crc32(data) {
12148
+ let c = 0xffffffff;
12149
+ for (const byte of data)
12150
+ c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
12151
+ return (c ^ 0xffffffff) >>> 0;
11897
12152
  }
11898
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctRowDetailDef, decorators: [{
11899
- type: Directive,
11900
- args: [{ selector: '[strctRowDetail]' }]
11901
- }] });
11902
- /** Muted note inside the action bar, before the buttons. */
11903
- class StrctDatagridActionBarCaption {
11904
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBarCaption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11905
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDatagridActionBarCaption, isStandalone: true, selector: "[strctDatagridActionBarCaption]", ngImport: i0 });
12153
+ const enc = new TextEncoder();
12154
+ /** ZIP with STORED entries (no compression — spreadsheets are small). */
12155
+ function zip(entries) {
12156
+ const chunks = [];
12157
+ const central = [];
12158
+ let offset = 0;
12159
+ const u16 = (n) => new Uint8Array([n & 0xff, (n >> 8) & 0xff]);
12160
+ const u32 = (n) => new Uint8Array([n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]);
12161
+ const cat = (...parts) => {
12162
+ const total = parts.reduce((s, p) => s + p.length, 0);
12163
+ const out = new Uint8Array(total);
12164
+ let o = 0;
12165
+ for (const p of parts) {
12166
+ out.set(p, o);
12167
+ o += p.length;
12168
+ }
12169
+ return out;
12170
+ };
12171
+ for (const { name, data } of entries) {
12172
+ const nameBytes = enc.encode(name);
12173
+ const crc = crc32(data);
12174
+ const local = cat(u32(0x04034b50), u16(20), // version needed
12175
+ u16(0), // flags
12176
+ u16(0), // method: stored
12177
+ u16(0), u16(0), // mod time/date
12178
+ u32(crc), u32(data.length), u32(data.length), u16(nameBytes.length), u16(0), nameBytes, data);
12179
+ central.push(cat(u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0), u32(crc), u32(data.length), u32(data.length), u16(nameBytes.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(offset), nameBytes));
12180
+ chunks.push(local);
12181
+ offset += local.length;
12182
+ }
12183
+ const centralStart = offset;
12184
+ const centralBytes = cat(...central);
12185
+ const end = cat(u32(0x06054b50), u16(0), u16(0), u16(entries.length), u16(entries.length), u32(centralBytes.length), u32(centralStart), u16(0));
12186
+ return cat(...chunks, centralBytes, end);
11906
12187
  }
11907
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBarCaption, decorators: [{
11908
- type: Directive,
11909
- args: [{ selector: '[strctDatagridActionBarCaption]' }]
11910
- }] });
11911
- /** Marks the persistent action-bar (toolbar) content shown above the grid. */
11912
- class StrctDatagridActionBar {
11913
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBar, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11914
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDatagridActionBar, isStandalone: true, selector: "[strctDatagridActionBar]", ngImport: i0 });
12188
+ const xml = (s) => String(s ?? '')
12189
+ .replace(/&/g, '&amp;')
12190
+ .replace(/</g, '&lt;')
12191
+ .replace(/>/g, '&gt;')
12192
+ .replace(/"/g, '&quot;');
12193
+ /** A1-style column ref: 0 → A, 26 → AA … */
12194
+ function colRef(i) {
12195
+ let s = '';
12196
+ for (let n = i; n >= 0; n = Math.floor(n / 26) - 1)
12197
+ s = String.fromCharCode(65 + (n % 26)) + s;
12198
+ return s;
12199
+ }
12200
+ /**
12201
+ * Build a single-sheet workbook: `header` as a bold-free first row, then
12202
+ * `rows`. Numbers stay numeric; everything else exports as inline strings.
12203
+ */
12204
+ function buildXlsx(header, rows, sheetName = 'Data') {
12205
+ const rowXml = (cells, r) => `<row r="${r}">` +
12206
+ cells
12207
+ .map((v, c) => {
12208
+ const ref = `${colRef(c)}${r}`;
12209
+ if (typeof v === 'number' && Number.isFinite(v)) {
12210
+ return `<c r="${ref}"><v>${v}</v></c>`;
12211
+ }
12212
+ return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${xml(v)}</t></is></c>`;
12213
+ })
12214
+ .join('') +
12215
+ '</row>';
12216
+ const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
12217
+ `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>` +
12218
+ rowXml(header, 1) +
12219
+ rows.map((cells, i) => rowXml(cells, i + 2)).join('') +
12220
+ `</sheetData></worksheet>`;
12221
+ const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
12222
+ `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ` +
12223
+ `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">` +
12224
+ `<sheets><sheet name="${xml(sheetName)}" sheetId="1" r:id="rId1"/></sheets></workbook>`;
12225
+ const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
12226
+ `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
12227
+ `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>` +
12228
+ `</Relationships>`;
12229
+ const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
12230
+ `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
12231
+ `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>` +
12232
+ `</Relationships>`;
12233
+ const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
12234
+ `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
12235
+ `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
12236
+ `<Default Extension="xml" ContentType="application/xml"/>` +
12237
+ `<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` +
12238
+ `<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
12239
+ `</Types>`;
12240
+ return zip([
12241
+ { name: '[Content_Types].xml', data: enc.encode(contentTypes) },
12242
+ { name: '_rels/.rels', data: enc.encode(rootRels) },
12243
+ { name: 'xl/workbook.xml', data: enc.encode(workbook) },
12244
+ { name: 'xl/_rels/workbook.xml.rels', data: enc.encode(workbookRels) },
12245
+ { name: 'xl/worksheets/sheet1.xml', data: enc.encode(sheet) },
12246
+ ]);
12247
+ }
12248
+
12249
+ const DG_LABELS = {
12250
+ row: 'row',
12251
+ rows: 'rows',
12252
+ selected: 'selected',
12253
+ selectAll: 'Select all rows on this page',
12254
+ selectRow: 'Select row',
12255
+ selectRowFor: (id) => `Select row ${id}`,
12256
+ selectAllInGroup: (group) => `Select all in ${group}`,
12257
+ openDetail: 'Open detail',
12258
+ toggleDetail: 'Toggle detail',
12259
+ closeDetail: 'Close detail',
12260
+ rowActions: 'Row actions',
12261
+ chooseColumns: 'Choose columns',
12262
+ refresh: 'Refresh data',
12263
+ actions: 'Actions',
12264
+ filterColumn: 'Filter',
12265
+ filterPlaceholder: 'Filter…',
12266
+ clearFilter: 'Clear filter',
12267
+ editCell: 'Edit',
12268
+ toggleChildren: 'Toggle children',
12269
+ quickFilter: 'Quick filter',
12270
+ };
12271
+ let datagridCounter = 0;
12272
+ /** Utility-column widths used when sticky columns are active (px). */
12273
+ const UTIL_W = { detail: 36, expand: 36, sel: 40 };
12274
+ /** Fallback width for a sticky column without an explicit px width. */
12275
+ const STICKY_FALLBACK_W = 120;
12276
+ /**
12277
+ * Marks the expandable-row detail template. The row is the template's implicit
12278
+ * context (`let-row`, or `let-row="row"`): `<ng-template strctRowDetail let-row> … {{ row['name'] }} … </ng-template>`.
12279
+ */
12280
+ class StrctRowDetailDef {
12281
+ template = inject(TemplateRef);
12282
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctRowDetailDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12283
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctRowDetailDef, isStandalone: true, selector: "[strctRowDetail]", ngImport: i0 });
12284
+ }
12285
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctRowDetailDef, decorators: [{
12286
+ type: Directive,
12287
+ args: [{ selector: '[strctRowDetail]' }]
12288
+ }] });
12289
+ /** Muted note inside the action bar, before the buttons. */
12290
+ class StrctDatagridActionBarCaption {
12291
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBarCaption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12292
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDatagridActionBarCaption, isStandalone: true, selector: "[strctDatagridActionBarCaption]", ngImport: i0 });
12293
+ }
12294
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBarCaption, decorators: [{
12295
+ type: Directive,
12296
+ args: [{ selector: '[strctDatagridActionBarCaption]' }]
12297
+ }] });
12298
+ /** Marks the persistent action-bar (toolbar) content shown above the grid. */
12299
+ class StrctDatagridActionBar {
12300
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBar, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12301
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDatagridActionBar, isStandalone: true, selector: "[strctDatagridActionBar]", ngImport: i0 });
11915
12302
  }
11916
12303
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBar, decorators: [{
11917
12304
  type: Directive,
@@ -12103,8 +12490,9 @@ class StrctDatagrid {
12103
12490
  rowAction = output();
12104
12491
  /** Server-side data request (lazy mode): load this page / sort and set `rows`. */
12105
12492
  lazyLoad = output();
12106
- /** An editable cell was committed (Enter / blur). The grid does not mutate
12107
- * rows — apply `value` to your data and pass the updated array back in. */
12493
+ /** An editable cell was committed (Enter / blur, or choosing in a `select`
12494
+ * editor). The grid does not mutate rows — apply the value to your data and
12495
+ * pass the updated array back in. */
12108
12496
  cellEdit = output();
12109
12497
  menu = inject(StrctMenuService);
12110
12498
  detailDef = contentChild(StrctRowDetailDef, /* @ts-ignore */
@@ -12479,9 +12867,80 @@ class StrctDatagrid {
12479
12867
  const e = this.editing();
12480
12868
  return !!e && e.rowKey === this.rowKey(row) && e.colKey === col.key;
12481
12869
  }
12870
+ /** The value a typed editor holds while it is open (committed on Enter / blur). */
12871
+ editDraft = signal(null, /* @ts-ignore */
12872
+ ...(ngDevMode ? [{ debugName: "editDraft" }] : /* istanbul ignore next */ []));
12482
12873
  startEdit(row, col) {
12483
12874
  this.editing.set({ rowKey: this.rowKey(row), colKey: col.key });
12484
- setTimeout(() => this.hostRef.nativeElement.querySelector('.strct-dg__editinput')?.focus());
12875
+ this.editDraft.set(row[col.key]);
12876
+ setTimeout(() => {
12877
+ const editor = this.hostRef.nativeElement.querySelector('.strct-dg__editinput, .strct-dg__editselect, .strct-dg__editnumber');
12878
+ if (!editor)
12879
+ return;
12880
+ // The stepper buttons come first in the DOM; the value is what the user
12881
+ // came to change, so focus lands on the field itself.
12882
+ (editor.matches('input')
12883
+ ? editor
12884
+ : (editor.querySelector('input') ??
12885
+ editor.querySelector('button')))?.focus();
12886
+ });
12887
+ }
12888
+ /** A select column reads as its option's label, not as the stored value. */
12889
+ displayValue(row, col) {
12890
+ const raw = row[col.key];
12891
+ if (col.editor !== 'select')
12892
+ return raw;
12893
+ return col.editorOptions?.find((o) => o.value === raw)?.label ?? raw;
12894
+ }
12895
+ /** Close the editor and emit `cellEdit` when the value actually changed. */
12896
+ commitValue(row, col, raw) {
12897
+ if (!this.isEditingCell(row, col))
12898
+ return;
12899
+ this.editing.set(null);
12900
+ const value = this.clampToColumn(col, raw);
12901
+ const previous = row[col.key];
12902
+ if (previous === value)
12903
+ return;
12904
+ this.cellEdit.emit({
12905
+ row,
12906
+ column: col,
12907
+ value: value == null ? '' : String(value),
12908
+ typedValue: value,
12909
+ previous,
12910
+ });
12911
+ }
12912
+ /** Enter / blur on a typed editor commits what it currently holds. */
12913
+ commitDraft(row, col, event) {
12914
+ event?.stopPropagation();
12915
+ this.commitValue(row, col, this.editDraft());
12916
+ }
12917
+ /**
12918
+ * A column that declares bounds means them: Enter commits what the field
12919
+ * holds, and `strct-number` only clamps on blur, so the grid clamps here
12920
+ * rather than letting an out-of-range value reach `cellEdit`.
12921
+ */
12922
+ clampToColumn(col, value) {
12923
+ if (col.editor !== 'number' || typeof value !== 'number' || !Number.isFinite(value))
12924
+ return value;
12925
+ const min = col.editorMin;
12926
+ const max = col.editorMax;
12927
+ let v = value;
12928
+ if (min != null && v < min)
12929
+ v = min;
12930
+ if (max != null && v > max)
12931
+ v = max;
12932
+ return v;
12933
+ }
12934
+ /**
12935
+ * A number editor is a field between two stepper buttons, so focus moves
12936
+ * inside it — only a move that actually leaves the editor is a blur.
12937
+ */
12938
+ commitOnLeave(row, col, event) {
12939
+ const editor = event.currentTarget;
12940
+ const next = event.relatedTarget;
12941
+ if (next instanceof Node && editor.contains(next))
12942
+ return;
12943
+ this.commitDraft(row, col, event);
12485
12944
  }
12486
12945
  commitEdit(row, col, event) {
12487
12946
  if (!this.isEditingCell(row, col))
@@ -12490,7 +12949,7 @@ class StrctDatagrid {
12490
12949
  this.editing.set(null);
12491
12950
  const previous = row[col.key];
12492
12951
  if (String(previous ?? '') !== value)
12493
- this.cellEdit.emit({ row, column: col, value, previous });
12952
+ this.cellEdit.emit({ row, column: col, value, typedValue: value, previous });
12494
12953
  }
12495
12954
  cancelEdit(event) {
12496
12955
  event?.stopPropagation();
@@ -13603,16 +14062,47 @@ class StrctDatagrid {
13603
14062
  }
13604
14063
  }
13605
14064
  @if (isEditingCell(row, col)) {
13606
- <input
13607
- class="strct-dg__editinput"
13608
- type="text"
13609
- [value]="row[col.key] ?? ''"
13610
- [attr.aria-label]="L().editCell + ': ' + col.label"
13611
- (keydown.enter)="commitEdit(row, col, $event)"
13612
- (keydown.escape)="cancelEdit($event)"
13613
- (blur)="commitEdit(row, col, $event)"
13614
- (click)="$event.stopPropagation()"
13615
- />
14065
+ @switch (col.editor ?? 'text') {
14066
+ @case ('select') {
14067
+ <!-- Choosing is the commit: there is nothing to
14068
+ confirm with Enter once an option is picked. -->
14069
+ <strct-select
14070
+ class="strct-dg__editselect"
14071
+ [options]="col.editorOptions ?? []"
14072
+ [listLabel]="L().editCell + ': ' + col.label"
14073
+ [ngModel]="row[col.key]"
14074
+ (ngModelChange)="commitValue(row, col, $event)"
14075
+ (keydown.escape)="cancelEdit($event)"
14076
+ (click)="$event.stopPropagation()"
14077
+ />
14078
+ }
14079
+ @case ('number') {
14080
+ <strct-number
14081
+ class="strct-dg__editnumber"
14082
+ [min]="col.editorMin ?? null"
14083
+ [max]="col.editorMax ?? null"
14084
+ [step]="col.editorStep ?? 1"
14085
+ [ngModel]="row[col.key]"
14086
+ (ngModelChange)="editDraft.set($event)"
14087
+ (keydown.enter)="commitDraft(row, col, $event)"
14088
+ (keydown.escape)="cancelEdit($event)"
14089
+ (focusout)="commitOnLeave(row, col, $event)"
14090
+ (click)="$event.stopPropagation()"
14091
+ />
14092
+ }
14093
+ @default {
14094
+ <input
14095
+ class="strct-dg__editinput"
14096
+ type="text"
14097
+ [value]="row[col.key] ?? ''"
14098
+ [attr.aria-label]="L().editCell + ': ' + col.label"
14099
+ (keydown.enter)="commitEdit(row, col, $event)"
14100
+ (keydown.escape)="cancelEdit($event)"
14101
+ (blur)="commitEdit(row, col, $event)"
14102
+ (click)="$event.stopPropagation()"
14103
+ />
14104
+ }
14105
+ }
13616
14106
  } @else if (cellTemplate(col.key); as tpl) {
13617
14107
  <ng-container
13618
14108
  [ngTemplateOutlet]="tpl"
@@ -13624,7 +14114,7 @@ class StrctDatagrid {
13624
14114
  }"
13625
14115
  />
13626
14116
  } @else {
13627
- {{ row[col.key] }}
14117
+ {{ displayValue(row, col) }}
13628
14118
  }
13629
14119
  </td>
13630
14120
  }
@@ -13759,7 +14249,7 @@ class StrctDatagrid {
13759
14249
  </div>
13760
14250
  </div>
13761
14251
  }
13762
- `, isInline: true, styles: [".strct-dg__radio{width:15px;height:15px;accent-color:var(--acc);cursor:pointer}.strct-dg__radio:disabled{cursor:not-allowed}.strct-dg__row--locked .strct-dg__sel{opacity:.55}.strct-dg__actioncaption:empty{display:none}.strct-dg__actioncaption{display:inline-flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);color:var(--t3)}.strct-dg-host{display:block;border:1px solid var(--b2);border-radius:var(--radius-lg);background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:var(--z-raised)}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:calc(var(--z-base) + 2)}.strct-dg thead th.strct-dg__cell--sticky{z-index:calc(var(--z-raised) + 1)}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2);white-space:nowrap;-webkit-user-select:none;user-select:none}.strct-dg__th--sortable{cursor:pointer}.strct-dg__th--sortable:hover{color:var(--t1)}.strct-dg__th--sortable:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__hd{display:inline-flex;align-items:center;gap:5px}.strct-dg__sorticon{color:var(--t3)}.strct-dg__th--sortable:hover .strct-dg__sorticon{color:var(--acc)}.strct-dg__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:calc(var(--z-base) + 1)}.strct-dg__resize:hover{background:var(--acc)}.strct-dg td{color:var(--t1)}.strct-dg tbody tr:last-child td{border-bottom:0}.strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:var(--acc-s)}.strct-dg__row--selected td{background:var(--acc-m)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel input{accent-color:var(--acc);width:15px;height:15px;cursor:pointer}.strct-dg__expandcol,.strct-dg__expandcell{width:1%;white-space:nowrap}.strct-dg__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:transform .15s ease,color .15s ease}.strct-dg__expandbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn--open{transform:rotate(90deg);color:var(--acc)}.strct-dg__detailbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__detailbtn:hover{color:var(--acc);background:var(--bg-3)}.strct-dg__detailbtn--active{color:var(--acc);background:var(--acc-m)}.strct-dg__detailrow td{background:var(--bg-2);padding:0}.strct-dg__detail{padding:14px 16px;font-size:13px;color:var(--t2)}.strct-dg__layout{display:flex;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-start-end-radius:0;border-end-end-radius:0}.strct-dg__row--clickable{cursor:pointer}.strct-dg__row--active td{background:var(--acc-m)}.strct-dg__layout--paned .strct-dg__row--active td:last-child{position:relative;padding-inline-end:26px}.strct-dg__layout--paned .strct-dg__row--active td:last-child:after{content:\"\";position:absolute;inset-inline-end:11px;top:50%;width:6px;height:6px;border-top:1.6px solid var(--acc);border-inline-end:1.6px solid var(--acc);transform:translateY(-50%) rotate(45deg)}.strct-dg__pane{flex:1;min-width:0;align-self:stretch;background:var(--bg-1);border:1px solid var(--b2);border-inline-start:2px solid var(--acc);border-radius:8px;border-start-start-radius:0;border-end-start-radius:0;overflow:hidden;animation:strct-dg-pane-in .14s ease}.strct-dg__pane-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--b1);font-size:13px;font-weight:600;color:var(--t1)}.strct-dg__pane-close{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer}.strct-dg__pane-close:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__pane-body{padding:14px 16px;font-size:13px;color:var(--t2)}@keyframes strct-dg-pane-in{0%{opacity:0;transform:translate(8px)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:200% 100%;animation:strct-dg-skeleton-sweep 1.1s linear infinite}@keyframes strct-dg-skeleton-sweep{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.strct-dg__skeleton-block{animation:none;background:var(--skeleton-hi, var(--acc18))}}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t3);padding:22px}.strct-dg__quickfilter{min-width:180px;max-width:260px}.strct-dg__quickfilter--end{order:2;margin-inline-start:auto}.strct-dg__filternote--end{order:3}.strct-dg__filternote{font-size:11.5px;color:var(--t3);font-variant-numeric:tabular-nums;white-space:nowrap}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);inset-inline-start:0;z-index:var(--z-sticky);min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:calc(var(--z-dropdown) + 10);display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg tbody tr[tabindex]:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctPagination, selector: "strct-pagination", inputs: ["prevLabel", "nextLabel", "regionLabel", "pageLabel", "total", "pageSize", "page"], outputs: ["pageChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: StrctCheckbox, selector: "strct-checkbox", inputs: ["checked", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange"] }, { kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctButtonGroup, selector: "strct-button-group" }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }, { kind: "component", type: StrctSearchbox, selector: "strct-searchbox", inputs: ["placeholder", "hint", "trigger", "clearable", "ariaLabel", "clearLabel", "value"], outputs: ["valueChange", "search", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14252
+ `, isInline: true, styles: [".strct-dg__radio{width:15px;height:15px;accent-color:var(--acc);cursor:pointer}.strct-dg__radio:disabled{cursor:not-allowed}.strct-dg__row--locked .strct-dg__sel{opacity:.55}.strct-dg__actioncaption:empty{display:none}.strct-dg__actioncaption{display:inline-flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);color:var(--t3)}.strct-dg-host{display:block;border:1px solid var(--b2);border-radius:var(--radius-lg);background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:var(--z-raised)}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:calc(var(--z-base) + 2)}.strct-dg thead th.strct-dg__cell--sticky{z-index:calc(var(--z-raised) + 1)}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2);white-space:nowrap;-webkit-user-select:none;user-select:none}.strct-dg__th--sortable{cursor:pointer}.strct-dg__th--sortable:hover{color:var(--t1)}.strct-dg__th--sortable:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__hd{display:inline-flex;align-items:center;gap:5px}.strct-dg__sorticon{color:var(--t3)}.strct-dg__th--sortable:hover .strct-dg__sorticon{color:var(--acc)}.strct-dg__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:calc(var(--z-base) + 1)}.strct-dg__resize:hover{background:var(--acc)}.strct-dg td{color:var(--t1)}.strct-dg tbody tr:last-child td{border-bottom:0}.strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:var(--acc-s)}.strct-dg__row--selected td{background:var(--acc-m)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel input{accent-color:var(--acc);width:15px;height:15px;cursor:pointer}.strct-dg__expandcol,.strct-dg__expandcell{width:1%;white-space:nowrap}.strct-dg__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:transform .15s ease,color .15s ease}.strct-dg__expandbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn--open{transform:rotate(90deg);color:var(--acc)}.strct-dg__detailbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__detailbtn:hover{color:var(--acc);background:var(--bg-3)}.strct-dg__detailbtn--active{color:var(--acc);background:var(--acc-m)}.strct-dg__detailrow td{background:var(--bg-2);padding:0}.strct-dg__detail{padding:14px 16px;font-size:13px;color:var(--t2)}.strct-dg__layout{display:flex;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-start-end-radius:0;border-end-end-radius:0}.strct-dg__row--clickable{cursor:pointer}.strct-dg__row--active td{background:var(--acc-m)}.strct-dg__layout--paned .strct-dg__row--active td:last-child{position:relative;padding-inline-end:26px}.strct-dg__layout--paned .strct-dg__row--active td:last-child:after{content:\"\";position:absolute;inset-inline-end:11px;top:50%;width:6px;height:6px;border-top:1.6px solid var(--acc);border-inline-end:1.6px solid var(--acc);transform:translateY(-50%) rotate(45deg)}.strct-dg__pane{flex:1;min-width:0;align-self:stretch;background:var(--bg-1);border:1px solid var(--b2);border-inline-start:2px solid var(--acc);border-radius:8px;border-start-start-radius:0;border-end-start-radius:0;overflow:hidden;animation:strct-dg-pane-in .14s ease}.strct-dg__pane-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--b1);font-size:13px;font-weight:600;color:var(--t1)}.strct-dg__pane-close{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer}.strct-dg__pane-close:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__pane-body{padding:14px 16px;font-size:13px;color:var(--t2)}@keyframes strct-dg-pane-in{0%{opacity:0;transform:translate(8px)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:200% 100%;animation:strct-dg-skeleton-sweep 1.1s linear infinite}@keyframes strct-dg-skeleton-sweep{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.strct-dg__skeleton-block{animation:none;background:var(--skeleton-hi, var(--acc18))}}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t3);padding:22px}.strct-dg__quickfilter{min-width:180px;max-width:260px}.strct-dg__quickfilter--end{order:2;margin-inline-start:auto}.strct-dg__filternote--end{order:3}.strct-dg__filternote{font-size:11.5px;color:var(--t3);font-variant-numeric:tabular-nums;white-space:nowrap}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);inset-inline-start:0;z-index:var(--z-sticky);min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:calc(var(--z-dropdown) + 10);display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg tbody tr[tabindex]:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editselect,.strct-dg__editnumber{display:block;width:100%}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctPagination, selector: "strct-pagination", inputs: ["prevLabel", "nextLabel", "regionLabel", "pageLabel", "total", "pageSize", "page"], outputs: ["pageChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: StrctCheckbox, selector: "strct-checkbox", inputs: ["checked", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange"] }, { kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctButtonGroup, selector: "strct-button-group" }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }, { kind: "component", type: StrctSearchbox, selector: "strct-searchbox", inputs: ["placeholder", "hint", "trigger", "clearable", "ariaLabel", "clearLabel", "value"], outputs: ["valueChange", "search", "activated"] }, { kind: "component", type: StrctSelect, selector: "strct-select", inputs: ["options", "placeholder", "listLabel", "emptyText", "disabled", "compareWith"] }, { kind: "component", type: StrctNumber, selector: "strct-number", inputs: ["min", "max", "step", "placeholder", "incrementLabel", "decrementLabel", "disabled"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
13763
14253
  }
13764
14254
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagrid, decorators: [{
13765
14255
  type: Component,
@@ -13772,6 +14262,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
13772
14262
  StrctButtonGroup,
13773
14263
  StrctOverlay,
13774
14264
  StrctSearchbox,
14265
+ StrctSelect,
14266
+ StrctNumber,
14267
+ FormsModule,
13775
14268
  ], template: `
13776
14269
  @if (actionBarDef() || quickFilterable()) {
13777
14270
  <div class="strct-dg__toolbar">
@@ -14136,16 +14629,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
14136
14629
  }
14137
14630
  }
14138
14631
  @if (isEditingCell(row, col)) {
14139
- <input
14140
- class="strct-dg__editinput"
14141
- type="text"
14142
- [value]="row[col.key] ?? ''"
14143
- [attr.aria-label]="L().editCell + ': ' + col.label"
14144
- (keydown.enter)="commitEdit(row, col, $event)"
14145
- (keydown.escape)="cancelEdit($event)"
14146
- (blur)="commitEdit(row, col, $event)"
14147
- (click)="$event.stopPropagation()"
14148
- />
14632
+ @switch (col.editor ?? 'text') {
14633
+ @case ('select') {
14634
+ <!-- Choosing is the commit: there is nothing to
14635
+ confirm with Enter once an option is picked. -->
14636
+ <strct-select
14637
+ class="strct-dg__editselect"
14638
+ [options]="col.editorOptions ?? []"
14639
+ [listLabel]="L().editCell + ': ' + col.label"
14640
+ [ngModel]="row[col.key]"
14641
+ (ngModelChange)="commitValue(row, col, $event)"
14642
+ (keydown.escape)="cancelEdit($event)"
14643
+ (click)="$event.stopPropagation()"
14644
+ />
14645
+ }
14646
+ @case ('number') {
14647
+ <strct-number
14648
+ class="strct-dg__editnumber"
14649
+ [min]="col.editorMin ?? null"
14650
+ [max]="col.editorMax ?? null"
14651
+ [step]="col.editorStep ?? 1"
14652
+ [ngModel]="row[col.key]"
14653
+ (ngModelChange)="editDraft.set($event)"
14654
+ (keydown.enter)="commitDraft(row, col, $event)"
14655
+ (keydown.escape)="cancelEdit($event)"
14656
+ (focusout)="commitOnLeave(row, col, $event)"
14657
+ (click)="$event.stopPropagation()"
14658
+ />
14659
+ }
14660
+ @default {
14661
+ <input
14662
+ class="strct-dg__editinput"
14663
+ type="text"
14664
+ [value]="row[col.key] ?? ''"
14665
+ [attr.aria-label]="L().editCell + ': ' + col.label"
14666
+ (keydown.enter)="commitEdit(row, col, $event)"
14667
+ (keydown.escape)="cancelEdit($event)"
14668
+ (blur)="commitEdit(row, col, $event)"
14669
+ (click)="$event.stopPropagation()"
14670
+ />
14671
+ }
14672
+ }
14149
14673
  } @else if (cellTemplate(col.key); as tpl) {
14150
14674
  <ng-container
14151
14675
  [ngTemplateOutlet]="tpl"
@@ -14157,7 +14681,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
14157
14681
  }"
14158
14682
  />
14159
14683
  } @else {
14160
- {{ row[col.key] }}
14684
+ {{ displayValue(row, col) }}
14161
14685
  }
14162
14686
  </td>
14163
14687
  }
@@ -14298,7 +14822,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
14298
14822
  '[class.strct-dg-host--singleline]': 'singleLine()',
14299
14823
  '[class.strct-dg-host--virtual]': 'virtual()',
14300
14824
  '[class.strct-dg-host--sticky]': 'stickyActive()',
14301
- }, styles: [".strct-dg__radio{width:15px;height:15px;accent-color:var(--acc);cursor:pointer}.strct-dg__radio:disabled{cursor:not-allowed}.strct-dg__row--locked .strct-dg__sel{opacity:.55}.strct-dg__actioncaption:empty{display:none}.strct-dg__actioncaption{display:inline-flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);color:var(--t3)}.strct-dg-host{display:block;border:1px solid var(--b2);border-radius:var(--radius-lg);background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:var(--z-raised)}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:calc(var(--z-base) + 2)}.strct-dg thead th.strct-dg__cell--sticky{z-index:calc(var(--z-raised) + 1)}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2);white-space:nowrap;-webkit-user-select:none;user-select:none}.strct-dg__th--sortable{cursor:pointer}.strct-dg__th--sortable:hover{color:var(--t1)}.strct-dg__th--sortable:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__hd{display:inline-flex;align-items:center;gap:5px}.strct-dg__sorticon{color:var(--t3)}.strct-dg__th--sortable:hover .strct-dg__sorticon{color:var(--acc)}.strct-dg__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:calc(var(--z-base) + 1)}.strct-dg__resize:hover{background:var(--acc)}.strct-dg td{color:var(--t1)}.strct-dg tbody tr:last-child td{border-bottom:0}.strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:var(--acc-s)}.strct-dg__row--selected td{background:var(--acc-m)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel input{accent-color:var(--acc);width:15px;height:15px;cursor:pointer}.strct-dg__expandcol,.strct-dg__expandcell{width:1%;white-space:nowrap}.strct-dg__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:transform .15s ease,color .15s ease}.strct-dg__expandbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn--open{transform:rotate(90deg);color:var(--acc)}.strct-dg__detailbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__detailbtn:hover{color:var(--acc);background:var(--bg-3)}.strct-dg__detailbtn--active{color:var(--acc);background:var(--acc-m)}.strct-dg__detailrow td{background:var(--bg-2);padding:0}.strct-dg__detail{padding:14px 16px;font-size:13px;color:var(--t2)}.strct-dg__layout{display:flex;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-start-end-radius:0;border-end-end-radius:0}.strct-dg__row--clickable{cursor:pointer}.strct-dg__row--active td{background:var(--acc-m)}.strct-dg__layout--paned .strct-dg__row--active td:last-child{position:relative;padding-inline-end:26px}.strct-dg__layout--paned .strct-dg__row--active td:last-child:after{content:\"\";position:absolute;inset-inline-end:11px;top:50%;width:6px;height:6px;border-top:1.6px solid var(--acc);border-inline-end:1.6px solid var(--acc);transform:translateY(-50%) rotate(45deg)}.strct-dg__pane{flex:1;min-width:0;align-self:stretch;background:var(--bg-1);border:1px solid var(--b2);border-inline-start:2px solid var(--acc);border-radius:8px;border-start-start-radius:0;border-end-start-radius:0;overflow:hidden;animation:strct-dg-pane-in .14s ease}.strct-dg__pane-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--b1);font-size:13px;font-weight:600;color:var(--t1)}.strct-dg__pane-close{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer}.strct-dg__pane-close:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__pane-body{padding:14px 16px;font-size:13px;color:var(--t2)}@keyframes strct-dg-pane-in{0%{opacity:0;transform:translate(8px)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:200% 100%;animation:strct-dg-skeleton-sweep 1.1s linear infinite}@keyframes strct-dg-skeleton-sweep{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.strct-dg__skeleton-block{animation:none;background:var(--skeleton-hi, var(--acc18))}}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t3);padding:22px}.strct-dg__quickfilter{min-width:180px;max-width:260px}.strct-dg__quickfilter--end{order:2;margin-inline-start:auto}.strct-dg__filternote--end{order:3}.strct-dg__filternote{font-size:11.5px;color:var(--t3);font-variant-numeric:tabular-nums;white-space:nowrap}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);inset-inline-start:0;z-index:var(--z-sticky);min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:calc(var(--z-dropdown) + 10);display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg tbody tr[tabindex]:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"] }]
14825
+ }, styles: [".strct-dg__radio{width:15px;height:15px;accent-color:var(--acc);cursor:pointer}.strct-dg__radio:disabled{cursor:not-allowed}.strct-dg__row--locked .strct-dg__sel{opacity:.55}.strct-dg__actioncaption:empty{display:none}.strct-dg__actioncaption{display:inline-flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);color:var(--t3)}.strct-dg-host{display:block;border:1px solid var(--b2);border-radius:var(--radius-lg);background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:var(--z-raised)}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:calc(var(--z-base) + 2)}.strct-dg thead th.strct-dg__cell--sticky{z-index:calc(var(--z-raised) + 1)}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2);white-space:nowrap;-webkit-user-select:none;user-select:none}.strct-dg__th--sortable{cursor:pointer}.strct-dg__th--sortable:hover{color:var(--t1)}.strct-dg__th--sortable:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__hd{display:inline-flex;align-items:center;gap:5px}.strct-dg__sorticon{color:var(--t3)}.strct-dg__th--sortable:hover .strct-dg__sorticon{color:var(--acc)}.strct-dg__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:calc(var(--z-base) + 1)}.strct-dg__resize:hover{background:var(--acc)}.strct-dg td{color:var(--t1)}.strct-dg tbody tr:last-child td{border-bottom:0}.strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:var(--acc-s)}.strct-dg__row--selected td{background:var(--acc-m)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel input{accent-color:var(--acc);width:15px;height:15px;cursor:pointer}.strct-dg__expandcol,.strct-dg__expandcell{width:1%;white-space:nowrap}.strct-dg__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:transform .15s ease,color .15s ease}.strct-dg__expandbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn--open{transform:rotate(90deg);color:var(--acc)}.strct-dg__detailbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__detailbtn:hover{color:var(--acc);background:var(--bg-3)}.strct-dg__detailbtn--active{color:var(--acc);background:var(--acc-m)}.strct-dg__detailrow td{background:var(--bg-2);padding:0}.strct-dg__detail{padding:14px 16px;font-size:13px;color:var(--t2)}.strct-dg__layout{display:flex;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-start-end-radius:0;border-end-end-radius:0}.strct-dg__row--clickable{cursor:pointer}.strct-dg__row--active td{background:var(--acc-m)}.strct-dg__layout--paned .strct-dg__row--active td:last-child{position:relative;padding-inline-end:26px}.strct-dg__layout--paned .strct-dg__row--active td:last-child:after{content:\"\";position:absolute;inset-inline-end:11px;top:50%;width:6px;height:6px;border-top:1.6px solid var(--acc);border-inline-end:1.6px solid var(--acc);transform:translateY(-50%) rotate(45deg)}.strct-dg__pane{flex:1;min-width:0;align-self:stretch;background:var(--bg-1);border:1px solid var(--b2);border-inline-start:2px solid var(--acc);border-radius:8px;border-start-start-radius:0;border-end-start-radius:0;overflow:hidden;animation:strct-dg-pane-in .14s ease}.strct-dg__pane-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--b1);font-size:13px;font-weight:600;color:var(--t1)}.strct-dg__pane-close{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer}.strct-dg__pane-close:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__pane-body{padding:14px 16px;font-size:13px;color:var(--t2)}@keyframes strct-dg-pane-in{0%{opacity:0;transform:translate(8px)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:200% 100%;animation:strct-dg-skeleton-sweep 1.1s linear infinite}@keyframes strct-dg-skeleton-sweep{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.strct-dg__skeleton-block{animation:none;background:var(--skeleton-hi, var(--acc18))}}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t3);padding:22px}.strct-dg__quickfilter{min-width:180px;max-width:260px}.strct-dg__quickfilter--end{order:2;margin-inline-start:auto}.strct-dg__filternote--end{order:3}.strct-dg__filternote{font-size:11.5px;color:var(--t3);font-variant-numeric:tabular-nums;white-space:nowrap}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);inset-inline-start:0;z-index:var(--z-sticky);min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:calc(var(--z-dropdown) + 10);display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg tbody tr[tabindex]:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editselect,.strct-dg__editnumber{display:block;width:100%}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"] }]
14302
14826
  }], ctorParameters: () => [], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], detailPane: [{ type: i0.Input, args: [{ isSignal: true, alias: "detailPane", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], singleLine: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleLine", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], resizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizable", required: false }] }], columnChooser: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnChooser", required: false }] }], sync: [{ type: i0.Input, args: [{ isSignal: true, alias: "sync", required: false }] }], footerActionsDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerActionsDisabled", required: false }] }], rowId: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowId", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selectedId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedId", required: false }] }, { type: i0.Output, args: ["selectedIdChange"] }], rowSelectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowSelectable", required: false }] }], groupSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupSelect", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], initialSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialSelection", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], virtual: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtual", required: false }] }], viewportHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportHeight", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], stateKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateKey", required: false }] }], columnState: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnState", required: false }] }, { type: i0.Output, args: ["columnStateChange"] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }], quickFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickFilter", required: false }] }, { type: i0.Output, args: ["quickFilterChange"] }], quickFilterFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickFilterFields", required: false }] }], quickFilterable: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickFilterable", required: false }] }], quickFilterAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickFilterAlign", required: false }] }], childrenKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "childrenKey", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], syncChange: [{ type: i0.Output, args: ["syncChange"] }], rowAction: [{ type: i0.Output, args: ["rowAction"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], cellEdit: [{ type: i0.Output, args: ["cellEdit"] }], detailDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctRowDetailDef), { isSignal: true }] }], actionBarDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctDatagridActionBar), { isSignal: true }] }], cellDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctCellDef), { isSignal: true }] }], onDocClickOverlays: [{
14303
14827
  type: HostListener,
14304
14828
  args: ['document:click', ['$event']]
@@ -19699,1441 +20223,1167 @@ class StrctTour {
19699
20223
  const w = 300;
19700
20224
  const h = 170;
19701
20225
  if (!r) {
19702
- return {
19703
- top: Math.max(20, window.innerHeight / 2 - h / 2),
19704
- left: Math.max(20, window.innerWidth / 2 - w / 2),
19705
- };
19706
- }
19707
- // Below the target if it fits, else above; clamped into the viewport.
19708
- let top = r.top + r.height + 14;
19709
- if (top + h > window.innerHeight - 12)
19710
- top = Math.max(12, r.top - h - 14);
19711
- const left = Math.min(Math.max(12, r.left), window.innerWidth - w - 12);
19712
- return { top, left };
19713
- }, /* @ts-ignore */
19714
- ...(ngDevMode ? [{ debugName: "cardPos" }] : /* istanbul ignore next */ []));
19715
- constructor() {
19716
- // Restart from step 0 on open; scroll the first target into view.
19717
- effect(() => {
19718
- if (this.open()) {
19719
- this.previousActive = saveFocusedElement();
19720
- this.index.set(0);
19721
- this.scrollToTarget();
19722
- this.focusNextButton();
19723
- }
19724
- else {
19725
- restoreFocus(this.previousActive);
19726
- this.previousActive = null;
19727
- }
19728
- });
19729
- }
19730
- next() {
19731
- if (this.index() >= this.steps().length - 1) {
19732
- this.open.set(false);
19733
- this.finished.emit();
19734
- return;
19735
- }
19736
- this.index.update((i) => i + 1);
19737
- this.scrollToTarget();
19738
- this.focusNextButton();
19739
- }
19740
- prev() {
19741
- this.index.update((i) => Math.max(0, i - 1));
19742
- this.scrollToTarget();
19743
- this.focusNextButton();
19744
- }
19745
- close() {
19746
- this.open.set(false);
19747
- this.dismissed.emit();
19748
- }
19749
- /** Wrap Tab focus within the card. */
19750
- onTab(event) {
19751
- const card = this.card();
19752
- if (card)
19753
- keepTabInside(event, card);
19754
- }
19755
- /** Escape closes; stopPropagation so a host modal/drawer does not also close. */
19756
- onEscapeKey(event) {
19757
- event.stopPropagation();
19758
- this.close();
19759
- }
19760
- /**
19761
- * Arrow keys step through the tour, mirrored under RTL. No shared direction
19762
- * helper exists (other components handle RTL in CSS only), so read the
19763
- * card's computed direction here.
19764
- */
19765
- onArrow(event, dir) {
19766
- event.preventDefault();
19767
- const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
19768
- if ((dir === 'right') !== rtl)
19769
- this.next();
19770
- else
19771
- this.prev();
19772
- }
19773
- /** Home/End jump to the first/last step. */
19774
- onHomeEnd(event, last) {
19775
- event.preventDefault();
19776
- this.index.set(last ? this.steps().length - 1 : 0);
19777
- this.scrollToTarget();
19778
- this.focusNextButton();
19779
- }
19780
- scrollToTarget() {
19781
- setTimeout(() => {
19782
- const sel = this.steps()[this.index()]?.target;
19783
- if (sel)
19784
- document.querySelector(sel)?.scrollIntoView?.({ block: 'center' });
19785
- this.measureTick.update((n) => n + 1);
19786
- });
19787
- }
19788
- card() {
19789
- return this.elementRef.nativeElement.querySelector('.strct-tour__card');
19790
- }
19791
- /** Move focus to the step's primary (Next/Done) action once the card has rendered. */
19792
- focusNextButton() {
19793
- setTimeout(() => {
19794
- if (!this.open())
19795
- return;
19796
- const card = this.card();
19797
- if (!card)
19798
- return;
19799
- const nav = card.querySelectorAll('.strct-tour__nav button');
19800
- const primary = nav[nav.length - 1];
19801
- if (primary)
19802
- primary.focus();
19803
- else
19804
- focusFirstIn(card);
19805
- });
19806
- }
19807
- onReflow() {
19808
- if (this.open())
19809
- this.measureTick.update((n) => n + 1);
19810
- }
19811
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
19812
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctTour, isStandalone: true, selector: "strct-tour", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: true, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, skipLabel: { classPropertyName: "skipLabel", publicName: "skipLabel", isSignal: true, isRequired: false, transformFunction: null }, doneLabel: { classPropertyName: "doneLabel", publicName: "doneLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", finished: "finished", dismissed: "dismissed" }, host: { listeners: { "window:resize": "onReflow()", "window:scroll": "onReflow()" } }, ngImport: i0, template: `
19813
- @if (open() && current(); as step) {
19814
- <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
19815
- <div
19816
- class="strct-tour__backdrop"
19817
- [class.strct-tour__backdrop--dim]="!spot()"
19818
- (click)="close()"
19819
- ></div>
19820
- @if (spot(); as r) {
19821
- <div
19822
- class="strct-tour__ring"
19823
- [style.top.px]="r.top - 6"
19824
- [style.left.px]="r.left - 6"
19825
- [style.width.px]="r.width + 12"
19826
- [style.height.px]="r.height + 12"
19827
- ></div>
19828
- }
19829
- <div
19830
- class="strct-tour__card"
19831
- role="dialog"
19832
- [attr.aria-labelledby]="titleId"
19833
- [style.top.px]="cardPos().top"
19834
- [style.left.px]="cardPos().left"
19835
- (keydown.tab)="onTab($event)"
19836
- (keydown.shift.tab)="onTab($event)"
19837
- (keydown.escape)="onEscapeKey($event)"
19838
- (keydown.arrowleft)="onArrow($event, 'left')"
19839
- (keydown.arrowright)="onArrow($event, 'right')"
19840
- (keydown.home)="onHomeEnd($event, false)"
19841
- (keydown.end)="onHomeEnd($event, true)"
19842
- >
19843
- <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
19844
- <div class="strct-tour__title" [id]="titleId">{{ step.title }}</div>
19845
- <div class="strct-tour__body">{{ step.body }}</div>
19846
- <div class="strct-tour__nav">
19847
- <button strct-button size="sm" variant="flat" (click)="close()">
19848
- {{ skipLabel() }}
19849
- </button>
19850
- <span class="strct-tour__grow"></span>
19851
- @if (index() > 0) {
19852
- <button strct-button size="sm" variant="neutral" (click)="prev()">
19853
- {{ backLabel() }}
19854
- </button>
19855
- }
19856
- <button strct-button size="sm" variant="primary" solid (click)="next()">
19857
- {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
19858
- </button>
19859
- </div>
19860
- </div>
19861
- }
19862
- `, isInline: true, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:transparent}.strct-tour__backdrop--dim{background:var(--backdrop)}.strct-tour__ring{position:fixed;z-index:calc(var(--z-tour) + 1);border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px var(--backdrop);pointer-events:none}.strct-tour__card{position:fixed;z-index:calc(var(--z-tour) + 2);width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
19863
- }
19864
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTour, decorators: [{
19865
- type: Component,
19866
- args: [{ selector: 'strct-tour', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton], template: `
19867
- @if (open() && current(); as step) {
19868
- <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
19869
- <div
19870
- class="strct-tour__backdrop"
19871
- [class.strct-tour__backdrop--dim]="!spot()"
19872
- (click)="close()"
19873
- ></div>
19874
- @if (spot(); as r) {
19875
- <div
19876
- class="strct-tour__ring"
19877
- [style.top.px]="r.top - 6"
19878
- [style.left.px]="r.left - 6"
19879
- [style.width.px]="r.width + 12"
19880
- [style.height.px]="r.height + 12"
19881
- ></div>
19882
- }
19883
- <div
19884
- class="strct-tour__card"
19885
- role="dialog"
19886
- [attr.aria-labelledby]="titleId"
19887
- [style.top.px]="cardPos().top"
19888
- [style.left.px]="cardPos().left"
19889
- (keydown.tab)="onTab($event)"
19890
- (keydown.shift.tab)="onTab($event)"
19891
- (keydown.escape)="onEscapeKey($event)"
19892
- (keydown.arrowleft)="onArrow($event, 'left')"
19893
- (keydown.arrowright)="onArrow($event, 'right')"
19894
- (keydown.home)="onHomeEnd($event, false)"
19895
- (keydown.end)="onHomeEnd($event, true)"
19896
- >
19897
- <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
19898
- <div class="strct-tour__title" [id]="titleId">{{ step.title }}</div>
19899
- <div class="strct-tour__body">{{ step.body }}</div>
19900
- <div class="strct-tour__nav">
19901
- <button strct-button size="sm" variant="flat" (click)="close()">
19902
- {{ skipLabel() }}
19903
- </button>
19904
- <span class="strct-tour__grow"></span>
19905
- @if (index() > 0) {
19906
- <button strct-button size="sm" variant="neutral" (click)="prev()">
19907
- {{ backLabel() }}
19908
- </button>
19909
- }
19910
- <button strct-button size="sm" variant="primary" solid (click)="next()">
19911
- {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
19912
- </button>
19913
- </div>
19914
- </div>
19915
- }
19916
- `, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:transparent}.strct-tour__backdrop--dim{background:var(--backdrop)}.strct-tour__ring{position:fixed;z-index:calc(var(--z-tour) + 1);border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px var(--backdrop);pointer-events:none}.strct-tour__card{position:fixed;z-index:calc(var(--z-tour) + 2);width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\n"] }]
19917
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], backLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLabel", required: false }] }], skipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], finished: [{ type: i0.Output, args: ["finished"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], onReflow: [{
19918
- type: HostListener,
19919
- args: ['window:resize']
19920
- }, {
19921
- type: HostListener,
19922
- args: ['window:scroll']
19923
- }] } });
19924
-
19925
- let menubarCounter = 0;
19926
- /**
19927
- * Horizontal menubar — the application-menu strip ("File · Edit · View") for
19928
- * dense tool-style consoles:
19929
- *
19930
- * <strct-menubar [menus]="menus" (picked)="run($event)" />
19931
- *
19932
- * Click (or Enter/Down) opens a menu and focus moves into it; Left/Right move
19933
- * across the bar and, while open, switch the open menu (the APG menubar
19934
- * convention). Up/Down (+Home/End) rove the open menu, Right/Left enter and
19935
- * leave an item's one-level `children` submenu. Escape and outside click
19936
- * close, returning focus to the bar. `(picked)` carries `{ menu, item }`.
19937
- */
19938
- class StrctMenubar {
19939
- host = inject(ElementRef);
19940
- /** Top-level menus. */
19941
- menus = input.required(/* @ts-ignore */
19942
- ...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
19943
- /** Accessible name of the bar (localizable). */
19944
- ariaLabel = input('Application menu', /* @ts-ignore */
19945
- ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
19946
- /** A menu entry was chosen. */
19947
- picked = output();
19948
- openId = signal(null, /* @ts-ignore */
19949
- ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
19950
- focusIdx = signal(0, /* @ts-ignore */
19951
- ...(ngDevMode ? [{ debugName: "focusIdx" }] : /* istanbul ignore next */ []));
19952
- /** Index of the item whose one-level submenu is open. */
19953
- subIdx = signal(null, /* @ts-ignore */
19954
- ...(ngDevMode ? [{ debugName: "subIdx" }] : /* istanbul ignore next */ []));
19955
- /** Top-level button that opened the menu — focus returns here on close. */
19956
- trigger = null;
19957
- toggleMenu(id, i) {
19958
- this.focusIdx.set(i);
19959
- if (this.openId() === id) {
19960
- this.closeMenu(false);
19961
- }
19962
- else {
19963
- this.trigger = saveFocusedElement();
19964
- this.openId.set(id);
19965
- this.subIdx.set(null);
19966
- }
19967
- }
19968
- onItemClick(menu, item, i) {
19969
- if (item.disabled)
19970
- return;
19971
- if (item.children?.length) {
19972
- this.subIdx.set(this.subIdx() === i ? null : i);
19973
- return;
19974
- }
19975
- this.pick(menu, item);
19976
- }
19977
- onItemHover(item, i) {
19978
- if (item.disabled)
19979
- return;
19980
- if (item.children?.length)
19981
- this.subIdx.set(i);
19982
- else if (this.subIdx() != null)
19983
- this.subIdx.set(null);
19984
- }
19985
- pick(menu, item) {
19986
- if (item.disabled)
19987
- return;
19988
- this.closeMenu(true);
19989
- item.action?.(item.data);
19990
- this.picked.emit({ menu, item });
19991
- }
19992
- onTopKey(event, i) {
19993
- const menus = this.menus();
19994
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
19995
- event.preventDefault();
19996
- if (this.openId()) {
19997
- // While a menu is open, moving across the bar switches the open menu.
19998
- this.switchTop(event.key === 'ArrowRight' ? 1 : -1);
19999
- }
20000
- else {
20001
- const dir = event.key === 'ArrowRight' ? 1 : -1;
20002
- const next = (i + dir + menus.length) % menus.length;
20003
- this.focusIdx.set(next);
20004
- setTimeout(() => this.topButtons()[next]?.focus());
20005
- }
20006
- }
20007
- else if (event.key === 'ArrowDown' || event.key === 'Enter') {
20008
- event.preventDefault();
20009
- this.trigger = saveFocusedElement();
20010
- this.openId.set(menus[i].id);
20011
- this.subIdx.set(null);
20012
- setTimeout(() => {
20013
- const menuEl = this.host.nativeElement.querySelector('.strct-mb__menu');
20014
- if (menuEl)
20015
- focusFirstIn(menuEl);
20016
- });
20017
- }
20018
- else if (event.key === 'Escape' && this.openId()) {
20019
- event.preventDefault();
20020
- event.stopPropagation();
20021
- this.closeMenu(true);
20022
- }
20023
- }
20024
- onMenuKey(event, menu) {
20025
- const active = document.activeElement;
20026
- const inSub = !!active?.classList.contains('strct-mb__subitem');
20027
- switch (event.key) {
20028
- case 'ArrowDown':
20029
- case 'ArrowUp': {
20030
- event.preventDefault();
20031
- event.stopPropagation();
20032
- const list = this.navButtons(inSub);
20033
- const dir = event.key === 'ArrowDown' ? 1 : -1;
20034
- const cur = list.indexOf(active);
20035
- list[(cur + dir + list.length) % list.length]?.focus();
20036
- break;
20037
- }
20038
- case 'Home':
20039
- case 'End': {
20040
- event.preventDefault();
20041
- event.stopPropagation();
20042
- const list = this.navButtons(inSub);
20043
- (event.key === 'Home' ? list[0] : list[list.length - 1])?.focus();
20044
- break;
20045
- }
20046
- case 'ArrowRight': {
20047
- if (inSub)
20048
- break; // deepest level — nothing further to open
20049
- event.preventDefault();
20050
- event.stopPropagation();
20051
- const idx = Number(active?.getAttribute('data-idx'));
20052
- if (menu.items[idx]?.children?.length && !menu.items[idx].disabled) {
20053
- this.subIdx.set(idx);
20054
- setTimeout(() => this.navButtons(true)[0]?.focus());
20055
- }
20056
- else {
20057
- this.switchTop(1);
20058
- }
20059
- break;
20060
- }
20061
- case 'ArrowLeft': {
20062
- event.preventDefault();
20063
- event.stopPropagation();
20064
- if (inSub) {
20065
- const parent = this.subIdx();
20066
- this.subIdx.set(null);
20067
- this.host.nativeElement
20068
- .querySelector(`.strct-mb__item[data-idx="${parent}"]`)
20069
- ?.focus();
20070
- }
20071
- else if (this.subIdx() != null) {
20072
- this.subIdx.set(null);
20073
- }
20074
- else {
20075
- this.switchTop(-1);
20076
- }
20077
- break;
20226
+ return {
20227
+ top: Math.max(20, window.innerHeight / 2 - h / 2),
20228
+ left: Math.max(20, window.innerWidth / 2 - w / 2),
20229
+ };
20230
+ }
20231
+ // Below the target if it fits, else above; clamped into the viewport.
20232
+ let top = r.top + r.height + 14;
20233
+ if (top + h > window.innerHeight - 12)
20234
+ top = Math.max(12, r.top - h - 14);
20235
+ const left = Math.min(Math.max(12, r.left), window.innerWidth - w - 12);
20236
+ return { top, left };
20237
+ }, /* @ts-ignore */
20238
+ ...(ngDevMode ? [{ debugName: "cardPos" }] : /* istanbul ignore next */ []));
20239
+ constructor() {
20240
+ // Restart from step 0 on open; scroll the first target into view.
20241
+ effect(() => {
20242
+ if (this.open()) {
20243
+ this.previousActive = saveFocusedElement();
20244
+ this.index.set(0);
20245
+ this.scrollToTarget();
20246
+ this.focusNextButton();
20078
20247
  }
20079
- case 'Escape': {
20080
- event.preventDefault();
20081
- event.stopPropagation();
20082
- this.closeMenu(true);
20083
- break;
20248
+ else {
20249
+ restoreFocus(this.previousActive);
20250
+ this.previousActive = null;
20084
20251
  }
20252
+ });
20253
+ }
20254
+ next() {
20255
+ if (this.index() >= this.steps().length - 1) {
20256
+ this.open.set(false);
20257
+ this.finished.emit();
20258
+ return;
20085
20259
  }
20260
+ this.index.update((i) => i + 1);
20261
+ this.scrollToTarget();
20262
+ this.focusNextButton();
20086
20263
  }
20087
- /** Switch the open menu to the next/previous top-level entry; focus follows. */
20088
- switchTop(dir) {
20089
- const menus = this.menus();
20090
- const next = (this.focusIdx() + dir + menus.length) % menus.length;
20091
- this.focusIdx.set(next);
20092
- this.openId.set(menus[next].id);
20093
- this.subIdx.set(null);
20094
- setTimeout(() => this.topButtons()[next]?.focus());
20264
+ prev() {
20265
+ this.index.update((i) => Math.max(0, i - 1));
20266
+ this.scrollToTarget();
20267
+ this.focusNextButton();
20095
20268
  }
20096
- closeMenu(returnFocus) {
20097
- this.openId.set(null);
20098
- this.subIdx.set(null);
20099
- if (returnFocus)
20100
- restoreFocus(this.trigger);
20101
- this.trigger = null;
20269
+ close() {
20270
+ this.open.set(false);
20271
+ this.dismissed.emit();
20102
20272
  }
20103
- uid = ++menubarCounter;
20104
- hintId(menuId, i, j) {
20105
- // menu ids are consumer data; aria-describedby splits on whitespace.
20106
- const menu = menuId.replace(/\s+/g, '_');
20107
- return `strct-mb-${this.uid}-${menu}-${i}${j == null ? '' : '-' + j}-hint`;
20273
+ /** Wrap Tab focus within the card. */
20274
+ onTab(event) {
20275
+ const card = this.card();
20276
+ if (card)
20277
+ keepTabInside(event, card);
20108
20278
  }
20109
- topButtons() {
20110
- return Array.from(this.host.nativeElement.querySelectorAll('.strct-mb__top'));
20279
+ /** Escape closes; stopPropagation so a host modal/drawer does not also close. */
20280
+ onEscapeKey(event) {
20281
+ event.stopPropagation();
20282
+ this.close();
20111
20283
  }
20112
- /** Keyboard-reachable item buttons of the open menu — either its items or the
20113
- * open submenu's: enabled ones, plus disabled ones that carry a hint (so the
20114
- * reason can be read). A disabled entry with nothing to say is skipped. */
20115
- navButtons(sub) {
20116
- const menuEl = this.host.nativeElement.querySelector('.strct-mb__menu');
20117
- if (!menuEl)
20118
- return [];
20119
- const sel = sub ? '.strct-mb__subitem' : '.strct-mb__item:not(.strct-mb__subitem)';
20120
- return Array.from(menuEl.querySelectorAll(sel)).filter((b) => b.getAttribute('aria-disabled') !== 'true' || b.hasAttribute('aria-describedby'));
20284
+ /**
20285
+ * Arrow keys step through the tour, mirrored under RTL. No shared direction
20286
+ * helper exists (other components handle RTL in CSS only), so read the
20287
+ * card's computed direction here.
20288
+ */
20289
+ onArrow(event, dir) {
20290
+ event.preventDefault();
20291
+ const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
20292
+ if ((dir === 'right') !== rtl)
20293
+ this.next();
20294
+ else
20295
+ this.prev();
20121
20296
  }
20122
- onDocClick(event) {
20123
- if (this.openId() && !this.host.nativeElement.contains(event.target)) {
20124
- this.closeMenu(false);
20125
- }
20297
+ /** Home/End jump to the first/last step. */
20298
+ onHomeEnd(event, last) {
20299
+ event.preventDefault();
20300
+ this.index.set(last ? this.steps().length - 1 : 0);
20301
+ this.scrollToTarget();
20302
+ this.focusNextButton();
20126
20303
  }
20127
- onEscape(event) {
20128
- if (!this.openId())
20129
- return;
20130
- event.stopPropagation();
20131
- this.closeMenu(true);
20304
+ scrollToTarget() {
20305
+ setTimeout(() => {
20306
+ const sel = this.steps()[this.index()]?.target;
20307
+ if (sel)
20308
+ document.querySelector(sel)?.scrollIntoView?.({ block: 'center' });
20309
+ this.measureTick.update((n) => n + 1);
20310
+ });
20132
20311
  }
20133
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctMenubar, deps: [], target: i0.ɵɵFactoryTarget.Component });
20134
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctMenubar, isStandalone: true, selector: "strct-menubar", inputs: { menus: { classPropertyName: "menus", publicName: "menus", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape($event)" } }, ngImport: i0, template: `
20135
- <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
20136
- @for (menu of menus(); track menu.id; let i = $index) {
20137
- <div class="strct-mb__wrap">
20138
- <button
20139
- type="button"
20140
- class="strct-mb__top"
20141
- role="menuitem"
20142
- [class.strct-mb__top--open]="openId() === menu.id"
20143
- [attr.aria-haspopup]="'menu'"
20144
- [attr.aria-expanded]="openId() === menu.id"
20145
- [attr.tabindex]="i === focusIdx() ? 0 : -1"
20146
- (click)="toggleMenu(menu.id, i)"
20147
- (keydown)="onTopKey($event, i)"
20148
- >
20149
- {{ menu.label }}
20150
- </button>
20151
- @if (openId() === menu.id) {
20152
- <div
20153
- class="strct-mb__menu"
20154
- role="menu"
20155
- tabindex="-1"
20156
- [attr.aria-label]="menu.label"
20157
- (keydown)="onMenuKey($event, menu)"
20158
- >
20159
- @for (item of menu.items; track $index; let i = $index) {
20160
- @if (item.divider) {
20161
- <div class="strct-mb__divider" role="separator"></div>
20162
- } @else {
20163
- <div class="strct-mb__row">
20164
- <button
20165
- type="button"
20166
- class="strct-mb__item"
20167
- role="menuitem"
20168
- [attr.data-idx]="i"
20169
- [class.strct-mb__item--critical]="item.critical"
20170
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
20171
- [attr.aria-expanded]="item.children?.length ? subIdx() === i : null"
20172
- [attr.aria-disabled]="item.disabled ? 'true' : null"
20173
- [attr.aria-describedby]="item.hint ? hintId(menu.id, i) : null"
20174
- [attr.title]="item.hint || null"
20175
- (click)="onItemClick(menu, item, i)"
20176
- (mouseenter)="onItemHover(item, i)"
20177
- >
20178
- @if (item.icon) {
20179
- <strct-icon [name]="item.icon" [size]="13" />
20180
- }
20181
- {{ item.label }}
20182
- @if (item.children?.length) {
20183
- <strct-icon class="strct-mb__caret" name="chevronRight" [size]="12" />
20184
- }
20185
- </button>
20186
- @if (item.hint) {
20187
- <span [id]="hintId(menu.id, i)" hidden>{{ item.hint }}</span>
20188
- }
20189
- @if (subIdx() === i && item.children?.length) {
20190
- <div class="strct-mb__submenu" role="menu" [attr.aria-label]="item.label">
20191
- @for (sub of item.children; track $index; let j = $index) {
20192
- @if (sub.divider) {
20193
- <div class="strct-mb__divider" role="separator"></div>
20194
- } @else {
20195
- <button
20196
- type="button"
20197
- class="strct-mb__item strct-mb__subitem"
20198
- role="menuitem"
20199
- [class.strct-mb__item--critical]="sub.critical"
20200
- [attr.aria-disabled]="sub.disabled ? 'true' : null"
20201
- [attr.aria-describedby]="sub.hint ? hintId(menu.id, i, j) : null"
20202
- [attr.title]="sub.hint || null"
20203
- (click)="pick(menu, sub)"
20204
- >
20205
- @if (sub.icon) {
20206
- <strct-icon [name]="sub.icon" [size]="13" />
20207
- }
20208
- {{ sub.label }}
20209
- </button>
20210
- @if (sub.hint) {
20211
- <span [id]="hintId(menu.id, i, j)" hidden>{{ sub.hint }}</span>
20212
- }
20213
- }
20214
- }
20215
- </div>
20216
- }
20217
- </div>
20218
- }
20219
- }
20220
- </div>
20312
+ card() {
20313
+ return this.elementRef.nativeElement.querySelector('.strct-tour__card');
20314
+ }
20315
+ /** Move focus to the step's primary (Next/Done) action once the card has rendered. */
20316
+ focusNextButton() {
20317
+ setTimeout(() => {
20318
+ if (!this.open())
20319
+ return;
20320
+ const card = this.card();
20321
+ if (!card)
20322
+ return;
20323
+ const nav = card.querySelectorAll('.strct-tour__nav button');
20324
+ const primary = nav[nav.length - 1];
20325
+ if (primary)
20326
+ primary.focus();
20327
+ else
20328
+ focusFirstIn(card);
20329
+ });
20330
+ }
20331
+ onReflow() {
20332
+ if (this.open())
20333
+ this.measureTick.update((n) => n + 1);
20334
+ }
20335
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
20336
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctTour, isStandalone: true, selector: "strct-tour", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: true, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, skipLabel: { classPropertyName: "skipLabel", publicName: "skipLabel", isSignal: true, isRequired: false, transformFunction: null }, doneLabel: { classPropertyName: "doneLabel", publicName: "doneLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", finished: "finished", dismissed: "dismissed" }, host: { listeners: { "window:resize": "onReflow()", "window:scroll": "onReflow()" } }, ngImport: i0, template: `
20337
+ @if (open() && current(); as step) {
20338
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
20339
+ <div
20340
+ class="strct-tour__backdrop"
20341
+ [class.strct-tour__backdrop--dim]="!spot()"
20342
+ (click)="close()"
20343
+ ></div>
20344
+ @if (spot(); as r) {
20345
+ <div
20346
+ class="strct-tour__ring"
20347
+ [style.top.px]="r.top - 6"
20348
+ [style.left.px]="r.left - 6"
20349
+ [style.width.px]="r.width + 12"
20350
+ [style.height.px]="r.height + 12"
20351
+ ></div>
20352
+ }
20353
+ <div
20354
+ class="strct-tour__card"
20355
+ role="dialog"
20356
+ [attr.aria-labelledby]="titleId"
20357
+ [style.top.px]="cardPos().top"
20358
+ [style.left.px]="cardPos().left"
20359
+ (keydown.tab)="onTab($event)"
20360
+ (keydown.shift.tab)="onTab($event)"
20361
+ (keydown.escape)="onEscapeKey($event)"
20362
+ (keydown.arrowleft)="onArrow($event, 'left')"
20363
+ (keydown.arrowright)="onArrow($event, 'right')"
20364
+ (keydown.home)="onHomeEnd($event, false)"
20365
+ (keydown.end)="onHomeEnd($event, true)"
20366
+ >
20367
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
20368
+ <div class="strct-tour__title" [id]="titleId">{{ step.title }}</div>
20369
+ <div class="strct-tour__body">{{ step.body }}</div>
20370
+ <div class="strct-tour__nav">
20371
+ <button strct-button size="sm" variant="flat" (click)="close()">
20372
+ {{ skipLabel() }}
20373
+ </button>
20374
+ <span class="strct-tour__grow"></span>
20375
+ @if (index() > 0) {
20376
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
20377
+ {{ backLabel() }}
20378
+ </button>
20221
20379
  }
20380
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
20381
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
20382
+ </button>
20222
20383
  </div>
20223
- }
20224
- </div>
20225
- `, isInline: true, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:var(--radius-md)}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:var(--z-dropdown);min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__menu:focus{outline:none}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not([aria-disabled=true]){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not([aria-disabled=true]){background:var(--critical-bg)}.strct-mb__item[aria-disabled=true]{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}.strct-mb__row{position:relative}.strct-mb__caret{margin-inline-start:auto;color:var(--t3)}[dir=rtl] .strct-mb__caret{transform:rotate(180deg)}.strct-mb__submenu{position:absolute;top:-5px;inset-inline-start:calc(100% + 2px);z-index:var(--z-base);min-width:160px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20384
+ </div>
20385
+ }
20386
+ `, isInline: true, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:transparent}.strct-tour__backdrop--dim{background:var(--backdrop)}.strct-tour__ring{position:fixed;z-index:calc(var(--z-tour) + 1);border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px var(--backdrop);pointer-events:none}.strct-tour__card{position:fixed;z-index:calc(var(--z-tour) + 2);width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20226
20387
  }
20227
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctMenubar, decorators: [{
20388
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTour, decorators: [{
20228
20389
  type: Component,
20229
- args: [{ selector: 'strct-menubar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
20230
- <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
20231
- @for (menu of menus(); track menu.id; let i = $index) {
20232
- <div class="strct-mb__wrap">
20233
- <button
20234
- type="button"
20235
- class="strct-mb__top"
20236
- role="menuitem"
20237
- [class.strct-mb__top--open]="openId() === menu.id"
20238
- [attr.aria-haspopup]="'menu'"
20239
- [attr.aria-expanded]="openId() === menu.id"
20240
- [attr.tabindex]="i === focusIdx() ? 0 : -1"
20241
- (click)="toggleMenu(menu.id, i)"
20242
- (keydown)="onTopKey($event, i)"
20243
- >
20244
- {{ menu.label }}
20390
+ args: [{ selector: 'strct-tour', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton], template: `
20391
+ @if (open() && current(); as step) {
20392
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
20393
+ <div
20394
+ class="strct-tour__backdrop"
20395
+ [class.strct-tour__backdrop--dim]="!spot()"
20396
+ (click)="close()"
20397
+ ></div>
20398
+ @if (spot(); as r) {
20399
+ <div
20400
+ class="strct-tour__ring"
20401
+ [style.top.px]="r.top - 6"
20402
+ [style.left.px]="r.left - 6"
20403
+ [style.width.px]="r.width + 12"
20404
+ [style.height.px]="r.height + 12"
20405
+ ></div>
20406
+ }
20407
+ <div
20408
+ class="strct-tour__card"
20409
+ role="dialog"
20410
+ [attr.aria-labelledby]="titleId"
20411
+ [style.top.px]="cardPos().top"
20412
+ [style.left.px]="cardPos().left"
20413
+ (keydown.tab)="onTab($event)"
20414
+ (keydown.shift.tab)="onTab($event)"
20415
+ (keydown.escape)="onEscapeKey($event)"
20416
+ (keydown.arrowleft)="onArrow($event, 'left')"
20417
+ (keydown.arrowright)="onArrow($event, 'right')"
20418
+ (keydown.home)="onHomeEnd($event, false)"
20419
+ (keydown.end)="onHomeEnd($event, true)"
20420
+ >
20421
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
20422
+ <div class="strct-tour__title" [id]="titleId">{{ step.title }}</div>
20423
+ <div class="strct-tour__body">{{ step.body }}</div>
20424
+ <div class="strct-tour__nav">
20425
+ <button strct-button size="sm" variant="flat" (click)="close()">
20426
+ {{ skipLabel() }}
20245
20427
  </button>
20246
- @if (openId() === menu.id) {
20247
- <div
20248
- class="strct-mb__menu"
20249
- role="menu"
20250
- tabindex="-1"
20251
- [attr.aria-label]="menu.label"
20252
- (keydown)="onMenuKey($event, menu)"
20253
- >
20254
- @for (item of menu.items; track $index; let i = $index) {
20255
- @if (item.divider) {
20256
- <div class="strct-mb__divider" role="separator"></div>
20257
- } @else {
20258
- <div class="strct-mb__row">
20259
- <button
20260
- type="button"
20261
- class="strct-mb__item"
20262
- role="menuitem"
20263
- [attr.data-idx]="i"
20264
- [class.strct-mb__item--critical]="item.critical"
20265
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
20266
- [attr.aria-expanded]="item.children?.length ? subIdx() === i : null"
20267
- [attr.aria-disabled]="item.disabled ? 'true' : null"
20268
- [attr.aria-describedby]="item.hint ? hintId(menu.id, i) : null"
20269
- [attr.title]="item.hint || null"
20270
- (click)="onItemClick(menu, item, i)"
20271
- (mouseenter)="onItemHover(item, i)"
20272
- >
20273
- @if (item.icon) {
20274
- <strct-icon [name]="item.icon" [size]="13" />
20275
- }
20276
- {{ item.label }}
20277
- @if (item.children?.length) {
20278
- <strct-icon class="strct-mb__caret" name="chevronRight" [size]="12" />
20279
- }
20280
- </button>
20281
- @if (item.hint) {
20282
- <span [id]="hintId(menu.id, i)" hidden>{{ item.hint }}</span>
20283
- }
20284
- @if (subIdx() === i && item.children?.length) {
20285
- <div class="strct-mb__submenu" role="menu" [attr.aria-label]="item.label">
20286
- @for (sub of item.children; track $index; let j = $index) {
20287
- @if (sub.divider) {
20288
- <div class="strct-mb__divider" role="separator"></div>
20289
- } @else {
20290
- <button
20291
- type="button"
20292
- class="strct-mb__item strct-mb__subitem"
20293
- role="menuitem"
20294
- [class.strct-mb__item--critical]="sub.critical"
20295
- [attr.aria-disabled]="sub.disabled ? 'true' : null"
20296
- [attr.aria-describedby]="sub.hint ? hintId(menu.id, i, j) : null"
20297
- [attr.title]="sub.hint || null"
20298
- (click)="pick(menu, sub)"
20299
- >
20300
- @if (sub.icon) {
20301
- <strct-icon [name]="sub.icon" [size]="13" />
20302
- }
20303
- {{ sub.label }}
20304
- </button>
20305
- @if (sub.hint) {
20306
- <span [id]="hintId(menu.id, i, j)" hidden>{{ sub.hint }}</span>
20307
- }
20308
- }
20309
- }
20310
- </div>
20311
- }
20312
- </div>
20313
- }
20314
- }
20315
- </div>
20428
+ <span class="strct-tour__grow"></span>
20429
+ @if (index() > 0) {
20430
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
20431
+ {{ backLabel() }}
20432
+ </button>
20316
20433
  }
20434
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
20435
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
20436
+ </button>
20317
20437
  </div>
20318
- }
20319
- </div>
20320
- `, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:var(--radius-md)}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:var(--z-dropdown);min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__menu:focus{outline:none}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not([aria-disabled=true]){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not([aria-disabled=true]){background:var(--critical-bg)}.strct-mb__item[aria-disabled=true]{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}.strct-mb__row{position:relative}.strct-mb__caret{margin-inline-start:auto;color:var(--t3)}[dir=rtl] .strct-mb__caret{transform:rotate(180deg)}.strct-mb__submenu{position:absolute;top:-5px;inset-inline-start:calc(100% + 2px);z-index:var(--z-base);min-width:160px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}\n"] }]
20321
- }], propDecorators: { menus: [{ type: i0.Input, args: [{ isSignal: true, alias: "menus", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], onDocClick: [{
20438
+ </div>
20439
+ }
20440
+ `, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:transparent}.strct-tour__backdrop--dim{background:var(--backdrop)}.strct-tour__ring{position:fixed;z-index:calc(var(--z-tour) + 1);border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px var(--backdrop);pointer-events:none}.strct-tour__card{position:fixed;z-index:calc(var(--z-tour) + 2);width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\n"] }]
20441
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], backLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLabel", required: false }] }], skipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], finished: [{ type: i0.Output, args: ["finished"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], onReflow: [{
20322
20442
  type: HostListener,
20323
- args: ['document:click', ['$event']]
20324
- }], onEscape: [{
20443
+ args: ['window:resize']
20444
+ }, {
20325
20445
  type: HostListener,
20326
- args: ['document:keydown.escape', ['$event']]
20446
+ args: ['window:scroll']
20327
20447
  }] } });
20328
20448
 
20329
- function normalize(combo) {
20330
- return combo
20331
- .toLowerCase()
20332
- .split('+')
20333
- .map((p) => p.trim())
20334
- .sort()
20335
- .join('+');
20336
- }
20337
- function comboOf(event) {
20338
- const parts = [];
20339
- if (event.ctrlKey)
20340
- parts.push('ctrl');
20341
- if (event.metaKey)
20342
- parts.push('meta');
20343
- if (event.altKey)
20344
- parts.push('alt');
20345
- if (event.shiftKey)
20346
- parts.push('shift');
20347
- parts.push(event.key.toLowerCase());
20348
- return parts.sort().join('+');
20349
- }
20350
- /** `mod` matches Ctrl (and ⌘ on mac keyboards) — expand to both. */
20351
- function expand(combo) {
20352
- const n = combo.toLowerCase();
20353
- if (!n.includes('mod'))
20354
- return [normalize(n)];
20355
- return [normalize(n.replace('mod', 'ctrl')), normalize(n.replace('mod', 'meta'))];
20356
- }
20449
+ let menubarCounter = 0;
20357
20450
  /**
20358
- * Application hotkeys, centrally registered so they are discoverable — the
20359
- * Blueprint pattern:
20451
+ * Horizontal menubar — the application-menu strip ("File · Edit · View") for
20452
+ * dense tool-style consoles:
20360
20453
  *
20361
- * private hotkeys = inject(StrctHotkeysService);
20362
- * ngOnInit() { this.dispose = this.hotkeys.register(
20363
- * { combo: 'mod+k', description: 'Open command palette', group: 'Global',
20364
- * handler: () => this.palette.open() }); }
20454
+ * <strct-menubar [menus]="menus" (picked)="run($event)" />
20365
20455
  *
20366
- * Combos: `'a'`, `'shift+?'`, `'ctrl+k'`, `'meta+shift+p'`, `'mod+k'` (mod = Ctrl/⌘).
20367
- * Plain-key combos are suppressed while typing in inputs / textareas /
20368
- * contenteditable; modifier combos always fire. Pair with
20369
- * `<strct-hotkeys-help/>` for the ⌘/ ("?") cheatsheet overlay.
20456
+ * Click (or Enter/Down) opens a menu and focus moves into it; Left/Right move
20457
+ * across the bar and, while open, switch the open menu (the APG menubar
20458
+ * convention). Up/Down (+Home/End) rove the open menu, Right/Left enter and
20459
+ * leave an item's one-level `children` submenu. Escape and outside click
20460
+ * close, returning focus to the bar. `(picked)` carries `{ menu, item }`.
20370
20461
  */
20371
- class StrctHotkeysService {
20372
- registry = signal([], /* @ts-ignore */
20373
- ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
20374
- /** All registered hotkeys (for the help overlay). */
20375
- hotkeys = this.registry.asReadonly();
20376
- listener = (event) => this.onKeydown(event);
20377
- constructor() {
20378
- document.addEventListener('keydown', this.listener);
20379
- }
20380
- /** Register a hotkey; the returned function unregisters it. */
20381
- register(hotkey) {
20382
- this.registry.update((list) => [...list, hotkey]);
20383
- return () => this.registry.update((list) => list.filter((h) => h !== hotkey));
20462
+ class StrctMenubar {
20463
+ host = inject(ElementRef);
20464
+ /** Top-level menus. */
20465
+ menus = input.required(/* @ts-ignore */
20466
+ ...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
20467
+ /** Accessible name of the bar (localizable). */
20468
+ ariaLabel = input('Application menu', /* @ts-ignore */
20469
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
20470
+ /** A menu entry was chosen. */
20471
+ picked = output();
20472
+ openId = signal(null, /* @ts-ignore */
20473
+ ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
20474
+ focusIdx = signal(0, /* @ts-ignore */
20475
+ ...(ngDevMode ? [{ debugName: "focusIdx" }] : /* istanbul ignore next */ []));
20476
+ /** Index of the item whose one-level submenu is open. */
20477
+ subIdx = signal(null, /* @ts-ignore */
20478
+ ...(ngDevMode ? [{ debugName: "subIdx" }] : /* istanbul ignore next */ []));
20479
+ /** Top-level button that opened the menu — focus returns here on close. */
20480
+ trigger = null;
20481
+ toggleMenu(id, i) {
20482
+ this.focusIdx.set(i);
20483
+ if (this.openId() === id) {
20484
+ this.closeMenu(false);
20485
+ }
20486
+ else {
20487
+ this.trigger = saveFocusedElement();
20488
+ this.openId.set(id);
20489
+ this.subIdx.set(null);
20490
+ }
20384
20491
  }
20385
- onKeydown(event) {
20386
- const combo = comboOf(event);
20387
- const typing = isTyping(event);
20388
- for (const hk of this.registry()) {
20389
- if (!expand(hk.combo).includes(combo))
20390
- continue;
20391
- // While typing, only modifier combos may fire.
20392
- if (typing && !(event.ctrlKey || event.metaKey || event.altKey))
20393
- continue;
20394
- event.preventDefault();
20395
- hk.handler(event);
20492
+ onItemClick(menu, item, i) {
20493
+ if (item.disabled)
20494
+ return;
20495
+ if (item.children?.length) {
20496
+ this.subIdx.set(this.subIdx() === i ? null : i);
20396
20497
  return;
20397
20498
  }
20499
+ this.pick(menu, item);
20398
20500
  }
20399
- ngOnDestroy() {
20400
- document.removeEventListener('keydown', this.listener);
20501
+ onItemHover(item, i) {
20502
+ if (item.disabled)
20503
+ return;
20504
+ if (item.children?.length)
20505
+ this.subIdx.set(i);
20506
+ else if (this.subIdx() != null)
20507
+ this.subIdx.set(null);
20401
20508
  }
20402
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
20403
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, providedIn: 'root' });
20404
- }
20405
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, decorators: [{
20406
- type: Injectable,
20407
- args: [{ providedIn: 'root' }]
20408
- }], ctorParameters: () => [] });
20409
- function isTyping(event) {
20410
- const el = event.target;
20411
- if (!el)
20412
- return false;
20413
- return (el.tagName === 'INPUT' ||
20414
- el.tagName === 'TEXTAREA' ||
20415
- el.tagName === 'SELECT' ||
20416
- el.isContentEditable);
20417
- }
20418
- /**
20419
- * The hotkey cheatsheet overlay. Mount once near the app root; it registers
20420
- * `shift+?` (typed as `?`) itself and lists everything in the service, grouped:
20421
- *
20422
- * <strct-hotkeys-help />
20423
- */
20424
- class StrctHotkeysHelp {
20425
- service = inject(StrctHotkeysService);
20426
- host = inject((ElementRef));
20427
- /** Overlay visibility (two-way; `?` toggles it too). */
20428
- open = model(false, /* @ts-ignore */
20429
- ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
20430
- /** Localizable strings. */
20431
- title = input('Keyboard shortcuts', /* @ts-ignore */
20432
- ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
20433
- emptyText = input('No shortcuts registered.', /* @ts-ignore */
20434
- ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
20435
- /** Accessible label of the × close button (localizable). */
20436
- closeLabel = input('Close', /* @ts-ignore */
20437
- ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
20438
- /** Element that had focus before the overlay opened, restored on close. */
20439
- previousActive = null;
20440
- grouped = computed(() => {
20441
- const groups = new Map();
20442
- for (const hk of this.service.hotkeys()) {
20443
- const name = hk.group ?? 'General';
20444
- const list = groups.get(name);
20445
- if (list)
20446
- list.push(hk);
20447
- else
20448
- groups.set(name, [hk]);
20449
- }
20450
- return [...groups.entries()].map(([name, keys]) => ({ name, keys }));
20451
- }, /* @ts-ignore */
20452
- ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
20453
- constructor() {
20454
- const dispose = this.service.register({
20455
- combo: 'shift+?',
20456
- description: 'Show this help',
20457
- group: 'General',
20458
- handler: () => this.open.update((v) => !v),
20459
- });
20460
- inject(DestroyRef).onDestroy(dispose);
20461
- // Focus lifecycle: remember the trigger, move focus into the dialog once
20462
- // it has rendered, and hand focus back on close.
20463
- effect(() => {
20464
- if (this.open()) {
20465
- this.previousActive = saveFocusedElement();
20466
- setTimeout(() => {
20467
- const dialog = this.host.nativeElement.querySelector('.strct-hkh');
20468
- if (dialog)
20469
- focusFirstIn(dialog);
20470
- });
20509
+ pick(menu, item) {
20510
+ if (item.disabled)
20511
+ return;
20512
+ this.closeMenu(true);
20513
+ item.action?.(item.data);
20514
+ this.picked.emit({ menu, item });
20515
+ }
20516
+ onTopKey(event, i) {
20517
+ const menus = this.menus();
20518
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
20519
+ event.preventDefault();
20520
+ if (this.openId()) {
20521
+ // While a menu is open, moving across the bar switches the open menu.
20522
+ this.switchTop(event.key === 'ArrowRight' ? 1 : -1);
20471
20523
  }
20472
20524
  else {
20473
- restoreFocus(this.previousActive);
20474
- this.previousActive = null;
20525
+ const dir = event.key === 'ArrowRight' ? 1 : -1;
20526
+ const next = (i + dir + menus.length) % menus.length;
20527
+ this.focusIdx.set(next);
20528
+ setTimeout(() => this.topButtons()[next]?.focus());
20475
20529
  }
20476
- });
20530
+ }
20531
+ else if (event.key === 'ArrowDown' || event.key === 'Enter') {
20532
+ event.preventDefault();
20533
+ this.trigger = saveFocusedElement();
20534
+ this.openId.set(menus[i].id);
20535
+ this.subIdx.set(null);
20536
+ setTimeout(() => {
20537
+ const menuEl = this.host.nativeElement.querySelector('.strct-mb__menu');
20538
+ if (menuEl)
20539
+ focusFirstIn(menuEl);
20540
+ });
20541
+ }
20542
+ else if (event.key === 'Escape' && this.openId()) {
20543
+ event.preventDefault();
20544
+ event.stopPropagation();
20545
+ this.closeMenu(true);
20546
+ }
20477
20547
  }
20478
- // Escape is handled locally — registering it in the service would swallow
20479
- // every Escape in the app (the service preventDefaults on match).
20480
- onEscape(event) {
20481
- if (!this.open())
20482
- return;
20483
- // Document-level: stopImmediatePropagation so a host modal/drawer with its
20484
- // own document listener doesn't also see the Escape the overlay consumed.
20485
- event.stopImmediatePropagation();
20486
- this.open.set(false);
20548
+ onMenuKey(event, menu) {
20549
+ const active = document.activeElement;
20550
+ const inSub = !!active?.classList.contains('strct-mb__subitem');
20551
+ switch (event.key) {
20552
+ case 'ArrowDown':
20553
+ case 'ArrowUp': {
20554
+ event.preventDefault();
20555
+ event.stopPropagation();
20556
+ const list = this.navButtons(inSub);
20557
+ const dir = event.key === 'ArrowDown' ? 1 : -1;
20558
+ const cur = list.indexOf(active);
20559
+ list[(cur + dir + list.length) % list.length]?.focus();
20560
+ break;
20561
+ }
20562
+ case 'Home':
20563
+ case 'End': {
20564
+ event.preventDefault();
20565
+ event.stopPropagation();
20566
+ const list = this.navButtons(inSub);
20567
+ (event.key === 'Home' ? list[0] : list[list.length - 1])?.focus();
20568
+ break;
20569
+ }
20570
+ case 'ArrowRight': {
20571
+ if (inSub)
20572
+ break; // deepest level — nothing further to open
20573
+ event.preventDefault();
20574
+ event.stopPropagation();
20575
+ const idx = Number(active?.getAttribute('data-idx'));
20576
+ if (menu.items[idx]?.children?.length && !menu.items[idx].disabled) {
20577
+ this.subIdx.set(idx);
20578
+ setTimeout(() => this.navButtons(true)[0]?.focus());
20579
+ }
20580
+ else {
20581
+ this.switchTop(1);
20582
+ }
20583
+ break;
20584
+ }
20585
+ case 'ArrowLeft': {
20586
+ event.preventDefault();
20587
+ event.stopPropagation();
20588
+ if (inSub) {
20589
+ const parent = this.subIdx();
20590
+ this.subIdx.set(null);
20591
+ this.host.nativeElement
20592
+ .querySelector(`.strct-mb__item[data-idx="${parent}"]`)
20593
+ ?.focus();
20594
+ }
20595
+ else if (this.subIdx() != null) {
20596
+ this.subIdx.set(null);
20597
+ }
20598
+ else {
20599
+ this.switchTop(-1);
20600
+ }
20601
+ break;
20602
+ }
20603
+ case 'Escape': {
20604
+ event.preventDefault();
20605
+ event.stopPropagation();
20606
+ this.closeMenu(true);
20607
+ break;
20608
+ }
20609
+ }
20487
20610
  }
20488
- parts(combo) {
20489
- return combo.split('+').map((p) => {
20490
- const map = {
20491
- mod: '⌘/Ctrl',
20492
- meta: '⌘',
20493
- ctrl: 'Ctrl',
20494
- alt: 'Alt',
20495
- shift: 'Shift',
20496
- escape: 'Esc',
20497
- };
20498
- return map[p.trim().toLowerCase()] ?? p.trim().toUpperCase();
20499
- });
20611
+ /** Switch the open menu to the next/previous top-level entry; focus follows. */
20612
+ switchTop(dir) {
20613
+ const menus = this.menus();
20614
+ const next = (this.focusIdx() + dir + menus.length) % menus.length;
20615
+ this.focusIdx.set(next);
20616
+ this.openId.set(menus[next].id);
20617
+ this.subIdx.set(null);
20618
+ setTimeout(() => this.topButtons()[next]?.focus());
20619
+ }
20620
+ closeMenu(returnFocus) {
20621
+ this.openId.set(null);
20622
+ this.subIdx.set(null);
20623
+ if (returnFocus)
20624
+ restoreFocus(this.trigger);
20625
+ this.trigger = null;
20626
+ }
20627
+ uid = ++menubarCounter;
20628
+ hintId(menuId, i, j) {
20629
+ // menu ids are consumer data; aria-describedby splits on whitespace.
20630
+ const menu = menuId.replace(/\s+/g, '_');
20631
+ return `strct-mb-${this.uid}-${menu}-${i}${j == null ? '' : '-' + j}-hint`;
20632
+ }
20633
+ topButtons() {
20634
+ return Array.from(this.host.nativeElement.querySelectorAll('.strct-mb__top'));
20635
+ }
20636
+ /** Keyboard-reachable item buttons of the open menu — either its items or the
20637
+ * open submenu's: enabled ones, plus disabled ones that carry a hint (so the
20638
+ * reason can be read). A disabled entry with nothing to say is skipped. */
20639
+ navButtons(sub) {
20640
+ const menuEl = this.host.nativeElement.querySelector('.strct-mb__menu');
20641
+ if (!menuEl)
20642
+ return [];
20643
+ const sel = sub ? '.strct-mb__subitem' : '.strct-mb__item:not(.strct-mb__subitem)';
20644
+ return Array.from(menuEl.querySelectorAll(sel)).filter((b) => b.getAttribute('aria-disabled') !== 'true' || b.hasAttribute('aria-describedby'));
20500
20645
  }
20501
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysHelp, deps: [], target: i0.ɵɵFactoryTarget.Component });
20502
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctHotkeysHelp, isStandalone: true, selector: "strct-hotkeys-help", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape($event)" } }, ngImport: i0, template: `
20503
- @if (open()) {
20504
- <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
20505
- <div class="strct-hkh" role="dialog" aria-modal="true" [attr.aria-label]="title()">
20506
- <div class="strct-hkh__head">
20507
- <span class="strct-hkh__title">{{ title() }}</span>
20508
- <span class="strct-hkh__headside">
20509
- <strct-kbd>?</strct-kbd>
20510
- <button
20511
- type="button"
20512
- class="strct-hkh__close"
20513
- [attr.aria-label]="closeLabel()"
20514
- (click)="open.set(false)"
20515
- >
20516
- <strct-icon name="close" [size]="14" />
20517
- </button>
20518
- </span>
20519
- </div>
20520
- @for (group of grouped(); track group.name) {
20521
- <div class="strct-hkh__group">
20522
- <div class="strct-hkh__groupname">{{ group.name }}</div>
20523
- @for (hk of group.keys; track hk.combo + hk.description) {
20524
- <div class="strct-hkh__row">
20525
- <span class="strct-hkh__desc">{{ hk.description }}</span>
20526
- <span class="strct-hkh__keys">
20527
- @for (part of parts(hk.combo); track $index) {
20528
- <strct-kbd>{{ part }}</strct-kbd>
20529
- }
20530
- </span>
20531
- </div>
20532
- }
20533
- </div>
20534
- } @empty {
20535
- <p class="strct-hkh__empty">{{ emptyText() }}</p>
20646
+ onDocClick(event) {
20647
+ if (this.openId() && !this.host.nativeElement.contains(event.target)) {
20648
+ this.closeMenu(false);
20536
20649
  }
20537
- </div>
20538
20650
  }
20539
- `, isInline: true, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:var(--backdrop)}.strct-hkh{position:fixed;z-index:calc(var(--z-tour) + 1);top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__headside{display:inline-flex;align-items:center;gap:8px}.strct-hkh__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-hkh__close:hover{color:var(--t1);background:var(--bg-3)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctKbd, selector: "strct-kbd" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20651
+ onEscape(event) {
20652
+ if (!this.openId())
20653
+ return;
20654
+ event.stopPropagation();
20655
+ this.closeMenu(true);
20656
+ }
20657
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctMenubar, deps: [], target: i0.ɵɵFactoryTarget.Component });
20658
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctMenubar, isStandalone: true, selector: "strct-menubar", inputs: { menus: { classPropertyName: "menus", publicName: "menus", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape($event)" } }, ngImport: i0, template: `
20659
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
20660
+ @for (menu of menus(); track menu.id; let i = $index) {
20661
+ <div class="strct-mb__wrap">
20662
+ <button
20663
+ type="button"
20664
+ class="strct-mb__top"
20665
+ role="menuitem"
20666
+ [class.strct-mb__top--open]="openId() === menu.id"
20667
+ [attr.aria-haspopup]="'menu'"
20668
+ [attr.aria-expanded]="openId() === menu.id"
20669
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
20670
+ (click)="toggleMenu(menu.id, i)"
20671
+ (keydown)="onTopKey($event, i)"
20672
+ >
20673
+ {{ menu.label }}
20674
+ </button>
20675
+ @if (openId() === menu.id) {
20676
+ <div
20677
+ class="strct-mb__menu"
20678
+ role="menu"
20679
+ tabindex="-1"
20680
+ [attr.aria-label]="menu.label"
20681
+ (keydown)="onMenuKey($event, menu)"
20682
+ >
20683
+ @for (item of menu.items; track $index; let i = $index) {
20684
+ @if (item.divider) {
20685
+ <div class="strct-mb__divider" role="separator"></div>
20686
+ } @else {
20687
+ <div class="strct-mb__row">
20688
+ <button
20689
+ type="button"
20690
+ class="strct-mb__item"
20691
+ role="menuitem"
20692
+ [attr.data-idx]="i"
20693
+ [class.strct-mb__item--critical]="item.critical"
20694
+ [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
20695
+ [attr.aria-expanded]="item.children?.length ? subIdx() === i : null"
20696
+ [attr.aria-disabled]="item.disabled ? 'true' : null"
20697
+ [attr.aria-describedby]="item.hint ? hintId(menu.id, i) : null"
20698
+ [attr.title]="item.hint || null"
20699
+ (click)="onItemClick(menu, item, i)"
20700
+ (mouseenter)="onItemHover(item, i)"
20701
+ >
20702
+ @if (item.icon) {
20703
+ <strct-icon [name]="item.icon" [size]="13" />
20704
+ }
20705
+ {{ item.label }}
20706
+ @if (item.children?.length) {
20707
+ <strct-icon class="strct-mb__caret" name="chevronRight" [size]="12" />
20708
+ }
20709
+ </button>
20710
+ @if (item.hint) {
20711
+ <span [id]="hintId(menu.id, i)" hidden>{{ item.hint }}</span>
20712
+ }
20713
+ @if (subIdx() === i && item.children?.length) {
20714
+ <div class="strct-mb__submenu" role="menu" [attr.aria-label]="item.label">
20715
+ @for (sub of item.children; track $index; let j = $index) {
20716
+ @if (sub.divider) {
20717
+ <div class="strct-mb__divider" role="separator"></div>
20718
+ } @else {
20719
+ <button
20720
+ type="button"
20721
+ class="strct-mb__item strct-mb__subitem"
20722
+ role="menuitem"
20723
+ [class.strct-mb__item--critical]="sub.critical"
20724
+ [attr.aria-disabled]="sub.disabled ? 'true' : null"
20725
+ [attr.aria-describedby]="sub.hint ? hintId(menu.id, i, j) : null"
20726
+ [attr.title]="sub.hint || null"
20727
+ (click)="pick(menu, sub)"
20728
+ >
20729
+ @if (sub.icon) {
20730
+ <strct-icon [name]="sub.icon" [size]="13" />
20731
+ }
20732
+ {{ sub.label }}
20733
+ </button>
20734
+ @if (sub.hint) {
20735
+ <span [id]="hintId(menu.id, i, j)" hidden>{{ sub.hint }}</span>
20736
+ }
20737
+ }
20738
+ }
20739
+ </div>
20740
+ }
20741
+ </div>
20742
+ }
20743
+ }
20744
+ </div>
20745
+ }
20746
+ </div>
20747
+ }
20748
+ </div>
20749
+ `, isInline: true, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:var(--radius-md)}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:var(--z-dropdown);min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__menu:focus{outline:none}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not([aria-disabled=true]){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not([aria-disabled=true]){background:var(--critical-bg)}.strct-mb__item[aria-disabled=true]{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}.strct-mb__row{position:relative}.strct-mb__caret{margin-inline-start:auto;color:var(--t3)}[dir=rtl] .strct-mb__caret{transform:rotate(180deg)}.strct-mb__submenu{position:absolute;top:-5px;inset-inline-start:calc(100% + 2px);z-index:var(--z-base);min-width:160px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20540
20750
  }
20541
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysHelp, decorators: [{
20751
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctMenubar, decorators: [{
20542
20752
  type: Component,
20543
- args: [{ selector: 'strct-hotkeys-help', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctKbd], template: `
20544
- @if (open()) {
20545
- <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
20546
- <div class="strct-hkh" role="dialog" aria-modal="true" [attr.aria-label]="title()">
20547
- <div class="strct-hkh__head">
20548
- <span class="strct-hkh__title">{{ title() }}</span>
20549
- <span class="strct-hkh__headside">
20550
- <strct-kbd>?</strct-kbd>
20551
- <button
20552
- type="button"
20553
- class="strct-hkh__close"
20554
- [attr.aria-label]="closeLabel()"
20555
- (click)="open.set(false)"
20753
+ args: [{ selector: 'strct-menubar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
20754
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
20755
+ @for (menu of menus(); track menu.id; let i = $index) {
20756
+ <div class="strct-mb__wrap">
20757
+ <button
20758
+ type="button"
20759
+ class="strct-mb__top"
20760
+ role="menuitem"
20761
+ [class.strct-mb__top--open]="openId() === menu.id"
20762
+ [attr.aria-haspopup]="'menu'"
20763
+ [attr.aria-expanded]="openId() === menu.id"
20764
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
20765
+ (click)="toggleMenu(menu.id, i)"
20766
+ (keydown)="onTopKey($event, i)"
20767
+ >
20768
+ {{ menu.label }}
20769
+ </button>
20770
+ @if (openId() === menu.id) {
20771
+ <div
20772
+ class="strct-mb__menu"
20773
+ role="menu"
20774
+ tabindex="-1"
20775
+ [attr.aria-label]="menu.label"
20776
+ (keydown)="onMenuKey($event, menu)"
20556
20777
  >
20557
- <strct-icon name="close" [size]="14" />
20558
- </button>
20559
- </span>
20560
- </div>
20561
- @for (group of grouped(); track group.name) {
20562
- <div class="strct-hkh__group">
20563
- <div class="strct-hkh__groupname">{{ group.name }}</div>
20564
- @for (hk of group.keys; track hk.combo + hk.description) {
20565
- <div class="strct-hkh__row">
20566
- <span class="strct-hkh__desc">{{ hk.description }}</span>
20567
- <span class="strct-hkh__keys">
20568
- @for (part of parts(hk.combo); track $index) {
20569
- <strct-kbd>{{ part }}</strct-kbd>
20570
- }
20571
- </span>
20572
- </div>
20573
- }
20574
- </div>
20575
- } @empty {
20576
- <p class="strct-hkh__empty">{{ emptyText() }}</p>
20577
- }
20578
- </div>
20579
- }
20580
- `, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:var(--backdrop)}.strct-hkh{position:fixed;z-index:calc(var(--z-tour) + 1);top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__headside{display:inline-flex;align-items:center;gap:8px}.strct-hkh__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-hkh__close:hover{color:var(--t1);background:var(--bg-3)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"] }]
20581
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], onEscape: [{
20778
+ @for (item of menu.items; track $index; let i = $index) {
20779
+ @if (item.divider) {
20780
+ <div class="strct-mb__divider" role="separator"></div>
20781
+ } @else {
20782
+ <div class="strct-mb__row">
20783
+ <button
20784
+ type="button"
20785
+ class="strct-mb__item"
20786
+ role="menuitem"
20787
+ [attr.data-idx]="i"
20788
+ [class.strct-mb__item--critical]="item.critical"
20789
+ [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
20790
+ [attr.aria-expanded]="item.children?.length ? subIdx() === i : null"
20791
+ [attr.aria-disabled]="item.disabled ? 'true' : null"
20792
+ [attr.aria-describedby]="item.hint ? hintId(menu.id, i) : null"
20793
+ [attr.title]="item.hint || null"
20794
+ (click)="onItemClick(menu, item, i)"
20795
+ (mouseenter)="onItemHover(item, i)"
20796
+ >
20797
+ @if (item.icon) {
20798
+ <strct-icon [name]="item.icon" [size]="13" />
20799
+ }
20800
+ {{ item.label }}
20801
+ @if (item.children?.length) {
20802
+ <strct-icon class="strct-mb__caret" name="chevronRight" [size]="12" />
20803
+ }
20804
+ </button>
20805
+ @if (item.hint) {
20806
+ <span [id]="hintId(menu.id, i)" hidden>{{ item.hint }}</span>
20807
+ }
20808
+ @if (subIdx() === i && item.children?.length) {
20809
+ <div class="strct-mb__submenu" role="menu" [attr.aria-label]="item.label">
20810
+ @for (sub of item.children; track $index; let j = $index) {
20811
+ @if (sub.divider) {
20812
+ <div class="strct-mb__divider" role="separator"></div>
20813
+ } @else {
20814
+ <button
20815
+ type="button"
20816
+ class="strct-mb__item strct-mb__subitem"
20817
+ role="menuitem"
20818
+ [class.strct-mb__item--critical]="sub.critical"
20819
+ [attr.aria-disabled]="sub.disabled ? 'true' : null"
20820
+ [attr.aria-describedby]="sub.hint ? hintId(menu.id, i, j) : null"
20821
+ [attr.title]="sub.hint || null"
20822
+ (click)="pick(menu, sub)"
20823
+ >
20824
+ @if (sub.icon) {
20825
+ <strct-icon [name]="sub.icon" [size]="13" />
20826
+ }
20827
+ {{ sub.label }}
20828
+ </button>
20829
+ @if (sub.hint) {
20830
+ <span [id]="hintId(menu.id, i, j)" hidden>{{ sub.hint }}</span>
20831
+ }
20832
+ }
20833
+ }
20834
+ </div>
20835
+ }
20836
+ </div>
20837
+ }
20838
+ }
20839
+ </div>
20840
+ }
20841
+ </div>
20842
+ }
20843
+ </div>
20844
+ `, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:var(--radius-md)}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:var(--z-dropdown);min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__menu:focus{outline:none}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not([aria-disabled=true]){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not([aria-disabled=true]){background:var(--critical-bg)}.strct-mb__item[aria-disabled=true]{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}.strct-mb__row{position:relative}.strct-mb__caret{margin-inline-start:auto;color:var(--t3)}[dir=rtl] .strct-mb__caret{transform:rotate(180deg)}.strct-mb__submenu{position:absolute;top:-5px;inset-inline-start:calc(100% + 2px);z-index:var(--z-base);min-width:160px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);display:flex;flex-direction:column}\n"] }]
20845
+ }], propDecorators: { menus: [{ type: i0.Input, args: [{ isSignal: true, alias: "menus", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], onDocClick: [{
20846
+ type: HostListener,
20847
+ args: ['document:click', ['$event']]
20848
+ }], onEscape: [{
20582
20849
  type: HostListener,
20583
20850
  args: ['document:keydown.escape', ['$event']]
20584
20851
  }] } });
20585
20852
 
20586
- let reorderCounter = 0;
20853
+ function normalize(combo) {
20854
+ return combo
20855
+ .toLowerCase()
20856
+ .split('+')
20857
+ .map((p) => p.trim())
20858
+ .sort()
20859
+ .join('+');
20860
+ }
20861
+ function comboOf(event) {
20862
+ const parts = [];
20863
+ if (event.ctrlKey)
20864
+ parts.push('ctrl');
20865
+ if (event.metaKey)
20866
+ parts.push('meta');
20867
+ if (event.altKey)
20868
+ parts.push('alt');
20869
+ if (event.shiftKey)
20870
+ parts.push('shift');
20871
+ parts.push(event.key.toLowerCase());
20872
+ return parts.sort().join('+');
20873
+ }
20874
+ /** `mod` matches Ctrl (and ⌘ on mac keyboards) — expand to both. */
20875
+ function expand(combo) {
20876
+ const n = combo.toLowerCase();
20877
+ if (!n.includes('mod'))
20878
+ return [normalize(n)];
20879
+ return [normalize(n.replace('mod', 'ctrl')), normalize(n.replace('mod', 'meta'))];
20880
+ }
20587
20881
  /**
20588
- * List drag-reorder primitive — the consumer owns the array:
20589
- *
20590
- * <ul strctReorder (reordered)="move($event)">
20591
- * @for (s of steps(); track s.id) {
20592
- * <li strctReorderItem>{{ s.label }}</li>
20593
- * }
20594
- * </ul>
20882
+ * Application hotkeys, centrally registered so they are discoverable — the
20883
+ * Blueprint pattern:
20595
20884
  *
20596
- * move({ from, to }: StrctReorderEvent) {
20597
- * this.steps.update((s) => { const c = [...s]; c.splice(to, 0, ...c.splice(from, 1)); return c; });
20598
- * }
20885
+ * private hotkeys = inject(StrctHotkeysService);
20886
+ * ngOnInit() { this.dispose = this.hotkeys.register(
20887
+ * { combo: 'mod+k', description: 'Open command palette', group: 'Global',
20888
+ * handler: () => this.palette.open() }); }
20599
20889
  *
20600
- * Items are HTML5-draggable; keyboard reorder is Alt+ArrowUp / Alt+ArrowDown
20601
- * on the focused item (items get `tabindex="0"` unless they already manage
20602
- * focus). Indexes are positions among the `strctReorderItem` siblings.
20603
- * Completed moves are announced in a live region (see `announcement`) and
20604
- * items reference the sr-only `instructions` via `aria-describedby`.
20890
+ * Combos: `'a'`, `'shift+?'`, `'ctrl+k'`, `'meta+shift+p'`, `'mod+k'` (mod = Ctrl/⌘).
20891
+ * Plain-key combos are suppressed while typing in inputs / textareas /
20892
+ * contenteditable; modifier combos always fire. Pair with
20893
+ * `<strct-hotkeys-help/>` for the ⌘/ ("?") cheatsheet overlay.
20605
20894
  */
20606
- class StrctReorder {
20607
- host = inject(ElementRef);
20608
- renderer = inject(Renderer2);
20609
- doc = inject(DOCUMENT);
20610
- announcer = inject(StrctAnnouncer);
20611
- /** Emits when a drag or keyboard move completes. */
20612
- reordered = output();
20613
- /** Disable all reordering (display-only mode). */
20614
- reorderDisabled = input(false, { ...(ngDevMode ? { debugName: "reorderDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
20615
- /**
20616
- * Keyboard instructions (localizable), rendered sr-only and referenced by
20617
- * every item's `aria-describedby`. Set to '' to opt out.
20618
- */
20619
- instructions = input('Press Alt+ArrowUp or Alt+ArrowDown to move this item', /* @ts-ignore */
20620
- ...(ngDevMode ? [{ debugName: "instructions" }] : /* istanbul ignore next */ []));
20621
- /**
20622
- * Builds the live-region announcement after a move (localizable):
20623
- * (moved item's text, new 1-based position, item count).
20624
- */
20625
- announcement = input((label, position, total) => `Moved ${label} to position ${position} of ${total}`, /* @ts-ignore */
20626
- ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
20627
- dragIndex = signal(null, /* @ts-ignore */
20628
- ...(ngDevMode ? [{ debugName: "dragIndex" }] : /* istanbul ignore next */ []));
20629
- overIndex = signal(null, /* @ts-ignore */
20630
- ...(ngDevMode ? [{ debugName: "overIndex" }] : /* istanbul ignore next */ []));
20631
- /** Id of the sr-only instructions element the items point to. */
20632
- instructionsId = `strct-reorder-hint-${++reorderCounter}`;
20633
- hint = null;
20895
+ class StrctHotkeysService {
20896
+ registry = signal([], /* @ts-ignore */
20897
+ ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
20898
+ /** All registered hotkeys (for the help overlay). */
20899
+ hotkeys = this.registry.asReadonly();
20900
+ listener = (event) => this.onKeydown(event);
20634
20901
  constructor() {
20635
- // Keep the sr-only instructions element in sync with the input.
20636
- effect(() => {
20637
- const text = this.instructions();
20638
- if (text && !this.hint) {
20639
- this.hint = this.renderer.createElement('span');
20640
- this.renderer.setAttribute(this.hint, 'id', this.instructionsId);
20641
- this.hint.style.cssText =
20642
- 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
20643
- this.renderer.appendChild(this.doc.body, this.hint);
20644
- }
20645
- if (this.hint)
20646
- this.hint.textContent = text;
20647
- });
20648
- }
20649
- items() {
20650
- return [...this.host.nativeElement.querySelectorAll('[strctReorderItem]')];
20651
- }
20652
- indexOf(el) {
20653
- return this.items().indexOf(el);
20654
- }
20655
- commit(from, to) {
20656
- this.dragIndex.set(null);
20657
- this.overIndex.set(null);
20658
- if (from < 0 || to < 0 || from === to)
20659
- return;
20660
- const label = this.items()[from]?.textContent?.trim() ?? '';
20661
- this.reordered.emit({ from, to });
20662
- this.announcer.announce(this.announcement()(label, to + 1, this.items().length));
20663
- }
20664
- onContainerDragOver(event) {
20665
- // Only the container's own empty space (below the last item); drags over
20666
- // items are handled by the items themselves.
20667
- if (this.dragIndex() == null)
20668
- return;
20669
- if (event.target.closest('[strctReorderItem]'))
20670
- return;
20671
- event.preventDefault();
20902
+ document.addEventListener('keydown', this.listener);
20672
20903
  }
20673
- onContainerDrop(event) {
20674
- const from = this.dragIndex();
20675
- if (from == null)
20676
- return; // an item already consumed this drop
20677
- if (event.target.closest('[strctReorderItem]'))
20678
- return;
20679
- event.preventDefault();
20680
- // Drop into empty space moves the item to the end.
20681
- this.commit(from, this.items().length - 1);
20904
+ /** Register a hotkey; the returned function unregisters it. */
20905
+ register(hotkey) {
20906
+ this.registry.update((list) => [...list, hotkey]);
20907
+ return () => this.registry.update((list) => list.filter((h) => h !== hotkey));
20682
20908
  }
20683
- onDragLeave(event) {
20684
- // dragleave also fires when moving between children; only clear the
20685
- // highlight when the drag truly leaves the container.
20686
- const next = event.relatedTarget;
20687
- if (next && this.host.nativeElement.contains(next))
20909
+ onKeydown(event) {
20910
+ const combo = comboOf(event);
20911
+ const typing = isTyping(event);
20912
+ for (const hk of this.registry()) {
20913
+ if (!expand(hk.combo).includes(combo))
20914
+ continue;
20915
+ // While typing, only modifier combos may fire.
20916
+ if (typing && !(event.ctrlKey || event.metaKey || event.altKey))
20917
+ continue;
20918
+ event.preventDefault();
20919
+ hk.handler(event);
20688
20920
  return;
20689
- this.overIndex.set(null);
20921
+ }
20690
20922
  }
20691
20923
  ngOnDestroy() {
20692
- if (this.hint) {
20693
- this.renderer.removeChild(this.doc.body, this.hint);
20694
- this.hint = null;
20695
- }
20924
+ document.removeEventListener('keydown', this.listener);
20696
20925
  }
20697
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
20698
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: StrctReorder, isStandalone: true, selector: "[strctReorder]", inputs: { reorderDisabled: { classPropertyName: "reorderDisabled", publicName: "reorderDisabled", isSignal: true, isRequired: false, transformFunction: null }, instructions: { classPropertyName: "instructions", publicName: "instructions", isSignal: true, isRequired: false, transformFunction: null }, announcement: { classPropertyName: "announcement", publicName: "announcement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { reordered: "reordered" }, host: { listeners: { "dragover": "onContainerDragOver($event)", "drop": "onContainerDrop($event)", "dragleave": "onDragLeave($event)" } }, ngImport: i0 });
20926
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
20927
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, providedIn: 'root' });
20699
20928
  }
20700
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorder, decorators: [{
20701
- type: Directive,
20702
- args: [{ selector: '[strctReorder]' }]
20703
- }], ctorParameters: () => [], propDecorators: { reordered: [{ type: i0.Output, args: ["reordered"] }], reorderDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderDisabled", required: false }] }], instructions: [{ type: i0.Input, args: [{ isSignal: true, alias: "instructions", required: false }] }], announcement: [{ type: i0.Input, args: [{ isSignal: true, alias: "announcement", required: false }] }], onContainerDragOver: [{
20704
- type: HostListener,
20705
- args: ['dragover', ['$event']]
20706
- }], onContainerDrop: [{
20707
- type: HostListener,
20708
- args: ['drop', ['$event']]
20709
- }], onDragLeave: [{
20710
- type: HostListener,
20711
- args: ['dragleave', ['$event']]
20712
- }] } });
20713
- /** One draggable row inside a `[strctReorder]` container. */
20714
- class StrctReorderItem {
20715
- list = inject(StrctReorder);
20716
- el = inject(ElementRef);
20717
- // Captured before our own binding ever writes tabindex, so it never flips.
20718
- hadOwnTabindex = this.el.nativeElement.hasAttribute('tabindex');
20719
- hostTabindex() {
20720
- return this.hadOwnTabindex ? null : 0;
20721
- }
20722
- isDragging() {
20723
- return this.list.dragIndex() === this.index();
20724
- }
20725
- isOver() {
20726
- return this.list.overIndex() === this.index() && this.list.dragIndex() !== this.index();
20727
- }
20728
- index() {
20729
- return this.list.indexOf(this.el.nativeElement);
20730
- }
20731
- onDragStart(event) {
20732
- if (this.list.reorderDisabled())
20733
- return;
20734
- this.list.dragIndex.set(this.index());
20735
- event.dataTransfer?.setData('text/plain', String(this.index()));
20736
- if (event.dataTransfer)
20737
- event.dataTransfer.effectAllowed = 'move';
20738
- }
20739
- onDragOver(event) {
20740
- if (this.list.dragIndex() == null)
20741
- return;
20742
- event.preventDefault();
20743
- this.list.overIndex.set(this.index());
20929
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, decorators: [{
20930
+ type: Injectable,
20931
+ args: [{ providedIn: 'root' }]
20932
+ }], ctorParameters: () => [] });
20933
+ function isTyping(event) {
20934
+ const el = event.target;
20935
+ if (!el)
20936
+ return false;
20937
+ return (el.tagName === 'INPUT' ||
20938
+ el.tagName === 'TEXTAREA' ||
20939
+ el.tagName === 'SELECT' ||
20940
+ el.isContentEditable);
20941
+ }
20942
+ /**
20943
+ * The hotkey cheatsheet overlay. Mount once near the app root; it registers
20944
+ * `shift+?` (typed as `?`) itself and lists everything in the service, grouped:
20945
+ *
20946
+ * <strct-hotkeys-help />
20947
+ */
20948
+ class StrctHotkeysHelp {
20949
+ service = inject(StrctHotkeysService);
20950
+ host = inject((ElementRef));
20951
+ /** Overlay visibility (two-way; `?` toggles it too). */
20952
+ open = model(false, /* @ts-ignore */
20953
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
20954
+ /** Localizable strings. */
20955
+ title = input('Keyboard shortcuts', /* @ts-ignore */
20956
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
20957
+ emptyText = input('No shortcuts registered.', /* @ts-ignore */
20958
+ ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
20959
+ /** Accessible label of the × close button (localizable). */
20960
+ closeLabel = input('Close', /* @ts-ignore */
20961
+ ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
20962
+ /** Element that had focus before the overlay opened, restored on close. */
20963
+ previousActive = null;
20964
+ grouped = computed(() => {
20965
+ const groups = new Map();
20966
+ for (const hk of this.service.hotkeys()) {
20967
+ const name = hk.group ?? 'General';
20968
+ const list = groups.get(name);
20969
+ if (list)
20970
+ list.push(hk);
20971
+ else
20972
+ groups.set(name, [hk]);
20973
+ }
20974
+ return [...groups.entries()].map(([name, keys]) => ({ name, keys }));
20975
+ }, /* @ts-ignore */
20976
+ ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
20977
+ constructor() {
20978
+ const dispose = this.service.register({
20979
+ combo: 'shift+?',
20980
+ description: 'Show this help',
20981
+ group: 'General',
20982
+ handler: () => this.open.update((v) => !v),
20983
+ });
20984
+ inject(DestroyRef).onDestroy(dispose);
20985
+ // Focus lifecycle: remember the trigger, move focus into the dialog once
20986
+ // it has rendered, and hand focus back on close.
20987
+ effect(() => {
20988
+ if (this.open()) {
20989
+ this.previousActive = saveFocusedElement();
20990
+ setTimeout(() => {
20991
+ const dialog = this.host.nativeElement.querySelector('.strct-hkh');
20992
+ if (dialog)
20993
+ focusFirstIn(dialog);
20994
+ });
20995
+ }
20996
+ else {
20997
+ restoreFocus(this.previousActive);
20998
+ this.previousActive = null;
20999
+ }
21000
+ });
20744
21001
  }
20745
- onDrop(event) {
20746
- event.preventDefault();
20747
- const from = this.list.dragIndex();
20748
- if (from == null)
21002
+ // Escape is handled locally — registering it in the service would swallow
21003
+ // every Escape in the app (the service preventDefaults on match).
21004
+ onEscape(event) {
21005
+ if (!this.open())
20749
21006
  return;
20750
- this.list.commit(from, this.index());
21007
+ // Document-level: stopImmediatePropagation so a host modal/drawer with its
21008
+ // own document listener doesn't also see the Escape the overlay consumed.
21009
+ event.stopImmediatePropagation();
21010
+ this.open.set(false);
20751
21011
  }
20752
- onDragEnd() {
20753
- this.list.dragIndex.set(null);
20754
- this.list.overIndex.set(null);
21012
+ parts(combo) {
21013
+ return combo.split('+').map((p) => {
21014
+ const map = {
21015
+ mod: '⌘/Ctrl',
21016
+ meta: '⌘',
21017
+ ctrl: 'Ctrl',
21018
+ alt: 'Alt',
21019
+ shift: 'Shift',
21020
+ escape: 'Esc',
21021
+ };
21022
+ return map[p.trim().toLowerCase()] ?? p.trim().toUpperCase();
21023
+ });
20755
21024
  }
20756
- onKeydown(event) {
20757
- if (this.list.reorderDisabled() || !event.altKey)
20758
- return;
20759
- if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
20760
- return;
20761
- event.preventDefault();
20762
- const from = this.index();
20763
- const to = event.key === 'ArrowUp' ? from - 1 : from + 1;
20764
- if (to < 0 || to >= this.list.items().length)
20765
- return;
20766
- this.list.commit(from, to);
20767
- // Keep focus on the moved item after the consumer re-renders.
20768
- setTimeout(() => this.list.items()[to]?.focus());
21025
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysHelp, deps: [], target: i0.ɵɵFactoryTarget.Component });
21026
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctHotkeysHelp, isStandalone: true, selector: "strct-hotkeys-help", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape($event)" } }, ngImport: i0, template: `
21027
+ @if (open()) {
21028
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
21029
+ <div class="strct-hkh" role="dialog" aria-modal="true" [attr.aria-label]="title()">
21030
+ <div class="strct-hkh__head">
21031
+ <span class="strct-hkh__title">{{ title() }}</span>
21032
+ <span class="strct-hkh__headside">
21033
+ <strct-kbd>?</strct-kbd>
21034
+ <button
21035
+ type="button"
21036
+ class="strct-hkh__close"
21037
+ [attr.aria-label]="closeLabel()"
21038
+ (click)="open.set(false)"
21039
+ >
21040
+ <strct-icon name="close" [size]="14" />
21041
+ </button>
21042
+ </span>
21043
+ </div>
21044
+ @for (group of grouped(); track group.name) {
21045
+ <div class="strct-hkh__group">
21046
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
21047
+ @for (hk of group.keys; track hk.combo + hk.description) {
21048
+ <div class="strct-hkh__row">
21049
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
21050
+ <span class="strct-hkh__keys">
21051
+ @for (part of parts(hk.combo); track $index) {
21052
+ <strct-kbd>{{ part }}</strct-kbd>
21053
+ }
21054
+ </span>
21055
+ </div>
21056
+ }
21057
+ </div>
21058
+ } @empty {
21059
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
21060
+ }
21061
+ </div>
20769
21062
  }
20770
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorderItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
20771
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctReorderItem, isStandalone: true, selector: "[strctReorderItem]", host: { listeners: { "dragstart": "onDragStart($event)", "dragover": "onDragOver($event)", "drop": "onDrop($event)", "dragend": "onDragEnd()", "keydown": "onKeydown($event)" }, properties: { "attr.draggable": "!list.reorderDisabled()", "attr.tabindex": "hostTabindex()", "class.strct-reorder--dragging": "isDragging()", "class.strct-reorder--over": "isOver()", "attr.aria-roledescription": "'sortable'", "attr.aria-keyshortcuts": "'Alt+ArrowUp Alt+ArrowDown'", "attr.aria-posinset": "index() + 1", "attr.aria-setsize": "list.items().length", "attr.aria-describedby": "list.instructions() ? list.instructionsId : null" } }, ngImport: i0 });
20772
- }
20773
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorderItem, decorators: [{
20774
- type: Directive,
20775
- args: [{
20776
- selector: '[strctReorderItem]',
20777
- host: {
20778
- '[attr.draggable]': '!list.reorderDisabled()',
20779
- '[attr.tabindex]': 'hostTabindex()',
20780
- '[class.strct-reorder--dragging]': 'isDragging()',
20781
- '[class.strct-reorder--over]': 'isOver()',
20782
- '[attr.aria-roledescription]': "'sortable'",
20783
- '[attr.aria-keyshortcuts]': "'Alt+ArrowUp Alt+ArrowDown'",
20784
- '[attr.aria-posinset]': 'index() + 1',
20785
- '[attr.aria-setsize]': 'list.items().length',
20786
- '[attr.aria-describedby]': 'list.instructions() ? list.instructionsId : null',
20787
- },
20788
- }]
20789
- }], propDecorators: { onDragStart: [{
20790
- type: HostListener,
20791
- args: ['dragstart', ['$event']]
20792
- }], onDragOver: [{
20793
- type: HostListener,
20794
- args: ['dragover', ['$event']]
20795
- }], onDrop: [{
20796
- type: HostListener,
20797
- args: ['drop', ['$event']]
20798
- }], onDragEnd: [{
20799
- type: HostListener,
20800
- args: ['dragend']
20801
- }], onKeydown: [{
20802
- type: HostListener,
20803
- args: ['keydown', ['$event']]
20804
- }] } });
20805
-
20806
- /**
20807
- * Default accessible text per status, used when `label` is not given
20808
- * (localizable — override via `label`). The canonical `StrctStatus` vocabulary
20809
- * has no 'ok'/'info': OK maps to 'success', info to 'accent'.
20810
- */
20811
- const DEFAULT_LABEL = {
20812
- neutral: 'Neutral',
20813
- accent: 'Info',
20814
- success: 'OK',
20815
- warning: 'Warning',
20816
- critical: 'Critical',
20817
- };
20818
- /**
20819
- * Presence/state dot that never relies on colour alone: the tone is painted by
20820
- * a pure-CSS dot while the state is also rendered as visually-hidden text, so
20821
- * screen readers (and colour-blind users copying text) get "Warning", not just
20822
- * a yellow circle. Use it inside avatars, menu rows, metric tiles — anywhere a
20823
- * bare coloured dot would otherwise carry meaning.
20824
- *
20825
- * <strct-status-dot status="success" />
20826
- * <strct-status-dot status="critical" size="sm" label="Node unreachable" />
20827
- */
20828
- class StrctStatusDot {
20829
- /** Status tone (canonical vocabulary: OK = 'success', info = 'accent'). */
20830
- status = input('neutral', /* @ts-ignore */
20831
- ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
20832
- /**
20833
- * Accessible text describing the state (localizable). Empty falls back to a
20834
- * per-status default ("OK", "Warning", …) so the dot is never color-only.
20835
- */
20836
- label = input('', /* @ts-ignore */
20837
- ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
20838
- /** Dot size: `sm` for dense rows, `md` standalone. */
20839
- size = input('md', /* @ts-ignore */
20840
- ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
20841
- text = computed(() => this.label() || DEFAULT_LABEL[this.status()], /* @ts-ignore */
20842
- ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
20843
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctStatusDot, deps: [], target: i0.ɵɵFactoryTarget.Component });
20844
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctStatusDot, isStandalone: true, selector: "strct-status-dot", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-dot--accent": "status() === 'accent'", "class.strct-dot--success": "status() === 'success'", "class.strct-dot--warning": "status() === 'warning'", "class.strct-dot--critical": "status() === 'critical'", "class.strct-dot--sm": "size() === 'sm'" }, classAttribute: "strct-dot" }, ngImport: i0, template: `
20845
- <span class="strct-dot__dot" aria-hidden="true"></span>
20846
- <span class="strct-dot__sr">{{ text() }}</span>
20847
- `, isInline: true, styles: [".strct-dot{display:inline-flex;align-items:center;flex:none;line-height:0}.strct-dot__dot{width:10px;height:10px;border-radius:50%;background:var(--t3)}.strct-dot--sm .strct-dot__dot{width:8px;height:8px}.strct-dot--accent .strct-dot__dot{background:var(--acc)}.strct-dot--success .strct-dot__dot{background:var(--success)}.strct-dot--warning .strct-dot__dot{background:var(--warning)}.strct-dot--critical .strct-dot__dot{background:var(--critical)}.strct-dot__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
21063
+ `, isInline: true, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:var(--backdrop)}.strct-hkh{position:fixed;z-index:calc(var(--z-tour) + 1);top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__headside{display:inline-flex;align-items:center;gap:8px}.strct-hkh__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-hkh__close:hover{color:var(--t1);background:var(--bg-3)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctKbd, selector: "strct-kbd" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20848
21064
  }
20849
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctStatusDot, decorators: [{
21065
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysHelp, decorators: [{
20850
21066
  type: Component,
20851
- args: [{ selector: 'strct-status-dot', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
20852
- <span class="strct-dot__dot" aria-hidden="true"></span>
20853
- <span class="strct-dot__sr">{{ text() }}</span>
20854
- `, host: {
20855
- class: 'strct-dot',
20856
- '[class.strct-dot--accent]': "status() === 'accent'",
20857
- '[class.strct-dot--success]': "status() === 'success'",
20858
- '[class.strct-dot--warning]': "status() === 'warning'",
20859
- '[class.strct-dot--critical]': "status() === 'critical'",
20860
- '[class.strct-dot--sm]': "size() === 'sm'",
20861
- }, styles: [".strct-dot{display:inline-flex;align-items:center;flex:none;line-height:0}.strct-dot__dot{width:10px;height:10px;border-radius:50%;background:var(--t3)}.strct-dot--sm .strct-dot__dot{width:8px;height:8px}.strct-dot--accent .strct-dot__dot{background:var(--acc)}.strct-dot--success .strct-dot__dot{background:var(--success)}.strct-dot--warning .strct-dot__dot{background:var(--warning)}.strct-dot--critical .strct-dot__dot{background:var(--critical)}.strct-dot__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"] }]
20862
- }], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
21067
+ args: [{ selector: 'strct-hotkeys-help', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctKbd], template: `
21068
+ @if (open()) {
21069
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
21070
+ <div class="strct-hkh" role="dialog" aria-modal="true" [attr.aria-label]="title()">
21071
+ <div class="strct-hkh__head">
21072
+ <span class="strct-hkh__title">{{ title() }}</span>
21073
+ <span class="strct-hkh__headside">
21074
+ <strct-kbd>?</strct-kbd>
21075
+ <button
21076
+ type="button"
21077
+ class="strct-hkh__close"
21078
+ [attr.aria-label]="closeLabel()"
21079
+ (click)="open.set(false)"
21080
+ >
21081
+ <strct-icon name="close" [size]="14" />
21082
+ </button>
21083
+ </span>
21084
+ </div>
21085
+ @for (group of grouped(); track group.name) {
21086
+ <div class="strct-hkh__group">
21087
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
21088
+ @for (hk of group.keys; track hk.combo + hk.description) {
21089
+ <div class="strct-hkh__row">
21090
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
21091
+ <span class="strct-hkh__keys">
21092
+ @for (part of parts(hk.combo); track $index) {
21093
+ <strct-kbd>{{ part }}</strct-kbd>
21094
+ }
21095
+ </span>
21096
+ </div>
21097
+ }
21098
+ </div>
21099
+ } @empty {
21100
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
21101
+ }
21102
+ </div>
21103
+ }
21104
+ `, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:var(--z-tour);background:var(--backdrop)}.strct-hkh{position:fixed;z-index:calc(var(--z-tour) + 1);top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__headside{display:inline-flex;align-items:center;gap:8px}.strct-hkh__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-hkh__close:hover{color:var(--t1);background:var(--bg-3)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"] }]
21105
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], onEscape: [{
21106
+ type: HostListener,
21107
+ args: ['document:keydown.escape', ['$event']]
21108
+ }] } });
20863
21109
 
21110
+ let reorderCounter = 0;
20864
21111
  /**
20865
- * Numeric stepper input: − / + buttons flanking a text field. The value is
20866
- * `number | null` (null = empty); typing is free-form (intermediate states
20867
- * like "-" are kept), committed values clamp to `min`/`max` on blur, and the
20868
- * buttons + keyboard always clamp immediately.
21112
+ * List drag-reorder primitive — the consumer owns the array:
20869
21113
  *
20870
- * <strct-number [min]="0" [max]="64" [step]="4" [(ngModel)]="vcpus" />
21114
+ * <ul strctReorder (reordered)="move($event)">
21115
+ * @for (s of steps(); track s.id) {
21116
+ * <li strctReorderItem>{{ s.label }}</li>
21117
+ * }
21118
+ * </ul>
20871
21119
  *
20872
- * Keyboard on the field: ArrowUp/Down ±step, PageUp/Down ±10×step,
20873
- * Home/End jump to min/max. The field exposes `role="spinbutton"` with
20874
- * aria-valuemin/max/now so assistive tech announces it as a number control.
21120
+ * move({ from, to }: StrctReorderEvent) {
21121
+ * this.steps.update((s) => { const c = [...s]; c.splice(to, 0, ...c.splice(from, 1)); return c; });
21122
+ * }
21123
+ *
21124
+ * Items are HTML5-draggable; keyboard reorder is Alt+ArrowUp / Alt+ArrowDown
21125
+ * on the focused item (items get `tabindex="0"` unless they already manage
21126
+ * focus). Indexes are positions among the `strctReorderItem` siblings.
21127
+ * Completed moves are announced in a live region (see `announcement`) and
21128
+ * items reference the sr-only `instructions` via `aria-describedby`.
20875
21129
  */
20876
- class StrctNumber {
20877
- /** Minimum allowed value; null = unbounded. */
20878
- min = input(null, /* @ts-ignore */
20879
- ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
20880
- /** Maximum allowed value; null = unbounded. */
20881
- max = input(null, /* @ts-ignore */
20882
- ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
20883
- /** Step increment for buttons and Arrow keys (PageUp/Down use 10×). */
20884
- step = input(1, /* @ts-ignore */
20885
- ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
20886
- /** Placeholder text when empty. */
20887
- placeholder = input('', /* @ts-ignore */
20888
- ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
20889
- /** Accessible label of the + button (localizable). */
20890
- incrementLabel = input('Increment', /* @ts-ignore */
20891
- ...(ngDevMode ? [{ debugName: "incrementLabel" }] : /* istanbul ignore next */ []));
20892
- /** Accessible label of the − button (localizable). */
20893
- decrementLabel = input('Decrement', /* @ts-ignore */
20894
- ...(ngDevMode ? [{ debugName: "decrementLabel" }] : /* istanbul ignore next */ []));
20895
- /** Static disable; forms' setDisabledState also drives the disabled state. */
20896
- disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
20897
- value = signal(null, /* @ts-ignore */
20898
- ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
20899
- /** Raw field text — may hold an unparseable intermediate state while typing. */
20900
- text = signal('', /* @ts-ignore */
20901
- ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
20902
- /** Disabled state pushed by the forms API (setDisabledState). */
20903
- cvaDisabled = signal(false, /* @ts-ignore */
20904
- ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
20905
- isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
20906
- ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
20907
- /** At/below min (or min unset → never) — greys out the − button. */
20908
- atMin = computed(() => {
20909
- const v = this.value();
20910
- const m = this.min();
20911
- return v !== null && m !== null && v <= m;
20912
- }, /* @ts-ignore */
20913
- ...(ngDevMode ? [{ debugName: "atMin" }] : /* istanbul ignore next */ []));
20914
- /** At/above max (or max unset → never) — greys out the + button. */
20915
- atMax = computed(() => {
20916
- const v = this.value();
20917
- const m = this.max();
20918
- return v !== null && m !== null && v >= m;
20919
- }, /* @ts-ignore */
20920
- ...(ngDevMode ? [{ debugName: "atMax" }] : /* istanbul ignore next */ []));
20921
- onChange = () => { };
20922
- onTouched = () => { };
20923
- onInput(event) {
20924
- const raw = event.target.value;
20925
- this.text.set(raw);
20926
- const trimmed = raw.trim();
20927
- if (trimmed === '') {
20928
- this.value.set(null);
20929
- this.onChange(null);
21130
+ class StrctReorder {
21131
+ host = inject(ElementRef);
21132
+ renderer = inject(Renderer2);
21133
+ doc = inject(DOCUMENT);
21134
+ announcer = inject(StrctAnnouncer);
21135
+ /** Emits when a drag or keyboard move completes. */
21136
+ reordered = output();
21137
+ /** Disable all reordering (display-only mode). */
21138
+ reorderDisabled = input(false, { ...(ngDevMode ? { debugName: "reorderDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
21139
+ /**
21140
+ * Keyboard instructions (localizable), rendered sr-only and referenced by
21141
+ * every item's `aria-describedby`. Set to '' to opt out.
21142
+ */
21143
+ instructions = input('Press Alt+ArrowUp or Alt+ArrowDown to move this item', /* @ts-ignore */
21144
+ ...(ngDevMode ? [{ debugName: "instructions" }] : /* istanbul ignore next */ []));
21145
+ /**
21146
+ * Builds the live-region announcement after a move (localizable):
21147
+ * (moved item's text, new 1-based position, item count).
21148
+ */
21149
+ announcement = input((label, position, total) => `Moved ${label} to position ${position} of ${total}`, /* @ts-ignore */
21150
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
21151
+ dragIndex = signal(null, /* @ts-ignore */
21152
+ ...(ngDevMode ? [{ debugName: "dragIndex" }] : /* istanbul ignore next */ []));
21153
+ overIndex = signal(null, /* @ts-ignore */
21154
+ ...(ngDevMode ? [{ debugName: "overIndex" }] : /* istanbul ignore next */ []));
21155
+ /** Id of the sr-only instructions element the items point to. */
21156
+ instructionsId = `strct-reorder-hint-${++reorderCounter}`;
21157
+ hint = null;
21158
+ constructor() {
21159
+ // Keep the sr-only instructions element in sync with the input.
21160
+ effect(() => {
21161
+ const text = this.instructions();
21162
+ if (text && !this.hint) {
21163
+ this.hint = this.renderer.createElement('span');
21164
+ this.renderer.setAttribute(this.hint, 'id', this.instructionsId);
21165
+ this.hint.style.cssText =
21166
+ 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
21167
+ this.renderer.appendChild(this.doc.body, this.hint);
21168
+ }
21169
+ if (this.hint)
21170
+ this.hint.textContent = text;
21171
+ });
21172
+ }
21173
+ items() {
21174
+ return [...this.host.nativeElement.querySelectorAll('[strctReorderItem]')];
21175
+ }
21176
+ indexOf(el) {
21177
+ return this.items().indexOf(el);
21178
+ }
21179
+ commit(from, to) {
21180
+ this.dragIndex.set(null);
21181
+ this.overIndex.set(null);
21182
+ if (from < 0 || to < 0 || from === to)
20930
21183
  return;
20931
- }
20932
- const parsed = Number(trimmed);
20933
- // Intermediate states ("-", "1.", "2e") don't parse yet — keep the text,
20934
- // hold the last value; clamping happens on blur.
20935
- if (Number.isFinite(parsed)) {
20936
- this.value.set(parsed);
20937
- this.onChange(parsed);
20938
- }
21184
+ const label = this.items()[from]?.textContent?.trim() ?? '';
21185
+ this.reordered.emit({ from, to });
21186
+ this.announcer.announce(this.announcement()(label, to + 1, this.items().length));
20939
21187
  }
20940
- onKeydown(event) {
20941
- if (this.isDisabled())
21188
+ onContainerDragOver(event) {
21189
+ // Only the container's own empty space (below the last item); drags over
21190
+ // items are handled by the items themselves.
21191
+ if (this.dragIndex() == null)
20942
21192
  return;
20943
- switch (event.key) {
20944
- case 'ArrowUp':
20945
- event.preventDefault();
20946
- this.stepBy(1);
20947
- break;
20948
- case 'ArrowDown':
20949
- event.preventDefault();
20950
- this.stepBy(-1);
20951
- break;
20952
- case 'PageUp':
20953
- event.preventDefault();
20954
- this.stepBy(1, 10);
20955
- break;
20956
- case 'PageDown':
20957
- event.preventDefault();
20958
- this.stepBy(-1, 10);
20959
- break;
20960
- case 'Home': {
20961
- const m = this.min();
20962
- if (m === null)
20963
- return;
20964
- event.preventDefault();
20965
- this.commitTo(m);
20966
- break;
20967
- }
20968
- case 'End': {
20969
- const m = this.max();
20970
- if (m === null)
20971
- return;
20972
- event.preventDefault();
20973
- this.commitTo(m);
20974
- break;
20975
- }
20976
- }
21193
+ if (event.target.closest('[strctReorderItem]'))
21194
+ return;
21195
+ event.preventDefault();
20977
21196
  }
20978
- /** Commit the pending text on blur: parse, clamp, restore on garbage. */
20979
- onBlur() {
20980
- const trimmed = this.text().trim();
20981
- if (trimmed === '') {
20982
- if (this.value() !== null) {
20983
- this.value.set(null);
20984
- this.onChange(null);
20985
- }
20986
- this.text.set('');
20987
- }
20988
- else {
20989
- const parsed = Number(trimmed);
20990
- if (Number.isFinite(parsed)) {
20991
- const clamped = this.clamp(parsed);
20992
- this.value.set(clamped);
20993
- if (clamped !== parsed)
20994
- this.onChange(clamped);
20995
- this.text.set(String(clamped));
20996
- }
20997
- else {
20998
- // Unparseable — restore the last committed value.
20999
- this.text.set(this.value() === null ? '' : String(this.value()));
21000
- }
21197
+ onContainerDrop(event) {
21198
+ const from = this.dragIndex();
21199
+ if (from == null)
21200
+ return; // an item already consumed this drop
21201
+ if (event.target.closest('[strctReorderItem]'))
21202
+ return;
21203
+ event.preventDefault();
21204
+ // Drop into empty space moves the item to the end.
21205
+ this.commit(from, this.items().length - 1);
21206
+ }
21207
+ onDragLeave(event) {
21208
+ // dragleave also fires when moving between children; only clear the
21209
+ // highlight when the drag truly leaves the container.
21210
+ const next = event.relatedTarget;
21211
+ if (next && this.host.nativeElement.contains(next))
21212
+ return;
21213
+ this.overIndex.set(null);
21214
+ }
21215
+ ngOnDestroy() {
21216
+ if (this.hint) {
21217
+ this.renderer.removeChild(this.doc.body, this.hint);
21218
+ this.hint = null;
21001
21219
  }
21002
- this.onTouched();
21003
21220
  }
21004
- /** Button / keyboard step: an empty field first lands on min (or 0). */
21005
- stepBy(dir, factor = 1) {
21006
- if (this.isDisabled())
21007
- return;
21008
- const v = this.value();
21009
- if (v === null) {
21010
- this.commitTo(this.min() ?? 0);
21011
- return;
21012
- }
21013
- this.commitTo(v + dir * factor * this.step());
21221
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
21222
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: StrctReorder, isStandalone: true, selector: "[strctReorder]", inputs: { reorderDisabled: { classPropertyName: "reorderDisabled", publicName: "reorderDisabled", isSignal: true, isRequired: false, transformFunction: null }, instructions: { classPropertyName: "instructions", publicName: "instructions", isSignal: true, isRequired: false, transformFunction: null }, announcement: { classPropertyName: "announcement", publicName: "announcement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { reordered: "reordered" }, host: { listeners: { "dragover": "onContainerDragOver($event)", "drop": "onContainerDrop($event)", "dragleave": "onDragLeave($event)" } }, ngImport: i0 });
21223
+ }
21224
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorder, decorators: [{
21225
+ type: Directive,
21226
+ args: [{ selector: '[strctReorder]' }]
21227
+ }], ctorParameters: () => [], propDecorators: { reordered: [{ type: i0.Output, args: ["reordered"] }], reorderDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderDisabled", required: false }] }], instructions: [{ type: i0.Input, args: [{ isSignal: true, alias: "instructions", required: false }] }], announcement: [{ type: i0.Input, args: [{ isSignal: true, alias: "announcement", required: false }] }], onContainerDragOver: [{
21228
+ type: HostListener,
21229
+ args: ['dragover', ['$event']]
21230
+ }], onContainerDrop: [{
21231
+ type: HostListener,
21232
+ args: ['drop', ['$event']]
21233
+ }], onDragLeave: [{
21234
+ type: HostListener,
21235
+ args: ['dragleave', ['$event']]
21236
+ }] } });
21237
+ /** One draggable row inside a `[strctReorder]` container. */
21238
+ class StrctReorderItem {
21239
+ list = inject(StrctReorder);
21240
+ el = inject(ElementRef);
21241
+ // Captured before our own binding ever writes tabindex, so it never flips.
21242
+ hadOwnTabindex = this.el.nativeElement.hasAttribute('tabindex');
21243
+ hostTabindex() {
21244
+ return this.hadOwnTabindex ? null : 0;
21245
+ }
21246
+ isDragging() {
21247
+ return this.list.dragIndex() === this.index();
21014
21248
  }
21015
- commitTo(n) {
21016
- const clamped = this.clamp(this.snap(n));
21017
- this.value.set(clamped);
21018
- this.text.set(String(clamped));
21019
- this.onChange(clamped);
21020
- this.onTouched();
21249
+ isOver() {
21250
+ return this.list.overIndex() === this.index() && this.list.dragIndex() !== this.index();
21021
21251
  }
21022
- clamp(n) {
21023
- const lo = this.min();
21024
- const hi = this.max();
21025
- if (lo !== null && n < lo)
21026
- return lo;
21027
- if (hi !== null && n > hi)
21028
- return hi;
21029
- return n;
21252
+ index() {
21253
+ return this.list.indexOf(this.el.nativeElement);
21030
21254
  }
21031
- /** Trim float dust from step arithmetic (0.1 + 0.2), not a grid snap. */
21032
- snap(n) {
21033
- const step = this.step();
21034
- if (!Number.isFinite(step) || step <= 0)
21035
- return n;
21036
- const decimals = (String(step).split('.')[1] ?? '').length;
21037
- return Number(n.toFixed(Math.min(decimals + 2, 12)));
21255
+ onDragStart(event) {
21256
+ if (this.list.reorderDisabled())
21257
+ return;
21258
+ this.list.dragIndex.set(this.index());
21259
+ event.dataTransfer?.setData('text/plain', String(this.index()));
21260
+ if (event.dataTransfer)
21261
+ event.dataTransfer.effectAllowed = 'move';
21038
21262
  }
21039
- writeValue(value) {
21040
- const parsed = value === null || value === undefined ? null : Number(value);
21041
- const v = parsed !== null && Number.isFinite(parsed) ? this.clamp(parsed) : null;
21042
- this.value.set(v);
21043
- this.text.set(v === null ? '' : String(v));
21263
+ onDragOver(event) {
21264
+ if (this.list.dragIndex() == null)
21265
+ return;
21266
+ event.preventDefault();
21267
+ this.list.overIndex.set(this.index());
21044
21268
  }
21045
- registerOnChange(fn) {
21046
- this.onChange = fn;
21269
+ onDrop(event) {
21270
+ event.preventDefault();
21271
+ const from = this.list.dragIndex();
21272
+ if (from == null)
21273
+ return;
21274
+ this.list.commit(from, this.index());
21047
21275
  }
21048
- registerOnTouched(fn) {
21049
- this.onTouched = fn;
21276
+ onDragEnd() {
21277
+ this.list.dragIndex.set(null);
21278
+ this.list.overIndex.set(null);
21050
21279
  }
21051
- setDisabledState(isDisabled) {
21052
- this.cvaDisabled.set(isDisabled);
21280
+ onKeydown(event) {
21281
+ if (this.list.reorderDisabled() || !event.altKey)
21282
+ return;
21283
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
21284
+ return;
21285
+ event.preventDefault();
21286
+ const from = this.index();
21287
+ const to = event.key === 'ArrowUp' ? from - 1 : from + 1;
21288
+ if (to < 0 || to >= this.list.items().length)
21289
+ return;
21290
+ this.list.commit(from, to);
21291
+ // Keep focus on the moved item after the consumer re-renders.
21292
+ setTimeout(() => this.list.items()[to]?.focus());
21053
21293
  }
21054
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctNumber, deps: [], target: i0.ɵɵFactoryTarget.Component });
21055
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctNumber, isStandalone: true, selector: "strct-number", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, incrementLabel: { classPropertyName: "incrementLabel", publicName: "incrementLabel", isSignal: true, isRequired: false, transformFunction: null }, decrementLabel: { classPropertyName: "decrementLabel", publicName: "decrementLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-num" }, providers: [
21056
- { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctNumber), multi: true },
21057
- ], ngImport: i0, template: `
21058
- <div class="strct-num__wrap">
21059
- <button
21060
- type="button"
21061
- class="strct-num__btn"
21062
- [attr.aria-label]="decrementLabel()"
21063
- [disabled]="isDisabled() || atMin()"
21064
- (click)="stepBy(-1)"
21065
- >
21066
- <strct-icon strictName="minus" [size]="12" [strokeWidth]="1.8" />
21067
- </button>
21068
- <input
21069
- type="text"
21070
- inputmode="decimal"
21071
- class="strct-control strct-num__input"
21072
- role="spinbutton"
21073
- [attr.aria-valuemin]="min()"
21074
- [attr.aria-valuemax]="max()"
21075
- [attr.aria-valuenow]="value()"
21076
- [placeholder]="placeholder()"
21077
- [value]="text()"
21078
- [disabled]="isDisabled()"
21079
- (input)="onInput($event)"
21080
- (keydown)="onKeydown($event)"
21081
- (blur)="onBlur()"
21082
- />
21083
- <button
21084
- type="button"
21085
- class="strct-num__btn"
21086
- [attr.aria-label]="incrementLabel()"
21087
- [disabled]="isDisabled() || atMax()"
21088
- (click)="stepBy(1)"
21089
- >
21090
- <strct-icon strictName="plus" [size]="12" [strokeWidth]="1.8" />
21091
- </button>
21092
- </div>
21093
- `, isInline: true, styles: [".strct-num{display:block;width:100%}.strct-num__wrap{display:flex;align-items:stretch;gap:var(--space-1)}.strct-num__input{flex:1;min-width:0;text-align:center;font-variant-numeric:tabular-nums}.strct-num__btn{display:inline-flex;align-items:center;justify-content:center;flex:none;padding-inline:var(--space-2);color:var(--t2);background:var(--bg-2);border:1px solid var(--b2);border-radius:var(--radius-md);cursor:pointer;transition:border-color .14s ease,color .14s ease,background .14s ease}.strct-num__btn:hover:not(:disabled){color:var(--t1);border-color:var(--b3);background:var(--bg-3)}.strct-num__btn:disabled{opacity:var(--disabled-opacity);cursor:not-allowed}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
21294
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorderItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
21295
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctReorderItem, isStandalone: true, selector: "[strctReorderItem]", host: { listeners: { "dragstart": "onDragStart($event)", "dragover": "onDragOver($event)", "drop": "onDrop($event)", "dragend": "onDragEnd()", "keydown": "onKeydown($event)" }, properties: { "attr.draggable": "!list.reorderDisabled()", "attr.tabindex": "hostTabindex()", "class.strct-reorder--dragging": "isDragging()", "class.strct-reorder--over": "isOver()", "attr.aria-roledescription": "'sortable'", "attr.aria-keyshortcuts": "'Alt+ArrowUp Alt+ArrowDown'", "attr.aria-posinset": "index() + 1", "attr.aria-setsize": "list.items().length", "attr.aria-describedby": "list.instructions() ? list.instructionsId : null" } }, ngImport: i0 });
21094
21296
  }
21095
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctNumber, decorators: [{
21297
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorderItem, decorators: [{
21298
+ type: Directive,
21299
+ args: [{
21300
+ selector: '[strctReorderItem]',
21301
+ host: {
21302
+ '[attr.draggable]': '!list.reorderDisabled()',
21303
+ '[attr.tabindex]': 'hostTabindex()',
21304
+ '[class.strct-reorder--dragging]': 'isDragging()',
21305
+ '[class.strct-reorder--over]': 'isOver()',
21306
+ '[attr.aria-roledescription]': "'sortable'",
21307
+ '[attr.aria-keyshortcuts]': "'Alt+ArrowUp Alt+ArrowDown'",
21308
+ '[attr.aria-posinset]': 'index() + 1',
21309
+ '[attr.aria-setsize]': 'list.items().length',
21310
+ '[attr.aria-describedby]': 'list.instructions() ? list.instructionsId : null',
21311
+ },
21312
+ }]
21313
+ }], propDecorators: { onDragStart: [{
21314
+ type: HostListener,
21315
+ args: ['dragstart', ['$event']]
21316
+ }], onDragOver: [{
21317
+ type: HostListener,
21318
+ args: ['dragover', ['$event']]
21319
+ }], onDrop: [{
21320
+ type: HostListener,
21321
+ args: ['drop', ['$event']]
21322
+ }], onDragEnd: [{
21323
+ type: HostListener,
21324
+ args: ['dragend']
21325
+ }], onKeydown: [{
21326
+ type: HostListener,
21327
+ args: ['keydown', ['$event']]
21328
+ }] } });
21329
+
21330
+ /**
21331
+ * Default accessible text per status, used when `label` is not given
21332
+ * (localizable — override via `label`). The canonical `StrctStatus` vocabulary
21333
+ * has no 'ok'/'info': OK maps to 'success', info to 'accent'.
21334
+ */
21335
+ const DEFAULT_LABEL = {
21336
+ neutral: 'Neutral',
21337
+ accent: 'Info',
21338
+ success: 'OK',
21339
+ warning: 'Warning',
21340
+ critical: 'Critical',
21341
+ };
21342
+ /**
21343
+ * Presence/state dot that never relies on colour alone: the tone is painted by
21344
+ * a pure-CSS dot while the state is also rendered as visually-hidden text, so
21345
+ * screen readers (and colour-blind users copying text) get "Warning", not just
21346
+ * a yellow circle. Use it inside avatars, menu rows, metric tiles — anywhere a
21347
+ * bare coloured dot would otherwise carry meaning.
21348
+ *
21349
+ * <strct-status-dot status="success" />
21350
+ * <strct-status-dot status="critical" size="sm" label="Node unreachable" />
21351
+ */
21352
+ class StrctStatusDot {
21353
+ /** Status tone (canonical vocabulary: OK = 'success', info = 'accent'). */
21354
+ status = input('neutral', /* @ts-ignore */
21355
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
21356
+ /**
21357
+ * Accessible text describing the state (localizable). Empty falls back to a
21358
+ * per-status default ("OK", "Warning", …) so the dot is never color-only.
21359
+ */
21360
+ label = input('', /* @ts-ignore */
21361
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
21362
+ /** Dot size: `sm` for dense rows, `md` standalone. */
21363
+ size = input('md', /* @ts-ignore */
21364
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
21365
+ text = computed(() => this.label() || DEFAULT_LABEL[this.status()], /* @ts-ignore */
21366
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
21367
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctStatusDot, deps: [], target: i0.ɵɵFactoryTarget.Component });
21368
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctStatusDot, isStandalone: true, selector: "strct-status-dot", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-dot--accent": "status() === 'accent'", "class.strct-dot--success": "status() === 'success'", "class.strct-dot--warning": "status() === 'warning'", "class.strct-dot--critical": "status() === 'critical'", "class.strct-dot--sm": "size() === 'sm'" }, classAttribute: "strct-dot" }, ngImport: i0, template: `
21369
+ <span class="strct-dot__dot" aria-hidden="true"></span>
21370
+ <span class="strct-dot__sr">{{ text() }}</span>
21371
+ `, isInline: true, styles: [".strct-dot{display:inline-flex;align-items:center;flex:none;line-height:0}.strct-dot__dot{width:10px;height:10px;border-radius:50%;background:var(--t3)}.strct-dot--sm .strct-dot__dot{width:8px;height:8px}.strct-dot--accent .strct-dot__dot{background:var(--acc)}.strct-dot--success .strct-dot__dot{background:var(--success)}.strct-dot--warning .strct-dot__dot{background:var(--warning)}.strct-dot--critical .strct-dot__dot{background:var(--critical)}.strct-dot__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
21372
+ }
21373
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctStatusDot, decorators: [{
21096
21374
  type: Component,
21097
- args: [{ selector: 'strct-number', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], providers: [
21098
- { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctNumber), multi: true },
21099
- ], template: `
21100
- <div class="strct-num__wrap">
21101
- <button
21102
- type="button"
21103
- class="strct-num__btn"
21104
- [attr.aria-label]="decrementLabel()"
21105
- [disabled]="isDisabled() || atMin()"
21106
- (click)="stepBy(-1)"
21107
- >
21108
- <strct-icon strictName="minus" [size]="12" [strokeWidth]="1.8" />
21109
- </button>
21110
- <input
21111
- type="text"
21112
- inputmode="decimal"
21113
- class="strct-control strct-num__input"
21114
- role="spinbutton"
21115
- [attr.aria-valuemin]="min()"
21116
- [attr.aria-valuemax]="max()"
21117
- [attr.aria-valuenow]="value()"
21118
- [placeholder]="placeholder()"
21119
- [value]="text()"
21120
- [disabled]="isDisabled()"
21121
- (input)="onInput($event)"
21122
- (keydown)="onKeydown($event)"
21123
- (blur)="onBlur()"
21124
- />
21125
- <button
21126
- type="button"
21127
- class="strct-num__btn"
21128
- [attr.aria-label]="incrementLabel()"
21129
- [disabled]="isDisabled() || atMax()"
21130
- (click)="stepBy(1)"
21131
- >
21132
- <strct-icon strictName="plus" [size]="12" [strokeWidth]="1.8" />
21133
- </button>
21134
- </div>
21135
- `, host: { class: 'strct-num' }, styles: [".strct-num{display:block;width:100%}.strct-num__wrap{display:flex;align-items:stretch;gap:var(--space-1)}.strct-num__input{flex:1;min-width:0;text-align:center;font-variant-numeric:tabular-nums}.strct-num__btn{display:inline-flex;align-items:center;justify-content:center;flex:none;padding-inline:var(--space-2);color:var(--t2);background:var(--bg-2);border:1px solid var(--b2);border-radius:var(--radius-md);cursor:pointer;transition:border-color .14s ease,color .14s ease,background .14s ease}.strct-num__btn:hover:not(:disabled){color:var(--t1);border-color:var(--b3);background:var(--bg-3)}.strct-num__btn:disabled{opacity:var(--disabled-opacity);cursor:not-allowed}\n"] }]
21136
- }], propDecorators: { min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], incrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementLabel", required: false }] }], decrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
21375
+ args: [{ selector: 'strct-status-dot', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
21376
+ <span class="strct-dot__dot" aria-hidden="true"></span>
21377
+ <span class="strct-dot__sr">{{ text() }}</span>
21378
+ `, host: {
21379
+ class: 'strct-dot',
21380
+ '[class.strct-dot--accent]': "status() === 'accent'",
21381
+ '[class.strct-dot--success]': "status() === 'success'",
21382
+ '[class.strct-dot--warning]': "status() === 'warning'",
21383
+ '[class.strct-dot--critical]': "status() === 'critical'",
21384
+ '[class.strct-dot--sm]': "size() === 'sm'",
21385
+ }, styles: [".strct-dot{display:inline-flex;align-items:center;flex:none;line-height:0}.strct-dot__dot{width:10px;height:10px;border-radius:50%;background:var(--t3)}.strct-dot--sm .strct-dot__dot{width:8px;height:8px}.strct-dot--accent .strct-dot__dot{background:var(--acc)}.strct-dot--success .strct-dot__dot{background:var(--success)}.strct-dot--warning .strct-dot__dot{background:var(--warning)}.strct-dot--critical .strct-dot__dot{background:var(--critical)}.strct-dot__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"] }]
21386
+ }], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
21137
21387
 
21138
21388
  /**
21139
21389
  * A generic anchored overlay panel for arbitrary projected content — the
@@ -23001,5 +23251,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
23001
23251
  * Generated bundle index. Do not edit.
23002
23252
  */
23003
23253
 
23004
- export { FOCUSABLE_SELECTOR, STRCT_DP_DOW, STRCT_DP_DOW_FULL, STRCT_DP_MONTHS, STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_NOTIFICATION_HISTORY_LIMIT, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, STRCT_WIZARD_DEFAULTS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCardHeaderActions, StrctCardHeaderMeta, StrctCardHeaderNote, StrctCascadeColumn, StrctCascadeHost, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChart, StrctCheckbox, StrctChips, StrctCode, StrctCodeInline, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctConfirmOutlet, StrctConfirmService, StrctContextMenu, StrctContextMenuTrigger, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatagridActionBarCaption, StrctDatepicker, StrctDatetimePicker, StrctDesc, StrctDescriptionList, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFile, StrctFilterBar, StrctFlow, StrctFooter, StrctGauge, StrctHeader, StrctHeatmap, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInlineEdit, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctList, StrctListItem, StrctListItemDescription, StrctListItemLeading, StrctListItemMeta, StrctListItemTrailing, StrctLogViewer, StrctLogin, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctModalContent, StrctNav, StrctNavItem, StrctNotificationCenter, StrctNumber, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPageHeaderTitleMeta, StrctPagination, StrctPassword, StrctPopover, StrctPopoverTrigger, StrctProgress, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderItem, StrctRowDetailDef, StrctSearchbox, StrctSectionHeader, StrctSectionHeaderActions, StrctSectionHeaderMeta, StrctSectionMenu, StrctSegmented, StrctSelect, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSignpostTrigger, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStatusDot, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctToolbar, StrctToolbarCaption, StrctToolbarSpacer, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctTreeSelect, StrctVerticalNav, StrctWatermark, StrctWizard, StrctWizardAside, focusFirstIn, keepTabInside, lockBodyScroll, parseAnsi, provideStrctWizardDefaults, registerStrctIcon, restoreFocus, saveFocusedElement, strctComputeDiff, strctDpPad, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctValidationIcon, strctValidationTone, unlockBodyScroll };
23254
+ export { FOCUSABLE_SELECTOR, STRCT_DP_DOW, STRCT_DP_DOW_FULL, STRCT_DP_MONTHS, STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_NOTIFICATION_HISTORY_LIMIT, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, STRCT_WIZARD_DEFAULTS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCardHeaderActions, StrctCardHeaderMeta, StrctCardHeaderNote, StrctCascadeColumn, StrctCascadeHost, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChart, StrctCheckbox, StrctChips, StrctCode, StrctCodeInline, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctConfirmOutlet, StrctConfirmService, StrctContextMenu, StrctContextMenuTrigger, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatagridActionBarCaption, StrctDatepicker, StrctDatetimePicker, StrctDesc, StrctDescriptionList, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFieldGroup, StrctFieldHint, StrctFieldPrefix, StrctFieldSuffix, StrctFile, StrctFilterBar, StrctFlow, StrctFooter, StrctGauge, StrctHeader, StrctHeatmap, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInlineEdit, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctList, StrctListItem, StrctListItemDescription, StrctListItemLeading, StrctListItemMeta, StrctListItemTrailing, StrctLogViewer, StrctLogin, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctModalContent, StrctNav, StrctNavItem, StrctNotificationCenter, StrctNumber, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPageHeaderTitleMeta, StrctPagination, StrctPassword, StrctPopover, StrctPopoverTrigger, StrctProgress, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderItem, StrctRowDetailDef, StrctSearchbox, StrctSectionHeader, StrctSectionHeaderActions, StrctSectionHeaderMeta, StrctSectionMenu, StrctSegmented, StrctSelect, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSignpostTrigger, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStatusDot, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctToolbar, StrctToolbarCaption, StrctToolbarSpacer, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctTreeSelect, StrctVerticalNav, StrctWatermark, StrctWizard, StrctWizardAside, focusFirstIn, keepTabInside, lockBodyScroll, parseAnsi, provideStrctWizardDefaults, registerStrctIcon, restoreFocus, saveFocusedElement, strctComputeDiff, strctDpPad, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctValidationIcon, strctValidationTone, unlockBodyScroll };
23005
23255
  //# sourceMappingURL=akcelik-strct.mjs.map