tyrell-components 1.0.0-TC56 → 1.0.0-TC58
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/css/tyrell.css +3 -3
- package/dist/tyrell.js +1 -1
- package/dist/tyrell.js.map +1 -0
- package/lib/base/ty-component.d.ts +15 -3
- package/lib/base/ty-component.js +26 -18
- package/lib/components/button.d.ts +4 -0
- package/lib/components/button.js +33 -6
- package/lib/components/calendar-month.js +3 -10
- package/lib/components/calendar.d.ts +5 -1
- package/lib/components/calendar.js +28 -34
- package/lib/components/checkbox.d.ts +3 -1
- package/lib/components/checkbox.js +9 -1
- package/lib/components/date-picker.d.ts +61 -7
- package/lib/components/date-picker.js +274 -133
- package/lib/components/file-upload.d.ts +4 -1
- package/lib/components/file-upload.js +17 -3
- package/lib/components/input.d.ts +82 -3
- package/lib/components/input.js +178 -20
- package/lib/components/modal.d.ts +2 -0
- package/lib/components/modal.js +49 -10
- package/lib/components/option.js +7 -5
- package/lib/components/popup.js +59 -10
- package/lib/components/radio.d.ts +4 -1
- package/lib/components/radio.js +10 -1
- package/lib/components/resize-observer.js +2 -0
- package/lib/components/scroll-container.js +2 -0
- package/lib/components/select.d.ts +34 -3
- package/lib/components/select.js +135 -11
- package/lib/components/selected-tags.d.ts +7 -1
- package/lib/components/selected-tags.js +10 -1
- package/lib/components/switch.d.ts +5 -1
- package/lib/components/switch.js +9 -1
- package/lib/components/textarea.d.ts +46 -2
- package/lib/components/textarea.js +60 -9
- package/lib/styles/button.js +20 -19
- package/lib/styles/date-picker.js +18 -1
- package/lib/styles/file-upload.d.ts +1 -1
- package/lib/styles/file-upload.js +3 -3
- package/lib/styles/icon.d.ts +1 -1
- package/lib/styles/icon.js +2 -2
- package/lib/styles/input.js +1 -1
- package/lib/styles/radio.js +2 -2
- package/lib/styles/select-base.js +26 -0
- package/lib/styles/textarea.d.ts +1 -1
- package/lib/styles/textarea.js +7 -0
- package/lib/utils/error-flavor.d.ts +8 -0
- package/lib/utils/error-flavor.js +1 -0
- package/lib/utils/property-manager.d.ts +6 -0
- package/lib/utils/property-manager.js +15 -5
- package/lib/utils/upgrade-properties.d.ts +21 -0
- package/lib/utils/upgrade-properties.js +47 -0
- package/lib/utils/validity.d.ts +72 -2
- package/lib/utils/validity.js +192 -8
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +3 -2
- package/lib/base/ty-component.d.ts.map +0 -1
- package/lib/base/ty-component.js.map +0 -1
- package/lib/components/button.d.ts.map +0 -1
- package/lib/components/button.js.map +0 -1
- package/lib/components/calendar-month.d.ts.map +0 -1
- package/lib/components/calendar-month.js.map +0 -1
- package/lib/components/calendar-navigation.d.ts.map +0 -1
- package/lib/components/calendar-navigation.js.map +0 -1
- package/lib/components/calendar.d.ts.map +0 -1
- package/lib/components/calendar.js.map +0 -1
- package/lib/components/checkbox.d.ts.map +0 -1
- package/lib/components/checkbox.js.map +0 -1
- package/lib/components/copy.d.ts.map +0 -1
- package/lib/components/copy.js.map +0 -1
- package/lib/components/date-picker.d.ts.map +0 -1
- package/lib/components/date-picker.js.map +0 -1
- package/lib/components/dropdown.d.ts +0 -403
- package/lib/components/dropdown.d.ts.map +0 -1
- package/lib/components/dropdown.js +0 -1315
- package/lib/components/dropdown.js.map +0 -1
- package/lib/components/icon.d.ts.map +0 -1
- package/lib/components/icon.js.map +0 -1
- package/lib/components/input.d.ts.map +0 -1
- package/lib/components/input.js.map +0 -1
- package/lib/components/modal.d.ts.map +0 -1
- package/lib/components/modal.js.map +0 -1
- package/lib/components/multiselect.d.ts +0 -389
- package/lib/components/multiselect.d.ts.map +0 -1
- package/lib/components/multiselect.js +0 -1301
- package/lib/components/multiselect.js.map +0 -1
- package/lib/components/option.d.ts.map +0 -1
- package/lib/components/option.js.map +0 -1
- package/lib/components/popup.d.ts.map +0 -1
- package/lib/components/popup.js.map +0 -1
- package/lib/components/resize-observer.d.ts.map +0 -1
- package/lib/components/resize-observer.js.map +0 -1
- package/lib/components/scroll-container.d.ts.map +0 -1
- package/lib/components/scroll-container.js.map +0 -1
- package/lib/components/step.d.ts.map +0 -1
- package/lib/components/step.js.map +0 -1
- package/lib/components/tab.d.ts.map +0 -1
- package/lib/components/tab.js.map +0 -1
- package/lib/components/tabs.d.ts.map +0 -1
- package/lib/components/tabs.js.map +0 -1
- package/lib/components/tag.d.ts.map +0 -1
- package/lib/components/tag.js.map +0 -1
- package/lib/components/textarea.d.ts.map +0 -1
- package/lib/components/textarea.js.map +0 -1
- package/lib/components/tooltip.d.ts.map +0 -1
- package/lib/components/tooltip.js.map +0 -1
- package/lib/components/wizard.d.ts.map +0 -1
- package/lib/components/wizard.js.map +0 -1
- package/lib/icons/fontawesome/brands.d.ts.map +0 -1
- package/lib/icons/fontawesome/brands.js.map +0 -1
- package/lib/icons/fontawesome/regular.d.ts.map +0 -1
- package/lib/icons/fontawesome/regular.js.map +0 -1
- package/lib/icons/fontawesome/solid.d.ts.map +0 -1
- package/lib/icons/fontawesome/solid.js.map +0 -1
- package/lib/icons/heroicons/micro.d.ts.map +0 -1
- package/lib/icons/heroicons/micro.js.map +0 -1
- package/lib/icons/heroicons/mini.d.ts.map +0 -1
- package/lib/icons/heroicons/mini.js.map +0 -1
- package/lib/icons/heroicons/outline.d.ts.map +0 -1
- package/lib/icons/heroicons/outline.js.map +0 -1
- package/lib/icons/heroicons/solid.d.ts.map +0 -1
- package/lib/icons/heroicons/solid.js.map +0 -1
- package/lib/icons/lucide.d.ts.map +0 -1
- package/lib/icons/lucide.js.map +0 -1
- package/lib/icons/material/filled.d.ts.map +0 -1
- package/lib/icons/material/filled.js.map +0 -1
- package/lib/icons/material/outlined.d.ts.map +0 -1
- package/lib/icons/material/outlined.js.map +0 -1
- package/lib/icons/material/round.d.ts.map +0 -1
- package/lib/icons/material/round.js.map +0 -1
- package/lib/icons/material/sharp.d.ts.map +0 -1
- package/lib/icons/material/sharp.js.map +0 -1
- package/lib/icons/material/two-tone.d.ts.map +0 -1
- package/lib/icons/material/two-tone.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/styles/button.d.ts.map +0 -1
- package/lib/styles/button.js.map +0 -1
- package/lib/styles/calendar-month.d.ts.map +0 -1
- package/lib/styles/calendar-month.js.map +0 -1
- package/lib/styles/calendar-navigation.d.ts.map +0 -1
- package/lib/styles/calendar-navigation.js.map +0 -1
- package/lib/styles/calendar.d.ts.map +0 -1
- package/lib/styles/calendar.js.map +0 -1
- package/lib/styles/copy.d.ts.map +0 -1
- package/lib/styles/copy.js.map +0 -1
- package/lib/styles/date-picker.d.ts.map +0 -1
- package/lib/styles/date-picker.js.map +0 -1
- package/lib/styles/dropdown.d.ts +0 -12
- package/lib/styles/dropdown.d.ts.map +0 -1
- package/lib/styles/dropdown.js +0 -725
- package/lib/styles/dropdown.js.map +0 -1
- package/lib/styles/icon.d.ts.map +0 -1
- package/lib/styles/icon.js.map +0 -1
- package/lib/styles/input.d.ts.map +0 -1
- package/lib/styles/input.js.map +0 -1
- package/lib/styles/modal.d.ts.map +0 -1
- package/lib/styles/modal.js.map +0 -1
- package/lib/styles/multiselect.d.ts +0 -6
- package/lib/styles/multiselect.d.ts.map +0 -1
- package/lib/styles/multiselect.js +0 -568
- package/lib/styles/multiselect.js.map +0 -1
- package/lib/styles/option.d.ts.map +0 -1
- package/lib/styles/option.js.map +0 -1
- package/lib/styles/popup.d.ts.map +0 -1
- package/lib/styles/popup.js.map +0 -1
- package/lib/styles/resize-observer.d.ts.map +0 -1
- package/lib/styles/resize-observer.js.map +0 -1
- package/lib/styles/scroll-container.d.ts.map +0 -1
- package/lib/styles/scroll-container.js.map +0 -1
- package/lib/styles/step.d.ts.map +0 -1
- package/lib/styles/step.js.map +0 -1
- package/lib/styles/tab.d.ts.map +0 -1
- package/lib/styles/tab.js.map +0 -1
- package/lib/styles/tabs.d.ts.map +0 -1
- package/lib/styles/tabs.js.map +0 -1
- package/lib/styles/tag.d.ts.map +0 -1
- package/lib/styles/tag.js.map +0 -1
- package/lib/styles/textarea.d.ts.map +0 -1
- package/lib/styles/textarea.js.map +0 -1
- package/lib/styles/tooltip.d.ts.map +0 -1
- package/lib/styles/tooltip.js.map +0 -1
- package/lib/styles/wizard.d.ts.map +0 -1
- package/lib/styles/wizard.js.map +0 -1
- package/lib/types/common.d.ts.map +0 -1
- package/lib/types/common.js.map +0 -1
- package/lib/utils/calendar-utils.d.ts.map +0 -1
- package/lib/utils/calendar-utils.js.map +0 -1
- package/lib/utils/icon-registry.d.ts.map +0 -1
- package/lib/utils/icon-registry.js.map +0 -1
- package/lib/utils/locale.d.ts.map +0 -1
- package/lib/utils/locale.js.map +0 -1
- package/lib/utils/number-format.d.ts.map +0 -1
- package/lib/utils/number-format.js.map +0 -1
- package/lib/utils/parse-boolean.d.ts +0 -39
- package/lib/utils/parse-boolean.d.ts.map +0 -1
- package/lib/utils/parse-boolean.js +0 -23
- package/lib/utils/parse-boolean.js.map +0 -1
- package/lib/utils/positioning.d.ts.map +0 -1
- package/lib/utils/positioning.js.map +0 -1
- package/lib/utils/property-capture.d.ts.map +0 -1
- package/lib/utils/property-capture.js.map +0 -1
- package/lib/utils/property-manager.d.ts.map +0 -1
- package/lib/utils/property-manager.js.map +0 -1
- package/lib/utils/resize-observer.d.ts.map +0 -1
- package/lib/utils/resize-observer.js.map +0 -1
- package/lib/utils/scroll-lock.d.ts.map +0 -1
- package/lib/utils/scroll-lock.js.map +0 -1
- package/lib/utils/styles.d.ts.map +0 -1
- package/lib/utils/styles.js.map +0 -1
- package/lib/version.d.ts.map +0 -1
- package/lib/version.js.map +0 -1
package/lib/styles/button.js
CHANGED
|
@@ -69,11 +69,25 @@ color: var(--ty-button-color, ${ink('-strong')});
|
|
|
69
69
|
export const buttonCustomFlavorCss = (base) => solidFlavor(base, 'neutral') + outlinedFlavor(base, 'neutral') + ghostFlavor(base, 'neutral');
|
|
70
70
|
export const buttonStyles = `
|
|
71
71
|
:host {
|
|
72
|
-
|
|
72
|
+
--_rung: var(--ty-button-height);
|
|
73
|
+
display: inline-grid;
|
|
74
|
+
grid-template: minmax(var(--_rung), 1fr) / minmax(min-content, 1fr);
|
|
75
|
+
height: fit-content;
|
|
73
76
|
font-family: inherit;
|
|
74
77
|
}
|
|
78
|
+
:host([action]) {
|
|
79
|
+
--_rung: var(--ty-button-action-size);
|
|
80
|
+
}
|
|
81
|
+
:host([action]:not([wide])) {
|
|
82
|
+
width: fit-content;
|
|
83
|
+
aspect-ratio: 1;
|
|
84
|
+
}
|
|
75
85
|
:host([wide]) {
|
|
76
|
-
display:
|
|
86
|
+
display: grid;
|
|
87
|
+
flex-grow: 1;
|
|
88
|
+
grid-template-rows: minmax(var(--_rung), max-content);
|
|
89
|
+
}
|
|
90
|
+
:host([wide]) button {
|
|
77
91
|
flex-grow: 1;
|
|
78
92
|
}
|
|
79
93
|
:host {
|
|
@@ -140,6 +154,7 @@ align-items: center;
|
|
|
140
154
|
justify-content: center;
|
|
141
155
|
gap: var(--ty-button-gap);
|
|
142
156
|
height: var(--ty-button-height);
|
|
157
|
+
min-height: 100%;
|
|
143
158
|
padding: var(--ty-button-pad-y) var(--ty-button-pad-x);
|
|
144
159
|
font-size: var(--ty-button-font);
|
|
145
160
|
line-height: var(--ty-button-leading);
|
|
@@ -214,13 +229,11 @@ justify-content: center;
|
|
|
214
229
|
::slotted(ty-icon) {
|
|
215
230
|
flex-shrink: 0;
|
|
216
231
|
}
|
|
217
|
-
:host([wide]) button {
|
|
218
|
-
flex-grow: 1;
|
|
219
|
-
}
|
|
220
232
|
button.action {
|
|
221
233
|
gap: 0px !important;
|
|
222
|
-
height:
|
|
223
|
-
width: var(--ty-button-action-size);
|
|
234
|
+
height: auto;
|
|
235
|
+
min-width: var(--ty-button-action-size);
|
|
236
|
+
min-height: var(--ty-button-action-size);
|
|
224
237
|
padding: 0px !important;
|
|
225
238
|
}
|
|
226
239
|
button.action ::slotted(ty-icon) {
|
|
@@ -272,18 +285,6 @@ button.pill.xs { padding-left: 1em; padding-right: 1em; }
|
|
|
272
285
|
button.pill.sm { padding-left: 1.125em; padding-right: 1.125em; }
|
|
273
286
|
button.pill.lg { padding-left: 1.5em; padding-right: 1.5em; }
|
|
274
287
|
button.pill.xl { padding-left: 1.75em; padding-right: 1.75em; }
|
|
275
|
-
button.pill:has(ty-icon:only-child),
|
|
276
|
-
button.pill:has(slot[name="start"]:only-child),
|
|
277
|
-
button.pill:has(slot[name="end"]:only-child) {
|
|
278
|
-
padding: 0;
|
|
279
|
-
aspect-ratio: 1;
|
|
280
|
-
min-width: var(--ty-size-md);
|
|
281
|
-
}
|
|
282
|
-
button.pill.xs:has(ty-icon:only-child) { min-width: 1.375rem; min-height: 1.375rem; }
|
|
283
|
-
button.pill.sm:has(ty-icon:only-child) { min-width: 1.5rem; min-height: 1.5rem; }
|
|
284
|
-
button.pill.md:has(ty-icon:only-child) { min-width: 2rem; min-height: 2rem; }
|
|
285
|
-
button.pill.lg:has(ty-icon:only-child) { min-width: 2.25rem; min-height: 2.25rem; }
|
|
286
|
-
button.pill.xl:has(ty-icon:only-child) { min-width: 2.5rem; min-height: 2.5rem; }
|
|
287
288
|
button.solid {
|
|
288
289
|
--_ring: var(--ty-color-neutral);
|
|
289
290
|
--_solid-bg: var(--ty-button-bg, var(--ty-solid-neutral));
|
|
@@ -92,7 +92,7 @@ align-items: center;
|
|
|
92
92
|
justify-content: center;
|
|
93
93
|
flex-shrink: 0;
|
|
94
94
|
margin-right: 0.5rem;
|
|
95
|
-
color: var(--ty-
|
|
95
|
+
color: var(--ty-text-soft);
|
|
96
96
|
}
|
|
97
97
|
.date-picker-stub ::slotted(ty-icon[slot="start"]) {
|
|
98
98
|
width: 1em;
|
|
@@ -134,6 +134,23 @@ border-color: var(--date-picker-accent-bold, var(--ty-date-picker-border-focus))
|
|
|
134
134
|
box-shadow: 0 0 0 3px var(--date-picker-ring, var(--ty-date-picker-shadow-focus));
|
|
135
135
|
}
|
|
136
136
|
${FLAVORS.filter((f) => f !== "neutral").map((f) => datePickerFlavor(f)).join("")}
|
|
137
|
+
.error-message {
|
|
138
|
+
font-size: var(--ty-font-xs);
|
|
139
|
+
line-height: var(--ty-leading-xs);
|
|
140
|
+
letter-spacing: var(--ty-tracking-xs);
|
|
141
|
+
color: var(--ty-color-danger);
|
|
142
|
+
margin-top: 4px;
|
|
143
|
+
padding-left: var(--ty-field-outer-pad-x);
|
|
144
|
+
}
|
|
145
|
+
.date-picker-stub.error {
|
|
146
|
+
border-color: var(--ty-color-danger);
|
|
147
|
+
background: var(--ty-bg-danger-soft);
|
|
148
|
+
}
|
|
149
|
+
.date-picker-stub.error:focus,
|
|
150
|
+
.date-picker-stub.error.open {
|
|
151
|
+
border-color: var(--ty-color-danger-bold);
|
|
152
|
+
box-shadow: 0 0 0 3px var(--ty-date-picker-shadow-focus);
|
|
153
|
+
}
|
|
137
154
|
.stub-text {
|
|
138
155
|
flex: 1;
|
|
139
156
|
white-space: nowrap;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const fileUploadStyles = "\n:host {\n display: block;\n font-family: inherit;\n width: 100%;\n}\n\n.file-upload-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n box-sizing: border-box;\n}\n\n.upload-label {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n font-weight: var(--ty-font-medium);\n color: var(--ty-label-color);\n margin-bottom: 6px;\n padding-left: 4px;\n display: flex;\n align-items: center;\n}\n\n.required-icon {\n color: var(--ty-color-danger);\n margin-left: 4px;\n font-size: 0.75rem;\n line-height: 1;\n}\n\n.drop-zone {\n border: 2px dashed var(--ty-border);\n border-radius: var(--ty-radius-base);\n padding: 2rem 1.5rem;\n text-align: center;\n cursor: pointer;\n transition: var(--ty-local-transition, border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease);\n outline: none;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n min-height: 140px;\n user-select: none;\n}\n\n.drop-zone:hover:not(.disabled) {\n border-color: var(--ty-input-border-hover);\n background: var(--ty-surface-content);\n}\n\n.drop-zone.focused {\n border-color: var(--ty-input-border-focus);\n box-shadow: 0 0 0 3px var(--ty-input-shadow-focus);\n}\n\n.drop-zone.drag-active {\n border-color: var(--ty-color-primary);\n border-style: solid;\n background: var(--ty-bg-primary-);\n}\n\n.drop-zone.drag-active .upload-icon {\n color: var(--ty-color-primary);\n transform: translateY(-2px);\n}\n\n.drop-zone.has-files {\n min-height: 80px;\n padding: 1.25rem 1.5rem;\n}\n\n.drop-zone.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.drop-zone.error {\n border-color: var(--ty-color-danger);\n}\n\n.drop-zone.error:not(.disabled) {\n background: var(--ty-bg-danger-);\n}\n\n.upload-icon {\n color: var(--ty-text-faint);\n width: 2.5rem;\n height: 2.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: var(--ty-local-transition, color 0.15s ease, transform 0.15s ease);\n pointer-events: none;\n}\n\n.upload-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.upload-hint {\n color: var(--ty-text-soft);\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n pointer-events: none;\n}\n\n.browse-link {\n color: var(--ty-color-primary);\n font-weight: var(--ty-font-medium);\n}\n\n.upload-sub-hint {\n color: var(--ty-text-faint);\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n pointer-events: none;\n}\n\n.file-list {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n margin-top: 0.5rem;\n}\n\n.file-item {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 0.75rem;\n border-radius: var(--ty-radius-base);\n background: var(--ty-surface-content);\n border: 1px solid var(--ty-border-soft);\n}\n\n.file-icon {\n color: var(--ty-text-faint);\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n display: flex;\n align-items: center;\n}\n\n.file-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.file-name {\n flex: 1;\n font-size: var(--ty-font-sm);\n color: var(--ty-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n}\n\n.file-size {\n font-size: var(--ty-font-xs);\n color: var(--ty-text-faint);\n flex-shrink: 0;\n font-variant-numeric: tabular-nums;\n}\n\n.file-remove {\n flex-shrink: 0;\n cursor: pointer;\n color: var(--ty-text-faint);\n width: 1.25rem;\n height: 1.25rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n transition: var(--ty-local-transition, color 0.15s ease, background-color 0.15s ease);\n border: none;\n background: none;\n padding: 0;\n outline: none;\n}\n\n.file-remove:hover {\n color: var(--ty-color-danger);\n background: var(--ty-bg-danger-);\n}\n\n.file-remove:focus-visible {\n box-shadow: 0 0 0 2px var(--ty-color-danger);\n}\n\n.file-remove svg {\n width: 12px;\n height: 12px;\n}\n\n.error-message {\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n color: var(--ty-color-danger);\n margin-top: 4px;\n padding-left: 4px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .drop-zone,\n .upload-icon,\n .file-remove {\n transition: none;\n }\n}\n\n@media (prefers-contrast: high) {\n .drop-zone {\n border-width: 3px;\n }\n}\n";
|
|
1
|
+
export declare const fileUploadStyles = "\n:host {\n display: block;\n font-family: inherit;\n width: 100%;\n}\n\n.file-upload-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n box-sizing: border-box;\n}\n\n.upload-label {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n font-weight: var(--ty-font-medium);\n color: var(--ty-label-color);\n margin-bottom: 6px;\n padding-left: 4px;\n display: flex;\n align-items: center;\n}\n\n.required-icon {\n color: var(--ty-color-danger);\n margin-left: 4px;\n font-size: 0.75rem;\n line-height: 1;\n}\n\n.drop-zone {\n border: 2px dashed var(--ty-border);\n border-radius: var(--ty-radius-base);\n padding: 2rem 1.5rem;\n text-align: center;\n cursor: pointer;\n transition: var(--ty-local-transition, border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease);\n outline: none;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n min-height: 140px;\n user-select: none;\n}\n\n.drop-zone:hover:not(.disabled) {\n border-color: var(--ty-input-border-hover);\n background: var(--ty-surface-content);\n}\n\n.drop-zone.focused {\n border-color: var(--ty-input-border-focus);\n box-shadow: 0 0 0 3px var(--ty-input-shadow-focus);\n}\n\n.drop-zone.drag-active {\n border-color: var(--ty-color-primary);\n border-style: solid;\n background: var(--ty-bg-primary-soft);\n}\n\n.drop-zone.drag-active .upload-icon {\n color: var(--ty-color-primary);\n transform: translateY(-2px);\n}\n\n.drop-zone.has-files {\n min-height: 80px;\n padding: 1.25rem 1.5rem;\n}\n\n.drop-zone.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.drop-zone.error {\n border-color: var(--ty-color-danger);\n}\n\n.drop-zone.error:not(.disabled) {\n background: var(--ty-bg-danger-soft);\n}\n\n.upload-icon {\n color: var(--ty-text-faint);\n width: 2.5rem;\n height: 2.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: var(--ty-local-transition, color 0.15s ease, transform 0.15s ease);\n pointer-events: none;\n}\n\n.upload-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.upload-hint {\n color: var(--ty-text-soft);\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n pointer-events: none;\n}\n\n.browse-link {\n color: var(--ty-color-primary);\n font-weight: var(--ty-font-medium);\n}\n\n.upload-sub-hint {\n color: var(--ty-text-faint);\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n pointer-events: none;\n}\n\n.file-list {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n margin-top: 0.5rem;\n}\n\n.file-item {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 0.75rem;\n border-radius: var(--ty-radius-base);\n background: var(--ty-surface-content);\n border: 1px solid var(--ty-border-soft);\n}\n\n.file-icon {\n color: var(--ty-text-faint);\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n display: flex;\n align-items: center;\n}\n\n.file-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.file-name {\n flex: 1;\n font-size: var(--ty-font-sm);\n color: var(--ty-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n}\n\n.file-size {\n font-size: var(--ty-font-xs);\n color: var(--ty-text-faint);\n flex-shrink: 0;\n font-variant-numeric: tabular-nums;\n}\n\n.file-remove {\n flex-shrink: 0;\n cursor: pointer;\n color: var(--ty-text-faint);\n width: 1.25rem;\n height: 1.25rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n transition: var(--ty-local-transition, color 0.15s ease, background-color 0.15s ease);\n border: none;\n background: none;\n padding: 0;\n outline: none;\n}\n\n.file-remove:hover {\n color: var(--ty-color-danger);\n background: var(--ty-bg-danger-soft);\n}\n\n.file-remove:focus-visible {\n box-shadow: 0 0 0 2px var(--ty-color-danger);\n}\n\n.file-remove svg {\n width: 12px;\n height: 12px;\n}\n\n.error-message {\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n color: var(--ty-color-danger);\n margin-top: 4px;\n padding-left: 4px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .drop-zone,\n .upload-icon,\n .file-remove {\n transition: none;\n }\n}\n\n@media (prefers-contrast: high) {\n .drop-zone {\n border-width: 3px;\n }\n}\n";
|
|
@@ -54,7 +54,7 @@ box-shadow: 0 0 0 3px var(--ty-input-shadow-focus);
|
|
|
54
54
|
.drop-zone.drag-active {
|
|
55
55
|
border-color: var(--ty-color-primary);
|
|
56
56
|
border-style: solid;
|
|
57
|
-
background: var(--ty-bg-primary-);
|
|
57
|
+
background: var(--ty-bg-primary-soft);
|
|
58
58
|
}
|
|
59
59
|
.drop-zone.drag-active .upload-icon {
|
|
60
60
|
color: var(--ty-color-primary);
|
|
@@ -73,7 +73,7 @@ pointer-events: none;
|
|
|
73
73
|
border-color: var(--ty-color-danger);
|
|
74
74
|
}
|
|
75
75
|
.drop-zone.error:not(.disabled) {
|
|
76
|
-
background: var(--ty-bg-danger-);
|
|
76
|
+
background: var(--ty-bg-danger-soft);
|
|
77
77
|
}
|
|
78
78
|
.upload-icon {
|
|
79
79
|
color: var(--ty-text-faint);
|
|
@@ -165,7 +165,7 @@ outline: none;
|
|
|
165
165
|
}
|
|
166
166
|
.file-remove:hover {
|
|
167
167
|
color: var(--ty-color-danger);
|
|
168
|
-
background: var(--ty-bg-danger-);
|
|
168
|
+
background: var(--ty-bg-danger-soft);
|
|
169
169
|
}
|
|
170
170
|
.file-remove:focus-visible {
|
|
171
171
|
box-shadow: 0 0 0 2px var(--ty-color-danger);
|
package/lib/styles/icon.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Icon Component Styles */
|
|
2
|
-
export declare const iconStyles = "\n:host {\n /* Display & Layout - CRITICAL: No containment to prevent paint deferral */\n /* Block-level flex, NOT inline-flex: as an inline box the icon generates a\n line box in its parent, so a shrink-wrapping wrapper (badge, .rounded-full\n chip) inherits the parent's line-height strut and renders taller than wide\n \u2014 a 12px icon in a 24px-line-height badge becomes a 22x34 oval. To place an\n icon inline with text, make the *text wrapper* inline-flex instead. */\n display: flex;\n align-items: center;\n justify-content: center;\n line-height: 0;\n \n width: var(--ty-icon-size, 1em);\n height: var(--ty-icon-size, 1em);\n \n aspect-ratio: 1 / 1;\n \n \n color: inherit;\n transition: color
|
|
2
|
+
export declare const iconStyles = "\n:host {\n /* Display & Layout - CRITICAL: No containment to prevent paint deferral */\n /* Block-level flex, NOT inline-flex: as an inline box the icon generates a\n line box in its parent, so a shrink-wrapping wrapper (badge, .rounded-full\n chip) inherits the parent's line-height strut and renders taller than wide\n \u2014 a 12px icon in a 24px-line-height badge becomes a 22x34 oval. To place an\n icon inline with text, make the *text wrapper* inline-flex instead. */\n display: flex;\n align-items: center;\n justify-content: center;\n line-height: 0;\n \n width: var(--ty-icon-size, 1em);\n height: var(--ty-icon-size, 1em);\n \n aspect-ratio: 1 / 1;\n \n \n color: inherit;\n transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n \n flex-shrink: 0;\n \n /* Visibility - Force immediate rendering */\n visibility: visible !important;\n opacity: 1;\n \n /* ANTI-FLICKER: Force browser to paint immediately */\n will-change: contents;\n \n /* Prevent any sub-pixel rendering issues */\n transform: translateZ(0);\n -webkit-font-smoothing: antialiased;\n}\n\n/* The SVG element inside shadow DOM (registry / fallback path) */\n:host svg {\n width: 100%;\n height: 100%;\n display: block;\n\n /* Force immediate SVG rendering */\n will-change: auto;\n transform: translateZ(0);\n}\n\n/* Slotted SVG (server-side path: <ty-icon><svg>...</svg></ty-icon>).\n ::slotted() targets light-DOM children so they fill the host the same way\n shadow-DOM SVG does. Color is inherited from :host via currentColor. */\n::slotted(svg),\n::slotted(img) {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n/* In a named slot the icon sits alongside text (button start/end), so inline\n layout is what the host expects there. */\n:host([slot]) {\n display: inline-flex;\n}\n\n:host(.icon-xs) {\n width: 0.75em;\n height: 0.75em;\n}\n\n:host(.icon-sm) {\n width: 0.875em;\n height: 0.875em;\n}\n\n:host(.icon-md) {\n width: 1em;\n height: 1em;\n}\n\n:host(.icon-lg) {\n width: 1.25em;\n height: 1.25em;\n}\n\n:host(.icon-xl) {\n width: 1.5em;\n height: 1.5em;\n}\n\n:host(.icon-2xl) {\n width: 2em;\n height: 2em;\n}\n\n/* Fixed pixel sizes */\n:host(.icon-12) {\n width: 12px;\n height: 12px;\n}\n\n:host(.icon-14) {\n width: 14px;\n height: 14px;\n}\n\n:host(.icon-16) {\n width: 16px;\n height: 16px;\n}\n\n:host(.icon-18) {\n width: 18px;\n height: 18px;\n}\n\n:host(.icon-20) {\n width: 20px;\n height: 20px;\n}\n\n:host(.icon-24) {\n width: 24px;\n height: 24px;\n}\n\n:host(.icon-32) {\n width: 32px;\n height: 32px;\n}\n\n:host(.icon-48) {\n width: 48px;\n height: 48px;\n}\n\n:host(.icon-64) {\n width: 64px;\n height: 64px;\n}\n\n:host(.icon-80) {\n width: 80px;\n height: 80px;\n}\n\n:host(.icon-96) {\n width: 96px;\n height: 96px;\n}\n\n:host(.icon-spin) {\n animation: icon-spin 1s linear infinite;\n}\n\n:host(.icon-spin.icon-tempo-slow) {\n animation: icon-spin 2s linear infinite;\n}\n\n:host(.icon-spin.icon-tempo-fast) {\n animation: icon-spin 0.5s linear infinite;\n}\n\n@keyframes icon-spin {\n from {\n transform: rotate(0deg);\n }\n\n to {\n transform: rotate(360deg);\n }\n}\n\n:host(.icon-pulse) {\n animation: icon-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n:host(.icon-pulse.icon-tempo-slow) {\n animation: icon-pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n:host(.icon-pulse.icon-tempo-fast) {\n animation: icon-pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n@keyframes icon-pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n\n 50% {\n opacity: 0.5;\n }\n}\n\n/* Accessibility: ensure icons are not announced by screen readers by default */\n:host {\n -webkit-user-select: none;\n user-select: none;\n}\n\n:host(:not(.icon-clickable)) {\n pointer-events: none;\n}\n\n:host(.icon-clickable) {\n cursor: pointer;\n pointer-events: auto;\n transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n:host(.icon-clickable:hover) {\n transform: scale(1.1);\n}\n\n:host(.icon-clickable:active) {\n transform: scale(0.95);\n}\n\n:host(.icon-disabled) {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n:host(.icon-block) {\n display: block;\n}\n";
|
package/lib/styles/icon.js
CHANGED
|
@@ -8,7 +8,7 @@ width: var(--ty-icon-size, 1em);
|
|
|
8
8
|
height: var(--ty-icon-size, 1em);
|
|
9
9
|
aspect-ratio: 1 / 1;
|
|
10
10
|
color: inherit;
|
|
11
|
-
transition: color
|
|
11
|
+
transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
12
12
|
flex-shrink: 0;
|
|
13
13
|
visibility: visible !important;
|
|
14
14
|
opacity: 1;
|
|
@@ -145,7 +145,7 @@ pointer-events: none;
|
|
|
145
145
|
:host(.icon-clickable) {
|
|
146
146
|
cursor: pointer;
|
|
147
147
|
pointer-events: auto;
|
|
148
|
-
transition: color
|
|
148
|
+
transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
149
149
|
}
|
|
150
150
|
:host(.icon-clickable:hover) {
|
|
151
151
|
transform: scale(1.1);
|
package/lib/styles/input.js
CHANGED
package/lib/styles/radio.js
CHANGED
|
@@ -27,13 +27,13 @@ display: flex;
|
|
|
27
27
|
flex-direction: column;
|
|
28
28
|
gap: 0.4rem;
|
|
29
29
|
font-family: inherit;
|
|
30
|
-
color: var(--ty-
|
|
30
|
+
color: var(--ty-text);
|
|
31
31
|
}
|
|
32
32
|
.radio-group-label {
|
|
33
33
|
font-size: var(--ty-font-sm);
|
|
34
34
|
line-height: var(--ty-leading-sm);
|
|
35
35
|
font-weight: var(--ty-font-medium);
|
|
36
|
-
color: var(--ty-
|
|
36
|
+
color: var(--ty-text);
|
|
37
37
|
display: flex;
|
|
38
38
|
align-items: center;
|
|
39
39
|
gap: 0.25rem;
|
|
@@ -239,6 +239,32 @@ background: transparent;
|
|
|
239
239
|
border-color: transparent;
|
|
240
240
|
box-shadow: none;
|
|
241
241
|
}
|
|
242
|
+
.error-message {
|
|
243
|
+
font-size: var(--ty-font-xs);
|
|
244
|
+
line-height: var(--ty-leading-xs);
|
|
245
|
+
letter-spacing: var(--ty-tracking-xs);
|
|
246
|
+
color: var(--ty-color-danger);
|
|
247
|
+
margin-top: 4px;
|
|
248
|
+
padding-left: var(--ty-field-outer-pad-x);
|
|
249
|
+
contain: inline-size;
|
|
250
|
+
}
|
|
251
|
+
:host([compact]) .error-message,
|
|
252
|
+
:host([has-trigger]) .error-message {
|
|
253
|
+
box-sizing: border-box;
|
|
254
|
+
width: max(100%, 20em);
|
|
255
|
+
}
|
|
256
|
+
:host([has-trigger]) .error-message {
|
|
257
|
+
padding-left: 0;
|
|
258
|
+
}
|
|
259
|
+
.select-stub.error:not(.custom-trigger) {
|
|
260
|
+
border-color: var(--ty-color-danger);
|
|
261
|
+
background: var(--ty-bg-danger-soft);
|
|
262
|
+
}
|
|
263
|
+
.select-stub.error:not(.custom-trigger):focus,
|
|
264
|
+
.select-stub.error:not(.custom-trigger):focus-visible {
|
|
265
|
+
border-color: var(--ty-color-danger-bold);
|
|
266
|
+
box-shadow: 0 0 0 3px var(--input-shadow-focus, var(--ty-input-shadow-focus));
|
|
267
|
+
}
|
|
242
268
|
.dropdown-mode-mobile .dropdown-wrapper:has(.mobile-dialog[open]) .select-chips {
|
|
243
269
|
display: none;
|
|
244
270
|
}
|
package/lib/styles/textarea.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Styles for ty-textarea component
|
|
3
3
|
* Enhanced textarea with auto-resize functionality
|
|
4
4
|
*/
|
|
5
|
-
export declare const textareaStyles = "\n\n:host {\n --ty-field-height: var(--ty-size-sm, 2rem);\n --ty-field-pad-x: 10px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-tracking: var(--ty-tracking-sm);\n --ty-field-label-font: var(--ty-font-xs);\n --ty-field-label-leading: var(--ty-leading-xs);\n --ty-field-label-gap: 5px;\n /* Text that sits OUTSIDE the field box (label, error message) has to line up\n with the value INSIDE it. The value is inset by the wrapper's 1px border\n plus --ty-field-pad-x; outside text only ever cleared the padding, so it\n started exactly 1px to the left of the value it labelled \u2014 at every rung,\n on every field. Resolved per rung: the calc reads whichever --ty-field-pad-x\n is winning on the host. */\n --ty-field-outer-pad-x: calc(var(--ty-field-pad-x) + 1px);\n --ty-field-control: 24px;\n --ty-field-icon: 16px;\n}\n\n:host([size=\"xs\"]) {\n --ty-field-height: var(--ty-size-xs, 1.75rem);\n --ty-field-pad-x: 8px;\n --ty-field-font: var(--ty-font-xs);\n --ty-field-leading: 16px;\n --ty-field-tracking: var(--ty-tracking-xs);\n --ty-field-label-font: var(--ty-font-xs);\n --ty-field-label-leading: var(--ty-leading-xs);\n --ty-field-label-gap: 4px;\n --ty-field-control: 20px;\n --ty-field-icon: 14px;\n}\n\n:host([size=\"md\"]) {\n --ty-field-height: var(--ty-size-md, 2.25rem);\n --ty-field-pad-x: 12px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-tracking: var(--ty-tracking-sm);\n --ty-field-label-font: var(--ty-font-sm);\n --ty-field-label-leading: var(--ty-leading-sm);\n --ty-field-label-gap: 6px;\n --ty-field-control: 28px;\n --ty-field-icon: 18px;\n}\n\n:host([size=\"lg\"]) {\n --ty-field-height: var(--ty-size-lg, 2.5rem);\n --ty-field-pad-x: 14px;\n --ty-field-font: var(--ty-font-base);\n --ty-field-leading: 24px;\n --ty-field-tracking: var(--ty-tracking-base);\n --ty-field-label-font: var(--ty-font-sm);\n --ty-field-label-leading: var(--ty-leading-sm);\n --ty-field-label-gap: 6px;\n --ty-field-control: 32px;\n --ty-field-icon: 20px;\n}\n\n:host([size=\"xl\"]) {\n --ty-field-height: var(--ty-size-xl, 2.75rem);\n --ty-field-pad-x: 16px;\n --ty-field-font: var(--ty-font-lg);\n --ty-field-leading: 28px;\n --ty-field-tracking: var(--ty-tracking-base);\n --ty-field-label-font: var(--ty-font-base);\n --ty-field-label-leading: var(--ty-leading-base);\n --ty-field-label-gap: 8px;\n --ty-field-control: 36px;\n --ty-field-icon: 22px;\n}\n\n/* Narrow viewports drop the two biggest rungs one step \u2014 same ladder, no\n component-specific overrides.\n\n A stepped-down rung must carry the WHOLE rung: --ty-field-control is the\n in-field button box (ty-copy's copy button, the date-picker's icon), and it\n sets the wrapper's content height. Leave it at the original rung and the\n field ends up taller than --ty-field-height \u2014 the ladder silently breaks on\n exactly one component. */\n@media (max-width: 640px) {\n :host([size=\"lg\"]) {\n --ty-field-height: var(--ty-size-md, 2.25rem);\n --ty-field-pad-x: 12px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-control: 28px;\n --ty-field-icon: 18px;\n }\n\n :host([size=\"xl\"]) {\n --ty-field-height: var(--ty-size-lg, 2.5rem);\n --ty-field-pad-x: 14px;\n --ty-field-font: var(--ty-font-base);\n --ty-field-leading: 24px;\n --ty-field-control: 32px;\n --ty-field-icon: 20px;\n }\n}\n\n@media (max-width: 480px) {\n :host([size=\"xl\"]) {\n --ty-field-height: var(--ty-size-md, 2.25rem);\n --ty-field-pad-x: 12px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-control: 28px;\n --ty-field-icon: 18px;\n }\n}\n\n:host {\n display: block;\n font-family: inherit;\n width: 100%;\n}\n\n.textarea-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n box-sizing: border-box;\n position: relative;\n /* For absolute positioned dummy element */\n}\n\n/* Bordered container \u2014 owns the border + active state (composer layout):\n header (top slot) \u00B7 textarea (borderless) \u00B7 footer (bottom slot). */\n.textarea-wrapper {\n position: relative;\n width: 100%;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n border: 1px solid var(--input-border, var(--ty-input-border));\n border-radius: var(--ty-radius-base);\n background: var(--input-bg, var(--ty-input-bg));\n transition: var(--ty-local-transition, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out);\n}\n\n/* Active ring tied to the TEXTAREA specifically (not :focus-within) so tabbing\n to a footer button doesn't make the whole field look focused. */\n.textarea-wrapper:has(textarea:focus) {\n border-color: var(--input-border-focus, var(--ty-input-border-focus));\n box-shadow: 0 0 0 3px var(--input-shadow-focus, var(--ty-input-shadow-focus));\n}\n\n/* Error + disabled reflected from the inner textarea (both in shadow DOM, so\n :has is reliable here \u2014 unlike slotted content). */\n.textarea-wrapper:has(textarea.error) {\n border-color: var(--ty-color-danger);\n background: var(--ty-bg-danger-soft);\n}\n.textarea-wrapper:has(textarea.error):has(textarea:focus) {\n border-color: var(--ty-color-danger-bold);\n}\n.textarea-wrapper:has(textarea:disabled) {\n opacity: 0.5;\n background: var(--input-disabled-bg, var(--ty-input-disabled-bg));\n border-color: var(--input-disabled-border, var(--ty-input-disabled-border));\n}\n\n/* Scroll region wraps just the textarea so the custom scrollbar never overlaps\n the header/footer. */\n.textarea-scroll {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* Custom scrollbar track positioned to the scroll region (textarea edges) */\n.textarea-scroll .ty-scrollbar-track-y {\n top: 2px;\n right: 2px;\n bottom: 2px;\n border-radius: 0 4px 4px 0;\n}\n\n/* Header / footer slot regions \u2014 zero footprint until they have content\n (the component toggles .has-content via slotchange). footer defaults to\n space-between so \"tools \u2026 submit\" works; a single wide button fills it. */\n.textarea-header,\n.textarea-footer {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n.textarea-footer { justify-content: space-between; }\n.textarea-header.has-content {\n padding: 6px 10px;\n border-bottom: 1px solid var(--ty-textarea-divider, var(--input-border, var(--ty-input-border)));\n}\n.textarea-footer.has-content {\n padding: 6px 10px;\n border-top: 1px solid var(--ty-textarea-divider, var(--input-border, var(--ty-input-border)));\n}\n\n.ty-field-label {\n font-size: var(--ty-field-label-font);\n line-height: var(--ty-field-label-leading);\n letter-spacing: var(--ty-tracking-sm);\n font-weight: var(--ty-font-medium);\n color: var(--ty-label-color);\n margin-bottom: var(--ty-field-label-gap);\n padding-left: var(--ty-field-outer-pad-x);\n}\n\n.required-icon {\n display: inline-flex;\n align-items: center;\n color: var(--ty-color-danger);\n width: 12px;\n height: 12px;\n vertical-align: middle;\n}\n\n.error-message {\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n letter-spacing: var(--ty-tracking-xs);\n color: var(--ty-color-danger);\n margin-top: 4px;\n padding-left: var(--ty-field-outer-pad-x);\n}\n\n/* Error state is reflected on .textarea-wrapper (see above). */\n\ntextarea {\n font-family: inherit;\n /* Borderless \u2014 the .textarea-wrapper owns the border/background now. */\n box-sizing: border-box;\n width: 100%;\n flex: 1 1 auto;\n border: none;\n background: transparent;\n color: var(--input-color, var(--ty-input-color));\n font-family: inherit;\n /* Linear-paired typography */\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n font-weight: var(--ty-font-normal);\n outline: none;\n\n min-height: 80px;\n padding: 12px 12px;\n\n overflow: hidden;\n /* Hide scrollbars since we're auto-resizing */\n resize: none;\n\n white-space: pre-wrap;\n word-wrap: break-word;\n}\n\n/* Hide native scrollbar for webkit when custom scrollbar is active\n (scrollbar-width: none handles Firefox; this handles Chrome/Safari) */\n:host([data-custom-scroll]) textarea::-webkit-scrollbar {\n display: none;\n}\n\n/* Disabled \u2014 visual state lives on the wrapper; just the cursor/text here. */\ntextarea:disabled {\n cursor: not-allowed;\n color: var(--input-disabled-color, var(--ty-input-disabled-color));\n}\n\ntextarea::placeholder {\n color: var(--input-placeholder, var(--ty-input-placeholder));\n font-weight: 400;\n}\n\ntextarea.resize-both {\n resize: both;\n}\n\ntextarea.resize-horizontal {\n resize: horizontal;\n}\n\ntextarea.resize-vertical {\n resize: vertical;\n}\n\ntextarea.resize-none {\n resize: none;\n}\n\n/* Auto-resize mode (default) disables manual resize */\ntextarea:not(.resize-both):not(.resize-horizontal):not(.resize-vertical) {\n resize: none;\n}\n\n.textarea-dummy {\n /* Hidden element that measures text height */\n position: absolute !important;\n top: 0 !important;\n left: 0 !important;\n visibility: hidden !important;\n white-space: pre-wrap !important;\n word-break: break-word !important;\n box-sizing: border-box !important;\n overflow: hidden !important;\n pointer-events: none !important;\n z-index: -1 !important;\n\n word-wrap: break-word !important;\n overflow-wrap: break-word !important;\n}\n\ntextarea.xs {\n min-height: 64px;\n padding: 8px var(--ty-field-pad-x);\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n letter-spacing: var(--ty-tracking-xs);\n}\n\ntextarea.sm {\n min-height: 72px;\n padding: 10px var(--ty-field-pad-x);\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n}\n\ntextarea.md {\n min-height: 80px;\n padding: 12px var(--ty-field-pad-x);\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n}\n\ntextarea.lg {\n min-height: 96px;\n padding: 14px var(--ty-field-pad-x);\n font-size: var(--ty-font-base);\n line-height: var(--ty-leading-base);\n letter-spacing: var(--ty-tracking-base);\n}\n\ntextarea.xl {\n min-height: 112px;\n padding: 16px var(--ty-field-pad-x);\n font-size: var(--ty-font-lg);\n line-height: var(--ty-leading-lg);\n letter-spacing: var(--ty-tracking-lg);\n}\n\ntextarea:focus-visible {\n outline: none;\n /* We use box-shadow instead */\n}\n\n@media (prefers-contrast: high) {\n textarea {\n border-width: 2px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n textarea {\n transition: none;\n }\n\n .textarea-dummy {\n transition: none;\n }\n}\n\n@container (max-width: 480px) {\n textarea.lg {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n\n textarea.xl {\n font-size: var(--ty-font-base);\n line-height: var(--ty-leading-base);\n letter-spacing: var(--ty-tracking-base);\n padding: 14px var(--ty-field-pad-x);\n min-height: 96px;\n }\n}\n\n@container (max-width: 320px) {\n textarea.xl {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n}\n\n/* Fallback for browsers without container query support */\n@media (max-width: 640px) {\n textarea.lg {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n\n textarea.xl {\n font-size: var(--ty-font-base);\n line-height: var(--ty-leading-base);\n letter-spacing: var(--ty-tracking-base);\n padding: 14px var(--ty-field-pad-x);\n min-height: 96px;\n }\n}\n\n@media (max-width: 480px) {\n textarea.xl {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n}\n\ntextarea {\n transition: var(--ty-local-transition,\n border-color 0.15s ease-in-out,\n box-shadow 0.15s ease-in-out,\n background-color 0.15s ease-in-out,\n height 0.1s ease-out);\n}\n\n/* Disable height transition on focus to avoid jarring effect */\ntextarea:focus {\n transition: var(--ty-local-transition,\n border-color 0.15s ease-in-out,\n box-shadow 0.15s ease-in-out,\n background-color 0.15s ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n textarea {\n transition: none;\n }\n}\n";
|
|
5
|
+
export declare const textareaStyles = "\n\n:host {\n --ty-field-height: var(--ty-size-sm, 2rem);\n --ty-field-pad-x: 10px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-tracking: var(--ty-tracking-sm);\n --ty-field-label-font: var(--ty-font-xs);\n --ty-field-label-leading: var(--ty-leading-xs);\n --ty-field-label-gap: 5px;\n /* Text that sits OUTSIDE the field box (label, error message) has to line up\n with the value INSIDE it. The value is inset by the wrapper's 1px border\n plus --ty-field-pad-x; outside text only ever cleared the padding, so it\n started exactly 1px to the left of the value it labelled \u2014 at every rung,\n on every field. Resolved per rung: the calc reads whichever --ty-field-pad-x\n is winning on the host. */\n --ty-field-outer-pad-x: calc(var(--ty-field-pad-x) + 1px);\n --ty-field-control: 24px;\n --ty-field-icon: 16px;\n}\n\n:host([size=\"xs\"]) {\n --ty-field-height: var(--ty-size-xs, 1.75rem);\n --ty-field-pad-x: 8px;\n --ty-field-font: var(--ty-font-xs);\n --ty-field-leading: 16px;\n --ty-field-tracking: var(--ty-tracking-xs);\n --ty-field-label-font: var(--ty-font-xs);\n --ty-field-label-leading: var(--ty-leading-xs);\n --ty-field-label-gap: 4px;\n --ty-field-control: 20px;\n --ty-field-icon: 14px;\n}\n\n:host([size=\"md\"]) {\n --ty-field-height: var(--ty-size-md, 2.25rem);\n --ty-field-pad-x: 12px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-tracking: var(--ty-tracking-sm);\n --ty-field-label-font: var(--ty-font-sm);\n --ty-field-label-leading: var(--ty-leading-sm);\n --ty-field-label-gap: 6px;\n --ty-field-control: 28px;\n --ty-field-icon: 18px;\n}\n\n:host([size=\"lg\"]) {\n --ty-field-height: var(--ty-size-lg, 2.5rem);\n --ty-field-pad-x: 14px;\n --ty-field-font: var(--ty-font-base);\n --ty-field-leading: 24px;\n --ty-field-tracking: var(--ty-tracking-base);\n --ty-field-label-font: var(--ty-font-sm);\n --ty-field-label-leading: var(--ty-leading-sm);\n --ty-field-label-gap: 6px;\n --ty-field-control: 32px;\n --ty-field-icon: 20px;\n}\n\n:host([size=\"xl\"]) {\n --ty-field-height: var(--ty-size-xl, 2.75rem);\n --ty-field-pad-x: 16px;\n --ty-field-font: var(--ty-font-lg);\n --ty-field-leading: 28px;\n --ty-field-tracking: var(--ty-tracking-base);\n --ty-field-label-font: var(--ty-font-base);\n --ty-field-label-leading: var(--ty-leading-base);\n --ty-field-label-gap: 8px;\n --ty-field-control: 36px;\n --ty-field-icon: 22px;\n}\n\n/* Narrow viewports drop the two biggest rungs one step \u2014 same ladder, no\n component-specific overrides.\n\n A stepped-down rung must carry the WHOLE rung: --ty-field-control is the\n in-field button box (ty-copy's copy button, the date-picker's icon), and it\n sets the wrapper's content height. Leave it at the original rung and the\n field ends up taller than --ty-field-height \u2014 the ladder silently breaks on\n exactly one component. */\n@media (max-width: 640px) {\n :host([size=\"lg\"]) {\n --ty-field-height: var(--ty-size-md, 2.25rem);\n --ty-field-pad-x: 12px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-control: 28px;\n --ty-field-icon: 18px;\n }\n\n :host([size=\"xl\"]) {\n --ty-field-height: var(--ty-size-lg, 2.5rem);\n --ty-field-pad-x: 14px;\n --ty-field-font: var(--ty-font-base);\n --ty-field-leading: 24px;\n --ty-field-control: 32px;\n --ty-field-icon: 20px;\n }\n}\n\n@media (max-width: 480px) {\n :host([size=\"xl\"]) {\n --ty-field-height: var(--ty-size-md, 2.25rem);\n --ty-field-pad-x: 12px;\n --ty-field-font: var(--ty-font-sm);\n --ty-field-leading: 20px;\n --ty-field-control: 28px;\n --ty-field-icon: 18px;\n }\n}\n\n:host {\n display: block;\n font-family: inherit;\n width: 100%;\n}\n\n.textarea-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n box-sizing: border-box;\n position: relative;\n /* For absolute positioned dummy element */\n}\n\n/* Bordered container \u2014 owns the border + active state (composer layout):\n header (top slot) \u00B7 textarea (borderless) \u00B7 footer (bottom slot). */\n.textarea-wrapper {\n position: relative;\n width: 100%;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n border: 1px solid var(--input-border, var(--ty-input-border));\n border-radius: var(--ty-radius-base);\n background: var(--input-bg, var(--ty-input-bg));\n transition: var(--ty-local-transition, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out);\n}\n\n/* Active ring tied to the TEXTAREA specifically (not :focus-within) so tabbing\n to a footer button doesn't make the whole field look focused. */\n.textarea-wrapper:has(textarea:focus) {\n border-color: var(--input-border-focus, var(--ty-input-border-focus));\n box-shadow: 0 0 0 3px var(--input-shadow-focus, var(--ty-input-shadow-focus));\n}\n\n/* Error + disabled reflected from the inner textarea (both in shadow DOM, so\n :has is reliable here \u2014 unlike slotted content). */\n.textarea-wrapper:has(textarea.error) {\n border-color: var(--ty-color-danger);\n background: var(--ty-bg-danger-soft);\n}\n.textarea-wrapper:has(textarea.error):has(textarea:focus) {\n border-color: var(--ty-color-danger-bold);\n}\n.textarea-wrapper:has(textarea:disabled) {\n opacity: 0.5;\n background: var(--input-disabled-bg, var(--ty-input-disabled-bg));\n border-color: var(--input-disabled-border, var(--ty-input-disabled-border));\n}\n\n/* Scroll region wraps just the textarea so the custom scrollbar never overlaps\n the header/footer. */\n.textarea-scroll {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* Custom scrollbar track positioned to the scroll region (textarea edges) */\n.textarea-scroll .ty-scrollbar-track-y {\n top: 2px;\n right: 2px;\n bottom: 2px;\n border-radius: 0 4px 4px 0;\n}\n\n/* Header / footer slot regions \u2014 zero footprint until they have content\n (the component toggles .has-content via slotchange). footer defaults to\n space-between so \"tools \u2026 submit\" works; a single wide button fills it. */\n.textarea-header,\n.textarea-footer {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n.textarea-footer { justify-content: space-between; }\n.textarea-header.has-content {\n padding: 6px 10px;\n border-bottom: 1px solid var(--ty-textarea-divider, var(--input-border, var(--ty-input-border)));\n}\n.textarea-footer.has-content {\n padding: 6px 10px;\n border-top: 1px solid var(--ty-textarea-divider, var(--input-border, var(--ty-input-border)));\n}\n\n.ty-field-label {\n font-size: var(--ty-field-label-font);\n line-height: var(--ty-field-label-leading);\n letter-spacing: var(--ty-tracking-sm);\n font-weight: var(--ty-font-medium);\n color: var(--ty-label-color);\n margin-bottom: var(--ty-field-label-gap);\n padding-left: var(--ty-field-outer-pad-x);\n display: flex;\n align-items: center;\n}\n\n/* Required marker \u2014 same as ty-input's (styles/input.ts). */\n.required-icon {\n display: inline-flex;\n align-items: center;\n color: var(--ty-color-danger);\n width: 12px;\n height: 12px;\n margin-left: 4px;\n vertical-align: middle;\n}\n\n.required-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.error-message {\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n letter-spacing: var(--ty-tracking-xs);\n color: var(--ty-color-danger);\n margin-top: 4px;\n padding-left: var(--ty-field-outer-pad-x);\n}\n\n/* Error state is reflected on .textarea-wrapper (see above). */\n\ntextarea {\n font-family: inherit;\n /* Borderless \u2014 the .textarea-wrapper owns the border/background now. */\n box-sizing: border-box;\n width: 100%;\n flex: 1 1 auto;\n border: none;\n background: transparent;\n color: var(--input-color, var(--ty-input-color));\n font-family: inherit;\n /* Linear-paired typography */\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n font-weight: var(--ty-font-normal);\n outline: none;\n\n min-height: 80px;\n padding: 12px 12px;\n\n overflow: hidden;\n /* Hide scrollbars since we're auto-resizing */\n resize: none;\n\n white-space: pre-wrap;\n word-wrap: break-word;\n}\n\n/* Hide native scrollbar for webkit when custom scrollbar is active\n (scrollbar-width: none handles Firefox; this handles Chrome/Safari) */\n:host([data-custom-scroll]) textarea::-webkit-scrollbar {\n display: none;\n}\n\n/* Disabled \u2014 visual state lives on the wrapper; just the cursor/text here. */\ntextarea:disabled {\n cursor: not-allowed;\n color: var(--input-disabled-color, var(--ty-input-disabled-color));\n}\n\ntextarea::placeholder {\n color: var(--input-placeholder, var(--ty-input-placeholder));\n font-weight: 400;\n}\n\ntextarea.resize-both {\n resize: both;\n}\n\ntextarea.resize-horizontal {\n resize: horizontal;\n}\n\ntextarea.resize-vertical {\n resize: vertical;\n}\n\ntextarea.resize-none {\n resize: none;\n}\n\n/* Auto-resize mode (default) disables manual resize */\ntextarea:not(.resize-both):not(.resize-horizontal):not(.resize-vertical) {\n resize: none;\n}\n\n.textarea-dummy {\n /* Hidden element that measures text height */\n position: absolute !important;\n top: 0 !important;\n left: 0 !important;\n visibility: hidden !important;\n white-space: pre-wrap !important;\n word-break: break-word !important;\n box-sizing: border-box !important;\n overflow: hidden !important;\n pointer-events: none !important;\n z-index: -1 !important;\n\n word-wrap: break-word !important;\n overflow-wrap: break-word !important;\n}\n\ntextarea.xs {\n min-height: 64px;\n padding: 8px var(--ty-field-pad-x);\n font-size: var(--ty-font-xs);\n line-height: var(--ty-leading-xs);\n letter-spacing: var(--ty-tracking-xs);\n}\n\ntextarea.sm {\n min-height: 72px;\n padding: 10px var(--ty-field-pad-x);\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n}\n\ntextarea.md {\n min-height: 80px;\n padding: 12px var(--ty-field-pad-x);\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n}\n\ntextarea.lg {\n min-height: 96px;\n padding: 14px var(--ty-field-pad-x);\n font-size: var(--ty-font-base);\n line-height: var(--ty-leading-base);\n letter-spacing: var(--ty-tracking-base);\n}\n\ntextarea.xl {\n min-height: 112px;\n padding: 16px var(--ty-field-pad-x);\n font-size: var(--ty-font-lg);\n line-height: var(--ty-leading-lg);\n letter-spacing: var(--ty-tracking-lg);\n}\n\ntextarea:focus-visible {\n outline: none;\n /* We use box-shadow instead */\n}\n\n@media (prefers-contrast: high) {\n textarea {\n border-width: 2px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n textarea {\n transition: none;\n }\n\n .textarea-dummy {\n transition: none;\n }\n}\n\n@container (max-width: 480px) {\n textarea.lg {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n\n textarea.xl {\n font-size: var(--ty-font-base);\n line-height: var(--ty-leading-base);\n letter-spacing: var(--ty-tracking-base);\n padding: 14px var(--ty-field-pad-x);\n min-height: 96px;\n }\n}\n\n@container (max-width: 320px) {\n textarea.xl {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n}\n\n/* Fallback for browsers without container query support */\n@media (max-width: 640px) {\n textarea.lg {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n\n textarea.xl {\n font-size: var(--ty-font-base);\n line-height: var(--ty-leading-base);\n letter-spacing: var(--ty-tracking-base);\n padding: 14px var(--ty-field-pad-x);\n min-height: 96px;\n }\n}\n\n@media (max-width: 480px) {\n textarea.xl {\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: 12px var(--ty-field-pad-x);\n min-height: 80px;\n }\n}\n\ntextarea {\n transition: var(--ty-local-transition,\n border-color 0.15s ease-in-out,\n box-shadow 0.15s ease-in-out,\n background-color 0.15s ease-in-out,\n height 0.1s ease-out);\n}\n\n/* Disable height transition on focus to avoid jarring effect */\ntextarea:focus {\n transition: var(--ty-local-transition,\n border-color 0.15s ease-in-out,\n box-shadow 0.15s ease-in-out,\n background-color 0.15s ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n textarea {\n transition: none;\n }\n}\n";
|
package/lib/styles/textarea.js
CHANGED
|
@@ -75,6 +75,8 @@ font-weight: var(--ty-font-medium);
|
|
|
75
75
|
color: var(--ty-label-color);
|
|
76
76
|
margin-bottom: var(--ty-field-label-gap);
|
|
77
77
|
padding-left: var(--ty-field-outer-pad-x);
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
78
80
|
}
|
|
79
81
|
.required-icon {
|
|
80
82
|
display: inline-flex;
|
|
@@ -82,8 +84,13 @@ align-items: center;
|
|
|
82
84
|
color: var(--ty-color-danger);
|
|
83
85
|
width: 12px;
|
|
84
86
|
height: 12px;
|
|
87
|
+
margin-left: 4px;
|
|
85
88
|
vertical-align: middle;
|
|
86
89
|
}
|
|
90
|
+
.required-icon svg {
|
|
91
|
+
width: 100%;
|
|
92
|
+
height: 100%;
|
|
93
|
+
}
|
|
87
94
|
.error-message {
|
|
88
95
|
font-size: var(--ty-font-xs);
|
|
89
96
|
line-height: var(--ty-leading-xs);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `error` shows a neutral ty-input, ty-select or ty-date-picker as danger by
|
|
3
|
+
* writing flavor="danger". The flavor attribute it replaced ('' for none) waits
|
|
4
|
+
* in this attribute until the error clears and it goes back. On the element,
|
|
5
|
+
* not in a field: a copy of it (cloneNode, innerHTML, a page snapshot) carries
|
|
6
|
+
* that danger as a plain attribute and must still tell it from the author's.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ERROR_FLAVOR = "data-error-flavor";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ERROR_FLAVOR = 'data-error-flavor';
|
|
@@ -29,6 +29,12 @@ export interface PropertyChange {
|
|
|
29
29
|
/**
|
|
30
30
|
* Manages component properties with unified lifecycle
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* Boolean coercion for a property set from JS, HTML-style: '' (what React 19
|
|
34
|
+
* and Vue hand a property for a bare attribute) is true; false, null,
|
|
35
|
+
* undefined, 0, 'false' and '0' are false.
|
|
36
|
+
*/
|
|
37
|
+
export declare function coerceBoolean(value: unknown): boolean;
|
|
32
38
|
export declare class PropertyManager<T = any> {
|
|
33
39
|
private _props;
|
|
34
40
|
private _config;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export function coerceBoolean(value) {
|
|
2
|
+
if (typeof value === 'string') {
|
|
3
|
+
const normalized = value.toLowerCase().trim();
|
|
4
|
+
return normalized !== 'false' && normalized !== '0';
|
|
5
|
+
}
|
|
6
|
+
return Boolean(value);
|
|
7
|
+
}
|
|
8
|
+
function defaultOf(config) {
|
|
9
|
+
return typeof config.default === 'function' ? config.default() : config.default;
|
|
10
|
+
}
|
|
1
11
|
export class PropertyManager {
|
|
2
12
|
constructor(config) {
|
|
3
13
|
this._props = new Map();
|
|
@@ -7,7 +17,7 @@ this._initializeDefaults();
|
|
|
7
17
|
_initializeDefaults() {
|
|
8
18
|
for (const [name, config] of Object.entries(this._config)) {
|
|
9
19
|
if (config.default !== undefined) {
|
|
10
|
-
this._props.set(name, config
|
|
20
|
+
this._props.set(name, defaultOf(config));
|
|
11
21
|
}
|
|
12
22
|
}
|
|
13
23
|
}
|
|
@@ -43,7 +53,7 @@ if (config.coerce) {
|
|
|
43
53
|
return config.coerce(value);
|
|
44
54
|
}
|
|
45
55
|
if (value === null || value === undefined) {
|
|
46
|
-
return config
|
|
56
|
+
return defaultOf(config) ?? null;
|
|
47
57
|
}
|
|
48
58
|
switch (config.type) {
|
|
49
59
|
case 'boolean':
|
|
@@ -58,7 +68,7 @@ return true;
|
|
|
58
68
|
return Boolean(value);
|
|
59
69
|
case 'number':
|
|
60
70
|
const num = Number(value);
|
|
61
|
-
return isNaN(num) ? (config
|
|
71
|
+
return isNaN(num) ? (defaultOf(config) ?? 0) : num;
|
|
62
72
|
case 'string':
|
|
63
73
|
return String(value);
|
|
64
74
|
case 'object':
|
|
@@ -69,7 +79,7 @@ return JSON.parse(value);
|
|
|
69
79
|
}
|
|
70
80
|
catch {
|
|
71
81
|
console.warn(`[PropertyManager] Failed to parse ${name}:`, value);
|
|
72
|
-
return config
|
|
82
|
+
return defaultOf(config) ?? (config.type === 'array' ? [] : {});
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
return value;
|
|
@@ -119,7 +129,7 @@ handleAlias(aliasName, value) {
|
|
|
119
129
|
for (const [propName, config] of Object.entries(this._config)) {
|
|
120
130
|
if (config.aliases && aliasName in config.aliases) {
|
|
121
131
|
if (value === null)
|
|
122
|
-
return this.updateProperty(propName, config
|
|
132
|
+
return this.updateProperty(propName, defaultOf(config), 'attribute');
|
|
123
133
|
const aliasedValue = config.aliases[aliasName];
|
|
124
134
|
return this.updateProperty(propName, aliasedValue, 'attribute');
|
|
125
135
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For a component whose attributeChangedCallback assigns through its own
|
|
3
|
+
* accessors: call it in the constructor. While an element upgrades, the
|
|
4
|
+
* attribute callbacks run before connectedCallback and would overwrite the
|
|
5
|
+
* early values (an own property takes the assignment); taken off here, they
|
|
6
|
+
* reach the setters in upgradeProperties() instead, after the attributes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function liftProperties(el: HTMLElement): void;
|
|
9
|
+
/**
|
|
10
|
+
* Properties set before the element upgraded (a framework rendering it
|
|
11
|
+
* before its definition loaded, a lazily defined element) are own data
|
|
12
|
+
* properties that shadow the class accessors, so later assignments would
|
|
13
|
+
* never reach a setter. Standard "upgrade property" pattern: read the own
|
|
14
|
+
* value, delete it, assign it again through the accessor. Covers every
|
|
15
|
+
* settable accessor on the prototype chain (value, checked, open, error,
|
|
16
|
+
* minLength, …). TyComponent does this itself; a component extending
|
|
17
|
+
* HTMLElement directly calls it at the start of connectedCallback.
|
|
18
|
+
* A value its setter rejects is reported, not thrown: connectedCallback
|
|
19
|
+
* goes on, as it would for an attribute with a bad value.
|
|
20
|
+
*/
|
|
21
|
+
export declare function upgradeProperties(el: HTMLElement): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const accessorCache = new WeakMap();
|
|
2
|
+
function settableAccessors(ctor) {
|
|
3
|
+
let names = accessorCache.get(ctor);
|
|
4
|
+
if (!names) {
|
|
5
|
+
const set = new Set();
|
|
6
|
+
for (let proto = ctor.prototype; proto && proto !== HTMLElement.prototype; proto = Object.getPrototypeOf(proto)) {
|
|
7
|
+
for (const [name, d] of Object.entries(Object.getOwnPropertyDescriptors(proto))) {
|
|
8
|
+
if (d.set)
|
|
9
|
+
set.add(name);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
names = [...set];
|
|
13
|
+
accessorCache.set(ctor, names);
|
|
14
|
+
}
|
|
15
|
+
return names;
|
|
16
|
+
}
|
|
17
|
+
const lifted = new WeakMap();
|
|
18
|
+
function takeOwn(el) {
|
|
19
|
+
const taken = [];
|
|
20
|
+
for (const name of settableAccessors(el.constructor)) {
|
|
21
|
+
if (!Object.prototype.hasOwnProperty.call(el, name))
|
|
22
|
+
continue;
|
|
23
|
+
taken.push([name, el[name]]);
|
|
24
|
+
delete el[name];
|
|
25
|
+
}
|
|
26
|
+
return taken;
|
|
27
|
+
}
|
|
28
|
+
export function liftProperties(el) {
|
|
29
|
+
const taken = takeOwn(el);
|
|
30
|
+
if (taken.length)
|
|
31
|
+
lifted.set(el, taken);
|
|
32
|
+
}
|
|
33
|
+
export function upgradeProperties(el) {
|
|
34
|
+
const early = [...(lifted.get(el) ?? []), ...takeOwn(el)];
|
|
35
|
+
lifted.delete(el);
|
|
36
|
+
for (const [name, value] of early) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
try {
|
|
40
|
+
;
|
|
41
|
+
el[name] = value;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
reportError(e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/lib/utils/validity.d.ts
CHANGED
|
@@ -9,18 +9,88 @@
|
|
|
9
9
|
* reportValidity() uses the component's own `error` display when it has one
|
|
10
10
|
* (ty-input, ty-textarea, ty-radio-group, ty-file-upload) instead of the
|
|
11
11
|
* browser's bubble, and clears that message again once the field is valid.
|
|
12
|
+
* Whenever the browser fires `invalid` on such a field — a submit attempt,
|
|
13
|
+
* form.reportValidity(), or checkValidity() on the field or its form — the
|
|
14
|
+
* message goes into that error line (no bubble), so a plain <form> needs no
|
|
15
|
+
* JS. For a check that shows nothing, read `field.validity.valid` or
|
|
16
|
+
* `form.matches(':invalid')`. A submit (a submitter click, Enter in a
|
|
17
|
+
* single-line field, form.requestSubmit()) and form.reportValidity() also
|
|
18
|
+
* move focus to the first failing field — the first in form order, like a
|
|
19
|
+
* native form, also when a later field shows a bubble. checkValidity() never
|
|
20
|
+
* does. An `error` the author set is never overwritten or removed;
|
|
21
|
+
* form.reset() clears every message the controller wrote.
|
|
12
22
|
*/
|
|
13
23
|
export declare class ValidityController {
|
|
14
24
|
private host;
|
|
15
25
|
private internals;
|
|
16
26
|
private own;
|
|
17
27
|
private custom;
|
|
18
|
-
/**
|
|
19
|
-
private
|
|
28
|
+
/** The message this controller last wrote into `error`. */
|
|
29
|
+
private written;
|
|
20
30
|
constructor(host: HTMLElement, internals: ElementInternals);
|
|
31
|
+
/**
|
|
32
|
+
* Show the current message, if any, in the error display (on blur after an
|
|
33
|
+
* edit). During a pointer interaction it waits until after the click.
|
|
34
|
+
*/
|
|
35
|
+
reveal(): void;
|
|
36
|
+
/**
|
|
37
|
+
* `error` still holds what this controller wrote (a reportValidity(), a
|
|
38
|
+
* submit, a blur reveal), so it's ours to update and clear. Once the author
|
|
39
|
+
* sets their own message, it's theirs.
|
|
40
|
+
*/
|
|
41
|
+
private get errorFromReport();
|
|
42
|
+
private writeError;
|
|
43
|
+
/** Into `error`, unless the author's own message is there. */
|
|
44
|
+
private showError;
|
|
45
|
+
/**
|
|
46
|
+
* form.reset(): drop the message this controller showed and any blur
|
|
47
|
+
* reveal still waiting, so the field looks untouched again. An `error` the
|
|
48
|
+
* author set stays.
|
|
49
|
+
*/
|
|
50
|
+
reset(): void;
|
|
21
51
|
/** The component's own validity (replaces the previous one). */
|
|
22
52
|
set(flags: ValidityStateFlags, message?: string, anchor?: HTMLElement): void;
|
|
23
53
|
setCustom(message: string): void;
|
|
24
54
|
report(): boolean;
|
|
25
55
|
private sync;
|
|
26
56
|
}
|
|
57
|
+
type ProbeTag = 'input' | 'textarea' | 'select';
|
|
58
|
+
/**
|
|
59
|
+
* The browser's own (localized) message for a rule: a detached native control
|
|
60
|
+
* gets the same attributes and value and reports what it would say. `true`
|
|
61
|
+
* sets a boolean attribute; '', false and null leave the attribute off. One
|
|
62
|
+
* probe per tag, never in the document, stripped of every attribute before
|
|
63
|
+
* each use. Works for required and min/max; length rules only report after a
|
|
64
|
+
* user edit, so a probe can't word them (see lengthValidity()).
|
|
65
|
+
*/
|
|
66
|
+
export declare function nativeMessage(tag: ProbeTag, attrs: Record<string, string | boolean | null | undefined>, value?: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* nativeMessage() plus the probe's own flags, for rules whose attribute the
|
|
69
|
+
* browser parses (min/max: ' 5 ' or '0x10' is no rule at all), so the flag
|
|
70
|
+
* and its message can't disagree. Read `validity` right away: the probe is
|
|
71
|
+
* reused by the next call.
|
|
72
|
+
*/
|
|
73
|
+
export declare function nativeValidity(tag: ProbeTag, attrs: Record<string, string | boolean | null | undefined>, value?: string): {
|
|
74
|
+
validity: ValidityState;
|
|
75
|
+
message: string;
|
|
76
|
+
};
|
|
77
|
+
/** Non-negative integer limit from an attribute string, or null when unset/invalid. */
|
|
78
|
+
export declare function toLimit(v: unknown): number | null;
|
|
79
|
+
/**
|
|
80
|
+
* minlength / maxlength for a text field (ty-input, ty-textarea): the failing
|
|
81
|
+
* flags with their messages. `control` is the inner native control carrying
|
|
82
|
+
* `minlength`; it only flags (and words, localized) a value the user typed,
|
|
83
|
+
* so its message is used then. Otherwise Ty's wording, and the author's
|
|
84
|
+
* too-short / too-long message always wins. Placeholders: {minlength}
|
|
85
|
+
* {maxlength} {length}.
|
|
86
|
+
*/
|
|
87
|
+
export declare function lengthValidity(value: string, rules: {
|
|
88
|
+
minlength: unknown;
|
|
89
|
+
maxlength: unknown;
|
|
90
|
+
tooShortMessage: string;
|
|
91
|
+
tooLongMessage: string;
|
|
92
|
+
}, control?: HTMLInputElement | HTMLTextAreaElement | null): {
|
|
93
|
+
tooShort?: string;
|
|
94
|
+
tooLong?: string;
|
|
95
|
+
};
|
|
96
|
+
export {};
|