@purestack/ts-components 1.1.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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/ts-components.d.mts +1662 -0
  4. package/dist/ts-components.d.mts.map +1 -0
  5. package/dist/ts-components.mjs +9200 -0
  6. package/dist/ts-components.mjs.map +1 -0
  7. package/package.json +130 -0
  8. package/src/defineComponents.ts +87 -0
  9. package/src/index.ts +270 -0
  10. package/src/registerStyles.ts +63 -0
  11. package/src/standard/alertBox/alertBox.ts +64 -0
  12. package/src/standard/autoId.ts +25 -0
  13. package/src/standard/badge/badge.ts +41 -0
  14. package/src/standard/badge/badgeStyle.ts +21 -0
  15. package/src/standard/barChart/barChart.ts +338 -0
  16. package/src/standard/barChart/barChartStyle.ts +81 -0
  17. package/src/standard/btn/btn.ts +191 -0
  18. package/src/standard/btn/btnStyle.ts +59 -0
  19. package/src/standard/btnGroup/btnGroup.ts +208 -0
  20. package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
  21. package/src/standard/chart/cartesianChart.ts +99 -0
  22. package/src/standard/chart/chartDefaults.ts +25 -0
  23. package/src/standard/chart/chartUtils.ts +84 -0
  24. package/src/standard/classicLogo/classicLogo.ts +429 -0
  25. package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
  26. package/src/standard/componentVariant.ts +153 -0
  27. package/src/standard/composer/composer.ts +1035 -0
  28. package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
  29. package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
  30. package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
  31. package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
  32. package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
  33. package/src/standard/composer/composerStyle.ts +156 -0
  34. package/src/standard/composer/emailHtmlPolicy.ts +474 -0
  35. package/src/standard/consent/consent.ts +173 -0
  36. package/src/standard/consent/consentStyle.ts +44 -0
  37. package/src/standard/contactForm/contactForm.ts +135 -0
  38. package/src/standard/contactForm/contactFormStyle.ts +116 -0
  39. package/src/standard/doughnutChart/doughnutChart.ts +527 -0
  40. package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
  41. package/src/standard/dropFiles/dropFiles.ts +480 -0
  42. package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
  43. package/src/standard/expandablePanel/expandablePanel.ts +106 -0
  44. package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
  45. package/src/standard/flex/flex.ts +269 -0
  46. package/src/standard/flex/flexStyle.ts +556 -0
  47. package/src/standard/footer/footer.ts +93 -0
  48. package/src/standard/footer/footerStyle.ts +39 -0
  49. package/src/standard/form/autoCompleteInput.ts +567 -0
  50. package/src/standard/form/form.ts +205 -0
  51. package/src/standard/form/formCheck.ts +96 -0
  52. package/src/standard/form/formInputField.ts +184 -0
  53. package/src/standard/form/formSelectField.ts +152 -0
  54. package/src/standard/form/formStyle.ts +585 -0
  55. package/src/standard/form/multiAutoCompleteInput.ts +880 -0
  56. package/src/standard/grid/grid.ts +177 -0
  57. package/src/standard/grid/gridStyle.ts +56 -0
  58. package/src/standard/icon/icon.ts +96 -0
  59. package/src/standard/icon/iconStyle.ts +50 -0
  60. package/src/standard/landing/codeShowcase.ts +54 -0
  61. package/src/standard/landing/comparisonColumn.ts +30 -0
  62. package/src/standard/landing/comparisonFeature.ts +22 -0
  63. package/src/standard/landing/comparisonTable.ts +38 -0
  64. package/src/standard/landing/ctaSection.ts +59 -0
  65. package/src/standard/landing/featureCard.ts +41 -0
  66. package/src/standard/landing/landing.ts +52 -0
  67. package/src/standard/landing/landingBand.ts +217 -0
  68. package/src/standard/landing/landingSection.ts +43 -0
  69. package/src/standard/landing/landingTypes.ts +141 -0
  70. package/src/standard/landing/metricItem.ts +25 -0
  71. package/src/standard/landing/metricStrip.ts +20 -0
  72. package/src/standard/lineChart/lineChart.ts +462 -0
  73. package/src/standard/lineChart/lineChartStyle.ts +91 -0
  74. package/src/standard/logo/logo.ts +192 -0
  75. package/src/standard/logo/logoStyle.ts +164 -0
  76. package/src/standard/modal/modal.ts +171 -0
  77. package/src/standard/modal/modalStore.ts +154 -0
  78. package/src/standard/modal/modalStyle.ts +190 -0
  79. package/src/standard/navMenu/navMenu.ts +278 -0
  80. package/src/standard/navMenu/navMenuStyle.ts +323 -0
  81. package/src/standard/pageLinks/pageLinks.ts +90 -0
  82. package/src/standard/pageScript/pageScript.ts +155 -0
  83. package/src/standard/pageToc/pageToc.ts +154 -0
  84. package/src/standard/pageToc/pageTocStyle.ts +372 -0
  85. package/src/standard/panel/panel.ts +55 -0
  86. package/src/standard/panel/panelStyle.ts +15 -0
  87. package/src/standard/pricing/pricing.ts +146 -0
  88. package/src/standard/searchBox/searchBox.ts +54 -0
  89. package/src/standard/searchBox/searchBoxStyle.ts +180 -0
  90. package/src/standard/sectionHeader/sectionHeader.ts +99 -0
  91. package/src/standard/signIn/signIn.ts +167 -0
  92. package/src/standard/signIn/signInStyle.ts +70 -0
  93. package/src/standard/tabs/tabs.ts +193 -0
  94. package/src/standard/tabs/tabsStyle.ts +231 -0
  95. package/src/standard/themeToggle/themeToggle.ts +33 -0
  96. package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
  97. package/src/standard/toast/toastHost.ts +42 -0
  98. package/src/standard/toast/toastStore.ts +50 -0
  99. package/src/standard/topBar/topBar.ts +101 -0
  100. package/src/standard/topBar/topBarStyle.ts +169 -0
  101. package/src/standard/virtualList/variableVirtualTable.ts +371 -0
  102. package/src/standard/virtualList/virtualList.ts +423 -0
  103. package/src/standard/virtualList/virtualListStyle.ts +34 -0
  104. package/src/standard/virtualList/virtualTable.ts +179 -0
  105. package/src/standard/virtualList/virtualTableStyle.ts +24 -0
  106. package/tsconfig.json +23 -0
@@ -0,0 +1,480 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import {
3
+ type ComponentHead,
4
+ type ComputedRef,
5
+ computed,
6
+ defineComponent,
7
+ html,
8
+ type RefOrValue,
9
+ type SRef,
10
+ sref,
11
+ unref,
12
+ } from 'regor'
13
+ import {
14
+ type ComponentVariant,
15
+ resolveComponentClasses,
16
+ } from '../componentVariant'
17
+
18
+ export interface DropFileItem {
19
+ id: string
20
+ file: File
21
+ name: string
22
+ type: string
23
+ size: number
24
+ sizeLabel: string
25
+ lastModified: number
26
+ icon: string
27
+ }
28
+
29
+ export type DropFilesIconMap = Record<string, string>
30
+
31
+ export interface DropFilesAddOptions {
32
+ accept?: RefOrValue<string> | string[] // TODO: components can never get values, string[] is wrong type, must be included in RefOrValue. check for other abuse and fix all.
33
+ iconMap?: DropFilesIconMap | SRef<DropFilesIconMap>
34
+ fileIcon?: RefOrValue<string>
35
+ multiple?: RefOrValue<boolean>
36
+ idSequence?: number
37
+ }
38
+
39
+ export interface DropFilesAddResult {
40
+ files: DropFileItem[]
41
+ idSequence: number
42
+ acceptedCount: number
43
+ }
44
+
45
+ export interface DropFiles {
46
+ files?: SRef<DropFileItem[]>
47
+ accept?: RefOrValue<string> | string[]
48
+ iconMap?: DropFilesIconMap | SRef<DropFilesIconMap>
49
+ label?: RefOrValue<string>
50
+ hint?: RefOrValue<string>
51
+ emptyTitle?: RefOrValue<string>
52
+ emptyText?: RefOrValue<string>
53
+ disabled?: RefOrValue<boolean>
54
+ multiple?: RefOrValue<boolean>
55
+ tone?: RefOrValue<SemanticTone>
56
+ variant?: RefOrValue<ComponentVariant>
57
+ uploadIcon?: RefOrValue<string>
58
+ fileIcon?: RefOrValue<string>
59
+ removeIcon?: RefOrValue<string>
60
+ onChange?: (files: DropFileItem[]) => void
61
+ inputElement?: ReturnType<typeof sref<HTMLInputElement | null>>
62
+ dragging?: SRef<boolean>
63
+ classes?: ComputedRef<string>
64
+ acceptValue?: ComputedRef<string>
65
+ resolvedMultiple?: ComputedRef<boolean>
66
+ resolvedUploadIcon?: ComputedRef<string>
67
+ resolvedRemoveIcon?: ComputedRef<string>
68
+ hasFiles?: ComputedRef<boolean>
69
+ openFileDialog?: () => void
70
+ handleZoneKeydown?: (event: KeyboardEvent) => void
71
+ handleInput?: (event: Event) => void
72
+ handlePaste?: (event: ClipboardEvent) => void
73
+ handleDragEnter?: (event: DragEvent) => void
74
+ handleDragOver?: (event: DragEvent) => void
75
+ handleDragLeave?: (event: DragEvent) => void
76
+ handleDrop?: (event: DragEvent) => void
77
+ removeFile?: (id: string) => void
78
+ clear?: () => void
79
+ }
80
+
81
+ const DEFAULT_UPLOAD_ICON = 'lucide:cloud-upload'
82
+ const DEFAULT_FILE_ICON = 'lucide:file'
83
+ const DEFAULT_REMOVE_ICON = 'lucide:trash-2'
84
+
85
+ const DEFAULT_ICON_MAP: DropFilesIconMap = {
86
+ 'image/*': 'lucide:file-image',
87
+ 'text/*': 'lucide:file-text',
88
+ 'application/pdf': 'lucide:file-text',
89
+ 'application/zip': 'lucide:file-archive',
90
+ 'application/x-zip-compressed': 'lucide:file-archive',
91
+ '.zip': 'lucide:file-archive',
92
+ }
93
+
94
+ const dropFilesTemplate = html`<div class="drop-files">
95
+ <span class="drop-files__label" r-if="label">{{ label }}</span>
96
+ <input
97
+ class="drop-files__input"
98
+ :ref="inputElement"
99
+ type="file"
100
+ :accept="acceptValue"
101
+ :multiple="resolvedMultiple"
102
+ :disabled="disabled"
103
+ tabindex="-1"
104
+ @change="handleInput"/>
105
+ <div
106
+ class="drop-files__zone"
107
+ :class="classes"
108
+ role="button"
109
+ tabindex="0"
110
+ :aria-disabled="disabled"
111
+ @click="openFileDialog"
112
+ @keydown="handleZoneKeydown"
113
+ @paste="handlePaste"
114
+ @dragenter="handleDragEnter"
115
+ @dragover="handleDragOver"
116
+ @dragleave="handleDragLeave"
117
+ @drop="handleDrop"
118
+ >
119
+ <Icon class="drop-files__zone-icon" :name="resolvedUploadIcon"/>
120
+ <span class="drop-files__zone-title">{{ emptyTitle || 'Add files' }}</span>
121
+ <span class="drop-files__zone-text">
122
+ {{ emptyText || 'Click, tap, paste, or drag files here.' }}
123
+ </span>
124
+ <span class="drop-files__hint" r-if="hint">{{ hint }}</span>
125
+ </div>
126
+ <ul class="drop-files__list" r-if="hasFiles" aria-label="Selected files">
127
+ <li class="drop-files__item" r-for="item in files">
128
+ <span class="drop-files__item-icon">
129
+ <Icon :name="item.icon"/>
130
+ </span>
131
+ <span class="drop-files__item-body">
132
+ <span class="drop-files__item-name">{{ item.name }}</span>
133
+ <span class="drop-files__item-meta">{{ item.sizeLabel }}</span>
134
+ </span>
135
+ <button
136
+ class="drop-files__remove"
137
+ type="button"
138
+ title="Remove file"
139
+ aria-label="Remove file"
140
+ :disabled="disabled"
141
+ @click="removeFile(item.id)"
142
+ >
143
+ <Icon :name="resolvedRemoveIcon"/>
144
+ </button>
145
+ </li>
146
+ </ul>
147
+ </div>`
148
+
149
+ function defineDropFilesComponent() {
150
+ return defineComponent<DropFiles>(dropFilesTemplate, {
151
+ props: [
152
+ 'files',
153
+ 'accept',
154
+ 'iconMap',
155
+ 'label',
156
+ 'hint',
157
+ 'emptyTitle',
158
+ 'emptyText',
159
+ 'disabled',
160
+ 'multiple',
161
+ 'tone',
162
+ 'variant',
163
+ 'uploadIcon',
164
+ 'fileIcon',
165
+ 'removeIcon',
166
+ 'onChange',
167
+ ],
168
+ context: (head) => resolveDropFiles(head),
169
+ })
170
+ }
171
+
172
+ export function defineDropFilesComponents() {
173
+ return {
174
+ dropFiles: defineDropFilesComponent(),
175
+ }
176
+ }
177
+
178
+ class DropFilesContext implements DropFiles {
179
+ readonly files: SRef<DropFileItem[]>
180
+ readonly accept?: RefOrValue<string> | string[]
181
+ readonly iconMap?: DropFilesIconMap | SRef<DropFilesIconMap>
182
+ readonly label?: RefOrValue<string>
183
+ readonly hint?: RefOrValue<string>
184
+ readonly emptyTitle?: RefOrValue<string>
185
+ readonly emptyText?: RefOrValue<string>
186
+ readonly disabled?: RefOrValue<boolean>
187
+ readonly multiple?: RefOrValue<boolean>
188
+ readonly tone?: RefOrValue<SemanticTone>
189
+ readonly variant?: RefOrValue<ComponentVariant>
190
+ readonly uploadIcon?: RefOrValue<string>
191
+ readonly fileIcon?: RefOrValue<string>
192
+ readonly removeIcon?: RefOrValue<string>
193
+ readonly onChange?: (files: DropFileItem[]) => void
194
+ readonly inputElement = sref<HTMLInputElement | null>(null)
195
+ readonly dragging = sref(false)
196
+ readonly classes: ComputedRef<string>
197
+ readonly acceptValue: ComputedRef<string>
198
+ readonly resolvedMultiple: ComputedRef<boolean>
199
+ readonly resolvedUploadIcon: ComputedRef<string>
200
+ readonly resolvedRemoveIcon: ComputedRef<string>
201
+ readonly hasFiles: ComputedRef<boolean>
202
+ private dragDepth = 0
203
+ private idSequence = 0
204
+
205
+ constructor(props: DropFiles) {
206
+ Object.assign(this, props)
207
+ this.files = props.files ?? sref<DropFileItem[]>([])
208
+ this.classes = computed(() =>
209
+ resolveComponentClasses(props, {
210
+ defaultVariant: 'surfaceAlt',
211
+ classes: [
212
+ this.dragging() ? 'drop-files__zone--dragging' : '',
213
+ unref(this.disabled) ? 'drop-files__zone--disabled' : '',
214
+ ],
215
+ }),
216
+ )
217
+ this.acceptValue = computed(() =>
218
+ resolveAccept(this.accept, this.iconMap).join(','),
219
+ )
220
+ this.resolvedMultiple = computed(() => unref(props.multiple) !== false)
221
+ this.resolvedUploadIcon = computed(
222
+ () => unref(props.uploadIcon) || DEFAULT_UPLOAD_ICON,
223
+ )
224
+ this.resolvedRemoveIcon = computed(
225
+ () => unref(props.removeIcon) || DEFAULT_REMOVE_ICON,
226
+ )
227
+ this.hasFiles = computed(() => this.files().length > 0)
228
+ }
229
+
230
+ openFileDialog = () => {
231
+ if (unref(this.disabled)) return
232
+ this.inputElement()?.click()
233
+ }
234
+
235
+ handleZoneKeydown = (event: KeyboardEvent) => {
236
+ if (event.key !== 'Enter' && event.key !== ' ') return
237
+ event.preventDefault()
238
+ this.openFileDialog()
239
+ }
240
+
241
+ handleInput = (event: Event) => {
242
+ if (unref(this.disabled)) return
243
+
244
+ const input = event.currentTarget as HTMLInputElement | null
245
+ this.addFileList(input?.files)
246
+ if (input) input.value = ''
247
+ }
248
+
249
+ handlePaste = (event: ClipboardEvent) => {
250
+ if (unref(this.disabled)) return
251
+
252
+ const files = event.clipboardData?.files
253
+ if (!files || files.length === 0) return
254
+
255
+ event.preventDefault()
256
+ this.addFileList(files)
257
+ }
258
+
259
+ handleDragEnter = (event: DragEvent) => {
260
+ if (unref(this.disabled) || !hasFileTransfer(event.dataTransfer)) return
261
+ event.preventDefault()
262
+ ++this.dragDepth
263
+ this.dragging(true)
264
+ }
265
+
266
+ handleDragOver = (event: DragEvent) => {
267
+ if (unref(this.disabled) || !hasFileTransfer(event.dataTransfer)) return
268
+ event.preventDefault()
269
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'
270
+ }
271
+
272
+ handleDragLeave = (event: DragEvent) => {
273
+ if (unref(this.disabled) || !hasFileTransfer(event.dataTransfer)) return
274
+ event.preventDefault()
275
+ this.dragDepth = Math.max(0, this.dragDepth - 1)
276
+ if (this.dragDepth === 0) this.dragging(false)
277
+ }
278
+
279
+ handleDrop = (event: DragEvent) => {
280
+ if (unref(this.disabled) || !hasFileTransfer(event.dataTransfer)) return
281
+ event.preventDefault()
282
+ this.dragDepth = 0
283
+ this.dragging(false)
284
+ this.addFileList(event.dataTransfer?.files)
285
+ }
286
+
287
+ removeFile = (id: string) => {
288
+ if (unref(this.disabled)) return
289
+ this.writeFiles(this.files().filter((item) => item.id !== id))
290
+ }
291
+
292
+ clear = () => {
293
+ if (unref(this.disabled)) return
294
+ this.writeFiles([])
295
+ }
296
+
297
+ private addFileList(fileList: FileList | null | undefined) {
298
+ if (!fileList || fileList.length === 0) return
299
+
300
+ const result = addDropFiles(this.files(), Array.from(fileList), {
301
+ accept: this.accept,
302
+ iconMap: this.iconMap,
303
+ fileIcon: this.fileIcon,
304
+ multiple: this.multiple,
305
+ idSequence: this.idSequence,
306
+ })
307
+ this.idSequence = result.idSequence
308
+ if (result.acceptedCount === 0) return
309
+ this.writeFiles(result.files)
310
+ }
311
+
312
+ private writeFiles(files: DropFileItem[]) {
313
+ this.files(files)
314
+ this.onChange?.(files)
315
+ }
316
+ }
317
+
318
+ function resolveDropFiles(head: ComponentHead<DropFiles>) {
319
+ return new DropFilesContext(head.props) as DropFiles
320
+ }
321
+
322
+ export function addDropFiles(
323
+ currentFiles: DropFileItem[],
324
+ files: File[],
325
+ options: DropFilesAddOptions = {},
326
+ ): DropFilesAddResult {
327
+ const accept = resolveAccept(options.accept, options.iconMap)
328
+ const accepted = files
329
+ .filter((file) => acceptsFile(file, accept))
330
+ .map((file) => createDropFileItem(file, options))
331
+
332
+ let idSequence = options.idSequence ?? 0
333
+ const sequenced = accepted.map((item) => {
334
+ ++idSequence
335
+ return {
336
+ ...item,
337
+ id: createFileId(item.file, idSequence),
338
+ }
339
+ })
340
+ if (sequenced.length === 0)
341
+ return { files: currentFiles, idSequence, acceptedCount: 0 }
342
+
343
+ if (unref(options.multiple) === false) {
344
+ return {
345
+ files: [sequenced[0]],
346
+ idSequence,
347
+ acceptedCount: 1,
348
+ }
349
+ }
350
+
351
+ const next = [...currentFiles]
352
+ let addedCount = 0
353
+ for (const item of sequenced) {
354
+ if (!next.some((existing) => isSameFile(existing.file, item.file))) {
355
+ next.push(item)
356
+ ++addedCount
357
+ }
358
+ }
359
+
360
+ return { files: next, idSequence, acceptedCount: addedCount }
361
+ }
362
+
363
+ function createDropFileItem(
364
+ file: File,
365
+ options: DropFilesAddOptions,
366
+ ): DropFileItem {
367
+ return {
368
+ id: '',
369
+ file,
370
+ name: file.name,
371
+ type: file.type,
372
+ size: file.size,
373
+ sizeLabel: formatFileSize(file.size),
374
+ lastModified: file.lastModified,
375
+ icon: resolveFileIcon(
376
+ file,
377
+ readIconMap(options.iconMap),
378
+ unref(options.fileIcon) || DEFAULT_FILE_ICON,
379
+ ),
380
+ }
381
+ }
382
+
383
+ function createFileId(file: File, sequence: number) {
384
+ return [sequence, file.name, file.size, file.lastModified].join(':')
385
+ }
386
+
387
+ function readAccept(value: RefOrValue<string> | string[] | undefined) {
388
+ return Array.isArray(value) ? value : unref(value)
389
+ }
390
+
391
+ function readIconMap(
392
+ value: DropFilesIconMap | SRef<DropFilesIconMap> | undefined,
393
+ ) {
394
+ return typeof value === 'function' ? value() : value
395
+ }
396
+
397
+ function resolveAccept(
398
+ accept: RefOrValue<string> | string[] | undefined,
399
+ iconMap: DropFilesIconMap | SRef<DropFilesIconMap> | undefined,
400
+ ) {
401
+ const resolvedAccept = normalizeAccept(readAccept(accept))
402
+ if (resolvedAccept.length > 0) return resolvedAccept
403
+
404
+ const resolvedIconMap = readIconMap(iconMap)
405
+ return resolvedIconMap ? normalizeAccept(Object.keys(resolvedIconMap)) : []
406
+ }
407
+
408
+ function normalizeAccept(value: string | string[] | undefined) {
409
+ if (Array.isArray(value)) {
410
+ return value
411
+ .flatMap((item) => item.split(','))
412
+ .map(normalizeAcceptToken)
413
+ .filter(Boolean)
414
+ }
415
+
416
+ if (!value) return []
417
+ return value.split(',').map(normalizeAcceptToken).filter(Boolean)
418
+ }
419
+
420
+ function normalizeAcceptToken(value: string) {
421
+ return value.trim().toLowerCase()
422
+ }
423
+
424
+ function acceptsFile(file: File, accept: string[]) {
425
+ if (accept.length === 0) return true
426
+ return accept.some((token) => matchesAcceptToken(file, token))
427
+ }
428
+
429
+ function matchesAcceptToken(file: File, token: string) {
430
+ if (!token) return false
431
+
432
+ const type = file.type.toLowerCase()
433
+ const name = file.name.toLowerCase()
434
+ if (token.startsWith('.')) return name.endsWith(token)
435
+ if (token.endsWith('/*')) return type.startsWith(token.slice(0, -1))
436
+ return type === token
437
+ }
438
+
439
+ function resolveFileIcon(
440
+ file: File,
441
+ iconMap: DropFilesIconMap | undefined,
442
+ fallback: string,
443
+ ) {
444
+ const mergedMap = { ...DEFAULT_ICON_MAP, ...(iconMap ?? {}) }
445
+ for (const [accept, icon] of Object.entries(mergedMap)) {
446
+ if (matchesAcceptToken(file, normalizeAcceptToken(accept)) && icon) {
447
+ return icon
448
+ }
449
+ }
450
+
451
+ return fallback
452
+ }
453
+
454
+ function hasFileTransfer(dataTransfer: DataTransfer | null) {
455
+ if (!dataTransfer) return false
456
+ return Array.from(dataTransfer.types).includes('Files')
457
+ }
458
+
459
+ function isSameFile(left: File, right: File) {
460
+ return (
461
+ left.name === right.name &&
462
+ left.size === right.size &&
463
+ left.type === right.type &&
464
+ left.lastModified === right.lastModified
465
+ )
466
+ }
467
+
468
+ function formatFileSize(size: number) {
469
+ if (!Number.isFinite(size) || size <= 0) return '0 B'
470
+
471
+ const units = ['B', 'KB', 'MB', 'GB']
472
+ let value = size
473
+ let unit = 0
474
+ while (value >= 1024 && unit < units.length - 1) {
475
+ value /= 1024
476
+ ++unit
477
+ }
478
+
479
+ return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`
480
+ }
@@ -0,0 +1,171 @@
1
+ import type { ThemePalette } from '@purestack/ts-style'
2
+ import { styleBuilder, type ThemeMode, themes } from '@purestack/ts-style'
3
+
4
+ export function registerDropFilesStyles() {
5
+ themes.forEach((theme, palette) => {
6
+ registerDropFilesFieldStyles(theme, palette)
7
+ registerDropFilesZoneStyles(theme, palette)
8
+ registerDropFilesListStyles(theme, palette)
9
+ })
10
+ }
11
+
12
+ function registerDropFilesFieldStyles(theme: ThemeMode, palette: ThemePalette) {
13
+ styleBuilder.select('.drop-files', theme).display('grid').gap('0.65em')
14
+
15
+ styleBuilder
16
+ .select('.drop-files__label', theme)
17
+ .apply(palette.applyFont(palette.font.size.xxs, palette.font.weight.w700))
18
+ .color(palette.current.tone)
19
+ .lineHeight('1')
20
+
21
+ styleBuilder
22
+ .select('.drop-files__input', theme)
23
+ .position('absolute')
24
+ .width('1px')
25
+ .height('1px')
26
+ .overflow('hidden')
27
+ .clip('rect(0 0 0 0)')
28
+ .whiteSpace('nowrap')
29
+ }
30
+
31
+ function registerDropFilesZoneStyles(theme: ThemeMode, palette: ThemePalette) {
32
+ styleBuilder
33
+ .select('.drop-files__zone', theme)
34
+ .display('grid')
35
+ .justifyItems('center')
36
+ .gap('0.35em')
37
+ .boxSizing('border-box')
38
+ .width('100%')
39
+ .minHeight('9em')
40
+ .padding('1.25em')
41
+ .borderRadius(palette.radii.md)
42
+ .border(`1px dashed ${palette.current.border.default}`)
43
+ .background(palette.current.surfaceAlt.rest.background)
44
+ .color(palette.current.text.default)
45
+ .cursor('pointer')
46
+ .textAlign('center')
47
+ .transition(
48
+ 'border-color 160ms ease, background 160ms ease, box-shadow 160ms ease',
49
+ )
50
+
51
+ styleBuilder
52
+ .select('.drop-files__zone:hover:not(.drop-files__zone--disabled)', theme)
53
+ .borderColor(palette.current.tone)
54
+ .background(palette.current.surface.rest.background)
55
+
56
+ styleBuilder
57
+ .select('.drop-files__zone:focus-visible', theme)
58
+ .outline('none')
59
+ .borderColor(palette.current.border.default)
60
+ .boxShadow(`0 0 0 3px ${palette.current.border.focus}`)
61
+
62
+ styleBuilder
63
+ .select('.drop-files__zone--dragging', theme)
64
+ .borderColor(palette.current.tone)
65
+ .background(palette.current.button.hover.background)
66
+ .boxShadow(`0 0 0 3px ${palette.current.border.focus}`)
67
+
68
+ styleBuilder
69
+ .select('.drop-files__zone--disabled', theme)
70
+ .cursor('not-allowed')
71
+ .opacity('0.58')
72
+
73
+ styleBuilder
74
+ .select('.drop-files__zone-icon', theme)
75
+ .width('2.2em')
76
+ .height('2.2em')
77
+ .color(palette.current.tone)
78
+
79
+ styleBuilder
80
+ .select('.drop-files__zone-title', theme)
81
+ .apply(palette.applyFont(palette.font.size.sm, palette.font.weight.w700))
82
+
83
+ styleBuilder
84
+ .select('.drop-files__zone-text, .drop-files__hint', theme)
85
+ .apply(palette.applyFont(palette.font.size.xs))
86
+ .color(palette.current.text.subtle)
87
+ }
88
+
89
+ function registerDropFilesListStyles(theme: ThemeMode, palette: ThemePalette) {
90
+ styleBuilder
91
+ .select('.drop-files__list', theme)
92
+ .display('grid')
93
+ .gap('0.45em')
94
+ .padding('0')
95
+ .margin('0')
96
+ .listStyle('none')
97
+
98
+ styleBuilder
99
+ .select('.drop-files__item', theme)
100
+ .display('grid')
101
+ .gridTemplateColumns('auto minmax(0, 1fr) auto')
102
+ .alignItems('center')
103
+ .gap('0.65em')
104
+ .boxSizing('border-box')
105
+ .padding('0.6em')
106
+ .borderRadius(palette.radii.md)
107
+ .border(`1px solid ${palette.current.border.subtle}`)
108
+ .background(palette.current.surface.rest.background)
109
+
110
+ styleBuilder
111
+ .select('.drop-files__item-icon', theme)
112
+ .display('inline-grid')
113
+ .placeItems('center')
114
+ .width('2em')
115
+ .height('2em')
116
+ .borderRadius(palette.radii.sm)
117
+ .background(palette.current.surfaceAlt.rest.background)
118
+ .color(palette.current.tone)
119
+
120
+ styleBuilder
121
+ .select('.drop-files__item-icon .icon', theme)
122
+ .width('1.1em')
123
+ .height('1.1em')
124
+
125
+ styleBuilder
126
+ .select('.drop-files__item-body', theme)
127
+ .display('grid')
128
+ .minWidth('0')
129
+ .gap('0.15em')
130
+
131
+ styleBuilder
132
+ .select('.drop-files__item-name', theme)
133
+ .overflow('hidden')
134
+ .textOverflow('ellipsis')
135
+ .whiteSpace('nowrap')
136
+ .apply(palette.applyFont(palette.font.size.sm, palette.font.weight.w700))
137
+
138
+ styleBuilder
139
+ .select('.drop-files__item-meta', theme)
140
+ .apply(palette.applyFont(palette.font.size.xxs))
141
+ .color(palette.current.text.subtle)
142
+
143
+ styleBuilder
144
+ .select('.drop-files__remove', theme)
145
+ .display('inline-grid')
146
+ .placeItems('center')
147
+ .width('2.1em')
148
+ .height('2.1em')
149
+ .padding('0')
150
+ .border('none')
151
+ .borderRadius(palette.radii.sm)
152
+ .background('transparent')
153
+ .color(palette.current.text.subtle)
154
+ .cursor('pointer')
155
+ .transition('background 150ms ease, color 150ms ease')
156
+
157
+ styleBuilder
158
+ .select('.drop-files__remove:hover:not(:disabled)', theme)
159
+ .background(palette.current.button.hover.background)
160
+ .color(palette.current.button.hover.text)
161
+
162
+ styleBuilder
163
+ .select('.drop-files__remove:disabled', theme)
164
+ .cursor('not-allowed')
165
+ .opacity('0.55')
166
+
167
+ styleBuilder
168
+ .select('.drop-files__remove .icon', theme)
169
+ .width('1em')
170
+ .height('1em')
171
+ }