@compsych-ui-components/angular 8.0.2 → 8.0.4
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/esm2022/index.js +6 -0
- package/esm2022/index.js.map +1 -1
- package/esm2022/lib/card-base/card.js +3 -3
- package/esm2022/lib/card-base/card.js.map +1 -1
- package/esm2022/lib/chip/chip.js +168 -0
- package/esm2022/lib/chip/chip.js.map +1 -0
- package/esm2022/lib/dropdown/dropdown.js +5 -177
- package/esm2022/lib/dropdown/dropdown.js.map +1 -1
- package/index.d.ts +13 -10
- package/lib/card-base/card.d.ts +3 -3
- package/lib/chip/chip.d.ts +108 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/card-base/card.ts","../../../../../../packages/angular/src/lib/card-base/card.html"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,SAAS,EACT,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,EACL,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAY,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,8DAA8D,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;;;AAwCnD;;;;;;;;;;;;;;;;;;;;GAoBG;AAgDH,MAAM,OAAO,aAAa;IACxB,2DAA2D;IAC3D,0DAA0D;IACjD,OAAO,GAAG,KAAK,CAAc,OAAO,8EAAC,CAAC;IAC/C,iDAAiD;IACxC,UAAU,GAAG,KAAK,CAAiB,UAAU,iFAAC,CAAC;IACxD,oDAAoD;IAC3C,MAAM,GAAG,KAAK,CAAa,IAAI,6EAAC,CAAC;IAC1C,2EAA2E;IAClE,OAAO,GAAG,KAAK,CAAc,IAAI,8EAAC,CAAC;IAC5C,4EAA4E;IACnE,IAAI,GAAG,KAAK,CAAW,IAAI,2EAAC,CAAC;IACtC,kFAAkF;IACzE,WAAW,GAAG,KAAK,CAAU,KAAK,kFAAC,CAAC;IAC7C,yCAAyC;IAChC,QAAQ,GAAG,KAAK,CAAU,KAAK,+EAAC,CAAC;IAC1C,oGAAoG;IAC3F,QAAQ,GAAG,KAAK,CAAU,KAAK,+EAAC,CAAC;IAC1C,sDAAsD;IAC7C,QAAQ,GAAG,KAAK,CAAS,EAAE,+EAAC,CAAC;IACtC;;;OAGG;IACM,SAAS,GAAG,KAAK,CAAU,KAAK,gFAAC,CAAC;IAC3C;;;OAGG;IACM,aAAa,GAAG,KAAK,CAAS,QAAQ,oFAAC,CAAC;IAEjD,2DAA2D;IAC3D,8DAA8D;IACrD,IAAI,GAAG,KAAK,CAAS,EAAE,2EAAC,CAAC;IAClC,mCAAmC;IAC1B,QAAQ,GAAG,KAAK,CAAS,EAAE,+EAAC,CAAC;IACtC;;;OAGG;IACM,aAAa,GAAG,KAAK,CAAoB,MAAM,oFAAC,CAAC;IAC1D;;;;OAIG;IACM,SAAS,GAAG,KAAK,CAAgB,SAAS,gFAAC,CAAC;IACrD,sBAAsB;IACb,KAAK,GAAG,KAAK,CAAS,EAAE,4EAAC,CAAC;IACnC,yBAAyB;IAChB,QAAQ,GAAG,KAAK,CAAS,EAAE,+EAAC,CAAC;IACtC,8CAA8C;IACrC,WAAW,GAAG,KAAK,CAAS,EAAE,kFAAC,CAAC;IACzC,2DAA2D;IAClD,QAAQ,GAAG,KAAK,CAAS,EAAE,+EAAC,CAAC;IACtC,iGAAiG;IACxF,mBAAmB,GAAG,KAAK,CAAS,EAAE,0FAAC,CAAC;IACjD,2GAA2G;IAClG,OAAO,GAAG,KAAK,CAAiB,EAAE,8EAAC,CAAC;IAC7C,iDAAiD;IACxC,WAAW,GAAG,KAAK,CAAS,EAAE,kFAAC,CAAC;IACzC,yEAAyE;IAChE,iBAAiB,GAAG,KAAK,CAAS,EAAE,wFAAC,CAAC;IAC/C,0EAA0E;IACjE,kBAAkB,GAAG,KAAK,CAAS,EAAE,yFAAC,CAAC;IAChD,2CAA2C;IAClC,cAAc,GAAG,KAAK,CAAU,KAAK,qFAAC,CAAC;IAChD,qHAAqH;IAC5G,UAAU,GAAG,KAAK,CAAU,KAAK,iFAAC,CAAC;IAC5C,4DAA4D;IACnD,SAAS,GAAG,KAAK,CAAU,IAAI,gFAAC,CAAC;IAC1C,6GAA6G;IACpG,YAAY,GAAG,KAAK,CAAU,IAAI,mFAAC,CAAC;IAC7C,6EAA6E;IACpE,OAAO,GAAG,KAAK,CAAU,KAAK,8EAAC,CAAC;IACzC;;;;OAIG;IACM,cAAc,GAAG,KAAK,CAAU,KAAK,qFAAC,CAAC;IAEhD,2DAA2D;IAC3D,oCAAoC;IAC3B,KAAK,GAAG,KAAK,CAAS,EAAE,4EAAC,CAAC;IACnC,wDAAwD;IAC/C,YAAY,GAAG,KAAK,CAAS,gBAAgB,mFAAC,CAAC;IACxD,kCAAkC;IACzB,WAAW,GAAG,KAAK,CAAkB,YAAY,kFAAC,CAAC;IAC5D,+DAA+D;IACtD,SAAS,GAAG,KAAK,CAAU,KAAK,gFAAC,CAAC;IAE3C,2DAA2D;IAC3D,kDAAkD;IACzC,UAAU,GAAG,KAAK,CAAiB,QAAQ,iFAAC,CAAC;IACtD,mCAAmC;IAC1B,KAAK,GAAG,KAAK,CAAkB,EAAE,4EAAC,CAAC;IAC5C,8DAA8D;IACrD,OAAO,GAAG,KAAK,CAAU,KAAK,8EAAC,CAAC;IAEzC,2DAA2D;IAC3D;;;;;;OAMG;IACM,YAAY,GAAG,KAAK,CAA+B,SAAS,mFAAC,CAAC;IACvE;;;;;OAKG;IACM,SAAS,GAAG,KAAK,CAAqB,SAAS,iFAAI,KAAK,EAAE,YAAY,GAAG,CAAC;IAEnF,wEAAwE;IAC/D,SAAS,GAAG,MAAM,EAAQ,CAAC;IACpC,6DAA6D;IACpD,WAAW,GAAG,MAAM,EAAQ,CAAC;IAErB,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACjC,MAAM,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;IAEzC,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,QAAQ,+EAAC,CAAC;IAEnE;;;;OAIG;IACM,qBAAqB,GAAG,QAAQ,CAAC,GAAG,EAAE,CAC7C,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,4FACrD,CAAC;IAEF,4EAA4E;IACnE,aAAa,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,oFAAC,CAAC;IACtG,mEAAmE;IAC1D,cAAc,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,qFAAC,CAAC;IAE3E,wEAAwE;IAC/D,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC,CAAC,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACtC,CAAC,+EAAC,CAAC;IAEH,wEAAwE;IAC/D,gBAAgB,GAAG,QAAQ,CAAc,GAAG,EAAE;QACrD,QAAQ,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACvB,KAAK,SAAS;gBACZ,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YACzC,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,CAAC,wCAAwC;YACvD,KAAK,OAAO;gBACV,OAAO,MAAM,CAAC,CAAC,gEAAgE;YACjF,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,UAAU,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,mBAAmB;YACzE;gBACE,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC;IACH,CAAC,uFAAC,CAAC;IAEH;QACE,MAAM,CAAC,GAAG,EAAE;YACV,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,UAAU;QACR,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE,OAAO;QACnD,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC;IAED,SAAS,CAAC,KAAoB;QAC5B,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE,OAAO;QACnD,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO;QACzB,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG;YAAE,OAAO;QACvD,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC;IAED,4DAA4D;IAC5D,OAAO,CAAC,IAAY,EAAE,IAAY,EAAE,WAAW,GAAG,IAAI;QACpD,IAAI,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,eAAe,IAAI,aAAa,IAAI,IAAI,CAAC,CAAC;QACrE,IAAI,WAAW,KAAK,GAAG,EAAE,CAAC;YACxB,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,oBAAoB,EAAE,iBAAiB,WAAW,GAAG,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,uBAAuB,CAAC,GAAG,CAAC,CAAC;IACrD,CAAC;wGA9LU,aAAa;4FAAb,aAAa,8wOCzH1B,ogZAgPA,ojrBDvHa,aAAa,+nBA3Cd,gBAAgB,oJAAE,eAAe;;4FA2ChC,aAAa;kBA/CzB,SAAS;+BACE,eAAe,cACb,IAAI,mBACC,uBAAuB,CAAC,MAAM,WACtC,CAAC,gBAAgB,EAAE,eAAe,gBAAgB,kBAC3C,CAAC,eAAe,CAAC,QAG3B;wBACJ,KAAK,EAAE,UAAU;wBACjB,4BAA4B,EAAE,6BAA6B;wBAC3D,0BAA0B,EAAE,2BAA2B;wBACvD,4BAA4B,EAAE,6BAA6B;wBAC3D,0BAA0B,EAAE,2BAA2B;wBACvD,2BAA2B,EAAE,yBAAyB;wBACtD,yBAAyB,EAAE,uBAAuB;wBAClD,uBAAuB,EAAE,uCAAuC;wBAChE,0BAA0B,EAAE,wBAAwB;wBACpD,2BAA2B,EAAE,qCAAqC;wBAClE,6BAA6B,EAAE,mBAAmB;wBAClD,6BAA6B,EAAE,mBAAmB;wBAClD,6BAA6B,EAAE,mBAAmB;wBAClD,4BAA4B,EAAE,+BAA+B;wBAC7D,0BAA0B,EAAE,6BAA6B;wBACzD,0BAA0B,EAAE,6BAA6B;wBACzD,0BAA0B,EAAE,6BAA6B;wBACzD,0BAA0B,EAAE,6BAA6B;wBACzD,eAAe,EAAE,6BAA6B;wBAC9C,+BAA+B,EAAE,eAAe;wBAChD,4BAA4B,EAAE,YAAY;wBAC1C,4BAA4B,EAAE,YAAY;wBAC1C,+BAA+B,EAAE,iBAAiB;wBAClD,2BAA2B,EAAE,WAAW;wBACxC,6BAA6B,EAAE,oDAAoD;wBACnF,2BAA2B,EAAE,kBAAkB;wBAC/C,8BAA8B,EAAE,aAAa;wBAC7C,aAAa,EAAE,iCAAiC;wBAChD,iBAAiB,EAAE,yCAAyC;wBAC5D,mBAAmB,EAAE,qBAAqB;wBAC1C,sBAAsB,EAAE,oBAAoB;wBAC5C,qBAAqB,EAAE,4DAA4D;wBACnF,sBAAsB,EAAE,kFAAkF;wBAC1G,sBAAsB,EAAE,qFAAqF;wBAC7G,SAAS,EAAE,cAAc;wBACzB,WAAW,EAAE,mBAAmB;qBACjC","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n computed,\r\n effect,\r\n inject,\r\n input,\r\n output,\r\n} from '@angular/core';\r\nimport { NgTemplateOutlet } from '@angular/common';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\nimport { RippleDirective } from './ripple.directive';\r\nimport { ButtonComponent } from '../button/button';\r\n\r\n/** A single metric item for the banner card's metric row. */\r\nexport interface BannerMetric {\r\n value: string | number;\r\n label?: string;\r\n /** Renders the metric in its primary-color \"current\" state. */\r\n current?: boolean;\r\n}\r\n\r\n/**\r\n * Card type — selects the projected/laid-out content. Mirrors the library's `variant` pattern.\r\n * `basic` is the Figma \"cards\" section (the \"Card Item\" family: icon + arrow +\r\n * title/subtitle, sized, across the outlined/filled/gradient/banner surfaces) —\r\n * distinct from the `content` category tiles.\r\n */\r\nexport type CardVariant = 'basic' | 'content' | 'action' | 'metric';\r\n/** Visual surface treatment. Mirrors the Figma \"Card Item\" Style property. */\r\nexport type CardAppearance = 'outlined' | 'filled' | 'gradient' | 'banner';\r\n/** Corner radius token: `md` (12px), `lg` (16px), `xl` (24px). */\r\nexport type CardRadius = 'md' | 'lg' | 'xl';\r\n/** Inner padding token (used by `variant=\"basic\"`; typed variants size themselves). */\r\nexport type CardPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl';\r\n/** Action-card layout. */\r\nexport type CardOrientation = 'horizontal' | 'vertical';\r\n/** Metric-card size. `medium` is the default (24px padding, headline-medium value); `small` is compact (16px padding, headline-small value). */\r\nexport type CardMetricSize = 'medium' | 'small';\r\n/** Base Card Item size (Figma \"Size\" property). Drives layout + type scale. */\r\nexport type CardSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';\r\n/** Heading level for the title / headline / action label (screen-reader heading semantics). */\r\nexport type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;\r\n/** How the content-variant category renders: trailing pill (`chip`) or leading plain-text `label`. */\r\nexport type CardCategoryStyle = 'chip' | 'label';\r\n/**\r\n * Content-variant icon badge treatment:\r\n * - `primary` — white icon on a filled primary (blue) circle (default).\r\n * - `neutral` — dark (on-surface) icon on a white circle with a thin outline.\r\n */\r\nexport type CardIconStyle = 'primary' | 'neutral';\r\n\r\n/**\r\n * Card — the single Cards component for the ComPsych Design System.\r\n *\r\n * Figma: Core Components — \"Cards\" (file VFBn7KCDy3FSIlvhNo3ylq, 1117:9829).\r\n *\r\n * One component, switched by `variant` (like Accordion's `variant`):\r\n * - `basic` — a bare surface; project your own content.\r\n * - `content` — icon badge / image, category chip, title, subtitle, description,\r\n * action pill, arrow; headline layout.\r\n * - `action` — compact interactive row (icon · label/description · arrow); horizontal/vertical.\r\n * - `metric` — centered value + label; `current` (selected) state.\r\n *\r\n * `appearance` is the orthogonal surface skin (outlined / filled / gradient / banner).\r\n * When `interactive`, the whole card is a button (role/tabindex/keyboard) with the\r\n * hover / focus / pressed ripple. All values are sys design tokens.\r\n *\r\n * <compsych-card variant=\"content\" appearance=\"gradient\" icon=\"circle-play\"\r\n * category=\"Video\" title=\"Mindful breathing\" actionLabel=\"Watch\" [interactive]=\"true\" />\r\n * <compsych-card variant=\"metric\" value=\"24\" label=\"Sessions\" [current]=\"true\" [interactive]=\"true\" />\r\n * <compsych-card>Projected content</compsych-card>\r\n */\r\n@Component({\r\n selector: 'compsych-card',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [NgTemplateOutlet, ButtonComponent, CardComponent],\r\n hostDirectives: [RippleDirective],\r\n templateUrl: './card.html',\r\n styleUrl: './card.css',\r\n host: {\r\n class: 'cmp-card',\r\n '[class.cmp-card--outlined]': \"appearance() === 'outlined'\",\r\n '[class.cmp-card--filled]': \"appearance() === 'filled'\",\r\n '[class.cmp-card--gradient]': \"appearance() === 'gradient'\",\r\n '[class.cmp-card--banner]': \"appearance() === 'banner'\",\r\n '[class.cmp-card--content]': \"variant() === 'content'\",\r\n '[class.cmp-card--basic]': \"variant() === 'basic'\",\r\n '[attr.data-card-size]': \"variant() === 'basic' ? size() : null\",\r\n '[class.cmp-card--metric]': \"variant() === 'metric'\",\r\n '[class.cmp-card--current]': \"variant() === 'metric' && current()\",\r\n '[class.cmp-card--radius-md]': \"radius() === 'md'\",\r\n '[class.cmp-card--radius-lg]': \"radius() === 'lg'\",\r\n '[class.cmp-card--radius-xl]': \"radius() === 'xl'\",\r\n '[class.cmp-card--pad-none]': \"effectivePadding() === 'none'\",\r\n '[class.cmp-card--pad-sm]': \"effectivePadding() === 'sm'\",\r\n '[class.cmp-card--pad-md]': \"effectivePadding() === 'md'\",\r\n '[class.cmp-card--pad-lg]': \"effectivePadding() === 'lg'\",\r\n '[class.cmp-card--pad-xl]': \"effectivePadding() === 'xl'\",\r\n '[style.width]': \"fullWidth() ? '100%' : null\",\r\n '[class.cmp-card--interactive]': 'interactive()',\r\n '[class.cmp-card--disabled]': 'disabled()',\r\n '[class.cmp-card--selected]': 'selected()',\r\n '[class.cmp-card--no-gradient]': '!showGradient()',\r\n '[class.cmp-card--compact]': 'compact()',\r\n '[class.cmp-card--metric-sm]': \"variant() === 'metric' && metricSize() === 'small'\",\r\n '[class.cmp-card--dark-bg]': 'darkBackground()',\r\n '[class.cmp-card--hide-image]': 'hideImage()',\r\n '[attr.role]': \"interactive() ? 'button' : null\",\r\n '[attr.tabindex]': 'interactive() && !disabled() ? 0 : null',\r\n '[attr.aria-label]': 'ariaLabel() ?? null',\r\n '[attr.aria-disabled]': 'disabled() || null',\r\n '[attr.aria-pressed]': \"interactive() && variant() === 'metric' ? current() : null\",\r\n '[style.border-color]': \"variant() === 'metric' && current() ? 'var(--accent-primary-sys-primary)' : null\",\r\n '[style.border-width]': \"variant() === 'metric' && current() ? 'var(--border-width-sys-stroke-thick)' : null\",\r\n '(click)': 'onActivate()',\r\n '(keydown)': 'onKeyDown($event)',\r\n },\r\n})\r\nexport class CardComponent {\r\n // ── Surface (all variants) ──────────────────────────────\r\n /** Card type. Defaults to `basic` (projected content). */\r\n readonly variant = input<CardVariant>('basic');\r\n /** Surface treatment. Defaults to `outlined`. */\r\n readonly appearance = input<CardAppearance>('outlined');\r\n /** Corner radius token. Defaults to `lg` (16px). */\r\n readonly radius = input<CardRadius>('lg');\r\n /** Inner padding for `variant=\"basic\"`. Typed variants size themselves. */\r\n readonly padding = input<CardPadding>('lg');\r\n /** Card Item size (`variant=\"basic\"`): xl/lg/md/sm/xs. Defaults to `md`. */\r\n readonly size = input<CardSize>('md');\r\n /** Whole-card click target: adds role/tabindex, hover/press states and ripple. */\r\n readonly interactive = input<boolean>(false);\r\n /** Disabled visual + non-interactive. */\r\n readonly disabled = input<boolean>(false);\r\n /** Applies the focused (blue outline) appearance. Useful for action cards to indicate selection. */\r\n readonly selected = input<boolean>(false);\r\n /** Background image URL for `appearance=\"banner\"`. */\r\n readonly imageSrc = input<string>('');\r\n /**\r\n * Hides the background image on an `appearance=\"banner\"` card and replaces it\r\n * with a `--accent-primary-sys-primary-container` solid background.\r\n */\r\n readonly hideImage = input<boolean>(false);\r\n /**\r\n * `background-position` for the `banner` appearance media (e.g. `'85% 0'` for an\r\n * off-center image). Defaults to `'center'`.\r\n */\r\n readonly imagePosition = input<string>('center');\r\n\r\n // ── Content variant ─────────────────────────────────────\r\n /** Lucide icon name (content badge / action leading icon). */\r\n readonly icon = input<string>('');\r\n /** Content: category chip text. */\r\n readonly category = input<string>('');\r\n /**\r\n * Content: how the category renders — `'chip'` (default) as the trailing pill,\r\n * or `'label'` as a leading plain-text label beside the icon.\r\n */\r\n readonly categoryStyle = input<CardCategoryStyle>('chip');\r\n /**\r\n * Content: icon badge treatment — `'primary'` (default) is a white icon on a\r\n * filled primary (blue) circle; `'neutral'` is a dark icon on a white circle\r\n * with a thin outline.\r\n */\r\n readonly iconStyle = input<CardIconStyle>('primary');\r\n /** Content: title. */\r\n readonly title = input<string>('');\r\n /** Content: subtitle. */\r\n readonly subtitle = input<string>('');\r\n /** Content/action: supporting description. */\r\n readonly description = input<string>('');\r\n /** Content: large display text for the headline layout. */\r\n readonly headline = input<string>('');\r\n /** Content (`appearance=\"banner\"`): supporting text rendered below the headline, in white-80. */\r\n readonly headlineDescription = input<string>('');\r\n /** Content (`appearance=\"banner\"`): metric cards rendered in a dark row below the headline description. */\r\n readonly metrics = input<BannerMetric[]>([]);\r\n /** Content: presentational action pill label. */\r\n readonly actionLabel = input<string>('');\r\n /** Content: leading Lucide icon name rendered inside the action pill. */\r\n readonly actionLeadingIcon = input<string>('');\r\n /** Content: trailing Lucide icon name rendered inside the action pill. */\r\n readonly actionTrailingIcon = input<string>('');\r\n /** Content: disables the action button. */\r\n readonly actionDisabled = input<boolean>(false);\r\n /** Content (outlined/filled): when true, replaces the full-width banner with a plain icon in the top-left corner. */\r\n readonly inlineIcon = input<boolean>(false);\r\n /** Content: show the trailing arrow-up-right affordance. */\r\n readonly showArrow = input<boolean>(true);\r\n /** Content (gradient): show the inner gradient panel background. Set to `false` to render a flat surface. */\r\n readonly showGradient = input<boolean>(true);\r\n /** Content: remove the fixed min-height so the card sizes to its content. */\r\n readonly compact = input<boolean>(false);\r\n /**\r\n * Renders the card on a dark surface: background becomes\r\n * `--transparent-neutral-sys-white-20` (closest token to rgba(255,255,255,0.16))\r\n * and text color becomes white.\r\n */\r\n readonly darkBackground = input<boolean>(false);\r\n\r\n // ── Action variant ──────────────────────────────────────\r\n /** Action/metric: primary label. */\r\n readonly label = input<string>('');\r\n /** Action: trailing Lucide icon (horizontal layout). */\r\n readonly trailingIcon = input<string>('arrow-up-right');\r\n /** Action: layout orientation. */\r\n readonly orientation = input<CardOrientation>('horizontal');\r\n /** Action: stretch the card to 100% of its container width. */\r\n readonly fullWidth = input<boolean>(false);\r\n\r\n // ── Metric variant ──────────────────────────────────────\r\n /** Metric: size variant. Defaults to `medium`. */\r\n readonly metricSize = input<CardMetricSize>('medium');\r\n /** Metric: the prominent value. */\r\n readonly value = input<string | number>('');\r\n /** Metric: selected/current state (primary border + text). */\r\n readonly current = input<boolean>(false);\r\n\r\n // ── Accessibility ───────────────────────────────────────\r\n /**\r\n * Opts the title / headline / action label into a heading in the\r\n * accessibility tree via `role=\"heading\"` + `aria-level`, so consumers that\r\n * replace real headings keep heading semantics and screen-reader heading\r\n * navigation (WCAG 1.3.1). Omit (default) to render as plain text — no visual\r\n * or behavioral change for existing usages.\r\n */\r\n readonly headingLevel = input<CardHeadingLevel | undefined>(undefined);\r\n /**\r\n * Accessible name for an `interactive` card (which is a `role=\"button\"`).\r\n * Required for image-only / icon-only interactive cards that have no text, and\r\n * useful to give text-heavy cards a concise name instead of their full content.\r\n * Bind as `aria-label`.\r\n */\r\n readonly ariaLabel = input<string | undefined>(undefined, { alias: 'aria-label' });\r\n\r\n /** Emits on click / Enter / Space when interactive and not disabled. */\r\n readonly cardClick = output<void>();\r\n /** Emits when the banner card's action button is clicked. */\r\n readonly actionClick = output<void>();\r\n\r\n private readonly sanitizer = inject(DomSanitizer);\r\n private readonly ripple = inject(RippleDirective);\r\n\r\n readonly isBanner = computed(() => this.appearance() === 'banner');\r\n\r\n /**\r\n * Heading level actually applied. Suppressed when the whole card is interactive\r\n * (`role=\"button\"`), since a heading nested in a button isn't exposed as a\r\n * heading — the two would conflict.\r\n */\r\n readonly effectiveHeadingLevel = computed(() =>\r\n this.interactive() ? undefined : this.headingLevel(),\r\n );\r\n\r\n /** Base Card Item plain-icon glyph size by `size` (48 md, 32 sm, 24 xs). */\r\n readonly basicIconSize = computed(() => (this.size() === 'sm' ? 32 : this.size() === 'xs' ? 24 : 48));\r\n /** Base Card Item arrow glyph size by `size` (24 md, 20 sm/xs). */\r\n readonly basicArrowSize = computed(() => (this.size() === 'md' ? 24 : 20));\r\n\r\n /** CSS `url(...)` for the media layer, or null when no image is set. */\r\n readonly mediaUrl = computed(() => {\r\n const src = this.imageSrc();\r\n return src ? `url(\"${src}\")` : null;\r\n });\r\n\r\n /** Padding resolved per variant; `basic` honors the `padding` input. */\r\n readonly effectivePadding = computed<CardPadding>(() => {\r\n switch (this.variant()) {\r\n case 'content':\r\n return this.isBanner() ? 'none' : 'sm';\r\n case 'action':\r\n return 'lg'; // 24px — Figma action card (60017:8036)\r\n case 'basic':\r\n return 'none'; // .bc controls its own padding per style (Figma Base Card Item)\r\n case 'metric':\r\n return this.metricSize() === 'small' ? 'sm' : 'lg'; // sm=16px, lg=24px\r\n default:\r\n return this.padding();\r\n }\r\n });\r\n\r\n constructor() {\r\n effect(() => {\r\n this.ripple.suppressed = !this.interactive() || this.disabled();\r\n });\r\n }\r\n\r\n onActivate(): void {\r\n if (!this.interactive() || this.disabled()) return;\r\n this.cardClick.emit();\r\n }\r\n\r\n onKeyDown(event: KeyboardEvent): void {\r\n if (!this.interactive() || this.disabled()) return;\r\n if (event.repeat) return;\r\n if (event.key !== 'Enter' && event.key !== ' ') return;\r\n event.preventDefault();\r\n this.cardClick.emit();\r\n }\r\n\r\n /** Renders a sized/weighted Lucide SVG for template use. */\r\n iconSvg(name: string, size: number, strokeWidth = 1.75): SafeHtml {\r\n let svg = LUCIDE_ICONS[name] ?? '';\r\n svg = svg.replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n if (strokeWidth !== 1.5) {\r\n svg = svg.replace('stroke-width=\"1.5\"', `stroke-width=\"${strokeWidth}\"`);\r\n }\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n}\r\n","@if (isBanner() && mediaUrl() && !hideImage()) {\r\n <div class=\"cmp-card__media\" [style.background-image]=\"mediaUrl()\" [style.background-position]=\"imagePosition()\"></div>\r\n <div class=\"cmp-card__scrim\" aria-hidden=\"true\"></div>\r\n}\r\n\r\n<div class=\"cmp-card__body\">\r\n @switch (variant()) {\r\n @case ('content') {\r\n @if (isBanner()) {\r\n <div class=\"cc cc--banner\">\r\n <div class=\"cc__img-top\">\r\n @if (icon()) {\r\n <div class=\"cc__media-icon\" [innerHTML]=\"iconSvg(icon(), 48)\" aria-hidden=\"true\"></div>\r\n }\r\n @if (category()) { <span class=\"cc__chip cc__chip--glass\">{{ category() }}</span> }\r\n </div>\r\n <div class=\"cc__img-foot\">\r\n @if (title() || subtitle() || headline() || description() || headlineDescription() || metrics().length) {\r\n <div class=\"cc__banner-text\">\r\n @if (title() || subtitle()) {\r\n <div class=\"cc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"cc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"cc__desc\">{{ description() }}</p> }\r\n @if (headline()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__headline', text: headline() }\" /> }\r\n @if (headlineDescription()) { <p class=\"cc__headline-desc\" [innerHTML]=\"headlineDescription()\"></p> }\r\n @if (metrics().length) {\r\n <div class=\"cc__metrics\">\r\n @for (m of metrics(); track $index) {\r\n <compsych-card variant=\"metric\" [value]=\"m.value\" [label]=\"m.label ?? ''\" [current]=\"m.current ?? false\" [darkBackground]=\"true\" metricSize=\"small\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n @if (actionLabel()) {\r\n <compsych-button variant=\"elevated\" [label]=\"actionLabel()\" [leadingIcon]=\"actionLeadingIcon()\" [trailingIcon]=\"actionTrailingIcon()\" [disabled]=\"actionDisabled()\" (click)=\"actionClick.emit(); $event.stopPropagation()\" />\r\n }\r\n </div>\r\n </div>\r\n } @else if (appearance() === 'gradient') {\r\n <div class=\"cc\">\r\n <div class=\"cc__top\" [class.cc__top--label]=\"categoryStyle() === 'label'\">\r\n @if (icon()) {\r\n <div class=\"cc__icon\" [class.cc__icon--neutral]=\"iconStyle() === 'neutral'\" [innerHTML]=\"iconSvg(icon(), 24)\" aria-hidden=\"true\"></div>\r\n }\r\n @if (categoryStyle() === 'label' && category()) {\r\n <span class=\"cc__category-label\">{{ category() }}</span>\r\n }\r\n <div class=\"cc__top-end\">\r\n @if (categoryStyle() === 'chip' && category()) { <span class=\"cc__chip\">{{ category() }}</span> }\r\n @if (showArrow()) {\r\n <span class=\"cc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', 24)\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"cc__text\">\r\n @if (title() || subtitle()) {\r\n <div class=\"cc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"cc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"cc__desc\">{{ description() }}</p> }\r\n </div>\r\n @if (actionLabel()) {\r\n <compsych-button variant=\"outlined\" [label]=\"actionLabel()\" [leadingIcon]=\"actionLeadingIcon()\" [trailingIcon]=\"actionTrailingIcon()\" [disabled]=\"actionDisabled()\" />\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"cc cc--with-icon\" [class.cc--filled]=\"appearance() === 'filled'\" [class.cc--inline-icon]=\"inlineIcon()\">\r\n @if (inlineIcon()) {\r\n @if (icon()) {\r\n <span class=\"cc__banner-icon\" [innerHTML]=\"iconSvg(icon(), 32, 1.5)\" aria-hidden=\"true\"></span>\r\n }\r\n } @else {\r\n <div class=\"cc__banner\">\r\n @if (icon()) {\r\n <span class=\"cc__banner-icon\" [innerHTML]=\"iconSvg(icon(), 48, 1.5)\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n }\r\n <div class=\"cc__container\">\r\n <div class=\"cc__title-group\">\r\n @if (category()) { <p class=\"cc__category-eyebrow\">{{ category() }}</p> }\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__title', text: title() }\" /> }\r\n </div>\r\n @if (description()) { <p class=\"cc__desc\">{{ description() }}</p> }\r\n @if (headline()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__headline', text: headline() }\" /> }\r\n </div>\r\n </div>\r\n }\r\n }\r\n\r\n @case ('action') {\r\n @if (orientation() === 'vertical') {\r\n <div class=\"ac ac--vertical\" [class.ac--no-desc]=\"!description()\">\r\n @if (icon()) {\r\n <div class=\"ac__badge\" [innerHTML]=\"iconSvg(icon(), 24, 1.5)\" aria-hidden=\"true\"></div>\r\n }\r\n <div class=\"ac__text\">\r\n <ng-container [ngTemplateOutlet]=\"labelTpl\" />\r\n @if (description()) { <span class=\"ac__desc\">{{ description() }}</span> }\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"ac\" [class.ac--no-desc]=\"!description()\">\r\n @if (icon()) {\r\n <div class=\"ac__icon\" [innerHTML]=\"iconSvg(icon(), 32)\" aria-hidden=\"true\"></div>\r\n }\r\n <div class=\"ac__text\">\r\n <ng-container [ngTemplateOutlet]=\"labelTpl\" />\r\n @if (description()) { <span class=\"ac__desc\">{{ description() }}</span> }\r\n </div>\r\n </div>\r\n }\r\n }\r\n\r\n @case ('metric') {\r\n <div class=\"mc\" [class.mc--current]=\"current()\">\r\n <span class=\"mc__value\">{{ value() }}</span>\r\n @if (label()) { <span class=\"mc__label\">{{ label() }}</span> }\r\n </div>\r\n }\r\n\r\n @case ('basic') {\r\n @if (size() === 'xs') {\r\n <!-- xs base card (Figma 243:3264): compact horizontal row — icon · title · arrow. -->\r\n <div class=\"bc bc--row\">\r\n @if (icon()) {\r\n <div class=\"bc__icon\" [innerHTML]=\"iconSvg(icon(), 24, 1.5)\" aria-hidden=\"true\"></div>\r\n }\r\n <div class=\"bc__row-text\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n </div>\r\n @if (showArrow()) {\r\n <span class=\"bc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', 20)\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n } @else if (size() === 'lg') {\r\n <!-- lg base card (Figma 243:3205): text + optional chip, button bottom-right. -->\r\n <div class=\"bc bc--promo\" [class.bc--banner]=\"isBanner()\">\r\n <div class=\"bc__text\">\r\n @if (label()) { <p class=\"bc__eyebrow\">{{ label() }}</p> }\r\n @if (title() || subtitle()) {\r\n <div class=\"bc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"bc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"bc__desc\">{{ description() }}</p> }\r\n @if (category()) { <span class=\"bc__chip\">{{ category() }}</span> }\r\n </div>\r\n @if (actionLabel()) { <compsych-button [variant]=\"appearance() === 'filled' ? 'elevated' : 'outlined'\" [label]=\"actionLabel()\" [disabled]=\"actionDisabled()\" /> }\r\n </div>\r\n } @else if (isBanner()) {\r\n <!-- md banner base card (Figma 59442:12042): text over the image, arrow bottom-right. -->\r\n <div class=\"bc bc--banner\">\r\n <div class=\"bc__text\">\r\n @if (title() || subtitle()) {\r\n <div class=\"bc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"bc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"bc__desc\">{{ description() }}</p> }\r\n </div>\r\n @if (showArrow()) {\r\n <span class=\"bc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', basicArrowSize())\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n } @else {\r\n <!-- md / sm base card (Figma 243:3174, 243:3179, 947:8067):\r\n icon + arrow row, then eyebrow / title / subtitle / description / button. -->\r\n <div class=\"bc\">\r\n <div class=\"bc__top\">\r\n @if (icon()) {\r\n @if (appearance() === 'gradient') {\r\n <div class=\"bc__icon bc__icon--badge\" [innerHTML]=\"iconSvg(icon(), 32)\" aria-hidden=\"true\"></div>\r\n } @else {\r\n <div class=\"bc__icon\" [innerHTML]=\"iconSvg(icon(), basicIconSize(), 1.5)\" aria-hidden=\"true\"></div>\r\n }\r\n }\r\n <div class=\"bc__top-end\">\r\n @if (category()) { <span class=\"bc__chip\">{{ category() }}</span> }\r\n @if (showArrow()) {\r\n <span class=\"bc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', basicArrowSize())\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"bc__body\">\r\n <div class=\"bc__text\">\r\n @if (label()) { <p class=\"bc__eyebrow\">{{ label() }}</p> }\r\n @if (title() || subtitle()) {\r\n <div class=\"bc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"bc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"bc__desc\">{{ description() }}</p> }\r\n </div>\r\n @if (actionLabel()) { <compsych-button [variant]=\"appearance() === 'filled' ? 'elevated' : 'outlined'\" [label]=\"actionLabel()\" [disabled]=\"actionDisabled()\" /> }\r\n </div>\r\n </div>\r\n }\r\n }\r\n\r\n @default {\r\n <ng-content />\r\n }\r\n }\r\n</div>\r\n\r\n<!-- Heading fragment: renders <h2>–<h6> when headingLevel is set, else <p>.\r\n Pass $implicit = CSS class string, text = display text.\r\n Used by all title/headline slots across content and basic variants. -->\r\n<ng-template #headingTpl let-cls let-text=\"text\">\r\n @switch (effectiveHeadingLevel()) {\r\n @case (2) { <h2 [class]=\"cls\">{{ text }}</h2> }\r\n @case (3) { <h3 [class]=\"cls\">{{ text }}</h3> }\r\n @case (4) { <h4 [class]=\"cls\">{{ text }}</h4> }\r\n @case (5) { <h5 [class]=\"cls\">{{ text }}</h5> }\r\n @case (6) { <h6 [class]=\"cls\">{{ text }}</h6> }\r\n @default { <p [class]=\"cls\">{{ text }}</p> }\r\n }\r\n</ng-template>\r\n\r\n<!-- Action-card label: same heading-level logic but defaults to <span> (inline) not <p>. -->\r\n<ng-template #labelTpl>\r\n @switch (effectiveHeadingLevel()) {\r\n @case (2) { <h2 class=\"ac__label\">{{ label() }}</h2> }\r\n @case (3) { <h3 class=\"ac__label\">{{ label() }}</h3> }\r\n @case (4) { <h4 class=\"ac__label\">{{ label() }}</h4> }\r\n @case (5) { <h5 class=\"ac__label\">{{ label() }}</h5> }\r\n @case (6) { <h6 class=\"ac__label\">{{ label() }}</h6> }\r\n @default { <span class=\"ac__label\">{{ label() }}</span> }\r\n }\r\n</ng-template>\r\n"]}
|
|
1
|
+
{"version":3,"file":"card.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/card-base/card.ts","../../../../../../packages/angular/src/lib/card-base/card.html"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n computed,\r\n effect,\r\n inject,\r\n input,\r\n output,\r\n} from '@angular/core';\r\nimport { NgTemplateOutlet } from '@angular/common';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\nimport { RippleDirective } from './ripple.directive';\r\nimport { ButtonComponent } from '../button/button';\r\n\r\n/** A single metric item for the banner card's metric row. */\r\nexport interface BannerMetric {\r\n value: string | number;\r\n label?: string;\r\n /** Renders the metric in its primary-color \"current\" state. */\r\n current?: boolean;\r\n}\r\n\r\n/**\r\n * Card type — selects the projected/laid-out content. Mirrors the library's `variant` pattern.\r\n * `basic` is the Figma \"cards\" section (the \"Card Item\" family: icon + arrow +\r\n * title/subtitle, sized, across the outlined/filled/gradient/banner surfaces) —\r\n * distinct from the `content` category tiles.\r\n */\r\nexport type CardVariant = 'basic' | 'content' | 'action' | 'metric';\r\n/** Visual surface treatment. Mirrors the Figma \"Card Item\" Style property. */\r\nexport type CardAppearance = 'outlined' | 'filled' | 'gradient' | 'banner';\r\n/** Corner radius token: `md` (12px), `lg` (16px), `xl` (24px). */\r\nexport type CardRadius = 'md' | 'lg' | 'xl';\r\n/** Inner padding token (used by `variant=\"basic\"`; typed variants size themselves). */\r\nexport type CardPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl';\r\n/** Action-card layout. */\r\nexport type CardOrientation = 'horizontal' | 'vertical';\r\n/** Metric-card size. `medium` is the default (24px padding, headline-medium value); `small` is compact (16px padding, headline-small value). */\r\nexport type CardMetricSize = 'medium' | 'small';\r\n/** Base Card Item size (Figma \"Size\" property). Drives layout + type scale. */\r\nexport type CardSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';\r\n/** Heading level for the title / headline / action label (screen-reader heading semantics). */\r\nexport type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;\r\n/** How the content-variant category renders: trailing pill (`chip`) or leading plain-text `label`. */\r\nexport type CardCategoryStyle = 'chip' | 'label';\r\n/**\r\n * Content-variant icon badge treatment:\r\n * - `primary` — white icon on a filled primary (blue) circle (default).\r\n * - `neutral` — dark (on-surface) icon on a white circle with a thin outline.\r\n */\r\nexport type CardIconStyle = 'primary' | 'neutral';\r\n\r\n/**\r\n * Card — the single Cards component for the ComPsych Design System.\r\n *\r\n * Figma: Core Components — \"Cards\" (file VFBn7KCDy3FSIlvhNo3ylq, 1117:9829).\r\n *\r\n * One component, switched by `variant` (like Accordion's `variant`):\r\n * - `basic` — a bare surface; project your own content.\r\n * - `content` — icon badge / image, category chip, title, subtitle, description,\r\n * action pill, arrow; headline layout.\r\n * - `action` — compact interactive row (icon · label/description · arrow); horizontal/vertical.\r\n * - `metric` — centered value + label; `current` (selected) state.\r\n *\r\n * `appearance` is the orthogonal surface skin (outlined / filled / gradient / banner).\r\n * When `interactive`, the whole card is a button (role/tabindex/keyboard) with the\r\n * hover / focus / pressed ripple. All values are sys design tokens.\r\n *\r\n * <compsych-card variant=\"content\" appearance=\"gradient\" icon=\"circle-play\"\r\n * category=\"Video\" title=\"Mindful breathing\" actionLabel=\"Watch\" [interactive]=\"true\" />\r\n * <compsych-card variant=\"metric\" value=\"24\" label=\"Sessions\" [current]=\"true\" [interactive]=\"true\" />\r\n * <compsych-card>Projected content</compsych-card>\r\n */\r\n@Component({\r\n selector: 'compsych-card',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [NgTemplateOutlet, ButtonComponent, CardComponent],\r\n hostDirectives: [RippleDirective],\r\n templateUrl: './card.html',\r\n styleUrl: './card.css',\r\n host: {\r\n class: 'cmp-card',\r\n '[class.cmp-card--outlined]': \"appearance() === 'outlined'\",\r\n '[class.cmp-card--filled]': \"appearance() === 'filled'\",\r\n '[class.cmp-card--gradient]': \"appearance() === 'gradient'\",\r\n '[class.cmp-card--banner]': \"appearance() === 'banner'\",\r\n '[class.cmp-card--content]': \"variant() === 'content'\",\r\n '[class.cmp-card--basic]': \"variant() === 'basic'\",\r\n '[attr.data-card-size]': \"variant() === 'basic' ? size() : null\",\r\n '[class.cmp-card--metric]': \"variant() === 'metric'\",\r\n '[class.cmp-card--current]': \"variant() === 'metric' && current()\",\r\n '[class.cmp-card--radius-md]': \"radius() === 'md'\",\r\n '[class.cmp-card--radius-lg]': \"radius() === 'lg'\",\r\n '[class.cmp-card--radius-xl]': \"radius() === 'xl'\",\r\n '[class.cmp-card--pad-none]': \"effectivePadding() === 'none'\",\r\n '[class.cmp-card--pad-sm]': \"effectivePadding() === 'sm'\",\r\n '[class.cmp-card--pad-md]': \"effectivePadding() === 'md'\",\r\n '[class.cmp-card--pad-lg]': \"effectivePadding() === 'lg'\",\r\n '[class.cmp-card--pad-xl]': \"effectivePadding() === 'xl'\",\r\n '[style.width]': \"fullWidth() ? '100%' : null\",\r\n '[class.cmp-card--interactive]': 'interactive()',\r\n '[class.cmp-card--disabled]': 'disabled()',\r\n '[class.cmp-card--selected]': 'selected()',\r\n '[class.cmp-card--no-gradient]': '!showGradient()',\r\n '[class.cmp-card--compact]': 'compact()',\r\n '[class.cmp-card--metric-sm]': \"variant() === 'metric' && metricSize() === 'small'\",\r\n '[class.cmp-card--dark-bg]': 'darkBackground()',\r\n '[class.cmp-card--hide-image]': 'hideImage()',\r\n '[attr.role]': \"interactive() ? 'button' : null\",\r\n '[attr.tabindex]': 'interactive() && !disabled() ? 0 : null',\r\n '[attr.aria-label]': 'ariaLabel() ?? null',\r\n '[attr.aria-disabled]': 'disabled() || null',\r\n '[attr.aria-pressed]': \"interactive() && variant() === 'metric' ? current() : null\",\r\n '[style.border-color]': \"variant() === 'metric' && current() ? 'var(--accent-primary-sys-primary)' : null\",\r\n '[style.border-width]': \"variant() === 'metric' && current() ? 'var(--border-width-sys-stroke-thick)' : null\",\r\n '(click)': 'onActivate()',\r\n '(keydown)': 'onKeyDown($event)',\r\n },\r\n})\r\nexport class CardComponent {\r\n // ── Surface (all variants) ──────────────────────────────\r\n /** Card type. Defaults to `basic` (projected content). */\r\n readonly variant = input<CardVariant>('basic');\r\n /** Surface treatment. Defaults to `outlined`. */\r\n readonly appearance = input<CardAppearance>('outlined');\r\n /** Corner radius token. Defaults to `lg` (16px). */\r\n readonly radius = input<CardRadius>('lg');\r\n /** Inner padding for `variant=\"basic\"`. Typed variants size themselves. */\r\n readonly padding = input<CardPadding>('lg');\r\n /** Card Item size (`variant=\"basic\"`): xl/lg/md/sm/xs. Defaults to `md`. */\r\n readonly size = input<CardSize>('md');\r\n /** Whole-card click target: adds role/tabindex, hover/press states and ripple. */\r\n readonly interactive = input<boolean>(false);\r\n /** Disabled visual + non-interactive. */\r\n readonly disabled = input<boolean>(false);\r\n /** Applies the focused (blue outline) appearance. Useful for action cards to indicate selection. */\r\n readonly selected = input<boolean>(false);\r\n /** Background image URL for `appearance=\"banner\"`. */\r\n readonly imageSrc = input<string>('');\r\n /**\r\n * Hides the background image on an `appearance=\"banner\"` card and replaces it\r\n * with a `--accent-primary-sys-primary-container` solid background.\r\n */\r\n readonly hideImage = input<boolean>(false);\r\n /**\r\n * `background-position` for the `banner` appearance media (e.g. `'85% 0'` for an\r\n * off-center image). Defaults to `'center'`.\r\n */\r\n readonly imagePosition = input<string>('center');\r\n\r\n // ── Content variant ─────────────────────────────────────\r\n /** Lucide icon name (content badge / action leading icon). */\r\n readonly icon = input<string>('');\r\n /** Content: category chip text. */\r\n readonly category = input<string>('');\r\n /**\r\n * Content: how the category renders — `'chip'` (default) as the trailing pill,\r\n * or `'label'` as a leading plain-text label beside the icon.\r\n */\r\n readonly categoryStyle = input<CardCategoryStyle>('chip');\r\n /**\r\n * Content: icon badge treatment — `'primary'` (default) is a white icon on a\r\n * filled primary (blue) circle; `'neutral'` is a dark icon on a white circle\r\n * with a thin outline.\r\n */\r\n readonly iconStyle = input<CardIconStyle>('primary');\r\n /** Content: title. */\r\n readonly title = input<string>('');\r\n /** Content: subtitle. */\r\n readonly subtitle = input<string>('');\r\n /** Content/action: supporting description. */\r\n readonly description = input<string>('');\r\n /** Content: large display text for the headline layout. */\r\n readonly headline = input<string>('');\r\n /** Content (`appearance=\"banner\"`): supporting text rendered below the headline, in white-80. */\r\n readonly headlineDescription = input<string>('');\r\n /** Content (`appearance=\"banner\"`): metric cards rendered in a dark row below the headline description. */\r\n readonly metrics = input<BannerMetric[]>([]);\r\n /** Content: presentational action pill label. */\r\n readonly actionLabel = input<string>('');\r\n /** Content: leading Lucide icon name rendered inside the action pill. */\r\n readonly actionLeadingIcon = input<string>('');\r\n /** Content: trailing Lucide icon name rendered inside the action pill. */\r\n readonly actionTrailingIcon = input<string>('');\r\n /** Content: disables the action button. */\r\n readonly actionDisabled = input<boolean>(false);\r\n /** Content (outlined/filled): when true, replaces the full-width banner with a plain icon in the top-left corner. */\r\n readonly inlineIcon = input<boolean>(false);\r\n /** Content: show the trailing arrow-up-right affordance. */\r\n readonly showArrow = input<boolean>(true);\r\n /** Content (gradient): show the inner gradient panel background. Set to `false` to render a flat surface. */\r\n readonly showGradient = input<boolean>(true);\r\n /** Content: remove the fixed min-height so the card sizes to its content. */\r\n readonly compact = input<boolean>(false);\r\n /**\r\n * Renders the card on a dark surface: background becomes\r\n * `--transparent-neutral-sys-white-20` (closest token to rgba(255,255,255,0.16))\r\n * and text color becomes white.\r\n */\r\n readonly darkBackground = input<boolean>(false);\r\n\r\n // ── Action variant ──────────────────────────────────────\r\n /** Action/metric: primary label. */\r\n readonly label = input<string>('');\r\n /** Action: trailing Lucide icon (horizontal layout). */\r\n readonly trailingIcon = input<string>('arrow-up-right');\r\n /** Action: layout orientation. */\r\n readonly orientation = input<CardOrientation>('horizontal');\r\n /** Action/basic: stretch the card to 100% of its container width. */\r\n readonly fullWidth = input<boolean>(false);\r\n\r\n // ── Metric variant ──────────────────────────────────────\r\n /** Metric: size variant. Defaults to `medium`. */\r\n readonly metricSize = input<CardMetricSize>('medium');\r\n /** Metric: the prominent value. */\r\n readonly value = input<string | number>('');\r\n /** Metric: selected/current state (primary border + text). */\r\n readonly current = input<boolean>(false);\r\n\r\n // ── Accessibility ───────────────────────────────────────\r\n /**\r\n * Opts the title / headline / action label into a heading in the\r\n * accessibility tree via `role=\"heading\"` + `aria-level`, so consumers that\r\n * replace real headings keep heading semantics and screen-reader heading\r\n * navigation (WCAG 1.3.1). Omit (default) to render as plain text — no visual\r\n * or behavioral change for existing usages.\r\n */\r\n readonly headingLevel = input<CardHeadingLevel | undefined>(undefined);\r\n /**\r\n * Accessible name for an `interactive` card (which is a `role=\"button\"`).\r\n * Required for image-only / icon-only interactive cards that have no text, and\r\n * useful to give text-heavy cards a concise name instead of their full content.\r\n * Bind as `aria-label`.\r\n */\r\n readonly ariaLabel = input<string | undefined>(undefined, { alias: 'aria-label' });\r\n\r\n /** Emits on click / Enter / Space when interactive and not disabled. */\r\n readonly cardClick = output<void>();\r\n /** Emits when the banner card's action button is clicked. */\r\n readonly actionClick = output<void>();\r\n\r\n private readonly sanitizer = inject(DomSanitizer);\r\n private readonly ripple = inject(RippleDirective);\r\n\r\n readonly isBanner = computed(() => this.appearance() === 'banner');\r\n\r\n /**\r\n * Heading level actually applied. Suppressed when the whole card is interactive\r\n * (`role=\"button\"`), since a heading nested in a button isn't exposed as a\r\n * heading — the two would conflict.\r\n */\r\n readonly effectiveHeadingLevel = computed(() =>\r\n this.interactive() ? undefined : this.headingLevel(),\r\n );\r\n\r\n /** Base Card Item plain-icon glyph size by `size` (48 md, 32 sm, 24 xs). */\r\n readonly basicIconSize = computed(() => (this.size() === 'sm' ? 32 : this.size() === 'xs' ? 24 : 48));\r\n /** Base Card Item arrow glyph size by `size` (24 md, 20 sm/xs). */\r\n readonly basicArrowSize = computed(() => (this.size() === 'md' ? 24 : 20));\r\n\r\n /** CSS `url(...)` for the media layer, or null when no image is set. */\r\n readonly mediaUrl = computed(() => {\r\n const src = this.imageSrc();\r\n return src ? `url(\"${src}\")` : null;\r\n });\r\n\r\n /** Padding resolved per variant; `basic` honors the `padding` input. */\r\n readonly effectivePadding = computed<CardPadding>(() => {\r\n switch (this.variant()) {\r\n case 'content':\r\n return this.isBanner() ? 'none' : 'sm';\r\n case 'action':\r\n return 'lg'; // 24px — Figma action card (60017:8036)\r\n case 'basic':\r\n return 'none'; // .bc controls its own padding per style (Figma Base Card Item)\r\n case 'metric':\r\n return this.metricSize() === 'small' ? 'sm' : 'lg'; // sm=16px, lg=24px\r\n default:\r\n return this.padding();\r\n }\r\n });\r\n\r\n constructor() {\r\n effect(() => {\r\n this.ripple.suppressed = !this.interactive() || this.disabled();\r\n });\r\n }\r\n\r\n onActivate(): void {\r\n if (!this.interactive() || this.disabled()) return;\r\n this.cardClick.emit();\r\n }\r\n\r\n onKeyDown(event: KeyboardEvent): void {\r\n if (!this.interactive() || this.disabled()) return;\r\n if (event.repeat) return;\r\n if (event.key !== 'Enter' && event.key !== ' ') return;\r\n event.preventDefault();\r\n this.cardClick.emit();\r\n }\r\n\r\n /** Renders a sized/weighted Lucide SVG for template use. */\r\n iconSvg(name: string, size: number, strokeWidth = 1.75): SafeHtml {\r\n let svg = LUCIDE_ICONS[name] ?? '';\r\n svg = svg.replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n if (strokeWidth !== 1.5) {\r\n svg = svg.replace('stroke-width=\"1.5\"', `stroke-width=\"${strokeWidth}\"`);\r\n }\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n}\r\n","@if (isBanner() && mediaUrl() && !hideImage()) {\r\n <div class=\"cmp-card__media\" [style.background-image]=\"mediaUrl()\" [style.background-position]=\"imagePosition()\"></div>\r\n <div class=\"cmp-card__scrim\" aria-hidden=\"true\"></div>\r\n}\r\n\r\n<div class=\"cmp-card__body\">\r\n @switch (variant()) {\r\n @case ('content') {\r\n @if (isBanner()) {\r\n <div class=\"cc cc--banner\">\r\n <div class=\"cc__img-top\">\r\n @if (icon()) {\r\n <div class=\"cc__media-icon\" [innerHTML]=\"iconSvg(icon(), 48)\" aria-hidden=\"true\"></div>\r\n }\r\n @if (category()) { <span class=\"cc__chip cc__chip--glass\">{{ category() }}</span> }\r\n </div>\r\n <div class=\"cc__img-foot\">\r\n @if (title() || subtitle() || headline() || description() || headlineDescription() || metrics().length) {\r\n <div class=\"cc__banner-text\">\r\n @if (title() || subtitle()) {\r\n <div class=\"cc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"cc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"cc__desc\">{{ description() }}</p> }\r\n @if (headline()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__headline', text: headline() }\" /> }\r\n @if (headlineDescription()) { <p class=\"cc__headline-desc\" [innerHTML]=\"headlineDescription()\"></p> }\r\n @if (metrics().length) {\r\n <div class=\"cc__metrics\">\r\n @for (m of metrics(); track $index) {\r\n <compsych-card variant=\"metric\" [value]=\"m.value\" [label]=\"m.label ?? ''\" [current]=\"m.current ?? false\" [darkBackground]=\"true\" metricSize=\"small\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n @if (actionLabel()) {\r\n <compsych-button variant=\"elevated\" [label]=\"actionLabel()\" [leadingIcon]=\"actionLeadingIcon()\" [trailingIcon]=\"actionTrailingIcon()\" [disabled]=\"actionDisabled()\" (click)=\"actionClick.emit(); $event.stopPropagation()\" />\r\n }\r\n </div>\r\n </div>\r\n } @else if (appearance() === 'gradient') {\r\n <div class=\"cc\">\r\n <div class=\"cc__top\" [class.cc__top--label]=\"categoryStyle() === 'label'\">\r\n @if (icon()) {\r\n <div class=\"cc__icon\" [class.cc__icon--neutral]=\"iconStyle() === 'neutral'\" [innerHTML]=\"iconSvg(icon(), 24)\" aria-hidden=\"true\"></div>\r\n }\r\n @if (categoryStyle() === 'label' && category()) {\r\n <span class=\"cc__category-label\">{{ category() }}</span>\r\n }\r\n <div class=\"cc__top-end\">\r\n @if (categoryStyle() === 'chip' && category()) { <span class=\"cc__chip\">{{ category() }}</span> }\r\n @if (showArrow()) {\r\n <span class=\"cc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', 24)\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"cc__text\">\r\n @if (title() || subtitle()) {\r\n <div class=\"cc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"cc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"cc__desc\">{{ description() }}</p> }\r\n </div>\r\n @if (actionLabel()) {\r\n <compsych-button variant=\"outlined\" [label]=\"actionLabel()\" [leadingIcon]=\"actionLeadingIcon()\" [trailingIcon]=\"actionTrailingIcon()\" [disabled]=\"actionDisabled()\" />\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"cc cc--with-icon\" [class.cc--filled]=\"appearance() === 'filled'\" [class.cc--inline-icon]=\"inlineIcon()\">\r\n @if (inlineIcon()) {\r\n @if (icon()) {\r\n <span class=\"cc__banner-icon\" [innerHTML]=\"iconSvg(icon(), 32, 1.5)\" aria-hidden=\"true\"></span>\r\n }\r\n } @else {\r\n <div class=\"cc__banner\">\r\n @if (icon()) {\r\n <span class=\"cc__banner-icon\" [innerHTML]=\"iconSvg(icon(), 48, 1.5)\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n }\r\n <div class=\"cc__container\">\r\n <div class=\"cc__title-group\">\r\n @if (category()) { <p class=\"cc__category-eyebrow\">{{ category() }}</p> }\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__title', text: title() }\" /> }\r\n </div>\r\n @if (description()) { <p class=\"cc__desc\">{{ description() }}</p> }\r\n @if (headline()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'cc__headline', text: headline() }\" /> }\r\n </div>\r\n </div>\r\n }\r\n }\r\n\r\n @case ('action') {\r\n @if (orientation() === 'vertical') {\r\n <div class=\"ac ac--vertical\" [class.ac--no-desc]=\"!description()\">\r\n @if (icon()) {\r\n <div class=\"ac__badge\" [innerHTML]=\"iconSvg(icon(), 24, 1.5)\" aria-hidden=\"true\"></div>\r\n }\r\n <div class=\"ac__text\">\r\n <ng-container [ngTemplateOutlet]=\"labelTpl\" />\r\n @if (description()) { <span class=\"ac__desc\">{{ description() }}</span> }\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"ac\" [class.ac--no-desc]=\"!description()\">\r\n @if (icon()) {\r\n <div class=\"ac__icon\" [innerHTML]=\"iconSvg(icon(), 32)\" aria-hidden=\"true\"></div>\r\n }\r\n <div class=\"ac__text\">\r\n <ng-container [ngTemplateOutlet]=\"labelTpl\" />\r\n @if (description()) { <span class=\"ac__desc\">{{ description() }}</span> }\r\n </div>\r\n </div>\r\n }\r\n }\r\n\r\n @case ('metric') {\r\n <div class=\"mc\" [class.mc--current]=\"current()\">\r\n <span class=\"mc__value\">{{ value() }}</span>\r\n @if (label()) { <span class=\"mc__label\">{{ label() }}</span> }\r\n </div>\r\n }\r\n\r\n @case ('basic') {\r\n @if (size() === 'xs') {\r\n <!-- xs base card (Figma 243:3264): compact horizontal row — icon · title · arrow. -->\r\n <div class=\"bc bc--row\">\r\n @if (icon()) {\r\n <div class=\"bc__icon\" [innerHTML]=\"iconSvg(icon(), 24, 1.5)\" aria-hidden=\"true\"></div>\r\n }\r\n <div class=\"bc__row-text\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n </div>\r\n @if (showArrow()) {\r\n <span class=\"bc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', 20)\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n } @else if (size() === 'lg') {\r\n <!-- lg base card (Figma 243:3205): text + optional chip, button bottom-right. -->\r\n <div class=\"bc bc--promo\" [class.bc--banner]=\"isBanner()\">\r\n <div class=\"bc__text\">\r\n @if (label()) { <p class=\"bc__eyebrow\">{{ label() }}</p> }\r\n @if (title() || subtitle()) {\r\n <div class=\"bc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"bc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"bc__desc\">{{ description() }}</p> }\r\n @if (category()) { <span class=\"bc__chip\">{{ category() }}</span> }\r\n </div>\r\n @if (actionLabel()) { <compsych-button [variant]=\"appearance() === 'filled' ? 'elevated' : 'outlined'\" [label]=\"actionLabel()\" [disabled]=\"actionDisabled()\" (click)=\"actionClick.emit(); $event.stopPropagation()\" /> }\r\n </div>\r\n } @else if (isBanner()) {\r\n <!-- md banner base card (Figma 59442:12042): text over the image, arrow bottom-right. -->\r\n <div class=\"bc bc--banner\">\r\n <div class=\"bc__text\">\r\n @if (title() || subtitle()) {\r\n <div class=\"bc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"bc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"bc__desc\">{{ description() }}</p> }\r\n </div>\r\n @if (showArrow()) {\r\n <span class=\"bc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', basicArrowSize())\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n } @else {\r\n <!-- md / sm base card (Figma 243:3174, 243:3179, 947:8067):\r\n icon + arrow row, then eyebrow / title / subtitle / description / button. -->\r\n <div class=\"bc\">\r\n <div class=\"bc__top\">\r\n @if (icon()) {\r\n @if (appearance() === 'gradient') {\r\n <div class=\"bc__icon bc__icon--badge\" [innerHTML]=\"iconSvg(icon(), 32)\" aria-hidden=\"true\"></div>\r\n } @else {\r\n <div class=\"bc__icon\" [innerHTML]=\"iconSvg(icon(), basicIconSize(), 1.5)\" aria-hidden=\"true\"></div>\r\n }\r\n }\r\n <div class=\"bc__top-end\">\r\n @if (category()) { <span class=\"bc__chip\">{{ category() }}</span> }\r\n @if (showArrow()) {\r\n <span class=\"bc__arrow\" [innerHTML]=\"iconSvg('arrow-up-right', basicArrowSize())\" aria-hidden=\"true\"></span>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"bc__body\">\r\n <div class=\"bc__text\">\r\n @if (label()) { <p class=\"bc__eyebrow\">{{ label() }}</p> }\r\n @if (title() || subtitle()) {\r\n <div class=\"bc__heading\">\r\n @if (title()) { <ng-container [ngTemplateOutlet]=\"headingTpl\" [ngTemplateOutletContext]=\"{ $implicit: 'bc__title', text: title() }\" /> }\r\n @if (subtitle()) { <p class=\"bc__subtitle\">{{ subtitle() }}</p> }\r\n </div>\r\n }\r\n @if (description()) { <p class=\"bc__desc\">{{ description() }}</p> }\r\n </div>\r\n @if (actionLabel()) { <compsych-button [variant]=\"appearance() === 'filled' ? 'elevated' : 'outlined'\" [label]=\"actionLabel()\" [disabled]=\"actionDisabled()\" (click)=\"actionClick.emit(); $event.stopPropagation()\" /> }\r\n </div>\r\n </div>\r\n }\r\n }\r\n\r\n @default {\r\n <ng-content />\r\n }\r\n }\r\n</div>\r\n\r\n<!-- Heading fragment: renders <h2>–<h6> when headingLevel is set, else <p>.\r\n Pass $implicit = CSS class string, text = display text.\r\n Used by all title/headline slots across content and basic variants. -->\r\n<ng-template #headingTpl let-cls let-text=\"text\">\r\n @switch (effectiveHeadingLevel()) {\r\n @case (2) { <h2 [class]=\"cls\">{{ text }}</h2> }\r\n @case (3) { <h3 [class]=\"cls\">{{ text }}</h3> }\r\n @case (4) { <h4 [class]=\"cls\">{{ text }}</h4> }\r\n @case (5) { <h5 [class]=\"cls\">{{ text }}</h5> }\r\n @case (6) { <h6 [class]=\"cls\">{{ text }}</h6> }\r\n @default { <p [class]=\"cls\">{{ text }}</p> }\r\n }\r\n</ng-template>\r\n\r\n<!-- Action-card label: same heading-level logic but defaults to <span> (inline) not <p>. -->\r\n<ng-template #labelTpl>\r\n @switch (effectiveHeadingLevel()) {\r\n @case (2) { <h2 class=\"ac__label\">{{ label() }}</h2> }\r\n @case (3) { <h3 class=\"ac__label\">{{ label() }}</h3> }\r\n @case (4) { <h4 class=\"ac__label\">{{ label() }}</h4> }\r\n @case (5) { <h5 class=\"ac__label\">{{ label() }}</h5> }\r\n @case (6) { <h6 class=\"ac__label\">{{ label() }}</h6> }\r\n @default { <span class=\"ac__label\">{{ label() }}</span> }\r\n }\r\n</ng-template>\r\n"]}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, } from '@angular/core';
|
|
2
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
3
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
4
|
+
import { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';
|
|
5
|
+
import { RippleDirective } from '../card-base/ripple.directive';
|
|
6
|
+
import * as i0 from "@angular/core";
|
|
7
|
+
import * as i1 from "../card-base/ripple.directive";
|
|
8
|
+
/**
|
|
9
|
+
* Iconography stroke scale (Figma Foundation → Tokens → Iconography, node
|
|
10
|
+
* 5860-11367): every icon size pairs with a stroke width so optical weight stays
|
|
11
|
+
* consistent as the glyph scales — 12/16 → 1, 20/24 → 1.5, 32+ → 2. Thresholds
|
|
12
|
+
* rather than a lookup so in-between sizes still resolve sensibly.
|
|
13
|
+
*/
|
|
14
|
+
function iconStrokeWidth(size) {
|
|
15
|
+
if (size >= 28)
|
|
16
|
+
return 2;
|
|
17
|
+
if (size >= 18)
|
|
18
|
+
return 1.5;
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Lucide glyphs are drawn on a fixed 24×24 viewBox, so a `stroke-width` attribute
|
|
23
|
+
* is in *user units* and shrinks with the rendered size — a 1 at 16px paints a
|
|
24
|
+
* 0.67px line. The DS stroke scale above is defined in ABSOLUTE px (the reference
|
|
25
|
+
* implementation switches on Lucide's `absoluteStrokeWidth`), so the attribute is
|
|
26
|
+
* scaled back up by 24/size to land on a true 1px / 1.5px line.
|
|
27
|
+
*/
|
|
28
|
+
function absoluteStrokeAttr(size) {
|
|
29
|
+
return (iconStrokeWidth(size) * 24) / size;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Chip — Figma "Chip" component set (Core Components › Molecules, node 14:942).
|
|
33
|
+
*
|
|
34
|
+
* Three axes: Style (Tonal / Outlined / Filled) × Usage (Neutral / Informative /
|
|
35
|
+
* Positive / Danger / Warning, plus Inverse on Tonal only) × Size (sm / md / lg /
|
|
36
|
+
* xl = 20 / 24 / 32 / 40). Booleans: leading icon, dismissible, badge — all off by
|
|
37
|
+
* default, as in Figma.
|
|
38
|
+
*
|
|
39
|
+
* Chips are INTERACTIVE BY DEFAULT (ruled 2026-09-02, Jean — the Material 3
|
|
40
|
+
* convention: every chip is something you press; a read-only status marker is a
|
|
41
|
+
* Badge). The icon + label render inside a real `<button>` whose hit area is the
|
|
42
|
+
* whole pill; `(click)` on the host is the chip's action. The interaction recipe is
|
|
43
|
+
* Button's (ruled 2026-08-24): the chip's own foreground as an 8% state layer,
|
|
44
|
+
* 120ms hover-in / 240ms release, the level-2 elevation lift, the shared press
|
|
45
|
+
* ripple, and the Focus Ring drawn around the pill. The Figma set authors no State
|
|
46
|
+
* variants — recorded as a reconciliation; the recipe follows them the day they
|
|
47
|
+
* exist. `[interactive]="false"` opts a chip out (a display-only tag inside another
|
|
48
|
+
* control) — then the host is a plain pill with no role.
|
|
49
|
+
*
|
|
50
|
+
* The dismiss affordance is a SIBLING button, never nested in the primary one
|
|
51
|
+
* (nested interactive controls are invalid); it emits `dismiss` and stops its click
|
|
52
|
+
* at the host so the chip's action does not fire. It is ALWAYS the outermost
|
|
53
|
+
* element — icon, label, badge, then the x (ruled 2026-09-02, Jean; the Figma file
|
|
54
|
+
* orders the badge after the Close Button). With a trailing control the pill's end
|
|
55
|
+
* padding tightens so the control sits concentric with the pill's end, and a
|
|
56
|
+
* leading icon mirrors that inset at the start (ruled 2026-09-02; the file authors
|
|
57
|
+
* symmetric padding).
|
|
58
|
+
*
|
|
59
|
+
* Colour recipe (usage → sys tokens; neutral maps to the surface family):
|
|
60
|
+
* tonal bg `<usage>`-container fg on-`<usage>`-container
|
|
61
|
+
* filled bg `<usage>`, white content (neutral: plain white pill — Figma
|
|
62
|
+
* authors a level-2 elevation on it; ruled 2026-08-24 that chips carry
|
|
63
|
+
* no resting shadow, Figma queued to update)
|
|
64
|
+
* outlined border `<usage>`-container, label `<usage>`, icon = label (neutral:
|
|
65
|
+
* outline / on-surface). EXCEPT warning: label and icon take
|
|
66
|
+
* on-warning-container — an accessibility exception ruled 2026-09-02
|
|
67
|
+
* (sys-warning on white is 3.09:1).
|
|
68
|
+
* inverse transparent-white-20 + backdrop blur, white content — for placement
|
|
69
|
+
* on photos. Tonal only, per the Figma set.
|
|
70
|
+
*
|
|
71
|
+
* Figma reconciliations this implementation carries (flagged to design, majority
|
|
72
|
+
* pattern followed instead):
|
|
73
|
+
* - Outlined/Informative binds primary-family colours in the file where its three
|
|
74
|
+
* status siblings all use their own family. Implemented info-family.
|
|
75
|
+
* - The Warning badge count binds on-surface in the file where every other usage
|
|
76
|
+
* uses the usage foreground. Implemented as the usage foreground.
|
|
77
|
+
* - The counter inverts on filled-status and inverse chips for AA (see chip.css).
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```html
|
|
81
|
+
* <compsych-chip label="Active" variant="tonal" usage="positive" size="md" />
|
|
82
|
+
* <compsych-chip label="Filter" leadingIcon="tag" [dismissible]="true"
|
|
83
|
+
* (dismiss)="remove()" (click)="apply()" />
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export class ChipComponent {
|
|
87
|
+
/** Figma "Style". Defaults to Tonal, as the set does. */
|
|
88
|
+
variant = input('tonal', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
89
|
+
/** Figma "Usage". `inverse` pairs with the tonal style only. */
|
|
90
|
+
usage = input('neutral', ...(ngDevMode ? [{ debugName: "usage" }] : /* istanbul ignore next */ []));
|
|
91
|
+
size = input('sm', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
92
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
93
|
+
/** Lucide icon name for the leading slot (Figma "Leading Icon" + swap). */
|
|
94
|
+
leadingIcon = input(...(ngDevMode ? [undefined, { debugName: "leadingIcon" }] : /* istanbul ignore next */ []));
|
|
95
|
+
/** Shows the dismiss affordance (Figma "Dismissible"). */
|
|
96
|
+
dismissible = input(false, ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
|
|
97
|
+
/** Badge count (Figma "Badge"). Undefined = no badge. */
|
|
98
|
+
badge = input(...(ngDevMode ? [undefined, { debugName: "badge" }] : /* istanbul ignore next */ []));
|
|
99
|
+
/** Accessible name for the dismiss button. */
|
|
100
|
+
dismissLabel = input('Remove', ...(ngDevMode ? [{ debugName: "dismissLabel" }] : /* istanbul ignore next */ []));
|
|
101
|
+
/**
|
|
102
|
+
* Chips are interactive by default (ruled 2026-09-02): icon + label sit in a real
|
|
103
|
+
* button and `(click)` on the host is the action. `false` renders a display-only
|
|
104
|
+
* pill — no role, no state layer, no ripple.
|
|
105
|
+
*/
|
|
106
|
+
interactive = input(true, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
|
|
107
|
+
/** Disables the primary button (interactive chips only). */
|
|
108
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
109
|
+
/** Emitted by the dismiss button. The host app removes the chip. */
|
|
110
|
+
dismiss = output();
|
|
111
|
+
sanitizer = inject(DomSanitizer);
|
|
112
|
+
ripple = inject(RippleDirective);
|
|
113
|
+
static nextId = 0;
|
|
114
|
+
/** Links the primary button to its counter (aria-describedby). */
|
|
115
|
+
badgeId = `cmp-chip-badge-${ChipComponent.nextId++}`;
|
|
116
|
+
constructor() {
|
|
117
|
+
// Ripple only on live chips — static or disabled chips stay inert.
|
|
118
|
+
effect(() => {
|
|
119
|
+
this.ripple.suppressed = !this.interactive() || this.disabled();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Leading icon px per size (Figma: 16 at sm/md, 20 at lg/xl). */
|
|
123
|
+
iconPx = computed(() => (this.size() === 'sm' || this.size() === 'md' ? 16 : 20), ...(ngDevMode ? [{ debugName: "iconPx" }] : /* istanbul ignore next */ []));
|
|
124
|
+
/** Dismiss "x" px per size (Figma close slot: 12 at sm/md, 16 at lg/xl). */
|
|
125
|
+
xPx = computed(() => (this.size() === 'sm' || this.size() === 'md' ? 12 : 16), ...(ngDevMode ? [{ debugName: "xPx" }] : /* istanbul ignore next */ []));
|
|
126
|
+
leadingIconSvg = computed(() => {
|
|
127
|
+
const name = this.leadingIcon();
|
|
128
|
+
return name ? this.iconSvg(name, this.iconPx()) : null;
|
|
129
|
+
}, ...(ngDevMode ? [{ debugName: "leadingIconSvg" }] : /* istanbul ignore next */ []));
|
|
130
|
+
dismissIconSvg = computed(() => this.iconSvg('x', this.xPx()), ...(ngDevMode ? [{ debugName: "dismissIconSvg" }] : /* istanbul ignore next */ []));
|
|
131
|
+
/** Renders a sized Lucide glyph at the DS absolute stroke weight for that size. */
|
|
132
|
+
iconSvg(name, size) {
|
|
133
|
+
let svg = LUCIDE_ICONS[name] ?? '';
|
|
134
|
+
svg = svg.replace('<svg ', `<svg width="${size}" height="${size}" `);
|
|
135
|
+
svg = svg.replace('stroke-width="1.5"', `stroke-width="${absoluteStrokeAttr(size)}"`);
|
|
136
|
+
return this.sanitizer.bypassSecurityTrustHtml(svg);
|
|
137
|
+
}
|
|
138
|
+
/** Dismiss is its own action — it must not also fire the chip's. */
|
|
139
|
+
onDismiss(event) {
|
|
140
|
+
event.stopPropagation();
|
|
141
|
+
this.dismiss.emit();
|
|
142
|
+
}
|
|
143
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
144
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ChipComponent, isStandalone: true, selector: "compsych-chip", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, usage: { classPropertyName: "usage", publicName: "usage", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, leadingIcon: { classPropertyName: "leadingIcon", publicName: "leadingIcon", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, dismissLabel: { classPropertyName: "dismissLabel", publicName: "dismissLabel", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { properties: { "class.cmp-chip--tonal": "variant() === 'tonal'", "class.cmp-chip--outlined": "variant() === 'outlined'", "class.cmp-chip--filled": "variant() === 'filled'", "class.cmp-chip--neutral": "usage() === 'neutral'", "class.cmp-chip--informative": "usage() === 'informative'", "class.cmp-chip--positive": "usage() === 'positive'", "class.cmp-chip--danger": "usage() === 'danger'", "class.cmp-chip--warning": "usage() === 'warning'", "class.cmp-chip--inverse": "usage() === 'inverse'", "class.cmp-chip--md": "size() === 'md'", "class.cmp-chip--lg": "size() === 'lg'", "class.cmp-chip--xl": "size() === 'xl'", "class.cmp-chip--interactive": "interactive()", "class.cmp-chip--dismissible": "dismissible()", "class.cmp-chip--has-icon": "!!leadingIcon()", "class.cmp-chip--has-badge": "badge() !== undefined" }, classAttribute: "cmp-chip" }, hostDirectives: [{ directive: i1.RippleDirective }], ngImport: i0, template: "<!-- Icon + label \u2014 one template, rendered inside the primary button on an\r\n interactive chip or bare on a static one. -->\r\n<ng-template #content>\r\n @if (leadingIconSvg(); as icon) {\r\n <span class=\"cmp-chip__icon\" [innerHTML]=\"icon\" aria-hidden=\"true\"></span>\r\n }\r\n <span class=\"cmp-chip__label\">{{ label() }}</span>\r\n</ng-template>\r\n\r\n@if (interactive()) {\r\n <!-- The chip's action. Its ::before stretches over the whole pill so the hit\r\n area is the chip, not the label; the dismiss button sits above it. -->\r\n <button\r\n type=\"button\"\r\n class=\"cmp-chip__primary\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-describedby]=\"badge() !== undefined ? badgeId : null\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n </button>\r\n} @else {\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n}\r\n\r\n@if (badge() !== undefined) {\r\n <!-- The counter. Retoned per style/usage through the --chip-badge-* hooks in\r\n chip.css, mirroring the way Figma overrides the Badge instance fill.\r\n Named by the primary button's aria-describedby. -->\r\n <span class=\"cmp-chip__badge\" [id]=\"badgeId\">{{ badge() }}</span>\r\n}\r\n\r\n<!-- Dismiss is ALWAYS the outermost element (ruled 2026-09-02, Jean). The Figma\r\n file orders icon, label, Close Button, Badge \u2014 recorded as a reconciliation. -->\r\n@if (dismissible()) {\r\n <!-- Disabled with the chip. The reference bundle leaves this control live on a\r\n disabled chip, which makes the dimmed pill misrepresent its state (4.1.2)\r\n and puts the dimmed \"x\" under the 3:1 non-text contrast floor without the\r\n inactive-component exemption (1.4.11). Flagged upstream. -->\r\n <button\r\n type=\"button\"\r\n class=\"cmp-chip__dismiss\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"dismissLabel()\"\r\n (click)=\"onDismiss($event)\"\r\n >\r\n <span class=\"cmp-chip__x\" [innerHTML]=\"dismissIconSvg()\" aria-hidden=\"true\"></span>\r\n </button>\r\n}\r\n", styles: [":host{--chip-bg: var(--surface-surface-container-sys-surface-container-high);--chip-fg: var(--surface-surface-sys-on-surface);--chip-icon: var(--chip-fg);--chip-border: transparent;--chip-ctl-bg: var(--transparent-neutral-sys-white-50);--chip-ctl-fg: var(--chip-fg);--chip-x-color: var(--surface-surface-sys-on-surface-variant);--chip-h: 20px;--chip-px: var(--spacing-padding-sys-padding-8);--chip-gap: var(--spacing-padding-sys-padding-4);--chip-font-size: var(--label-small-sys-font-size);--chip-line-height: var(--label-small-sys-line-height);--chip-tracking: var(--label-small-sys-tracking);--chip-font-weight: var(--label-small-sys-font-weight);--chip-ctl-size: 16px;--chip-icon-size: 16px;--chip-badge-bg: var(--chip-ctl-bg);--chip-badge-fg: var(--chip-ctl-fg);--chip-badge-px: var(--spacing-padding-sys-padding-4);--chip-badge-font-size: var(--label-small-sys-font-size);--chip-badge-line-height: var(--label-small-sys-line-height);--chip-badge-tracking: var(--label-small-sys-tracking);--chip-badge-font-weight: var(--label-small-sys-font-weight-emphasized);--chip-px-end: max( var(--spacing-padding-sys-padding-4), calc((var(--chip-h) - var(--chip-ctl-size)) / 2) );--chip-px-start: max( var(--spacing-padding-sys-padding-4), calc((var(--chip-h) - var(--chip-icon-size)) / 2) );display:inline-flex;align-items:center;gap:var(--chip-gap);box-sizing:border-box;height:var(--chip-h);padding-inline:calc(var(--chip-px) - var(--border-width-sys-stroke-thin));position:relative;background:var(--chip-bg);color:var(--chip-fg);border:var(--border-width-sys-stroke-thin) solid var(--chip-border);border-radius:var(--border-radius-sys-radius-full);font-family:var(--label-small-sys-font-family);font-size:var(--chip-font-size);line-height:var(--chip-line-height);letter-spacing:var(--chip-tracking);font-weight:var(--chip-font-weight);white-space:nowrap}.cmp-chip__icon{display:inline-flex;flex-shrink:0;color:var(--chip-icon);line-height:0}.cmp-chip__icon svg,.cmp-chip__x svg{display:block}.cmp-chip__x{display:inline-flex;align-items:center;justify-content:center;line-height:0}.cmp-chip__label{min-width:0;overflow:hidden;text-overflow:ellipsis}.cmp-chip__primary{display:inline-flex;align-items:center;flex:0 1 auto;min-width:0;gap:var(--chip-gap);margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;letter-spacing:inherit;cursor:pointer;-webkit-appearance:none;appearance:none}.cmp-chip__primary:before{content:\"\";position:absolute;inset:calc(var(--border-width-sys-stroke-thin) * -1);border-radius:var(--border-radius-sys-radius-full)}.cmp-chip__primary:focus-visible{outline:none}.cmp-chip__primary:disabled{cursor:not-allowed}:host(.cmp-chip--dismissible),:host(.cmp-chip--has-badge){padding-inline-end:calc(var(--chip-px-end) - var(--border-width-sys-stroke-thin))}:host(.cmp-chip--has-icon){padding-inline-start:calc(var(--chip-px-start) - var(--border-width-sys-stroke-thin))}.cmp-chip__dismiss{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--chip-ctl-size);height:var(--chip-ctl-size);padding:0;border:0;border-radius:var(--border-radius-sys-radius-full);background:var(--chip-ctl-bg);color:var(--chip-x-color);cursor:pointer;line-height:0;-webkit-appearance:none;appearance:none}.cmp-chip__dismiss:disabled{cursor:not-allowed}.cmp-chip__dismiss:focus-visible{outline:var(--border-width-sys-stroke-medium) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thin)}.cmp-chip__badge{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;box-sizing:border-box;min-width:var(--chip-ctl-size);height:var(--chip-ctl-size);max-width:100%;padding-inline:var(--chip-badge-px);border-radius:var(--border-radius-sys-radius-full);background:var(--chip-badge-bg);color:var(--chip-badge-fg);font-size:var(--chip-badge-font-size);line-height:var(--chip-badge-line-height);letter-spacing:var(--chip-badge-tracking);font-weight:var(--chip-badge-font-weight);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host(.cmp-chip--interactive){cursor:pointer;transition:box-shadow .12s ease-out}:host(.cmp-chip--interactive) .cmp-chip__dismiss{position:relative;z-index:1}:host(.cmp-chip--interactive:has(.cmp-chip__primary:disabled)){cursor:not-allowed;opacity:.64}:host(.cmp-chip--interactive:has(.cmp-chip__primary:disabled):hover){box-shadow:none}:host(.cmp-chip--interactive:has(.cmp-chip__primary:disabled):hover):after{opacity:0}:host(.cmp-chip--interactive):after{content:\"\";position:absolute;inset:calc(var(--border-width-sys-stroke-thin) * -1);border-radius:var(--border-radius-sys-radius-full);background:var(--chip-fg);opacity:0;pointer-events:none;transition:opacity .24s ease-out}:host(.cmp-chip--interactive:hover){box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}:host(.cmp-chip--interactive:hover):after{opacity:.08;transition-duration:.12s}:host(.cmp-chip--interactive:has(.cmp-chip__primary:focus-visible)){outline:var(--border-width-sys-stroke-medium) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thin)}:host(.cmp-chip--md){--chip-h: 24px;--chip-px: var(--spacing-padding-sys-padding-12);--chip-font-size: var(--label-medium-sys-font-size);--chip-line-height: var(--label-medium-sys-line-height);--chip-tracking: var(--label-medium-sys-tracking);--chip-font-weight: var(--label-medium-sys-font-weight)}:host(.cmp-chip--lg){--chip-h: 32px;--chip-px: var(--spacing-padding-sys-padding-16);--chip-gap: var(--spacing-padding-sys-padding-8);--chip-font-size: var(--label-medium-sys-font-size);--chip-line-height: var(--label-medium-sys-line-height);--chip-tracking: var(--label-medium-sys-tracking);--chip-font-weight: var(--label-medium-sys-font-weight);--chip-ctl-size: 20px;--chip-icon-size: 20px}:host(.cmp-chip--xl){--chip-h: 40px;--chip-px: var(--spacing-padding-sys-padding-16);--chip-gap: var(--spacing-padding-sys-padding-8);--chip-font-size: var(--label-large-sys-font-size);--chip-line-height: var(--label-large-sys-line-height);--chip-tracking: var(--label-large-sys-tracking);--chip-font-weight: var(--label-large-sys-font-weight);--chip-ctl-size: 24px;--chip-icon-size: 20px;--chip-badge-px: var(--spacing-padding-sys-padding-8);--chip-badge-font-size: var(--label-medium-sys-font-size);--chip-badge-line-height: var(--label-medium-sys-line-height);--chip-badge-tracking: var(--label-medium-sys-tracking);--chip-badge-font-weight: var(--label-medium-sys-font-weight-emphasized)}:host(.cmp-chip--tonal.cmp-chip--informative){--chip-bg: var(--custom-info-sys-info-container);--chip-fg: var(--custom-info-sys-on-info-container)}:host(.cmp-chip--tonal.cmp-chip--positive){--chip-bg: var(--custom-success-sys-success-container);--chip-fg: var(--custom-success-sys-on-success-container)}:host(.cmp-chip--tonal.cmp-chip--danger){--chip-bg: var(--error-sys-error-container);--chip-fg: var(--error-sys-on-error-container)}:host(.cmp-chip--tonal.cmp-chip--warning){--chip-bg: var(--custom-warning-sys-warning-container);--chip-fg: var(--custom-warning-sys-on-warning-container)}:host(.cmp-chip--inverse){--chip-bg: var(--transparent-neutral-sys-white-20);--chip-fg: var(--surface-inverse-sys-inverse-on-surface);--chip-ctl-bg: var(--transparent-neutral-sys-white-20);--chip-x-color: var(--surface-inverse-sys-inverse-on-surface);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}:host(.cmp-chip--filled.cmp-chip--neutral){--chip-bg: var(--surface-surface-container-sys-surface-container-lowest)}:host(.cmp-chip--filled.cmp-chip--informative),:host(.cmp-chip--filled.cmp-chip--positive),:host(.cmp-chip--filled.cmp-chip--danger),:host(.cmp-chip--filled.cmp-chip--warning){--chip-fg: var(--surface-inverse-sys-inverse-on-surface);--chip-ctl-bg: var(--transparent-neutral-sys-white-20);--chip-x-color: var(--surface-inverse-sys-inverse-on-surface)}:host(.cmp-chip--filled.cmp-chip--informative){--chip-bg: var(--custom-info-sys-info)}:host(.cmp-chip--filled.cmp-chip--positive){--chip-bg: var(--custom-success-sys-success)}:host(.cmp-chip--filled.cmp-chip--danger){--chip-bg: var(--error-sys-error)}:host(.cmp-chip--filled.cmp-chip--warning){--chip-bg: var(--custom-warning-sys-warning)}:host(.cmp-chip--filled.cmp-chip--informative) .cmp-chip__badge,:host(.cmp-chip--filled.cmp-chip--positive) .cmp-chip__badge,:host(.cmp-chip--filled.cmp-chip--danger) .cmp-chip__badge,:host(.cmp-chip--filled.cmp-chip--warning) .cmp-chip__badge{--chip-badge-bg: var(--surface-surface-container-sys-surface-container-lowest);--chip-badge-fg: var(--chip-bg)}:host(.cmp-chip--inverse) .cmp-chip__badge{--chip-badge-bg: var(--surface-surface-container-sys-surface-container-lowest);--chip-badge-fg: var(--surface-surface-sys-on-surface)}:host(.cmp-chip--filled.cmp-chip--neutral) .cmp-chip__badge{--chip-badge-bg: var(--surface-surface-container-sys-surface-container);--chip-badge-fg: var(--surface-surface-sys-on-surface)}:host(.cmp-chip--outlined){--chip-bg: transparent;--chip-border: var(--outline-sys-outline)}:host(.cmp-chip--outlined.cmp-chip--informative){--chip-border: var(--custom-info-sys-info-container);--chip-fg: var(--custom-info-sys-info)}:host(.cmp-chip--outlined.cmp-chip--positive){--chip-border: var(--custom-success-sys-success-container);--chip-fg: var(--custom-success-sys-success)}:host(.cmp-chip--outlined.cmp-chip--danger){--chip-border: var(--error-sys-error-container);--chip-fg: var(--error-sys-error)}:host(.cmp-chip--outlined.cmp-chip--warning){--chip-border: var(--custom-warning-sys-warning-container);--chip-fg: var(--custom-warning-sys-on-warning-container)}@media(forced-colors:active){:host(.cmp-chip--interactive:has(.cmp-chip__primary:focus-visible)),.cmp-chip__dismiss:focus-visible{outline:var(--border-width-sys-stroke-medium) solid Highlight;outline-offset:var(--border-width-sys-stroke-medium)}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
145
|
+
}
|
|
146
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChipComponent, decorators: [{
|
|
147
|
+
type: Component,
|
|
148
|
+
args: [{ selector: 'compsych-chip', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], hostDirectives: [RippleDirective], host: {
|
|
149
|
+
class: 'cmp-chip',
|
|
150
|
+
'[class.cmp-chip--tonal]': "variant() === 'tonal'",
|
|
151
|
+
'[class.cmp-chip--outlined]': "variant() === 'outlined'",
|
|
152
|
+
'[class.cmp-chip--filled]': "variant() === 'filled'",
|
|
153
|
+
'[class.cmp-chip--neutral]': "usage() === 'neutral'",
|
|
154
|
+
'[class.cmp-chip--informative]': "usage() === 'informative'",
|
|
155
|
+
'[class.cmp-chip--positive]': "usage() === 'positive'",
|
|
156
|
+
'[class.cmp-chip--danger]': "usage() === 'danger'",
|
|
157
|
+
'[class.cmp-chip--warning]': "usage() === 'warning'",
|
|
158
|
+
'[class.cmp-chip--inverse]': "usage() === 'inverse'",
|
|
159
|
+
'[class.cmp-chip--md]': "size() === 'md'",
|
|
160
|
+
'[class.cmp-chip--lg]': "size() === 'lg'",
|
|
161
|
+
'[class.cmp-chip--xl]': "size() === 'xl'",
|
|
162
|
+
'[class.cmp-chip--interactive]': 'interactive()',
|
|
163
|
+
'[class.cmp-chip--dismissible]': 'dismissible()',
|
|
164
|
+
'[class.cmp-chip--has-icon]': '!!leadingIcon()',
|
|
165
|
+
'[class.cmp-chip--has-badge]': 'badge() !== undefined',
|
|
166
|
+
}, template: "<!-- Icon + label \u2014 one template, rendered inside the primary button on an\r\n interactive chip or bare on a static one. -->\r\n<ng-template #content>\r\n @if (leadingIconSvg(); as icon) {\r\n <span class=\"cmp-chip__icon\" [innerHTML]=\"icon\" aria-hidden=\"true\"></span>\r\n }\r\n <span class=\"cmp-chip__label\">{{ label() }}</span>\r\n</ng-template>\r\n\r\n@if (interactive()) {\r\n <!-- The chip's action. Its ::before stretches over the whole pill so the hit\r\n area is the chip, not the label; the dismiss button sits above it. -->\r\n <button\r\n type=\"button\"\r\n class=\"cmp-chip__primary\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-describedby]=\"badge() !== undefined ? badgeId : null\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n </button>\r\n} @else {\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n}\r\n\r\n@if (badge() !== undefined) {\r\n <!-- The counter. Retoned per style/usage through the --chip-badge-* hooks in\r\n chip.css, mirroring the way Figma overrides the Badge instance fill.\r\n Named by the primary button's aria-describedby. -->\r\n <span class=\"cmp-chip__badge\" [id]=\"badgeId\">{{ badge() }}</span>\r\n}\r\n\r\n<!-- Dismiss is ALWAYS the outermost element (ruled 2026-09-02, Jean). The Figma\r\n file orders icon, label, Close Button, Badge \u2014 recorded as a reconciliation. -->\r\n@if (dismissible()) {\r\n <!-- Disabled with the chip. The reference bundle leaves this control live on a\r\n disabled chip, which makes the dimmed pill misrepresent its state (4.1.2)\r\n and puts the dimmed \"x\" under the 3:1 non-text contrast floor without the\r\n inactive-component exemption (1.4.11). Flagged upstream. -->\r\n <button\r\n type=\"button\"\r\n class=\"cmp-chip__dismiss\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"dismissLabel()\"\r\n (click)=\"onDismiss($event)\"\r\n >\r\n <span class=\"cmp-chip__x\" [innerHTML]=\"dismissIconSvg()\" aria-hidden=\"true\"></span>\r\n </button>\r\n}\r\n", styles: [":host{--chip-bg: var(--surface-surface-container-sys-surface-container-high);--chip-fg: var(--surface-surface-sys-on-surface);--chip-icon: var(--chip-fg);--chip-border: transparent;--chip-ctl-bg: var(--transparent-neutral-sys-white-50);--chip-ctl-fg: var(--chip-fg);--chip-x-color: var(--surface-surface-sys-on-surface-variant);--chip-h: 20px;--chip-px: var(--spacing-padding-sys-padding-8);--chip-gap: var(--spacing-padding-sys-padding-4);--chip-font-size: var(--label-small-sys-font-size);--chip-line-height: var(--label-small-sys-line-height);--chip-tracking: var(--label-small-sys-tracking);--chip-font-weight: var(--label-small-sys-font-weight);--chip-ctl-size: 16px;--chip-icon-size: 16px;--chip-badge-bg: var(--chip-ctl-bg);--chip-badge-fg: var(--chip-ctl-fg);--chip-badge-px: var(--spacing-padding-sys-padding-4);--chip-badge-font-size: var(--label-small-sys-font-size);--chip-badge-line-height: var(--label-small-sys-line-height);--chip-badge-tracking: var(--label-small-sys-tracking);--chip-badge-font-weight: var(--label-small-sys-font-weight-emphasized);--chip-px-end: max( var(--spacing-padding-sys-padding-4), calc((var(--chip-h) - var(--chip-ctl-size)) / 2) );--chip-px-start: max( var(--spacing-padding-sys-padding-4), calc((var(--chip-h) - var(--chip-icon-size)) / 2) );display:inline-flex;align-items:center;gap:var(--chip-gap);box-sizing:border-box;height:var(--chip-h);padding-inline:calc(var(--chip-px) - var(--border-width-sys-stroke-thin));position:relative;background:var(--chip-bg);color:var(--chip-fg);border:var(--border-width-sys-stroke-thin) solid var(--chip-border);border-radius:var(--border-radius-sys-radius-full);font-family:var(--label-small-sys-font-family);font-size:var(--chip-font-size);line-height:var(--chip-line-height);letter-spacing:var(--chip-tracking);font-weight:var(--chip-font-weight);white-space:nowrap}.cmp-chip__icon{display:inline-flex;flex-shrink:0;color:var(--chip-icon);line-height:0}.cmp-chip__icon svg,.cmp-chip__x svg{display:block}.cmp-chip__x{display:inline-flex;align-items:center;justify-content:center;line-height:0}.cmp-chip__label{min-width:0;overflow:hidden;text-overflow:ellipsis}.cmp-chip__primary{display:inline-flex;align-items:center;flex:0 1 auto;min-width:0;gap:var(--chip-gap);margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;letter-spacing:inherit;cursor:pointer;-webkit-appearance:none;appearance:none}.cmp-chip__primary:before{content:\"\";position:absolute;inset:calc(var(--border-width-sys-stroke-thin) * -1);border-radius:var(--border-radius-sys-radius-full)}.cmp-chip__primary:focus-visible{outline:none}.cmp-chip__primary:disabled{cursor:not-allowed}:host(.cmp-chip--dismissible),:host(.cmp-chip--has-badge){padding-inline-end:calc(var(--chip-px-end) - var(--border-width-sys-stroke-thin))}:host(.cmp-chip--has-icon){padding-inline-start:calc(var(--chip-px-start) - var(--border-width-sys-stroke-thin))}.cmp-chip__dismiss{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--chip-ctl-size);height:var(--chip-ctl-size);padding:0;border:0;border-radius:var(--border-radius-sys-radius-full);background:var(--chip-ctl-bg);color:var(--chip-x-color);cursor:pointer;line-height:0;-webkit-appearance:none;appearance:none}.cmp-chip__dismiss:disabled{cursor:not-allowed}.cmp-chip__dismiss:focus-visible{outline:var(--border-width-sys-stroke-medium) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thin)}.cmp-chip__badge{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;box-sizing:border-box;min-width:var(--chip-ctl-size);height:var(--chip-ctl-size);max-width:100%;padding-inline:var(--chip-badge-px);border-radius:var(--border-radius-sys-radius-full);background:var(--chip-badge-bg);color:var(--chip-badge-fg);font-size:var(--chip-badge-font-size);line-height:var(--chip-badge-line-height);letter-spacing:var(--chip-badge-tracking);font-weight:var(--chip-badge-font-weight);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host(.cmp-chip--interactive){cursor:pointer;transition:box-shadow .12s ease-out}:host(.cmp-chip--interactive) .cmp-chip__dismiss{position:relative;z-index:1}:host(.cmp-chip--interactive:has(.cmp-chip__primary:disabled)){cursor:not-allowed;opacity:.64}:host(.cmp-chip--interactive:has(.cmp-chip__primary:disabled):hover){box-shadow:none}:host(.cmp-chip--interactive:has(.cmp-chip__primary:disabled):hover):after{opacity:0}:host(.cmp-chip--interactive):after{content:\"\";position:absolute;inset:calc(var(--border-width-sys-stroke-thin) * -1);border-radius:var(--border-radius-sys-radius-full);background:var(--chip-fg);opacity:0;pointer-events:none;transition:opacity .24s ease-out}:host(.cmp-chip--interactive:hover){box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}:host(.cmp-chip--interactive:hover):after{opacity:.08;transition-duration:.12s}:host(.cmp-chip--interactive:has(.cmp-chip__primary:focus-visible)){outline:var(--border-width-sys-stroke-medium) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thin)}:host(.cmp-chip--md){--chip-h: 24px;--chip-px: var(--spacing-padding-sys-padding-12);--chip-font-size: var(--label-medium-sys-font-size);--chip-line-height: var(--label-medium-sys-line-height);--chip-tracking: var(--label-medium-sys-tracking);--chip-font-weight: var(--label-medium-sys-font-weight)}:host(.cmp-chip--lg){--chip-h: 32px;--chip-px: var(--spacing-padding-sys-padding-16);--chip-gap: var(--spacing-padding-sys-padding-8);--chip-font-size: var(--label-medium-sys-font-size);--chip-line-height: var(--label-medium-sys-line-height);--chip-tracking: var(--label-medium-sys-tracking);--chip-font-weight: var(--label-medium-sys-font-weight);--chip-ctl-size: 20px;--chip-icon-size: 20px}:host(.cmp-chip--xl){--chip-h: 40px;--chip-px: var(--spacing-padding-sys-padding-16);--chip-gap: var(--spacing-padding-sys-padding-8);--chip-font-size: var(--label-large-sys-font-size);--chip-line-height: var(--label-large-sys-line-height);--chip-tracking: var(--label-large-sys-tracking);--chip-font-weight: var(--label-large-sys-font-weight);--chip-ctl-size: 24px;--chip-icon-size: 20px;--chip-badge-px: var(--spacing-padding-sys-padding-8);--chip-badge-font-size: var(--label-medium-sys-font-size);--chip-badge-line-height: var(--label-medium-sys-line-height);--chip-badge-tracking: var(--label-medium-sys-tracking);--chip-badge-font-weight: var(--label-medium-sys-font-weight-emphasized)}:host(.cmp-chip--tonal.cmp-chip--informative){--chip-bg: var(--custom-info-sys-info-container);--chip-fg: var(--custom-info-sys-on-info-container)}:host(.cmp-chip--tonal.cmp-chip--positive){--chip-bg: var(--custom-success-sys-success-container);--chip-fg: var(--custom-success-sys-on-success-container)}:host(.cmp-chip--tonal.cmp-chip--danger){--chip-bg: var(--error-sys-error-container);--chip-fg: var(--error-sys-on-error-container)}:host(.cmp-chip--tonal.cmp-chip--warning){--chip-bg: var(--custom-warning-sys-warning-container);--chip-fg: var(--custom-warning-sys-on-warning-container)}:host(.cmp-chip--inverse){--chip-bg: var(--transparent-neutral-sys-white-20);--chip-fg: var(--surface-inverse-sys-inverse-on-surface);--chip-ctl-bg: var(--transparent-neutral-sys-white-20);--chip-x-color: var(--surface-inverse-sys-inverse-on-surface);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}:host(.cmp-chip--filled.cmp-chip--neutral){--chip-bg: var(--surface-surface-container-sys-surface-container-lowest)}:host(.cmp-chip--filled.cmp-chip--informative),:host(.cmp-chip--filled.cmp-chip--positive),:host(.cmp-chip--filled.cmp-chip--danger),:host(.cmp-chip--filled.cmp-chip--warning){--chip-fg: var(--surface-inverse-sys-inverse-on-surface);--chip-ctl-bg: var(--transparent-neutral-sys-white-20);--chip-x-color: var(--surface-inverse-sys-inverse-on-surface)}:host(.cmp-chip--filled.cmp-chip--informative){--chip-bg: var(--custom-info-sys-info)}:host(.cmp-chip--filled.cmp-chip--positive){--chip-bg: var(--custom-success-sys-success)}:host(.cmp-chip--filled.cmp-chip--danger){--chip-bg: var(--error-sys-error)}:host(.cmp-chip--filled.cmp-chip--warning){--chip-bg: var(--custom-warning-sys-warning)}:host(.cmp-chip--filled.cmp-chip--informative) .cmp-chip__badge,:host(.cmp-chip--filled.cmp-chip--positive) .cmp-chip__badge,:host(.cmp-chip--filled.cmp-chip--danger) .cmp-chip__badge,:host(.cmp-chip--filled.cmp-chip--warning) .cmp-chip__badge{--chip-badge-bg: var(--surface-surface-container-sys-surface-container-lowest);--chip-badge-fg: var(--chip-bg)}:host(.cmp-chip--inverse) .cmp-chip__badge{--chip-badge-bg: var(--surface-surface-container-sys-surface-container-lowest);--chip-badge-fg: var(--surface-surface-sys-on-surface)}:host(.cmp-chip--filled.cmp-chip--neutral) .cmp-chip__badge{--chip-badge-bg: var(--surface-surface-container-sys-surface-container);--chip-badge-fg: var(--surface-surface-sys-on-surface)}:host(.cmp-chip--outlined){--chip-bg: transparent;--chip-border: var(--outline-sys-outline)}:host(.cmp-chip--outlined.cmp-chip--informative){--chip-border: var(--custom-info-sys-info-container);--chip-fg: var(--custom-info-sys-info)}:host(.cmp-chip--outlined.cmp-chip--positive){--chip-border: var(--custom-success-sys-success-container);--chip-fg: var(--custom-success-sys-success)}:host(.cmp-chip--outlined.cmp-chip--danger){--chip-border: var(--error-sys-error-container);--chip-fg: var(--error-sys-error)}:host(.cmp-chip--outlined.cmp-chip--warning){--chip-border: var(--custom-warning-sys-warning-container);--chip-fg: var(--custom-warning-sys-on-warning-container)}@media(forced-colors:active){:host(.cmp-chip--interactive:has(.cmp-chip__primary:focus-visible)),.cmp-chip__dismiss:focus-visible{outline:var(--border-width-sys-stroke-medium) solid Highlight;outline-offset:var(--border-width-sys-stroke-medium)}}\n"] }]
|
|
167
|
+
}], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], usage: [{ type: i0.Input, args: [{ isSignal: true, alias: "usage", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], dismissLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissLabel", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
168
|
+
//# sourceMappingURL=chip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chip.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/chip/chip.ts","../../../../../../packages/angular/src/lib/chip/chip.html"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n computed,\r\n effect,\r\n inject,\r\n input,\r\n output,\r\n} from '@angular/core';\r\nimport { NgTemplateOutlet } from '@angular/common';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\nimport { RippleDirective } from '../card-base/ripple.directive';\r\n\r\n/** Figma \"Style\". */\r\nexport type ChipVariant = 'tonal' | 'outlined' | 'filled';\r\n/** Figma \"Usage\". `inverse` pairs with the tonal style only. */\r\nexport type ChipUsage = 'neutral' | 'informative' | 'positive' | 'danger' | 'warning' | 'inverse';\r\n/** Chip dimensions — 20 / 24 / 32 / 40 tall. */\r\nexport type ChipSize = 'sm' | 'md' | 'lg' | 'xl';\r\n\r\n/**\r\n * Iconography stroke scale (Figma Foundation → Tokens → Iconography, node\r\n * 5860-11367): every icon size pairs with a stroke width so optical weight stays\r\n * consistent as the glyph scales — 12/16 → 1, 20/24 → 1.5, 32+ → 2. Thresholds\r\n * rather than a lookup so in-between sizes still resolve sensibly.\r\n */\r\nfunction iconStrokeWidth(size: number): number {\r\n if (size >= 28) return 2;\r\n if (size >= 18) return 1.5;\r\n return 1;\r\n}\r\n\r\n/**\r\n * Lucide glyphs are drawn on a fixed 24×24 viewBox, so a `stroke-width` attribute\r\n * is in *user units* and shrinks with the rendered size — a 1 at 16px paints a\r\n * 0.67px line. The DS stroke scale above is defined in ABSOLUTE px (the reference\r\n * implementation switches on Lucide's `absoluteStrokeWidth`), so the attribute is\r\n * scaled back up by 24/size to land on a true 1px / 1.5px line.\r\n */\r\nfunction absoluteStrokeAttr(size: number): number {\r\n return (iconStrokeWidth(size) * 24) / size;\r\n}\r\n\r\n/**\r\n * Chip — Figma \"Chip\" component set (Core Components › Molecules, node 14:942).\r\n *\r\n * Three axes: Style (Tonal / Outlined / Filled) × Usage (Neutral / Informative /\r\n * Positive / Danger / Warning, plus Inverse on Tonal only) × Size (sm / md / lg /\r\n * xl = 20 / 24 / 32 / 40). Booleans: leading icon, dismissible, badge — all off by\r\n * default, as in Figma.\r\n *\r\n * Chips are INTERACTIVE BY DEFAULT (ruled 2026-09-02, Jean — the Material 3\r\n * convention: every chip is something you press; a read-only status marker is a\r\n * Badge). The icon + label render inside a real `<button>` whose hit area is the\r\n * whole pill; `(click)` on the host is the chip's action. The interaction recipe is\r\n * Button's (ruled 2026-08-24): the chip's own foreground as an 8% state layer,\r\n * 120ms hover-in / 240ms release, the level-2 elevation lift, the shared press\r\n * ripple, and the Focus Ring drawn around the pill. The Figma set authors no State\r\n * variants — recorded as a reconciliation; the recipe follows them the day they\r\n * exist. `[interactive]=\"false\"` opts a chip out (a display-only tag inside another\r\n * control) — then the host is a plain pill with no role.\r\n *\r\n * The dismiss affordance is a SIBLING button, never nested in the primary one\r\n * (nested interactive controls are invalid); it emits `dismiss` and stops its click\r\n * at the host so the chip's action does not fire. It is ALWAYS the outermost\r\n * element — icon, label, badge, then the x (ruled 2026-09-02, Jean; the Figma file\r\n * orders the badge after the Close Button). With a trailing control the pill's end\r\n * padding tightens so the control sits concentric with the pill's end, and a\r\n * leading icon mirrors that inset at the start (ruled 2026-09-02; the file authors\r\n * symmetric padding).\r\n *\r\n * Colour recipe (usage → sys tokens; neutral maps to the surface family):\r\n * tonal bg `<usage>`-container fg on-`<usage>`-container\r\n * filled bg `<usage>`, white content (neutral: plain white pill — Figma\r\n * authors a level-2 elevation on it; ruled 2026-08-24 that chips carry\r\n * no resting shadow, Figma queued to update)\r\n * outlined border `<usage>`-container, label `<usage>`, icon = label (neutral:\r\n * outline / on-surface). EXCEPT warning: label and icon take\r\n * on-warning-container — an accessibility exception ruled 2026-09-02\r\n * (sys-warning on white is 3.09:1).\r\n * inverse transparent-white-20 + backdrop blur, white content — for placement\r\n * on photos. Tonal only, per the Figma set.\r\n *\r\n * Figma reconciliations this implementation carries (flagged to design, majority\r\n * pattern followed instead):\r\n * - Outlined/Informative binds primary-family colours in the file where its three\r\n * status siblings all use their own family. Implemented info-family.\r\n * - The Warning badge count binds on-surface in the file where every other usage\r\n * uses the usage foreground. Implemented as the usage foreground.\r\n * - The counter inverts on filled-status and inverse chips for AA (see chip.css).\r\n *\r\n * @example\r\n * ```html\r\n * <compsych-chip label=\"Active\" variant=\"tonal\" usage=\"positive\" size=\"md\" />\r\n * <compsych-chip label=\"Filter\" leadingIcon=\"tag\" [dismissible]=\"true\"\r\n * (dismiss)=\"remove()\" (click)=\"apply()\" />\r\n * ```\r\n */\r\n@Component({\r\n selector: 'compsych-chip',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [NgTemplateOutlet],\r\n // Button's press feedback on interactive chips (ruled 2026-08-24) — suppressed\r\n // on static and disabled chips (see constructor), following the Card.\r\n hostDirectives: [RippleDirective],\r\n templateUrl: './chip.html',\r\n styleUrl: './chip.css',\r\n host: {\r\n class: 'cmp-chip',\r\n '[class.cmp-chip--tonal]': \"variant() === 'tonal'\",\r\n '[class.cmp-chip--outlined]': \"variant() === 'outlined'\",\r\n '[class.cmp-chip--filled]': \"variant() === 'filled'\",\r\n '[class.cmp-chip--neutral]': \"usage() === 'neutral'\",\r\n '[class.cmp-chip--informative]': \"usage() === 'informative'\",\r\n '[class.cmp-chip--positive]': \"usage() === 'positive'\",\r\n '[class.cmp-chip--danger]': \"usage() === 'danger'\",\r\n '[class.cmp-chip--warning]': \"usage() === 'warning'\",\r\n '[class.cmp-chip--inverse]': \"usage() === 'inverse'\",\r\n '[class.cmp-chip--md]': \"size() === 'md'\",\r\n '[class.cmp-chip--lg]': \"size() === 'lg'\",\r\n '[class.cmp-chip--xl]': \"size() === 'xl'\",\r\n '[class.cmp-chip--interactive]': 'interactive()',\r\n '[class.cmp-chip--dismissible]': 'dismissible()',\r\n '[class.cmp-chip--has-icon]': '!!leadingIcon()',\r\n '[class.cmp-chip--has-badge]': 'badge() !== undefined',\r\n },\r\n})\r\nexport class ChipComponent {\r\n /** Figma \"Style\". Defaults to Tonal, as the set does. */\r\n readonly variant = input<ChipVariant>('tonal');\r\n /** Figma \"Usage\". `inverse` pairs with the tonal style only. */\r\n readonly usage = input<ChipUsage>('neutral');\r\n readonly size = input<ChipSize>('sm');\r\n readonly label = input.required<string>();\r\n /** Lucide icon name for the leading slot (Figma \"Leading Icon\" + swap). */\r\n readonly leadingIcon = input<string>();\r\n /** Shows the dismiss affordance (Figma \"Dismissible\"). */\r\n readonly dismissible = input<boolean>(false);\r\n /** Badge count (Figma \"Badge\"). Undefined = no badge. */\r\n readonly badge = input<number | string>();\r\n /** Accessible name for the dismiss button. */\r\n readonly dismissLabel = input<string>('Remove');\r\n /**\r\n * Chips are interactive by default (ruled 2026-09-02): icon + label sit in a real\r\n * button and `(click)` on the host is the action. `false` renders a display-only\r\n * pill — no role, no state layer, no ripple.\r\n */\r\n readonly interactive = input<boolean>(true);\r\n /** Disables the primary button (interactive chips only). */\r\n readonly disabled = input<boolean>(false);\r\n\r\n /** Emitted by the dismiss button. The host app removes the chip. */\r\n readonly dismiss = output<void>();\r\n\r\n private readonly sanitizer = inject(DomSanitizer);\r\n private readonly ripple = inject(RippleDirective);\r\n\r\n private static nextId = 0;\r\n /** Links the primary button to its counter (aria-describedby). */\r\n protected readonly badgeId = `cmp-chip-badge-${ChipComponent.nextId++}`;\r\n\r\n constructor() {\r\n // Ripple only on live chips — static or disabled chips stay inert.\r\n effect(() => {\r\n this.ripple.suppressed = !this.interactive() || this.disabled();\r\n });\r\n }\r\n\r\n /** Leading icon px per size (Figma: 16 at sm/md, 20 at lg/xl). */\r\n readonly iconPx = computed(() => (this.size() === 'sm' || this.size() === 'md' ? 16 : 20));\r\n /** Dismiss \"x\" px per size (Figma close slot: 12 at sm/md, 16 at lg/xl). */\r\n readonly xPx = computed(() => (this.size() === 'sm' || this.size() === 'md' ? 12 : 16));\r\n\r\n protected readonly leadingIconSvg = computed<SafeHtml | null>(() => {\r\n const name = this.leadingIcon();\r\n return name ? this.iconSvg(name, this.iconPx()) : null;\r\n });\r\n\r\n protected readonly dismissIconSvg = computed<SafeHtml>(() => this.iconSvg('x', this.xPx()));\r\n\r\n /** Renders a sized Lucide glyph at the DS absolute stroke weight for that size. */\r\n private iconSvg(name: string, size: number): SafeHtml {\r\n let svg = LUCIDE_ICONS[name] ?? '';\r\n svg = svg.replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n svg = svg.replace('stroke-width=\"1.5\"', `stroke-width=\"${absoluteStrokeAttr(size)}\"`);\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n\r\n /** Dismiss is its own action — it must not also fire the chip's. */\r\n protected onDismiss(event: Event): void {\r\n event.stopPropagation();\r\n this.dismiss.emit();\r\n }\r\n}\r\n","<!-- Icon + label — one template, rendered inside the primary button on an\r\n interactive chip or bare on a static one. -->\r\n<ng-template #content>\r\n @if (leadingIconSvg(); as icon) {\r\n <span class=\"cmp-chip__icon\" [innerHTML]=\"icon\" aria-hidden=\"true\"></span>\r\n }\r\n <span class=\"cmp-chip__label\">{{ label() }}</span>\r\n</ng-template>\r\n\r\n@if (interactive()) {\r\n <!-- The chip's action. Its ::before stretches over the whole pill so the hit\r\n area is the chip, not the label; the dismiss button sits above it. -->\r\n <button\r\n type=\"button\"\r\n class=\"cmp-chip__primary\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-describedby]=\"badge() !== undefined ? badgeId : null\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n </button>\r\n} @else {\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n}\r\n\r\n@if (badge() !== undefined) {\r\n <!-- The counter. Retoned per style/usage through the --chip-badge-* hooks in\r\n chip.css, mirroring the way Figma overrides the Badge instance fill.\r\n Named by the primary button's aria-describedby. -->\r\n <span class=\"cmp-chip__badge\" [id]=\"badgeId\">{{ badge() }}</span>\r\n}\r\n\r\n<!-- Dismiss is ALWAYS the outermost element (ruled 2026-09-02, Jean). The Figma\r\n file orders icon, label, Close Button, Badge — recorded as a reconciliation. -->\r\n@if (dismissible()) {\r\n <!-- Disabled with the chip. The reference bundle leaves this control live on a\r\n disabled chip, which makes the dimmed pill misrepresent its state (4.1.2)\r\n and puts the dimmed \"x\" under the 3:1 non-text contrast floor without the\r\n inactive-component exemption (1.4.11). Flagged upstream. -->\r\n <button\r\n type=\"button\"\r\n class=\"cmp-chip__dismiss\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"dismissLabel()\"\r\n (click)=\"onDismiss($event)\"\r\n >\r\n <span class=\"cmp-chip__x\" [innerHTML]=\"dismissIconSvg()\" aria-hidden=\"true\"></span>\r\n </button>\r\n}\r\n"]}
|