tyrell-components 1.0.0-TC53 → 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,
@@ -94,9 +95,7 @@ dialog.close();
94
95
  }
95
96
  });
96
97
  this.render();
97
- requestAnimationFrame(() => {
98
98
  this.initializeState();
99
- });
100
99
  this._childObserver = new MutationObserver(() => {
101
100
  this.ensureOptionRoles();
102
101
  if (this._state.selectedValues.length > 0) {
@@ -187,8 +186,8 @@ break;
187
186
  case "externalSearch":
188
187
  this._externalSearch = newValue;
189
188
  break;
190
- case "searchable":
191
- this._searchable = newValue;
189
+ case "notSearchable":
190
+ this._notSearchable = newValue;
192
191
  if (this.isConnected && this.shadowRoot)
193
192
  this.updateSearchVisibility();
194
193
  break;
@@ -209,6 +208,9 @@ break;
209
208
  case "available-label":
210
209
  this._availableLabel = newValue || "Available";
211
210
  break;
211
+ case "selected-label":
212
+ this._selectedLabel = newValue || "Selected";
213
+ break;
212
214
  case "no-options-message":
213
215
  this._noOptionsMessage = newValue || "No options available";
214
216
  break;
@@ -268,13 +270,23 @@ this.updateSelectionDisplay();
268
270
  this.updateMobileSelectedState();
269
271
  }
270
272
  else {
271
- const allTags = this.getTagElements();
272
- const preSelectedTags = allTags
273
+ this.resolveDeclarativeSelection();
274
+ }
275
+ }
276
+ resolveDeclarativeSelection() {
277
+ if (this.getAttribute("value") || this._state.selectedValues.length > 0) {
278
+ return;
279
+ }
280
+ const preSelectedTags = this.getTagElements()
273
281
  .filter((tag) => tag.hasAttribute("selected"))
274
282
  .map((tag) => this.getTagData(tag).value);
275
283
  if (preSelectedTags.length > 0) {
276
284
  this.updateComponentValue(preSelectedTags, false);
285
+ return;
277
286
  }
287
+ if (document.readyState === "loading") {
288
+ document.addEventListener("DOMContentLoaded", () => { if (this.isConnected)
289
+ this.resolveDeclarativeSelection(); }, { once: true });
278
290
  }
279
291
  }
280
292
  getTagElements() {
@@ -295,10 +307,7 @@ deselectTag(tag) {
295
307
  tag.removeAttribute("selected");
296
308
  }
297
309
  getSelectedValues() {
298
- return this.getTagElements()
299
- .filter((tag) => tag.hasAttribute("selected"))
300
- .map((tag) => this.getTagData(tag).value)
301
- .filter((value) => value !== "");
310
+ return [...this._state.selectedValues];
302
311
  }
303
312
  syncSelectedTags(selectedValues) {
304
313
  const selectedSet = new Set(selectedValues);
@@ -612,7 +621,7 @@ this.closeDropdown();
612
621
  }
613
622
  return;
614
623
  }
615
- if (tag.hasAttribute("selected")) {
624
+ if (currentValues.includes(tagValue)) {
616
625
  const newValues = currentValues.filter((v) => v !== tagValue);
617
626
  this.updateComponentValue(newValues, true, "remove", tagValue);
618
627
  }
@@ -693,6 +702,8 @@ e.stopPropagation();
693
702
  e.preventDefault();
694
703
  }
695
704
  handleSearchInput(e) {
705
+ if (this._state.mode === "mobile")
706
+ this.expandMobileSection("available");
696
707
  const target = e.target;
697
708
  const query = target.value;
698
709
  this._state.search = query;
@@ -725,7 +736,12 @@ switch (e.key) {
725
736
  case "Escape":
726
737
  e.preventDefault();
727
738
  e.stopPropagation();
739
+ if (this._state.mode === "mobile") {
740
+ this.closeMobileModal();
741
+ }
742
+ else {
728
743
  this.closeDropdown();
744
+ }
729
745
  break;
730
746
  case "Enter":
731
747
  e.preventDefault();
@@ -741,43 +757,33 @@ break;
741
757
  case "ArrowUp":
742
758
  e.preventDefault();
743
759
  e.stopPropagation();
744
- let newIndexUp;
745
- if (tagsCount === 0) {
746
- newIndexUp = -1;
747
- }
748
- else if (currentHighlightedIndex === -1) {
749
- newIndexUp = tagsCount - 1;
750
- }
751
- else if (currentHighlightedIndex === 0) {
752
- newIndexUp = tagsCount - 1;
753
- }
754
- else {
755
- newIndexUp = currentHighlightedIndex - 1;
756
- }
760
+ const newIndexUp = this.nextEnabledTagIndex(filteredTags, currentHighlightedIndex, -1);
757
761
  this._state.highlightedIndex = newIndexUp;
758
762
  this.highlightTag(filteredTags, newIndexUp);
759
763
  break;
760
764
  case "ArrowDown":
761
765
  e.preventDefault();
762
766
  e.stopPropagation();
763
- let newIndexDown;
764
- if (tagsCount === 0) {
765
- newIndexDown = -1;
766
- }
767
- else if (currentHighlightedIndex === -1) {
768
- newIndexDown = 0;
769
- }
770
- else if (currentHighlightedIndex === tagsCount - 1) {
771
- newIndexDown = 0;
772
- }
773
- else {
774
- newIndexDown = currentHighlightedIndex + 1;
775
- }
767
+ const newIndexDown = this.nextEnabledTagIndex(filteredTags, currentHighlightedIndex, 1);
776
768
  this._state.highlightedIndex = newIndexDown;
777
769
  this.highlightTag(filteredTags, newIndexDown);
778
770
  break;
779
771
  }
780
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
+ }
781
787
  filterTags(tags, query) {
782
788
  if (!query || query.trim() === "") {
783
789
  return tags;
@@ -1093,12 +1099,21 @@ ${CHEVRON_DOWN_SVG}
1093
1099
  <!-- HEADER (matches dropdown.ts) -->
1094
1100
  ${searchHeaderHtml}
1095
1101
  <!-- BODY: options list with toggleable selected state -->
1096
- <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>
1097
1111
  <!-- AVAILABLE LIST (always visible, takes remaining space) -->
1098
1112
  <div class="mobile-available-section" data-empty="false">
1099
- <div class="section-header">
1113
+ <button class="section-header" type="button" data-section="available" aria-expanded="true">
1100
1114
  <span class="section-title">${this._availableLabel}</span>
1101
- </div>
1115
+ ${CHEVRON_DOWN_SVG}
1116
+ </button>
1102
1117
  <div class="section-content dropdown-options-wrapper" id="${listboxId}" role="listbox" ${this._multiple ? 'aria-multiselectable="true"' : ""}>
1103
1118
  <slot id="options-slot"></slot>
1104
1119
  <div class="empty-state">${this._noOptionsMessage}</div>
@@ -1121,6 +1136,10 @@ this.setupMobileEventListeners();
1121
1136
  this.updateSelectionDisplay();
1122
1137
  }
1123
1138
  setupMobileEventListeners() {
1139
+ if (!this._keyboardHandler) {
1140
+ this._keyboardHandler = this.handleKeyboard.bind(this);
1141
+ document.addEventListener("keydown", this._keyboardHandler);
1142
+ }
1124
1143
  const shadow = this.shadowRoot;
1125
1144
  const stub = shadow.querySelector(".select-stub");
1126
1145
  const optionsSlot = shadow.querySelector("#options-slot");
@@ -1140,6 +1159,9 @@ optionsSlot.addEventListener("click", (e) => this.handleMobileTagClick(e));
1140
1159
  if (searchInput) {
1141
1160
  searchInput.addEventListener("input", (e) => this.handleSearchInput(e));
1142
1161
  }
1162
+ shadow.querySelectorAll(".mobile-body .section-header").forEach((header) => {
1163
+ header.addEventListener("click", () => this.expandMobileSection(header.dataset.section === "selected" ? "selected" : "available"));
1164
+ });
1143
1165
  if (closeButton) {
1144
1166
  closeButton.addEventListener("click", () => this.closeMobileModal());
1145
1167
  }
@@ -1178,9 +1200,10 @@ updateMobileSelectedState() {
1178
1200
  if (this._state.mode !== "mobile")
1179
1201
  return;
1180
1202
  const shadow = this.shadowRoot;
1203
+ this.renderMobileSelectedChips();
1181
1204
  const availableSection = shadow.querySelector(".mobile-available-section");
1182
1205
  if (availableSection) {
1183
- const visibleAvailable = this.getTagElements().filter((tag) => !tag.hasAttribute("hidden")).length;
1206
+ const visibleAvailable = this.getTagElements().filter((tag) => !tag.hasAttribute("hidden") && !tag.hasAttribute("selected")).length;
1184
1207
  availableSection.setAttribute("data-empty", String(visibleAvailable === 0));
1185
1208
  const availableTitleSpan = shadow.querySelector(".mobile-available-section .section-title");
1186
1209
  if (availableTitleSpan) {
@@ -1188,14 +1211,59 @@ availableTitleSpan.textContent = `${this._availableLabel} (${visibleAvailable})`
1188
1211
  }
1189
1212
  }
1190
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
+ }
1191
1261
  shouldShowSearch() {
1192
1262
  if (this._externalSearch)
1193
1263
  return true;
1194
1264
  if (this._allowCreate)
1195
1265
  return true;
1196
- if (this._searchable === "always")
1197
- return true;
1198
- if (this._searchable === "never")
1266
+ if (this._notSearchable)
1199
1267
  return false;
1200
1268
  return this.getTagElements().length > SEARCH_AUTO_THRESHOLD;
1201
1269
  }
@@ -1234,10 +1302,11 @@ clearBtn.hidden = !(count > 0 && this._clearable && !this._disabled && !this._re
1234
1302
  }
1235
1303
  const wantClone = !this._multiple && count > 0 ? values[0] : null;
1236
1304
  this.syncSelectedClone(wantClone);
1237
- stub.classList.toggle("has-clone", wantClone !== null);
1305
+ const hasClone = this._selectedClone !== null;
1306
+ stub.classList.toggle("has-clone", hasClone);
1238
1307
  const textEl = stub.querySelector(".dropdown-placeholder");
1239
1308
  if (textEl) {
1240
- textEl.hidden = wantClone !== null;
1309
+ textEl.hidden = hasClone;
1241
1310
  const showLabels = count > 0 && this._multiple && !this._compact;
1242
1311
  if (showLabels) {
1243
1312
  textEl.textContent = this.getSelectedItems(values)
@@ -1245,6 +1314,10 @@ textEl.textContent = this.getSelectedItems(values)
1245
1314
  .join(", ");
1246
1315
  textEl.classList.remove("placeholder-shown");
1247
1316
  }
1317
+ else if (wantClone !== null) {
1318
+ textEl.textContent = values.join(", ");
1319
+ textEl.classList.remove("placeholder-shown");
1320
+ }
1248
1321
  else {
1249
1322
  textEl.textContent = this._placeholder;
1250
1323
  textEl.classList.add("placeholder-shown");
@@ -1254,9 +1327,14 @@ textEl.classList.add("placeholder-shown");
1254
1327
  syncSelectedClone(value) {
1255
1328
  const existing = this.querySelector(':scope > [cloned][slot="selected"]');
1256
1329
  if (existing?.getAttribute("value") === value) {
1330
+ const stale = value !== null &&
1331
+ existing.childNodes.length === 0 &&
1332
+ (this.getTagElements().find((el) => this.getTagData(el).value === value)?.childNodes.length ?? 0) > 0;
1333
+ if (!stale) {
1257
1334
  this._selectedClone = existing;
1258
1335
  return;
1259
1336
  }
1337
+ }
1260
1338
  existing?.remove();
1261
1339
  if (value === null) {
1262
1340
  this._selectedClone = null;
@@ -1270,6 +1348,10 @@ clone.setAttribute("slot", "selected");
1270
1348
  clone.setAttribute("cloned", "true");
1271
1349
  this.appendChild(clone);
1272
1350
  this._selectedClone = clone;
1351
+ if (option.childNodes.length === 0 && document.readyState === "loading") {
1352
+ document.addEventListener("DOMContentLoaded", () => { if (this.isConnected)
1353
+ this.updateSelectionDisplay(); }, { once: true });
1354
+ }
1273
1355
  }
1274
1356
  else if (this._selectedClone?.getAttribute("value") === value) {
1275
1357
  const restored = this._selectedClone.cloneNode(true);
@@ -1352,11 +1434,11 @@ return this.getProperty("required");
1352
1434
  set required(value) {
1353
1435
  this.setProperty("required", value);
1354
1436
  }
1355
- get searchable() {
1356
- return this.getProperty("searchable");
1437
+ get notSearchable() {
1438
+ return this.getProperty("notSearchable");
1357
1439
  }
1358
- set searchable(value) {
1359
- this.setProperty("searchable", value);
1440
+ set notSearchable(value) {
1441
+ this.setProperty("notSearchable", value);
1360
1442
  }
1361
1443
  get externalSearch() {
1362
1444
  return this.getProperty("externalSearch");
@@ -1480,17 +1562,10 @@ default: "none",
1480
1562
  aliases: { "create-transform": true },
1481
1563
  coerce: (v) => (v === "slug" ? "slug" : "none"),
1482
1564
  },
1483
- searchable: {
1484
- type: "string",
1565
+ notSearchable: {
1566
+ type: "boolean",
1485
1567
  visual: true,
1486
- default: "auto",
1487
- coerce: (v) => {
1488
- if (v === true || v === "" || v === "true")
1489
- return "always";
1490
- if (v === false || v === "false")
1491
- return "never";
1492
- return "auto";
1493
- },
1568
+ default: false,
1494
1569
  },
1495
1570
  align: {
1496
1571
  type: "string",
@@ -1533,6 +1608,11 @@ type: "string",
1533
1608
  visual: true,
1534
1609
  default: "Available",
1535
1610
  },
1611
+ "selected-label": {
1612
+ type: "string",
1613
+ visual: true,
1614
+ default: "Selected",
1615
+ },
1536
1616
  "no-options-message": {
1537
1617
  type: "string",
1538
1618
  visual: true,
@@ -29,6 +29,10 @@ if (!this.style.display)
29
29
  this.style.display = 'contents';
30
30
  this.bind();
31
31
  this.renderChips();
32
+ if (document.readyState === 'loading') {
33
+ document.addEventListener('DOMContentLoaded', () => { if (this.isConnected)
34
+ this.renderChips(); }, { once: true });
35
+ }
32
36
  this._childObserver = new MutationObserver(() => {
33
37
  const template = this.template();
34
38
  if (template !== this._lastTemplate) {
@@ -2,6 +2,7 @@ import { ensureStyles } from '../utils/styles.js';
2
2
  import { tabsStyles } from '../styles/tabs.js';
3
3
  const eventHandlers = new WeakMap();
4
4
  const resizeObservers = new WeakMap();
5
+ const pendingInitialSnap = new WeakSet();
5
6
  function getTabsAttributes(el) {
6
7
  return {
7
8
  width: el.getAttribute('width') || '100%',
@@ -40,6 +41,11 @@ function findTabIndex(tabs, tabId) {
40
41
  const index = tabs.findIndex(tab => getTabId(tab) === tabId);
41
42
  return index >= 0 ? index : undefined;
42
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
+ }
43
49
  function getActiveTabId(el, tabs) {
44
50
  const activeAttr = el.getAttribute('active');
45
51
  if (activeAttr && findTabIndex(tabs, activeAttr) !== undefined) {
@@ -171,17 +177,13 @@ const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
171
177
  const buttonsContainer = shadowRoot.querySelector('.tab-buttons');
172
178
  if (!button || !buttonsContainer)
173
179
  return null;
174
- const buttonRect = button.getBoundingClientRect();
175
180
  const left = button.offsetLeft;
176
181
  const top = button.offsetTop;
177
- return {
178
- left,
179
- top,
180
- width: buttonRect.width,
181
- height: buttonRect.height,
182
- };
182
+ const width = button.offsetWidth;
183
+ const height = button.offsetHeight;
184
+ return { left, top, width, height };
183
185
  }
184
- function updateMarker(el, activeId) {
186
+ function updateMarker(el, activeId, forceSnap = false) {
185
187
  const shadowRoot = el.shadowRoot;
186
188
  if (!shadowRoot)
187
189
  return;
@@ -191,7 +193,7 @@ return;
191
193
  const position = calculateMarkerPosition(el, shadowRoot, activeId);
192
194
  if (!position)
193
195
  return;
194
- const snap = !marker.style.left || marker.offsetWidth === 0;
196
+ const snap = forceSnap || !marker.style.left || marker.offsetWidth === 0;
195
197
  if (snap)
196
198
  marker.style.transition = 'none';
197
199
  marker.style.left = `${position.left}px`;
@@ -216,7 +218,7 @@ el.style.setProperty('--active-index', String(newIndex));
216
218
  updateTransform(el, newIndex);
217
219
  updateAriaAttributes(el, shadowRoot, tabId);
218
220
  updatePanelInteraction(el, tabId);
219
- updateMarker(el, tabId);
221
+ updateMarker(el, tabId, pendingInitialSnap.delete(el));
220
222
  shadowRoot.querySelectorAll('.tab-overflow-item').forEach((item) => {
221
223
  item.setAttribute('data-active', String(item.dataset.tabId === tabId));
222
224
  });
@@ -470,11 +472,12 @@ if (buttons) {
470
472
  const buttonsHeight = buttons.offsetHeight;
471
473
  el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
472
474
  }
473
- updateTransform(el, activeIndex);
475
+ const current = currentActive(el);
476
+ updateTransform(el, current.index);
474
477
  updateOverflow(el);
475
- if (activeId) {
476
- updateMarker(el, activeId);
477
- scrollActiveIntoView(el, activeId, true);
478
+ if (current.id) {
479
+ updateMarker(el, current.id);
480
+ scrollActiveIntoView(el, current.id, true);
478
481
  }
479
482
  });
480
483
  if (activeId) {
@@ -482,6 +485,7 @@ updatePanelInteraction(el, activeId);
482
485
  }
483
486
  }
484
487
  else {
488
+ pendingInitialSnap.add(el);
485
489
  shadowRoot.innerHTML = `
486
490
  <div class="tabs-container" data-placement="${placement}"${fixed ? ' data-fixed' : ''}>
487
491
  ${renderTabButtons(el, tabs, activeId)}
@@ -498,11 +502,12 @@ if (buttons) {
498
502
  const buttonsHeight = buttons.offsetHeight;
499
503
  el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
500
504
  }
501
- updateTransform(el, activeIndex);
505
+ const current = currentActive(el);
506
+ updateTransform(el, current.index);
502
507
  updateOverflow(el);
503
- if (activeId) {
504
- updateMarker(el, activeId);
505
- scrollActiveIntoView(el, activeId, false);
508
+ if (current.id) {
509
+ updateMarker(el, current.id);
510
+ scrollActiveIntoView(el, current.id, false);
506
511
  }
507
512
  });
508
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 {