@spartan-ng/cli 1.0.5 → 1.1.0-beta.1

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/generators/base/lib/build-dependency-array.js +1 -1
  3. package/src/generators/base/lib/build-dependency-array.js.map +1 -1
  4. package/src/generators/base/lib/styles/transform-style-map.js +15 -5
  5. package/src/generators/base/lib/styles/transform-style-map.js.map +1 -1
  6. package/src/generators/healthcheck/generator.js +2 -0
  7. package/src/generators/healthcheck/generator.js.map +1 -1
  8. package/src/generators/healthcheck/healthchecks/scaffold-integrity.d.ts +2 -0
  9. package/src/generators/healthcheck/healthchecks/scaffold-integrity.js +40 -0
  10. package/src/generators/healthcheck/healthchecks/scaffold-integrity.js.map +1 -0
  11. package/src/generators/ui/libs/calendar/files/index.ts.template +3 -1
  12. package/src/generators/ui/libs/calendar/files/lib/hlm-month-year-calendar.ts.template +115 -0
  13. package/src/generators/ui/libs/date-picker/files/index.ts.template +13 -1
  14. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-multi-input.ts.template +85 -0
  15. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker-input.ts.template +23 -124
  16. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker-multi.token.ts.template +57 -0
  17. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker-multi.ts.template +25 -5
  18. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker-trigger.ts.template +8 -5
  19. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker.token.ts.template +14 -36
  20. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker.ts.template +18 -15
  21. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-range-input.ts.template +86 -0
  22. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-range-picker.token.ts.template +36 -1
  23. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-range-picker.ts.template +38 -7
  24. package/src/generators/ui/libs/date-picker/files/lib/hlm-month-year-input.ts.template +86 -0
  25. package/src/generators/ui/libs/date-picker/files/lib/hlm-month-year-picker.token.ts.template +85 -0
  26. package/src/generators/ui/libs/date-picker/files/lib/hlm-month-year-picker.ts.template +185 -0
  27. package/src/generators/ui/libs/dialog/files/lib/hlm-dialog.service.ts.template +16 -3
  28. package/src/generators/ui/libs/sidebar/files/lib/hlm-sidebar-trigger.ts.template +1 -1
  29. package/src/generators/ui/primitive-deps.js +5 -5
  30. package/src/generators/ui/primitive-deps.js.map +1 -1
  31. package/src/generators/ui/supported-ui-libraries.json +44 -43
  32. package/src/utils/version-utils.d.ts +1 -0
  33. package/src/utils/version-utils.js +13 -0
  34. package/src/utils/version-utils.js.map +1 -1
  35. package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker-trigger.token.ts.template +0 -11
@@ -1,29 +1,23 @@
1
- import { BooleanInput } from '@angular/cdk/coercion';
2
- import {
3
- booleanAttribute,
4
- ChangeDetectionStrategy,
5
- Component,
6
- computed,
7
- effect,
8
- ElementRef,
9
- inject,
10
- input,
11
- linkedSignal,
12
- } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
13
2
  import { NgIcon, provideIcons } from '@ng-icons/core';
14
3
  import { lucideCalendar, lucideX } from '@ng-icons/lucide';
4
+ import {
5
+ BrnDateInput,
6
+ type BrnDatePickerTriggerBase,
7
+ provideBrnDatePickerTrigger,
8
+ } from '@spartan-ng/brain/date-picker';
15
9
  import { HlmInputGroup, HlmInputGroupImports } from '<%- importAlias %>/input-group';
16
- import { HlmDatePickerTriggerBase, provideHlmDatePickerTrigger } from './hlm-date-picker-trigger.token';
17
- import { injectHlmDatePicker, injectHlmDatePickerConfig } from './hlm-date-picker.token';
10
+ import { injectHlmDatePickerConfig } from './hlm-date-picker.token';
18
11
 
19
12
  @Component({
20
13
  selector: 'hlm-date-picker-input',
21
14
  imports: [HlmInputGroupImports, NgIcon],
22
- providers: [provideIcons({ lucideCalendar, lucideX }), provideHlmDatePickerTrigger(HlmDatePickerInput)],
15
+ providers: [provideIcons({ lucideCalendar, lucideX }), provideBrnDatePickerTrigger(HlmDatePickerInput)],
23
16
  changeDetection: ChangeDetectionStrategy.OnPush,
24
17
  hostDirectives: [HlmInputGroup],
25
18
  template: `
26
19
  <input
20
+ #input
27
21
  hlmInputGroupInput
28
22
  [value]="_inputValue()"
29
23
  [id]="inputId()"
@@ -34,7 +28,8 @@ import { injectHlmDatePicker, injectHlmDatePickerConfig } from './hlm-date-picke
34
28
  (keydown.arrowDown)="_open()"
35
29
  (keydown.enter)="_handleEnter($event)"
36
30
  (input)="_handleInputChange($event)"
37
- (blur)="_commitDate()"
31
+ (focus)="_handleFocus()"
32
+ (blur)="_handleBlur()"
38
33
  />
39
34
  <hlm-input-group-addon align="inline-end">
40
35
  @if (_showClearButton()) {
@@ -61,127 +56,31 @@ import { injectHlmDatePicker, injectHlmDatePickerConfig } from './hlm-date-picke
61
56
  </hlm-input-group-addon>
62
57
  `,
63
58
  })
64
- export class HlmDatePickerInput<T> implements HlmDatePickerTriggerBase {
65
- private static _nextId = 0;
66
- private readonly _host = inject(ElementRef);
67
- private readonly _datePicker = injectHlmDatePicker<T>();
59
+ export class HlmDatePickerInput<T> extends BrnDateInput<T> implements BrnDatePickerTriggerBase {
68
60
  private readonly _config = injectHlmDatePickerConfig<T>();
69
61
 
70
- protected readonly _popover = this._datePicker.popover;
71
- protected readonly _disabled = this._datePicker.disabledState;
72
-
73
- public readonly inputId = input(`hlm-date-picker-input-${HlmDatePickerInput._nextId++}`);
74
-
75
- public readonly placeholder = input('');
76
-
77
- public readonly inputValue = input<string>('');
78
-
79
62
  /**
80
- * Parses input text into a date value. Return `undefined` for invalid
63
+ * Parses input text into a date value. Return `null` for invalid
81
64
  * input - the picker's date is cleared while the text is preserved so
82
65
  * the user can fix it.
83
66
  *
84
67
  * Defaults to `parseDate` from `HlmDatePickerConfig`.
85
68
  */
86
- public readonly parseDate = input<(value: string) => T | undefined>(this._config.parseDate);
87
-
88
- public readonly forceInvalid = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
89
-
90
- /** Show a clear button that resets the input and picker date. Hidden when empty. */
91
- public readonly showClear = input<boolean, BooleanInput>(true, { transform: booleanAttribute });
92
-
93
- /** Open the popover on input click. */
94
- public readonly openOnClick = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
95
-
96
- /** Accessible label for the clear button. */
97
- public readonly clearAriaLabel = input<string>('Clear date');
98
-
99
- /** Accessible label for the calendar trigger button. */
100
- public readonly calendarAriaLabel = input<string>('Open calendar');
101
-
102
- /** @internal Id used by the trigger contract for labeling. */
103
- public readonly triggerId = this.inputId;
69
+ public readonly parseDate = input<(value: string) => T | null>(this._config.parseDate);
104
70
 
105
71
  /**
106
- * Text shown in the input. Mirrors the picker's `formattedDate` and the
107
- * parent's `inputValue`, and accepts user writes via `_handleInputChange`.
108
- * Commits only happen on blur / Enter, so in-progress text isn't clobbered.
72
+ * Formats the current date into the input/edit format shown while the
73
+ * input is focused. On blur the picker's display format is restored.
74
+ *
75
+ * Defaults to `formatInputDate` from `HlmDatePickerConfig`.
109
76
  */
110
- protected readonly _inputValue = linkedSignal<{ formatted: string | undefined; inputValue: string }, string>({
111
- source: () => ({
112
- formatted: this._datePicker.formattedDate(),
113
- inputValue: this.inputValue(),
114
- }),
115
- computation: (source, previous) => {
116
- // First render: prefer formatted, fall back to inputValue.
117
- if (previous === undefined) {
118
- return source.formatted ?? source.inputValue;
119
- }
120
-
121
- // Picker's formatted date changed - snap to canonical format.
122
- if (source.formatted !== previous.source.formatted) {
123
- if (source.formatted !== undefined) {
124
- return source.formatted;
125
- }
126
- // Cleared externally vs. user has invalid text in flight: only
127
- // mirror the clear when the displayed text was in sync.
128
- return previous.value === previous.source.formatted ? '' : previous.value;
129
- }
130
-
131
- // Parent updated inputValue - reflect it.
132
- if (source.inputValue !== previous.source.inputValue) {
133
- return source.inputValue;
134
- }
135
-
136
- return previous.value;
137
- },
138
- });
139
-
140
- constructor() {
141
- effect(() => this._popover()?.setOrigin(this._host.nativeElement));
142
- }
143
-
144
- protected _handleInputChange(event: Event) {
145
- const text = (event.target as HTMLInputElement).value;
146
- this._inputValue.set(text);
147
- }
148
-
149
- protected readonly _showClearButton = computed(() => this.showClear() && this._inputValue().length > 0);
150
-
151
- protected _clear() {
152
- this._inputValue.set('');
153
- this._datePicker.updateDate?.(undefined);
154
- this._datePicker.touched?.();
155
- }
156
-
157
- protected _handleEnter(event: Event) {
158
- event.preventDefault();
159
- this._commitDate();
160
- this._popover().close();
161
- }
162
-
163
- protected _commitDate() {
164
- const value = this._inputValue();
165
-
166
- if (!value) {
167
- this._datePicker.updateDate?.(undefined);
168
- this._datePicker.touched?.();
169
- return;
170
- }
171
-
172
- // Invalid parse: clear the picker date, keep the text so the user can fix it.
173
- const parsed = this.parseDate()(value);
174
- this._datePicker.updateDate?.(parsed ?? undefined);
175
- this._datePicker.touched?.();
176
- }
77
+ public readonly formatInputDate = input<(date: T) => string>(this._config.formatInputDate);
177
78
 
178
- protected _open() {
179
- this._popover().open();
79
+ protected parseValue(value: string): T | null {
80
+ return this.parseDate()(value);
180
81
  }
181
82
 
182
- protected _handleClick() {
183
- if (this.openOnClick()) {
184
- this._open();
185
- }
83
+ protected formatInputValue(value: T): string {
84
+ return this.formatInputDate()(value);
186
85
  }
187
86
  }
@@ -14,6 +14,15 @@ export interface HlmDatePickerMultiConfig<T> {
14
14
  */
15
15
  formatDates: (dates: T[]) => string;
16
16
 
17
+ /**
18
+ * Defines how the dates should be displayed while the input is focused,
19
+ * i.e. the format the user is expected to type in.
20
+ *
21
+ * @param dates
22
+ * @returns formatted dates in the input/edit format
23
+ */
24
+ formatInputDates: (dates: T[]) => string;
25
+
17
26
  /**
18
27
  * Defines how the date should be transformed before saving to model/form.
19
28
  *
@@ -21,13 +30,61 @@ export interface HlmDatePickerMultiConfig<T> {
21
30
  * @returns transformed date
22
31
  */
23
32
  transformDates: (dates: T[]) => T[];
33
+
34
+ /**
35
+ * Parse a user-entered string into a date.
36
+ *
37
+ * @param value the raw string from the input
38
+ * @returns the parsed date, or `null` when the value can't be parsed
39
+ */
40
+ parseDate: (value: string) => T[] | null;
24
41
  }
25
42
 
26
43
  function getDefaultConfig<T>(): HlmDatePickerMultiConfig<T> {
27
44
  return {
28
45
  formatDates: (dates) => dates.map((date) => (date instanceof Date ? date.toDateString() : `${date}`)).join(', '),
46
+ formatInputDates: (dates) =>
47
+ dates
48
+ .map((date) => {
49
+ if (!(date instanceof Date)) return `${date}`;
50
+
51
+ const day = String(date.getDate()).padStart(2, '0');
52
+ const month = String(date.getMonth() + 1).padStart(2, '0');
53
+ const year = date.getFullYear();
54
+
55
+ return `${day}/${month}/${year}`;
56
+ })
57
+ .join(', '),
29
58
  transformDates: (dates) => dates,
30
59
  autoCloseOnMaxSelection: false,
60
+ parseDate: (value) => {
61
+ if (typeof value !== 'string') return null;
62
+
63
+ const parts = value.split(',').map((v) => v.trim());
64
+ const result: Date[] = [];
65
+
66
+ for (const part of parts) {
67
+ const match = part.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
68
+ if (!match) return null;
69
+
70
+ const day = Number(match[1]);
71
+ const month = Number(match[2]);
72
+ const year = Number(match[3]);
73
+
74
+ if (month < 1 || month > 12) return null;
75
+ if (day < 1 || day > 31) return null;
76
+
77
+ const date = new Date(year, month - 1, day);
78
+
79
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
80
+ return null;
81
+ }
82
+
83
+ result.push(date);
84
+ }
85
+
86
+ return result as T[];
87
+ },
31
88
  };
32
89
  }
33
90
 
@@ -14,6 +14,7 @@ import {
14
14
  viewChild,
15
15
  } from '@angular/core';
16
16
  import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
17
+ import { type BrnDatePickerBase, BrnDatePickerTriggerToken, provideBrnDatePicker } from '@spartan-ng/brain/date-picker';
17
18
  import { BrnFieldControl, provideBrnLabelable } from '@spartan-ng/brain/field';
18
19
  import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
19
20
  import type { BrnOverlayState } from '@spartan-ng/brain/overlay';
@@ -21,8 +22,6 @@ import { BrnPopover } from '@spartan-ng/brain/popover';
21
22
  import { HlmCalendarMulti } from '<%- importAlias %>/calendar';
22
23
  import { HlmPopoverImports } from '<%- importAlias %>/popover';
23
24
  import { injectHlmDatePickerMultiConfig } from './hlm-date-picker-multi.token';
24
- import { HlmDatePickerTriggerToken } from './hlm-date-picker-trigger.token';
25
- import { HlmDatePickerBase, provideHlmDatePicker } from './hlm-date-picker.token';
26
25
 
27
26
  export const HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR = {
28
27
  provide: NG_VALUE_ACCESSOR,
@@ -35,7 +34,7 @@ export const HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR = {
35
34
  imports: [HlmPopoverImports, HlmCalendarMulti],
36
35
  providers: [
37
36
  HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR,
38
- provideHlmDatePicker(HlmDatePickerMulti),
37
+ provideBrnDatePicker(HlmDatePickerMulti),
39
38
  provideBrnLabelable(HlmDatePickerMulti),
40
39
  ],
41
40
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -63,12 +62,12 @@ export const HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR = {
63
62
  </hlm-popover>
64
63
  `,
65
64
  })
66
- export class HlmDatePickerMulti<T> implements HlmDatePickerBase<T>, ControlValueAccessor {
65
+ export class HlmDatePickerMulti<T> implements BrnDatePickerBase<T[]>, ControlValueAccessor {
67
66
  private readonly _config = injectHlmDatePickerMultiConfig<T>();
68
67
 
69
68
  public readonly popover = viewChild.required(BrnPopover);
70
69
 
71
- private readonly _trigger = contentChild(HlmDatePickerTriggerToken);
70
+ private readonly _trigger = contentChild(BrnDatePickerTriggerToken);
72
71
 
73
72
  /** Show dropdowns to navigate between months or years. */
74
73
  public readonly captionLayout = input<'dropdown' | 'label' | 'dropdown-months' | 'dropdown-years'>('label');
@@ -128,6 +127,9 @@ export class HlmDatePickerMulti<T> implements HlmDatePickerBase<T>, ControlValue
128
127
 
129
128
  public readonly hasDate = computed(() => !!this._mutableDate()?.length);
130
129
 
130
+ /** @internal The current raw value, used by inputs to reformat on focus. */
131
+ public readonly value = computed(() => this._mutableDate() ?? null);
132
+
131
133
  protected _onChange?: ChangeFn<T[]>;
132
134
  protected _onTouched?: TouchFn;
133
135
 
@@ -151,6 +153,24 @@ export class HlmDatePickerMulti<T> implements HlmDatePickerBase<T>, ControlValue
151
153
  }
152
154
  }
153
155
 
156
+ /**
157
+ * Commit dates to the picker. Updates the internal model, notifies form
158
+ * controls, and emits `dateChange`. Intended to be called from a text input
159
+ * that parses user-entered values. Pass `null` to clear the selection.
160
+ */
161
+ public updateDate(value: T[] | null) {
162
+ if (this._disabled()) return;
163
+ const transformedDate = value ? this.transformDates()(value) : undefined;
164
+
165
+ this._mutableDate.set(transformedDate);
166
+ this._onChange?.(transformedDate ?? []);
167
+ this.dateChange.emit(transformedDate ?? []);
168
+ }
169
+
170
+ public touched(): void {
171
+ this._onTouched?.();
172
+ }
173
+
154
174
  /** CONTROL VALUE ACCESSOR */
155
175
  public writeValue(value: T[] | null): void {
156
176
  this._mutableDate.set(value ? this.transformDates()(value) : undefined);
@@ -2,18 +2,21 @@ import { BooleanInput } from '@angular/cdk/coercion';
2
2
  import { booleanAttribute, ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
3
3
  import { NgIcon, provideIcons } from '@ng-icons/core';
4
4
  import { lucideChevronDown } from '@ng-icons/lucide';
5
+ import {
6
+ type BrnDatePickerTriggerBase,
7
+ injectBrnDatePicker,
8
+ provideBrnDatePickerTrigger,
9
+ } from '@spartan-ng/brain/date-picker';
5
10
  import { BrnFieldControl, BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
6
11
  import { ButtonVariants, HlmButtonImports } from '<%- importAlias %>/button';
7
12
  import { HlmPopoverTrigger } from '<%- importAlias %>/popover';
8
13
  import { hlm } from '<%- importAlias %>/utils';
9
14
  import { ClassValue } from 'clsx';
10
- import { HlmDatePickerTriggerBase, provideHlmDatePickerTrigger } from './hlm-date-picker-trigger.token';
11
- import { injectHlmDatePicker } from './hlm-date-picker.token';
12
15
 
13
16
  @Component({
14
17
  selector: 'hlm-date-picker-trigger',
15
18
  imports: [HlmButtonImports, HlmPopoverTrigger, NgIcon, BrnFieldControlDescribedBy],
16
- providers: [provideIcons({ lucideChevronDown }), provideHlmDatePickerTrigger(HlmDatePickerTrigger)],
19
+ providers: [provideIcons({ lucideChevronDown }), provideBrnDatePickerTrigger(HlmDatePickerTrigger)],
17
20
  changeDetection: ChangeDetectionStrategy.OnPush,
18
21
  host: { 'data-slot': 'date-picker-trigger' },
19
22
  template: `
@@ -48,11 +51,11 @@ import { injectHlmDatePicker } from './hlm-date-picker.token';
48
51
  </button>
49
52
  `,
50
53
  })
51
- export class HlmDatePickerTrigger implements HlmDatePickerTriggerBase {
54
+ export class HlmDatePickerTrigger implements BrnDatePickerTriggerBase {
52
55
  private static _nextId = 0;
53
56
 
54
57
  private readonly _fieldControl = inject(BrnFieldControl, { optional: true });
55
- private readonly _datePicker = injectHlmDatePicker();
58
+ private readonly _datePicker = injectBrnDatePicker();
56
59
 
57
60
  private readonly _invalid = this._fieldControl?.invalid;
58
61
  protected readonly _spartanInvalid = computed(() => this.forceInvalid() || this._fieldControl?.spartanInvalid());
@@ -1,36 +1,4 @@
1
- import {
2
- type ExistingProvider,
3
- inject,
4
- InjectionToken,
5
- type Signal,
6
- type Type,
7
- type ValueProvider,
8
- } from '@angular/core';
9
- import type { BrnPopover } from '@spartan-ng/brain/popover';
10
-
11
- export interface HlmDatePickerBase<T> {
12
- popover: Signal<BrnPopover>;
13
- disabledState: Signal<boolean>;
14
- formattedDate: Signal<string | undefined>;
15
- hasDate: Signal<boolean>;
16
- /** Commit a date to the picker (e.g. from a parsed input). Pass `undefined` to clear. Optional. */
17
- updateDate?(value: T | undefined): void;
18
- // used for ControlValueAccessor
19
- touched?(): void;
20
- }
21
-
22
- export const HlmDatePickerToken = new InjectionToken<HlmDatePickerBase<unknown>>('HlmDatePickerToken');
23
-
24
- export function provideHlmDatePicker(instance: Type<HlmDatePickerBase<unknown>>): ExistingProvider {
25
- return { provide: HlmDatePickerToken, useExisting: instance };
26
- }
27
-
28
- /**
29
- * Inject the date picker component.
30
- */
31
- export function injectHlmDatePicker<T>(): HlmDatePickerBase<T> {
32
- return inject(HlmDatePickerToken) as HlmDatePickerBase<T>;
33
- }
1
+ import { inject, InjectionToken, type ValueProvider } from '@angular/core';
34
2
 
35
3
  export interface HlmDatePickerConfig<T> {
36
4
  /**
@@ -46,6 +14,15 @@ export interface HlmDatePickerConfig<T> {
46
14
  */
47
15
  formatDate: (date: T) => string;
48
16
 
17
+ /**
18
+ * Defines how the date should be displayed while the input is focused,
19
+ * i.e. the format the user is expected to type in.
20
+ *
21
+ * @param date
22
+ * @returns formatted date in the input/edit format
23
+ */
24
+ formatInputDate: (date: T) => string;
25
+
49
26
  /**
50
27
  * Defines how the date should be transformed before saving to model/form.
51
28
  *
@@ -58,18 +35,19 @@ export interface HlmDatePickerConfig<T> {
58
35
  * Parse a user-entered string into a date.
59
36
  *
60
37
  * @param value the raw string from the input
61
- * @returns the parsed date, or `undefined` when the value can't be parsed
38
+ * @returns the parsed date, or `null` when the value can't be parsed
62
39
  */
63
- parseDate: (value: string) => T | undefined;
40
+ parseDate: (value: string) => T | null;
64
41
  }
65
42
 
66
43
  function getDefaultConfig<T>(): HlmDatePickerConfig<T> {
67
44
  return {
68
45
  formatDate: (date) => (date instanceof Date ? date.toDateString() : `${date}`),
46
+ formatInputDate: (date) => (date instanceof Date ? date.toDateString() : `${date}`),
69
47
  transformDate: (date) => date,
70
48
  parseDate: (value) => {
71
49
  const date = new Date(value);
72
- return isNaN(date.getTime()) ? undefined : (date as T);
50
+ return isNaN(date.getTime()) ? null : (date as T);
73
51
  },
74
52
  autoCloseOnSelect: false,
75
53
  };
@@ -13,14 +13,14 @@ import {
13
13
  viewChild,
14
14
  } from '@angular/core';
15
15
  import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
16
+ import { type BrnDatePickerBase, BrnDatePickerTriggerToken, provideBrnDatePicker } from '@spartan-ng/brain/date-picker';
16
17
  import { BrnFieldControl, provideBrnLabelable } from '@spartan-ng/brain/field';
17
18
  import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
18
19
  import type { BrnOverlayState } from '@spartan-ng/brain/overlay';
19
20
  import { BrnPopover } from '@spartan-ng/brain/popover';
20
21
  import { HlmCalendar } from '<%- importAlias %>/calendar';
21
22
  import { HlmPopoverImports } from '<%- importAlias %>/popover';
22
- import { HlmDatePickerTriggerToken } from './hlm-date-picker-trigger.token';
23
- import { HlmDatePickerBase, injectHlmDatePickerConfig, provideHlmDatePicker } from './hlm-date-picker.token';
23
+ import { injectHlmDatePickerConfig } from './hlm-date-picker.token';
24
24
 
25
25
  export const HLM_DATE_PICKER_VALUE_ACCESSOR = {
26
26
  provide: NG_VALUE_ACCESSOR,
@@ -31,7 +31,7 @@ export const HLM_DATE_PICKER_VALUE_ACCESSOR = {
31
31
  @Component({
32
32
  selector: 'hlm-date-picker',
33
33
  imports: [HlmPopoverImports, HlmCalendar],
34
- providers: [HLM_DATE_PICKER_VALUE_ACCESSOR, provideHlmDatePicker(HlmDatePicker), provideBrnLabelable(HlmDatePicker)],
34
+ providers: [HLM_DATE_PICKER_VALUE_ACCESSOR, provideBrnDatePicker(HlmDatePicker), provideBrnLabelable(HlmDatePicker)],
35
35
  changeDetection: ChangeDetectionStrategy.OnPush,
36
36
  hostDirectives: [BrnFieldControl],
37
37
  host: { class: 'block' },
@@ -56,12 +56,12 @@ export const HLM_DATE_PICKER_VALUE_ACCESSOR = {
56
56
  </hlm-popover>
57
57
  `,
58
58
  })
59
- export class HlmDatePicker<T> implements HlmDatePickerBase<T>, ControlValueAccessor {
59
+ export class HlmDatePicker<T> implements BrnDatePickerBase<T>, ControlValueAccessor {
60
60
  private readonly _config = injectHlmDatePickerConfig<T>();
61
61
 
62
62
  public readonly popover = viewChild.required(BrnPopover);
63
63
 
64
- private readonly _trigger = contentChild(HlmDatePickerTriggerToken);
64
+ private readonly _trigger = contentChild(BrnDatePickerTriggerToken);
65
65
 
66
66
  /** Show dropdowns to navigate between months or years. */
67
67
  public readonly captionLayout = input<'dropdown' | 'label' | 'dropdown-months' | 'dropdown-years'>('label');
@@ -108,13 +108,16 @@ export class HlmDatePicker<T> implements HlmDatePickerBase<T>, ControlValueAcces
108
108
  return date ? this.formatDate()(date) : undefined;
109
109
  });
110
110
 
111
- public readonly dateChange = output<T>();
111
+ public readonly dateChange = output<T | null>();
112
112
 
113
113
  public readonly labelableId = computed(() => this._trigger()?.triggerId());
114
114
 
115
115
  public readonly hasDate = computed(() => !!this._mutableDate());
116
116
 
117
- protected _onChange?: ChangeFn<T>;
117
+ /** @internal The current raw value, used by inputs to reformat on focus. */
118
+ public readonly value = computed(() => this._mutableDate() ?? null);
119
+
120
+ protected _onChange?: ChangeFn<T | null>;
118
121
  protected _onTouched?: TouchFn;
119
122
 
120
123
  protected _onStateChange(state: BrnOverlayState) {
@@ -124,7 +127,7 @@ export class HlmDatePicker<T> implements HlmDatePickerBase<T>, ControlValueAcces
124
127
 
125
128
  protected _handleChange(value: T | undefined) {
126
129
  if (this._disabled()) return;
127
- this.updateDate(value);
130
+ this.updateDate(value ?? null);
128
131
 
129
132
  if (this.autoCloseOnSelect()) {
130
133
  this._popoverState.set('closed');
@@ -137,13 +140,13 @@ export class HlmDatePicker<T> implements HlmDatePickerBase<T>, ControlValueAcces
137
140
  * close the popover - it's intended to be called from a text input that
138
141
  * is parsing user-entered values while typing.
139
142
  */
140
- public updateDate(value: T | undefined) {
143
+ public updateDate(value: T | null) {
141
144
  if (this._disabled()) return;
142
- const transformedDate = value !== undefined ? this.transformDate()(value) : undefined;
145
+ const transformedDate = value != null ? this.transformDate()(value) : undefined;
143
146
 
144
147
  this._mutableDate.set(transformedDate);
145
- this._onChange?.(transformedDate as T);
146
- this.dateChange.emit(transformedDate as T);
148
+ this._onChange?.(transformedDate ?? null);
149
+ this.dateChange.emit(transformedDate ?? null);
147
150
  }
148
151
 
149
152
  /** CONTROL VALUE ACCESSOR */
@@ -151,7 +154,7 @@ export class HlmDatePicker<T> implements HlmDatePickerBase<T>, ControlValueAcces
151
154
  this._mutableDate.set(value ? this.transformDate()(value) : undefined);
152
155
  }
153
156
 
154
- public registerOnChange(fn: ChangeFn<T>): void {
157
+ public registerOnChange(fn: ChangeFn<T | null>): void {
155
158
  this._onChange = fn;
156
159
  }
157
160
 
@@ -177,7 +180,7 @@ export class HlmDatePicker<T> implements HlmDatePickerBase<T>, ControlValueAcces
177
180
 
178
181
  public reset() {
179
182
  this._mutableDate.set(undefined);
180
- this._onChange?.(undefined as T);
181
- this.dateChange.emit(undefined as T);
183
+ this._onChange?.(null);
184
+ this.dateChange.emit(null);
182
185
  }
183
186
  }
@@ -0,0 +1,86 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+ import { NgIcon, provideIcons } from '@ng-icons/core';
3
+ import { lucideCalendar, lucideX } from '@ng-icons/lucide';
4
+ import {
5
+ BrnDateInput,
6
+ type BrnDatePickerTriggerBase,
7
+ provideBrnDatePickerTrigger,
8
+ } from '@spartan-ng/brain/date-picker';
9
+ import { HlmInputGroup, HlmInputGroupImports } from '<%- importAlias %>/input-group';
10
+ import { injectHlmDateRangePickerConfig } from './hlm-date-range-picker.token';
11
+
12
+ @Component({
13
+ selector: 'hlm-date-range-input',
14
+ imports: [HlmInputGroupImports, NgIcon],
15
+ providers: [provideIcons({ lucideCalendar, lucideX }), provideBrnDatePickerTrigger(HlmDateRangeInput)],
16
+ changeDetection: ChangeDetectionStrategy.OnPush,
17
+ hostDirectives: [HlmInputGroup],
18
+ template: `
19
+ <input
20
+ #input
21
+ hlmInputGroupInput
22
+ [value]="_inputValue()"
23
+ [id]="inputId()"
24
+ [placeholder]="placeholder()"
25
+ [disabled]="_disabled()"
26
+ [forceInvalid]="forceInvalid()"
27
+ (click)="_handleClick()"
28
+ (keydown.arrowDown)="_open()"
29
+ (keydown.enter)="_handleEnter($event)"
30
+ (input)="_handleInputChange($event)"
31
+ (focus)="_handleFocus()"
32
+ (blur)="_handleBlur()"
33
+ />
34
+ <hlm-input-group-addon align="inline-end">
35
+ @if (_showClearButton()) {
36
+ <button
37
+ hlmInputGroupButton
38
+ size="icon-xs"
39
+ variant="ghost"
40
+ [attr.aria-label]="clearAriaLabel()"
41
+ (click)="_clear()"
42
+ [disabled]="_disabled()"
43
+ >
44
+ <ng-icon name="lucideX" />
45
+ </button>
46
+ }
47
+ <button
48
+ hlmInputGroupButton
49
+ size="icon-xs"
50
+ [attr.aria-label]="calendarAriaLabel()"
51
+ (click)="_popover().open()"
52
+ [disabled]="_disabled()"
53
+ >
54
+ <ng-icon name="lucideCalendar" />
55
+ </button>
56
+ </hlm-input-group-addon>
57
+ `,
58
+ })
59
+ export class HlmDateRangeInput<T> extends BrnDateInput<[T, T]> implements BrnDatePickerTriggerBase {
60
+ private readonly _config = injectHlmDateRangePickerConfig<T>();
61
+
62
+ /**
63
+ * Parses input text into a date range. Return `null` for invalid
64
+ * input - the picker's range is cleared while the text is preserved so
65
+ * the user can fix it.
66
+ *
67
+ * Defaults to `parseDate` from `HlmDateRangePickerConfig`.
68
+ */
69
+ public readonly parseDate = input<(value: string) => [T, T] | null>(this._config.parseDate);
70
+
71
+ /**
72
+ * Formats the current range into the input/edit format shown while the
73
+ * input is focused. On blur the picker's display format is restored.
74
+ *
75
+ * Defaults to `formatInputDates` from `HlmDateRangePickerConfig`.
76
+ */
77
+ public readonly formatInputDates = input<(dates: [T | null, T | null]) => string>(this._config.formatInputDates);
78
+
79
+ protected override parseValue(value: string): [T, T] | null {
80
+ return this.parseDate()(value);
81
+ }
82
+
83
+ protected override formatInputValue(value: [T, T]): string {
84
+ return this.formatInputDates()(value);
85
+ }
86
+ }