@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 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/x/render-media.ts
26
- function dimension(value) {
27
- return Number.isFinite(value) && value > 0 ? Math.round(value) : void 0;
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
- function sizeAttrs(media) {
30
- const width = dimension(media.width);
31
- const height = dimension(media.height);
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 = dimension(media.width);
40
- const height = dimension(media.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 renderItem(media, protocols, permalink) {
48
- const error = el("div", {
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
- let content;
53
- if (media.type === "photo") {
54
- const url = getSafeUrl(media.url, protocols);
55
- if (media.unavailable || !url) return renderUnavailable(permalink);
56
- const image = el("img", {
57
- src: url,
58
- alt: media.alt || "Post image",
59
- ...sizeAttrs(media),
60
- loading: "lazy",
61
- decoding: "async",
62
- referrerpolicy: "no-referrer"
63
- });
64
- const link = el("a", {
65
- href: url,
66
- target: "_blank",
67
- rel: "noopener noreferrer"
68
- }, image);
69
- image.addEventListener("error", () => {
70
- link.hidden = true;
71
- error.hidden = false;
72
- });
73
- content = link;
74
- } else {
75
- const sources = media.sources.flatMap((source) => {
76
- const url = getSafeUrl(source.url, protocols);
77
- return url ? [{
78
- ...source,
79
- url
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 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
- if (media.unavailable || sources.length === 0) return renderUnavailable(permalink);
88
- const gif = media.type === "gif";
89
- const video = el("video", {
90
- controls: true,
91
- playsInline: true,
92
- preload: "none",
93
- "aria-label": gif ? "Animated GIF" : "Post video",
94
- poster: media.poster && getSafeUrl(media.poster, protocols),
95
- ...sizeAttrs(media),
96
- loop: gif
97
- }, sources, renderLink("Watch on X", permalink));
98
- video.muted = gif;
99
- const showError = () => {
100
- video.hidden = true;
101
- error.hidden = false;
102
- };
103
- video.addEventListener("error", showError);
104
- let failedSources = 0;
105
- for (const source of sources) source.addEventListener("error", () => {
106
- failedSources++;
107
- if (failedSources === sources.length) showError();
108
- }, { once: true });
109
- content = video;
110
- }
111
- return el("div", {
112
- "data-media-item": "",
113
- "data-orientation": getOrientation(media)
114
- }, content, error);
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
- }, media.map((item) => renderItem(item, protocols, permalink)));
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, 0.75rem));
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, var(--_tint));
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, 0.75rem);
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.5rem;
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: 2rem;
140
- height: 2rem;
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.25;
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.875em;
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.25rem;
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.375rem;
189
- font-size: 0.8125em;
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.625rem 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.25rem;
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
- /* A single photo or video fills the width at its own ratio, up to 20rem
238
- * tall; taller media is cropped and the photo links to the original. */
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: auto;
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
- & [data-media][data-count='3'] [data-media-item]:first-child :is(img, video) {
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-media-item] :is(img, video) {
321
- max-height: 10rem;
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
- & [data-quoted]:has(> [data-media]):not(:has(> [data-media] video)) {
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.1",
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": "./src/x/index.ts",
16
- "./x/theme.css": "./src/x/theme.css",
17
- "./youtube": "./src/youtube/index.ts",
18
- "./youtube/theme.css": "./src/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.0",
31
- "@post-embed/types": "^0.4.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": "workspace:*",
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
+ }