@egose/shadcn-theme-ng-tw 0.5.0 → 0.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.
@@ -12,7 +12,7 @@ import { HlmBtn, provideBrnButtonConfig } from '@egose/shadcn-theme-ng-tw/button
12
12
  class HlmAlertDialogOverlay {
13
13
  _classSettable = injectCustomClassSettable({ optional: true, host: true });
14
14
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
15
- _computedClass = computed(() => hlm('tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:isolate tw:bg-black/10 tw:duration-100 tw:supports-backdrop-filter:backdrop-blur-xs', this.userClass()), /* @ts-ignore */
15
+ _computedClass = computed(() => hlm("tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:isolate tw:bg-black/10 tw:duration-100 tw:supports-backdrop-filter:backdrop-blur-xs", this.userClass()), /* @ts-ignore */
16
16
  ...(ngDevMode ? [{ debugName: "_computedClass" }] : /* istanbul ignore next */ []));
17
17
  constructor() {
18
18
  effect(() => {
@@ -106,7 +106,7 @@ class HlmAlertDialogContent {
106
106
  size = input('default', /* @ts-ignore */
107
107
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
108
108
  constructor() {
109
- classes(() => 'tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 tw:bg-popover tw:text-popover-foreground tw:ring-foreground/10 tw:gap-6 tw:rounded-xl tw:p-6 tw:ring-1 tw:duration-100 tw:data-[size=default]:max-w-xs tw:data-[size=sm]:max-w-xs tw:data-[size=default]:sm:max-w-lg tw:group/alert-dialog-content tw:grid tw:w-full tw:outline-none');
109
+ classes(() => "tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 tw:bg-popover tw:text-popover-foreground tw:ring-foreground/10 tw:gap-6 tw:rounded-xl tw:p-6 tw:ring-1 tw:duration-100 tw:data-[size=default]:max-w-xs tw:data-[size=sm]:max-w-xs tw:data-[size=default]:sm:max-w-lg tw:group/alert-dialog-content tw:grid tw:w-full tw:outline-none");
110
110
  }
111
111
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmAlertDialogContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
112
112
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: HlmAlertDialogContent, isStandalone: true, selector: "[hlmAlertDialogContent],hlm-alert-dialog-content", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "alert-dialog-content" }, properties: { "attr.data-state": "state()", "attr.data-size": "size()" } }, ngImport: i0 });
@@ -52,7 +52,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
52
52
 
53
53
  class HlmAutocompleteContent {
54
54
  constructor() {
55
- classes(() => 'tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:max-h-72 tw:min-w-36 tw:overflow-hidden tw:rounded-md tw:shadow-md tw:ring-1 tw:duration-100 tw:group/autocomplete-content tw:flex tw:w-(--brn-autocomplete-width) tw:flex-col tw:p-0');
55
+ classes(() => "tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:max-h-72 tw:min-w-36 tw:overflow-hidden tw:rounded-md tw:shadow-md tw:ring-1 tw:duration-100 tw:group/autocomplete-content tw:flex tw:w-(--brn-autocomplete-width) tw:flex-col tw:p-0");
56
56
  }
57
57
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmAutocompleteContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
58
58
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: HlmAutocompleteContent, isStandalone: true, selector: "[hlmAutocompleteContent],hlm-autocomplete-content", hostDirectives: [{ directive: i1.BrnAutocompleteContent }], ngImport: i0 });
@@ -159,7 +159,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
159
159
  class HlmComboboxContent {
160
160
  constructor() {
161
161
  classes(() => [
162
- 'tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:**:has-[[data-slot=input-group-control]:focus-visible]:border-input tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:**:data-[slot=input-group]:bg-input/30 tw:**:data-[slot=input-group]:border-input/30 tw:max-h-72 tw:min-w-36 tw:overflow-hidden tw:rounded-md tw:shadow-md tw:ring-1 tw:duration-100 tw:**:has-[[data-slot=input-group-control]:focus-visible]:ring-0 tw:**:data-[slot=input-group]:m-1 tw:**:data-[slot=input-group]:mb-0 tw:**:data-[slot=input-group]:h-8 tw:**:data-[slot=input-group]:shadow-none tw:group/combobox-content tw:relative tw:flex tw:w-(--brn-combobox-width) tw:flex-col tw:p-0',
162
+ "tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:**:has-[[data-slot=input-group-control]:focus-visible]:border-input tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:**:data-[slot=input-group]:bg-input/30 tw:**:data-[slot=input-group]:border-input/30 tw:max-h-72 tw:min-w-36 tw:overflow-hidden tw:rounded-md tw:shadow-md tw:ring-1 tw:duration-100 tw:**:has-[[data-slot=input-group-control]:focus-visible]:ring-0 tw:**:data-[slot=input-group]:m-1 tw:**:data-[slot=input-group]:mb-0 tw:**:data-[slot=input-group]:h-8 tw:**:data-[slot=input-group]:shadow-none tw:group/combobox-content tw:relative tw:flex tw:w-(--brn-combobox-width) tw:flex-col tw:p-0",
163
163
  ]);
164
164
  }
165
165
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmComboboxContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -9,7 +9,7 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
9
9
  import { lucideX } from '@ng-icons/lucide';
10
10
  import { HlmBtn } from '@egose/shadcn-theme-ng-tw/button';
11
11
 
12
- const hlmDialogOverlayClass = hlm('tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:isolate tw:bg-black/10 tw:duration-100 tw:supports-backdrop-filter:backdrop-blur-xs');
12
+ const hlmDialogOverlayClass = hlm("tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:isolate tw:bg-black/10 tw:duration-100 tw:supports-backdrop-filter:backdrop-blur-xs");
13
13
  class HlmDialogOverlay {
14
14
  _classSettable = injectCustomClassSettable({ optional: true, host: true });
15
15
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
@@ -93,7 +93,7 @@ class HlmDialogContent {
93
93
  _dynamicComponentClass = this._dialogContext?.$dynamicComponentClass;
94
94
  constructor() {
95
95
  classes(() => [
96
- 'tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 tw:ring-foreground/10 tw:grid tw:max-w-[calc(100%-2rem)] tw:gap-6 tw:rounded-xl tw:p-6 tw:text-sm tw:ring-1 tw:duration-100 tw:sm:max-w-md tw:relative tw:mx-auto tw:w-full tw:outline-none tw:sm:mx-0',
96
+ "tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 tw:ring-foreground/10 tw:grid tw:max-w-[calc(100%-2rem)] tw:gap-6 tw:rounded-xl tw:p-6 tw:text-sm tw:ring-1 tw:duration-100 tw:sm:max-w-md tw:relative tw:mx-auto tw:w-full tw:outline-none tw:sm:mx-0",
97
97
  this._dynamicComponentClass,
98
98
  ]);
99
99
  }
@@ -9,7 +9,7 @@ import { hlm, classes } from '@egose/shadcn-theme-ng-tw/utils';
9
9
  class HlmDrawerOverlay {
10
10
  _classSettable = injectCustomClassSettable({ optional: true, host: true });
11
11
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
12
- _computedClass = computed(() => hlm('tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:bg-black/10 tw:supports-backdrop-filter:backdrop-blur-xs tw:transition-opacity tw:duration-150 tw:data-ending-style:opacity-0 tw:data-starting-style:opacity-0', this.userClass()), /* @ts-ignore */
12
+ _computedClass = computed(() => hlm("tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:bg-black/10 tw:supports-backdrop-filter:backdrop-blur-xs tw:transition-opacity tw:duration-150 tw:data-ending-style:opacity-0 tw:data-starting-style:opacity-0", this.userClass()), /* @ts-ignore */
13
13
  ...(ngDevMode ? [{ debugName: "_computedClass" }] : /* istanbul ignore next */ []));
14
14
  constructor() {
15
15
  effect(() => {
@@ -22,7 +22,7 @@ class HlmDropdownMenu {
22
22
  ...(ngDevMode ? [{ debugName: "_side" }] : /* istanbul ignore next */ []));
23
23
  sideOffset = input(1, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
24
24
  constructor() {
25
- classes(() => 'tw:motion-safe:data-open:animate-in tw:motion-safe:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:bg-popover tw:text-popover-foreground tw:min-w-32 tw:rounded-md tw:p-1 tw:shadow-md tw:ring-1 tw:duration-100 tw:my-[--spacing(var(--side-offset))] tw:overflow-x-hidden tw:overflow-y-auto tw:outline-none');
25
+ classes(() => "tw:motion-safe:data-open:animate-in tw:motion-safe:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:bg-popover tw:text-popover-foreground tw:min-w-32 tw:rounded-md tw:p-1 tw:shadow-md tw:ring-1 tw:duration-100 tw:my-[--spacing(var(--side-offset))] tw:overflow-x-hidden tw:overflow-y-auto tw:outline-none");
26
26
  this.setSideFromTransformOrigin();
27
27
  // this is a best effort, but does not seem to work currently
28
28
  // TODO: figure out a way for us to know the host is about to be closed. might not be possible with CDK
@@ -369,7 +369,7 @@ class HlmDropdownMenuSub {
369
369
  // this is a best effort, but does not seem to work currently
370
370
  // TODO: figure out a way for us to know the host is about to be closed. might not be possible with CDK
371
371
  this._host.closed.pipe(takeUntilDestroyed()).subscribe(() => this._state.set('closed'));
372
- classes(() => 'tw:motion-safe:data-open:animate-in tw:motion-safe:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:bg-popover tw:text-popover-foreground tw:min-w-[96px] tw:rounded-md tw:p-1 tw:shadow-lg tw:ring-1 tw:duration-100 tw:w-auto');
372
+ classes(() => "tw:motion-safe:data-open:animate-in tw:motion-safe:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:bg-popover tw:text-popover-foreground tw:min-w-[96px] tw:rounded-md tw:p-1 tw:shadow-lg tw:ring-1 tw:duration-100 tw:w-auto");
373
373
  }
374
374
  setSideFromTransformOrigin() {
375
375
  const side = this._menuSide?.side() ?? 'right';
@@ -94,7 +94,7 @@ Selector (from source): `eg-form-checkbox` (standalone component).
94
94
  | `id` | `string \| undefined` | — | Fallback id (second priority). |
95
95
  | `name` | `string \| undefined` | — | Checkbox `name` attribute (defaults to `controlName`). |
96
96
  | `checked` | `boolean` | `false` | Initial checked state passed to `hlm-checkbox`. |
97
- | `required` | `boolean` | `false` | Shows a red `*` and sets checkbox `required`. |
97
+ | `required` | `boolean` | `false` | Shows a red `*` and sets checkbox `required` (boolean-coerced, so bare `required` works). Presentation only — it does not validate by itself; add `Validators.requiredTrue` to the `FormControl` or the control is never invalid and no error is shown. |
98
98
  | `disabled` | `boolean` | `false` | Locks interaction via checkbox `wrapperDisabled` (does not write to the form control). |
99
99
  | `class` | `ClassValue` | `''` | Extra host classes. |
100
100
  | `checkboxClass` | `string` | `''` | Extra classes for `hlm-checkbox`. |
@@ -1,5 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, signal, input, computed, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { InjectionToken, inject, DestroyRef, signal, input, computed, booleanAttribute, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3
4
  import * as i1 from '@angular/forms';
4
5
  import { FormGroupDirective, ReactiveFormsModule, ControlContainer } from '@angular/forms';
5
6
  import { HlmLabel } from '@egose/shadcn-theme-ng-tw/label';
@@ -18,6 +19,7 @@ function injectEgFormCheckboxConfig() {
18
19
 
19
20
  class EgFormCheckbox {
20
21
  formGroupDirective = inject(FormGroupDirective);
22
+ _destroyRef = inject(DestroyRef);
21
23
  _errorMessages = injectEgFormErrorMessages();
22
24
  generatedId = inject(HlmFormIdGenerator).generate('eg-form-checkbox');
23
25
  _config = injectEgFormCheckboxConfig();
@@ -34,6 +36,14 @@ class EgFormCheckbox {
34
36
  ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
35
37
  _submitted = signal(false, /* @ts-ignore */
36
38
  ...(ngDevMode ? [{ debugName: "_submitted" }] : /* istanbul ignore next */ []));
39
+ /**
40
+ * `touched`/`dirty` are plain flags with no observable: a stepper surfacing
41
+ * errors via `markAllAsTouched()` (no status change, no submit) would
42
+ * otherwise stay invisible under OnPush. Polled in `ngDoCheck` so the
43
+ * computed below recomputes on interaction changes too.
44
+ */
45
+ _interacted = signal(false, /* @ts-ignore */
46
+ ...(ngDevMode ? [{ debugName: "_interacted" }] : /* istanbul ignore next */ []));
37
47
  _trackedControl = null;
38
48
  _controlSub = null;
39
49
  // Classes
@@ -64,8 +74,13 @@ class EgFormCheckbox {
64
74
  ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
65
75
  checked = input(false, /* @ts-ignore */
66
76
  ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
67
- required = input(false, /* @ts-ignore */
68
- ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
77
+ /**
78
+ * Shows the red `*` marker and sets the native `required` on the checkbox.
79
+ * Note: this does not add validation by itself — pair it with
80
+ * `Validators.requiredTrue` on the `FormControl`, otherwise the control is
81
+ * never invalid and no error is displayed.
82
+ */
83
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
69
84
  disabled = input(false, /* @ts-ignore */
70
85
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
71
86
  effectiveId = computed(() => this.controlId() || this.id() || this.generatedId, /* @ts-ignore */
@@ -100,10 +115,9 @@ class EgFormCheckbox {
100
115
  showError = computed(() => {
101
116
  this._status();
102
117
  this._submitted();
118
+ this._interacted();
103
119
  const control = this.formGroupDirective.form.get(this.controlName());
104
- return (!!this.resolvedError() &&
105
- !!control?.invalid &&
106
- (control.dirty || control.touched || this._submitted()));
120
+ return !!this.resolvedError() && !!control?.invalid && (this._interacted() || this._submitted());
107
121
  }, /* @ts-ignore */
108
122
  ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
109
123
  describedBy() {
@@ -111,6 +125,16 @@ class EgFormCheckbox {
111
125
  }
112
126
  ngAfterContentInit() {
113
127
  this._trackControl();
128
+ // `ngSubmit` emits synchronously during event dispatch, before change
129
+ // detection traverses. Flipping the signal here (instead of only polling
130
+ // `submitted` in `ngDoCheck`) marks this OnPush view dirty up-front, so
131
+ // the error renders in the same pass even when no `@Input()` changed and
132
+ // the control itself never became touched/dirty. `ngDoCheck` below stays
133
+ // as the fallback (e.g. `resetForm()` clears `submitted` silently).
134
+ this.formGroupDirective.ngSubmit.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => {
135
+ if (!this._submitted())
136
+ this._submitted.set(true);
137
+ });
114
138
  }
115
139
  ngDoCheck() {
116
140
  const submitted = !!this.formGroupDirective?.submitted;
@@ -119,11 +143,14 @@ class EgFormCheckbox {
119
143
  const control = this.formGroupDirective.form.get(this.controlName()) ?? null;
120
144
  if (control !== this._trackedControl) {
121
145
  this._trackControl();
146
+ return;
122
147
  }
123
- else if (control && this._status() !== control.status) {
124
- // Fallback poll: catches status changes missed by the subscription
125
- // (e.g. control replaced without re-init).
126
- this._status.set(control.status);
148
+ if (control) {
149
+ if (this._status() !== control.status)
150
+ this._status.set(control.status);
151
+ const interacted = control.touched || control.dirty;
152
+ if (this._interacted() !== interacted)
153
+ this._interacted.set(interacted);
127
154
  }
128
155
  }
129
156
  ngOnDestroy() {
@@ -136,6 +163,7 @@ class EgFormCheckbox {
136
163
  const control = this.formGroupDirective.form.get(this.controlName()) ?? null;
137
164
  this._trackedControl = control;
138
165
  this._status.set(control?.status ?? null);
166
+ this._interacted.set(!!control && (control.touched || control.dirty));
139
167
  if (control) {
140
168
  this._controlSub = control.statusChanges.subscribe((status) => {
141
169
  if (this._status() !== status)
@@ -1,9 +1,11 @@
1
1
  import * as _angular_core from '@angular/core';
2
2
  import { DoCheck, AfterContentInit, OnDestroy, ValueProvider } from '@angular/core';
3
+ import { BooleanInput } from '@angular/cdk/coercion';
3
4
  import { ClassValue } from 'clsx';
4
5
 
5
6
  declare class EgFormCheckbox implements DoCheck, AfterContentInit, OnDestroy {
6
7
  private readonly formGroupDirective;
8
+ private readonly _destroyRef;
7
9
  private readonly _errorMessages;
8
10
  private readonly generatedId;
9
11
  private readonly _config;
@@ -18,6 +20,13 @@ declare class EgFormCheckbox implements DoCheck, AfterContentInit, OnDestroy {
18
20
  */
19
21
  private readonly _status;
20
22
  private readonly _submitted;
23
+ /**
24
+ * `touched`/`dirty` are plain flags with no observable: a stepper surfacing
25
+ * errors via `markAllAsTouched()` (no status change, no submit) would
26
+ * otherwise stay invisible under OnPush. Polled in `ngDoCheck` so the
27
+ * computed below recomputes on interaction changes too.
28
+ */
29
+ private readonly _interacted;
21
30
  private _trackedControl;
22
31
  private _controlSub;
23
32
  readonly userClass: _angular_core.InputSignal<ClassValue>;
@@ -36,7 +45,13 @@ declare class EgFormCheckbox implements DoCheck, AfterContentInit, OnDestroy {
36
45
  id: _angular_core.InputSignal<string | undefined>;
37
46
  name: _angular_core.InputSignal<string | undefined>;
38
47
  checked: _angular_core.InputSignal<boolean>;
39
- required: _angular_core.InputSignal<boolean>;
48
+ /**
49
+ * Shows the red `*` marker and sets the native `required` on the checkbox.
50
+ * Note: this does not add validation by itself — pair it with
51
+ * `Validators.requiredTrue` on the `FormControl`, otherwise the control is
52
+ * never invalid and no error is displayed.
53
+ */
54
+ required: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
40
55
  disabled: _angular_core.InputSignal<boolean>;
41
56
  readonly effectiveId: _angular_core.Signal<string>;
42
57
  readonly errorId: _angular_core.Signal<string>;
@@ -21,6 +21,14 @@ class EgFormField {
21
21
  */
22
22
  submittedSignal = signal(false, /* @ts-ignore */
23
23
  ...(ngDevMode ? [{ debugName: "submittedSignal" }] : /* istanbul ignore next */ []));
24
+ /**
25
+ * `touched`/`dirty` are plain flags with no observable: surfacing errors via
26
+ * `markAllAsTouched()` (e.g. a stepper blocking `Next` without changing the
27
+ * control status) would otherwise stay invisible under OnPush. Polled in
28
+ * `ngDoCheck` so `hasError` recomputes on interaction changes too.
29
+ */
30
+ interactedSignal = signal(false, /* @ts-ignore */
31
+ ...(ngDevMode ? [{ debugName: "interactedSignal" }] : /* istanbul ignore next */ []));
24
32
  sub = new Subscription();
25
33
  ngAfterContentInit() {
26
34
  const ctrlDir = this.control();
@@ -29,32 +37,41 @@ class EgFormField {
29
37
  this.statusSignal.set(status);
30
38
  }));
31
39
  this.statusSignal.set(ctrlDir.control.status);
40
+ this.interactedSignal.set(ctrlDir.control.touched || ctrlDir.control.dirty);
32
41
  }
33
42
  }
34
43
  ngDoCheck() {
35
44
  const submitted = !!this.formGroupDirective?.submitted;
36
45
  if (this.submittedSignal() !== submitted)
37
46
  this.submittedSignal.set(submitted);
47
+ // NOTE: `control` itself may still be unset on the first pass (this hook
48
+ // runs before the content child's setup), so optional-chain everything.
49
+ const ctrl = this.control()?.control ?? null;
50
+ if (ctrl) {
51
+ const interacted = ctrl.touched || ctrl.dirty;
52
+ if (this.interactedSignal() !== interacted)
53
+ this.interactedSignal.set(interacted);
54
+ }
38
55
  }
39
56
  ngOnDestroy() {
40
57
  this.sub.unsubscribe();
41
58
  }
42
59
  hasError = computed(() => {
43
- const ctrlDir = this.control();
44
- if (!ctrlDir)
60
+ const ctrl = this.control()?.control ?? null;
61
+ if (!ctrl)
45
62
  return false;
46
63
  this.statusSignal();
47
64
  this.submittedSignal();
48
- return (!!ctrlDir.control.errors &&
49
- (ctrlDir.control.touched || ctrlDir.control.dirty || !!this.formGroupDirective?.submitted));
65
+ this.interactedSignal();
66
+ return !!ctrl.errors && (this.interactedSignal() || this.submittedSignal());
50
67
  }, /* @ts-ignore */
51
68
  ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
52
69
  firstErrorKey = computed(() => {
53
- const ctrlDir = this.control();
54
- if (!ctrlDir || !ctrlDir.control.errors)
70
+ const ctrl = this.control()?.control ?? null;
71
+ if (!ctrl?.errors)
55
72
  return null;
56
73
  this.statusSignal();
57
- return Object.keys(ctrlDir.control.errors)[0] ?? null;
74
+ return Object.keys(ctrl.errors)[0] ?? null;
58
75
  }, /* @ts-ignore */
59
76
  ...(ngDevMode ? [{ debugName: "firstErrorKey" }] : /* istanbul ignore next */ []));
60
77
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormField, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -16,6 +16,13 @@ declare class EgFormField implements AfterContentInit, DoCheck, OnDestroy {
16
16
  * even when the control was never touched/dirty.
17
17
  */
18
18
  private submittedSignal;
19
+ /**
20
+ * `touched`/`dirty` are plain flags with no observable: surfacing errors via
21
+ * `markAllAsTouched()` (e.g. a stepper blocking `Next` without changing the
22
+ * control status) would otherwise stay invisible under OnPush. Polled in
23
+ * `ngDoCheck` so `hasError` recomputes on interaction changes too.
24
+ */
25
+ private interactedSignal;
19
26
  private sub;
20
27
  ngAfterContentInit(): void;
21
28
  ngDoCheck(): void;
@@ -31,7 +31,7 @@ class HlmHoverCardContent {
31
31
  this._renderer.setAttribute(this._element.nativeElement, 'data-side', this.side());
32
32
  });
33
33
  classes(() => [
34
- 'tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:bg-popover tw:text-popover-foreground tw:w-64 tw:rounded-lg tw:p-4 tw:text-sm tw:shadow-md tw:ring-1 tw:duration-100 tw:z-50 tw:outline-none',
34
+ "tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:bg-popover tw:text-popover-foreground tw:w-64 tw:rounded-lg tw:p-4 tw:text-sm tw:shadow-md tw:ring-1 tw:duration-100 tw:z-50 tw:outline-none",
35
35
  'tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
36
36
  ]);
37
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@egose/shadcn-theme-ng-tw",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Angular shadcn UI theme",
5
5
  "author": "Junmin Ahn",
6
6
  "bundles": [
@@ -33,7 +33,7 @@ class HlmPopoverContent {
33
33
  effect(() => {
34
34
  this._renderer.setAttribute(this._element.nativeElement, 'data-state', this.state());
35
35
  });
36
- classes(() => 'tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:gap-4 tw:rounded-md tw:p-4 tw:text-sm tw:shadow-md tw:ring-1 tw:duration-100 tw:relative tw:flex tw:w-72 tw:flex-col tw:outline-none');
36
+ classes(() => "tw:bg-popover tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:gap-4 tw:rounded-md tw:p-4 tw:text-sm tw:shadow-md tw:ring-1 tw:duration-100 tw:relative tw:flex tw:w-72 tw:flex-col tw:outline-none");
37
37
  }
38
38
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmPopoverContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
39
39
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: HlmPopoverContent, isStandalone: true, selector: "[hlmPopoverContent],hlm-popover-content", host: { attributes: { "data-slot": "popover-content" } }, ngImport: i0 });
@@ -92,7 +92,7 @@ class HlmSelectContent {
92
92
  ...(ngDevMode ? [{ debugName: "_computedListboxClasses" }] : /* istanbul ignore next */ []));
93
93
  showScroll = input(false, { ...(ngDevMode ? { debugName: "showScroll" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
94
94
  constructor() {
95
- classes(() => 'tw:bg-popover no-scrollbar tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:max-h-72 tw:min-w-36 tw:flex-col tw:rounded-md tw:shadow-md tw:ring-1 tw:duration-100 tw:relative tw:flex tw:w-(--brn-select-width) tw:overflow-x-hidden tw:overflow-y-auto');
95
+ classes(() => "tw:bg-popover no-scrollbar tw:text-popover-foreground tw:data-open:animate-in tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-open:fade-in-0 tw:data-closed:zoom-out-95 tw:data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:ring-foreground/10 tw:max-h-72 tw:min-w-36 tw:flex-col tw:rounded-md tw:shadow-md tw:ring-1 tw:duration-100 tw:relative tw:flex tw:w-(--brn-select-width) tw:overflow-x-hidden tw:overflow-y-auto");
96
96
  }
97
97
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmSelectContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
98
98
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmSelectContent, isStandalone: true, selector: "hlm-select-content", inputs: { showScroll: { classPropertyName: "showScroll", publicName: "showScroll", isSignal: true, isRequired: false, transformFunction: null } }, hostDirectives: [{ directive: i1.BrnSelectContent }], ngImport: i0, template: `
@@ -274,11 +274,11 @@ const stepTransition = cva('', {
274
274
  variants: {
275
275
  motion: {
276
276
  none: '',
277
- 'enter-forward': 'tw:animate-in fade-in slide-in-from-right-8 tw:fill-mode-both',
278
- 'enter-backward': 'tw:animate-in fade-in slide-in-from-left-8 tw:fill-mode-both',
279
- 'leave-forward': 'tw:animate-out fade-out slide-out-to-left-8 tw:fill-mode-both tw:absolute tw:top-0 tw:right-0 tw:left-0',
280
- 'leave-backward': 'tw:animate-out fade-out slide-out-to-right-8 tw:fill-mode-both tw:absolute tw:top-0 tw:right-0 tw:left-0',
281
- 'enter-bottom': 'tw:animate-in fade-in slide-in-from-bottom-2 tw:fill-mode-both tw:duration-400 tw:[animation-timing-function:cubic-bezier(0.34,1.56,0.64,1)]',
277
+ 'enter-forward': 'tw:animate-in tw:fade-in tw:slide-in-from-right-8 tw:fill-mode-both',
278
+ 'enter-backward': 'tw:animate-in tw:fade-in tw:slide-in-from-left-8 tw:fill-mode-both',
279
+ 'leave-forward': 'tw:animate-out tw:fade-out tw:slide-out-to-left-8 tw:fill-mode-both tw:absolute tw:top-0 tw:right-0 tw:left-0',
280
+ 'leave-backward': 'tw:animate-out tw:fade-out tw:slide-out-to-right-8 tw:fill-mode-both tw:absolute tw:top-0 tw:right-0 tw:left-0',
281
+ 'enter-bottom': 'tw:animate-in tw:fade-in tw:slide-in-from-bottom-2 tw:fill-mode-both tw:duration-400 tw:[animation-timing-function:cubic-bezier(0.34,1.56,0.64,1)]',
282
282
  },
283
283
  },
284
284
  defaultVariants: { motion: 'none' },
@@ -6,7 +6,7 @@ import { hlm } from '@egose/shadcn-theme-ng-tw/utils';
6
6
  import { cva } from 'class-variance-authority';
7
7
 
8
8
  const DEFAULT_TOOLTIP_SVG_CLASS = 'bg-foreground fill-foreground z-50 block size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]';
9
- const DEFAULT_TOOLTIP_CONTENT_CLASSES = hlm('tw:data-open:animate-in data-open:fade-in-0 tw:data-open:zoom-in-95 tw:data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 tw:data-[state=delayed-open]:zoom-in-95 tw:data-closed:animate-out data-closed:fade-out-0 tw:data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:rounded-md tw:px-3 tw:py-1.5 tw:text-xs tw:bg-foreground tw:text-background tw:data-open:animate-in data-open:fade-in-0 tw:data-open:zoom-in-95 tw:data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 tw:data-[state=delayed-open]:zoom-in-95 tw:data-closed:animate-out data-closed:fade-out-0 tw:data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:w-fit tw:origin-(--radix-tooltip-content-transform-origin) tw:text-balance');
9
+ const DEFAULT_TOOLTIP_CONTENT_CLASSES = hlm("tw:data-open:animate-in tw:data-open:fade-in-0 tw:data-open:zoom-in-95 tw:data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 tw:data-[state=delayed-open]:zoom-in-95 tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:rounded-md tw:px-3 tw:py-1.5 tw:text-xs tw:bg-foreground tw:text-background tw:data-open:animate-in tw:data-open:fade-in-0 tw:data-open:zoom-in-95 tw:data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 tw:data-[state=delayed-open]:zoom-in-95 tw:data-closed:animate-out tw:data-closed:fade-out-0 tw:data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:w-fit tw:origin-(--radix-tooltip-content-transform-origin) tw:text-balance");
10
10
  const tooltipPositionVariants = cva('tw:absolute', {
11
11
  variants: {
12
12
  position: {