@lifecorner/angular-ui-kit 3.25.1 → 3.27.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.
Files changed (75) hide show
  1. package/fesm2022/lifecorner-angular-ui-kit-button.mjs +18 -2
  2. package/fesm2022/lifecorner-angular-ui-kit-button.mjs.map +1 -1
  3. package/fesm2022/lifecorner-angular-ui-kit-callout.mjs +2 -2
  4. package/fesm2022/lifecorner-angular-ui-kit-callout.mjs.map +1 -1
  5. package/fesm2022/lifecorner-angular-ui-kit-chat.mjs +2 -2
  6. package/fesm2022/lifecorner-angular-ui-kit-chat.mjs.map +1 -1
  7. package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs +1 -0
  8. package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs.map +1 -1
  9. package/fesm2022/lifecorner-angular-ui-kit-color-picker.mjs +2 -2
  10. package/fesm2022/lifecorner-angular-ui-kit-color-picker.mjs.map +1 -1
  11. package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs +1 -0
  12. package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs.map +1 -1
  13. package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs +1 -0
  14. package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs.map +1 -1
  15. package/fesm2022/lifecorner-angular-ui-kit-date-range-picker.mjs +2 -2
  16. package/fesm2022/lifecorner-angular-ui-kit-date-range-picker.mjs.map +1 -1
  17. package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs +2 -2
  18. package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs.map +1 -1
  19. package/fesm2022/lifecorner-angular-ui-kit-dependency-viewer.mjs +47 -9
  20. package/fesm2022/lifecorner-angular-ui-kit-dependency-viewer.mjs.map +1 -1
  21. package/fesm2022/lifecorner-angular-ui-kit-description-list.mjs +2 -2
  22. package/fesm2022/lifecorner-angular-ui-kit-description-list.mjs.map +1 -1
  23. package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs +2 -2
  24. package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs.map +1 -1
  25. package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs +8 -4
  26. package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs.map +1 -1
  27. package/fesm2022/lifecorner-angular-ui-kit-filter-bar.mjs +2 -2
  28. package/fesm2022/lifecorner-angular-ui-kit-filter-bar.mjs.map +1 -1
  29. package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs +2 -2
  30. package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
  31. package/fesm2022/lifecorner-angular-ui-kit-gantt-chart.mjs +2 -2
  32. package/fesm2022/lifecorner-angular-ui-kit-gantt-chart.mjs.map +1 -1
  33. package/fesm2022/lifecorner-angular-ui-kit-header.mjs +3 -2
  34. package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
  35. package/fesm2022/lifecorner-angular-ui-kit-input.mjs +2 -2
  36. package/fesm2022/lifecorner-angular-ui-kit-input.mjs.map +1 -1
  37. package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs +2 -2
  38. package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs.map +1 -1
  39. package/fesm2022/lifecorner-angular-ui-kit-list.mjs +1 -0
  40. package/fesm2022/lifecorner-angular-ui-kit-list.mjs.map +1 -1
  41. package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs +2 -2
  42. package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs.map +1 -1
  43. package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs +1 -0
  44. package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs.map +1 -1
  45. package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs +2 -2
  46. package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs.map +1 -1
  47. package/fesm2022/lifecorner-angular-ui-kit-number-input.mjs +2 -2
  48. package/fesm2022/lifecorner-angular-ui-kit-number-input.mjs.map +1 -1
  49. package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs +2 -2
  50. package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs.map +1 -1
  51. package/fesm2022/lifecorner-angular-ui-kit-scatter-plot.mjs +2 -2
  52. package/fesm2022/lifecorner-angular-ui-kit-scatter-plot.mjs.map +1 -1
  53. package/fesm2022/lifecorner-angular-ui-kit-search-input.mjs +2 -2
  54. package/fesm2022/lifecorner-angular-ui-kit-search-input.mjs.map +1 -1
  55. package/fesm2022/lifecorner-angular-ui-kit-select.mjs +2 -2
  56. package/fesm2022/lifecorner-angular-ui-kit-select.mjs.map +1 -1
  57. package/fesm2022/lifecorner-angular-ui-kit-split-pane.mjs +2 -2
  58. package/fesm2022/lifecorner-angular-ui-kit-split-pane.mjs.map +1 -1
  59. package/fesm2022/lifecorner-angular-ui-kit-table.mjs +3 -2
  60. package/fesm2022/lifecorner-angular-ui-kit-table.mjs.map +1 -1
  61. package/fesm2022/lifecorner-angular-ui-kit-tabs.mjs +2 -2
  62. package/fesm2022/lifecorner-angular-ui-kit-tabs.mjs.map +1 -1
  63. package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs +2 -2
  64. package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs.map +1 -1
  65. package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs +2 -2
  66. package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs.map +1 -1
  67. package/fesm2022/lifecorner-angular-ui-kit-toggle-group.mjs +2 -2
  68. package/fesm2022/lifecorner-angular-ui-kit-toggle-group.mjs.map +1 -1
  69. package/package.json +1 -1
  70. package/src/styles/_reset.scss +4 -2
  71. package/src/styles/focus-indicators.spec.ts +199 -0
  72. package/types/lifecorner-angular-ui-kit-button.d.ts +20 -0
  73. package/types/lifecorner-angular-ui-kit-dependency-viewer.d.ts +33 -4
  74. package/types/lifecorner-angular-ui-kit-description-list.d.ts +4 -0
  75. package/types/lifecorner-angular-ui-kit-file-upload.d.ts +11 -1
@@ -0,0 +1,199 @@
1
+ import { compile } from 'sass';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * Request 042 (3.27, design 042 D-1 to D-9): the guard for F-18 and F-19.
6
+ * Every element of design §6.1 rows B–F has a `:focus-visible` indicator of its
7
+ * own (kit ring: 2 px `--color-primary-500`), `Highlight` in forced colours;
8
+ * the global button reset only takes the ring from mouse focus; a field's hover
9
+ * rule never applies while it is focused. Read from the compiled SCSS sources.
10
+ */
11
+ const sheets = new Map<string, string>();
12
+ const css = (file: string): string => {
13
+ if (!sheets.has(file)) sheets.set(file, compile(resolve(__dirname, file.startsWith('_') ? file : `../lib/${file}`)).css);
14
+ return sheets.get(file) as string;
15
+ };
16
+
17
+ const rulesOf = (text: string) =>
18
+ [...text.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({
19
+ selectors: m[1].split(/,(?![^(]*\))/).map((s) => s.trim()),
20
+ body: m[2],
21
+ }));
22
+ const decls = (body: string): Record<string, string> =>
23
+ Object.fromEntries([...body.matchAll(/([\w-]+):\s*([^;]+);/g)].map((m) => [m[1], m[2].replace(/\s+/g, ' ').trim()]));
24
+ const forcedBlock = /@media \(forced-colors: active\) \{((?:[^{}]*\{[^{}]*\})*)\s*\}/g;
25
+
26
+ /** Every rule naming `selector`, merged in source order, forced-colours blocks left out. */
27
+ const fills = (file: string, selector: string): Record<string, string> => {
28
+ const found = rulesOf(css(file).replace(forcedBlock, '')).filter((r) => r.selectors.includes(selector));
29
+ expect([file, selector, found.length > 0]).toEqual([file, selector, true]);
30
+ return Object.assign({}, ...found.map((r) => decls(r.body)));
31
+ };
32
+ /** The same, inside the forced-colours blocks only. */
33
+ const forcedFills = (file: string, selector: string): Record<string, string> => {
34
+ const inner = [...css(file).matchAll(forcedBlock)].map((m) => m[1]).join('\n');
35
+ return Object.assign({}, ...rulesOf(inner).filter((r) => r.selectors.includes(selector)).map((r) => decls(r.body)));
36
+ };
37
+
38
+ const RING = '2px solid var(--color-primary-500)';
39
+
40
+ /** Design 042 §3: [sheet, element class, outline offset]. Rows B–D, kanban (D-3), remove (D-8), dep canvas (D-7). */
41
+ const rings: [string, string, string][] = [
42
+ ['rich-text-editor/rich-text-editor.component.scss', '.lc-rich-text-editor__toolbar-btn', '2px'],
43
+ ['rich-text-editor/rich-text-editor.component.scss', '.lc-rich-text-editor__mode-btn', '2px'],
44
+ ['document-viewer/document-viewer.component.scss', '.doc-viewer__btn', '2px'],
45
+ ['document-viewer/document-viewer.component.scss', '.doc-viewer__download-btn', '2px'],
46
+ ['filter-bar/filter-bar.component.scss', '.lc-filter-bar__toggle-btn', '-2px'],
47
+ ['select/select.component.scss', '.lc-select__clear', '2px'],
48
+ ['gallery/gallery.component.scss', '.gallery__filter-btn', '2px'],
49
+ ['log-viewer/log-viewer.component.scss', '.lc-log-viewer__level-pill', '2px'],
50
+ ['callout/callout.component.scss', '.callout__dismiss', '2px'],
51
+ ['notification-center/notification-center.component.scss', '.lc-nc__action-btn', '2px'],
52
+ ['notification-center/notification-center.component.scss', '.lc-nc__filter', '2px'],
53
+ ['tabs/tabs.component.scss', '.lc-tabs__panel', '-2px'],
54
+ ['chat/chat.component.scss', '.lc-chat__attachment', '2px'],
55
+ ['chat/chat.component.scss', '.lc-chat__attach-btn', '2px'],
56
+ ['chat/chat.component.scss', '.lc-chat__send-btn', '2px'],
57
+ ['chat/chat.component.scss', '.lc-chat__pending-remove', '2px'],
58
+ ['description-list/description-list.component.scss', '.lc-dl__link', '2px'],
59
+ ['color-picker/color-picker.component.scss', '.color-picker__native', '2px'],
60
+ ['table/table.component.scss', '.lc-table__checkbox', '2px'],
61
+ ['number-input/number-input.component.scss', '.number-input__button', '-2px'],
62
+ ['tag-input/tag-input.component.scss', '.lc-tag-input__tag-remove', '2px'],
63
+ ['file-upload/file-upload.component.scss', '.file-upload__file-remove', '2px'],
64
+ // Review 042 r1: inset, an outside ring measured 2.19:1 on an over-limit column (light).
65
+ ['kanban-board/kanban-board.component.scss', '.lc-kanban__card', '-2px'],
66
+ ['dependency-viewer/dependency-viewer.component.scss', '.dep-viewer__canvas--scroll', '-2px'],
67
+ ];
68
+
69
+ describe('Focus indicators (request 042)', () => {
70
+ describe('D-1: the global button reset', () => {
71
+ it('AC-5: takes the outline from mouse focus only, so a button with no ring of its own keeps the browser ring on Tab', () => {
72
+ const reset = css('_reset.scss');
73
+ expect(reset).toMatch(/\nbutton:focus:not\(:focus-visible\) \{\s*outline: none;\s*\}/);
74
+ expect(reset).not.toMatch(/\nbutton:focus \{/);
75
+ });
76
+ });
77
+
78
+ describe('D-2: the kit ring on every §6.1 B–D element', () => {
79
+ it.each(rings)('AC-2: %s %s draws a solid 2 px primary-500 outline (offset %s), no halo', (file, cls, offset) => {
80
+ const ring = fills(file, `${cls}:focus-visible`);
81
+ expect([ring.outline, ring['outline-offset']]).toEqual([RING, offset]);
82
+ expect(ring['box-shadow']).toBeUndefined();
83
+ expect(ring.transition).toBeUndefined();
84
+ });
85
+
86
+ it.each(rings)('AC-3: %s %s turns Highlight in forced colours', (file, cls) => {
87
+ expect(forcedFills(file, `${cls}:focus-visible`)['outline-color']).toBe('Highlight');
88
+ });
89
+
90
+ it.each(rings)('AC-4: %s %s draws no ring on mouse focus (no outline on a bare :focus rule)', (file, cls) => {
91
+ const bare = rulesOf(css(file).replace(forcedBlock, '')).filter((r) => r.selectors.includes(`${cls}:focus`));
92
+ for (const r of bare) expect([cls, decls(r.body).outline ?? 'none']).toEqual([cls, 'none']);
93
+ });
94
+
95
+ it('AC-2: the dimmed buttons turn fully opaque on focus (the ring is not dimmed with them)', () => {
96
+ expect(fills('callout/callout.component.scss', '.callout__dismiss').opacity).toBe('0.6');
97
+ expect(fills('callout/callout.component.scss', '.callout__dismiss:focus-visible').opacity).toBe('1');
98
+ expect(fills('tag-input/tag-input.component.scss', '.lc-tag-input__tag-remove').opacity).toBe('0.7');
99
+ expect(fills('tag-input/tag-input.component.scss', '.lc-tag-input__tag-remove:focus-visible').opacity).toBe('1');
100
+ });
101
+
102
+ // Offset 2px → -2px by review 042 r1: outside, the ring sat on the column tint
103
+ // (2.19:1 on an over-limit column, light); inset it sits on the card (4.36 / 9.12).
104
+ it('AC-2: the kanban card drops its 20 % halo (1.28:1) for an inset ring on the card and keeps the border change', () => {
105
+ expect(fills('kanban-board/kanban-board.component.scss', '.lc-kanban__card:focus-visible')).toEqual({
106
+ outline: RING,
107
+ 'outline-offset': '-2px',
108
+ 'border-color': 'var(--color-primary-500)',
109
+ });
110
+ });
111
+ });
112
+
113
+ describe('D-3: indicators that forced colours drop', () => {
114
+ const forced: [string, string, Record<string, string>][] = [
115
+ ['toggle-group/toggle-group.component.scss', '.lc-toggle-group__btn:focus-visible', { outline: '2px solid Highlight', 'outline-offset': '-2px' }],
116
+ ['list/list.component.scss', '.lc-list__item:focus-visible', { outline: '2px solid Highlight', 'outline-offset': '-2px' }],
117
+ ['gantt-chart/gantt-chart.component.scss', '.lc-gantt__bar:focus-visible', { outline: '2px solid Highlight', 'outline-offset': '2px' }],
118
+ ['split-pane/split-pane.component.scss', '.lc-split-pane__separator:focus-visible', { outline: '2px solid Highlight', 'outline-offset': '-2px' }],
119
+ ['scatter-plot/scatter-plot.component.scss', '.lc-scatter-plot__dot:focus-visible', { stroke: 'Highlight' }],
120
+ ];
121
+
122
+ it.each(forced)('AC-3: %s %s has its own forced-colours indicator', (file, selector, expected) => {
123
+ expect(forcedFills(file, selector)).toEqual(expected);
124
+ });
125
+
126
+ it('AC-7: their normal-mode indicators are unchanged', () => {
127
+ expect(fills('toggle-group/toggle-group.component.scss', '.lc-toggle-group__btn:focus-visible')['box-shadow']).toBe('inset 0 0 0 2px var(--color-primary-500)');
128
+ expect(fills('list/list.component.scss', '.lc-list__item:focus-visible')['box-shadow']).toBe('inset 0 0 0 2px var(--color-primary-500)');
129
+ expect(fills('gantt-chart/gantt-chart.component.scss', '.lc-gantt__bar:focus-visible')['box-shadow']).toBe('0 0 0 2px var(--color-surface), 0 0 0 4px var(--color-primary-500)');
130
+ expect(fills('split-pane/split-pane.component.scss', '.lc-split-pane__separator:focus-visible::before').background).toBe('var(--color-primary-500)');
131
+ expect(fills('scatter-plot/scatter-plot.component.scss', '.lc-scatter-plot__dot:focus-visible').stroke).toBe('var(--color-text-primary)');
132
+ });
133
+ });
134
+
135
+ describe('D-9: the rich-text editing surface', () => {
136
+ const frame = '.lc-rich-text-editor:has(.lc-rich-text-editor__rich:focus-visible, .lc-rich-text-editor__textarea:focus-visible)';
137
+ const file = 'rich-text-editor/rich-text-editor.component.scss';
138
+
139
+ it('AC-2: the frame shows the field ring (lc-input pattern); the surfaces keep outline: none', () => {
140
+ expect(fills(file, frame)).toEqual({ 'border-color': 'var(--color-primary-500)', outline: RING, 'outline-offset': '0px' });
141
+ expect(fills(file, '.lc-rich-text-editor__rich').outline).toBe('none');
142
+ expect(fills(file, '.lc-rich-text-editor__textarea').outline).toBe('none');
143
+ });
144
+
145
+ it('AC-3: the ancestor ring has its own Highlight rule', () => {
146
+ expect(forcedFills(file, frame)).toEqual({ 'border-color': 'Highlight', 'outline-color': 'Highlight' });
147
+ });
148
+ });
149
+
150
+ describe('D-4: the light header on a dark page', () => {
151
+ const file = 'header/header.component.scss';
152
+ const own = ['.lc-header__nav-link', '.lc-header__logo', '.lc-header__context-info', '.lc-header__profile-trigger'];
153
+
154
+ it('AC-2: the header-owned rings read the private ring colour, primary-500 by default', () => {
155
+ for (const cls of own) {
156
+ expect([cls, fills(file, `${cls}:focus-visible`).outline]).toEqual([cls, '2px solid var(--_lc-header-ring, var(--color-primary-500))']);
157
+ }
158
+ });
159
+
160
+ it('AC-2: theme="light" pins the light primary-500: 4.36:1 on its white bar in both themes (dark primary-500 was 1.89)', () => {
161
+ const light = fills(file, '.lc-header--light');
162
+ expect([light['--_lc-header-ring'], light['--lc-header-bg']]).toEqual(['#208497', '#ffffff']);
163
+ const lum = (hex: string) =>
164
+ [1, 3, 5]
165
+ .map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
166
+ .map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
167
+ .reduce((sum, v, i) => sum + v * [0.2126, 0.7152, 0.0722][i], 0);
168
+ expect(Math.floor(((lum('#ffffff') + 0.05) / (lum('#208497') + 0.05)) * 100) / 100).toBe(4.36);
169
+ });
170
+ });
171
+
172
+ describe('D-6: focus wins over hover in every field with the pattern (F-19)', () => {
173
+ // [sheet, hover selector after the fix, declarations it keeps]
174
+ const hovers: [string, string, Record<string, string>][] = [
175
+ ['textarea/textarea.component.scss', '.lc-textarea--outline:hover:not(:disabled):not(:read-only):not(:focus)', { 'border-color': 'var(--color-primary-400)' }],
176
+ ['textarea/textarea.component.scss', '.lc-textarea--filled:hover:not(:disabled):not(:read-only):not(:focus)', { 'background-color': 'var(--color-surface-hover)' }],
177
+ ['datepicker/datepicker.component.scss', '.lc-datepicker:hover:not(:disabled):not([readonly]):not(:focus)', { 'border-color': 'var(--color-primary-400)' }],
178
+ ['datepicker/datepicker.component.scss', '.lc-datepicker.lc-datepicker--filled:hover:not(:disabled):not([readonly]):not(:focus)', { 'background-color': 'var(--color-surface-hover)', 'border-color': 'var(--color-border)' }],
179
+ ['input/input.component.scss', '.input-container:hover:not(.input-disabled):not(.input-readonly):not(:focus-within)', { 'border-color': 'var(--color-text-secondary)' }],
180
+ ['select/select.component.scss', '.lc-select:hover:not(.lc-select--disabled):not(:focus)', { 'border-color': 'var(--color-text-secondary)' }],
181
+ ['select/select.component.scss', '.lc-select.lc-select--filled:hover:not(.lc-select--disabled):not(:focus)', { 'background-color': 'var(--color-surface-hover)', 'border-color': 'var(--color-border)' }],
182
+ ['search-input/search-input.component.scss', '.search-input:hover:not(.search-input--disabled):not(:focus-within)', { 'border-color': 'var(--color-text-secondary)' }],
183
+ ['tag-input/tag-input.component.scss', '.lc-tag-input__container:hover:not(.lc-tag-input__container--focused)', { 'border-color': 'var(--color-text-secondary)' }],
184
+ ['date-range-picker/date-range-picker.component.scss', '.lc-drp__field:hover:not(.lc-drp__field--disabled):not(:focus-within)', { 'border-color': 'var(--color-text-secondary)' }],
185
+ ];
186
+
187
+ it.each(hovers)('AC-6/AC-7: %s %s keeps its look and excludes focus', (file, selector, expected) => {
188
+ expect(fills(file, selector)).toEqual(expected);
189
+ });
190
+
191
+ it.each(hovers)('AC-6: %s has no other hover rule on that field', (file, selector) => {
192
+ const field = selector.slice(0, selector.indexOf(':hover'));
193
+ const others = rulesOf(css(file).replace(forcedBlock, ''))
194
+ .flatMap((r) => r.selectors)
195
+ .filter((s) => s.startsWith(`${field}:hover`) && s !== selector);
196
+ expect(others).toEqual([]);
197
+ });
198
+ });
199
+ });
@@ -54,6 +54,19 @@ export declare class ButtonComponent {
54
54
  readonly iconOnly: import("@angular/core").InputSignal<boolean>;
55
55
  readonly fullWidth: import("@angular/core").InputSignal<boolean>;
56
56
  readonly ariaLabel: import("@angular/core").InputSignal<string>;
57
+ /**
58
+ * Id, or space-separated ids, of elements that describe the button, set as
59
+ * `aria-describedby` on the inner `<button>`: a screen reader reads the
60
+ * button's name and then their text, also while `disabled` or `loading`.
61
+ * Use it to tie a visible reason line ("Wählen Sie zuerst eine Datei.") to a
62
+ * disabled button; `disabledReason` stays a tooltip for pointers (with both
63
+ * set, the description comes from the ids, so the tooltip is not read
64
+ * twice). Empty renders no attribute. The kit does not check that the ids
65
+ * exist. Don't put `aria-describedby` on `<lc-button>` itself: it would stay
66
+ * on the host. (3.26)
67
+ * @default ''
68
+ */
69
+ readonly ariaDescribedBy: import("@angular/core").InputSignal<string>;
57
70
  readonly type: import("@angular/core").InputSignal<ButtonType>;
58
71
  /**
59
72
  * Native tooltip (title attribute) on the button.
@@ -146,6 +159,11 @@ export declare class ButtonComponent {
146
159
  "required": false;
147
160
  "isSignal": true;
148
161
  };
162
+ "ariaDescribedBy": {
163
+ "alias": "ariaDescribedBy";
164
+ "required": false;
165
+ "isSignal": true;
166
+ };
149
167
  "type": {
150
168
  "alias": "type";
151
169
  "required": false;
@@ -186,6 +204,8 @@ export declare class ButtonComponent {
186
204
  * `title`, `aria-*`) passes through untouched, and the component adds no event
187
205
  * listeners: after server-side rendering or prerendering the markup is a plain
188
206
  * `<a class="btn btn-primary btn-md" href="…">` that works without JavaScript.
207
+ * For a description, set `aria-describedby` on the anchor itself; unlike
208
+ * `lc-button` there is no `ariaDescribedBy` input (3.26).
189
209
  * `routerLink` can sit on the same anchor for in-app navigation.
190
210
  *
191
211
  * Same variants, sizes and modifiers as `lc-button` (it shares its stylesheet).
@@ -269,19 +269,30 @@ export declare class DependencyViewerComponent {
269
269
  * `contain` fits both axes, so everything is visible at once but a big graph
270
270
  * shrinks to get there. `fit-width` fits the width only and lets the viewer grow
271
271
  * as tall as the graph needs, so the nodes keep a readable size and the *page*
272
- * scrolls; the graph itself never scrolls internally. Past roughly 20 nodes a
273
- * process view generally wants `fit-width`.
272
+ * scrolls; past `minNodeSize`, a graph wider than the frame scrolls sideways
273
+ * inside it. Past roughly 20 nodes a process view generally wants `fit-width`.
274
274
  */
275
275
  readonly fitMode: import("@angular/core").InputSignal<DependencyFitMode | null>;
276
276
  /**
277
277
  * Smallest node width, in px, that fitting may shrink to. Below it the fit stops
278
278
  * scaling down and lets the graph overflow instead — which `fit-width` turns into
279
- * page scroll, and `contain` into clipping. Pair it with `fit-width`.
279
+ * sideways scrolling of the frame (pinned to the start, see `regionLabel`), and
280
+ * `contain` into clipping. Pair it with `fit-width`.
280
281
  *
281
282
  * Nodes are 160px wide unscaled (wider — up to 240px — when the graph's labels
282
- * need it), so e.g. `80` allows shrinking to half size.
283
+ * need it), so e.g. `80` allows shrinking to half size. `200` keeps the 12 px
284
+ * labels at 10 px or more whatever the node width (160–240 px): the phone
285
+ * recipe is `fitMode="fit-width"` with `minNodeSize` 200.
283
286
  */
284
287
  readonly minNodeSize: import("@angular/core").InputSignal<number | null>;
288
+ /**
289
+ * Accessible name of the canvas while it scrolls sideways (`fitMode="fit-width"`
290
+ * with a `minNodeSize` the frame can't hold). It is then a focusable
291
+ * `role="region"`, scrolled with the arrow keys. Set a short, localised name
292
+ * that is unique on the page. Not used otherwise.
293
+ * @default 'Dependency graph'
294
+ */
295
+ readonly regionLabel: import("@angular/core").InputSignal<string>;
285
296
  /**
286
297
  * Viewport navigation: drag-to-pan, wheel-zoom and the toolbar's zoom buttons.
287
298
  * `false` freezes the viewport — node selection, expansion and collapse keep
@@ -378,6 +389,19 @@ export declare class DependencyViewerComponent {
378
389
  protected effectiveFitMode: import("@angular/core").Signal<DependencyFitMode>;
379
390
  /** The host only gives up its configured height when the fit made it grow. */
380
391
  protected hostHeight: import("@angular/core").Signal<string>;
392
+ /**
393
+ * Horizontal extent of the graph, in layout units, while `fit-width` is held
394
+ * at `minNodeSize` and the graph is wider than the frame; `null` otherwise.
395
+ * The canvas then scrolls sideways instead of clipping both ends.
396
+ */
397
+ private scrollBox;
398
+ /** Whether the canvas scrolls sideways (see `scrollBox`). */
399
+ protected scrolling: import("@angular/core").Signal<boolean>;
400
+ /**
401
+ * Scroll width of the canvas: the graph's right edge at the current pan and
402
+ * zoom. The svg's own box is unscaled, so it can't be the scroll extent.
403
+ */
404
+ protected scrollWidth: import("@angular/core").Signal<string | null>;
381
405
  /** `minNodeSize` expressed as the scale it forbids fitting to go below. */
382
406
  private minFitScale;
383
407
  constructor();
@@ -500,6 +524,11 @@ export declare class DependencyViewerComponent {
500
524
  "required": false;
501
525
  "isSignal": true;
502
526
  };
527
+ "regionLabel": {
528
+ "alias": "regionLabel";
529
+ "required": false;
530
+ "isSignal": true;
531
+ };
503
532
  "interactive": {
504
533
  "alias": "interactive";
505
534
  "required": false;
@@ -46,6 +46,10 @@ export type DescriptionListSeparator = 'line' | 'divider' | 'none';
46
46
  * Features:
47
47
  * - `rows` layout: term on the left, value on the right, with an optional
48
48
  * dotted leader line connecting them (the DS2.0 "spec sheet" look).
49
+ * - `rows`: a row that is too narrow is shared — term and value each keep
50
+ * their width up to half of the free width (row − 32 px); the longer one
51
+ * wraps in the rest: the term at spaces, the value at spaces or inside an
52
+ * unbroken string (3.26).
49
53
  * - `stacked` layout: term above value, for narrow columns.
50
54
  * - Per-row value emphasis and optional links.
51
55
  * - Semantic markup (`<dl>`/`<dt>`/`<dd>`) for accessibility; `clickable`
@@ -78,7 +78,12 @@ export declare class FileUploadComponent {
78
78
  * @default 'Max:'
79
79
  */
80
80
  maxSizeLabel: import("@angular/core").InputSignal<string>;
81
- /** Accessible name of a selected file's remove button. */
81
+ /**
82
+ * Accessible name of a selected file's remove button, followed by the file
83
+ * name ("Remove file bericht.csv"), so each button in the list has its own
84
+ * name.
85
+ * @default 'Remove file'
86
+ */
82
87
  removeFileAriaLabel: import("@angular/core").InputSignal<string>;
83
88
  /**
84
89
  * BCP 47 locale of the shown sizes (max line, drop zone name, file list):
@@ -119,6 +124,11 @@ export declare class FileUploadComponent {
119
124
  protected onDrop(event: DragEvent): void;
120
125
  protected onFileChange(event: Event): void;
121
126
  protected openFilePicker(): void;
127
+ /**
128
+ * Enter or Space on the zone (3.25.2): no page scroll on Space, one open per
129
+ * press (auto-repeat ignored).
130
+ */
131
+ protected onKeyActivate(event: Event): void;
122
132
  protected removeFile(index: number): void;
123
133
  protected formatSize(bytes: number): string;
124
134
  /** Why `file` is rejected: type first, then size; null when it is accepted. */