@lifecorner/angular-ui-kit 3.23.0 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-progress-ring.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/progress-ring/progress-ring.component.ts","../../../../libs/angular-ui-kit/src/lib/progress-ring/progress-ring.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n computed,\n} from '@angular/core';\n\nexport type ProgressRingColor = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';\nexport type ProgressRingSize = 'xs' | 'sm' | 'md' | 'lg';\n\n@Component({\n selector: 'lc-progress-ring',\n standalone: true,\n templateUrl: './progress-ring.component.html',\n styleUrls: ['./progress-ring.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Progress ring component for circular progress indication.\n *\n * Features:\n * - Circular SVG progress arc (0–100%)\n * - Color theme variants (primary, secondary, success, warning, error)\n * - Size presets (xs, sm, md, lg)\n * - Optional percentage value display\n * - Animated stroke transitions\n * - Accessible `progressbar` role with `aria-valuenow` and a configurable label\n *\n * @example\n * ```html\n * <lc-progress-ring [value]=\"65\" color=\"success\" [showValue]=\"true\" />\n * ```\n */\nexport class ProgressRingComponent {\n /** Value between 0 and 100. */\n value = input<number>(0);\n\n /** Color theme. */\n color = input<ProgressRingColor>('primary');\n\n /** Size preset. */\n size = input<ProgressRingSize>('md');\n\n /** Show percentage text in center. */\n showValue = input<boolean>(true);\n\n /**\n * Alias of `showValue`, named like `lc-progress-bar`'s `showLabel`. When\n * set it wins over `showValue`.\n */\n showLabel = input<boolean | undefined>(undefined);\n\n /** Accessible name of the progress ring. */\n ariaLabel = input<string>('Progress');\n\n private readonly SIZE_MAP: Record<ProgressRingSize, number> = { xs: 32, sm: 48, md: 64, lg: 96 };\n private readonly STROKE_MAP: Record<ProgressRingSize, number> = { xs: 3, sm: 4, md: 5, lg: 7 };\n\n protected readonly svgSize = computed(() => this.SIZE_MAP[this.size()]);\n protected readonly strokeW = computed(() => this.STROKE_MAP[this.size()]);\n protected readonly viewBox = computed(() => `0 0 ${this.svgSize()} ${this.svgSize()}`);\n protected readonly center = computed(() => this.svgSize() / 2);\n protected readonly radius = computed(() => (this.svgSize() - this.strokeW() * 2) / 2);\n protected readonly circumference = computed(() => 2 * Math.PI * this.radius());\n\n /** `value` clamped to the 0–100 range (drives arc, text and ARIA). */\n protected readonly clampedValue = computed(() => Math.min(Math.max(this.value(), 0), 100));\n\n protected readonly shouldShowValue = computed(() => this.showLabel() ?? this.showValue());\n\n protected readonly dashOffset = computed(() => {\n const c = this.circumference();\n return c - (this.clampedValue() / 100) * c;\n });\n\n protected readonly fontSize = computed(() => {\n const map: Record<ProgressRingSize, string> = { xs: '0.5rem', sm: '0.625rem', md: '0.875rem', lg: '1.25rem' };\n return map[this.size()];\n });\n\n protected readonly displayValue = computed(() => `${Math.round(this.clampedValue())}%`);\n}\n","<svg\n [attr.width]=\"svgSize()\" [attr.height]=\"svgSize()\" [attr.viewBox]=\"viewBox()\"\n class=\"lc-progress-ring\"\n role=\"progressbar\"\n [attr.aria-valuenow]=\"clampedValue()\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <circle\n [attr.cx]=\"center()\" [attr.cy]=\"center()\" [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeW()\"\n fill=\"none\" class=\"lc-progress-ring__track\"\n />\n <circle\n [attr.cx]=\"center()\" [attr.cy]=\"center()\" [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeW()\"\n [attr.stroke-dasharray]=\"circumference()\" [attr.stroke-dashoffset]=\"dashOffset()\"\n fill=\"none\" stroke-linecap=\"round\"\n [class]=\"'lc-progress-ring__value lc-progress-ring__value--' + color()\"\n [attr.transform]=\"'rotate(-90 ' + center() + ' ' + center() + ')'\"\n />\n @if (shouldShowValue()) {\n <text [attr.x]=\"center()\" [attr.y]=\"center()\" text-anchor=\"middle\" dominant-baseline=\"central\" class=\"lc-progress-ring__text\" [style.font-size]=\"fontSize()\">\n {{ displayValue() }}\n </text>\n }\n</svg>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-progress-ring.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/progress-ring/progress-ring.component.ts","../../../../libs/angular-ui-kit/src/lib/progress-ring/progress-ring.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n computed,\n} from '@angular/core';\n\nexport type ProgressRingColor = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';\nexport type ProgressRingSize = 'xs' | 'sm' | 'md' | 'lg';\n\n@Component({\n selector: 'lc-progress-ring',\n standalone: true,\n templateUrl: './progress-ring.component.html',\n styleUrls: ['./progress-ring.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Progress ring component for circular progress indication.\n *\n * Features:\n * - Circular SVG progress arc (0–100%)\n * - Color theme variants (primary, secondary, success, warning, error)\n * - Size presets (xs, sm, md, lg)\n * - Optional percentage value display\n * - Animated stroke transitions\n * - Accessible `progressbar` role with `aria-valuenow` and a configurable label\n *\n * @example\n * ```html\n * <lc-progress-ring [value]=\"65\" color=\"success\" [showValue]=\"true\" />\n * ```\n */\nexport class ProgressRingComponent {\n /** Value between 0 and 100. */\n value = input<number>(0);\n\n /** Color theme. */\n color = input<ProgressRingColor>('primary');\n\n /** Size preset. */\n size = input<ProgressRingSize>('md');\n\n /** Show percentage text in center. */\n showValue = input<boolean>(true);\n\n /**\n * Alias of `showValue`, named like `lc-progress-bar`'s `showLabel`. When\n * set it wins over `showValue`.\n */\n showLabel = input<boolean | undefined>(undefined);\n\n /** Accessible name of the progress ring. */\n ariaLabel = input<string>('Progress');\n\n private readonly SIZE_MAP: Record<ProgressRingSize, number> = { xs: 32, sm: 48, md: 64, lg: 96 };\n private readonly STROKE_MAP: Record<ProgressRingSize, number> = { xs: 3, sm: 4, md: 5, lg: 7 };\n\n protected readonly svgSize = computed(() => this.SIZE_MAP[this.size()]);\n protected readonly strokeW = computed(() => this.STROKE_MAP[this.size()]);\n protected readonly viewBox = computed(() => `0 0 ${this.svgSize()} ${this.svgSize()}`);\n protected readonly center = computed(() => this.svgSize() / 2);\n protected readonly radius = computed(() => (this.svgSize() - this.strokeW() * 2) / 2);\n protected readonly circumference = computed(() => 2 * Math.PI * this.radius());\n\n /** `value` clamped to the 0–100 range (drives arc, text and ARIA). */\n protected readonly clampedValue = computed(() => Math.min(Math.max(this.value(), 0), 100));\n\n protected readonly shouldShowValue = computed(() => this.showLabel() ?? this.showValue());\n\n protected readonly dashOffset = computed(() => {\n const c = this.circumference();\n return c - (this.clampedValue() / 100) * c;\n });\n\n protected readonly fontSize = computed(() => {\n const map: Record<ProgressRingSize, string> = { xs: '0.5rem', sm: '0.625rem', md: '0.875rem', lg: '1.25rem' };\n return map[this.size()];\n });\n\n protected readonly displayValue = computed(() => `${Math.round(this.clampedValue())}%`);\n}\n","<svg\n [attr.width]=\"svgSize()\" [attr.height]=\"svgSize()\" [attr.viewBox]=\"viewBox()\"\n class=\"lc-progress-ring\"\n role=\"progressbar\"\n [attr.aria-valuenow]=\"clampedValue()\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <circle\n [attr.cx]=\"center()\" [attr.cy]=\"center()\" [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeW()\"\n fill=\"none\" class=\"lc-progress-ring__track\"\n />\n <circle\n [attr.cx]=\"center()\" [attr.cy]=\"center()\" [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeW()\"\n [attr.stroke-dasharray]=\"circumference()\" [attr.stroke-dashoffset]=\"dashOffset()\"\n fill=\"none\" stroke-linecap=\"round\"\n [class]=\"'lc-progress-ring__value lc-progress-ring__value--' + color()\"\n [attr.transform]=\"'rotate(-90 ' + center() + ' ' + center() + ')'\"\n />\n @if (shouldShowValue()) {\n <text [attr.x]=\"center()\" [attr.y]=\"center()\" text-anchor=\"middle\" dominant-baseline=\"central\" class=\"lc-progress-ring__text\" [style.font-size]=\"fontSize()\">\n {{ displayValue() }}\n </text>\n }\n</svg>\n"],"mappings":";;AAiCA,IAAa,wBAAb,MAAa,sBAAqB;CAEhC,QAAQ,MAAc,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAGxB,QAAQ,MAAyB,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAG3C,OAAO,MAAwB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAGpC,YAAY,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAMhC,YAAY,MAA2B,QAAU,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAGjD,YAAY,MAAc,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEpB,WAA6C;EAAE,IAAI;EAAI,IAAI;EAAI,IAAI;EAAI,IAAI;CAAE;CAC7E,aAA+C;EAAE,IAAI;EAAG,IAAI;EAAG,IAAI;EAAG,IAAI;CAAC;CAEzE,UAAU,eAAe,KAAK,SAAS,KAAK,KAAI,IAAI,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CACpD,UAAU,eAAe,KAAK,WAAW,KAAK,KAAI,IAAI,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CACtD,UAAU,eAAe,OAAO,KAAK,QAAO,EAAE,GAAI,KAAK,QAAO,KAAK,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CACnE,SAAS,eAAe,KAAK,QAAO,IAAK,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAC3C,SAAS,gBAAgB,KAAK,QAAO,IAAK,KAAK,QAAO,IAAK,KAAK,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAClE,gBAAgB,eAAe,IAAI,KAAK,KAAK,KAAK,OAAM,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAG3D,eAAe,eAAe,KAAK,IAAI,KAAK,IAAI,KAAK,MAAK,GAAI,CAAC,GAAG,GAAG,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEvE,kBAAkB,eAAe,KAAK,UAAS,KAAM,KAAK,UAAS,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAEtE,aAAa,eAAc;EAC5C,MAAM,IAAI,KAAK,cAAa;EAC5B,OAAO,IAAK,KAAK,aAAY,IAAK,MAAO;CAC3C,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,WAAW,eAAc;EAE1C,OAAO;GADyC,IAAI;GAAU,IAAI;GAAY,IAAI;GAAY,IAAI;EAC3F,EAAI,KAAK,KAAI;CACtB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,eAAe,eAAe,GAAG,KAAK,MAAM,KAAK,aAAY,CAAE,EAAC,IAAI,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CA/C5E,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAqB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAArB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAqB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UCjClC;EA4BA,QAAA,CAAA,oqBAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;ADKa,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAqB,YAAA,CAAA;EAvBjC,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAkB,YAChB;GAAI,iBAGC,wBAAwB;GAAM,UAAA;GAAA,QAAA,CAAA,oqBAAA;EAAA,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifecorner/angular-ui-kit",
3
- "version": "3.23.0",
3
+ "version": "3.24.0",
4
4
  "description": "LifeCorner Design System – Angular UI component library",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,6 +51,10 @@
51
51
  --color-secondary-700: #{$color-secondary-200};
52
52
  --color-secondary-800: #{$color-secondary-100};
53
53
  --color-secondary-900: #{$color-secondary-50};
54
+ // Secondary data marks (progress fill, ring arc), not a text colour: the raw
55
+ // 500 step, at least 3:1 against the track and the page and 1.92:1 apart from
56
+ // --color-info (request 036: the ring's inverted secondary-600 was 1.07:1).
57
+ --color-secondary-graphic: #{$color-secondary-500};
54
58
 
55
59
  // ============================================================================
56
60
  // Colors - Neutral (Inverted for dark mode)
@@ -44,6 +44,10 @@
44
44
  --color-secondary-700: #{$color-secondary-700};
45
45
  --color-secondary-800: #{$color-secondary-800};
46
46
  --color-secondary-900: #{$color-secondary-900};
47
+ // Secondary data marks (progress fill, ring arc), not a text colour: at
48
+ // least 3:1 against the --color-surface-hover track and the page in both
49
+ // themes (request 036, F-11: the bar fill was secondary-500, 2.83:1).
50
+ --color-secondary-graphic: #{$color-secondary-600};
47
51
 
48
52
  // ============================================================================
49
53
  // Colors - Neutral (Grays)
@@ -743,11 +743,13 @@ describe('Component pairings of 3.21.1 (request 029)', () => {
743
743
  );
744
744
  });
745
745
 
746
- it('AC-10: the progress track is unchanged; primary and secondary stay raw', () => {
746
+ it('AC-10: the progress track is unchanged; primary stays raw (secondary: request 036)', () => {
747
747
  // The base rule (the forced-colours block repeats the selector with an outline).
748
748
  expect(sheets.progress).toMatch(/\n\.progress-bar__track \{\s*flex: 1;\s*background-color: var\(--color-surface-hover\);/);
749
749
  expect(fills('progress', '.progress-bar--primary .progress-bar__fill')['background-color']).toMatch(/^#[0-9a-f]{6}$/i);
750
- expect(fills('progress', '.progress-bar--secondary .progress-bar__fill')['background-color']).toMatch(/^#[0-9a-f]{6}$/i);
750
+ // 036 (F-11): the raw secondary-500 was 2.83:1 against the light track;
751
+ // secondary now reads the data-mark token (4.19 light, 4.05 dark).
752
+ expect(fills('progress', '.progress-bar--secondary .progress-bar__fill')['background-color']).toBe('var(--color-secondary-graphic)');
751
753
  });
752
754
 
753
755
  it('AC-9: kanban priorities, dependency relations, arrows and node borders read the inks', () => {
@@ -967,7 +969,8 @@ describe('Component pairings of 3.22.4 (request 034)', () => {
967
969
  ['nc', '.lc-nc__item--unread .lc-nc__item-priority--low', { 'background-image': flat('--color-surface-hover') }],
968
970
  // F-2: the ring arcs take the progress-bar inks.
969
971
  ['ring', '.lc-progress-ring__value--primary', { stroke: 'var(--color-primary-500)' }],
970
- ['ring', '.lc-progress-ring__value--secondary', { stroke: 'var(--color-secondary-600)' }],
972
+ // 036: secondary reads the data-mark token (dark secondary-600 was 1.07:1 from info).
973
+ ['ring', '.lc-progress-ring__value--secondary', { stroke: 'var(--color-secondary-graphic)' }],
971
974
  ...['success', 'warning', 'error', 'info'].map((v): Row => ['ring', `.lc-progress-ring__value--${v}`, { stroke: ink(v) }]),
972
975
  // F-3: subtle line tints, secondary line numbers.
973
976
  ['diff', '.lc-diff__line--added', { background: 'var(--color-success-subtle)' }],
@@ -1021,9 +1024,11 @@ describe('Component pairings of 3.22.4 (request 034)', () => {
1021
1024
  // The dash style still tells the relations apart (not colour alone).
1022
1025
  expect(depTs).toMatch(/\n {2}uses:\s*\{ color: '[^']+', dashed: true,/);
1023
1026
  expect(depTs).toMatch(/\n {2}extends:\s*\{ color: '[^']+', dashed: true,/);
1024
- // Out of scope (follow-up F-9): `depends` and the tree edges keep their neutral lines.
1025
- expect(relationColor('depends')).toBe('var(--color-neutral-400)');
1026
- expect(depTs.match(/color: 'var\(--color-neutral-300\)', dashed: false, isCrossRef: false/g)).toHaveLength(2);
1027
+ // Follow-up F-9, done in request 036: `depends` reads the secondary text
1028
+ // (neutral-400 was 2.53:1 light) and the tree edges the field border
1029
+ // (neutral-300 was 1.47:1 light); their contrast is in the 036 block.
1030
+ expect(relationColor('depends')).toBe('var(--color-text-secondary)');
1031
+ expect(depTs.match(/color: 'var\(--color-field-border\)', dashed: false, isCrossRef: false/g)).toHaveLength(2);
1027
1032
  });
1028
1033
 
1029
1034
  // The shared status values of 3.22.3, per palette [light, dark] (AC-2).
@@ -1043,12 +1048,15 @@ describe('Component pairings of 3.22.4 (request 034)', () => {
1043
1048
  * `+` (4.85 L, 5.17 D). Graphic minimum: the card border on hover and
1044
1049
  * selected and the `uses` cross-reference line (3.75 L), the `uses` line
1045
1050
  * (4.76 D). The terminal is always dark: 5.28 (debug level and meta).
1051
+ * Request 036: the dark ring secondary moved from the inverted
1052
+ * secondary-600 (8.35, 1.07:1 from info) to --color-secondary-graphic
1053
+ * (4.05), which is now the dark graphic minimum.
1046
1054
  */
1047
1055
  const expected = {
1048
1056
  text: [4.85, 5.17],
1049
- graphic: [3.75, 4.76],
1057
+ graphic: [3.75, 4.05],
1050
1058
  ncUnread: { high: [5.52, 5.55], urgent: [10.97, 8.37], low: [5.3, 5.99], category: [5.3, 5.99] },
1051
- ringTrack: { primary: [3.96, 6.69], secondary: [4.19, 8.35], success: [4.81, 6.15], warning: [5.47, 5.6], error: [11.43, 6.37], info: [9.22, 7.8] },
1059
+ ringTrack: { primary: [3.96, 6.69], secondary: [4.19, 4.05], success: [4.81, 6.15], warning: [5.47, 5.6], error: [11.43, 6.37], info: [9.22, 7.8] },
1052
1060
  diff: {
1053
1061
  numAdded: [6.91, 6.86], numRemoved: [6.58, 9.26], numContext: [7.23, 9.25],
1054
1062
  plus: [4.85, 5.17], minus: [10.97, 7.21], contentAdded: [16.24, 9.75], contentRemoved: [15.46, 13.16],
@@ -1267,3 +1275,278 @@ describe('Component pairings of 3.22.4 (request 034)', () => {
1267
1275
  expect(levels).toEqual([5.81, 7.78, 11.43, 6.19]);
1268
1276
  });
1269
1277
  });
1278
+
1279
+ describe('Component pairings of 3.23.1 (request 036)', () => {
1280
+ const lib = (path: string) => resolve(__dirname, '../lib', path);
1281
+ const sheets = {
1282
+ dl: compile(lib('description-list/description-list.component.scss')).css,
1283
+ nc: compile(lib('notification-center/notification-center.component.scss')).css,
1284
+ dep: compile(lib('dependency-viewer/dependency-viewer.component.scss')).css,
1285
+ log: compile(lib('log-viewer/log-viewer.component.scss')).css,
1286
+ progress: compile(lib('progress-bar/progress-bar.component.scss')).css,
1287
+ ring: compile(lib('progress-ring/progress-ring.component.scss')).css,
1288
+ };
1289
+ type Sheet = keyof typeof sheets;
1290
+ const depTs = readFileSync(lib('dependency-viewer/dependency-viewer.component.ts'), 'utf8');
1291
+ const depHtml = readFileSync(lib('dependency-viewer/dependency-viewer.component.html'), 'utf8');
1292
+
1293
+ /** Every rule as its selector list and body (`a, b { … }` names both). */
1294
+ const rulesOf = (css: string) =>
1295
+ [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({
1296
+ selectors: m[1].split(/,(?![^(]*\))/).map((s) => s.trim()),
1297
+ body: m[2],
1298
+ }));
1299
+ const decls = (body: string): Record<string, string> =>
1300
+ Object.fromEntries([...body.matchAll(/([\w-]+):\s*([^;]+);/g)].map((m) => [m[1], m[2].trim()]));
1301
+ const forcedBlock = /@media \(forced-colors: active\) \{((?:[^{}]*\{[^{}]*\})*)\s*\}/g;
1302
+ /**
1303
+ * The declarations of every rule that names `selector`, merged in source
1304
+ * order; rules in forced-colours blocks are left out (`Mark` is no colour).
1305
+ */
1306
+ const fills = (sheet: Sheet, selector: string): Record<string, string> => {
1307
+ const found = rulesOf(sheets[sheet].replace(forcedBlock, '')).filter((r) => r.selectors.includes(selector));
1308
+ expect(found.length).toBeGreaterThan(0);
1309
+ return Object.assign({}, ...found.map((r) => decls(r.body)));
1310
+ };
1311
+ /** The body of the `@media (forced-colors: active)` blocks of a sheet. */
1312
+ const forced = (sheet: Sheet) => [...sheets[sheet].matchAll(forcedBlock)].map((m) => m[1]).join('\n');
1313
+ const relationColor = (relation: string) => {
1314
+ const found = depTs.match(new RegExp(`\\n ${relation}:\\s*\\{ color: '([^']+)'`));
1315
+ expect(found).not.toBeNull();
1316
+ return (found as RegExpMatchArray)[1];
1317
+ };
1318
+
1319
+ const secondary = 'var(--color-text-secondary)';
1320
+ const graphic = 'var(--color-secondary-graphic)';
1321
+ type Row = [Sheet, string, Record<string, string>];
1322
+
1323
+ const declared: Row[] = [
1324
+ // A (BA-10): the link and primary inks, a 2 px outline instead of the halo.
1325
+ ['dl', '.lc-dl__value--primary', { color: 'var(--color-on-primary-subtle)', 'font-weight': 'var(--font-weight-semibold)' }],
1326
+ ['dl', '.lc-dl__link', { color: 'var(--color-link)', 'text-decoration': 'none' }],
1327
+ ['dl', '.lc-dl__link:hover', { color: 'var(--color-link-hover)', 'text-decoration': 'underline' }],
1328
+ ['dl', '.lc-dl__row--clickable:focus-visible', { outline: '2px solid var(--color-primary-500)', 'outline-offset': '2px' }],
1329
+ // F-8: unread message and time in the secondary ink.
1330
+ ['nc', '.lc-nc__item--unread .lc-nc__item-message', { color: secondary }],
1331
+ ['nc', '.lc-nc__item--unread .lc-nc__item-time', { color: secondary }],
1332
+ // F-10: an opaque marker with its own ink.
1333
+ ['log', '.lc-log-viewer__match', { 'background-color': 'var(--color-warning)', color: 'var(--color-on-warning)' }],
1334
+ ['log', '.lc-log-viewer--terminal .lc-log-viewer__match', { 'background-color': '#f9e2af', color: '#1e1e2e' }],
1335
+ // F-13: gray is Overlay2 (Mocha) and Subtext1 (Latte).
1336
+ ['log', '.ansi-gray', { color: '#9399b2' }],
1337
+ ['log', ':root.light .lc-log-viewer--log .ansi-gray', { color: '#5c5f77' }],
1338
+ // F-11 and the ring: the secondary data-mark token.
1339
+ ['progress', '.progress-bar--secondary .progress-bar__fill', { 'background-color': graphic }],
1340
+ ['progress', '.progress-bar-circular--secondary .progress-bar-circular__fill', { stroke: graphic }],
1341
+ ['ring', '.lc-progress-ring__value--secondary', { stroke: graphic }],
1342
+ ];
1343
+
1344
+ it.each(declared)('AC-1…13: %s %s declares the design 036 values', (sheet, selector, expectedDecls) => {
1345
+ const found = fills(sheet, selector);
1346
+ for (const [property, value] of Object.entries(expectedDecls)) expect(found[property]).toBe(value);
1347
+ });
1348
+
1349
+ it('AC-2: the clickable row has no halo any more; forced colours draw the outline in Highlight', () => {
1350
+ const clickable = rulesOf(sheets.dl).filter((r) => r.selectors.some((s) => s.includes('lc-dl__row--clickable')));
1351
+ for (const r of clickable) expect(decls(r.body)['box-shadow']).toBeUndefined();
1352
+ expect(sheets.dl).not.toMatch(/color-mix/);
1353
+ expect(forced('dl')).toMatch(/\.lc-dl__row--clickable:focus-visible \{\s*outline-color: Highlight;\s*\}/);
1354
+ });
1355
+
1356
+ it('AC-9: forced colours paint every search match, the terminal one too, with Mark / MarkText', () => {
1357
+ expect(forced('log')).toMatch(
1358
+ /\.lc-log-viewer__match,\s*\.lc-log-viewer--terminal \.lc-log-viewer__match \{\s*background-color: Mark;\s*color: MarkText;\s*\}/,
1359
+ );
1360
+ });
1361
+
1362
+ it('AC-8: depends, the structural edges and both arrows read the secondary text; tree edges the field border', () => {
1363
+ expect(relationColor('depends')).toBe(secondary);
1364
+ expect(depTs).toContain(`color: edge.structural ? '${secondary}' : style.color,`);
1365
+ expect(depTs.match(/color: 'var\(--color-field-border\)', dashed: false, isCrossRef: false/g)).toHaveLength(2);
1366
+ for (const id of ['default', 'depends']) {
1367
+ expect(depHtml).toMatch(new RegExp(`<marker id="arrow-${id}"[^>]*><path [^>]*fill="var\\(--color-text-secondary\\)"/>`));
1368
+ }
1369
+ expect(depTs).not.toMatch(/--color-neutral-400/);
1370
+ expect(depHtml).not.toMatch(/--color-neutral-400/);
1371
+ // `depends` stays solid; the relations stay apart by dash and label too.
1372
+ expect(depTs).toMatch(/\n {2}depends:\s*\{ color: '[^']+', dashed: false,/);
1373
+ });
1374
+
1375
+ it('AC-12: the log tool buttons stay 28 × 28 (WCAG 2.5.8 by size)', () => {
1376
+ const btn = fills('log', '.lc-log-viewer__tool-btn');
1377
+ expect([btn.width, btn.height]).toEqual(['28px', '28px']);
1378
+ });
1379
+
1380
+ /**
1381
+ * Design 036 §5, floored, per palette [light, dark]. Pages: background,
1382
+ * surface, surface-2; a hovered row is --color-surface-hover over them; an
1383
+ * unread NC row is --color-surface-selected over surface and surface-2; the
1384
+ * progress track is --color-surface-hover over the pages.
1385
+ */
1386
+ const expected = {
1387
+ dl: {
1388
+ linkPage: [5.94, 8.96], linkHover: [5.64, 7.92], linkHoverInkPage: [8.74, 10.53], linkHoverInkHover: [8.29, 9.3],
1389
+ primaryPage: [8.74, 7.57], primaryHover: [8.29, 6.69], ring: [4.17, 7.57],
1390
+ },
1391
+ ncUnread: [6.49, 7.05],
1392
+ dep: { dependsLabel: [7.55, 11.15], dependsLine: [7.55, 11.15], dependsCrossRef: [4.52, 7.55], dependsLegend: [7.23, 9.25], treeSurface: [4.83, 6.7], treeSurface2: [4.62, 5.56] },
1393
+ dependsVsUses: [1.29, 1.66],
1394
+ match: [7.85, 7.85],
1395
+ secondary: { track: [4.19, 4.05], page: [4.41, 4.59], vsInfo: [2.19, 1.92] },
1396
+ // The log variant's gray: Latte in light, Mocha in dark; plain surface and the worst line.
1397
+ ansiGray: { plain: [6.25, 6.11], worst: [4.63, 4.55] },
1398
+ };
1399
+ const floor = (n: number) => Math.floor(n * 100) / 100;
1400
+
1401
+ describe.each(palettes.map(([name, tokens], i) => [name, tokens, i] as const))('%s palette', (_palette, tokens, i) => {
1402
+ const c = (name: string) => color(tokens, name);
1403
+ const value = (text: string): Rgba => {
1404
+ const ref = text.match(/^var\((--[\w-]+)\)$/);
1405
+ return ref ? c(ref[1]) : parse(text);
1406
+ };
1407
+ const lowest = (fg: Rgba, bases: Rgba[]) => Math.min(...bases.map((bg) => ratio(over(fg, bg), bg)));
1408
+ const S = c('--color-surface');
1409
+ const S2 = c('--color-surface-2');
1410
+ const BG = c('--color-background');
1411
+ const pages = [BG, S, S2];
1412
+ const hovered = pages.map((p) => over(c('--color-surface-hover'), p));
1413
+ let cache: ReturnType<typeof measure> | undefined;
1414
+ /** Every §5 cell, measured once on first use (inside a test, not at collection). */
1415
+ const measured = () => (cache ??= measure());
1416
+
1417
+ function measure() {
1418
+ const dlInk = (selector: string) => value(fills('dl', selector).color);
1419
+ const ringInk = value(fills('dl', '.lc-dl__row--clickable:focus-visible').outline.replace(/^2px solid /, ''));
1420
+ const dl = {
1421
+ linkPage: lowest(dlInk('.lc-dl__link'), pages),
1422
+ linkHover: lowest(dlInk('.lc-dl__link'), hovered),
1423
+ linkHoverInkPage: lowest(dlInk('.lc-dl__link:hover'), pages),
1424
+ linkHoverInkHover: lowest(dlInk('.lc-dl__link:hover'), hovered),
1425
+ primaryPage: lowest(dlInk('.lc-dl__value--primary'), pages),
1426
+ primaryHover: lowest(dlInk('.lc-dl__value--primary'), hovered),
1427
+ // The outline sits 2 px outside the row, on the card or the page.
1428
+ ring: lowest(ringInk, pages),
1429
+ };
1430
+
1431
+ const unreadRows = [S, S2].map((row) => over(c('--color-surface-selected'), row));
1432
+ const ncUnread = Math.min(
1433
+ lowest(value(fills('nc', '.lc-nc__item--unread .lc-nc__item-message').color), unreadRows),
1434
+ lowest(value(fills('nc', '.lc-nc__item--unread .lc-nc__item-time').color), unreadRows),
1435
+ );
1436
+
1437
+ // Labels on their surface halo, lines on the viewer surface (cross-refs
1438
+ // at their opacity), legend lines on the legend's surface-2.
1439
+ const halo = value(fills('dep', '.dep-viewer__edge-label').stroke);
1440
+ const lineOpacity = Number(fills('dep', '.dep-viewer__edge--cross-ref').opacity);
1441
+ const legend = value(fills('dep', '.dep-viewer__legend').background);
1442
+ const depends = value(relationColor('depends'));
1443
+ const tree = c('--color-field-border');
1444
+ const dep = {
1445
+ dependsLabel: lowest(depends, [halo]),
1446
+ dependsLine: lowest(depends, [S]),
1447
+ dependsCrossRef: lowest([depends[0], depends[1], depends[2], depends[3] * lineOpacity], [S]),
1448
+ dependsLegend: lowest(depends, [legend]),
1449
+ treeSurface: lowest(tree, [S]),
1450
+ treeSurface2: lowest(tree, [S2]),
1451
+ };
1452
+ const dependsVsUses = ratio(depends, value(relationColor('uses')));
1453
+
1454
+ const mark = fills('log', '.lc-log-viewer__match');
1455
+ const match = ratio(value(mark.color), value(mark['background-color']));
1456
+
1457
+ const track = value(fills('progress', '.progress-bar__track')['background-color']);
1458
+ const bar = value(fills('progress', '.progress-bar--secondary .progress-bar__fill')['background-color']);
1459
+ const ringTrack = value(fills('ring', '.lc-progress-ring__track').stroke);
1460
+ const arc = (v: string) => value(fills('ring', `.lc-progress-ring__value--${v}`).stroke);
1461
+ const secondaryCells = {
1462
+ track: Math.min(lowest(bar, pages.map((p) => over(track, p))), lowest(arc('secondary'), pages.map((p) => over(ringTrack, p)))),
1463
+ page: Math.min(lowest(bar, pages), lowest(arc('secondary'), pages)),
1464
+ vsInfo: ratio(arc('secondary'), arc('info')),
1465
+ };
1466
+
1467
+ // The log variant: surface, the warn and error tints, each with and without hover.
1468
+ const logBg = value(fills('log', '.lc-log-viewer--log')['background-color']);
1469
+ const hover = parse(fills('log', '.lc-log-viewer__line:hover')['background-color']);
1470
+ const tintOf = (lv: string) => over(parse(fills('log', `.lc-log-viewer--log .lc-log-viewer__line--${lv}`)['background-color']), logBg);
1471
+ const logLines = [logBg, tintOf('warn'), tintOf('error')].flatMap((b) => [b, over(hover, b)]);
1472
+ const gray = parse(fills('log', i === 0 ? ':root.light .lc-log-viewer--log .ansi-gray' : '.ansi-gray').color);
1473
+ const ansiGray = { plain: lowest(gray, [logBg]), worst: lowest(gray, logLines) };
1474
+
1475
+ return { dl, ncUnread, dep, dependsVsUses, match, secondaryCells, ansiGray };
1476
+ }
1477
+
1478
+ it('AC-1, AC-2: dl link and primary values reach 4.5:1 on the page and a hovered row; the focus ring 3:1', () => {
1479
+ const { dl } = measured();
1480
+ for (const key of Object.keys(dl) as (keyof typeof dl)[]) {
1481
+ expect([key, floor(dl[key])]).toEqual([key, expected.dl[key][i]]);
1482
+ }
1483
+ expect(Math.min(dl.linkPage, dl.linkHover, dl.linkHoverInkPage, dl.linkHoverInkHover, dl.primaryPage, dl.primaryHover)).toBeGreaterThanOrEqual(4.5);
1484
+ expect(dl.ring).toBeGreaterThanOrEqual(3);
1485
+ });
1486
+
1487
+ it('AC-7: the NC unread message and time reach 4.5:1 on every unread row', () => {
1488
+ const { ncUnread } = measured();
1489
+ expect(floor(ncUnread)).toBe(expected.ncUnread[i]);
1490
+ expect(ncUnread).toBeGreaterThanOrEqual(4.5);
1491
+ });
1492
+
1493
+ it('AC-8: the depends label reaches 4.5:1; depends, cross-ref, legend and tree lines 3:1', () => {
1494
+ const { dep, dependsVsUses } = measured();
1495
+ for (const key of Object.keys(dep) as (keyof typeof dep)[]) {
1496
+ expect([key, floor(dep[key])]).toEqual([key, expected.dep[key][i]]);
1497
+ }
1498
+ expect(dep.dependsLabel).toBeGreaterThanOrEqual(4.5);
1499
+ expect(Math.min(...Object.values(dep))).toBeGreaterThanOrEqual(3);
1500
+ expect(floor(dependsVsUses)).toBe(expected.dependsVsUses[i]);
1501
+ });
1502
+
1503
+ it('AC-9: the search match ink reaches 4.5:1 on its opaque marker', () => {
1504
+ const { match } = measured();
1505
+ expect(floor(match)).toBe(expected.match[i]);
1506
+ expect(match).toBeGreaterThanOrEqual(4.5);
1507
+ });
1508
+
1509
+ it('AC-10, AC-11: the secondary bar fill and ring arc reach 3:1 on the track and the page, 1.5:1 from info', () => {
1510
+ const { secondaryCells } = measured();
1511
+ for (const key of Object.keys(secondaryCells) as (keyof typeof secondaryCells)[]) {
1512
+ expect([key, floor(secondaryCells[key])]).toEqual([key, expected.secondary[key][i]]);
1513
+ }
1514
+ expect(Math.min(secondaryCells.track, secondaryCells.page)).toBeGreaterThanOrEqual(3);
1515
+ expect(secondaryCells.vsInfo).toBeGreaterThanOrEqual(1.5);
1516
+ });
1517
+
1518
+ it('AC-13: the log variant gray ANSI text reaches 4.5:1 on every line', () => {
1519
+ const { ansiGray } = measured();
1520
+ expect([floor(ansiGray.plain), floor(ansiGray.worst)]).toEqual([expected.ansiGray.plain[i], expected.ansiGray.worst[i]]);
1521
+ expect(ansiGray.worst).toBeGreaterThanOrEqual(4.5);
1522
+ });
1523
+
1524
+ it('AC-15: the shared status values keep their hex; the new token has the design 036 values', () => {
1525
+ const shared: Record<string, [string, string]> = {
1526
+ '--color-success': ['#8ea475', '#8ea475'],
1527
+ '--color-error': ['#9d0e0e', '#9d0e0e'],
1528
+ '--color-warning': ['#e1a040', '#e1a040'],
1529
+ '--color-info': ['#3b6588', '#3b6588'],
1530
+ '--color-secondary-graphic': ['#5a798c', '#7097af'],
1531
+ };
1532
+ for (const [name, hexes] of Object.entries(shared)) expect([name, hex(tokens, name)]).toEqual([name, hexes[i]]);
1533
+ });
1534
+ });
1535
+
1536
+ it('AC-9, AC-13: the terminal match reads 12.90:1 on every line; gray 4.5:1 on every line', () => {
1537
+ const T = parse(fills('log', '.lc-log-viewer--terminal')['background-color']);
1538
+ const hover = parse(fills('log', '.lc-log-viewer__line:hover')['background-color']);
1539
+ const tint = (lv: string) => over(parse(fills('log', `.lc-log-viewer--terminal .lc-log-viewer__line--${lv}`)['background-color']), T);
1540
+ const lines = [T, tint('warn'), tint('error')].flatMap((b) => [b, over(hover, b)]);
1541
+ const mark = fills('log', '.lc-log-viewer--terminal .lc-log-viewer__match');
1542
+ const gray = parse(fills('log', '.ansi-gray').color);
1543
+ const lowest = (fg: Rgba, bases: Rgba[]) => Math.min(...bases.map((bg) => ratio(fg, bg)));
1544
+ const terminal = {
1545
+ match: ratio(parse(mark.color), parse(mark['background-color'])),
1546
+ grayPlain: ratio(gray, T),
1547
+ grayWorst: lowest(gray, lines),
1548
+ };
1549
+ expect(Object.fromEntries(Object.entries(terminal).map(([k, v]) => [k, floor(v)]))).toEqual({ match: 12.9, grayPlain: 5.81, grayWorst: 4.61 });
1550
+ expect(Math.min(...Object.values(terminal))).toBeGreaterThanOrEqual(4.5);
1551
+ });
1552
+ });
@@ -1,3 +1,4 @@
1
+ import { Params } from "@angular/router";
1
2
  import * as i0 from "@angular/core";
2
3
  /** Visual variant of `lc-card`. */
3
4
  export type CardVariant = 'elevated' | 'outlined' | 'flat' | 'raised' | 'dashed';
@@ -12,6 +13,8 @@ export type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;
12
13
  * - Border radius options (none, sm, md, lg, xl, full)
13
14
  * - Optional title display, with an optional leading icon tile
14
15
  * - Content projection for flexible body content
16
+ * - Link mode (3.24): `href` or `route` make the whole card a real link,
17
+ * the title stays a heading
15
18
  *
16
19
  * @example
17
20
  * ```html
@@ -23,6 +26,12 @@ export type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;
23
26
  * <lc-card title="Repository" subtitle="Checkout &amp; profile" icon="git-branch">
24
27
  * ...
25
28
  * </lc-card>
29
+ *
30
+ * <!-- An overview card that opens a page: heading h2 with a link inside -->
31
+ * <lc-card title="SAP-Ist" variant="outlined" [headingLevel]="2"
32
+ * [route]="['/quellen', 'sap-ist']" [queryParams]="{ ansicht: 'laeufe' }">
33
+ * ...
34
+ * </lc-card>
26
35
  * ```
27
36
  */
28
37
  export declare class CardComponent {
@@ -75,7 +84,8 @@ export declare class CardComponent {
75
84
  * `--shadow-auth-card`, no accent line — the sign-in card
76
85
  * - dashed: the muted "coming soon" tile — `--color-muted-tile` fill, 1 px
77
86
  * dashed `--color-muted-tile-border`, secondary ink, icons in
78
- * `--color-muted-tile-icon`. Never clickable: `clickable` is ignored
87
+ * `--color-muted-tile-icon`. Never clickable: `clickable` is ignored;
88
+ * `href` and `route` are ignored too
79
89
  * @default 'elevated'
80
90
  */
81
91
  variant: import("@angular/core").InputSignal<CardVariant>;
@@ -85,7 +95,8 @@ export declare class CardComponent {
85
95
  */
86
96
  padding: import("@angular/core").InputSignal<"sm" | "md" | "none" | "xs" | "lg">;
87
97
  /**
88
- * Whether the card is clickable (hover effects, cursor pointer)
98
+ * Whether the card is clickable (hover effects, cursor pointer). Has no
99
+ * effect on a link card (`href`/`route`).
89
100
  * @default false
90
101
  */
91
102
  clickable: import("@angular/core").InputSignal<boolean>;
@@ -107,18 +118,60 @@ export declare class CardComponent {
107
118
  */
108
119
  headingLevel: import("@angular/core").InputSignal<CardHeadingLevel>;
109
120
  /**
110
- * ARIA label for accessibility
121
+ * Accessible name. On a link card (`href`/`route`) it names the link
122
+ * instead of the card; it must contain the visible title (WCAG 2.5.3) and
123
+ * the heading reads it too, so keep it short. Without it the link is named
124
+ * by the title, and the card's content (badge, list) is read as normal
125
+ * content. On other cards it labels the card.
126
+ * @default undefined
111
127
  */
112
128
  ariaLabel: import("@angular/core").InputSignal<string | undefined>;
113
129
  /**
114
- * Emitted when card is clicked (if clickable)
130
+ * URL the card links to (3.24, quellen row 1). The card becomes a real
131
+ * link: the built-in `title` is an `<a href>` inside its heading, and the
132
+ * link covers the whole card (click, Enter, middle click, "open in new
133
+ * tab", "copy link"). No router needed; works in prerendered HTML. Wins
134
+ * over `route`. Ignored by `dashed`. On a link card `clickable` has no
135
+ * effect and `cardClick` is not emitted.
136
+ * @default undefined
137
+ */
138
+ href: import("@angular/core").InputSignal<string | undefined>;
139
+ /**
140
+ * In-app route the card links to through the Angular router (`RouterLink`
141
+ * commands: `'/quellen/usm'` or `['/quellen', id]`), relative to the
142
+ * consumer's route, rendered as a real `<a href>` like `href`. Needs the
143
+ * app's router (`provideRouter`); without one, use `href`. `''` and `null`
144
+ * mean no link.
145
+ * @default undefined
146
+ */
147
+ route: import("@angular/core").InputSignal<string | readonly unknown[] | null | undefined>;
148
+ /**
149
+ * Query parameters of `route`, as `RouterLink`'s `queryParams`
150
+ * (`{ ansicht: 'laeufe' }`). Ignored with `href` (write the query into the
151
+ * URL).
152
+ * @default undefined
153
+ */
154
+ queryParams: import("@angular/core").InputSignal<Params | null | undefined>;
155
+ /**
156
+ * Emitted when a `clickable` card is clicked or activated with Enter or
157
+ * Space. Not emitted on a link card (`href`/`route`): the link navigates by
158
+ * itself (one navigation); bind the native `(click)` for analytics.
115
159
  */
116
160
  readonly cardClick: import("@angular/core").OutputEmitterRef<MouseEvent>;
161
+ /**
162
+ * How the card links (3.24): `href` wins over `route`; `''`/`null` routes
163
+ * are no link, and the dashed "coming soon" tile never links.
164
+ */
165
+ protected readonly linkMode: import("@angular/core").Signal<"href" | "route" | null>;
117
166
  /**
118
167
  * Whether the card acts as a button: `clickable`, except for the dashed
119
- * "coming soon" tile, which is never interactive.
168
+ * "coming soon" tile, which is never interactive, and a link card, whose
169
+ * anchor is the only interactive element.
120
170
  */
121
171
  protected isClickable: import("@angular/core").Signal<boolean>;
172
+ /** `ariaLabel` on the card; on a link card it names the anchor instead. */
173
+ protected readonly cardAriaLabel: import("@angular/core").Signal<string | undefined>;
174
+ constructor();
122
175
  /**
123
176
  * Compute card CSS classes based on inputs
124
177
  */
@@ -207,6 +260,21 @@ export declare class CardComponent {
207
260
  "required": false;
208
261
  "isSignal": true;
209
262
  };
263
+ "href": {
264
+ "alias": "href";
265
+ "required": false;
266
+ "isSignal": true;
267
+ };
268
+ "route": {
269
+ "alias": "route";
270
+ "required": false;
271
+ "isSignal": true;
272
+ };
273
+ "queryParams": {
274
+ "alias": "queryParams";
275
+ "required": false;
276
+ "isSignal": true;
277
+ };
210
278
  }, {
211
279
  "cardClick": "cardClick";
212
280
  }, never, ["[card-badge]", "[card-header-action]", "[card-header]", "*", "[card-footer]"], true, never>;
@@ -6,6 +6,16 @@ export interface FileUploadFile {
6
6
  size: number;
7
7
  type: string;
8
8
  }
9
+ /** Why `lc-file-upload` rejected a file. */
10
+ export type FileUploadRejectionCode = 'size' | 'type';
11
+ /** Payload of `lc-file-upload`'s `fileRejected`. */
12
+ export interface FileUploadRejection {
13
+ file: File;
14
+ /** Fixed English text ("File type not accepted", "File too large"); translate from `code`. */
15
+ reason: string;
16
+ /** `'type'`: matches no `accept` token (checked first); `'size'`: over `maxSizeBytes`. */
17
+ code: FileUploadRejectionCode;
18
+ }
9
19
  /**
10
20
  * File upload / dropzone component with drag & drop support.
11
21
  *
@@ -21,11 +31,21 @@ export interface FileUploadFile {
21
31
  */
22
32
  export declare class FileUploadComponent {
23
33
  /**
24
- * Accepted file types (e.g. ".pdf,.jpg,image/*").
34
+ * Accepted types, comma-separated, case-insensitive: extensions (`.csv`),
35
+ * MIME types (`text/csv`), wildcards (`image/*`); `*\/*` accepts every type.
36
+ * Since 3.24 checked for picked and dropped files alike; a file matching no
37
+ * token is rejected with `code: 'type'`. Tokens that are neither, such as a
38
+ * MIME type with parameters (`text/csv;charset=utf-8`), are ignored, as by
39
+ * the browser's picker; none left = every type. Prefer extensions: systems report MIME
40
+ * types differently (a CSV can arrive as `application/vnd.ms-excel`).
41
+ * @default ''
25
42
  */
26
43
  accept: import("@angular/core").InputSignal<string>;
27
44
  /**
28
- * Allow multiple files.
45
+ * Allow multiple files. `false`: one file. A new pick or drop replaces the
46
+ * selection with its first accepted file; if all its files are rejected
47
+ * (size or type), the selection is cleared and `filesSelected` emits `[]`.
48
+ * `true`: accepted files are added to the selection.
29
49
  * @default false
30
50
  */
31
51
  multiple: import("@angular/core").InputSignal<boolean>;
@@ -60,17 +80,24 @@ export declare class FileUploadComponent {
60
80
  maxSizeLabel: import("@angular/core").InputSignal<string>;
61
81
  /** Accessible name of a selected file's remove button. */
62
82
  removeFileAriaLabel: import("@angular/core").InputSignal<string>;
83
+ /**
84
+ * BCP 47 locale of the shown sizes (max line, drop zone name, file list):
85
+ * `'de-DE'` gives "20,0 MB". Units stay B/KB/MB (1024 base). Empty keeps
86
+ * the built-in notation ("20.0 MB"); an invalid locale falls back to it.
87
+ * @default ''
88
+ */
89
+ locale: import("@angular/core").InputSignal<string>;
63
90
  /**
64
91
  * Emitted with selected files after validation.
65
92
  */
66
93
  readonly filesSelected: import("@angular/core").OutputEmitterRef<FileUploadFile[]>;
67
94
  /**
68
- * Emitted when a file is rejected (too large or wrong type).
95
+ * Emitted once per rejected file, before `filesSelected`. `code`: `'type'`
96
+ * (matches no `accept` token; checked first) or `'size'` (over
97
+ * `maxSizeBytes`). `reason` is a fixed English text ("File type not
98
+ * accepted", "File too large"); translate from `code`.
69
99
  */
70
- readonly fileRejected: import("@angular/core").OutputEmitterRef<{
71
- file: File;
72
- reason: string;
73
- }>;
100
+ readonly fileRejected: import("@angular/core").OutputEmitterRef<FileUploadRejection>;
74
101
  protected isDragOver: import("@angular/core").WritableSignal<boolean>;
75
102
  protected selectedFiles: import("@angular/core").WritableSignal<FileUploadFile[]>;
76
103
  protected readonly fileInputRef: import("@angular/core").Signal<ElementRef<HTMLInputElement> | undefined>;
@@ -80,6 +107,13 @@ export declare class FileUploadComponent {
80
107
  * accepted and max lines it shows, comma-separated.
81
108
  */
82
109
  protected readonly dropzoneAriaLabel: import("@angular/core").Signal<string>;
110
+ /**
111
+ * The size notation of `locale` (3.24): whole bytes, KB/MB to one decimal.
112
+ * Null keeps the built-in `toFixed` notation (no or an invalid locale).
113
+ */
114
+ private readonly sizeFormats;
115
+ /** The `accept` tokens the check knows, lower-cased; empty = every type. */
116
+ private readonly acceptTokens;
83
117
  protected onDragOver(event: DragEvent): void;
84
118
  protected onDragLeave(event: DragEvent): void;
85
119
  protected onDrop(event: DragEvent): void;
@@ -87,6 +121,8 @@ export declare class FileUploadComponent {
87
121
  protected openFilePicker(): void;
88
122
  protected removeFile(index: number): void;
89
123
  protected formatSize(bytes: number): string;
124
+ /** Why `file` is rejected: type first, then size; null when it is accepted. */
125
+ private rejectionOf;
90
126
  private processFiles;
91
127
  static ɵfac: i0.ɵɵFactoryDeclaration<FileUploadComponent, never>;
92
128
  static ɵcmp: i0.ɵɵComponentDeclaration<FileUploadComponent, "lc-file-upload", never, {
@@ -130,6 +166,11 @@ export declare class FileUploadComponent {
130
166
  "required": false;
131
167
  "isSignal": true;
132
168
  };
169
+ "locale": {
170
+ "alias": "locale";
171
+ "required": false;
172
+ "isSignal": true;
173
+ };
133
174
  }, {
134
175
  "filesSelected": "filesSelected";
135
176
  "fileRejected": "fileRejected";