nuxt-og-image 6.5.3 → 6.7.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 (108) hide show
  1. package/README.md +2 -0
  2. package/dist/chunks/tw4.cjs +7 -7
  3. package/dist/chunks/tw4.mjs +7 -7
  4. package/dist/chunks/uno.cjs +6 -6
  5. package/dist/chunks/uno.mjs +6 -6
  6. package/dist/cli.cjs +12 -5
  7. package/dist/cli.mjs +10 -3
  8. package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
  9. package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
  10. package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
  11. package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
  12. package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
  13. package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
  14. package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
  15. package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
  16. package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
  17. package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
  18. package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
  19. package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
  20. package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
  21. package/dist/devtools/lib/og-image/keys.ts +8 -0
  22. package/dist/devtools/lib/og-image/og-image.ts +536 -0
  23. package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
  24. package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
  25. package/dist/devtools/lib/og-image/rpc.ts +73 -0
  26. package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
  27. package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
  28. package/dist/devtools/lib/og-image/shared.ts +30 -0
  29. package/dist/devtools/lib/og-image/templates.ts +9 -0
  30. package/dist/devtools/lib/og-image/types.ts +38 -0
  31. package/dist/devtools/lib/og-image/util/logic.ts +21 -0
  32. package/dist/devtools/nuxt.config.ts +6 -0
  33. package/dist/devtools/pages/og-image/debug.vue +32 -0
  34. package/dist/devtools/pages/og-image/docs.vue +3 -0
  35. package/dist/devtools/pages/og-image/index.vue +1682 -0
  36. package/dist/devtools/pages/og-image/templates.vue +150 -0
  37. package/dist/devtools/pages/og-image.vue +184 -0
  38. package/dist/module.cjs +5 -5
  39. package/dist/module.d.cts +5 -3
  40. package/dist/module.d.mts +5 -3
  41. package/dist/module.d.ts +5 -3
  42. package/dist/module.json +1 -1
  43. package/dist/module.mjs +5 -5
  44. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.d.ts +1 -1
  45. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +6 -1
  46. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +13 -6
  47. package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +36 -0
  48. package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +47 -0
  49. package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +1 -1
  50. package/dist/runtime/server/og-image/bindings/font-assets/node.js +14 -7
  51. package/dist/runtime/server/og-image/bindings/takumi/node-dev.js +7 -1
  52. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
  53. package/dist/runtime/server/og-image/fonts.d.ts +4 -0
  54. package/dist/runtime/server/og-image/fonts.js +21 -5
  55. package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
  56. package/dist/runtime/server/og-image/takumi/renderer.js +61 -19
  57. package/dist/runtime/server/util/ssrf.d.ts +10 -0
  58. package/dist/runtime/server/util/ssrf.js +9 -1
  59. package/dist/runtime/types.d.ts +13 -0
  60. package/dist/shared/{nuxt-og-image.DdbTs-xp.mjs → nuxt-og-image.CPSxZhVO.mjs} +122 -496
  61. package/dist/shared/nuxt-og-image.DM30kcDz.mjs +660 -0
  62. package/dist/shared/nuxt-og-image.RI9DgjgV.cjs +689 -0
  63. package/dist/shared/{nuxt-og-image.CfTPCtaS.cjs → nuxt-og-image.yDJBWXBt.cjs} +181 -555
  64. package/package.json +48 -45
  65. package/dist/devtools/200.html +0 -1
  66. package/dist/devtools/404.html +0 -1
  67. package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
  68. package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
  69. package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
  70. package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
  71. package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
  72. package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
  73. package/dist/devtools/_nuxt/BZ_CG9ll.js +0 -30
  74. package/dist/devtools/_nuxt/BdMalyEU.js +0 -4
  75. package/dist/devtools/_nuxt/BhkGaWEe.js +0 -3
  76. package/dist/devtools/_nuxt/Bm4Q-nTW.js +0 -3
  77. package/dist/devtools/_nuxt/BsRKQH5X.js +0 -1
  78. package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
  79. package/dist/devtools/_nuxt/CBcJg2GG.js +0 -2
  80. package/dist/devtools/_nuxt/CDaXKRRu.js +0 -1
  81. package/dist/devtools/_nuxt/CM-t-6ZT.js +0 -1
  82. package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
  83. package/dist/devtools/_nuxt/CXAM78_A.js +0 -1
  84. package/dist/devtools/_nuxt/CYDI7SLa.js +0 -1
  85. package/dist/devtools/_nuxt/CkbDfutK.js +0 -152
  86. package/dist/devtools/_nuxt/Co8Tk_C2.js +0 -1
  87. package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
  88. package/dist/devtools/_nuxt/D49I50CU.js +0 -1
  89. package/dist/devtools/_nuxt/DevtoolsSection.DkT2bJJh.css +0 -1
  90. package/dist/devtools/_nuxt/DevtoolsSnippet.zV_SDojn.css +0 -1
  91. package/dist/devtools/_nuxt/HWyD49jH.js +0 -1
  92. package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
  93. package/dist/devtools/_nuxt/PONEy9N-.js +0 -1
  94. package/dist/devtools/_nuxt/builds/latest.json +0 -1
  95. package/dist/devtools/_nuxt/builds/meta/0f3dbc15-8308-4956-81b9-5507cac7018c.json +0 -1
  96. package/dist/devtools/_nuxt/entry.D7u2asd2.css +0 -2
  97. package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
  98. package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
  99. package/dist/devtools/_nuxt/pO9PmtPW.js +0 -6
  100. package/dist/devtools/_nuxt/pages.0Jk10xlb.css +0 -1
  101. package/dist/devtools/_nuxt/renderer-select.CN5IWe60.css +0 -1
  102. package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
  103. package/dist/devtools/debug/index.html +0 -1
  104. package/dist/devtools/docs/index.html +0 -1
  105. package/dist/devtools/index.html +0 -1
  106. package/dist/devtools/templates/index.html +0 -1
  107. package/dist/shared/nuxt-og-image.CJa2KCie.mjs +0 -189
  108. package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
@@ -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>
@@ -0,0 +1,197 @@
1
+ <script setup lang="ts">
2
+ import { onBeforeUnmount, ref, toValue, watch } from '#imports'
3
+ import { options } from '../../lib/og-image/util/logic'
4
+
5
+ const props = defineProps<{
6
+ src: string
7
+ aspectRatio: number
8
+ maxHeight?: number
9
+ maxWidth?: number
10
+ minHeight?: number
11
+ }>()
12
+
13
+ const emit = defineEmits(['load'])
14
+
15
+ const error = ref<string[] | false>(false)
16
+ const isLoading = ref(false)
17
+ const showSpinner = ref(false)
18
+ const loadStart = ref(0)
19
+ let spinnerTimer: ReturnType<typeof setTimeout> | undefined
20
+
21
+ watch(() => props.src, () => {
22
+ isLoading.value = true
23
+ showSpinner.value = false
24
+ error.value = false
25
+ loadStart.value = Date.now()
26
+ clearTimeout(spinnerTimer)
27
+ spinnerTimer = setTimeout(() => {
28
+ if (isLoading.value)
29
+ showSpinner.value = true
30
+ }, 200)
31
+ }, { immediate: true })
32
+
33
+ function onLoad() {
34
+ isLoading.value = false
35
+ showSpinner.value = false
36
+ clearTimeout(spinnerTimer)
37
+ emit('load', { timeTaken: Date.now() - loadStart.value, sizeKb: '' })
38
+ }
39
+
40
+ onBeforeUnmount(() => clearTimeout(spinnerTimer))
41
+
42
+ function onError() {
43
+ isLoading.value = false
44
+ showSpinner.value = false
45
+ clearTimeout(spinnerTimer)
46
+ $fetch(props.src).catch((err: { data?: { stack?: string[] } }) => {
47
+ error.value = err.data?.stack || ['Failed to load image']
48
+ })
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <div
54
+ class="image-loader"
55
+ :style="{ aspectRatio, minHeight }"
56
+ :class="{
57
+ 'is-valid': !error && !isLoading,
58
+ 'is-error': error,
59
+ }"
60
+ >
61
+ <img
62
+ v-show="!isLoading && !error"
63
+ :src="src"
64
+ :style="{ aspectRatio, maxWidth: `${toValue(options.width) || 1200}px` }"
65
+ @load="onLoad"
66
+ @error="onError"
67
+ >
68
+
69
+ <!-- Loading state -->
70
+ <div v-if="showSpinner" class="loading-state">
71
+ <div class="loading-spinner" />
72
+ </div>
73
+
74
+ <!-- Error state -->
75
+ <div v-if="error" class="error-state">
76
+ <div class="error-header">
77
+ <UIcon name="carbon:warning" class="error-icon" />
78
+ <span class="error-type">
79
+ {{ error.join('\n').includes('satori') ? 'SatoriError' : 'ImageError' }}
80
+ </span>
81
+ </div>
82
+ <p class="error-message">
83
+ {{ error[0]?.replace('Error:', '') }}
84
+ </p>
85
+ <pre v-if="error.length > 1" class="error-stack">{{ error.slice(1).join('\n') }}</pre>
86
+ </div>
87
+ </div>
88
+ </template>
89
+
90
+ <style scoped>
91
+ .image-loader {
92
+ height: 100%;
93
+ margin: 0 auto;
94
+ width: 100%;
95
+ transition: box-shadow 300ms cubic-bezier(0.22, 1, 0.36, 1);
96
+ overflow: hidden;
97
+ background: var(--color-surface-sunken);
98
+ }
99
+
100
+ .image-loader img {
101
+ width: 100%;
102
+ height: 100%;
103
+ object-fit: contain;
104
+ background: var(--color-surface-sunken);
105
+ }
106
+
107
+ .image-loader.is-valid {
108
+ cursor: pointer;
109
+ }
110
+
111
+ .image-loader.is-valid:hover {
112
+ box-shadow: 0 4px 20px oklch(0% 0 0 / 0.1);
113
+ }
114
+
115
+ .dark .image-loader.is-valid:hover {
116
+ box-shadow: 0 4px 20px oklch(0% 0 0 / 0.3);
117
+ }
118
+
119
+ /* Loading state */
120
+ .loading-state {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ height: 100%;
125
+ min-height: 200px;
126
+ }
127
+
128
+ .loading-spinner {
129
+ width: 2rem;
130
+ height: 2rem;
131
+ border: 2px solid var(--color-border);
132
+ border-top-color: var(--seo-green);
133
+ border-radius: 50%;
134
+ animation: spin 0.8s linear infinite;
135
+ }
136
+
137
+ @keyframes spin {
138
+ to {
139
+ transform: rotate(360deg);
140
+ }
141
+ }
142
+
143
+ /* Error state */
144
+ .image-loader.is-error {
145
+ overflow-x: auto;
146
+ background: oklch(97% 0.015 25);
147
+ border: 1px solid oklch(90% 0.05 25);
148
+ }
149
+
150
+ .dark .image-loader.is-error {
151
+ background: oklch(18% 0.02 25);
152
+ border-color: oklch(28% 0.04 25);
153
+ }
154
+
155
+ .error-state {
156
+ padding: 1.25rem;
157
+ }
158
+
159
+ .error-header {
160
+ display: inline-flex;
161
+ align-items: center;
162
+ gap: 0.5rem;
163
+ padding: 0.375rem 0.75rem;
164
+ font-size: 0.75rem;
165
+ font-weight: 600;
166
+ color: oklch(50% 0.15 25);
167
+ background: oklch(92% 0.06 25);
168
+ border-radius: var(--radius-sm);
169
+ margin-bottom: 1rem;
170
+ }
171
+
172
+ .dark .error-header {
173
+ color: oklch(80% 0.1 25);
174
+ background: oklch(28% 0.06 25);
175
+ }
176
+
177
+ .error-icon {
178
+ font-size: 0.875rem;
179
+ }
180
+
181
+ .error-message {
182
+ font-size: 0.9375rem;
183
+ font-weight: 600;
184
+ margin-bottom: 1rem;
185
+ line-height: 1.5;
186
+ color: var(--color-text);
187
+ }
188
+
189
+ .error-stack {
190
+ font-size: 0.75rem;
191
+ font-family: var(--font-mono);
192
+ color: var(--color-text-muted);
193
+ white-space: pre-wrap;
194
+ word-break: break-all;
195
+ line-height: 1.6;
196
+ }
197
+ </style>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ aspectRatio?: number
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div class="linkedin-card">
9
+ <div class="linkedin-image">
10
+ <slot />
11
+ <div class="linkedin-image-overlay" />
12
+ </div>
13
+ <div class="linkedin-content">
14
+ <p class="linkedin-title">
15
+ <slot name="title" />
16
+ </p>
17
+ <p class="linkedin-sitename">
18
+ <slot name="siteName" />
19
+ </p>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <style>
25
+ .linkedin-card {
26
+ width: 100%;
27
+ max-width: 552px;
28
+ margin: 0 auto;
29
+ border-radius: 8px;
30
+ overflow: hidden;
31
+ border: 1px solid #e0e0e0;
32
+ background: oklch(100% 0 0);
33
+ box-shadow: 0 4px 24px oklch(0% 0 0 / 0.1);
34
+ font-family: 'Hubot Sans', -apple-system, BlinkMacSystemFont, sans-serif;
35
+ }
36
+
37
+ .dark .linkedin-card {
38
+ background: #1b1f23;
39
+ border-color: oklch(28% 0.02 285);
40
+ box-shadow: 0 4px 24px oklch(0% 0 0 / 0.2);
41
+ }
42
+
43
+ .linkedin-image {
44
+ aspect-ratio: 2 / 1;
45
+ width: 100%;
46
+ position: relative;
47
+ overflow: hidden;
48
+ background: #f3f2ef;
49
+ }
50
+
51
+ .dark .linkedin-image {
52
+ background: oklch(22% 0.02 285);
53
+ }
54
+
55
+ .linkedin-image img,
56
+ .linkedin-image > *:first-child:not(.linkedin-image-overlay) {
57
+ width: 100%;
58
+ height: 100%;
59
+ object-fit: cover;
60
+ }
61
+
62
+ .linkedin-image-overlay {
63
+ position: absolute;
64
+ inset: 0;
65
+ box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.05);
66
+ }
67
+
68
+ .dark .linkedin-image-overlay {
69
+ box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.05);
70
+ }
71
+
72
+ .linkedin-content {
73
+ padding: 12px 14px;
74
+ }
75
+
76
+ .linkedin-title {
77
+ font-size: 14px;
78
+ font-weight: 600;
79
+ color: oklch(15% 0.02 285);
80
+ margin: 0 0 4px;
81
+ line-height: 1.3;
82
+ display: -webkit-box;
83
+ -webkit-line-clamp: 2;
84
+ -webkit-box-orient: vertical;
85
+ overflow: hidden;
86
+ }
87
+
88
+ .dark .linkedin-title {
89
+ color: oklch(95% 0.01 285);
90
+ }
91
+
92
+ .linkedin-sitename {
93
+ font-size: 12px;
94
+ color: oklch(50% 0.03 285);
95
+ margin: 0;
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ }
100
+ </style>