nuxt-og-image 6.5.3 → 6.6.0

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 (75) hide show
  1. package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
  2. package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
  3. package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
  4. package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
  5. package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
  6. package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
  7. package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
  8. package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
  9. package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
  10. package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
  11. package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
  12. package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
  13. package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
  14. package/dist/devtools/lib/og-image/keys.ts +8 -0
  15. package/dist/devtools/lib/og-image/og-image.ts +536 -0
  16. package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
  17. package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
  18. package/dist/devtools/lib/og-image/rpc.ts +73 -0
  19. package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
  20. package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
  21. package/dist/devtools/lib/og-image/shared.ts +30 -0
  22. package/dist/devtools/lib/og-image/templates.ts +9 -0
  23. package/dist/devtools/lib/og-image/types.ts +38 -0
  24. package/dist/devtools/lib/og-image/util/logic.ts +21 -0
  25. package/dist/devtools/nuxt.config.ts +6 -0
  26. package/dist/devtools/pages/og-image/debug.vue +32 -0
  27. package/dist/devtools/pages/og-image/docs.vue +3 -0
  28. package/dist/devtools/pages/og-image/index.vue +1682 -0
  29. package/dist/devtools/pages/og-image/templates.vue +150 -0
  30. package/dist/devtools/pages/og-image.vue +184 -0
  31. package/dist/module.json +1 -1
  32. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
  33. package/package.json +14 -15
  34. package/dist/devtools/200.html +0 -1
  35. package/dist/devtools/404.html +0 -1
  36. package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
  37. package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
  38. package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
  39. package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
  40. package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
  41. package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
  42. package/dist/devtools/_nuxt/BZ_CG9ll.js +0 -30
  43. package/dist/devtools/_nuxt/BdMalyEU.js +0 -4
  44. package/dist/devtools/_nuxt/BhkGaWEe.js +0 -3
  45. package/dist/devtools/_nuxt/Bm4Q-nTW.js +0 -3
  46. package/dist/devtools/_nuxt/BsRKQH5X.js +0 -1
  47. package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
  48. package/dist/devtools/_nuxt/CBcJg2GG.js +0 -2
  49. package/dist/devtools/_nuxt/CDaXKRRu.js +0 -1
  50. package/dist/devtools/_nuxt/CM-t-6ZT.js +0 -1
  51. package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
  52. package/dist/devtools/_nuxt/CXAM78_A.js +0 -1
  53. package/dist/devtools/_nuxt/CYDI7SLa.js +0 -1
  54. package/dist/devtools/_nuxt/CkbDfutK.js +0 -152
  55. package/dist/devtools/_nuxt/Co8Tk_C2.js +0 -1
  56. package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
  57. package/dist/devtools/_nuxt/D49I50CU.js +0 -1
  58. package/dist/devtools/_nuxt/DevtoolsSection.DkT2bJJh.css +0 -1
  59. package/dist/devtools/_nuxt/DevtoolsSnippet.zV_SDojn.css +0 -1
  60. package/dist/devtools/_nuxt/HWyD49jH.js +0 -1
  61. package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
  62. package/dist/devtools/_nuxt/PONEy9N-.js +0 -1
  63. package/dist/devtools/_nuxt/builds/latest.json +0 -1
  64. package/dist/devtools/_nuxt/builds/meta/0f3dbc15-8308-4956-81b9-5507cac7018c.json +0 -1
  65. package/dist/devtools/_nuxt/entry.D7u2asd2.css +0 -2
  66. package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
  67. package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
  68. package/dist/devtools/_nuxt/pO9PmtPW.js +0 -6
  69. package/dist/devtools/_nuxt/pages.0Jk10xlb.css +0 -1
  70. package/dist/devtools/_nuxt/renderer-select.CN5IWe60.css +0 -1
  71. package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
  72. package/dist/devtools/debug/index.html +0 -1
  73. package/dist/devtools/docs/index.html +0 -1
  74. package/dist/devtools/index.html +0 -1
  75. package/dist/devtools/templates/index.html +0 -1
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ import type { AddComponentResult } from '../../lib/og-image/templates'
3
+ import { computed, ref, watch } from 'vue'
4
+ import { AddComponentDialogPromise } from '../../lib/og-image/templates'
5
+
6
+ function handleClose(_a: unknown, resolve: (value: AddComponentResult | false) => void) {
7
+ resolve(false)
8
+ }
9
+
10
+ const RE_WORD_CHARS_ONLY = /^\w+$/
11
+
12
+ const componentName = ref('')
13
+ const nameError = ref('')
14
+
15
+ function validateName(value: string): string {
16
+ if (!value.trim())
17
+ return 'Name is required'
18
+ if (!RE_WORD_CHARS_ONLY.test(value.trim()))
19
+ return 'Only letters, numbers, and underscores allowed'
20
+ return ''
21
+ }
22
+
23
+ watch(componentName, (v) => {
24
+ nameError.value = v ? validateName(v) : ''
25
+ })
26
+
27
+ const previewName = computed(() => {
28
+ const name = componentName.value.trim()
29
+ if (!name)
30
+ return ''
31
+ return name.charAt(0).toUpperCase() + name.slice(1)
32
+ })
33
+
34
+ function submit(resolve: (value: AddComponentResult | false) => void) {
35
+ const name = componentName.value.trim()
36
+ const error = validateName(name)
37
+ if (error) {
38
+ nameError.value = error
39
+ return
40
+ }
41
+ resolve({ name: name.charAt(0).toUpperCase() + name.slice(1) })
42
+ componentName.value = ''
43
+ nameError.value = ''
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <AddComponentDialogPromise v-slot="{ resolve }">
49
+ <UModal
50
+ :open="true"
51
+ title="Create OG Image Component"
52
+ @update:open="handleClose('a', resolve)"
53
+ @close="handleClose('b', resolve)"
54
+ >
55
+ <template #body>
56
+ <div class="flex flex-col gap-4">
57
+ <UFormField label="Component Name" :error="nameError" hint="Auto-capitalized to PascalCase">
58
+ <UInput
59
+ v-model="componentName"
60
+ placeholder="MyOgImage"
61
+ autofocus
62
+ class="w-full"
63
+ @keydown.enter="submit(resolve)"
64
+ />
65
+ </UFormField>
66
+
67
+ <div v-if="previewName" class="text-xs text-[var(--color-text-subtle)] font-mono bg-[var(--color-surface-sunken)] rounded-lg px-3 py-2">
68
+ defineOgImage('{{ previewName }}')
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <template #footer>
74
+ <div class="flex justify-end gap-3">
75
+ <UButton variant="ghost" color="neutral" @click="resolve(false)">
76
+ Cancel
77
+ </UButton>
78
+ <UButton color="primary" :disabled="!componentName.trim() || !!nameError" @click="submit(resolve)">
79
+ Create
80
+ </UButton>
81
+ </div>
82
+ </template>
83
+ </UModal>
84
+ </AddComponentDialogPromise>
85
+ </template>
@@ -0,0 +1,134 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <div class="bluesky-card">
6
+ <div class="bluesky-image">
7
+ <slot />
8
+ <div class="bluesky-image-overlay" />
9
+ </div>
10
+ <div class="bluesky-content">
11
+ <p class="bluesky-title">
12
+ <slot name="title" />
13
+ </p>
14
+ <p class="bluesky-description">
15
+ <slot name="description" />
16
+ </p>
17
+ <div class="bluesky-domain">
18
+ <svg class="bluesky-globe" viewBox="0 0 16 16" fill="none">
19
+ <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.3" />
20
+ <ellipse cx="8" cy="8" rx="3.5" ry="7" stroke="currentColor" stroke-width="1.3" />
21
+ <line x1="1.5" y1="5.5" x2="14.5" y2="5.5" stroke="currentColor" stroke-width="1.3" />
22
+ <line x1="1.5" y1="10.5" x2="14.5" y2="10.5" stroke="currentColor" stroke-width="1.3" />
23
+ </svg>
24
+ <slot name="siteName" />
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <style>
31
+ .bluesky-card {
32
+ width: 100%;
33
+ max-width: 513px;
34
+ margin: 0 auto;
35
+ border-radius: 12px;
36
+ overflow: hidden;
37
+ border: 1px solid oklch(88% 0.01 260);
38
+ background: oklch(100% 0 0);
39
+ box-shadow: 0 4px 24px oklch(0% 0 0 / 0.08);
40
+ font-family: 'Hubot Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
41
+ }
42
+
43
+ .dark .bluesky-card {
44
+ background: #171d27;
45
+ border-color: rgb(44, 58, 78);
46
+ box-shadow: 0 4px 24px oklch(0% 0 0 / 0.25);
47
+ }
48
+
49
+ .bluesky-image {
50
+ aspect-ratio: 2 / 1;
51
+ width: 100%;
52
+ position: relative;
53
+ overflow: hidden;
54
+ background: oklch(96% 0.01 260);
55
+ }
56
+
57
+ .dark .bluesky-image {
58
+ background: oklch(22% 0.02 260);
59
+ }
60
+
61
+ .bluesky-image img,
62
+ .bluesky-image > *:first-child:not(.bluesky-image-overlay) {
63
+ width: 100%;
64
+ height: 100%;
65
+ object-fit: cover;
66
+ }
67
+
68
+ .bluesky-image-overlay {
69
+ position: absolute;
70
+ inset: 0;
71
+ box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.05);
72
+ pointer-events: none;
73
+ }
74
+
75
+ .dark .bluesky-image-overlay {
76
+ box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.05);
77
+ }
78
+
79
+ .bluesky-content {
80
+ padding: 12px 14px;
81
+ }
82
+
83
+ .bluesky-title {
84
+ font-size: 15px;
85
+ font-weight: 600;
86
+ color: oklch(15% 0.02 260);
87
+ margin: 0 0 2px;
88
+ line-height: 1.35;
89
+ display: -webkit-box;
90
+ -webkit-line-clamp: 2;
91
+ -webkit-box-orient: vertical;
92
+ overflow: hidden;
93
+ }
94
+
95
+ .dark .bluesky-title {
96
+ color: oklch(95% 0.01 260);
97
+ }
98
+
99
+ .bluesky-description {
100
+ font-size: 13px;
101
+ color: oklch(50% 0.02 260);
102
+ margin: 0 0 8px;
103
+ line-height: 1.4;
104
+ display: -webkit-box;
105
+ -webkit-line-clamp: 2;
106
+ -webkit-box-orient: vertical;
107
+ overflow: hidden;
108
+ }
109
+
110
+ .dark .bluesky-description {
111
+ color: oklch(65% 0.02 260);
112
+ }
113
+
114
+ .bluesky-domain {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 4px;
118
+ font-size: 11.3px;
119
+ letter-spacing: 0px;
120
+ color: rgb(171, 184, 201);
121
+ padding-top: 8px;
122
+ border-top: 1px solid oklch(92% 0.01 260);
123
+ }
124
+
125
+ .dark .bluesky-domain {
126
+ border-top-color: oklch(28% 0.02 260);
127
+ }
128
+
129
+ .bluesky-globe {
130
+ width: 13px;
131
+ height: 13px;
132
+ flex-shrink: 0;
133
+ }
134
+ </style>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import type { Ref } from 'vue'
3
+ import type { GlobalDebugResponse } from '../../lib/og-image/types'
4
+ import { computed, ref, watch } from 'vue'
5
+ import { inject } from '#imports'
6
+ import { GlobalDebugKey } from '../../lib/og-image/keys'
7
+ import { CreateOgImageDialogPromise } from '../../lib/og-image/templates'
8
+
9
+ function handleClose(_a: unknown, resolve: (value: string | false) => void) {
10
+ resolve(false)
11
+ }
12
+
13
+ const globalDebug = inject(GlobalDebugKey) as Ref<GlobalDebugResponse | null>
14
+
15
+ const componentDirs = computed(() => globalDebug.value?.runtimeConfig?.componentDirs || [])
16
+ const selected = ref(componentDirs.value[0])
17
+ watch(componentDirs, (dirs) => {
18
+ if (!selected.value && dirs.length > 0)
19
+ selected.value = dirs[0]
20
+ })
21
+ </script>
22
+
23
+ <template>
24
+ <CreateOgImageDialogPromise v-slot="{ resolve, args }">
25
+ <UModal
26
+ :open="true"
27
+ title="Eject Component"
28
+ description="Copy a community template to an OG Image component directory in your project. You can configure directories using componentDirs in nuxt.config."
29
+ @update:open="handleClose('a', resolve)"
30
+ @close="handleClose('b', resolve)"
31
+ >
32
+ <template #body>
33
+ <URadioGroup
34
+ v-model="selected"
35
+ :items="componentDirs.map((dir: string) => ({
36
+ label: `./components/${dir}/${args[0]}.vue`,
37
+ value: dir,
38
+ }))"
39
+ variant="card"
40
+ legend="Choose Output Path"
41
+ />
42
+ </template>
43
+
44
+ <template #footer>
45
+ <div class="flex justify-end gap-3">
46
+ <UButton variant="ghost" color="neutral" @click="resolve(false)">
47
+ Cancel
48
+ </UButton>
49
+ <UButton color="primary" @click="resolve(selected || false)">
50
+ Create Component
51
+ </UButton>
52
+ </div>
53
+ </template>
54
+ </UModal>
55
+ </CreateOgImageDialogPromise>
56
+ </template>
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <div class="discord-card">
6
+ <div class="discord-body">
7
+ <div class="discord-sitename">
8
+ <slot name="siteName" />
9
+ </div>
10
+ <div class="discord-title">
11
+ <slot name="title" />
12
+ </div>
13
+ <div class="discord-description">
14
+ <slot name="description" />
15
+ </div>
16
+ <div class="discord-image">
17
+ <slot />
18
+ <div class="discord-image-overlay" />
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <style>
25
+ .discord-card {
26
+ display: flex;
27
+ max-width: 432px;
28
+ margin: 0 auto;
29
+ border-radius: 8px;
30
+ border: 1px solid #e3e5e8;
31
+ border-left: 4px solid #e3e5e8;
32
+ background: #f2f3f5;
33
+ overflow: hidden;
34
+ font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
35
+ }
36
+
37
+ .dark .discord-card {
38
+ background: #2b2d31;
39
+ border-color: #232428;
40
+ border-left-color: #1e1f22;
41
+ }
42
+
43
+ .discord-body {
44
+ flex: 1;
45
+ min-width: 0;
46
+ padding: 8px 16px 16px 12px;
47
+ }
48
+
49
+ .discord-sitename {
50
+ font-size: 12px;
51
+ font-weight: 400;
52
+ color: #4e5058;
53
+ margin-bottom: 2px;
54
+ line-height: 16px;
55
+ }
56
+
57
+ .dark .discord-sitename {
58
+ color: #b5bac1;
59
+ }
60
+
61
+ .discord-title {
62
+ font-size: 16px;
63
+ font-weight: 700;
64
+ color: #006ce7;
65
+ cursor: pointer;
66
+ margin-bottom: 4px;
67
+ line-height: 22px;
68
+ display: -webkit-box;
69
+ -webkit-line-clamp: 2;
70
+ -webkit-box-orient: vertical;
71
+ overflow: hidden;
72
+ }
73
+
74
+ .dark .discord-title {
75
+ color: #00a8fc;
76
+ }
77
+
78
+ .discord-title:hover {
79
+ text-decoration: underline;
80
+ }
81
+
82
+ .discord-description {
83
+ font-size: 14px;
84
+ color: #313338;
85
+ line-height: 18px;
86
+ margin-bottom: 16px;
87
+ display: -webkit-box;
88
+ -webkit-line-clamp: 3;
89
+ -webkit-box-orient: vertical;
90
+ overflow: hidden;
91
+ white-space: pre-wrap;
92
+ }
93
+
94
+ .dark .discord-description {
95
+ color: #dbdee1;
96
+ }
97
+
98
+ .discord-image {
99
+ border-radius: 4px;
100
+ overflow: hidden;
101
+ position: relative;
102
+ max-width: 400px;
103
+ max-height: 300px;
104
+ }
105
+
106
+ .discord-image img,
107
+ .discord-image > *:first-child:not(.discord-image-overlay) {
108
+ max-width: 100%;
109
+ max-height: 300px;
110
+ border-radius: 4px;
111
+ display: block;
112
+ }
113
+
114
+ .discord-image-overlay {
115
+ position: absolute;
116
+ inset: 0;
117
+ box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.04);
118
+ border-radius: 4px;
119
+ pointer-events: none;
120
+ }
121
+
122
+ .dark .discord-image-overlay {
123
+ box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.06);
124
+ }
125
+ </style>
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ aspectRatio?: number
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div class="facebook-card">
9
+ <div class="facebook-image">
10
+ <slot />
11
+ <div class="facebook-image-overlay" />
12
+ </div>
13
+ <div class="facebook-content">
14
+ <p class="facebook-sitename">
15
+ <slot name="siteName" />
16
+ </p>
17
+ <p class="facebook-title">
18
+ <slot name="title" />
19
+ </p>
20
+ <p class="facebook-description">
21
+ <slot name="description" />
22
+ </p>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <style>
28
+ .facebook-card {
29
+ max-width: 500px;
30
+ margin: 0 auto;
31
+ border-radius: 0;
32
+ overflow: hidden;
33
+ border: 1px solid #dadde1;
34
+ background: #f0f2f5;
35
+ box-shadow: 0 4px 24px oklch(0% 0 0 / 0.1);
36
+ font-family: 'Hubot Sans', Helvetica, Arial, sans-serif;
37
+ }
38
+
39
+ .dark .facebook-card {
40
+ background: #242526;
41
+ border-color: oklch(28% 0.02 285);
42
+ box-shadow: 0 4px 24px oklch(0% 0 0 / 0.2);
43
+ }
44
+
45
+ .facebook-image {
46
+ aspect-ratio: 2 / 1;
47
+ width: 100%;
48
+ position: relative;
49
+ overflow: hidden;
50
+ background: #e4e6e9;
51
+ }
52
+
53
+ .dark .facebook-image {
54
+ background: oklch(25% 0.02 285);
55
+ }
56
+
57
+ .facebook-image img,
58
+ .facebook-image > *:first-child:not(.facebook-image-overlay) {
59
+ width: 100%;
60
+ height: 100%;
61
+ object-fit: cover;
62
+ }
63
+
64
+ .facebook-image-overlay {
65
+ position: absolute;
66
+ inset: 0;
67
+ box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.05);
68
+ }
69
+
70
+ .dark .facebook-image-overlay {
71
+ box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.05);
72
+ }
73
+
74
+ .facebook-content {
75
+ padding: 12px 14px;
76
+ background: #f0f2f5;
77
+ }
78
+
79
+ .dark .facebook-content {
80
+ background: #242526;
81
+ }
82
+
83
+ .facebook-sitename {
84
+ font-size: 12px;
85
+ color: #606770;
86
+ text-transform: uppercase;
87
+ letter-spacing: 0.03em;
88
+ margin: 0 0 4px;
89
+ white-space: nowrap;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ }
93
+
94
+ .dark .facebook-sitename {
95
+ color: oklch(60% 0.02 285);
96
+ }
97
+
98
+ .facebook-title {
99
+ font-size: 16px;
100
+ font-weight: 600;
101
+ color: #1c1e21;
102
+ margin: 0 0 4px;
103
+ line-height: 1.3;
104
+ display: -webkit-box;
105
+ -webkit-line-clamp: 2;
106
+ -webkit-box-orient: vertical;
107
+ overflow: hidden;
108
+ }
109
+
110
+ .dark .facebook-title {
111
+ color: oklch(95% 0.01 285);
112
+ }
113
+
114
+ .facebook-description {
115
+ font-size: 14px;
116
+ color: #606770;
117
+ margin: 0;
118
+ line-height: 1.4;
119
+ display: -webkit-box;
120
+ -webkit-line-clamp: 2;
121
+ -webkit-box-orient: vertical;
122
+ overflow: hidden;
123
+ }
124
+
125
+ .dark .facebook-description {
126
+ color: oklch(70% 0.02 285);
127
+ }
128
+ </style>
@@ -0,0 +1,93 @@
1
+ <script setup lang="ts">
2
+ import { useDebounceFn, useResizeObserver } from '@vueuse/core'
3
+ import { withQuery } from 'ufo'
4
+ import { onMounted, ref, toValue, watch } from '#imports'
5
+ import { options } from '../../lib/og-image/util/logic'
6
+
7
+ const props = defineProps<{
8
+ src: string
9
+ aspectRatio: number
10
+ }>()
11
+ const emit = defineEmits(['load'])
12
+
13
+ const src = ref()
14
+ const iframe = ref<HTMLIFrameElement>()
15
+ const container = ref<HTMLElement>()
16
+
17
+ const setSource = useDebounceFn(() => {
18
+ const frame = iframe.value
19
+ if (!frame || !src.value || !import.meta.client)
20
+ return
21
+ // eslint-disable-next-line harlanzw/nuxt-no-unsafe-date
22
+ const now = Date.now()
23
+ frame.src = ''
24
+
25
+ // Calculate scale based on container width vs content width
26
+ const contentWidth = toValue(options.value.width) || 1200
27
+ const containerWidth = container.value?.offsetWidth
28
+ // Use scale 1 if container hasn't rendered yet (offsetWidth is 0)
29
+ const scale = containerWidth ? Math.min(1, containerWidth / contentWidth) : 1
30
+
31
+ frame.style.opacity = '0'
32
+ frame.onload = () => {
33
+ frame.style.opacity = '1'
34
+ if (import.meta.client)
35
+ emit('load', { timeTaken: Date.now() - now })
36
+ }
37
+ frame.src = withQuery(src.value, { scale })
38
+ }, 200)
39
+
40
+ onMounted(() => {
41
+ watch(() => props.src, (val) => {
42
+ if (src.value !== val) {
43
+ if (iframe.value) {
44
+ src.value = val
45
+ setSource()
46
+ }
47
+ }
48
+ }, {
49
+ immediate: true,
50
+ })
51
+
52
+ watch(iframe, () => {
53
+ setSource()
54
+ })
55
+ })
56
+
57
+ // Recalculate scale when container resizes or configured width changes
58
+ useResizeObserver(container, () => {
59
+ if (src.value)
60
+ setSource()
61
+ })
62
+ watch(() => toValue(options.value.width), () => {
63
+ if (src.value)
64
+ setSource()
65
+ }, { flush: 'post' })
66
+ </script>
67
+
68
+ <template>
69
+ <div ref="container" class="iframe-loader" :style="{ aspectRatio, maxWidth: `${toValue(options.width) || 1200}px` }">
70
+ <iframe ref="iframe" />
71
+ </div>
72
+ </template>
73
+
74
+ <style scoped>
75
+ .iframe-loader {
76
+ position: relative;
77
+ width: 100%;
78
+ margin: 0 auto;
79
+ border-radius: var(--radius-md);
80
+ overflow: hidden;
81
+ background: var(--color-surface-sunken);
82
+ }
83
+
84
+ .iframe-loader iframe {
85
+ position: absolute;
86
+ top: 0;
87
+ left: 0;
88
+ width: 100%;
89
+ height: 100%;
90
+ border: none;
91
+ transition: opacity 0.3s ease;
92
+ }
93
+ </style>