tyrell-components 1.0.0-TC50 → 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.
@@ -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 = () => stub.classList.toggle("custom-trigger", slot.assignedElements().length > 0);
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 _restoreDisplay;
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._restoreDisplay = 'contents';
15
+ this._pickerOpen = false;
16
16
  this._onPickerOpen = () => {
17
- this._restoreDisplay = this.style.display || 'contents';
18
- this.style.display = 'none';
17
+ this._pickerOpen = true;
19
18
  };
20
19
  this._onPickerClose = () => {
21
- this.style.display = this._restoreDisplay;
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.style.display = this._restoreDisplay;
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) {
@@ -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) {
@@ -16,8 +17,11 @@ function getChildTabs(el) {
16
17
  function getTabId(tab) {
17
18
  return tab.getAttribute('id');
18
19
  }
20
+ function getSlotLabel(tabsEl, tabId) {
21
+ return tabsEl.querySelector(`:scope > [slot='label-${tabId}']`);
22
+ }
19
23
  function hasSlotLabel(tabsEl, tabId) {
20
- return tabsEl.querySelector(`[slot='label-${tabId}']`) !== null;
24
+ return getSlotLabel(tabsEl, tabId) !== null;
21
25
  }
22
26
  function getTabLabelType(tabsEl, tab) {
23
27
  const tabId = getTabId(tab);
@@ -137,7 +141,7 @@ function updateAriaAttributes(el, shadowRoot, activeId) {
137
141
  button.setAttribute('aria-selected', String(isActive));
138
142
  button.setAttribute('tabindex', isActive ? '0' : '-1');
139
143
  button.setAttribute('data-active', String(isActive));
140
- const slottedLabel = el.querySelector(`[slot='label-${tabId}']`);
144
+ const slottedLabel = getSlotLabel(el, tabId);
141
145
  if (slottedLabel) {
142
146
  slottedLabel.setAttribute('data-active', String(isActive));
143
147
  }
@@ -213,6 +217,9 @@ function updateActiveTabState(el, tabId, previousId) {
213
217
  updateAriaAttributes(el, shadowRoot, tabId);
214
218
  updatePanelInteraction(el, tabId);
215
219
  updateMarker(el, tabId);
220
+ shadowRoot.querySelectorAll('.tab-overflow-item').forEach((item) => {
221
+ item.setAttribute('data-active', String(item.dataset.tabId === tabId));
222
+ });
216
223
  const newPanel = tabs[newIndex];
217
224
  if (newPanel?.resetScroll) {
218
225
  newPanel.resetScroll();
@@ -236,10 +243,11 @@ function setupResizeObserver(el) {
236
243
  if (activeIndex !== undefined) {
237
244
  updateTransform(el, activeIndex);
238
245
  }
246
+ updateOverflow(el);
239
247
  if (activeId) {
240
248
  updateMarker(el, activeId);
249
+ scrollActiveIntoView(el, activeId, false);
241
250
  }
242
- updateOverflow(el);
243
251
  });
244
252
  observer.observe(el);
245
253
  resizeObservers.set(el, observer);
@@ -270,16 +278,18 @@ function renderTabButtons(tabsEl, tabs, activeId) {
270
278
  data-active="${active}"
271
279
  ${disabled ? 'disabled aria-disabled="true"' : ''}
272
280
  >
273
- ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
281
+ ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
274
282
  </button>`;
275
283
  }).join('');
276
284
  return `
277
285
  <div class="tab-buttons" role="tablist" part="buttons-container">
278
- <div class="marker-wrapper" part="marker-wrapper">
279
- ${!hasCustomMarker(tabsEl) ? '<div class="default-marker"></div>' : ''}
280
- <slot name="marker"></slot>
286
+ <div class="tab-strip" part="strip">
287
+ <div class="marker-wrapper" part="marker-wrapper">
288
+ ${!hasCustomMarker(tabsEl) ? '<div class="default-marker"></div>' : ''}
289
+ <slot name="marker"></slot>
290
+ </div>
291
+ ${buttons}
281
292
  </div>
282
- ${buttons}
283
293
  </div>
284
294
  `;
285
295
  }
@@ -302,15 +312,14 @@ function renderButtonsOnly(tabsEl, tabs, activeId) {
302
312
  data-active="${active}"
303
313
  ${disabled ? 'disabled aria-disabled="true"' : ''}
304
314
  >
305
- ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
315
+ ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
306
316
  </button>`;
307
317
  }).join('');
308
318
  }
309
- const OVERFLOW_TRIGGER_WIDTH = 40;
310
319
  function getTabMenuLabel(tabsEl, tab) {
311
320
  const tabId = getTabId(tab);
312
321
  if (tabId && getTabLabelType(tabsEl, tab) === 'slot') {
313
- const slotted = tabsEl.querySelector(`[slot='label-${tabId}']`);
322
+ const slotted = getSlotLabel(tabsEl, tabId);
314
323
  return slotted?.textContent?.trim() || tabId;
315
324
  }
316
325
  return tab.getAttribute('label') || 'Tab';
@@ -318,29 +327,28 @@ function getTabMenuLabel(tabsEl, tab) {
318
327
  function removeOverflowMenu(shadowRoot) {
319
328
  shadowRoot.querySelector('.tab-overflow-trigger')?.remove();
320
329
  }
321
- function renderOverflowMenu(el, container, hiddenButtons, allTabs) {
322
- if (hiddenButtons.length === 0)
323
- return;
324
- const tabsById = new Map(allTabs.map((tab) => [getTabId(tab), tab]));
330
+ function renderOverflowMenu(el, container, allTabs) {
331
+ const activeId = getActiveTabId(el, allTabs);
325
332
  const trigger = document.createElement('button');
326
333
  trigger.className = 'tab-overflow-trigger';
327
334
  trigger.type = 'button';
328
- trigger.setAttribute('aria-label', `${hiddenButtons.length} more tabs`);
335
+ trigger.setAttribute('aria-label', `${allTabs.length} tabs`);
329
336
  trigger.textContent = '⋯';
330
337
  const popup = document.createElement('ty-popup');
331
338
  popup.setAttribute('placement', 'bottom');
332
339
  const menu = document.createElement('div');
333
340
  menu.className = 'tab-overflow-menu';
334
341
  menu.setAttribute('role', 'menu');
335
- hiddenButtons.forEach((btn) => {
336
- const tabId = btn.dataset.tabId;
337
- const tab = tabId ? tabsById.get(tabId) : undefined;
338
- if (!tabId || !tab)
342
+ allTabs.forEach((tab) => {
343
+ const tabId = getTabId(tab);
344
+ if (!tabId)
339
345
  return;
340
346
  const item = document.createElement('button');
341
347
  item.type = 'button';
342
348
  item.className = 'tab-overflow-item';
343
349
  item.setAttribute('role', 'menuitem');
350
+ item.setAttribute('data-tab-id', tabId);
351
+ item.setAttribute('data-active', String(tabId === activeId));
344
352
  item.textContent = getTabMenuLabel(el, tab);
345
353
  if (isTabDisabled(tab)) {
346
354
  item.disabled = true;
@@ -362,54 +370,65 @@ function updateOverflow(el) {
362
370
  if (!shadowRoot)
363
371
  return;
364
372
  const container = shadowRoot.querySelector('.tab-buttons');
365
- if (!container)
373
+ const strip = shadowRoot.querySelector('.tab-strip');
374
+ if (!container || !strip)
366
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;
367
384
  removeOverflowMenu(shadowRoot);
368
- const buttons = Array.from(container.querySelectorAll('.tab-button'));
369
- buttons.forEach((b) => b.classList.remove('overflow-hidden'));
370
- if (buttons.length === 0)
385
+ if (strip.clientWidth <= 0)
371
386
  return;
372
- const available = container.clientWidth;
373
- if (available <= 0)
387
+ const buttons = strip.querySelectorAll('.tab-button');
388
+ const last = buttons[buttons.length - 1];
389
+ const contentWidth = last ? last.offsetLeft + last.offsetWidth : 0;
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);
374
396
  return;
375
- let total = 0;
376
- let cutoff = buttons.length;
377
- for (let i = 0; i < buttons.length; i++) {
378
- total += buttons[i].offsetWidth;
379
- if (total > available) {
380
- cutoff = i;
381
- break;
382
- }
383
397
  }
384
- if (cutoff >= buttons.length)
385
- return;
386
- total = 0;
387
- cutoff = buttons.length;
388
- for (let i = 0; i < buttons.length; i++) {
389
- total += buttons[i].offsetWidth;
390
- if (total > available - OVERFLOW_TRIGGER_WIDTH) {
391
- cutoff = i;
392
- break;
393
- }
398
+ renderOverflowMenu(el, container, getChildTabs(el));
399
+ const triggerCost = container.clientWidth - strip.clientWidth;
400
+ if (hidden <= triggerCost) {
401
+ removeOverflowMenu(shadowRoot);
394
402
  }
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);
403
+ strip.scrollLeft = savedScrollLeft;
404
+ updateEdgeFades(strip);
405
+ }
406
+ function updateEdgeFades(strip) {
407
+ const maxScroll = strip.scrollWidth - strip.clientWidth;
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));
411
+ }
412
+ function scrollActiveIntoView(el, activeId, smooth) {
413
+ const shadowRoot = el.shadowRoot;
414
+ if (!shadowRoot)
415
+ return;
416
+ const strip = shadowRoot.querySelector('.tab-strip');
417
+ const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
418
+ if (!strip || !button || strip.clientWidth <= 0)
419
+ return;
420
+ const centered = button.offsetLeft + button.offsetWidth / 2 - strip.clientWidth / 2;
421
+ const target = Math.max(0, Math.min(centered, strip.scrollWidth - strip.clientWidth));
422
+ if (Math.round(target) !== Math.round(strip.scrollLeft)) {
423
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
424
+ strip.scrollTo({ left: target, behavior: smooth && !reduce ? 'smooth' : 'auto' });
404
425
  }
405
- buttons.forEach((b) => b.classList.toggle('overflow-hidden', hidden.has(b)));
406
- renderOverflowMenu(el, container, buttons.filter((b) => hidden.has(b)), tabs);
407
426
  }
408
427
  function render(el) {
409
428
  const shadowRoot = el.shadowRoot;
410
429
  if (!shadowRoot)
411
430
  return;
412
- const { width, height, placement } = getTabsAttributes(el);
431
+ const { width, height, placement, fixed } = getTabsAttributes(el);
413
432
  const tabs = getChildTabs(el);
414
433
  const activeId = getActiveTabId(el, tabs);
415
434
  const activeIndex = activeId ? (findTabIndex(tabs, activeId) ?? 0) : 0;
@@ -423,27 +442,26 @@ function render(el) {
423
442
  });
424
443
  const existingContainer = shadowRoot.querySelector('.tabs-container');
425
444
  const existingButtons = shadowRoot.querySelector('.tab-buttons');
445
+ const existingStrip = shadowRoot.querySelector('.tab-strip');
426
446
  const existingViewport = shadowRoot.querySelector('.panels-viewport');
427
447
  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
448
  el.style.setProperty('--tabs-width', width.includes('%') ? '100%' : width);
434
449
  el.style.setProperty('--tabs-height', height);
435
450
  el.style.setProperty('--active-index', String(activeIndex));
436
- if (existingContainer && existingButtons && existingViewport) {
451
+ if (existingContainer && existingButtons && existingStrip && existingViewport) {
437
452
  existingContainer.setAttribute('data-placement', placement);
453
+ existingContainer.toggleAttribute('data-fixed', fixed);
454
+ const savedScrollLeft = existingStrip.scrollLeft;
438
455
  existingButtons.querySelector('.tab-overflow-trigger')?.remove();
439
- const allButtons = existingButtons.querySelectorAll('.tab-button');
456
+ const allButtons = existingStrip.querySelectorAll('.tab-button');
440
457
  allButtons.forEach(button => button.remove());
441
458
  const buttonsHtml = renderButtonsOnly(el, tabs, activeId);
442
459
  const tempDiv = document.createElement('div');
443
460
  tempDiv.innerHTML = buttonsHtml;
444
461
  Array.from(tempDiv.children).forEach(button => {
445
- existingButtons.appendChild(button);
462
+ existingStrip.appendChild(button);
446
463
  });
464
+ existingStrip.scrollLeft = savedScrollLeft;
447
465
  setupEventListeners(el, shadowRoot, tabs);
448
466
  updateAriaAttributes(el, shadowRoot, activeId || '');
449
467
  requestAnimationFrame(() => {
@@ -453,10 +471,11 @@ function render(el) {
453
471
  el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
454
472
  }
455
473
  updateTransform(el, activeIndex);
474
+ updateOverflow(el);
456
475
  if (activeId) {
457
476
  updateMarker(el, activeId);
477
+ scrollActiveIntoView(el, activeId, true);
458
478
  }
459
- updateOverflow(el);
460
479
  });
461
480
  if (activeId) {
462
481
  updatePanelInteraction(el, activeId);
@@ -464,7 +483,7 @@ function render(el) {
464
483
  }
465
484
  else {
466
485
  shadowRoot.innerHTML = `
467
- <div class="tabs-container" data-placement="${placement}">
486
+ <div class="tabs-container" data-placement="${placement}"${fixed ? ' data-fixed' : ''}>
468
487
  ${renderTabButtons(el, tabs, activeId)}
469
488
  <div class="panels-viewport" part="panels-container">
470
489
  <div class="panels-wrapper">
@@ -480,14 +499,17 @@ function render(el) {
480
499
  el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
481
500
  }
482
501
  updateTransform(el, activeIndex);
502
+ updateOverflow(el);
483
503
  if (activeId) {
484
504
  updateMarker(el, activeId);
505
+ scrollActiveIntoView(el, activeId, false);
485
506
  }
486
- updateOverflow(el);
487
507
  });
488
508
  setupEventListeners(el, shadowRoot, tabs);
489
509
  updateAriaAttributes(el, shadowRoot, activeId || '');
490
510
  setupResizeObserver(el);
511
+ const strip = shadowRoot.querySelector('.tab-strip');
512
+ strip?.addEventListener('scroll', () => updateEdgeFades(strip), { passive: true });
491
513
  if (activeId) {
492
514
  updatePanelInteraction(el, activeId);
493
515
  }
@@ -511,7 +533,7 @@ function cleanup(el) {
511
533
  }
512
534
  export class TyTabs extends HTMLElement {
513
535
  static get observedAttributes() {
514
- return ['width', 'height', 'active', 'placement'];
536
+ return ['width', 'height', 'active', 'placement', 'fixed'];
515
537
  }
516
538
  constructor() {
517
539
  super();
@@ -540,10 +562,14 @@ export class TyTabs extends HTMLElement {
540
562
  }
541
563
  attributeChangedCallback(name, oldValue, newValue) {
542
564
  if (name === 'active') {
543
- if (newValue) {
544
- updateActiveTabState(this, newValue, oldValue);
565
+ if (!newValue)
566
+ return;
567
+ if (!this.shadowRoot?.querySelector(`[data-tab-id='${newValue}']`)) {
568
+ render(this);
569
+ return;
545
570
  }
546
- render(this);
571
+ updateActiveTabState(this, newValue, oldValue);
572
+ scrollActiveIntoView(this, newValue, true);
547
573
  }
548
574
  else {
549
575
  render(this);
@@ -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);
@@ -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 /* For absolute positioned marker */\n \n /* Default minimal styling - customize via ::part(buttons-container) */\n border-bottom: 1px solid var(--ty-border);\n background: transparent;\n}\n\n.tabs-container[data-placement=\"bottom\"] .tab-buttons {\n border-bottom: none;\n border-top: 1px solid var(--ty-border);\n}\n\n/* Marker wrapper \u2014 exposed as a CSS Part for custom marker styling */\n.marker-wrapper {\n position: absolute;\n z-index: 0;\n /* Behind buttons */\n pointer-events: none;\n /* Don't block clicks */\n transition: left var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n width var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n height var(--transition-duration, 300ms) var(--transition-easing, ease-in-out),\n top var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);\n}\n\n.default-marker {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 2px;\n width: 100%;\n background: var(--ty-color-primary);\n pointer-events: none;\n}\n\n/* Hide default marker when custom marker is slotted */\n.marker-wrapper:has(::slotted([slot=\"marker\"])) .default-marker {\n display: none;\n}\n\n::slotted([slot=\"marker\"]) {\n display: block;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n}\n\n.tab-button {\n min-width: 120px;\n padding: 6px 12px;\n border: none;\n background: transparent;\n cursor: pointer;\n font: inherit;\n color: var(--ty-text-soft);\n transition: var(--ty-local-transition, color 200ms, background-color 200ms);\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n font-weight: var(--ty-font-bold);\n font-size: var(--ty-font-sm);\n position: relative;\n /* Establish stacking context */\n z-index: 10;\n /* Above marker */\n}\n\n.tab-button[aria-selected=true] {\n color: var(--ty-text-strong);\n}\n\n.tab-button[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.tab-button:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n.tab-button.overflow-hidden {\n display: none;\n}\n\n/* Overflow \"more\" trigger + menu */\n.tab-overflow-trigger {\n flex-shrink: 0;\n width: 40px;\n min-width: 40px;\n padding: 6px;\n border: none;\n background: transparent;\n cursor: pointer;\n color: var(--ty-text-soft);\n font-size: 18px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n}\n\n.tab-overflow-trigger:hover {\n color: var(--ty-text-strong);\n}\n\n.tab-overflow-trigger:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n.tab-overflow-menu {\n display: flex;\n flex-direction: column;\n min-width: 160px;\n max-height: 300px;\n overflow-y: auto;\n padding: var(--ty-spacing-2, 4px);\n background: var(--ty-surface-floating);\n border-radius: var(--ty-radius-md);\n box-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n}\n\n.tab-overflow-item {\n padding: var(--ty-spacing-2, 6px) var(--ty-spacing-3, 12px);\n border: none;\n background: transparent;\n text-align: left;\n cursor: pointer;\n border-radius: var(--ty-radius-md);\n color: var(--ty-text);\n font: inherit;\n font-size: var(--ty-font-sm);\n white-space: nowrap;\n}\n\n.tab-overflow-item:hover {\n background: var(--ty-bg-neutral-soft);\n color: var(--ty-text-strong);\n}\n\n.tab-overflow-item[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* Panels viewport \u2014 exposed as a CSS Part */\n.panels-viewport {\n position: relative;\n flex: 1;\n overflow: hidden;\n /* Critical: hides off-screen panels */\n min-height: 0;\n /* Allows flex child to shrink */\n}\n\n/* Panels wrapper slides horizontally */\n.panels-wrapper {\n display: flex;\n height: 100%;\n transition: transform var(--transition-duration, 300ms) var(--transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panels-wrapper {\n transition-duration: 0ms !important;\n }\n\n .marker-wrapper {\n transition-duration: 0ms !important;\n }\n}\n\n::slotted(ty-tab) {\n /* Carousel page width. --tabs-page-width is the measured pixel width, set by\n the ResizeObserver for percentage-width tabs so every panel is exactly one\n page wide. It is deliberately NOT --tabs-width: that one sizes :host, and\n feeding a measured pixel value back into the host's own width is a\n self-referential loop (see setupResizeObserver). */\n width: var(--tabs-page-width, var(--tabs-width, 100%));\n height: 100%;\n flex-shrink: 0;\n}\n";
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";