@cdevhub/ngx-tw 0.2.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/LICENSE +21 -0
- package/README.md +134 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
- package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
- package/package.json +265 -0
- package/theme/_base.css +394 -0
- package/theme/_dark.css +510 -0
- package/theme/_high-contrast.css +290 -0
- package/theme/_semantic.css +317 -0
- package/theme/_typography.css +9 -0
- package/theme/index.css +54 -0
- package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
- package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
- package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
- package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
- package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
- package/types/cdevhub-ngx-tw-button.d.ts +74 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
- package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
- package/types/cdevhub-ngx-tw-card.d.ts +48 -0
- package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
- package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
- package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
- package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
- package/types/cdevhub-ngx-tw-core.d.ts +719 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
- package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
- package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
- package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
- package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
- package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
- package/types/cdevhub-ngx-tw-input.d.ts +110 -0
- package/types/cdevhub-ngx-tw-item.d.ts +70 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
- package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
- package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
- package/types/cdevhub-ngx-tw-select.d.ts +384 -0
- package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
- package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
- package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
- package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
- package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
- package/types/cdevhub-ngx-tw-split.d.ts +265 -0
- package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
- package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
- package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
- package/types/cdevhub-ngx-tw-table.d.ts +479 -0
- package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
- package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
- package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
- package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
- package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
- package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
- package/types/cdevhub-ngx-tw.d.ts +49 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, ElementRef, computed, isDevMode, Directive, input, contentChild, contentChildren, viewChild, signal, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { tv } from 'tailwind-variants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contract every ngx-tw form-field-compatible control must implement. A
|
|
7
|
+
* concrete control provides itself under {@link TW_FORM_FIELD_CONTROL} so the
|
|
8
|
+
* surrounding `FormFieldComponent` can mirror its state and wire ARIA.
|
|
9
|
+
*/
|
|
10
|
+
class FormFieldControl {
|
|
11
|
+
/** Active validation errors map keyed by validator name (e.g. `{ required: true, email: true }`). Returns `null` when the control has no validation errors. Drives `[twError match="…"]` filtering inside the form-field. Optional — controls without a backing `NgControl` may omit it. */
|
|
12
|
+
errors;
|
|
13
|
+
/** Optional consumer-supplied `aria-labelledby` ids the form-field preserves when merging in the projected label id. Controls that consume label pushdown (non-native triggers) override this. */
|
|
14
|
+
userAriaLabelledby;
|
|
15
|
+
/**
|
|
16
|
+
* Called by the form-field to push the merged `aria-labelledby` ids onto the control's host element.
|
|
17
|
+
* Default is a no-op — native `<input>` controls rely on the label's `for=` attribute for the canonical association.
|
|
18
|
+
* Non-native controls (combobox triggers, date-pickers, etc.) override this to set the attribute explicitly.
|
|
19
|
+
*/
|
|
20
|
+
setLabelledByIds(_ids) {
|
|
21
|
+
// Default no-op. Controls override when label pushdown is required for accessibility.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Injection token matching {@link FormFieldControl}. Controls register via
|
|
26
|
+
* `providers: [{ provide: TW_FORM_FIELD_CONTROL, useExisting: MyControl }]`.
|
|
27
|
+
*/
|
|
28
|
+
const TW_FORM_FIELD_CONTROL = new InjectionToken('TW_FORM_FIELD_CONTROL');
|
|
29
|
+
// ── Unique ID counters ──
|
|
30
|
+
let nextLabelId = 0;
|
|
31
|
+
let nextHintId = 0;
|
|
32
|
+
let nextErrorId = 0;
|
|
33
|
+
// ── tv() config ──
|
|
34
|
+
const formFieldVariants = tv({
|
|
35
|
+
slots: {
|
|
36
|
+
root: 'block text-fg',
|
|
37
|
+
controlWrapper: 'relative flex items-stretch gap-2 rounded-md bg-transparent transition-colors duration-normal motion-reduce:transition-none',
|
|
38
|
+
infix: 'relative flex-1 flex items-center min-w-0 [&_input::placeholder]:transition-opacity [&_input::placeholder]:duration-normal [&_textarea::placeholder]:transition-opacity [&_textarea::placeholder]:duration-normal',
|
|
39
|
+
labelWrapper: 'absolute pointer-events-none flex items-center max-w-[calc(100%-1.5rem)] transition-[top,left,color,transform] duration-normal motion-reduce:transition-none origin-[top_left]',
|
|
40
|
+
label: 'text-fg-muted truncate transition-[font-size,color] duration-normal motion-reduce:transition-none',
|
|
41
|
+
requiredMarker: 'text-error-600 ml-0.5',
|
|
42
|
+
prefix: 'flex items-center shrink-0 text-fg-muted',
|
|
43
|
+
suffix: 'flex items-center shrink-0 text-fg-muted',
|
|
44
|
+
subscriptWrapper: 'mt-1 text-xs flex gap-2',
|
|
45
|
+
hint: 'text-fg-muted',
|
|
46
|
+
error: 'text-error-600 font-medium',
|
|
47
|
+
},
|
|
48
|
+
variants: {
|
|
49
|
+
appearance: {
|
|
50
|
+
outline: {
|
|
51
|
+
controlWrapper: 'border border-border hover:border-border-strong',
|
|
52
|
+
},
|
|
53
|
+
filled: {
|
|
54
|
+
controlWrapper: 'bg-surface-muted border-b border-border hover:bg-surface-sunken',
|
|
55
|
+
infix: 'items-end',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
size: {
|
|
59
|
+
xs: {},
|
|
60
|
+
sm: {},
|
|
61
|
+
md: {},
|
|
62
|
+
lg: {},
|
|
63
|
+
xl: {},
|
|
64
|
+
},
|
|
65
|
+
color: {
|
|
66
|
+
primary: {},
|
|
67
|
+
secondary: {},
|
|
68
|
+
accent: {},
|
|
69
|
+
neutral: {},
|
|
70
|
+
info: {},
|
|
71
|
+
success: {},
|
|
72
|
+
warning: {},
|
|
73
|
+
error: {},
|
|
74
|
+
},
|
|
75
|
+
focused: {
|
|
76
|
+
true: {},
|
|
77
|
+
false: {},
|
|
78
|
+
},
|
|
79
|
+
invalid: {
|
|
80
|
+
true: {},
|
|
81
|
+
false: {},
|
|
82
|
+
},
|
|
83
|
+
labelFloated: {
|
|
84
|
+
true: {
|
|
85
|
+
labelWrapper: 'top-1 left-2',
|
|
86
|
+
},
|
|
87
|
+
false: {
|
|
88
|
+
labelWrapper: 'top-1/2 -translate-y-1/2 left-3',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
subscriptReserve: {
|
|
92
|
+
true: { subscriptWrapper: 'min-h-5' },
|
|
93
|
+
false: {},
|
|
94
|
+
},
|
|
95
|
+
disabled: {
|
|
96
|
+
true: { root: 'opacity-50 pointer-events-none' },
|
|
97
|
+
false: {},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
compoundVariants: [
|
|
101
|
+
// ── Padding × (appearance, size) ──
|
|
102
|
+
{ appearance: 'outline', size: 'xs', class: { controlWrapper: 'px-2 py-1' } },
|
|
103
|
+
{ appearance: 'outline', size: 'sm', class: { controlWrapper: 'px-3 py-1.5' } },
|
|
104
|
+
{ appearance: 'outline', size: 'md', class: { controlWrapper: 'px-3 py-2' } },
|
|
105
|
+
{ appearance: 'outline', size: 'lg', class: { controlWrapper: 'px-4 py-2.5' } },
|
|
106
|
+
{ appearance: 'outline', size: 'xl', class: { controlWrapper: 'px-5 py-3' } },
|
|
107
|
+
{ appearance: 'filled', size: 'xs', class: { controlWrapper: 'px-2 pt-5 pb-1' } },
|
|
108
|
+
{ appearance: 'filled', size: 'sm', class: { controlWrapper: 'px-3 pt-5 pb-1.5' } },
|
|
109
|
+
{ appearance: 'filled', size: 'md', class: { controlWrapper: 'px-3 pt-6 pb-2' } },
|
|
110
|
+
{ appearance: 'filled', size: 'lg', class: { controlWrapper: 'px-4 pt-7 pb-2.5' } },
|
|
111
|
+
{ appearance: 'filled', size: 'xl', class: { controlWrapper: 'px-5 pt-8 pb-3' } },
|
|
112
|
+
// ── Label font-size × (size, labelFloated) ──
|
|
113
|
+
{ size: 'xs', labelFloated: false, class: { label: 'text-xs' } },
|
|
114
|
+
{ size: 'xs', labelFloated: true, class: { label: 'text-2xs' } },
|
|
115
|
+
{ size: 'sm', labelFloated: false, class: { label: 'text-sm' } },
|
|
116
|
+
{ size: 'sm', labelFloated: true, class: { label: 'text-xs' } },
|
|
117
|
+
{ size: 'md', labelFloated: false, class: { label: 'text-sm' } },
|
|
118
|
+
{ size: 'md', labelFloated: true, class: { label: 'text-xs' } },
|
|
119
|
+
{ size: 'lg', labelFloated: false, class: { label: 'text-base' } },
|
|
120
|
+
{ size: 'lg', labelFloated: true, class: { label: 'text-sm' } },
|
|
121
|
+
{ size: 'xl', labelFloated: false, class: { label: 'text-base' } },
|
|
122
|
+
{ size: 'xl', labelFloated: true, class: { label: 'text-sm' } },
|
|
123
|
+
// ── Outline + focused → colored border ──
|
|
124
|
+
{ appearance: 'outline', focused: true, color: 'primary', class: { controlWrapper: 'border-primary-500' } },
|
|
125
|
+
{ appearance: 'outline', focused: true, color: 'secondary', class: { controlWrapper: 'border-secondary-500' } },
|
|
126
|
+
{ appearance: 'outline', focused: true, color: 'accent', class: { controlWrapper: 'border-accent-500' } },
|
|
127
|
+
{ appearance: 'outline', focused: true, color: 'neutral', class: { controlWrapper: 'border-border-strong' } },
|
|
128
|
+
{ appearance: 'outline', focused: true, color: 'info', class: { controlWrapper: 'border-info-500' } },
|
|
129
|
+
{ appearance: 'outline', focused: true, color: 'success', class: { controlWrapper: 'border-success-500' } },
|
|
130
|
+
{ appearance: 'outline', focused: true, color: 'warning', class: { controlWrapper: 'border-warning-500' } },
|
|
131
|
+
{ appearance: 'outline', focused: true, color: 'error', class: { controlWrapper: 'border-error-500' } },
|
|
132
|
+
// ── Filled + focused → colored bottom border ──
|
|
133
|
+
{ appearance: 'filled', focused: true, color: 'primary', class: { controlWrapper: 'border-b-2 border-primary-500' } },
|
|
134
|
+
{ appearance: 'filled', focused: true, color: 'secondary', class: { controlWrapper: 'border-b-2 border-secondary-500' } },
|
|
135
|
+
{ appearance: 'filled', focused: true, color: 'accent', class: { controlWrapper: 'border-b-2 border-accent-500' } },
|
|
136
|
+
{ appearance: 'filled', focused: true, color: 'neutral', class: { controlWrapper: 'border-b-2 border-border-strong' } },
|
|
137
|
+
{ appearance: 'filled', focused: true, color: 'info', class: { controlWrapper: 'border-b-2 border-info-500' } },
|
|
138
|
+
{ appearance: 'filled', focused: true, color: 'success', class: { controlWrapper: 'border-b-2 border-success-500' } },
|
|
139
|
+
{ appearance: 'filled', focused: true, color: 'warning', class: { controlWrapper: 'border-b-2 border-warning-500' } },
|
|
140
|
+
{ appearance: 'filled', focused: true, color: 'error', class: { controlWrapper: 'border-b-2 border-error-500' } },
|
|
141
|
+
// ── Floated label + focused → colored label ──
|
|
142
|
+
{ labelFloated: true, focused: true, color: 'primary', class: { label: 'text-primary-600' } },
|
|
143
|
+
{ labelFloated: true, focused: true, color: 'secondary', class: { label: 'text-secondary-600' } },
|
|
144
|
+
{ labelFloated: true, focused: true, color: 'accent', class: { label: 'text-accent-600' } },
|
|
145
|
+
{ labelFloated: true, focused: true, color: 'neutral', class: { label: 'text-fg' } },
|
|
146
|
+
{ labelFloated: true, focused: true, color: 'info', class: { label: 'text-info-600' } },
|
|
147
|
+
{ labelFloated: true, focused: true, color: 'success', class: { label: 'text-success-600' } },
|
|
148
|
+
{ labelFloated: true, focused: true, color: 'warning', class: { label: 'text-warning-600' } },
|
|
149
|
+
{ labelFloated: true, focused: true, color: 'error', class: { label: 'text-error-600' } },
|
|
150
|
+
// ── Outline + floated label → sit centered on the top border with a surface notch ──
|
|
151
|
+
{ appearance: 'outline', labelFloated: true, class: { labelWrapper: 'bg-surface px-1 top-0 -translate-y-1/2' } },
|
|
152
|
+
// ── Filled + resting label → align vertically with the input baseline (input is items-end inside an asymmetric pt/pb container) ──
|
|
153
|
+
{ appearance: 'filled', labelFloated: false, class: { labelWrapper: 'top-[calc(50%+0.5rem)]' } },
|
|
154
|
+
// ── Invalid overrides (declared last so twMerge lets them win) ──
|
|
155
|
+
{ invalid: true, appearance: 'outline', class: { controlWrapper: 'border-error-500' } },
|
|
156
|
+
{ invalid: true, appearance: 'filled', class: { controlWrapper: 'border-b-2 border-error-500' } },
|
|
157
|
+
{ invalid: true, labelFloated: true, class: { label: 'text-error-600' } },
|
|
158
|
+
],
|
|
159
|
+
defaultVariants: {
|
|
160
|
+
appearance: 'outline',
|
|
161
|
+
size: 'md',
|
|
162
|
+
color: 'primary',
|
|
163
|
+
focused: false,
|
|
164
|
+
invalid: false,
|
|
165
|
+
labelFloated: false,
|
|
166
|
+
subscriptReserve: true,
|
|
167
|
+
disabled: false,
|
|
168
|
+
},
|
|
169
|
+
}, {
|
|
170
|
+
twMerge: true,
|
|
171
|
+
});
|
|
172
|
+
// ── LabelDirective ──
|
|
173
|
+
class LabelDirective {
|
|
174
|
+
formField = inject(FormFieldComponent);
|
|
175
|
+
elementRef = inject((ElementRef));
|
|
176
|
+
/** @internal */
|
|
177
|
+
id = `tw-form-field-label-${nextLabelId++}`;
|
|
178
|
+
/** @internal */
|
|
179
|
+
controlId = computed(() => this.formField.control()?.id() ?? null, ...(ngDevMode ? [{ debugName: "controlId" }] : /* istanbul ignore next */ []));
|
|
180
|
+
/** @internal */
|
|
181
|
+
classes = computed(() => this.formField.labelClasses(), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
182
|
+
constructor() {
|
|
183
|
+
if (isDevMode()) {
|
|
184
|
+
const tag = this.elementRef.nativeElement.tagName;
|
|
185
|
+
if (tag !== 'LABEL') {
|
|
186
|
+
console.warn(`[tw-form-field] twLabel should be applied to a <label> element for the native \`for\`/\`id\` association to work; got <${tag.toLowerCase()}>. Non-native controls (e.g. tw-select) still receive label pushdown via aria-labelledby.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: LabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
191
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: LabelDirective, isStandalone: true, selector: "[twLabel]", host: { properties: { "attr.id": "id", "attr.for": "controlId()", "class": "classes()" } }, ngImport: i0 });
|
|
192
|
+
}
|
|
193
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: LabelDirective, decorators: [{
|
|
194
|
+
type: Directive,
|
|
195
|
+
args: [{
|
|
196
|
+
selector: '[twLabel]',
|
|
197
|
+
host: {
|
|
198
|
+
'[attr.id]': 'id',
|
|
199
|
+
'[attr.for]': 'controlId()',
|
|
200
|
+
'[class]': 'classes()',
|
|
201
|
+
},
|
|
202
|
+
}]
|
|
203
|
+
}], ctorParameters: () => [] });
|
|
204
|
+
// ── HintDirective ──
|
|
205
|
+
class HintDirective {
|
|
206
|
+
formField = inject(FormFieldComponent);
|
|
207
|
+
/** @internal */
|
|
208
|
+
id = `tw-form-field-hint-${nextHintId++}`;
|
|
209
|
+
/** Alignment within the subscript row. `'end'` pushes the hint to the right. When unset, the form-field's `hintAlign` input supplies the default. */
|
|
210
|
+
align = input(undefined, ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
|
|
211
|
+
/** @internal */
|
|
212
|
+
effectiveAlign = computed(() => this.align() ?? this.formField.hintAlign(), ...(ngDevMode ? [{ debugName: "effectiveAlign" }] : /* istanbul ignore next */ []));
|
|
213
|
+
/** @internal */
|
|
214
|
+
classes = computed(() => {
|
|
215
|
+
const base = this.formField.hintClasses();
|
|
216
|
+
return this.effectiveAlign() === 'end' ? `${base} ml-auto` : base;
|
|
217
|
+
}, ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
218
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: HintDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
219
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: HintDirective, isStandalone: true, selector: "[twHint]", inputs: { align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.id": "id", "class": "classes()" } }, ngImport: i0 });
|
|
220
|
+
}
|
|
221
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: HintDirective, decorators: [{
|
|
222
|
+
type: Directive,
|
|
223
|
+
args: [{
|
|
224
|
+
selector: '[twHint]',
|
|
225
|
+
host: {
|
|
226
|
+
'[attr.id]': 'id',
|
|
227
|
+
'[class]': 'classes()',
|
|
228
|
+
},
|
|
229
|
+
}]
|
|
230
|
+
}], propDecorators: { align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }] } });
|
|
231
|
+
// ── ErrorDirective ──
|
|
232
|
+
class ErrorDirective {
|
|
233
|
+
formField = inject(FormFieldComponent);
|
|
234
|
+
/** @internal */
|
|
235
|
+
id = `tw-form-field-error-${nextErrorId++}`;
|
|
236
|
+
/** Validation error key this message is bound to (e.g. `'required'`, `'email'`, `'minlength'`). When set, the error renders only while the surrounding control reports that key in its active validation errors. When omitted, the error displays whenever the form-field is in an error state — useful as a generic fallback. */
|
|
237
|
+
match = input(undefined, ...(ngDevMode ? [{ debugName: "match" }] : /* istanbul ignore next */ []));
|
|
238
|
+
/** @internal Whether this error should display given the control's active error keys. */
|
|
239
|
+
shouldShow = computed(() => {
|
|
240
|
+
const key = this.match();
|
|
241
|
+
if (!key)
|
|
242
|
+
return true;
|
|
243
|
+
return this.formField.activeErrorKeys().has(key);
|
|
244
|
+
}, ...(ngDevMode ? [{ debugName: "shouldShow" }] : /* istanbul ignore next */ []));
|
|
245
|
+
/** @internal */
|
|
246
|
+
classes = computed(() => this.formField.errorClasses(), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
247
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ErrorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
248
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: ErrorDirective, isStandalone: true, selector: "[twError]", inputs: { match: { classPropertyName: "match", publicName: "match", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "alert", "aria-live": "polite" }, properties: { "attr.id": "id", "class": "classes()", "class.hidden": "!shouldShow()" } }, ngImport: i0 });
|
|
249
|
+
}
|
|
250
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ErrorDirective, decorators: [{
|
|
251
|
+
type: Directive,
|
|
252
|
+
args: [{
|
|
253
|
+
selector: '[twError]',
|
|
254
|
+
host: {
|
|
255
|
+
'role': 'alert',
|
|
256
|
+
'aria-live': 'polite',
|
|
257
|
+
'[attr.id]': 'id',
|
|
258
|
+
'[class]': 'classes()',
|
|
259
|
+
'[class.hidden]': '!shouldShow()',
|
|
260
|
+
},
|
|
261
|
+
}]
|
|
262
|
+
}], propDecorators: { match: [{ type: i0.Input, args: [{ isSignal: true, alias: "match", required: false }] }] } });
|
|
263
|
+
// ── PrefixDirective ──
|
|
264
|
+
class PrefixDirective {
|
|
265
|
+
formField = inject(FormFieldComponent);
|
|
266
|
+
/** @internal Used by the form-field to measure the prefix area and align the resting label after it. */
|
|
267
|
+
elementRef = inject((ElementRef));
|
|
268
|
+
/** @internal */
|
|
269
|
+
prefixClasses = computed(() => this.formField.prefixClasses(), ...(ngDevMode ? [{ debugName: "prefixClasses" }] : /* istanbul ignore next */ []));
|
|
270
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PrefixDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
271
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: PrefixDirective, isStandalone: true, selector: "[twPrefix]", host: { properties: { "class": "prefixClasses()" } }, ngImport: i0 });
|
|
272
|
+
}
|
|
273
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PrefixDirective, decorators: [{
|
|
274
|
+
type: Directive,
|
|
275
|
+
args: [{
|
|
276
|
+
selector: '[twPrefix]',
|
|
277
|
+
host: {
|
|
278
|
+
'[class]': 'prefixClasses()',
|
|
279
|
+
},
|
|
280
|
+
}]
|
|
281
|
+
}] });
|
|
282
|
+
// ── SuffixDirective ──
|
|
283
|
+
class SuffixDirective {
|
|
284
|
+
formField = inject(FormFieldComponent);
|
|
285
|
+
/** @internal */
|
|
286
|
+
suffixClasses = computed(() => this.formField.suffixClasses(), ...(ngDevMode ? [{ debugName: "suffixClasses" }] : /* istanbul ignore next */ []));
|
|
287
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SuffixDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
288
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SuffixDirective, isStandalone: true, selector: "[twSuffix]", host: { properties: { "class": "suffixClasses()" } }, ngImport: i0 });
|
|
289
|
+
}
|
|
290
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SuffixDirective, decorators: [{
|
|
291
|
+
type: Directive,
|
|
292
|
+
args: [{
|
|
293
|
+
selector: '[twSuffix]',
|
|
294
|
+
host: {
|
|
295
|
+
'[class]': 'suffixClasses()',
|
|
296
|
+
},
|
|
297
|
+
}]
|
|
298
|
+
}] });
|
|
299
|
+
// ── PrefixIconDirective / SuffixIconDirective ──
|
|
300
|
+
// Opt-in directives that apply a glyph-sized adornment (`size-5 text-fg-muted shrink-0`) so consumers
|
|
301
|
+
// don't pick `size-4` vs `size-5` ad-hoc. Use these for SVG icons; use [twPrefix]/[twSuffix]
|
|
302
|
+
// when the adornment is text (currency, units, kbd hints).
|
|
303
|
+
// They extend Prefix/SuffixDirective so the form-field's content query picks them up as a prefix/suffix
|
|
304
|
+
// when computing the resting-label offset.
|
|
305
|
+
class PrefixIconDirective extends PrefixDirective {
|
|
306
|
+
/** @internal */
|
|
307
|
+
prefixClasses = computed(() => `${this.formField.prefixClasses()} size-5`, ...(ngDevMode ? [{ debugName: "prefixClasses" }] : /* istanbul ignore next */ []));
|
|
308
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PrefixIconDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
309
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: PrefixIconDirective, isStandalone: true, selector: "[twPrefixIcon]", host: { properties: { "class": "prefixClasses()" } }, providers: [{ provide: PrefixDirective, useExisting: PrefixIconDirective }], usesInheritance: true, ngImport: i0 });
|
|
310
|
+
}
|
|
311
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PrefixIconDirective, decorators: [{
|
|
312
|
+
type: Directive,
|
|
313
|
+
args: [{
|
|
314
|
+
selector: '[twPrefixIcon]',
|
|
315
|
+
host: {
|
|
316
|
+
'[class]': 'prefixClasses()',
|
|
317
|
+
},
|
|
318
|
+
providers: [{ provide: PrefixDirective, useExisting: PrefixIconDirective }],
|
|
319
|
+
}]
|
|
320
|
+
}] });
|
|
321
|
+
class SuffixIconDirective extends SuffixDirective {
|
|
322
|
+
/** @internal */
|
|
323
|
+
suffixClasses = computed(() => `${this.formField.suffixClasses()} size-5`, ...(ngDevMode ? [{ debugName: "suffixClasses" }] : /* istanbul ignore next */ []));
|
|
324
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SuffixIconDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
325
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SuffixIconDirective, isStandalone: true, selector: "[twSuffixIcon]", host: { properties: { "class": "suffixClasses()" } }, providers: [{ provide: SuffixDirective, useExisting: SuffixIconDirective }], usesInheritance: true, ngImport: i0 });
|
|
326
|
+
}
|
|
327
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SuffixIconDirective, decorators: [{
|
|
328
|
+
type: Directive,
|
|
329
|
+
args: [{
|
|
330
|
+
selector: '[twSuffixIcon]',
|
|
331
|
+
host: {
|
|
332
|
+
'[class]': 'suffixClasses()',
|
|
333
|
+
},
|
|
334
|
+
providers: [{ provide: SuffixDirective, useExisting: SuffixIconDirective }],
|
|
335
|
+
}]
|
|
336
|
+
}] });
|
|
337
|
+
// ── FormFieldComponent ──
|
|
338
|
+
class FormFieldComponent {
|
|
339
|
+
/** Visual appearance of the field container. `'outline'` draws a full border around the control; `'filled'` uses a filled surface with a bottom border. Defaults to `'outline'`. */
|
|
340
|
+
appearance = input('outline', ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
|
|
341
|
+
/** Density of the field container. Maps to the inline-padding scale (`px-2 py-1` xs … `px-5 py-3` xl) and the floating-label font scale. Defaults to `'md'`. */
|
|
342
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
343
|
+
/** Floating label behavior. `'auto'` floats when focused or non-empty; `'always'` stays floated; `'never'` disables floating entirely (the label wrapper is not rendered and the wrapped control's placeholder is always visible). Defaults to `'auto'`. */
|
|
344
|
+
floatLabel = input('auto', ...(ngDevMode ? [{ debugName: "floatLabel" }] : /* istanbul ignore next */ []));
|
|
345
|
+
/** Subscript sizing strategy. `'fixed'` always reserves a 20px row for hints/errors so adjacent fields align; `'dynamic'` collapses the row when no hint/error is projected. Defaults to `'fixed'`. */
|
|
346
|
+
subscriptSizing = input('fixed', ...(ngDevMode ? [{ debugName: "subscriptSizing" }] : /* istanbul ignore next */ []));
|
|
347
|
+
/** Hides the visual required marker (`*`) even when the wrapped control is required. `aria-required` on the control is unaffected. Defaults to `false`. */
|
|
348
|
+
hideRequiredMarker = input(false, ...(ngDevMode ? [{ debugName: "hideRequiredMarker" }] : /* istanbul ignore next */ []));
|
|
349
|
+
/** Semantic color for focused/active accents (focused border, floated label). Defaults to `'primary'`. */
|
|
350
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
351
|
+
/** Default alignment for a `twHint` element that does not specify its own `align`. Defaults to `'start'`. */
|
|
352
|
+
hintAlign = input('start', ...(ngDevMode ? [{ debugName: "hintAlign" }] : /* istanbul ignore next */ []));
|
|
353
|
+
/** @internal */
|
|
354
|
+
control = contentChild(TW_FORM_FIELD_CONTROL, { ...(ngDevMode ? { debugName: "control" } : /* istanbul ignore next */ {}), descendants: true });
|
|
355
|
+
/** @internal */
|
|
356
|
+
labelChild = contentChild(LabelDirective, { ...(ngDevMode ? { debugName: "labelChild" } : /* istanbul ignore next */ {}), descendants: true });
|
|
357
|
+
/** @internal */
|
|
358
|
+
hintChildren = contentChildren(HintDirective, { ...(ngDevMode ? { debugName: "hintChildren" } : /* istanbul ignore next */ {}), descendants: true });
|
|
359
|
+
/** @internal */
|
|
360
|
+
errorChildren = contentChildren(ErrorDirective, { ...(ngDevMode ? { debugName: "errorChildren" } : /* istanbul ignore next */ {}), descendants: true });
|
|
361
|
+
/** @internal */
|
|
362
|
+
prefixChildren = contentChildren(PrefixDirective, { ...(ngDevMode ? { debugName: "prefixChildren" } : /* istanbul ignore next */ {}), descendants: true });
|
|
363
|
+
/** @internal */
|
|
364
|
+
infixRef = viewChild('infix', ...(ngDevMode ? [{ debugName: "infixRef" }] : /* istanbul ignore next */ []));
|
|
365
|
+
/** @internal */
|
|
366
|
+
hasLabel = computed(() => !!this.labelChild(), ...(ngDevMode ? [{ debugName: "hasLabel" }] : /* istanbul ignore next */ []));
|
|
367
|
+
/** @internal */
|
|
368
|
+
hasPrefix = computed(() => this.prefixChildren().length > 0, ...(ngDevMode ? [{ debugName: "hasPrefix" }] : /* istanbul ignore next */ []));
|
|
369
|
+
/** @internal Resting-label left offset in pixels, mirroring the infix's `offsetLeft` so the label sits right after any prefix. */
|
|
370
|
+
restingLabelOffset = signal(12, ...(ngDevMode ? [{ debugName: "restingLabelOffset" }] : /* istanbul ignore next */ []));
|
|
371
|
+
/** @internal */
|
|
372
|
+
isFocused = computed(() => !!this.control()?.focused(), ...(ngDevMode ? [{ debugName: "isFocused" }] : /* istanbul ignore next */ []));
|
|
373
|
+
/** @internal */
|
|
374
|
+
isInvalid = computed(() => !!this.control()?.errorState(), ...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
375
|
+
/** @internal */
|
|
376
|
+
isDisabled = computed(() => !!this.control()?.disabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
377
|
+
/** @internal */
|
|
378
|
+
isRequired = computed(() => !!this.control()?.required(), ...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
|
|
379
|
+
/** @internal Whether the floating label wrapper should render at all. `'never'` opts out entirely. */
|
|
380
|
+
shouldRenderLabelWrapper = computed(() => this.hasLabel() && this.floatLabel() !== 'never', ...(ngDevMode ? [{ debugName: "shouldRenderLabelWrapper" }] : /* istanbul ignore next */ []));
|
|
381
|
+
/** @internal */
|
|
382
|
+
shouldLabelFloat = computed(() => {
|
|
383
|
+
const mode = this.floatLabel();
|
|
384
|
+
if (mode === 'never')
|
|
385
|
+
return false;
|
|
386
|
+
if (mode === 'always')
|
|
387
|
+
return true;
|
|
388
|
+
const ctrl = this.control();
|
|
389
|
+
if (!ctrl)
|
|
390
|
+
return false;
|
|
391
|
+
return ctrl.focused() || !ctrl.empty();
|
|
392
|
+
}, ...(ngDevMode ? [{ debugName: "shouldLabelFloat" }] : /* istanbul ignore next */ []));
|
|
393
|
+
/** @internal Inline `left` value for the label wrapper. Floated labels pin to the container edge; resting labels align with the infix's left (after any prefix). */
|
|
394
|
+
labelLeft = computed(() => this.shouldLabelFloat() ? '0.5rem' : `${this.restingLabelOffset()}px`, ...(ngDevMode ? [{ debugName: "labelLeft" }] : /* istanbul ignore next */ []));
|
|
395
|
+
/** @internal */
|
|
396
|
+
subscriptMode = computed(() => this.isInvalid() && this.errorChildren().length > 0 ? 'error' : 'hint', ...(ngDevMode ? [{ debugName: "subscriptMode" }] : /* istanbul ignore next */ []));
|
|
397
|
+
/** @internal Set of validation error keys currently active on the bound control (e.g. `Set('required', 'email')`). Empty when the control has no `errors` signal or no active errors. Read by `[twError match="…"]` to decide whether to display. */
|
|
398
|
+
activeErrorKeys = computed(() => {
|
|
399
|
+
const errs = this.control()?.errors?.();
|
|
400
|
+
return new Set(errs ? Object.keys(errs) : []);
|
|
401
|
+
}, ...(ngDevMode ? [{ debugName: "activeErrorKeys" }] : /* istanbul ignore next */ []));
|
|
402
|
+
/** @internal Whether the subscript row has anything to render right now. */
|
|
403
|
+
hasSubscriptContent = computed(() => {
|
|
404
|
+
if (this.subscriptMode() === 'error')
|
|
405
|
+
return this.errorChildren().length > 0;
|
|
406
|
+
return this.hintChildren().length > 0;
|
|
407
|
+
}, ...(ngDevMode ? [{ debugName: "hasSubscriptContent" }] : /* istanbul ignore next */ []));
|
|
408
|
+
/** @internal Whether the subscript wrapper renders. In `'fixed'` mode it always renders to reserve vertical space; in `'dynamic'` mode it collapses when empty. */
|
|
409
|
+
shouldRenderSubscript = computed(() => {
|
|
410
|
+
if (this.subscriptSizing() === 'fixed')
|
|
411
|
+
return true;
|
|
412
|
+
return this.hasSubscriptContent();
|
|
413
|
+
}, ...(ngDevMode ? [{ debugName: "shouldRenderSubscript" }] : /* istanbul ignore next */ []));
|
|
414
|
+
controlTypeClass = computed(() => {
|
|
415
|
+
const type = this.control()?.controlType;
|
|
416
|
+
return type ? `tw-form-field-type-${type}` : '';
|
|
417
|
+
}, ...(ngDevMode ? [{ debugName: "controlTypeClass" }] : /* istanbul ignore next */ []));
|
|
418
|
+
variantResult = computed(() => formFieldVariants({
|
|
419
|
+
appearance: this.appearance(),
|
|
420
|
+
size: this.size(),
|
|
421
|
+
color: this.color(),
|
|
422
|
+
focused: this.isFocused(),
|
|
423
|
+
invalid: this.isInvalid(),
|
|
424
|
+
labelFloated: this.shouldLabelFloat(),
|
|
425
|
+
subscriptReserve: this.subscriptSizing() === 'fixed',
|
|
426
|
+
disabled: this.isDisabled(),
|
|
427
|
+
}), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
|
|
428
|
+
/** @internal */
|
|
429
|
+
rootClasses = computed(() => {
|
|
430
|
+
const base = this.variantResult().root();
|
|
431
|
+
const type = this.controlTypeClass();
|
|
432
|
+
return type ? `${base} ${type}` : base;
|
|
433
|
+
}, ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
|
|
434
|
+
/** @internal */
|
|
435
|
+
controlWrapperClasses = computed(() => this.variantResult().controlWrapper(), ...(ngDevMode ? [{ debugName: "controlWrapperClasses" }] : /* istanbul ignore next */ []));
|
|
436
|
+
/** @internal */
|
|
437
|
+
infixClasses = computed(() => {
|
|
438
|
+
const base = this.variantResult().infix();
|
|
439
|
+
// Hide the placeholder while the resting label is occupying the same space.
|
|
440
|
+
// `floatLabel='never'` keeps the placeholder visible regardless of label projection.
|
|
441
|
+
const hidePlaceholder = this.shouldRenderLabelWrapper() && !this.shouldLabelFloat();
|
|
442
|
+
return hidePlaceholder
|
|
443
|
+
? `${base} [&_input::placeholder]:opacity-0 [&_textarea::placeholder]:opacity-0`
|
|
444
|
+
: base;
|
|
445
|
+
}, ...(ngDevMode ? [{ debugName: "infixClasses" }] : /* istanbul ignore next */ []));
|
|
446
|
+
/** @internal */
|
|
447
|
+
labelWrapperClasses = computed(() => this.variantResult().labelWrapper(), ...(ngDevMode ? [{ debugName: "labelWrapperClasses" }] : /* istanbul ignore next */ []));
|
|
448
|
+
/** @internal */
|
|
449
|
+
labelClasses = computed(() => this.variantResult().label(), ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
|
|
450
|
+
/** @internal */
|
|
451
|
+
requiredMarkerClasses = computed(() => this.variantResult().requiredMarker(), ...(ngDevMode ? [{ debugName: "requiredMarkerClasses" }] : /* istanbul ignore next */ []));
|
|
452
|
+
/** @internal */
|
|
453
|
+
prefixClasses = computed(() => this.variantResult().prefix(), ...(ngDevMode ? [{ debugName: "prefixClasses" }] : /* istanbul ignore next */ []));
|
|
454
|
+
/** @internal */
|
|
455
|
+
suffixClasses = computed(() => this.variantResult().suffix(), ...(ngDevMode ? [{ debugName: "suffixClasses" }] : /* istanbul ignore next */ []));
|
|
456
|
+
/** @internal */
|
|
457
|
+
subscriptWrapperClasses = computed(() => this.variantResult().subscriptWrapper(), ...(ngDevMode ? [{ debugName: "subscriptWrapperClasses" }] : /* istanbul ignore next */ []));
|
|
458
|
+
/** @internal */
|
|
459
|
+
hintClasses = computed(() => this.variantResult().hint(), ...(ngDevMode ? [{ debugName: "hintClasses" }] : /* istanbul ignore next */ []));
|
|
460
|
+
/** @internal */
|
|
461
|
+
errorClasses = computed(() => this.variantResult().error(), ...(ngDevMode ? [{ debugName: "errorClasses" }] : /* istanbul ignore next */ []));
|
|
462
|
+
constructor() {
|
|
463
|
+
// Push merged aria-describedby ids to the control whenever hint/error lists, the subscript mode, or per-error `match` visibility change.
|
|
464
|
+
effect(() => {
|
|
465
|
+
const ctrl = this.control();
|
|
466
|
+
if (!ctrl)
|
|
467
|
+
return;
|
|
468
|
+
const ids = [];
|
|
469
|
+
if (this.subscriptMode() === 'error') {
|
|
470
|
+
for (const e of this.errorChildren()) {
|
|
471
|
+
// Filter by `shouldShow()` so `[twError match="…"]` entries that
|
|
472
|
+
// don't match the active validation keys are not announced.
|
|
473
|
+
if (e.shouldShow())
|
|
474
|
+
ids.push(e.id);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
for (const h of this.hintChildren())
|
|
479
|
+
ids.push(h.id);
|
|
480
|
+
}
|
|
481
|
+
const userIds = ctrl.userAriaDescribedBy?.();
|
|
482
|
+
if (userIds) {
|
|
483
|
+
for (const id of userIds.split(/\s+/)) {
|
|
484
|
+
if (id)
|
|
485
|
+
ids.push(id);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
untracked(() => ctrl.setDescribedByIds(ids));
|
|
489
|
+
});
|
|
490
|
+
// Push merged aria-labelledby ids (projected label id + user-supplied) to the control.
|
|
491
|
+
// Native inputs ignore this in practice (the `for=`/`id` association wins) but non-native
|
|
492
|
+
// triggers (combobox, date-picker) need the explicit attribute for screen readers.
|
|
493
|
+
effect(() => {
|
|
494
|
+
const ctrl = this.control();
|
|
495
|
+
if (!ctrl)
|
|
496
|
+
return;
|
|
497
|
+
const ids = [];
|
|
498
|
+
const labelId = this.labelChild()?.id;
|
|
499
|
+
if (labelId && this.shouldRenderLabelWrapper())
|
|
500
|
+
ids.push(labelId);
|
|
501
|
+
const userIds = ctrl.userAriaLabelledby?.();
|
|
502
|
+
if (userIds) {
|
|
503
|
+
for (const id of userIds.split(/\s+/)) {
|
|
504
|
+
if (id)
|
|
505
|
+
ids.push(id);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
untracked(() => ctrl.setLabelledByIds(ids));
|
|
509
|
+
});
|
|
510
|
+
// Dev-mode validation: at most one hint per alignment.
|
|
511
|
+
// Reports via `console.error` instead of `throw` so an authoring mistake
|
|
512
|
+
// surfaces during development without crashing the surrounding effect graph
|
|
513
|
+
// (which would otherwise leave Angular in an unrecoverable error state).
|
|
514
|
+
effect(() => {
|
|
515
|
+
if (!isDevMode())
|
|
516
|
+
return;
|
|
517
|
+
const hints = this.hintChildren();
|
|
518
|
+
const startCount = hints.filter((h) => h.effectiveAlign() === 'start').length;
|
|
519
|
+
const endCount = hints.filter((h) => h.effectiveAlign() === 'end').length;
|
|
520
|
+
if (startCount > 1 || endCount > 1) {
|
|
521
|
+
console.error('tw-form-field: only one twHint per alignment ("start" / "end") is allowed.');
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
// Track the infix's left offset so the resting label can sit right after any prefix.
|
|
525
|
+
effect((onCleanup) => {
|
|
526
|
+
const prefixes = this.prefixChildren();
|
|
527
|
+
if (prefixes.length === 0) {
|
|
528
|
+
this.restingLabelOffset.set(12);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const infix = this.infixRef()?.nativeElement;
|
|
532
|
+
if (!infix)
|
|
533
|
+
return;
|
|
534
|
+
const update = () => this.restingLabelOffset.set(infix.offsetLeft);
|
|
535
|
+
update();
|
|
536
|
+
if (typeof ResizeObserver === 'undefined')
|
|
537
|
+
return;
|
|
538
|
+
const ro = new ResizeObserver(update);
|
|
539
|
+
for (const p of prefixes) {
|
|
540
|
+
ro.observe(p.elementRef.nativeElement);
|
|
541
|
+
}
|
|
542
|
+
ro.observe(infix);
|
|
543
|
+
onCleanup(() => ro.disconnect());
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
ngAfterContentInit() {
|
|
547
|
+
if (isDevMode() && !this.control()) {
|
|
548
|
+
throw new Error('tw-form-field requires a child control providing TW_FORM_FIELD_CONTROL. ' +
|
|
549
|
+
'Place a control (e.g. <input twInput />) inside the <tw-form-field>.');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/** @internal */
|
|
553
|
+
_onContainerClick(event) {
|
|
554
|
+
this.control()?.onContainerClick(event);
|
|
555
|
+
}
|
|
556
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
557
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: FormFieldComponent, isStandalone: true, selector: "tw-form-field", inputs: { appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, floatLabel: { classPropertyName: "floatLabel", publicName: "floatLabel", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null }, hideRequiredMarker: { classPropertyName: "hideRequiredMarker", publicName: "hideRequiredMarker", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, hintAlign: { classPropertyName: "hintAlign", publicName: "hintAlign", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "_onContainerClick($event)" }, properties: { "class": "rootClasses()" } }, queries: [{ propertyName: "control", first: true, predicate: TW_FORM_FIELD_CONTROL, descendants: true, isSignal: true }, { propertyName: "labelChild", first: true, predicate: LabelDirective, descendants: true, isSignal: true }, { propertyName: "hintChildren", predicate: HintDirective, descendants: true, isSignal: true }, { propertyName: "errorChildren", predicate: ErrorDirective, descendants: true, isSignal: true }, { propertyName: "prefixChildren", predicate: PrefixDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "infixRef", first: true, predicate: ["infix"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [class]=\"controlWrapperClasses()\">\n <ng-content select=\"[twPrefix]\" />\n <ng-content select=\"[twPrefixIcon]\" />\n <div #infix [class]=\"infixClasses()\">\n <ng-content />\n </div>\n <ng-content select=\"[twSuffix]\" />\n <ng-content select=\"[twSuffixIcon]\" />\n @if (shouldRenderLabelWrapper()) {\n <div [class]=\"labelWrapperClasses()\" [style.left]=\"labelLeft()\">\n <ng-content select=\"[twLabel]\" />\n @if (!hideRequiredMarker() && isRequired()) {\n <span [class]=\"requiredMarkerClasses()\" aria-hidden=\"true\">*</span>\n }\n </div>\n }\n</div>\n\n@if (shouldRenderSubscript()) {\n <div [class]=\"subscriptWrapperClasses()\">\n @if (subscriptMode() === 'error') {\n <ng-content select=\"[twError]\" />\n } @else {\n <ng-content select=\"[twHint]\" />\n }\n </div>\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
558
|
+
}
|
|
559
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormFieldComponent, decorators: [{
|
|
560
|
+
type: Component,
|
|
561
|
+
args: [{ selector: 'tw-form-field', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
562
|
+
'[class]': 'rootClasses()',
|
|
563
|
+
'(click)': '_onContainerClick($event)',
|
|
564
|
+
}, template: "<div [class]=\"controlWrapperClasses()\">\n <ng-content select=\"[twPrefix]\" />\n <ng-content select=\"[twPrefixIcon]\" />\n <div #infix [class]=\"infixClasses()\">\n <ng-content />\n </div>\n <ng-content select=\"[twSuffix]\" />\n <ng-content select=\"[twSuffixIcon]\" />\n @if (shouldRenderLabelWrapper()) {\n <div [class]=\"labelWrapperClasses()\" [style.left]=\"labelLeft()\">\n <ng-content select=\"[twLabel]\" />\n @if (!hideRequiredMarker() && isRequired()) {\n <span [class]=\"requiredMarkerClasses()\" aria-hidden=\"true\">*</span>\n }\n </div>\n }\n</div>\n\n@if (shouldRenderSubscript()) {\n <div [class]=\"subscriptWrapperClasses()\">\n @if (subscriptMode() === 'error') {\n <ng-content select=\"[twError]\" />\n } @else {\n <ng-content select=\"[twHint]\" />\n }\n </div>\n}\n" }]
|
|
565
|
+
}], ctorParameters: () => [], propDecorators: { appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], floatLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "floatLabel", required: false }] }], subscriptSizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "subscriptSizing", required: false }] }], hideRequiredMarker: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideRequiredMarker", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], hintAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintAlign", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TW_FORM_FIELD_CONTROL), { ...{ descendants: true }, isSignal: true }] }], labelChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => LabelDirective), { ...{ descendants: true }, isSignal: true }] }], hintChildren: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => HintDirective), { ...{ descendants: true }, isSignal: true }] }], errorChildren: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ErrorDirective), { ...{ descendants: true }, isSignal: true }] }], prefixChildren: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => PrefixDirective), { ...{ descendants: true }, isSignal: true }] }], infixRef: [{ type: i0.ViewChild, args: ['infix', { isSignal: true }] }] } });
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Generated bundle index. Do not edit.
|
|
569
|
+
*/
|
|
570
|
+
|
|
571
|
+
export { ErrorDirective, FormFieldComponent, FormFieldControl, HintDirective, LabelDirective, PrefixDirective, PrefixIconDirective, SuffixDirective, SuffixIconDirective, TW_FORM_FIELD_CONTROL };
|
|
572
|
+
//# sourceMappingURL=cdevhub-ngx-tw-form-field.mjs.map
|