@bagelink/vue 1.15.75 → 1.15.80

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 (34) hide show
  1. package/dist/components/Image.vue.d.ts +0 -5
  2. package/dist/components/Image.vue.d.ts.map +1 -1
  3. package/dist/components/PageTitle.vue.d.ts +10 -0
  4. package/dist/components/PageTitle.vue.d.ts.map +1 -1
  5. package/dist/components/RouterWrapper.vue.d.ts +6 -2
  6. package/dist/components/RouterWrapper.vue.d.ts.map +1 -1
  7. package/dist/components/layout/AppContent.vue.d.ts +2 -0
  8. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  9. package/dist/components/layout/AppSidebar.vue.d.ts +5 -0
  10. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  11. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  12. package/dist/components/lightbox/LightboxImage.vue.d.ts +6 -0
  13. package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -0
  14. package/dist/components/lightbox/index.d.ts.map +1 -1
  15. package/dist/composables/index.d.ts +1 -0
  16. package/dist/composables/index.d.ts.map +1 -1
  17. package/dist/composables/useImageSrc.d.ts +20 -0
  18. package/dist/composables/useImageSrc.d.ts.map +1 -0
  19. package/dist/index.cjs +34 -34
  20. package/dist/index.mjs +4296 -4262
  21. package/dist/style.css +1 -1
  22. package/package.json +1 -1
  23. package/src/components/Image.vue +3 -79
  24. package/src/components/PageTitle.vue +14 -28
  25. package/src/components/RouterWrapper.vue +22 -0
  26. package/src/components/layout/AppContent.vue +3 -1
  27. package/src/components/layout/AppSidebar.vue +8 -3
  28. package/src/components/lightbox/Lightbox.vue +2 -1
  29. package/src/components/lightbox/LightboxImage.vue +14 -0
  30. package/src/components/lightbox/index.ts +4 -0
  31. package/src/composables/index.ts +1 -0
  32. package/src/composables/useImageSrc.ts +88 -0
  33. package/src/styles/color-variants.css +13 -4
  34. package/src/utils/index.ts +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.75",
4
+ "version": "1.15.80",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  defineOptions({ name: 'BglImage', inheritAttrs: true })
3
- import { Skeleton, normalizeDimension, appendScript, awaitGlobal, normalizeURL, Icon, pathKeyToURL } from '@bagelink/vue'
4
- import { computed, ref, useSlots, watch } from 'vue'
3
+ import { Skeleton, normalizeDimension, Icon, useImageSrc } from '@bagelink/vue'
4
+ import { computed, useSlots } from 'vue'
5
5
 
6
6
  interface ImageProps {
7
7
  src?: string
@@ -23,85 +23,9 @@ interface ImageProps {
23
23
  rounded?: boolean | number
24
24
  }
25
25
 
26
- declare global {
27
- interface Window {
28
- heic2any: any
29
- }
30
- }
31
-
32
26
  const props = defineProps<ImageProps>()
33
27
 
34
- const imageSrc = ref<string | undefined>(undefined)
35
- const loadingError = ref(false)
36
-
37
- function getImageUrl(): string | undefined {
38
- if ((props.src === undefined || props.src === '') && (props.pathKey === undefined || props.pathKey === '') && (props.modelValue === undefined || props.modelValue === '')) { return }
39
- return pathKeyToURL(props.src ?? props.pathKey ?? props.modelValue)
40
- }
41
-
42
- async function getCachedImage(url: string): Promise<string | undefined> {
43
- if (!('caches' in window)) { return undefined }
44
- try {
45
- const imgCache = await window.caches.open('img-cache')
46
- const cachedResponse = await imgCache.match(url)
47
- if (cachedResponse) {
48
- return URL.createObjectURL(await cachedResponse.blob())
49
- }
50
- } catch (error) {
51
- console.warn('Cache access error:', error)
52
- }
53
- return undefined
54
- }
55
-
56
- async function cacheImage(url: string, blob: Blob) {
57
- if (!('caches' in window)) { return }
58
- try {
59
- const imgCache = await window.caches.open('img-cache')
60
- await imgCache.put(url, new Response(blob))
61
- } catch (error) {
62
- console.warn('Cache write error:', error)
63
- }
64
- }
65
-
66
- async function convertHeicImage(url: string): Promise<string> {
67
- await appendScript('https://cdnjs.cloudflare.com/ajax/libs/heic2any/0.0.1/index.min.js')
68
- const heic2any = await awaitGlobal<(opts: { blob: Blob }) => Promise<Blob>>('heic2any')
69
- const response = await fetch(normalizeURL(url))
70
- const blob = await response.blob()
71
- const convertedBlob = await heic2any({ blob })
72
- await cacheImage(url, convertedBlob)
73
- return URL.createObjectURL(convertedBlob)
74
- }
75
-
76
- async function loadImage() {
77
- loadingError.value = false
78
- const url = getImageUrl()
79
- if (url === undefined || url === '') {
80
- imageSrc.value = undefined
81
- return
82
- }
83
-
84
- try {
85
- const ext = url.split('.').pop()?.toLowerCase().split('?')[0]
86
-
87
- if (ext === 'heic') {
88
- const cachedSrc = await getCachedImage(url)
89
- if (cachedSrc !== undefined && cachedSrc !== '') {
90
- imageSrc.value = cachedSrc
91
- return
92
- }
93
- imageSrc.value = await convertHeicImage(url)
94
- } else {
95
- imageSrc.value = url
96
- }
97
- } catch (error) {
98
- console.error('Image loading error:', error)
99
- loadingError.value = true
100
- imageSrc.value = undefined
101
- }
102
- }
103
-
104
- watch(() => [props.src, props.pathKey, props.modelValue], loadImage, { immediate: true })
28
+ const { imageSrc, loadingError } = useImageSrc(() => props.src ?? props.pathKey ?? props.modelValue)
105
29
 
106
30
  // ── Framed mode (ratio / gradient / blend / rounded / overlay slot) ──────────
107
31
  const slots = useSlots()
@@ -4,36 +4,22 @@ defineProps({
4
4
  type: String,
5
5
  default: '',
6
6
  },
7
-
7
+ subtitle: {
8
+ type: String,
9
+ default: '',
10
+ },
8
11
  })
9
12
  </script>
10
13
 
11
14
  <template>
12
- <div class="page-top">
13
- <h1 class="top-title m-0">
14
- <slot /> {{ value }}
15
- </h1>
15
+ <div class="page-top flex gap-1">
16
+ <div class="grid gap-025">
17
+ <h1 class="m-0 semibold txt-20 m_txt16 line-height-1">
18
+ <slot /> {{ value }}
19
+ </h1>
20
+ <p v-if="subtitle || $slots.subtitle" class="top-subtitle m-0 txt-13 m_txt12 align-items-center line-height-12 opacity-6">
21
+ <slot name="subtitle">{{ subtitle }}</slot>
22
+ </p>
23
+ </div>
16
24
  </div>
17
- </template>
18
-
19
- <style>
20
-
21
- .top-title {
22
- font-weight: 600;
23
- font-size: 20px;
24
- line-height: 1;
25
- }
26
-
27
- .page-top {
28
- display: flex;
29
- align-items: center;
30
- gap: 1rem;
31
- }
32
-
33
- @media screen and (max-width: 910px) {
34
- .top-title {
35
- font-size: 16px;
36
- }
37
- }
38
-
39
- </style>
25
+ </template>
@@ -1,8 +1,30 @@
1
1
  <script setup lang="ts">
2
+ import { nextTick, ref, watch } from 'vue'
3
+ import { useRoute } from 'vue-router'
2
4
  import Loading from './Loading.vue'
5
+
6
+ // Reset the scroll position on every route change. The app's scroll container is
7
+ // the inner `.pageContent` element in AppContent (the window itself doesn't
8
+ // scroll), so a plain vue-router `scrollBehavior` wouldn't help. We scroll the
9
+ // nearest `.pageContent` ancestor back to the top after the new view mounts.
10
+ const anchor = ref<HTMLElement | null>(null)
11
+ const route = useRoute()
12
+
13
+ watch(
14
+ () => route.fullPath,
15
+ async () => {
16
+ await nextTick()
17
+ const container = anchor.value?.closest('.pageContent') as HTMLElement | null
18
+ if (container) container.scrollTop = 0
19
+ // Fallback: also reset the window in case a view scrolls the page itself.
20
+ else window.scrollTo({ top: 0 })
21
+ },
22
+ )
3
23
  </script>
4
24
 
5
25
  <template>
26
+ <!-- Zero-size anchor used to locate the scrolling `.pageContent` ancestor. -->
27
+ <span ref="anchor" aria-hidden="true" style="display: none" />
6
28
  <RouterView v-slot="{ Component, route }">
7
29
  <slot v-if="!Component">
8
30
  <div class="w-100p h-100vh flex justify-content-center">
@@ -4,6 +4,8 @@ import { useAppLayout } from './appLayoutContext'
4
4
 
5
5
  interface Props {
6
6
  title?: string
7
+ /** Secondary line under the title (page description / context). */
8
+ subtitle?: string
7
9
  showMenuButton?: boolean
8
10
  backTo?: string | Record<string, any>
9
11
  border?: boolean
@@ -41,7 +43,7 @@ const { isOpen, toggleMenu, sidebarCardStyle } = useAppLayout()
41
43
  <Btn v-if="backTo" icon="arrow_back" thin :to="backTo" class="back-btn bg-bg m_-ms-05 m_-me-05 color-black" />
42
44
 
43
45
  <!-- Page Title -->
44
- <PageTitle v-if="title">
46
+ <PageTitle v-if="title || subtitle" :subtitle="subtitle">
45
47
  {{ title }}
46
48
  </PageTitle>
47
49
 
@@ -24,6 +24,8 @@ interface Props {
24
24
  activeColor?: string
25
25
  logoHeight?: string
26
26
  name?: string
27
+ /** Small tagline rendered under the app name (hidden when collapsed). */
28
+ nameSubtitle?: string
27
29
  frame?: boolean
28
30
  activeRoutes?: string[]
29
31
  centerlinks?: boolean
@@ -162,9 +164,12 @@ const sidebarStyles = computed(() => {
162
164
  v-if="props.logo" :src="props.logo" :alt="props.logoAlt" class="contain"
163
165
  :style="{ height: props.logoHeight }"
164
166
  >
165
- <span class="nav-text">
166
- {{ props.name }}
167
- </span>
167
+ <slot name="brand" v-bind="{ isOpen: isVisuallyOpen }">
168
+ <span class="nav-text flex column line-height-1 gap-025 align-items-start">
169
+ <span>{{ props.name }}</span>
170
+ <span v-if="props.nameSubtitle" class="txt11 opacity-6">{{ props.nameSubtitle }}</span>
171
+ </span>
172
+ </slot>
168
173
  </router-link>
169
174
 
170
175
  <!-- Navigation Links -->
@@ -3,6 +3,7 @@ import type { LightboxItem } from './lightbox.types'
3
3
 
4
4
  import { BglVideo, Btn, Icon, Zoomer, Image, normalizeURL, Swiper, downloadFile, useEscapeKey } from '@bagelink/vue'
5
5
  import { computed, ref, watch } from 'vue'
6
+ import LightboxImage from './LightboxImage.vue'
6
7
 
7
8
  const isOpen = ref(false)
8
9
  const group = ref<LightboxItem[]>([])
@@ -88,7 +89,7 @@ defineExpose({ open, close })
88
89
  <Zoomer v-if="item.type === 'image'" v-model:zoom="zoom" :disabled="!item?.enableZoom"
89
90
  :mouse-wheel-to-zoom="false" :double-click-to-zoom="true" :max-scale="5" :min-scale="1"
90
91
  :aspect-ratio="0" :limit-translation="true" @click.stop>
91
- <Image :draggable="false" :src="item?.src" alt="Preview" class="lightbox-image" />
92
+ <LightboxImage :src="item?.src" />
92
93
  </Zoomer>
93
94
 
94
95
  <BglVideo v-else-if="item?.type === 'video' && item?.src" :src="item?.src" autoplay controls
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ // Plain <img> for the lightbox preview that still gets shared src resolution +
3
+ // HEIC conversion via useImageSrc. A bare <img> (not <Image>) is required here
4
+ // so the natural dimensions drive layout — <Image>'s framed mode renders an
5
+ // absolute-fill img that collapses the zoomer to 0×0.
6
+ import { useImageSrc } from '@bagelink/vue'
7
+
8
+ const props = defineProps<{ src?: string }>()
9
+ const { imageSrc } = useImageSrc(() => props.src)
10
+ </script>
11
+
12
+ <template>
13
+ <img :draggable="false" :src="imageSrc" alt="Preview" class="lightbox-image">
14
+ </template>
@@ -70,6 +70,10 @@ function urlToName(url: string): string {
70
70
 
71
71
  function determineFileType(url: any): string {
72
72
  if (typeof url !== 'string' || !url) { return 'unknown' }
73
+ // Extensionless sources (object URLs, data URLs, signed/CDN URLs) can't be sniffed
74
+ // by extension — fall back to MIME hints so they still preview as images/video.
75
+ if (url.startsWith('blob:') || /^data:image\//i.test(url)) { return 'image' }
76
+ if (/^data:video\//i.test(url)) { return 'video' }
73
77
  const extension = (url.split('.').pop() || '').toLowerCase()
74
78
  const altExtension = url.split('?')[0].split('.').pop()?.toLowerCase() || ''
75
79
  if (IMAGE_FORMATS_REGEXP.test(extension) || IMAGE_FORMATS_REGEXP.test(altExtension)) { return 'image' }
@@ -10,6 +10,7 @@ export { useEscapeKey } from './useEscapeKey'
10
10
  export { useExcel } from './useExcel'
11
11
  export type { GradientDir, GradientDirProp, GradientProp } from './useGradientVariant'
12
12
  export { useGradientVariant } from './useGradientVariant'
13
+ export { useImageSrc } from './useImageSrc'
13
14
  export { useLocalStore } from './useLocalStore'
14
15
  export { usePolling } from './usePolling'
15
16
  export { useQuery } from './useQuery'
@@ -0,0 +1,88 @@
1
+ import type { MaybeRefOrGetter } from 'vue'
2
+ import { appendScript, awaitGlobal, normalizeURL, pathKeyToURL } from '@bagelink/vue'
3
+ import { ref, toValue, watch } from 'vue'
4
+
5
+ declare global {
6
+ interface Window {
7
+ heic2any: any
8
+ }
9
+ }
10
+
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
+ }
33
+ }
34
+
35
+ async function convertHeicImage(url: string): Promise<string> {
36
+ await appendScript('https://cdnjs.cloudflare.com/ajax/libs/heic2any/0.0.1/index.min.js')
37
+ const heic2any = await awaitGlobal<(opts: { blob: Blob }) => Promise<Blob>>('heic2any')
38
+ const response = await fetch(normalizeURL(url))
39
+ const blob = await response.blob()
40
+ const convertedBlob = await heic2any({ blob })
41
+ await cacheImage(url, convertedBlob)
42
+ return URL.createObjectURL(convertedBlob)
43
+ }
44
+
45
+ /**
46
+ * Resolve an image source for display: turns a pathKey/URL into a usable URL via
47
+ * `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>
49
+ * and the lightbox so both get the same resolution + HEIC handling.
50
+ *
51
+ * @param source ref/getter of the raw src or pathKey
52
+ * @returns `{ imageSrc, loadingError }` reactive refs
53
+ */
54
+ export function useImageSrc(source: MaybeRefOrGetter<string | undefined>) {
55
+ const imageSrc = ref<string | undefined>(undefined)
56
+ const loadingError = ref(false)
57
+
58
+ async function load() {
59
+ loadingError.value = false
60
+ const url = pathKeyToURL(toValue(source))
61
+ if (url === undefined || url === '') {
62
+ imageSrc.value = undefined
63
+ return
64
+ }
65
+
66
+ try {
67
+ const ext = url.split('.').pop()?.toLowerCase().split('?')[0]
68
+ 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)
75
+ } else {
76
+ imageSrc.value = url
77
+ }
78
+ } catch (error) {
79
+ console.error('Image loading error:', error)
80
+ loadingError.value = true
81
+ imageSrc.value = undefined
82
+ }
83
+ }
84
+
85
+ watch(() => toValue(source), load, { immediate: true })
86
+
87
+ return { imageSrc, loadingError }
88
+ }
@@ -137,12 +137,21 @@
137
137
  * The `via` slot collapses to nothing when unset (2-stop), expands when set
138
138
  * (3-stop) — exactly like gradients.css. */
139
139
  [class*="pair-"].gradient {
140
- --bgl-grad-default-from: var(--bgl-pair-tone, var(--bgl-primary));
141
- --bgl-grad-default-to: color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-primary)) 55%, #000);
140
+ /* Auto (single-tone) gradient: a vivid sweep from a lighter take on the tone
141
+ to a darker one, giving real depth without going muddy.
142
+
143
+ IMPORTANT: gradients.css declares --bgl-grad-from/to on :root (= transparent),
144
+ so a `var(--bgl-grad-from, fallback)` would never use the fallback. We instead
145
+ *set* --bgl-grad-from/to right here to the auto values. The Btn/Badge
146
+ components override them again via inline style for explicit 2–3 stop
147
+ gradients, and gradients.css `from-*`/`to-*` utilities still win by being
148
+ later/inline. */
149
+ --bgl-grad-from: color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-primary)) 90%, #fff);
150
+ --bgl-grad-to: color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-primary)) 55%, #000);
142
151
  background-image: linear-gradient(
143
152
  var(--bgl-grad-angle, 135deg),
144
- var(--bgl-grad-from, var(--bgl-grad-default-from)),
145
- var(--bgl-grad-via, ) var(--bgl-grad-to, var(--bgl-grad-default-to))
153
+ var(--bgl-grad-from),
154
+ var(--bgl-grad-via, ) var(--bgl-grad-to)
146
155
  ) !important;
147
156
  color: var(--bgl-white) !important;
148
157
  border: none !important;
@@ -257,7 +257,7 @@ export type { ComparisonOperator, FilterCondition, LogicalOperator, QueryConditi
257
257
  export { anyOf, buildQuery, evaluateQuery, parseQuery, queryFilter, range, search } from './queryFilter'
258
258
  export type { ShowdownConverter, ShowdownOptions } from './showdown'
259
259
 
260
- const URL_REGEX = /^https?:\/\/|^\/\//
260
+ const URL_REGEX = /^https?:\/\/|^\/\/|^blob:|^data:/
261
261
 
262
262
  export function pathKeyToURL(pathKey?: string | null): string | undefined {
263
263
  if (pathKey == null || pathKey === '') {