webcake-ui-kit 1.2.16 → 1.2.18

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
+ }
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <BaseIcon v-bind="$attrs" v-on="listeners">
3
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
4
+ <path
5
+ d="M11.625 2.125C12.1082 2.125 12.5 2.51675 12.5 3C12.5 3.48325 12.1082 3.875 11.625 3.875H5C4.37868 3.875 3.875 4.37868 3.875 5V19C3.875 19.6213 4.37868 20.125 5 20.125H19C19.6213 20.125 20.125 19.6213 20.125 19V11.875C20.125 11.3918 20.5168 11 21 11C21.4832 11 21.875 11.3918 21.875 11.875V19C21.875 20.5878 20.5878 21.875 19 21.875H5C3.41218 21.875 2.125 20.5878 2.125 19V5C2.125 3.41218 3.41218 2.125 5 2.125H11.625Z"
6
+ fill="currentColor"
7
+ />
8
+ <path
9
+ fill-rule="evenodd"
10
+ clip-rule="evenodd"
11
+ d="M17.875 2.125C20.0151 2.125 21.75 3.8599 21.75 6C21.75 8.1401 20.0151 9.875 17.875 9.875C15.7349 9.875 14 8.1401 14 6C14 3.8599 15.7349 2.125 17.875 2.125ZM17.875 3.875C16.7014 3.875 15.75 4.8264 15.75 6C15.75 6.45107 15.8912 6.86887 16.1309 7.21289C16.1531 7.24478 16.1773 7.27508 16.2012 7.30566C16.3675 7.51859 16.573 7.6982 16.8076 7.83496C16.8234 7.84415 16.8385 7.85449 16.8545 7.86328C17.1575 8.02958 17.505 8.125 17.875 8.125C19.0486 8.125 20 7.1736 20 6C20 4.8264 19.0486 3.875 17.875 3.875Z"
12
+ fill="currentColor"
13
+ />
14
+ </svg>
15
+ </BaseIcon>
16
+ </template>
17
+
18
+ <script>
19
+ import BaseIcon from './BaseIcon.vue'
20
+
21
+ export default {
22
+ name: 'Popup',
23
+ components: { BaseIcon },
24
+ inheritAttrs: false,
25
+ emits: [],
26
+ computed: {
27
+ listeners() {
28
+ return this.$listeners || {}
29
+ }
30
+ }
31
+ }
32
+ </script>
@@ -1774,5 +1774,6 @@ export { default as WkiSkewWidth } from './SkewWidth.vue'
1774
1774
  export { default as WkiStrokeLine } from './StrokeLine.vue'
1775
1775
  export { default as WkiTopline } from './Topline.vue'
1776
1776
  export { default as WkiLettersSpacing } from './LettersSpacing.vue'
1777
+ export { default as WkiPopup } from './Popup.vue'
1777
1778
 
1778
1779
  // --- AUTO-GENERATED LUCIDE EXPORTS END ---
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).