ng-hub-ui-forms 21.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "ng-hub-ui-forms",
3
+ "version": "21.0.0",
4
+ "license": "MIT",
5
+ "description": "Accessible, signal-based form fields for Angular (input, textarea, slider, select, datepicker) with automatic error display for controls, FormGroups and FormArrays. Reactive Forms today, Signal Forms ready. Part of the ng-hub-ui family.",
6
+ "author": "Carlos Morcillo <carlos.morcillo@me.com> (https://www.carlosmorcillo.com)",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/carlos-morcillo/ng-hub-ui-forms.git"
10
+ },
11
+ "homepage": "https://hubui.dev/",
12
+ "keywords": [
13
+ "angular",
14
+ "forms",
15
+ "form-controls",
16
+ "input",
17
+ "textarea",
18
+ "slider",
19
+ "select",
20
+ "datepicker",
21
+ "validation",
22
+ "reactive-forms",
23
+ "signal-forms",
24
+ "ui-component",
25
+ "angular-library",
26
+ "ng-hub-ui",
27
+ "typescript",
28
+ "signals",
29
+ "angular21"
30
+ ],
31
+ "peerDependencies": {
32
+ "@angular/cdk": ">=21.0.0",
33
+ "@angular/common": ">=21.0.0",
34
+ "@angular/core": ">=21.0.0",
35
+ "@angular/forms": ">=21.0.0",
36
+ "@angular/platform-browser": ">=21.0.0"
37
+ },
38
+ "dependencies": {
39
+ "tslib": "^2.3.0"
40
+ },
41
+ "sideEffects": false,
42
+ "module": "fesm2022/ng-hub-ui-forms.mjs",
43
+ "typings": "types/ng-hub-ui-forms.d.ts",
44
+ "exports": {
45
+ "./package.json": {
46
+ "default": "./package.json"
47
+ },
48
+ ".": {
49
+ "types": "./types/ng-hub-ui-forms.d.ts",
50
+ "default": "./fesm2022/ng-hub-ui-forms.mjs"
51
+ },
52
+ "./signals": {
53
+ "types": "./types/ng-hub-ui-forms-signals.d.ts",
54
+ "default": "./fesm2022/ng-hub-ui-forms-signals.mjs"
55
+ }
56
+ },
57
+ "type": "module"
58
+ }
@@ -0,0 +1,63 @@
1
+ # `ng-hub-ui-forms/signals`
2
+
3
+ Opt-in **Angular Signal Forms** integration for `ng-hub-ui-forms`.
4
+
5
+ This is a **secondary entry point**: it is the only place that imports
6
+ `@angular/forms/signals`. The core `ng-hub-ui-forms` package never imports it, so the
7
+ core stays compatible with **Angular 21** (where Signal Forms is `@experimental`).
8
+ This entry point is **recommended on Angular >= 22**, where Signal Forms is stable.
9
+
10
+ ```ts
11
+ import { HubSignalFieldControl, hubSignalErrorMessages } from 'ng-hub-ui-forms/signals';
12
+ ```
13
+
14
+ ## What's here
15
+
16
+ ### `HubSignalFieldControl<TValue>`
17
+
18
+ Abstract `@Directive()` base implementing Angular's
19
+ [`FormValueControl<TValue>`](https://angular.dev/guide/forms/signals) contract, so a
20
+ custom field binds to a `FieldTree` through the `Field` directive:
21
+
22
+ ```html
23
+ <my-hub-signal-input [formField]="form.email" />
24
+ ```
25
+
26
+ It exposes the two-way `value` model the `Field` directive keeps in sync, plus the
27
+ optional state inputs it auto-wires (`errors`, `disabled`, `touched`, `required`), and
28
+ resolves messages through the same `HubErrorDisplay` pipeline as the CVA core — so
29
+ Reactive and Signal fields render identical error copy.
30
+
31
+ ```ts
32
+ @Component({
33
+ selector: 'my-hub-signal-input',
34
+ template: `
35
+ <input [value]="value()" (input)="value.set($any($event.target).value)" [disabled]="disabled()" />
36
+ @if (isInvalid()) { @for (msg of messages(); track msg) { <small [innerHTML]="msg"></small> } }
37
+ `
38
+ })
39
+ export class MyHubSignalInput extends HubSignalFieldControl<string> {}
40
+ ```
41
+
42
+ ### `hubSignalErrorMessages(errors, fn?)`
43
+
44
+ Maps a field's `field().errors()` to human-readable strings, reusing the core
45
+ `defaultInvalidFeedback` (or any override, e.g. `HUB_FORMS_CONFIG.invalidFeedbackTemplateFn`).
46
+ Single source of truth for error copy across both form models.
47
+
48
+ ## Interop with Reactive Forms
49
+
50
+ Use Angular's `@angular/forms/signals-compat` helpers to mix models during migration:
51
+
52
+ - `compatForm(model, schema?)` — build a `FieldTree` whose model may contain `AbstractControl`s.
53
+ - `SignalFormControl<T>` — drive a Signal field tree from an `AbstractControl` inside a `FormGroup`.
54
+ - `NG_STATUS_CLASSES` — keep the standard `ng-valid`/`ng-invalid`/`ng-touched`… CSS classes.
55
+
56
+ ## Status / next steps (roadmap Fase 3b)
57
+
58
+ Scaffolded and building. Pending, opt-in follow-ups:
59
+
60
+ - [ ] Provide signal-native field components (`hub-input`, `hub-select`, …) that extend
61
+ `HubSignalFieldControl` (the existing fields are CVA-based and stay as-is).
62
+ - [ ] `compatForm` / `SignalFormControl` usage examples and parallel docs (Reactive vs Signal).
63
+ - [ ] Smoke test on Angular 21 (experimental) and 22 (stable).
@@ -0,0 +1,132 @@
1
+ // ─────────────────────────────────────────────────────────────────────────
2
+ // ng-hub-ui-forms — shared field chrome
3
+ // ─────────────────────────────────────────────────────────────────────────
4
+ // Common structure shared by every field (input, textarea, select, slider…):
5
+ // wrapper, label, required mark, helper text, error feedback, and the base
6
+ // control skin. Defined ONCE; components reuse these `.hub-field__*` classes
7
+ // and only add their component-specific bits.
8
+ // ─────────────────────────────────────────────────────────────────────────
9
+
10
+ .hub-field {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--hub-form-field-gap);
14
+
15
+ &--horizontal {
16
+ display: grid;
17
+ // Label column shrinks to its content but never grows past the max width;
18
+ // the control takes whatever space is left.
19
+ grid-template-columns: minmax(0, auto) minmax(0, 1fr);
20
+ align-items: center;
21
+ column-gap: var(--hub-form-row-gap);
22
+ row-gap: var(--hub-form-field-gap);
23
+
24
+ // Label sits in the first column, aligned to the control row. It is capped
25
+ // at the max width and ellipsizes when its text is longer.
26
+ > .hub-field__label {
27
+ grid-column: 1;
28
+ display: block;
29
+ max-width: var(--hub-form-label-horizontal-max-width);
30
+ overflow: hidden;
31
+ white-space: nowrap;
32
+ text-overflow: ellipsis;
33
+ }
34
+
35
+ // Control, helper text and validation feedback all stack in the second
36
+ // column, so the form-text / errors stay BELOW the control even when the
37
+ // label is placed horizontally.
38
+ > .hub-field__body,
39
+ > .hub-field__form-text,
40
+ > .hub-field__feedback {
41
+ grid-column: 2;
42
+ min-width: 0;
43
+ }
44
+ }
45
+
46
+ &__label {
47
+ display: inline-flex;
48
+ align-items: baseline;
49
+ gap: 0.15rem;
50
+ margin-bottom: var(--hub-label-margin-bottom);
51
+ color: var(--hub-label-color);
52
+ font-size: var(--hub-label-font-size);
53
+ font-weight: var(--hub-label-font-weight);
54
+ }
55
+
56
+ &__required {
57
+ color: var(--hub-form-required-color);
58
+ }
59
+
60
+ &--disabled .hub-field__label {
61
+ opacity: var(--hub-form-disabled-opacity);
62
+ }
63
+ }
64
+
65
+ // Base control skin (input / textarea / counter input share it).
66
+ .hub-field__control {
67
+ display: block;
68
+ width: 100%;
69
+ padding: var(--hub-input-padding-y) var(--hub-input-padding-x);
70
+ color: var(--hub-input-color);
71
+ background-color: var(--hub-input-bg);
72
+ font-family: inherit;
73
+ font-size: var(--hub-input-font-size);
74
+ line-height: var(--hub-input-line-height);
75
+ border: var(--hub-input-border-width) solid var(--hub-input-border-color);
76
+ border-radius: var(--hub-input-border-radius);
77
+ transition: var(--hub-input-transition);
78
+ appearance: none;
79
+
80
+ &::placeholder {
81
+ color: var(--hub-input-placeholder-color);
82
+ opacity: 1;
83
+ }
84
+
85
+ &:focus {
86
+ outline: 0;
87
+ border-color: var(--hub-input-focus-border-color);
88
+ box-shadow: var(--hub-input-focus-box-shadow);
89
+ }
90
+
91
+ &:disabled {
92
+ opacity: var(--hub-form-disabled-opacity);
93
+ cursor: not-allowed;
94
+ }
95
+
96
+ &--invalid {
97
+ border-color: var(--hub-form-invalid-border-color);
98
+
99
+ &:focus {
100
+ border-color: var(--hub-form-invalid-border-color);
101
+ box-shadow: 0 0 0 var(--hub-form-focus-ring-width) var(--hub-form-invalid-focus-ring-color);
102
+ }
103
+ }
104
+ }
105
+
106
+ // Helper text below the control.
107
+ .hub-field__form-text {
108
+ margin-top: var(--hub-form-text-margin-top);
109
+ color: var(--hub-form-text-color);
110
+ font-size: var(--hub-form-text-font-size);
111
+
112
+ &--disabled {
113
+ opacity: var(--hub-form-disabled-opacity);
114
+ }
115
+ }
116
+
117
+ // Validation error feedback.
118
+ .hub-field__feedback {
119
+ margin-top: var(--hub-form-feedback-margin-top);
120
+ color: var(--hub-form-feedback-color);
121
+ font-size: var(--hub-form-feedback-font-size);
122
+
123
+ ul {
124
+ list-style: none;
125
+ margin: 0;
126
+ padding: 0;
127
+ }
128
+
129
+ &-text {
130
+ display: block;
131
+ }
132
+ }
@@ -0,0 +1,179 @@
1
+ // ─────────────────────────────────────────────────────────────────────────
2
+ // ng-hub-ui-forms — canonical design tokens
3
+ // ─────────────────────────────────────────────────────────────────────────
4
+ // Single source of truth for the form component tokens. Names follow the
5
+ // ng-hub-ui CSS variable library; every token chains to the `--hub-sys-*` /
6
+ // `--hub-ref-*` system layers (provided by hub-tokens.css) so the fields react
7
+ // to themes (light/dark) at runtime. Defined once at `:root`.
8
+ // ─────────────────────────────────────────────────────────────────────────
9
+
10
+ :root {
11
+ // ── Shared form contract (used by every field) ──────────────────────────
12
+ --hub-form-field-gap: var(--hub-ref-space-1, 0.25rem); // label ↔ control
13
+ --hub-form-row-gap: var(--hub-ref-space-3, 1rem); // horizontal label gutter
14
+ --hub-form-label-horizontal-max-width: 12rem; // horizontal label caps here, then ellipsizes
15
+
16
+ --hub-form-invalid-color: var(--hub-sys-color-danger, #dc3545);
17
+ --hub-form-invalid-border-color: var(--hub-sys-color-danger, #dc3545);
18
+ --hub-form-invalid-focus-ring-color: var(--hub-sys-color-danger-subtle, rgba(220, 53, 69, 0.25));
19
+
20
+ --hub-form-feedback-color: var(--hub-form-invalid-color);
21
+ --hub-form-feedback-font-size: var(--hub-ref-font-size-sm, 0.875rem);
22
+ --hub-form-feedback-margin-top: var(--hub-ref-space-1, 0.25rem);
23
+
24
+ --hub-form-text-color: var(--hub-sys-text-muted, #6c757d);
25
+ --hub-form-text-font-size: var(--hub-ref-font-size-sm, 0.875rem);
26
+ --hub-form-text-margin-top: var(--hub-ref-space-1, 0.25rem);
27
+
28
+ --hub-form-focus-ring-width: var(--hub-sys-focus-ring-width, 0.25rem);
29
+ --hub-form-focus-ring-color: var(--hub-sys-focus-ring-color, rgba(13, 110, 253, 0.25));
30
+
31
+ --hub-form-required-color: var(--hub-sys-color-danger, #dc3545);
32
+ --hub-form-disabled-opacity: var(--hub-sys-opacity-disabled, 0.65);
33
+ --hub-form-transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
34
+
35
+ // ── Label ────────────────────────────────────────────────────────────────
36
+ --hub-label-color: var(--hub-sys-text-primary, #212529);
37
+ --hub-label-font-size: var(--hub-ref-font-size-sm, 0.875rem);
38
+ --hub-label-font-weight: var(--hub-ref-font-weight-medium, 500);
39
+ --hub-label-margin-bottom: 0;
40
+
41
+ // ── Input ─────────────────────────────────────────────────────────────────
42
+ --hub-input-color: var(--hub-sys-text-primary, #212529);
43
+ --hub-input-bg: var(--hub-sys-surface-page, #fff);
44
+ --hub-input-font-family: var(--hub-ref-font-family-base, system-ui, sans-serif);
45
+ --hub-input-font-size: var(--hub-ref-font-size-base, 1rem);
46
+ --hub-input-font-weight: var(--hub-ref-font-weight-base, 400);
47
+ --hub-input-line-height: 1.5;
48
+ --hub-input-padding-y: 0.375rem;
49
+ --hub-input-padding-x: 0.75rem;
50
+ --hub-input-border-width: var(--hub-ref-border-width, 1px);
51
+ --hub-input-border-color: var(--hub-sys-border-color-default, #dee2e6);
52
+ --hub-input-border-radius: var(--hub-ref-radius-md, 0.375rem);
53
+ --hub-input-focus-border-color: var(--hub-sys-color-primary, #0d6efd);
54
+ --hub-input-focus-box-shadow: 0 0 0 var(--hub-form-focus-ring-width) var(--hub-form-focus-ring-color);
55
+ --hub-input-placeholder-color: var(--hub-sys-text-muted, #6c757d);
56
+ --hub-input-transition: var(--hub-form-transition);
57
+ --hub-input-wrapper-gap: var(--hub-ref-space-2, 0.5rem);
58
+
59
+ // Input-group addons
60
+ --hub-input-group-addon-bg: var(--hub-sys-surface-elevated, #f8f9fa);
61
+ --hub-input-group-addon-color: var(--hub-sys-text-muted, #6c757d);
62
+ --hub-input-group-addon-border-color: var(--hub-input-border-color);
63
+
64
+ // Input · counter format (−/+)
65
+ --hub-input-counter-button-bg: var(--hub-sys-surface-elevated, #f8f9fa);
66
+ --hub-input-counter-button-color: var(--hub-sys-text-primary, #212529);
67
+ --hub-input-counter-button-width: 2.5rem;
68
+
69
+ // Input · color format
70
+ --hub-input-color-size: 2.5rem;
71
+
72
+ // ── Textarea (inherits input) ──────────────────────────────────────────────
73
+ --hub-textarea-padding-x: var(--hub-input-padding-x);
74
+ --hub-textarea-padding-y: var(--hub-input-padding-y);
75
+ --hub-textarea-border-radius: var(--hub-input-border-radius);
76
+ --hub-textarea-min-height: 4.5rem;
77
+
78
+ // ── Check (checkbox / radio / switch) ──────────────────────────────────────
79
+ --hub-check-input-width: 1.15rem;
80
+ --hub-check-input-height: 1.15rem;
81
+ --hub-check-input-bg: var(--hub-input-bg);
82
+ --hub-check-input-border-width: var(--hub-input-border-width);
83
+ --hub-check-input-border-color: var(--hub-sys-border-color-default, #dee2e6);
84
+ --hub-check-input-border-radius: var(--hub-ref-radius-sm, 0.25rem);
85
+ --hub-check-input-checked-bg: var(--hub-sys-color-primary, #0d6efd);
86
+ --hub-check-input-checked-border-color: var(--hub-sys-color-primary, #0d6efd);
87
+ --hub-check-input-checked-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2.5' d='M3 8.5 6.5 12 13 4.5'/%3E%3C/svg%3E");
88
+ --hub-check-label-gap: var(--hub-ref-space-2, 0.5rem);
89
+ --hub-check-radio-border-radius: 50%;
90
+
91
+ // ── Switch ──────────────────────────────────────────────────────────────────
92
+ --hub-switch-width: 2.25rem;
93
+ --hub-switch-height: 1.25rem;
94
+ --hub-switch-track-off: var(--hub-sys-border-color-default, #dee2e6);
95
+ --hub-switch-track-on: var(--hub-sys-color-primary, #0d6efd);
96
+ --hub-switch-thumb: var(--hub-sys-surface-page, #fff);
97
+
98
+ // ── Slider ────────────────────────────────────────────────────────────────
99
+ --hub-slider-track-width: 100%;
100
+ --hub-slider-track-height: 0.375rem;
101
+ --hub-slider-track-border-radius: var(--hub-ref-radius-pill, 50rem);
102
+ --hub-slider-track-bg: var(--hub-sys-border-color-default, #dee2e6);
103
+ --hub-slider-track-fill-bg: var(--hub-sys-color-primary, #0d6efd);
104
+ --hub-slider-thumb-width: 1.1rem;
105
+ --hub-slider-thumb-height: 1.1rem;
106
+ --hub-slider-thumb-border-radius: 50%;
107
+ --hub-slider-thumb-bg: var(--hub-sys-color-primary, #0d6efd);
108
+ --hub-slider-thumb-border: 2px solid var(--hub-sys-surface-page, #fff);
109
+ --hub-slider-thumb-shadow: 0 0 0 1px var(--hub-sys-border-color-default, #dee2e6);
110
+ --hub-slider-tooltip-bg: var(--hub-sys-text-primary, #212529);
111
+ --hub-slider-tooltip-color: var(--hub-sys-surface-page, #fff);
112
+ --hub-slider-tooltip-border-radius: var(--hub-ref-radius-sm, 0.25rem);
113
+
114
+ // ── Select ────────────────────────────────────────────────────────────────
115
+ --hub-select-color: var(--hub-input-color);
116
+ --hub-select-bg: var(--hub-input-bg);
117
+ --hub-select-font-size: var(--hub-input-font-size);
118
+ --hub-select-border-width: var(--hub-input-border-width);
119
+ --hub-select-border-color: var(--hub-input-border-color);
120
+ --hub-select-border-radius: var(--hub-input-border-radius);
121
+ --hub-select-focus-border-color: var(--hub-input-focus-border-color);
122
+ --hub-select-focus-box-shadow: var(--hub-input-focus-box-shadow);
123
+ --hub-select-placeholder-color: var(--hub-input-placeholder-color);
124
+ --hub-select-padding-x: var(--hub-input-padding-x);
125
+ --hub-select-min-height: 2.5rem;
126
+ --hub-select-arrow-color: var(--hub-sys-text-muted, #6c757d);
127
+ --hub-select-clear-color: var(--hub-sys-text-muted, #6c757d);
128
+ --hub-select-clear-hover-color: var(--hub-sys-color-danger, #dc3545);
129
+ --hub-select-option-color: var(--hub-sys-text-primary, #212529);
130
+ --hub-select-option-padding-x: var(--hub-ref-space-3, 0.75rem);
131
+ --hub-select-option-padding-y: var(--hub-ref-space-2, 0.5rem);
132
+ --hub-select-option-marked-bg: var(--hub-sys-surface-elevated, #f1f3f5);
133
+ --hub-select-option-selected-bg: var(--hub-sys-color-primary, #0d6efd);
134
+ --hub-select-option-selected-color: #fff;
135
+ --hub-select-optgroup-color: var(--hub-sys-text-muted, #6c757d);
136
+ --hub-select-value-bg: var(--hub-sys-surface-elevated, #f8f9fa);
137
+ --hub-select-value-color: var(--hub-sys-text-primary, #212529);
138
+ --hub-select-value-border-radius: var(--hub-ref-radius-sm, 0.25rem);
139
+ --hub-select-dropdown-bg: var(--hub-sys-surface-page, #fff);
140
+ --hub-select-dropdown-border-color: var(--hub-sys-border-color-default, #dee2e6);
141
+ --hub-select-dropdown-border-radius: var(--hub-ref-radius-md, 0.375rem);
142
+ --hub-select-dropdown-box-shadow: var(--hub-sys-shadow, 0 0.5rem 1rem rgba(0, 0, 0, 0.12));
143
+
144
+ // ── Datepicker ──────────────────────────────────────────────────────────────
145
+ --hub-datepicker-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%236c757d' d='M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z'/%3E%3C/svg%3E");
146
+ --hub-datepicker-icon-size: 1rem;
147
+ --hub-datepicker-icon-width: 2.5rem;
148
+
149
+ // Calendar panel + cells (canonical `--hub-daterangepicker-*`).
150
+ --hub-daterangepicker-bg: var(--hub-sys-surface-page, #fff);
151
+ --hub-daterangepicker-color: var(--hub-sys-text-primary, #212529);
152
+ --hub-daterangepicker-border-color: var(--hub-sys-border-color-default, #dee2e6);
153
+ --hub-daterangepicker-border-radius: var(--hub-ref-radius-md, 0.375rem);
154
+ --hub-daterangepicker-box-shadow: var(--hub-sys-shadow, 0 0.5rem 1rem rgba(0, 0, 0, 0.12));
155
+ --hub-daterangepicker-padding: var(--hub-ref-space-3, 1rem);
156
+ --hub-daterangepicker-cell-size: 2rem;
157
+ --hub-daterangepicker-cell-color: var(--hub-sys-text-primary, #212529);
158
+ --hub-daterangepicker-cell-border-radius: var(--hub-ref-radius-sm, 0.25rem);
159
+ --hub-daterangepicker-cell-hover-bg: var(--hub-sys-surface-elevated, #f5f5f5);
160
+ --hub-daterangepicker-active-bg: var(--hub-sys-color-primary, #0d6efd);
161
+ --hub-daterangepicker-active-color: #fff;
162
+ // Light primary tint (theme-aware): `--hub-sys-color-primary-subtle` is a *dark* navy in
163
+ // this design system, so derive a soft tint from the primary instead.
164
+ --hub-daterangepicker-in-range-bg: color-mix(in srgb, var(--hub-sys-color-primary, #0d6efd) 14%, transparent);
165
+ --hub-daterangepicker-off-color: var(--hub-sys-text-muted, #adb5bd);
166
+ --hub-daterangepicker-nav-arrow-color: var(--hub-sys-text-muted, #6c757d);
167
+ --hub-daterangepicker-nav-arrow-hover-color: var(--hub-sys-text-primary, #212529);
168
+
169
+ // Select · buttons format
170
+ --hub-select-button-bg: var(--hub-sys-surface-page, #fff);
171
+ --hub-select-button-color: var(--hub-sys-text-primary, #212529);
172
+ --hub-select-button-border-color: var(--hub-sys-border-color-default, #dee2e6);
173
+ --hub-select-button-padding-x: var(--hub-ref-space-3, 0.75rem);
174
+ --hub-select-button-padding-y: var(--hub-ref-space-2, 0.5rem);
175
+ --hub-select-button-gap: var(--hub-ref-space-2, 0.5rem);
176
+ --hub-select-button-selected-bg: var(--hub-sys-color-primary, #0d6efd);
177
+ --hub-select-button-selected-color: #fff;
178
+ --hub-select-button-selected-border-color: var(--hub-sys-color-primary, #0d6efd);
179
+ }
@@ -0,0 +1,13 @@
1
+ // ─────────────────────────────────────────────────────────────────────────
2
+ // ng-hub-ui-forms — global stylesheet
3
+ // ─────────────────────────────────────────────────────────────────────────
4
+ // Import this ONCE in your application styles. It provides the canonical design
5
+ // tokens and the shared field chrome consumed by every ng-hub-ui-forms
6
+ // component. Component-specific structure ships with each component.
7
+ //
8
+ // // styles.scss
9
+ // @use 'ng-hub-ui-forms/styles';
10
+ // ─────────────────────────────────────────────────────────────────────────
11
+
12
+ @use 'tokens';
13
+ @use 'field';
@@ -0,0 +1,65 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { ModelSignal, Signal } from '@angular/core';
3
+ import * as ng_hub_ui_forms from 'ng-hub-ui-forms';
4
+ import { FormValueControl, ValidationError } from '@angular/forms/signals';
5
+
6
+ /**
7
+ * Abstract base for Hub form fields that integrate with Angular **Signal Forms**
8
+ * via the `Field` directive (`[formField]`).
9
+ *
10
+ * It implements the {@link FormValueControl} contract — exposing a two-way `value`
11
+ * model the `Field` directive keeps in sync with the bound `FieldTree`, plus the
12
+ * optional state inputs (`errors`, `disabled`, `touched`, `required`) the directive
13
+ * auto-wires — and resolves error messages through the same `HubErrorDisplay`
14
+ * pipeline used by the CVA core (so Reactive and Signal fields look identical).
15
+ *
16
+ * Decorated with `@Directive()` so subclasses (`@Component`) inherit its signal
17
+ * inputs/model, mirroring the core `HubFieldControl` pattern. It lives in the opt-in
18
+ * `ng-hub-ui-forms/signals` entry point and is the only place that depends on
19
+ * `@angular/forms/signals`; the core package never imports it.
20
+ *
21
+ * @template TValue The value type the field edits.
22
+ */
23
+ declare abstract class HubSignalFieldControl<TValue> implements FormValueControl<TValue> {
24
+ /** Hub forms configuration providing the default error-message factory. */
25
+ protected readonly hubFormsConfig: ng_hub_ui_forms.HubFormsConfig;
26
+ /** Two-way model the `Field` directive keeps in sync with the bound `FieldTree` value. */
27
+ readonly value: ModelSignal<TValue>;
28
+ /** Validation errors bound from the field by the `Field` directive. */
29
+ readonly errors: _angular_core.InputSignal<readonly ValidationError.WithOptionalFieldTree[]>;
30
+ /** Disabled state bound from the field by the `Field` directive. */
31
+ readonly disabled: _angular_core.InputSignal<boolean>;
32
+ /** Touched state bound from the field by the `Field` directive. */
33
+ readonly touched: _angular_core.InputSignal<boolean>;
34
+ /** Whether the field is required, bound from the field by the `Field` directive. */
35
+ readonly required: _angular_core.InputSignal<boolean>;
36
+ /** Human-readable messages for the current `errors()`, via the shared Hub error display. */
37
+ readonly messages: Signal<string[]>;
38
+ /** Whether the field should render its invalid state (touched and holding errors). */
39
+ readonly isInvalid: Signal<boolean>;
40
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<HubSignalFieldControl<any>, never>;
41
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<HubSignalFieldControl<any>, never, never, { "value": { "alias": "value"; "required": true; "isSignal": true; }; "errors": { "alias": "errors"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, never>;
42
+ }
43
+
44
+ /**
45
+ * Signal Forms adapter for the shared Hub error-display logic.
46
+ *
47
+ * It mirrors the core `HubErrorDisplay` (CVA/Reactive) behaviour for the Signal
48
+ * Forms model: given the validation errors a field exposes through `field().errors()`,
49
+ * it produces the same human-readable messages. The single source of truth for the
50
+ * default copy stays in the core `defaultInvalidFeedback` (reused here), so Reactive
51
+ * and Signal fields render identical messages.
52
+ *
53
+ * Resolution order per error:
54
+ * 1. The error's own `message` (Signal Forms validators may attach one).
55
+ * 2. The provided `fn` override (typically `HUB_FORMS_CONFIG.invalidFeedbackTemplateFn`
56
+ * or a per-field override), called with the error `kind` and the error object.
57
+ *
58
+ * @param errors Validation errors read from a Signal Forms field (`field().errors()`).
59
+ * @param fn Message factory used when an error carries no inline `message`. Defaults
60
+ * to the core `defaultInvalidFeedback`.
61
+ * @returns The resolved, human-readable error messages in error order.
62
+ */
63
+ declare function hubSignalErrorMessages(errors: readonly ValidationError[] | null | undefined, fn?: (key: string, value: unknown) => string): string[];
64
+
65
+ export { HubSignalFieldControl, hubSignalErrorMessages };