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.
- package/README.md +252 -0
- package/fesm2022/ng-hub-ui-forms-signals.mjs +93 -0
- package/fesm2022/ng-hub-ui-forms-signals.mjs.map +1 -0
- package/fesm2022/ng-hub-ui-forms.mjs +5882 -0
- package/fesm2022/ng-hub-ui-forms.mjs.map +1 -0
- package/ng-hub-ui-forms-21.0.0.tgz +0 -0
- package/package.json +58 -0
- package/signals/README.md +63 -0
- package/src/lib/styles/_field.scss +132 -0
- package/src/lib/styles/_tokens.scss +179 -0
- package/src/lib/styles/index.scss +13 -0
- package/types/ng-hub-ui-forms-signals.d.ts +65 -0
- package/types/ng-hub-ui-forms.d.ts +1463 -0
|
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 };
|