tyrell-components 1.0.0-TC47 → 1.0.0-TC49

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.
Files changed (41) hide show
  1. package/css/tyrell-theme.css +88 -92
  2. package/css/tyrell.css +19 -0
  3. package/dist/tyrell-theme.css +88 -92
  4. package/dist/tyrell.css +19 -0
  5. package/dist/tyrell.js +1 -1
  6. package/lib/base/ty-component.d.ts +8 -3
  7. package/lib/base/ty-component.js +13 -5
  8. package/lib/components/date-picker.js +2 -8
  9. package/lib/components/file-upload.d.ts +6 -0
  10. package/lib/components/file-upload.js +19 -4
  11. package/lib/components/input.js +1 -0
  12. package/lib/components/modal.d.ts +12 -0
  13. package/lib/components/modal.js +56 -13
  14. package/lib/components/popup.js +4 -4
  15. package/lib/components/radio.js +10 -2
  16. package/lib/components/resize-observer.d.ts +1 -0
  17. package/lib/components/resize-observer.js +12 -1
  18. package/lib/components/select.js +3 -5
  19. package/lib/components/selected-tags.js +1 -1
  20. package/lib/components/tabs.d.ts +2 -0
  21. package/lib/components/tabs.js +15 -0
  22. package/lib/components/tooltip.js +22 -0
  23. package/lib/components/wizard.d.ts +2 -0
  24. package/lib/components/wizard.js +15 -0
  25. package/lib/styles/button.js +4 -1
  26. package/lib/styles/date-picker.js +27 -1
  27. package/lib/styles/modal.d.ts +1 -1
  28. package/lib/styles/modal.js +18 -0
  29. package/lib/styles/option.d.ts +1 -1
  30. package/lib/styles/option.js +15 -0
  31. package/lib/styles/popup.d.ts +1 -1
  32. package/lib/styles/popup.js +22 -5
  33. package/lib/styles/select-base.js +53 -5
  34. package/lib/styles/select.d.ts +1 -1
  35. package/lib/styles/select.js +5 -1
  36. package/lib/utils/number-format.d.ts +6 -1
  37. package/lib/utils/number-format.js +9 -2
  38. package/lib/utils/positioning.js +4 -4
  39. package/lib/version.d.ts +1 -1
  40. package/lib/version.js +1 -1
  41. package/package.json +1 -1
@@ -132,9 +132,14 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
132
132
  * two genuinely separate clicks are always in different tasks.
133
133
  */
134
134
  protected isDuplicateActivationClick(): boolean;
135
- /**
136
- * Form reset callback - called when form.reset() is invoked
137
- * Resets component to its default value
135
+ private _resetDefaults;
136
+ /**
137
+ * Form reset callback - called when form.reset() is invoked.
138
+ * Restores every formValue-bearing property to its attribute-declared
139
+ * default captured at first connect (native defaultValue/defaultChecked
140
+ * semantics). Non-form state (e.g. a checkbox's submission `value`, a
141
+ * ty-radio's option `value`) is deliberately left alone — the old
142
+ * blanket 'value' reset corrupted radio groups by wiping child values.
138
143
  */
139
144
  formResetCallback(): void;
140
145
  /**
@@ -5,6 +5,7 @@ export class TyComponent extends HTMLElement {
5
5
  this._connected = false;
6
6
  this._rendered = false;
7
7
  this._handlingActivationClick = false;
8
+ this._resetDefaults = null;
8
9
  const ctor = this.constructor;
9
10
  this.props = new PropertyManager(ctor.properties);
10
11
  this._internals = this.attachInternals();
@@ -79,6 +80,16 @@ export class TyComponent extends HTMLElement {
79
80
  }
80
81
  connectedCallback() {
81
82
  this._connected = true;
83
+ if (!this._resetDefaults) {
84
+ this._resetDefaults = new Map();
85
+ const ctor = this.constructor;
86
+ for (const [name, config] of Object.entries(ctor.properties)) {
87
+ if (!config.formValue)
88
+ continue;
89
+ const attr = this.getAttribute(this._toKebabCase(name));
90
+ this._resetDefaults.set(name, attr ?? config.default ?? null);
91
+ }
92
+ }
82
93
  const preSetProps = this._capturePreSetProperties();
83
94
  if (preSetProps.length > 0) {
84
95
  this._handlePropertyChanges(preSetProps);
@@ -178,11 +189,8 @@ export class TyComponent extends HTMLElement {
178
189
  return false;
179
190
  }
180
191
  formResetCallback() {
181
- const ctor = this.constructor;
182
- const valueConfig = ctor.properties['value'];
183
- if (valueConfig) {
184
- const defaultValue = valueConfig.default ?? '';
185
- this.setProperty('value', defaultValue);
192
+ for (const [name, def] of this._resetDefaults ?? []) {
193
+ this.setProperty(name, def);
186
194
  }
187
195
  this.onFormReset();
188
196
  }
@@ -980,10 +980,7 @@ export class TyDatePicker extends TyComponent {
980
980
  if (isMobileTouch())
981
981
  return;
982
982
  this.updateState({ open: true });
983
- this.dispatchEvent(new CustomEvent('open', {
984
- bubbles: true,
985
- composed: true
986
- }));
983
+ this.dispatchEvent(new CustomEvent('open'));
987
984
  this.render();
988
985
  requestAnimationFrame(() => {
989
986
  if (!this.shadowRoot)
@@ -1022,10 +1019,7 @@ export class TyDatePicker extends TyComponent {
1022
1019
  if (stubEl)
1023
1020
  stubEl.setAttribute('aria-expanded', 'false');
1024
1021
  }
1025
- this.dispatchEvent(new CustomEvent('close', {
1026
- bubbles: true,
1027
- composed: true
1028
- }));
1022
+ this.dispatchEvent(new CustomEvent('close'));
1029
1023
  this.render();
1030
1024
  }
1031
1025
  cleanup() {
@@ -80,6 +80,12 @@ export declare class TyFileUpload extends TyComponent<FileUploadState> implement
80
80
  protected onPropertiesChanged(_changes: PropertyChange[]): void;
81
81
  protected onFormReset(): void;
82
82
  protected getFormValue(): FormData | string | null;
83
+ /**
84
+ * Match a file against the `accept` attribute the way native inputs do:
85
+ * comma-separated list of `.ext`, `type/subtype` or `type/*` entries.
86
+ * No/blank accept matches everything.
87
+ */
88
+ private matchesAccept;
83
89
  get files(): File[];
84
90
  get multiple(): boolean;
85
91
  set multiple(v: boolean);
@@ -44,15 +44,30 @@ export class TyFileUpload extends TyComponent {
44
44
  fileInput.value = "";
45
45
  }
46
46
  getFormValue() {
47
- if (this._files.length === 0)
47
+ if (this._files.length === 0 || !this.name)
48
48
  return null;
49
49
  const fd = new FormData();
50
- const fieldName = this.name || "files";
51
50
  for (const file of this._files) {
52
- fd.append(fieldName, file);
51
+ fd.append(this.name, file);
53
52
  }
54
53
  return fd;
55
54
  }
55
+ matchesAccept(file) {
56
+ const accept = this.accept?.trim();
57
+ if (!accept)
58
+ return true;
59
+ return accept.split(",").some((raw) => {
60
+ const entry = raw.trim().toLowerCase();
61
+ if (!entry)
62
+ return false;
63
+ if (entry.startsWith("."))
64
+ return file.name.toLowerCase().endsWith(entry);
65
+ const type = file.type.toLowerCase();
66
+ if (entry.endsWith("/*"))
67
+ return type.startsWith(entry.slice(0, -1));
68
+ return type === entry;
69
+ });
70
+ }
56
71
  get files() {
57
72
  return [...this._files];
58
73
  }
@@ -178,7 +193,7 @@ export class TyFileUpload extends TyComponent {
178
193
  e.stopPropagation();
179
194
  this._isDragging = false;
180
195
  dropZone.classList.remove("drag-active");
181
- const files = Array.from(e.dataTransfer?.files ?? []);
196
+ const files = Array.from(e.dataTransfer?.files ?? []).filter((f) => this.matchesAccept(f));
182
197
  if (files.length > 0)
183
198
  this.setFiles(files);
184
199
  };
@@ -81,6 +81,7 @@ export class TyInput extends TyComponent {
81
81
  }
82
82
  onFormReset() {
83
83
  this._shadowValue = null;
84
+ this.initializeShadowValue();
84
85
  this._isFocused = false;
85
86
  if (this._inputDebounceTimer !== null) {
86
87
  clearTimeout(this._inputDebounceTimer);
@@ -48,6 +48,18 @@ export declare class TyModal extends HTMLElement {
48
48
  force?: boolean;
49
49
  }) => void;
50
50
  static get observedAttributes(): string[];
51
+ get open(): boolean;
52
+ set open(v: boolean);
53
+ get backdrop(): boolean;
54
+ set backdrop(v: boolean);
55
+ get closeOnOutsideClick(): boolean;
56
+ set closeOnOutsideClick(v: boolean);
57
+ get closeOnEscape(): boolean;
58
+ set closeOnEscape(v: boolean);
59
+ get preventOutsideClick(): boolean;
60
+ set preventOutsideClick(v: boolean);
61
+ get preventEscape(): boolean;
62
+ set preventEscape(v: boolean);
51
63
  constructor();
52
64
  connectedCallback(): void;
53
65
  disconnectedCallback(): void;
@@ -7,6 +7,7 @@ const closeButtonHandlers = new WeakMap();
7
7
  const hoverEnterHandlers = new WeakMap();
8
8
  const hoverLeaveHandlers = new WeakMap();
9
9
  const modalIds = new WeakMap();
10
+ const pendingCloseReasons = new WeakMap();
10
11
  const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`;
11
12
  function parseBoolAttr(el, name) {
12
13
  if (!el.hasAttribute(name)) {
@@ -22,12 +23,16 @@ function getModalAttributes(el) {
22
23
  return {
23
24
  open: parseBoolAttr(el, 'open'),
24
25
  backdrop: el.hasAttribute('backdrop') ? parseBoolAttr(el, 'backdrop') : true,
25
- closeOnOutsideClick: el.hasAttribute('close-on-outside-click')
26
- ? parseBoolAttr(el, 'close-on-outside-click')
27
- : true,
28
- closeOnEscape: el.hasAttribute('close-on-escape')
29
- ? parseBoolAttr(el, 'close-on-escape')
30
- : true,
26
+ closeOnOutsideClick: parseBoolAttr(el, 'prevent-outside-click')
27
+ ? false
28
+ : el.hasAttribute('close-on-outside-click')
29
+ ? parseBoolAttr(el, 'close-on-outside-click')
30
+ : true,
31
+ closeOnEscape: parseBoolAttr(el, 'prevent-escape')
32
+ ? false
33
+ : el.hasAttribute('close-on-escape')
34
+ ? parseBoolAttr(el, 'close-on-escape')
35
+ : true,
31
36
  };
32
37
  }
33
38
  function getModalDialog(shadowRoot) {
@@ -47,7 +52,6 @@ function isMobile() {
47
52
  function dispatchModalEvent(el, eventType, detail) {
48
53
  const event = new CustomEvent(eventType, {
49
54
  detail,
50
- bubbles: true,
51
55
  cancelable: true,
52
56
  });
53
57
  el.dispatchEvent(event);
@@ -80,14 +84,13 @@ function closeModal(el, reason = 'programmatic', opts) {
80
84
  if (!opts?.force) {
81
85
  const beforeClose = new CustomEvent('beforeclose', {
82
86
  detail: { reason },
83
- bubbles: true,
84
- composed: true,
85
87
  cancelable: true,
86
88
  });
87
89
  el.dispatchEvent(beforeClose);
88
90
  if (beforeClose.defaultPrevented)
89
91
  return;
90
92
  }
93
+ pendingCloseReasons.set(el, reason);
91
94
  el.removeAttribute('open');
92
95
  }
93
96
  function openModal(el) {
@@ -157,6 +160,14 @@ function setupEscapeKey(el, dialog, enabled) {
157
160
  escapeKeyHandlers.set(el, handler);
158
161
  dialog.addEventListener('keydown', handler);
159
162
  }
163
+ else {
164
+ const handler = (e) => {
165
+ if (e.key === 'Escape')
166
+ e.preventDefault();
167
+ };
168
+ escapeKeyHandlers.set(el, handler);
169
+ dialog.addEventListener('keydown', handler);
170
+ }
160
171
  }
161
172
  function setupCloseButton(el, dialog, showButton) {
162
173
  const closeButton = dialog.querySelector('.close-button');
@@ -226,6 +237,11 @@ function render(el) {
226
237
  }
227
238
  setupBackdropClick(el, dialog, attributes.closeOnOutsideClick);
228
239
  setupEscapeKey(el, dialog, attributes.closeOnEscape);
240
+ dialog.oncancel = (event) => {
241
+ event.preventDefault();
242
+ if (getModalAttributes(el).closeOnEscape)
243
+ closeModal(el, 'escape');
244
+ };
229
245
  setupCloseButton(el, dialog, attributes.closeOnOutsideClick);
230
246
  setupCloseButtonHover(el, dialog);
231
247
  if (attributes.open) {
@@ -237,6 +253,7 @@ function render(el) {
237
253
  else {
238
254
  dialog.show();
239
255
  }
256
+ syncBackdropZoom();
240
257
  dispatchModalEvent(el, 'open', {});
241
258
  }
242
259
  }
@@ -244,9 +261,10 @@ function render(el) {
244
261
  if (dialog.open) {
245
262
  unlockScroll(modalId);
246
263
  dialog.close();
247
- dispatchModalEvent(el, 'close', {
248
- reason: 'programmatic'
249
- });
264
+ syncBackdropZoom();
265
+ const reason = pendingCloseReasons.get(el) ?? 'programmatic';
266
+ pendingCloseReasons.delete(el);
267
+ dispatchModalEvent(el, 'close', { reason });
250
268
  }
251
269
  }
252
270
  dialog.onclose = (event) => {
@@ -260,8 +278,16 @@ function render(el) {
260
278
  returnValue: dialog.returnValue || undefined
261
279
  });
262
280
  }
281
+ syncBackdropZoom();
263
282
  };
264
283
  }
284
+ function syncBackdropZoom() {
285
+ const anyOpen = Array.from(document.querySelectorAll('ty-modal[open][backdrop-zoom], ty-dialog[open][backdrop-zoom]')).some((m) => {
286
+ const v = m.getAttribute('backdrop-zoom').toLowerCase().trim();
287
+ return v !== 'false' && v !== '0';
288
+ });
289
+ document.documentElement.classList.toggle('ty-modal-zoom', anyOpen);
290
+ }
265
291
  function cleanup(el) {
266
292
  const modalId = modalIds.get(el);
267
293
  const shadowRoot = el.shadowRoot;
@@ -270,6 +296,7 @@ function cleanup(el) {
270
296
  unlockScroll(modalId);
271
297
  modalIds.delete(el);
272
298
  }
299
+ syncBackdropZoom();
273
300
  if (!dialog)
274
301
  return;
275
302
  const backdropHandler = backdropClickHandlers.get(el);
@@ -306,7 +333,23 @@ function cleanup(el) {
306
333
  }
307
334
  export class TyModal extends HTMLElement {
308
335
  static get observedAttributes() {
309
- return ['open', 'backdrop', 'close-on-outside-click', 'close-on-escape', 'label'];
336
+ return ['open', 'backdrop', 'close-on-outside-click', 'close-on-escape', 'prevent-outside-click', 'prevent-escape', 'label'];
337
+ }
338
+ get open() { return parseBoolAttr(this, 'open'); }
339
+ set open(v) { v ? this.setAttribute('open', 'true') : this.removeAttribute('open'); }
340
+ get backdrop() { return getModalAttributes(this).backdrop; }
341
+ set backdrop(v) { this.setAttribute('backdrop', String(!!v)); }
342
+ get closeOnOutsideClick() { return getModalAttributes(this).closeOnOutsideClick; }
343
+ set closeOnOutsideClick(v) { this.setAttribute('close-on-outside-click', String(!!v)); }
344
+ get closeOnEscape() { return getModalAttributes(this).closeOnEscape; }
345
+ set closeOnEscape(v) { this.setAttribute('close-on-escape', String(!!v)); }
346
+ get preventOutsideClick() { return parseBoolAttr(this, 'prevent-outside-click'); }
347
+ set preventOutsideClick(v) {
348
+ v ? this.setAttribute('prevent-outside-click', '') : this.removeAttribute('prevent-outside-click');
349
+ }
350
+ get preventEscape() { return parseBoolAttr(this, 'prevent-escape'); }
351
+ set preventEscape(v) {
352
+ v ? this.setAttribute('prevent-escape', '') : this.removeAttribute('prevent-escape');
310
353
  }
311
354
  constructor() {
312
355
  super();
@@ -58,6 +58,7 @@ function updatePosition(el) {
58
58
  });
59
59
  el.style.setProperty('--popup-x', `${positionData.x}px`);
60
60
  el.style.setProperty('--popup-y', `${positionData.y}px`);
61
+ dialog.dataset.side = positionData.placement.split('-')[0];
61
62
  dialog.classList.add('position-calculated');
62
63
  }
63
64
  function closePopup(el, force = false) {
@@ -76,8 +77,7 @@ function closePopup(el, force = false) {
76
77
  setTimeout(() => {
77
78
  dialog.classList.remove('position-calculated');
78
79
  dialog.close();
79
- }, 150);
80
- el.dispatchEvent(new CustomEvent('close', { bubbles: true }));
80
+ }, 200);
81
81
  }
82
82
  function openPopup(el) {
83
83
  const popupId = getPopupId(el);
@@ -95,7 +95,7 @@ function openPopup(el) {
95
95
  dialog.classList.add('preparing-animation');
96
96
  requestAnimationFrame(() => {
97
97
  dialog.classList.add('open');
98
- el.dispatchEvent(new CustomEvent('open', { bubbles: true }));
98
+ el.dispatchEvent(new CustomEvent('open'));
99
99
  });
100
100
  });
101
101
  }
@@ -211,7 +211,7 @@ function render(el) {
211
211
  unlockScroll(popupId);
212
212
  dialog.classList.remove('open');
213
213
  dialog.classList.remove('preparing-animation');
214
- el.dispatchEvent(new CustomEvent('close', { bubbles: true }));
214
+ el.dispatchEvent(new CustomEvent('close'));
215
215
  });
216
216
  }
217
217
  el.closePopup = () => closePopup(el, true);
@@ -176,8 +176,16 @@ export class TyRadioGroup extends TyComponent {
176
176
  const radios = this.getRadios();
177
177
  for (const radio of radios) {
178
178
  radio.checked = radio.value === this.value;
179
- if (this.disabled)
180
- radio.disabled = true;
179
+ if (this.disabled) {
180
+ if (!radio.disabled) {
181
+ radio.disabled = true;
182
+ radio.dataset.tyGroupDisabled = "";
183
+ }
184
+ }
185
+ else if ("tyGroupDisabled" in radio.dataset) {
186
+ radio.disabled = false;
187
+ delete radio.dataset.tyGroupDisabled;
188
+ }
181
189
  radio.size = this.size;
182
190
  radio.flavor = this.flavor;
183
191
  }
@@ -13,6 +13,7 @@ export declare class TyResizeObserver extends HTMLElement {
13
13
  set debounce(value: number);
14
14
  constructor();
15
15
  connectedCallback(): void;
16
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
16
17
  disconnectedCallback(): void;
17
18
  private setupObserver;
18
19
  private handleResize;
@@ -22,6 +22,15 @@ export class TyResizeObserver extends HTMLElement {
22
22
  connectedCallback() {
23
23
  this.setupObserver();
24
24
  }
25
+ attributeChangedCallback(name, oldValue, newValue) {
26
+ if (name === 'id' && oldValue && oldValue !== newValue) {
27
+ removeSize(oldValue);
28
+ if (newValue && this.isConnected) {
29
+ const { width, height } = this.getBoundingClientRect();
30
+ updateSize(newValue, width, height);
31
+ }
32
+ }
33
+ }
25
34
  disconnectedCallback() {
26
35
  this.cleanup();
27
36
  }
@@ -60,4 +69,6 @@ export class TyResizeObserver extends HTMLElement {
60
69
  }
61
70
  }
62
71
  }
63
- customElements.define('ty-resize-observer', TyResizeObserver);
72
+ if (!customElements.get('ty-resize-observer')) {
73
+ customElements.define('ty-resize-observer', TyResizeObserver);
74
+ }
@@ -379,6 +379,7 @@ export class TySelect extends TyComponent {
379
379
  popupHeight: estimatedHeight,
380
380
  align: anchored.align,
381
381
  side: anchored.side,
382
+ gap: 8,
382
383
  });
383
384
  const positionBelow = pos.below;
384
385
  const x = pos.x - wrapPadding;
@@ -436,8 +437,8 @@ export class TySelect extends TyComponent {
436
437
  this._scrollLockId = lockId;
437
438
  lockScroll(lockId);
438
439
  dialog.showModal();
439
- dialog.classList.add("open");
440
440
  this.calculatePosition();
441
+ dialog.classList.add("open");
441
442
  this._state.open = true;
442
443
  const chevron = shadow.querySelector(".dropdown-chevron");
443
444
  if (chevron)
@@ -516,6 +517,7 @@ export class TySelect extends TyComponent {
516
517
  this._scrollLockId = lockId;
517
518
  lockScroll(lockId);
518
519
  dialog.showModal();
520
+ dialog.getBoundingClientRect();
519
521
  dialog.classList.add("open");
520
522
  this._state.open = true;
521
523
  const tags = this.getTagElements().map((el) => this.getTagData(el));
@@ -845,8 +847,6 @@ export class TySelect extends TyComponent {
845
847
  fireOpenEvent() {
846
848
  this.dispatchEvent(new CustomEvent("open", {
847
849
  detail: { mode: this._state.mode, element: this },
848
- bubbles: true,
849
- composed: true,
850
850
  }));
851
851
  if (this._externalSearch) {
852
852
  this.fireSearchEvent("");
@@ -855,8 +855,6 @@ export class TySelect extends TyComponent {
855
855
  fireCloseEvent() {
856
856
  this.dispatchEvent(new CustomEvent("close", {
857
857
  detail: { mode: this._state.mode, element: this },
858
- bubbles: true,
859
- composed: true,
860
858
  }));
861
859
  }
862
860
  dispatchChangeEvent(detail) {
@@ -105,7 +105,7 @@ export class TySelectedOptions extends HTMLElement {
105
105
  return ctx;
106
106
  }
107
107
  templateSource(template) {
108
- if (template.content.firstElementChild)
108
+ if (template.content.hasChildNodes())
109
109
  return template.content;
110
110
  const frag = document.createDocumentFragment();
111
111
  template.childNodes.forEach(n => frag.appendChild(n.cloneNode(true)));
@@ -27,6 +27,8 @@ export interface TabChangeDetail {
27
27
  export declare class TyTabs extends HTMLElement {
28
28
  static get observedAttributes(): string[];
29
29
  constructor();
30
+ private _childObserver;
31
+ private _childRenderQueued;
30
32
  connectedCallback(): void;
31
33
  disconnectedCallback(): void;
32
34
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
@@ -515,12 +515,27 @@ export class TyTabs extends HTMLElement {
515
515
  }
516
516
  constructor() {
517
517
  super();
518
+ this._childObserver = null;
519
+ this._childRenderQueued = false;
518
520
  this.attachShadow({ mode: 'open' });
519
521
  }
520
522
  connectedCallback() {
521
523
  render(this);
524
+ this._childObserver = new MutationObserver(() => {
525
+ if (this._childRenderQueued)
526
+ return;
527
+ this._childRenderQueued = true;
528
+ queueMicrotask(() => {
529
+ this._childRenderQueued = false;
530
+ if (this.isConnected)
531
+ render(this);
532
+ });
533
+ });
534
+ this._childObserver.observe(this, { childList: true });
522
535
  }
523
536
  disconnectedCallback() {
537
+ this._childObserver?.disconnect();
538
+ this._childObserver = null;
524
539
  cleanup(this);
525
540
  }
526
541
  attributeChangedCallback(name, oldValue, newValue) {
@@ -57,7 +57,28 @@ function removeDescribedBy(anchor, id) {
57
57
  anchor.removeAttribute('aria-describedby');
58
58
  }
59
59
  }
60
+ function ensureTooltipMotionStyles() {
61
+ if (document.getElementById('ty-tooltip-motion'))
62
+ return;
63
+ const style = document.createElement('style');
64
+ style.id = 'ty-tooltip-motion';
65
+ style.textContent = `
66
+ .ty-tooltip-popover:popover-open {
67
+ transition:
68
+ opacity var(--ty-tooltip-duration, 120ms) ease-out,
69
+ translate var(--ty-tooltip-duration, 120ms) ease-out;
70
+ }
71
+ @starting-style {
72
+ .ty-tooltip-popover:popover-open { opacity: 0; translate: 0 4px; }
73
+ .ty-tooltip-popover:popover-open[data-side="bottom"] { translate: 0 -4px; }
74
+ .ty-tooltip-popover:popover-open[data-side="left"] { translate: 4px 0; }
75
+ .ty-tooltip-popover:popover-open[data-side="right"] { translate: -4px 0; }
76
+ }
77
+ `;
78
+ document.head.appendChild(style);
79
+ }
60
80
  function getOrCreatePopover(el) {
81
+ ensureTooltipMotionStyles();
61
82
  let popover = popoverElements.get(el);
62
83
  if (!popover) {
63
84
  popover = document.createElement('div');
@@ -143,6 +164,7 @@ function updatePosition(el) {
143
164
  });
144
165
  popover.style.left = `${position.x}px`;
145
166
  popover.style.top = `${position.y}px`;
167
+ popover.dataset.side = position.placement.split('-')[0];
146
168
  }
147
169
  function cleanupAutoUpdate(el) {
148
170
  const cleanup = autoUpdateCleanup.get(el);
@@ -30,6 +30,8 @@ export interface WizardStepChangeDetail {
30
30
  export declare class TyWizard extends HTMLElement {
31
31
  static get observedAttributes(): string[];
32
32
  constructor();
33
+ private _childObserver;
34
+ private _childRenderQueued;
33
35
  connectedCallback(): void;
34
36
  disconnectedCallback(): void;
35
37
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
@@ -399,12 +399,27 @@ export class TyWizard extends HTMLElement {
399
399
  }
400
400
  constructor() {
401
401
  super();
402
+ this._childObserver = null;
403
+ this._childRenderQueued = false;
402
404
  this.attachShadow({ mode: 'open' });
403
405
  }
404
406
  connectedCallback() {
405
407
  render(this);
408
+ this._childObserver = new MutationObserver(() => {
409
+ if (this._childRenderQueued)
410
+ return;
411
+ this._childRenderQueued = true;
412
+ queueMicrotask(() => {
413
+ this._childRenderQueued = false;
414
+ if (this.isConnected)
415
+ render(this);
416
+ });
417
+ });
418
+ this._childObserver.observe(this, { childList: true });
406
419
  }
407
420
  disconnectedCallback() {
421
+ this._childObserver?.disconnect();
422
+ this._childObserver = null;
408
423
  cleanup(this);
409
424
  }
410
425
  attributeChangedCallback(name, oldValue, newValue) {
@@ -8,7 +8,7 @@ const solidFlavor = (f, fb) => {
8
8
  const solid = (suffix) => tokenRef('--ty-solid', f, suffix, fb);
9
9
  const borderL = f === 'neutral'
10
10
  ? ' --_border-l: var(--ty-button-border-l-neutral, var(--ty-button-border-l, -0.15));'
11
- : ' --_border-l: var(--ty-button-border-l, -0.15);';
11
+ : ' --_border-l: var(--_muted-border-l, var(--ty-button-border-l, -0.15));';
12
12
  const plusNudge = f === 'neutral' ? nudgedMid : nudged;
13
13
  return `
14
14
  button.solid.${f} { --_ring: ${tokenRef('--ty-color', f, '', fb)}; --_solid-bg: var(--ty-button-bg, var(--_muted-solid-bg, ${solid('')})); --_solid-rest: var(--ty-button-bg, var(--_muted-solid-bg, ${solid('')}));${borderL} color: var(--ty-button-color, var(--_muted-solid-fg, ${solid('-fg')})); }
@@ -288,6 +288,7 @@ button.muted {
288
288
  --_muted-solid-fg: var(--ty-solid-neutral-fg);
289
289
  --_muted-outlined-line: var(--ty-color-neutral);
290
290
  --_muted-ghost-fg: var(--ty-color-neutral);
291
+ --_muted-border-l: var(--ty-button-border-l-neutral, var(--ty-button-border-l, -0.15));
291
292
  }
292
293
  button.muted.tone-plus {
293
294
  --_muted-solid-bg: var(--ty-solid-neutral-strong);
@@ -310,6 +311,7 @@ button.muted.tone-minus {
310
311
  --_muted-solid-fg: unset;
311
312
  --_muted-outlined-line: unset;
312
313
  --_muted-ghost-fg: unset;
314
+ --_muted-border-l: unset;
313
315
  }
314
316
  }
315
317
  button.muted:active:not(:disabled),
@@ -318,6 +320,7 @@ button.muted:focus-visible:not(:disabled) {
318
320
  --_muted-solid-fg: unset;
319
321
  --_muted-outlined-line: unset;
320
322
  --_muted-ghost-fg: unset;
323
+ --_muted-border-l: unset;
321
324
  }
322
325
 
323
326
  button.pill {
@@ -252,7 +252,15 @@ ${FLAVORS.filter((f) => f !== "neutral").map((f) => datePickerFlavor(f)).join(""
252
252
  padding: var(--calendar-padding, 8px);
253
253
 
254
254
  opacity: 0;
255
- transition: opacity 200ms ease;
255
+ /* Entrance motion uses the individual scale/translate properties so it
256
+ composes with the collision-shift transform below (they apply first). */
257
+ scale: 0.96;
258
+ translate: 0 -6px;
259
+ transform-origin: top center;
260
+ transition:
261
+ opacity var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1)),
262
+ scale var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1)),
263
+ translate var(--ty-popup-duration, 180ms) var(--ty-popup-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
256
264
 
257
265
  transform: translate(var(--calendar-offset-x, 0px), var(--calendar-offset-y, 0px));
258
266
  top: -1000px;
@@ -269,10 +277,28 @@ ${FLAVORS.filter((f) => f !== "neutral").map((f) => datePickerFlavor(f)).join(""
269
277
  bottom: var(--calendar-y);
270
278
  top: auto;
271
279
  flex-direction: column-reverse;
280
+ translate: 0 6px;
281
+ transform-origin: bottom center;
272
282
  }
273
283
 
274
284
  .calendar-dialog.open {
275
285
  opacity: 1;
286
+ scale: 1;
287
+ translate: 0 0;
288
+ }
289
+
290
+ /* First-frame "from" values — see select-base.ts: transitions can't start on
291
+ an element that was display:none last frame; @starting-style fixes that. */
292
+ @starting-style {
293
+ .calendar-dialog.open {
294
+ opacity: 0;
295
+ scale: 0.96;
296
+ translate: 0 -6px;
297
+ }
298
+
299
+ .calendar-dialog.open.position-above {
300
+ translate: 0 6px;
301
+ }
276
302
  }
277
303
 
278
304
  .calendar-dialog::backdrop {