@arsedizioni/ars-utils 22.1.5 → 22.5.2

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 (78) hide show
  1. package/README.md +158 -10
  2. package/clipper.ui/clipper.scss +564 -0
  3. package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs +1 -1
  4. package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs.map +1 -1
  5. package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs +11 -7
  6. package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs.map +1 -1
  7. package/fesm2022/arsedizioni-ars-utils-core.date.mjs +609 -0
  8. package/fesm2022/arsedizioni-ars-utils-core.date.mjs.map +1 -0
  9. package/fesm2022/arsedizioni-ars-utils-core.markdown.mjs +591 -0
  10. package/fesm2022/arsedizioni-ars-utils-core.markdown.mjs.map +1 -0
  11. package/fesm2022/arsedizioni-ars-utils-core.mjs +270 -1449
  12. package/fesm2022/arsedizioni-ars-utils-core.mjs.map +1 -1
  13. package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs +1 -1
  14. package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs.map +1 -1
  15. package/fesm2022/arsedizioni-ars-utils-support.common.mjs +1 -1
  16. package/fesm2022/arsedizioni-ars-utils-support.common.mjs.map +1 -1
  17. package/fesm2022/arsedizioni-ars-utils-ui.application.mjs +14 -4108
  18. package/fesm2022/arsedizioni-ars-utils-ui.application.mjs.map +1 -1
  19. package/fesm2022/arsedizioni-ars-utils-ui.controls.mjs +962 -0
  20. package/fesm2022/arsedizioni-ars-utils-ui.controls.mjs.map +1 -0
  21. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.auth.mjs +506 -0
  22. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.auth.mjs.map +1 -0
  23. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.mjs +450 -0
  24. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.mjs.map +1 -0
  25. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.prompt.mjs +184 -0
  26. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.prompt.mjs.map +1 -0
  27. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.select.mjs +825 -0
  28. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.select.mjs.map +1 -0
  29. package/fesm2022/arsedizioni-ars-utils-ui.files.mjs +708 -0
  30. package/fesm2022/arsedizioni-ars-utils-ui.files.mjs.map +1 -0
  31. package/fesm2022/arsedizioni-ars-utils-ui.filters.mjs +426 -0
  32. package/fesm2022/arsedizioni-ars-utils-ui.filters.mjs.map +1 -0
  33. package/fesm2022/arsedizioni-ars-utils-ui.help.mjs +531 -0
  34. package/fesm2022/arsedizioni-ars-utils-ui.help.mjs.map +1 -0
  35. package/fesm2022/arsedizioni-ars-utils-ui.mjs +54 -1227
  36. package/fesm2022/arsedizioni-ars-utils-ui.mjs.map +1 -1
  37. package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs +1049 -0
  38. package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs.map +1 -0
  39. package/fesm2022/arsedizioni-ars-utils-ui.notifications.mjs +263 -0
  40. package/fesm2022/arsedizioni-ars-utils-ui.notifications.mjs.map +1 -0
  41. package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs +4 -3
  42. package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs.map +1 -1
  43. package/fesm2022/arsedizioni-ars-utils-ui.paginator.mjs +53 -0
  44. package/fesm2022/arsedizioni-ars-utils-ui.paginator.mjs.map +1 -0
  45. package/fesm2022/arsedizioni-ars-utils-ui.shell.mjs +517 -0
  46. package/fesm2022/arsedizioni-ars-utils-ui.shell.mjs.map +1 -0
  47. package/fesm2022/arsedizioni-ars-utils-ui.tinymce.mjs +246 -0
  48. package/fesm2022/arsedizioni-ars-utils-ui.tinymce.mjs.map +1 -0
  49. package/fesm2022/arsedizioni-ars-utils.mjs +13 -12
  50. package/fesm2022/arsedizioni-ars-utils.mjs.map +1 -1
  51. package/help/package.json +4 -0
  52. package/package.json +80 -14
  53. package/styles/ui.colors.scss +23 -0
  54. package/support.ui/package.json +4 -0
  55. package/tinymce/package.json +4 -0
  56. package/types/arsedizioni-ars-utils-clipper.common.d.ts +1 -2
  57. package/types/arsedizioni-ars-utils-clipper.ui.d.ts +4 -1
  58. package/types/arsedizioni-ars-utils-core.d.ts +684 -1004
  59. package/types/arsedizioni-ars-utils-core.date.d.ts +304 -0
  60. package/types/arsedizioni-ars-utils-core.markdown.d.ts +159 -0
  61. package/types/arsedizioni-ars-utils-ui.application.d.ts +10 -2022
  62. package/types/arsedizioni-ars-utils-ui.controls.d.ts +413 -0
  63. package/types/arsedizioni-ars-utils-ui.d.ts +183 -566
  64. package/types/arsedizioni-ars-utils-ui.dialogs.auth.d.ts +224 -0
  65. package/types/arsedizioni-ars-utils-ui.dialogs.d.ts +228 -0
  66. package/types/arsedizioni-ars-utils-ui.dialogs.prompt.d.ts +47 -0
  67. package/types/arsedizioni-ars-utils-ui.dialogs.select.d.ts +309 -0
  68. package/types/arsedizioni-ars-utils-ui.files.d.ts +271 -0
  69. package/types/arsedizioni-ars-utils-ui.filters.d.ts +163 -0
  70. package/types/arsedizioni-ars-utils-ui.help.d.ts +220 -0
  71. package/types/arsedizioni-ars-utils-ui.navigation.d.ts +650 -0
  72. package/types/arsedizioni-ars-utils-ui.notifications.d.ts +73 -0
  73. package/types/arsedizioni-ars-utils-ui.paginator.d.ts +23 -0
  74. package/types/arsedizioni-ars-utils-ui.shell.d.ts +345 -0
  75. package/types/arsedizioni-ars-utils-ui.tinymce.d.ts +63 -0
  76. package/types/arsedizioni-ars-utils.d.ts +4 -10
  77. package/ui.tinymce/README.md +12 -0
  78. package/ui.tinymce/langs/it.js +466 -0
@@ -1,632 +1,16 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Injectable, makeEnvironmentProviders, InjectionToken, ElementRef, afterNextRender, Directive, input, DestroyRef, HostListener, output, forwardRef, effect, Pipe, EventEmitter, signal, computed, Service, PLATFORM_ID, RendererFactory2 } from '@angular/core';
3
- import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
2
+ import { EventEmitter, signal, computed, inject, ElementRef, afterNextRender, Directive, input, DestroyRef, HostListener, output, forwardRef, effect, Pipe, Service, PLATFORM_ID, RendererFactory2 } from '@angular/core';
3
+ import { SelectionModel } from '@angular/cdk/collections';
4
+ import { parseISO, parse, format, endOfDay } from 'date-fns';
5
+ import { it } from 'date-fns/locale';
4
6
  import { TZDate } from '@date-fns/tz';
5
- import { format, getYear, getMonth, getDate, getDay, getDaysInMonth, parseISO, parse, addYears, addMonths, addDays, isDate, isValid, addSeconds, endOfDay } from 'date-fns';
6
- import { HTTP_INTERCEPTORS } from '@angular/common/http';
7
7
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
8
  import { Subject, filter as filter$1, map as map$1, BehaviorSubject } from 'rxjs';
9
9
  import { debounceTime, filter, map } from 'rxjs/operators';
10
- import { it } from 'date-fns/locale';
11
10
  import { NG_VALIDATORS } from '@angular/forms';
12
11
  import { DomSanitizer } from '@angular/platform-browser';
13
- import { SelectionModel } from '@angular/cdk/collections';
14
12
  import { isPlatformBrowser } from '@angular/common';
15
13
 
16
- /**
17
- * Creates an array of the given length, filling each slot with the result of `valueFunction`.
18
- * @param length - Number of elements to create.
19
- * @param valueFunction - Factory called with each index to produce the element value.
20
- * @returns Typed array of `length` elements.
21
- */
22
- function range(length, valueFunction) {
23
- const valuesArray = Array(length);
24
- for (let i = 0; i < length; i++) {
25
- valuesArray[i] = valueFunction(i);
26
- }
27
- return valuesArray;
28
- }
29
- // date-fns doesn't have a way to read/print month names or days of the week directly,
30
- // so we get them by formatting a date with a format that produces the desired month/day.
31
- const MONTH_FORMATS = {
32
- long: 'LLLL',
33
- short: 'LLL',
34
- narrow: 'LLLLL',
35
- };
36
- const DAY_OF_WEEK_FORMATS = {
37
- long: 'EEEE',
38
- short: 'EEE',
39
- narrow: 'EEEEE',
40
- };
41
- const MAT_DATE_FNS_FORMATS = {
42
- parse: {
43
- dateInput: 'P',
44
- timeInput: ['p', 'HH:mm', 'H:mm'],
45
- },
46
- display: {
47
- dateInput: 'P',
48
- monthYearLabel: 'LLL uuuu',
49
- dateA11yLabel: 'PP',
50
- monthYearA11yLabel: 'LLLL uuuu',
51
- timeInput: 'p',
52
- timeOptionLabel: 'p',
53
- },
54
- };
55
- /**
56
- * date-fns adapter that integrates Angular Material's date picker with the date-fns library,
57
- * applying `Europe/Rome` timezone for all parsed and created dates.
58
- */
59
- class DateFnsAdapter extends DateAdapter {
60
- constructor() {
61
- super();
62
- const matDateLocale = inject(MAT_DATE_LOCALE, { optional: true });
63
- if (matDateLocale && typeof matDateLocale === 'object') {
64
- this.setLocale(matDateLocale);
65
- }
66
- }
67
- /**
68
- * Wraps a date/instant into a Europe/Rome TZDate whose JSON serialisation emits a
69
- * naive local datetime string ("yyyy-MM-dd'T'HH:mm:ss") instead of a UTC instant,
70
- * so the wall-clock value the user entered is preserved end-to-end (no timezone shift).
71
- * @param value - The source Date or Unix timestamp in milliseconds.
72
- * @returns a Europe/Rome TZDate whose toJSON yields a naive local datetime string.
73
- */
74
- static toLocal(value) {
75
- const ms = typeof value === 'number' ? value : value.getTime();
76
- const t = new TZDate(ms, 'Europe/Rome');
77
- Object.defineProperty(t, 'toJSON', {
78
- value: () => format(t, "yyyy-MM-dd'T'HH:mm:ss"),
79
- enumerable: false,
80
- configurable: true,
81
- writable: true,
82
- });
83
- return t;
84
- }
85
- /**
86
- * Returns the year component of the given date.
87
- * @param date - The source date.
88
- */
89
- getYear(date) {
90
- return getYear(date);
91
- }
92
- /**
93
- * Returns the zero-based month index of the given date (0 = January).
94
- * @param date - The source date.
95
- */
96
- getMonth(date) {
97
- return getMonth(date);
98
- }
99
- /**
100
- * Returns the day-of-month of the given date (1-based).
101
- * @param date - The source date.
102
- */
103
- getDate(date) {
104
- return getDate(date);
105
- }
106
- /**
107
- * Returns the day-of-week of the given date (0 = Sunday).
108
- * @param date - The source date.
109
- */
110
- getDayOfWeek(date) {
111
- return getDay(date);
112
- }
113
- /**
114
- * Returns an array of 12 month name strings formatted for the active locale.
115
- * @param style - One of `'long'`, `'short'`, or `'narrow'`.
116
- */
117
- getMonthNames(style) {
118
- const pattern = MONTH_FORMATS[style];
119
- return range(12, i => this.format(new Date(2017, i, 1), pattern));
120
- }
121
- /**
122
- * Returns an array of 31 day-of-month label strings formatted using `Intl.DateTimeFormat`
123
- * when available, falling back to plain numeric strings.
124
- */
125
- getDateNames() {
126
- const dtf = typeof Intl !== 'undefined'
127
- ? new Intl.DateTimeFormat(this.locale?.code, {
128
- day: 'numeric',
129
- timeZone: 'utc',
130
- })
131
- : null;
132
- return range(31, i => {
133
- if (dtf) {
134
- // date-fns doesn't appear to support this functionality.
135
- // Fall back to `Intl` on supported browsers.
136
- const date = new Date();
137
- date.setUTCFullYear(2017, 0, i + 1);
138
- date.setUTCHours(0, 0, 0, 0);
139
- return dtf.format(date).replace(/[\u200e\u200f]/g, '');
140
- }
141
- return String(i + 1);
142
- });
143
- }
144
- /**
145
- * Returns an array of 7 day-of-week name strings formatted for the active locale.
146
- * @param style - One of `'long'`, `'short'`, or `'narrow'`.
147
- */
148
- getDayOfWeekNames(style) {
149
- const pattern = DAY_OF_WEEK_FORMATS[style];
150
- return range(7, i => this.format(new Date(2017, 0, i + 1), pattern));
151
- }
152
- /**
153
- * Returns the four-digit year string for the given date.
154
- * @param date - The source date.
155
- */
156
- getYearName(date) {
157
- return this.format(date, 'y');
158
- }
159
- /**
160
- * Returns the first day of the week for the active locale (0 = Sunday, 1 = Monday, …).
161
- */
162
- getFirstDayOfWeek() {
163
- return this.locale?.options?.weekStartsOn ?? 0;
164
- }
165
- /**
166
- * Returns the number of days in the month of the given date.
167
- * @param date - The source date.
168
- */
169
- getNumDaysInMonth(date) {
170
- return getDaysInMonth(date);
171
- }
172
- /**
173
- * Creates an independent copy of the given date.
174
- * @param date - The date to clone.
175
- */
176
- clone(date) {
177
- return DateFnsAdapter.toLocal(date.getTime());
178
- }
179
- /**
180
- * Creates a `Date` in the `Europe/Rome` timezone for the given year, month, and day.
181
- * Throws an `Error` when any component is out of range.
182
- * @param year - Full four-digit year.
183
- * @param month - Zero-based month index (0 = January, 11 = December).
184
- * @param date - Day-of-month (1-based).
185
- */
186
- createDate(year, month, date) {
187
- if (month < 0 || month > 11) {
188
- throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
189
- }
190
- if (date < 1) {
191
- throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
192
- }
193
- if (month < 0 || month > 11) {
194
- throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
195
- }
196
- const result = new TZDate(year, month, date, 0, 0, 0, 0, 'Europe/Rome');
197
- if (result.getMonth() !== month) {
198
- throw Error(`Invalid date "${date}" for month with index "${month}".`);
199
- }
200
- return DateFnsAdapter.toLocal(result);
201
- }
202
- /**
203
- * Returns the current date and time in the Europe/Rome timezone, preserving the wall-clock value.
204
- */
205
- today() {
206
- return DateFnsAdapter.toLocal(new Date());
207
- }
208
- /**
209
- * Parses a value into a `Date`.
210
- * - Strings are first attempted as ISO 8601, then matched against each format in `parseFormat`.
211
- * - Numbers are treated as Unix timestamps (milliseconds).
212
- * - Existing `Date` instances are cloned.
213
- * @param value - The value to parse.
214
- * @param parseFormat - A format string or an array of format strings (date-fns tokens).
215
- * @returns A valid `Date` in `Europe/Rome`, an invalid sentinel, or `null` for unrecognised input.
216
- */
217
- parse(value, parseFormat) {
218
- if (typeof value === 'string' && value.length > 0) {
219
- const iso8601Date = parseISO(value);
220
- if (this.isValid(iso8601Date)) {
221
- return DateFnsAdapter.toLocal(iso8601Date);
222
- }
223
- const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
224
- if (!formats.length) {
225
- throw Error('Formats array must not be empty.');
226
- }
227
- for (const currentFormat of formats) {
228
- const fromFormat = parse(value, currentFormat, new TZDate(new Date(), 'Europe/Rome'), {
229
- locale: this.locale
230
- });
231
- if (this.isValid(fromFormat)) {
232
- return DateFnsAdapter.toLocal(fromFormat);
233
- }
234
- }
235
- return this.invalid();
236
- }
237
- else if (typeof value === 'number') {
238
- return DateFnsAdapter.toLocal(value);
239
- }
240
- else if (value instanceof Date) {
241
- return this.clone(value);
242
- }
243
- return null;
244
- }
245
- /**
246
- * Formats a `Date` using the given date-fns display format string.
247
- * Throws an `Error` when `date` is not valid.
248
- * @param date - The date to format.
249
- * @param displayFormat - A date-fns format string (e.g. `'P'`, `'LLL uuuu'`).
250
- */
251
- format(date, displayFormat) {
252
- if (!this.isValid(date)) {
253
- throw Error('DateFnsAdapter: Cannot format invalid date.');
254
- }
255
- return format(date, displayFormat, { locale: this.locale });
256
- }
257
- /**
258
- * Adds the given number of whole years to a date.
259
- * @param date - The base date.
260
- * @param years - Number of years to add (can be negative).
261
- */
262
- addCalendarYears(date, years) {
263
- return DateFnsAdapter.toLocal(addYears(date, years));
264
- }
265
- /**
266
- * Adds the given number of whole months to a date.
267
- * @param date - The base date.
268
- * @param months - Number of months to add (can be negative).
269
- */
270
- addCalendarMonths(date, months) {
271
- return DateFnsAdapter.toLocal(addMonths(date, months));
272
- }
273
- /**
274
- * Adds the given number of whole days to a date.
275
- * @param date - The base date.
276
- * @param days - Number of days to add (can be negative).
277
- */
278
- addCalendarDays(date, days) {
279
- return DateFnsAdapter.toLocal(addDays(date, days));
280
- }
281
- /**
282
- * Serialises a date to an ISO 8601 date-only string (`yyyy-MM-dd`) using its Europe/Rome calendar day,
283
- * independent of the browser timezone.
284
- * @param date - The date to serialise.
285
- * @returns the yyyy-MM-dd string for the date in Europe/Rome.
286
- */
287
- toIso8601(date) {
288
- return format(new TZDate(date, 'Europe/Rome'), 'yyyy-MM-dd');
289
- }
290
- /**
291
- * Returns the given value when it is a valid `Date`, or `null` for an empty string.
292
- * Deserialises valid ISO 8601 strings into `Date` instances.
293
- * Delegates all other values to the base-class implementation.
294
- * @param value - The raw value to deserialise.
295
- */
296
- deserialize(value) {
297
- if (typeof value === 'string') {
298
- if (!value) {
299
- return null;
300
- }
301
- const date = parseISO(value);
302
- if (this.isValid(date)) {
303
- return DateFnsAdapter.toLocal(date);
304
- }
305
- }
306
- return super.deserialize(value);
307
- }
308
- /**
309
- * Returns `true` when `obj` is an instance of `Date`.
310
- * @param obj - The object to test.
311
- */
312
- isDateInstance(obj) {
313
- return isDate(obj);
314
- }
315
- /**
316
- * Returns `true` when `date` represents a valid point in time.
317
- * @param date - The date to validate.
318
- */
319
- isValid(date) {
320
- return isValid(date);
321
- }
322
- /**
323
- * Returns a sentinel `Date` that represents an invalid date (`new Date(NaN)`).
324
- */
325
- invalid() {
326
- return new Date(NaN);
327
- }
328
- /**
329
- * Returns the hours component (0-23) of the given date.
330
- * @param date - The source date.
331
- */
332
- getHours(date) {
333
- return date.getHours();
334
- }
335
- /**
336
- * Returns the minutes component (0-59) of the given date.
337
- * @param date - The source date.
338
- */
339
- getMinutes(date) {
340
- return date.getMinutes();
341
- }
342
- /**
343
- * Returns the seconds component (0-59) of the given date.
344
- * @param date - The source date.
345
- */
346
- getSeconds(date) {
347
- return date.getSeconds();
348
- }
349
- /**
350
- * Returns a clone of `target` with its time-of-day set to the given hours/minutes/seconds,
351
- * keeping the date part unchanged.
352
- * @param target - The date whose time should be set.
353
- * @param hours - New hours (0-23).
354
- * @param minutes - New minutes (0-59).
355
- * @param seconds - New seconds (0-59).
356
- */
357
- setTime(target, hours, minutes, seconds) {
358
- const clone = this.clone(target);
359
- clone.setHours(hours, minutes, seconds, 0);
360
- return clone;
361
- }
362
- /**
363
- * Adds the given number of seconds to a date.
364
- * @param date - The base date.
365
- * @param amount - Number of seconds to add (can be negative).
366
- */
367
- addSeconds(date, amount) {
368
- return DateFnsAdapter.toLocal(addSeconds(date, amount));
369
- }
370
- /**
371
- * Parses a time-only value into a `Date` (today's date, in `Europe/Rome`, with the parsed
372
- * time-of-day set). The wall-clock time is preserved exactly as entered.
373
- * @param value - The value to parse (a string, or an existing `Date`).
374
- * @param parseFormat - A format string or an array of format strings (date-fns tokens).
375
- * @returns the parsed Date, an invalid sentinel, or null for unrecognised input.
376
- */
377
- parseTime(value, parseFormat) {
378
- if (typeof value === 'string' && value.length > 0) {
379
- const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
380
- if (!formats.length) {
381
- throw Error('Formats array must not be empty.');
382
- }
383
- const referenceDate = new TZDate(new Date(), 'Europe/Rome');
384
- for (const currentFormat of formats) {
385
- const fromFormat = parse(value, currentFormat, referenceDate, { locale: this.locale });
386
- if (this.isValid(fromFormat)) {
387
- return DateFnsAdapter.toLocal(fromFormat);
388
- }
389
- }
390
- return this.invalid();
391
- }
392
- else if (value instanceof Date) {
393
- return this.clone(value);
394
- }
395
- return null;
396
- }
397
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DateFnsAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
398
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DateFnsAdapter }); }
399
- }
400
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DateFnsAdapter, decorators: [{
401
- type: Injectable
402
- }], ctorParameters: () => [] });
403
- /**
404
- * Standalone providers for the ARS date-fns adapter.
405
- *
406
- * Configures Angular Material to use {@link DateFnsAdapter} (Europe/Rome timezone)
407
- * and the matching {@link MAT_DATE_FNS_FORMATS}. Also supports `mat-timepicker` since
408
- * {@link DateFnsAdapter} implements the time-related `DateAdapter` methods.
409
- *
410
- * @example
411
- * bootstrapApplication(AppComponent, {
412
- * providers: [provideArsDateFns()]
413
- * });
414
- */
415
- function provideArsDateFns() {
416
- return makeEnvironmentProviders([
417
- {
418
- provide: DateAdapter,
419
- useClass: DateFnsAdapter
420
- },
421
- { provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS }
422
- ]);
423
- }
424
-
425
- /** Default application timezone: all dates are serialised as Europe/Rome wall-clock values. */
426
- const DEFAULT_TIME_ZONE = 'Europe/Rome';
427
- /**
428
- * IANA timezone used to serialise dates towards the backend.
429
- * Override it in the application providers to run the same code in another zone:
430
- * `{ provide: ARS_TIME_ZONE, useValue: 'Europe/Zurich' }`.
431
- */
432
- const ARS_TIME_ZONE = new InjectionToken('ARS_TIME_ZONE', {
433
- providedIn: 'root',
434
- factory: () => DEFAULT_TIME_ZONE,
435
- });
436
- /** Naive local datetime pattern (no timezone designator): the server reads it as DateTimeKind.Unspecified. */
437
- const NAIVE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss";
438
- /**
439
- * Returns `true` for payloads that must be forwarded untouched
440
- * (binary or already-encoded bodies).
441
- * @param body - The request body to test.
442
- */
443
- function isOpaqueBody(body) {
444
- return (body instanceof FormData ||
445
- body instanceof Blob ||
446
- body instanceof ArrayBuffer ||
447
- ArrayBuffer.isView(body) ||
448
- body instanceof URLSearchParams ||
449
- typeof body === 'string');
450
- }
451
- /**
452
- * Recursively replaces every `Date` found in `value` with its naive local
453
- * datetime string, returning a NEW structure: the original object graph — and
454
- * therefore any bound form model — is never mutated.
455
- *
456
- * @param value - The value to normalise (object, array, Date or primitive).
457
- * @param timeZone - IANA timezone used to compute the wall-clock value.
458
- * @param seen - Guard against circular references.
459
- * @returns The normalised value, safe to hand over to `JSON.stringify`.
460
- */
461
- function normalizeDates(value, timeZone, seen) {
462
- if (value instanceof Date) {
463
- // Invalid dates would make format() throw a RangeError: send null instead.
464
- if (isNaN(value.getTime()))
465
- return null;
466
- return format(new TZDate(value.getTime(), timeZone), NAIVE_PATTERN);
467
- }
468
- if (value === null || typeof value !== 'object')
469
- return value;
470
- if (isOpaqueBody(value))
471
- return value;
472
- // Circular reference: leave it as is, JSON.stringify will report it anyway.
473
- if (seen.has(value))
474
- return value;
475
- seen.add(value);
476
- if (Array.isArray(value)) {
477
- return value.map(item => normalizeDates(item, timeZone, seen));
478
- }
479
- const source = value;
480
- const result = {};
481
- for (const key of Object.keys(source)) {
482
- result[key] = normalizeDates(source[key], timeZone, seen);
483
- }
484
- return result;
485
- }
486
- /**
487
- * Rewrites the request body, if any, replacing Dates with naive local strings.
488
- * Shared by the functional and the class-based interceptor.
489
- * @param req - The outgoing request.
490
- * @param timeZone - IANA timezone used to compute the wall-clock value.
491
- * @returns The original request when there is nothing to convert, a clone otherwise.
492
- */
493
- function localizeRequest(req, timeZone) {
494
- const body = req.body;
495
- if (body === null || body === undefined || isOpaqueBody(body))
496
- return req;
497
- return req.clone({ body: normalizeDates(body, timeZone, new WeakSet()) });
498
- }
499
- /**
500
- * Functional interceptor that serialises every `Date` in an outgoing request
501
- * body as a naive local datetime string (`yyyy-MM-ddTHH:mm:ss`: no `Z`, no offset).
502
- *
503
- * WHY: `JSON.stringify` calls `Date.prototype.toJSON`, which emits a UTC instant.
504
- * A date picked as 29/07/2026 00:00 in Rome (UTC+2) becomes
505
- * `2026-07-28T22:00:00.000Z`, so the server stores the 28th — the classic
506
- * "off by one day" bug. Sending the wall-clock value instead makes
507
- * System.Text.Json produce a `DateTime` with `Kind = Unspecified` and the exact
508
- * day/time the user selected.
509
- *
510
- * This is the single serialisation boundary of the application: no call site has
511
- * to convert anything, and it also covers dates that lost a per-instance
512
- * `toJSON` along the way (e.g. after `structuredClone`).
513
- *
514
- * Register it explicitly in `provideHttpClient(withInterceptors([...]))`.
515
- * If you would rather have the library register it for you, use
516
- * {@link provideArsLocalDates} instead.
517
- *
518
- * @example
519
- * provideHttpClient(withInterceptors([
520
- * arsLocalDateInterceptor,
521
- * evolutionAuthInterceptor(...),
522
- * ]));
523
- */
524
- const arsLocalDateInterceptor = (req, next) => next(localizeRequest(req, inject(ARS_TIME_ZONE)));
525
- /**
526
- * Class-based twin of {@link arsLocalDateInterceptor}.
527
- *
528
- * Exists because a functional `HttpInterceptorFn` can ONLY be registered inside
529
- * `provideHttpClient(withInterceptors([...]))`, while a class registered on the
530
- * `HTTP_INTERCEPTORS` multi-token can be contributed by any `EnvironmentProviders` —
531
- * which is what lets {@link provideArsLocalDates} (and `provideArsDateFns`) wire it up
532
- * on their own.
533
- *
534
- * REQUIRES `withInterceptorsFromDi()` in the application's `provideHttpClient()`:
535
- * without it Angular never reads `HTTP_INTERCEPTORS` and this interceptor is
536
- * silently skipped.
537
- */
538
- class ArsLocalDateInterceptor {
539
- constructor() {
540
- this.timeZone = inject(ARS_TIME_ZONE);
541
- }
542
- /**
543
- * Normalises the request body before handing it to the next handler.
544
- * @param req - The outgoing request.
545
- * @param next - The next handler in the chain.
546
- */
547
- intercept(req, next) {
548
- return next.handle(localizeRequest(req, this.timeZone));
549
- }
550
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ArsLocalDateInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
551
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ArsLocalDateInterceptor }); }
552
- }
553
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ArsLocalDateInterceptor, decorators: [{
554
- type: Injectable
555
- }] });
556
- /**
557
- * Standalone providers for date serialisation towards the backend.
558
- *
559
- * Registers {@link ArsLocalDateInterceptor} so that every `Date` in a request
560
- * body travels as a naive local datetime string instead of a UTC instant.
561
- *
562
- * IMPORTANT: the application MUST call `withInterceptorsFromDi()`, otherwise
563
- * these providers have no effect at all (no error is raised).
564
- *
565
- * @param timeZone - IANA timezone name (default: `Europe/Rome`).
566
- *
567
- * @example
568
- * providers: [
569
- * provideArsLocalDates(),
570
- * provideHttpClient(withInterceptors([evolutionAuthInterceptor(...)]), withInterceptorsFromDi()),
571
- * ]
572
- */
573
- function provideArsLocalDates(timeZone = DEFAULT_TIME_ZONE) {
574
- return makeEnvironmentProviders([
575
- { provide: ARS_TIME_ZONE, useValue: timeZone },
576
- { provide: HTTP_INTERCEPTORS, useClass: ArsLocalDateInterceptor, multi: true },
577
- ]);
578
- }
579
- /**
580
- * Formats a date as a date-only ISO string (`yyyy-MM-dd`) using its wall-clock
581
- * calendar day in the given timezone. Use it for query-string parameters and for
582
- * DTO fields mapped to a .NET `DateOnly`, which the interceptor cannot detect.
583
- *
584
- * @param value - The date to serialise.
585
- * @param timeZone - IANA timezone name (default: `Europe/Rome`).
586
- * @returns The `yyyy-MM-dd` string, or `undefined` for empty/invalid input.
587
- */
588
- function toLocalDateOnlyString(value, timeZone = DEFAULT_TIME_ZONE) {
589
- if (!value || isNaN(value.getTime()))
590
- return undefined;
591
- return format(new TZDate(value.getTime(), timeZone), 'yyyy-MM-dd');
592
- }
593
- /**
594
- * Formats a date as a naive local datetime string (`yyyy-MM-ddTHH:mm:ss`), the
595
- * same representation produced by the interceptor. Useful for query-string
596
- * parameters, which never pass through the request body.
597
- *
598
- * @param value - The date to serialise.
599
- * @param timeZone - IANA timezone name (default: `Europe/Rome`).
600
- * @returns The naive datetime string, or `undefined` for empty/invalid input.
601
- */
602
- function toLocalDateTimeString(value, timeZone = DEFAULT_TIME_ZONE) {
603
- if (!value || isNaN(value.getTime()))
604
- return undefined;
605
- return format(new TZDate(value.getTime(), timeZone), NAIVE_PATTERN);
606
- }
607
-
608
- /**
609
- * Directive that moves browser focus to the host element after the first render cycle.
610
- * Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
611
- */
612
- class AutoFocusDirective {
613
- constructor() {
614
- this.elementRef = inject(ElementRef);
615
- afterNextRender(() => {
616
- this.elementRef.nativeElement?.focus();
617
- });
618
- }
619
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
620
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
621
- }
622
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AutoFocusDirective, decorators: [{
623
- type: Directive,
624
- args: [{
625
- selector: '[autoFocus]',
626
- standalone: true
627
- }]
628
- }], ctorParameters: () => [] });
629
-
630
14
  class FileInfo {
631
15
  isValid() {
632
16
  return this.valid;
@@ -644,6 +28,7 @@ class RelationModel {
644
28
  }
645
29
  class UpdateRelationsModel {
646
30
  }
31
+ /** Identifies the external identity provider used for an OAuth login. */
647
32
  var LoginOAuthType;
648
33
  (function (LoginOAuthType) {
649
34
  LoginOAuthType[LoginOAuthType["None"] = 0] = "None";
@@ -678,564 +63,23 @@ class DateInterval {
678
63
  return undefined;
679
64
  }
680
65
  constructor(from, to) {
681
- this.from = from;
682
- this.to = to;
683
- }
684
- clear() {
685
- this.from = undefined;
686
- this.to = undefined;
687
- }
688
- }
689
-
690
- /**
691
- * Zero-dependency Markdown to HTML converter.
692
- *
693
- * Design goals:
694
- * - Single line-based pass over blocks (no repeated full-string regex passes).
695
- * - Inline formatting never touches code spans / code blocks (stash & restore).
696
- * - No HTML entity escaping by default in normal text: the source passes as-is.
697
- * Exception: the content of code spans / code blocks is ALWAYS escaped, so
698
- * things like List<string> render correctly (the browser displays the
699
- * original characters; entities never reach the user or the clipboard).
700
- * Opt-in full escaping via { escapeHtml: true } for untrusted input.
701
- * - URL sanitization on links/images (javascript:, vbscript:, data: are dropped).
702
- * - Bounded result cache for repeated renders (Angular change detection friendly).
703
- *
704
- * Supported syntax: headings, paragraphs, hard/soft breaks, hr, blockquotes
705
- * (nested), fenced code blocks, inline code, bold/italic/strikethrough, links
706
- * (with title), images, autolinked bare URLs, ordered/unordered lists (nested,
707
- * task lists, ordered start offset), GFM tables with alignment, raw HTML
708
- * blocks (e.g. <table>...</table>) passed through verbatim when escapeHtml
709
- * is false: their newlines stay plain newlines, never converted to <br>.
710
- *
711
- * Known simplifications (documented, by design):
712
- * - A blank line terminates a list.
713
- * - Setext headings (=== / ---) are not supported, use # syntax.
714
- * - Reference-style links [text][ref] are not supported.
715
- */
716
- class MarkdownUtils {
717
- // #region Cache
718
- static { this.cache = new Map(); }
719
- static { this.CACHE_MAX = 200; }
720
- /** Clears the internal result cache. */
721
- static clearCache() {
722
- this.cache.clear();
723
- }
724
- // #endregion
725
- // #region Public API
726
- /**
727
- * Convert markdown to HTML.
728
- * @param markdown : the markdown source
729
- * @param options : conversion options
730
- * @returns : the HTML string ('' when input is falsy)
731
- */
732
- static toHtml(markdown, options) {
733
- if (!markdown)
734
- return '';
735
- const opts = { escapeHtml: false, breaks: true, ...options };
736
- const key = `${opts.escapeHtml ? 1 : 0}${opts.breaks ? 1 : 0}|${markdown}`;
737
- const hit = this.cache.get(key);
738
- if (hit !== undefined)
739
- return hit;
740
- // Normalize line endings and fix glued sentence punctuation ("dettaglio!Ecco").
741
- // Only when the next char is uppercase (a new sentence), so URLs with query
742
- // strings ("/page?id=1") and code identifiers are left untouched.
743
- const source = markdown
744
- .replace(/\r\n?/g, '\n')
745
- .replace(/([a-zà-ÿ][!?])([A-ZÀ-Þ])/g, '$1 $2');
746
- const html = this.parseBlocks(source.split('\n'), opts).trim();
747
- if (this.cache.size >= this.CACHE_MAX) {
748
- this.cache.delete(this.cache.keys().next().value); // FIFO eviction
749
- }
750
- this.cache.set(key, html);
751
- return html;
752
- }
753
- // #endregion
754
- // #region Block parser (single pass)
755
- /** Block-level HTML tags that start a raw HTML block (passthrough, no <p>/<br>). */
756
- static { this.htmlBlockTags = new Set([
757
- 'address', 'article', 'aside', 'blockquote', 'caption', 'colgroup', 'col',
758
- 'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
759
- 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
760
- 'hr', 'iframe', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'summary',
761
- 'table', 'tbody', 'td', 'tfoot', 'thead', 'th', 'tr', 'ul',
762
- ]); }
763
- /** Void/self-contained tags: a single tag, no closing tag expected. */
764
- static { this.htmlVoidTags = new Set(['hr', 'col', 'img', 'input']); }
765
- /**
766
- * Precompiled: first block-level tag occurrence anywhere in a line.
767
- * Anchored on the '<' literal, so scanning is cheap; the lookahead
768
- * prevents partial matches ('<td' must not match inside '<tdx').
769
- */
770
- static { this.htmlBlockScanRe = new RegExp(`<(\\/?)(${[...MarkdownUtils.htmlBlockTags].join('|')})(?=[\\s/>])`, 'gi'); }
771
- /** Result of scanning a line for the start of a raw HTML block. */
772
- static findHtmlBlockStart(line) {
773
- // HTML comment candidate (skipping occurrences inside inline code spans)
774
- let comment = line.indexOf('<!--');
775
- while (comment !== -1 && this.insideCodeSpan(line, comment)) {
776
- comment = line.indexOf('<!--', comment + 4);
777
- }
778
- // Block tag candidate (same code-span guard)
779
- const re = this.htmlBlockScanRe;
780
- re.lastIndex = 0;
781
- let m;
782
- let tagHit;
783
- while ((m = re.exec(line)) !== null) {
784
- if (!this.insideCodeSpan(line, m.index)) {
785
- tagHit = { index: m.index, tag: m[2].toLowerCase(), isClose: m[1] === '/' };
786
- break;
787
- }
788
- }
789
- if (comment !== -1 && (!tagHit || comment < tagHit.index)) {
790
- return { index: comment, tag: '', isClose: false, isComment: true };
791
- }
792
- return tagHit ? { ...tagHit, isComment: false } : undefined;
793
- }
794
- /** True when `index` falls inside an inline code span (odd backtick count before it). */
795
- static insideCodeSpan(line, index) {
796
- let count = 0;
797
- for (let i = 0; i < index; i++) {
798
- if (line.charCodeAt(i) === 96 /* ` */)
799
- count++;
800
- }
801
- return (count & 1) === 1;
802
- }
803
- /**
804
- * indexOf-based depth scanner (no regex, no allocations): walks `text`
805
- * adjusting `depth` for <tag ...> / </tag> occurrences of the SAME tag.
806
- * Self-closing <tag ... /> forms do not alter depth.
807
- * @returns [newDepth, endIndex] where endIndex is the position right after
808
- * the '>' that balanced the element, or -1 when still open.
809
- */
810
- static scanHtmlDepth(text, tag, depth) {
811
- const lower = text.toLowerCase();
812
- const open = '<' + tag;
813
- const close = '</' + tag;
814
- let i = 0;
815
- while ((i = lower.indexOf('<', i)) !== -1) {
816
- if (lower.startsWith(close, i) && this.isTagBoundary(lower, i + close.length)) {
817
- depth--;
818
- const gt = lower.indexOf('>', i);
819
- const end = gt === -1 ? lower.length : gt + 1;
820
- if (depth <= 0)
821
- return [0, end];
822
- i = end;
823
- }
824
- else if (lower.startsWith(open, i) && this.isTagBoundary(lower, i + open.length)) {
825
- const gt = lower.indexOf('>', i);
826
- const selfClosing = gt !== -1 && lower.charCodeAt(gt - 1) === 47 /* / */;
827
- if (selfClosing) {
828
- if (depth === 0)
829
- return [0, gt + 1]; // standalone self-closed element
830
- }
831
- else {
832
- depth++;
833
- }
834
- i = gt === -1 ? lower.length : gt + 1;
835
- }
836
- else {
837
- i++;
838
- }
839
- }
840
- return [depth, -1];
841
- }
842
- /** A tag token must be followed by whitespace, '/', '>' or end of line. */
843
- static isTagBoundary(s, idx) {
844
- if (idx >= s.length)
845
- return true; // tag opening continues on the next line
846
- const c = s.charCodeAt(idx);
847
- return c === 62 /* > */ || c === 47 /* / */ || c === 32 /* space */ || c === 9 /* tab */;
848
- }
849
- /**
850
- * Re-injects the remainder of a partially consumed line so the main loop
851
- * processes it as markdown. Returns the index the loop should resume from.
852
- */
853
- static pushBack(lines, i, remainder) {
854
- if (remainder.trim()) {
855
- lines[i] = remainder;
856
- return i - 1; // the loop's i++ re-processes the remainder
857
- }
858
- return i;
859
- }
860
- /**
861
- * Emits a raw HTML block starting at `hit.index` of lines[i], consuming
862
- * following lines until the element is balanced. Text after the block on
863
- * the closing line is pushed back for markdown processing.
864
- * @returns the index of the last consumed line
865
- */
866
- static emitHtmlBlock(lines, i, hit, out) {
867
- const line = lines[i];
868
- // HTML comment: raw until '-->'
869
- if (hit.isComment) {
870
- let end = line.indexOf('-->', hit.index);
871
- if (end !== -1) {
872
- out.push(line.slice(hit.index, end + 3));
873
- return this.pushBack(lines, i, line.slice(end + 3));
874
- }
875
- out.push(line.slice(hit.index));
876
- while (++i < lines.length) {
877
- end = lines[i].indexOf('-->');
878
- if (end !== -1) {
879
- out.push(lines[i].slice(0, end + 3));
880
- return this.pushBack(lines, i, lines[i].slice(end + 3));
881
- }
882
- out.push(lines[i]);
883
- }
884
- return lines.length - 1; // unterminated: consumed to EOF
885
- }
886
- // Stray closing tag or void tag: emit just the tag, resume after '>'
887
- if (hit.isClose || this.htmlVoidTags.has(hit.tag)) {
888
- const gt = line.indexOf('>', hit.index);
889
- const end = gt === -1 ? line.length : gt + 1;
890
- out.push(line.slice(hit.index, end));
891
- return this.pushBack(lines, i, line.slice(end));
892
- }
893
- // Opening block tag: raw until the element is balanced (depth === 0)
894
- let [depth, split] = this.scanHtmlDepth(line.slice(hit.index), hit.tag, 0);
895
- if (split !== -1) {
896
- out.push(line.slice(hit.index, hit.index + split));
897
- return this.pushBack(lines, i, line.slice(hit.index + split));
898
- }
899
- out.push(line.slice(hit.index));
900
- while (depth > 0 && ++i < lines.length) {
901
- [depth, split] = this.scanHtmlDepth(lines[i], hit.tag, depth);
902
- if (split !== -1) {
903
- out.push(lines[i].slice(0, split));
904
- return this.pushBack(lines, i, lines[i].slice(split));
905
- }
906
- out.push(lines[i]);
907
- }
908
- return Math.min(i, lines.length - 1);
909
- }
910
- static parseBlocks(lines, opts) {
911
- const out = [];
912
- let paragraph = [];
913
- const flush = () => {
914
- if (paragraph.length > 0) {
915
- const sep = opts.breaks ? '<br>' : ' ';
916
- out.push(`<p>${paragraph.map(l => this.inline(l, opts)).join(sep)}</p>`);
917
- paragraph = [];
918
- }
919
- };
920
- for (let i = 0; i < lines.length; i++) {
921
- const line = lines[i];
922
- const trimmed = line.trim();
923
- // --- Fenced code block -------------------------------------------------
924
- const fence = trimmed.match(/^```([\w+-]+)?\s*$/);
925
- if (fence) {
926
- flush();
927
- const lang = fence[1] ? ` class="language-${fence[1]}"` : '';
928
- const code = [];
929
- i++;
930
- while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
931
- code.push(lines[i]);
932
- i++;
933
- }
934
- out.push(`<pre><code${lang}>${this.escape(code.join('\n'))}</code></pre>`);
935
- continue;
936
- }
937
- // --- Blank line --------------------------------------------------------
938
- if (!trimmed) {
939
- flush();
940
- continue;
941
- }
942
- // --- Raw HTML block passthrough (only with escaping disabled) ------------
943
- // A block-level HTML element found ANYWHERE in the line starts a raw
944
- // block: text before it joins the current paragraph, the element is
945
- // emitted as-is (plain newlines, no <p>, no <br>) until balanced, and
946
- // any text after the closing tag resumes markdown processing.
947
- if (!opts.escapeHtml && line.includes('<')) {
948
- const hit = this.findHtmlBlockStart(line);
949
- if (hit) {
950
- const before = line.slice(0, hit.index).trim();
951
- if (before)
952
- paragraph.push(before);
953
- flush();
954
- i = this.emitHtmlBlock(lines, i, hit, out);
955
- continue;
956
- }
957
- }
958
- // --- ATX heading -------------------------------------------------------
959
- const heading = trimmed.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
960
- if (heading) {
961
- flush();
962
- const level = heading[1].length;
963
- out.push(`<h${level}>${this.inline(heading[2], opts)}</h${level}>`);
964
- continue;
965
- }
966
- // --- Horizontal rule ---------------------------------------------------
967
- if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
968
- flush();
969
- out.push('<hr>');
970
- continue;
971
- }
972
- // --- Blockquote (consecutive lines, one level stripped, recursive) ------
973
- if (/^>\s?/.test(trimmed)) {
974
- flush();
975
- const quoted = [];
976
- while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
977
- quoted.push(lines[i].trim().replace(/^>\s?/, ''));
978
- i++;
979
- }
980
- i--;
981
- out.push(`<blockquote>${this.parseBlocks(quoted, opts)}</blockquote>`);
982
- continue;
983
- }
984
- // --- Table (header row + separator row) ---------------------------------
985
- if (trimmed.includes('|') &&
986
- i + 1 < lines.length &&
987
- lines[i + 1].includes('-') &&
988
- /^\|?[\s:\-|]+\|?$/.test(lines[i + 1].trim())) {
989
- flush();
990
- const aligns = this.tableAligns(lines[i + 1].trim());
991
- const head = this.tableRow(trimmed, 'th', aligns, opts);
992
- const body = [];
993
- i += 2;
994
- while (i < lines.length && lines[i].trim().includes('|')) {
995
- body.push(this.tableRow(lines[i].trim(), 'td', aligns, opts));
996
- i++;
997
- }
998
- i--;
999
- out.push(`<table>\n<thead>\n${head}\n</thead>` +
1000
- (body.length > 0 ? `\n<tbody>\n${body.join('\n')}\n</tbody>` : '') +
1001
- `\n</table>`);
1002
- continue;
1003
- }
1004
- // --- List ---------------------------------------------------------------
1005
- if (MarkdownUtils.listItemRe.test(line)) {
1006
- flush();
1007
- const [html, last] = this.parseList(lines, i, opts);
1008
- out.push(html);
1009
- i = last;
1010
- continue;
1011
- }
1012
- // --- Plain text: accumulate into current paragraph ----------------------
1013
- paragraph.push(trimmed);
1014
- }
1015
- flush();
1016
- return out.join('\n');
1017
- }
1018
- // #endregion
1019
- // #region Lists
1020
- /** Matches "- item", "* item", "+ item", "1. item", "1) item" with leading indent. */
1021
- static { this.listItemRe = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/; }
1022
- /**
1023
- * Gathers the contiguous list block starting at `start`, builds it (with
1024
- * nesting) and returns [html, indexOfLastConsumedLine].
1025
- */
1026
- static parseList(lines, start, opts) {
1027
- const block = [];
1028
- let i = start;
1029
- while (i < lines.length) {
1030
- const l = lines[i];
1031
- if (!l.trim())
1032
- break; // blank line ends the list
1033
- if (this.listItemRe.test(l) || /^\s{2,}\S/.test(l)) {
1034
- block.push(l);
1035
- i++;
1036
- }
1037
- else
1038
- break;
1039
- }
1040
- return [this.buildList(block, opts), i - 1];
1041
- }
1042
- static buildList(block, opts) {
1043
- const first = block[0].match(this.listItemRe);
1044
- const baseIndent = first[1].length;
1045
- const ordered = /^\d/.test(first[2]);
1046
- const startNum = ordered ? parseInt(first[2], 10) : 1;
1047
- const items = [];
1048
- for (const l of block) {
1049
- const m = l.match(this.listItemRe);
1050
- if (m && m[1].length <= baseIndent) {
1051
- items.push({ text: m[3], childLines: [] });
1052
- }
1053
- else if (items.length > 0) {
1054
- // Continuation or nested content: strip one nesting level of indent
1055
- items[items.length - 1].childLines.push(l.length > baseIndent + 2 ? l.slice(baseIndent + 2) : l.trim());
1056
- }
1057
- }
1058
- const lis = items.map(item => {
1059
- let inner;
1060
- // GFM task list: - [ ] / - [x]
1061
- const task = item.text.match(/^\[([ xX])\]\s+(.*)$/);
1062
- if (task) {
1063
- const checked = task[1] !== ' ' ? ' checked' : '';
1064
- inner = `<input type="checkbox" disabled${checked}> ${this.inline(task[2], opts)}`;
1065
- }
1066
- else {
1067
- inner = this.inline(item.text, opts);
1068
- }
1069
- if (item.childLines.length > 0) {
1070
- inner += '\n' + this.parseBlocks(item.childLines, opts);
1071
- }
1072
- return `<li>${inner}</li>`;
1073
- });
1074
- const tag = ordered ? 'ol' : 'ul';
1075
- const startAttr = ordered && startNum > 1 ? ` start="${startNum}"` : '';
1076
- return `<${tag}${startAttr}>\n${lis.join('\n')}\n</${tag}>`;
1077
- }
1078
- // #endregion
1079
- // #region Tables
1080
- static tableAligns(separator) {
1081
- return separator
1082
- .replace(/^\||\|$/g, '')
1083
- .split('|')
1084
- .map(cell => {
1085
- const t = cell.trim();
1086
- if (t.startsWith(':') && t.endsWith(':'))
1087
- return 'center';
1088
- if (t.endsWith(':'))
1089
- return 'right';
1090
- if (t.startsWith(':'))
1091
- return 'left';
1092
- return undefined;
1093
- });
1094
- }
1095
- static tableRow(row, tag, aligns, opts) {
1096
- const cells = row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
1097
- const cellsHtml = cells
1098
- .map((cell, j) => {
1099
- const align = aligns[j] ? ` align="${aligns[j]}"` : '';
1100
- return `<${tag}${align}>${this.inline(cell, opts)}</${tag}>`;
1101
- })
1102
- .join('');
1103
- return `<tr>${cellsHtml}</tr>`;
1104
- }
1105
- // #endregion
1106
- // #region Inline formatting
1107
- /**
1108
- * Applies inline markdown to a single text segment.
1109
- * Generated HTML (code, links, images, autolinks) is stashed behind \u0000
1110
- * placeholders so later regex passes can never corrupt it.
1111
- */
1112
- static inline(text, opts) {
1113
- const store = [];
1114
- const stash = (html) => `\u0000${store.push(html) - 1}\u0000`;
1115
- // 1. Inline code: stash first so later passes can't touch it.
1116
- // Content is ALWAYS escaped so <, >, & inside code survive in the DOM
1117
- // (the browser renders entities back to the original characters).
1118
- let s = text.replace(/`([^`]+)`/g, (_m, code) => stash(`<code>${this.escape(code)}</code>`));
1119
- // 2. Escape raw HTML as entities (opt-in only)
1120
- if (opts.escapeHtml)
1121
- s = this.escape(s);
1122
- // 3. Images: ![alt](url "title")
1123
- s = s.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+(?:"|&quot;)(.+?)(?:"|&quot;))?\s*\)/g, (_m, alt, src, title) => {
1124
- const url = this.safeUrl(src);
1125
- if (!url)
1126
- return alt;
1127
- const t = title ? ` title="${this.escapeAttr(title)}"` : '';
1128
- return stash(`<img src="${url}" alt="${this.escapeAttr(alt)}"${t} loading="lazy">`);
1129
- });
1130
- // 4. Links: [text](url "title")
1131
- s = s.replace(/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+(?:"|&quot;)(.+?)(?:"|&quot;))?\s*\)/g, (_m, txt, href, title) => {
1132
- const url = this.safeUrl(href);
1133
- if (!url)
1134
- return txt;
1135
- const t = title ? ` title="${this.escapeAttr(title)}"` : '';
1136
- return stash(`<a href="${url}"${t} target="_blank" rel="noopener noreferrer">${txt}</a>`);
1137
- });
1138
- // 5. Autolink remaining bare URLs (markdown links are already stashed)
1139
- s = s.replace(/https?:\/\/[^\s<>"']+/g, url => stash(`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`));
1140
- // 6. Bold / italic / strikethrough.
1141
- // Delimiters must hug the text; underscore emphasis only at word
1142
- // boundaries so identifiers like snake_case are left untouched.
1143
- s = s.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>');
1144
- s = s.replace(/(?<![\w*])___(?=\S)([\s\S]*?\S)___(?![\w*])/g, '<strong><em>$1</em></strong>');
1145
- s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>');
1146
- s = s.replace(/(?<![\w*])__(?=\S)([\s\S]*?\S)__(?![\w*])/g, '<strong>$1</strong>');
1147
- s = s.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '<em>$1</em>');
1148
- s = s.replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>');
1149
- s = s.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
1150
- // 7. Restore stashed HTML
1151
- return s.replace(/\u0000(\d+)\u0000/g, (_m, idx) => store[Number(idx)] ?? '');
1152
- }
1153
- // #endregion
1154
- // #region Security helpers
1155
- /** Escapes quotes for safe interpolation inside a double-quoted HTML attribute. */
1156
- static escapeAttr(s) {
1157
- return s.replace(/&(?!(?:amp|lt|gt|quot|#\d+);)/g, '&amp;').replace(/"/g, '&quot;');
1158
- }
1159
- /** Escapes &, <, > and " for safe HTML interpolation. */
1160
- static escape(s) {
1161
- return s
1162
- .replace(/&/g, '&amp;')
1163
- .replace(/</g, '&lt;')
1164
- .replace(/>/g, '&gt;')
1165
- .replace(/"/g, '&quot;');
1166
- }
1167
- /**
1168
- * Returns a sanitized URL or undefined when the scheme is dangerous.
1169
- * Blocks javascript:, vbscript: and data: (also when obfuscated with
1170
- * whitespace/control characters, e.g. "java\tscript:").
1171
- */
1172
- static safeUrl(url) {
1173
- const compact = url.replace(/[\s\u0000-\u001f]/g, '');
1174
- if (/^(javascript|vbscript|data):/i.test(compact))
1175
- return undefined;
1176
- return url.replace(/"/g, '%22');
1177
- }
1178
- // #endregion
1179
- // #region Clipboard
1180
- /**
1181
- * Copies markdown content to the clipboard in two flavors:
1182
- * - text/html : the rendered HTML (rich paste into Word, Outlook, Gmail...)
1183
- * - text/plain : the original markdown source (paste into editors, IDEs...)
1184
- * @param markdown : the markdown source
1185
- * @param options : conversion options for the HTML flavor
1186
- * @returns : true on success
1187
- */
1188
- static async copyToClipboard(markdown, options) {
1189
- if (!markdown || typeof navigator === 'undefined' || !navigator.clipboard)
1190
- return false;
1191
- try {
1192
- if (typeof ClipboardItem !== 'undefined') {
1193
- const html = this.toHtml(markdown, options);
1194
- await navigator.clipboard.write([
1195
- new ClipboardItem({
1196
- 'text/html': new Blob([html], { type: 'text/html' }),
1197
- 'text/plain': new Blob([markdown], { type: 'text/plain' }),
1198
- }),
1199
- ]);
1200
- }
1201
- else {
1202
- // Older engines: plain text only
1203
- await navigator.clipboard.writeText(markdown);
1204
- }
1205
- return true;
1206
- }
1207
- catch {
1208
- // Clipboard API requires a secure context and user activation
1209
- return false;
1210
- }
1211
- }
1212
- /**
1213
- * Copies plain text (e.g. the content of a single code block) to the clipboard.
1214
- * @param text : the text to copy
1215
- * @returns : true on success
1216
- */
1217
- static async copyText(text) {
1218
- if (!text || typeof navigator === 'undefined' || !navigator.clipboard)
1219
- return false;
1220
- try {
1221
- await navigator.clipboard.writeText(text);
1222
- return true;
1223
- }
1224
- catch {
1225
- return false;
1226
- }
66
+ this.from = from;
67
+ this.to = to;
1227
68
  }
1228
- /**
1229
- * Extracts the visible plain text from a rendered element
1230
- * (what the user sees, entities already decoded by the browser).
1231
- * @param element : the element hosting the rendered markdown
1232
- * @returns : the plain text
1233
- */
1234
- static elementToText(element) {
1235
- return element.innerText ?? element.textContent ?? '';
69
+ clear() {
70
+ this.from = undefined;
71
+ this.to = undefined;
1236
72
  }
1237
73
  }
1238
74
 
75
+ const UtilsMessages = {
76
+ /**
77
+ * Messages
78
+ */
79
+ // Select dialog
80
+ UTILS_DIALOGS_SELECT_OPTIONS_CHANGED: '§utils-dialogs-select-options-changed'
81
+ };
82
+
1239
83
  var DateFormat;
1240
84
  (function (DateFormat) {
1241
85
  DateFormat[DateFormat["Short"] = 1] = "Short";
@@ -1504,15 +348,6 @@ class SystemUtils {
1504
348
  return '';
1505
349
  return s.replace(/https?:\/\/[^\s<]+/gi, v => `<a href="${v}" target="_blank" rel="noopener">${v}</a>`);
1506
350
  }
1507
- /**
1508
- * Convert markdown to html
1509
- * @param markdown : the markdown data
1510
- * @param escapeHtml : true to escape HTML. Default is false
1511
- * @returns the html
1512
- */
1513
- static markdownToHtml(markdown, escapeHtml = false) {
1514
- return MarkdownUtils.toHtml(markdown, { escapeHtml: escapeHtml });
1515
- }
1516
351
  /**
1517
352
  * Compare two names
1518
353
  * @param a : name a
@@ -1902,6 +737,32 @@ class SystemUtils {
1902
737
  return undefined;
1903
738
  return new TZDate(parts[0], parts[1], parts[2], 12);
1904
739
  }
740
+ /**
741
+ * Converts a Date, timestamp or ISO string into a Europe/Rome Date whose JSON serialisation
742
+ * emits a naive local datetime string ("yyyy-MM-dd'T'HH:mm:ss"), with no timezone designator,
743
+ * so the wall-clock value round-trips to the server unchanged. Use this instead of `new Date(...)`
744
+ * when reviving dates received from the API, to stay consistent with the DateFnsAdapter (values
745
+ * entered through the datepicker already behave this way).
746
+ * @param value : the source Date, Unix timestamp (ms) or ISO string
747
+ * @returns : a Europe/Rome Date serialising as naive local time, or undefined for empty/invalid input
748
+ */
749
+ static toLocalDate(value) {
750
+ if (value == null || value === '')
751
+ return undefined;
752
+ const ms = value instanceof Date ? value.getTime()
753
+ : typeof value === 'number' ? value
754
+ : new Date(value).getTime();
755
+ if (isNaN(ms))
756
+ return undefined;
757
+ const t = new TZDate(ms, 'Europe/Rome');
758
+ Object.defineProperty(t, 'toJSON', {
759
+ value: () => format(t, "yyyy-MM-dd'T'HH:mm:ss"),
760
+ enumerable: false,
761
+ configurable: true,
762
+ writable: true,
763
+ });
764
+ return t;
765
+ }
1905
766
  /**
1906
767
  * Format a date
1907
768
  * @param value : the date or string to format
@@ -1937,32 +798,6 @@ class SystemUtils {
1937
798
  return format(value, fmt, { locale: locale });
1938
799
  }
1939
800
  }
1940
- /**
1941
- * Converts a Date, timestamp or ISO string into a Europe/Rome Date whose JSON serialisation
1942
- * emits a naive local datetime string ("yyyy-MM-dd'T'HH:mm:ss"), with no timezone designator,
1943
- * so the wall-clock value round-trips to the server unchanged. Use this instead of `new Date(...)`
1944
- * when reviving dates received from the API, to stay consistent with the DateFnsAdapter (values
1945
- * entered through the datepicker already behave this way).
1946
- * @param value : the source Date, Unix timestamp (ms) or ISO string
1947
- * @returns : a Europe/Rome Date serialising as naive local time, or undefined for empty/invalid input
1948
- */
1949
- static toLocalDate(value) {
1950
- if (value == null || value === '')
1951
- return undefined;
1952
- const ms = value instanceof Date ? value.getTime()
1953
- : typeof value === 'number' ? value
1954
- : new Date(value).getTime();
1955
- if (isNaN(ms))
1956
- return undefined;
1957
- const t = new TZDate(ms, 'Europe/Rome');
1958
- Object.defineProperty(t, 'toJSON', {
1959
- value: () => format(t, "yyyy-MM-dd'T'HH:mm:ss"),
1960
- enumerable: false,
1961
- configurable: true,
1962
- writable: true,
1963
- });
1964
- return t;
1965
- }
1966
801
  /**
1967
802
  * Update a DateInterval object according to a string
1968
803
  * @param value : string value
@@ -2261,38 +1096,234 @@ class SystemUtils {
2261
1096
  else
2262
1097
  return [];
2263
1098
  }
2264
- /** Cache for resolved color luminance results. */
2265
- static { this.colorLightCache = new Map(); }
1099
+ /** Cache for resolved color luminance results. */
1100
+ static { this.colorLightCache = new Map(); }
1101
+ /**
1102
+ * Check if a color is light or dark
1103
+ * @param color : the color
1104
+ * @param minimumLuminance : the lumimance to consider
1105
+ * @returns true if the color is light
1106
+ */
1107
+ static isColorLight(color, minimumLuminance = 186) {
1108
+ if (!this.isBrowser())
1109
+ return true; // SSR fallback
1110
+ const cacheKey = `${color}|${minimumLuminance}`;
1111
+ const cached = this.colorLightCache.get(cacheKey);
1112
+ if (cached !== undefined)
1113
+ return cached;
1114
+ const tempDiv = document.createElement('div');
1115
+ tempDiv.style.color = color;
1116
+ document.body.appendChild(tempDiv);
1117
+ const rgb = getComputedStyle(tempDiv).color;
1118
+ document.body.removeChild(tempDiv);
1119
+ const result = rgb.match(/\d+/g);
1120
+ let light = true; // fallback
1121
+ if (result && result.length >= 3) {
1122
+ const r = parseInt(result[0], 10);
1123
+ const g = parseInt(result[1], 10);
1124
+ const b = parseInt(result[2], 10);
1125
+ light = (0.299 * r + 0.587 * g + 0.114 * b) > minimumLuminance;
1126
+ }
1127
+ this.colorLightCache.set(cacheKey, light);
1128
+ return light;
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * Generic selection model that tracks a set of selected items identified by a lookup field.
1134
+ * Wraps Angular CDK's `SelectionModel` and adds lookup-based add/remove logic.
1135
+ *
1136
+ * @typeParam T - The item type held in the selection.
1137
+ * @typeParam V - The type of the lookup key used to identify items.
1138
+ */
1139
+ class SelectableModel {
1140
+ /**
1141
+ * Snapshot of all items currently tracked by this model (selected or previously toggled).
1142
+ * Backed by a signal — reads are always up to date.
1143
+ */
1144
+ get all() {
1145
+ return this._all();
1146
+ }
1147
+ /** The underlying CDK `SelectionModel` (provides `.selected`, `.isSelected`, etc.). */
1148
+ get current() {
1149
+ return this._current;
1150
+ }
1151
+ /**
1152
+ * @param allowMultiSelect - When `true` (default), multiple items can be selected simultaneously.
1153
+ * @param lookupFieldName - Name of the field used as the unique key when searching the internal list
1154
+ * (default: `'id'`). Keys of `T` are autocompleted; plain strings remain accepted.
1155
+ */
1156
+ constructor(allowMultiSelect = true, lookupFieldName = 'id') {
1157
+ /**
1158
+ * Emits whenever the selection changes.
1159
+ * Carries the affected item, or `undefined` when the whole selection is cleared.
1160
+ *
1161
+ * Note: kept as `EventEmitter` (instead of a plain RxJS `Subject`) for
1162
+ * backward compatibility with existing consumers; `EventEmitter` extends
1163
+ * `Subject`, so `.subscribe()`, `pipe(...)` and `takeUntilDestroyed` all
1164
+ * work as expected.
1165
+ */
1166
+ this.changed = new EventEmitter();
1167
+ this._all = signal([], /* @ts-ignore */
1168
+ ...(ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []));
1169
+ /** Signal that is `true` when at least one item is tracked in the internal list. */
1170
+ this.hasValue = computed(() => this._all().length > 0, /* @ts-ignore */
1171
+ ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
1172
+ /** Signal with the number of items currently tracked in the internal list. */
1173
+ this.count = computed(() => this._all().length, /* @ts-ignore */
1174
+ ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
1175
+ /** Signal that is `true` when exactly one item is currently selected. */
1176
+ this.selectedOne = computed(() => this._all().length === 1, /* @ts-ignore */
1177
+ ...(ngDevMode ? [{ debugName: "selectedOne" }] : /* istanbul ignore next */ []));
1178
+ /** Signal that is `true` when two or more items are currently selected. */
1179
+ this.selectedMany = computed(() => this._all().length > 1, /* @ts-ignore */
1180
+ ...(ngDevMode ? [{ debugName: "selectedMany" }] : /* istanbul ignore next */ []));
1181
+ /** Signal that is `true` when at least one element is selected. */
1182
+ this.selectedAny = computed(() => this._all().length > 0, /* @ts-ignore */
1183
+ ...(ngDevMode ? [{ debugName: "selectedAny" }] : /* istanbul ignore next */ []));
1184
+ /** Signal that is `true` when no item is currently selected. */
1185
+ this.selectedNone = computed(() => this._all().length === 0, /* @ts-ignore */
1186
+ ...(ngDevMode ? [{ debugName: "selectedNone" }] : /* istanbul ignore next */ []));
1187
+ /** Signal with the first currently selected item, or `undefined` when none is selected. */
1188
+ this.first = computed(() => this._all()[0], /* @ts-ignore */
1189
+ ...(ngDevMode ? [{ debugName: "first" }] : /* istanbul ignore next */ []));
1190
+ this._current = new SelectionModel(allowMultiSelect, [], true,
1191
+ // Compare by lookup key so different instances of the same logical item
1192
+ // (e.g. after a data reload) are treated as the same selection entry.
1193
+ (a, b) => a[lookupFieldName] === b[lookupFieldName]);
1194
+ this._lookupFieldName = lookupFieldName;
1195
+ }
1196
+ /**
1197
+ * Toggles the CDK selection state of an item that already exists in the tracked list.
1198
+ * Has no effect when `lookupValue` does not match any tracked item.
1199
+ * @param item - The item whose selection state should be toggled.
1200
+ * @param lookupValue - The key value used to locate the item in the internal list.
1201
+ */
1202
+ updateCurrent(item, lookupValue) {
1203
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1204
+ if (p !== -1) {
1205
+ this._current.toggle(item);
1206
+ this.changed.emit(item);
1207
+ }
1208
+ }
1209
+ /**
1210
+ * Toggles an item in both the internal list and the CDK selection.
1211
+ * Adds the item when it is not yet tracked; removes it when it is.
1212
+ * @param item - The item to toggle.
1213
+ * @param lookupValue - The key value used to locate or register the item.
1214
+ */
1215
+ toggle(item, lookupValue) {
1216
+ if (lookupValue === undefined)
1217
+ return;
1218
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1219
+ if (p === -1) {
1220
+ this._all.update(arr => [...arr, item]);
1221
+ }
1222
+ else {
1223
+ this._all.update(arr => arr.filter((_, i) => i !== p));
1224
+ }
1225
+ this._current.toggle(item);
1226
+ this.changed.emit(item);
1227
+ }
1228
+ /**
1229
+ * Adds an item to the internal list (when not already present) and marks it as selected.
1230
+ * @param item - The item to select.
1231
+ * @param lookupValue - The key value used to locate or register the item.
1232
+ */
1233
+ select(item, lookupValue) {
1234
+ if (lookupValue === undefined)
1235
+ return;
1236
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1237
+ if (p === -1) {
1238
+ this._all.update(arr => [...arr, item]);
1239
+ }
1240
+ this._current.select(item);
1241
+ this.changed.emit(item);
1242
+ }
1243
+ /**
1244
+ * Removes an item from the internal list and deselects it in the CDK model.
1245
+ * Has no effect when the item is not currently tracked.
1246
+ * @param item - The item to deselect.
1247
+ * @param lookupValue - The key value used to locate the item in the internal list.
1248
+ */
1249
+ deselect(item, lookupValue) {
1250
+ if (lookupValue === undefined)
1251
+ return;
1252
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1253
+ if (p !== -1) {
1254
+ this._all.update(arr => arr.filter((_, i) => i !== p));
1255
+ this._current.deselect(item);
1256
+ this.changed.emit(item);
1257
+ }
1258
+ }
1259
+ /**
1260
+ * Deselects all items whose lookup key is contained in `lookupValues`.
1261
+ * @param lookupValues - Array of key values identifying the items to deselect.
1262
+ */
1263
+ deselectByValues(lookupValues) {
1264
+ if (lookupValues.length === 0)
1265
+ return;
1266
+ for (const key of lookupValues) {
1267
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, key);
1268
+ if (p !== -1) {
1269
+ const item = this._all()[p];
1270
+ this._all.update(arr => arr.filter((_, i) => i !== p));
1271
+ this._current.deselect(item);
1272
+ this.changed.emit(item);
1273
+ }
1274
+ }
1275
+ }
1276
+ /**
1277
+ * Runs `clearFunc` against every currently selected item, then clears the CDK selection.
1278
+ * The internal tracked list is **not** affected; only the CDK selection is cleared.
1279
+ * @param clearFunc - Callback invoked for each currently selected item before clearing.
1280
+ */
1281
+ clearCurrent(clearFunc) {
1282
+ for (const item of this._current.selected) {
1283
+ clearFunc(item);
1284
+ }
1285
+ this._current.clear();
1286
+ this.changed.emit(undefined);
1287
+ }
1288
+ /**
1289
+ * Clears both the internal tracked list and the CDK selection.
1290
+ */
1291
+ clear() {
1292
+ this._all.set([]);
1293
+ this._current.clear();
1294
+ this.changed.emit(undefined);
1295
+ }
2266
1296
  /**
2267
- * Check if a color is light or dark
2268
- * @param color : the color
2269
- * @param minimumLuminance : the lumimance to consider
2270
- * @returns true if the color is light
1297
+ * Returns `true` when the item identified by `lookupValue` is present in the tracked list.
1298
+ * @param lookupValue - The key value to look up.
2271
1299
  */
2272
- static isColorLight(color, minimumLuminance = 186) {
2273
- if (!this.isBrowser())
2274
- return true; // SSR fallback
2275
- const cacheKey = `${color}|${minimumLuminance}`;
2276
- const cached = this.colorLightCache.get(cacheKey);
2277
- if (cached !== undefined)
2278
- return cached;
2279
- const tempDiv = document.createElement('div');
2280
- tempDiv.style.color = color;
2281
- document.body.appendChild(tempDiv);
2282
- const rgb = getComputedStyle(tempDiv).color;
2283
- document.body.removeChild(tempDiv);
2284
- const result = rgb.match(/\d+/g);
2285
- let light = true; // fallback
2286
- if (result && result.length >= 3) {
2287
- const r = parseInt(result[0], 10);
2288
- const g = parseInt(result[1], 10);
2289
- const b = parseInt(result[2], 10);
2290
- light = (0.299 * r + 0.587 * g + 0.114 * b) > minimumLuminance;
2291
- }
2292
- this.colorLightCache.set(cacheKey, light);
2293
- return light;
1300
+ isSelected(lookupValue) {
1301
+ return (lookupValue !== undefined &&
1302
+ SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) !== -1);
1303
+ }
1304
+ }
1305
+
1306
+ /**
1307
+ * Directive that moves browser focus to the host element after the first render cycle.
1308
+ * Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
1309
+ */
1310
+ class AutoFocusDirective {
1311
+ constructor() {
1312
+ this.elementRef = inject(ElementRef);
1313
+ afterNextRender(() => {
1314
+ this.elementRef.nativeElement?.focus();
1315
+ });
2294
1316
  }
1317
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1318
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
2295
1319
  }
1320
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AutoFocusDirective, decorators: [{
1321
+ type: Directive,
1322
+ args: [{
1323
+ selector: '[autoFocus]',
1324
+ standalone: true
1325
+ }]
1326
+ }], ctorParameters: () => [] });
2296
1327
 
2297
1328
  /**
2298
1329
  * Directive that listens to `keyup` events on a date input and debounces changes
@@ -3166,34 +2197,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
3166
2197
  }]
3167
2198
  }] });
3168
2199
 
3169
- /**
3170
- * Pipe that converts a Markdown string to sanitized HTML using `SystemUtils.markdownToHtml`.
3171
- *
3172
- * Usage: `{{ text | formatMarkdown }}`
3173
- */
3174
- class FormatMarkdownPipe {
3175
- constructor() {
3176
- this.sanitizer = inject(DomSanitizer);
3177
- }
3178
- /**
3179
- * Transforms a Markdown string into sanitized HTML.
3180
- * @param value - The Markdown input to convert. Treated as an empty string when `undefined`.
3181
- * @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
3182
- */
3183
- transform(value) {
3184
- return this.sanitizer.bypassSecurityTrustHtml(SystemUtils.markdownToHtml(value ?? ''));
3185
- }
3186
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: FormatMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
3187
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.3", ngImport: i0, type: FormatMarkdownPipe, isStandalone: true, name: "formatMarkdown" }); }
3188
- }
3189
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: FormatMarkdownPipe, decorators: [{
3190
- type: Pipe,
3191
- args: [{
3192
- name: 'formatMarkdown',
3193
- standalone: true
3194
- }]
3195
- }] });
3196
-
3197
2200
  /**
3198
2201
  * General-purpose formatting pipe that converts a raw value to a locale-aware string
3199
2202
  * based on the specified format type.
@@ -3436,188 +2439,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
3436
2439
  }]
3437
2440
  }] });
3438
2441
 
3439
- const UtilsMessages = {
3440
- /**
3441
- * Messages
3442
- */
3443
- // Select dialog
3444
- UTILS_DIALOGS_SELECT_OPTIONS_CHANGED: '§utils-dialogs-select-options-changed'
3445
- };
3446
-
3447
- /**
3448
- * Generic selection model that tracks a set of selected items identified by a lookup field.
3449
- * Wraps Angular CDK's `SelectionModel` and adds lookup-based add/remove logic.
3450
- *
3451
- * @typeParam T - The item type held in the selection.
3452
- * @typeParam V - The type of the lookup key used to identify items.
3453
- */
3454
- class SelectableModel {
3455
- /**
3456
- * Snapshot of all items currently tracked by this model (selected or previously toggled).
3457
- * Backed by a signal — reads are always up to date.
3458
- */
3459
- get all() {
3460
- return this._all();
3461
- }
3462
- /** The underlying CDK `SelectionModel` (provides `.selected`, `.isSelected`, etc.). */
3463
- get current() {
3464
- return this._current;
3465
- }
3466
- /**
3467
- * @param allowMultiSelect - When `true` (default), multiple items can be selected simultaneously.
3468
- * @param lookupFieldName - Name of the field used as the unique key when searching the internal list
3469
- * (default: `'id'`). Keys of `T` are autocompleted; plain strings remain accepted.
3470
- */
3471
- constructor(allowMultiSelect = true, lookupFieldName = 'id') {
3472
- /**
3473
- * Emits whenever the selection changes.
3474
- * Carries the affected item, or `undefined` when the whole selection is cleared.
3475
- *
3476
- * Note: kept as `EventEmitter` (instead of a plain RxJS `Subject`) for
3477
- * backward compatibility with existing consumers; `EventEmitter` extends
3478
- * `Subject`, so `.subscribe()`, `pipe(...)` and `takeUntilDestroyed` all
3479
- * work as expected.
3480
- */
3481
- this.changed = new EventEmitter();
3482
- this._all = signal([], /* @ts-ignore */
3483
- ...(ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []));
3484
- /** Signal that is `true` when at least one item is tracked in the internal list. */
3485
- this.hasValue = computed(() => this._all().length > 0, /* @ts-ignore */
3486
- ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
3487
- /** Signal with the number of items currently tracked in the internal list. */
3488
- this.count = computed(() => this._all().length, /* @ts-ignore */
3489
- ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
3490
- /** Signal that is `true` when exactly one item is currently selected. */
3491
- this.selectedOne = computed(() => this._all().length === 1, /* @ts-ignore */
3492
- ...(ngDevMode ? [{ debugName: "selectedOne" }] : /* istanbul ignore next */ []));
3493
- /** Signal that is `true` when two or more items are currently selected. */
3494
- this.selectedMany = computed(() => this._all().length > 1, /* @ts-ignore */
3495
- ...(ngDevMode ? [{ debugName: "selectedMany" }] : /* istanbul ignore next */ []));
3496
- /** Signal that is `true` when no item is currently selected. */
3497
- this.selectedNone = computed(() => this._all().length === 0, /* @ts-ignore */
3498
- ...(ngDevMode ? [{ debugName: "selectedNone" }] : /* istanbul ignore next */ []));
3499
- /** Signal with the first currently selected item, or `undefined` when none is selected. */
3500
- this.selectedFirst = computed(() => this._all()[0], /* @ts-ignore */
3501
- ...(ngDevMode ? [{ debugName: "selectedFirst" }] : /* istanbul ignore next */ []));
3502
- /** Signal with a snapshot of all currently selected items. */
3503
- this.selectedAll = computed(() => this._all(), /* @ts-ignore */
3504
- ...(ngDevMode ? [{ debugName: "selectedAll" }] : /* istanbul ignore next */ []));
3505
- this._current = new SelectionModel(allowMultiSelect, [], true,
3506
- // Compare by lookup key so different instances of the same logical item
3507
- // (e.g. after a data reload) are treated as the same selection entry.
3508
- (a, b) => a[lookupFieldName] === b[lookupFieldName]);
3509
- this._lookupFieldName = lookupFieldName;
3510
- }
3511
- /**
3512
- * Toggles the CDK selection state of an item that already exists in the tracked list.
3513
- * Has no effect when `lookupValue` does not match any tracked item.
3514
- * @param item - The item whose selection state should be toggled.
3515
- * @param lookupValue - The key value used to locate the item in the internal list.
3516
- */
3517
- updateCurrent(item, lookupValue) {
3518
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
3519
- if (p !== -1) {
3520
- this._current.toggle(item);
3521
- this.changed.emit(item);
3522
- }
3523
- }
3524
- /**
3525
- * Toggles an item in both the internal list and the CDK selection.
3526
- * Adds the item when it is not yet tracked; removes it when it is.
3527
- * @param item - The item to toggle.
3528
- * @param lookupValue - The key value used to locate or register the item.
3529
- */
3530
- toggle(item, lookupValue) {
3531
- if (lookupValue === undefined)
3532
- return;
3533
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
3534
- if (p === -1) {
3535
- this._all.update(arr => [...arr, item]);
3536
- }
3537
- else {
3538
- this._all.update(arr => arr.filter((_, i) => i !== p));
3539
- }
3540
- this._current.toggle(item);
3541
- this.changed.emit(item);
3542
- }
3543
- /**
3544
- * Adds an item to the internal list (when not already present) and marks it as selected.
3545
- * @param item - The item to select.
3546
- * @param lookupValue - The key value used to locate or register the item.
3547
- */
3548
- select(item, lookupValue) {
3549
- if (lookupValue === undefined)
3550
- return;
3551
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
3552
- if (p === -1) {
3553
- this._all.update(arr => [...arr, item]);
3554
- }
3555
- this._current.select(item);
3556
- this.changed.emit(item);
3557
- }
3558
- /**
3559
- * Removes an item from the internal list and deselects it in the CDK model.
3560
- * Has no effect when the item is not currently tracked.
3561
- * @param item - The item to deselect.
3562
- * @param lookupValue - The key value used to locate the item in the internal list.
3563
- */
3564
- deselect(item, lookupValue) {
3565
- if (lookupValue === undefined)
3566
- return;
3567
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
3568
- if (p !== -1) {
3569
- this._all.update(arr => arr.filter((_, i) => i !== p));
3570
- this._current.deselect(item);
3571
- this.changed.emit(item);
3572
- }
3573
- }
3574
- /**
3575
- * Deselects all items whose lookup key is contained in `lookupValues`.
3576
- * @param lookupValues - Array of key values identifying the items to deselect.
3577
- */
3578
- deselectByValues(lookupValues) {
3579
- if (lookupValues.length === 0)
3580
- return;
3581
- for (const key of lookupValues) {
3582
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, key);
3583
- if (p !== -1) {
3584
- const item = this._all()[p];
3585
- this._all.update(arr => arr.filter((_, i) => i !== p));
3586
- this._current.deselect(item);
3587
- this.changed.emit(item);
3588
- }
3589
- }
3590
- }
3591
- /**
3592
- * Runs `clearFunc` against every currently selected item, then clears the CDK selection.
3593
- * The internal tracked list is **not** affected; only the CDK selection is cleared.
3594
- * @param clearFunc - Callback invoked for each currently selected item before clearing.
3595
- */
3596
- clearCurrent(clearFunc) {
3597
- for (const item of this._current.selected) {
3598
- clearFunc(item);
3599
- }
3600
- this._current.clear();
3601
- this.changed.emit(undefined);
3602
- }
3603
- /**
3604
- * Clears both the internal tracked list and the CDK selection.
3605
- */
3606
- clear() {
3607
- this._all.set([]);
3608
- this._current.clear();
3609
- this.changed.emit(undefined);
3610
- }
3611
- /**
3612
- * Returns `true` when the item identified by `lookupValue` is present in the tracked list.
3613
- * @param lookupValue - The key value to look up.
3614
- */
3615
- isSelected(lookupValue) {
3616
- return (lookupValue !== undefined &&
3617
- SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) !== -1);
3618
- }
3619
- }
3620
-
3621
2442
  /**
3622
2443
  * Standard Broadcast Messages Manager
3623
2444
  * https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
@@ -4125,12 +2946,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
4125
2946
  }], ctorParameters: () => [] });
4126
2947
 
4127
2948
  /*
4128
- * Public API Surface of scm-utils
2949
+ * Public API Surface of @arsedizioni/ars-utils/core
4129
2950
  */
4130
2951
 
4131
2952
  /**
4132
2953
  * Generated bundle index. Do not edit.
4133
2954
  */
4134
2955
 
4135
- export { ARS_TIME_ZONE, ArsLocalDateInterceptor, AutoFocusDirective, BroadcastChannelManager, BroadcastService, CHANNEL_NAME, CopyClipboardDirective, DEFAULT_TIME_ZONE, DateFnsAdapter, DateFormat, DateInterval, DateIntervalChangeDirective, DeleteModel, EmailsValidatorDirective, EnvironmentService, EqualsValidatorDirective, FileInfo, FileSizeValidatorDirective, FormatHtmlPipe, FormatMarkdownPipe, FormatPipe, GroupModel, GuidValidatorDirective, IDModel, ImportModel, LoginOAuthType, MAT_DATE_FNS_FORMATS, MaxTermsValidatorDirective, NotEmptyValidatorDirective, NotEqualValidatorDirective, NotFutureValidatorDirective, PasswordValidatorDirective, QueryModel, RelationModel, RemoveFocusDirective, ReplacePipe, SafeHtmlPipe, SafeUrlPipe, ScreenService, SearchCallbackPipe, SearchFilterPipe, SelectableModel, SplashService, SqlDateValidatorDirective, SystemUtils, ThemeService, TimeValidatorDirective, UpdateRelationsModel, UrlValidatorDirective, UtilsMessages, ValidIfDirective, ValidatorDirective, ValueModel, arsLocalDateInterceptor, provideArsDateFns, provideArsLocalDates, toLocalDateOnlyString, toLocalDateTimeString };
2956
+ export { AutoFocusDirective, BroadcastChannelManager, BroadcastService, CHANNEL_NAME, CopyClipboardDirective, DateFormat, DateInterval, DateIntervalChangeDirective, DeleteModel, EmailsValidatorDirective, EnvironmentService, EqualsValidatorDirective, FileInfo, FileSizeValidatorDirective, FormatHtmlPipe, FormatPipe, GroupModel, GuidValidatorDirective, IDModel, ImportModel, LoginOAuthType, MaxTermsValidatorDirective, NotEmptyValidatorDirective, NotEqualValidatorDirective, NotFutureValidatorDirective, PasswordValidatorDirective, QueryModel, RelationModel, RemoveFocusDirective, ReplacePipe, SafeHtmlPipe, SafeUrlPipe, ScreenService, SearchCallbackPipe, SearchFilterPipe, SelectableModel, SplashService, SqlDateValidatorDirective, SystemUtils, ThemeService, TimeValidatorDirective, UpdateRelationsModel, UrlValidatorDirective, UtilsMessages, ValidIfDirective, ValidatorDirective, ValueModel };
4136
2957
  //# sourceMappingURL=arsedizioni-ars-utils-core.mjs.map