@akcelik/strct 4.9.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.
@@ -11853,194 +11853,468 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
11853
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 }] }] } });
11854
11854
 
11855
11855
  /**
11856
- * 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.
11857
11860
  *
11858
- * An .xlsx file is a ZIP of SpreadsheetML parts. This builds the five
11859
- * required parts with inline strings and zips them with STORED (uncompressed)
11860
- * entries — no runtime dependency, byte-accurate CRC32, opens in Excel,
11861
- * 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.
11862
11866
  */
11863
- const CRC_TABLE = (() => {
11864
- const t = new Uint32Array(256);
11865
- for (let n = 0; n < 256; n++) {
11866
- let c = n;
11867
- for (let k = 0; k < 8; k++)
11868
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
11869
- 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
+ }
11870
11930
  }
11871
- return t;
11872
- })();
11873
- function crc32(data) {
11874
- let c = 0xffffffff;
11875
- for (const byte of data)
11876
- c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
11877
- return (c ^ 0xffffffff) >>> 0;
11878
- }
11879
- const enc = new TextEncoder();
11880
- /** ZIP with STORED entries (no compression — spreadsheets are small). */
11881
- function zip(entries) {
11882
- const chunks = [];
11883
- const central = [];
11884
- let offset = 0;
11885
- const u16 = (n) => new Uint8Array([n & 0xff, (n >> 8) & 0xff]);
11886
- const u32 = (n) => new Uint8Array([n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]);
11887
- const cat = (...parts) => {
11888
- const total = parts.reduce((s, p) => s + p.length, 0);
11889
- const out = new Uint8Array(total);
11890
- let o = 0;
11891
- for (const p of parts) {
11892
- out.set(p, o);
11893
- 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
+ }
11894
11967
  }
11895
- return out;
11896
- };
11897
- for (const { name, data } of entries) {
11898
- const nameBytes = enc.encode(name);
11899
- const crc = crc32(data);
11900
- const local = cat(u32(0x04034b50), u16(20), // version needed
11901
- u16(0), // flags
11902
- u16(0), // method: stored
11903
- u16(0), u16(0), // mod time/date
11904
- u32(crc), u32(data.length), u32(data.length), u16(nameBytes.length), u16(0), nameBytes, data);
11905
- 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));
11906
- chunks.push(local);
11907
- offset += local.length;
11908
11968
  }
11909
- const centralStart = offset;
11910
- const centralBytes = cat(...central);
11911
- const end = cat(u32(0x06054b50), u16(0), u16(0), u16(entries.length), u16(entries.length), u32(centralBytes.length), u32(centralStart), u16(0));
11912
- return cat(...chunks, centralBytes, end);
11913
- }
11914
- const xml = (s) => String(s ?? '')
11915
- .replace(/&/g, '&amp;')
11916
- .replace(/</g, '&lt;')
11917
- .replace(/>/g, '&gt;')
11918
- .replace(/"/g, '&quot;');
11919
- /** A1-style column ref: 0 → A, 26 → AA … */
11920
- function colRef(i) {
11921
- let s = '';
11922
- for (let n = i; n >= 0; n = Math.floor(n / 26) - 1)
11923
- s = String.fromCharCode(65 + (n % 26)) + s;
11924
- return s;
11925
- }
11926
- /**
11927
- * Build a single-sheet workbook: `header` as a bold-free first row, then
11928
- * `rows`. Numbers stay numeric; everything else exports as inline strings.
11929
- */
11930
- function buildXlsx(header, rows, sheetName = 'Data') {
11931
- const rowXml = (cells, r) => `<row r="${r}">` +
11932
- cells
11933
- .map((v, c) => {
11934
- const ref = `${colRef(c)}${r}`;
11935
- if (typeof v === 'number' && Number.isFinite(v)) {
11936
- 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);
11937
11976
  }
11938
- return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${xml(v)}</t></is></c>`;
11939
- })
11940
- .join('') +
11941
- '</row>';
11942
- const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11943
- `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>` +
11944
- rowXml(header, 1) +
11945
- rows.map((cells, i) => rowXml(cells, i + 2)).join('') +
11946
- `</sheetData></worksheet>`;
11947
- const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11948
- `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ` +
11949
- `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">` +
11950
- `<sheets><sheet name="${xml(sheetName)}" sheetId="1" r:id="rId1"/></sheets></workbook>`;
11951
- const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11952
- `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
11953
- `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>` +
11954
- `</Relationships>`;
11955
- const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11956
- `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
11957
- `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>` +
11958
- `</Relationships>`;
11959
- const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
11960
- `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
11961
- `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
11962
- `<Default Extension="xml" ContentType="application/xml"/>` +
11963
- `<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` +
11964
- `<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
11965
- `</Types>`;
11966
- return zip([
11967
- { name: '[Content_Types].xml', data: enc.encode(contentTypes) },
11968
- { name: '_rels/.rels', data: enc.encode(rootRels) },
11969
- { name: 'xl/workbook.xml', data: enc.encode(workbook) },
11970
- { name: 'xl/_rels/workbook.xml.rels', data: enc.encode(workbookRels) },
11971
- { name: 'xl/worksheets/sheet1.xml', data: enc.encode(sheet) },
11972
- ]);
11973
- }
11974
-
11975
- const DG_LABELS = {
11976
- row: 'row',
11977
- rows: 'rows',
11978
- selected: 'selected',
11979
- selectAll: 'Select all rows on this page',
11980
- selectRow: 'Select row',
11981
- selectRowFor: (id) => `Select row ${id}`,
11982
- selectAllInGroup: (group) => `Select all in ${group}`,
11983
- openDetail: 'Open detail',
11984
- toggleDetail: 'Toggle detail',
11985
- closeDetail: 'Close detail',
11986
- rowActions: 'Row actions',
11987
- chooseColumns: 'Choose columns',
11988
- refresh: 'Refresh data',
11989
- actions: 'Actions',
11990
- filterColumn: 'Filter',
11991
- filterPlaceholder: 'Filter…',
11992
- clearFilter: 'Clear filter',
11993
- editCell: 'Edit',
11994
- toggleChildren: 'Toggle children',
11995
- quickFilter: 'Quick filter',
11996
- };
11997
- let datagridCounter = 0;
11998
- /** Utility-column widths used when sticky columns are active (px). */
11999
- const UTIL_W = { detail: 36, expand: 36, sel: 40 };
12000
- /** Fallback width for a sticky column without an explicit px width. */
12001
- const STICKY_FALLBACK_W = 120;
12002
- /**
12003
- * Marks the expandable-row detail template. The row is the template's implicit
12004
- * context (`let-row`, or `let-row="row"`): `<ng-template strctRowDetail let-row> … {{ row['name'] }} … </ng-template>`.
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 });
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 }] }] } });
12128
+
12129
+ /**
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.
12005
12136
  */
12006
- class StrctRowDetailDef {
12007
- template = inject(TemplateRef);
12008
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctRowDetailDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12009
- 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;
12010
12152
  }
12011
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctRowDetailDef, decorators: [{
12012
- type: Directive,
12013
- args: [{ selector: '[strctRowDetail]' }]
12014
- }] });
12015
- /** Muted note inside the action bar, before the buttons. */
12016
- class StrctDatagridActionBarCaption {
12017
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBarCaption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12018
- 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);
12019
12187
  }
12020
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBarCaption, decorators: [{
12021
- type: Directive,
12022
- args: [{ selector: '[strctDatagridActionBarCaption]' }]
12023
- }] });
12024
- /** Marks the persistent action-bar (toolbar) content shown above the grid. */
12025
- class StrctDatagridActionBar {
12026
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBar, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12027
- 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;
12028
12199
  }
12029
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBar, decorators: [{
12030
- type: Directive,
12031
- args: [{ selector: '[strctDatagridActionBar]' }]
12032
- }] });
12033
12200
  /**
12034
- * Interactive data table: sortable columns, row selection (Shift-click a row
12035
- * checkbox to carry its new state across a whole block), expandable detail
12036
- * rows, a batch action bar and built-in paging.
12037
- * <strct-datagrid [columns]="cols" [rows]="data" selectable expandable [pageSize]="10">
12038
- * <button strct-button strctDatagridActions size="sm">Migrate</button>
12039
- * <ng-template strctRowDetail let-row> … </ng-template>
12040
- * </strct-datagrid>
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.
12041
12203
  */
12042
- class StrctDatagrid {
12043
- /** Column definitions. */
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 });
12302
+ }
12303
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagridActionBar, decorators: [{
12304
+ type: Directive,
12305
+ args: [{ selector: '[strctDatagridActionBar]' }]
12306
+ }] });
12307
+ /**
12308
+ * Interactive data table: sortable columns, row selection (Shift-click a row
12309
+ * checkbox to carry its new state across a whole block), expandable detail
12310
+ * rows, a batch action bar and built-in paging.
12311
+ * <strct-datagrid [columns]="cols" [rows]="data" selectable expandable [pageSize]="10">
12312
+ * <button strct-button strctDatagridActions size="sm">Migrate</button>
12313
+ * <ng-template strctRowDetail let-row> … </ng-template>
12314
+ * </strct-datagrid>
12315
+ */
12316
+ class StrctDatagrid {
12317
+ /** Column definitions. */
12044
12318
  columns = input.required(/* @ts-ignore */
12045
12319
  ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
12046
12320
  /** Data rows. */
@@ -12216,8 +12490,9 @@ class StrctDatagrid {
12216
12490
  rowAction = output();
12217
12491
  /** Server-side data request (lazy mode): load this page / sort and set `rows`. */
12218
12492
  lazyLoad = output();
12219
- /** An editable cell was committed (Enter / blur). The grid does not mutate
12220
- * 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. */
12221
12496
  cellEdit = output();
12222
12497
  menu = inject(StrctMenuService);
12223
12498
  detailDef = contentChild(StrctRowDetailDef, /* @ts-ignore */
@@ -12592,9 +12867,80 @@ class StrctDatagrid {
12592
12867
  const e = this.editing();
12593
12868
  return !!e && e.rowKey === this.rowKey(row) && e.colKey === col.key;
12594
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 */ []));
12595
12873
  startEdit(row, col) {
12596
12874
  this.editing.set({ rowKey: this.rowKey(row), colKey: col.key });
12597
- 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);
12598
12944
  }
12599
12945
  commitEdit(row, col, event) {
12600
12946
  if (!this.isEditingCell(row, col))
@@ -12603,7 +12949,7 @@ class StrctDatagrid {
12603
12949
  this.editing.set(null);
12604
12950
  const previous = row[col.key];
12605
12951
  if (String(previous ?? '') !== value)
12606
- this.cellEdit.emit({ row, column: col, value, previous });
12952
+ this.cellEdit.emit({ row, column: col, value, typedValue: value, previous });
12607
12953
  }
12608
12954
  cancelEdit(event) {
12609
12955
  event?.stopPropagation();
@@ -13716,16 +14062,47 @@ class StrctDatagrid {
13716
14062
  }
13717
14063
  }
13718
14064
  @if (isEditingCell(row, col)) {
13719
- <input
13720
- class="strct-dg__editinput"
13721
- type="text"
13722
- [value]="row[col.key] ?? ''"
13723
- [attr.aria-label]="L().editCell + ': ' + col.label"
13724
- (keydown.enter)="commitEdit(row, col, $event)"
13725
- (keydown.escape)="cancelEdit($event)"
13726
- (blur)="commitEdit(row, col, $event)"
13727
- (click)="$event.stopPropagation()"
13728
- />
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
+ }
13729
14106
  } @else if (cellTemplate(col.key); as tpl) {
13730
14107
  <ng-container
13731
14108
  [ngTemplateOutlet]="tpl"
@@ -13737,7 +14114,7 @@ class StrctDatagrid {
13737
14114
  }"
13738
14115
  />
13739
14116
  } @else {
13740
- {{ row[col.key] }}
14117
+ {{ displayValue(row, col) }}
13741
14118
  }
13742
14119
  </td>
13743
14120
  }
@@ -13872,7 +14249,7 @@ class StrctDatagrid {
13872
14249
  </div>
13873
14250
  </div>
13874
14251
  }
13875
- `, 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 });
13876
14253
  }
13877
14254
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDatagrid, decorators: [{
13878
14255
  type: Component,
@@ -13885,6 +14262,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
13885
14262
  StrctButtonGroup,
13886
14263
  StrctOverlay,
13887
14264
  StrctSearchbox,
14265
+ StrctSelect,
14266
+ StrctNumber,
14267
+ FormsModule,
13888
14268
  ], template: `
13889
14269
  @if (actionBarDef() || quickFilterable()) {
13890
14270
  <div class="strct-dg__toolbar">
@@ -14249,16 +14629,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
14249
14629
  }
14250
14630
  }
14251
14631
  @if (isEditingCell(row, col)) {
14252
- <input
14253
- class="strct-dg__editinput"
14254
- type="text"
14255
- [value]="row[col.key] ?? ''"
14256
- [attr.aria-label]="L().editCell + ': ' + col.label"
14257
- (keydown.enter)="commitEdit(row, col, $event)"
14258
- (keydown.escape)="cancelEdit($event)"
14259
- (blur)="commitEdit(row, col, $event)"
14260
- (click)="$event.stopPropagation()"
14261
- />
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
+ }
14262
14673
  } @else if (cellTemplate(col.key); as tpl) {
14263
14674
  <ng-container
14264
14675
  [ngTemplateOutlet]="tpl"
@@ -14270,7 +14681,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
14270
14681
  }"
14271
14682
  />
14272
14683
  } @else {
14273
- {{ row[col.key] }}
14684
+ {{ displayValue(row, col) }}
14274
14685
  }
14275
14686
  </td>
14276
14687
  }
@@ -14411,7 +14822,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
14411
14822
  '[class.strct-dg-host--singleline]': 'singleLine()',
14412
14823
  '[class.strct-dg-host--virtual]': 'virtual()',
14413
14824
  '[class.strct-dg-host--sticky]': 'stickyActive()',
14414
- }, 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"] }]
14415
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: [{
14416
14827
  type: HostListener,
14417
14828
  args: ['document:click', ['$event']]
@@ -19812,1441 +20223,1167 @@ class StrctTour {
19812
20223
  const w = 300;
19813
20224
  const h = 170;
19814
20225
  if (!r) {
19815
- return {
19816
- top: Math.max(20, window.innerHeight / 2 - h / 2),
19817
- left: Math.max(20, window.innerWidth / 2 - w / 2),
19818
- };
19819
- }
19820
- // Below the target if it fits, else above; clamped into the viewport.
19821
- let top = r.top + r.height + 14;
19822
- if (top + h > window.innerHeight - 12)
19823
- top = Math.max(12, r.top - h - 14);
19824
- const left = Math.min(Math.max(12, r.left), window.innerWidth - w - 12);
19825
- return { top, left };
19826
- }, /* @ts-ignore */
19827
- ...(ngDevMode ? [{ debugName: "cardPos" }] : /* istanbul ignore next */ []));
19828
- constructor() {
19829
- // Restart from step 0 on open; scroll the first target into view.
19830
- effect(() => {
19831
- if (this.open()) {
19832
- this.previousActive = saveFocusedElement();
19833
- this.index.set(0);
19834
- this.scrollToTarget();
19835
- this.focusNextButton();
19836
- }
19837
- else {
19838
- restoreFocus(this.previousActive);
19839
- this.previousActive = null;
19840
- }
19841
- });
19842
- }
19843
- next() {
19844
- if (this.index() >= this.steps().length - 1) {
19845
- this.open.set(false);
19846
- this.finished.emit();
19847
- return;
19848
- }
19849
- this.index.update((i) => i + 1);
19850
- this.scrollToTarget();
19851
- this.focusNextButton();
19852
- }
19853
- prev() {
19854
- this.index.update((i) => Math.max(0, i - 1));
19855
- this.scrollToTarget();
19856
- this.focusNextButton();
19857
- }
19858
- close() {
19859
- this.open.set(false);
19860
- this.dismissed.emit();
19861
- }
19862
- /** Wrap Tab focus within the card. */
19863
- onTab(event) {
19864
- const card = this.card();
19865
- if (card)
19866
- keepTabInside(event, card);
19867
- }
19868
- /** Escape closes; stopPropagation so a host modal/drawer does not also close. */
19869
- onEscapeKey(event) {
19870
- event.stopPropagation();
19871
- this.close();
19872
- }
19873
- /**
19874
- * Arrow keys step through the tour, mirrored under RTL. No shared direction
19875
- * helper exists (other components handle RTL in CSS only), so read the
19876
- * card's computed direction here.
19877
- */
19878
- onArrow(event, dir) {
19879
- event.preventDefault();
19880
- const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
19881
- if ((dir === 'right') !== rtl)
19882
- this.next();
19883
- else
19884
- this.prev();
19885
- }
19886
- /** Home/End jump to the first/last step. */
19887
- onHomeEnd(event, last) {
19888
- event.preventDefault();
19889
- this.index.set(last ? this.steps().length - 1 : 0);
19890
- this.scrollToTarget();
19891
- this.focusNextButton();
19892
- }
19893
- scrollToTarget() {
19894
- setTimeout(() => {
19895
- const sel = this.steps()[this.index()]?.target;
19896
- if (sel)
19897
- document.querySelector(sel)?.scrollIntoView?.({ block: 'center' });
19898
- this.measureTick.update((n) => n + 1);
19899
- });
19900
- }
19901
- card() {
19902
- return this.elementRef.nativeElement.querySelector('.strct-tour__card');
19903
- }
19904
- /** Move focus to the step's primary (Next/Done) action once the card has rendered. */
19905
- focusNextButton() {
19906
- setTimeout(() => {
19907
- if (!this.open())
19908
- return;
19909
- const card = this.card();
19910
- if (!card)
19911
- return;
19912
- const nav = card.querySelectorAll('.strct-tour__nav button');
19913
- const primary = nav[nav.length - 1];
19914
- if (primary)
19915
- primary.focus();
19916
- else
19917
- focusFirstIn(card);
19918
- });
19919
- }
19920
- onReflow() {
19921
- if (this.open())
19922
- this.measureTick.update((n) => n + 1);
19923
- }
19924
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
19925
- 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: `
19926
- @if (open() && current(); as step) {
19927
- <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
19928
- <div
19929
- class="strct-tour__backdrop"
19930
- [class.strct-tour__backdrop--dim]="!spot()"
19931
- (click)="close()"
19932
- ></div>
19933
- @if (spot(); as r) {
19934
- <div
19935
- class="strct-tour__ring"
19936
- [style.top.px]="r.top - 6"
19937
- [style.left.px]="r.left - 6"
19938
- [style.width.px]="r.width + 12"
19939
- [style.height.px]="r.height + 12"
19940
- ></div>
19941
- }
19942
- <div
19943
- class="strct-tour__card"
19944
- role="dialog"
19945
- [attr.aria-labelledby]="titleId"
19946
- [style.top.px]="cardPos().top"
19947
- [style.left.px]="cardPos().left"
19948
- (keydown.tab)="onTab($event)"
19949
- (keydown.shift.tab)="onTab($event)"
19950
- (keydown.escape)="onEscapeKey($event)"
19951
- (keydown.arrowleft)="onArrow($event, 'left')"
19952
- (keydown.arrowright)="onArrow($event, 'right')"
19953
- (keydown.home)="onHomeEnd($event, false)"
19954
- (keydown.end)="onHomeEnd($event, true)"
19955
- >
19956
- <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
19957
- <div class="strct-tour__title" [id]="titleId">{{ step.title }}</div>
19958
- <div class="strct-tour__body">{{ step.body }}</div>
19959
- <div class="strct-tour__nav">
19960
- <button strct-button size="sm" variant="flat" (click)="close()">
19961
- {{ skipLabel() }}
19962
- </button>
19963
- <span class="strct-tour__grow"></span>
19964
- @if (index() > 0) {
19965
- <button strct-button size="sm" variant="neutral" (click)="prev()">
19966
- {{ backLabel() }}
19967
- </button>
19968
- }
19969
- <button strct-button size="sm" variant="primary" solid (click)="next()">
19970
- {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
19971
- </button>
19972
- </div>
19973
- </div>
19974
- }
19975
- `, 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 });
19976
- }
19977
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTour, decorators: [{
19978
- type: Component,
19979
- args: [{ selector: 'strct-tour', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton], template: `
19980
- @if (open() && current(); as step) {
19981
- <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
19982
- <div
19983
- class="strct-tour__backdrop"
19984
- [class.strct-tour__backdrop--dim]="!spot()"
19985
- (click)="close()"
19986
- ></div>
19987
- @if (spot(); as r) {
19988
- <div
19989
- class="strct-tour__ring"
19990
- [style.top.px]="r.top - 6"
19991
- [style.left.px]="r.left - 6"
19992
- [style.width.px]="r.width + 12"
19993
- [style.height.px]="r.height + 12"
19994
- ></div>
19995
- }
19996
- <div
19997
- class="strct-tour__card"
19998
- role="dialog"
19999
- [attr.aria-labelledby]="titleId"
20000
- [style.top.px]="cardPos().top"
20001
- [style.left.px]="cardPos().left"
20002
- (keydown.tab)="onTab($event)"
20003
- (keydown.shift.tab)="onTab($event)"
20004
- (keydown.escape)="onEscapeKey($event)"
20005
- (keydown.arrowleft)="onArrow($event, 'left')"
20006
- (keydown.arrowright)="onArrow($event, 'right')"
20007
- (keydown.home)="onHomeEnd($event, false)"
20008
- (keydown.end)="onHomeEnd($event, true)"
20009
- >
20010
- <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
20011
- <div class="strct-tour__title" [id]="titleId">{{ step.title }}</div>
20012
- <div class="strct-tour__body">{{ step.body }}</div>
20013
- <div class="strct-tour__nav">
20014
- <button strct-button size="sm" variant="flat" (click)="close()">
20015
- {{ skipLabel() }}
20016
- </button>
20017
- <span class="strct-tour__grow"></span>
20018
- @if (index() > 0) {
20019
- <button strct-button size="sm" variant="neutral" (click)="prev()">
20020
- {{ backLabel() }}
20021
- </button>
20022
- }
20023
- <button strct-button size="sm" variant="primary" solid (click)="next()">
20024
- {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
20025
- </button>
20026
- </div>
20027
- </div>
20028
- }
20029
- `, 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"] }]
20030
- }], 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: [{
20031
- type: HostListener,
20032
- args: ['window:resize']
20033
- }, {
20034
- type: HostListener,
20035
- args: ['window:scroll']
20036
- }] } });
20037
-
20038
- let menubarCounter = 0;
20039
- /**
20040
- * Horizontal menubar — the application-menu strip ("File · Edit · View") for
20041
- * dense tool-style consoles:
20042
- *
20043
- * <strct-menubar [menus]="menus" (picked)="run($event)" />
20044
- *
20045
- * Click (or Enter/Down) opens a menu and focus moves into it; Left/Right move
20046
- * across the bar and, while open, switch the open menu (the APG menubar
20047
- * convention). Up/Down (+Home/End) rove the open menu, Right/Left enter and
20048
- * leave an item's one-level `children` submenu. Escape and outside click
20049
- * close, returning focus to the bar. `(picked)` carries `{ menu, item }`.
20050
- */
20051
- class StrctMenubar {
20052
- host = inject(ElementRef);
20053
- /** Top-level menus. */
20054
- menus = input.required(/* @ts-ignore */
20055
- ...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
20056
- /** Accessible name of the bar (localizable). */
20057
- ariaLabel = input('Application menu', /* @ts-ignore */
20058
- ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
20059
- /** A menu entry was chosen. */
20060
- picked = output();
20061
- openId = signal(null, /* @ts-ignore */
20062
- ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
20063
- focusIdx = signal(0, /* @ts-ignore */
20064
- ...(ngDevMode ? [{ debugName: "focusIdx" }] : /* istanbul ignore next */ []));
20065
- /** Index of the item whose one-level submenu is open. */
20066
- subIdx = signal(null, /* @ts-ignore */
20067
- ...(ngDevMode ? [{ debugName: "subIdx" }] : /* istanbul ignore next */ []));
20068
- /** Top-level button that opened the menu — focus returns here on close. */
20069
- trigger = null;
20070
- toggleMenu(id, i) {
20071
- this.focusIdx.set(i);
20072
- if (this.openId() === id) {
20073
- this.closeMenu(false);
20074
- }
20075
- else {
20076
- this.trigger = saveFocusedElement();
20077
- this.openId.set(id);
20078
- this.subIdx.set(null);
20079
- }
20080
- }
20081
- onItemClick(menu, item, i) {
20082
- if (item.disabled)
20083
- return;
20084
- if (item.children?.length) {
20085
- this.subIdx.set(this.subIdx() === i ? null : i);
20086
- return;
20087
- }
20088
- this.pick(menu, item);
20089
- }
20090
- onItemHover(item, i) {
20091
- if (item.disabled)
20092
- return;
20093
- if (item.children?.length)
20094
- this.subIdx.set(i);
20095
- else if (this.subIdx() != null)
20096
- this.subIdx.set(null);
20097
- }
20098
- pick(menu, item) {
20099
- if (item.disabled)
20100
- return;
20101
- this.closeMenu(true);
20102
- item.action?.(item.data);
20103
- this.picked.emit({ menu, item });
20104
- }
20105
- onTopKey(event, i) {
20106
- const menus = this.menus();
20107
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
20108
- event.preventDefault();
20109
- if (this.openId()) {
20110
- // While a menu is open, moving across the bar switches the open menu.
20111
- this.switchTop(event.key === 'ArrowRight' ? 1 : -1);
20112
- }
20113
- else {
20114
- const dir = event.key === 'ArrowRight' ? 1 : -1;
20115
- const next = (i + dir + menus.length) % menus.length;
20116
- this.focusIdx.set(next);
20117
- setTimeout(() => this.topButtons()[next]?.focus());
20118
- }
20119
- }
20120
- else if (event.key === 'ArrowDown' || event.key === 'Enter') {
20121
- event.preventDefault();
20122
- this.trigger = saveFocusedElement();
20123
- this.openId.set(menus[i].id);
20124
- this.subIdx.set(null);
20125
- setTimeout(() => {
20126
- const menuEl = this.host.nativeElement.querySelector('.strct-mb__menu');
20127
- if (menuEl)
20128
- focusFirstIn(menuEl);
20129
- });
20130
- }
20131
- else if (event.key === 'Escape' && this.openId()) {
20132
- event.preventDefault();
20133
- event.stopPropagation();
20134
- this.closeMenu(true);
20135
- }
20136
- }
20137
- onMenuKey(event, menu) {
20138
- const active = document.activeElement;
20139
- const inSub = !!active?.classList.contains('strct-mb__subitem');
20140
- switch (event.key) {
20141
- case 'ArrowDown':
20142
- case 'ArrowUp': {
20143
- event.preventDefault();
20144
- event.stopPropagation();
20145
- const list = this.navButtons(inSub);
20146
- const dir = event.key === 'ArrowDown' ? 1 : -1;
20147
- const cur = list.indexOf(active);
20148
- list[(cur + dir + list.length) % list.length]?.focus();
20149
- break;
20150
- }
20151
- case 'Home':
20152
- case 'End': {
20153
- event.preventDefault();
20154
- event.stopPropagation();
20155
- const list = this.navButtons(inSub);
20156
- (event.key === 'Home' ? list[0] : list[list.length - 1])?.focus();
20157
- break;
20158
- }
20159
- case 'ArrowRight': {
20160
- if (inSub)
20161
- break; // deepest level — nothing further to open
20162
- event.preventDefault();
20163
- event.stopPropagation();
20164
- const idx = Number(active?.getAttribute('data-idx'));
20165
- if (menu.items[idx]?.children?.length && !menu.items[idx].disabled) {
20166
- this.subIdx.set(idx);
20167
- setTimeout(() => this.navButtons(true)[0]?.focus());
20168
- }
20169
- else {
20170
- this.switchTop(1);
20171
- }
20172
- break;
20173
- }
20174
- case 'ArrowLeft': {
20175
- event.preventDefault();
20176
- event.stopPropagation();
20177
- if (inSub) {
20178
- const parent = this.subIdx();
20179
- this.subIdx.set(null);
20180
- this.host.nativeElement
20181
- .querySelector(`.strct-mb__item[data-idx="${parent}"]`)
20182
- ?.focus();
20183
- }
20184
- else if (this.subIdx() != null) {
20185
- this.subIdx.set(null);
20186
- }
20187
- else {
20188
- this.switchTop(-1);
20189
- }
20190
- 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();
20191
20247
  }
20192
- case 'Escape': {
20193
- event.preventDefault();
20194
- event.stopPropagation();
20195
- this.closeMenu(true);
20196
- break;
20248
+ else {
20249
+ restoreFocus(this.previousActive);
20250
+ this.previousActive = null;
20197
20251
  }
20252
+ });
20253
+ }
20254
+ next() {
20255
+ if (this.index() >= this.steps().length - 1) {
20256
+ this.open.set(false);
20257
+ this.finished.emit();
20258
+ return;
20198
20259
  }
20260
+ this.index.update((i) => i + 1);
20261
+ this.scrollToTarget();
20262
+ this.focusNextButton();
20199
20263
  }
20200
- /** Switch the open menu to the next/previous top-level entry; focus follows. */
20201
- switchTop(dir) {
20202
- const menus = this.menus();
20203
- const next = (this.focusIdx() + dir + menus.length) % menus.length;
20204
- this.focusIdx.set(next);
20205
- this.openId.set(menus[next].id);
20206
- this.subIdx.set(null);
20207
- setTimeout(() => this.topButtons()[next]?.focus());
20264
+ prev() {
20265
+ this.index.update((i) => Math.max(0, i - 1));
20266
+ this.scrollToTarget();
20267
+ this.focusNextButton();
20208
20268
  }
20209
- closeMenu(returnFocus) {
20210
- this.openId.set(null);
20211
- this.subIdx.set(null);
20212
- if (returnFocus)
20213
- restoreFocus(this.trigger);
20214
- this.trigger = null;
20269
+ close() {
20270
+ this.open.set(false);
20271
+ this.dismissed.emit();
20215
20272
  }
20216
- uid = ++menubarCounter;
20217
- hintId(menuId, i, j) {
20218
- // menu ids are consumer data; aria-describedby splits on whitespace.
20219
- const menu = menuId.replace(/\s+/g, '_');
20220
- 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);
20221
20278
  }
20222
- topButtons() {
20223
- 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();
20224
20283
  }
20225
- /** Keyboard-reachable item buttons of the open menu — either its items or the
20226
- * open submenu's: enabled ones, plus disabled ones that carry a hint (so the
20227
- * reason can be read). A disabled entry with nothing to say is skipped. */
20228
- navButtons(sub) {
20229
- const menuEl = this.host.nativeElement.querySelector('.strct-mb__menu');
20230
- if (!menuEl)
20231
- return [];
20232
- const sel = sub ? '.strct-mb__subitem' : '.strct-mb__item:not(.strct-mb__subitem)';
20233
- 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();
20234
20296
  }
20235
- onDocClick(event) {
20236
- if (this.openId() && !this.host.nativeElement.contains(event.target)) {
20237
- this.closeMenu(false);
20238
- }
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();
20239
20303
  }
20240
- onEscape(event) {
20241
- if (!this.openId())
20242
- return;
20243
- event.stopPropagation();
20244
- 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
+ });
20245
20311
  }
20246
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctMenubar, deps: [], target: i0.ɵɵFactoryTarget.Component });
20247
- 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: `
20248
- <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
20249
- @for (menu of menus(); track menu.id; let i = $index) {
20250
- <div class="strct-mb__wrap">
20251
- <button
20252
- type="button"
20253
- class="strct-mb__top"
20254
- role="menuitem"
20255
- [class.strct-mb__top--open]="openId() === menu.id"
20256
- [attr.aria-haspopup]="'menu'"
20257
- [attr.aria-expanded]="openId() === menu.id"
20258
- [attr.tabindex]="i === focusIdx() ? 0 : -1"
20259
- (click)="toggleMenu(menu.id, i)"
20260
- (keydown)="onTopKey($event, i)"
20261
- >
20262
- {{ menu.label }}
20263
- </button>
20264
- @if (openId() === menu.id) {
20265
- <div
20266
- class="strct-mb__menu"
20267
- role="menu"
20268
- tabindex="-1"
20269
- [attr.aria-label]="menu.label"
20270
- (keydown)="onMenuKey($event, menu)"
20271
- >
20272
- @for (item of menu.items; track $index; let i = $index) {
20273
- @if (item.divider) {
20274
- <div class="strct-mb__divider" role="separator"></div>
20275
- } @else {
20276
- <div class="strct-mb__row">
20277
- <button
20278
- type="button"
20279
- class="strct-mb__item"
20280
- role="menuitem"
20281
- [attr.data-idx]="i"
20282
- [class.strct-mb__item--critical]="item.critical"
20283
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
20284
- [attr.aria-expanded]="item.children?.length ? subIdx() === i : null"
20285
- [attr.aria-disabled]="item.disabled ? 'true' : null"
20286
- [attr.aria-describedby]="item.hint ? hintId(menu.id, i) : null"
20287
- [attr.title]="item.hint || null"
20288
- (click)="onItemClick(menu, item, i)"
20289
- (mouseenter)="onItemHover(item, i)"
20290
- >
20291
- @if (item.icon) {
20292
- <strct-icon [name]="item.icon" [size]="13" />
20293
- }
20294
- {{ item.label }}
20295
- @if (item.children?.length) {
20296
- <strct-icon class="strct-mb__caret" name="chevronRight" [size]="12" />
20297
- }
20298
- </button>
20299
- @if (item.hint) {
20300
- <span [id]="hintId(menu.id, i)" hidden>{{ item.hint }}</span>
20301
- }
20302
- @if (subIdx() === i && item.children?.length) {
20303
- <div class="strct-mb__submenu" role="menu" [attr.aria-label]="item.label">
20304
- @for (sub of item.children; track $index; let j = $index) {
20305
- @if (sub.divider) {
20306
- <div class="strct-mb__divider" role="separator"></div>
20307
- } @else {
20308
- <button
20309
- type="button"
20310
- class="strct-mb__item strct-mb__subitem"
20311
- role="menuitem"
20312
- [class.strct-mb__item--critical]="sub.critical"
20313
- [attr.aria-disabled]="sub.disabled ? 'true' : null"
20314
- [attr.aria-describedby]="sub.hint ? hintId(menu.id, i, j) : null"
20315
- [attr.title]="sub.hint || null"
20316
- (click)="pick(menu, sub)"
20317
- >
20318
- @if (sub.icon) {
20319
- <strct-icon [name]="sub.icon" [size]="13" />
20320
- }
20321
- {{ sub.label }}
20322
- </button>
20323
- @if (sub.hint) {
20324
- <span [id]="hintId(menu.id, i, j)" hidden>{{ sub.hint }}</span>
20325
- }
20326
- }
20327
- }
20328
- </div>
20329
- }
20330
- </div>
20331
- }
20332
- }
20333
- </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>
20334
20379
  }
20380
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
20381
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
20382
+ </button>
20335
20383
  </div>
20336
- }
20337
- </div>
20338
- `, 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 });
20339
20387
  }
20340
- 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: [{
20341
20389
  type: Component,
20342
- args: [{ selector: 'strct-menubar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
20343
- <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
20344
- @for (menu of menus(); track menu.id; let i = $index) {
20345
- <div class="strct-mb__wrap">
20346
- <button
20347
- type="button"
20348
- class="strct-mb__top"
20349
- role="menuitem"
20350
- [class.strct-mb__top--open]="openId() === menu.id"
20351
- [attr.aria-haspopup]="'menu'"
20352
- [attr.aria-expanded]="openId() === menu.id"
20353
- [attr.tabindex]="i === focusIdx() ? 0 : -1"
20354
- (click)="toggleMenu(menu.id, i)"
20355
- (keydown)="onTopKey($event, i)"
20356
- >
20357
- {{ 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() }}
20358
20427
  </button>
20359
- @if (openId() === menu.id) {
20360
- <div
20361
- class="strct-mb__menu"
20362
- role="menu"
20363
- tabindex="-1"
20364
- [attr.aria-label]="menu.label"
20365
- (keydown)="onMenuKey($event, menu)"
20366
- >
20367
- @for (item of menu.items; track $index; let i = $index) {
20368
- @if (item.divider) {
20369
- <div class="strct-mb__divider" role="separator"></div>
20370
- } @else {
20371
- <div class="strct-mb__row">
20372
- <button
20373
- type="button"
20374
- class="strct-mb__item"
20375
- role="menuitem"
20376
- [attr.data-idx]="i"
20377
- [class.strct-mb__item--critical]="item.critical"
20378
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
20379
- [attr.aria-expanded]="item.children?.length ? subIdx() === i : null"
20380
- [attr.aria-disabled]="item.disabled ? 'true' : null"
20381
- [attr.aria-describedby]="item.hint ? hintId(menu.id, i) : null"
20382
- [attr.title]="item.hint || null"
20383
- (click)="onItemClick(menu, item, i)"
20384
- (mouseenter)="onItemHover(item, i)"
20385
- >
20386
- @if (item.icon) {
20387
- <strct-icon [name]="item.icon" [size]="13" />
20388
- }
20389
- {{ item.label }}
20390
- @if (item.children?.length) {
20391
- <strct-icon class="strct-mb__caret" name="chevronRight" [size]="12" />
20392
- }
20393
- </button>
20394
- @if (item.hint) {
20395
- <span [id]="hintId(menu.id, i)" hidden>{{ item.hint }}</span>
20396
- }
20397
- @if (subIdx() === i && item.children?.length) {
20398
- <div class="strct-mb__submenu" role="menu" [attr.aria-label]="item.label">
20399
- @for (sub of item.children; track $index; let j = $index) {
20400
- @if (sub.divider) {
20401
- <div class="strct-mb__divider" role="separator"></div>
20402
- } @else {
20403
- <button
20404
- type="button"
20405
- class="strct-mb__item strct-mb__subitem"
20406
- role="menuitem"
20407
- [class.strct-mb__item--critical]="sub.critical"
20408
- [attr.aria-disabled]="sub.disabled ? 'true' : null"
20409
- [attr.aria-describedby]="sub.hint ? hintId(menu.id, i, j) : null"
20410
- [attr.title]="sub.hint || null"
20411
- (click)="pick(menu, sub)"
20412
- >
20413
- @if (sub.icon) {
20414
- <strct-icon [name]="sub.icon" [size]="13" />
20415
- }
20416
- {{ sub.label }}
20417
- </button>
20418
- @if (sub.hint) {
20419
- <span [id]="hintId(menu.id, i, j)" hidden>{{ sub.hint }}</span>
20420
- }
20421
- }
20422
- }
20423
- </div>
20424
- }
20425
- </div>
20426
- }
20427
- }
20428
- </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>
20429
20433
  }
20434
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
20435
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
20436
+ </button>
20430
20437
  </div>
20431
- }
20432
- </div>
20433
- `, 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"] }]
20434
- }], 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: [{
20435
20442
  type: HostListener,
20436
- args: ['document:click', ['$event']]
20437
- }], onEscape: [{
20443
+ args: ['window:resize']
20444
+ }, {
20438
20445
  type: HostListener,
20439
- args: ['document:keydown.escape', ['$event']]
20446
+ args: ['window:scroll']
20440
20447
  }] } });
20441
20448
 
20442
- function normalize(combo) {
20443
- return combo
20444
- .toLowerCase()
20445
- .split('+')
20446
- .map((p) => p.trim())
20447
- .sort()
20448
- .join('+');
20449
- }
20450
- function comboOf(event) {
20451
- const parts = [];
20452
- if (event.ctrlKey)
20453
- parts.push('ctrl');
20454
- if (event.metaKey)
20455
- parts.push('meta');
20456
- if (event.altKey)
20457
- parts.push('alt');
20458
- if (event.shiftKey)
20459
- parts.push('shift');
20460
- parts.push(event.key.toLowerCase());
20461
- return parts.sort().join('+');
20462
- }
20463
- /** `mod` matches Ctrl (and ⌘ on mac keyboards) — expand to both. */
20464
- function expand(combo) {
20465
- const n = combo.toLowerCase();
20466
- if (!n.includes('mod'))
20467
- return [normalize(n)];
20468
- return [normalize(n.replace('mod', 'ctrl')), normalize(n.replace('mod', 'meta'))];
20469
- }
20449
+ let menubarCounter = 0;
20470
20450
  /**
20471
- * Application hotkeys, centrally registered so they are discoverable — the
20472
- * Blueprint pattern:
20451
+ * Horizontal menubar — the application-menu strip ("File · Edit · View") for
20452
+ * dense tool-style consoles:
20473
20453
  *
20474
- * private hotkeys = inject(StrctHotkeysService);
20475
- * ngOnInit() { this.dispose = this.hotkeys.register(
20476
- * { combo: 'mod+k', description: 'Open command palette', group: 'Global',
20477
- * handler: () => this.palette.open() }); }
20454
+ * <strct-menubar [menus]="menus" (picked)="run($event)" />
20478
20455
  *
20479
- * Combos: `'a'`, `'shift+?'`, `'ctrl+k'`, `'meta+shift+p'`, `'mod+k'` (mod = Ctrl/⌘).
20480
- * Plain-key combos are suppressed while typing in inputs / textareas /
20481
- * contenteditable; modifier combos always fire. Pair with
20482
- * `<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 }`.
20483
20461
  */
20484
- class StrctHotkeysService {
20485
- registry = signal([], /* @ts-ignore */
20486
- ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
20487
- /** All registered hotkeys (for the help overlay). */
20488
- hotkeys = this.registry.asReadonly();
20489
- listener = (event) => this.onKeydown(event);
20490
- constructor() {
20491
- document.addEventListener('keydown', this.listener);
20492
- }
20493
- /** Register a hotkey; the returned function unregisters it. */
20494
- register(hotkey) {
20495
- this.registry.update((list) => [...list, hotkey]);
20496
- 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
+ }
20497
20491
  }
20498
- onKeydown(event) {
20499
- const combo = comboOf(event);
20500
- const typing = isTyping(event);
20501
- for (const hk of this.registry()) {
20502
- if (!expand(hk.combo).includes(combo))
20503
- continue;
20504
- // While typing, only modifier combos may fire.
20505
- if (typing && !(event.ctrlKey || event.metaKey || event.altKey))
20506
- continue;
20507
- event.preventDefault();
20508
- 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);
20509
20497
  return;
20510
20498
  }
20499
+ this.pick(menu, item);
20511
20500
  }
20512
- ngOnDestroy() {
20513
- 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);
20514
20508
  }
20515
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
20516
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, providedIn: 'root' });
20517
- }
20518
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysService, decorators: [{
20519
- type: Injectable,
20520
- args: [{ providedIn: 'root' }]
20521
- }], ctorParameters: () => [] });
20522
- function isTyping(event) {
20523
- const el = event.target;
20524
- if (!el)
20525
- return false;
20526
- return (el.tagName === 'INPUT' ||
20527
- el.tagName === 'TEXTAREA' ||
20528
- el.tagName === 'SELECT' ||
20529
- el.isContentEditable);
20530
- }
20531
- /**
20532
- * The hotkey cheatsheet overlay. Mount once near the app root; it registers
20533
- * `shift+?` (typed as `?`) itself and lists everything in the service, grouped:
20534
- *
20535
- * <strct-hotkeys-help />
20536
- */
20537
- class StrctHotkeysHelp {
20538
- service = inject(StrctHotkeysService);
20539
- host = inject((ElementRef));
20540
- /** Overlay visibility (two-way; `?` toggles it too). */
20541
- open = model(false, /* @ts-ignore */
20542
- ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
20543
- /** Localizable strings. */
20544
- title = input('Keyboard shortcuts', /* @ts-ignore */
20545
- ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
20546
- emptyText = input('No shortcuts registered.', /* @ts-ignore */
20547
- ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
20548
- /** Accessible label of the × close button (localizable). */
20549
- closeLabel = input('Close', /* @ts-ignore */
20550
- ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
20551
- /** Element that had focus before the overlay opened, restored on close. */
20552
- previousActive = null;
20553
- grouped = computed(() => {
20554
- const groups = new Map();
20555
- for (const hk of this.service.hotkeys()) {
20556
- const name = hk.group ?? 'General';
20557
- const list = groups.get(name);
20558
- if (list)
20559
- list.push(hk);
20560
- else
20561
- groups.set(name, [hk]);
20562
- }
20563
- return [...groups.entries()].map(([name, keys]) => ({ name, keys }));
20564
- }, /* @ts-ignore */
20565
- ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
20566
- constructor() {
20567
- const dispose = this.service.register({
20568
- combo: 'shift+?',
20569
- description: 'Show this help',
20570
- group: 'General',
20571
- handler: () => this.open.update((v) => !v),
20572
- });
20573
- inject(DestroyRef).onDestroy(dispose);
20574
- // Focus lifecycle: remember the trigger, move focus into the dialog once
20575
- // it has rendered, and hand focus back on close.
20576
- effect(() => {
20577
- if (this.open()) {
20578
- this.previousActive = saveFocusedElement();
20579
- setTimeout(() => {
20580
- const dialog = this.host.nativeElement.querySelector('.strct-hkh');
20581
- if (dialog)
20582
- focusFirstIn(dialog);
20583
- });
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);
20584
20523
  }
20585
20524
  else {
20586
- restoreFocus(this.previousActive);
20587
- 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());
20588
20529
  }
20589
- });
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
+ }
20590
20547
  }
20591
- // Escape is handled locally — registering it in the service would swallow
20592
- // every Escape in the app (the service preventDefaults on match).
20593
- onEscape(event) {
20594
- if (!this.open())
20595
- return;
20596
- // Document-level: stopImmediatePropagation so a host modal/drawer with its
20597
- // own document listener doesn't also see the Escape the overlay consumed.
20598
- event.stopImmediatePropagation();
20599
- 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
+ }
20600
20610
  }
20601
- parts(combo) {
20602
- return combo.split('+').map((p) => {
20603
- const map = {
20604
- mod: '⌘/Ctrl',
20605
- meta: '⌘',
20606
- ctrl: 'Ctrl',
20607
- alt: 'Alt',
20608
- shift: 'Shift',
20609
- escape: 'Esc',
20610
- };
20611
- return map[p.trim().toLowerCase()] ?? p.trim().toUpperCase();
20612
- });
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'));
20613
20645
  }
20614
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHotkeysHelp, deps: [], target: i0.ɵɵFactoryTarget.Component });
20615
- 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: `
20616
- @if (open()) {
20617
- <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
20618
- <div class="strct-hkh" role="dialog" aria-modal="true" [attr.aria-label]="title()">
20619
- <div class="strct-hkh__head">
20620
- <span class="strct-hkh__title">{{ title() }}</span>
20621
- <span class="strct-hkh__headside">
20622
- <strct-kbd>?</strct-kbd>
20623
- <button
20624
- type="button"
20625
- class="strct-hkh__close"
20626
- [attr.aria-label]="closeLabel()"
20627
- (click)="open.set(false)"
20628
- >
20629
- <strct-icon name="close" [size]="14" />
20630
- </button>
20631
- </span>
20632
- </div>
20633
- @for (group of grouped(); track group.name) {
20634
- <div class="strct-hkh__group">
20635
- <div class="strct-hkh__groupname">{{ group.name }}</div>
20636
- @for (hk of group.keys; track hk.combo + hk.description) {
20637
- <div class="strct-hkh__row">
20638
- <span class="strct-hkh__desc">{{ hk.description }}</span>
20639
- <span class="strct-hkh__keys">
20640
- @for (part of parts(hk.combo); track $index) {
20641
- <strct-kbd>{{ part }}</strct-kbd>
20642
- }
20643
- </span>
20644
- </div>
20645
- }
20646
- </div>
20647
- } @empty {
20648
- <p class="strct-hkh__empty">{{ emptyText() }}</p>
20646
+ onDocClick(event) {
20647
+ if (this.openId() && !this.host.nativeElement.contains(event.target)) {
20648
+ this.closeMenu(false);
20649
20649
  }
20650
- </div>
20651
20650
  }
20652
- `, 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 });
20653
20750
  }
20654
- 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: [{
20655
20752
  type: Component,
20656
- args: [{ selector: 'strct-hotkeys-help', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctKbd], template: `
20657
- @if (open()) {
20658
- <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
20659
- <div class="strct-hkh" role="dialog" aria-modal="true" [attr.aria-label]="title()">
20660
- <div class="strct-hkh__head">
20661
- <span class="strct-hkh__title">{{ title() }}</span>
20662
- <span class="strct-hkh__headside">
20663
- <strct-kbd>?</strct-kbd>
20664
- <button
20665
- type="button"
20666
- class="strct-hkh__close"
20667
- [attr.aria-label]="closeLabel()"
20668
- (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)"
20669
20777
  >
20670
- <strct-icon name="close" [size]="14" />
20671
- </button>
20672
- </span>
20673
- </div>
20674
- @for (group of grouped(); track group.name) {
20675
- <div class="strct-hkh__group">
20676
- <div class="strct-hkh__groupname">{{ group.name }}</div>
20677
- @for (hk of group.keys; track hk.combo + hk.description) {
20678
- <div class="strct-hkh__row">
20679
- <span class="strct-hkh__desc">{{ hk.description }}</span>
20680
- <span class="strct-hkh__keys">
20681
- @for (part of parts(hk.combo); track $index) {
20682
- <strct-kbd>{{ part }}</strct-kbd>
20683
- }
20684
- </span>
20685
- </div>
20686
- }
20687
- </div>
20688
- } @empty {
20689
- <p class="strct-hkh__empty">{{ emptyText() }}</p>
20690
- }
20691
- </div>
20692
- }
20693
- `, 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"] }]
20694
- }], 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: [{
20695
20849
  type: HostListener,
20696
20850
  args: ['document:keydown.escape', ['$event']]
20697
20851
  }] } });
20698
20852
 
20699
- 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
+ }
20700
20881
  /**
20701
- * List drag-reorder primitive — the consumer owns the array:
20702
- *
20703
- * <ul strctReorder (reordered)="move($event)">
20704
- * @for (s of steps(); track s.id) {
20705
- * <li strctReorderItem>{{ s.label }}</li>
20706
- * }
20707
- * </ul>
20882
+ * Application hotkeys, centrally registered so they are discoverable — the
20883
+ * Blueprint pattern:
20708
20884
  *
20709
- * move({ from, to }: StrctReorderEvent) {
20710
- * this.steps.update((s) => { const c = [...s]; c.splice(to, 0, ...c.splice(from, 1)); return c; });
20711
- * }
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() }); }
20712
20889
  *
20713
- * Items are HTML5-draggable; keyboard reorder is Alt+ArrowUp / Alt+ArrowDown
20714
- * on the focused item (items get `tabindex="0"` unless they already manage
20715
- * focus). Indexes are positions among the `strctReorderItem` siblings.
20716
- * Completed moves are announced in a live region (see `announcement`) and
20717
- * 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.
20718
20894
  */
20719
- class StrctReorder {
20720
- host = inject(ElementRef);
20721
- renderer = inject(Renderer2);
20722
- doc = inject(DOCUMENT);
20723
- announcer = inject(StrctAnnouncer);
20724
- /** Emits when a drag or keyboard move completes. */
20725
- reordered = output();
20726
- /** Disable all reordering (display-only mode). */
20727
- reorderDisabled = input(false, { ...(ngDevMode ? { debugName: "reorderDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
20728
- /**
20729
- * Keyboard instructions (localizable), rendered sr-only and referenced by
20730
- * every item's `aria-describedby`. Set to '' to opt out.
20731
- */
20732
- instructions = input('Press Alt+ArrowUp or Alt+ArrowDown to move this item', /* @ts-ignore */
20733
- ...(ngDevMode ? [{ debugName: "instructions" }] : /* istanbul ignore next */ []));
20734
- /**
20735
- * Builds the live-region announcement after a move (localizable):
20736
- * (moved item's text, new 1-based position, item count).
20737
- */
20738
- announcement = input((label, position, total) => `Moved ${label} to position ${position} of ${total}`, /* @ts-ignore */
20739
- ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
20740
- dragIndex = signal(null, /* @ts-ignore */
20741
- ...(ngDevMode ? [{ debugName: "dragIndex" }] : /* istanbul ignore next */ []));
20742
- overIndex = signal(null, /* @ts-ignore */
20743
- ...(ngDevMode ? [{ debugName: "overIndex" }] : /* istanbul ignore next */ []));
20744
- /** Id of the sr-only instructions element the items point to. */
20745
- instructionsId = `strct-reorder-hint-${++reorderCounter}`;
20746
- 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);
20747
20901
  constructor() {
20748
- // Keep the sr-only instructions element in sync with the input.
20749
- effect(() => {
20750
- const text = this.instructions();
20751
- if (text && !this.hint) {
20752
- this.hint = this.renderer.createElement('span');
20753
- this.renderer.setAttribute(this.hint, 'id', this.instructionsId);
20754
- this.hint.style.cssText =
20755
- 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
20756
- this.renderer.appendChild(this.doc.body, this.hint);
20757
- }
20758
- if (this.hint)
20759
- this.hint.textContent = text;
20760
- });
20761
- }
20762
- items() {
20763
- return [...this.host.nativeElement.querySelectorAll('[strctReorderItem]')];
20764
- }
20765
- indexOf(el) {
20766
- return this.items().indexOf(el);
20767
- }
20768
- commit(from, to) {
20769
- this.dragIndex.set(null);
20770
- this.overIndex.set(null);
20771
- if (from < 0 || to < 0 || from === to)
20772
- return;
20773
- const label = this.items()[from]?.textContent?.trim() ?? '';
20774
- this.reordered.emit({ from, to });
20775
- this.announcer.announce(this.announcement()(label, to + 1, this.items().length));
20776
- }
20777
- onContainerDragOver(event) {
20778
- // Only the container's own empty space (below the last item); drags over
20779
- // items are handled by the items themselves.
20780
- if (this.dragIndex() == null)
20781
- return;
20782
- if (event.target.closest('[strctReorderItem]'))
20783
- return;
20784
- event.preventDefault();
20902
+ document.addEventListener('keydown', this.listener);
20785
20903
  }
20786
- onContainerDrop(event) {
20787
- const from = this.dragIndex();
20788
- if (from == null)
20789
- return; // an item already consumed this drop
20790
- if (event.target.closest('[strctReorderItem]'))
20791
- return;
20792
- event.preventDefault();
20793
- // Drop into empty space moves the item to the end.
20794
- 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));
20795
20908
  }
20796
- onDragLeave(event) {
20797
- // dragleave also fires when moving between children; only clear the
20798
- // highlight when the drag truly leaves the container.
20799
- const next = event.relatedTarget;
20800
- 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);
20801
20920
  return;
20802
- this.overIndex.set(null);
20921
+ }
20803
20922
  }
20804
20923
  ngOnDestroy() {
20805
- if (this.hint) {
20806
- this.renderer.removeChild(this.doc.body, this.hint);
20807
- this.hint = null;
20808
- }
20924
+ document.removeEventListener('keydown', this.listener);
20809
20925
  }
20810
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
20811
- 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' });
20812
20928
  }
20813
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorder, decorators: [{
20814
- type: Directive,
20815
- args: [{ selector: '[strctReorder]' }]
20816
- }], 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: [{
20817
- type: HostListener,
20818
- args: ['dragover', ['$event']]
20819
- }], onContainerDrop: [{
20820
- type: HostListener,
20821
- args: ['drop', ['$event']]
20822
- }], onDragLeave: [{
20823
- type: HostListener,
20824
- args: ['dragleave', ['$event']]
20825
- }] } });
20826
- /** One draggable row inside a `[strctReorder]` container. */
20827
- class StrctReorderItem {
20828
- list = inject(StrctReorder);
20829
- el = inject(ElementRef);
20830
- // Captured before our own binding ever writes tabindex, so it never flips.
20831
- hadOwnTabindex = this.el.nativeElement.hasAttribute('tabindex');
20832
- hostTabindex() {
20833
- return this.hadOwnTabindex ? null : 0;
20834
- }
20835
- isDragging() {
20836
- return this.list.dragIndex() === this.index();
20837
- }
20838
- isOver() {
20839
- return this.list.overIndex() === this.index() && this.list.dragIndex() !== this.index();
20840
- }
20841
- index() {
20842
- return this.list.indexOf(this.el.nativeElement);
20843
- }
20844
- onDragStart(event) {
20845
- if (this.list.reorderDisabled())
20846
- return;
20847
- this.list.dragIndex.set(this.index());
20848
- event.dataTransfer?.setData('text/plain', String(this.index()));
20849
- if (event.dataTransfer)
20850
- event.dataTransfer.effectAllowed = 'move';
20851
- }
20852
- onDragOver(event) {
20853
- if (this.list.dragIndex() == null)
20854
- return;
20855
- event.preventDefault();
20856
- 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
+ });
20857
21001
  }
20858
- onDrop(event) {
20859
- event.preventDefault();
20860
- const from = this.list.dragIndex();
20861
- 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())
20862
21006
  return;
20863
- 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);
20864
21011
  }
20865
- onDragEnd() {
20866
- this.list.dragIndex.set(null);
20867
- 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
+ });
20868
21024
  }
20869
- onKeydown(event) {
20870
- if (this.list.reorderDisabled() || !event.altKey)
20871
- return;
20872
- if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
20873
- return;
20874
- event.preventDefault();
20875
- const from = this.index();
20876
- const to = event.key === 'ArrowUp' ? from - 1 : from + 1;
20877
- if (to < 0 || to >= this.list.items().length)
20878
- return;
20879
- this.list.commit(from, to);
20880
- // Keep focus on the moved item after the consumer re-renders.
20881
- 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>
20882
21062
  }
20883
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorderItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
20884
- 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 });
20885
- }
20886
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctReorderItem, decorators: [{
20887
- type: Directive,
20888
- args: [{
20889
- selector: '[strctReorderItem]',
20890
- host: {
20891
- '[attr.draggable]': '!list.reorderDisabled()',
20892
- '[attr.tabindex]': 'hostTabindex()',
20893
- '[class.strct-reorder--dragging]': 'isDragging()',
20894
- '[class.strct-reorder--over]': 'isOver()',
20895
- '[attr.aria-roledescription]': "'sortable'",
20896
- '[attr.aria-keyshortcuts]': "'Alt+ArrowUp Alt+ArrowDown'",
20897
- '[attr.aria-posinset]': 'index() + 1',
20898
- '[attr.aria-setsize]': 'list.items().length',
20899
- '[attr.aria-describedby]': 'list.instructions() ? list.instructionsId : null',
20900
- },
20901
- }]
20902
- }], propDecorators: { onDragStart: [{
20903
- type: HostListener,
20904
- args: ['dragstart', ['$event']]
20905
- }], onDragOver: [{
20906
- type: HostListener,
20907
- args: ['dragover', ['$event']]
20908
- }], onDrop: [{
20909
- type: HostListener,
20910
- args: ['drop', ['$event']]
20911
- }], onDragEnd: [{
20912
- type: HostListener,
20913
- args: ['dragend']
20914
- }], onKeydown: [{
20915
- type: HostListener,
20916
- args: ['keydown', ['$event']]
20917
- }] } });
20918
-
20919
- /**
20920
- * Default accessible text per status, used when `label` is not given
20921
- * (localizable — override via `label`). The canonical `StrctStatus` vocabulary
20922
- * has no 'ok'/'info': OK maps to 'success', info to 'accent'.
20923
- */
20924
- const DEFAULT_LABEL = {
20925
- neutral: 'Neutral',
20926
- accent: 'Info',
20927
- success: 'OK',
20928
- warning: 'Warning',
20929
- critical: 'Critical',
20930
- };
20931
- /**
20932
- * Presence/state dot that never relies on colour alone: the tone is painted by
20933
- * a pure-CSS dot while the state is also rendered as visually-hidden text, so
20934
- * screen readers (and colour-blind users copying text) get "Warning", not just
20935
- * a yellow circle. Use it inside avatars, menu rows, metric tiles — anywhere a
20936
- * bare coloured dot would otherwise carry meaning.
20937
- *
20938
- * <strct-status-dot status="success" />
20939
- * <strct-status-dot status="critical" size="sm" label="Node unreachable" />
20940
- */
20941
- class StrctStatusDot {
20942
- /** Status tone (canonical vocabulary: OK = 'success', info = 'accent'). */
20943
- status = input('neutral', /* @ts-ignore */
20944
- ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
20945
- /**
20946
- * Accessible text describing the state (localizable). Empty falls back to a
20947
- * per-status default ("OK", "Warning", …) so the dot is never color-only.
20948
- */
20949
- label = input('', /* @ts-ignore */
20950
- ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
20951
- /** Dot size: `sm` for dense rows, `md` standalone. */
20952
- size = input('md', /* @ts-ignore */
20953
- ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
20954
- text = computed(() => this.label() || DEFAULT_LABEL[this.status()], /* @ts-ignore */
20955
- ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
20956
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctStatusDot, deps: [], target: i0.ɵɵFactoryTarget.Component });
20957
- 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: `
20958
- <span class="strct-dot__dot" aria-hidden="true"></span>
20959
- <span class="strct-dot__sr">{{ text() }}</span>
20960
- `, 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 });
20961
21064
  }
20962
- 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: [{
20963
21066
  type: Component,
20964
- args: [{ selector: 'strct-status-dot', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
20965
- <span class="strct-dot__dot" aria-hidden="true"></span>
20966
- <span class="strct-dot__sr">{{ text() }}</span>
20967
- `, host: {
20968
- class: 'strct-dot',
20969
- '[class.strct-dot--accent]': "status() === 'accent'",
20970
- '[class.strct-dot--success]': "status() === 'success'",
20971
- '[class.strct-dot--warning]': "status() === 'warning'",
20972
- '[class.strct-dot--critical]': "status() === 'critical'",
20973
- '[class.strct-dot--sm]': "size() === 'sm'",
20974
- }, 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"] }]
20975
- }], 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
+ }] } });
20976
21109
 
21110
+ let reorderCounter = 0;
20977
21111
  /**
20978
- * Numeric stepper input: − / + buttons flanking a text field. The value is
20979
- * `number | null` (null = empty); typing is free-form (intermediate states
20980
- * like "-" are kept), committed values clamp to `min`/`max` on blur, and the
20981
- * buttons + keyboard always clamp immediately.
21112
+ * List drag-reorder primitive — the consumer owns the array:
20982
21113
  *
20983
- * <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>
20984
21119
  *
20985
- * Keyboard on the field: ArrowUp/Down ±step, PageUp/Down ±10×step,
20986
- * Home/End jump to min/max. The field exposes `role="spinbutton"` with
20987
- * 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`.
20988
21129
  */
20989
- class StrctNumber {
20990
- /** Minimum allowed value; null = unbounded. */
20991
- min = input(null, /* @ts-ignore */
20992
- ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
20993
- /** Maximum allowed value; null = unbounded. */
20994
- max = input(null, /* @ts-ignore */
20995
- ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
20996
- /** Step increment for buttons and Arrow keys (PageUp/Down use 10×). */
20997
- step = input(1, /* @ts-ignore */
20998
- ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
20999
- /** Placeholder text when empty. */
21000
- placeholder = input('', /* @ts-ignore */
21001
- ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
21002
- /** Accessible label of the + button (localizable). */
21003
- incrementLabel = input('Increment', /* @ts-ignore */
21004
- ...(ngDevMode ? [{ debugName: "incrementLabel" }] : /* istanbul ignore next */ []));
21005
- /** Accessible label of the − button (localizable). */
21006
- decrementLabel = input('Decrement', /* @ts-ignore */
21007
- ...(ngDevMode ? [{ debugName: "decrementLabel" }] : /* istanbul ignore next */ []));
21008
- /** Static disable; forms' setDisabledState also drives the disabled state. */
21009
- disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
21010
- value = signal(null, /* @ts-ignore */
21011
- ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
21012
- /** Raw field text — may hold an unparseable intermediate state while typing. */
21013
- text = signal('', /* @ts-ignore */
21014
- ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
21015
- /** Disabled state pushed by the forms API (setDisabledState). */
21016
- cvaDisabled = signal(false, /* @ts-ignore */
21017
- ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
21018
- isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
21019
- ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
21020
- /** At/below min (or min unset → never) — greys out the − button. */
21021
- atMin = computed(() => {
21022
- const v = this.value();
21023
- const m = this.min();
21024
- return v !== null && m !== null && v <= m;
21025
- }, /* @ts-ignore */
21026
- ...(ngDevMode ? [{ debugName: "atMin" }] : /* istanbul ignore next */ []));
21027
- /** At/above max (or max unset → never) — greys out the + button. */
21028
- atMax = computed(() => {
21029
- const v = this.value();
21030
- const m = this.max();
21031
- return v !== null && m !== null && v >= m;
21032
- }, /* @ts-ignore */
21033
- ...(ngDevMode ? [{ debugName: "atMax" }] : /* istanbul ignore next */ []));
21034
- onChange = () => { };
21035
- onTouched = () => { };
21036
- onInput(event) {
21037
- const raw = event.target.value;
21038
- this.text.set(raw);
21039
- const trimmed = raw.trim();
21040
- if (trimmed === '') {
21041
- this.value.set(null);
21042
- 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)
21043
21183
  return;
21044
- }
21045
- const parsed = Number(trimmed);
21046
- // Intermediate states ("-", "1.", "2e") don't parse yet — keep the text,
21047
- // hold the last value; clamping happens on blur.
21048
- if (Number.isFinite(parsed)) {
21049
- this.value.set(parsed);
21050
- this.onChange(parsed);
21051
- }
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));
21052
21187
  }
21053
- onKeydown(event) {
21054
- 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)
21055
21192
  return;
21056
- switch (event.key) {
21057
- case 'ArrowUp':
21058
- event.preventDefault();
21059
- this.stepBy(1);
21060
- break;
21061
- case 'ArrowDown':
21062
- event.preventDefault();
21063
- this.stepBy(-1);
21064
- break;
21065
- case 'PageUp':
21066
- event.preventDefault();
21067
- this.stepBy(1, 10);
21068
- break;
21069
- case 'PageDown':
21070
- event.preventDefault();
21071
- this.stepBy(-1, 10);
21072
- break;
21073
- case 'Home': {
21074
- const m = this.min();
21075
- if (m === null)
21076
- return;
21077
- event.preventDefault();
21078
- this.commitTo(m);
21079
- break;
21080
- }
21081
- case 'End': {
21082
- const m = this.max();
21083
- if (m === null)
21084
- return;
21085
- event.preventDefault();
21086
- this.commitTo(m);
21087
- break;
21088
- }
21089
- }
21193
+ if (event.target.closest('[strctReorderItem]'))
21194
+ return;
21195
+ event.preventDefault();
21090
21196
  }
21091
- /** Commit the pending text on blur: parse, clamp, restore on garbage. */
21092
- onBlur() {
21093
- const trimmed = this.text().trim();
21094
- if (trimmed === '') {
21095
- if (this.value() !== null) {
21096
- this.value.set(null);
21097
- this.onChange(null);
21098
- }
21099
- this.text.set('');
21100
- }
21101
- else {
21102
- const parsed = Number(trimmed);
21103
- if (Number.isFinite(parsed)) {
21104
- const clamped = this.clamp(parsed);
21105
- this.value.set(clamped);
21106
- if (clamped !== parsed)
21107
- this.onChange(clamped);
21108
- this.text.set(String(clamped));
21109
- }
21110
- else {
21111
- // Unparseable — restore the last committed value.
21112
- this.text.set(this.value() === null ? '' : String(this.value()));
21113
- }
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;
21114
21219
  }
21115
- this.onTouched();
21116
21220
  }
21117
- /** Button / keyboard step: an empty field first lands on min (or 0). */
21118
- stepBy(dir, factor = 1) {
21119
- if (this.isDisabled())
21120
- return;
21121
- const v = this.value();
21122
- if (v === null) {
21123
- this.commitTo(this.min() ?? 0);
21124
- return;
21125
- }
21126
- 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();
21127
21248
  }
21128
- commitTo(n) {
21129
- const clamped = this.clamp(this.snap(n));
21130
- this.value.set(clamped);
21131
- this.text.set(String(clamped));
21132
- this.onChange(clamped);
21133
- this.onTouched();
21249
+ isOver() {
21250
+ return this.list.overIndex() === this.index() && this.list.dragIndex() !== this.index();
21134
21251
  }
21135
- clamp(n) {
21136
- const lo = this.min();
21137
- const hi = this.max();
21138
- if (lo !== null && n < lo)
21139
- return lo;
21140
- if (hi !== null && n > hi)
21141
- return hi;
21142
- return n;
21252
+ index() {
21253
+ return this.list.indexOf(this.el.nativeElement);
21143
21254
  }
21144
- /** Trim float dust from step arithmetic (0.1 + 0.2), not a grid snap. */
21145
- snap(n) {
21146
- const step = this.step();
21147
- if (!Number.isFinite(step) || step <= 0)
21148
- return n;
21149
- const decimals = (String(step).split('.')[1] ?? '').length;
21150
- 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';
21151
21262
  }
21152
- writeValue(value) {
21153
- const parsed = value === null || value === undefined ? null : Number(value);
21154
- const v = parsed !== null && Number.isFinite(parsed) ? this.clamp(parsed) : null;
21155
- this.value.set(v);
21156
- 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());
21157
21268
  }
21158
- registerOnChange(fn) {
21159
- 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());
21160
21275
  }
21161
- registerOnTouched(fn) {
21162
- this.onTouched = fn;
21276
+ onDragEnd() {
21277
+ this.list.dragIndex.set(null);
21278
+ this.list.overIndex.set(null);
21163
21279
  }
21164
- setDisabledState(isDisabled) {
21165
- 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());
21166
21293
  }
21167
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctNumber, deps: [], target: i0.ɵɵFactoryTarget.Component });
21168
- 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: [
21169
- { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctNumber), multi: true },
21170
- ], ngImport: i0, template: `
21171
- <div class="strct-num__wrap">
21172
- <button
21173
- type="button"
21174
- class="strct-num__btn"
21175
- [attr.aria-label]="decrementLabel()"
21176
- [disabled]="isDisabled() || atMin()"
21177
- (click)="stepBy(-1)"
21178
- >
21179
- <strct-icon strictName="minus" [size]="12" [strokeWidth]="1.8" />
21180
- </button>
21181
- <input
21182
- type="text"
21183
- inputmode="decimal"
21184
- class="strct-control strct-num__input"
21185
- role="spinbutton"
21186
- [attr.aria-valuemin]="min()"
21187
- [attr.aria-valuemax]="max()"
21188
- [attr.aria-valuenow]="value()"
21189
- [placeholder]="placeholder()"
21190
- [value]="text()"
21191
- [disabled]="isDisabled()"
21192
- (input)="onInput($event)"
21193
- (keydown)="onKeydown($event)"
21194
- (blur)="onBlur()"
21195
- />
21196
- <button
21197
- type="button"
21198
- class="strct-num__btn"
21199
- [attr.aria-label]="incrementLabel()"
21200
- [disabled]="isDisabled() || atMax()"
21201
- (click)="stepBy(1)"
21202
- >
21203
- <strct-icon strictName="plus" [size]="12" [strokeWidth]="1.8" />
21204
- </button>
21205
- </div>
21206
- `, 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 });
21207
21296
  }
21208
- 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: [{
21209
21374
  type: Component,
21210
- args: [{ selector: 'strct-number', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], providers: [
21211
- { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctNumber), multi: true },
21212
- ], template: `
21213
- <div class="strct-num__wrap">
21214
- <button
21215
- type="button"
21216
- class="strct-num__btn"
21217
- [attr.aria-label]="decrementLabel()"
21218
- [disabled]="isDisabled() || atMin()"
21219
- (click)="stepBy(-1)"
21220
- >
21221
- <strct-icon strictName="minus" [size]="12" [strokeWidth]="1.8" />
21222
- </button>
21223
- <input
21224
- type="text"
21225
- inputmode="decimal"
21226
- class="strct-control strct-num__input"
21227
- role="spinbutton"
21228
- [attr.aria-valuemin]="min()"
21229
- [attr.aria-valuemax]="max()"
21230
- [attr.aria-valuenow]="value()"
21231
- [placeholder]="placeholder()"
21232
- [value]="text()"
21233
- [disabled]="isDisabled()"
21234
- (input)="onInput($event)"
21235
- (keydown)="onKeydown($event)"
21236
- (blur)="onBlur()"
21237
- />
21238
- <button
21239
- type="button"
21240
- class="strct-num__btn"
21241
- [attr.aria-label]="incrementLabel()"
21242
- [disabled]="isDisabled() || atMax()"
21243
- (click)="stepBy(1)"
21244
- >
21245
- <strct-icon strictName="plus" [size]="12" [strokeWidth]="1.8" />
21246
- </button>
21247
- </div>
21248
- `, 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"] }]
21249
- }], 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 }] }] } });
21250
21387
 
21251
21388
  /**
21252
21389
  * A generic anchored overlay panel for arbitrary projected content — the