webcake-ui-kit 1.2.15 → 1.2.17

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.
@@ -0,0 +1,216 @@
1
+ // Pure, framework-agnostic helpers for WkUpload.
2
+ // No Vue, no component state, no DOM component refs — everything here is unit-testable
3
+ // in isolation. The SFC (Upload.vue) wires these into Options-API lifecycle.
4
+
5
+ // Sentinel returned from `beforeUpload` to drop a file entirely (not added to the list).
6
+ // Mirrors Ant Design's `Upload.LIST_IGNORE` but as a plain string so it survives being
7
+ // passed through props/Promises without identity issues across Vue 2 / Vue 3.
8
+ export const LIST_IGNORE = 'WK_UPLOAD_LIST_IGNORE'
9
+
10
+ export const STATUS_UPLOADING = 'uploading'
11
+ export const STATUS_DONE = 'done'
12
+ export const STATUS_ERROR = 'error'
13
+ export const STATUS_REMOVED = 'removed'
14
+
15
+ let uidSeed = 0
16
+
17
+ // Stable-ish unique id for a file entry. Not cryptographic — only needs to be unique
18
+ // within a single Upload instance's lifetime so we can track/replace items by uid.
19
+ export function genUid() {
20
+ uidSeed += 1
21
+ return `wk-upload-${Date.now().toString(36)}-${uidSeed}`
22
+ }
23
+
24
+ // Does `file` satisfy the `accept` attribute? `accept` is a comma list of extensions
25
+ // (`.png`), exact MIME (`image/png`), or wildcard MIME (`image/*`). Empty accept = allow all.
26
+ export function attrAccept(file, accept) {
27
+ if (!accept || !file) return true
28
+ const accepts = String(accept)
29
+ .split(',')
30
+ .map(t => t.trim())
31
+ .filter(Boolean)
32
+ if (!accepts.length) return true
33
+ const name = file.name || ''
34
+ const mime = file.type || ''
35
+ const baseMime = mime.replace(/\/.*$/, '')
36
+ return accepts.some(type => {
37
+ if (type.charAt(0) === '.') {
38
+ return name.toLowerCase().slice(-type.length) === type.toLowerCase()
39
+ }
40
+ if (/\/\*$/.test(type)) {
41
+ return baseMime === type.replace(/\/.*$/, '')
42
+ }
43
+ if (/^[^/]+\/[^/]+$/.test(type)) {
44
+ return mime === type
45
+ }
46
+ return false
47
+ })
48
+ }
49
+
50
+ export function isImageFileType(type) {
51
+ return typeof type === 'string' && type.indexOf('image/') === 0
52
+ }
53
+
54
+ const IMAGE_EXT = /\.(webp|svg|png|gif|jpe?g|jfif|bmp|ico|heic|heif|avif)$/i
55
+
56
+ // Heuristic: should this file be rendered as an image thumbnail?
57
+ export function isImageUrl(file) {
58
+ if (!file) return false
59
+ if (isImageFileType(file.type)) return true
60
+ const url = file.thumbUrl || file.url || ''
61
+ if (!url) return false
62
+ if (/^data:image\//.test(url)) return true
63
+ if (/^data:/.test(url)) return false
64
+ if (IMAGE_EXT.test(url.split('?')[0])) return true
65
+ // Unknown remote URL with no extension — don't assume it's an image.
66
+ return false
67
+ }
68
+
69
+ // Read a File into a data URL for thumbnail preview. Resolves '' when not previewable
70
+ // or when running outside a browser (jsdom without FileReader).
71
+ export function previewImage(file) {
72
+ return new Promise(resolve => {
73
+ const raw = file && file.originFileObj ? file.originFileObj : file
74
+ if (!raw || !isImageFileType(raw.type) || typeof FileReader === 'undefined') {
75
+ resolve('')
76
+ return
77
+ }
78
+ const reader = new FileReader()
79
+ reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '')
80
+ reader.onerror = () => resolve('')
81
+ reader.readAsDataURL(raw)
82
+ })
83
+ }
84
+
85
+ // Human-readable byte size, e.g. 25600 -> "25 KB", 1234567 -> "1.2 MB".
86
+ export function formatSize(bytes) {
87
+ if (bytes == null || isNaN(bytes)) return ''
88
+ if (bytes <= 0) return '0 B'
89
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
90
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
91
+ const val = bytes / Math.pow(1024, i)
92
+ const rounded = val >= 10 || i === 0 ? Math.round(val) : Math.round(val * 10) / 10
93
+ return `${rounded} ${units[i]}`
94
+ }
95
+
96
+ // Wrap a raw File in the normalized upload-item shape used throughout the component.
97
+ export function file2Obj(file, uid) {
98
+ return {
99
+ uid: uid || file.uid || genUid(),
100
+ name: file.name,
101
+ size: file.size,
102
+ type: file.type,
103
+ lastModified: file.lastModified,
104
+ percent: 0,
105
+ status: STATUS_UPLOADING,
106
+ originFileObj: file,
107
+ url: undefined,
108
+ thumbUrl: undefined,
109
+ response: undefined,
110
+ error: undefined
111
+ }
112
+ }
113
+
114
+ // Immutable "replace item with matching uid, else append". Never mutates `fileList`.
115
+ export function updateFileList(file, fileList) {
116
+ const next = (fileList || []).slice()
117
+ const idx = next.findIndex(item => item.uid === file.uid)
118
+ if (idx === -1) next.push(file)
119
+ else next[idx] = file
120
+ return next
121
+ }
122
+
123
+ export function getFileItem(file, fileList) {
124
+ const uid = file && file.uid
125
+ return (fileList || []).filter(item => item.uid === uid)[0]
126
+ }
127
+
128
+ // Immutable removal by uid. Returns null when nothing was removed (caller can no-op).
129
+ export function removeFileItem(file, fileList) {
130
+ const uid = file && file.uid
131
+ const next = (fileList || []).filter(item => item.uid !== uid)
132
+ return next.length === (fileList || []).length ? null : next
133
+ }
134
+
135
+ function getBody(xhr) {
136
+ const text = xhr.responseText || xhr.response
137
+ if (!text) return text
138
+ try {
139
+ return JSON.parse(text)
140
+ } catch (e) {
141
+ return text
142
+ }
143
+ }
144
+
145
+ function getError(option, xhr) {
146
+ const msg = `cannot ${option.method} ${option.action} ${xhr.status}`
147
+ const err = new Error(msg)
148
+ err.status = xhr.status
149
+ err.method = option.method
150
+ err.url = option.action
151
+ return err
152
+ }
153
+
154
+ // Default XHR-based uploader. Returns a handle with `.abort()` so the caller can cancel
155
+ // on remove/unmount. Fires onProgress/onSuccess/onError. Never throws synchronously.
156
+ export function defaultRequest(option) {
157
+ const {
158
+ action,
159
+ method = 'post',
160
+ filename,
161
+ file,
162
+ data,
163
+ headers = {},
164
+ withCredentials = false,
165
+ name = 'file',
166
+ onProgress,
167
+ onSuccess,
168
+ onError
169
+ } = option
170
+
171
+ const xhr = new XMLHttpRequest()
172
+
173
+ if (onProgress && xhr.upload) {
174
+ xhr.upload.onprogress = e => {
175
+ let percent = 0
176
+ if (e.total > 0) percent = (e.loaded / e.total) * 100
177
+ onProgress({ percent, loaded: e.loaded, total: e.total })
178
+ }
179
+ }
180
+
181
+ const formData = new FormData()
182
+ if (data) {
183
+ Object.keys(data).forEach(key => {
184
+ const val = data[key]
185
+ if (Array.isArray(val)) val.forEach(v => formData.append(`${key}[]`, v))
186
+ else formData.append(key, val)
187
+ })
188
+ }
189
+ const raw = file && file.originFileObj ? file.originFileObj : file
190
+ formData.append(name, raw, (raw && raw.name) || filename)
191
+
192
+ xhr.onerror = e => {
193
+ if (onError) onError(e)
194
+ }
195
+ xhr.onload = () => {
196
+ if (xhr.status < 200 || xhr.status >= 300) {
197
+ if (onError) onError(getError(option, xhr), getBody(xhr))
198
+ return
199
+ }
200
+ if (onSuccess) onSuccess(getBody(xhr), xhr)
201
+ }
202
+
203
+ xhr.open(method, action, true)
204
+ if (withCredentials && 'withCredentials' in xhr) xhr.withCredentials = true
205
+ Object.keys(headers).forEach(h => {
206
+ if (headers[h] !== null && headers[h] !== undefined) xhr.setRequestHeader(h, headers[h])
207
+ })
208
+
209
+ xhr.send(formData)
210
+
211
+ return {
212
+ abort() {
213
+ xhr.abort()
214
+ }
215
+ }
216
+ }
@@ -0,0 +1,149 @@
1
+ .ui-upload {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--spacing-sm);
5
+ width: 100%;
6
+ font-family: var(--font-family-body);
7
+ }
8
+
9
+ .ui-upload__input {
10
+ display: none;
11
+ }
12
+
13
+ /* ---- Dropzone trigger (default / picture) ---- */
14
+ .ui-upload__dropzone {
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ justify-content: center;
19
+ gap: var(--spacing-xs);
20
+ padding: var(--spacing-md);
21
+ border: 1px dashed var(--border-primary);
22
+ border-radius: var(--rounded-xl);
23
+ background: var(--accent-0);
24
+ cursor: pointer;
25
+ outline: none;
26
+ transition:
27
+ background 0.15s ease,
28
+ border-color 0.15s ease;
29
+ }
30
+
31
+ .ui-upload__dropzone:hover,
32
+ .ui-upload__dropzone--over {
33
+ background: var(--secondary-hover);
34
+ }
35
+
36
+ .ui-upload__dropzone:focus-visible {
37
+ border-color: var(--border-focus);
38
+ box-shadow: 0 0 0 3px var(--focus-ring);
39
+ }
40
+
41
+ .ui-upload__dropzone--disabled {
42
+ cursor: not-allowed;
43
+ opacity: 0.6;
44
+ }
45
+
46
+ .ui-upload__dropzone-inner {
47
+ display: flex;
48
+ flex-direction: column;
49
+ align-items: center;
50
+ gap: var(--spacing-xs);
51
+ width: 100%;
52
+ }
53
+
54
+ .ui-upload__feature-icon {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ min-width: var(--spacing-36);
59
+ min-height: var(--spacing-36);
60
+ padding: var(--spacing-xs);
61
+ color: var(--secondary-fg);
62
+ background: var(--muted-bg);
63
+ border: 1px solid var(--border-primary);
64
+ border-radius: var(--rounded-full);
65
+ }
66
+
67
+ .ui-upload__dropzone-text {
68
+ display: flex;
69
+ flex-direction: column;
70
+ align-items: center;
71
+ gap: var(--spacing-2xs);
72
+ width: 100%;
73
+ text-align: center;
74
+ }
75
+
76
+ .ui-upload__dropzone-title {
77
+ margin: 0;
78
+ font-size: var(--paragraph-small-font-size);
79
+ line-height: var(--paragraph-small-line-height);
80
+ color: var(--primary-fg);
81
+ }
82
+
83
+ .ui-upload__link {
84
+ color: var(--primary-brand-fg);
85
+ font-weight: var(--paragraph-medium-font-weight);
86
+ }
87
+
88
+ .ui-upload__dropzone-hint {
89
+ margin: 0;
90
+ font-size: var(--paragraph-mini-font-size);
91
+ line-height: var(--paragraph-mini-line-height);
92
+ color: var(--muted-fg);
93
+ }
94
+
95
+ /* ---- Add-tile trigger (picture-card / picture-circle) ---- */
96
+ .ui-upload__card-trigger {
97
+ display: flex;
98
+ flex-direction: column;
99
+ align-items: center;
100
+ justify-content: center;
101
+ gap: var(--spacing-xs);
102
+ width: 140px;
103
+ height: 140px;
104
+ color: var(--muted-fg);
105
+ background: var(--primary-bg);
106
+ border: 1px solid var(--border-primary);
107
+ border-radius: var(--rounded-xl);
108
+ cursor: pointer;
109
+ outline: none;
110
+ transition: background 0.15s ease;
111
+ }
112
+
113
+ .ui-upload__card-trigger:hover,
114
+ .ui-upload__card-trigger--over {
115
+ background: var(--primary-hover);
116
+ }
117
+
118
+ .ui-upload__card-trigger:focus-visible {
119
+ border-color: var(--border-focus);
120
+ box-shadow: 0 0 0 3px var(--focus-ring);
121
+ }
122
+
123
+ .ui-upload__card-trigger--disabled {
124
+ cursor: not-allowed;
125
+ opacity: 0.6;
126
+ }
127
+
128
+ .ui-upload__card-trigger-text {
129
+ font-size: var(--paragraph-mini-font-size);
130
+ line-height: var(--paragraph-mini-line-height);
131
+ }
132
+
133
+ .ui-upload--picture-circle .ui-upload__card-trigger {
134
+ border-radius: var(--rounded-full);
135
+ }
136
+
137
+ /* ---- Preview dialog body ---- */
138
+ .ui-upload__preview {
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ }
143
+
144
+ .ui-upload__preview-img {
145
+ display: block;
146
+ max-width: 100%;
147
+ max-height: 70vh;
148
+ border-radius: var(--rounded-lg);
149
+ }
package/src/index.js CHANGED
@@ -48,6 +48,7 @@ export { default as WkToggle } from './components/toggle/Toggle.vue'
48
48
  export { default as WkToggleGroup } from './components/toggle-group/ToggleGroup.vue'
49
49
  export { default as WkTooltip } from './components/tooltip/Tooltip.vue'
50
50
  export { default as WkTypography } from './components/typography/Typography.vue'
51
+ export { default as WkUpload } from './components/upload/Upload.vue'
51
52
  export { default as WkDatePicker } from './components/date-picker/DatePicker.vue'
52
53
 
53
54
  // Imperative singleton service (no SFC export — mounts its own container to body).