@bagelink/vue 1.15.668 → 1.17.2

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/dist/assetTransport.d.ts +50 -0
  3. package/dist/assetTransport.d.ts.map +1 -0
  4. package/dist/assetTransport.test.d.ts +2 -0
  5. package/dist/assetTransport.test.d.ts.map +1 -0
  6. package/dist/assets.d.ts +46 -0
  7. package/dist/assets.d.ts.map +1 -0
  8. package/dist/assets.test.d.ts +2 -0
  9. package/dist/assets.test.d.ts.map +1 -0
  10. package/dist/components/Image.vue.d.ts +7 -1
  11. package/dist/components/Image.vue.d.ts.map +1 -1
  12. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts +11 -0
  13. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts.map +1 -0
  14. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -2
  15. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
  16. package/dist/components/lightbox/AssetPreview.vue.d.ts +9 -0
  17. package/dist/components/lightbox/AssetPreview.vue.d.ts.map +1 -0
  18. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  19. package/dist/components/lightbox/LightboxImage.vue.d.ts +2 -1
  20. package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -1
  21. package/dist/components/lightbox/index.d.ts.map +1 -1
  22. package/dist/components/lightbox/lightbox.types.d.ts +7 -0
  23. package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
  24. package/dist/composables/useImageSrc.d.ts +8 -4
  25. package/dist/composables/useImageSrc.d.ts.map +1 -1
  26. package/dist/core.css +1 -1
  27. package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -3
  28. package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
  29. package/dist/form-flow/form-flow.d.ts +11 -6
  30. package/dist/form-flow/form-flow.d.ts.map +1 -1
  31. package/dist/form-flow/schema-fields.d.ts +3 -0
  32. package/dist/form-flow/schema-fields.d.ts.map +1 -1
  33. package/dist/index.cjs +62 -62
  34. package/dist/index.css +1 -1
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.mjs +19554 -18982
  38. package/dist/style.css +1 -1
  39. package/dist/types/BagelForm.d.ts +3 -1
  40. package/dist/types/BagelForm.d.ts.map +1 -1
  41. package/package.json +104 -105
  42. package/src/assetTransport.test.ts +125 -0
  43. package/src/assetTransport.ts +164 -0
  44. package/src/assets.test.ts +123 -0
  45. package/src/assets.ts +245 -0
  46. package/src/components/Image.vue +19 -3
  47. package/src/components/form/inputs/Upload/AssetUploadInput.vue +67 -0
  48. package/src/components/form/inputs/Upload/UploadInput.vue +5 -1
  49. package/src/components/lightbox/AssetPreview.vue +16 -0
  50. package/src/components/lightbox/Lightbox.vue +8 -3
  51. package/src/components/lightbox/LightboxImage.vue +4 -3
  52. package/src/components/lightbox/index.ts +29 -4
  53. package/src/components/lightbox/lightbox.types.ts +5 -1
  54. package/src/composables/useImageSrc.ts +47 -37
  55. package/src/form-flow/form-flow.ts +26 -7
  56. package/src/form-flow/schema-fields.ts +23 -1
  57. package/src/index.ts +3 -0
  58. package/src/types/BagelForm.ts +3 -1
@@ -1,4 +1,8 @@
1
- export type XORSrcPath = { src: string, pathKey?: never } | { src?: never, pathKey: string }
1
+ import type { AssetReference } from '../../assets'
2
+
3
+ export type XORSrcPath = { src: string, pathKey?: never, asset?: never }
4
+ | { src?: never, pathKey: string, asset?: never }
5
+ | { src?: never, pathKey?: never, asset: AssetReference }
2
6
 
3
7
  export type LightboxItem = XORSrcPath & {
4
8
  type: string
@@ -1,6 +1,7 @@
1
1
  import type { MaybeRefOrGetter } from 'vue'
2
2
  import { appendScript, awaitGlobal, normalizeURL, pathKeyToURL } from '../index'
3
- import { ref, toValue, watch } from 'vue'
3
+ import { computed, onScopeDispose, ref, toValue, watch } from 'vue'
4
+ import { useAssetDelivery, type AssetDeliveryRequest } from '../assets'
4
5
 
5
6
  declare global {
6
7
  interface Window {
@@ -8,74 +9,76 @@ declare global {
8
9
  }
9
10
  }
10
11
 
11
- async function getCachedImage(url: string): Promise<string | undefined> {
12
- if (!('caches' in window)) { return undefined }
13
- try {
14
- const imgCache = await window.caches.open('img-cache')
15
- const cachedResponse = await imgCache.match(url)
16
- if (cachedResponse) {
17
- return URL.createObjectURL(await cachedResponse.blob())
18
- }
19
- } catch (error) {
20
- console.warn('Cache access error:', error)
21
- }
22
- return undefined
23
- }
24
-
25
- async function cacheImage(url: string, blob: Blob) {
26
- if (!('caches' in window)) { return }
27
- try {
28
- const imgCache = await window.caches.open('img-cache')
29
- await imgCache.put(url, new Response(blob))
30
- } catch (error) {
31
- console.warn('Cache write error:', error)
32
- }
12
+ let legacyCacheCleanup: Promise<boolean> | undefined
13
+ /** Remove only the retired sensitive image cache, once per browser session. */
14
+ export function clearLegacyImageCache() {
15
+ if (typeof window === 'undefined' || !('caches' in window)) return
16
+ legacyCacheCleanup ??= window.caches.delete('img-cache').catch(() => false)
17
+ return legacyCacheCleanup
33
18
  }
34
19
 
35
- async function convertHeicImage(url: string): Promise<string> {
20
+ async function convertHeicImage(url: string, signal: AbortSignal): Promise<string> {
36
21
  await appendScript('https://cdnjs.cloudflare.com/ajax/libs/heic2any/0.0.1/index.min.js')
37
22
  const heic2any = await awaitGlobal<(opts: { blob: Blob }) => Promise<Blob>>('heic2any')
38
- const response = await fetch(normalizeURL(url))
23
+ const response = await fetch(normalizeURL(url), { signal, cache: 'no-store' })
24
+ if (!response.ok) throw new Error('Image request failed')
39
25
  const blob = await response.blob()
40
26
  const convertedBlob = await heic2any({ blob })
41
- await cacheImage(url, convertedBlob)
42
27
  return URL.createObjectURL(convertedBlob)
43
28
  }
44
29
 
45
30
  /**
46
31
  * Resolve an image source for display: turns a pathKey/URL into a usable URL via
47
32
  * `pathKeyToURL`, and transparently converts `.heic` files to a displayable blob
48
- * (with a cache) since browsers can't render HEIC in <img>. Shared by <Image>
33
+ * (in memory only) since browsers can't render HEIC in <img>. Shared by <Image>
49
34
  * and the lightbox so both get the same resolution + HEIC handling.
50
35
  *
51
36
  * @param source ref/getter of the raw src or pathKey
52
37
  * @returns `{ imageSrc, loadingError }` reactive refs
53
38
  */
54
- export function useImageSrc(source: MaybeRefOrGetter<string | undefined>) {
39
+ export function useImageSrc(source: MaybeRefOrGetter<string | AssetDeliveryRequest | undefined>) {
40
+ void clearLegacyImageCache()
41
+ const asset = useAssetDelivery(() => {
42
+ const value = toValue(source)
43
+ return typeof value === 'object' ? { ...value, representation: value.representation ?? 'preview' } : undefined
44
+ })
55
45
  const imageSrc = ref<string | undefined>(undefined)
56
46
  const loadingError = ref(false)
47
+ let controller: AbortController | undefined
48
+ let generation = 0
49
+ let objectUrl: string | undefined
50
+ function cleanup() {
51
+ controller?.abort()
52
+ if (objectUrl) URL.revokeObjectURL(objectUrl)
53
+ objectUrl = undefined
54
+ }
57
55
 
58
56
  async function load() {
57
+ const current = ++generation
58
+ cleanup()
59
59
  loadingError.value = false
60
- const url = pathKeyToURL(toValue(source))
60
+ imageSrc.value = undefined
61
+ const value = toValue(source)
62
+ if (typeof value === 'object') return
63
+ const url = pathKeyToURL(value)
61
64
  if (url === undefined || url === '') {
62
65
  imageSrc.value = undefined
63
66
  return
64
67
  }
65
68
 
66
69
  try {
70
+ controller = new AbortController()
67
71
  const ext = url.split('.').pop()?.toLowerCase().split('?')[0]
68
72
  if (ext === 'heic') {
69
- const cachedSrc = await getCachedImage(url)
70
- if (cachedSrc !== undefined && cachedSrc !== '') {
71
- imageSrc.value = cachedSrc
72
- return
73
- }
74
- imageSrc.value = await convertHeicImage(url)
73
+ const converted = await convertHeicImage(url, controller.signal)
74
+ if (current !== generation) { URL.revokeObjectURL(converted); return }
75
+ objectUrl = converted
76
+ imageSrc.value = converted
75
77
  } else {
76
78
  imageSrc.value = url
77
79
  }
78
80
  } catch (error) {
81
+ if (current !== generation) return
79
82
  console.error('Image loading error:', error)
80
83
  loadingError.value = true
81
84
  imageSrc.value = undefined
@@ -83,6 +86,13 @@ export function useImageSrc(source: MaybeRefOrGetter<string | undefined>) {
83
86
  }
84
87
 
85
88
  watch(() => toValue(source), load, { immediate: true })
86
-
87
- return { imageSrc, loadingError }
89
+ onScopeDispose(() => { generation++; cleanup() })
90
+ return {
91
+ imageSrc: computed(() => typeof toValue(source) === 'object' ? asset.url.value : imageSrc.value),
92
+ loadingError: computed(() => typeof toValue(source) === 'object' ? asset.error.value : loadingError.value),
93
+ onError: () => {
94
+ if (typeof toValue(source) === 'object') asset.onError()
95
+ else loadingError.value = true
96
+ },
97
+ }
88
98
  }
@@ -17,6 +17,10 @@ import {
17
17
  isCustomFormFieldType,
18
18
  warnMissingCustomFormField,
19
19
  } from './custom-field-registry'
20
+ import type {
21
+ JSONSchemaObject as FormJSONSchemaObject,
22
+ JSONSchemaProperty as FormJSONSchemaProperty,
23
+ } from './schema-fields'
20
24
 
21
25
  // ============================================================================
22
26
  // Core Types
@@ -412,6 +416,8 @@ export const $ = {
412
416
  noFilePlaceholder?: string
413
417
  btnPlaceholder?: string
414
418
  style?: string
419
+ /** Upload to the immutable assets catalog and persist a `{asset_id, version_id}` pin. */
420
+ assetMode?: boolean
415
421
  }),
416
422
  config?: BaseFieldConfig & {
417
423
  multiple?: boolean
@@ -428,6 +434,8 @@ export const $ = {
428
434
  noFilePlaceholder?: string
429
435
  btnPlaceholder?: string
430
436
  style?: string
437
+ /** Upload to the immutable assets catalog and persist a `{asset_id, version_id}` pin. */
438
+ assetMode?: boolean
431
439
  }
432
440
  ): FieldBuilder<string | string[]> {
433
441
  return new Field('upload', parseArgs(labelOrConfig, config))
@@ -806,8 +814,10 @@ export function multiStepSchema<T extends Record<string, any> = Record<string, a
806
814
  // JSONSchema Import
807
815
  // ============================================================================
808
816
 
809
- interface JSONSchemaProperty {
810
- 'type'?: string
817
+ type JSONSchemaProperty = Omit<
818
+ FormJSONSchemaProperty,
819
+ 'items' | 'properties' | 'x-field-type'
820
+ > & {
811
821
  'title'?: string
812
822
  'description'?: string
813
823
  'default'?: any
@@ -855,6 +865,7 @@ interface JSONSchemaProperty {
855
865
  'x-placeholder'?: string
856
866
  'x-select-multiselect'?: boolean
857
867
  'x-select-searchable'?: boolean
868
+ 'x-asset'?: boolean
858
869
  'x-radio-inline'?: boolean
859
870
  'x-radio-align'?: 'start' | 'center' | 'end'
860
871
  'x-radio-text-align'?: 'left' | 'center' | 'right'
@@ -929,8 +940,8 @@ function normalizeOptionsSourceResponse(
929
940
  })
930
941
  }
931
942
 
932
- interface JSONSchemaObject {
933
- 'type'?: string
943
+ type JSONSchemaObject = Omit<FormJSONSchemaObject, 'properties'> & {
944
+ 'type': 'object'
934
945
  'title'?: string
935
946
  'properties'?: Record<string, JSONSchemaProperty>
936
947
  'required'?: string[]
@@ -1039,8 +1050,8 @@ function resolveUnionType(prop: JSONSchemaProperty, _rootSchema: JSONSchemaObjec
1039
1050
  // Prefer object > array > string > number > boolean
1040
1051
  const typeOrder = ['object', 'array', 'string', 'number', 'integer', 'boolean']
1041
1052
  const sorted = nonNullTypes.sort((a, b) => {
1042
- const aIdx = typeOrder.indexOf(a.type || 'string')
1043
- const bIdx = typeOrder.indexOf(b.type || 'string')
1053
+ const aIdx = typeOrder.indexOf(Array.isArray(a.type) ? (a.type[0] ?? 'string') : (a.type ?? 'string'))
1054
+ const bIdx = typeOrder.indexOf(Array.isArray(b.type) ? (b.type[0] ?? 'string') : (b.type ?? 'string'))
1044
1055
  return aIdx - bIdx
1045
1056
  })
1046
1057
 
@@ -1092,8 +1103,12 @@ function createFieldFromProperty(
1092
1103
  const xUi = resolvedProp['x-ui'] || {}
1093
1104
  // Field type may be declared either as `x-ui.fieldType` (this module's own
1094
1105
  // round-trip) or the flat `x-field-type` (FormBuilder designer). Multiselect
1095
- // is a `select` flagged multi.
1106
+ // is a `select` flagged multi. `x-asset` is an immutable-assets file field
1107
+ // whose JSON-schema type is `object` (the pin), not `string`.
1096
1108
  let fieldType = xUi.fieldType || resolvedProp['x-field-type'] || inferFieldType(resolvedProp)
1109
+ if (resolvedProp['x-asset'] === true && fieldType !== 'file' && fieldType !== 'upload') {
1110
+ fieldType = 'file'
1111
+ }
1097
1112
  if (fieldType === 'select' && resolvedProp['x-select-multiselect']) fieldType = 'multiselect'
1098
1113
 
1099
1114
  // Designer-authored options: [{label,value}] pairs preserve labels (raw
@@ -1232,6 +1247,10 @@ function createFieldFromProperty(
1232
1247
  break
1233
1248
  case 'file':
1234
1249
  case 'upload':
1250
+ // `x-asset` marks a field as managed by the immutable assets catalog:
1251
+ // the widget uploads and persists a `{asset_id, version_id}` pin, and
1252
+ // the JSON-schema type is `object`, not `string`.
1253
+ if (resolvedProp['x-asset'] === true) config.assetMode = true
1235
1254
  field = $.upload(config)
1236
1255
  break
1237
1256
  case 'array': {
@@ -95,6 +95,8 @@ export interface FormField {
95
95
  fileTheme?: 'dropzone' | 'basic'
96
96
  fileShowIcon?: boolean
97
97
  fileIcon?: string
98
+ /** Upload to the immutable assets catalog; persist a `{asset_id, version_id}` pin. */
99
+ assetMode?: boolean
98
100
  dateEnableTime?: boolean
99
101
  dateMin?: string
100
102
  dateMax?: string
@@ -163,6 +165,7 @@ export interface JSONSchemaProperty {
163
165
  'x-file-theme'?: 'dropzone' | 'basic'
164
166
  'x-file-show-icon'?: boolean
165
167
  'x-file-icon'?: string
168
+ 'x-asset'?: boolean
166
169
  'x-date-enable-time'?: boolean
167
170
  'x-date-min'?: string
168
171
  'x-date-max'?: string
@@ -351,7 +354,26 @@ export function fieldsToSchema(
351
354
  prop.type = 'string'
352
355
  break
353
356
  case 'file':
354
- if (field.fileMultiple === true) {
357
+ if (field.assetMode === true) {
358
+ // Immutable assets catalog: persist a `{asset_id, version_id}` pin.
359
+ // The JSON-schema type is `object` (the pin), and `x-asset` is the
360
+ // authoritative marker — never a `string` URL.
361
+ const assetRef = {
362
+ type: 'object',
363
+ properties: {
364
+ asset_id: { type: 'string', format: 'uuid' },
365
+ version_id: { type: 'string', format: 'uuid' },
366
+ },
367
+ required: ['asset_id', 'version_id'],
368
+ }
369
+ if (field.fileMultiple === true) {
370
+ prop.type = 'array'
371
+ prop.items = assetRef
372
+ } else {
373
+ Object.assign(prop, assetRef)
374
+ }
375
+ prop['x-asset'] = true
376
+ } else if (field.fileMultiple === true) {
355
377
  prop.type = 'array'
356
378
  prop.items = { type: 'string' }
357
379
  } else {
package/src/index.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from './components'
2
+ export * from './assets'
3
+ export * from './assetTransport'
4
+ export { default as AssetPreview } from './components/lightbox/AssetPreview.vue'
2
5
  export * from './composables'
3
6
  // Dialog (native <dialog> based)
4
7
  export * from './dialog'
@@ -192,6 +192,8 @@ export interface UploadInputValueObject {
192
192
  }
193
193
 
194
194
  export interface UploadInputProps extends BagelInputShellProps {
195
+ /** Opt-in stable reference output; defaults to legacy path-key behavior. */
196
+ assetMode?: boolean
195
197
  id?: string
196
198
  name?: string
197
199
  label?: string
@@ -200,7 +202,7 @@ export interface UploadInputProps extends BagelInputShellProps {
200
202
  * Path-key string(s). Legacy/migrated records may also provide an image object
201
203
  * (e.g. `{ imageURL, imageName, extension, altText }`); it is normalized internally.
202
204
  */
203
- modelValue?: string | string[] | UploadInputValueObject | (string | UploadInputValueObject)[]
205
+ modelValue?: string | string[] | UploadInputValueObject | import('../assets').AssetReference | (string | UploadInputValueObject | import('../assets').AssetReference)[]
204
206
  /** Optional upload handler. When omitted, the shared file-storage uploader is used. */
205
207
  upload?: (file: File) => Promise<string>
206
208
  width?: string