tyrell-components 1.0.0-TC51 → 1.0.0-TC52

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.
@@ -11,6 +11,7 @@ export interface TabsAttributes {
11
11
  height: string;
12
12
  active: string | null;
13
13
  placement: 'top' | 'bottom';
14
+ fixed: boolean;
14
15
  }
15
16
  /**
16
17
  * Tab change event detail
@@ -8,6 +8,7 @@ function getTabsAttributes(el) {
8
8
  height: el.getAttribute('height') || '400px',
9
9
  active: el.getAttribute('active'),
10
10
  placement: (el.getAttribute('placement') || 'top'),
11
+ fixed: el.hasAttribute('fixed'),
11
12
  };
12
13
  }
13
14
  function getChildTabs(el) {
@@ -216,6 +217,9 @@ function updateActiveTabState(el, tabId, previousId) {
216
217
  updateAriaAttributes(el, shadowRoot, tabId);
217
218
  updatePanelInteraction(el, tabId);
218
219
  updateMarker(el, tabId);
220
+ shadowRoot.querySelectorAll('.tab-overflow-item').forEach((item) => {
221
+ item.setAttribute('data-active', String(item.dataset.tabId === tabId));
222
+ });
219
223
  const newPanel = tabs[newIndex];
220
224
  if (newPanel?.resetScroll) {
221
225
  newPanel.resetScroll();
@@ -274,7 +278,7 @@ function renderTabButtons(tabsEl, tabs, activeId) {
274
278
  data-active="${active}"
275
279
  ${disabled ? 'disabled aria-disabled="true"' : ''}
276
280
  >
277
- ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
281
+ ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
278
282
  </button>`;
279
283
  }).join('');
280
284
  return `
@@ -308,7 +312,7 @@ function renderButtonsOnly(tabsEl, tabs, activeId) {
308
312
  data-active="${active}"
309
313
  ${disabled ? 'disabled aria-disabled="true"' : ''}
310
314
  >
311
- ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
315
+ ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
312
316
  </button>`;
313
317
  }).join('');
314
318
  }
@@ -343,6 +347,7 @@ function renderOverflowMenu(el, container, allTabs) {
343
347
  item.type = 'button';
344
348
  item.className = 'tab-overflow-item';
345
349
  item.setAttribute('role', 'menuitem');
350
+ item.setAttribute('data-tab-id', tabId);
346
351
  item.setAttribute('data-active', String(tabId === activeId));
347
352
  item.textContent = getTabMenuLabel(el, tab);
348
353
  if (isTabDisabled(tab)) {
@@ -368,23 +373,41 @@ function updateOverflow(el) {
368
373
  const strip = shadowRoot.querySelector('.tab-strip');
369
374
  if (!container || !strip)
370
375
  return;
376
+ if (getTabsAttributes(el).fixed) {
377
+ removeOverflowMenu(shadowRoot);
378
+ strip.classList.add('unclipped');
379
+ strip.style.removeProperty('--fade-left');
380
+ strip.style.removeProperty('--fade-right');
381
+ return;
382
+ }
383
+ const savedScrollLeft = strip.scrollLeft;
371
384
  removeOverflowMenu(shadowRoot);
372
385
  if (strip.clientWidth <= 0)
373
386
  return;
374
- updateEdgeFades(strip);
375
387
  const buttons = strip.querySelectorAll('.tab-button');
376
388
  const last = buttons[buttons.length - 1];
377
389
  const contentWidth = last ? last.offsetLeft + last.offsetWidth : 0;
378
- const overflowing = contentWidth > strip.clientWidth + 1;
379
- strip.classList.toggle('unclipped', !overflowing);
380
- if (!overflowing)
390
+ const stripWidth = strip.clientWidth;
391
+ const hidden = contentWidth - stripWidth;
392
+ strip.classList.toggle('unclipped', hidden <= 1);
393
+ if (hidden <= 1) {
394
+ strip.scrollLeft = savedScrollLeft;
395
+ updateEdgeFades(strip);
381
396
  return;
397
+ }
382
398
  renderOverflowMenu(el, container, getChildTabs(el));
399
+ const triggerCost = container.clientWidth - strip.clientWidth;
400
+ if (hidden <= triggerCost) {
401
+ removeOverflowMenu(shadowRoot);
402
+ }
403
+ strip.scrollLeft = savedScrollLeft;
404
+ updateEdgeFades(strip);
383
405
  }
384
406
  function updateEdgeFades(strip) {
385
407
  const maxScroll = strip.scrollWidth - strip.clientWidth;
386
- strip.style.setProperty('--fade-left', strip.scrollLeft > 1 ? '28px' : '0px');
387
- strip.style.setProperty('--fade-right', strip.scrollLeft < maxScroll - 1 ? '28px' : '0px');
408
+ const fade = (distance) => `${distance > 1 ? Math.min(28, distance) : 0}px`;
409
+ strip.style.setProperty('--fade-left', fade(strip.scrollLeft));
410
+ strip.style.setProperty('--fade-right', fade(maxScroll - strip.scrollLeft));
388
411
  }
389
412
  function scrollActiveIntoView(el, activeId, smooth) {
390
413
  const shadowRoot = el.shadowRoot;
@@ -405,7 +428,7 @@ function render(el) {
405
428
  const shadowRoot = el.shadowRoot;
406
429
  if (!shadowRoot)
407
430
  return;
408
- const { width, height, placement } = getTabsAttributes(el);
431
+ const { width, height, placement, fixed } = getTabsAttributes(el);
409
432
  const tabs = getChildTabs(el);
410
433
  const activeId = getActiveTabId(el, tabs);
411
434
  const activeIndex = activeId ? (findTabIndex(tabs, activeId) ?? 0) : 0;
@@ -427,8 +450,9 @@ function render(el) {
427
450
  el.style.setProperty('--active-index', String(activeIndex));
428
451
  if (existingContainer && existingButtons && existingStrip && existingViewport) {
429
452
  existingContainer.setAttribute('data-placement', placement);
430
- existingButtons.querySelector('.tab-overflow-trigger')?.remove();
453
+ existingContainer.toggleAttribute('data-fixed', fixed);
431
454
  const savedScrollLeft = existingStrip.scrollLeft;
455
+ existingButtons.querySelector('.tab-overflow-trigger')?.remove();
432
456
  const allButtons = existingStrip.querySelectorAll('.tab-button');
433
457
  allButtons.forEach(button => button.remove());
434
458
  const buttonsHtml = renderButtonsOnly(el, tabs, activeId);
@@ -459,7 +483,7 @@ function render(el) {
459
483
  }
460
484
  else {
461
485
  shadowRoot.innerHTML = `
462
- <div class="tabs-container" data-placement="${placement}">
486
+ <div class="tabs-container" data-placement="${placement}"${fixed ? ' data-fixed' : ''}>
463
487
  ${renderTabButtons(el, tabs, activeId)}
464
488
  <div class="panels-viewport" part="panels-container">
465
489
  <div class="panels-wrapper">
@@ -509,7 +533,7 @@ function cleanup(el) {
509
533
  }
510
534
  export class TyTabs extends HTMLElement {
511
535
  static get observedAttributes() {
512
- return ['width', 'height', 'active', 'placement'];
536
+ return ['width', 'height', 'active', 'placement', 'fixed'];
513
537
  }
514
538
  constructor() {
515
539
  super();
@@ -538,10 +562,14 @@ export class TyTabs extends HTMLElement {
538
562
  }
539
563
  attributeChangedCallback(name, oldValue, newValue) {
540
564
  if (name === 'active') {
541
- if (newValue) {
542
- updateActiveTabState(this, newValue, oldValue);
565
+ if (!newValue)
566
+ return;
567
+ if (!this.shadowRoot?.querySelector(`[data-tab-id='${newValue}']`)) {
568
+ render(this);
569
+ return;
543
570
  }
544
- render(this);
571
+ updateActiveTabState(this, newValue, oldValue);
572
+ scrollActiveIntoView(this, newValue, true);
545
573
  }
546
574
  else {
547
575
  render(this);
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Tab Component Styles
3
3
  */
4
- export declare const tabStyles = "\n:host {\n width: var(--tabs-width, 100%);\n height: calc(var(--tabs-height, 400px) - var(--buttons-height, 48px));\n display: block;\n box-sizing: border-box;\n flex-shrink: 0;\n transition: opacity var(--transition-duration, 400ms) var(--transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0ms !important;\n }\n}\n\n.tab-wrapper {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.tab-panel {\n width: 100%;\n height: 100%;\n overflow: auto;\n box-sizing: border-box;\n}\n\n.tab-panel.ty-custom-scroll {\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n.tab-panel.ty-custom-scroll::-webkit-scrollbar {\n display: none;\n}\n\n.tab-wrapper:hover .ty-scrollbar-track-y.has-overflow {\n opacity: 1;\n}\n\n\n/* Vertical track */\n.ty-scrollbar-track-y {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-y.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-y.has-overflow:hover,\n.ty-scrollbar-track-y.has-overflow.scrolling,\n.ty-scrollbar-track-y.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-y::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-y:hover::before,\n.ty-scrollbar-track-y.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-y {\n position: absolute;\n right: 0;\n width: 100%;\n min-height: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-y:hover,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-y:active,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Horizontal track */\n.ty-scrollbar-track-x {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-x.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-x.has-overflow:hover,\n.ty-scrollbar-track-x.has-overflow.scrolling,\n.ty-scrollbar-track-x.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-x::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-x:hover::before,\n.ty-scrollbar-track-x.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-x {\n position: absolute;\n bottom: 0;\n height: 100%;\n min-width: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-x:hover,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-x:active,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Touch devices: hide the custom scrollbar */\n@media (pointer: coarse) and (hover: none) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x {\n display: none !important;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x,\n .ty-scrollbar-thumb-y,\n .ty-scrollbar-thumb-x {\n transition-duration: 0ms !important;\n }\n}\n\n";
4
+ export declare const tabStyles = "\n:host {\n width: var(--tabs-width, 100%);\n height: calc(var(--tabs-height, 400px) - var(--buttons-height, 48px));\n display: block;\n box-sizing: border-box;\n flex-shrink: 0;\n transition: opacity var(--ty-tabs-transition-duration, 400ms) var(--ty-tabs-transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0ms !important;\n }\n}\n\n.tab-wrapper {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.tab-panel {\n width: 100%;\n height: 100%;\n overflow: auto;\n box-sizing: border-box;\n}\n\n.tab-panel.ty-custom-scroll {\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n.tab-panel.ty-custom-scroll::-webkit-scrollbar {\n display: none;\n}\n\n.tab-wrapper:hover .ty-scrollbar-track-y.has-overflow {\n opacity: 1;\n}\n\n\n/* Vertical track */\n.ty-scrollbar-track-y {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-y.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-y.has-overflow:hover,\n.ty-scrollbar-track-y.has-overflow.scrolling,\n.ty-scrollbar-track-y.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-y::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-y:hover::before,\n.ty-scrollbar-track-y.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-y {\n position: absolute;\n right: 0;\n width: 100%;\n min-height: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-y:hover,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-y:active,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Horizontal track */\n.ty-scrollbar-track-x {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-x.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-x.has-overflow:hover,\n.ty-scrollbar-track-x.has-overflow.scrolling,\n.ty-scrollbar-track-x.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-x::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-x:hover::before,\n.ty-scrollbar-track-x.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-x {\n position: absolute;\n bottom: 0;\n height: 100%;\n min-width: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-x:hover,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-x:active,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Touch devices: hide the custom scrollbar */\n@media (pointer: coarse) and (hover: none) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x {\n display: none !important;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x,\n .ty-scrollbar-thumb-y,\n .ty-scrollbar-thumb-x {\n transition-duration: 0ms !important;\n }\n}\n\n";
package/lib/styles/tab.js CHANGED
@@ -6,7 +6,7 @@ export const tabStyles = `
6
6
  display: block;
7
7
  box-sizing: border-box;
8
8
  flex-shrink: 0;
9
- transition: opacity var(--transition-duration, 400ms) var(--transition-easing, ease-in-out);
9
+ transition: opacity var(--ty-tabs-transition-duration, 400ms) var(--ty-tabs-transition-easing, ease-in-out);
10
10
  }
11
11
 
12
12
  @media (prefers-reduced-motion: reduce) {
@@ -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\n /* Default minimal styling - customize via ::part(buttons-container) */\n background: transparent;\n}\n\n/* Separator drawn as a pseudo-element, not a border: a border sits OUTSIDE the\n strip's box, so the marker (absolutely positioned inside the strip) could only\n ever stop just above it, reading as two stacked lines. As an overlaid line it\n lands inside the marker's range, and .tab-strip's z-index lets the marker\n cover it. */\n.tab-buttons::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 1px;\n /* --tabs-separator: transparent removes the line (e.g. with a pill marker) */\n background: var(--tabs-separator, var(--ty-border));\n pointer-events: none;\n}\n\n.tabs-container[data-placement=\"bottom\"] .tab-buttons::after {\n bottom: auto;\n top: 0;\n}\n\n/* Scrollable strip holding the buttons + marker. Tabs never collapse \u2014 the\n strip scrolls (scrollbar hidden; touch/wheel still work) and activation\n glides the active tab into view. The \"\u2026\" trigger sits OUTSIDE this strip,\n pinned at the edge. position:relative makes it the offsetParent for the\n marker, so the marker scrolls with the buttons and can't desync. */\n.tab-strip {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n position: relative;\n /* Paints the marker + buttons over .tab-buttons::after (the separator line) */\n z-index: 1;\n overflow-x: auto;\n scrollbar-width: none;\n /* Edge fades \u2014 a mask, not overlays, so it works over any background.\n --fade-left/right are set from the scroll listener: 0px at a hard edge,\n 28px when more tabs continue past that side. */\n --fade-left: 0px;\n --fade-right: 0px;\n -webkit-mask-image: linear-gradient(to right,\n transparent, black var(--fade-left),\n black calc(100% - var(--fade-right)), transparent);\n mask-image: linear-gradient(to right,\n transparent, black var(--fade-left),\n black calc(100% - var(--fade-right)), transparent);\n}\n\n.tab-strip::-webkit-scrollbar {\n display: none;\n}\n\n/* When every tab fits there is nothing to scroll, so drop the clipping \u2014\n both overflow and the mask cut a custom marker's box-shadow off at the\n strip edge. Set by updateOverflow(); clipping stays the pre-measurement\n default so overflowing tabs can't spill for a frame on first render. */\n.tab-strip.unclipped {\n overflow: visible;\n -webkit-mask-image: none;\n mask-image: none;\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: 3px;\n width: 100%;\n background: var(--ty-color-primary);\n pointer-events: none;\n}\n\n/* Bottom placement: marker rides the top edge, next to the content it marks */\n.tabs-container[data-placement=\"bottom\"] .default-marker {\n bottom: auto;\n top: 0;\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/* Overflow \"more\" trigger (pill) + menu */\n.tab-overflow-trigger {\n flex-shrink: 0;\n align-self: center;\n min-width: 36px;\n height: 24px;\n margin: 0 8px;\n padding: 0 10px;\n border: 1px solid var(--ty-border);\n border-radius: 9999px;\n background: transparent;\n cursor: pointer;\n color: var(--ty-text-soft);\n font-size: 16px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n transition: color 200ms, border-color 200ms;\n}\n\n.tab-overflow-trigger:hover {\n color: var(--ty-text-strong);\n border-color: var(--ty-text-soft);\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[data-active=\"true\"] {\n color: var(--ty-color-primary);\n font-weight: var(--ty-font-bold);\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";
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\n /* Default minimal styling - customize via ::part(buttons-container) */\n background: transparent;\n}\n\n/* Separator drawn as a pseudo-element, not a border: a border sits OUTSIDE the\n strip's box, so the marker (absolutely positioned inside the strip) could only\n ever stop just above it, reading as two stacked lines. As an overlaid line it\n lands inside the marker's range, and .tab-strip's z-index lets the marker\n cover it. */\n.tab-buttons::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 1px;\n /* --ty-tabs-separator: transparent removes the line (e.g. with a pill marker) */\n background: var(--ty-tabs-separator, var(--ty-border));\n pointer-events: none;\n}\n\n.tabs-container[data-placement=\"bottom\"] .tab-buttons::after {\n bottom: auto;\n top: 0;\n}\n\n/* Scrollable strip holding the buttons + marker. Tabs never collapse \u2014 the\n strip scrolls (scrollbar hidden; touch/wheel still work) and activation\n glides the active tab into view. The \"\u2026\" trigger sits OUTSIDE this strip,\n pinned at the edge. position:relative makes it the offsetParent for the\n marker, so the marker scrolls with the buttons and can't desync. */\n.tab-strip {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n position: relative;\n /* Paints the marker + buttons over .tab-buttons::after (the separator line) */\n z-index: 1;\n overflow-x: auto;\n scrollbar-width: none;\n /* Edge fades \u2014 a mask, not overlays, so it works over any background.\n --fade-left/right are set from the scroll listener: 0px at a hard edge,\n 28px when more tabs continue past that side. */\n --fade-left: 0px;\n --fade-right: 0px;\n -webkit-mask-image: linear-gradient(to right,\n transparent, black var(--fade-left),\n black calc(100% - var(--fade-right)), transparent);\n mask-image: linear-gradient(to right,\n transparent, black var(--fade-left),\n black calc(100% - var(--fade-right)), transparent);\n}\n\n.tab-strip::-webkit-scrollbar {\n display: none;\n}\n\n/* When every tab fits there is nothing to scroll, so drop the clipping \u2014\n both overflow and the mask cut a custom marker's box-shadow off at the\n strip edge. Set by updateOverflow(); clipping stays the pre-measurement\n default so overflowing tabs can't spill for a frame on first render. */\n.tab-strip.unclipped {\n overflow: visible;\n -webkit-mask-image: none;\n mask-image: none;\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(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),\n width var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),\n height var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),\n top var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out);\n}\n\n.default-marker {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 3px;\n width: 100%;\n background: var(--ty-color-primary);\n pointer-events: none;\n}\n\n/* Bottom placement: marker rides the top edge, next to the content it marks */\n.tabs-container[data-placement=\"bottom\"] .default-marker {\n bottom: auto;\n top: 0;\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 /* Material's floor for SCROLLABLE tabs. 120px is the fixed-tab number and it\n manufactured overflow: five short labels claimed 600px whether they needed\n it or not. Equal-width rhythm is what the \"fixed\" attribute is for. */\n min-width: var(--ty-tab-min-width, 72px);\n /* Scrollable tabs keep their natural width and let the strip scroll \u2014 that is\n the whole contract. Without this they are flex items with the default\n shrink of 1, so they squeeze toward the floor and their labels ellipsize\n before the strip ever overflows. Squeezing is what \"fixed\" is for. */\n flex-shrink: 0;\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/* Fixed tabs: the bar is divided between them instead of scrolling. Material\n calls this \"fixed tabs\" \u2014 for a small, known set of short labels. No\n min-width floor, no overflow, no \"\u2026\". */\n.tabs-container[data-fixed] .tab-button {\n flex: 1 1 0;\n min-width: 0;\n}\n\n/* Fixed tabs only. A scrollable tab is never narrower than its label, so\n ellipsizing there would hide text the user could simply scroll to. */\n.tabs-container[data-fixed] .tab-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\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/* Overflow \"more\" trigger (pill) + menu */\n.tab-overflow-trigger {\n flex-shrink: 0;\n align-self: center;\n min-width: 36px;\n height: 24px;\n margin: 0 8px;\n padding: 0 10px;\n border: 1px solid var(--ty-border);\n border-radius: 9999px;\n background: transparent;\n cursor: pointer;\n color: var(--ty-text-soft);\n font-size: 16px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n transition: color 200ms, border-color 200ms;\n}\n\n.tab-overflow-trigger:hover {\n color: var(--ty-text-strong);\n border-color: var(--ty-text-soft);\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[data-active=\"true\"] {\n color: var(--ty-color-primary);\n font-weight: var(--ty-font-bold);\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(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-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";
@@ -42,8 +42,8 @@ export const tabsStyles = `
42
42
  right: 0;
43
43
  bottom: 0;
44
44
  height: 1px;
45
- /* --tabs-separator: transparent removes the line (e.g. with a pill marker) */
46
- background: var(--tabs-separator, var(--ty-border));
45
+ /* --ty-tabs-separator: transparent removes the line (e.g. with a pill marker) */
46
+ background: var(--ty-tabs-separator, var(--ty-border));
47
47
  pointer-events: none;
48
48
  }
49
49
 
@@ -100,10 +100,10 @@ export const tabsStyles = `
100
100
  /* Behind buttons */
101
101
  pointer-events: none;
102
102
  /* Don't block clicks */
103
- transition: left var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),
104
- width var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),
105
- height var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),
106
- top var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);
103
+ transition: left var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),
104
+ width var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),
105
+ height var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),
106
+ top var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out);
107
107
  }
108
108
 
109
109
  .default-marker {
@@ -122,11 +122,6 @@ export const tabsStyles = `
122
122
  top: 0;
123
123
  }
124
124
 
125
- /* Hide default marker when custom marker is slotted */
126
- .marker-wrapper:has(::slotted([slot="marker"])) .default-marker {
127
- display: none;
128
- }
129
-
130
125
  ::slotted([slot="marker"]) {
131
126
  display: block;
132
127
  width: 100%;
@@ -135,7 +130,15 @@ export const tabsStyles = `
135
130
  }
136
131
 
137
132
  .tab-button {
138
- min-width: 120px;
133
+ /* Material's floor for SCROLLABLE tabs. 120px is the fixed-tab number and it
134
+ manufactured overflow: five short labels claimed 600px whether they needed
135
+ it or not. Equal-width rhythm is what the "fixed" attribute is for. */
136
+ min-width: var(--ty-tab-min-width, 72px);
137
+ /* Scrollable tabs keep their natural width and let the strip scroll — that is
138
+ the whole contract. Without this they are flex items with the default
139
+ shrink of 1, so they squeeze toward the floor and their labels ellipsize
140
+ before the strip ever overflows. Squeezing is what "fixed" is for. */
141
+ flex-shrink: 0;
139
142
  padding: 6px 12px;
140
143
  border: none;
141
144
  background: transparent;
@@ -156,6 +159,22 @@ export const tabsStyles = `
156
159
  /* Above marker */
157
160
  }
158
161
 
162
+ /* Fixed tabs: the bar is divided between them instead of scrolling. Material
163
+ calls this "fixed tabs" — for a small, known set of short labels. No
164
+ min-width floor, no overflow, no "…". */
165
+ .tabs-container[data-fixed] .tab-button {
166
+ flex: 1 1 0;
167
+ min-width: 0;
168
+ }
169
+
170
+ /* Fixed tabs only. A scrollable tab is never narrower than its label, so
171
+ ellipsizing there would hide text the user could simply scroll to. */
172
+ .tabs-container[data-fixed] .tab-label {
173
+ overflow: hidden;
174
+ text-overflow: ellipsis;
175
+ white-space: nowrap;
176
+ }
177
+
159
178
  .tab-button[aria-selected=true] {
160
179
  color: var(--ty-text-strong);
161
180
  }
@@ -258,7 +277,7 @@ export const tabsStyles = `
258
277
  .panels-wrapper {
259
278
  display: flex;
260
279
  height: 100%;
261
- transition: transform var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);
280
+ transition: transform var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out);
262
281
  }
263
282
 
264
283
  @media (prefers-reduced-motion: reduce) {
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-TC51";
7
+ export declare const VERSION = "1.0.0-TC52";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.0-TC51';
1
+ export const VERSION = '1.0.0-TC52';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyrell-components",
3
- "version": "1.0.0-TC51",
3
+ "version": "1.0.0-TC52",
4
4
  "description": "Tyrell Components - Framework-agnostic web components with semantic design system",
5
5
  "type": "module",
6
6
  "sideEffects": [