tyrell-components 1.0.0-TC54 → 1.0.0-TC55

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.
@@ -20,10 +20,10 @@ elementIds.set(element, id);
20
20
  return id;
21
21
  }
22
22
  import { REQUIRED_ICON_SVG } from "../utils/icons.js";
23
+ const SEARCH_AUTO_THRESHOLD = 7;
23
24
  const CHEVRON_DOWN_SVG = `<svg viewBox="0 0 20 20" fill="currentColor">
24
25
  <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
25
26
  </svg>`;
26
- const SEARCH_AUTO_THRESHOLD = 7;
27
27
  function slugify(text) {
28
28
  return text
29
29
  .trim()
@@ -55,11 +55,12 @@ this._clearable = true;
55
55
  this._externalSearch = false;
56
56
  this._allowCreate = false;
57
57
  this._createTransform = "none";
58
- this._searchable = "auto";
58
+ this._notSearchable = false;
59
59
  this._loading = false;
60
60
  this._scrollLockId = null;
61
61
  this._size = DEFAULT_SIZE;
62
62
  this._availableLabel = "Available";
63
+ this._selectedLabel = "Selected";
63
64
  this._noOptionsMessage = "No options available";
64
65
  this._state = {
65
66
  open: false,
@@ -185,8 +186,8 @@ break;
185
186
  case "externalSearch":
186
187
  this._externalSearch = newValue;
187
188
  break;
188
- case "searchable":
189
- this._searchable = newValue;
189
+ case "notSearchable":
190
+ this._notSearchable = newValue;
190
191
  if (this.isConnected && this.shadowRoot)
191
192
  this.updateSearchVisibility();
192
193
  break;
@@ -207,6 +208,9 @@ break;
207
208
  case "available-label":
208
209
  this._availableLabel = newValue || "Available";
209
210
  break;
211
+ case "selected-label":
212
+ this._selectedLabel = newValue || "Selected";
213
+ break;
210
214
  case "no-options-message":
211
215
  this._noOptionsMessage = newValue || "No options available";
212
216
  break;
@@ -698,6 +702,8 @@ e.stopPropagation();
698
702
  e.preventDefault();
699
703
  }
700
704
  handleSearchInput(e) {
705
+ if (this._state.mode === "mobile")
706
+ this.expandMobileSection("available");
701
707
  const target = e.target;
702
708
  const query = target.value;
703
709
  this._state.search = query;
@@ -730,7 +736,12 @@ switch (e.key) {
730
736
  case "Escape":
731
737
  e.preventDefault();
732
738
  e.stopPropagation();
739
+ if (this._state.mode === "mobile") {
740
+ this.closeMobileModal();
741
+ }
742
+ else {
733
743
  this.closeDropdown();
744
+ }
734
745
  break;
735
746
  case "Enter":
736
747
  e.preventDefault();
@@ -746,43 +757,33 @@ break;
746
757
  case "ArrowUp":
747
758
  e.preventDefault();
748
759
  e.stopPropagation();
749
- let newIndexUp;
750
- if (tagsCount === 0) {
751
- newIndexUp = -1;
752
- }
753
- else if (currentHighlightedIndex === -1) {
754
- newIndexUp = tagsCount - 1;
755
- }
756
- else if (currentHighlightedIndex === 0) {
757
- newIndexUp = tagsCount - 1;
758
- }
759
- else {
760
- newIndexUp = currentHighlightedIndex - 1;
761
- }
760
+ const newIndexUp = this.nextEnabledTagIndex(filteredTags, currentHighlightedIndex, -1);
762
761
  this._state.highlightedIndex = newIndexUp;
763
762
  this.highlightTag(filteredTags, newIndexUp);
764
763
  break;
765
764
  case "ArrowDown":
766
765
  e.preventDefault();
767
766
  e.stopPropagation();
768
- let newIndexDown;
769
- if (tagsCount === 0) {
770
- newIndexDown = -1;
771
- }
772
- else if (currentHighlightedIndex === -1) {
773
- newIndexDown = 0;
774
- }
775
- else if (currentHighlightedIndex === tagsCount - 1) {
776
- newIndexDown = 0;
777
- }
778
- else {
779
- newIndexDown = currentHighlightedIndex + 1;
780
- }
767
+ const newIndexDown = this.nextEnabledTagIndex(filteredTags, currentHighlightedIndex, 1);
781
768
  this._state.highlightedIndex = newIndexDown;
782
769
  this.highlightTag(filteredTags, newIndexDown);
783
770
  break;
784
771
  }
785
772
  }
773
+ nextEnabledTagIndex(tags, fromIndex, direction) {
774
+ const count = tags.length;
775
+ if (count === 0)
776
+ return -1;
777
+ let index = fromIndex === -1
778
+ ? (direction === 1 ? 0 : count - 1)
779
+ : (fromIndex + direction + count) % count;
780
+ for (let i = 0; i < count; i++) {
781
+ if (!tags[index].element.hasAttribute("disabled"))
782
+ return index;
783
+ index = (index + direction + count) % count;
784
+ }
785
+ return -1;
786
+ }
786
787
  filterTags(tags, query) {
787
788
  if (!query || query.trim() === "") {
788
789
  return tags;
@@ -1098,12 +1099,21 @@ ${CHEVRON_DOWN_SVG}
1098
1099
  <!-- HEADER (matches dropdown.ts) -->
1099
1100
  ${searchHeaderHtml}
1100
1101
  <!-- BODY: options list with toggleable selected state -->
1101
- <div class="mobile-body">
1102
+ <div class="mobile-body" data-expanded="available">
1103
+ <!-- SELECTED STRIP (pinned above the list, collapses when empty) -->
1104
+ <div class="mobile-selected-section" data-empty="true">
1105
+ <button class="section-header" type="button" data-section="selected" aria-expanded="false">
1106
+ <span class="section-title">${this._selectedLabel} <span class="section-count">(0)</span></span>
1107
+ ${CHEVRON_DOWN_SVG}
1108
+ </button>
1109
+ <div class="section-content mobile-selected-chips"></div>
1110
+ </div>
1102
1111
  <!-- AVAILABLE LIST (always visible, takes remaining space) -->
1103
1112
  <div class="mobile-available-section" data-empty="false">
1104
- <div class="section-header">
1113
+ <button class="section-header" type="button" data-section="available" aria-expanded="true">
1105
1114
  <span class="section-title">${this._availableLabel}</span>
1106
- </div>
1115
+ ${CHEVRON_DOWN_SVG}
1116
+ </button>
1107
1117
  <div class="section-content dropdown-options-wrapper" id="${listboxId}" role="listbox" ${this._multiple ? 'aria-multiselectable="true"' : ""}>
1108
1118
  <slot id="options-slot"></slot>
1109
1119
  <div class="empty-state">${this._noOptionsMessage}</div>
@@ -1126,6 +1136,10 @@ this.setupMobileEventListeners();
1126
1136
  this.updateSelectionDisplay();
1127
1137
  }
1128
1138
  setupMobileEventListeners() {
1139
+ if (!this._keyboardHandler) {
1140
+ this._keyboardHandler = this.handleKeyboard.bind(this);
1141
+ document.addEventListener("keydown", this._keyboardHandler);
1142
+ }
1129
1143
  const shadow = this.shadowRoot;
1130
1144
  const stub = shadow.querySelector(".select-stub");
1131
1145
  const optionsSlot = shadow.querySelector("#options-slot");
@@ -1145,6 +1159,9 @@ optionsSlot.addEventListener("click", (e) => this.handleMobileTagClick(e));
1145
1159
  if (searchInput) {
1146
1160
  searchInput.addEventListener("input", (e) => this.handleSearchInput(e));
1147
1161
  }
1162
+ shadow.querySelectorAll(".mobile-body .section-header").forEach((header) => {
1163
+ header.addEventListener("click", () => this.expandMobileSection(header.dataset.section === "selected" ? "selected" : "available"));
1164
+ });
1148
1165
  if (closeButton) {
1149
1166
  closeButton.addEventListener("click", () => this.closeMobileModal());
1150
1167
  }
@@ -1183,9 +1200,10 @@ updateMobileSelectedState() {
1183
1200
  if (this._state.mode !== "mobile")
1184
1201
  return;
1185
1202
  const shadow = this.shadowRoot;
1203
+ this.renderMobileSelectedChips();
1186
1204
  const availableSection = shadow.querySelector(".mobile-available-section");
1187
1205
  if (availableSection) {
1188
- const visibleAvailable = this.getTagElements().filter((tag) => !tag.hasAttribute("hidden")).length;
1206
+ const visibleAvailable = this.getTagElements().filter((tag) => !tag.hasAttribute("hidden") && !tag.hasAttribute("selected")).length;
1189
1207
  availableSection.setAttribute("data-empty", String(visibleAvailable === 0));
1190
1208
  const availableTitleSpan = shadow.querySelector(".mobile-available-section .section-title");
1191
1209
  if (availableTitleSpan) {
@@ -1193,14 +1211,59 @@ availableTitleSpan.textContent = `${this._availableLabel} (${visibleAvailable})`
1193
1211
  }
1194
1212
  }
1195
1213
  }
1214
+ expandMobileSection(section) {
1215
+ const shadow = this.shadowRoot;
1216
+ const body = shadow?.querySelector(".mobile-body");
1217
+ if (!body)
1218
+ return;
1219
+ if (section === "selected" && this._state.selectedValues.length === 0)
1220
+ return;
1221
+ body.dataset.expanded = section;
1222
+ shadow.querySelectorAll(".mobile-body .section-header").forEach((header) => {
1223
+ const owns = header.dataset.section === section;
1224
+ header.setAttribute("aria-expanded", String(owns));
1225
+ });
1226
+ }
1227
+ renderMobileSelectedChips() {
1228
+ const shadow = this.shadowRoot;
1229
+ const section = shadow.querySelector(".mobile-selected-section");
1230
+ const host = shadow.querySelector(".mobile-selected-chips");
1231
+ if (!section || !host)
1232
+ return;
1233
+ const selected = this._state.selectedValues;
1234
+ section.setAttribute("data-empty", String(selected.length === 0));
1235
+ const count = section.querySelector(".section-count");
1236
+ if (count)
1237
+ count.textContent = `(${selected.length})`;
1238
+ if (selected.length === 0)
1239
+ this.expandMobileSection("available");
1240
+ const labelFor = (value) => {
1241
+ const option = this.getTagElements().find((tag) => this.getTagData(tag).value === value);
1242
+ return option ? this.getTagData(option).text : value;
1243
+ };
1244
+ host.textContent = "";
1245
+ for (const value of selected) {
1246
+ const chip = document.createElement("button");
1247
+ chip.type = "button";
1248
+ chip.className = "mobile-selected-chip";
1249
+ chip.dataset.value = value;
1250
+ chip.setAttribute("aria-label", `Remove ${labelFor(value)}`);
1251
+ chip.innerHTML = `<span class="chip-label"></span>${CLEAR_ICON_SVG}`;
1252
+ chip.querySelector(".chip-label").textContent = labelFor(value);
1253
+ chip.addEventListener("click", (e) => {
1254
+ e.preventDefault();
1255
+ e.stopPropagation();
1256
+ this.deselectValue(value);
1257
+ });
1258
+ host.appendChild(chip);
1259
+ }
1260
+ }
1196
1261
  shouldShowSearch() {
1197
1262
  if (this._externalSearch)
1198
1263
  return true;
1199
1264
  if (this._allowCreate)
1200
1265
  return true;
1201
- if (this._searchable === "always")
1202
- return true;
1203
- if (this._searchable === "never")
1266
+ if (this._notSearchable)
1204
1267
  return false;
1205
1268
  return this.getTagElements().length > SEARCH_AUTO_THRESHOLD;
1206
1269
  }
@@ -1371,11 +1434,11 @@ return this.getProperty("required");
1371
1434
  set required(value) {
1372
1435
  this.setProperty("required", value);
1373
1436
  }
1374
- get searchable() {
1375
- return this.getProperty("searchable");
1437
+ get notSearchable() {
1438
+ return this.getProperty("notSearchable");
1376
1439
  }
1377
- set searchable(value) {
1378
- this.setProperty("searchable", value);
1440
+ set notSearchable(value) {
1441
+ this.setProperty("notSearchable", value);
1379
1442
  }
1380
1443
  get externalSearch() {
1381
1444
  return this.getProperty("externalSearch");
@@ -1499,17 +1562,10 @@ default: "none",
1499
1562
  aliases: { "create-transform": true },
1500
1563
  coerce: (v) => (v === "slug" ? "slug" : "none"),
1501
1564
  },
1502
- searchable: {
1503
- type: "string",
1565
+ notSearchable: {
1566
+ type: "boolean",
1504
1567
  visual: true,
1505
- default: "auto",
1506
- coerce: (v) => {
1507
- if (v === true || v === "" || v === "true")
1508
- return "always";
1509
- if (v === false || v === "false")
1510
- return "never";
1511
- return "auto";
1512
- },
1568
+ default: false,
1513
1569
  },
1514
1570
  align: {
1515
1571
  type: "string",
@@ -1552,6 +1608,11 @@ type: "string",
1552
1608
  visual: true,
1553
1609
  default: "Available",
1554
1610
  },
1611
+ "selected-label": {
1612
+ type: "string",
1613
+ visual: true,
1614
+ default: "Selected",
1615
+ },
1555
1616
  "no-options-message": {
1556
1617
  type: "string",
1557
1618
  visual: true,
@@ -41,6 +41,11 @@ function findTabIndex(tabs, tabId) {
41
41
  const index = tabs.findIndex(tab => getTabId(tab) === tabId);
42
42
  return index >= 0 ? index : undefined;
43
43
  }
44
+ function currentActive(el) {
45
+ const tabs = getChildTabs(el);
46
+ const id = getActiveTabId(el, tabs);
47
+ return { id, index: (id ? findTabIndex(tabs, id) : undefined) ?? 0 };
48
+ }
44
49
  function getActiveTabId(el, tabs) {
45
50
  const activeAttr = el.getAttribute('active');
46
51
  if (activeAttr && findTabIndex(tabs, activeAttr) !== undefined) {
@@ -467,11 +472,12 @@ if (buttons) {
467
472
  const buttonsHeight = buttons.offsetHeight;
468
473
  el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
469
474
  }
470
- updateTransform(el, activeIndex);
475
+ const current = currentActive(el);
476
+ updateTransform(el, current.index);
471
477
  updateOverflow(el);
472
- if (activeId) {
473
- updateMarker(el, activeId);
474
- scrollActiveIntoView(el, activeId, true);
478
+ if (current.id) {
479
+ updateMarker(el, current.id);
480
+ scrollActiveIntoView(el, current.id, true);
475
481
  }
476
482
  });
477
483
  if (activeId) {
@@ -496,11 +502,12 @@ if (buttons) {
496
502
  const buttonsHeight = buttons.offsetHeight;
497
503
  el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
498
504
  }
499
- updateTransform(el, activeIndex);
505
+ const current = currentActive(el);
506
+ updateTransform(el, current.index);
500
507
  updateOverflow(el);
501
- if (activeId) {
502
- updateMarker(el, activeId);
503
- scrollActiveIntoView(el, activeId, false);
508
+ if (current.id) {
509
+ updateMarker(el, current.id);
510
+ scrollActiveIntoView(el, current.id, false);
504
511
  }
505
512
  });
506
513
  setupEventListeners(el, shadowRoot, tabs);
@@ -39,7 +39,7 @@ border-radius: var(--ty-radius-md);
39
39
  font-family: inherit;
40
40
  user-select: none;
41
41
  width: var(--calendar-width, fit-content);
42
- min-width: 280px;
42
+ min-width: var(--ty-calendar-width, 280px);
43
43
  max-width: var(--calendar-max-width, none);
44
44
  }
45
45
  .calendar-row {
@@ -135,7 +135,7 @@ opacity: 0.7;
135
135
  }
136
136
  .calendar-size-sm {
137
137
  padding: 0.5rem;
138
- min-width: 240px;
138
+ min-width: var(--ty-calendar-width, 240px);
139
139
  }
140
140
  .calendar-size-sm .calendar-header-cell {
141
141
  font-size: 0.625rem;
@@ -150,7 +150,7 @@ min-height: 1.5rem;
150
150
  }
151
151
  .calendar-size-md {
152
152
  padding: 0.75rem;
153
- min-width: 280px;
153
+ min-width: var(--ty-calendar-width, 280px);
154
154
  }
155
155
  .calendar-size-md .calendar-header-cell {
156
156
  font-size: 0.6875rem;
@@ -165,7 +165,7 @@ min-height: 2rem;
165
165
  }
166
166
  .calendar-size-lg {
167
167
  padding: 1rem;
168
- min-width: 360px;
168
+ min-width: var(--ty-calendar-width, 360px);
169
169
  }
170
170
  .calendar-size-lg .calendar-header-cell {
171
171
  font-size: 0.75rem;
@@ -1,2 +1,2 @@
1
1
  /** Calendar Navigation Styles */
2
- export declare const calendarNavigationStyles = "\n/* Theming tokens applied as var(--ty-calendar-nav-*, <default>) at point of\n use (NOT on :host) so consumers can override them from outside \u2014 this\n component is nested inside ty-calendar's shadow, so a :host default would\n block inherited overrides. --ty-calendar-accent still cascades in. */\n\n:host([data-size=\"md\"]) {\n --nav-btn-size: 2rem;\n --nav-btn-icon-size: 1.25rem;\n --nav-font-size: 1rem;\n --nav-padding: 0.5rem 0.75rem;\n --nav-default-width: 280px;\n}\n\n:host([data-size=\"sm\"]) {\n --nav-btn-size: 1.5rem;\n --nav-btn-icon-size: 1rem;\n --nav-font-size: 0.875rem;\n --nav-padding: 0.375rem 0.5rem;\n --nav-default-width: 240px;\n}\n\n:host([data-size=\"lg\"]) {\n --nav-btn-size: 2.5rem;\n --nav-btn-icon-size: 1.5rem;\n --nav-font-size: 1.125rem;\n --nav-padding: 0.625rem 1rem;\n --nav-default-width: 360px;\n}\n\n.calendar-navigation-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--nav-padding);\n font-family: inherit;\n user-select: none;\n width: var(--nav-width, var(--nav-default-width));\n box-sizing: border-box;\n}\n\n/* Navigation groups: left, center, right */\n.nav-group {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.nav-group-left,\n.nav-group-right {\n flex: 0 0 auto;\n}\n\n.nav-group-center {\n flex: 1;\n justify-content: center;\n}\n\n.nav-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--nav-btn-size);\n height: var(--nav-btn-size);\n padding: 0;\n border: none;\n border-radius: var(--ty-radius-base);\n background-color: transparent;\n color: var(--ty-calendar-nav-color, var(--ty-color-neutral));\n cursor: pointer;\n transition: var(--ty-local-transition, all 0.15s ease);\n outline: none;\n}\n\n.nav-btn:hover {\n background-color: var(--ty-calendar-nav-hover-bg, var(--ty-bg-neutral-soft));\n color: var(--ty-calendar-nav-hover-color, var(--ty-color-neutral-strong));\n}\n\n.nav-btn:active {\n background-color: var(--ty-calendar-nav-active-bg, var(--ty-bg-neutral));\n transform: scale(0.95);\n}\n\n.nav-btn:disabled {\n opacity: 0.35;\n cursor: default;\n pointer-events: none;\n}\n\n.nav-btn:focus-visible {\n outline: 2px solid var(--ty-calendar-nav-focus-outline, var(--ty-calendar-accent, var(--ty-color-primary)));\n outline-offset: 2px;\n}\n\n.nav-btn svg {\n width: var(--nav-btn-icon-size);\n height: var(--nav-btn-icon-size);\n display: block;\n}\n\n.month-year-display {\n font-size: var(--nav-font-size);\n font-weight: 600;\n color: var(--ty-calendar-nav-title-color, var(--ty-color-neutral-strong));\n text-align: center;\n white-space: nowrap;\n letter-spacing: -0.01em;\n}\n";
2
+ export declare const calendarNavigationStyles = "\n/* Theming tokens applied as var(--ty-calendar-nav-*, <default>) at point of\n use (NOT on :host) so consumers can override them from outside \u2014 this\n component is nested inside ty-calendar's shadow, so a :host default would\n block inherited overrides. --ty-calendar-accent still cascades in. */\n\n:host([data-size=\"md\"]) {\n --nav-btn-size: 2rem;\n --nav-btn-icon-size: 1.25rem;\n --nav-font-size: 0.875rem;\n --nav-padding: 0.5rem 0.75rem;\n --nav-default-width: 280px;\n}\n\n:host([data-size=\"sm\"]) {\n --nav-btn-size: 1.5rem;\n --nav-btn-icon-size: 1rem;\n --nav-font-size: 0.875rem;\n --nav-padding: 0.375rem 0.5rem;\n --nav-default-width: 240px;\n}\n\n:host([data-size=\"lg\"]) {\n --nav-btn-size: 2.5rem;\n --nav-btn-icon-size: 1.5rem;\n --nav-font-size: 0.875rem;\n --nav-padding: 0.625rem 1rem;\n --nav-default-width: 360px;\n}\n\n.calendar-navigation-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--nav-padding);\n font-family: inherit;\n user-select: none;\n /* --ty-calendar-width is the public knob: set it on ty-calendar (or any\n ancestor) and BOTH the nav and the day grid follow it, because custom\n properties inherit through shadow boundaries. --nav-default-width can't\n serve that role \u2014 it's declared on :host([data-size]) inside this shadow\n root, and a declaration on the element always beats a value inherited\n into it, so an outside override of it is silently dead. --nav-width\n (set inline by the width attribute) still wins over both. */\n width: var(--nav-width, var(--ty-calendar-width, var(--nav-default-width)));\n box-sizing: border-box;\n}\n\n/* Navigation groups: left, center, right */\n.nav-group {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.nav-group-left,\n.nav-group-right {\n flex: 0 0 auto;\n}\n\n.nav-group-center {\n flex: 1;\n justify-content: center;\n /* Flex items default to min-width:auto, i.e. they refuse to shrink below\n their content. A long month name (\"September 2026\") therefore widened\n this group and shoved the arrow groups outward \u2014 the nav visibly\n re-laid-out as you paged month to month. min-width:0 lets the centre\n take exactly the leftover space, so the arrows stay pinned whatever the\n label says, and an over-long label ellipsizes instead of pushing. */\n min-width: 0;\n}\n\n.nav-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--nav-btn-size);\n height: var(--nav-btn-size);\n padding: 0;\n border: none;\n border-radius: var(--ty-radius-base);\n background-color: transparent;\n color: var(--ty-calendar-nav-color, var(--ty-color-neutral));\n cursor: pointer;\n transition: var(--ty-local-transition, all 0.15s ease);\n outline: none;\n}\n\n.nav-btn:hover {\n background-color: var(--ty-calendar-nav-hover-bg, var(--ty-bg-neutral-soft));\n color: var(--ty-calendar-nav-hover-color, var(--ty-color-neutral-strong));\n}\n\n.nav-btn:active {\n background-color: var(--ty-calendar-nav-active-bg, var(--ty-bg-neutral));\n transform: scale(0.95);\n}\n\n.nav-btn:disabled {\n opacity: 0.35;\n cursor: default;\n pointer-events: none;\n}\n\n.nav-btn:focus-visible {\n outline: 2px solid var(--ty-calendar-nav-focus-outline, var(--ty-calendar-accent, var(--ty-color-primary)));\n outline-offset: 2px;\n}\n\n.nav-btn svg {\n width: var(--nav-btn-icon-size);\n height: var(--nav-btn-icon-size);\n display: block;\n}\n\n/* 14px uppercase at every size, not a per-size type scale: the label is the\n widest thing in the bar and the only one whose width depends on the data\n (\"May\" vs \"September\"), so scaling it up on lg was what made the arrows\n shuffle around. Small caps read as chrome, stay legible, and leave the\n centre column comfortably wider than the longest localized month name.\n Ellipsis is a backstop for a narrow --nav-width, not the sizing strategy. */\n.month-year-display {\n font-size: var(--nav-font-size);\n font-weight: 600;\n color: var(--ty-calendar-nav-title-color, var(--ty-color-neutral-strong));\n text-align: center;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n";
@@ -2,7 +2,7 @@ export const calendarNavigationStyles = `
2
2
  :host([data-size="md"]) {
3
3
  --nav-btn-size: 2rem;
4
4
  --nav-btn-icon-size: 1.25rem;
5
- --nav-font-size: 1rem;
5
+ --nav-font-size: 0.875rem;
6
6
  --nav-padding: 0.5rem 0.75rem;
7
7
  --nav-default-width: 280px;
8
8
  }
@@ -16,7 +16,7 @@ export const calendarNavigationStyles = `
16
16
  :host([data-size="lg"]) {
17
17
  --nav-btn-size: 2.5rem;
18
18
  --nav-btn-icon-size: 1.5rem;
19
- --nav-font-size: 1.125rem;
19
+ --nav-font-size: 0.875rem;
20
20
  --nav-padding: 0.625rem 1rem;
21
21
  --nav-default-width: 360px;
22
22
  }
@@ -27,7 +27,7 @@ justify-content: space-between;
27
27
  padding: var(--nav-padding);
28
28
  font-family: inherit;
29
29
  user-select: none;
30
- width: var(--nav-width, var(--nav-default-width));
30
+ width: var(--nav-width, var(--ty-calendar-width, var(--nav-default-width)));
31
31
  box-sizing: border-box;
32
32
  }
33
33
  .nav-group {
@@ -42,6 +42,7 @@ flex: 0 0 auto;
42
42
  .nav-group-center {
43
43
  flex: 1;
44
44
  justify-content: center;
45
+ min-width: 0;
45
46
  }
46
47
  .nav-btn {
47
48
  display: inline-flex;
@@ -86,6 +87,9 @@ font-weight: 600;
86
87
  color: var(--ty-calendar-nav-title-color, var(--ty-color-neutral-strong));
87
88
  text-align: center;
88
89
  white-space: nowrap;
89
- letter-spacing: -0.01em;
90
+ text-transform: uppercase;
91
+ letter-spacing: 0.04em;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
90
94
  }
91
95
  `;
@@ -2,4 +2,4 @@
2
2
  * Calendar Orchestration Styles
3
3
  * Simple container for navigation + month display
4
4
  */
5
- export declare const calendarStyles = "\n.calendar-container {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n box-sizing: border-box;\n font-family: inherit;\n}\n\n@media (max-width: 320px) {\n .calendar-container {\n gap: 0.375rem;\n }\n}\n";
5
+ export declare const calendarStyles = "\n.calendar-container {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n box-sizing: border-box;\n font-family: inherit;\n /* Nav is a fixed width and the grid is fit-content, so in any host wider\n than them (a popup, a stretched cell) the default stretch alignment\n left-pinned both. Centring keeps the block centred in whatever space it\n gets \u2014 without widening the calendar itself to fill it. */\n align-items: center;\n}\n\n@media (max-width: 320px) {\n .calendar-container {\n gap: 0.375rem;\n }\n}\n";
@@ -5,6 +5,7 @@ flex-direction: column;
5
5
  gap: 0.125rem;
6
6
  box-sizing: border-box;
7
7
  font-family: inherit;
8
+ align-items: center;
8
9
  }
9
10
  @media (max-width: 320px) {
10
11
  .calendar-container {
@@ -555,18 +555,26 @@ font-weight: 500;
555
555
  color: var(--ty-text-faint);
556
556
  margin-left: 0.25rem;
557
557
  }
558
- .dropdown-mode-mobile .mobile-selected-section {
558
+ .dropdown-mode-mobile .mobile-body .section-header {
559
+ height: 36px;
560
+ flex: 0 0 36px;
561
+ }
562
+ .dropdown-mode-mobile .mobile-selected-section,
563
+ .dropdown-mode-mobile .mobile-available-section {
559
564
  display: flex;
560
565
  flex-direction: column;
561
566
  background: var(--ty-input-bg);
562
567
  overflow: hidden;
563
- flex: 0 0 auto;
564
- max-height: 40%;
565
- transition: max-height 200ms cubic-bezier(0.4, 0, 0.2, 1);
568
+ min-height: 36px;
569
+ flex: 0 1 36px;
570
+ transition: flex-grow 220ms cubic-bezier(0.4, 0, 0.2, 1);
571
+ }
572
+ .dropdown-mode-mobile .mobile-body[data-expanded="selected"] .mobile-selected-section,
573
+ .dropdown-mode-mobile .mobile-body[data-expanded="available"] .mobile-available-section {
574
+ flex-grow: 1;
566
575
  }
567
576
  .dropdown-mode-mobile .mobile-selected-section[data-empty="true"] {
568
- max-height: 36px;
569
- flex: 0 0 36px;
577
+ flex-grow: 0;
570
578
  }
571
579
  .dropdown-mode-mobile .mobile-selected-section .section-content {
572
580
  flex: 1;
@@ -581,14 +589,6 @@ align-content: flex-start;
581
589
  -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 12px), transparent);
582
590
  mask-image: linear-gradient(to bottom, black calc(100% - 12px), transparent);
583
591
  }
584
- .dropdown-mode-mobile .mobile-available-section {
585
- display: flex;
586
- flex-direction: column;
587
- background: var(--ty-input-bg);
588
- overflow: hidden;
589
- flex: 1 1 auto;
590
- min-height: 0;
591
- }
592
592
  .dropdown-mode-mobile .mobile-available-section .section-content {
593
593
  flex: 1;
594
594
  min-height: 0;
@@ -600,6 +600,73 @@ flex-wrap: wrap;
600
600
  gap: 0.5rem;
601
601
  align-content: flex-start;
602
602
  }
603
+ @media (prefers-reduced-motion: reduce) {
604
+ .dropdown-mode-mobile .mobile-selected-section,
605
+ .dropdown-mode-mobile .mobile-available-section {
606
+ transition: none;
607
+ }
608
+ }
609
+ .dropdown-mode-mobile .mobile-body .section-header {
610
+ display: flex;
611
+ align-items: center;
612
+ justify-content: space-between;
613
+ width: 100%;
614
+ border: none;
615
+ background: transparent;
616
+ font: inherit;
617
+ text-align: left;
618
+ cursor: pointer;
619
+ -webkit-tap-highlight-color: transparent;
620
+ }
621
+ .dropdown-mode-mobile .mobile-body .section-header svg {
622
+ width: 1rem;
623
+ height: 1rem;
624
+ flex-shrink: 0;
625
+ opacity: 0.5;
626
+ transition: transform 180ms ease;
627
+ }
628
+ .dropdown-mode-mobile .mobile-body .section-header[aria-expanded="true"] svg {
629
+ transform: rotate(180deg);
630
+ }
631
+ .dropdown-mode-mobile .mobile-selected-section[data-empty="true"] .section-header {
632
+ cursor: default;
633
+ }
634
+ .dropdown-mode-mobile .mobile-selected-section[data-empty="true"] .section-header svg {
635
+ display: none;
636
+ }
637
+ .dropdown-mode-mobile .mobile-selected-chip {
638
+ display: inline-flex;
639
+ align-items: center;
640
+ gap: 0.375rem;
641
+ padding: 0.25rem 0.5rem 0.25rem 0.625rem;
642
+ border: 1px solid var(--ty-border);
643
+ border-radius: 9999px;
644
+ background: var(--ty-surface-content);
645
+ color: var(--ty-text);
646
+ font: inherit;
647
+ font-size: var(--ty-font-sm);
648
+ line-height: var(--ty-leading-sm);
649
+ cursor: pointer;
650
+ transition: transform 0.1s ease, border-color 0.15s ease;
651
+ animation: ty-select-tag-enter 180ms cubic-bezier(0.16, 1, 0.3, 1);
652
+ }
653
+ .dropdown-mode-mobile .mobile-selected-chip svg {
654
+ width: 0.875rem;
655
+ height: 0.875rem;
656
+ flex-shrink: 0;
657
+ opacity: 0.6;
658
+ }
659
+ @media (hover: hover) {
660
+ .dropdown-mode-mobile .mobile-selected-chip:hover {
661
+ border-color: var(--ty-text-soft);
662
+ }
663
+ .dropdown-mode-mobile .mobile-selected-chip:hover svg {
664
+ opacity: 1;
665
+ }
666
+ }
667
+ .dropdown-mode-mobile .mobile-selected-chip:active {
668
+ transform: scale(0.96);
669
+ }
603
670
  .dropdown-mode-mobile .empty-state {
604
671
  display: none;
605
672
  width: 100%;
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-TC54";
7
+ export declare const VERSION = "1.0.0-TC55";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.0-TC54';
1
+ export const VERSION = '1.0.0-TC55';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyrell-components",
3
- "version": "1.0.0-TC54",
3
+ "version": "1.0.0-TC55",
4
4
  "description": "Tyrell Components - Framework-agnostic web components with semantic design system",
5
5
  "type": "module",
6
6
  "sideEffects": [