@meowdown/embed 0.0.1 → 0.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/LICENSE +21 -0
- package/dist/{render-link-BqiZxTy9.js → is-plain-click-QDpLdFP7.js} +24 -18
- package/dist/x/index.d.ts +28 -2
- package/dist/x/index.js +194 -83
- package/dist/x/theme.css +81 -35
- package/dist/youtube/index.d.ts +32 -1
- package/dist/youtube/index.js +41 -9
- package/package.json +12 -18
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ocavue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -53,23 +53,6 @@ function isPromiseLike(value) {
|
|
|
53
53
|
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/root.ts
|
|
58
|
-
/**
|
|
59
|
-
* The direct `<div data-root>` child that holds the rendered snapshot. Reused
|
|
60
|
-
* across updates so other children the host appended survive; created on
|
|
61
|
-
* first use.
|
|
62
|
-
*/
|
|
63
|
-
function getRootContainer(host) {
|
|
64
|
-
let container = host.querySelector(":scope > div[data-root]");
|
|
65
|
-
if (!container) {
|
|
66
|
-
container = host.ownerDocument.createElement("div");
|
|
67
|
-
container.dataset.root = "";
|
|
68
|
-
host.append(container);
|
|
69
|
-
}
|
|
70
|
-
return container;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
56
|
//#endregion
|
|
74
57
|
//#region src/safe-url.ts
|
|
75
58
|
function getSafeUrl(value, protocols) {
|
|
@@ -97,4 +80,27 @@ function renderLink(content, destination) {
|
|
|
97
80
|
}
|
|
98
81
|
|
|
99
82
|
//#endregion
|
|
100
|
-
|
|
83
|
+
//#region src/root.ts
|
|
84
|
+
/**
|
|
85
|
+
* The direct `<div data-root>` child that holds the rendered snapshot. Reused
|
|
86
|
+
* across updates so other children the host appended survive; created on
|
|
87
|
+
* first use.
|
|
88
|
+
*/
|
|
89
|
+
function getRootContainer(host) {
|
|
90
|
+
let container = host.querySelector(":scope > div[data-root]");
|
|
91
|
+
if (!container) {
|
|
92
|
+
container = host.ownerDocument.createElement("div");
|
|
93
|
+
container.dataset.root = "";
|
|
94
|
+
host.append(container);
|
|
95
|
+
}
|
|
96
|
+
return container;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/is-plain-click.ts
|
|
101
|
+
function isPlainClick(event) {
|
|
102
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { useFetch as a, getSafeUrl as i, getRootContainer as n, renderLink as r, isPlainClick as t };
|
package/dist/x/index.d.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { n as Resolver, t as FetchProps } from "../fetch-Djnul03u.js";
|
|
2
2
|
import { HostElement, State } from "@aria-ui/core";
|
|
3
|
-
import { XPost } from "@post-embed/types";
|
|
3
|
+
import { XPost, XPostMedia } from "@post-embed/types";
|
|
4
|
+
//#region src/x/media-click.d.ts
|
|
5
|
+
export declare const X_POST_MEDIA_CLICK = "meowdown-embed-media-click";
|
|
6
|
+
interface XPostMediaClickDetail {
|
|
7
|
+
/**
|
|
8
|
+
* The activated item. Its URLs already passed the `mediaUrlProtocols` check,
|
|
9
|
+
* and video sources are sorted best first.
|
|
10
|
+
*/
|
|
11
|
+
media: XPostMedia;
|
|
12
|
+
/**
|
|
13
|
+
* Every displayable item of the same post, in order, for paging.
|
|
14
|
+
*/
|
|
15
|
+
items: XPostMedia[];
|
|
16
|
+
/**
|
|
17
|
+
* Position of `media` in `items`.
|
|
18
|
+
*/
|
|
19
|
+
index: number;
|
|
20
|
+
/**
|
|
21
|
+
* The rendered thumbnail: the photo `<img>`, or the video poster.
|
|
22
|
+
*/
|
|
23
|
+
element: HTMLElement;
|
|
24
|
+
}
|
|
25
|
+
type XPostMediaClickEvent = CustomEvent<XPostMediaClickDetail>;
|
|
26
|
+
//#endregion
|
|
4
27
|
//#region src/x/x-post.d.ts
|
|
5
28
|
interface XPostProps extends FetchProps<XPost> {
|
|
6
29
|
mediaUrlProtocols: readonly string[] | null;
|
|
@@ -17,6 +40,9 @@ declare global {
|
|
|
17
40
|
interface HTMLElementTagNameMap {
|
|
18
41
|
'meowdown-embed-x': XPostElement;
|
|
19
42
|
}
|
|
43
|
+
interface HTMLElementEventMap {
|
|
44
|
+
'meowdown-embed-media-click': XPostMediaClickEvent;
|
|
45
|
+
}
|
|
20
46
|
}
|
|
21
47
|
//#endregion
|
|
22
|
-
export type { Resolver, XPostElement, XPostProps };
|
|
48
|
+
export type { Resolver, XPostElement, XPostMediaClickDetail, XPostMediaClickEvent, XPostProps };
|
package/dist/x/index.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as useFetch, i as getSafeUrl, n as getRootContainer, r as renderLink, t as isPlainClick } from "../is-plain-click-QDpLdFP7.js";
|
|
2
2
|
import { defineCustomElement, defineProps, registerCustomElement, useEffect } from "@aria-ui/core";
|
|
3
3
|
import { parseXPost, parseXPostId } from "@post-embed/schema";
|
|
4
4
|
import el from "crelt";
|
|
5
5
|
|
|
6
|
+
//#region src/x/media-click.ts
|
|
7
|
+
const X_POST_MEDIA_CLICK = "meowdown-embed-media-click";
|
|
8
|
+
/**
|
|
9
|
+
* Returns false when a listener called `preventDefault()`: the host shows the
|
|
10
|
+
* media itself, so the card must not run its own default.
|
|
11
|
+
*/
|
|
12
|
+
function dispatchMediaClick(target, detail) {
|
|
13
|
+
return target.dispatchEvent(new CustomEvent(X_POST_MEDIA_CLICK, {
|
|
14
|
+
detail,
|
|
15
|
+
bubbles: true,
|
|
16
|
+
cancelable: true,
|
|
17
|
+
composed: true
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
6
22
|
//#region src/x/render-author.ts
|
|
7
23
|
function renderAuthor(author, protocols, date) {
|
|
8
24
|
if (!author.name && !author.handle) return;
|
|
@@ -22,103 +38,197 @@ function renderAuthor(author, protocols, date) {
|
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
//#endregion
|
|
25
|
-
//#region src/
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
//#region src/to-positive-number.ts
|
|
42
|
+
/**
|
|
43
|
+
* The value rounded to an integer, or nothing unless it is a finite number
|
|
44
|
+
* above zero.
|
|
45
|
+
*/
|
|
46
|
+
function toPositiveNumber(value) {
|
|
47
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.round(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/x/sort-video-sources.ts
|
|
52
|
+
/**
|
|
53
|
+
* Orders the sources the way a `<video>` should try them: it plays the first
|
|
54
|
+
* `<source>` it supports, so MP4 files come before HLS playlists (which only
|
|
55
|
+
* Safari plays natively), and within a type the highest bitrate comes first.
|
|
56
|
+
* A source without a bitrate counts as 0. Returns a new array.
|
|
57
|
+
*/
|
|
58
|
+
function sortVideoSources(sources) {
|
|
59
|
+
return [...sources].sort((a, b) => {
|
|
60
|
+
return Number(b.type === "video/mp4") - Number(a.type === "video/mp4") || (b.bitrate || 0) - (a.bitrate || 0);
|
|
61
|
+
});
|
|
28
62
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/x/render-media.ts
|
|
66
|
+
function getSizeAttrs(media) {
|
|
32
67
|
return {
|
|
33
|
-
width,
|
|
34
|
-
height
|
|
35
|
-
style: width && height ? `--_ratio: ${width} / ${height}` : void 0
|
|
68
|
+
width: toPositiveNumber(media.width),
|
|
69
|
+
height: toPositiveNumber(media.height)
|
|
36
70
|
};
|
|
37
71
|
}
|
|
72
|
+
function getRatioStyle(media) {
|
|
73
|
+
const { width, height } = getSizeAttrs(media);
|
|
74
|
+
return width && height ? `--_ratio: ${width} / ${height}` : void 0;
|
|
75
|
+
}
|
|
38
76
|
function getOrientation(media) {
|
|
39
|
-
const width =
|
|
40
|
-
const height =
|
|
77
|
+
const width = toPositiveNumber(media.width);
|
|
78
|
+
const height = toPositiveNumber(media.height);
|
|
41
79
|
if (!width || !height) return;
|
|
42
80
|
return height > width * 1.1 ? "portrait" : width > height * 1.1 ? "landscape" : "square";
|
|
43
81
|
}
|
|
82
|
+
function getSafeMediaUrl(value, protocols) {
|
|
83
|
+
const url = getSafeUrl(value, protocols);
|
|
84
|
+
if (!url) console.warn(`[meowdown] Ignored unsafe media URL: ${value}`);
|
|
85
|
+
return url;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The item with only the URLs the card may load, or nothing when it has none
|
|
89
|
+
* left to show.
|
|
90
|
+
*/
|
|
91
|
+
function getDisplayable(media, protocols) {
|
|
92
|
+
if (media.unavailable) return;
|
|
93
|
+
if (media.type === "photo") {
|
|
94
|
+
const url = getSafeMediaUrl(media.url, protocols);
|
|
95
|
+
return url ? {
|
|
96
|
+
...media,
|
|
97
|
+
url
|
|
98
|
+
} : void 0;
|
|
99
|
+
}
|
|
100
|
+
const sources = sortVideoSources(media.sources.flatMap((source) => {
|
|
101
|
+
const url = getSafeMediaUrl(source.url, protocols);
|
|
102
|
+
return url ? [{
|
|
103
|
+
...source,
|
|
104
|
+
url
|
|
105
|
+
}] : [];
|
|
106
|
+
}));
|
|
107
|
+
if (sources.length === 0) return;
|
|
108
|
+
const poster = media.poster && getSafeMediaUrl(media.poster, protocols);
|
|
109
|
+
return {
|
|
110
|
+
...media,
|
|
111
|
+
sources,
|
|
112
|
+
poster
|
|
113
|
+
};
|
|
114
|
+
}
|
|
44
115
|
function renderUnavailable(permalink) {
|
|
45
116
|
return el("div", { "data-media-unavailable": "" }, "Media unavailable. ", renderLink("View on X", permalink));
|
|
46
117
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
118
|
+
function renderError(permalink) {
|
|
119
|
+
return el("div", {
|
|
49
120
|
"data-media-error": "",
|
|
50
121
|
hidden: true
|
|
51
122
|
}, "Media could not be loaded. ", renderLink("View on X", permalink));
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}] : [];
|
|
81
|
-
}).sort((a, b) => {
|
|
82
|
-
return Number(b.type === "video/mp4") - Number(a.type === "video/mp4") || (b.bitrate || 0) - (a.bitrate || 0);
|
|
83
|
-
}).map((source) => el("source", {
|
|
123
|
+
}
|
|
124
|
+
function renderPhoto(media, error, onClick) {
|
|
125
|
+
const image = el("img", {
|
|
126
|
+
src: media.url,
|
|
127
|
+
alt: media.alt || "Post image",
|
|
128
|
+
...getSizeAttrs(media),
|
|
129
|
+
loading: "lazy",
|
|
130
|
+
decoding: "async",
|
|
131
|
+
referrerpolicy: "no-referrer"
|
|
132
|
+
});
|
|
133
|
+
const link = el("a", {
|
|
134
|
+
href: media.url,
|
|
135
|
+
target: "_blank",
|
|
136
|
+
rel: "noopener noreferrer"
|
|
137
|
+
}, image);
|
|
138
|
+
image.addEventListener("error", () => {
|
|
139
|
+
link.hidden = true;
|
|
140
|
+
error.hidden = false;
|
|
141
|
+
});
|
|
142
|
+
link.addEventListener("click", (event) => {
|
|
143
|
+
if (isPlainClick(event) && !onClick(image)) event.preventDefault();
|
|
144
|
+
});
|
|
145
|
+
return link;
|
|
146
|
+
}
|
|
147
|
+
function renderPlayer(media, error, permalink) {
|
|
148
|
+
const gif = media.type === "gif";
|
|
149
|
+
const sources = media.sources.map((source) => {
|
|
150
|
+
return el("source", {
|
|
84
151
|
src: source.url,
|
|
85
152
|
type: source.type
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
video.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
const video = el("video", {
|
|
156
|
+
controls: true,
|
|
157
|
+
playsInline: true,
|
|
158
|
+
"aria-label": gif ? "Animated GIF" : "Post video",
|
|
159
|
+
poster: media.poster,
|
|
160
|
+
...getSizeAttrs(media),
|
|
161
|
+
loop: gif
|
|
162
|
+
}, sources, renderLink("Watch on X", permalink));
|
|
163
|
+
video.muted = gif;
|
|
164
|
+
const showError = () => {
|
|
165
|
+
video.hidden = true;
|
|
166
|
+
error.hidden = false;
|
|
167
|
+
};
|
|
168
|
+
video.addEventListener("error", showError);
|
|
169
|
+
let failedSources = 0;
|
|
170
|
+
for (const source of sources) source.addEventListener("error", () => {
|
|
171
|
+
failedSources++;
|
|
172
|
+
if (failedSources === sources.length) showError();
|
|
173
|
+
}, { once: true });
|
|
174
|
+
return video;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A poster button. Its click plays the video in place, unless a
|
|
178
|
+
* `meowdown-embed-media-click` listener takes over.
|
|
179
|
+
*/
|
|
180
|
+
function renderVideo(media, error, onClick, permalink) {
|
|
181
|
+
const gif = media.type === "gif";
|
|
182
|
+
const poster = media.poster ? el("img", {
|
|
183
|
+
src: media.poster,
|
|
184
|
+
alt: "",
|
|
185
|
+
...getSizeAttrs(media),
|
|
186
|
+
loading: "lazy",
|
|
187
|
+
decoding: "async",
|
|
188
|
+
referrerpolicy: "no-referrer"
|
|
189
|
+
}) : void 0;
|
|
190
|
+
const button = el("button", {
|
|
191
|
+
"data-poster": "",
|
|
192
|
+
type: "button",
|
|
193
|
+
"aria-label": gif ? "Play GIF" : "Play video"
|
|
194
|
+
}, poster, el("span", {
|
|
195
|
+
"data-play": "",
|
|
196
|
+
"aria-hidden": "true"
|
|
197
|
+
}));
|
|
198
|
+
button.addEventListener("click", () => {
|
|
199
|
+
if (!onClick(poster ?? button)) return;
|
|
200
|
+
const video = renderPlayer(media, error, permalink);
|
|
201
|
+
button.replaceWith(video);
|
|
202
|
+
video.focus();
|
|
203
|
+
video.play().catch(() => {});
|
|
204
|
+
});
|
|
205
|
+
return button;
|
|
115
206
|
}
|
|
116
207
|
function renderMedia(media, protocols, permalink) {
|
|
117
208
|
if (!media?.length) return;
|
|
209
|
+
const displayable = media.map((item) => getDisplayable(item, protocols));
|
|
210
|
+
const items = displayable.filter((item) => item != null);
|
|
118
211
|
return el("div", {
|
|
119
212
|
"data-media": "",
|
|
120
213
|
"data-count": String(media.length)
|
|
121
|
-
},
|
|
214
|
+
}, displayable.map((item) => {
|
|
215
|
+
if (!item) return renderUnavailable(permalink);
|
|
216
|
+
const error = renderError(permalink);
|
|
217
|
+
const onClick = (element) => {
|
|
218
|
+
return dispatchMediaClick(element, {
|
|
219
|
+
media: item,
|
|
220
|
+
items,
|
|
221
|
+
index: items.indexOf(item),
|
|
222
|
+
element
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
return el("div", {
|
|
226
|
+
"data-media-item": "",
|
|
227
|
+
"data-type": item.type,
|
|
228
|
+
"data-orientation": getOrientation(item),
|
|
229
|
+
style: getRatioStyle(item)
|
|
230
|
+
}, item.type === "photo" ? renderPhoto(item, error, onClick) : renderVideo(item, error, onClick, permalink), error);
|
|
231
|
+
}));
|
|
122
232
|
}
|
|
123
233
|
|
|
124
234
|
//#endregion
|
|
@@ -149,14 +259,14 @@ function renderDate(post) {
|
|
|
149
259
|
|
|
150
260
|
//#endregion
|
|
151
261
|
//#region src/x/render-post.ts
|
|
152
|
-
function renderBody(post) {
|
|
262
|
+
function renderBody(post, collapseBlankLines = false) {
|
|
153
263
|
const permalink = getPermalink(post);
|
|
154
264
|
return el("p", {
|
|
155
265
|
"data-body": "",
|
|
156
266
|
dir: "auto",
|
|
157
267
|
lang: post.lang || void 0
|
|
158
268
|
}, el("span", { "data-text": "" }, post.body.map((segment) => {
|
|
159
|
-
return segment.type === "link" ? renderLink(segment.text, segment.url) : segment.text.split("\n").map((line, index) => {
|
|
269
|
+
return segment.type === "link" ? renderLink(segment.text, segment.url) : segment.text.split(collapseBlankLines ? /\n+/ : "\n").map((line, index) => {
|
|
160
270
|
return [index ? el("br", {}) : void 0, line];
|
|
161
271
|
});
|
|
162
272
|
})), post.truncated && permalink ? [" ", renderLink("Show more", permalink)] : void 0);
|
|
@@ -169,7 +279,7 @@ function renderQuoted(post, protocols) {
|
|
|
169
279
|
return el("article", {
|
|
170
280
|
"data-quoted": "",
|
|
171
281
|
"aria-label": "Quoted post"
|
|
172
|
-
}, renderAuthor(post.author, protocols, renderDate(post)), renderBody(post), renderMedia(post.media, protocols, getPermalink(post)), renderEdit(post));
|
|
282
|
+
}, renderAuthor(post.author, protocols, renderDate(post)), renderBody(post, true), renderMedia(post.media, protocols, getPermalink(post)), renderEdit(post));
|
|
173
283
|
}
|
|
174
284
|
function renderPost(post, protocols = null) {
|
|
175
285
|
const reply = post.replyTo;
|
|
@@ -192,17 +302,18 @@ function useXPost(host, props) {
|
|
|
192
302
|
const url = props.url.get();
|
|
193
303
|
const value = result && !result.issues ? result.value : void 0;
|
|
194
304
|
const valid = value && (props.data.get() != null || !url || parseXPostId(url) === value.id);
|
|
195
|
-
container.replaceChildren(valid ? renderPost(value, protocols) : renderFallback(pending.get()));
|
|
305
|
+
container.replaceChildren(valid ? renderPost(value, protocols) : renderFallback(pending.get(), url));
|
|
196
306
|
return () => {
|
|
197
307
|
for (const video of container.querySelectorAll("video")) video.pause();
|
|
198
308
|
};
|
|
199
309
|
});
|
|
200
310
|
}
|
|
201
|
-
function renderFallback(pending) {
|
|
311
|
+
function renderFallback(pending, url) {
|
|
312
|
+
const permalink = url != null && parseXPostId(url) ? url : void 0;
|
|
202
313
|
return el("article", pending ? {
|
|
203
314
|
"data-fallback": "",
|
|
204
315
|
"data-pending": ""
|
|
205
|
-
} : { "data-fallback": "" }, el("header", { "data-author": "" }, el("bdi", {}, "X post")), el("p", { "data-body": "" }, pending ? "Loading this post…" : "This post is unavailable."), pending ? null : el("footer", { "data-footer": "" }, "No saved post could be displayed."));
|
|
316
|
+
} : { "data-fallback": "" }, el("header", { "data-author": "" }, el("bdi", {}, "X post")), el("p", { "data-body": "" }, pending ? "Loading this post…" : "This post is unavailable."), pending ? null : el("footer", { "data-footer": "" }, permalink ? renderLink("View on X", permalink) : "No saved post could be displayed."));
|
|
206
317
|
}
|
|
207
318
|
const XPost = defineCustomElement(useXPost, defineProps({
|
|
208
319
|
data: {
|
|
@@ -230,4 +341,4 @@ function registerXPost(name = "meowdown-embed-x") {
|
|
|
230
341
|
}
|
|
231
342
|
|
|
232
343
|
//#endregion
|
|
233
|
-
export { registerXPost, useXPost };
|
|
344
|
+
export { X_POST_MEDIA_CLICK, registerXPost, useXPost };
|
package/dist/x/theme.css
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
--_muted: var(--meowdown-embed-muted-color, color-mix(in srgb, currentColor 55%, transparent));
|
|
58
58
|
--_link: var(--meowdown-embed-link-color, #1d9bf0);
|
|
59
59
|
--_tint: color-mix(in srgb, currentColor 5%, transparent);
|
|
60
|
+
--_media-height: 7.5rem;
|
|
60
61
|
|
|
61
62
|
display: block;
|
|
62
63
|
/* As wide as the content, between 18rem and 28rem: a short text post stays
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
[data-meowdown-embed='x'] [data-root]::before {
|
|
78
79
|
content: '';
|
|
79
80
|
display: block;
|
|
80
|
-
width: calc(18rem - 2 * var(--meowdown-embed-padding,
|
|
81
|
+
width: calc(18rem - 2 * var(--meowdown-embed-padding, 1rem) - 2px);
|
|
81
82
|
max-width: 100%;
|
|
82
83
|
height: 0;
|
|
83
84
|
}
|
|
@@ -86,9 +87,10 @@
|
|
|
86
87
|
display: block;
|
|
87
88
|
position: relative;
|
|
88
89
|
color: var(--meowdown-embed-color, inherit);
|
|
89
|
-
background: var(--meowdown-embed-background,
|
|
90
|
+
background: var(--meowdown-embed-background, transparent);
|
|
91
|
+
border: 1px solid var(--_hairline);
|
|
90
92
|
border-radius: var(--meowdown-embed-radius, 0.75rem);
|
|
91
|
-
padding: var(--meowdown-embed-padding,
|
|
93
|
+
padding: var(--meowdown-embed-padding, 1rem);
|
|
92
94
|
font-size: 1em;
|
|
93
95
|
font-weight: 400;
|
|
94
96
|
font-style: normal;
|
|
@@ -130,14 +132,14 @@
|
|
|
130
132
|
& [data-author] {
|
|
131
133
|
display: flex;
|
|
132
134
|
align-items: center;
|
|
133
|
-
gap: 0.
|
|
135
|
+
gap: 0.625rem;
|
|
134
136
|
min-width: 0;
|
|
135
137
|
}
|
|
136
138
|
& [data-avatar] {
|
|
137
139
|
display: block;
|
|
138
140
|
flex: none;
|
|
139
|
-
width:
|
|
140
|
-
height:
|
|
141
|
+
width: 2.5rem;
|
|
142
|
+
height: 2.5rem;
|
|
141
143
|
object-fit: cover;
|
|
142
144
|
border-radius: 50%;
|
|
143
145
|
background: var(--_tint);
|
|
@@ -152,7 +154,7 @@
|
|
|
152
154
|
display: block;
|
|
153
155
|
min-width: 0;
|
|
154
156
|
flex: 1;
|
|
155
|
-
line-height: 1.
|
|
157
|
+
line-height: 1.3;
|
|
156
158
|
}
|
|
157
159
|
& [data-author-name] {
|
|
158
160
|
display: flex;
|
|
@@ -162,7 +164,7 @@
|
|
|
162
164
|
font-weight: 600;
|
|
163
165
|
}
|
|
164
166
|
& [data-author-details] > bdi {
|
|
165
|
-
font-size: 0.
|
|
167
|
+
font-size: 0.9375em;
|
|
166
168
|
color: var(--_muted);
|
|
167
169
|
}
|
|
168
170
|
& [data-author-details] > bdi > a {
|
|
@@ -174,7 +176,7 @@
|
|
|
174
176
|
align-self: flex-start;
|
|
175
177
|
padding-inline-start: 0.25rem;
|
|
176
178
|
font-size: 0.8125em;
|
|
177
|
-
line-height: 1.
|
|
179
|
+
line-height: 1.3rem;
|
|
178
180
|
white-space: nowrap;
|
|
179
181
|
color: var(--_muted);
|
|
180
182
|
}
|
|
@@ -185,18 +187,18 @@
|
|
|
185
187
|
/* Body */
|
|
186
188
|
& [data-reply-to] {
|
|
187
189
|
display: block;
|
|
188
|
-
margin-block-start: 0.
|
|
189
|
-
font-size: 0.
|
|
190
|
+
margin-block-start: 0.625rem;
|
|
191
|
+
font-size: 0.875em;
|
|
190
192
|
color: var(--_muted);
|
|
191
193
|
}
|
|
192
194
|
& [data-body] {
|
|
193
195
|
display: block;
|
|
194
|
-
margin-block: 0.
|
|
196
|
+
margin-block: 0.75rem 0;
|
|
195
197
|
font-size: 1em;
|
|
196
198
|
line-height: 1.5;
|
|
197
199
|
}
|
|
198
200
|
& [data-reply-to] + [data-body] {
|
|
199
|
-
margin-block-start: 0.
|
|
201
|
+
margin-block-start: 0.375rem;
|
|
200
202
|
}
|
|
201
203
|
& [data-text] {
|
|
202
204
|
white-space: pre-wrap;
|
|
@@ -207,14 +209,13 @@
|
|
|
207
209
|
display: grid;
|
|
208
210
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
209
211
|
gap: 2px;
|
|
212
|
+
height: var(--_media-height);
|
|
213
|
+
max-width: calc(2 * var(--_media-height));
|
|
210
214
|
margin-block-start: 0.75rem;
|
|
211
215
|
border-radius: 0.5rem;
|
|
212
216
|
overflow: hidden;
|
|
213
217
|
background: var(--_hairline);
|
|
214
218
|
}
|
|
215
|
-
& [data-media][data-count='1'] {
|
|
216
|
-
grid-template-columns: minmax(0, 1fr);
|
|
217
|
-
}
|
|
218
219
|
& [data-media-item] {
|
|
219
220
|
display: block;
|
|
220
221
|
position: relative;
|
|
@@ -234,34 +235,79 @@
|
|
|
234
235
|
display: block;
|
|
235
236
|
height: 100%;
|
|
236
237
|
}
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
& [data-media-item] > [data-poster] {
|
|
239
|
+
position: relative;
|
|
240
|
+
width: 100%;
|
|
241
|
+
height: 100%;
|
|
242
|
+
padding: 0;
|
|
243
|
+
border: 0;
|
|
244
|
+
background: var(--_tint);
|
|
245
|
+
font: inherit;
|
|
246
|
+
color: inherit;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
}
|
|
249
|
+
& [data-play] {
|
|
250
|
+
position: absolute;
|
|
251
|
+
inset: 50% auto auto 50%;
|
|
252
|
+
width: 2.25rem;
|
|
253
|
+
height: 2.25rem;
|
|
254
|
+
translate: -50% -50%;
|
|
255
|
+
border-radius: 50%;
|
|
256
|
+
background: rgb(0 0 0 / 60%);
|
|
257
|
+
}
|
|
258
|
+
& [data-play]::after {
|
|
259
|
+
content: '';
|
|
260
|
+
position: absolute;
|
|
261
|
+
inset: 50% auto auto 54%;
|
|
262
|
+
translate: -50% -50%;
|
|
263
|
+
border-block: 0.375rem solid transparent;
|
|
264
|
+
border-inline-start: 0.6rem solid #fff;
|
|
265
|
+
}
|
|
239
266
|
& [data-media-item] img,
|
|
240
267
|
& [data-media-item] video {
|
|
241
268
|
display: block;
|
|
242
269
|
width: 100%;
|
|
243
|
-
height:
|
|
244
|
-
aspect-ratio: var(--_ratio, auto);
|
|
245
|
-
max-height: 20rem;
|
|
270
|
+
height: 100%;
|
|
246
271
|
object-fit: cover;
|
|
247
272
|
background: var(--_tint);
|
|
248
273
|
}
|
|
249
274
|
& [data-media-item][data-orientation='portrait'] :is(img, video) {
|
|
250
275
|
object-position: center 30%;
|
|
251
276
|
}
|
|
252
|
-
& [data-media]:not([data-count='1']) [data-media-item] :is(img, video) {
|
|
253
|
-
height: 100%;
|
|
254
|
-
aspect-ratio: 3 / 2;
|
|
255
|
-
}
|
|
256
277
|
& [data-media][data-count='3'] [data-media-item]:first-child {
|
|
257
278
|
grid-row: span 2;
|
|
258
279
|
}
|
|
259
|
-
|
|
280
|
+
/* A single photo or video is a thumbnail of fixed height and its own ratio,
|
|
281
|
+
* never cropped; media wider than the card shrinks to fit. A video playing
|
|
282
|
+
* in place grows, and turns a grid into a stack so its player has room. */
|
|
283
|
+
& [data-media]:is([data-count='1'], :has(video)) {
|
|
284
|
+
display: block;
|
|
285
|
+
height: auto;
|
|
286
|
+
max-width: none;
|
|
287
|
+
border-radius: 0;
|
|
288
|
+
overflow: visible;
|
|
289
|
+
background: none;
|
|
290
|
+
}
|
|
291
|
+
& [data-media]:is([data-count='1'], :has(video)) > [data-media-item] {
|
|
292
|
+
width: calc(var(--_media-height) * (var(--_ratio, 16 / 9)));
|
|
293
|
+
max-width: 100%;
|
|
294
|
+
aspect-ratio: var(--_ratio, 16 / 9);
|
|
295
|
+
border-radius: 0.5rem;
|
|
296
|
+
}
|
|
297
|
+
& [data-media]:has(video) > [data-media-item] + [data-media-item] {
|
|
298
|
+
margin-block-start: 0.375rem;
|
|
299
|
+
}
|
|
300
|
+
& [data-media-item]:has(> video) {
|
|
301
|
+
--_media-height: 20rem;
|
|
302
|
+
}
|
|
303
|
+
& [data-media-item]:has(> [data-media-error]:not([hidden])) {
|
|
304
|
+
width: auto;
|
|
260
305
|
aspect-ratio: auto;
|
|
261
306
|
}
|
|
262
307
|
& [data-media-unavailable],
|
|
263
308
|
& [data-media-error] {
|
|
264
309
|
display: block;
|
|
310
|
+
border-radius: 0.5rem;
|
|
265
311
|
padding: 0.75rem 1rem;
|
|
266
312
|
color: var(--_muted);
|
|
267
313
|
background: var(--_tint);
|
|
@@ -269,11 +315,13 @@
|
|
|
269
315
|
|
|
270
316
|
/* Quoted post */
|
|
271
317
|
& [data-quoted] {
|
|
318
|
+
--_media-height: 5rem;
|
|
319
|
+
|
|
272
320
|
display: block;
|
|
273
321
|
margin-block-start: 0.75rem;
|
|
274
322
|
padding: 0.75rem;
|
|
323
|
+
border: 1px solid var(--_hairline);
|
|
275
324
|
border-radius: 0.5rem;
|
|
276
|
-
background: var(--_tint);
|
|
277
325
|
}
|
|
278
326
|
& [data-quoted] [data-avatar] {
|
|
279
327
|
width: 1.375rem;
|
|
@@ -317,12 +365,15 @@
|
|
|
317
365
|
margin-block-start: 0.5rem;
|
|
318
366
|
border-radius: 0.375rem;
|
|
319
367
|
}
|
|
320
|
-
& [data-quoted] [data-
|
|
321
|
-
|
|
368
|
+
& [data-quoted] [data-play] {
|
|
369
|
+
scale: 0.75;
|
|
322
370
|
}
|
|
323
371
|
/* Quoted photos shrink to one thumbnail beside the text, with a count
|
|
324
372
|
* badge; a quoted video or GIF stays below the text so its player works. */
|
|
325
|
-
&
|
|
373
|
+
&
|
|
374
|
+
[data-quoted]:has(> [data-media]):not(
|
|
375
|
+
:has(> [data-media] [data-poster], > [data-media] video)
|
|
376
|
+
) {
|
|
326
377
|
display: grid;
|
|
327
378
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
328
379
|
grid-template-areas:
|
|
@@ -355,11 +406,6 @@
|
|
|
355
406
|
& > [data-media] > [data-media-item]:nth-child(n + 2) {
|
|
356
407
|
display: none;
|
|
357
408
|
}
|
|
358
|
-
& > [data-media] > [data-media-item] img {
|
|
359
|
-
height: 100%;
|
|
360
|
-
max-height: none;
|
|
361
|
-
aspect-ratio: auto;
|
|
362
|
-
}
|
|
363
409
|
& > [data-media]:not([data-count='1'])::after {
|
|
364
410
|
content: attr(data-count);
|
|
365
411
|
position: absolute;
|
package/dist/youtube/index.d.ts
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import { n as Resolver, t as FetchProps } from "../fetch-Djnul03u.js";
|
|
2
2
|
import { HostElement, State } from "@aria-ui/core";
|
|
3
3
|
import { YouTubeVideo } from "@post-embed/types";
|
|
4
|
+
//#region src/youtube/video-click.d.ts
|
|
5
|
+
export declare const YOUTUBE_VIDEO_CLICK = "meowdown-embed-youtube-click";
|
|
6
|
+
interface YouTubeVideoClickDetail {
|
|
7
|
+
/**
|
|
8
|
+
* The snapshot the card rendered.
|
|
9
|
+
*/
|
|
10
|
+
video: YouTubeVideo;
|
|
11
|
+
videoId: string;
|
|
12
|
+
/**
|
|
13
|
+
* Where playback starts, from the `t` or `start` parameter of the URL.
|
|
14
|
+
*/
|
|
15
|
+
startSeconds: number;
|
|
16
|
+
/**
|
|
17
|
+
* A Short, which plays in portrait.
|
|
18
|
+
*/
|
|
19
|
+
short: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The player page for an `<iframe>`. It starts playing as soon as it loads.
|
|
22
|
+
*/
|
|
23
|
+
embedUrl: string;
|
|
24
|
+
/**
|
|
25
|
+
* The rendered thumbnail: the poster `<img>`, or the poster itself when the
|
|
26
|
+
* snapshot has no thumbnail.
|
|
27
|
+
*/
|
|
28
|
+
element: HTMLElement;
|
|
29
|
+
}
|
|
30
|
+
type YouTubeVideoClickEvent = CustomEvent<YouTubeVideoClickDetail>;
|
|
31
|
+
//#endregion
|
|
4
32
|
//#region src/youtube/youtube-video.d.ts
|
|
5
33
|
interface YouTubeVideoProps extends FetchProps<YouTubeVideo> {
|
|
6
34
|
/**
|
|
@@ -21,6 +49,9 @@ declare global {
|
|
|
21
49
|
interface HTMLElementTagNameMap {
|
|
22
50
|
'meowdown-embed-youtube': YouTubeVideoElement;
|
|
23
51
|
}
|
|
52
|
+
interface HTMLElementEventMap {
|
|
53
|
+
'meowdown-embed-youtube-click': YouTubeVideoClickEvent;
|
|
54
|
+
}
|
|
24
55
|
}
|
|
25
56
|
//#endregion
|
|
26
|
-
export type { Resolver, YouTubeVideoElement, YouTubeVideoProps };
|
|
57
|
+
export type { Resolver, YouTubeVideoClickDetail, YouTubeVideoClickEvent, YouTubeVideoElement, YouTubeVideoProps };
|
package/dist/youtube/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as useFetch, i as getSafeUrl, n as getRootContainer, r as renderLink, t as isPlainClick } from "../is-plain-click-QDpLdFP7.js";
|
|
2
2
|
import { defineCustomElement, defineProps, registerCustomElement, useEffect } from "@aria-ui/core";
|
|
3
3
|
import { parseYouTubeVideo } from "@post-embed/schema";
|
|
4
4
|
import el from "crelt";
|
|
@@ -56,6 +56,22 @@ function getEmbedUrl(ref) {
|
|
|
56
56
|
return `https://www.youtube-nocookie.com/embed/${ref.videoId}?${params}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/youtube/video-click.ts
|
|
61
|
+
const YOUTUBE_VIDEO_CLICK = "meowdown-embed-youtube-click";
|
|
62
|
+
/**
|
|
63
|
+
* Returns false when a listener called `preventDefault()`: the host plays the
|
|
64
|
+
* video itself, so the card must not run its own default.
|
|
65
|
+
*/
|
|
66
|
+
function dispatchVideoClick(target, detail) {
|
|
67
|
+
return target.dispatchEvent(new CustomEvent(YOUTUBE_VIDEO_CLICK, {
|
|
68
|
+
detail,
|
|
69
|
+
bubbles: true,
|
|
70
|
+
cancelable: true,
|
|
71
|
+
composed: true
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
//#endregion
|
|
60
76
|
//#region src/youtube/render-video.ts
|
|
61
77
|
function renderFrame(ref, title) {
|
|
@@ -75,7 +91,7 @@ function renderVideo(video, ref, playback) {
|
|
|
75
91
|
target: "_blank",
|
|
76
92
|
rel: "noopener noreferrer"
|
|
77
93
|
};
|
|
78
|
-
const
|
|
94
|
+
const image = poster ? el("img", {
|
|
79
95
|
src: poster,
|
|
80
96
|
alt: "",
|
|
81
97
|
width: video.thumbnail_width || void 0,
|
|
@@ -83,10 +99,19 @@ function renderVideo(video, ref, playback) {
|
|
|
83
99
|
loading: "lazy",
|
|
84
100
|
decoding: "async",
|
|
85
101
|
referrerpolicy: "no-referrer"
|
|
86
|
-
}) : void 0
|
|
102
|
+
}) : void 0;
|
|
103
|
+
const posterContent = [image, el("span", {
|
|
87
104
|
"data-play": "",
|
|
88
105
|
"aria-hidden": "true"
|
|
89
106
|
})];
|
|
107
|
+
const onClick = (element) => {
|
|
108
|
+
return dispatchVideoClick(element, {
|
|
109
|
+
video,
|
|
110
|
+
...ref,
|
|
111
|
+
embedUrl: getEmbedUrl(ref),
|
|
112
|
+
element
|
|
113
|
+
});
|
|
114
|
+
};
|
|
90
115
|
let posterElement;
|
|
91
116
|
if (playback === "inline") {
|
|
92
117
|
const button = el("button", {
|
|
@@ -95,16 +120,23 @@ function renderVideo(video, ref, playback) {
|
|
|
95
120
|
"aria-label": `Play: ${title}`
|
|
96
121
|
}, posterContent);
|
|
97
122
|
button.addEventListener("click", () => {
|
|
123
|
+
if (!onClick(image ?? button)) return;
|
|
98
124
|
const frame = renderFrame(ref, title);
|
|
99
125
|
button.replaceWith(frame);
|
|
100
126
|
frame.focus();
|
|
101
127
|
});
|
|
102
128
|
posterElement = button;
|
|
103
|
-
} else
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
} else {
|
|
130
|
+
const link = el("a", {
|
|
131
|
+
"data-poster": "",
|
|
132
|
+
href: watchUrl,
|
|
133
|
+
...external
|
|
134
|
+
}, posterContent);
|
|
135
|
+
link.addEventListener("click", (event) => {
|
|
136
|
+
if (isPlainClick(event) && !onClick(image ?? link)) event.preventDefault();
|
|
137
|
+
});
|
|
138
|
+
posterElement = link;
|
|
139
|
+
}
|
|
108
140
|
return el("article", { "data-orientation": ref.short ? "portrait" : void 0 }, posterElement, el("div", { "data-details": "" }, el("a", {
|
|
109
141
|
"data-title": "",
|
|
110
142
|
href: watchUrl,
|
|
@@ -161,4 +193,4 @@ function registerYouTubeVideo(name = "meowdown-embed-youtube") {
|
|
|
161
193
|
}
|
|
162
194
|
|
|
163
195
|
//#endregion
|
|
164
|
-
export { registerYouTubeVideo, useYouTubeVideo };
|
|
196
|
+
export { YOUTUBE_VIDEO_CLICK, registerYouTubeVideo, useYouTubeVideo };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meowdown/embed",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -12,27 +12,24 @@
|
|
|
12
12
|
"**/*.css"
|
|
13
13
|
],
|
|
14
14
|
"exports": {
|
|
15
|
-
"./x": "./
|
|
16
|
-
"./x/theme.css": "./
|
|
17
|
-
"./youtube": "./
|
|
18
|
-
"./youtube/theme.css": "./
|
|
15
|
+
"./x": "./dist/x/index.js",
|
|
16
|
+
"./x/theme.css": "./dist/x/theme.css",
|
|
17
|
+
"./youtube": "./dist/youtube/index.js",
|
|
18
|
+
"./youtube/theme.css": "./dist/youtube/theme.css"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"./dist/**/*.js",
|
|
22
22
|
"./dist/**/*.d.ts",
|
|
23
23
|
"./dist/**/*.css"
|
|
24
24
|
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsdown"
|
|
27
|
-
},
|
|
28
25
|
"dependencies": {
|
|
29
26
|
"@aria-ui/core": "^0.2.1",
|
|
30
|
-
"@post-embed/schema": "^0.5.
|
|
31
|
-
"@post-embed/types": "^0.
|
|
27
|
+
"@post-embed/schema": "^0.5.2",
|
|
28
|
+
"@post-embed/types": "^0.5.0",
|
|
32
29
|
"crelt": "^1.0.7"
|
|
33
30
|
},
|
|
34
31
|
"devDependencies": {
|
|
35
|
-
"@meowdown/vitest": "
|
|
32
|
+
"@meowdown/vitest": "0.0.0",
|
|
36
33
|
"@ocavue/tsconfig": "^0.7.1",
|
|
37
34
|
"@ocavue/utils": "^1.8.0",
|
|
38
35
|
"@vitest/browser-playwright": "^5.0.1",
|
|
@@ -40,12 +37,9 @@
|
|
|
40
37
|
"vitest": "^5.0.1"
|
|
41
38
|
},
|
|
42
39
|
"publishConfig": {
|
|
43
|
-
"exports": {
|
|
44
|
-
"./x": "./dist/x/index.js",
|
|
45
|
-
"./x/theme.css": "./dist/x/theme.css",
|
|
46
|
-
"./youtube": "./dist/youtube/index.js",
|
|
47
|
-
"./youtube/theme.css": "./dist/youtube/theme.css"
|
|
48
|
-
},
|
|
49
40
|
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown"
|
|
50
44
|
}
|
|
51
|
-
}
|
|
45
|
+
}
|