@pure-ds/core 0.3.0

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 (129) hide show
  1. package/CSS-INTELLISENSE-LIMITATION.md +98 -0
  2. package/CSS-INTELLISENSE-QUICK-REF.md +238 -0
  3. package/INTELLISENSE.md +384 -0
  4. package/LICENSE +15 -0
  5. package/custom-elements-manifest.config.js +30 -0
  6. package/custom-elements.json +2003 -0
  7. package/dist/types/index.d.ts +2 -0
  8. package/dist/types/packages/pds-configurator/src/figma-export.d.ts +13 -0
  9. package/dist/types/packages/pds-configurator/src/figma-export.d.ts.map +1 -0
  10. package/dist/types/packages/pds-configurator/src/pds-config-form.d.ts +2 -0
  11. package/dist/types/packages/pds-configurator/src/pds-config-form.d.ts.map +1 -0
  12. package/dist/types/packages/pds-configurator/src/pds-configurator.d.ts +2 -0
  13. package/dist/types/packages/pds-configurator/src/pds-configurator.d.ts.map +1 -0
  14. package/dist/types/packages/pds-configurator/src/pds-demo.d.ts +2 -0
  15. package/dist/types/packages/pds-configurator/src/pds-demo.d.ts.map +1 -0
  16. package/dist/types/pds.config.d.ts +13 -0
  17. package/dist/types/pds.config.d.ts.map +1 -0
  18. package/dist/types/pds.d.ts +408 -0
  19. package/dist/types/public/assets/js/app.d.ts +2 -0
  20. package/dist/types/public/assets/js/app.d.ts.map +1 -0
  21. package/dist/types/public/assets/js/pds.d.ts +23 -0
  22. package/dist/types/public/assets/js/pds.d.ts.map +1 -0
  23. package/dist/types/public/assets/pds/components/pds-calendar.d.ts +23 -0
  24. package/dist/types/public/assets/pds/components/pds-calendar.d.ts.map +1 -0
  25. package/dist/types/public/assets/pds/components/pds-drawer.d.ts +2 -0
  26. package/dist/types/public/assets/pds/components/pds-drawer.d.ts.map +1 -0
  27. package/dist/types/public/assets/pds/components/pds-icon.d.ts +53 -0
  28. package/dist/types/public/assets/pds/components/pds-icon.d.ts.map +1 -0
  29. package/dist/types/public/assets/pds/components/pds-jsonform.d.ts +104 -0
  30. package/dist/types/public/assets/pds/components/pds-jsonform.d.ts.map +1 -0
  31. package/dist/types/public/assets/pds/components/pds-richtext.d.ts +121 -0
  32. package/dist/types/public/assets/pds/components/pds-richtext.d.ts.map +1 -0
  33. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts +61 -0
  34. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts.map +1 -0
  35. package/dist/types/public/assets/pds/components/pds-splitpanel.d.ts +1 -0
  36. package/dist/types/public/assets/pds/components/pds-splitpanel.d.ts.map +1 -0
  37. package/dist/types/public/assets/pds/components/pds-tabstrip.d.ts +39 -0
  38. package/dist/types/public/assets/pds/components/pds-tabstrip.d.ts.map +1 -0
  39. package/dist/types/public/assets/pds/components/pds-toaster.d.ts +111 -0
  40. package/dist/types/public/assets/pds/components/pds-toaster.d.ts.map +1 -0
  41. package/dist/types/public/assets/pds/components/pds-upload.d.ts +83 -0
  42. package/dist/types/public/assets/pds/components/pds-upload.d.ts.map +1 -0
  43. package/dist/types/src/js/app.d.ts +2 -0
  44. package/dist/types/src/js/app.d.ts.map +1 -0
  45. package/dist/types/src/js/common/ask.d.ts +22 -0
  46. package/dist/types/src/js/common/ask.d.ts.map +1 -0
  47. package/dist/types/src/js/common/common.d.ts +3 -0
  48. package/dist/types/src/js/common/common.d.ts.map +1 -0
  49. package/dist/types/src/js/common/font-loader.d.ts +24 -0
  50. package/dist/types/src/js/common/font-loader.d.ts.map +1 -0
  51. package/dist/types/src/js/common/msg.d.ts +3 -0
  52. package/dist/types/src/js/common/msg.d.ts.map +1 -0
  53. package/dist/types/src/js/lit.d.ts +25 -0
  54. package/dist/types/src/js/lit.d.ts.map +1 -0
  55. package/dist/types/src/js/pds-configurator/figma-export.d.ts +13 -0
  56. package/dist/types/src/js/pds-configurator/figma-export.d.ts.map +1 -0
  57. package/dist/types/src/js/pds-configurator/pds-config-form.d.ts +2 -0
  58. package/dist/types/src/js/pds-configurator/pds-config-form.d.ts.map +1 -0
  59. package/dist/types/src/js/pds-configurator/pds-configurator.d.ts +2 -0
  60. package/dist/types/src/js/pds-configurator/pds-configurator.d.ts.map +1 -0
  61. package/dist/types/src/js/pds-configurator/pds-demo.d.ts +2 -0
  62. package/dist/types/src/js/pds-configurator/pds-demo.d.ts.map +1 -0
  63. package/dist/types/src/js/pds-core/pds-config.d.ts +758 -0
  64. package/dist/types/src/js/pds-core/pds-config.d.ts.map +1 -0
  65. package/dist/types/src/js/pds-core/pds-enhancer-metadata.d.ts +6 -0
  66. package/dist/types/src/js/pds-core/pds-enhancer-metadata.d.ts.map +1 -0
  67. package/dist/types/src/js/pds-core/pds-enhancers.d.ts +14 -0
  68. package/dist/types/src/js/pds-core/pds-enhancers.d.ts.map +1 -0
  69. package/dist/types/src/js/pds-core/pds-enums.d.ts +87 -0
  70. package/dist/types/src/js/pds-core/pds-enums.d.ts.map +1 -0
  71. package/dist/types/src/js/pds-core/pds-generator.d.ts +741 -0
  72. package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -0
  73. package/dist/types/src/js/pds-core/pds-ontology.d.ts +48 -0
  74. package/dist/types/src/js/pds-core/pds-ontology.d.ts.map +1 -0
  75. package/dist/types/src/js/pds-core/pds-paths.d.ts +37 -0
  76. package/dist/types/src/js/pds-core/pds-paths.d.ts.map +1 -0
  77. package/dist/types/src/js/pds-core/pds-query.d.ts +102 -0
  78. package/dist/types/src/js/pds-core/pds-query.d.ts.map +1 -0
  79. package/dist/types/src/js/pds-core/pds-registry.d.ts +40 -0
  80. package/dist/types/src/js/pds-core/pds-registry.d.ts.map +1 -0
  81. package/dist/types/src/js/pds.d.ts +109 -0
  82. package/dist/types/src/js/pds.d.ts.map +1 -0
  83. package/dist/types/src/pds-core/pds-api.d.ts +31 -0
  84. package/dist/types/src/pds-core/pds-api.d.ts.map +1 -0
  85. package/package.json +104 -0
  86. package/packages/pds-cli/README.md +15 -0
  87. package/packages/pds-cli/bin/generate-css-data.js +565 -0
  88. package/packages/pds-cli/bin/generate-manifest.js +352 -0
  89. package/packages/pds-cli/bin/pds-build-icons.js +152 -0
  90. package/packages/pds-cli/bin/pds-dx.js +114 -0
  91. package/packages/pds-cli/bin/pds-static.js +556 -0
  92. package/packages/pds-cli/bin/pds.js +127 -0
  93. package/packages/pds-cli/bin/postinstall.js +380 -0
  94. package/packages/pds-cli/bin/sync-assets.js +252 -0
  95. package/packages/pds-cli/lib/asset-roots.js +47 -0
  96. package/packages/pds-cli/lib/fs-writer.js +75 -0
  97. package/pds.css-data.json +5 -0
  98. package/pds.html-data.json +5 -0
  99. package/public/assets/js/app.js +5719 -0
  100. package/public/assets/js/lit.js +131 -0
  101. package/public/assets/js/pds.js +3423 -0
  102. package/public/assets/pds/components/pds-calendar.js +837 -0
  103. package/public/assets/pds/components/pds-drawer.js +857 -0
  104. package/public/assets/pds/components/pds-icon.js +338 -0
  105. package/public/assets/pds/components/pds-jsonform.js +1775 -0
  106. package/public/assets/pds/components/pds-richtext.js +1035 -0
  107. package/public/assets/pds/components/pds-scrollrow.js +331 -0
  108. package/public/assets/pds/components/pds-splitpanel.js +401 -0
  109. package/public/assets/pds/components/pds-tabstrip.js +251 -0
  110. package/public/assets/pds/components/pds-toaster.js +446 -0
  111. package/public/assets/pds/components/pds-upload.js +657 -0
  112. package/public/assets/pds/custom-elements.json +2003 -0
  113. package/public/assets/pds/icons/pds-icons.svg +498 -0
  114. package/public/assets/pds/pds-css-complete.json +1861 -0
  115. package/public/assets/pds/pds.css-data.json +2152 -0
  116. package/public/assets/pds/vscode-custom-data.json +824 -0
  117. package/readme.md +1870 -0
  118. package/src/js/pds-core/pds-config.js +1162 -0
  119. package/src/js/pds-core/pds-enhancer-metadata.js +75 -0
  120. package/src/js/pds-core/pds-enhancers.js +357 -0
  121. package/src/js/pds-core/pds-enums.js +86 -0
  122. package/src/js/pds-core/pds-generator.js +5317 -0
  123. package/src/js/pds-core/pds-ontology.js +256 -0
  124. package/src/js/pds-core/pds-paths.js +109 -0
  125. package/src/js/pds-core/pds-query.js +571 -0
  126. package/src/js/pds-core/pds-registry.js +129 -0
  127. package/src/js/pds-core/pds.d.ts +129 -0
  128. package/src/js/pds.d.ts +408 -0
  129. package/src/js/pds.js +1579 -0
@@ -0,0 +1,657 @@
1
+ /**
2
+ * Drag-and-drop file uploader that participates in native forms.
3
+ *
4
+ * @element pds-upload
5
+ * @formAssociated
6
+ *
7
+ * @attr {string} accept - Comma separated list of accepted MIME types and file extensions
8
+ * @attr {boolean} multiple - Allows selecting more than one file
9
+ * @attr {boolean} disabled - Disables interaction with the drop zone and button
10
+ * @attr {number} max-files - Optional cap on the number of files the user may select
11
+ */
12
+ class UploadArea extends HTMLElement {
13
+ static get observedAttributes() {
14
+ return ["accept", "multiple", "disabled", "max-files"];
15
+ }
16
+
17
+ // Private fields
18
+ #root;
19
+ #files;
20
+ #objectUrls;
21
+ #container;
22
+ #btnSelect;
23
+ #tiles;
24
+ #input;
25
+ #internals;
26
+
27
+ constructor() {
28
+ super();
29
+ this.#root = this.attachShadow({ mode: "open" });
30
+
31
+ this.#files = []; // Array<File>
32
+ this.#objectUrls = new Map(); // File -> objectUrl
33
+
34
+ // Attach element internals for form participation
35
+ this.#internals = this.attachInternals();
36
+
37
+ // Render structure first
38
+ this.#renderStructure();
39
+
40
+ // Adopt primitives stylesheet asynchronously
41
+ this.#adoptStyles();
42
+
43
+ this.#container = this.#root.querySelector(".container");
44
+ this.#btnSelect = this.#root.querySelector(".btn-select");
45
+ this.#tiles = this.#root.querySelector(".tiles");
46
+ this.#input = this.#root.querySelector('input[type="file"]');
47
+ }
48
+
49
+ #renderStructure() {
50
+ this.#root.innerHTML = `
51
+ <div class="container" role="button" tabindex="0" aria-disabled="false">
52
+ <div class="instructions">
53
+ <div class="headline">Drop files here or</div>
54
+ <div class="sub">Click to browse, or drag & drop files to upload.</div>
55
+ </div>
56
+ <button type="button" class="btn-select">Select</button>
57
+ </div>
58
+
59
+ <div class="tiles" aria-live="polite" aria-relevant="additions removals"></div>
60
+
61
+ <input type="file" multiple />
62
+ `;
63
+ }
64
+
65
+ async #adoptStyles() {
66
+ // Component-specific styles (button styles come from primitives)
67
+ const componentStyles = PDS.createStylesheet(/*css*/`
68
+ @layer upload {
69
+ :host {
70
+ display: block;
71
+ font-family: var(--font-family-body, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial);
72
+ }
73
+
74
+ .container {
75
+ position: relative;
76
+ border: 2px dashed var(--color-border, #d1d5db);
77
+ border-radius: var(--radius-md, 8px);
78
+ padding: var(--spacing-3, 12px);
79
+ background: var(--color-input-bg);
80
+ display: flex;
81
+ gap: var(--spacing-3, 12px);
82
+ align-items: center;
83
+ cursor: pointer;
84
+ transition: border-color .15s ease, background .15s ease, box-shadow .12s ease;
85
+ }
86
+
87
+ .container[aria-disabled="true"] {
88
+ opacity: 0.6;
89
+ cursor: not-allowed;
90
+ }
91
+
92
+ .container.dragover {
93
+ border-color: var(--color-primary-500);
94
+ box-shadow: 0 2px 8px var(--color-primary-500)20;
95
+ }
96
+
97
+ .instructions {
98
+ flex: 1;
99
+ min-width: 0;
100
+ }
101
+
102
+ .headline {
103
+ font-weight: var(--font-weight-semibold, 600);
104
+ color: var(--color-text-primary);
105
+ margin: 0 0 var(--spacing-1, 6px) 0;
106
+ font-size: var(--font-size-sm, 14px);
107
+ }
108
+
109
+ .sub {
110
+ font-size: var(--font-size-xs, 13px);
111
+ color: var(--color-text-secondary);
112
+ margin: 0;
113
+ }
114
+
115
+ .tiles {
116
+ display: flex;
117
+ gap: var(--spacing-2, 8px);
118
+ flex-wrap: wrap;
119
+ margin-top: var(--spacing-3, 12px);
120
+ }
121
+
122
+ .tile {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ gap: var(--spacing-2, 8px);
126
+ background: var(--color-surface-subtle);
127
+ border: 1px solid var(--color-border);
128
+ padding: var(--spacing-1, 6px);
129
+ border-radius: var(--radius-md, 6px);
130
+ min-width: 120px;
131
+ max-width: 220px;
132
+ }
133
+
134
+ .thumb {
135
+ width: 40px;
136
+ height: 40px;
137
+ flex: 0 0 40px;
138
+ border-radius: var(--radius-sm, 4px);
139
+ background: var(--color-surface-base, #fff);
140
+ border: 1px solid var(--color-border);
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ overflow: hidden;
145
+ }
146
+
147
+ .thumb img {
148
+ width: 100%;
149
+ height: 100%;
150
+ object-fit: cover;
151
+ }
152
+
153
+ .meta {
154
+ flex: 1;
155
+ min-width: 0;
156
+ font-size: var(--font-size-xs, 12px);
157
+ color: var(--color-text-primary);
158
+ }
159
+
160
+ .meta .name {
161
+ font-weight: var(--font-weight-semibold, 600);
162
+ white-space: nowrap;
163
+ overflow: hidden;
164
+ text-overflow: ellipsis;
165
+ }
166
+
167
+ .meta .size {
168
+ color: var(--color-text-secondary);
169
+ font-size: var(--font-size-xs, 11px);
170
+ }
171
+
172
+ .remove {
173
+ background: transparent;
174
+ border: none;
175
+ color: var(--color-danger-600);
176
+ cursor: pointer;
177
+ padding: var(--spacing-1, 4px);
178
+ font-size: var(--font-size-sm, 14px);
179
+ line-height: 1;
180
+ }
181
+
182
+ .remove:hover {
183
+ color: var(--color-danger-700);
184
+ }
185
+
186
+ input[type="file"] {
187
+ display: none;
188
+ }
189
+
190
+ /* Focus styles */
191
+ .container:focus {
192
+ outline: none;
193
+ box-shadow: 0 0 0 3px var(--color-primary-500)20;
194
+ }
195
+ }
196
+ `);
197
+
198
+ // Adopt primitives (for button) + component styles
199
+ await PDS.adoptLayers(this.#root, ["primitives", "components", "utilities"], [componentStyles]);
200
+ }
201
+
202
+ // Form-associated lifecycle callbacks
203
+ /**
204
+ * Invoked when the element becomes associated with a `<form>`.
205
+ */
206
+ formAssociatedCallback() {
207
+ // Hook reserved for future enhancements (e.g., default validity state)
208
+ }
209
+
210
+ /**
211
+ * Sync disabled state across internal controls when the host form toggles.
212
+ * @param {boolean} disabled
213
+ */
214
+ formDisabledCallback(disabled) {
215
+ this.#container.setAttribute("aria-disabled", disabled);
216
+ }
217
+
218
+ /**
219
+ * Clear selected files when the host form resets.
220
+ */
221
+ formResetCallback() {
222
+ this.clear();
223
+ }
224
+
225
+ /**
226
+ * Restore previously submitted files during BFCache or session restores.
227
+ * @param {File[]} state
228
+ */
229
+ formStateRestoreCallback(state) {
230
+ if (state) {
231
+ this.#files = state;
232
+ this.#updateTiles();
233
+ }
234
+ }
235
+
236
+ // Custom methods for form participation
237
+ /**
238
+ * Comma separated list of file names for form serialisation.
239
+ * @returns {string}
240
+ */
241
+ get value() {
242
+ return this.#files.map((file) => file.name).join(", ");
243
+ }
244
+
245
+ /**
246
+ * Value is derived from the selected files and cannot be set manually.
247
+ * @param {string} val
248
+ */
249
+ set value(val) {
250
+ // No-op by design: value is derived from files
251
+ }
252
+
253
+ /**
254
+ * Run constraint validation leveraging ElementInternals.
255
+ * @returns {boolean}
256
+ */
257
+ checkValidity() {
258
+ return this.#internals.checkValidity();
259
+ }
260
+
261
+ /**
262
+ * Report validity issues using the browser UI.
263
+ * @returns {boolean}
264
+ */
265
+ reportValidity() {
266
+ return this.#internals.reportValidity();
267
+ }
268
+
269
+ /**
270
+ * Clear all selected files and update the UI accordingly.
271
+ */
272
+ clear() {
273
+ this.#files = [];
274
+ this.#updateTiles();
275
+ }
276
+
277
+ #updateTiles() {
278
+ // Update the UI to reflect the current files
279
+ this.#tiles.innerHTML = "";
280
+ this.#files.forEach((file) => {
281
+ const tile = document.createElement("div");
282
+ tile.className = "tile";
283
+ tile.textContent = file.name;
284
+ this.#tiles.appendChild(tile);
285
+ });
286
+ }
287
+
288
+ /**
289
+ * Lifecycle hook used to upgrade properties and wire event listeners.
290
+ */
291
+ connectedCallback() {
292
+ this.#upgradeProperty("accept");
293
+ this.#upgradeProperty("multiple");
294
+ this.#upgradeProperty("disabled");
295
+ this.#upgradeProperty("max-files");
296
+
297
+ this.#applyAttributesToInput();
298
+
299
+ this.#container.addEventListener("click", this.#onClick);
300
+ this.#container.addEventListener("keydown", this.#onKey);
301
+ this.#btnSelect.addEventListener("click", this.#onSelectClick);
302
+ this.#input.addEventListener("change", this.#onFileInput);
303
+
304
+ // Drag & drop
305
+ this.#container.addEventListener("dragover", this.#onDragOver);
306
+ this.#container.addEventListener("dragenter", this.#onDragEnter);
307
+ this.#container.addEventListener("dragleave", this.#onDragLeave);
308
+ this.#container.addEventListener("drop", this.#onDrop);
309
+ }
310
+
311
+ /**
312
+ * Clean up listeners and revoke object URLs when disconnected.
313
+ */
314
+ disconnectedCallback() {
315
+ this.#container.removeEventListener("click", this.#onClick);
316
+ this.#container.removeEventListener("keydown", this.#onKey);
317
+ this.#btnSelect.removeEventListener("click", this.#onSelectClick);
318
+ this.#input.removeEventListener("change", this.#onFileInput);
319
+
320
+ this.#container.removeEventListener("dragover", this.#onDragOver);
321
+ this.#container.removeEventListener("dragenter", this.#onDragEnter);
322
+ this.#container.removeEventListener("dragleave", this.#onDragLeave);
323
+ this.#container.removeEventListener("drop", this.#onDrop);
324
+
325
+ this.#revokeAllObjectUrls();
326
+ }
327
+
328
+ /**
329
+ * Mirror attribute mutations onto the hidden file input.
330
+ * @param {string} name
331
+ * @param {string|null} oldVal
332
+ * @param {string|null} newVal
333
+ */
334
+ attributeChangedCallback(name, oldVal, newVal) {
335
+ if (oldVal === newVal) return;
336
+ switch (name) {
337
+ case "accept":
338
+ case "multiple":
339
+ case "disabled":
340
+ case "max-files":
341
+ this.#applyAttributesToInput();
342
+ break;
343
+ }
344
+ }
345
+
346
+ // Public API
347
+ /**
348
+ * Retrieve a shallow copy of the current file selection.
349
+ * @returns {File[]}
350
+ */
351
+ getFiles() {
352
+ return Array.from(this.#files);
353
+ }
354
+
355
+ /**
356
+ * Remove all files and emit a change notification.
357
+ */
358
+ clear() {
359
+ this.#files = [];
360
+ this.#updateInputFiles();
361
+ this.#renderTiles();
362
+ this.#emitFilesChanged();
363
+ }
364
+
365
+ // Internals
366
+ #upgradeProperty(prop) {
367
+ if (this.hasOwnProperty(prop)) {
368
+ let value = this[prop];
369
+ delete this[prop];
370
+ this[prop] = value;
371
+ }
372
+ }
373
+
374
+ #applyAttributesToInput() {
375
+ // accept
376
+ if (this.hasAttribute("accept")) {
377
+ this.#input.setAttribute("accept", this.getAttribute("accept"));
378
+ } else {
379
+ this.#input.removeAttribute("accept");
380
+ }
381
+
382
+ // multiple
383
+ if (this.hasAttribute("multiple")) {
384
+ this.#input.setAttribute("multiple", "");
385
+ this.#container.setAttribute("aria-multiselectable", "true");
386
+ } else {
387
+ this.#input.removeAttribute("multiple");
388
+ this.#container.removeAttribute("aria-multiselectable");
389
+ }
390
+
391
+ // disabled
392
+ const disabled = this.hasAttribute("disabled");
393
+ this.#input.disabled = disabled;
394
+ this.#container.setAttribute("aria-disabled", disabled ? "true" : "false");
395
+ this.#btnSelect.disabled = disabled;
396
+ if (disabled) {
397
+ this.#container.tabIndex = -1;
398
+ } else {
399
+ this.#container.tabIndex = 0;
400
+ }
401
+ }
402
+
403
+ #onClick = (e) => {
404
+ if (this.hasAttribute("disabled")) return;
405
+ // Ensure clicks on the whole container open file picker
406
+ if (e.target === this.#btnSelect) return; // handled separately
407
+ this.#input.click();
408
+ };
409
+
410
+ #onSelectClick = (e) => {
411
+ e.stopPropagation();
412
+ if (this.hasAttribute("disabled")) return;
413
+ this.#input.click();
414
+ };
415
+
416
+ #onKey = (e) => {
417
+ if (this.hasAttribute("disabled")) return;
418
+ if (e.key === "Enter" || e.key === " ") {
419
+ e.preventDefault();
420
+ this.#input.click();
421
+ }
422
+ };
423
+
424
+ #onFileInput = (e) => {
425
+ if (!this.#input.files) return;
426
+ this.#addFiles(this.#input.files);
427
+ // reset input to allow selecting same file again
428
+ this.#input.value = "";
429
+ };
430
+
431
+ #onDragOver = (e) => {
432
+ e.preventDefault();
433
+ e.dataTransfer.dropEffect = "copy";
434
+ };
435
+
436
+ #onDragEnter = (e) => {
437
+ e.preventDefault();
438
+ if (this.hasAttribute("disabled")) return;
439
+ this.#container.classList.add("dragover");
440
+ };
441
+
442
+ #onDragLeave = (e) => {
443
+ // Only remove when leaving the container entirely
444
+ const rect = this.#container.getBoundingClientRect();
445
+ if (
446
+ e.clientX < rect.left ||
447
+ e.clientX > rect.right ||
448
+ e.clientY < rect.top ||
449
+ e.clientY > rect.bottom
450
+ ) {
451
+ this.#container.classList.remove("dragover");
452
+ }
453
+ };
454
+
455
+ #onDrop = (e) => {
456
+ e.preventDefault();
457
+ this.#container.classList.remove("dragover");
458
+ if (this.hasAttribute("disabled")) return;
459
+ const dtFiles =
460
+ e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files : null;
461
+ if (dtFiles && dtFiles.length) {
462
+ this.#addFiles(dtFiles);
463
+ }
464
+ };
465
+
466
+ #addFiles(fileListLike) {
467
+ const incoming = Array.from(fileListLike);
468
+
469
+ // If accept is present, filter by accept patterns
470
+ const accept = this.getAttribute("accept");
471
+ const acceptList = accept
472
+ ? accept
473
+ .split(",")
474
+ .map((s) => s.trim())
475
+ .filter(Boolean)
476
+ : null;
477
+ const acceptedFiles = acceptList
478
+ ? incoming.filter((f) => this.#fileMatchesAccept(f, acceptList))
479
+ : incoming;
480
+
481
+ // max-files handling
482
+ const maxAttr = this.getAttribute("max-files");
483
+ let max = maxAttr ? parseInt(maxAttr, 10) : null;
484
+ if (Number.isNaN(max)) max = null;
485
+
486
+ if (this.hasAttribute("multiple")) {
487
+ // Append while respecting max
488
+ const available =
489
+ max != null ? Math.max(0, max - this.#files.length) : Infinity;
490
+ const toTake = acceptedFiles.slice(0, available);
491
+ this.#files = this.#files.concat(toTake);
492
+ } else {
493
+ // Only first accepted file
494
+ if (acceptedFiles.length > 0) {
495
+ this.#files = [acceptedFiles[0]];
496
+ }
497
+ }
498
+
499
+ this.#updateInputFiles();
500
+ this.#renderTiles();
501
+ this.#emitFilesChanged();
502
+ }
503
+
504
+ #fileMatchesAccept(file, acceptList) {
505
+ // Accept patterns can be MIME types, wildcards like image/*, or extensions like .png
506
+ const name = (file.name || "").toLowerCase();
507
+ const type = (file.type || "").toLowerCase();
508
+
509
+ for (const pattern of acceptList) {
510
+ if (pattern.startsWith(".")) {
511
+ if (name.endsWith(pattern.toLowerCase())) return true;
512
+ } else if (pattern.endsWith("/*")) {
513
+ const base = pattern.slice(0, -2).toLowerCase();
514
+ if (type.startsWith(base + "/")) return true;
515
+ } else {
516
+ if (type === pattern.toLowerCase()) return true;
517
+ }
518
+ }
519
+ return false;
520
+ }
521
+
522
+ #updateInputFiles() {
523
+ // Create a DataTransfer and assign files to hidden input for form compatibility
524
+ try {
525
+ const dt = new DataTransfer();
526
+ for (const f of this.#files) dt.items.add(f);
527
+ this.#input.files = dt.files;
528
+ } catch (err) {
529
+ // Some older browsers may not support DataTransfer() constructor.
530
+ // In that case, we can't sync input.files but component still works.
531
+ }
532
+ }
533
+
534
+ #renderTiles() {
535
+ // Clear tiles
536
+ this.#tiles.innerHTML = "";
537
+ this.#revokeUnusedObjectUrls();
538
+
539
+ this.#files.forEach((file, idx) => {
540
+ const tile = document.createElement("div");
541
+ tile.className = "tile";
542
+ tile.dataset.index = String(idx);
543
+
544
+ const thumb = document.createElement("div");
545
+ thumb.className = "thumb";
546
+ // If image, create preview
547
+ if (file.type && file.type.startsWith("image/")) {
548
+ const url = URL.createObjectURL(file);
549
+ this.#objectUrls.set(file, url);
550
+ const img = document.createElement("img");
551
+ img.src = url;
552
+ img.alt = file.name;
553
+ thumb.appendChild(img);
554
+ } else {
555
+ // non-image icon (simple file icon)
556
+ thumb.innerHTML = this.#fileIconSvg();
557
+ }
558
+
559
+ const meta = document.createElement("div");
560
+ meta.className = "meta";
561
+ const name = document.createElement("div");
562
+ name.className = "name";
563
+ name.textContent = file.name;
564
+ const size = document.createElement("div");
565
+ size.className = "size";
566
+ size.textContent = this.#formatBytes(file.size);
567
+ meta.appendChild(name);
568
+ meta.appendChild(size);
569
+
570
+ const remove = document.createElement("button");
571
+ remove.className = "remove";
572
+ remove.type = "button";
573
+ remove.title = `Remove ${file.name}`;
574
+ remove.setAttribute("aria-label", `Remove ${file.name}`);
575
+ remove.textContent = "✕";
576
+ remove.addEventListener("click", (e) => {
577
+ e.stopPropagation();
578
+ this.#removeFileAtIndex(idx);
579
+ });
580
+
581
+ tile.appendChild(thumb);
582
+ tile.appendChild(meta);
583
+ tile.appendChild(remove);
584
+
585
+ this.#tiles.appendChild(tile);
586
+ });
587
+ }
588
+
589
+ #removeFileAtIndex(i) {
590
+ const file = this.#files[i];
591
+ if (!file) return;
592
+ this.#files.splice(i, 1);
593
+ this.#updateInputFiles();
594
+ this.#renderTiles();
595
+ this.#emitFilesChanged();
596
+ // revoke object url for removed file
597
+ if (this.#objectUrls.has(file)) {
598
+ URL.revokeObjectURL(this.#objectUrls.get(file));
599
+ this.#objectUrls.delete(file);
600
+ }
601
+ }
602
+
603
+ #emitFilesChanged() {
604
+ const filesArray = this.getFiles();
605
+ this.dispatchEvent(
606
+ new CustomEvent("files-changed", {
607
+ detail: { files: filesArray },
608
+ bubbles: true,
609
+ composed: true,
610
+ })
611
+ );
612
+ }
613
+
614
+ #formatBytes(bytes) {
615
+ if (bytes === 0) return "0 B";
616
+ const k = 1024;
617
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
618
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
619
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
620
+ }
621
+
622
+ #fileIconSvg() {
623
+ // Minimal inline SVG for generic file icon
624
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
625
+ <rect x="3" y="3" width="18" height="18" rx="2" fill="#fff" stroke="#e5e7eb"/>
626
+ <path d="M7 7h6v2H7zM7 11h10v2H7z" fill="#9ca3af"/>
627
+ </svg>`;
628
+ }
629
+
630
+ #revokeAllObjectUrls() {
631
+ for (const url of this.#objectUrls.values()) {
632
+ try {
633
+ URL.revokeObjectURL(url);
634
+ } catch (_) {}
635
+ }
636
+ this.#objectUrls.clear();
637
+ }
638
+
639
+ #revokeUnusedObjectUrls() {
640
+ // Remove any object URLs for files no longer present
641
+ const currentFiles = new Set(this.#files);
642
+ for (const [file, url] of Array.from(this.#objectUrls.entries())) {
643
+ if (!currentFiles.has(file)) {
644
+ try {
645
+ URL.revokeObjectURL(url);
646
+ } catch (_) {}
647
+ this.#objectUrls.delete(file);
648
+ }
649
+ }
650
+ }
651
+ }
652
+
653
+ if (!customElements.get("pds-upload")) {
654
+ customElements.define("pds-upload", UploadArea);
655
+ }
656
+
657
+ export default UploadArea;