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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/css/tyrell-theme.css +167 -178
  3. package/css/tyrell.css +19 -0
  4. package/dist/tyrell-theme.css +167 -178
  5. package/dist/tyrell.css +19 -0
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.d.ts +8 -3
  8. package/lib/base/ty-component.js +13 -5
  9. package/lib/components/date-picker.d.ts +8 -0
  10. package/lib/components/date-picker.js +13 -9
  11. package/lib/components/file-upload.d.ts +6 -0
  12. package/lib/components/file-upload.js +19 -4
  13. package/lib/components/input.js +1 -0
  14. package/lib/components/modal.d.ts +8 -0
  15. package/lib/components/modal.js +29 -6
  16. package/lib/components/popup.js +6 -10
  17. package/lib/components/radio.js +10 -2
  18. package/lib/components/resize-observer.d.ts +1 -0
  19. package/lib/components/resize-observer.js +12 -1
  20. package/lib/components/select.d.ts +11 -3
  21. package/lib/components/select.js +26 -9
  22. package/lib/components/selected-tags.js +1 -1
  23. package/lib/components/tabs.d.ts +2 -0
  24. package/lib/components/tabs.js +18 -1
  25. package/lib/components/tooltip.js +24 -6
  26. package/lib/components/wizard.d.ts +2 -0
  27. package/lib/components/wizard.js +15 -0
  28. package/lib/styles/button.js +9 -2
  29. package/lib/styles/calendar-month.js +1 -1
  30. package/lib/styles/calendar-navigation.d.ts +1 -1
  31. package/lib/styles/calendar-navigation.js +1 -1
  32. package/lib/styles/calendar.d.ts +1 -1
  33. package/lib/styles/calendar.js +1 -1
  34. package/lib/styles/date-picker.js +30 -4
  35. package/lib/styles/file-upload.d.ts +1 -1
  36. package/lib/styles/file-upload.js +1 -1
  37. package/lib/styles/icon.d.ts +1 -1
  38. package/lib/styles/icon.js +12 -1
  39. package/lib/styles/input.js +2 -1
  40. package/lib/styles/modal.d.ts +1 -1
  41. package/lib/styles/modal.js +18 -0
  42. package/lib/styles/option.d.ts +1 -1
  43. package/lib/styles/option.js +16 -1
  44. package/lib/styles/popup.d.ts +1 -1
  45. package/lib/styles/popup.js +22 -5
  46. package/lib/styles/radio.js +1 -1
  47. package/lib/styles/select-base.js +59 -9
  48. package/lib/styles/select.d.ts +1 -1
  49. package/lib/styles/select.js +5 -1
  50. package/lib/styles/tabs.d.ts +1 -1
  51. package/lib/styles/tabs.js +6 -1
  52. package/lib/styles/textarea.d.ts +1 -1
  53. package/lib/styles/textarea.js +2 -1
  54. package/lib/styles/wizard.d.ts +1 -1
  55. package/lib/styles/wizard.js +3 -3
  56. package/lib/utils/number-format.d.ts +6 -1
  57. package/lib/utils/number-format.js +9 -2
  58. package/lib/utils/positioning.d.ts +50 -5
  59. package/lib/utils/positioning.js +52 -8
  60. package/lib/version.d.ts +1 -1
  61. package/lib/version.js +1 -1
  62. 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: var(--ty-font-sans);
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
- transition: opacity 400ms ease;
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: var(--ty-font-sans);
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: var(--ty-font-sans);
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(-8px) scale(0.95);
414
+ transform: translateY(-6px) scale(0.96);
415
+ transform-origin: top center;
367
416
  transition:
368
- opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),
369
- transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
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: var(--ty-font-sans);
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);
@@ -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";
@@ -110,7 +110,11 @@ export const selectStyles = `
110
110
  }
111
111
  .select-stub.has-clone slot[name="selected"] {
112
112
  display: block;
113
- flex: 1 1 auto;
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
  }
@@ -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";
@@ -204,7 +204,12 @@ export const tabsStyles = `
204
204
  }
205
205
 
206
206
  ::slotted(ty-tab) {
207
- width: var(--tabs-width, 100%);
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
  }
@@ -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: var(--ty-font-sans);\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 /* 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";
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";
@@ -1,7 +1,7 @@
1
1
  export const textareaStyles = `
2
2
  :host {
3
3
  display: block;
4
- font-family: var(--ty-font-sans);
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%;
@@ -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.6) - 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.6) - 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.6) - 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";
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";
@@ -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.6) - l) * 1000, 1) 0 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.6) - l) * 1000, 1) 0 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.6) - l) * 1000, 1) 0 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("$1,234") // 1234
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 intPart = stripped.slice(0, lastSep).replace(/[.,]/g, '');
47
- const decPart = stripped.slice(lastSep + 1);
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) or
135
- * right edge ("end"). Either way the result is clamped into the
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 */
@@ -12,7 +12,7 @@ export const placements = {
12
12
  horizontal: 'end',
13
13
  },
14
14
  'right-start': {
15
- vertical: 'center',
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: 'center',
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 - containerPadding;
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 below = spaceBelow >= o.popupHeight + gap + padding || spaceBelow >= spaceAbove;
182
- const rawX = o.align === "end" ? o.anchorRect.right - o.popupWidth : o.anchorRect.left;
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
@@ -4,4 +4,4 @@
4
4
  * This is automatically synced with package.json during build.
5
5
  * Use `npm version` to bump versions.
6
6
  */
7
- export declare const VERSION = "1.0.0-TC46";
7
+ export declare const VERSION = "1.0.0-TC48";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.0-TC46';
1
+ export const VERSION = '1.0.0-TC48';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyrell-components",
3
- "version": "1.0.0-TC46",
3
+ "version": "1.0.0-TC48",
4
4
  "description": "Tyrell Components - Framework-agnostic web components with semantic design system",
5
5
  "type": "module",
6
6
  "sideEffects": [