@framer-framer/vue 3.2.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.
- package/README.md +142 -0
- package/dist/index.d.mts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +192 -0
- package/dist/index.mjs +164 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @framer-framer/vue
|
|
2
|
+
|
|
3
|
+
Vue 3 embed component for [framer-framer](https://github.com/piroz/framer-framer). Resolve any URL to embed HTML with a simple component or composable.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @framer-framer/vue framer-framer vue
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### `<Embed>` Component
|
|
14
|
+
|
|
15
|
+
```vue
|
|
16
|
+
<script setup>
|
|
17
|
+
import { Embed } from '@framer-framer/vue';
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<Embed
|
|
22
|
+
url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
23
|
+
:max-width="640"
|
|
24
|
+
@load="(result) => console.log('Loaded:', result.provider)"
|
|
25
|
+
@error="(err) => console.error(err)"
|
|
26
|
+
/>
|
|
27
|
+
</template>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Custom Slots
|
|
31
|
+
|
|
32
|
+
```vue
|
|
33
|
+
<template>
|
|
34
|
+
<Embed url="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
|
|
35
|
+
<!-- Custom loading skeleton -->
|
|
36
|
+
<template #loading>
|
|
37
|
+
<div class="skeleton" />
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<!-- Custom error display -->
|
|
41
|
+
<template #error="{ error }">
|
|
42
|
+
<div class="error">Failed: {{ error.message }}</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<!-- Custom embed rendering -->
|
|
46
|
+
<template #default="{ result }">
|
|
47
|
+
<div v-html="result.html" />
|
|
48
|
+
<p>{{ result.title }} — {{ result.provider }}</p>
|
|
49
|
+
</template>
|
|
50
|
+
</Embed>
|
|
51
|
+
</template>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `useEmbed()` Composable
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<script setup>
|
|
58
|
+
import { useEmbed } from '@framer-framer/vue';
|
|
59
|
+
|
|
60
|
+
const { result, loading, error } = useEmbed(
|
|
61
|
+
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
62
|
+
{ maxWidth: 640 }
|
|
63
|
+
);
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div v-if="loading">Loading...</div>
|
|
68
|
+
<div v-else-if="error">Error: {{ error.message }}</div>
|
|
69
|
+
<div v-else-if="result" v-html="result.html" />
|
|
70
|
+
</template>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The composable also accepts a reactive ref or getter as the URL:
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<script setup>
|
|
77
|
+
import { ref } from 'vue';
|
|
78
|
+
import { useEmbed } from '@framer-framer/vue';
|
|
79
|
+
|
|
80
|
+
const url = ref('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
|
81
|
+
const { result, loading, error } = useEmbed(url);
|
|
82
|
+
</script>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Props
|
|
86
|
+
|
|
87
|
+
| Prop | Type | Required | Description |
|
|
88
|
+
|------|------|----------|-------------|
|
|
89
|
+
| `url` | `string` | Yes | URL to resolve |
|
|
90
|
+
| `maxWidth` | `number` | No | Max embed width |
|
|
91
|
+
| `maxHeight` | `number` | No | Max embed height |
|
|
92
|
+
| `options` | `EmbedOptions` | No | Options passed to framer-framer's `embed()` |
|
|
93
|
+
|
|
94
|
+
## Events
|
|
95
|
+
|
|
96
|
+
| Event | Payload | Description |
|
|
97
|
+
|-------|---------|-------------|
|
|
98
|
+
| `@load` | `EmbedResult` | Emitted when the embed is resolved |
|
|
99
|
+
| `@error` | `Error` | Emitted when resolution fails |
|
|
100
|
+
|
|
101
|
+
## Slots
|
|
102
|
+
|
|
103
|
+
| Slot | Props | Description |
|
|
104
|
+
|------|-------|-------------|
|
|
105
|
+
| `default` | `{ result: EmbedResult }` | Custom embed rendering |
|
|
106
|
+
| `loading` | — | Custom loading skeleton |
|
|
107
|
+
| `error` | `{ error: Error }` | Custom error display |
|
|
108
|
+
|
|
109
|
+
## Nuxt Integration
|
|
110
|
+
|
|
111
|
+
`@framer-framer/vue` works with Nuxt 3 out of the box. Since `embed()` calls fetch external oEmbed APIs, use the component in client-only mode to avoid SSR issues:
|
|
112
|
+
|
|
113
|
+
```vue
|
|
114
|
+
<template>
|
|
115
|
+
<ClientOnly>
|
|
116
|
+
<Embed url="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />
|
|
117
|
+
<template #fallback>
|
|
118
|
+
<div>Loading embed...</div>
|
|
119
|
+
</template>
|
|
120
|
+
</ClientOnly>
|
|
121
|
+
</template>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For server-side resolution, use the `useEmbed()` composable inside `onMounted`:
|
|
125
|
+
|
|
126
|
+
```vue
|
|
127
|
+
<script setup>
|
|
128
|
+
import { ref, onMounted } from 'vue';
|
|
129
|
+
import { embed } from 'framer-framer';
|
|
130
|
+
|
|
131
|
+
const html = ref('');
|
|
132
|
+
|
|
133
|
+
onMounted(async () => {
|
|
134
|
+
const result = await embed('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
|
135
|
+
html.value = result.html;
|
|
136
|
+
});
|
|
137
|
+
</script>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
import { PropType, SlotsType, Ref, MaybeRefOrGetter } from 'vue';
|
|
3
|
+
import { EmbedOptions, EmbedResult } from 'framer-framer';
|
|
4
|
+
|
|
5
|
+
/** Theme mode for Embed components */
|
|
6
|
+
type Theme = "light" | "dark" | "auto";
|
|
7
|
+
|
|
8
|
+
declare const Embed: vue.DefineComponent<vue.ExtractPropTypes<{
|
|
9
|
+
url: {
|
|
10
|
+
type: StringConstructor;
|
|
11
|
+
required: true;
|
|
12
|
+
};
|
|
13
|
+
maxWidth: {
|
|
14
|
+
type: NumberConstructor;
|
|
15
|
+
default: undefined;
|
|
16
|
+
};
|
|
17
|
+
maxHeight: {
|
|
18
|
+
type: NumberConstructor;
|
|
19
|
+
default: undefined;
|
|
20
|
+
};
|
|
21
|
+
options: {
|
|
22
|
+
type: PropType<EmbedOptions>;
|
|
23
|
+
default: undefined;
|
|
24
|
+
};
|
|
25
|
+
theme: {
|
|
26
|
+
type: PropType<Theme>;
|
|
27
|
+
default: string;
|
|
28
|
+
};
|
|
29
|
+
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}> | null, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
|
|
32
|
+
load: (_result: EmbedResult) => true;
|
|
33
|
+
error: (_error: Error) => true;
|
|
34
|
+
}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
|
|
35
|
+
url: {
|
|
36
|
+
type: StringConstructor;
|
|
37
|
+
required: true;
|
|
38
|
+
};
|
|
39
|
+
maxWidth: {
|
|
40
|
+
type: NumberConstructor;
|
|
41
|
+
default: undefined;
|
|
42
|
+
};
|
|
43
|
+
maxHeight: {
|
|
44
|
+
type: NumberConstructor;
|
|
45
|
+
default: undefined;
|
|
46
|
+
};
|
|
47
|
+
options: {
|
|
48
|
+
type: PropType<EmbedOptions>;
|
|
49
|
+
default: undefined;
|
|
50
|
+
};
|
|
51
|
+
theme: {
|
|
52
|
+
type: PropType<Theme>;
|
|
53
|
+
default: string;
|
|
54
|
+
};
|
|
55
|
+
}>> & Readonly<{
|
|
56
|
+
onLoad?: ((_result: EmbedResult) => any) | undefined;
|
|
57
|
+
onError?: ((_error: Error) => any) | undefined;
|
|
58
|
+
}>, {
|
|
59
|
+
maxWidth: number;
|
|
60
|
+
maxHeight: number;
|
|
61
|
+
options: EmbedOptions;
|
|
62
|
+
theme: Theme;
|
|
63
|
+
}, SlotsType<{
|
|
64
|
+
default?: {
|
|
65
|
+
result: EmbedResult;
|
|
66
|
+
};
|
|
67
|
+
loading?: Record<string, never>;
|
|
68
|
+
error?: {
|
|
69
|
+
error: Error;
|
|
70
|
+
};
|
|
71
|
+
}>, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
|
|
72
|
+
|
|
73
|
+
/** Props for the Embed component */
|
|
74
|
+
interface EmbedProps {
|
|
75
|
+
/** URL to resolve */
|
|
76
|
+
url: string;
|
|
77
|
+
/** Max embed width */
|
|
78
|
+
maxWidth?: number;
|
|
79
|
+
/** Max embed height */
|
|
80
|
+
maxHeight?: number;
|
|
81
|
+
/** Options passed to framer-framer's embed() */
|
|
82
|
+
options?: EmbedOptions;
|
|
83
|
+
/** Theme mode: 'light', 'dark', or 'auto' (default: 'auto') */
|
|
84
|
+
theme?: Theme;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface UseEmbedOptions {
|
|
88
|
+
/** Max embed width */
|
|
89
|
+
maxWidth?: number;
|
|
90
|
+
/** Max embed height */
|
|
91
|
+
maxHeight?: number;
|
|
92
|
+
/** Additional options passed to framer-framer's embed() */
|
|
93
|
+
embedOptions?: EmbedOptions;
|
|
94
|
+
}
|
|
95
|
+
interface UseEmbedReturn {
|
|
96
|
+
result: Ref<EmbedResult | null>;
|
|
97
|
+
loading: Ref<boolean>;
|
|
98
|
+
error: Ref<Error | null>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Composable that resolves a URL to embed data using framer-framer.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const { result, loading, error } = useEmbed('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
declare function useEmbed(url: MaybeRefOrGetter<string>, options?: UseEmbedOptions): UseEmbedReturn;
|
|
109
|
+
|
|
110
|
+
export { Embed, type EmbedProps, type Theme, type UseEmbedOptions, type UseEmbedReturn, useEmbed };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
import { PropType, SlotsType, Ref, MaybeRefOrGetter } from 'vue';
|
|
3
|
+
import { EmbedOptions, EmbedResult } from 'framer-framer';
|
|
4
|
+
|
|
5
|
+
/** Theme mode for Embed components */
|
|
6
|
+
type Theme = "light" | "dark" | "auto";
|
|
7
|
+
|
|
8
|
+
declare const Embed: vue.DefineComponent<vue.ExtractPropTypes<{
|
|
9
|
+
url: {
|
|
10
|
+
type: StringConstructor;
|
|
11
|
+
required: true;
|
|
12
|
+
};
|
|
13
|
+
maxWidth: {
|
|
14
|
+
type: NumberConstructor;
|
|
15
|
+
default: undefined;
|
|
16
|
+
};
|
|
17
|
+
maxHeight: {
|
|
18
|
+
type: NumberConstructor;
|
|
19
|
+
default: undefined;
|
|
20
|
+
};
|
|
21
|
+
options: {
|
|
22
|
+
type: PropType<EmbedOptions>;
|
|
23
|
+
default: undefined;
|
|
24
|
+
};
|
|
25
|
+
theme: {
|
|
26
|
+
type: PropType<Theme>;
|
|
27
|
+
default: string;
|
|
28
|
+
};
|
|
29
|
+
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}> | null, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
|
|
32
|
+
load: (_result: EmbedResult) => true;
|
|
33
|
+
error: (_error: Error) => true;
|
|
34
|
+
}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
|
|
35
|
+
url: {
|
|
36
|
+
type: StringConstructor;
|
|
37
|
+
required: true;
|
|
38
|
+
};
|
|
39
|
+
maxWidth: {
|
|
40
|
+
type: NumberConstructor;
|
|
41
|
+
default: undefined;
|
|
42
|
+
};
|
|
43
|
+
maxHeight: {
|
|
44
|
+
type: NumberConstructor;
|
|
45
|
+
default: undefined;
|
|
46
|
+
};
|
|
47
|
+
options: {
|
|
48
|
+
type: PropType<EmbedOptions>;
|
|
49
|
+
default: undefined;
|
|
50
|
+
};
|
|
51
|
+
theme: {
|
|
52
|
+
type: PropType<Theme>;
|
|
53
|
+
default: string;
|
|
54
|
+
};
|
|
55
|
+
}>> & Readonly<{
|
|
56
|
+
onLoad?: ((_result: EmbedResult) => any) | undefined;
|
|
57
|
+
onError?: ((_error: Error) => any) | undefined;
|
|
58
|
+
}>, {
|
|
59
|
+
maxWidth: number;
|
|
60
|
+
maxHeight: number;
|
|
61
|
+
options: EmbedOptions;
|
|
62
|
+
theme: Theme;
|
|
63
|
+
}, SlotsType<{
|
|
64
|
+
default?: {
|
|
65
|
+
result: EmbedResult;
|
|
66
|
+
};
|
|
67
|
+
loading?: Record<string, never>;
|
|
68
|
+
error?: {
|
|
69
|
+
error: Error;
|
|
70
|
+
};
|
|
71
|
+
}>, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
|
|
72
|
+
|
|
73
|
+
/** Props for the Embed component */
|
|
74
|
+
interface EmbedProps {
|
|
75
|
+
/** URL to resolve */
|
|
76
|
+
url: string;
|
|
77
|
+
/** Max embed width */
|
|
78
|
+
maxWidth?: number;
|
|
79
|
+
/** Max embed height */
|
|
80
|
+
maxHeight?: number;
|
|
81
|
+
/** Options passed to framer-framer's embed() */
|
|
82
|
+
options?: EmbedOptions;
|
|
83
|
+
/** Theme mode: 'light', 'dark', or 'auto' (default: 'auto') */
|
|
84
|
+
theme?: Theme;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface UseEmbedOptions {
|
|
88
|
+
/** Max embed width */
|
|
89
|
+
maxWidth?: number;
|
|
90
|
+
/** Max embed height */
|
|
91
|
+
maxHeight?: number;
|
|
92
|
+
/** Additional options passed to framer-framer's embed() */
|
|
93
|
+
embedOptions?: EmbedOptions;
|
|
94
|
+
}
|
|
95
|
+
interface UseEmbedReturn {
|
|
96
|
+
result: Ref<EmbedResult | null>;
|
|
97
|
+
loading: Ref<boolean>;
|
|
98
|
+
error: Ref<Error | null>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Composable that resolves a URL to embed data using framer-framer.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const { result, loading, error } = useEmbed('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
declare function useEmbed(url: MaybeRefOrGetter<string>, options?: UseEmbedOptions): UseEmbedReturn;
|
|
109
|
+
|
|
110
|
+
export { Embed, type EmbedProps, type Theme, type UseEmbedOptions, type UseEmbedReturn, useEmbed };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Embed: () => Embed,
|
|
24
|
+
useEmbed: () => useEmbed
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/Embed.ts
|
|
29
|
+
var import_vue2 = require("vue");
|
|
30
|
+
|
|
31
|
+
// src/theme.ts
|
|
32
|
+
var themeStyleId = "framer-framer-theme";
|
|
33
|
+
var themeCSS = `
|
|
34
|
+
[data-framer-theme="light"] {
|
|
35
|
+
--framer-bg: #ffffff;
|
|
36
|
+
--framer-border: #e5e7eb;
|
|
37
|
+
--framer-skeleton-bg: #e5e7eb;
|
|
38
|
+
--framer-error-bg: #fef2f2;
|
|
39
|
+
--framer-error-border: #fca5a5;
|
|
40
|
+
--framer-error-text: #991b1b;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[data-framer-theme="dark"] {
|
|
44
|
+
--framer-bg: #1f2937;
|
|
45
|
+
--framer-border: #374151;
|
|
46
|
+
--framer-skeleton-bg: #374151;
|
|
47
|
+
--framer-error-bg: #451a1a;
|
|
48
|
+
--framer-error-border: #991b1b;
|
|
49
|
+
--framer-error-text: #fca5a5;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[data-framer-theme="auto"] {
|
|
53
|
+
--framer-bg: #ffffff;
|
|
54
|
+
--framer-border: #e5e7eb;
|
|
55
|
+
--framer-skeleton-bg: #e5e7eb;
|
|
56
|
+
--framer-error-bg: #fef2f2;
|
|
57
|
+
--framer-error-border: #fca5a5;
|
|
58
|
+
--framer-error-text: #991b1b;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@media (prefers-color-scheme: dark) {
|
|
62
|
+
[data-framer-theme="auto"] {
|
|
63
|
+
--framer-bg: #1f2937;
|
|
64
|
+
--framer-border: #374151;
|
|
65
|
+
--framer-skeleton-bg: #374151;
|
|
66
|
+
--framer-error-bg: #451a1a;
|
|
67
|
+
--framer-error-border: #991b1b;
|
|
68
|
+
--framer-error-text: #fca5a5;
|
|
69
|
+
}
|
|
70
|
+
}`;
|
|
71
|
+
|
|
72
|
+
// src/useEmbed.ts
|
|
73
|
+
var import_framer_framer = require("framer-framer");
|
|
74
|
+
var import_vue = require("vue");
|
|
75
|
+
function useEmbed(url, options) {
|
|
76
|
+
const result = (0, import_vue.ref)(null);
|
|
77
|
+
const loading = (0, import_vue.ref)(true);
|
|
78
|
+
const error = (0, import_vue.ref)(null);
|
|
79
|
+
async function resolve(targetUrl) {
|
|
80
|
+
if (!targetUrl) {
|
|
81
|
+
result.value = null;
|
|
82
|
+
loading.value = false;
|
|
83
|
+
error.value = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
loading.value = true;
|
|
87
|
+
error.value = null;
|
|
88
|
+
try {
|
|
89
|
+
const embedResult = await (0, import_framer_framer.embed)(targetUrl, {
|
|
90
|
+
...options?.embedOptions,
|
|
91
|
+
maxWidth: options?.maxWidth,
|
|
92
|
+
maxHeight: options?.maxHeight
|
|
93
|
+
});
|
|
94
|
+
result.value = embedResult;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
97
|
+
result.value = null;
|
|
98
|
+
} finally {
|
|
99
|
+
loading.value = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
(0, import_vue.watch)(
|
|
103
|
+
() => (0, import_vue.toValue)(url),
|
|
104
|
+
(newUrl) => {
|
|
105
|
+
resolve(newUrl);
|
|
106
|
+
},
|
|
107
|
+
{ immediate: true }
|
|
108
|
+
);
|
|
109
|
+
return { result, loading, error };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/Embed.ts
|
|
113
|
+
var Embed = (0, import_vue2.defineComponent)({
|
|
114
|
+
name: "Embed",
|
|
115
|
+
props: {
|
|
116
|
+
url: {
|
|
117
|
+
type: String,
|
|
118
|
+
required: true
|
|
119
|
+
},
|
|
120
|
+
maxWidth: {
|
|
121
|
+
type: Number,
|
|
122
|
+
default: void 0
|
|
123
|
+
},
|
|
124
|
+
maxHeight: {
|
|
125
|
+
type: Number,
|
|
126
|
+
default: void 0
|
|
127
|
+
},
|
|
128
|
+
options: {
|
|
129
|
+
type: Object,
|
|
130
|
+
default: void 0
|
|
131
|
+
},
|
|
132
|
+
theme: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: "auto"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
emits: {
|
|
138
|
+
load: (_result) => true,
|
|
139
|
+
error: (_error) => true
|
|
140
|
+
},
|
|
141
|
+
slots: Object,
|
|
142
|
+
setup(props, { emit, slots }) {
|
|
143
|
+
const { result, loading, error } = useEmbed(() => props.url, {
|
|
144
|
+
maxWidth: props.maxWidth,
|
|
145
|
+
maxHeight: props.maxHeight,
|
|
146
|
+
embedOptions: props.options
|
|
147
|
+
});
|
|
148
|
+
let lastEmittedUrl = null;
|
|
149
|
+
let lastEmittedError = null;
|
|
150
|
+
return () => {
|
|
151
|
+
const themeStyle = (0, import_vue2.h)("style", { id: themeStyleId }, themeCSS);
|
|
152
|
+
const themeAttr = props.theme ?? "auto";
|
|
153
|
+
if (loading.value) {
|
|
154
|
+
const loadingContent = slots.loading ? slots.loading() : (0, import_vue2.h)("div", { class: "framer-framer-loading" });
|
|
155
|
+
return (0, import_vue2.h)("div", { "data-framer-theme": themeAttr }, [themeStyle, loadingContent]);
|
|
156
|
+
}
|
|
157
|
+
if (error.value) {
|
|
158
|
+
if (lastEmittedError !== error.value) {
|
|
159
|
+
lastEmittedError = error.value;
|
|
160
|
+
emit("error", error.value);
|
|
161
|
+
}
|
|
162
|
+
const errorContent = slots.error ? slots.error({ error: error.value }) : (0, import_vue2.h)("div", { class: "framer-framer-error" }, error.value.message);
|
|
163
|
+
return (0, import_vue2.h)("div", { "data-framer-theme": themeAttr }, [themeStyle, errorContent]);
|
|
164
|
+
}
|
|
165
|
+
if (result.value) {
|
|
166
|
+
if (lastEmittedUrl !== result.value.url) {
|
|
167
|
+
lastEmittedUrl = result.value.url;
|
|
168
|
+
emit("load", result.value);
|
|
169
|
+
}
|
|
170
|
+
if (slots.default) {
|
|
171
|
+
return (0, import_vue2.h)("div", { "data-framer-theme": themeAttr }, [
|
|
172
|
+
themeStyle,
|
|
173
|
+
slots.default({ result: result.value })
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
return (0, import_vue2.h)("div", { "data-framer-theme": themeAttr }, [
|
|
177
|
+
themeStyle,
|
|
178
|
+
(0, import_vue2.h)("div", {
|
|
179
|
+
class: "framer-framer-embed",
|
|
180
|
+
innerHTML: result.value.html
|
|
181
|
+
})
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
189
|
+
0 && (module.exports = {
|
|
190
|
+
Embed,
|
|
191
|
+
useEmbed
|
|
192
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/Embed.ts
|
|
2
|
+
import { defineComponent, h } from "vue";
|
|
3
|
+
|
|
4
|
+
// src/theme.ts
|
|
5
|
+
var themeStyleId = "framer-framer-theme";
|
|
6
|
+
var themeCSS = `
|
|
7
|
+
[data-framer-theme="light"] {
|
|
8
|
+
--framer-bg: #ffffff;
|
|
9
|
+
--framer-border: #e5e7eb;
|
|
10
|
+
--framer-skeleton-bg: #e5e7eb;
|
|
11
|
+
--framer-error-bg: #fef2f2;
|
|
12
|
+
--framer-error-border: #fca5a5;
|
|
13
|
+
--framer-error-text: #991b1b;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[data-framer-theme="dark"] {
|
|
17
|
+
--framer-bg: #1f2937;
|
|
18
|
+
--framer-border: #374151;
|
|
19
|
+
--framer-skeleton-bg: #374151;
|
|
20
|
+
--framer-error-bg: #451a1a;
|
|
21
|
+
--framer-error-border: #991b1b;
|
|
22
|
+
--framer-error-text: #fca5a5;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-framer-theme="auto"] {
|
|
26
|
+
--framer-bg: #ffffff;
|
|
27
|
+
--framer-border: #e5e7eb;
|
|
28
|
+
--framer-skeleton-bg: #e5e7eb;
|
|
29
|
+
--framer-error-bg: #fef2f2;
|
|
30
|
+
--framer-error-border: #fca5a5;
|
|
31
|
+
--framer-error-text: #991b1b;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
[data-framer-theme="auto"] {
|
|
36
|
+
--framer-bg: #1f2937;
|
|
37
|
+
--framer-border: #374151;
|
|
38
|
+
--framer-skeleton-bg: #374151;
|
|
39
|
+
--framer-error-bg: #451a1a;
|
|
40
|
+
--framer-error-border: #991b1b;
|
|
41
|
+
--framer-error-text: #fca5a5;
|
|
42
|
+
}
|
|
43
|
+
}`;
|
|
44
|
+
|
|
45
|
+
// src/useEmbed.ts
|
|
46
|
+
import { embed } from "framer-framer";
|
|
47
|
+
import { ref, toValue, watch } from "vue";
|
|
48
|
+
function useEmbed(url, options) {
|
|
49
|
+
const result = ref(null);
|
|
50
|
+
const loading = ref(true);
|
|
51
|
+
const error = ref(null);
|
|
52
|
+
async function resolve(targetUrl) {
|
|
53
|
+
if (!targetUrl) {
|
|
54
|
+
result.value = null;
|
|
55
|
+
loading.value = false;
|
|
56
|
+
error.value = null;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
loading.value = true;
|
|
60
|
+
error.value = null;
|
|
61
|
+
try {
|
|
62
|
+
const embedResult = await embed(targetUrl, {
|
|
63
|
+
...options?.embedOptions,
|
|
64
|
+
maxWidth: options?.maxWidth,
|
|
65
|
+
maxHeight: options?.maxHeight
|
|
66
|
+
});
|
|
67
|
+
result.value = embedResult;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
70
|
+
result.value = null;
|
|
71
|
+
} finally {
|
|
72
|
+
loading.value = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
watch(
|
|
76
|
+
() => toValue(url),
|
|
77
|
+
(newUrl) => {
|
|
78
|
+
resolve(newUrl);
|
|
79
|
+
},
|
|
80
|
+
{ immediate: true }
|
|
81
|
+
);
|
|
82
|
+
return { result, loading, error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/Embed.ts
|
|
86
|
+
var Embed = defineComponent({
|
|
87
|
+
name: "Embed",
|
|
88
|
+
props: {
|
|
89
|
+
url: {
|
|
90
|
+
type: String,
|
|
91
|
+
required: true
|
|
92
|
+
},
|
|
93
|
+
maxWidth: {
|
|
94
|
+
type: Number,
|
|
95
|
+
default: void 0
|
|
96
|
+
},
|
|
97
|
+
maxHeight: {
|
|
98
|
+
type: Number,
|
|
99
|
+
default: void 0
|
|
100
|
+
},
|
|
101
|
+
options: {
|
|
102
|
+
type: Object,
|
|
103
|
+
default: void 0
|
|
104
|
+
},
|
|
105
|
+
theme: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: "auto"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
emits: {
|
|
111
|
+
load: (_result) => true,
|
|
112
|
+
error: (_error) => true
|
|
113
|
+
},
|
|
114
|
+
slots: Object,
|
|
115
|
+
setup(props, { emit, slots }) {
|
|
116
|
+
const { result, loading, error } = useEmbed(() => props.url, {
|
|
117
|
+
maxWidth: props.maxWidth,
|
|
118
|
+
maxHeight: props.maxHeight,
|
|
119
|
+
embedOptions: props.options
|
|
120
|
+
});
|
|
121
|
+
let lastEmittedUrl = null;
|
|
122
|
+
let lastEmittedError = null;
|
|
123
|
+
return () => {
|
|
124
|
+
const themeStyle = h("style", { id: themeStyleId }, themeCSS);
|
|
125
|
+
const themeAttr = props.theme ?? "auto";
|
|
126
|
+
if (loading.value) {
|
|
127
|
+
const loadingContent = slots.loading ? slots.loading() : h("div", { class: "framer-framer-loading" });
|
|
128
|
+
return h("div", { "data-framer-theme": themeAttr }, [themeStyle, loadingContent]);
|
|
129
|
+
}
|
|
130
|
+
if (error.value) {
|
|
131
|
+
if (lastEmittedError !== error.value) {
|
|
132
|
+
lastEmittedError = error.value;
|
|
133
|
+
emit("error", error.value);
|
|
134
|
+
}
|
|
135
|
+
const errorContent = slots.error ? slots.error({ error: error.value }) : h("div", { class: "framer-framer-error" }, error.value.message);
|
|
136
|
+
return h("div", { "data-framer-theme": themeAttr }, [themeStyle, errorContent]);
|
|
137
|
+
}
|
|
138
|
+
if (result.value) {
|
|
139
|
+
if (lastEmittedUrl !== result.value.url) {
|
|
140
|
+
lastEmittedUrl = result.value.url;
|
|
141
|
+
emit("load", result.value);
|
|
142
|
+
}
|
|
143
|
+
if (slots.default) {
|
|
144
|
+
return h("div", { "data-framer-theme": themeAttr }, [
|
|
145
|
+
themeStyle,
|
|
146
|
+
slots.default({ result: result.value })
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
return h("div", { "data-framer-theme": themeAttr }, [
|
|
150
|
+
themeStyle,
|
|
151
|
+
h("div", {
|
|
152
|
+
class: "framer-framer-embed",
|
|
153
|
+
innerHTML: result.value.html
|
|
154
|
+
})
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
export {
|
|
162
|
+
Embed,
|
|
163
|
+
useEmbed
|
|
164
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@framer-framer/vue",
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Vue 3 embed component for framer-framer",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"lint": "biome check src test",
|
|
27
|
+
"lint:fix": "biome check --write src test",
|
|
28
|
+
"format": "biome format src test",
|
|
29
|
+
"format:fix": "biome format --write src test",
|
|
30
|
+
"ci": "npm run lint && npm run typecheck && npm run test && npm run build",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"framer-framer": "^3.0.0",
|
|
35
|
+
"vue": "^3.3.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^2.4.4",
|
|
39
|
+
"@vue/test-utils": "^2.4.0",
|
|
40
|
+
"framer-framer": "^3.2.0",
|
|
41
|
+
"happy-dom": "^20.8.8",
|
|
42
|
+
"tsup": "^8.0.0",
|
|
43
|
+
"typescript": "^5.4.0",
|
|
44
|
+
"vitest": "^4.0.18",
|
|
45
|
+
"vue": "^3.5.0"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git@github.com:piroz/framer-framer.git",
|
|
50
|
+
"directory": "packages/vue"
|
|
51
|
+
},
|
|
52
|
+
"author": "piroz",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"bugs": "https://github.com/piroz/framer-framer/issues",
|
|
55
|
+
"keywords": [
|
|
56
|
+
"vue",
|
|
57
|
+
"vue3",
|
|
58
|
+
"embed",
|
|
59
|
+
"oembed",
|
|
60
|
+
"component",
|
|
61
|
+
"framer-framer"
|
|
62
|
+
]
|
|
63
|
+
}
|