@bagelink/vue 1.15.271 → 1.15.273

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.271",
4
+ "version": "1.15.273",
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: false })
3
3
  import { Skeleton, normalizeDimension, Icon, useImageSrc } from '@bagelink/vue'
4
- import { Comment, computed, useSlots, type StyleValue, type VNode } from 'vue'
4
+ import { Comment, computed, ref, useSlots, watch, type StyleValue, type VNode } from 'vue'
5
5
 
6
6
  interface ImageProps {
7
7
  src?: string
@@ -26,6 +26,15 @@ interface ImageProps {
26
26
  const props = defineProps<ImageProps>()
27
27
 
28
28
  const { imageSrc, loadingError } = useImageSrc(() => props.src ?? props.pathKey ?? props.modelValue)
29
+ const imageLoaded = ref(false)
30
+
31
+ watch(imageSrc, () => {
32
+ imageLoaded.value = false
33
+ })
34
+
35
+ function onImageLoad() {
36
+ imageLoaded.value = true
37
+ }
29
38
 
30
39
  // ── Framed mode (ratio / gradient / blend / rounded / overlay slot) ──────────
31
40
  const slots = useSlots()
@@ -89,10 +98,11 @@ const imgFit = computed(() => props.fit ?? 'cover')
89
98
  :src="imageSrc"
90
99
  :alt="alt"
91
100
  class="bgl-image-frame-img h-100p"
92
- :class="frameHasHeight ? 'absolute-fill' : 'bgl-image-frame-img--flow'"
101
+ :class="[frameHasHeight ? 'absolute-fill' : 'bgl-image-frame-img--flow', { 'opacity-0': !imageLoaded }]"
93
102
  :style="{ objectFit: imgFit, mixBlendMode: blend } as StyleValue"
103
+ @load="onImageLoad"
94
104
  >
95
- <Skeleton v-else-if="!gradientCss && !loadingError" class="absolute-fill" />
105
+ <Skeleton v-if="!imageLoaded && !loadingError" class="absolute-fill" />
96
106
  <div v-else-if="loadingError && !gradientCss" class="flex-center absolute-fill error-image">
97
107
  <Icon name="broken_image" />
98
108
  </div>
@@ -100,6 +110,7 @@ const imgFit = computed(() => props.fit ?? 'cover')
100
110
  </div>
101
111
 
102
112
  <figcaption v-else-if="caption">
113
+ <div class="relative">
103
114
  <img
104
115
  v-if="imageSrc"
105
116
  :src="imageSrc"
@@ -107,39 +118,38 @@ const imgFit = computed(() => props.fit ?? 'cover')
107
118
  :alt="alt"
108
119
  :width="normalizeDimension(width)"
109
120
  :height="normalizeDimension(height)"
121
+ :class="{ 'opacity-0': !imageLoaded }"
122
+ @load="onImageLoad"
110
123
  >
111
124
  <Skeleton
112
- v-else
125
+ v-if="!imageLoaded && !loadingError"
113
126
  class="img-web-kit"
114
127
  :width="normalizeDimension(width)"
115
128
  :height="normalizeDimension(height)"
116
129
  />
130
+ </div>
117
131
  </figcaption>
118
132
 
119
- <img
120
- v-else-if="imageSrc"
121
- :src="imageSrc"
122
- v-bind="$attrs"
123
- :alt="alt"
124
- :width="normalizeDimension(width)"
125
- :height="normalizeDimension(height)"
126
- >
127
133
  <div
128
- v-else-if="loadingError"
129
- class="flex-center error-image"
130
- :style="{
131
- width: normalizeDimension(width),
132
- height: normalizeDimension(height),
133
- }"
134
+ v-else
135
+ v-bind="$attrs"
136
+ class="relative"
137
+ :style="{ width: normalizeDimension(width), height: normalizeDimension(height) }"
134
138
  >
135
- <Icon name="broken_image" />
139
+ <img
140
+ v-if="imageSrc"
141
+ :src="imageSrc"
142
+ :alt="alt"
143
+ class="w-100p h-100p"
144
+ :class="{ 'opacity-0': !imageLoaded }"
145
+ :style="{ objectFit: imgFit }"
146
+ @load="onImageLoad"
147
+ >
148
+ <div v-if="loadingError" class="flex-center absolute-fill error-image">
149
+ <Icon name="broken_image" />
150
+ </div>
151
+ <Skeleton v-else-if="!imageLoaded" class="absolute-fill" />
136
152
  </div>
137
- <Skeleton
138
- v-else
139
- class="img-web-kit"
140
- :width="normalizeDimension(width)"
141
- :height="normalizeDimension(height)"
142
- />
143
153
  </template>
144
154
 
145
155
  <style scoped>
@@ -16,6 +16,8 @@ const props = defineProps({
16
16
  placeholder: { type: String, default: '' },
17
17
  height: { type: String, default: '240px' },
18
18
  disabled: { type: Boolean, default: false },
19
+ /** Wrap long lines instead of scrolling horizontally */
20
+ wrap: { type: Boolean, default: false },
19
21
  theme: { type: String as PropType<HighlightTheme>, default: 'dark' },
20
22
  frame: { type: Boolean, default: undefined },
21
23
  outline: { type: Boolean, default: undefined },
@@ -162,7 +164,7 @@ watch(() => props.modelValue, () => {
162
164
  v-if="loaded"
163
165
  ref="editorRef"
164
166
  class="code-editor-grandpa hljs"
165
- :class="isLightTheme ? 'theme-light' : 'theme-dark'"
167
+ :class="[isLightTheme ? 'theme-light' : 'theme-dark', { 'code-wrap': wrap }]"
166
168
  >
167
169
  <div class="editor-content-papa">
168
170
  <pre class="code-display"><code v-html="formattedCode" /></pre>
@@ -284,6 +286,24 @@ watch(() => props.modelValue, () => {
284
286
  color: transparent;
285
287
  -webkit-text-fill-color: transparent;
286
288
  }
289
+
290
+ /* Wrap mode: long lines break onto the next line instead of scrolling.
291
+ The content box must fill the width so wrapping has a boundary to break at. */
292
+ .code-editor-grandpa.code-wrap {
293
+ overflow-x: hidden;
294
+ }
295
+
296
+ .code-editor-grandpa.code-wrap .editor-content-papa {
297
+ width: 100%;
298
+ }
299
+
300
+ .code-editor-grandpa.code-wrap .code-display,
301
+ .code-editor-grandpa.code-wrap .code-input,
302
+ .code-editor-grandpa.code-wrap .code-placeholder {
303
+ white-space: pre-wrap;
304
+ overflow-wrap: anywhere;
305
+ word-break: break-word;
306
+ }
287
307
  </style>
288
308
 
289
309
  <style>
@@ -181,6 +181,11 @@ th {
181
181
  pointer-events: none;
182
182
  }
183
183
 
184
+ /* Linked image: the wrapping <a> behaves like the image block so layout is unchanged */
185
+ .richtext-editor-content .image-figure > a {
186
+ display: block;
187
+ }
188
+
184
189
  .richtext-editor-content .image-figure figcaption {
185
190
  margin-top: 0.5rem;
186
191
  font-size: 0.9em;
@@ -230,7 +230,9 @@ const imageForm = ref({
230
230
  height: '',
231
231
  credit: '',
232
232
  figcaption: false,
233
- align: 'full' // full, start, end
233
+ align: 'full', // full, start, end
234
+ link: '',
235
+ linkNewTab: false
234
236
  })
235
237
  let pendingImageData: {
236
238
  selection: Selection
@@ -708,6 +710,11 @@ function openImageModal(existingImage: HTMLElement | null = null) {
708
710
  const alignSource = existingImage.tagName === 'IMG' ? img : existingImage
709
711
  const align = alignSource?.getAttribute('data-align') || 'full'
710
712
 
713
+ // Read link from the anchor wrapping the image (if any)
714
+ const anchor = img?.closest('a')
715
+ const link = anchor?.getAttribute('href') || ''
716
+ const linkNewTab = anchor?.getAttribute('target') === '_blank'
717
+
711
718
  imageForm.value = {
712
719
  src: img?.src || '',
713
720
  alt: img?.alt || '',
@@ -715,7 +722,9 @@ function openImageModal(existingImage: HTMLElement | null = null) {
715
722
  height: img?.getAttribute('data-height') || img?.style.height || '',
716
723
  credit,
717
724
  figcaption: !!figcaption,
718
- align
725
+ align,
726
+ link,
727
+ linkNewTab
719
728
  }
720
729
  } else {
721
730
  // Reset form for new image
@@ -726,7 +735,9 @@ function openImageModal(existingImage: HTMLElement | null = null) {
726
735
  height: '',
727
736
  credit: '',
728
737
  figcaption: false,
729
- align: 'full'
738
+ align: 'full',
739
+ link: '',
740
+ linkNewTab: false
730
741
  }
731
742
  }
732
743
 
@@ -791,6 +802,21 @@ function submitImage() {
791
802
  return figcaption
792
803
  }
793
804
 
805
+ // Wrap the image in an anchor when a link is provided, otherwise return the bare image
806
+ function createImageContent(): HTMLElement {
807
+ const link = imageForm.value.link?.trim()
808
+ if (!link) { return img }
809
+
810
+ const anchor = doc!.createElement('a')
811
+ anchor.setAttribute('href', link)
812
+ if (imageForm.value.linkNewTab) {
813
+ anchor.setAttribute('target', '_blank')
814
+ anchor.setAttribute('rel', 'noopener noreferrer')
815
+ }
816
+ anchor.appendChild(img)
817
+ return anchor
818
+ }
819
+
794
820
  // Create or update figure/image - always wrap in figure
795
821
  let elementToUpdate: HTMLElement
796
822
  if (existingImage) {
@@ -801,7 +827,7 @@ function submitImage() {
801
827
  if (imageForm.value.credit) {
802
828
  figure.setAttribute('data-credit', imageForm.value.credit)
803
829
  }
804
- figure.appendChild(img)
830
+ figure.appendChild(createImageContent())
805
831
 
806
832
  // Add caption if needed (alt with checkbox checked OR credit exists)
807
833
  if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
@@ -816,7 +842,7 @@ function submitImage() {
816
842
  if (imageForm.value.credit) {
817
843
  existingImage.setAttribute('data-credit', imageForm.value.credit)
818
844
  }
819
- existingImage.appendChild(img)
845
+ existingImage.appendChild(createImageContent())
820
846
 
821
847
  // Add caption if needed (alt with checkbox checked OR credit exists)
822
848
  if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
@@ -831,7 +857,7 @@ function submitImage() {
831
857
  if (imageForm.value.credit) {
832
858
  figure.setAttribute('data-credit', imageForm.value.credit)
833
859
  }
834
- figure.appendChild(img)
860
+ figure.appendChild(createImageContent())
835
861
 
836
862
  // Add caption if needed (alt with checkbox checked OR credit exists)
837
863
  if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
@@ -2931,6 +2957,13 @@ async function initEditor() {
2931
2957
  width: 100%;
2932
2958
  }
2933
2959
 
2960
+ /* Linked images: the wrapping <a> should behave like the image block.
2961
+ In the editor, disable navigation so clicks open the edit modal. */
2962
+ figure.image-figure > a {
2963
+ display: block;
2964
+ pointer-events: none;
2965
+ }
2966
+
2934
2967
  /* Selected state — applied via JS on click, stripped by getCleanHTML() */
2935
2968
  .media-selected {
2936
2969
  outline: 2px solid #007bff !important;
@@ -3352,7 +3385,7 @@ const imgTransform = computed({
3352
3385
  />
3353
3386
  </div>
3354
3387
  <CodeEditor
3355
- v-if="editor.state.isSplitView" v-model="editor.state.content" language="html"
3388
+ v-if="editor.state.isSplitView" v-model="editor.state.content" language="html" wrap
3356
3389
  :height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight"
3357
3390
  @update:modelValue="editor.updateState.content('html')"
3358
3391
  />
@@ -3537,6 +3570,13 @@ const imgTransform = computed({
3537
3570
  help="Full width fills the container. Start/End float the image so text flows alongside."
3538
3571
  />
3539
3572
  <TextInput v-model="imageForm.credit" label="Photo Credit" placeholder="Photographer name (optional)" />
3573
+ <TextInput
3574
+ v-model="imageForm.link" label="Link URL" type="url" placeholder="https://example.com (optional)"
3575
+ help="Make the image clickable by linking it to a URL"
3576
+ />
3577
+ <CheckInput
3578
+ v-if="imageForm.link" v-model="imageForm.linkNewTab" label="Open link in new tab"
3579
+ />
3540
3580
  <CheckInput
3541
3581
  v-model="imageForm.figcaption" label="Show alt text as caption"
3542
3582
  help="Photo credit will always be shown if provided"
@@ -3889,9 +3929,21 @@ background: var(--bgl-richtext-color);
3889
3929
  font-size: var(--richtext-font-size, 16px);
3890
3930
  }
3891
3931
 
3892
- .split-view {
3932
+ /* Split view: editor + code side by side, each taking half the width.
3933
+ Use a compound selector so it overrides the base `.editor-container`
3934
+ `display: flex` regardless of source order or other modifiers. */
3935
+ .editor-container.split-view {
3893
3936
  display: grid;
3894
3937
  grid-template-columns: 1fr 1fr;
3938
+ align-items: stretch;
3939
+ }
3940
+
3941
+ /* Prevent either pane from overflowing its track (the code editor has
3942
+ min-width: 100% internally, which would otherwise blow out the grid). */
3943
+ .editor-container.split-view > .content-area,
3944
+ .editor-container.split-view > :deep(.code-editor-container) {
3945
+ min-width: 0;
3946
+ overflow: hidden;
3895
3947
  }
3896
3948
 
3897
3949
  .editableContent {