tyrell-components 1.0.0-TC52 → 1.0.0-TC53

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 (105) hide show
  1. package/css/tyrell-theme.css +25 -2
  2. package/css/tyrell.css +4 -0
  3. package/dist/tyrell-colors-static.css +1 -316
  4. package/dist/tyrell-theme.css +1 -1443
  5. package/dist/tyrell.css +1 -1390
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.js +196 -196
  8. package/lib/components/button.d.ts +1 -1
  9. package/lib/components/button.js +260 -259
  10. package/lib/components/calendar-month.d.ts +2 -2
  11. package/lib/components/calendar-month.js +426 -416
  12. package/lib/components/calendar-navigation.js +238 -238
  13. package/lib/components/calendar.js +605 -602
  14. package/lib/components/checkbox.d.ts +1 -1
  15. package/lib/components/checkbox.js +273 -272
  16. package/lib/components/copy.d.ts +1 -1
  17. package/lib/components/copy.js +279 -278
  18. package/lib/components/date-picker.d.ts +1 -1
  19. package/lib/components/date-picker.js +1103 -1108
  20. package/lib/components/file-upload.js +404 -404
  21. package/lib/components/icon.js +141 -141
  22. package/lib/components/input.d.ts +1 -1
  23. package/lib/components/input.js +593 -596
  24. package/lib/components/modal.js +319 -319
  25. package/lib/components/option.js +167 -167
  26. package/lib/components/popup.js +233 -233
  27. package/lib/components/radio.d.ts +2 -2
  28. package/lib/components/radio.js +407 -406
  29. package/lib/components/resize-observer.js +67 -67
  30. package/lib/components/scroll-container.js +294 -294
  31. package/lib/components/select.d.ts +1 -1
  32. package/lib/components/select.js +1524 -1532
  33. package/lib/components/selected-tags.js +184 -184
  34. package/lib/components/step.js +62 -62
  35. package/lib/components/switch.d.ts +1 -1
  36. package/lib/components/switch.js +214 -213
  37. package/lib/components/tab.js +62 -62
  38. package/lib/components/tabs.js +507 -507
  39. package/lib/components/tag.d.ts +1 -1
  40. package/lib/components/tag.js +236 -235
  41. package/lib/components/textarea.d.ts +1 -1
  42. package/lib/components/textarea.js +457 -456
  43. package/lib/components/tooltip.js +283 -283
  44. package/lib/components/wizard.js +384 -384
  45. package/lib/icons/heroicons/micro.js +392 -392
  46. package/lib/icons/heroicons/mini.js +382 -382
  47. package/lib/icons/heroicons/outline.js +337 -337
  48. package/lib/icons/heroicons/solid.js +407 -407
  49. package/lib/icons/lucide.js +22843 -22843
  50. package/lib/icons/material/filled.js +9653 -9653
  51. package/lib/icons/material/outlined.js +10271 -10271
  52. package/lib/icons/material/round.js +10495 -10495
  53. package/lib/icons/material/sharp.js +9905 -9905
  54. package/lib/icons/material/two-tone.js +12792 -12792
  55. package/lib/index.js +21 -21
  56. package/lib/styles/button.js +176 -288
  57. package/lib/styles/calendar-month.js +123 -183
  58. package/lib/styles/calendar-navigation.js +60 -79
  59. package/lib/styles/calendar.js +8 -9
  60. package/lib/styles/checkbox.js +45 -72
  61. package/lib/styles/copy.js +67 -91
  62. package/lib/styles/custom-scrollbar.js +80 -100
  63. package/lib/styles/date-picker.js +261 -331
  64. package/lib/styles/field-size.d.ts +6 -0
  65. package/lib/styles/field-size.js +91 -0
  66. package/lib/styles/file-upload.js +139 -169
  67. package/lib/styles/icon.js +92 -156
  68. package/lib/styles/input.js +125 -241
  69. package/lib/styles/modal.js +67 -102
  70. package/lib/styles/option.d.ts +1 -1
  71. package/lib/styles/option.js +47 -67
  72. package/lib/styles/popup.js +34 -65
  73. package/lib/styles/radio.js +69 -96
  74. package/lib/styles/resize-observer.js +2 -5
  75. package/lib/styles/scroll-container.js +100 -130
  76. package/lib/styles/select-base.js +525 -778
  77. package/lib/styles/select.js +140 -228
  78. package/lib/styles/step.js +22 -29
  79. package/lib/styles/switch.js +34 -55
  80. package/lib/styles/tab.js +22 -29
  81. package/lib/styles/tabs.js +171 -252
  82. package/lib/styles/tag.js +135 -188
  83. package/lib/styles/textarea.d.ts +1 -1
  84. package/lib/styles/textarea.js +205 -271
  85. package/lib/styles/tooltip.js +3 -3
  86. package/lib/styles/wizard.js +170 -226
  87. package/lib/types/common.d.ts +2 -0
  88. package/lib/types/common.js +6 -5
  89. package/lib/utils/calendar-utils.js +100 -100
  90. package/lib/utils/custom-scrollbar.js +272 -272
  91. package/lib/utils/flavor-sheet.js +14 -14
  92. package/lib/utils/icon-registry.js +127 -127
  93. package/lib/utils/loader-registry.js +3 -3
  94. package/lib/utils/locale.js +34 -34
  95. package/lib/utils/mobile.js +1 -1
  96. package/lib/utils/number-format.js +54 -54
  97. package/lib/utils/positioning.js +212 -212
  98. package/lib/utils/property-capture.js +36 -36
  99. package/lib/utils/property-manager.js +125 -125
  100. package/lib/utils/resize-observer.js +23 -23
  101. package/lib/utils/scroll-lock.js +57 -57
  102. package/lib/utils/styles.js +17 -17
  103. package/lib/version.d.ts +1 -1
  104. package/lib/version.js +1 -1
  105. package/package.json +4 -4
@@ -5,415 +5,415 @@ const UPLOAD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
5
5
  const FILE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
6
6
  const X_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
7
7
  function formatBytes(bytes) {
8
- if (bytes < 1024)
9
- return `${bytes} B`;
10
- if (bytes < 1024 * 1024)
11
- return `${(bytes / 1024).toFixed(1)} KB`;
12
- if (bytes < 1024 * 1024 * 1024)
13
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
14
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
8
+ if (bytes < 1024)
9
+ return `${bytes} B`;
10
+ if (bytes < 1024 * 1024)
11
+ return `${(bytes / 1024).toFixed(1)} KB`;
12
+ if (bytes < 1024 * 1024 * 1024)
13
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
14
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
15
15
  }
16
16
  export class TyFileUpload extends TyComponent {
17
- constructor() {
18
- super();
19
- this._files = [];
20
- this._isDragging = false;
21
- this._listenersSetup = false;
22
- this._dropZoneClickHandler = null;
23
- this._dropZoneKeydownHandler = null;
24
- this._dropZoneFocusHandler = null;
25
- this._dropZoneBlurHandler = null;
26
- this._dragoverHandler = null;
27
- this._dragleaveHandler = null;
28
- this._dropHandler = null;
29
- this._inputChangeHandler = null;
30
- ensureStyles(this.shadowRoot, { css: fileUploadStyles, id: "ty-file-upload" });
31
- }
32
- onConnect() { }
33
- onDisconnect() {
34
- this.removeEventListeners();
35
- }
36
- onPropertiesChanged(_changes) { }
37
- onFormReset() {
38
- this._files = [];
39
- this.updateFormValue();
40
- if (this._connected)
41
- this.render();
42
- const fileInput = this.shadowRoot?.querySelector(".file-input");
43
- if (fileInput)
44
- fileInput.value = "";
45
- }
46
- getFormValue() {
47
- if (this._files.length === 0 || !this.name)
48
- return null;
49
- const fd = new FormData();
50
- for (const file of this._files) {
51
- fd.append(this.name, file);
52
- }
53
- return fd;
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
- }
71
- get files() {
72
- return [...this._files];
73
- }
74
- get multiple() {
75
- return this.getProperty("multiple");
76
- }
77
- set multiple(v) {
78
- this.setProperty("multiple", v);
79
- }
80
- get accept() {
81
- return this.getProperty("accept");
82
- }
83
- set accept(v) {
84
- this.setProperty("accept", v);
85
- }
86
- get name() {
87
- return this.getProperty("name");
88
- }
89
- set name(v) {
90
- this.setProperty("name", v);
91
- }
92
- get disabled() {
93
- return this.getProperty("disabled");
94
- }
95
- set disabled(v) {
96
- this.setProperty("disabled", v);
97
- }
98
- get required() {
99
- return this.getProperty("required");
100
- }
101
- set required(v) {
102
- this.setProperty("required", v);
103
- }
104
- get label() {
105
- return this.getProperty("label");
106
- }
107
- set label(v) {
108
- this.setProperty("label", v);
109
- }
110
- get placeholder() {
111
- return this.getProperty("placeholder");
112
- }
113
- set placeholder(v) {
114
- this.setProperty("placeholder", v);
115
- }
116
- get error() {
117
- return this.getProperty("error");
118
- }
119
- set error(v) {
120
- this.setProperty("error", v);
121
- }
122
- get form() {
123
- return this._internals.form;
124
- }
125
- setFiles(newFiles) {
126
- this._files = this.multiple ? newFiles : newFiles.slice(0, 1);
127
- this.updateFormValue();
128
- this.render();
129
- const detail = {
130
- value: this._files,
131
- files: this._files,
132
- names: this._files.map((f) => f.name),
133
- };
134
- setTimeout(() => {
135
- this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true, composed: true }));
136
- }, 0);
137
- }
138
- removeFile(index) {
139
- const updated = [...this._files];
140
- updated.splice(index, 1);
141
- this.setFiles(updated);
142
- const fileInput = this.shadowRoot?.querySelector(".file-input");
143
- if (fileInput)
144
- fileInput.value = "";
145
- }
146
- setupEventListeners() {
147
- if (this._listenersSetup)
148
- return;
149
- const shadow = this.shadowRoot;
150
- const dropZone = shadow.querySelector(".drop-zone");
151
- const fileInput = shadow.querySelector(".file-input");
152
- this._dropZoneClickHandler = (e) => {
153
- if (this.disabled)
154
- return;
155
- if (e.target.closest(".file-remove"))
156
- return;
157
- fileInput.click();
158
- };
159
- this._dropZoneKeydownHandler = (e) => {
160
- if (this.disabled)
161
- return;
162
- if (e.key === "Enter" || e.key === " ") {
163
- e.preventDefault();
164
- fileInput.click();
165
- }
166
- };
167
- this._dropZoneFocusHandler = () => {
168
- dropZone.classList.add("focused");
169
- };
170
- this._dropZoneBlurHandler = () => {
171
- dropZone.classList.remove("focused");
172
- };
173
- this._dragoverHandler = (e) => {
174
- if (this.disabled)
175
- return;
176
- e.preventDefault();
177
- e.stopPropagation();
178
- if (!this._isDragging) {
179
- this._isDragging = true;
180
- dropZone.classList.add("drag-active");
181
- }
182
- };
183
- this._dragleaveHandler = (e) => {
184
- if (!dropZone.contains(e.relatedTarget)) {
185
- this._isDragging = false;
186
- dropZone.classList.remove("drag-active");
187
- }
188
- };
189
- this._dropHandler = (e) => {
190
- if (this.disabled)
191
- return;
192
- e.preventDefault();
193
- e.stopPropagation();
194
- this._isDragging = false;
195
- dropZone.classList.remove("drag-active");
196
- const files = Array.from(e.dataTransfer?.files ?? []).filter((f) => this.matchesAccept(f));
197
- if (files.length > 0)
198
- this.setFiles(files);
199
- };
200
- this._inputChangeHandler = (e) => {
201
- const input = e.target;
202
- if (input.files && input.files.length > 0) {
203
- this.setFiles(Array.from(input.files));
204
- }
205
- };
206
- dropZone.addEventListener("click", this._dropZoneClickHandler);
207
- dropZone.addEventListener("keydown", this._dropZoneKeydownHandler);
208
- dropZone.addEventListener("focus", this._dropZoneFocusHandler);
209
- dropZone.addEventListener("blur", this._dropZoneBlurHandler);
210
- dropZone.addEventListener("dragover", this._dragoverHandler);
211
- dropZone.addEventListener("dragleave", this._dragleaveHandler);
212
- dropZone.addEventListener("drop", this._dropHandler);
213
- fileInput.addEventListener("change", this._inputChangeHandler);
214
- this._listenersSetup = true;
215
- }
216
- removeEventListeners() {
217
- if (!this._listenersSetup)
218
- return;
219
- const shadow = this.shadowRoot;
220
- if (!shadow)
221
- return;
222
- const dropZone = shadow.querySelector(".drop-zone");
223
- const fileInput = shadow.querySelector(".file-input");
224
- if (!dropZone || !fileInput)
225
- return;
226
- if (this._dropZoneClickHandler)
227
- dropZone.removeEventListener("click", this._dropZoneClickHandler);
228
- if (this._dropZoneKeydownHandler)
229
- dropZone.removeEventListener("keydown", this._dropZoneKeydownHandler);
230
- if (this._dropZoneFocusHandler)
231
- dropZone.removeEventListener("focus", this._dropZoneFocusHandler);
232
- if (this._dropZoneBlurHandler)
233
- dropZone.removeEventListener("blur", this._dropZoneBlurHandler);
234
- if (this._dragoverHandler)
235
- dropZone.removeEventListener("dragover", this._dragoverHandler);
236
- if (this._dragleaveHandler)
237
- dropZone.removeEventListener("dragleave", this._dragleaveHandler);
238
- if (this._dropHandler)
239
- dropZone.removeEventListener("drop", this._dropHandler);
240
- if (this._inputChangeHandler)
241
- fileInput.removeEventListener("change", this._inputChangeHandler);
242
- this._listenersSetup = false;
243
- }
244
- buildDropZoneClasses() {
245
- const classes = ["drop-zone"];
246
- if (this._isDragging)
247
- classes.push("drag-active");
248
- if (this._files.length > 0)
249
- classes.push("has-files");
250
- if (this.disabled)
251
- classes.push("disabled");
252
- if (this.error)
253
- classes.push("error");
254
- return classes.join(" ");
255
- }
256
- renderFileList(fileList) {
257
- fileList.innerHTML = "";
258
- for (let i = 0; i < this._files.length; i++) {
259
- const file = this._files[i];
260
- const item = document.createElement("div");
261
- item.className = "file-item";
262
- const icon = document.createElement("span");
263
- icon.className = "file-icon";
264
- icon.innerHTML = FILE_ICON;
265
- const name = document.createElement("span");
266
- name.className = "file-name";
267
- name.textContent = file.name;
268
- name.title = file.name;
269
- const size = document.createElement("span");
270
- size.className = "file-size";
271
- size.textContent = formatBytes(file.size);
272
- const removeBtn = document.createElement("button");
273
- removeBtn.className = "file-remove";
274
- removeBtn.type = "button";
275
- removeBtn.innerHTML = X_ICON;
276
- removeBtn.setAttribute("aria-label", `Remove ${file.name}`);
277
- const capturedIndex = i;
278
- removeBtn.addEventListener("click", (e) => {
279
- e.stopPropagation();
280
- this.removeFile(capturedIndex);
281
- });
282
- item.appendChild(icon);
283
- item.appendChild(name);
284
- item.appendChild(size);
285
- item.appendChild(removeBtn);
286
- fileList.appendChild(item);
287
- }
288
- }
289
- render() {
290
- const shadow = this.shadowRoot;
291
- let container = shadow.querySelector(".file-upload-container");
292
- if (!container) {
293
- container = document.createElement("div");
294
- container.className = "file-upload-container";
295
- const labelEl = document.createElement("div");
296
- labelEl.className = "upload-label";
297
- container.appendChild(labelEl);
298
- const dropZone = document.createElement("div");
299
- dropZone.className = "drop-zone";
300
- dropZone.setAttribute("role", "button");
301
- const fileInput = document.createElement("input");
302
- fileInput.type = "file";
303
- fileInput.className = "file-input";
304
- fileInput.style.display = "none";
305
- dropZone.appendChild(fileInput);
306
- const uploadIcon = document.createElement("div");
307
- uploadIcon.className = "upload-icon";
308
- uploadIcon.innerHTML = UPLOAD_ICON;
309
- dropZone.appendChild(uploadIcon);
310
- const hintEl = document.createElement("div");
311
- hintEl.className = "upload-hint";
312
- dropZone.appendChild(hintEl);
313
- const subHintEl = document.createElement("div");
314
- subHintEl.className = "upload-sub-hint";
315
- dropZone.appendChild(subHintEl);
316
- container.appendChild(dropZone);
317
- const fileList = document.createElement("div");
318
- fileList.className = "file-list";
319
- container.appendChild(fileList);
320
- const errorEl = document.createElement("div");
321
- errorEl.className = "error-message";
322
- container.appendChild(errorEl);
323
- shadow.appendChild(container);
324
- this.setupEventListeners();
325
- }
326
- const labelEl = container.querySelector(".upload-label");
327
- if (this.label) {
328
- labelEl.style.display = "";
329
- labelEl.innerHTML = "";
330
- labelEl.appendChild(document.createTextNode(this.label));
331
- if (this.required) {
332
- const req = document.createElement("span");
333
- req.className = "required-icon";
334
- req.textContent = "*";
335
- req.setAttribute("aria-hidden", "true");
336
- labelEl.appendChild(req);
337
- }
338
- }
339
- else {
340
- labelEl.style.display = "none";
341
- }
342
- const dropZone = container.querySelector(".drop-zone");
343
- dropZone.className = this.buildDropZoneClasses();
344
- dropZone.tabIndex = this.disabled ? -1 : 0;
345
- dropZone.setAttribute("aria-disabled", String(this.disabled));
346
- dropZone.setAttribute("aria-label", this.placeholder || "Upload files");
347
- const fileInput = container.querySelector(".file-input");
348
- fileInput.multiple = this.multiple;
349
- fileInput.accept = this.accept;
350
- fileInput.disabled = this.disabled;
351
- const uploadIcon = container.querySelector(".upload-icon");
352
- uploadIcon.style.display = this._files.length > 0 ? "none" : "";
353
- const hintEl = container.querySelector(".upload-hint");
354
- if (this._files.length > 0) {
355
- hintEl.innerHTML = `<strong>${this._files.length} file${this._files.length !== 1 ? "s" : ""} selected</strong>`;
356
- }
357
- else {
358
- hintEl.innerHTML = `Drop files here or <span class="browse-link">click to browse</span>`;
359
- }
360
- const subHintEl = container.querySelector(".upload-sub-hint");
361
- if (this.accept && this._files.length === 0) {
362
- subHintEl.textContent = `Accepted: ${this.accept}`;
363
- subHintEl.style.display = "";
364
- }
365
- else {
366
- subHintEl.style.display = "none";
367
- }
368
- const fileList = container.querySelector(".file-list");
369
- this.renderFileList(fileList);
370
- const errorEl = container.querySelector(".error-message");
371
- errorEl.textContent = this.error;
372
- errorEl.style.display = this.error ? "" : "none";
373
- }
17
+ constructor() {
18
+ super();
19
+ this._files = [];
20
+ this._isDragging = false;
21
+ this._listenersSetup = false;
22
+ this._dropZoneClickHandler = null;
23
+ this._dropZoneKeydownHandler = null;
24
+ this._dropZoneFocusHandler = null;
25
+ this._dropZoneBlurHandler = null;
26
+ this._dragoverHandler = null;
27
+ this._dragleaveHandler = null;
28
+ this._dropHandler = null;
29
+ this._inputChangeHandler = null;
30
+ ensureStyles(this.shadowRoot, { css: fileUploadStyles, id: "ty-file-upload" });
31
+ }
32
+ onConnect() { }
33
+ onDisconnect() {
34
+ this.removeEventListeners();
35
+ }
36
+ onPropertiesChanged(_changes) { }
37
+ onFormReset() {
38
+ this._files = [];
39
+ this.updateFormValue();
40
+ if (this._connected)
41
+ this.render();
42
+ const fileInput = this.shadowRoot?.querySelector(".file-input");
43
+ if (fileInput)
44
+ fileInput.value = "";
45
+ }
46
+ getFormValue() {
47
+ if (this._files.length === 0 || !this.name)
48
+ return null;
49
+ const fd = new FormData();
50
+ for (const file of this._files) {
51
+ fd.append(this.name, file);
52
+ }
53
+ return fd;
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
+ }
71
+ get files() {
72
+ return [...this._files];
73
+ }
74
+ get multiple() {
75
+ return this.getProperty("multiple");
76
+ }
77
+ set multiple(v) {
78
+ this.setProperty("multiple", v);
79
+ }
80
+ get accept() {
81
+ return this.getProperty("accept");
82
+ }
83
+ set accept(v) {
84
+ this.setProperty("accept", v);
85
+ }
86
+ get name() {
87
+ return this.getProperty("name");
88
+ }
89
+ set name(v) {
90
+ this.setProperty("name", v);
91
+ }
92
+ get disabled() {
93
+ return this.getProperty("disabled");
94
+ }
95
+ set disabled(v) {
96
+ this.setProperty("disabled", v);
97
+ }
98
+ get required() {
99
+ return this.getProperty("required");
100
+ }
101
+ set required(v) {
102
+ this.setProperty("required", v);
103
+ }
104
+ get label() {
105
+ return this.getProperty("label");
106
+ }
107
+ set label(v) {
108
+ this.setProperty("label", v);
109
+ }
110
+ get placeholder() {
111
+ return this.getProperty("placeholder");
112
+ }
113
+ set placeholder(v) {
114
+ this.setProperty("placeholder", v);
115
+ }
116
+ get error() {
117
+ return this.getProperty("error");
118
+ }
119
+ set error(v) {
120
+ this.setProperty("error", v);
121
+ }
122
+ get form() {
123
+ return this._internals.form;
124
+ }
125
+ setFiles(newFiles) {
126
+ this._files = this.multiple ? newFiles : newFiles.slice(0, 1);
127
+ this.updateFormValue();
128
+ this.render();
129
+ const detail = {
130
+ value: this._files,
131
+ files: this._files,
132
+ names: this._files.map((f) => f.name),
133
+ };
134
+ setTimeout(() => {
135
+ this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true, composed: true }));
136
+ }, 0);
137
+ }
138
+ removeFile(index) {
139
+ const updated = [...this._files];
140
+ updated.splice(index, 1);
141
+ this.setFiles(updated);
142
+ const fileInput = this.shadowRoot?.querySelector(".file-input");
143
+ if (fileInput)
144
+ fileInput.value = "";
145
+ }
146
+ setupEventListeners() {
147
+ if (this._listenersSetup)
148
+ return;
149
+ const shadow = this.shadowRoot;
150
+ const dropZone = shadow.querySelector(".drop-zone");
151
+ const fileInput = shadow.querySelector(".file-input");
152
+ this._dropZoneClickHandler = (e) => {
153
+ if (this.disabled)
154
+ return;
155
+ if (e.target.closest(".file-remove"))
156
+ return;
157
+ fileInput.click();
158
+ };
159
+ this._dropZoneKeydownHandler = (e) => {
160
+ if (this.disabled)
161
+ return;
162
+ if (e.key === "Enter" || e.key === " ") {
163
+ e.preventDefault();
164
+ fileInput.click();
165
+ }
166
+ };
167
+ this._dropZoneFocusHandler = () => {
168
+ dropZone.classList.add("focused");
169
+ };
170
+ this._dropZoneBlurHandler = () => {
171
+ dropZone.classList.remove("focused");
172
+ };
173
+ this._dragoverHandler = (e) => {
174
+ if (this.disabled)
175
+ return;
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ if (!this._isDragging) {
179
+ this._isDragging = true;
180
+ dropZone.classList.add("drag-active");
181
+ }
182
+ };
183
+ this._dragleaveHandler = (e) => {
184
+ if (!dropZone.contains(e.relatedTarget)) {
185
+ this._isDragging = false;
186
+ dropZone.classList.remove("drag-active");
187
+ }
188
+ };
189
+ this._dropHandler = (e) => {
190
+ if (this.disabled)
191
+ return;
192
+ e.preventDefault();
193
+ e.stopPropagation();
194
+ this._isDragging = false;
195
+ dropZone.classList.remove("drag-active");
196
+ const files = Array.from(e.dataTransfer?.files ?? []).filter((f) => this.matchesAccept(f));
197
+ if (files.length > 0)
198
+ this.setFiles(files);
199
+ };
200
+ this._inputChangeHandler = (e) => {
201
+ const input = e.target;
202
+ if (input.files && input.files.length > 0) {
203
+ this.setFiles(Array.from(input.files));
204
+ }
205
+ };
206
+ dropZone.addEventListener("click", this._dropZoneClickHandler);
207
+ dropZone.addEventListener("keydown", this._dropZoneKeydownHandler);
208
+ dropZone.addEventListener("focus", this._dropZoneFocusHandler);
209
+ dropZone.addEventListener("blur", this._dropZoneBlurHandler);
210
+ dropZone.addEventListener("dragover", this._dragoverHandler);
211
+ dropZone.addEventListener("dragleave", this._dragleaveHandler);
212
+ dropZone.addEventListener("drop", this._dropHandler);
213
+ fileInput.addEventListener("change", this._inputChangeHandler);
214
+ this._listenersSetup = true;
215
+ }
216
+ removeEventListeners() {
217
+ if (!this._listenersSetup)
218
+ return;
219
+ const shadow = this.shadowRoot;
220
+ if (!shadow)
221
+ return;
222
+ const dropZone = shadow.querySelector(".drop-zone");
223
+ const fileInput = shadow.querySelector(".file-input");
224
+ if (!dropZone || !fileInput)
225
+ return;
226
+ if (this._dropZoneClickHandler)
227
+ dropZone.removeEventListener("click", this._dropZoneClickHandler);
228
+ if (this._dropZoneKeydownHandler)
229
+ dropZone.removeEventListener("keydown", this._dropZoneKeydownHandler);
230
+ if (this._dropZoneFocusHandler)
231
+ dropZone.removeEventListener("focus", this._dropZoneFocusHandler);
232
+ if (this._dropZoneBlurHandler)
233
+ dropZone.removeEventListener("blur", this._dropZoneBlurHandler);
234
+ if (this._dragoverHandler)
235
+ dropZone.removeEventListener("dragover", this._dragoverHandler);
236
+ if (this._dragleaveHandler)
237
+ dropZone.removeEventListener("dragleave", this._dragleaveHandler);
238
+ if (this._dropHandler)
239
+ dropZone.removeEventListener("drop", this._dropHandler);
240
+ if (this._inputChangeHandler)
241
+ fileInput.removeEventListener("change", this._inputChangeHandler);
242
+ this._listenersSetup = false;
243
+ }
244
+ buildDropZoneClasses() {
245
+ const classes = ["drop-zone"];
246
+ if (this._isDragging)
247
+ classes.push("drag-active");
248
+ if (this._files.length > 0)
249
+ classes.push("has-files");
250
+ if (this.disabled)
251
+ classes.push("disabled");
252
+ if (this.error)
253
+ classes.push("error");
254
+ return classes.join(" ");
255
+ }
256
+ renderFileList(fileList) {
257
+ fileList.innerHTML = "";
258
+ for (let i = 0; i < this._files.length; i++) {
259
+ const file = this._files[i];
260
+ const item = document.createElement("div");
261
+ item.className = "file-item";
262
+ const icon = document.createElement("span");
263
+ icon.className = "file-icon";
264
+ icon.innerHTML = FILE_ICON;
265
+ const name = document.createElement("span");
266
+ name.className = "file-name";
267
+ name.textContent = file.name;
268
+ name.title = file.name;
269
+ const size = document.createElement("span");
270
+ size.className = "file-size";
271
+ size.textContent = formatBytes(file.size);
272
+ const removeBtn = document.createElement("button");
273
+ removeBtn.className = "file-remove";
274
+ removeBtn.type = "button";
275
+ removeBtn.innerHTML = X_ICON;
276
+ removeBtn.setAttribute("aria-label", `Remove ${file.name}`);
277
+ const capturedIndex = i;
278
+ removeBtn.addEventListener("click", (e) => {
279
+ e.stopPropagation();
280
+ this.removeFile(capturedIndex);
281
+ });
282
+ item.appendChild(icon);
283
+ item.appendChild(name);
284
+ item.appendChild(size);
285
+ item.appendChild(removeBtn);
286
+ fileList.appendChild(item);
287
+ }
288
+ }
289
+ render() {
290
+ const shadow = this.shadowRoot;
291
+ let container = shadow.querySelector(".file-upload-container");
292
+ if (!container) {
293
+ container = document.createElement("div");
294
+ container.className = "file-upload-container";
295
+ const labelEl = document.createElement("div");
296
+ labelEl.className = "upload-label";
297
+ container.appendChild(labelEl);
298
+ const dropZone = document.createElement("div");
299
+ dropZone.className = "drop-zone";
300
+ dropZone.setAttribute("role", "button");
301
+ const fileInput = document.createElement("input");
302
+ fileInput.type = "file";
303
+ fileInput.className = "file-input";
304
+ fileInput.style.display = "none";
305
+ dropZone.appendChild(fileInput);
306
+ const uploadIcon = document.createElement("div");
307
+ uploadIcon.className = "upload-icon";
308
+ uploadIcon.innerHTML = UPLOAD_ICON;
309
+ dropZone.appendChild(uploadIcon);
310
+ const hintEl = document.createElement("div");
311
+ hintEl.className = "upload-hint";
312
+ dropZone.appendChild(hintEl);
313
+ const subHintEl = document.createElement("div");
314
+ subHintEl.className = "upload-sub-hint";
315
+ dropZone.appendChild(subHintEl);
316
+ container.appendChild(dropZone);
317
+ const fileList = document.createElement("div");
318
+ fileList.className = "file-list";
319
+ container.appendChild(fileList);
320
+ const errorEl = document.createElement("div");
321
+ errorEl.className = "error-message";
322
+ container.appendChild(errorEl);
323
+ shadow.appendChild(container);
324
+ this.setupEventListeners();
325
+ }
326
+ const labelEl = container.querySelector(".upload-label");
327
+ if (this.label) {
328
+ labelEl.style.display = "";
329
+ labelEl.innerHTML = "";
330
+ labelEl.appendChild(document.createTextNode(this.label));
331
+ if (this.required) {
332
+ const req = document.createElement("span");
333
+ req.className = "required-icon";
334
+ req.textContent = "*";
335
+ req.setAttribute("aria-hidden", "true");
336
+ labelEl.appendChild(req);
337
+ }
338
+ }
339
+ else {
340
+ labelEl.style.display = "none";
341
+ }
342
+ const dropZone = container.querySelector(".drop-zone");
343
+ dropZone.className = this.buildDropZoneClasses();
344
+ dropZone.tabIndex = this.disabled ? -1 : 0;
345
+ dropZone.setAttribute("aria-disabled", String(this.disabled));
346
+ dropZone.setAttribute("aria-label", this.placeholder || "Upload files");
347
+ const fileInput = container.querySelector(".file-input");
348
+ fileInput.multiple = this.multiple;
349
+ fileInput.accept = this.accept;
350
+ fileInput.disabled = this.disabled;
351
+ const uploadIcon = container.querySelector(".upload-icon");
352
+ uploadIcon.style.display = this._files.length > 0 ? "none" : "";
353
+ const hintEl = container.querySelector(".upload-hint");
354
+ if (this._files.length > 0) {
355
+ hintEl.innerHTML = `<strong>${this._files.length} file${this._files.length !== 1 ? "s" : ""} selected</strong>`;
356
+ }
357
+ else {
358
+ hintEl.innerHTML = `Drop files here or <span class="browse-link">click to browse</span>`;
359
+ }
360
+ const subHintEl = container.querySelector(".upload-sub-hint");
361
+ if (this.accept && this._files.length === 0) {
362
+ subHintEl.textContent = `Accepted: ${this.accept}`;
363
+ subHintEl.style.display = "";
364
+ }
365
+ else {
366
+ subHintEl.style.display = "none";
367
+ }
368
+ const fileList = container.querySelector(".file-list");
369
+ this.renderFileList(fileList);
370
+ const errorEl = container.querySelector(".error-message");
371
+ errorEl.textContent = this.error;
372
+ errorEl.style.display = this.error ? "" : "none";
373
+ }
374
374
  }
375
375
  TyFileUpload.formAssociated = true;
376
376
  TyFileUpload.properties = {
377
- multiple: {
378
- type: "boolean",
379
- visual: true,
380
- default: false,
381
- },
382
- accept: {
383
- type: "string",
384
- visual: true,
385
- default: "",
386
- },
387
- name: {
388
- type: "string",
389
- default: "",
390
- },
391
- disabled: {
392
- type: "boolean",
393
- visual: true,
394
- default: false,
395
- },
396
- required: {
397
- type: "boolean",
398
- visual: true,
399
- default: false,
400
- },
401
- label: {
402
- type: "string",
403
- visual: true,
404
- default: "",
405
- },
406
- placeholder: {
407
- type: "string",
408
- visual: true,
409
- default: "Drop files here or click to browse",
410
- },
411
- error: {
412
- type: "string",
413
- visual: true,
414
- default: "",
415
- },
377
+ multiple: {
378
+ type: "boolean",
379
+ visual: true,
380
+ default: false,
381
+ },
382
+ accept: {
383
+ type: "string",
384
+ visual: true,
385
+ default: "",
386
+ },
387
+ name: {
388
+ type: "string",
389
+ default: "",
390
+ },
391
+ disabled: {
392
+ type: "boolean",
393
+ visual: true,
394
+ default: false,
395
+ },
396
+ required: {
397
+ type: "boolean",
398
+ visual: true,
399
+ default: false,
400
+ },
401
+ label: {
402
+ type: "string",
403
+ visual: true,
404
+ default: "",
405
+ },
406
+ placeholder: {
407
+ type: "string",
408
+ visual: true,
409
+ default: "Drop files here or click to browse",
410
+ },
411
+ error: {
412
+ type: "string",
413
+ visual: true,
414
+ default: "",
415
+ },
416
416
  };
417
417
  if (!customElements.get("ty-file-upload")) {
418
- customElements.define("ty-file-upload", TyFileUpload);
418
+ customElements.define("ty-file-upload", TyFileUpload);
419
419
  }