tyrell-components 1.0.0-TC46 → 1.0.0-TC48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/css/tyrell-theme.css +167 -178
- package/css/tyrell.css +19 -0
- package/dist/tyrell-theme.css +167 -178
- package/dist/tyrell.css +19 -0
- package/dist/tyrell.js +1 -1
- package/lib/base/ty-component.d.ts +8 -3
- package/lib/base/ty-component.js +13 -5
- package/lib/components/date-picker.d.ts +8 -0
- package/lib/components/date-picker.js +13 -9
- package/lib/components/file-upload.d.ts +6 -0
- package/lib/components/file-upload.js +19 -4
- package/lib/components/input.js +1 -0
- package/lib/components/modal.d.ts +8 -0
- package/lib/components/modal.js +29 -6
- package/lib/components/popup.js +6 -10
- package/lib/components/radio.js +10 -2
- package/lib/components/resize-observer.d.ts +1 -0
- package/lib/components/resize-observer.js +12 -1
- package/lib/components/select.d.ts +11 -3
- package/lib/components/select.js +26 -9
- package/lib/components/selected-tags.js +1 -1
- package/lib/components/tabs.d.ts +2 -0
- package/lib/components/tabs.js +18 -1
- package/lib/components/tooltip.js +24 -6
- package/lib/components/wizard.d.ts +2 -0
- package/lib/components/wizard.js +15 -0
- package/lib/styles/button.js +9 -2
- package/lib/styles/calendar-month.js +1 -1
- package/lib/styles/calendar-navigation.d.ts +1 -1
- package/lib/styles/calendar-navigation.js +1 -1
- package/lib/styles/calendar.d.ts +1 -1
- package/lib/styles/calendar.js +1 -1
- package/lib/styles/date-picker.js +30 -4
- package/lib/styles/file-upload.d.ts +1 -1
- package/lib/styles/file-upload.js +1 -1
- package/lib/styles/icon.d.ts +1 -1
- package/lib/styles/icon.js +12 -1
- package/lib/styles/input.js +2 -1
- package/lib/styles/modal.d.ts +1 -1
- package/lib/styles/modal.js +18 -0
- package/lib/styles/option.d.ts +1 -1
- package/lib/styles/option.js +16 -1
- package/lib/styles/popup.d.ts +1 -1
- package/lib/styles/popup.js +22 -5
- package/lib/styles/radio.js +1 -1
- package/lib/styles/select-base.js +59 -9
- package/lib/styles/select.d.ts +1 -1
- package/lib/styles/select.js +5 -1
- package/lib/styles/tabs.d.ts +1 -1
- package/lib/styles/tabs.js +6 -1
- package/lib/styles/textarea.d.ts +1 -1
- package/lib/styles/textarea.js +2 -1
- package/lib/styles/wizard.d.ts +1 -1
- package/lib/styles/wizard.js +3 -3
- package/lib/utils/number-format.d.ts +6 -1
- package/lib/utils/number-format.js +9 -2
- package/lib/utils/positioning.d.ts +50 -5
- package/lib/utils/positioning.js +52 -8
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
|
@@ -31,7 +31,7 @@ export const selectBaseStyles = `
|
|
|
31
31
|
:host {
|
|
32
32
|
display: block;
|
|
33
33
|
width: 100%;
|
|
34
|
-
font-family:
|
|
34
|
+
font-family: inherit;
|
|
35
35
|
--mobile-border-color: var(--ty-border, #5858587d);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -57,7 +57,17 @@ export const selectBaseStyles = `
|
|
|
57
57
|
/* Modal handles z-index automatically */
|
|
58
58
|
|
|
59
59
|
opacity: 0;
|
|
60
|
-
|
|
60
|
+
/* Panel-level entrance: individual scale/translate compose with the
|
|
61
|
+
collision-shift transform below. The single-select skin flattens the
|
|
62
|
+
inner .dropdown-options animation (select.ts: "panel appears as one"),
|
|
63
|
+
so the dialog carries the motion for both skins. */
|
|
64
|
+
scale: 0.96;
|
|
65
|
+
translate: 0 -6px;
|
|
66
|
+
transform-origin: top center;
|
|
67
|
+
transition:
|
|
68
|
+
opacity var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1)),
|
|
69
|
+
scale var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1)),
|
|
70
|
+
translate var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
61
71
|
|
|
62
72
|
transform: translate(var(--dropdown-offset-x, 0px), var(--dropdown-offset-y, 0px));
|
|
63
73
|
top: -1000px;
|
|
@@ -80,6 +90,8 @@ export const selectBaseStyles = `
|
|
|
80
90
|
bottom: var(--dropdown-y);
|
|
81
91
|
top: auto;
|
|
82
92
|
flex-direction: column-reverse;
|
|
93
|
+
translate: 0 6px;
|
|
94
|
+
transform-origin: bottom center;
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
.dropdown-dialog.position-above .dropdown-header {
|
|
@@ -94,8 +106,15 @@ export const selectBaseStyles = `
|
|
|
94
106
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1), var(--ty-shadow-md);
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
.dropdown-dialog.position-above .dropdown-options {
|
|
110
|
+
transform: translateY(6px) scale(0.96);
|
|
111
|
+
transform-origin: bottom center;
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
.dropdown-dialog.open {
|
|
98
115
|
opacity: 1;
|
|
116
|
+
scale: 1;
|
|
117
|
+
translate: 0 0;
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
.dropdown-dialog.open .dropdown-options {
|
|
@@ -103,6 +122,31 @@ export const selectBaseStyles = `
|
|
|
103
122
|
transform: translateY(0) scale(1);
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
/* Transitions never start on an element that was display:none last frame —
|
|
126
|
+
showModal() + .open land in the same frame, so the enter transition was
|
|
127
|
+
silently skipped. @starting-style supplies the first-frame "from" values
|
|
128
|
+
(this is the platform mechanism for animating dialog/top-layer entry). */
|
|
129
|
+
@starting-style {
|
|
130
|
+
.dropdown-dialog.open {
|
|
131
|
+
opacity: 0;
|
|
132
|
+
scale: 0.96;
|
|
133
|
+
translate: 0 -6px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.dropdown-dialog.open.position-above {
|
|
137
|
+
translate: 0 6px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.dropdown-dialog.open .dropdown-options {
|
|
141
|
+
opacity: 0;
|
|
142
|
+
transform: translateY(-6px) scale(0.96);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.dropdown-dialog.open.position-above .dropdown-options {
|
|
146
|
+
transform: translateY(6px) scale(0.96);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
106
150
|
.dropdown-dialog::backdrop {
|
|
107
151
|
background: transparent;
|
|
108
152
|
}
|
|
@@ -115,6 +159,7 @@ export const selectBaseStyles = `
|
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
.dropdown-search-input {
|
|
162
|
+
font-family: inherit;
|
|
118
163
|
width: 100%;
|
|
119
164
|
min-width: 0;
|
|
120
165
|
box-sizing: border-box;
|
|
@@ -122,7 +167,7 @@ export const selectBaseStyles = `
|
|
|
122
167
|
color: var(--input-color, var(--ty-input-color));
|
|
123
168
|
border: 1px solid var(--input-border, var(--ty-input-border));
|
|
124
169
|
border-radius: var(--ty-radius-md);
|
|
125
|
-
font-family:
|
|
170
|
+
font-family: inherit;
|
|
126
171
|
font-size: var(--ty-font-sm);
|
|
127
172
|
line-height: var(--ty-leading-sm);
|
|
128
173
|
letter-spacing: var(--ty-tracking-sm);
|
|
@@ -184,7 +229,7 @@ export const selectBaseStyles = `
|
|
|
184
229
|
color: var(--input-color, var(--ty-input-color));
|
|
185
230
|
border: 1px solid var(--select-accent, var(--input-border, var(--ty-input-border)));
|
|
186
231
|
border-radius: var(--ty-radius-md);
|
|
187
|
-
font-family:
|
|
232
|
+
font-family: inherit;
|
|
188
233
|
font-size: var(--ty-font-sm);
|
|
189
234
|
line-height: var(--ty-leading-sm);
|
|
190
235
|
letter-spacing: var(--ty-tracking-sm);
|
|
@@ -326,8 +371,11 @@ export const selectBaseStyles = `
|
|
|
326
371
|
min-width: 0;
|
|
327
372
|
}
|
|
328
373
|
|
|
374
|
+
/* No flex-grow here — the placeholder only ever renders inside .select-stub,
|
|
375
|
+
and select.ts sets flex: 1 1 0% on it. A bare flex-grow: 1 (implicit
|
|
376
|
+
flex-basis:auto) is the wrap bug that fix exists to prevent; leaving it
|
|
377
|
+
here would silently reintroduce it if the specific rule ever moved. */
|
|
329
378
|
.dropdown-placeholder {
|
|
330
|
-
flex-grow: 1;
|
|
331
379
|
color: var(--input-placeholder, var(--ty-input-placeholder, #9ca3af));
|
|
332
380
|
font-size: var(--ty-font-sm);
|
|
333
381
|
line-height: var(--ty-leading-sm);
|
|
@@ -363,10 +411,11 @@ export const selectBaseStyles = `
|
|
|
363
411
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
|
364
412
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
365
413
|
|
|
366
|
-
transform: translateY(-
|
|
414
|
+
transform: translateY(-6px) scale(0.96);
|
|
415
|
+
transform-origin: top center;
|
|
367
416
|
transition:
|
|
368
|
-
opacity
|
|
369
|
-
transform
|
|
417
|
+
opacity var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1)),
|
|
418
|
+
transform var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
370
419
|
|
|
371
420
|
display: flex;
|
|
372
421
|
flex-wrap: wrap;
|
|
@@ -623,6 +672,7 @@ export const selectBaseStyles = `
|
|
|
623
672
|
}
|
|
624
673
|
|
|
625
674
|
.dropdown-mode-mobile .mobile-search-input {
|
|
675
|
+
font-family: inherit;
|
|
626
676
|
flex: 1;
|
|
627
677
|
min-width: 0;
|
|
628
678
|
box-sizing: border-box;
|
|
@@ -631,7 +681,7 @@ export const selectBaseStyles = `
|
|
|
631
681
|
border: 2px solid;
|
|
632
682
|
border-color: var(--mobile-border-color);
|
|
633
683
|
border-radius: var(--ty-radius-md);
|
|
634
|
-
font-family:
|
|
684
|
+
font-family: inherit;
|
|
635
685
|
font-size: var(--ty-font-sm);
|
|
636
686
|
line-height: var(--ty-leading-sm);
|
|
637
687
|
letter-spacing: var(--ty-tracking-sm);
|
package/lib/styles/select.d.ts
CHANGED
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
* - .compact: content-hugging trigger for toolbars, count badge
|
|
9
9
|
* - .custom-trigger (slot="trigger"): all chrome stripped
|
|
10
10
|
*/
|
|
11
|
-
export declare const selectStyles = "\n/* Compact skin hugs its content (field skin = base styles) */\n.select-stub.compact {\n display: inline-flex;\n flex-wrap: nowrap;\n width: auto;\n gap: 0.5rem;\n /* extra right padding = room for the absolutely-positioned chevron */\n padding: 0.5rem 2.25rem 0.5rem 0.875rem;\n}\n\n/* Consumer-provided trigger (slot=\"trigger\"): no chrome at all */\n.select-stub.custom-trigger {\n display: inline-block;\n width: auto;\n border: none;\n background: none;\n padding: 0;\n min-height: 0;\n border-radius: 0;\n}\n.select-stub.custom-trigger:hover {\n border: none;\n}\n\n.select-count {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.25rem;\n height: 1.25rem;\n padding: 0 0.375rem;\n border-radius: 9999px;\n /* --ty-text-primary never existed as a token \u2014 this silently fell back\n to inherited/unset color. Now follows the select's own flavor. */\n background: color-mix(in oklab, var(--select-accent, var(--ty-color-primary)) 15%, transparent);\n color: var(--select-accent-bold, var(--ty-color-primary-strong));\n font-size: var(--ty-font-xs);\n font-weight: var(--ty-font-semibold);\n font-style: normal;\n}\n.select-count[hidden] {\n display: none;\n}\n\n/* Clear (x) button \u2014 normal flex child of .select-stub, not absolutely\n positioned like .dropdown-chevron: it just takes its own space in the\n row, no padding-right gutter math needed. Lives in the trigger slot's\n fallback content, so it's automatically absent under slot=\"trigger\". */\n.select-clear {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1rem;\n height: 1rem;\n padding: 0;\n border: none;\n background: none;\n color: var(--input-placeholder, var(--ty-input-placeholder));\n cursor: pointer;\n border-radius: var(--ty-radius-sm);\n transition: var(--ty-local-transition, all 0.15s ease);\n}\n.select-clear[hidden] {\n display: none;\n}\n.select-clear:hover {\n /* Destructive utility action \u2014 neutral at rest, warns danger-red on\n hover regardless of the field's own flavor. */\n color: var(--ty-color-danger);\n background-color: var(--ty-bg-danger-soft);\n}\n.select-clear svg {\n width: 100%;\n height: 100%;\n}\n\n/* Selection text (field skin): single line, ellipsis, input colors.\n flex-basis 0, not auto \u2014 .select-stub is flex-wrap:wrap, and auto's\n hypothetical size is the FULL untruncated text width (wrap-line\n assignment happens before shrinking), which pushed fixed-size siblings\n (.select-clear) onto a second line even though the text visibly had\n room after truncating. 0 makes this item contribute ~nothing to the\n line-fit check up front; grow/shrink + overflow:hidden still truncate\n it identically. */\n.select-stub .dropdown-placeholder {\n font-style: normal;\n flex: 1 1 0%;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--input-color, var(--ty-input-color));\n}\n\n.select-stub .dropdown-placeholder.placeholder-shown {\n color: var(--input-placeholder, var(--ty-input-placeholder));\n}\n.select-stub .dropdown-placeholder[hidden] {\n display: none;\n}\n\n/* Single-select display clone (slot=\"selected\"): the selected ty-option\n projected into the stub \u2014 its own :host([cloned]) styling strips the\n list-row chrome; here it just takes the text's flex slot. */\n.select-stub slot[name=\"selected\"] {\n display: none;\n}\n.select-stub.has-clone slot[name=\"selected\"] {\n display: block;\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n}\n.select-stub.compact.has-clone slot[name=\"selected\"] {\n flex: 0 1 auto;\n}\n\n/* start/end slot content (icons etc.) \u2014 spacing comes from the stub's flex\n gap; muted like ty-input adornments. end sits before the chevron. */\n.select-stub ::slotted([slot=\"start\"]),\n.select-stub ::slotted([slot=\"end\"]) {\n flex-shrink: 0;\n color: var(--ty-text-soft);\n}\n\n/* Compact skin: the text IS the trigger label \u2014 no grow, regular text color.\n flex-basis: auto (not the field skin's 0%) \u2014 content-hugging needs its\n natural content width as the starting point; 0% would collapse it since\n grow is also 0 here (nothing left to expand it back out). */\n.select-stub.compact .dropdown-placeholder {\n flex: 0 1 auto;\n color: var(--ty-text);\n}\n\n/* Trigger stays visible while the popup is open (the popup hides it) */\n.dropdown-wrapper:has(.dropdown-chevron.open) .select-stub {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* One-panel popup: search header fused with the options list */\n\n/* Single unified shadow around the whole panel (header + list as one silhouette) */\n.dropdown-dialog.open {\n filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.14)) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.08));\n}\n\n/* Card-style header: the search input keeps its own bordered-field look\n (base styles) and sits inset in the panel, TagPicker-style \u2014 no divider,\n whitespace separates it from the list. The header's OWN surface/border is\n part of the floating panel's chrome, not a form field \u2014 --ty-surface-\n floating/--ty-floating-border, matching .dropdown-options below it (same\n reasoning as the base-panel fix: --ty-input-* is deliberately faint for\n resting form fields and read as a missing/mismatched edge here). */\n.dropdown-header {\n background: var(--ty-surface-floating);\n border: 1px solid var(--ty-floating-border);\n border-bottom: none;\n border-radius: var(--ty-radius-lg) var(--ty-radius-lg) 0 0;\n padding: var(--ty-spacing-2) var(--ty-spacing-2) var(--ty-spacing-1);\n}\n.dropdown-dialog.position-below .dropdown-header {\n margin-bottom: 0;\n}\n.dropdown-dialog.position-above .dropdown-header {\n margin-top: 0;\n border-top: none;\n border-bottom: 1px solid var(--ty-floating-border);\n border-radius: 0 0 var(--ty-radius-lg) var(--ty-radius-lg);\n padding: var(--ty-spacing-1) var(--ty-spacing-2) var(--ty-spacing-2);\n}\n\n/* Magnifier adornment inside the search field (header padding + field inset) */\n.dropdown-search-icon {\n position: absolute;\n top: 50%;\n left: calc(var(--ty-spacing-2) + var(--ty-spacing-3));\n transform: translateY(-50%);\n width: 1rem;\n height: 1rem;\n color: var(--input-placeholder, var(--ty-input-placeholder));\n pointer-events: none;\n}\n.dropdown-search-icon svg {\n width: 100%;\n height: 100%;\n}\n\n/* Mobile header: flex row, icon flows inline before the input. Same 32x32\n footprint as .mobile-close-button (18px glyph centered in the box, not a\n bare 18px glyph flush at the edge) so the row has two matching bookends \u2014\n a bare icon on one side and a 32px circle on the other read as lopsided\n even with equal flex gap on both sides. Matching them also centers the\n search input's flex:1 fill symmetrically between the two. Fixed white +\n 18px glyph, same reasoning as .mobile-header-label \u2014 this sits on the\n dark ::backdrop blur in both themes, not a theme surface. */\n.dropdown-mode-mobile .mobile-header-content .dropdown-search-icon {\n position: static;\n transform: none;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n color: #ffffff;\n}\n.dropdown-mode-mobile .mobile-header-content .dropdown-search-icon svg {\n width: 18px;\n height: 18px;\n}\n\n/* Search input: bordered field from base styles; just size + icon room */\n.dropdown-search-input {\n min-height: 2.25rem;\n padding-right: var(--ty-spacing-2);\n /* room for the magnifier adornment */\n padding-left: calc(var(--ty-spacing-3) + 1rem + var(--ty-spacing-2));\n}\n/* Trigger already has a chevron - no second one inside the search row */\n.dropdown-search-chevron {\n display: none;\n}\n\n/* With the search row hidden nothing inside is focusable, so the <dialog>\n itself receives focus on open \u2014 suppress its UA focus ring */\n.dropdown-dialog:focus,\n.dropdown-dialog:focus-visible {\n outline: none;\n}\n\n/* Search row hidden ('auto' below threshold / searchable=\"false\"):\n the options panel owns the whole silhouette */\n.dropdown-header[hidden] {\n display: none;\n}\n.dropdown-header[hidden] ~ .dropdown-options-wrapper .dropdown-options {\n border-top: 1px solid var(--ty-floating-border);\n border-radius: var(--ty-radius-lg);\n}\n.mobile-search-input[hidden],\n.mobile-header-content .dropdown-search-icon[hidden] {\n display: none;\n}\n\n/* Options: vertical list rows fused under the search header.\n No own shadow/animation - the panel appears as one via dialog opacity. */\n.dropdown-options {\n flex-direction: column;\n flex-wrap: nowrap;\n align-items: stretch;\n gap: 0;\n /* Card-style list: rounded rows sit inset from the panel edges */\n padding: var(--ty-spacing-1);\n border-top: none;\n border-radius: 0 0 var(--ty-radius-lg) var(--ty-radius-lg);\n box-shadow: none;\n transform: none;\n transition: none;\n /* ~9-10 visible rows before scrolling (custom scrollbar, native hidden) */\n max-height: 24rem;\n}\n.dropdown-dialog.position-below .dropdown-options {\n box-shadow: none;\n}\n.dropdown-dialog.position-above .dropdown-options {\n border-top: 1px solid var(--ty-floating-border);\n border-bottom: none;\n border-radius: var(--ty-radius-lg) var(--ty-radius-lg) 0 0;\n}\n\n/* Loading panel continues the fused silhouette exactly like .dropdown-options \u2014\n the base layer styles it as a floating card (radius + shadow), which left a\n see-through gap under the search header while loading. */\n.dropdown-loading {\n border-top: none;\n border-radius: 0 0 var(--ty-radius-lg) var(--ty-radius-lg);\n box-shadow: none;\n}\n.dropdown-dialog.position-above .dropdown-loading {\n border-top: 1px solid var(--ty-floating-border);\n border-bottom: none;\n border-radius: var(--ty-radius-lg) var(--ty-radius-lg) 0 0;\n}\n\n/* Selection tick lives inside ty-option (lucide check on [selected]) */\n\n/* Mobile modal: same vertical list */\n.dropdown-mode-mobile .section-content ::slotted(ty-option) {\n display: block;\n width: 100%;\n}\n";
|
|
11
|
+
export declare const selectStyles = "\n/* Compact skin hugs its content (field skin = base styles) */\n.select-stub.compact {\n display: inline-flex;\n flex-wrap: nowrap;\n width: auto;\n gap: 0.5rem;\n /* extra right padding = room for the absolutely-positioned chevron */\n padding: 0.5rem 2.25rem 0.5rem 0.875rem;\n}\n\n/* Consumer-provided trigger (slot=\"trigger\"): no chrome at all */\n.select-stub.custom-trigger {\n display: inline-block;\n width: auto;\n border: none;\n background: none;\n padding: 0;\n min-height: 0;\n border-radius: 0;\n}\n.select-stub.custom-trigger:hover {\n border: none;\n}\n\n.select-count {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.25rem;\n height: 1.25rem;\n padding: 0 0.375rem;\n border-radius: 9999px;\n /* --ty-text-primary never existed as a token \u2014 this silently fell back\n to inherited/unset color. Now follows the select's own flavor. */\n background: color-mix(in oklab, var(--select-accent, var(--ty-color-primary)) 15%, transparent);\n color: var(--select-accent-bold, var(--ty-color-primary-strong));\n font-size: var(--ty-font-xs);\n font-weight: var(--ty-font-semibold);\n font-style: normal;\n}\n.select-count[hidden] {\n display: none;\n}\n\n/* Clear (x) button \u2014 normal flex child of .select-stub, not absolutely\n positioned like .dropdown-chevron: it just takes its own space in the\n row, no padding-right gutter math needed. Lives in the trigger slot's\n fallback content, so it's automatically absent under slot=\"trigger\". */\n.select-clear {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1rem;\n height: 1rem;\n padding: 0;\n border: none;\n background: none;\n color: var(--input-placeholder, var(--ty-input-placeholder));\n cursor: pointer;\n border-radius: var(--ty-radius-sm);\n transition: var(--ty-local-transition, all 0.15s ease);\n}\n.select-clear[hidden] {\n display: none;\n}\n.select-clear:hover {\n /* Destructive utility action \u2014 neutral at rest, warns danger-red on\n hover regardless of the field's own flavor. */\n color: var(--ty-color-danger);\n background-color: var(--ty-bg-danger-soft);\n}\n.select-clear svg {\n width: 100%;\n height: 100%;\n}\n\n/* Selection text (field skin): single line, ellipsis, input colors.\n flex-basis 0, not auto \u2014 .select-stub is flex-wrap:wrap, and auto's\n hypothetical size is the FULL untruncated text width (wrap-line\n assignment happens before shrinking), which pushed fixed-size siblings\n (.select-clear) onto a second line even though the text visibly had\n room after truncating. 0 makes this item contribute ~nothing to the\n line-fit check up front; grow/shrink + overflow:hidden still truncate\n it identically. */\n.select-stub .dropdown-placeholder {\n font-style: normal;\n flex: 1 1 0%;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--input-color, var(--ty-input-color));\n}\n\n.select-stub .dropdown-placeholder.placeholder-shown {\n color: var(--input-placeholder, var(--ty-input-placeholder));\n}\n.select-stub .dropdown-placeholder[hidden] {\n display: none;\n}\n\n/* Single-select display clone (slot=\"selected\"): the selected ty-option\n projected into the stub \u2014 its own :host([cloned]) styling strips the\n list-row chrome; here it just takes the text's flex slot. */\n.select-stub slot[name=\"selected\"] {\n display: none;\n}\n.select-stub.has-clone slot[name=\"selected\"] {\n display: block;\n /* flex-basis 0, not auto \u2014 same reasoning as .dropdown-placeholder above:\n .select-stub is flex-wrap:wrap, and auto's hypothetical size is the\n clone's full untruncated text width, which pushes .select-clear onto\n a second line. */\n flex: 1 1 0%;\n min-width: 0;\n overflow: hidden;\n}\n.select-stub.compact.has-clone slot[name=\"selected\"] {\n flex: 0 1 auto;\n}\n\n/* start/end slot content (icons etc.) \u2014 spacing comes from the stub's flex\n gap; muted like ty-input adornments. end sits before the chevron. */\n.select-stub ::slotted([slot=\"start\"]),\n.select-stub ::slotted([slot=\"end\"]) {\n flex-shrink: 0;\n color: var(--ty-text-soft);\n}\n\n/* Compact skin: the text IS the trigger label \u2014 no grow, regular text color.\n flex-basis: auto (not the field skin's 0%) \u2014 content-hugging needs its\n natural content width as the starting point; 0% would collapse it since\n grow is also 0 here (nothing left to expand it back out). */\n.select-stub.compact .dropdown-placeholder {\n flex: 0 1 auto;\n color: var(--ty-text);\n}\n\n/* Trigger stays visible while the popup is open (the popup hides it) */\n.dropdown-wrapper:has(.dropdown-chevron.open) .select-stub {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* One-panel popup: search header fused with the options list */\n\n/* Single unified shadow around the whole panel (header + list as one silhouette) */\n.dropdown-dialog.open {\n filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.14)) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.08));\n}\n\n/* Card-style header: the search input keeps its own bordered-field look\n (base styles) and sits inset in the panel, TagPicker-style \u2014 no divider,\n whitespace separates it from the list. The header's OWN surface/border is\n part of the floating panel's chrome, not a form field \u2014 --ty-surface-\n floating/--ty-floating-border, matching .dropdown-options below it (same\n reasoning as the base-panel fix: --ty-input-* is deliberately faint for\n resting form fields and read as a missing/mismatched edge here). */\n.dropdown-header {\n background: var(--ty-surface-floating);\n border: 1px solid var(--ty-floating-border);\n border-bottom: none;\n border-radius: var(--ty-radius-lg) var(--ty-radius-lg) 0 0;\n padding: var(--ty-spacing-2) var(--ty-spacing-2) var(--ty-spacing-1);\n}\n.dropdown-dialog.position-below .dropdown-header {\n margin-bottom: 0;\n}\n.dropdown-dialog.position-above .dropdown-header {\n margin-top: 0;\n border-top: none;\n border-bottom: 1px solid var(--ty-floating-border);\n border-radius: 0 0 var(--ty-radius-lg) var(--ty-radius-lg);\n padding: var(--ty-spacing-1) var(--ty-spacing-2) var(--ty-spacing-2);\n}\n\n/* Magnifier adornment inside the search field (header padding + field inset) */\n.dropdown-search-icon {\n position: absolute;\n top: 50%;\n left: calc(var(--ty-spacing-2) + var(--ty-spacing-3));\n transform: translateY(-50%);\n width: 1rem;\n height: 1rem;\n color: var(--input-placeholder, var(--ty-input-placeholder));\n pointer-events: none;\n}\n.dropdown-search-icon svg {\n width: 100%;\n height: 100%;\n}\n\n/* Mobile header: flex row, icon flows inline before the input. Same 32x32\n footprint as .mobile-close-button (18px glyph centered in the box, not a\n bare 18px glyph flush at the edge) so the row has two matching bookends \u2014\n a bare icon on one side and a 32px circle on the other read as lopsided\n even with equal flex gap on both sides. Matching them also centers the\n search input's flex:1 fill symmetrically between the two. Fixed white +\n 18px glyph, same reasoning as .mobile-header-label \u2014 this sits on the\n dark ::backdrop blur in both themes, not a theme surface. */\n.dropdown-mode-mobile .mobile-header-content .dropdown-search-icon {\n position: static;\n transform: none;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n color: #ffffff;\n}\n.dropdown-mode-mobile .mobile-header-content .dropdown-search-icon svg {\n width: 18px;\n height: 18px;\n}\n\n/* Search input: bordered field from base styles; just size + icon room */\n.dropdown-search-input {\n min-height: 2.25rem;\n padding-right: var(--ty-spacing-2);\n /* room for the magnifier adornment */\n padding-left: calc(var(--ty-spacing-3) + 1rem + var(--ty-spacing-2));\n}\n/* Trigger already has a chevron - no second one inside the search row */\n.dropdown-search-chevron {\n display: none;\n}\n\n/* With the search row hidden nothing inside is focusable, so the <dialog>\n itself receives focus on open \u2014 suppress its UA focus ring */\n.dropdown-dialog:focus,\n.dropdown-dialog:focus-visible {\n outline: none;\n}\n\n/* Search row hidden ('auto' below threshold / searchable=\"false\"):\n the options panel owns the whole silhouette */\n.dropdown-header[hidden] {\n display: none;\n}\n.dropdown-header[hidden] ~ .dropdown-options-wrapper .dropdown-options {\n border-top: 1px solid var(--ty-floating-border);\n border-radius: var(--ty-radius-lg);\n}\n.mobile-search-input[hidden],\n.mobile-header-content .dropdown-search-icon[hidden] {\n display: none;\n}\n\n/* Options: vertical list rows fused under the search header.\n No own shadow/animation - the panel appears as one via dialog opacity. */\n.dropdown-options {\n flex-direction: column;\n flex-wrap: nowrap;\n align-items: stretch;\n gap: 0;\n /* Card-style list: rounded rows sit inset from the panel edges */\n padding: var(--ty-spacing-1);\n border-top: none;\n border-radius: 0 0 var(--ty-radius-lg) var(--ty-radius-lg);\n box-shadow: none;\n transform: none;\n transition: none;\n /* ~9-10 visible rows before scrolling (custom scrollbar, native hidden) */\n max-height: 24rem;\n}\n.dropdown-dialog.position-below .dropdown-options {\n box-shadow: none;\n}\n.dropdown-dialog.position-above .dropdown-options {\n border-top: 1px solid var(--ty-floating-border);\n border-bottom: none;\n border-radius: var(--ty-radius-lg) var(--ty-radius-lg) 0 0;\n}\n\n/* Loading panel continues the fused silhouette exactly like .dropdown-options \u2014\n the base layer styles it as a floating card (radius + shadow), which left a\n see-through gap under the search header while loading. */\n.dropdown-loading {\n border-top: none;\n border-radius: 0 0 var(--ty-radius-lg) var(--ty-radius-lg);\n box-shadow: none;\n}\n.dropdown-dialog.position-above .dropdown-loading {\n border-top: 1px solid var(--ty-floating-border);\n border-bottom: none;\n border-radius: var(--ty-radius-lg) var(--ty-radius-lg) 0 0;\n}\n\n/* Selection tick lives inside ty-option (lucide check on [selected]) */\n\n/* Mobile modal: same vertical list */\n.dropdown-mode-mobile .section-content ::slotted(ty-option) {\n display: block;\n width: 100%;\n}\n";
|
package/lib/styles/select.js
CHANGED
|
@@ -110,7 +110,11 @@ export const selectStyles = `
|
|
|
110
110
|
}
|
|
111
111
|
.select-stub.has-clone slot[name="selected"] {
|
|
112
112
|
display: block;
|
|
113
|
-
flex
|
|
113
|
+
/* flex-basis 0, not auto — same reasoning as .dropdown-placeholder above:
|
|
114
|
+
.select-stub is flex-wrap:wrap, and auto's hypothetical size is the
|
|
115
|
+
clone's full untruncated text width, which pushes .select-clear onto
|
|
116
|
+
a second line. */
|
|
117
|
+
flex: 1 1 0%;
|
|
114
118
|
min-width: 0;
|
|
115
119
|
overflow: hidden;
|
|
116
120
|
}
|
package/lib/styles/tabs.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Tabs Component Styles — ty-tabs container: top/bottom placement, animated
|
|
3
3
|
* active marker, carousel viewport. Customizable via CSS Parts (::part).
|
|
4
4
|
*/
|
|
5
|
-
export declare const tabsStyles = "\n:host {\n display: block;\n width: var(--tabs-width, 100%);\n height: var(--tabs-height, 400px);\n box-sizing: border-box;\n}\n\n.tabs-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n}\n\n.tabs-container[data-placement=\"bottom\"] {\n flex-direction: column-reverse;\n}\n\n/* Tab buttons container \u2014 exposed as a CSS Part for full styling control */\n.tab-buttons {\n display: flex;\n gap: 0;\n flex-shrink: 0;\n position: relative;\n /* For absolute positioned marker */\n \n /* Default minimal styling - customize via ::part(buttons-container) */\n border-bottom: 1px solid var(--ty-border);\n background: transparent;\n}\n\n.tabs-container[data-placement=\"bottom\"] .tab-buttons {\n border-bottom: none;\n border-top: 1px solid var(--ty-border);\n}\n\n/* Marker wrapper \u2014 exposed as a CSS Part for custom marker styling */\n.marker-wrapper {\n position: absolute;\n z-index: 0;\n /* Behind buttons */\n pointer-events: none;\n /* Don't block clicks */\n transition: left var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n width var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n height var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n top var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);\n}\n\n.default-marker {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 2px;\n width: 100%;\n background: var(--ty-color-primary);\n pointer-events: none;\n}\n\n/* Hide default marker when custom marker is slotted */\n.marker-wrapper:has(::slotted([slot=\"marker\"])) .default-marker {\n display: none;\n}\n\n::slotted([slot=\"marker\"]) {\n display: block;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n}\n\n.tab-button {\n min-width: 120px;\n padding: 6px 12px;\n border: none;\n background: transparent;\n cursor: pointer;\n font: inherit;\n color: var(--ty-text-soft);\n transition: var(--ty-local-transition, color 200ms, background-color 200ms);\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n font-weight: var(--ty-font-bold);\n font-size: var(--ty-font-sm);\n position: relative;\n /* Establish stacking context */\n z-index: 10;\n /* Above marker */\n}\n\n.tab-button[aria-selected=true] {\n color: var(--ty-text-strong);\n}\n\n.tab-button[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.tab-button:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n.tab-button.overflow-hidden {\n display: none;\n}\n\n/* Overflow \"more\" trigger + menu */\n.tab-overflow-trigger {\n flex-shrink: 0;\n width: 40px;\n min-width: 40px;\n padding: 6px;\n border: none;\n background: transparent;\n cursor: pointer;\n color: var(--ty-text-soft);\n font-size: 18px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n}\n\n.tab-overflow-trigger:hover {\n color: var(--ty-text-strong);\n}\n\n.tab-overflow-trigger:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n.tab-overflow-menu {\n display: flex;\n flex-direction: column;\n min-width: 160px;\n max-height: 300px;\n overflow-y: auto;\n padding: var(--ty-spacing-2, 4px);\n background: var(--ty-surface-floating);\n border-radius: var(--ty-radius-md);\n box-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n}\n\n.tab-overflow-item {\n padding: var(--ty-spacing-2, 6px) var(--ty-spacing-3, 12px);\n border: none;\n background: transparent;\n text-align: left;\n cursor: pointer;\n border-radius: var(--ty-radius-md);\n color: var(--ty-text);\n font: inherit;\n font-size: var(--ty-font-sm);\n white-space: nowrap;\n}\n\n.tab-overflow-item:hover {\n background: var(--ty-bg-neutral-soft);\n color: var(--ty-text-strong);\n}\n\n.tab-overflow-item[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* Panels viewport \u2014 exposed as a CSS Part */\n.panels-viewport {\n position: relative;\n flex: 1;\n overflow: hidden;\n /* Critical: hides off-screen panels */\n min-height: 0;\n /* Allows flex child to shrink */\n}\n\n/* Panels wrapper slides horizontally */\n.panels-wrapper {\n display: flex;\n height: 100%;\n transition: transform var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panels-wrapper {\n transition-duration: 0ms !important;\n }\n\n .marker-wrapper {\n transition-duration: 0ms !important;\n }\n}\n\n::slotted(ty-tab) {\n width: var(--tabs-width, 100%);\n height: 100%;\n flex-shrink: 0;\n}\n";
|
|
5
|
+
export declare const tabsStyles = "\n:host {\n display: block;\n width: var(--tabs-width, 100%);\n height: var(--tabs-height, 400px);\n box-sizing: border-box;\n}\n\n.tabs-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n}\n\n.tabs-container[data-placement=\"bottom\"] {\n flex-direction: column-reverse;\n}\n\n/* Tab buttons container \u2014 exposed as a CSS Part for full styling control */\n.tab-buttons {\n display: flex;\n gap: 0;\n flex-shrink: 0;\n position: relative;\n /* For absolute positioned marker */\n \n /* Default minimal styling - customize via ::part(buttons-container) */\n border-bottom: 1px solid var(--ty-border);\n background: transparent;\n}\n\n.tabs-container[data-placement=\"bottom\"] .tab-buttons {\n border-bottom: none;\n border-top: 1px solid var(--ty-border);\n}\n\n/* Marker wrapper \u2014 exposed as a CSS Part for custom marker styling */\n.marker-wrapper {\n position: absolute;\n z-index: 0;\n /* Behind buttons */\n pointer-events: none;\n /* Don't block clicks */\n transition: left var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n width var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n height var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n top var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);\n}\n\n.default-marker {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 2px;\n width: 100%;\n background: var(--ty-color-primary);\n pointer-events: none;\n}\n\n/* Hide default marker when custom marker is slotted */\n.marker-wrapper:has(::slotted([slot=\"marker\"])) .default-marker {\n display: none;\n}\n\n::slotted([slot=\"marker\"]) {\n display: block;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n}\n\n.tab-button {\n min-width: 120px;\n padding: 6px 12px;\n border: none;\n background: transparent;\n cursor: pointer;\n font: inherit;\n color: var(--ty-text-soft);\n transition: var(--ty-local-transition, color 200ms, background-color 200ms);\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n font-weight: var(--ty-font-bold);\n font-size: var(--ty-font-sm);\n position: relative;\n /* Establish stacking context */\n z-index: 10;\n /* Above marker */\n}\n\n.tab-button[aria-selected=true] {\n color: var(--ty-text-strong);\n}\n\n.tab-button[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.tab-button:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n.tab-button.overflow-hidden {\n display: none;\n}\n\n/* Overflow \"more\" trigger + menu */\n.tab-overflow-trigger {\n flex-shrink: 0;\n width: 40px;\n min-width: 40px;\n padding: 6px;\n border: none;\n background: transparent;\n cursor: pointer;\n color: var(--ty-text-soft);\n font-size: 18px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n}\n\n.tab-overflow-trigger:hover {\n color: var(--ty-text-strong);\n}\n\n.tab-overflow-trigger:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n.tab-overflow-menu {\n display: flex;\n flex-direction: column;\n min-width: 160px;\n max-height: 300px;\n overflow-y: auto;\n padding: var(--ty-spacing-2, 4px);\n background: var(--ty-surface-floating);\n border-radius: var(--ty-radius-md);\n box-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n}\n\n.tab-overflow-item {\n padding: var(--ty-spacing-2, 6px) var(--ty-spacing-3, 12px);\n border: none;\n background: transparent;\n text-align: left;\n cursor: pointer;\n border-radius: var(--ty-radius-md);\n color: var(--ty-text);\n font: inherit;\n font-size: var(--ty-font-sm);\n white-space: nowrap;\n}\n\n.tab-overflow-item:hover {\n background: var(--ty-bg-neutral-soft);\n color: var(--ty-text-strong);\n}\n\n.tab-overflow-item[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* Panels viewport \u2014 exposed as a CSS Part */\n.panels-viewport {\n position: relative;\n flex: 1;\n overflow: hidden;\n /* Critical: hides off-screen panels */\n min-height: 0;\n /* Allows flex child to shrink */\n}\n\n/* Panels wrapper slides horizontally */\n.panels-wrapper {\n display: flex;\n height: 100%;\n transition: transform var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panels-wrapper {\n transition-duration: 0ms !important;\n }\n\n .marker-wrapper {\n transition-duration: 0ms !important;\n }\n}\n\n::slotted(ty-tab) {\n /* Carousel page width. --tabs-page-width is the measured pixel width, set by\n the ResizeObserver for percentage-width tabs so every panel is exactly one\n page wide. It is deliberately NOT --tabs-width: that one sizes :host, and\n feeding a measured pixel value back into the host's own width is a\n self-referential loop (see setupResizeObserver). */\n width: var(--tabs-page-width, var(--tabs-width, 100%));\n height: 100%;\n flex-shrink: 0;\n}\n";
|
package/lib/styles/tabs.js
CHANGED
|
@@ -204,7 +204,12 @@ export const tabsStyles = `
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
::slotted(ty-tab) {
|
|
207
|
-
width
|
|
207
|
+
/* Carousel page width. --tabs-page-width is the measured pixel width, set by
|
|
208
|
+
the ResizeObserver for percentage-width tabs so every panel is exactly one
|
|
209
|
+
page wide. It is deliberately NOT --tabs-width: that one sizes :host, and
|
|
210
|
+
feeding a measured pixel value back into the host's own width is a
|
|
211
|
+
self-referential loop (see setupResizeObserver). */
|
|
212
|
+
width: var(--tabs-page-width, var(--tabs-width, 100%));
|
|
208
213
|
height: 100%;
|
|
209
214
|
flex-shrink: 0;
|
|
210
215
|
}
|
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:host {\n display: block;\n font-family:
|
|
5
|
+
export declare const textareaStyles = "\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-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: 12px;\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: 12px;\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 10px;\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 10px;\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 12px;\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 14px;\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 16px;\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 12px;\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 14px;\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 12px;\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 12px;\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 14px;\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 12px;\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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const textareaStyles = `
|
|
2
2
|
:host {
|
|
3
3
|
display: block;
|
|
4
|
-
font-family:
|
|
4
|
+
font-family: inherit;
|
|
5
5
|
width: 100%;
|
|
6
6
|
}
|
|
7
7
|
|
|
@@ -117,6 +117,7 @@ export const textareaStyles = `
|
|
|
117
117
|
/* Error state is reflected on .textarea-wrapper (see above). */
|
|
118
118
|
|
|
119
119
|
textarea {
|
|
120
|
+
font-family: inherit;
|
|
120
121
|
/* Borderless — the .textarea-wrapper owns the border/background now. */
|
|
121
122
|
box-sizing: border-box;
|
|
122
123
|
width: 100%;
|
package/lib/styles/wizard.d.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
* circles read the matching accent var directly (no `-bg` indirection), and
|
|
7
7
|
* the brand-layer flavor seeds cascade through for per-shade control.
|
|
8
8
|
*/
|
|
9
|
-
export declare const wizardStyles = "\n/* Theming tokens \u2014 every default chains back to the brand layer / global\n scale tokens. */\n\n:host {\n display: block;\n width: var(--ty-wizard-width, 100%); /* set by width attribute \u2014 not a public token */\n height: var(--ty-wizard-height, 700px); /* set by height attribute \u2014 not a public token */\n box-sizing: border-box;\n\n /* State accents \u2014 the primary override surface. One variable retints the\n * matching circle background, border, and glow. */\n --ty-wizard-active-accent: var(--ty-color-primary);\n --ty-wizard-completed-accent: var(--ty-color-success);\n --ty-wizard-error-accent: var(--ty-color-danger);\n --ty-wizard-pending-accent: var(--ty-color-neutral);\n\n /* Container chrome \u2014 routes through global scales. Override --ty-radius-lg\n * or --ty-shadow-md app-wide and the wizard follows; override these locally\n * to give the wizard a distinct look. */\n --ty-wizard-bg: var(--ty-surface-floating);\n --ty-wizard-border: var(--ty-border);\n --ty-wizard-radius: var(--ty-radius-lg, 12px);\n --ty-wizard-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n\n --ty-wizard-header-bg: var(--ty-surface-content);\n --ty-wizard-header-border: var(--ty-border-soft, var(--ty-border));\n --ty-wizard-header-padding: 24px 24px 16px;\n\n --ty-wizard-progress-track: var(--ty-border);\n --ty-wizard-progress-fill: var(--ty-wizard-completed-accent);\n --ty-wizard-progress-height: 2px;\n\n /* Motion \u2014 routes through global motion tokens if defined. */\n --ty-wizard-transition-duration: var(--ty-transition-duration, 300ms);\n --ty-wizard-transition-easing: var(--ty-transition-easing, ease-in-out);\n\n --ty-wizard-circle-size: 32px;\n --ty-wizard-circle-border-width: 2px;\n\n --ty-wizard-panels-bg: var(--ty-surface-elevated);\n\n --ty-wizard-focus-outline: var(--ty-wizard-active-accent);\n}\n\n.wizard-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n border-radius: var(--ty-wizard-radius);\n border: 1px solid var(--ty-wizard-border);\n background: var(--ty-wizard-bg);\n box-shadow: var(--ty-wizard-shadow);\n overflow: hidden;\n}\n\n.step-indicators-wrapper {\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n position: relative;\n padding: var(--ty-wizard-header-padding);\n border-bottom: 1px solid var(--ty-wizard-header-border);\n background: var(--ty-wizard-header-bg);\n}\n\n/* Progress line, behind the step circles */\n.progress-line {\n position: absolute;\n /*\n * With equal-width indicators (flex: 1), each takes 100%/N of the width.\n * Circle centers sit at 50%/N from the left edge of each indicator.\n * Inset = 50% / step-count from each side keeps the line between circle centres.\n */\n left: calc(50% / var(--ty-wizard-step-count, 4));\n right: calc(50% / var(--ty-wizard-step-count, 4));\n top: calc(var(--ty-wizard-circle-size, 32px) / 2 - 1px);\n height: var(--ty-wizard-progress-height);\n background: var(--ty-wizard-progress-track);\n z-index: 0;\n pointer-events: none;\n transition: opacity var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n}\n\n.progress-overlay {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--ty-wizard-progress-fill);\n transition: width var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n will-change: width;\n}\n\n.step-indicators {\n display: flex;\n align-items: flex-start;\n position: relative;\n}\n\n.step-indicator {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n background: transparent;\n border: none;\n padding: 0;\n font: inherit;\n transition: opacity 200ms;\n flex: 1;\n min-width: 0;\n}\n\n.step-indicator[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.step-indicator:focus-visible {\n outline: 2px solid var(--ty-wizard-focus-outline);\n outline-offset: 4px;\n border-radius: 50%;\n}\n\n.step-circle {\n width: var(--ty-wizard-circle-size);\n height: var(--ty-wizard-circle-size);\n border-radius: 50%;\n border: var(--ty-wizard-circle-border-width) solid;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n transition: var(--ty-local-transition, all var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing));\n box-sizing: border-box;\n flex-shrink: 0;\n}\n\n.step-circle[data-state=\"completed\"] {\n background: var(--ty-wizard-completed-accent);\n border-color: var(--ty-color-success-strong);\n /* Derived from the accent this circle actually paints, not from\n --ty-solid-success-fg: the two diverge in dark mode (--ty-solid-l\n dims solid fills by 0.25 L), which stranded white text at 3.5:1. */\n color: oklch(\n from var(--ty-wizard-completed-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.
|
|
9
|
+
export declare const wizardStyles = "\n/* Theming tokens \u2014 every default chains back to the brand layer / global\n scale tokens. */\n\n:host {\n display: block;\n width: var(--ty-wizard-width, 100%); /* set by width attribute \u2014 not a public token */\n height: var(--ty-wizard-height, 700px); /* set by height attribute \u2014 not a public token */\n box-sizing: border-box;\n\n /* State accents \u2014 the primary override surface. One variable retints the\n * matching circle background, border, and glow. */\n --ty-wizard-active-accent: var(--ty-color-primary);\n --ty-wizard-completed-accent: var(--ty-color-success);\n --ty-wizard-error-accent: var(--ty-color-danger);\n --ty-wizard-pending-accent: var(--ty-color-neutral);\n\n /* Container chrome \u2014 routes through global scales. Override --ty-radius-lg\n * or --ty-shadow-md app-wide and the wizard follows; override these locally\n * to give the wizard a distinct look. */\n --ty-wizard-bg: var(--ty-surface-floating);\n --ty-wizard-border: var(--ty-border);\n --ty-wizard-radius: var(--ty-radius-lg, 12px);\n --ty-wizard-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n\n --ty-wizard-header-bg: var(--ty-surface-content);\n --ty-wizard-header-border: var(--ty-border-soft, var(--ty-border));\n --ty-wizard-header-padding: 24px 24px 16px;\n\n --ty-wizard-progress-track: var(--ty-border);\n --ty-wizard-progress-fill: var(--ty-wizard-completed-accent);\n --ty-wizard-progress-height: 2px;\n\n /* Motion \u2014 routes through global motion tokens if defined. */\n --ty-wizard-transition-duration: var(--ty-transition-duration, 300ms);\n --ty-wizard-transition-easing: var(--ty-transition-easing, ease-in-out);\n\n --ty-wizard-circle-size: 32px;\n --ty-wizard-circle-border-width: 2px;\n\n --ty-wizard-panels-bg: var(--ty-surface-elevated);\n\n --ty-wizard-focus-outline: var(--ty-wizard-active-accent);\n}\n\n.wizard-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n border-radius: var(--ty-wizard-radius);\n border: 1px solid var(--ty-wizard-border);\n background: var(--ty-wizard-bg);\n box-shadow: var(--ty-wizard-shadow);\n overflow: hidden;\n}\n\n.step-indicators-wrapper {\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n position: relative;\n padding: var(--ty-wizard-header-padding);\n border-bottom: 1px solid var(--ty-wizard-header-border);\n background: var(--ty-wizard-header-bg);\n}\n\n/* Progress line, behind the step circles */\n.progress-line {\n position: absolute;\n /*\n * With equal-width indicators (flex: 1), each takes 100%/N of the width.\n * Circle centers sit at 50%/N from the left edge of each indicator.\n * Inset = 50% / step-count from each side keeps the line between circle centres.\n */\n left: calc(50% / var(--ty-wizard-step-count, 4));\n right: calc(50% / var(--ty-wizard-step-count, 4));\n top: calc(var(--ty-wizard-circle-size, 32px) / 2 - 1px);\n height: var(--ty-wizard-progress-height);\n background: var(--ty-wizard-progress-track);\n z-index: 0;\n pointer-events: none;\n transition: opacity var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n}\n\n.progress-overlay {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--ty-wizard-progress-fill);\n transition: width var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n will-change: width;\n}\n\n.step-indicators {\n display: flex;\n align-items: flex-start;\n position: relative;\n}\n\n.step-indicator {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n background: transparent;\n border: none;\n padding: 0;\n font: inherit;\n transition: opacity 200ms;\n flex: 1;\n min-width: 0;\n}\n\n.step-indicator[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.step-indicator:focus-visible {\n outline: 2px solid var(--ty-wizard-focus-outline);\n outline-offset: 4px;\n border-radius: 50%;\n}\n\n.step-circle {\n width: var(--ty-wizard-circle-size);\n height: var(--ty-wizard-circle-size);\n border-radius: 50%;\n border: var(--ty-wizard-circle-border-width) solid;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n transition: var(--ty-local-transition, all var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing));\n box-sizing: border-box;\n flex-shrink: 0;\n}\n\n.step-circle[data-state=\"completed\"] {\n background: var(--ty-wizard-completed-accent);\n border-color: var(--ty-color-success-strong);\n /* Derived from the accent this circle actually paints, not from\n --ty-solid-success-fg: the two diverge in dark mode (--ty-solid-l\n dims solid fills by 0.25 L), which stranded white text at 3.5:1. */\n color: oklch(\n from var(--ty-wizard-completed-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-completed-accent) 12%, transparent);\n}\n\n.step-circle[data-state=\"active\"] {\n background: var(--ty-wizard-active-accent);\n border-color: var(--ty-color-primary-strong);\n color: oklch(\n from var(--ty-wizard-active-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-active-accent) 12%, transparent);\n}\n\n.step-circle[data-state=\"pending\"] {\n background: var(--ty-surface-elevated);\n border-color: var(--ty-border);\n color: var(--ty-color-neutral);\n}\n\n.step-circle[data-state=\"error\"] {\n background: var(--ty-wizard-error-accent);\n border-color: var(--ty-color-danger-strong);\n color: oklch(\n from var(--ty-wizard-error-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-error-accent) 12%, transparent);\n}\n\n.step-text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: center;\n}\n\n.step-label {\n font-size: var(--ty-font-sm, 14px);\n font-weight: var(--ty-font-semibold, 600);\n color: var(--ty-color-neutral);\n transition: var(--ty-local-transition, color 200ms);\n}\n\n.step-indicator[aria-selected=\"true\"] .step-label {\n color: var(--ty-color-neutral-strong);\n}\n\n.step-indicator[aria-selected=\"false\"] .step-label {\n color: var(--ty-color-neutral);\n}\n\n.step-description {\n font-size: var(--ty-font-xs, 12px);\n font-weight: var(--ty-font-normal, 400);\n color: var(--ty-color-neutral);\n transition: var(--ty-local-transition, color 200ms);\n text-align: center;\n max-width: 120px;\n}\n\n.step-indicator[aria-selected=\"true\"] .step-description {\n color: var(--ty-color-neutral-bold);\n}\n\n/* Custom indicator content via slots */\n::slotted([slot^=\"indicator-\"]) {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n\n.panels-viewport {\n position: relative;\n flex: 1;\n overflow: hidden;\n min-height: 0;\n background: var(--ty-wizard-panels-bg);\n}\n\n/* Panels wrapper slides horizontally */\n.panels-wrapper {\n display: flex;\n height: 100%;\n transition: transform var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n will-change: transform;\n}\n\n::slotted(ty-step) {\n width: var(--ty-wizard-width, 100%);\n height: 100%;\n flex-shrink: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panels-wrapper {\n transition-duration: 0ms !important;\n }\n\n .progress-overlay {\n transition-duration: 0ms !important;\n }\n\n .step-circle {\n transition-duration: 0ms !important;\n }\n\n .progress-line {\n transition-duration: 0ms !important;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n";
|
package/lib/styles/wizard.js
CHANGED
|
@@ -151,7 +151,7 @@ export const wizardStyles = `
|
|
|
151
151
|
dims solid fills by 0.25 L), which stranded white text at 3.5:1. */
|
|
152
152
|
color: oklch(
|
|
153
153
|
from var(--ty-wizard-completed-accent)
|
|
154
|
-
clamp(0, (var(--ty-solid-fg-threshold, 0.
|
|
154
|
+
clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0
|
|
155
155
|
);
|
|
156
156
|
box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-completed-accent) 12%, transparent);
|
|
157
157
|
}
|
|
@@ -161,7 +161,7 @@ export const wizardStyles = `
|
|
|
161
161
|
border-color: var(--ty-color-primary-strong);
|
|
162
162
|
color: oklch(
|
|
163
163
|
from var(--ty-wizard-active-accent)
|
|
164
|
-
clamp(0, (var(--ty-solid-fg-threshold, 0.
|
|
164
|
+
clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0
|
|
165
165
|
);
|
|
166
166
|
box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-active-accent) 12%, transparent);
|
|
167
167
|
}
|
|
@@ -177,7 +177,7 @@ export const wizardStyles = `
|
|
|
177
177
|
border-color: var(--ty-color-danger-strong);
|
|
178
178
|
color: oklch(
|
|
179
179
|
from var(--ty-wizard-error-accent)
|
|
180
|
-
clamp(0, (var(--ty-solid-fg-threshold, 0.
|
|
180
|
+
clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0
|
|
181
181
|
);
|
|
182
182
|
box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-error-accent) 12%, transparent);
|
|
183
183
|
}
|
|
@@ -33,6 +33,10 @@ export declare function formatNumber(value: number, config: FormatConfig): strin
|
|
|
33
33
|
* Simple rule: the last occurring . or , is the decimal separator.
|
|
34
34
|
* Everything else is stripped. This works for both US (1,234.56) and
|
|
35
35
|
* European (1.234,56) input, and for mobile keyboards that use , as decimal.
|
|
36
|
+
* One grouping exception: a repeated separator ("1.234.567") can't be a
|
|
37
|
+
* decimal point, so it's stripped as thousands grouping. A LONE separator is
|
|
38
|
+
* always decimal — "$1,234" pasted from formatted text parses as 1.234; this
|
|
39
|
+
* known ambiguity is resolved in favor of typed input ("12,50" → 12.5).
|
|
36
40
|
*
|
|
37
41
|
* @example
|
|
38
42
|
* ```typescript
|
|
@@ -41,7 +45,8 @@ export declare function formatNumber(value: number, config: FormatConfig): strin
|
|
|
41
45
|
* parseNumericValue("12,50") // 12.5 (last separator is ,)
|
|
42
46
|
* parseNumericValue("12.50") // 12.5 (last separator is .)
|
|
43
47
|
* parseNumericValue("1234") // 1234
|
|
44
|
-
* parseNumericValue("
|
|
48
|
+
* parseNumericValue("1.234.567") // 1234567 (repeated separator = grouping)
|
|
49
|
+
* parseNumericValue("$1,234") // 1.234 (lone separator = decimal; known paste ambiguity)
|
|
45
50
|
* parseNumericValue("15%") // 15
|
|
46
51
|
* parseNumericValue("") // null
|
|
47
52
|
* ```
|
|
@@ -43,8 +43,15 @@ export function parseNumericValue(value) {
|
|
|
43
43
|
const parsed = parseFloat(stripped);
|
|
44
44
|
return isNaN(parsed) ? null : parsed;
|
|
45
45
|
}
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const sepChar = stripped[lastSep];
|
|
47
|
+
const before = stripped.slice(0, lastSep);
|
|
48
|
+
const after = stripped.slice(lastSep + 1);
|
|
49
|
+
if (before.includes(sepChar)) {
|
|
50
|
+
const parsed = parseFloat(stripped.replace(/[.,]/g, ''));
|
|
51
|
+
return isNaN(parsed) ? null : parsed;
|
|
52
|
+
}
|
|
53
|
+
const intPart = before.replace(/[.,]/g, '');
|
|
54
|
+
const decPart = after;
|
|
48
55
|
const parsed = parseFloat(intPart + '.' + decPart);
|
|
49
56
|
return isNaN(parsed) ? null : parsed;
|
|
50
57
|
}
|
|
@@ -12,7 +12,7 @@ export type PlacementOrientation = 'horizontal' | 'vertical';
|
|
|
12
12
|
/**
|
|
13
13
|
* Vertical alignment options
|
|
14
14
|
*/
|
|
15
|
-
export type VerticalAlign = 'top' | 'center' | 'bottom' | 'end';
|
|
15
|
+
export type VerticalAlign = 'top' | 'start' | 'center' | 'bottom' | 'end';
|
|
16
16
|
/**
|
|
17
17
|
* Horizontal alignment options
|
|
18
18
|
*/
|
|
@@ -110,6 +110,47 @@ export declare const placementPreferences: {
|
|
|
110
110
|
tooltip: Placement[];
|
|
111
111
|
dropdown: Placement[];
|
|
112
112
|
};
|
|
113
|
+
/** The four sides a floating element can sit on. */
|
|
114
|
+
export type PlacementSide = 'top' | 'right' | 'bottom' | 'left';
|
|
115
|
+
/** Cross-axis alignment against the anchor. Omitted means centred. */
|
|
116
|
+
export type PlacementAlign = 'start' | 'end';
|
|
117
|
+
/** Split `'left-end'` into `{ side: 'left', align: 'end' }`. */
|
|
118
|
+
export declare function parsePlacement(placement: Placement): {
|
|
119
|
+
side: PlacementSide;
|
|
120
|
+
align?: PlacementAlign;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Fallback order to try for a requested placement, best-fit first.
|
|
124
|
+
*
|
|
125
|
+
* FLIP BEFORE RE-ALIGN. When a placement overflows it is almost always the
|
|
126
|
+
* SIDE axis that ran out of room, and re-aligning on the same side does not
|
|
127
|
+
* change the fit at all: `bottom-start` and `bottom-end` need identical
|
|
128
|
+
* vertical space. Flipping to the opposite side is the move that actually
|
|
129
|
+
* helps, so the requested alignment is carried across the flip first, and only
|
|
130
|
+
* then do we try other alignments.
|
|
131
|
+
*
|
|
132
|
+
* This also keeps the old hand-written chains intact for the four bare sides —
|
|
133
|
+
* `top` still degrades to `bottom` as its very next candidate, exactly as
|
|
134
|
+
* before aligned placements existed.
|
|
135
|
+
*
|
|
136
|
+
* Callers previously hand-wrote a 4-entry chain per side, which silently
|
|
137
|
+
* dropped every aligned placement onto the default chain.
|
|
138
|
+
*/
|
|
139
|
+
export declare function preferenceChain(placement: Placement): Placement[];
|
|
140
|
+
/**
|
|
141
|
+
* Map a `Placement` onto `computeAnchoredPosition`'s inputs, so dropdown-style
|
|
142
|
+
* components (ty-select, ty-date-picker) accept the same `placement` vocabulary
|
|
143
|
+
* as ty-popup / ty-tooltip.
|
|
144
|
+
*
|
|
145
|
+
* Dropdowns only ever live above or below their trigger, so a left/right
|
|
146
|
+
* placement degrades to "auto" side while keeping its alignment. Bare sides
|
|
147
|
+
* default to start-aligned, matching a native <select> and the behaviour these
|
|
148
|
+
* components had before `placement` existed.
|
|
149
|
+
*/
|
|
150
|
+
export declare function placementToAnchored(placement?: Placement | '' | null): {
|
|
151
|
+
side: 'top' | 'bottom' | 'auto';
|
|
152
|
+
align: 'start' | 'center' | 'end';
|
|
153
|
+
};
|
|
113
154
|
/**
|
|
114
155
|
* Find the best position for the floating element
|
|
115
156
|
* Tries all preference placements and returns the one that fits best
|
|
@@ -131,10 +172,14 @@ export interface AnchoredPopupOptions {
|
|
|
131
172
|
gap?: number;
|
|
132
173
|
/** Minimum distance from viewport edges (px, default 8) */
|
|
133
174
|
padding?: number;
|
|
134
|
-
/** Horizontal anchor edge: trigger's left edge ("start", default)
|
|
135
|
-
* right edge ("end"). Either way the result is clamped
|
|
136
|
-
* viewport, same as before. */
|
|
137
|
-
align?: "start" | "end";
|
|
175
|
+
/** Horizontal anchor edge: trigger's left edge ("start", default), its
|
|
176
|
+
* right edge ("end"), or centred on it. Either way the result is clamped
|
|
177
|
+
* into the viewport, same as before. */
|
|
178
|
+
align?: "start" | "center" | "end";
|
|
179
|
+
/** Preferred vertical side. "auto" (default) keeps the historic behaviour:
|
|
180
|
+
* below when it fits, otherwise whichever side has more room. Naming a
|
|
181
|
+
* side honours it when it fits and still flips rather than clipping. */
|
|
182
|
+
side?: "top" | "bottom" | "auto";
|
|
138
183
|
}
|
|
139
184
|
export interface AnchoredPopupPosition {
|
|
140
185
|
/** Popup left edge, viewport coords, clamped into view */
|
package/lib/utils/positioning.js
CHANGED
|
@@ -12,7 +12,7 @@ export const placements = {
|
|
|
12
12
|
horizontal: 'end',
|
|
13
13
|
},
|
|
14
14
|
'right-start': {
|
|
15
|
-
vertical: '
|
|
15
|
+
vertical: 'start',
|
|
16
16
|
horizontal: 'end',
|
|
17
17
|
orientation: 'vertical',
|
|
18
18
|
},
|
|
@@ -39,7 +39,7 @@ export const placements = {
|
|
|
39
39
|
horizontal: 'end',
|
|
40
40
|
},
|
|
41
41
|
'left-start': {
|
|
42
|
-
vertical: '
|
|
42
|
+
vertical: 'start',
|
|
43
43
|
horizontal: 'start',
|
|
44
44
|
orientation: 'vertical',
|
|
45
45
|
},
|
|
@@ -68,6 +68,41 @@ export const placementPreferences = {
|
|
|
68
68
|
'bottom', 'top', 'right', 'left'
|
|
69
69
|
],
|
|
70
70
|
};
|
|
71
|
+
const OPPOSITE_SIDE = {
|
|
72
|
+
top: 'bottom',
|
|
73
|
+
bottom: 'top',
|
|
74
|
+
left: 'right',
|
|
75
|
+
right: 'left',
|
|
76
|
+
};
|
|
77
|
+
export function parsePlacement(placement) {
|
|
78
|
+
const [side, align] = placement.split('-');
|
|
79
|
+
return { side, align };
|
|
80
|
+
}
|
|
81
|
+
export function preferenceChain(placement) {
|
|
82
|
+
const { side, align } = parsePlacement(placement);
|
|
83
|
+
const opposite = OPPOSITE_SIDE[side];
|
|
84
|
+
const perpendicular = side === 'top' || side === 'bottom' ? ['right', 'left'] : ['bottom', 'top'];
|
|
85
|
+
const exact = (s) => (align ? `${s}-${align}` : s);
|
|
86
|
+
const rest = (s) => (align
|
|
87
|
+
? [s, `${s}-${align === 'start' ? 'end' : 'start'}`]
|
|
88
|
+
: [`${s}-start`, `${s}-end`]);
|
|
89
|
+
return [
|
|
90
|
+
exact(side),
|
|
91
|
+
exact(opposite),
|
|
92
|
+
...rest(side),
|
|
93
|
+
...rest(opposite),
|
|
94
|
+
...perpendicular.flatMap((s) => [exact(s), ...rest(s)]),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
export function placementToAnchored(placement) {
|
|
98
|
+
if (!placement)
|
|
99
|
+
return { side: 'auto', align: 'start' };
|
|
100
|
+
const { side, align } = parsePlacement(placement);
|
|
101
|
+
return {
|
|
102
|
+
side: side === 'top' || side === 'bottom' ? side : 'auto',
|
|
103
|
+
align: align ?? 'start',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
71
106
|
function getElementRect(el) {
|
|
72
107
|
const rect = el.getBoundingClientRect();
|
|
73
108
|
return {
|
|
@@ -105,13 +140,13 @@ function calculatePlacement(options) {
|
|
|
105
140
|
}
|
|
106
141
|
else {
|
|
107
142
|
if (horizontal === 'start') {
|
|
108
|
-
x = targetRect.left;
|
|
143
|
+
x = targetRect.left - containerPadding;
|
|
109
144
|
}
|
|
110
145
|
else if (horizontal === 'center') {
|
|
111
146
|
x = targetRect.centerX - floatingRect.width / 2;
|
|
112
147
|
}
|
|
113
148
|
else {
|
|
114
|
-
x = targetRect.right - floatingRect.width;
|
|
149
|
+
x = targetRect.right - floatingRect.width + containerPadding;
|
|
115
150
|
}
|
|
116
151
|
}
|
|
117
152
|
let y;
|
|
@@ -120,10 +155,10 @@ function calculatePlacement(options) {
|
|
|
120
155
|
y = targetRect.centerY - floatingRect.height / 2;
|
|
121
156
|
}
|
|
122
157
|
else if (vertical === 'end') {
|
|
123
|
-
y = targetRect.bottom - floatingRect.height
|
|
158
|
+
y = targetRect.bottom - floatingRect.height + containerPadding;
|
|
124
159
|
}
|
|
125
160
|
else {
|
|
126
|
-
y = targetRect.top;
|
|
161
|
+
y = targetRect.top - containerPadding;
|
|
127
162
|
}
|
|
128
163
|
}
|
|
129
164
|
else {
|
|
@@ -178,8 +213,17 @@ export function computeAnchoredPosition(o) {
|
|
|
178
213
|
const vh = window.innerHeight;
|
|
179
214
|
const spaceBelow = vh - o.anchorRect.bottom;
|
|
180
215
|
const spaceAbove = o.anchorRect.top;
|
|
181
|
-
const
|
|
182
|
-
const
|
|
216
|
+
const needed = o.popupHeight + gap + padding;
|
|
217
|
+
const fitsBelow = spaceBelow >= needed;
|
|
218
|
+
const fitsAbove = spaceAbove >= needed;
|
|
219
|
+
const side = o.side ?? "auto";
|
|
220
|
+
const below = side === "bottom" ? (fitsBelow || !fitsAbove ? true : false)
|
|
221
|
+
: side === "top" ? (fitsAbove || !fitsBelow ? false : true)
|
|
222
|
+
: fitsBelow || spaceBelow >= spaceAbove;
|
|
223
|
+
const rawX = o.align === "end" ? o.anchorRect.right - o.popupWidth
|
|
224
|
+
: o.align === "center"
|
|
225
|
+
? o.anchorRect.left + (o.anchorRect.width - o.popupWidth) / 2
|
|
226
|
+
: o.anchorRect.left;
|
|
183
227
|
const x = Math.max(padding, Math.min(rawX, vw - o.popupWidth - padding));
|
|
184
228
|
return {
|
|
185
229
|
x,
|
package/lib/version.d.ts
CHANGED
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.0-
|
|
1
|
+
export const VERSION = '1.0.0-TC48';
|