@meowdown/embed 0.0.1 → 0.1.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/x/index.d.ts +28 -2
- package/dist/x/index.js +192 -79
- package/dist/x/theme.css +81 -35
- 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.
|
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
|
@@ -3,6 +3,22 @@ import { defineCustomElement, defineProps, registerCustomElement, useEffect } fr
|
|
|
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,200 @@ 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
|
-
|
|
82
|
-
|
|
83
|
-
}).map((source) => el("source", {
|
|
123
|
+
}
|
|
124
|
+
function isPlainClick(event) {
|
|
125
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
126
|
+
}
|
|
127
|
+
function renderPhoto(media, error, onClick) {
|
|
128
|
+
const image = el("img", {
|
|
129
|
+
src: media.url,
|
|
130
|
+
alt: media.alt || "Post image",
|
|
131
|
+
...getSizeAttrs(media),
|
|
132
|
+
loading: "lazy",
|
|
133
|
+
decoding: "async",
|
|
134
|
+
referrerpolicy: "no-referrer"
|
|
135
|
+
});
|
|
136
|
+
const link = el("a", {
|
|
137
|
+
href: media.url,
|
|
138
|
+
target: "_blank",
|
|
139
|
+
rel: "noopener noreferrer"
|
|
140
|
+
}, image);
|
|
141
|
+
image.addEventListener("error", () => {
|
|
142
|
+
link.hidden = true;
|
|
143
|
+
error.hidden = false;
|
|
144
|
+
});
|
|
145
|
+
link.addEventListener("click", (event) => {
|
|
146
|
+
if (isPlainClick(event) && !onClick(image)) event.preventDefault();
|
|
147
|
+
});
|
|
148
|
+
return link;
|
|
149
|
+
}
|
|
150
|
+
function renderPlayer(media, error, permalink) {
|
|
151
|
+
const gif = media.type === "gif";
|
|
152
|
+
const sources = media.sources.map((source) => {
|
|
153
|
+
return el("source", {
|
|
84
154
|
src: source.url,
|
|
85
155
|
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
|
-
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
const video = el("video", {
|
|
159
|
+
controls: true,
|
|
160
|
+
playsInline: true,
|
|
161
|
+
"aria-label": gif ? "Animated GIF" : "Post video",
|
|
162
|
+
poster: media.poster,
|
|
163
|
+
...getSizeAttrs(media),
|
|
164
|
+
loop: gif
|
|
165
|
+
}, sources, renderLink("Watch on X", permalink));
|
|
166
|
+
video.muted = gif;
|
|
167
|
+
const showError = () => {
|
|
168
|
+
video.hidden = true;
|
|
169
|
+
error.hidden = false;
|
|
170
|
+
};
|
|
171
|
+
video.addEventListener("error", showError);
|
|
172
|
+
let failedSources = 0;
|
|
173
|
+
for (const source of sources) source.addEventListener("error", () => {
|
|
174
|
+
failedSources++;
|
|
175
|
+
if (failedSources === sources.length) showError();
|
|
176
|
+
}, { once: true });
|
|
177
|
+
return video;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A poster button. Its click plays the video in place, unless a
|
|
181
|
+
* `meowdown-embed-media-click` listener takes over.
|
|
182
|
+
*/
|
|
183
|
+
function renderVideo(media, error, onClick, permalink) {
|
|
184
|
+
const gif = media.type === "gif";
|
|
185
|
+
const poster = media.poster ? el("img", {
|
|
186
|
+
src: media.poster,
|
|
187
|
+
alt: "",
|
|
188
|
+
...getSizeAttrs(media),
|
|
189
|
+
loading: "lazy",
|
|
190
|
+
decoding: "async",
|
|
191
|
+
referrerpolicy: "no-referrer"
|
|
192
|
+
}) : void 0;
|
|
193
|
+
const button = el("button", {
|
|
194
|
+
"data-poster": "",
|
|
195
|
+
type: "button",
|
|
196
|
+
"aria-label": gif ? "Play GIF" : "Play video"
|
|
197
|
+
}, poster, el("span", {
|
|
198
|
+
"data-play": "",
|
|
199
|
+
"aria-hidden": "true"
|
|
200
|
+
}));
|
|
201
|
+
button.addEventListener("click", () => {
|
|
202
|
+
if (!onClick(poster ?? button)) return;
|
|
203
|
+
const video = renderPlayer(media, error, permalink);
|
|
204
|
+
button.replaceWith(video);
|
|
205
|
+
video.focus();
|
|
206
|
+
video.play().catch(() => {});
|
|
207
|
+
});
|
|
208
|
+
return button;
|
|
115
209
|
}
|
|
116
210
|
function renderMedia(media, protocols, permalink) {
|
|
117
211
|
if (!media?.length) return;
|
|
212
|
+
const displayable = media.map((item) => getDisplayable(item, protocols));
|
|
213
|
+
const items = displayable.filter((item) => item != null);
|
|
118
214
|
return el("div", {
|
|
119
215
|
"data-media": "",
|
|
120
216
|
"data-count": String(media.length)
|
|
121
|
-
},
|
|
217
|
+
}, displayable.map((item) => {
|
|
218
|
+
if (!item) return renderUnavailable(permalink);
|
|
219
|
+
const error = renderError(permalink);
|
|
220
|
+
const onClick = (element) => {
|
|
221
|
+
return dispatchMediaClick(element, {
|
|
222
|
+
media: item,
|
|
223
|
+
items,
|
|
224
|
+
index: items.indexOf(item),
|
|
225
|
+
element
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
return el("div", {
|
|
229
|
+
"data-media-item": "",
|
|
230
|
+
"data-type": item.type,
|
|
231
|
+
"data-orientation": getOrientation(item),
|
|
232
|
+
style: getRatioStyle(item)
|
|
233
|
+
}, item.type === "photo" ? renderPhoto(item, error, onClick) : renderVideo(item, error, onClick, permalink), error);
|
|
234
|
+
}));
|
|
122
235
|
}
|
|
123
236
|
|
|
124
237
|
//#endregion
|
|
@@ -149,14 +262,14 @@ function renderDate(post) {
|
|
|
149
262
|
|
|
150
263
|
//#endregion
|
|
151
264
|
//#region src/x/render-post.ts
|
|
152
|
-
function renderBody(post) {
|
|
265
|
+
function renderBody(post, collapseBlankLines = false) {
|
|
153
266
|
const permalink = getPermalink(post);
|
|
154
267
|
return el("p", {
|
|
155
268
|
"data-body": "",
|
|
156
269
|
dir: "auto",
|
|
157
270
|
lang: post.lang || void 0
|
|
158
271
|
}, 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) => {
|
|
272
|
+
return segment.type === "link" ? renderLink(segment.text, segment.url) : segment.text.split(collapseBlankLines ? /\n+/ : "\n").map((line, index) => {
|
|
160
273
|
return [index ? el("br", {}) : void 0, line];
|
|
161
274
|
});
|
|
162
275
|
})), post.truncated && permalink ? [" ", renderLink("Show more", permalink)] : void 0);
|
|
@@ -169,7 +282,7 @@ function renderQuoted(post, protocols) {
|
|
|
169
282
|
return el("article", {
|
|
170
283
|
"data-quoted": "",
|
|
171
284
|
"aria-label": "Quoted post"
|
|
172
|
-
}, renderAuthor(post.author, protocols, renderDate(post)), renderBody(post), renderMedia(post.media, protocols, getPermalink(post)), renderEdit(post));
|
|
285
|
+
}, renderAuthor(post.author, protocols, renderDate(post)), renderBody(post, true), renderMedia(post.media, protocols, getPermalink(post)), renderEdit(post));
|
|
173
286
|
}
|
|
174
287
|
function renderPost(post, protocols = null) {
|
|
175
288
|
const reply = post.replyTo;
|
|
@@ -230,4 +343,4 @@ function registerXPost(name = "meowdown-embed-x") {
|
|
|
230
343
|
}
|
|
231
344
|
|
|
232
345
|
//#endregion
|
|
233
|
-
export { registerXPost, useXPost };
|
|
346
|
+
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/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.1.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
|
+
}
|