tyrell-components 1.0.0-TC47 → 1.0.0-TC48
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/css/tyrell-theme.css +88 -92
- package/css/tyrell.css +19 -0
- package/dist/tyrell-theme.css +88 -92
- package/dist/tyrell.css +19 -0
- package/dist/tyrell.js +1 -1
- package/lib/base/ty-component.d.ts +8 -3
- package/lib/base/ty-component.js +13 -5
- package/lib/components/date-picker.js +2 -8
- package/lib/components/file-upload.d.ts +6 -0
- package/lib/components/file-upload.js +19 -4
- package/lib/components/input.js +1 -0
- package/lib/components/modal.d.ts +8 -0
- package/lib/components/modal.js +29 -6
- package/lib/components/popup.js +4 -4
- package/lib/components/radio.js +10 -2
- package/lib/components/resize-observer.d.ts +1 -0
- package/lib/components/resize-observer.js +12 -1
- package/lib/components/select.js +3 -5
- package/lib/components/selected-tags.js +1 -1
- package/lib/components/tabs.d.ts +2 -0
- package/lib/components/tabs.js +15 -0
- package/lib/components/tooltip.js +22 -0
- package/lib/components/wizard.d.ts +2 -0
- package/lib/components/wizard.js +15 -0
- package/lib/styles/button.js +4 -1
- package/lib/styles/date-picker.js +27 -1
- package/lib/styles/modal.d.ts +1 -1
- package/lib/styles/modal.js +18 -0
- package/lib/styles/option.d.ts +1 -1
- package/lib/styles/option.js +15 -0
- package/lib/styles/popup.d.ts +1 -1
- package/lib/styles/popup.js +22 -5
- package/lib/styles/select-base.js +53 -5
- package/lib/styles/select.d.ts +1 -1
- package/lib/styles/select.js +5 -1
- package/lib/utils/number-format.d.ts +6 -1
- package/lib/utils/number-format.js +9 -2
- package/lib/utils/positioning.js +4 -4
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- 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
|
-
|
|
137
|
-
*
|
|
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
|
/**
|
package/lib/base/ty-component.js
CHANGED
|
@@ -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
|
|
182
|
-
|
|
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(
|
|
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
|
};
|
package/lib/components/input.js
CHANGED
|
@@ -48,6 +48,14 @@ 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);
|
|
51
59
|
constructor();
|
|
52
60
|
connectedCallback(): void;
|
|
53
61
|
disconnectedCallback(): void;
|
package/lib/components/modal.js
CHANGED
|
@@ -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)) {
|
|
@@ -47,7 +48,6 @@ function isMobile() {
|
|
|
47
48
|
function dispatchModalEvent(el, eventType, detail) {
|
|
48
49
|
const event = new CustomEvent(eventType, {
|
|
49
50
|
detail,
|
|
50
|
-
bubbles: true,
|
|
51
51
|
cancelable: true,
|
|
52
52
|
});
|
|
53
53
|
el.dispatchEvent(event);
|
|
@@ -80,14 +80,13 @@ function closeModal(el, reason = 'programmatic', opts) {
|
|
|
80
80
|
if (!opts?.force) {
|
|
81
81
|
const beforeClose = new CustomEvent('beforeclose', {
|
|
82
82
|
detail: { reason },
|
|
83
|
-
bubbles: true,
|
|
84
|
-
composed: true,
|
|
85
83
|
cancelable: true,
|
|
86
84
|
});
|
|
87
85
|
el.dispatchEvent(beforeClose);
|
|
88
86
|
if (beforeClose.defaultPrevented)
|
|
89
87
|
return;
|
|
90
88
|
}
|
|
89
|
+
pendingCloseReasons.set(el, reason);
|
|
91
90
|
el.removeAttribute('open');
|
|
92
91
|
}
|
|
93
92
|
function openModal(el) {
|
|
@@ -226,6 +225,11 @@ function render(el) {
|
|
|
226
225
|
}
|
|
227
226
|
setupBackdropClick(el, dialog, attributes.closeOnOutsideClick);
|
|
228
227
|
setupEscapeKey(el, dialog, attributes.closeOnEscape);
|
|
228
|
+
dialog.oncancel = (event) => {
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
if (getModalAttributes(el).closeOnEscape)
|
|
231
|
+
closeModal(el, 'escape');
|
|
232
|
+
};
|
|
229
233
|
setupCloseButton(el, dialog, attributes.closeOnOutsideClick);
|
|
230
234
|
setupCloseButtonHover(el, dialog);
|
|
231
235
|
if (attributes.open) {
|
|
@@ -237,6 +241,7 @@ function render(el) {
|
|
|
237
241
|
else {
|
|
238
242
|
dialog.show();
|
|
239
243
|
}
|
|
244
|
+
syncBackdropZoom();
|
|
240
245
|
dispatchModalEvent(el, 'open', {});
|
|
241
246
|
}
|
|
242
247
|
}
|
|
@@ -244,9 +249,10 @@ function render(el) {
|
|
|
244
249
|
if (dialog.open) {
|
|
245
250
|
unlockScroll(modalId);
|
|
246
251
|
dialog.close();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
syncBackdropZoom();
|
|
253
|
+
const reason = pendingCloseReasons.get(el) ?? 'programmatic';
|
|
254
|
+
pendingCloseReasons.delete(el);
|
|
255
|
+
dispatchModalEvent(el, 'close', { reason });
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
dialog.onclose = (event) => {
|
|
@@ -260,8 +266,16 @@ function render(el) {
|
|
|
260
266
|
returnValue: dialog.returnValue || undefined
|
|
261
267
|
});
|
|
262
268
|
}
|
|
269
|
+
syncBackdropZoom();
|
|
263
270
|
};
|
|
264
271
|
}
|
|
272
|
+
function syncBackdropZoom() {
|
|
273
|
+
const anyOpen = Array.from(document.querySelectorAll('ty-modal[open][backdrop-zoom], ty-dialog[open][backdrop-zoom]')).some((m) => {
|
|
274
|
+
const v = m.getAttribute('backdrop-zoom').toLowerCase().trim();
|
|
275
|
+
return v !== 'false' && v !== '0';
|
|
276
|
+
});
|
|
277
|
+
document.documentElement.classList.toggle('ty-modal-zoom', anyOpen);
|
|
278
|
+
}
|
|
265
279
|
function cleanup(el) {
|
|
266
280
|
const modalId = modalIds.get(el);
|
|
267
281
|
const shadowRoot = el.shadowRoot;
|
|
@@ -270,6 +284,7 @@ function cleanup(el) {
|
|
|
270
284
|
unlockScroll(modalId);
|
|
271
285
|
modalIds.delete(el);
|
|
272
286
|
}
|
|
287
|
+
syncBackdropZoom();
|
|
273
288
|
if (!dialog)
|
|
274
289
|
return;
|
|
275
290
|
const backdropHandler = backdropClickHandlers.get(el);
|
|
@@ -308,6 +323,14 @@ export class TyModal extends HTMLElement {
|
|
|
308
323
|
static get observedAttributes() {
|
|
309
324
|
return ['open', 'backdrop', 'close-on-outside-click', 'close-on-escape', 'label'];
|
|
310
325
|
}
|
|
326
|
+
get open() { return parseBoolAttr(this, 'open'); }
|
|
327
|
+
set open(v) { v ? this.setAttribute('open', 'true') : this.removeAttribute('open'); }
|
|
328
|
+
get backdrop() { return getModalAttributes(this).backdrop; }
|
|
329
|
+
set backdrop(v) { this.setAttribute('backdrop', String(!!v)); }
|
|
330
|
+
get closeOnOutsideClick() { return getModalAttributes(this).closeOnOutsideClick; }
|
|
331
|
+
set closeOnOutsideClick(v) { this.setAttribute('close-on-outside-click', String(!!v)); }
|
|
332
|
+
get closeOnEscape() { return getModalAttributes(this).closeOnEscape; }
|
|
333
|
+
set closeOnEscape(v) { this.setAttribute('close-on-escape', String(!!v)); }
|
|
311
334
|
constructor() {
|
|
312
335
|
super();
|
|
313
336
|
this.attachShadow({ mode: 'open' });
|
package/lib/components/popup.js
CHANGED
|
@@ -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
|
-
},
|
|
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'
|
|
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'
|
|
214
|
+
el.dispatchEvent(new CustomEvent('close'));
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
217
|
el.closePopup = () => closePopup(el, true);
|
package/lib/components/radio.js
CHANGED
|
@@ -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
|
|
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.
|
|
72
|
+
if (!customElements.get('ty-resize-observer')) {
|
|
73
|
+
customElements.define('ty-resize-observer', TyResizeObserver);
|
|
74
|
+
}
|
package/lib/components/select.js
CHANGED
|
@@ -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.
|
|
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)));
|
package/lib/components/tabs.d.ts
CHANGED
|
@@ -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;
|
package/lib/components/tabs.js
CHANGED
|
@@ -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;
|
package/lib/components/wizard.js
CHANGED
|
@@ -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) {
|
package/lib/styles/button.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/lib/styles/modal.d.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* CSS for the modal component wrapper.
|
|
5
5
|
* The modal is a pure wrapper - all content styling is user-defined.
|
|
6
6
|
*/
|
|
7
|
-
export declare const modalStyles = "\n:host {\n display: contents;\n /* Don't interfere with layout */\n}\n\n/* Part of the modal wrapper functionality, not user content styling */\n.close-button {\n position: absolute;\n top: -8px;\n right: -8px;\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n transition: var(--ty-transition-colors);\n z-index: 1;\n color: #d8d8d8;\n\n /* Remove all default browser styling that could cause outlines/borders */\n outline: none;\n box-shadow: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n\n /* Remove mobile tap highlights and focus rings */\n -webkit-tap-highlight-color: transparent;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.close-button:hover {\n color: white;\n}\n\n/* Explicitly remove focus and active states for all interaction modes */\n.close-button:focus,\n.close-button:focus-visible,\n.close-button:focus-within,\n.close-button:active {\n outline: none !important;\n box-shadow: none !important;\n border: none !important;\n background: transparent !important;\n}\n\n/* Override hover color even in focus/active states */\n.close-button:hover,\n.close-button:focus:hover,\n.close-button:active:hover {\n color: white;\n}\n\n.close-button.hide {\n visibility: hidden;\n}\n\n/* Pure wrapper: only essential modal functionality, no styling of user content */\n.ty-modal-dialog {\n /* Reset default dialog styles */\n padding: 18px;\n border: none;\n margin: 0;\n background: transparent;\n\n /* Centered */\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n\n z-index: 1000;\n overflow: hidden;\n\n /* No sizing - let user content determine size */\n max-width: 85vw;\n max-height: 90vh;\n\n outline: none;\n}\n\n/* Backdrop is part of modal functionality, not user content */\n.ty-modal-dialog::backdrop {\n background: var(--ty-modal-backdrop, rgba(0, 0, 0, 0.5));\n backdrop-filter: var(--ty-modal-backdrop-blur, blur(2px));\n animation: ty-modal-backdrop-enter var(--ty-modal-duration, 200ms) ease-out;\n}\n\n/* When backdrop is disabled */\n.ty-modal-dialog:not([data-backdrop])::backdrop {\n background: transparent;\n backdrop-filter: none;\n}\n\n/* Minimal container - no styling imposed on user content */\n.modal-content {\n color: var(--ty-text);\n}\n\n@keyframes ty-modal-backdrop-enter {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n";
|
|
7
|
+
export declare const modalStyles = "\n:host {\n display: contents;\n /* Don't interfere with layout */\n}\n\n/* Part of the modal wrapper functionality, not user content styling */\n.close-button {\n position: absolute;\n top: -8px;\n right: -8px;\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n transition: var(--ty-transition-colors);\n z-index: 1;\n color: #d8d8d8;\n\n /* Remove all default browser styling that could cause outlines/borders */\n outline: none;\n box-shadow: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n\n /* Remove mobile tap highlights and focus rings */\n -webkit-tap-highlight-color: transparent;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.close-button:hover {\n color: white;\n}\n\n/* Explicitly remove focus and active states for all interaction modes */\n.close-button:focus,\n.close-button:focus-visible,\n.close-button:focus-within,\n.close-button:active {\n outline: none !important;\n box-shadow: none !important;\n border: none !important;\n background: transparent !important;\n}\n\n/* Override hover color even in focus/active states */\n.close-button:hover,\n.close-button:focus:hover,\n.close-button:active:hover {\n color: white;\n}\n\n.close-button.hide {\n visibility: hidden;\n}\n\n/* Pure wrapper: only essential modal functionality, no styling of user content */\n.ty-modal-dialog {\n /* Reset default dialog styles */\n padding: 18px;\n border: none;\n margin: 0;\n background: transparent;\n\n /* Centered */\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n\n z-index: 1000;\n overflow: hidden;\n\n /* No sizing - let user content determine size */\n max-width: 85vw;\n max-height: 90vh;\n\n outline: none;\n}\n\n/* Panel zoom-in synced with the backdrop fade (shadcn-style zoom-in-95).\n Enter-only: animating exit on a native <dialog> means delaying close(). */\n.ty-modal-dialog[open] {\n animation: ty-modal-enter var(--ty-modal-duration, 200ms) ease-out;\n}\n\n@keyframes ty-modal-enter {\n from {\n opacity: 0;\n transform: translate(-50%, -50%) scale(0.93);\n }\n\n to {\n opacity: 1;\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n/* Backdrop is part of modal functionality, not user content */\n.ty-modal-dialog::backdrop {\n background: var(--ty-modal-backdrop, rgba(0, 0, 0, 0.5));\n backdrop-filter: var(--ty-modal-backdrop-blur, blur(2px));\n animation: ty-modal-backdrop-enter var(--ty-modal-duration, 200ms) ease-out;\n}\n\n/* When backdrop is disabled */\n.ty-modal-dialog:not([data-backdrop])::backdrop {\n background: transparent;\n backdrop-filter: none;\n}\n\n/* Minimal container - no styling imposed on user content */\n.modal-content {\n color: var(--ty-text);\n}\n\n@keyframes ty-modal-backdrop-enter {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n";
|
package/lib/styles/modal.js
CHANGED
|
@@ -84,6 +84,24 @@ export const modalStyles = `
|
|
|
84
84
|
outline: none;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/* Panel zoom-in synced with the backdrop fade (shadcn-style zoom-in-95).
|
|
88
|
+
Enter-only: animating exit on a native <dialog> means delaying close(). */
|
|
89
|
+
.ty-modal-dialog[open] {
|
|
90
|
+
animation: ty-modal-enter var(--ty-modal-duration, 200ms) ease-out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@keyframes ty-modal-enter {
|
|
94
|
+
from {
|
|
95
|
+
opacity: 0;
|
|
96
|
+
transform: translate(-50%, -50%) scale(0.93);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
to {
|
|
100
|
+
opacity: 1;
|
|
101
|
+
transform: translate(-50%, -50%) scale(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
/* Backdrop is part of modal functionality, not user content */
|
|
88
106
|
.ty-modal-dialog::backdrop {
|
|
89
107
|
background: var(--ty-modal-backdrop, rgba(0, 0, 0, 0.5));
|
package/lib/styles/option.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Option Component Styles */
|
|
2
|
-
export declare const optionStyles = "\n.option-content {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--ty-spacing-2);\n box-sizing: border-box;\n width: 100%;\n\n cursor: pointer;\n user-select: none;\n\n transition: var(--ty-transition-all);\n\n /* Linear-paired typography */\n color: var(--ty-text);\n font-family: inherit;\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: var(--ty-spacing-2) var(--ty-spacing-3);\n border-radius: var(--ty-radius-md);\n}\n\n:host([cloned]) .option-content {\n padding: 0px;\n background: transparent;\n}\n\n.option-content:hover {\n background: var(--ty-bg-neutral-soft);\n}\n\n.option-content[highlighted] {\n background: var(--ty-bg-neutral-soft);\n color: var(--ty-text-strong);\n font-weight: var(--ty-font-semibold);\n}\n\n/* Selected: quiet \u2014 bolder text color only, no background slab, no weight change */\n.option-content[selected] {\n background: transparent;\n color: var(--ty-text-bold);\n}\n\n/* Lucide check \u2014 right-aligned, visible only on selected options */\n.option-check {\n display: none;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n margin-left: auto;\n color: var(--ty-color-primary);\n}\n\n.option-content[selected] .option-check {\n display: block;\n}\n\n/* Never show the check on the copy cloned into the select's trigger field */\n:host([cloned]) .option-check {\n display: none;\n}\n\n.option-content[selected]:hover {\n background: var(--ty-bg-neutral-soft);\n}\n\n.option-content[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.option-content[hidden] {}\n\n.option-content * {\n pointer-events: none;\n}\n";
|
|
2
|
+
export declare const optionStyles = "\n.option-content {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--ty-spacing-2);\n box-sizing: border-box;\n width: 100%;\n\n cursor: pointer;\n user-select: none;\n\n transition: var(--ty-transition-all);\n\n /* Linear-paired typography */\n color: var(--ty-text);\n font-family: inherit;\n font-size: var(--ty-font-sm);\n line-height: var(--ty-leading-sm);\n letter-spacing: var(--ty-tracking-sm);\n padding: var(--ty-spacing-2) var(--ty-spacing-3);\n border-radius: var(--ty-radius-md);\n}\n\n/* Cloned copy (projected into the select's trigger field): host defaults to\n inline like any unstyled custom element, which ignores width/overflow \u2014\n force a block box so the parent's flex-basis:0 truncation (select.ts\n .select-stub.has-clone slot[name=\"selected\"]) actually clips it, and\n collapse the content to a single ellipsized line instead of wrapping. */\n:host([cloned]) {\n display: block;\n min-width: 0;\n overflow: hidden;\n}\n:host([cloned]) .option-content {\n display: block;\n padding: 0px;\n background: transparent;\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.option-content:hover {\n background: var(--ty-bg-neutral-soft);\n}\n\n.option-content[highlighted] {\n background: var(--ty-bg-neutral-soft);\n color: var(--ty-text-strong);\n font-weight: var(--ty-font-semibold);\n}\n\n/* Selected: quiet \u2014 bolder text color only, no background slab, no weight change */\n.option-content[selected] {\n background: transparent;\n color: var(--ty-text-bold);\n}\n\n/* Lucide check \u2014 right-aligned, visible only on selected options */\n.option-check {\n display: none;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n margin-left: auto;\n color: var(--ty-color-primary);\n}\n\n.option-content[selected] .option-check {\n display: block;\n}\n\n/* Never show the check on the copy cloned into the select's trigger field */\n:host([cloned]) .option-check {\n display: none;\n}\n\n.option-content[selected]:hover {\n background: var(--ty-bg-neutral-soft);\n}\n\n.option-content[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.option-content[hidden] {}\n\n.option-content * {\n pointer-events: none;\n}\n";
|