tyrell-components 1.0.0-TC49 → 1.0.0-TC51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tyrell.js +1 -1
- package/lib/components/modal.d.ts +2 -0
- package/lib/components/select.js +5 -1
- package/lib/components/selected-tags.d.ts +1 -1
- package/lib/components/selected-tags.js +7 -5
- package/lib/components/tabs.js +62 -64
- package/lib/styles/select-base.js +9 -0
- package/lib/styles/tabs.d.ts +1 -1
- package/lib/styles/tabs.js +85 -17
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
|
@@ -52,8 +52,10 @@ export declare class TyModal extends HTMLElement {
|
|
|
52
52
|
set open(v: boolean);
|
|
53
53
|
get backdrop(): boolean;
|
|
54
54
|
set backdrop(v: boolean);
|
|
55
|
+
/** @deprecated Use preventOutsideClick — prevent-* wins when both are set. */
|
|
55
56
|
get closeOnOutsideClick(): boolean;
|
|
56
57
|
set closeOnOutsideClick(v: boolean);
|
|
58
|
+
/** @deprecated Use preventEscape — prevent-* wins when both are set. */
|
|
57
59
|
get closeOnEscape(): boolean;
|
|
58
60
|
set closeOnEscape(v: boolean);
|
|
59
61
|
get preventOutsideClick(): boolean;
|
package/lib/components/select.js
CHANGED
|
@@ -895,7 +895,11 @@ export class TySelect extends TyComponent {
|
|
|
895
895
|
const slot = shadow.querySelector('slot[name="trigger"]');
|
|
896
896
|
if (!stub || !slot)
|
|
897
897
|
return;
|
|
898
|
-
const sync = () =>
|
|
898
|
+
const sync = () => {
|
|
899
|
+
const hasTrigger = slot.assignedElements().length > 0;
|
|
900
|
+
stub.classList.toggle("custom-trigger", hasTrigger);
|
|
901
|
+
this.toggleAttribute("has-trigger", hasTrigger);
|
|
902
|
+
};
|
|
899
903
|
slot.addEventListener("slotchange", sync);
|
|
900
904
|
sync();
|
|
901
905
|
}
|
|
@@ -11,7 +11,7 @@ export declare class TySelectedOptions extends HTMLElement {
|
|
|
11
11
|
private _childObserver;
|
|
12
12
|
private _pickerObserver;
|
|
13
13
|
private _lastTemplate;
|
|
14
|
-
private
|
|
14
|
+
private _pickerOpen;
|
|
15
15
|
private _onPickerOpen;
|
|
16
16
|
private _onPickerClose;
|
|
17
17
|
connectedCallback(): void;
|
|
@@ -12,13 +12,13 @@ export class TySelectedOptions extends HTMLElement {
|
|
|
12
12
|
this._childObserver = null;
|
|
13
13
|
this._pickerObserver = null;
|
|
14
14
|
this._lastTemplate = null;
|
|
15
|
-
this.
|
|
15
|
+
this._pickerOpen = false;
|
|
16
16
|
this._onPickerOpen = () => {
|
|
17
|
-
this.
|
|
18
|
-
this.style.display = 'none';
|
|
17
|
+
this._pickerOpen = true;
|
|
19
18
|
};
|
|
20
19
|
this._onPickerClose = () => {
|
|
21
|
-
this.
|
|
20
|
+
this._pickerOpen = false;
|
|
21
|
+
this.renderChips();
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
static get observedAttributes() {
|
|
@@ -81,7 +81,7 @@ export class TySelectedOptions extends HTMLElement {
|
|
|
81
81
|
this._pickerObserver?.disconnect();
|
|
82
82
|
this._pickerObserver = null;
|
|
83
83
|
this._picker = null;
|
|
84
|
-
this.
|
|
84
|
+
this._pickerOpen = false;
|
|
85
85
|
}
|
|
86
86
|
values() {
|
|
87
87
|
return parseValues(this._picker?.value ?? '');
|
|
@@ -156,6 +156,8 @@ export class TySelectedOptions extends HTMLElement {
|
|
|
156
156
|
return tag;
|
|
157
157
|
}
|
|
158
158
|
renderChips() {
|
|
159
|
+
if (this._pickerOpen)
|
|
160
|
+
return;
|
|
159
161
|
const template = this.template();
|
|
160
162
|
this._lastTemplate = template;
|
|
161
163
|
if (!this._picker) {
|
package/lib/components/tabs.js
CHANGED
|
@@ -16,8 +16,11 @@ function getChildTabs(el) {
|
|
|
16
16
|
function getTabId(tab) {
|
|
17
17
|
return tab.getAttribute('id');
|
|
18
18
|
}
|
|
19
|
+
function getSlotLabel(tabsEl, tabId) {
|
|
20
|
+
return tabsEl.querySelector(`:scope > [slot='label-${tabId}']`);
|
|
21
|
+
}
|
|
19
22
|
function hasSlotLabel(tabsEl, tabId) {
|
|
20
|
-
return tabsEl
|
|
23
|
+
return getSlotLabel(tabsEl, tabId) !== null;
|
|
21
24
|
}
|
|
22
25
|
function getTabLabelType(tabsEl, tab) {
|
|
23
26
|
const tabId = getTabId(tab);
|
|
@@ -137,7 +140,7 @@ function updateAriaAttributes(el, shadowRoot, activeId) {
|
|
|
137
140
|
button.setAttribute('aria-selected', String(isActive));
|
|
138
141
|
button.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
139
142
|
button.setAttribute('data-active', String(isActive));
|
|
140
|
-
const slottedLabel = el
|
|
143
|
+
const slottedLabel = getSlotLabel(el, tabId);
|
|
141
144
|
if (slottedLabel) {
|
|
142
145
|
slottedLabel.setAttribute('data-active', String(isActive));
|
|
143
146
|
}
|
|
@@ -236,10 +239,11 @@ function setupResizeObserver(el) {
|
|
|
236
239
|
if (activeIndex !== undefined) {
|
|
237
240
|
updateTransform(el, activeIndex);
|
|
238
241
|
}
|
|
242
|
+
updateOverflow(el);
|
|
239
243
|
if (activeId) {
|
|
240
244
|
updateMarker(el, activeId);
|
|
245
|
+
scrollActiveIntoView(el, activeId, false);
|
|
241
246
|
}
|
|
242
|
-
updateOverflow(el);
|
|
243
247
|
});
|
|
244
248
|
observer.observe(el);
|
|
245
249
|
resizeObservers.set(el, observer);
|
|
@@ -275,11 +279,13 @@ function renderTabButtons(tabsEl, tabs, activeId) {
|
|
|
275
279
|
}).join('');
|
|
276
280
|
return `
|
|
277
281
|
<div class="tab-buttons" role="tablist" part="buttons-container">
|
|
278
|
-
<div class="
|
|
279
|
-
|
|
280
|
-
|
|
282
|
+
<div class="tab-strip" part="strip">
|
|
283
|
+
<div class="marker-wrapper" part="marker-wrapper">
|
|
284
|
+
${!hasCustomMarker(tabsEl) ? '<div class="default-marker"></div>' : ''}
|
|
285
|
+
<slot name="marker"></slot>
|
|
286
|
+
</div>
|
|
287
|
+
${buttons}
|
|
281
288
|
</div>
|
|
282
|
-
${buttons}
|
|
283
289
|
</div>
|
|
284
290
|
`;
|
|
285
291
|
}
|
|
@@ -306,11 +312,10 @@ function renderButtonsOnly(tabsEl, tabs, activeId) {
|
|
|
306
312
|
</button>`;
|
|
307
313
|
}).join('');
|
|
308
314
|
}
|
|
309
|
-
const OVERFLOW_TRIGGER_WIDTH = 40;
|
|
310
315
|
function getTabMenuLabel(tabsEl, tab) {
|
|
311
316
|
const tabId = getTabId(tab);
|
|
312
317
|
if (tabId && getTabLabelType(tabsEl, tab) === 'slot') {
|
|
313
|
-
const slotted = tabsEl
|
|
318
|
+
const slotted = getSlotLabel(tabsEl, tabId);
|
|
314
319
|
return slotted?.textContent?.trim() || tabId;
|
|
315
320
|
}
|
|
316
321
|
return tab.getAttribute('label') || 'Tab';
|
|
@@ -318,29 +323,27 @@ function getTabMenuLabel(tabsEl, tab) {
|
|
|
318
323
|
function removeOverflowMenu(shadowRoot) {
|
|
319
324
|
shadowRoot.querySelector('.tab-overflow-trigger')?.remove();
|
|
320
325
|
}
|
|
321
|
-
function renderOverflowMenu(el, container,
|
|
322
|
-
|
|
323
|
-
return;
|
|
324
|
-
const tabsById = new Map(allTabs.map((tab) => [getTabId(tab), tab]));
|
|
326
|
+
function renderOverflowMenu(el, container, allTabs) {
|
|
327
|
+
const activeId = getActiveTabId(el, allTabs);
|
|
325
328
|
const trigger = document.createElement('button');
|
|
326
329
|
trigger.className = 'tab-overflow-trigger';
|
|
327
330
|
trigger.type = 'button';
|
|
328
|
-
trigger.setAttribute('aria-label', `${
|
|
331
|
+
trigger.setAttribute('aria-label', `${allTabs.length} tabs`);
|
|
329
332
|
trigger.textContent = '⋯';
|
|
330
333
|
const popup = document.createElement('ty-popup');
|
|
331
334
|
popup.setAttribute('placement', 'bottom');
|
|
332
335
|
const menu = document.createElement('div');
|
|
333
336
|
menu.className = 'tab-overflow-menu';
|
|
334
337
|
menu.setAttribute('role', 'menu');
|
|
335
|
-
|
|
336
|
-
const tabId =
|
|
337
|
-
|
|
338
|
-
if (!tabId || !tab)
|
|
338
|
+
allTabs.forEach((tab) => {
|
|
339
|
+
const tabId = getTabId(tab);
|
|
340
|
+
if (!tabId)
|
|
339
341
|
return;
|
|
340
342
|
const item = document.createElement('button');
|
|
341
343
|
item.type = 'button';
|
|
342
344
|
item.className = 'tab-overflow-item';
|
|
343
345
|
item.setAttribute('role', 'menuitem');
|
|
346
|
+
item.setAttribute('data-active', String(tabId === activeId));
|
|
344
347
|
item.textContent = getTabMenuLabel(el, tab);
|
|
345
348
|
if (isTabDisabled(tab)) {
|
|
346
349
|
item.disabled = true;
|
|
@@ -362,48 +365,41 @@ function updateOverflow(el) {
|
|
|
362
365
|
if (!shadowRoot)
|
|
363
366
|
return;
|
|
364
367
|
const container = shadowRoot.querySelector('.tab-buttons');
|
|
365
|
-
|
|
368
|
+
const strip = shadowRoot.querySelector('.tab-strip');
|
|
369
|
+
if (!container || !strip)
|
|
366
370
|
return;
|
|
367
371
|
removeOverflowMenu(shadowRoot);
|
|
368
|
-
|
|
369
|
-
buttons.forEach((b) => b.classList.remove('overflow-hidden'));
|
|
370
|
-
if (buttons.length === 0)
|
|
372
|
+
if (strip.clientWidth <= 0)
|
|
371
373
|
return;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
+
updateEdgeFades(strip);
|
|
375
|
+
const buttons = strip.querySelectorAll('.tab-button');
|
|
376
|
+
const last = buttons[buttons.length - 1];
|
|
377
|
+
const contentWidth = last ? last.offsetLeft + last.offsetWidth : 0;
|
|
378
|
+
const overflowing = contentWidth > strip.clientWidth + 1;
|
|
379
|
+
strip.classList.toggle('unclipped', !overflowing);
|
|
380
|
+
if (!overflowing)
|
|
374
381
|
return;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (
|
|
382
|
+
renderOverflowMenu(el, container, getChildTabs(el));
|
|
383
|
+
}
|
|
384
|
+
function updateEdgeFades(strip) {
|
|
385
|
+
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');
|
|
388
|
+
}
|
|
389
|
+
function scrollActiveIntoView(el, activeId, smooth) {
|
|
390
|
+
const shadowRoot = el.shadowRoot;
|
|
391
|
+
if (!shadowRoot)
|
|
385
392
|
return;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
cutoff = Math.max(cutoff, 1);
|
|
396
|
-
const tabs = getChildTabs(el);
|
|
397
|
-
const activeId = getActiveTabId(el, tabs);
|
|
398
|
-
const hidden = new Set(buttons.slice(cutoff));
|
|
399
|
-
const activeButton = activeId ? buttons.find((b) => b.dataset.tabId === activeId) : undefined;
|
|
400
|
-
if (activeButton && hidden.has(activeButton)) {
|
|
401
|
-
const lastVisible = buttons[cutoff - 1];
|
|
402
|
-
hidden.delete(activeButton);
|
|
403
|
-
hidden.add(lastVisible);
|
|
393
|
+
const strip = shadowRoot.querySelector('.tab-strip');
|
|
394
|
+
const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
|
|
395
|
+
if (!strip || !button || strip.clientWidth <= 0)
|
|
396
|
+
return;
|
|
397
|
+
const centered = button.offsetLeft + button.offsetWidth / 2 - strip.clientWidth / 2;
|
|
398
|
+
const target = Math.max(0, Math.min(centered, strip.scrollWidth - strip.clientWidth));
|
|
399
|
+
if (Math.round(target) !== Math.round(strip.scrollLeft)) {
|
|
400
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
401
|
+
strip.scrollTo({ left: target, behavior: smooth && !reduce ? 'smooth' : 'auto' });
|
|
404
402
|
}
|
|
405
|
-
buttons.forEach((b) => b.classList.toggle('overflow-hidden', hidden.has(b)));
|
|
406
|
-
renderOverflowMenu(el, container, buttons.filter((b) => hidden.has(b)), tabs);
|
|
407
403
|
}
|
|
408
404
|
function render(el) {
|
|
409
405
|
const shadowRoot = el.shadowRoot;
|
|
@@ -423,27 +419,25 @@ function render(el) {
|
|
|
423
419
|
});
|
|
424
420
|
const existingContainer = shadowRoot.querySelector('.tabs-container');
|
|
425
421
|
const existingButtons = shadowRoot.querySelector('.tab-buttons');
|
|
422
|
+
const existingStrip = shadowRoot.querySelector('.tab-strip');
|
|
426
423
|
const existingViewport = shadowRoot.querySelector('.panels-viewport');
|
|
427
424
|
ensureStyles(shadowRoot, { css: tabsStyles, id: 'ty-tabs' });
|
|
428
|
-
if (tabs.length > 7) {
|
|
429
|
-
console.warn(`[ty-tabs] More than 7 tabs detected (${tabs.length} tabs). ` +
|
|
430
|
-
'This may cause overflow and poor UX. ' +
|
|
431
|
-
'Consider using sidebar navigation, accordion menu, or other patterns.');
|
|
432
|
-
}
|
|
433
425
|
el.style.setProperty('--tabs-width', width.includes('%') ? '100%' : width);
|
|
434
426
|
el.style.setProperty('--tabs-height', height);
|
|
435
427
|
el.style.setProperty('--active-index', String(activeIndex));
|
|
436
|
-
if (existingContainer && existingButtons && existingViewport) {
|
|
428
|
+
if (existingContainer && existingButtons && existingStrip && existingViewport) {
|
|
437
429
|
existingContainer.setAttribute('data-placement', placement);
|
|
438
430
|
existingButtons.querySelector('.tab-overflow-trigger')?.remove();
|
|
439
|
-
const
|
|
431
|
+
const savedScrollLeft = existingStrip.scrollLeft;
|
|
432
|
+
const allButtons = existingStrip.querySelectorAll('.tab-button');
|
|
440
433
|
allButtons.forEach(button => button.remove());
|
|
441
434
|
const buttonsHtml = renderButtonsOnly(el, tabs, activeId);
|
|
442
435
|
const tempDiv = document.createElement('div');
|
|
443
436
|
tempDiv.innerHTML = buttonsHtml;
|
|
444
437
|
Array.from(tempDiv.children).forEach(button => {
|
|
445
|
-
|
|
438
|
+
existingStrip.appendChild(button);
|
|
446
439
|
});
|
|
440
|
+
existingStrip.scrollLeft = savedScrollLeft;
|
|
447
441
|
setupEventListeners(el, shadowRoot, tabs);
|
|
448
442
|
updateAriaAttributes(el, shadowRoot, activeId || '');
|
|
449
443
|
requestAnimationFrame(() => {
|
|
@@ -453,10 +447,11 @@ function render(el) {
|
|
|
453
447
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
454
448
|
}
|
|
455
449
|
updateTransform(el, activeIndex);
|
|
450
|
+
updateOverflow(el);
|
|
456
451
|
if (activeId) {
|
|
457
452
|
updateMarker(el, activeId);
|
|
453
|
+
scrollActiveIntoView(el, activeId, true);
|
|
458
454
|
}
|
|
459
|
-
updateOverflow(el);
|
|
460
455
|
});
|
|
461
456
|
if (activeId) {
|
|
462
457
|
updatePanelInteraction(el, activeId);
|
|
@@ -480,14 +475,17 @@ function render(el) {
|
|
|
480
475
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
481
476
|
}
|
|
482
477
|
updateTransform(el, activeIndex);
|
|
478
|
+
updateOverflow(el);
|
|
483
479
|
if (activeId) {
|
|
484
480
|
updateMarker(el, activeId);
|
|
481
|
+
scrollActiveIntoView(el, activeId, false);
|
|
485
482
|
}
|
|
486
|
-
updateOverflow(el);
|
|
487
483
|
});
|
|
488
484
|
setupEventListeners(el, shadowRoot, tabs);
|
|
489
485
|
updateAriaAttributes(el, shadowRoot, activeId || '');
|
|
490
486
|
setupResizeObserver(el);
|
|
487
|
+
const strip = shadowRoot.querySelector('.tab-strip');
|
|
488
|
+
strip?.addEventListener('scroll', () => updateEdgeFades(strip), { passive: true });
|
|
491
489
|
if (activeId) {
|
|
492
490
|
updatePanelInteraction(el, activeId);
|
|
493
491
|
}
|
|
@@ -44,6 +44,15 @@ export const selectBaseStyles = `
|
|
|
44
44
|
width: auto;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/* A custom slot="trigger" is the same situation as compact: the trigger is
|
|
48
|
+
the consumer's own button, not a form field — hug it so chips/toolbar
|
|
49
|
+
items can sit right next to it. [has-trigger] is reflected by
|
|
50
|
+
setupTriggerSlot() because Blink does not parse :has() inside :host(). */
|
|
51
|
+
:host([has-trigger]) {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
width: auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
.dropdown-dialog {
|
|
48
57
|
position: fixed;
|
|
49
58
|
width: var(--dropdown-width, 200px);
|
package/lib/styles/tabs.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Tabs Component Styles — ty-tabs container: top/bottom placement, animated
|
|
3
3
|
* active marker, carousel viewport. Customizable via CSS Parts (::part).
|
|
4
4
|
*/
|
|
5
|
-
export declare const tabsStyles = "\n:host {\n display: block;\n width: var(--tabs-width, 100%);\n height: var(--tabs-height, 400px);\n box-sizing: border-box;\n}\n\n.tabs-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n}\n\n.tabs-container[data-placement=\"bottom\"] {\n flex-direction: column-reverse;\n}\n\n/* Tab buttons container \u2014 exposed as a CSS Part for full styling control */\n.tab-buttons {\n display: flex;\n gap: 0;\n flex-shrink: 0;\n position: relative;\n
|
|
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";
|
package/lib/styles/tabs.js
CHANGED
|
@@ -25,16 +25,72 @@ export const tabsStyles = `
|
|
|
25
25
|
gap: 0;
|
|
26
26
|
flex-shrink: 0;
|
|
27
27
|
position: relative;
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
|
|
30
29
|
/* Default minimal styling - customize via ::part(buttons-container) */
|
|
31
|
-
border-bottom: 1px solid var(--ty-border);
|
|
32
30
|
background: transparent;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
/* Separator drawn as a pseudo-element, not a border: a border sits OUTSIDE the
|
|
34
|
+
strip's box, so the marker (absolutely positioned inside the strip) could only
|
|
35
|
+
ever stop just above it, reading as two stacked lines. As an overlaid line it
|
|
36
|
+
lands inside the marker's range, and .tab-strip's z-index lets the marker
|
|
37
|
+
cover it. */
|
|
38
|
+
.tab-buttons::after {
|
|
39
|
+
content: "";
|
|
40
|
+
position: absolute;
|
|
41
|
+
left: 0;
|
|
42
|
+
right: 0;
|
|
43
|
+
bottom: 0;
|
|
44
|
+
height: 1px;
|
|
45
|
+
/* --tabs-separator: transparent removes the line (e.g. with a pill marker) */
|
|
46
|
+
background: var(--tabs-separator, var(--ty-border));
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.tabs-container[data-placement="bottom"] .tab-buttons::after {
|
|
51
|
+
bottom: auto;
|
|
52
|
+
top: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Scrollable strip holding the buttons + marker. Tabs never collapse — the
|
|
56
|
+
strip scrolls (scrollbar hidden; touch/wheel still work) and activation
|
|
57
|
+
glides the active tab into view. The "…" trigger sits OUTSIDE this strip,
|
|
58
|
+
pinned at the edge. position:relative makes it the offsetParent for the
|
|
59
|
+
marker, so the marker scrolls with the buttons and can't desync. */
|
|
60
|
+
.tab-strip {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex: 1 1 auto;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
position: relative;
|
|
65
|
+
/* Paints the marker + buttons over .tab-buttons::after (the separator line) */
|
|
66
|
+
z-index: 1;
|
|
67
|
+
overflow-x: auto;
|
|
68
|
+
scrollbar-width: none;
|
|
69
|
+
/* Edge fades — a mask, not overlays, so it works over any background.
|
|
70
|
+
--fade-left/right are set from the scroll listener: 0px at a hard edge,
|
|
71
|
+
28px when more tabs continue past that side. */
|
|
72
|
+
--fade-left: 0px;
|
|
73
|
+
--fade-right: 0px;
|
|
74
|
+
-webkit-mask-image: linear-gradient(to right,
|
|
75
|
+
transparent, black var(--fade-left),
|
|
76
|
+
black calc(100% - var(--fade-right)), transparent);
|
|
77
|
+
mask-image: linear-gradient(to right,
|
|
78
|
+
transparent, black var(--fade-left),
|
|
79
|
+
black calc(100% - var(--fade-right)), transparent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.tab-strip::-webkit-scrollbar {
|
|
83
|
+
display: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* When every tab fits there is nothing to scroll, so drop the clipping —
|
|
87
|
+
both overflow and the mask cut a custom marker's box-shadow off at the
|
|
88
|
+
strip edge. Set by updateOverflow(); clipping stays the pre-measurement
|
|
89
|
+
default so overflowing tabs can't spill for a frame on first render. */
|
|
90
|
+
.tab-strip.unclipped {
|
|
91
|
+
overflow: visible;
|
|
92
|
+
-webkit-mask-image: none;
|
|
93
|
+
mask-image: none;
|
|
38
94
|
}
|
|
39
95
|
|
|
40
96
|
/* Marker wrapper — exposed as a CSS Part for custom marker styling */
|
|
@@ -54,12 +110,18 @@ export const tabsStyles = `
|
|
|
54
110
|
position: absolute;
|
|
55
111
|
bottom: 0;
|
|
56
112
|
left: 0;
|
|
57
|
-
height:
|
|
113
|
+
height: 3px;
|
|
58
114
|
width: 100%;
|
|
59
115
|
background: var(--ty-color-primary);
|
|
60
116
|
pointer-events: none;
|
|
61
117
|
}
|
|
62
118
|
|
|
119
|
+
/* Bottom placement: marker rides the top edge, next to the content it marks */
|
|
120
|
+
.tabs-container[data-placement="bottom"] .default-marker {
|
|
121
|
+
bottom: auto;
|
|
122
|
+
top: 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
63
125
|
/* Hide default marker when custom marker is slotted */
|
|
64
126
|
.marker-wrapper:has(::slotted([slot="marker"])) .default-marker {
|
|
65
127
|
display: none;
|
|
@@ -109,31 +171,32 @@ export const tabsStyles = `
|
|
|
109
171
|
outline-offset: -2px;
|
|
110
172
|
}
|
|
111
173
|
|
|
112
|
-
|
|
113
|
-
display: none;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* Overflow "more" trigger + menu */
|
|
174
|
+
/* Overflow "more" trigger (pill) + menu */
|
|
117
175
|
.tab-overflow-trigger {
|
|
118
176
|
flex-shrink: 0;
|
|
119
|
-
|
|
120
|
-
min-width:
|
|
121
|
-
|
|
122
|
-
|
|
177
|
+
align-self: center;
|
|
178
|
+
min-width: 36px;
|
|
179
|
+
height: 24px;
|
|
180
|
+
margin: 0 8px;
|
|
181
|
+
padding: 0 10px;
|
|
182
|
+
border: 1px solid var(--ty-border);
|
|
183
|
+
border-radius: 9999px;
|
|
123
184
|
background: transparent;
|
|
124
185
|
cursor: pointer;
|
|
125
186
|
color: var(--ty-text-soft);
|
|
126
|
-
font-size:
|
|
187
|
+
font-size: 16px;
|
|
127
188
|
line-height: 1;
|
|
128
189
|
display: inline-flex;
|
|
129
190
|
align-items: center;
|
|
130
191
|
justify-content: center;
|
|
131
192
|
position: relative;
|
|
132
193
|
z-index: 10;
|
|
194
|
+
transition: color 200ms, border-color 200ms;
|
|
133
195
|
}
|
|
134
196
|
|
|
135
197
|
.tab-overflow-trigger:hover {
|
|
136
198
|
color: var(--ty-text-strong);
|
|
199
|
+
border-color: var(--ty-text-soft);
|
|
137
200
|
}
|
|
138
201
|
|
|
139
202
|
.tab-overflow-trigger:focus-visible {
|
|
@@ -171,6 +234,11 @@ export const tabsStyles = `
|
|
|
171
234
|
color: var(--ty-text-strong);
|
|
172
235
|
}
|
|
173
236
|
|
|
237
|
+
.tab-overflow-item[data-active="true"] {
|
|
238
|
+
color: var(--ty-color-primary);
|
|
239
|
+
font-weight: var(--ty-font-bold);
|
|
240
|
+
}
|
|
241
|
+
|
|
174
242
|
.tab-overflow-item[disabled] {
|
|
175
243
|
opacity: 0.5;
|
|
176
244
|
cursor: not-allowed;
|
package/lib/version.d.ts
CHANGED
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.0-
|
|
1
|
+
export const VERSION = '1.0.0-TC51';
|