@flyos/design-system 3.1.0 → 3.2.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.
@@ -47,7 +47,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
47
47
  // tools/publish-library.ps1 at bump time, and asserted by the spec beside this file.
48
48
  // Used only for the diagnostic message; the duplicate-instance detection itself is
49
49
  // version-agnostic, so a stale literal misnames a fork rather than hiding one.
50
- const FLY_DS_VERSION = '3.1.0';
50
+ const FLY_DS_VERSION = '3.2.0';
51
51
  const FLY_DS_REGISTRY_KEY = '__FLY_DS_INSTANCES__';
52
52
  /**
53
53
  * Records this design-system instance on the shared `scope` and returns the
@@ -1498,6 +1498,14 @@ const DS_BASELINE_LOCALES = {
1498
1498
  'gantt.aria.resize_start': 'Resize start date',
1499
1499
  'gantt.aria.resize_end': 'Resize end date',
1500
1500
  'gantt.aria.link_handle': 'Drag to link from {{label}}',
1501
+ 'gantt.aria.link_handle_start': 'Drag to link from the start of {{label}}',
1502
+ 'gantt.aria.label_resize': 'Resize the label column',
1503
+ 'gantt.aria.link': '{{type}} dependency: {{from}} to {{to}}',
1504
+ 'gantt.aria.delete_link': 'Delete dependency',
1505
+ 'gantt.link_type.fs': 'Finish-to-start',
1506
+ 'gantt.link_type.ss': 'Start-to-start',
1507
+ 'gantt.link_type.ff': 'Finish-to-finish',
1508
+ 'gantt.link_type.sf': 'Start-to-finish',
1501
1509
  // dynamic-form (fly-dynamic-form / fly-survey-form)
1502
1510
  'form.required_error': 'This field is required.',
1503
1511
  'form.yes': 'Yes',
@@ -1880,6 +1888,14 @@ const DS_BASELINE_LOCALES = {
1880
1888
  'gantt.aria.resize_start': 'تغيير تاريخ البدء',
1881
1889
  'gantt.aria.resize_end': 'تغيير تاريخ الانتهاء',
1882
1890
  'gantt.aria.link_handle': 'اسحب للربط من {{label}}',
1891
+ 'gantt.aria.link_handle_start': 'اسحب للربط من بداية {{label}}',
1892
+ 'gantt.aria.label_resize': 'تغيير عرض عمود التسميات',
1893
+ 'gantt.aria.link': 'ارتباط {{type}}: من {{from}} إلى {{to}}',
1894
+ 'gantt.aria.delete_link': 'حذف الارتباط',
1895
+ 'gantt.link_type.fs': 'من النهاية إلى البداية',
1896
+ 'gantt.link_type.ss': 'من البداية إلى البداية',
1897
+ 'gantt.link_type.ff': 'من النهاية إلى النهاية',
1898
+ 'gantt.link_type.sf': 'من البداية إلى النهاية',
1883
1899
  // dynamic-form (fly-dynamic-form / fly-survey-form)
1884
1900
  'form.required_error': 'هذا الحقل مطلوب.',
1885
1901
  'form.yes': 'نعم',
@@ -2262,6 +2278,14 @@ const DS_BASELINE_LOCALES = {
2262
2278
  'gantt.aria.resize_start': 'Redimensionner la date de début',
2263
2279
  'gantt.aria.resize_end': 'Redimensionner la date de fin',
2264
2280
  'gantt.aria.link_handle': 'Glisser pour lier depuis {{label}}',
2281
+ 'gantt.aria.link_handle_start': 'Glisser pour lier depuis le début de {{label}}',
2282
+ 'gantt.aria.label_resize': 'Redimensionner la colonne des libellés',
2283
+ 'gantt.aria.link': 'Liaison {{type}} : de {{from}} à {{to}}',
2284
+ 'gantt.aria.delete_link': 'Supprimer la liaison',
2285
+ 'gantt.link_type.fs': 'Fin à début',
2286
+ 'gantt.link_type.ss': 'Début à début',
2287
+ 'gantt.link_type.ff': 'Fin à fin',
2288
+ 'gantt.link_type.sf': 'Début à fin',
2265
2289
  // dynamic-form (fly-dynamic-form / fly-survey-form)
2266
2290
  'form.required_error': 'Ce champ est obligatoire.',
2267
2291
  'form.yes': 'Oui',
@@ -2643,6 +2667,14 @@ const DS_BASELINE_LOCALES = {
2643
2667
  'gantt.aria.resize_start': 'آغاز کی تاریخ تبدیل کریں',
2644
2668
  'gantt.aria.resize_end': 'اختتام کی تاریخ تبدیل کریں',
2645
2669
  'gantt.aria.link_handle': '{{label}} سے لنک کرنے کے لیے گھسیٹیں',
2670
+ 'gantt.aria.link_handle_start': '{{label}} کے آغاز سے لنک کرنے کے لیے گھسیٹیں',
2671
+ 'gantt.aria.label_resize': 'لیبل کالم کی چوڑائی تبدیل کریں',
2672
+ 'gantt.aria.link': '{{type}} انحصار: {{from}} سے {{to}} تک',
2673
+ 'gantt.aria.delete_link': 'انحصار حذف کریں',
2674
+ 'gantt.link_type.fs': 'اختتام سے آغاز',
2675
+ 'gantt.link_type.ss': 'آغاز سے آغاز',
2676
+ 'gantt.link_type.ff': 'اختتام سے اختتام',
2677
+ 'gantt.link_type.sf': 'آغاز سے اختتام',
2646
2678
  // dynamic-form (fly-dynamic-form / fly-survey-form)
2647
2679
  'form.required_error': 'یہ خانہ لازمی ہے۔',
2648
2680
  'form.yes': 'ہاں',
@@ -12422,6 +12454,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
12422
12454
  }, template: "<!-- \u2500\u2500 Frequency selector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n<div class=\"fly-cron__row\">\r\n <label class=\"fly-cron__label\" [attr.for]=\"modeSelectId\">{{ 'cron.frequency_label' | translate }}</label>\r\n <!-- Selection is synced imperatively by an effect() (see the component) \u2014 a [value] /\r\n [selected] binding is applied before the @for options exist on cold render and falls\r\n back to the first option. The #modeSelect ref is what the effect drives. -->\r\n <select\r\n #modeSelect\r\n class=\"fly-cron__select\"\r\n [id]=\"modeSelectId\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (change)=\"onModeChange($any($event.target).value)\"\r\n >\r\n @for (m of availableModes(); track m) {\r\n <option [value]=\"m\">{{ ('cron.mode.' + m) | translate }}</option>\r\n }\r\n </select>\r\n</div>\r\n\r\n<!-- \u2500\u2500 Mode-specific editor \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n@switch (mode()) {\r\n @case ('minutes') {\r\n <div class=\"fly-cron__row\">\r\n <span class=\"fly-cron__label\">{{ 'cron.every_label' | translate }}</span>\r\n <input\r\n class=\"fly-cron__num\"\r\n type=\"number\"\r\n min=\"1\"\r\n max=\"59\"\r\n [value]=\"everyN()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (input)=\"onEveryN($any($event.target).value)\"\r\n />\r\n <span class=\"fly-cron__suffix\">{{ 'cron.unit_minutes' | translate }}</span>\r\n </div>\r\n }\r\n\r\n @case ('hourly') {\r\n <div class=\"fly-cron__row fly-cron__row--wrap\">\r\n <span class=\"fly-cron__label\">{{ 'cron.every_label' | translate }}</span>\r\n <input\r\n class=\"fly-cron__num\"\r\n type=\"number\"\r\n min=\"1\"\r\n max=\"23\"\r\n [value]=\"everyN()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (input)=\"onEveryN($any($event.target).value)\"\r\n />\r\n <span class=\"fly-cron__suffix\">{{ 'cron.unit_hours' | translate }}</span>\r\n <span class=\"fly-cron__suffix\">{{ 'cron.at_minute_label' | translate }}</span>\r\n <input\r\n class=\"fly-cron__num\"\r\n type=\"number\"\r\n min=\"0\"\r\n max=\"59\"\r\n [value]=\"minute()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (input)=\"onMinute($any($event.target).value)\"\r\n />\r\n </div>\r\n }\r\n\r\n @case ('daily') {\r\n <div class=\"fly-cron__row\">\r\n <label class=\"fly-cron__label\" [attr.for]=\"timeInputId\">{{ 'cron.time_label' | translate }}</label>\r\n <input\r\n class=\"fly-cron__time\"\r\n type=\"time\"\r\n [id]=\"timeInputId\"\r\n [value]=\"timeValue()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (input)=\"onTime($any($event.target).value)\"\r\n />\r\n </div>\r\n }\r\n\r\n @case ('weekly') {\r\n <div class=\"fly-cron__row fly-cron__row--wrap\">\r\n <span class=\"fly-cron__label\">{{ 'cron.on_days_label' | translate }}</span>\r\n <div class=\"fly-cron__weekdays\" role=\"group\" [attr.aria-label]=\"'cron.on_days_label' | translate\">\r\n @for (d of weekdayOrder(); track d) {\r\n <button\r\n type=\"button\"\r\n role=\"checkbox\"\r\n class=\"fly-cron__chip\"\r\n [class.fly-cron__chip--on]=\"isWeekday(d)\"\r\n [attr.aria-checked]=\"isWeekday(d)\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (click)=\"toggleWeekday(d)\"\r\n >{{ ('cron.weekday_short.' + d) | translate }}</button>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"fly-cron__row\">\r\n <label class=\"fly-cron__label\" [attr.for]=\"timeInputId\">{{ 'cron.time_label' | translate }}</label>\r\n <input\r\n class=\"fly-cron__time\"\r\n type=\"time\"\r\n [id]=\"timeInputId\"\r\n [value]=\"timeValue()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (input)=\"onTime($any($event.target).value)\"\r\n />\r\n </div>\r\n }\r\n\r\n @case ('monthly') {\r\n <div class=\"fly-cron__row fly-cron__row--wrap\">\r\n <span class=\"fly-cron__label\">{{ 'cron.day_of_month_label' | translate }}</span>\r\n <input\r\n class=\"fly-cron__num\"\r\n type=\"number\"\r\n min=\"1\"\r\n max=\"31\"\r\n [value]=\"dayOfMonth()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (input)=\"onDayOfMonth($any($event.target).value)\"\r\n />\r\n <label class=\"fly-cron__label\" [attr.for]=\"timeInputId\">{{ 'cron.time_label' | translate }}</label>\r\n <input\r\n class=\"fly-cron__time\"\r\n type=\"time\"\r\n [id]=\"timeInputId\"\r\n [value]=\"timeValue()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (input)=\"onTime($any($event.target).value)\"\r\n />\r\n </div>\r\n }\r\n\r\n @case ('advanced') {\r\n <div class=\"fly-cron__advanced\">\r\n @for (f of advancedFieldDefs(); track f.index) {\r\n <div class=\"fly-cron__field\">\r\n <label class=\"fly-cron__field-label\" [attr.for]=\"fieldInputId(f.index)\">{{ f.labelKey | translate }}</label>\r\n <input\r\n [id]=\"fieldInputId(f.index)\"\r\n class=\"fly-cron__field-input fly-cron__mono\"\r\n type=\"text\"\r\n inputmode=\"text\"\r\n autocomplete=\"off\"\r\n spellcheck=\"false\"\r\n placeholder=\"*\"\r\n [value]=\"fields()[f.index]\"\r\n [disabled]=\"effectiveDisabled()\"\r\n [attr.aria-invalid]=\"!fieldValid(f.index)\"\r\n [attr.aria-label]=\"f.labelKey | translate\"\r\n (input)=\"onFieldInput(f.index, $any($event.target).value)\"\r\n />\r\n </div>\r\n }\r\n </div>\r\n <p class=\"fly-cron__hint\">{{ 'cron.advanced.hint' | translate }}</p>\r\n }\r\n}\r\n\r\n<!-- \u2500\u2500 Summary + resulting expression \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n@if (mode() !== 'none') {\r\n <div class=\"fly-cron__summary\" [class.fly-cron__summary--error]=\"!valid()\" aria-live=\"polite\">\r\n <span class=\"fly-cron__summary-text\">{{ summary() }}</span>\r\n @if (expression(); as expr) {\r\n <code class=\"fly-cron__expr fly-cron__mono\">{{ expr }}</code>\r\n }\r\n </div>\r\n}\r\n\r\n@if (errorKey(); as ek) {\r\n <p class=\"fly-cron__error\" role=\"alert\">{{ ek | translate }}</p>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_surface-active: var(--surface-active, #eef2ff);--_surface-muted: var(--surface-section, #f8fafc);--_border: var(--surface-border, #e2e8f0);--_text: var(--text-color, #0f172a);--_text-subtle: var(--text-color-secondary, #64748b);--_primary: var(--primary-color);--_primary-contrast: var(--primary-color-text, #fff);--_danger: var(--red-500, #ef4444);--_radius: 8px;display:block;inline-size:100%;font-size:13px;color:var(--_text)}:host(.fly-cron-builder--disabled){opacity:.6}.fly-cron__row{display:flex;align-items:center;gap:8px;margin-block-end:10px}.fly-cron__row--wrap{flex-wrap:wrap}.fly-cron__label{flex:0 0 auto;color:var(--_text-subtle);font-weight:600}.fly-cron__suffix{flex:0 0 auto;color:var(--_text-subtle)}.fly-cron__select,.fly-cron__num,.fly-cron__time{block-size:2.25rem;padding-inline:10px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit}.fly-cron__select{flex:1 1 auto;min-inline-size:10rem;cursor:pointer}.fly-cron__num{inline-size:4.5rem;text-align:end}.fly-cron__time{inline-size:auto}.fly-cron__select:focus-visible,.fly-cron__num:focus-visible,.fly-cron__time:focus-visible,.fly-cron__field-input:focus-visible{outline:2px solid var(--_primary);outline-offset:1px}.fly-cron__select:disabled,.fly-cron__num:disabled,.fly-cron__time:disabled,.fly-cron__field-input:disabled{opacity:.55;cursor:not-allowed}.fly-cron__weekdays{display:flex;flex-wrap:wrap;gap:6px}.fly-cron__chip{min-inline-size:2.5rem;block-size:2rem;padding-inline:10px;border:1px solid var(--_border);border-radius:999px;background:var(--_surface);color:var(--_text);font:inherit;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}.fly-cron__chip:hover:not(:disabled){background:var(--_surface-hover)}.fly-cron__chip--on{background:var(--_primary);border-color:var(--_primary);color:var(--_primary-contrast);font-weight:600}.fly-cron__chip:focus-visible{outline:2px solid var(--_primary);outline-offset:2px}.fly-cron__chip:disabled{cursor:not-allowed}.fly-cron__advanced{display:flex;flex-wrap:wrap;gap:8px;margin-block-end:6px}.fly-cron__field{display:flex;flex-direction:column;gap:4px}.fly-cron__field-label{color:var(--_text-subtle);font-size:11px;font-weight:600}.fly-cron__field-input{inline-size:5rem;block-size:2.25rem;padding-inline:8px;border:1px solid var(--_border);border-radius:6px;background:var(--_surface);color:var(--_text);font:inherit;text-align:center}.fly-cron__field-input[aria-invalid=true]{border-color:var(--_danger)}.fly-cron__hint{margin:0 0 8px;color:var(--_text-subtle);font-size:11px}.fly-cron__summary{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface-muted)}.fly-cron__summary--error{border-color:var(--_danger)}.fly-cron__summary-text{flex:1 1 auto}.fly-cron__expr{flex:0 0 auto;padding:2px 8px;border-radius:6px;background:var(--_surface);border:1px solid var(--_border);color:var(--_text-subtle);font-size:12px;direction:ltr;unicode-bidi:isolate}.fly-cron__mono{font-family:var(--font-family-mono, ui-monospace, \"SFMono-Regular\", \"Menlo\", \"Consolas\", monospace)}.fly-cron__error{margin:6px 0 0;color:var(--_danger);font-size:12px}\n"] }]
12423
12455
  }], propDecorators: { seconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "seconds", required: false }] }], allowNone: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowNone", required: false }] }], modes: [{ type: i0.Input, args: [{ isSignal: true, alias: "modes", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], cronChange: [{ type: i0.Output, args: ["cronChange"] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], modeSelectRef: [{ type: i0.ViewChild, args: ['modeSelect', { isSignal: true }] }] } });
12424
12456
 
12457
+ /**
12458
+ * Public data contract for {@link FlyGanttComponent}.
12459
+ *
12460
+ * The types here are deliberately **app-agnostic** — no Projects/Tasks vocabulary leaks in.
12461
+ * A consumer maps its own domain rows to {@link GanttRow}[] + {@link GanttDependency}[],
12462
+ * feeds them in, and reacts to the emitted change events by persisting itself. The Gantt
12463
+ * never calls a backend and never mutates its inputs.
12464
+ */
12465
+ /** Immutable list of the supported zoom levels, coarsest-last. */
12466
+ const GANTT_ZOOMS = ['day', 'week', 'month', 'quarter'];
12467
+ /** Immutable list of the supported relationship kinds, `FS` first. */
12468
+ const GANTT_DEPENDENCY_TYPES = [
12469
+ 'FS',
12470
+ 'SS',
12471
+ 'FF',
12472
+ 'SF',
12473
+ ];
12474
+ /**
12475
+ * Alpha the chart paints {@link GanttRow.backgroundColor} at. Deliberately low and fixed: the
12476
+ * tint is a *band behind* the bars and the row label, so a consumer can pass a saturated brand
12477
+ * colour without pre-computing a pastel that survives both themes.
12478
+ */
12479
+ const GANTT_ROW_TINT_ALPHA = 0.16;
12480
+
12425
12481
  const MS_PER_DAY = 86_400_000;
12426
12482
  /** Pixels allotted to one calendar day at each zoom level. Everything scales off this. */
12427
12483
  const ZOOM_PX_PER_DAY = {
@@ -12677,11 +12733,16 @@ function resolveLinks(deps, renderableIds) {
12677
12733
  continue;
12678
12734
  if (!renderableIds.has(d.fromId) || !renderableIds.has(d.toId))
12679
12735
  continue;
12680
- const key = `${d.fromId}${d.toId}`;
12736
+ // `\u0000` (written as an escape, not as a literal NUL — a raw NUL byte in the source makes
12737
+ // the whole file read as binary to grep/ripgrep and silently excludes it from code search)
12738
+ // cannot occur in a row id, so it is a collision-proof pair separator.
12739
+ const key = `${d.fromId}\u0000${d.toId}`;
12740
+ // One relationship per ordered pair, first declaration wins: MS Project models at most one
12741
+ // link between two tasks, so a second edge for the same pair is a duplicate, not an SS+FF.
12681
12742
  if (seen.has(key))
12682
12743
  continue;
12683
12744
  seen.add(key);
12684
- out.push({ fromId: d.fromId, toId: d.toId });
12745
+ out.push({ fromId: d.fromId, toId: d.toId, type: d.type ?? 'FS' });
12685
12746
  }
12686
12747
  return out;
12687
12748
  }
@@ -12795,15 +12856,37 @@ const HEADER_H = HEADER_UPPER_H + HEADER_LOWER_H;
12795
12856
  const BAR_V_PAD = 6;
12796
12857
  /** Edge-handle hit width (px) for resize + the link connector radius. */
12797
12858
  const HANDLE_W = 8;
12859
+ /** Label-pane width bounds (px) the splitter clamps to. */
12860
+ const MIN_LABEL_W = 140;
12861
+ const MAX_LABEL_W = 720;
12862
+ /** Keyboard resize steps (plain arrow / with Shift). */
12863
+ const LABEL_STEP = 16;
12864
+ const LABEL_STEP_COARSE = 64;
12865
+ /** Straight run (px) a dependency elbow travels out of a bar edge before it turns. */
12866
+ const LINK_STUB = 12;
12867
+ /** Clamp any candidate label-pane width into the splitter's range. */
12868
+ function clampLabelWidth(px) {
12869
+ return Math.max(MIN_LABEL_W, Math.min(MAX_LABEL_W, Math.round(px)));
12870
+ }
12871
+ /**
12872
+ * Relationship kind implied by the edge a gesture left and the edge it landed on. The name is
12873
+ * literally `<predecessor endpoint><successor endpoint>`, so this is a lookup, not a rule.
12874
+ */
12875
+ function linkTypeFor(from, to) {
12876
+ if (from === 'finish')
12877
+ return to === 'start' ? 'FS' : 'FF';
12878
+ return to === 'start' ? 'SS' : 'SF';
12879
+ }
12798
12880
  /**
12799
12881
  * **`fly-gantt`** — the design-system SVG Gantt chart.
12800
12882
  *
12801
12883
  * A reusable, **data-contract-driven** timeline: feed it {@link GanttRow}[] + optional
12802
12884
  * {@link GanttDependency}[] and it renders a two-pane chart — an indented, collapsible label
12803
12885
  * tree on the inline-start side and a scrolling time grid (header ticks, weekend shading, a
12804
- * today line, task bars with progress fill, milestone diamonds and finish-to-start dependency
12805
- * arrows) on the other. Pointer drag moves/resizes bars and draws new dependencies; the
12806
- * keyboard moves the selection and nudges dates. **It never persists** every mutation is an
12886
+ * today line, task bars with progress fill, milestone diamonds, optional per-row colour bands
12887
+ * and MS-Project-style dependency arrows) on the other. The pane divider is draggable, pointer
12888
+ * drag moves/resizes bars and draws new dependencies, and the keyboard moves the selection,
12889
+ * nudges dates and removes the selected link. **It never persists** — every mutation is an
12807
12890
  * output event the consumer acts on.
12808
12891
  *
12809
12892
  * Rendered on plain inline SVG (no third-party charting lib, no CDN asset). All geometry is
@@ -12812,8 +12895,10 @@ const HANDLE_W = 8;
12812
12895
  *
12813
12896
  * Self-sufficient i18n: every user-visible string comes from the `gantt.*` keys in
12814
12897
  * `DS_BASELINE_LOCALES` (en/ar/fr/ur), overridable by any consumer key of the same name.
12815
- * Styling uses DS theme tokens (`--surface-card`, `--surface-border`, `--primary-color`, …)
12816
- * with light-neutral fallbacks, mirroring the other DS controls.
12898
+ * Styling reads the **app-content** token family (`--bg-2`, `--bg-3`, `--line-3`, `--ink`,
12899
+ * `--accent`, …) that dresses `fly-data-table` and the rest of the app-surface kit, so a Gantt
12900
+ * placed among those controls matches them; the shell-chrome family (`--surface-card`, …) and
12901
+ * then a light-neutral literal are chained as fallbacks for consumers that map neither.
12817
12902
  *
12818
12903
  * **Scale limit:** renders up to `maxRows` (default 500) visible rows; beyond that the list is
12819
12904
  * capped and a footer notes the overflow. Heavy windowing/virtualization is intentionally
@@ -12830,8 +12915,14 @@ class FlyGanttComponent {
12830
12915
  showToday = input(true, ...(ngDevMode ? [{ debugName: "showToday" }] : /* istanbul ignore next */ []));
12831
12916
  /** Globally disable drag/resize/link (individual rows can also be `readonly`). */
12832
12917
  readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
12833
- /** Inline-start label-pane width in px. */
12918
+ /**
12919
+ * Inline-start label-pane width in px — the **initial** width. The user can drag the pane
12920
+ * divider from there; once they have, this input only re-seeds the pane if
12921
+ * {@link resetLabelWidth} is called (double-clicking the divider does exactly that).
12922
+ */
12834
12923
  labelWidth = input(240, ...(ngDevMode ? [{ debugName: "labelWidth" }] : /* istanbul ignore next */ []));
12924
+ /** Let the user drag the divider between the label pane and the time grid. */
12925
+ resizableLabels = input(true, ...(ngDevMode ? [{ debugName: "resizableLabels" }] : /* istanbul ignore next */ []));
12835
12926
  /** Row band height in px. */
12836
12927
  rowHeight = input(34, ...(ngDevMode ? [{ debugName: "rowHeight" }] : /* istanbul ignore next */ []));
12837
12928
  /** Hard cap on rendered rows; beyond it the list is truncated (see class doc). */
@@ -12839,10 +12930,17 @@ class FlyGanttComponent {
12839
12930
  // ── Outputs ────────────────────────────────────────────────────────────────
12840
12931
  /** Fires on a committed bar move / resize with the new ISO `start`/`end`. */
12841
12932
  rowDatesChange = output();
12842
- /** Fires when the user drags a link from one bar's finish connector onto another bar. */
12933
+ /**
12934
+ * Fires when the user drags a link between two rows. `type` is derived from the edge the drag
12935
+ * started on and the edge of the target row it was dropped nearest to.
12936
+ */
12843
12937
  dependencyCreate = output();
12938
+ /** Fires when the user removes the selected dependency arrow (badge click or Delete). */
12939
+ dependencyDelete = output();
12844
12940
  rowClick = output();
12845
12941
  rowDblClick = output();
12942
+ /** Fires once per committed divider drag / keyboard resize with the new pane width in px. */
12943
+ labelWidthChange = output();
12846
12944
  // ── Identity + geometry constants (template-visible) ─────────────────────────
12847
12945
  _uid = ++_flyGanttUid;
12848
12946
  markerId = `fly-gantt-${this._uid}-arrow`;
@@ -12850,20 +12948,30 @@ class FlyGanttComponent {
12850
12948
  HEADER_UPPER_H = HEADER_UPPER_H;
12851
12949
  HEADER_LOWER_H = HEADER_LOWER_H;
12852
12950
  HANDLE_W = HANDLE_W;
12951
+ MIN_LABEL_W = MIN_LABEL_W;
12952
+ MAX_LABEL_W = MAX_LABEL_W;
12853
12953
  // ── Interaction state ─────────────────────────────────────────────────────────
12854
12954
  selectedId = signal(null, ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
12955
+ /** Key (`from~to~type`) of the dependency arrow the user has selected, if any. */
12956
+ selectedLinkKey = signal(null, ...(ngDevMode ? [{ debugName: "selectedLinkKey" }] : /* istanbul ignore next */ []));
12855
12957
  _collapsed = signal(new Set(), ...(ngDevMode ? [{ debugName: "_collapsed" }] : /* istanbul ignore next */ []));
12856
12958
  /** Live drag preview `{id,start,end}` folded into geometry while a gesture runs. */
12857
12959
  _dragPreview = signal(null, ...(ngDevMode ? [{ debugName: "_dragPreview" }] : /* istanbul ignore next */ []));
12858
- /** Rubber-band link gesture render-space anchor + cursor, and its origin row. */
12960
+ /** Rubber-band link gesture render-space anchor + cursor, and its origin row + edge. */
12859
12961
  linkGesture = signal(null, ...(ngDevMode ? [{ debugName: "linkGesture" }] : /* istanbul ignore next */ []));
12860
12962
  dragTooltip = signal(null, ...(ngDevMode ? [{ debugName: "dragTooltip" }] : /* istanbul ignore next */ []));
12963
+ /** True while a divider drag is in flight (suppresses text selection host-wide). */
12964
+ resizingLabels = signal(false, ...(ngDevMode ? [{ debugName: "resizingLabels" }] : /* istanbul ignore next */ []));
12965
+ /** User-chosen pane width; `null` = still following the {@link labelWidth} input. */
12966
+ _labelWidthOverride = signal(null, ...(ngDevMode ? [{ debugName: "_labelWidthOverride" }] : /* istanbul ignore next */ []));
12861
12967
  bodyRef = viewChild('bodySvg', ...(ngDevMode ? [{ debugName: "bodyRef" }] : /* istanbul ignore next */ []));
12862
12968
  // ── Derived model ──────────────────────────────────────────────────────────────
12863
12969
  rtl = computed(() => this.i18n.isRtl(), ...(ngDevMode ? [{ debugName: "rtl" }] : /* istanbul ignore next */ []));
12864
12970
  pxPerDay = computed(() => ZOOM_PX_PER_DAY[this.zoom()], ...(ngDevMode ? [{ debugName: "pxPerDay" }] : /* istanbul ignore next */ []));
12865
12971
  domain = computed(() => computeDomain(this.rows(), this.zoom()), ...(ngDevMode ? [{ debugName: "domain" }] : /* istanbul ignore next */ []));
12866
12972
  innerWidth = computed(() => Math.max(1, dateToX(this.domain().end, this.domain().start, this.pxPerDay())), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
12973
+ /** Label-pane width actually rendered — the user's dragged width, else the input. */
12974
+ effectiveLabelWidth = computed(() => clampLabelWidth(this._labelWidthOverride() ?? this.labelWidth()), ...(ngDevMode ? [{ debugName: "effectiveLabelWidth" }] : /* istanbul ignore next */ []));
12867
12975
  /** Full visible list after tree flatten/collapse. */
12868
12976
  _flat = computed(() => flattenRows(this.rows(), this._collapsed()), ...(ngDevMode ? [{ debugName: "_flat" }] : /* istanbul ignore next */ []));
12869
12977
  /** Capped list actually rendered. */
@@ -12909,6 +13017,7 @@ class FlyGanttComponent {
12909
13017
  }
12910
13018
  const editable = !globalReadonly && !row.readonly && !isGroup;
12911
13019
  const color = row.color ?? null;
13020
+ const tint = this._tintFor(row);
12912
13021
  if (isMilestone) {
12913
13022
  const at = start ?? end;
12914
13023
  const logical = at ? dateToX(at, dom.start, ppd) : 0;
@@ -12916,7 +13025,7 @@ class FlyGanttComponent {
12916
13025
  return {
12917
13026
  flat, index, y, midY, shape: at ? 'milestone' : 'empty',
12918
13027
  x: pointX, width: 0, progressWidth: 0, pointX,
12919
- startX: pointX, endX: pointX, color,
13028
+ startX: pointX, endX: pointX, color, tint,
12920
13029
  ariaLabel: this._ariaFor(row, start, end, 'milestone'),
12921
13030
  editable: editable && !!at,
12922
13031
  };
@@ -12924,7 +13033,7 @@ class FlyGanttComponent {
12924
13033
  if (!start || !end) {
12925
13034
  return {
12926
13035
  flat, index, y, midY, shape: 'empty',
12927
- x: 0, width: 0, progressWidth: 0, pointX: 0, startX: 0, endX: 0, color,
13036
+ x: 0, width: 0, progressWidth: 0, pointX: 0, startX: 0, endX: 0, color, tint,
12928
13037
  ariaLabel: this._ariaFor(row, start, end, isGroup ? 'group' : 'bar'),
12929
13038
  editable: false,
12930
13039
  };
@@ -12942,12 +13051,14 @@ class FlyGanttComponent {
12942
13051
  pointX: 0,
12943
13052
  startX: mapX(xs, w, rtl),
12944
13053
  endX: mapX(xe, w, rtl),
12945
- color,
13054
+ color, tint,
12946
13055
  ariaLabel: this._ariaFor(row, start, end, isGroup ? 'group' : 'bar'),
12947
13056
  editable: isGroup ? false : editable,
12948
13057
  };
12949
13058
  });
12950
13059
  }, ...(ngDevMode ? [{ debugName: "rowVms" }] : /* istanbul ignore next */ []));
13060
+ /** Only the rows carrying a tint, so the band pass does not emit an empty rect per row. */
13061
+ tintedRows = computed(() => this.rowVms().filter((vm) => vm.tint !== null), ...(ngDevMode ? [{ debugName: "tintedRows" }] : /* istanbul ignore next */ []));
12951
13062
  /** Dependency arrows between currently-rendered rows (missing/collapsed endpoints skipped). */
12952
13063
  linkVms = computed(() => {
12953
13064
  const vmById = new Map();
@@ -12962,7 +13073,21 @@ class FlyGanttComponent {
12962
13073
  const to = vmById.get(link.toId);
12963
13074
  if (!from || !to)
12964
13075
  continue;
12965
- out.push({ key: `${link.fromId}~${link.toId}`, path: this._elbow(from, to, rtl) });
13076
+ const routed = this._route(from, to, link.type, rtl);
13077
+ out.push({
13078
+ key: `${link.fromId}~${link.toId}~${link.type}`,
13079
+ fromId: link.fromId,
13080
+ toId: link.toId,
13081
+ type: link.type,
13082
+ path: routed.path,
13083
+ badgeX: routed.badgeX,
13084
+ badgeY: routed.badgeY,
13085
+ ariaLabel: this.i18n.t('gantt.aria.link', {
13086
+ type: this.i18n.t(`gantt.link_type.${link.type.toLowerCase()}`),
13087
+ from: from.flat.row.label,
13088
+ to: to.flat.row.label,
13089
+ }),
13090
+ });
12966
13091
  }
12967
13092
  return out;
12968
13093
  }, ...(ngDevMode ? [{ debugName: "linkVms" }] : /* istanbul ignore next */ []));
@@ -12980,6 +13105,13 @@ class FlyGanttComponent {
12980
13105
  const { pointX: cx, midY: cy } = vm;
12981
13106
  return `${cx},${cy - r} ${cx + r},${cy} ${cx},${cy + r} ${cx - r},${cy}`;
12982
13107
  }
13108
+ /**
13109
+ * Link connector x for a milestone. A diamond has no width, so the connector is nudged clear
13110
+ * of the marker in the forward-in-time direction (which flips under RTL).
13111
+ */
13112
+ milestoneConnectorX(vm) {
13113
+ return vm.pointX + (this.milestoneR() + 5) * (this.rtl() ? -1 : 1);
13114
+ }
12983
13115
  /** Summary-bracket path (thin bar + down-turned end caps) for a group row. */
12984
13116
  groupPath(vm) {
12985
13117
  const top = this.barTop(vm);
@@ -12995,6 +13127,88 @@ class FlyGanttComponent {
12995
13127
  indentFor(depth) {
12996
13128
  return 8 + depth * 16;
12997
13129
  }
13130
+ // ── Label-pane divider ─────────────────────────────────────────────────────────
13131
+ /**
13132
+ * Pointer-drag the divider. Forward-in-time is `+x` LTR and `-x` RTL, and the label pane sits
13133
+ * on the inline-start side in both, so the same sign flip that mirrors the timeline also
13134
+ * mirrors "drag outward = wider".
13135
+ */
13136
+ onLabelResizePointerDown(ev) {
13137
+ if (!this.resizableLabels() || ev.button !== 0)
13138
+ return;
13139
+ ev.preventDefault();
13140
+ ev.stopPropagation();
13141
+ const startClientX = ev.clientX;
13142
+ const startWidth = this.effectiveLabelWidth();
13143
+ const dirSign = this.rtl() ? -1 : 1;
13144
+ this.resizingLabels.set(true);
13145
+ const onMove = (e) => {
13146
+ this._labelWidthOverride.set(clampLabelWidth(startWidth + (e.clientX - startClientX) * dirSign));
13147
+ };
13148
+ const onUp = () => {
13149
+ document.removeEventListener('pointermove', onMove);
13150
+ document.removeEventListener('pointerup', onUp);
13151
+ this.resizingLabels.set(false);
13152
+ const final = this.effectiveLabelWidth();
13153
+ if (final !== startWidth)
13154
+ this.labelWidthChange.emit(final);
13155
+ };
13156
+ document.addEventListener('pointermove', onMove);
13157
+ document.addEventListener('pointerup', onUp);
13158
+ this.destroyRef.onDestroy(() => {
13159
+ document.removeEventListener('pointermove', onMove);
13160
+ document.removeEventListener('pointerup', onUp);
13161
+ });
13162
+ }
13163
+ /**
13164
+ * Keyboard resize on the focused divider. ArrowRight/Left grow/shrink by the *inline* meaning
13165
+ * of the key, so under RTL the arrow that points away from the pane is still the one that
13166
+ * widens it. `Home`/`End` jump to the bounds, Enter/Escape restore the input width.
13167
+ *
13168
+ * Typed `Event` for the same reason as {@link onLabelRowKeydown}: Angular's strict template
13169
+ * checker types `(keydown)` `$event` as `Event`, so the cast is done here once.
13170
+ */
13171
+ onLabelResizeKeydown(ev) {
13172
+ if (!this.resizableLabels())
13173
+ return;
13174
+ const key = ev.key;
13175
+ const coarse = ev.shiftKey;
13176
+ const step = coarse ? LABEL_STEP_COARSE : LABEL_STEP;
13177
+ const grow = this.rtl() ? 'ArrowLeft' : 'ArrowRight';
13178
+ const shrink = this.rtl() ? 'ArrowRight' : 'ArrowLeft';
13179
+ let next;
13180
+ if (key === grow)
13181
+ next = this.effectiveLabelWidth() + step;
13182
+ else if (key === shrink)
13183
+ next = this.effectiveLabelWidth() - step;
13184
+ else if (key === 'Home')
13185
+ next = MIN_LABEL_W;
13186
+ else if (key === 'End')
13187
+ next = MAX_LABEL_W;
13188
+ else if (key === 'Enter' || key === 'Escape') {
13189
+ // Stop the grid-level Enter handler from also activating the selected row.
13190
+ ev.preventDefault();
13191
+ ev.stopPropagation();
13192
+ this.resetLabelWidth();
13193
+ return;
13194
+ }
13195
+ else
13196
+ return;
13197
+ ev.preventDefault();
13198
+ ev.stopPropagation();
13199
+ const clamped = clampLabelWidth(next);
13200
+ if (clamped === this.effectiveLabelWidth())
13201
+ return;
13202
+ this._labelWidthOverride.set(clamped);
13203
+ this.labelWidthChange.emit(clamped);
13204
+ }
13205
+ /** Drop the user's dragged width and fall back to the {@link labelWidth} input. */
13206
+ resetLabelWidth() {
13207
+ if (this._labelWidthOverride() === null)
13208
+ return;
13209
+ this._labelWidthOverride.set(null);
13210
+ this.labelWidthChange.emit(this.effectiveLabelWidth());
13211
+ }
12998
13212
  // ── Group collapse ─────────────────────────────────────────────────────────────
12999
13213
  isCollapsed(id) {
13000
13214
  return this._collapsed().has(id);
@@ -13022,13 +13236,39 @@ class FlyGanttComponent {
13022
13236
  // ── Row activation ─────────────────────────────────────────────────────────────
13023
13237
  onRowClick(id) {
13024
13238
  this.selectedId.set(id);
13239
+ // A row and a link are alternative selections — picking one drops the other, so Delete is
13240
+ // never ambiguous about which thing it would remove.
13241
+ this.selectedLinkKey.set(null);
13025
13242
  this.rowClick.emit(id);
13026
13243
  }
13027
13244
  onRowDblClick(id) {
13028
13245
  this.rowDblClick.emit(id);
13029
13246
  }
13247
+ // ── Dependency arrow selection + removal ───────────────────────────────────────
13248
+ onLinkClick(ev, vm) {
13249
+ ev.stopPropagation();
13250
+ this.selectedLinkKey.set(this.selectedLinkKey() === vm.key ? null : vm.key);
13251
+ }
13252
+ deleteLink(ev, vm) {
13253
+ ev.stopPropagation();
13254
+ ev.preventDefault();
13255
+ if (this.readonly())
13256
+ return;
13257
+ this.selectedLinkKey.set(null);
13258
+ this.dependencyDelete.emit({ fromId: vm.fromId, toId: vm.toId, type: vm.type });
13259
+ }
13030
13260
  // ── Keyboard ───────────────────────────────────────────────────────────────────
13031
13261
  onGridKeydown(ev) {
13262
+ // Link removal is checked before the row branches: while an arrow is selected the grid's
13263
+ // Delete belongs to it, and selecting an arrow already cleared the row selection.
13264
+ if (ev.key === 'Delete' || ev.key === 'Backspace') {
13265
+ const selected = this.linkVms().find((l) => l.key === this.selectedLinkKey());
13266
+ if (selected) {
13267
+ ev.preventDefault();
13268
+ this.deleteLink(ev, selected);
13269
+ }
13270
+ return;
13271
+ }
13032
13272
  const list = this.visibleRows();
13033
13273
  if (list.length === 0)
13034
13274
  return;
@@ -13095,6 +13335,7 @@ class FlyGanttComponent {
13095
13335
  ev.preventDefault();
13096
13336
  ev.stopPropagation();
13097
13337
  this.selectedId.set(vm.flat.row.id);
13338
+ this.selectedLinkKey.set(null);
13098
13339
  const dates = this._resolvedDates().get(vm.flat.row.id);
13099
13340
  if (!dates?.start || !dates?.end)
13100
13341
  return;
@@ -13149,13 +13390,19 @@ class FlyGanttComponent {
13149
13390
  });
13150
13391
  }
13151
13392
  // ── Pointer drag: dependency create ──────────────────────────────────────────────
13152
- onLinkPointerDown(ev, vm) {
13393
+ /**
13394
+ * Start a link gesture from one end of a bar. Which end it started on, plus which end of the
13395
+ * target row it is dropped nearest to, is what selects the {@link GanttDependencyType} — so all
13396
+ * four MS-Project relationships are reachable with the same single drag.
13397
+ */
13398
+ onLinkPointerDown(ev, vm, anchor) {
13153
13399
  if (this.readonly() || vm.flat.row.readonly || ev.button !== 0)
13154
13400
  return;
13155
13401
  ev.preventDefault();
13156
13402
  ev.stopPropagation();
13157
13403
  const local = this._toBodyPoint(ev);
13158
- this.linkGesture.set({ fromId: vm.flat.row.id, x0: vm.endX, y0: vm.midY, x1: local.x, y1: local.y });
13404
+ const x0 = vm.shape === 'milestone' ? vm.pointX : anchor === 'finish' ? vm.endX : vm.startX;
13405
+ this.linkGesture.set({ fromId: vm.flat.row.id, anchor, x0, y0: vm.midY, x1: local.x, y1: local.y });
13159
13406
  const onMove = (e) => {
13160
13407
  const p = this._toBodyPoint(e);
13161
13408
  const g = this.linkGesture();
@@ -13169,9 +13416,13 @@ class FlyGanttComponent {
13169
13416
  this.linkGesture.set(null);
13170
13417
  if (!g)
13171
13418
  return;
13172
- const targetId = this._rowIdAtPoint(e.clientX, e.clientY);
13173
- if (targetId && targetId !== g.fromId) {
13174
- this.dependencyCreate.emit({ fromId: g.fromId, toId: targetId });
13419
+ const target = this._dropTargetAt(e.clientX, e.clientY);
13420
+ if (target && target.id !== g.fromId) {
13421
+ this.dependencyCreate.emit({
13422
+ fromId: g.fromId,
13423
+ toId: target.id,
13424
+ type: linkTypeFor(g.anchor, target.anchor),
13425
+ });
13175
13426
  }
13176
13427
  };
13177
13428
  document.addEventListener('pointermove', onMove);
@@ -13189,6 +13440,17 @@ class FlyGanttComponent {
13189
13440
  return `M ${g.x0} ${g.y0} L ${g.x1} ${g.y1}`;
13190
13441
  }, ...(ngDevMode ? [{ debugName: "linkGesturePath" }] : /* istanbul ignore next */ []));
13191
13442
  // ── Internals ────────────────────────────────────────────────────────────────────
13443
+ /**
13444
+ * Resolve a row's `backgroundColor` into the low-alpha band colour actually painted.
13445
+ * `color-mix` does the fade in the consumer's own colour space, so a token, a hex, an
13446
+ * `rgb()` or an `oklch()` all work and none of them need pre-computing per theme.
13447
+ */
13448
+ _tintFor(row) {
13449
+ const raw = row.backgroundColor?.trim();
13450
+ if (!raw)
13451
+ return null;
13452
+ return `color-mix(in srgb, ${raw} ${Math.round(GANTT_ROW_TINT_ALPHA * 100)}%, transparent)`;
13453
+ }
13192
13454
  _mapTicks(ticks) {
13193
13455
  const w = this.innerWidth();
13194
13456
  const rtl = this.rtl();
@@ -13197,16 +13459,41 @@ class FlyGanttComponent {
13197
13459
  // Reflect each cell: its inline-start edge becomes innerWidth - (x + width).
13198
13460
  return ticks.map((t) => ({ ...t, x: w - t.x - t.width }));
13199
13461
  }
13200
- /** Elbow path from a predecessor's finish edge to a successor's start edge (render space). */
13201
- _elbow(from, to, rtl) {
13462
+ /**
13463
+ * Route a dependency elbow for any of the four relationship kinds (render space).
13464
+ *
13465
+ * The kind names the two endpoints, so it decides both the anchor x's and the direction the
13466
+ * arrow travels: leaving a *finish* edge moves forward in time, leaving a *start* edge moves
13467
+ * backward; entering a *start* edge approaches from behind, entering a *finish* edge from
13468
+ * ahead. `fwd` folds RTL in, so none of that branches on direction.
13469
+ *
13470
+ * One turn suffices when the entry stub lies ahead of the exit stub in the approach
13471
+ * direction. When it does not — overlapping or reversed bars, which SS/FF/SF hit constantly —
13472
+ * a straight drop would cut back through the bars, so the elbow detours via the gap between
13473
+ * the two rows instead.
13474
+ */
13475
+ _route(from, to, type, rtl) {
13202
13476
  const fwd = rtl ? -1 : 1;
13203
- const stub = 12 * fwd;
13204
- const x1 = from.shape === 'milestone' ? from.pointX : from.endX;
13477
+ const fromFinish = type === 'FS' || type === 'FF';
13478
+ const toStart = type === 'FS' || type === 'SS';
13479
+ const x1 = from.shape === 'milestone' ? from.pointX : fromFinish ? from.endX : from.startX;
13480
+ const x2 = to.shape === 'milestone' ? to.pointX : toStart ? to.startX : to.endX;
13205
13481
  const y1 = from.midY;
13206
- const x2 = to.shape === 'milestone' ? to.pointX : to.startX;
13207
13482
  const y2 = to.midY;
13208
- // finish stub out vertical to target row into the start edge (arrowhead via marker).
13209
- return `M ${x1} ${y1} H ${x1 + stub} V ${y2} H ${x2}`;
13483
+ const d1 = (fromFinish ? 1 : -1) * fwd; // exit direction
13484
+ const d2 = (toStart ? 1 : -1) * fwd; // approach direction
13485
+ const exit = x1 + LINK_STUB * d1;
13486
+ const entry = x2 - LINK_STUB * d2;
13487
+ if ((entry - exit) * d2 >= 0) {
13488
+ return { path: `M ${x1} ${y1} H ${exit} V ${y2} H ${x2}`, badgeX: exit, badgeY: (y1 + y2) / 2 };
13489
+ }
13490
+ // Distinct rows always differ in y; the guard only keeps a degenerate input off the bar.
13491
+ const yMid = y1 === y2 ? y1 - this.rowHeight() / 2 : (y1 + y2) / 2;
13492
+ return {
13493
+ path: `M ${x1} ${y1} H ${exit} V ${yMid} H ${entry} V ${y2} H ${x2}`,
13494
+ badgeX: entry,
13495
+ badgeY: yMid,
13496
+ };
13210
13497
  }
13211
13498
  /** Convert a pointer event to a coordinate inside the body SVG (render space). */
13212
13499
  _toBodyPoint(ev) {
@@ -13216,11 +13503,25 @@ class FlyGanttComponent {
13216
13503
  const rect = svg.getBoundingClientRect();
13217
13504
  return { x: ev.clientX - rect.left, y: ev.clientY - rect.top };
13218
13505
  }
13219
- /** The row id under a viewport point (for link drop-targeting), via the bar's `data-row-id`. */
13220
- _rowIdAtPoint(clientX, clientY) {
13506
+ /**
13507
+ * The row a link gesture was dropped on **and which of its ends** the drop landed nearest —
13508
+ * the second half of the type derivation. A milestone has no width, so both of its ends are
13509
+ * the same point and it is reported as `start`, giving the FS/SS pair anyone actually wants.
13510
+ */
13511
+ _dropTargetAt(clientX, clientY) {
13221
13512
  const el = document.elementFromPoint(clientX, clientY);
13222
13513
  const holder = el?.closest('[data-row-id]');
13223
- return holder?.getAttribute('data-row-id') ?? null;
13514
+ const id = holder?.getAttribute('data-row-id');
13515
+ if (!id)
13516
+ return null;
13517
+ const vm = this.rowVms().find((v) => v.flat.row.id === id);
13518
+ if (!vm || vm.shape === 'milestone' || vm.shape === 'empty')
13519
+ return { id, anchor: 'start' };
13520
+ const x = this._toBodyPoint({ clientX, clientY }).x;
13521
+ return {
13522
+ id,
13523
+ anchor: Math.abs(x - vm.endX) < Math.abs(x - vm.startX) ? 'finish' : 'start',
13524
+ };
13224
13525
  }
13225
13526
  _ariaFor(row, start, end, shape) {
13226
13527
  const t = (k, p) => this.i18n.t(k, p);
@@ -13241,7 +13542,7 @@ class FlyGanttComponent {
13241
13542
  : t('gantt.aria.bar', { label: row.label, start: toIsoDate(start), end: toIsoDate(end), progress });
13242
13543
  }
13243
13544
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyGanttComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13244
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyGanttComponent, isStandalone: true, selector: "fly-gantt", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, dependencies: { classPropertyName: "dependencies", publicName: "dependencies", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, showToday: { classPropertyName: "showToday", publicName: "showToday", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, labelWidth: { classPropertyName: "labelWidth", publicName: "labelWidth", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowDatesChange: "rowDatesChange", dependencyCreate: "dependencyCreate", rowClick: "rowClick", rowDblClick: "rowDblClick" }, host: { properties: { "class.fly-gantt--rtl": "rtl()", "class.fly-gantt--readonly": "readonly()", "attr.dir": "i18n.direction()" }, classAttribute: "fly-gantt" }, viewQueries: [{ propertyName: "bodyRef", first: true, predicate: ["bodySvg"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\r\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\r\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\r\n<div\r\n class=\"fly-gantt__scroll\"\r\n role=\"grid\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\r\n [attr.aria-rowcount]=\"visibleRows().length\"\r\n (keydown)=\"onGridKeydown($event)\"\r\n>\r\n <div\r\n class=\"fly-gantt__canvas\"\r\n [style.grid-template-columns]=\"labelWidth() + 'px ' + innerWidth() + 'px'\"\r\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\r\n >\r\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\"></div>\r\n\r\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n class=\"fly-gantt__time-header\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"HEADER_H\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\r\n }\r\n @for (t of upperTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\" />\r\n }\r\n </svg>\r\n\r\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__labels\" role=\"rowgroup\">\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <div\r\n class=\"fly-gantt__label-row\"\r\n role=\"row\"\r\n tabindex=\"-1\"\r\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\r\n [style.height.px]=\"rowHeight()\"\r\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.aria-label]=\"vm.ariaLabel\"\r\n (click)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\r\n >\r\n <span class=\"fly-gantt__label-inner\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\r\n @if (vm.flat.hasChildren) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-gantt__chevron\"\r\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\r\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\r\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\r\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\r\n >\u25B8</button>\r\n } @else {\r\n <span class=\"fly-gantt__chevron-spacer\"></span>\r\n }\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [title]=\"vm.flat.row.label\"\r\n >{{ vm.flat.row.label }}</span>\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time grid body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n #bodySvg\r\n class=\"fly-gantt__body\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"bodyHeight()\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + bodyHeight()\"\r\n >\r\n <defs>\r\n <marker\r\n [attr.id]=\"markerId\"\r\n markerWidth=\"8\"\r\n markerHeight=\"8\"\r\n refX=\"6\"\r\n refY=\"4\"\r\n orient=\"auto\"\r\n markerUnits=\"userSpaceOnUse\"\r\n >\r\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\r\n </marker>\r\n </defs>\r\n\r\n <!-- Weekend shading + vertical gridlines (behind everything). -->\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"0\" [attr.width]=\"b.width\" [attr.height]=\"bodyHeight()\" />\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n\r\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\r\n <rect\r\n class=\"fly-gantt__row-hit\"\r\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\r\n }\r\n @case ('milestone') {\r\n <polygon\r\n class=\"fly-gantt__milestone\"\r\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\r\n [attr.points]=\"diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n }\r\n @case ('bar') {\r\n <g class=\"fly-gantt__bar-group\">\r\n <rect\r\n class=\"fly-gantt__bar\"\r\n [class.fly-gantt__bar--editable]=\"vm.editable\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.width\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.progressWidth > 0) {\r\n <rect\r\n class=\"fly-gantt__progress\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.progressWidth\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n />\r\n }\r\n @if (vm.editable) {\r\n <!-- Resize handles at both edges. -->\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\r\n />\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\r\n />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- Finish connector: drag to draw a dependency to another row. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.endX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm)\"\r\n />\r\n }\r\n </g>\r\n }\r\n }\r\n </g>\r\n }\r\n\r\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). -->\r\n @for (link of linkVms(); track link.key) {\r\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- In-flight link rubber band. -->\r\n @if (linkGesturePath(); as gp) {\r\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- Today line (over the bars). -->\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"0\" [attr.x2]=\"todayX()\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n </svg>\r\n </div>\r\n\r\n @if (visibleRows().length === 0) {\r\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\r\n }\r\n @if (overflowCount() > 0) {\r\n <p class=\"fly-gantt__overflow\">\r\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\r\n </p>\r\n }\r\n</div>\r\n\r\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\r\n@if (dragTooltip(); as tip) {\r\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-alt: var(--glass-bg-elevated, #f8fafc);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--surface-border, #e2e8f0);--_grid-line: var(--surface-border, #eef2f7);--_text: var(--text-color, #0f172a);--_text-subtle: var(--text-color-secondary, #64748b);--_primary: var(--primary-color);--_primary-contrast: var(--primary-color-text, #fff);--_bar: var(--primary-color);--_bar-progress: var(--primary-600, #4f46e5);--_weekend: rgb(100 116 139 / 9%);--_today: var(--red-500, #ef4444);--_link: var(--text-color-secondary, #64748b);--_radius: 8px;display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid var(--_primary);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_primary)}.fly-gantt__label-inner{display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform .12s ease}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid var(--_primary);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__body{display:block;touch-action:pan-x pan-y}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_primary);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity .12s ease}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_primary);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-subtle);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13545
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyGanttComponent, isStandalone: true, selector: "fly-gantt", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, dependencies: { classPropertyName: "dependencies", publicName: "dependencies", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, showToday: { classPropertyName: "showToday", publicName: "showToday", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, labelWidth: { classPropertyName: "labelWidth", publicName: "labelWidth", isSignal: true, isRequired: false, transformFunction: null }, resizableLabels: { classPropertyName: "resizableLabels", publicName: "resizableLabels", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowDatesChange: "rowDatesChange", dependencyCreate: "dependencyCreate", dependencyDelete: "dependencyDelete", rowClick: "rowClick", rowDblClick: "rowDblClick", labelWidthChange: "labelWidthChange" }, host: { properties: { "class.fly-gantt--rtl": "rtl()", "class.fly-gantt--readonly": "readonly()", "class.fly-gantt--resizing": "resizingLabels()", "attr.dir": "i18n.direction()" }, classAttribute: "fly-gantt" }, viewQueries: [{ propertyName: "bodyRef", first: true, predicate: ["bodySvg"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\n<div\n class=\"fly-gantt__scroll\"\n role=\"grid\"\n tabindex=\"0\"\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\n [attr.aria-rowcount]=\"visibleRows().length\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div\n class=\"fly-gantt__canvas\"\n [style.grid-template-columns]=\"effectiveLabelWidth() + 'px ' + innerWidth() + 'px'\"\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\n >\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\">\n @if (resizableLabels()) {\n <!-- The divider lives in the corner because the corner is the one cell pinned on BOTH\n axes: the grip stays reachable however far the user has scrolled. The full-height\n strip down the label pane below is the same gesture, presentational only. -->\n <div\n class=\"fly-gantt__splitter\"\n role=\"separator\"\n tabindex=\"0\"\n aria-orientation=\"vertical\"\n [attr.aria-label]=\"'gantt.aria.label_resize' | translate\"\n [attr.aria-valuenow]=\"effectiveLabelWidth()\"\n [attr.aria-valuemin]=\"MIN_LABEL_W\"\n [attr.aria-valuemax]=\"MAX_LABEL_W\"\n (pointerdown)=\"onLabelResizePointerDown($event)\"\n (keydown)=\"onLabelResizeKeydown($event)\"\n (dblclick)=\"resetLabelWidth()\"\n ></div>\n }\n </div>\n\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <svg\n class=\"fly-gantt__time-header\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"HEADER_H\"\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\n aria-hidden=\"true\"\n >\n @for (b of weekendBands(); track b.key) {\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\n }\n @for (t of upperTicks(); track t.key) {\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\n }\n @for (t of lowerTicks(); track t.key) {\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\n }\n @if (todayX() !== null) {\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\" />\n }\n </svg>\n\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fly-gantt__labels\" role=\"rowgroup\">\n @for (vm of rowVms(); track vm.flat.row.id) {\n <div\n class=\"fly-gantt__label-row\"\n role=\"row\"\n tabindex=\"-1\"\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\n [class.fly-gantt__label-row--tinted]=\"vm.tint !== null\"\n [style.height.px]=\"rowHeight()\"\n [style.--fly-gantt-row-tint]=\"vm.tint\"\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\n [attr.aria-label]=\"vm.ariaLabel\"\n (click)=\"onRowClick(vm.flat.row.id)\"\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\n >\n <span class=\"fly-gantt__label-inner\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\n @if (vm.flat.hasChildren) {\n <button\n type=\"button\"\n class=\"fly-gantt__chevron\"\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\n >\u25B8</button>\n } @else {\n <span class=\"fly-gantt__chevron-spacer\"></span>\n }\n @if (vm.color) {\n <!-- Colour chip: the row's own colour at full strength, so a milestone stays\n identifiable in the label pane where the band tint is deliberately faint. -->\n <span class=\"fly-gantt__label-swatch\" [style.background]=\"vm.color\" aria-hidden=\"true\"></span>\n }\n <span\n class=\"fly-gantt__label-text\"\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\n [title]=\"vm.flat.row.label\"\n >{{ vm.flat.row.label }}</span>\n </span>\n </div>\n }\n @if (resizableLabels()) {\n <!-- aria-hidden so this presentational twin of the corner separator is not a second\n (and structurally invalid) child of the rowgroup in the accessibility tree. -->\n <div\n class=\"fly-gantt__splitter fly-gantt__splitter--rail\"\n aria-hidden=\"true\"\n (pointerdown)=\"onLabelResizePointerDown($event)\"\n (dblclick)=\"resetLabelWidth()\"\n ></div>\n }\n </div>\n\n <!-- \u2500\u2500 Time grid body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <svg\n #bodySvg\n class=\"fly-gantt__body\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"bodyHeight()\"\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + bodyHeight()\"\n >\n <defs>\n <marker\n [attr.id]=\"markerId\"\n markerWidth=\"8\"\n markerHeight=\"8\"\n refX=\"6\"\n refY=\"4\"\n orient=\"auto\"\n markerUnits=\"userSpaceOnUse\"\n >\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\n </marker>\n </defs>\n\n <!-- Row colour bands sit at the very bottom of the stack, so the weekend shading and the\n gridlines below still read through them. -->\n @for (vm of tintedRows(); track vm.flat.row.id) {\n <rect\n class=\"fly-gantt__row-band\"\n [attr.x]=\"0\"\n [attr.y]=\"vm.y\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"rowHeight()\"\n [style.fill]=\"vm.tint\"\n />\n }\n\n <!-- Weekend shading + vertical gridlines (behind the bars). -->\n @for (b of weekendBands(); track b.key) {\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"0\" [attr.width]=\"b.width\" [attr.height]=\"bodyHeight()\" />\n }\n @for (t of lowerTicks(); track t.key) {\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"bodyHeight()\" />\n }\n\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\n @for (vm of rowVms(); track vm.flat.row.id) {\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\n <rect\n class=\"fly-gantt__row-hit\"\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\n [attr.x]=\"0\"\n [attr.y]=\"vm.y\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"rowHeight()\"\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\n />\n\n @switch (vm.shape) {\n @case ('group') {\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\n }\n @case ('milestone') {\n <polygon\n class=\"fly-gantt__milestone\"\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\n [attr.points]=\"diamondPoints(vm)\"\n [style.fill]=\"vm.color\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\n />\n @if (!readonly() && !vm.flat.row.readonly) {\n <!-- A diamond has no width, so one connector serves it; it reads as the finish\n end, which is what makes a milestone\u2192task drag the FS everyone expects. -->\n <circle\n class=\"fly-gantt__connector\"\n [attr.cx]=\"milestoneConnectorX(vm)\"\n [attr.cy]=\"vm.midY\"\n r=\"4\"\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\n />\n }\n }\n @case ('bar') {\n <g class=\"fly-gantt__bar-group\">\n <rect\n class=\"fly-gantt__bar\"\n [class.fly-gantt__bar--editable]=\"vm.editable\"\n [attr.x]=\"vm.x\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"vm.width\"\n [attr.height]=\"barHeight()\"\n rx=\"4\"\n [style.fill]=\"vm.color\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\n />\n @if (vm.progressWidth > 0) {\n <rect\n class=\"fly-gantt__progress\"\n [attr.x]=\"vm.x\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"vm.progressWidth\"\n [attr.height]=\"barHeight()\"\n rx=\"4\"\n />\n }\n @if (vm.editable) {\n <!-- Resize handles at both edges. -->\n <rect\n class=\"fly-gantt__handle\"\n [attr.x]=\"vm.x\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"HANDLE_W\"\n [attr.height]=\"barHeight()\"\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\n />\n <rect\n class=\"fly-gantt__handle\"\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"HANDLE_W\"\n [attr.height]=\"barHeight()\"\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\n />\n }\n @if (!readonly() && !vm.flat.row.readonly) {\n <!-- One connector per END. Dragging from the start edge yields an SS/SF link\n and from the finish edge an FS/FF one \u2014 the drop edge picks which. -->\n <circle\n class=\"fly-gantt__connector\"\n [attr.cx]=\"vm.startX\"\n [attr.cy]=\"vm.midY\"\n r=\"4\"\n [attr.aria-label]=\"'gantt.aria.link_handle_start' | translate: { label: vm.flat.row.label }\"\n (pointerdown)=\"onLinkPointerDown($event, vm, 'start')\"\n />\n <circle\n class=\"fly-gantt__connector\"\n [attr.cx]=\"vm.endX\"\n [attr.cy]=\"vm.midY\"\n r=\"4\"\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\n />\n }\n </g>\n }\n }\n </g>\n }\n\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). Each carries a fat\n transparent twin so a 1.5px line is still clickable at pointer precision. -->\n @for (link of linkVms(); track link.key) {\n <g class=\"fly-gantt__link-group\" [class.fly-gantt__link-group--selected]=\"selectedLinkKey() === link.key\">\n <path\n class=\"fly-gantt__link-hit\"\n [attr.d]=\"link.path\"\n [attr.aria-label]=\"link.ariaLabel\"\n (click)=\"onLinkClick($event, link)\"\n />\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\n @if (selectedLinkKey() === link.key && !readonly()) {\n <g\n class=\"fly-gantt__link-delete\"\n [attr.transform]=\"'translate(' + link.badgeX + ',' + link.badgeY + ')'\"\n [attr.aria-label]=\"'gantt.aria.delete_link' | translate\"\n (click)=\"deleteLink($event, link)\"\n >\n <circle class=\"fly-gantt__link-delete-bg\" r=\"8\" />\n <path class=\"fly-gantt__link-delete-x\" d=\"M-3.5,-3.5 L3.5,3.5 M3.5,-3.5 L-3.5,3.5\" />\n </g>\n }\n </g>\n }\n\n <!-- In-flight link rubber band. -->\n @if (linkGesturePath(); as gp) {\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\n }\n\n <!-- Today line (over the bars). -->\n @if (todayX() !== null) {\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"0\" [attr.x2]=\"todayX()\" [attr.y2]=\"bodyHeight()\" />\n }\n </svg>\n </div>\n\n @if (visibleRows().length === 0) {\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\n }\n @if (overflowCount() > 0) {\n <p class=\"fly-gantt__overflow\">\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\n </p>\n }\n</div>\n\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\n@if (dragTooltip(); as tip) {\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\n}\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--bg-2, var(--surface-card, #fff));--_surface-alt: var(--bg-3, var(--glass-bg-elevated, #f8fafc));--_surface-hover: var(--bg-hover, var(--surface-hover, #f1f5f9));--_border: var(--w08, var(--surface-border, #e2e8f0));--_border-strong: var(--w1, var(--surface-border, #cbd5e1));--_grid-line: var(--line-3, var(--surface-border, #eef2f7));--_text: var(--ink, var(--text-color, #0f172a));--_text-subtle: var(--ink-3, var(--text-color-secondary, #64748b));--_text-faint: var(--ink-4, var(--text-color-secondary, #94a3b8));--_accent: var(--accent);--_bar: var(--_accent);--_bar-progress: color-mix(in srgb, var(--_accent) 72%, #000);--_weekend: color-mix(in srgb, var(--_text-subtle) 9%, transparent);--_today: var(--danger, #ef4444);--_link: var(--_text-subtle);--_radius: var(--r-lg, 8px);--_transition: var(--t-state, .12s ease);display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}:host(.fly-gantt--resizing){cursor:col-resize;-webkit-user-select:none;user-select:none}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);box-shadow:var(--shadow-card, none);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid var(--_accent);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border-strong)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border-strong)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__splitter{position:absolute;inset-block:0;inset-inline-end:-3px;inline-size:7px;z-index:5;cursor:col-resize;touch-action:none}.fly-gantt__splitter:after{content:\"\";position:absolute;inset-block:0;inset-inline-start:3px;inline-size:1px;background:transparent;transition:background var(--_transition)}.fly-gantt__splitter:hover:after,.fly-gantt__splitter:focus-visible:after{background:var(--_accent)}.fly-gantt__splitter:focus-visible{outline:2px solid var(--_accent);outline-offset:-1px}:host(.fly-gantt--resizing) .fly-gantt__splitter:after{background:var(--_accent)}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{position:relative;display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer;transition:background var(--_transition)}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_accent)}.fly-gantt__label-row--tinted:before{content:\"\";position:absolute;inset:0;background:var(--fly-gantt-row-tint);pointer-events:none}.fly-gantt__label-inner{position:relative;display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__label-swatch{flex:0 0 auto;inline-size:8px;block-size:8px;border-radius:2px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform var(--_transition)}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid var(--_accent);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__body{display:block;touch-action:pan-x pan-y}.fly-gantt__row-band{pointer-events:none}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_accent);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_accent);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link-hit{fill:none;stroke:transparent;stroke-width:11;pointer-events:stroke;cursor:pointer}.fly-gantt__link-group:hover .fly-gantt__link{stroke:var(--_text)}.fly-gantt__link-group--selected .fly-gantt__link{stroke:var(--_accent);stroke-width:2.5}.fly-gantt__link-delete-bg{fill:var(--_accent)}.fly-gantt__link-delete{cursor:pointer}.fly-gantt__link-delete-x{stroke:var(--_surface);stroke-width:1.75;stroke-linecap:round;fill:none;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-faint);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13245
13546
  }
13246
13547
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyGanttComponent, decorators: [{
13247
13548
  type: Component,
@@ -13249,20 +13550,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
13249
13550
  class: 'fly-gantt',
13250
13551
  '[class.fly-gantt--rtl]': 'rtl()',
13251
13552
  '[class.fly-gantt--readonly]': 'readonly()',
13553
+ '[class.fly-gantt--resizing]': 'resizingLabels()',
13252
13554
  '[attr.dir]': 'i18n.direction()',
13253
- }, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\r\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\r\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\r\n<div\r\n class=\"fly-gantt__scroll\"\r\n role=\"grid\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\r\n [attr.aria-rowcount]=\"visibleRows().length\"\r\n (keydown)=\"onGridKeydown($event)\"\r\n>\r\n <div\r\n class=\"fly-gantt__canvas\"\r\n [style.grid-template-columns]=\"labelWidth() + 'px ' + innerWidth() + 'px'\"\r\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\r\n >\r\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\"></div>\r\n\r\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n class=\"fly-gantt__time-header\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"HEADER_H\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\r\n }\r\n @for (t of upperTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\" />\r\n }\r\n </svg>\r\n\r\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__labels\" role=\"rowgroup\">\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <div\r\n class=\"fly-gantt__label-row\"\r\n role=\"row\"\r\n tabindex=\"-1\"\r\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\r\n [style.height.px]=\"rowHeight()\"\r\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.aria-label]=\"vm.ariaLabel\"\r\n (click)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\r\n >\r\n <span class=\"fly-gantt__label-inner\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\r\n @if (vm.flat.hasChildren) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-gantt__chevron\"\r\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\r\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\r\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\r\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\r\n >\u25B8</button>\r\n } @else {\r\n <span class=\"fly-gantt__chevron-spacer\"></span>\r\n }\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [title]=\"vm.flat.row.label\"\r\n >{{ vm.flat.row.label }}</span>\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time grid body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n #bodySvg\r\n class=\"fly-gantt__body\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"bodyHeight()\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + bodyHeight()\"\r\n >\r\n <defs>\r\n <marker\r\n [attr.id]=\"markerId\"\r\n markerWidth=\"8\"\r\n markerHeight=\"8\"\r\n refX=\"6\"\r\n refY=\"4\"\r\n orient=\"auto\"\r\n markerUnits=\"userSpaceOnUse\"\r\n >\r\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\r\n </marker>\r\n </defs>\r\n\r\n <!-- Weekend shading + vertical gridlines (behind everything). -->\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"0\" [attr.width]=\"b.width\" [attr.height]=\"bodyHeight()\" />\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n\r\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\r\n <rect\r\n class=\"fly-gantt__row-hit\"\r\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\r\n }\r\n @case ('milestone') {\r\n <polygon\r\n class=\"fly-gantt__milestone\"\r\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\r\n [attr.points]=\"diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n }\r\n @case ('bar') {\r\n <g class=\"fly-gantt__bar-group\">\r\n <rect\r\n class=\"fly-gantt__bar\"\r\n [class.fly-gantt__bar--editable]=\"vm.editable\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.width\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.progressWidth > 0) {\r\n <rect\r\n class=\"fly-gantt__progress\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.progressWidth\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n />\r\n }\r\n @if (vm.editable) {\r\n <!-- Resize handles at both edges. -->\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\r\n />\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\r\n />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- Finish connector: drag to draw a dependency to another row. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.endX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm)\"\r\n />\r\n }\r\n </g>\r\n }\r\n }\r\n </g>\r\n }\r\n\r\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). -->\r\n @for (link of linkVms(); track link.key) {\r\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- In-flight link rubber band. -->\r\n @if (linkGesturePath(); as gp) {\r\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- Today line (over the bars). -->\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"0\" [attr.x2]=\"todayX()\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n </svg>\r\n </div>\r\n\r\n @if (visibleRows().length === 0) {\r\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\r\n }\r\n @if (overflowCount() > 0) {\r\n <p class=\"fly-gantt__overflow\">\r\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\r\n </p>\r\n }\r\n</div>\r\n\r\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\r\n@if (dragTooltip(); as tip) {\r\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-alt: var(--glass-bg-elevated, #f8fafc);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--surface-border, #e2e8f0);--_grid-line: var(--surface-border, #eef2f7);--_text: var(--text-color, #0f172a);--_text-subtle: var(--text-color-secondary, #64748b);--_primary: var(--primary-color);--_primary-contrast: var(--primary-color-text, #fff);--_bar: var(--primary-color);--_bar-progress: var(--primary-600, #4f46e5);--_weekend: rgb(100 116 139 / 9%);--_today: var(--red-500, #ef4444);--_link: var(--text-color-secondary, #64748b);--_radius: 8px;display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid var(--_primary);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_primary)}.fly-gantt__label-inner{display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform .12s ease}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid var(--_primary);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__body{display:block;touch-action:pan-x pan-y}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_primary);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity .12s ease}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_primary);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-subtle);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"] }]
13254
- }], propDecorators: { rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], dependencies: [{ type: i0.Input, args: [{ isSignal: true, alias: "dependencies", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], showToday: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToday", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], labelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelWidth", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], rowDatesChange: [{ type: i0.Output, args: ["rowDatesChange"] }], dependencyCreate: [{ type: i0.Output, args: ["dependencyCreate"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDblClick: [{ type: i0.Output, args: ["rowDblClick"] }], bodyRef: [{ type: i0.ViewChild, args: ['bodySvg', { isSignal: true }] }] } });
13255
-
13256
- /**
13257
- * Public data contract for {@link FlyGanttComponent}.
13258
- *
13259
- * The types here are deliberately **app-agnostic** — no Projects/Tasks vocabulary leaks in.
13260
- * A consumer maps its own domain rows to {@link GanttRow}[] + {@link GanttDependency}[],
13261
- * feeds them in, and reacts to the emitted change events by persisting itself. The Gantt
13262
- * never calls a backend and never mutates its inputs.
13263
- */
13264
- /** Immutable list of the supported zoom levels, coarsest-last. */
13265
- const GANTT_ZOOMS = ['day', 'week', 'month', 'quarter'];
13555
+ }, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\n<div\n class=\"fly-gantt__scroll\"\n role=\"grid\"\n tabindex=\"0\"\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\n [attr.aria-rowcount]=\"visibleRows().length\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div\n class=\"fly-gantt__canvas\"\n [style.grid-template-columns]=\"effectiveLabelWidth() + 'px ' + innerWidth() + 'px'\"\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\n >\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\">\n @if (resizableLabels()) {\n <!-- The divider lives in the corner because the corner is the one cell pinned on BOTH\n axes: the grip stays reachable however far the user has scrolled. The full-height\n strip down the label pane below is the same gesture, presentational only. -->\n <div\n class=\"fly-gantt__splitter\"\n role=\"separator\"\n tabindex=\"0\"\n aria-orientation=\"vertical\"\n [attr.aria-label]=\"'gantt.aria.label_resize' | translate\"\n [attr.aria-valuenow]=\"effectiveLabelWidth()\"\n [attr.aria-valuemin]=\"MIN_LABEL_W\"\n [attr.aria-valuemax]=\"MAX_LABEL_W\"\n (pointerdown)=\"onLabelResizePointerDown($event)\"\n (keydown)=\"onLabelResizeKeydown($event)\"\n (dblclick)=\"resetLabelWidth()\"\n ></div>\n }\n </div>\n\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <svg\n class=\"fly-gantt__time-header\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"HEADER_H\"\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\n aria-hidden=\"true\"\n >\n @for (b of weekendBands(); track b.key) {\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\n }\n @for (t of upperTicks(); track t.key) {\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\n }\n @for (t of lowerTicks(); track t.key) {\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\n }\n @if (todayX() !== null) {\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\" />\n }\n </svg>\n\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fly-gantt__labels\" role=\"rowgroup\">\n @for (vm of rowVms(); track vm.flat.row.id) {\n <div\n class=\"fly-gantt__label-row\"\n role=\"row\"\n tabindex=\"-1\"\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\n [class.fly-gantt__label-row--tinted]=\"vm.tint !== null\"\n [style.height.px]=\"rowHeight()\"\n [style.--fly-gantt-row-tint]=\"vm.tint\"\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\n [attr.aria-label]=\"vm.ariaLabel\"\n (click)=\"onRowClick(vm.flat.row.id)\"\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\n >\n <span class=\"fly-gantt__label-inner\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\n @if (vm.flat.hasChildren) {\n <button\n type=\"button\"\n class=\"fly-gantt__chevron\"\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\n >\u25B8</button>\n } @else {\n <span class=\"fly-gantt__chevron-spacer\"></span>\n }\n @if (vm.color) {\n <!-- Colour chip: the row's own colour at full strength, so a milestone stays\n identifiable in the label pane where the band tint is deliberately faint. -->\n <span class=\"fly-gantt__label-swatch\" [style.background]=\"vm.color\" aria-hidden=\"true\"></span>\n }\n <span\n class=\"fly-gantt__label-text\"\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\n [title]=\"vm.flat.row.label\"\n >{{ vm.flat.row.label }}</span>\n </span>\n </div>\n }\n @if (resizableLabels()) {\n <!-- aria-hidden so this presentational twin of the corner separator is not a second\n (and structurally invalid) child of the rowgroup in the accessibility tree. -->\n <div\n class=\"fly-gantt__splitter fly-gantt__splitter--rail\"\n aria-hidden=\"true\"\n (pointerdown)=\"onLabelResizePointerDown($event)\"\n (dblclick)=\"resetLabelWidth()\"\n ></div>\n }\n </div>\n\n <!-- \u2500\u2500 Time grid body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <svg\n #bodySvg\n class=\"fly-gantt__body\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"bodyHeight()\"\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + bodyHeight()\"\n >\n <defs>\n <marker\n [attr.id]=\"markerId\"\n markerWidth=\"8\"\n markerHeight=\"8\"\n refX=\"6\"\n refY=\"4\"\n orient=\"auto\"\n markerUnits=\"userSpaceOnUse\"\n >\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\n </marker>\n </defs>\n\n <!-- Row colour bands sit at the very bottom of the stack, so the weekend shading and the\n gridlines below still read through them. -->\n @for (vm of tintedRows(); track vm.flat.row.id) {\n <rect\n class=\"fly-gantt__row-band\"\n [attr.x]=\"0\"\n [attr.y]=\"vm.y\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"rowHeight()\"\n [style.fill]=\"vm.tint\"\n />\n }\n\n <!-- Weekend shading + vertical gridlines (behind the bars). -->\n @for (b of weekendBands(); track b.key) {\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"0\" [attr.width]=\"b.width\" [attr.height]=\"bodyHeight()\" />\n }\n @for (t of lowerTicks(); track t.key) {\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"bodyHeight()\" />\n }\n\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\n @for (vm of rowVms(); track vm.flat.row.id) {\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\n <rect\n class=\"fly-gantt__row-hit\"\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\n [attr.x]=\"0\"\n [attr.y]=\"vm.y\"\n [attr.width]=\"innerWidth()\"\n [attr.height]=\"rowHeight()\"\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\n />\n\n @switch (vm.shape) {\n @case ('group') {\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\n }\n @case ('milestone') {\n <polygon\n class=\"fly-gantt__milestone\"\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\n [attr.points]=\"diamondPoints(vm)\"\n [style.fill]=\"vm.color\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\n />\n @if (!readonly() && !vm.flat.row.readonly) {\n <!-- A diamond has no width, so one connector serves it; it reads as the finish\n end, which is what makes a milestone\u2192task drag the FS everyone expects. -->\n <circle\n class=\"fly-gantt__connector\"\n [attr.cx]=\"milestoneConnectorX(vm)\"\n [attr.cy]=\"vm.midY\"\n r=\"4\"\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\n />\n }\n }\n @case ('bar') {\n <g class=\"fly-gantt__bar-group\">\n <rect\n class=\"fly-gantt__bar\"\n [class.fly-gantt__bar--editable]=\"vm.editable\"\n [attr.x]=\"vm.x\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"vm.width\"\n [attr.height]=\"barHeight()\"\n rx=\"4\"\n [style.fill]=\"vm.color\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\n />\n @if (vm.progressWidth > 0) {\n <rect\n class=\"fly-gantt__progress\"\n [attr.x]=\"vm.x\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"vm.progressWidth\"\n [attr.height]=\"barHeight()\"\n rx=\"4\"\n />\n }\n @if (vm.editable) {\n <!-- Resize handles at both edges. -->\n <rect\n class=\"fly-gantt__handle\"\n [attr.x]=\"vm.x\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"HANDLE_W\"\n [attr.height]=\"barHeight()\"\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\n />\n <rect\n class=\"fly-gantt__handle\"\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\n [attr.y]=\"barTop(vm)\"\n [attr.width]=\"HANDLE_W\"\n [attr.height]=\"barHeight()\"\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\n />\n }\n @if (!readonly() && !vm.flat.row.readonly) {\n <!-- One connector per END. Dragging from the start edge yields an SS/SF link\n and from the finish edge an FS/FF one \u2014 the drop edge picks which. -->\n <circle\n class=\"fly-gantt__connector\"\n [attr.cx]=\"vm.startX\"\n [attr.cy]=\"vm.midY\"\n r=\"4\"\n [attr.aria-label]=\"'gantt.aria.link_handle_start' | translate: { label: vm.flat.row.label }\"\n (pointerdown)=\"onLinkPointerDown($event, vm, 'start')\"\n />\n <circle\n class=\"fly-gantt__connector\"\n [attr.cx]=\"vm.endX\"\n [attr.cy]=\"vm.midY\"\n r=\"4\"\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\n />\n }\n </g>\n }\n }\n </g>\n }\n\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). Each carries a fat\n transparent twin so a 1.5px line is still clickable at pointer precision. -->\n @for (link of linkVms(); track link.key) {\n <g class=\"fly-gantt__link-group\" [class.fly-gantt__link-group--selected]=\"selectedLinkKey() === link.key\">\n <path\n class=\"fly-gantt__link-hit\"\n [attr.d]=\"link.path\"\n [attr.aria-label]=\"link.ariaLabel\"\n (click)=\"onLinkClick($event, link)\"\n />\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\n @if (selectedLinkKey() === link.key && !readonly()) {\n <g\n class=\"fly-gantt__link-delete\"\n [attr.transform]=\"'translate(' + link.badgeX + ',' + link.badgeY + ')'\"\n [attr.aria-label]=\"'gantt.aria.delete_link' | translate\"\n (click)=\"deleteLink($event, link)\"\n >\n <circle class=\"fly-gantt__link-delete-bg\" r=\"8\" />\n <path class=\"fly-gantt__link-delete-x\" d=\"M-3.5,-3.5 L3.5,3.5 M3.5,-3.5 L-3.5,3.5\" />\n </g>\n }\n </g>\n }\n\n <!-- In-flight link rubber band. -->\n @if (linkGesturePath(); as gp) {\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\n }\n\n <!-- Today line (over the bars). -->\n @if (todayX() !== null) {\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"0\" [attr.x2]=\"todayX()\" [attr.y2]=\"bodyHeight()\" />\n }\n </svg>\n </div>\n\n @if (visibleRows().length === 0) {\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\n }\n @if (overflowCount() > 0) {\n <p class=\"fly-gantt__overflow\">\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\n </p>\n }\n</div>\n\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\n@if (dragTooltip(); as tip) {\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\n}\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--bg-2, var(--surface-card, #fff));--_surface-alt: var(--bg-3, var(--glass-bg-elevated, #f8fafc));--_surface-hover: var(--bg-hover, var(--surface-hover, #f1f5f9));--_border: var(--w08, var(--surface-border, #e2e8f0));--_border-strong: var(--w1, var(--surface-border, #cbd5e1));--_grid-line: var(--line-3, var(--surface-border, #eef2f7));--_text: var(--ink, var(--text-color, #0f172a));--_text-subtle: var(--ink-3, var(--text-color-secondary, #64748b));--_text-faint: var(--ink-4, var(--text-color-secondary, #94a3b8));--_accent: var(--accent);--_bar: var(--_accent);--_bar-progress: color-mix(in srgb, var(--_accent) 72%, #000);--_weekend: color-mix(in srgb, var(--_text-subtle) 9%, transparent);--_today: var(--danger, #ef4444);--_link: var(--_text-subtle);--_radius: var(--r-lg, 8px);--_transition: var(--t-state, .12s ease);display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}:host(.fly-gantt--resizing){cursor:col-resize;-webkit-user-select:none;user-select:none}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);box-shadow:var(--shadow-card, none);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid var(--_accent);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border-strong)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border-strong)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__splitter{position:absolute;inset-block:0;inset-inline-end:-3px;inline-size:7px;z-index:5;cursor:col-resize;touch-action:none}.fly-gantt__splitter:after{content:\"\";position:absolute;inset-block:0;inset-inline-start:3px;inline-size:1px;background:transparent;transition:background var(--_transition)}.fly-gantt__splitter:hover:after,.fly-gantt__splitter:focus-visible:after{background:var(--_accent)}.fly-gantt__splitter:focus-visible{outline:2px solid var(--_accent);outline-offset:-1px}:host(.fly-gantt--resizing) .fly-gantt__splitter:after{background:var(--_accent)}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{position:relative;display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer;transition:background var(--_transition)}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_accent)}.fly-gantt__label-row--tinted:before{content:\"\";position:absolute;inset:0;background:var(--fly-gantt-row-tint);pointer-events:none}.fly-gantt__label-inner{position:relative;display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__label-swatch{flex:0 0 auto;inline-size:8px;block-size:8px;border-radius:2px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform var(--_transition)}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid var(--_accent);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__body{display:block;touch-action:pan-x pan-y}.fly-gantt__row-band{pointer-events:none}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_accent);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_accent);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link-hit{fill:none;stroke:transparent;stroke-width:11;pointer-events:stroke;cursor:pointer}.fly-gantt__link-group:hover .fly-gantt__link{stroke:var(--_text)}.fly-gantt__link-group--selected .fly-gantt__link{stroke:var(--_accent);stroke-width:2.5}.fly-gantt__link-delete-bg{fill:var(--_accent)}.fly-gantt__link-delete{cursor:pointer}.fly-gantt__link-delete-x{stroke:var(--_surface);stroke-width:1.75;stroke-linecap:round;fill:none;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-faint);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"] }]
13556
+ }], propDecorators: { rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], dependencies: [{ type: i0.Input, args: [{ isSignal: true, alias: "dependencies", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], showToday: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToday", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], labelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelWidth", required: false }] }], resizableLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableLabels", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], rowDatesChange: [{ type: i0.Output, args: ["rowDatesChange"] }], dependencyCreate: [{ type: i0.Output, args: ["dependencyCreate"] }], dependencyDelete: [{ type: i0.Output, args: ["dependencyDelete"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDblClick: [{ type: i0.Output, args: ["rowDblClick"] }], labelWidthChange: [{ type: i0.Output, args: ["labelWidthChange"] }], bodyRef: [{ type: i0.ViewChild, args: ['bodySvg', { isSignal: true }] }] } });
13266
13557
 
13267
13558
  // ── Bundled ISO 3166-1 country list ──────────────────────────────────────────
13268
13559
  // Ships with <fly-survey-form> so a `Country` question renders a searchable
@@ -17032,13 +17323,13 @@ class FlyMagicActionsComponent {
17032
17323
  this.isOverflowing.set(!!el && el.scrollWidth - el.clientWidth > 1);
17033
17324
  }
17034
17325
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyMagicActionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17035
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyMagicActionsComponent, isStandalone: true, selector: "fly-magic-actions", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: true, transformFunction: null }, primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.dir": "i18n.direction()" } }, viewQueries: [{ propertyName: "rowRef", first: true, predicate: ["row"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Icon-only buttons carry the accessible name via [attr.aria-label] \u2014 the SVG\n glyph itself is aria-hidden (decorative, skill \u00A7accessibility rule 5). `kind:\n 'text'` actions rely on their own visible text instead. Disabled rows stay\n `aria-disabled` (never native `disabled`) so they remain focusable/hoverable \u2014\n a disabled action's whole point is to explain ITSELF via the auto-suffixed\n tooltip, which native `disabled` would suppress.\n\n The glyph is composed from ALLOWLISTED nodes bound as real attributes, never\n from `[innerHTML]`: `iconPath` is publisher-supplied, and an `<svg>` fragment\n is not a closed world (`<foreignObject>` is an HTML integration point, `<a\n xlink:href=\"javascript:\u2026\">` is live under the very click this button invites).\n `magic-actions-icon.ts` carries the full rationale; the shape below is its\n consequence \u2014 an attribute binding cannot introduce an element or a handler,\n so there is nothing left to sanitize.\n\n That block is authored ONCE, as `#actionButton`, and stamped by every place a\n contributed action is painted (group items and `primary`). It used to be\n inline in the group loop, which was fine while this component rendered groups\n only; with `primary` here too, a second copy would mean the next edit to the\n SECURITY-CRITICAL binding shape has to be made twice and can be made once.\n-->\n<!--\n `data-action` carries the publisher's action `id` verbatim \u2014 the one stable, LOCALE-INDEPENDENT\n handle on a rendered action. Every other identifying attribute here is translated\n (`aria-label`, the tooltip), so an e2e suite that targeted them had to assert in one language,\n and one that targeted a bespoke button lost its hook entirely when the action moved into the\n bar. That is not hypothetical: PPM's project-closure suite could not adopt the contract for\n exactly this reason. It is an ATTRIBUTE, not a class, so it never participates in styling, and\n `id` is already unique across the whole contribution (the projection enforces it), so it is\n unique in the DOM too.\n-->\n<ng-template #actionButton let-projected>\n <!--\n `let-projected` is `any` \u2014 Angular infers nothing for an `ngTemplateOutlet`\n context, and there is no built-in way to declare one. `asAction` is an\n identity function whose only job is to give this block a TYPED local, so\n every read below (`.kind`, `.disabled`, `.menu`) is checked by the template\n compiler instead of silently resolving to `undefined` on a typo. Without it\n the one place a `MagicBarActionView` is actually painted would be the one\n place its shape is not verified.\n -->\n @let action = asAction(projected);\n @if (action.kind === 'text') {\n <button\n type=\"button\"\n class=\"fma-btn fma-btn--text\"\n [attr.data-action]=\"action.id\"\n [attr.data-tone]=\"toneAttr(action)\"\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\n [attr.aria-pressed]=\"pressedAttr(action)\"\n [flyTooltip]=\"tooltipFor(action)\"\n [flyTooltipDelay]=\"450\"\n (click)=\"activate(action, $event)\"\n >{{ resolvedLabel(action) }}</button>\n } @else {\n <button\n type=\"button\"\n class=\"fma-btn\"\n [class.fma-btn--badge]=\"hasBadge(action)\"\n [attr.data-action]=\"action.id\"\n [attr.data-tone]=\"toneAttr(action)\"\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\n [attr.aria-pressed]=\"pressedAttr(action)\"\n [attr.aria-haspopup]=\"action.menu ? 'menu' : null\"\n [attr.aria-expanded]=\"action.menu ? (openMenuActionId() === action.id ? 'true' : 'false') : null\"\n [attr.aria-label]=\"action.menu ? triggerAriaLabel(action) : resolvedLabel(action)\"\n [flyTooltip]=\"tooltipFor(action)\"\n [flyTooltipDelay]=\"450\"\n (click)=\"activate(action, $event)\"\n >\n <svg\n class=\"fma-btn__icon\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n @for (node of iconNodes(action); track $index) {\n @switch (node.tag) {\n @case ('path') {\n <svg:path\n [attr.d]=\"node.attrs['d']\"\n [attr.fill-rule]=\"node.attrs['fill-rule']\"\n [attr.clip-rule]=\"node.attrs['clip-rule']\"\n />\n }\n @case ('circle') {\n <svg:circle\n [attr.cx]=\"node.attrs['cx']\"\n [attr.cy]=\"node.attrs['cy']\"\n [attr.r]=\"node.attrs['r']\"\n />\n }\n @case ('rect') {\n <svg:rect\n [attr.x]=\"node.attrs['x']\"\n [attr.y]=\"node.attrs['y']\"\n [attr.width]=\"node.attrs['width']\"\n [attr.height]=\"node.attrs['height']\"\n [attr.rx]=\"node.attrs['rx']\"\n [attr.ry]=\"node.attrs['ry']\"\n />\n }\n @case ('ellipse') {\n <svg:ellipse\n [attr.cx]=\"node.attrs['cx']\"\n [attr.cy]=\"node.attrs['cy']\"\n [attr.rx]=\"node.attrs['rx']\"\n [attr.ry]=\"node.attrs['ry']\"\n />\n }\n @case ('line') {\n <svg:line\n [attr.x1]=\"node.attrs['x1']\"\n [attr.y1]=\"node.attrs['y1']\"\n [attr.x2]=\"node.attrs['x2']\"\n [attr.y2]=\"node.attrs['y2']\"\n />\n }\n @case ('polyline') {\n <svg:polyline [attr.points]=\"node.attrs['points']\" />\n }\n @case ('polygon') {\n <svg:polygon [attr.points]=\"node.attrs['points']\" />\n }\n }\n }\n </svg>\n @if (hasBadge(action)) {\n <span class=\"fma-btn__badge\" aria-hidden=\"true\"></span>\n }\n </button>\n }\n</ng-template>\n\n<!--\n `.fma` is the SCROLLING row and holds the contributed groups only. The accent\n CTA is a SIBLING of it, outside the fade mask and outside the scroll container,\n because it is the row's anchor \u2014 a call to action that dissolves into a\n gradient, or that you have to scroll sideways to reach, inverts the affordance.\n It is also what makes `isOverflowing` measurable at all: see the component.\n-->\n@if (groups().length) {\n <div #row class=\"fma\" [class.fma--overflow]=\"isOverflowing()\">\n @for (group of groups(); track group.id) {\n <div\n class=\"fma-group\"\n [class.fma-group--framed]=\"group.framed\"\n role=\"group\"\n [attr.aria-label]=\"i18n.t(group.labelKey)\"\n >\n @for (action of group.items; track action.id) {\n <ng-container\n [ngTemplateOutlet]=\"actionButton\"\n [ngTemplateOutletContext]=\"{ $implicit: action }\"\n />\n }\n </div>\n @if (group.framed) {\n <span class=\"fma-sep\" aria-hidden=\"true\"></span>\n }\n }\n </div>\n}\n\n<!--\n The accent CTA. The disc is a WRAPPER, not a modifier on the button, and that\n is load-bearing rather than cosmetic: `.fma-btn`'s hover/press feedback is an\n `animation` (`iconWiggle` / `iconBounce`), and an animation that touches\n `transform` beats a declared `transform` \u2014 so a hover lift authored on the\n button itself would simply never render. The wrapper owns the disc, the lift\n and the shadow; the button stays transparent and keeps the DS's own toolbar\n feedback. It re-points `--chrome-ink` rather than styling `.fma-btn` through a\n descendant selector so the glyph colour inherits the way custom properties do.\n-->\n@if (primary(); as p) {\n <span class=\"fma-primary\" [class.fma-primary--disabled]=\"p.disabled\">\n <ng-container\n [ngTemplateOutlet]=\"actionButton\"\n [ngTemplateOutletContext]=\"{ $implicit: p }\"\n />\n </span>\n}\n\n<!--\n ONE radio menu, hoisted out of the group loops (S1-review F9). Only one is open\n at a time (`openMenuActionId`), so a single instance is enough \u2014 and rendering\n it beside its button would insert a non-`.fma-btn` element into the group,\n silently shifting every later button's `:nth-child` index and with it the\n `toolbarPopIn` stagger delay the `.fma-btn` rule assigns. Keeping every\n staggered parent's children homogeneous is what makes that rule's comment true.\n It portals itself to <body>, so its position is unaffected by where it is\n declared.\n-->\n@if (openAction(); as open) {\n <fly-context-menu\n [anchor]=\"menuAnchorEl()\"\n [sections]=\"menuSections(open)\"\n (action)=\"onMenuAction(open, $event)\"\n (closed)=\"closeMenu()\"\n />\n}\n", styles: [":host{display:flex;align-items:center;gap:8px;min-inline-size:0}.fma{display:flex;align-items:center;gap:8px;flex:0 1 auto;min-inline-size:0;overflow:hidden;flex-wrap:nowrap}.fma--overflow{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%)}.fma--overflow::-webkit-scrollbar{display:none}:host([dir=rtl]) .fma--overflow{-webkit-mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%)}.fma-group{display:flex;align-items:center;gap:8px;flex:none}.fma-group--framed{gap:2px}.fma-sep{flex:0 0 auto;inline-size:1px;block-size:18px;background:var(--w14)}.fma-btn{position:relative;flex:0 0 auto;inline-size:30px;block-size:30px;display:grid;place-items:center;border:0;border-radius:50%;background:transparent;color:var(--chrome-ink);cursor:pointer;padding:0;animation:toolbarPopIn .3s var(--nova-ease-structural) both}.fma-btn:nth-child(1){animation-delay:0ms}.fma-btn:nth-child(2){animation-delay:40ms}.fma-btn:nth-child(3){animation-delay:80ms}.fma-btn:nth-child(4){animation-delay:.12s}.fma-btn:nth-child(5){animation-delay:.16s}.fma-btn:nth-child(6){animation-delay:.2s}.fma-btn:nth-child(7){animation-delay:.24s}.fma-btn:nth-child(8){animation-delay:.28s}.fma-btn:nth-child(9){animation-delay:.32s}.fma-btn:nth-child(10){animation-delay:.36s}.fma-btn:nth-child(11){animation-delay:.4s}.fma-btn:nth-child(12){animation-delay:.44s}.fma-btn:nth-child(n+13){animation-delay:.44s}.fma-btn:not([aria-disabled=true]):hover{background:var(--w1);color:var(--chrome-ink-hover);animation:iconWiggle .5s ease}.fma-btn:not([aria-disabled=true]):active{animation:iconBounce .38s var(--nova-ease-overshoot)}.fma-btn:focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:inherit}.fma-btn[data-tone=danger]{color:var(--sys-red)}.fma-btn[data-tone=danger]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-red) 18%,transparent);color:var(--sys-red)}.fma-btn[data-tone=success]{color:var(--sys-green)}.fma-btn[data-tone=success]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-green) 18%,transparent);color:var(--sys-green)}.fma-btn[aria-pressed=true]{background:var(--tint-sel);color:var(--chrome-ink-hover)}.fma-btn[aria-pressed=true][data-tone=success]{background:color-mix(in oklab,var(--sys-green) 22%,transparent);color:var(--sys-green)}.fma-btn[aria-disabled=true]{opacity:.38;filter:saturate(.6);animation:none;cursor:default}.fma-btn__icon{inline-size:16px;block-size:16px;pointer-events:none}.fma-btn__badge{position:absolute;inset-block-start:3px;inset-inline-end:3px;inline-size:7px;block-size:7px;border-radius:50%;background:var(--accent);pointer-events:none}.fma-primary{--chrome-ink: var(--on-accent);--chrome-ink-hover: var(--on-accent);flex:none;display:grid;place-items:center;inline-size:30px;block-size:30px;border-radius:50%;background:var(--accent);transition:box-shadow var(--nova-duration-hover) var(--nova-ease-micro),transform var(--nova-duration-hover) var(--nova-ease-micro)}.fma-primary:hover{box-shadow:0 8px 20px var(--tint-sel);transform:translateY(-1px)}.fma-primary--disabled{opacity:.38;filter:saturate(.6)}.fma-primary--disabled:hover{box-shadow:none;transform:none}.fma-btn--text{inline-size:auto;block-size:30px;border-radius:99px;padding-inline:12px;font-family:inherit;font-size:12.5px;font-weight:600;line-height:1;white-space:nowrap}@media(pointer:coarse){.fma-btn{inline-size:44px;block-size:44px}.fma-btn--text{inline-size:auto;min-inline-size:44px}.fma-primary{inline-size:44px;block-size:44px}}\n"], dependencies: [{ kind: "component", type: ContextMenuComponent, selector: "fly-context-menu", inputs: ["x", "y", "anchor", "align", "placement", "sections", "boundary"], outputs: ["action", "closed"] }, { kind: "directive", type: FlyTooltipDirective, selector: "[flyTooltip], [data-tooltip]", inputs: ["flyTooltip", "flyTooltipPlacement", "flyTooltipDisabled", "flyTooltipDelay"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
17326
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyMagicActionsComponent, isStandalone: true, selector: "fly-magic-actions", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: true, transformFunction: null }, primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.dir": "i18n.direction()" } }, viewQueries: [{ propertyName: "rowRef", first: true, predicate: ["row"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\r\n Icon-only buttons carry the accessible name via [attr.aria-label] \u2014 the SVG\r\n glyph itself is aria-hidden (decorative, skill \u00A7accessibility rule 5). `kind:\r\n 'text'` actions rely on their own visible text instead. Disabled rows stay\r\n `aria-disabled` (never native `disabled`) so they remain focusable/hoverable \u2014\r\n a disabled action's whole point is to explain ITSELF via the auto-suffixed\r\n tooltip, which native `disabled` would suppress.\r\n\r\n The glyph is composed from ALLOWLISTED nodes bound as real attributes, never\r\n from `[innerHTML]`: `iconPath` is publisher-supplied, and an `<svg>` fragment\r\n is not a closed world (`<foreignObject>` is an HTML integration point, `<a\r\n xlink:href=\"javascript:\u2026\">` is live under the very click this button invites).\r\n `magic-actions-icon.ts` carries the full rationale; the shape below is its\r\n consequence \u2014 an attribute binding cannot introduce an element or a handler,\r\n so there is nothing left to sanitize.\r\n\r\n That block is authored ONCE, as `#actionButton`, and stamped by every place a\r\n contributed action is painted (group items and `primary`). It used to be\r\n inline in the group loop, which was fine while this component rendered groups\r\n only; with `primary` here too, a second copy would mean the next edit to the\r\n SECURITY-CRITICAL binding shape has to be made twice and can be made once.\r\n-->\r\n<!--\r\n `data-action` carries the publisher's action `id` verbatim \u2014 the one stable, LOCALE-INDEPENDENT\r\n handle on a rendered action. Every other identifying attribute here is translated\r\n (`aria-label`, the tooltip), so an e2e suite that targeted them had to assert in one language,\r\n and one that targeted a bespoke button lost its hook entirely when the action moved into the\r\n bar. That is not hypothetical: PPM's project-closure suite could not adopt the contract for\r\n exactly this reason. It is an ATTRIBUTE, not a class, so it never participates in styling, and\r\n `id` is already unique across the whole contribution (the projection enforces it), so it is\r\n unique in the DOM too.\r\n-->\r\n<ng-template #actionButton let-projected>\r\n <!--\r\n `let-projected` is `any` \u2014 Angular infers nothing for an `ngTemplateOutlet`\r\n context, and there is no built-in way to declare one. `asAction` is an\r\n identity function whose only job is to give this block a TYPED local, so\r\n every read below (`.kind`, `.disabled`, `.menu`) is checked by the template\r\n compiler instead of silently resolving to `undefined` on a typo. Without it\r\n the one place a `MagicBarActionView` is actually painted would be the one\r\n place its shape is not verified.\r\n -->\r\n @let action = asAction(projected);\r\n @if (action.kind === 'text') {\r\n <button\r\n type=\"button\"\r\n class=\"fma-btn fma-btn--text\"\r\n [attr.data-action]=\"action.id\"\r\n [attr.data-tone]=\"toneAttr(action)\"\r\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\r\n [attr.aria-pressed]=\"pressedAttr(action)\"\r\n [flyTooltip]=\"tooltipFor(action)\"\r\n [flyTooltipDelay]=\"450\"\r\n (click)=\"activate(action, $event)\"\r\n >{{ resolvedLabel(action) }}</button>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"fma-btn\"\r\n [class.fma-btn--badge]=\"hasBadge(action)\"\r\n [attr.data-action]=\"action.id\"\r\n [attr.data-tone]=\"toneAttr(action)\"\r\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\r\n [attr.aria-pressed]=\"pressedAttr(action)\"\r\n [attr.aria-haspopup]=\"action.menu ? 'menu' : null\"\r\n [attr.aria-expanded]=\"action.menu ? (openMenuActionId() === action.id ? 'true' : 'false') : null\"\r\n [attr.aria-label]=\"action.menu ? triggerAriaLabel(action) : resolvedLabel(action)\"\r\n [flyTooltip]=\"tooltipFor(action)\"\r\n [flyTooltipDelay]=\"450\"\r\n (click)=\"activate(action, $event)\"\r\n >\r\n <svg\r\n class=\"fma-btn__icon\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"1.8\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (node of iconNodes(action); track $index) {\r\n @switch (node.tag) {\r\n @case ('path') {\r\n <svg:path\r\n [attr.d]=\"node.attrs['d']\"\r\n [attr.fill-rule]=\"node.attrs['fill-rule']\"\r\n [attr.clip-rule]=\"node.attrs['clip-rule']\"\r\n />\r\n }\r\n @case ('circle') {\r\n <svg:circle\r\n [attr.cx]=\"node.attrs['cx']\"\r\n [attr.cy]=\"node.attrs['cy']\"\r\n [attr.r]=\"node.attrs['r']\"\r\n />\r\n }\r\n @case ('rect') {\r\n <svg:rect\r\n [attr.x]=\"node.attrs['x']\"\r\n [attr.y]=\"node.attrs['y']\"\r\n [attr.width]=\"node.attrs['width']\"\r\n [attr.height]=\"node.attrs['height']\"\r\n [attr.rx]=\"node.attrs['rx']\"\r\n [attr.ry]=\"node.attrs['ry']\"\r\n />\r\n }\r\n @case ('ellipse') {\r\n <svg:ellipse\r\n [attr.cx]=\"node.attrs['cx']\"\r\n [attr.cy]=\"node.attrs['cy']\"\r\n [attr.rx]=\"node.attrs['rx']\"\r\n [attr.ry]=\"node.attrs['ry']\"\r\n />\r\n }\r\n @case ('line') {\r\n <svg:line\r\n [attr.x1]=\"node.attrs['x1']\"\r\n [attr.y1]=\"node.attrs['y1']\"\r\n [attr.x2]=\"node.attrs['x2']\"\r\n [attr.y2]=\"node.attrs['y2']\"\r\n />\r\n }\r\n @case ('polyline') {\r\n <svg:polyline [attr.points]=\"node.attrs['points']\" />\r\n }\r\n @case ('polygon') {\r\n <svg:polygon [attr.points]=\"node.attrs['points']\" />\r\n }\r\n }\r\n }\r\n </svg>\r\n @if (hasBadge(action)) {\r\n <span class=\"fma-btn__badge\" aria-hidden=\"true\"></span>\r\n }\r\n </button>\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n `.fma` is the SCROLLING row and holds the contributed groups only. The accent\r\n CTA is a SIBLING of it, outside the fade mask and outside the scroll container,\r\n because it is the row's anchor \u2014 a call to action that dissolves into a\r\n gradient, or that you have to scroll sideways to reach, inverts the affordance.\r\n It is also what makes `isOverflowing` measurable at all: see the component.\r\n-->\r\n@if (groups().length) {\r\n <div #row class=\"fma\" [class.fma--overflow]=\"isOverflowing()\">\r\n @for (group of groups(); track group.id) {\r\n <div\r\n class=\"fma-group\"\r\n [class.fma-group--framed]=\"group.framed\"\r\n role=\"group\"\r\n [attr.aria-label]=\"i18n.t(group.labelKey)\"\r\n >\r\n @for (action of group.items; track action.id) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionButton\"\r\n [ngTemplateOutletContext]=\"{ $implicit: action }\"\r\n />\r\n }\r\n </div>\r\n @if (group.framed) {\r\n <span class=\"fma-sep\" aria-hidden=\"true\"></span>\r\n }\r\n }\r\n </div>\r\n}\r\n\r\n<!--\r\n The accent CTA. The disc is a WRAPPER, not a modifier on the button, and that\r\n is load-bearing rather than cosmetic: `.fma-btn`'s hover/press feedback is an\r\n `animation` (`iconWiggle` / `iconBounce`), and an animation that touches\r\n `transform` beats a declared `transform` \u2014 so a hover lift authored on the\r\n button itself would simply never render. The wrapper owns the disc, the lift\r\n and the shadow; the button stays transparent and keeps the DS's own toolbar\r\n feedback. It re-points `--chrome-ink` rather than styling `.fma-btn` through a\r\n descendant selector so the glyph colour inherits the way custom properties do.\r\n-->\r\n@if (primary(); as p) {\r\n <span class=\"fma-primary\" [class.fma-primary--disabled]=\"p.disabled\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionButton\"\r\n [ngTemplateOutletContext]=\"{ $implicit: p }\"\r\n />\r\n </span>\r\n}\r\n\r\n<!--\r\n ONE radio menu, hoisted out of the group loops (S1-review F9). Only one is open\r\n at a time (`openMenuActionId`), so a single instance is enough \u2014 and rendering\r\n it beside its button would insert a non-`.fma-btn` element into the group,\r\n silently shifting every later button's `:nth-child` index and with it the\r\n `toolbarPopIn` stagger delay the `.fma-btn` rule assigns. Keeping every\r\n staggered parent's children homogeneous is what makes that rule's comment true.\r\n It portals itself to <body>, so its position is unaffected by where it is\r\n declared.\r\n-->\r\n@if (openAction(); as open) {\r\n <fly-context-menu\r\n [anchor]=\"menuAnchorEl()\"\r\n [sections]=\"menuSections(open)\"\r\n (action)=\"onMenuAction(open, $event)\"\r\n (closed)=\"closeMenu()\"\r\n />\r\n}\r\n", styles: [":host{display:flex;align-items:center;gap:8px;min-inline-size:0}.fma{display:flex;align-items:center;gap:8px;flex:0 1 auto;min-inline-size:0;overflow:hidden;flex-wrap:nowrap}.fma--overflow{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%)}.fma--overflow::-webkit-scrollbar{display:none}:host([dir=rtl]) .fma--overflow{-webkit-mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%)}.fma-group{display:flex;align-items:center;gap:8px;flex:none}.fma-group--framed{gap:2px}.fma-sep{flex:0 0 auto;inline-size:1px;block-size:18px;background:var(--w14)}.fma-btn{position:relative;flex:0 0 auto;inline-size:30px;block-size:30px;display:grid;place-items:center;border:0;border-radius:50%;background:transparent;color:var(--chrome-ink);cursor:pointer;padding:0;animation:toolbarPopIn .3s var(--nova-ease-structural) both}.fma-btn:nth-child(1){animation-delay:0ms}.fma-btn:nth-child(2){animation-delay:40ms}.fma-btn:nth-child(3){animation-delay:80ms}.fma-btn:nth-child(4){animation-delay:.12s}.fma-btn:nth-child(5){animation-delay:.16s}.fma-btn:nth-child(6){animation-delay:.2s}.fma-btn:nth-child(7){animation-delay:.24s}.fma-btn:nth-child(8){animation-delay:.28s}.fma-btn:nth-child(9){animation-delay:.32s}.fma-btn:nth-child(10){animation-delay:.36s}.fma-btn:nth-child(11){animation-delay:.4s}.fma-btn:nth-child(12){animation-delay:.44s}.fma-btn:nth-child(n+13){animation-delay:.44s}.fma-btn:not([aria-disabled=true]):hover{background:var(--w1);color:var(--chrome-ink-hover);animation:iconWiggle .5s ease}.fma-btn:not([aria-disabled=true]):active{animation:iconBounce .38s var(--nova-ease-overshoot)}.fma-btn:focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:inherit}.fma-btn[data-tone=danger]{color:var(--sys-red)}.fma-btn[data-tone=danger]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-red) 18%,transparent);color:var(--sys-red)}.fma-btn[data-tone=success]{color:var(--sys-green)}.fma-btn[data-tone=success]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-green) 18%,transparent);color:var(--sys-green)}.fma-btn[aria-pressed=true]{background:var(--tint-sel);color:var(--chrome-ink-hover)}.fma-btn[aria-pressed=true][data-tone=success]{background:color-mix(in oklab,var(--sys-green) 22%,transparent);color:var(--sys-green)}.fma-btn[aria-disabled=true]{opacity:.38;filter:saturate(.6);animation:none;cursor:default}.fma-btn__icon{inline-size:16px;block-size:16px;pointer-events:none}.fma-btn__badge{position:absolute;inset-block-start:3px;inset-inline-end:3px;inline-size:7px;block-size:7px;border-radius:50%;background:var(--accent);pointer-events:none}.fma-primary{--chrome-ink: var(--on-accent);--chrome-ink-hover: var(--on-accent);flex:none;display:grid;place-items:center;inline-size:30px;block-size:30px;border-radius:50%;background:var(--accent);transition:box-shadow var(--nova-duration-hover) var(--nova-ease-micro),transform var(--nova-duration-hover) var(--nova-ease-micro)}.fma-primary:hover{box-shadow:0 8px 20px var(--tint-sel);transform:translateY(-1px)}.fma-primary--disabled{opacity:.38;filter:saturate(.6)}.fma-primary--disabled:hover{box-shadow:none;transform:none}.fma-btn--text{inline-size:auto;block-size:30px;border-radius:99px;padding-inline:12px;font-family:inherit;font-size:12.5px;font-weight:600;line-height:1;white-space:nowrap}@media(pointer:coarse){.fma-btn{inline-size:44px;block-size:44px}.fma-btn--text{inline-size:auto;min-inline-size:44px}.fma-primary{inline-size:44px;block-size:44px}}\n"], dependencies: [{ kind: "component", type: ContextMenuComponent, selector: "fly-context-menu", inputs: ["x", "y", "anchor", "align", "placement", "sections", "boundary"], outputs: ["action", "closed"] }, { kind: "directive", type: FlyTooltipDirective, selector: "[flyTooltip], [data-tooltip]", inputs: ["flyTooltip", "flyTooltipPlacement", "flyTooltipDisabled", "flyTooltipDelay"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
17036
17327
  }
17037
17328
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyMagicActionsComponent, decorators: [{
17038
17329
  type: Component,
17039
17330
  args: [{ selector: 'fly-magic-actions', standalone: true, imports: [ContextMenuComponent, FlyTooltipDirective, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
17040
17331
  '[attr.dir]': 'i18n.direction()',
17041
- }, template: "<!--\n Icon-only buttons carry the accessible name via [attr.aria-label] \u2014 the SVG\n glyph itself is aria-hidden (decorative, skill \u00A7accessibility rule 5). `kind:\n 'text'` actions rely on their own visible text instead. Disabled rows stay\n `aria-disabled` (never native `disabled`) so they remain focusable/hoverable \u2014\n a disabled action's whole point is to explain ITSELF via the auto-suffixed\n tooltip, which native `disabled` would suppress.\n\n The glyph is composed from ALLOWLISTED nodes bound as real attributes, never\n from `[innerHTML]`: `iconPath` is publisher-supplied, and an `<svg>` fragment\n is not a closed world (`<foreignObject>` is an HTML integration point, `<a\n xlink:href=\"javascript:\u2026\">` is live under the very click this button invites).\n `magic-actions-icon.ts` carries the full rationale; the shape below is its\n consequence \u2014 an attribute binding cannot introduce an element or a handler,\n so there is nothing left to sanitize.\n\n That block is authored ONCE, as `#actionButton`, and stamped by every place a\n contributed action is painted (group items and `primary`). It used to be\n inline in the group loop, which was fine while this component rendered groups\n only; with `primary` here too, a second copy would mean the next edit to the\n SECURITY-CRITICAL binding shape has to be made twice and can be made once.\n-->\n<!--\n `data-action` carries the publisher's action `id` verbatim \u2014 the one stable, LOCALE-INDEPENDENT\n handle on a rendered action. Every other identifying attribute here is translated\n (`aria-label`, the tooltip), so an e2e suite that targeted them had to assert in one language,\n and one that targeted a bespoke button lost its hook entirely when the action moved into the\n bar. That is not hypothetical: PPM's project-closure suite could not adopt the contract for\n exactly this reason. It is an ATTRIBUTE, not a class, so it never participates in styling, and\n `id` is already unique across the whole contribution (the projection enforces it), so it is\n unique in the DOM too.\n-->\n<ng-template #actionButton let-projected>\n <!--\n `let-projected` is `any` \u2014 Angular infers nothing for an `ngTemplateOutlet`\n context, and there is no built-in way to declare one. `asAction` is an\n identity function whose only job is to give this block a TYPED local, so\n every read below (`.kind`, `.disabled`, `.menu`) is checked by the template\n compiler instead of silently resolving to `undefined` on a typo. Without it\n the one place a `MagicBarActionView` is actually painted would be the one\n place its shape is not verified.\n -->\n @let action = asAction(projected);\n @if (action.kind === 'text') {\n <button\n type=\"button\"\n class=\"fma-btn fma-btn--text\"\n [attr.data-action]=\"action.id\"\n [attr.data-tone]=\"toneAttr(action)\"\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\n [attr.aria-pressed]=\"pressedAttr(action)\"\n [flyTooltip]=\"tooltipFor(action)\"\n [flyTooltipDelay]=\"450\"\n (click)=\"activate(action, $event)\"\n >{{ resolvedLabel(action) }}</button>\n } @else {\n <button\n type=\"button\"\n class=\"fma-btn\"\n [class.fma-btn--badge]=\"hasBadge(action)\"\n [attr.data-action]=\"action.id\"\n [attr.data-tone]=\"toneAttr(action)\"\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\n [attr.aria-pressed]=\"pressedAttr(action)\"\n [attr.aria-haspopup]=\"action.menu ? 'menu' : null\"\n [attr.aria-expanded]=\"action.menu ? (openMenuActionId() === action.id ? 'true' : 'false') : null\"\n [attr.aria-label]=\"action.menu ? triggerAriaLabel(action) : resolvedLabel(action)\"\n [flyTooltip]=\"tooltipFor(action)\"\n [flyTooltipDelay]=\"450\"\n (click)=\"activate(action, $event)\"\n >\n <svg\n class=\"fma-btn__icon\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n @for (node of iconNodes(action); track $index) {\n @switch (node.tag) {\n @case ('path') {\n <svg:path\n [attr.d]=\"node.attrs['d']\"\n [attr.fill-rule]=\"node.attrs['fill-rule']\"\n [attr.clip-rule]=\"node.attrs['clip-rule']\"\n />\n }\n @case ('circle') {\n <svg:circle\n [attr.cx]=\"node.attrs['cx']\"\n [attr.cy]=\"node.attrs['cy']\"\n [attr.r]=\"node.attrs['r']\"\n />\n }\n @case ('rect') {\n <svg:rect\n [attr.x]=\"node.attrs['x']\"\n [attr.y]=\"node.attrs['y']\"\n [attr.width]=\"node.attrs['width']\"\n [attr.height]=\"node.attrs['height']\"\n [attr.rx]=\"node.attrs['rx']\"\n [attr.ry]=\"node.attrs['ry']\"\n />\n }\n @case ('ellipse') {\n <svg:ellipse\n [attr.cx]=\"node.attrs['cx']\"\n [attr.cy]=\"node.attrs['cy']\"\n [attr.rx]=\"node.attrs['rx']\"\n [attr.ry]=\"node.attrs['ry']\"\n />\n }\n @case ('line') {\n <svg:line\n [attr.x1]=\"node.attrs['x1']\"\n [attr.y1]=\"node.attrs['y1']\"\n [attr.x2]=\"node.attrs['x2']\"\n [attr.y2]=\"node.attrs['y2']\"\n />\n }\n @case ('polyline') {\n <svg:polyline [attr.points]=\"node.attrs['points']\" />\n }\n @case ('polygon') {\n <svg:polygon [attr.points]=\"node.attrs['points']\" />\n }\n }\n }\n </svg>\n @if (hasBadge(action)) {\n <span class=\"fma-btn__badge\" aria-hidden=\"true\"></span>\n }\n </button>\n }\n</ng-template>\n\n<!--\n `.fma` is the SCROLLING row and holds the contributed groups only. The accent\n CTA is a SIBLING of it, outside the fade mask and outside the scroll container,\n because it is the row's anchor \u2014 a call to action that dissolves into a\n gradient, or that you have to scroll sideways to reach, inverts the affordance.\n It is also what makes `isOverflowing` measurable at all: see the component.\n-->\n@if (groups().length) {\n <div #row class=\"fma\" [class.fma--overflow]=\"isOverflowing()\">\n @for (group of groups(); track group.id) {\n <div\n class=\"fma-group\"\n [class.fma-group--framed]=\"group.framed\"\n role=\"group\"\n [attr.aria-label]=\"i18n.t(group.labelKey)\"\n >\n @for (action of group.items; track action.id) {\n <ng-container\n [ngTemplateOutlet]=\"actionButton\"\n [ngTemplateOutletContext]=\"{ $implicit: action }\"\n />\n }\n </div>\n @if (group.framed) {\n <span class=\"fma-sep\" aria-hidden=\"true\"></span>\n }\n }\n </div>\n}\n\n<!--\n The accent CTA. The disc is a WRAPPER, not a modifier on the button, and that\n is load-bearing rather than cosmetic: `.fma-btn`'s hover/press feedback is an\n `animation` (`iconWiggle` / `iconBounce`), and an animation that touches\n `transform` beats a declared `transform` \u2014 so a hover lift authored on the\n button itself would simply never render. The wrapper owns the disc, the lift\n and the shadow; the button stays transparent and keeps the DS's own toolbar\n feedback. It re-points `--chrome-ink` rather than styling `.fma-btn` through a\n descendant selector so the glyph colour inherits the way custom properties do.\n-->\n@if (primary(); as p) {\n <span class=\"fma-primary\" [class.fma-primary--disabled]=\"p.disabled\">\n <ng-container\n [ngTemplateOutlet]=\"actionButton\"\n [ngTemplateOutletContext]=\"{ $implicit: p }\"\n />\n </span>\n}\n\n<!--\n ONE radio menu, hoisted out of the group loops (S1-review F9). Only one is open\n at a time (`openMenuActionId`), so a single instance is enough \u2014 and rendering\n it beside its button would insert a non-`.fma-btn` element into the group,\n silently shifting every later button's `:nth-child` index and with it the\n `toolbarPopIn` stagger delay the `.fma-btn` rule assigns. Keeping every\n staggered parent's children homogeneous is what makes that rule's comment true.\n It portals itself to <body>, so its position is unaffected by where it is\n declared.\n-->\n@if (openAction(); as open) {\n <fly-context-menu\n [anchor]=\"menuAnchorEl()\"\n [sections]=\"menuSections(open)\"\n (action)=\"onMenuAction(open, $event)\"\n (closed)=\"closeMenu()\"\n />\n}\n", styles: [":host{display:flex;align-items:center;gap:8px;min-inline-size:0}.fma{display:flex;align-items:center;gap:8px;flex:0 1 auto;min-inline-size:0;overflow:hidden;flex-wrap:nowrap}.fma--overflow{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%)}.fma--overflow::-webkit-scrollbar{display:none}:host([dir=rtl]) .fma--overflow{-webkit-mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%)}.fma-group{display:flex;align-items:center;gap:8px;flex:none}.fma-group--framed{gap:2px}.fma-sep{flex:0 0 auto;inline-size:1px;block-size:18px;background:var(--w14)}.fma-btn{position:relative;flex:0 0 auto;inline-size:30px;block-size:30px;display:grid;place-items:center;border:0;border-radius:50%;background:transparent;color:var(--chrome-ink);cursor:pointer;padding:0;animation:toolbarPopIn .3s var(--nova-ease-structural) both}.fma-btn:nth-child(1){animation-delay:0ms}.fma-btn:nth-child(2){animation-delay:40ms}.fma-btn:nth-child(3){animation-delay:80ms}.fma-btn:nth-child(4){animation-delay:.12s}.fma-btn:nth-child(5){animation-delay:.16s}.fma-btn:nth-child(6){animation-delay:.2s}.fma-btn:nth-child(7){animation-delay:.24s}.fma-btn:nth-child(8){animation-delay:.28s}.fma-btn:nth-child(9){animation-delay:.32s}.fma-btn:nth-child(10){animation-delay:.36s}.fma-btn:nth-child(11){animation-delay:.4s}.fma-btn:nth-child(12){animation-delay:.44s}.fma-btn:nth-child(n+13){animation-delay:.44s}.fma-btn:not([aria-disabled=true]):hover{background:var(--w1);color:var(--chrome-ink-hover);animation:iconWiggle .5s ease}.fma-btn:not([aria-disabled=true]):active{animation:iconBounce .38s var(--nova-ease-overshoot)}.fma-btn:focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:inherit}.fma-btn[data-tone=danger]{color:var(--sys-red)}.fma-btn[data-tone=danger]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-red) 18%,transparent);color:var(--sys-red)}.fma-btn[data-tone=success]{color:var(--sys-green)}.fma-btn[data-tone=success]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-green) 18%,transparent);color:var(--sys-green)}.fma-btn[aria-pressed=true]{background:var(--tint-sel);color:var(--chrome-ink-hover)}.fma-btn[aria-pressed=true][data-tone=success]{background:color-mix(in oklab,var(--sys-green) 22%,transparent);color:var(--sys-green)}.fma-btn[aria-disabled=true]{opacity:.38;filter:saturate(.6);animation:none;cursor:default}.fma-btn__icon{inline-size:16px;block-size:16px;pointer-events:none}.fma-btn__badge{position:absolute;inset-block-start:3px;inset-inline-end:3px;inline-size:7px;block-size:7px;border-radius:50%;background:var(--accent);pointer-events:none}.fma-primary{--chrome-ink: var(--on-accent);--chrome-ink-hover: var(--on-accent);flex:none;display:grid;place-items:center;inline-size:30px;block-size:30px;border-radius:50%;background:var(--accent);transition:box-shadow var(--nova-duration-hover) var(--nova-ease-micro),transform var(--nova-duration-hover) var(--nova-ease-micro)}.fma-primary:hover{box-shadow:0 8px 20px var(--tint-sel);transform:translateY(-1px)}.fma-primary--disabled{opacity:.38;filter:saturate(.6)}.fma-primary--disabled:hover{box-shadow:none;transform:none}.fma-btn--text{inline-size:auto;block-size:30px;border-radius:99px;padding-inline:12px;font-family:inherit;font-size:12.5px;font-weight:600;line-height:1;white-space:nowrap}@media(pointer:coarse){.fma-btn{inline-size:44px;block-size:44px}.fma-btn--text{inline-size:auto;min-inline-size:44px}.fma-primary{inline-size:44px;block-size:44px}}\n"] }]
17332
+ }, template: "<!--\r\n Icon-only buttons carry the accessible name via [attr.aria-label] \u2014 the SVG\r\n glyph itself is aria-hidden (decorative, skill \u00A7accessibility rule 5). `kind:\r\n 'text'` actions rely on their own visible text instead. Disabled rows stay\r\n `aria-disabled` (never native `disabled`) so they remain focusable/hoverable \u2014\r\n a disabled action's whole point is to explain ITSELF via the auto-suffixed\r\n tooltip, which native `disabled` would suppress.\r\n\r\n The glyph is composed from ALLOWLISTED nodes bound as real attributes, never\r\n from `[innerHTML]`: `iconPath` is publisher-supplied, and an `<svg>` fragment\r\n is not a closed world (`<foreignObject>` is an HTML integration point, `<a\r\n xlink:href=\"javascript:\u2026\">` is live under the very click this button invites).\r\n `magic-actions-icon.ts` carries the full rationale; the shape below is its\r\n consequence \u2014 an attribute binding cannot introduce an element or a handler,\r\n so there is nothing left to sanitize.\r\n\r\n That block is authored ONCE, as `#actionButton`, and stamped by every place a\r\n contributed action is painted (group items and `primary`). It used to be\r\n inline in the group loop, which was fine while this component rendered groups\r\n only; with `primary` here too, a second copy would mean the next edit to the\r\n SECURITY-CRITICAL binding shape has to be made twice and can be made once.\r\n-->\r\n<!--\r\n `data-action` carries the publisher's action `id` verbatim \u2014 the one stable, LOCALE-INDEPENDENT\r\n handle on a rendered action. Every other identifying attribute here is translated\r\n (`aria-label`, the tooltip), so an e2e suite that targeted them had to assert in one language,\r\n and one that targeted a bespoke button lost its hook entirely when the action moved into the\r\n bar. That is not hypothetical: PPM's project-closure suite could not adopt the contract for\r\n exactly this reason. It is an ATTRIBUTE, not a class, so it never participates in styling, and\r\n `id` is already unique across the whole contribution (the projection enforces it), so it is\r\n unique in the DOM too.\r\n-->\r\n<ng-template #actionButton let-projected>\r\n <!--\r\n `let-projected` is `any` \u2014 Angular infers nothing for an `ngTemplateOutlet`\r\n context, and there is no built-in way to declare one. `asAction` is an\r\n identity function whose only job is to give this block a TYPED local, so\r\n every read below (`.kind`, `.disabled`, `.menu`) is checked by the template\r\n compiler instead of silently resolving to `undefined` on a typo. Without it\r\n the one place a `MagicBarActionView` is actually painted would be the one\r\n place its shape is not verified.\r\n -->\r\n @let action = asAction(projected);\r\n @if (action.kind === 'text') {\r\n <button\r\n type=\"button\"\r\n class=\"fma-btn fma-btn--text\"\r\n [attr.data-action]=\"action.id\"\r\n [attr.data-tone]=\"toneAttr(action)\"\r\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\r\n [attr.aria-pressed]=\"pressedAttr(action)\"\r\n [flyTooltip]=\"tooltipFor(action)\"\r\n [flyTooltipDelay]=\"450\"\r\n (click)=\"activate(action, $event)\"\r\n >{{ resolvedLabel(action) }}</button>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"fma-btn\"\r\n [class.fma-btn--badge]=\"hasBadge(action)\"\r\n [attr.data-action]=\"action.id\"\r\n [attr.data-tone]=\"toneAttr(action)\"\r\n [attr.aria-disabled]=\"action.disabled ? 'true' : null\"\r\n [attr.aria-pressed]=\"pressedAttr(action)\"\r\n [attr.aria-haspopup]=\"action.menu ? 'menu' : null\"\r\n [attr.aria-expanded]=\"action.menu ? (openMenuActionId() === action.id ? 'true' : 'false') : null\"\r\n [attr.aria-label]=\"action.menu ? triggerAriaLabel(action) : resolvedLabel(action)\"\r\n [flyTooltip]=\"tooltipFor(action)\"\r\n [flyTooltipDelay]=\"450\"\r\n (click)=\"activate(action, $event)\"\r\n >\r\n <svg\r\n class=\"fma-btn__icon\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"1.8\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (node of iconNodes(action); track $index) {\r\n @switch (node.tag) {\r\n @case ('path') {\r\n <svg:path\r\n [attr.d]=\"node.attrs['d']\"\r\n [attr.fill-rule]=\"node.attrs['fill-rule']\"\r\n [attr.clip-rule]=\"node.attrs['clip-rule']\"\r\n />\r\n }\r\n @case ('circle') {\r\n <svg:circle\r\n [attr.cx]=\"node.attrs['cx']\"\r\n [attr.cy]=\"node.attrs['cy']\"\r\n [attr.r]=\"node.attrs['r']\"\r\n />\r\n }\r\n @case ('rect') {\r\n <svg:rect\r\n [attr.x]=\"node.attrs['x']\"\r\n [attr.y]=\"node.attrs['y']\"\r\n [attr.width]=\"node.attrs['width']\"\r\n [attr.height]=\"node.attrs['height']\"\r\n [attr.rx]=\"node.attrs['rx']\"\r\n [attr.ry]=\"node.attrs['ry']\"\r\n />\r\n }\r\n @case ('ellipse') {\r\n <svg:ellipse\r\n [attr.cx]=\"node.attrs['cx']\"\r\n [attr.cy]=\"node.attrs['cy']\"\r\n [attr.rx]=\"node.attrs['rx']\"\r\n [attr.ry]=\"node.attrs['ry']\"\r\n />\r\n }\r\n @case ('line') {\r\n <svg:line\r\n [attr.x1]=\"node.attrs['x1']\"\r\n [attr.y1]=\"node.attrs['y1']\"\r\n [attr.x2]=\"node.attrs['x2']\"\r\n [attr.y2]=\"node.attrs['y2']\"\r\n />\r\n }\r\n @case ('polyline') {\r\n <svg:polyline [attr.points]=\"node.attrs['points']\" />\r\n }\r\n @case ('polygon') {\r\n <svg:polygon [attr.points]=\"node.attrs['points']\" />\r\n }\r\n }\r\n }\r\n </svg>\r\n @if (hasBadge(action)) {\r\n <span class=\"fma-btn__badge\" aria-hidden=\"true\"></span>\r\n }\r\n </button>\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n `.fma` is the SCROLLING row and holds the contributed groups only. The accent\r\n CTA is a SIBLING of it, outside the fade mask and outside the scroll container,\r\n because it is the row's anchor \u2014 a call to action that dissolves into a\r\n gradient, or that you have to scroll sideways to reach, inverts the affordance.\r\n It is also what makes `isOverflowing` measurable at all: see the component.\r\n-->\r\n@if (groups().length) {\r\n <div #row class=\"fma\" [class.fma--overflow]=\"isOverflowing()\">\r\n @for (group of groups(); track group.id) {\r\n <div\r\n class=\"fma-group\"\r\n [class.fma-group--framed]=\"group.framed\"\r\n role=\"group\"\r\n [attr.aria-label]=\"i18n.t(group.labelKey)\"\r\n >\r\n @for (action of group.items; track action.id) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionButton\"\r\n [ngTemplateOutletContext]=\"{ $implicit: action }\"\r\n />\r\n }\r\n </div>\r\n @if (group.framed) {\r\n <span class=\"fma-sep\" aria-hidden=\"true\"></span>\r\n }\r\n }\r\n </div>\r\n}\r\n\r\n<!--\r\n The accent CTA. The disc is a WRAPPER, not a modifier on the button, and that\r\n is load-bearing rather than cosmetic: `.fma-btn`'s hover/press feedback is an\r\n `animation` (`iconWiggle` / `iconBounce`), and an animation that touches\r\n `transform` beats a declared `transform` \u2014 so a hover lift authored on the\r\n button itself would simply never render. The wrapper owns the disc, the lift\r\n and the shadow; the button stays transparent and keeps the DS's own toolbar\r\n feedback. It re-points `--chrome-ink` rather than styling `.fma-btn` through a\r\n descendant selector so the glyph colour inherits the way custom properties do.\r\n-->\r\n@if (primary(); as p) {\r\n <span class=\"fma-primary\" [class.fma-primary--disabled]=\"p.disabled\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionButton\"\r\n [ngTemplateOutletContext]=\"{ $implicit: p }\"\r\n />\r\n </span>\r\n}\r\n\r\n<!--\r\n ONE radio menu, hoisted out of the group loops (S1-review F9). Only one is open\r\n at a time (`openMenuActionId`), so a single instance is enough \u2014 and rendering\r\n it beside its button would insert a non-`.fma-btn` element into the group,\r\n silently shifting every later button's `:nth-child` index and with it the\r\n `toolbarPopIn` stagger delay the `.fma-btn` rule assigns. Keeping every\r\n staggered parent's children homogeneous is what makes that rule's comment true.\r\n It portals itself to <body>, so its position is unaffected by where it is\r\n declared.\r\n-->\r\n@if (openAction(); as open) {\r\n <fly-context-menu\r\n [anchor]=\"menuAnchorEl()\"\r\n [sections]=\"menuSections(open)\"\r\n (action)=\"onMenuAction(open, $event)\"\r\n (closed)=\"closeMenu()\"\r\n />\r\n}\r\n", styles: [":host{display:flex;align-items:center;gap:8px;min-inline-size:0}.fma{display:flex;align-items:center;gap:8px;flex:0 1 auto;min-inline-size:0;overflow:hidden;flex-wrap:nowrap}.fma--overflow{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%)}.fma--overflow::-webkit-scrollbar{display:none}:host([dir=rtl]) .fma--overflow{-webkit-mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to left,#000 calc(100% - 28px),transparent 100%)}.fma-group{display:flex;align-items:center;gap:8px;flex:none}.fma-group--framed{gap:2px}.fma-sep{flex:0 0 auto;inline-size:1px;block-size:18px;background:var(--w14)}.fma-btn{position:relative;flex:0 0 auto;inline-size:30px;block-size:30px;display:grid;place-items:center;border:0;border-radius:50%;background:transparent;color:var(--chrome-ink);cursor:pointer;padding:0;animation:toolbarPopIn .3s var(--nova-ease-structural) both}.fma-btn:nth-child(1){animation-delay:0ms}.fma-btn:nth-child(2){animation-delay:40ms}.fma-btn:nth-child(3){animation-delay:80ms}.fma-btn:nth-child(4){animation-delay:.12s}.fma-btn:nth-child(5){animation-delay:.16s}.fma-btn:nth-child(6){animation-delay:.2s}.fma-btn:nth-child(7){animation-delay:.24s}.fma-btn:nth-child(8){animation-delay:.28s}.fma-btn:nth-child(9){animation-delay:.32s}.fma-btn:nth-child(10){animation-delay:.36s}.fma-btn:nth-child(11){animation-delay:.4s}.fma-btn:nth-child(12){animation-delay:.44s}.fma-btn:nth-child(n+13){animation-delay:.44s}.fma-btn:not([aria-disabled=true]):hover{background:var(--w1);color:var(--chrome-ink-hover);animation:iconWiggle .5s ease}.fma-btn:not([aria-disabled=true]):active{animation:iconBounce .38s var(--nova-ease-overshoot)}.fma-btn:focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:inherit}.fma-btn[data-tone=danger]{color:var(--sys-red)}.fma-btn[data-tone=danger]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-red) 18%,transparent);color:var(--sys-red)}.fma-btn[data-tone=success]{color:var(--sys-green)}.fma-btn[data-tone=success]:not([aria-disabled=true]):hover{background:color-mix(in oklab,var(--sys-green) 18%,transparent);color:var(--sys-green)}.fma-btn[aria-pressed=true]{background:var(--tint-sel);color:var(--chrome-ink-hover)}.fma-btn[aria-pressed=true][data-tone=success]{background:color-mix(in oklab,var(--sys-green) 22%,transparent);color:var(--sys-green)}.fma-btn[aria-disabled=true]{opacity:.38;filter:saturate(.6);animation:none;cursor:default}.fma-btn__icon{inline-size:16px;block-size:16px;pointer-events:none}.fma-btn__badge{position:absolute;inset-block-start:3px;inset-inline-end:3px;inline-size:7px;block-size:7px;border-radius:50%;background:var(--accent);pointer-events:none}.fma-primary{--chrome-ink: var(--on-accent);--chrome-ink-hover: var(--on-accent);flex:none;display:grid;place-items:center;inline-size:30px;block-size:30px;border-radius:50%;background:var(--accent);transition:box-shadow var(--nova-duration-hover) var(--nova-ease-micro),transform var(--nova-duration-hover) var(--nova-ease-micro)}.fma-primary:hover{box-shadow:0 8px 20px var(--tint-sel);transform:translateY(-1px)}.fma-primary--disabled{opacity:.38;filter:saturate(.6)}.fma-primary--disabled:hover{box-shadow:none;transform:none}.fma-btn--text{inline-size:auto;block-size:30px;border-radius:99px;padding-inline:12px;font-family:inherit;font-size:12.5px;font-weight:600;line-height:1;white-space:nowrap}@media(pointer:coarse){.fma-btn{inline-size:44px;block-size:44px}.fma-btn--text{inline-size:auto;min-inline-size:44px}.fma-primary{inline-size:44px;block-size:44px}}\n"] }]
17042
17333
  }], ctorParameters: () => [], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: true }] }], primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], rowRef: [{ type: i0.ViewChild, args: ['row', { isSignal: true }] }] } });
17043
17334
 
17044
17335
  /**
@@ -25502,5 +25793,5 @@ const AUDIENCE_ERROR_CODES = {
25502
25793
  * Generated bundle index. Do not edit.
25503
25794
  */
25504
25795
 
25505
- export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, BilingualFieldComponent, CRON_MODES, CRON_WEEKDAYS, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DashboardKpiComponent, DialogResult, ENTITY_LINK_LAUNCHER, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLYOS_LAUNCH_REQUEST_EVENT, FLYOS_REMOTE_ROUTE_EVENT, FLY_ACCENT_PROPERTY, FLY_ADMIN_ROLES, FLY_CHUNK_RELOAD_FLAG, FLY_COMPACT_THRESHOLD, FLY_COUNTRIES, FLY_CURRENCIES_BRIEF_ENDPOINT, FLY_EMOJI_BY_ID, FLY_EMOJI_CATEGORIES, FLY_EMOJI_CATEGORY_BY_ID, FLY_EMOJI_DEFAULT_CATEGORIES, FLY_EMOJI_PACK, FLY_EMOJI_PACK_MISSING_MESSAGE, FLY_EMOJI_PREVIEW_COUNT, FLY_EMPTY_VALUE, FLY_LOCALE_CATALOG, FLY_MAGIC_BAR_EVENT, FLY_MAGIC_BAR_ICONS, FLY_MAGIC_BAR_STORE_KEY, FLY_NF_CACHE_HEAL_FLAG, FLY_RELOAD_DEBOUNCE_MS, FLY_RELOAD_REARM_MS, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_ROUTES, FLY_SCAN_PENDING_DEFAULT_DELAY_MS, FLY_SCAN_PENDING_MAX_RETRIES, FLY_SCAN_PENDING_STATUS, FLY_SEARCH_DEBOUNCE_MS, FLY_SKIN_TONES, FLY_STANDALONE_AUTH_CONFIG, FLY_THEME_MODE_IDS, FLY_VIEWPORT_IS_MOBILE, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, FlyAppUnavailableComponent, FlyAuthLiveRefresh, FlyBadgeWallComponent, FlyBlockUiComponent, FlyBreadcrumbComponent, FlyButtonComponent, FlyBytesPipe, FlyCaptchaComponent, FlyCardActionsComponent, FlyCardBodyComponent, FlyCardComponent, FlyCardFooterComponent, FlyCardGridComponent, FlyCardMediaComponent, FlyCardMetaComponent, FlyCardStatusComponent, FlyCardTitleComponent, FlyCellDirective, FlyCheckboxComponent, FlyChipComponent, FlyChunkReloadErrorHandler, FlyClickOutsideDirective, FlyCollapsibleComponent, FlyCommentThreadComponent, FlyCompactNumberPipe, FlyConfirmDialogComponent, FlyControlDirective, FlyCronBuilderComponent, FlyCurrencyCatalogService, FlyCurrencySelectorComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDeepLinkPrefetchService, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileDownloadService, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLeaderboardComponent, FlyMagicActionsComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyMoneyPipe, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStandaloneMagicActionsComponent, FlyStateMessageComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyUserDirectoryService, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, GANTT_ZOOMS, I18nService, LAUNCH_CONTEXT, MAGIC_BAR_SEARCH_WIDTH_DEFAULT, MAGIC_BAR_SEARCH_WIDTH_MAX, MAGIC_BAR_SEARCH_WIDTH_MIN, MagicBarRegistry, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, NOVA_PRIORITY_TONE, NOVA_STATUS_TONE, NOVA_TONE_FALLBACK, OverlayStack, PRESENCE_COLORS, PageHeaderComponent, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, STATE_DEFAULT_ICONS, STATE_DEFAULT_MESSAGE_KEYS, STEP_UP_CODE, STEP_UP_REAUTH_HANDLER, STEP_UP_RETURN_KEY, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, StatusBadgeComponent, StepUpService, ToastHostComponent, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext, canGoPrev, captureFocus, clampMagicBarSearchWidth, clampPage, clampSliderValue, connectRemoteLaunch, enterActivatesNatively, filterGroup, filterSuggestions, findLocaleByDialect, findLocaleByPrefix, firstEnabledIndex, firstModuleIndex, flattenModules, flyAdminGuard, flyApiErrorMessage, flyCaptchaBase64Utf8, flyCaptchaBuildToken, flyCaptchaSha256Hex, flyCompactNumber, flyDebounced, flyDecimalNumber, flyDownloadBlob, flyDuration, flyEmojiFor, flyEmojiIsTonable, flyEmojiPackUrl, flyExportFileName, flyFirstEnabledIndex, flyFormatBytes, flyFormatDate, flyFormatDateTime, flyFormatMoney, flyFormatTime, flyFullNumber, flyNextEnabledIndex, flyNextSegmentIndex, flyRelativeTime, flyResolveActiveTab, flyRoleGuard, flyScanPendingDelayMs, flyScanRetry, flySignedCompact, flySkinToneLabelKey, flyStandaloneAuthGuard, flyStandaloneAuthInterceptor, flyToDateOnly, flyToPage, flyUnwrap, flyUnwrapCurrencies, flyUnwrapLenient, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, loadRemoteStyles, magicBarOwnerKey, magicBarSearchText, matchFlyRoutePattern, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeRoles, novaPriorityToneVar, novaStatusToneVar, overlayStack, parseCron, parseStepUpChallenge, parseTags, prefetchRemoteStyles, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
25796
+ export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, BilingualFieldComponent, CRON_MODES, CRON_WEEKDAYS, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DashboardKpiComponent, DialogResult, ENTITY_LINK_LAUNCHER, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLYOS_LAUNCH_REQUEST_EVENT, FLYOS_REMOTE_ROUTE_EVENT, FLY_ACCENT_PROPERTY, FLY_ADMIN_ROLES, FLY_CHUNK_RELOAD_FLAG, FLY_COMPACT_THRESHOLD, FLY_COUNTRIES, FLY_CURRENCIES_BRIEF_ENDPOINT, FLY_EMOJI_BY_ID, FLY_EMOJI_CATEGORIES, FLY_EMOJI_CATEGORY_BY_ID, FLY_EMOJI_DEFAULT_CATEGORIES, FLY_EMOJI_PACK, FLY_EMOJI_PACK_MISSING_MESSAGE, FLY_EMOJI_PREVIEW_COUNT, FLY_EMPTY_VALUE, FLY_LOCALE_CATALOG, FLY_MAGIC_BAR_EVENT, FLY_MAGIC_BAR_ICONS, FLY_MAGIC_BAR_STORE_KEY, FLY_NF_CACHE_HEAL_FLAG, FLY_RELOAD_DEBOUNCE_MS, FLY_RELOAD_REARM_MS, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_ROUTES, FLY_SCAN_PENDING_DEFAULT_DELAY_MS, FLY_SCAN_PENDING_MAX_RETRIES, FLY_SCAN_PENDING_STATUS, FLY_SEARCH_DEBOUNCE_MS, FLY_SKIN_TONES, FLY_STANDALONE_AUTH_CONFIG, FLY_THEME_MODE_IDS, FLY_VIEWPORT_IS_MOBILE, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, FlyAppUnavailableComponent, FlyAuthLiveRefresh, FlyBadgeWallComponent, FlyBlockUiComponent, FlyBreadcrumbComponent, FlyButtonComponent, FlyBytesPipe, FlyCaptchaComponent, FlyCardActionsComponent, FlyCardBodyComponent, FlyCardComponent, FlyCardFooterComponent, FlyCardGridComponent, FlyCardMediaComponent, FlyCardMetaComponent, FlyCardStatusComponent, FlyCardTitleComponent, FlyCellDirective, FlyCheckboxComponent, FlyChipComponent, FlyChunkReloadErrorHandler, FlyClickOutsideDirective, FlyCollapsibleComponent, FlyCommentThreadComponent, FlyCompactNumberPipe, FlyConfirmDialogComponent, FlyControlDirective, FlyCronBuilderComponent, FlyCurrencyCatalogService, FlyCurrencySelectorComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDeepLinkPrefetchService, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileDownloadService, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLeaderboardComponent, FlyMagicActionsComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyMoneyPipe, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStandaloneMagicActionsComponent, FlyStateMessageComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyUserDirectoryService, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, GANTT_DEPENDENCY_TYPES, GANTT_ROW_TINT_ALPHA, GANTT_ZOOMS, I18nService, LAUNCH_CONTEXT, MAGIC_BAR_SEARCH_WIDTH_DEFAULT, MAGIC_BAR_SEARCH_WIDTH_MAX, MAGIC_BAR_SEARCH_WIDTH_MIN, MagicBarRegistry, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, NOVA_PRIORITY_TONE, NOVA_STATUS_TONE, NOVA_TONE_FALLBACK, OverlayStack, PRESENCE_COLORS, PageHeaderComponent, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, STATE_DEFAULT_ICONS, STATE_DEFAULT_MESSAGE_KEYS, STEP_UP_CODE, STEP_UP_REAUTH_HANDLER, STEP_UP_RETURN_KEY, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, StatusBadgeComponent, StepUpService, ToastHostComponent, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext, canGoPrev, captureFocus, clampMagicBarSearchWidth, clampPage, clampSliderValue, connectRemoteLaunch, enterActivatesNatively, filterGroup, filterSuggestions, findLocaleByDialect, findLocaleByPrefix, firstEnabledIndex, firstModuleIndex, flattenModules, flyAdminGuard, flyApiErrorMessage, flyCaptchaBase64Utf8, flyCaptchaBuildToken, flyCaptchaSha256Hex, flyCompactNumber, flyDebounced, flyDecimalNumber, flyDownloadBlob, flyDuration, flyEmojiFor, flyEmojiIsTonable, flyEmojiPackUrl, flyExportFileName, flyFirstEnabledIndex, flyFormatBytes, flyFormatDate, flyFormatDateTime, flyFormatMoney, flyFormatTime, flyFullNumber, flyNextEnabledIndex, flyNextSegmentIndex, flyRelativeTime, flyResolveActiveTab, flyRoleGuard, flyScanPendingDelayMs, flyScanRetry, flySignedCompact, flySkinToneLabelKey, flyStandaloneAuthGuard, flyStandaloneAuthInterceptor, flyToDateOnly, flyToPage, flyUnwrap, flyUnwrapCurrencies, flyUnwrapLenient, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, loadRemoteStyles, magicBarOwnerKey, magicBarSearchText, matchFlyRoutePattern, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeRoles, novaPriorityToneVar, novaStatusToneVar, overlayStack, parseCron, parseStepUpChallenge, parseTags, prefetchRemoteStyles, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
25506
25797
  //# sourceMappingURL=flyos-design-system.mjs.map