@foxeltech/angular-ui 0.7.75 → 0.7.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/components.css CHANGED
@@ -339,14 +339,27 @@ input[type='password'] {
339
339
  @apply font-mono text-sm text-text-primary tracking-normal;
340
340
  }
341
341
 
342
+ /* ---- Nút: viền hairline + bóng mờ ở MỌI biến thể (anh Tú 04/09) --------
343
+ Demo `.og-button`: border `--og-rule`, hover `0 2px 7px rgba(0,0,0,.12)`;
344
+ `.bi-newbtn`: viền tối hơn nền 1 bậc + `0 1px 2px rgba(0,0,0,.15)`. Nút nền
345
+ đặc dùng viền inset tối 8% để không phải khai màu viền riêng cho gradient. */
342
346
  .btn-default {
343
347
  @apply btn-common bg-button-default hover:opacity-75 text-text-inverse;
348
+ box-shadow:
349
+ inset 0 0 0 1px rgb(0 0 0 / 0.08),
350
+ 0 1px 2px rgb(0 0 0 / 0.12);
344
351
  }
345
352
  .btn-primary {
346
353
  @apply btn-common bg-gradient-primary hover:opacity-75 text-text-focus;
354
+ box-shadow:
355
+ inset 0 0 0 1px rgb(0 0 0 / 0.08),
356
+ 0 1px 2px rgb(0 0 0 / 0.12);
347
357
  }
348
358
  .btn-success {
349
359
  @apply btn-common bg-gradient-success hover:opacity-75 text-surface;
360
+ box-shadow:
361
+ inset 0 0 0 1px rgb(0 0 0 / 0.08),
362
+ 0 1px 2px rgb(0 0 0 / 0.12);
350
363
  }
351
364
  /* Nút hành động chính theo demo (`.bi-newbtn`): màu cố định, không phải accent. */
352
365
  /* ---- Tông severity theo ĐÚNG mô hình demo (đo 03/09) -----------------
@@ -604,13 +617,24 @@ input[type='password'] {
604
617
  }
605
618
 
606
619
  .btn-cta {
607
- @apply btn-common bg-cta hover:bg-cta-hover text-white;
620
+ @apply btn-common bg-cta hover:bg-cta-hover text-white border border-cta-hover;
608
621
  box-shadow:
609
622
  inset 0 1px 0 rgb(255 255 255 / 0.18),
610
623
  0 1px 2px rgb(0 0 0 / 0.15);
611
624
  }
625
+ .btn-cta:hover {
626
+ box-shadow:
627
+ inset 0 1px 0 rgb(255 255 255 / 0.18),
628
+ 0 2px 6px rgb(0 0 0 / 0.2);
629
+ }
630
+ /* Outline = `.og-button--outline`: nền surface, viền --og-border, bóng 1px;
631
+ hover nổi lên 0 2px 7px như demo. */
612
632
  .btn-alternative {
613
- @apply btn-common hover:bg-bg-hover text-text-primary border border-border-default;
633
+ @apply btn-common bg-surface hover:bg-bg-hover text-text-primary border border-border-default;
634
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
635
+ }
636
+ .btn-alternative:hover {
637
+ box-shadow: 0 2px 7px rgb(0 0 0 / 0.12);
614
638
  }
615
639
  .btn-menu-top {
616
640
  @apply btn-menu-common border-b border-border-default pt-small pb-semi;
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, Injector, Directive, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, Injectable, viewChild, ViewChild, Inject, EventEmitter, signal, computed, NgModule, HostListener, output, ChangeDetectorRef, contentChild, TemplateRef, contentChildren, Output, ViewChildren, ElementRef, model } from '@angular/core';
2
+ import { inject, DestroyRef, Injector, Directive, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, Injectable, viewChild, ViewChild, Inject, EventEmitter, signal, computed, NgModule, HostListener, output, ChangeDetectorRef, contentChild, TemplateRef, contentChildren, Output, ViewChildren, ElementRef, model, viewChildren, afterRenderEffect, untracked } from '@angular/core';
3
3
  import { MatPaginator } from '@angular/material/paginator';
4
4
  import { MatTableDataSource, MatTable } from '@angular/material/table';
5
5
  import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
@@ -1657,7 +1657,23 @@ function toneBadgeSolid(tone) {
1657
1657
  * neutral, thay vì sinh ra class rác như thời còn ghép chuỗi `'bg-' + tone`.
1658
1658
  */
1659
1659
  const isTone = (t) => typeof t === 'string' && Object.prototype.hasOwnProperty.call(TONE_TINT, t);
1660
- /** Chỉ MÀU của tông, không kèm hình dạng pill — cho ô icon, chip, ô nền. */
1660
+ /**
1661
+ * Chỉ MÀU của tông, không kèm hình dạng pill — cho ô icon, chip, ô nền.
1662
+ *
1663
+ * ⚠️ Đây là họ SOLID: `tone-tint-critical` đặt `background: --og-text`
1664
+ * (gần đen) VÀ `color: --og-surface` (trắng) trên CÙNG element.
1665
+ *
1666
+ * Hệ quả bắt buộc nhớ:
1667
+ * 1. Chỉ dùng cho **badge một phần tử**. Với hộp nhiều dòng thì con sẽ cần màu
1668
+ * chữ riêng, và nếu con ghi đè bằng `toneFg()` (đỏ sẫm) thì thành đỏ sẫm
1669
+ * trên gần đen — không đọc được. Đã bị đúng vậy ở hộp RISK của
1670
+ * `info-binary-detail` (anh Tú 03/09).
1671
+ * 2. **KHÔNG** đặt `toneFg()` lên phần tử con của một phần tử đã có
1672
+ * `toneTint()` — nó phá màu tương phản mà `toneTint` vừa đặt.
1673
+ *
1674
+ * Hộp/khối nhiều dòng thì dùng `toneScale()`: nền nhạt (color-mix 12%) + chữ
1675
+ * đúng tông, con chỉ cần THỪA HƯỞNG.
1676
+ */
1661
1677
  function toneTint(tone) {
1662
1678
  return isTone(tone) ? TONE_TINT[tone] : 'tone-tint-neutral';
1663
1679
  }
@@ -4357,6 +4373,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4357
4373
  * - titleStyle 'heading' (default): icon + heading, the pre-V2 look
4358
4374
  * - titleStyle 'eyebrow': the V2 design's panel header — small tracked
4359
4375
  * caption on the left, muted [aside] text on the right
4376
+ *
4377
+ * The host is a flex column (not the custom element's inline default) so that
4378
+ * in a CSS grid row every card stretches to the row height and panels line up
4379
+ * (anh Tú 04/09); a footer projected with `mt-auto` sticks to the bottom.
4360
4380
  */
4361
4381
  class SectionCardComponent {
4362
4382
  /** Caller-translated heading. */
@@ -4372,11 +4392,11 @@ class SectionCardComponent {
4372
4392
  return iconTone ? toneText(iconTone) : 'text-text-secondary';
4373
4393
  }
4374
4394
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4375
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, titleStyle: { classPropertyName: "titleStyle", publicName: "titleStyle", isSignal: true, isRequired: false, transformFunction: null }, aside: { classPropertyName: "aside", publicName: "aside", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4395
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, titleStyle: { classPropertyName: "titleStyle", publicName: "titleStyle", isSignal: true, isRequired: false, transformFunction: null }, aside: { classPropertyName: "aside", publicName: "aside", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex flex-col min-w-0" }, ngImport: i0, template: "<!-- flex column filling the host: cards in one grid row share a height and a\n projected footer can sit at the bottom with `mt-auto` (design 01). -->\n<div class=\"card-common bg-surface flex flex-col flex-1 min-h-0\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4376
4396
  }
4377
4397
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, decorators: [{
4378
4398
  type: Component,
4379
- args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n" }]
4399
+ args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], host: { class: 'flex flex-col min-w-0' }, template: "<!-- flex column filling the host: cards in one grid row share a height and a\n projected footer can sit at the bottom with `mt-auto` (design 01). -->\n<div class=\"card-common bg-surface flex flex-col flex-1 min-h-0\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n" }]
4380
4400
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{
4381
4401
  type: Input
4382
4402
  }], iconTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTone", required: false }] }], titleStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleStyle", required: false }] }], aside: [{ type: i0.Input, args: [{ isSignal: true, alias: "aside", required: false }] }] } });
@@ -4669,6 +4689,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4669
4689
  * <fx-ui-pending-badge [field]="'confirmed-chains weekly series'" />
4670
4690
  *
4671
4691
  * Keep the copy factual: it states that data is missing, never fakes a value.
4692
+ * Styling is neutral on purpose (anh Tú 04/09): a hint, not a warning.
4672
4693
  */
4673
4694
  class PendingBadgeComponent {
4674
4695
  /** Caller-translated chip text. */
@@ -4684,11 +4705,11 @@ class PendingBadgeComponent {
4684
4705
  return this.field() ? `${this.label()} — ${this.field()}` : this.label();
4685
4706
  }
4686
4707
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4687
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PendingBadgeComponent, isStandalone: true, selector: "fx-ui-pending-badge", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4708
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PendingBadgeComponent, isStandalone: true, selector: "fx-ui-pending-badge", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-muted/60 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-border-subtle bg-neutral-soft text-muted txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4688
4709
  }
4689
4710
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, decorators: [{
4690
4711
  type: Component,
4691
- args: [{ selector: 'fx-ui-pending-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n" }]
4712
+ args: [{ selector: 'fx-ui-pending-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-muted/60 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-border-subtle bg-neutral-soft text-muted txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n" }]
4692
4713
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
4693
4714
 
4694
4715
  /**
@@ -4696,10 +4717,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4696
4717
  * reviewed before its endpoint exists (anh Tú 29/08: "muốn có full giao diện
4697
4718
  * trước để review").
4698
4719
  *
4699
- * The frame makes the status unmistakable dashed outline, a corner label and
4700
- * the pending marker naming the missing field so sample shapes can never be
4701
- * mistaken for measured values. Drop the wrapper (and the sample input) the
4702
- * moment the API lands.
4720
+ * The marker is deliberately quiet (anh 04/09: no yellow frame, just a
4721
+ * subtle "Pending API" chip): a neutral chip above the block, with the label
4722
+ * and missing field in its tooltip. The content itself is non-interactive so
4723
+ * sample shapes are never mistaken for live state. Drop the wrapper (and the
4724
+ * sample input) the moment the API lands.
4703
4725
  *
4704
4726
  * The pending chip is inlined rather than composed from fx-ui-pending-badge:
4705
4727
  * one lib component importing another inside the same barrel tripped NG0919
@@ -4710,21 +4732,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4710
4732
  * </fx-ui-sample-frame>
4711
4733
  */
4712
4734
  class SampleFrameComponent {
4713
- /** Caller-translated corner label. */
4735
+ /** Caller-translated block label — surfaces in the chip tooltip only. */
4714
4736
  label = input('Sample layout', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4715
4737
  /** Field the backend still owes — shown on the pending badge tooltip. */
4716
4738
  field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
4717
4739
  /** Caller-translated pending-chip text. */
4718
4740
  pendingLabel = input('Pending API', ...(ngDevMode ? [{ debugName: "pendingLabel" }] : /* istanbul ignore next */ []));
4719
4741
  get tooltip() {
4720
- return this.field() ? `${this.pendingLabel()} ${this.field()}` : this.pendingLabel();
4742
+ const head = `${this.label()} · ${this.pendingLabel()}`;
4743
+ return this.field() ? `${head} — ${this.field()}` : head;
4721
4744
  }
4722
4745
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4723
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.20", type: SampleFrameComponent, isStandalone: true, selector: "fx-ui-sample-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4746
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.20", type: SampleFrameComponent, isStandalone: true, selector: "fx-ui-sample-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex flex-col min-h-0" }, ngImport: i0, template: "<div class=\"relative flex flex-col flex-1 min-h-0\">\n <!-- One quiet chip marks the block as sample data; the tooltip names the\n missing field. flex-wrap so the chip drops a line in narrow rails. -->\n <div class=\"flex flex-wrap items-center gap-small mb-small\">\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-border-subtle bg-neutral-soft text-muted txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90 flex-1 min-h-0 flex flex-col\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4724
4747
  }
4725
4748
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, decorators: [{
4726
4749
  type: Component,
4727
- args: [{ selector: 'fx-ui-sample-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
4750
+ args: [{ selector: 'fx-ui-sample-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], host: { class: 'flex flex-col min-h-0' }, template: "<div class=\"relative flex flex-col flex-1 min-h-0\">\n <!-- One quiet chip marks the block as sample data; the tooltip names the\n missing field. flex-wrap so the chip drops a line in narrow rails. -->\n <div class=\"flex flex-wrap items-center gap-small mb-small\">\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-border-subtle bg-neutral-soft text-muted txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90 flex-1 min-h-0 flex flex-col\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
4728
4751
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], pendingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingLabel", required: false }] }] } });
4729
4752
 
4730
4753
  /**
@@ -4822,12 +4845,53 @@ class SegmentedComponent {
4822
4845
  * (design refs 04/05).
4823
4846
  */
4824
4847
  variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4848
+ /**
4849
+ * 'equal' (default): every segment takes the same width (view toggles such
4850
+ * as Catalog | Table). 'content': each segment is as wide as its label —
4851
+ * the demo's detail-pane tabs ("Affected software · Summary · Evidence"),
4852
+ * where equal columns would truncate the long label in a narrow pane.
4853
+ */
4854
+ fit = input('equal', ...(ngDevMode ? [{ debugName: "fit" }] : /* istanbul ignore next */ []));
4855
+ optionEls = viewChildren('opt', ...(ngDevMode ? [{ debugName: "optionEls" }] : /* istanbul ignore next */ []));
4856
+ /** Measured box of the active segment — only used when fit === 'content'. */
4857
+ thumbBox = signal(null, ...(ngDevMode ? [{ debugName: "thumbBox" }] : /* istanbul ignore next */ []));
4858
+ constructor() {
4859
+ // Content-fit segments have no shared width to derive the thumb from, so
4860
+ // measure the active button after each render that could move it.
4861
+ afterRenderEffect(() => {
4862
+ const index = this.activeIndex();
4863
+ this.options();
4864
+ const els = this.optionEls();
4865
+ if (this.fit() !== 'content' || index < 0 || !els[index]) {
4866
+ untracked(() => this.thumbBox.set(null));
4867
+ return;
4868
+ }
4869
+ const el = els[index].nativeElement;
4870
+ const box = { left: el.offsetLeft, width: el.offsetWidth };
4871
+ untracked(() => {
4872
+ const prev = this.thumbBox();
4873
+ if (!prev || prev.left !== box.left || prev.width !== box.width)
4874
+ this.thumbBox.set(box);
4875
+ });
4876
+ });
4877
+ }
4825
4878
  /** Index the sliding thumb sits on; -1 hides it (no/unknown selection). */
4826
4879
  activeIndex = computed(() => this.options().findIndex((o) => o.value === this.value()), ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
4827
4880
  /** Equal-width tracks → the thumb is 1/n of the inner width and slides by
4828
4881
  * multiples of its own width. Pure CSS transform, nothing to measure. */
4829
- thumbWidth = computed(() => `calc((100% - 6px) / ${this.options().length || 1})`, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
4830
- thumbTransform = computed(() => `translateX(${this.activeIndex() * 100}%)`, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
4882
+ thumbWidth = computed(() => {
4883
+ const box = this.thumbBox();
4884
+ if (this.fit() === 'content')
4885
+ return box ? `${box.width}px` : '0px';
4886
+ return `calc((100% - 6px) / ${this.options().length || 1})`;
4887
+ }, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
4888
+ thumbTransform = computed(() => {
4889
+ const box = this.thumbBox();
4890
+ if (this.fit() === 'content')
4891
+ return box ? `translateX(${box.left - 3}px)` : 'none';
4892
+ return `translateX(${this.activeIndex() * 100}%)`;
4893
+ }, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
4894
+ gridClass = computed(() => (this.fit() === 'content' ? 'auto-cols-auto' : 'auto-cols-fr'), ...(ngDevMode ? [{ debugName: "gridClass" }] : /* istanbul ignore next */ []));
4831
4895
  select(option) {
4832
4896
  if (option.disabled || option.value === this.value())
4833
4897
  return;
@@ -4845,12 +4909,12 @@ class SegmentedComponent {
4845
4909
  return this.variant() === 'pill' ? 'text-accent-contrast' : 'text-strong';
4846
4910
  }
4847
4911
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4848
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4912
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "optionEls", predicate: ["opt"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4849
4913
  }
4850
4914
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, decorators: [{
4851
4915
  type: Component,
4852
- args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
4853
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
4916
+ args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
4917
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], optionEls: [{ type: i0.ViewChildren, args: ['opt', { isSignal: true }] }] } });
4854
4918
 
4855
4919
  /**
4856
4920
  * V2 ListRow — presentational body for master-detail #row templates and other
@@ -4989,11 +5053,11 @@ class ProjectCardComponent {
4989
5053
  return tone ? TREND_BLOCK[tone] : 'bg-bg-hover';
4990
5054
  }
4991
5055
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4992
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ProjectCardComponent, isStandalone: true, selector: "fx-ui-project-card", inputs: { card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { open: "open" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5056
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ProjectCardComponent, isStandalone: true, selector: "fx-ui-project-card", inputs: { card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { open: "open" }, host: { classAttribute: "flex flex-col min-w-0" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex-1 flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <!-- min-h 46px = name (24) + mt-xs (2) + sub (20): reserves the two-line\n header even when a card has no sub line, so trend/metric rows line\n up across the grid row (measured 04/09). -->\n <div class=\"min-w-0 min-h-[46px]\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <!-- mt-auto: footers line up across a grid row whatever each card holds -->\n <div\n class=\"flex items-center justify-between w-full pt-normal mt-auto border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4993
5057
  }
4994
5058
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, decorators: [{
4995
5059
  type: Component,
4996
- args: [{ selector: 'fx-ui-project-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, SparkbarComponent, HeroIconComponent], template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n" }]
5060
+ args: [{ selector: 'fx-ui-project-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, SparkbarComponent, HeroIconComponent], host: { class: 'flex flex-col min-w-0' }, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex-1 flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <!-- min-h 46px = name (24) + mt-xs (2) + sub (20): reserves the two-line\n header even when a card has no sub line, so trend/metric rows line\n up across the grid row (measured 04/09). -->\n <div class=\"min-w-0 min-h-[46px]\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <!-- mt-auto: footers line up across a grid row whatever each card holds -->\n <div\n class=\"flex items-center justify-between w-full pt-normal mt-auto border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n" }]
4997
5061
  }], propDecorators: { card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: true }] }], open: [{ type: i0.Output, args: ["open"] }] } });
4998
5062
 
4999
5063
  /**