@postrun/react 2.8.0 → 2.10.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/dist/index.cjs CHANGED
@@ -1125,6 +1125,38 @@ var ACTIONS = [
1125
1125
  function XPreviewActions() {
1126
1126
  return /* @__PURE__ */ jsxRuntime.jsx("div", { style: ROW, children: ACTIONS.map(({ label, Icon: Icon2 }) => /* @__PURE__ */ jsxRuntime.jsx(Icon2, { size: 18, "aria-label": label, role: "img" }, label)) });
1127
1127
  }
1128
+ function twitterMediaUrl(src) {
1129
+ try {
1130
+ const url = new URL(src);
1131
+ const extension = url.pathname.split(".").pop();
1132
+ if (!extension) {
1133
+ return src;
1134
+ }
1135
+ url.pathname = url.pathname.replace(`.${extension}`, "");
1136
+ url.searchParams.set("format", extension);
1137
+ url.searchParams.set("name", "small");
1138
+ return url.toString();
1139
+ } catch {
1140
+ return src;
1141
+ }
1142
+ }
1143
+ function rawSrcLookup(...lists) {
1144
+ const map = /* @__PURE__ */ new Map();
1145
+ for (const list of lists) {
1146
+ for (const item of list) {
1147
+ const poster = item.posterSrc ?? item.src;
1148
+ map.set(twitterMediaUrl(item.src), item.src);
1149
+ map.set(twitterMediaUrl(poster), poster);
1150
+ }
1151
+ }
1152
+ return map;
1153
+ }
1154
+ function makeRawMediaImg(rawBySrc) {
1155
+ return function RawMediaImg(props) {
1156
+ const raw = typeof props.src === "string" ? rawBySrc.get(props.src) ?? props.src : props.src;
1157
+ return /* @__PURE__ */ jsxRuntime.jsx("img", { ...props, src: raw });
1158
+ };
1159
+ }
1128
1160
  var {
1129
1161
  extractCashtagsWithIndices,
1130
1162
  extractHashtagsWithIndices,
@@ -1175,9 +1207,12 @@ function buildUser(author) {
1175
1207
  return {
1176
1208
  id_str: "",
1177
1209
  name: author.name,
1178
- profile_image_url_https: author.avatarUrl ?? PLACEHOLDER_AVATAR,
1210
+ profile_image_url_https: author.avatar_url ?? PLACEHOLDER_AVATAR,
1179
1211
  profile_image_shape: "Circle",
1180
- screen_name: author.handle,
1212
+ // `username`/`avatar_url` derive from the SDK Connection and are nullable.
1213
+ screen_name: author.username ?? "",
1214
+ // Legacy (pre-X-era) verification flag — always false for organic posts; the
1215
+ // X Blue checkmark rides on `is_blue_verified`.
1181
1216
  verified: false,
1182
1217
  is_blue_verified: verified
1183
1218
  };
@@ -1240,7 +1275,11 @@ function buildQuoted(quote, hasQuoteId) {
1240
1275
  if (!quote && !hasQuoteId) {
1241
1276
  return void 0;
1242
1277
  }
1243
- const author = quote?.author ?? { name: "Quoted post", handle: "" };
1278
+ const author = quote?.author ?? {
1279
+ name: "Quoted post",
1280
+ username: "",
1281
+ avatar_url: null
1282
+ };
1244
1283
  const body = quote?.body ?? "";
1245
1284
  const media = quote?.media ?? [];
1246
1285
  return {
@@ -1306,7 +1345,7 @@ function XPostPreviewImpl({
1306
1345
  showActions = true,
1307
1346
  className,
1308
1347
  style,
1309
- components
1348
+ components: componentsProp
1310
1349
  }) {
1311
1350
  const resolvedMedia = useResolvedMedia(
1312
1351
  media,
@@ -1314,6 +1353,15 @@ function XPostPreviewImpl({
1314
1353
  altSignatureOf(variant.media)
1315
1354
  );
1316
1355
  const resolvedQuotedMedia = useResolvedMedia(quotedTweet?.media, void 0, "");
1356
+ const components = react.useMemo(
1357
+ () => ({
1358
+ MediaImg: makeRawMediaImg(
1359
+ rawSrcLookup(resolvedMedia, resolvedQuotedMedia)
1360
+ ),
1361
+ ...componentsProp
1362
+ }),
1363
+ [resolvedMedia, resolvedQuotedMedia, componentsProp]
1364
+ );
1317
1365
  const tweet = react.useMemo(() => {
1318
1366
  const resolvedQuoted = quotedTweet ? {
1319
1367
  author: quotedTweet.author,
@@ -1337,6 +1385,7 @@ function XPostPreviewImpl({
1337
1385
  resolvedQuotedMedia,
1338
1386
  replyToHandle
1339
1387
  ]);
1388
+ const isEmpty = !variant.body && resolvedMedia.length === 0 && !variant.settings?.poll && !tweet.quoted_tweet;
1340
1389
  return /* @__PURE__ */ jsxRuntime.jsx(
1341
1390
  "div",
1342
1391
  {
@@ -1346,7 +1395,7 @@ function XPostPreviewImpl({
1346
1395
  children: /* @__PURE__ */ jsxRuntime.jsxs(reactTweet.TweetContainer, { children: [
1347
1396
  /* @__PURE__ */ jsxRuntime.jsx(reactTweet.TweetHeader, { tweet, components }),
1348
1397
  tweet.in_reply_to_screen_name ? /* @__PURE__ */ jsxRuntime.jsx(reactTweet.TweetInReplyTo, { tweet }) : null,
1349
- /* @__PURE__ */ jsxRuntime.jsx(reactTweet.TweetBody, { tweet }),
1398
+ isEmpty ? /* @__PURE__ */ jsxRuntime.jsx(EmptyBody, {}) : /* @__PURE__ */ jsxRuntime.jsx(reactTweet.TweetBody, { tweet }),
1350
1399
  tweet.mediaDetails?.length ? /* @__PURE__ */ jsxRuntime.jsx(reactTweet.TweetMedia, { tweet, components }) : null,
1351
1400
  variant.settings?.poll ? /* @__PURE__ */ jsxRuntime.jsx(XPoll, { poll: variant.settings.poll }) : null,
1352
1401
  tweet.quoted_tweet ? /* @__PURE__ */ jsxRuntime.jsx(reactTweet.QuotedTweet, { tweet: tweet.quoted_tweet }) : null,
@@ -1355,6 +1404,18 @@ function XPostPreviewImpl({
1355
1404
  }
1356
1405
  );
1357
1406
  }
1407
+ function EmptyBody() {
1408
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { style: emptyBodyStyle, children: "What's happening?" });
1409
+ }
1410
+ var emptyBodyStyle = {
1411
+ margin: "var(--tweet-body-margin)",
1412
+ fontSize: "var(--tweet-body-font-size)",
1413
+ fontWeight: "var(--tweet-body-font-weight)",
1414
+ lineHeight: "var(--tweet-body-line-height)",
1415
+ color: "var(--tweet-font-color-secondary)",
1416
+ whiteSpace: "pre-wrap",
1417
+ overflowWrap: "break-word"
1418
+ };
1358
1419
  var XPostPreview = react.memo(XPostPreviewImpl);
1359
1420
 
1360
1421
  // src/preview/linkedin/theme.ts
@@ -1368,14 +1429,15 @@ var LI_VAR = {
1368
1429
  function varRef(name) {
1369
1430
  return `var(${name})`;
1370
1431
  }
1432
+ var PALETTE_VARS = {
1433
+ [LI_VAR.bg]: "light-dark(#ffffff, #1b1f23)",
1434
+ [LI_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.9))",
1435
+ [LI_VAR.muted]: "light-dark(rgba(0,0,0,0.6), rgba(255,255,255,0.6))",
1436
+ [LI_VAR.border]: "light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.15))",
1437
+ [LI_VAR.accent]: "light-dark(rgb(10,102,194), rgb(112,181,249))"
1438
+ };
1371
1439
  function paletteVars() {
1372
- return {
1373
- [LI_VAR.bg]: "light-dark(#ffffff, #1b1f23)",
1374
- [LI_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.9))",
1375
- [LI_VAR.muted]: "light-dark(rgba(0,0,0,0.6), rgba(255,255,255,0.6))",
1376
- [LI_VAR.border]: "light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.15))",
1377
- [LI_VAR.accent]: "light-dark(rgb(10,102,194), rgb(112,181,249))"
1378
- };
1440
+ return PALETTE_VARS;
1379
1441
  }
1380
1442
  function colorSchemeFor(theme) {
1381
1443
  return theme === "auto" ? "light dark" : theme;
@@ -1521,7 +1583,7 @@ function Header({ author, visibility, time = "Now" }) {
1521
1583
  /* @__PURE__ */ jsxRuntime.jsx(
1522
1584
  "img",
1523
1585
  {
1524
- src: author.avatarUrl ?? PLACEHOLDER_AVATAR2,
1586
+ src: author.avatar_url ?? PLACEHOLDER_AVATAR2,
1525
1587
  alt: "",
1526
1588
  width: 48,
1527
1589
  height: 48,
@@ -1620,7 +1682,6 @@ function Tile({
1620
1682
  overlay ? /* @__PURE__ */ jsxRuntime.jsx(
1621
1683
  "div",
1622
1684
  {
1623
- "aria-hidden": false,
1624
1685
  style: {
1625
1686
  position: "absolute",
1626
1687
  inset: 0,
@@ -1643,12 +1704,13 @@ function ImageMosaic({ media }) {
1643
1704
  return null;
1644
1705
  }
1645
1706
  if (media.length === 1) {
1707
+ const ratio = only.width && only.height ? `${only.width} / ${only.height}` : void 0;
1646
1708
  return /* @__PURE__ */ jsxRuntime.jsx(
1647
1709
  "img",
1648
1710
  {
1649
1711
  src: only.src,
1650
1712
  alt: only.alt ?? "",
1651
- style: { width: "100%", height: "auto", display: "block" }
1713
+ style: { width: "100%", height: "auto", display: "block", aspectRatio: ratio }
1652
1714
  }
1653
1715
  );
1654
1716
  }
@@ -1791,7 +1853,18 @@ function linkify(text, colors, mentionNames) {
1791
1853
  );
1792
1854
  } else if (token.startsWith("#")) {
1793
1855
  nodes.push(
1794
- /* @__PURE__ */ jsxRuntime.jsx("a", { href: "#", style: linkStyle(colors.accent), children: token }, key++)
1856
+ // Preview-only hashtag: no real destination, so prevent the host SPA from
1857
+ // navigating / scrolling to top on click.
1858
+ /* @__PURE__ */ jsxRuntime.jsx(
1859
+ "a",
1860
+ {
1861
+ href: "#",
1862
+ style: linkStyle(colors.accent),
1863
+ onClick: (e) => e.preventDefault(),
1864
+ children: token
1865
+ },
1866
+ key++
1867
+ )
1795
1868
  );
1796
1869
  } else {
1797
1870
  nodes.push(
@@ -1809,32 +1882,41 @@ function PostBody({ text, mentionNames = [], colors }) {
1809
1882
  const [expanded, setExpanded] = react.useState(false);
1810
1883
  const isLong = text.length > FOLD_CHARS;
1811
1884
  const shown = isLong && !expanded ? truncateAtWord(text, FOLD_CHARS) : text;
1885
+ const nodes = react.useMemo(
1886
+ () => linkify(shown, colors, mentionNames),
1887
+ [shown, colors, mentionNames]
1888
+ );
1889
+ const toggleStyle2 = {
1890
+ background: "none",
1891
+ border: "none",
1892
+ padding: 0,
1893
+ cursor: "pointer",
1894
+ color: colors.muted,
1895
+ fontWeight: 600,
1896
+ fontSize: 14
1897
+ };
1812
1898
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: BODY_STYLE, children: [
1813
- linkify(shown, colors, mentionNames),
1814
- isLong && !expanded ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1815
- "\u2026",
1816
- " ",
1899
+ nodes,
1900
+ isLong ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1901
+ !expanded ? "\u2026 " : " ",
1817
1902
  /* @__PURE__ */ jsxRuntime.jsx(
1818
1903
  "button",
1819
1904
  {
1820
1905
  type: "button",
1821
- onClick: () => setExpanded(true),
1822
- style: {
1823
- background: "none",
1824
- border: "none",
1825
- padding: 0,
1826
- cursor: "pointer",
1827
- color: colors.muted,
1828
- fontWeight: 600,
1829
- fontSize: 14
1830
- },
1831
- children: "more"
1906
+ "aria-expanded": expanded,
1907
+ onClick: () => setExpanded((v) => !v),
1908
+ style: toggleStyle2,
1909
+ children: expanded ? "see less" : "more"
1832
1910
  }
1833
1911
  )
1834
1912
  ] }) : null
1835
1913
  ] });
1836
1914
  }
1837
1915
  var FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
1916
+ var BODY_COLORS = {
1917
+ accent: varRef(LI_VAR.accent),
1918
+ muted: varRef(LI_VAR.muted)
1919
+ };
1838
1920
  function LinkedInPostPreviewImpl({
1839
1921
  variant,
1840
1922
  author,
@@ -1851,11 +1933,10 @@ function LinkedInPostPreviewImpl({
1851
1933
  altSignatureOf(variant.media)
1852
1934
  );
1853
1935
  const visibility = variant.settings?.visibility ?? "PUBLIC";
1854
- const mentionNames = (variant.settings?.mentions ?? []).map((m) => m.name);
1855
- const bodyColors = {
1856
- accent: varRef(LI_VAR.accent),
1857
- muted: varRef(LI_VAR.muted)
1858
- };
1936
+ const mentionNames = react.useMemo(
1937
+ () => (variant.settings?.mentions ?? []).map((m) => m.name),
1938
+ [variant.settings?.mentions]
1939
+ );
1859
1940
  const cardStyle5 = {
1860
1941
  ...paletteVars(),
1861
1942
  colorScheme: colorSchemeFor(theme),
@@ -1863,11 +1944,16 @@ function LinkedInPostPreviewImpl({
1863
1944
  color: varRef(LI_VAR.text),
1864
1945
  border: `1px solid ${varRef(LI_VAR.border)}`,
1865
1946
  borderRadius: 10,
1947
+ // Pin the width (see InstagramPostPreview): without it the card shrinks to its
1948
+ // content, so an empty post collapses to a fraction of a populated card's width.
1949
+ width: "100%",
1866
1950
  maxWidth: 552,
1951
+ boxSizing: "border-box",
1867
1952
  overflow: "hidden",
1868
1953
  fontFamily: FONT_STACK,
1869
1954
  ...style
1870
1955
  };
1956
+ const hasContent = Boolean(variant.body) || resolvedMedia.length > 0 || Boolean(variant.settings?.content_kind && variant.settings.content_kind !== "text");
1871
1957
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: cardStyle5, children: [
1872
1958
  /* @__PURE__ */ jsxRuntime.jsx(Header, { author, visibility, time }),
1873
1959
  variant.body ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "8px 16px 0" }, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -1875,13 +1961,20 @@ function LinkedInPostPreviewImpl({
1875
1961
  {
1876
1962
  text: variant.body,
1877
1963
  mentionNames,
1878
- colors: bodyColors
1964
+ colors: BODY_COLORS
1879
1965
  }
1880
1966
  ) }) : null,
1881
1967
  renderContent(variant, resolvedMedia),
1968
+ hasContent ? null : /* @__PURE__ */ jsxRuntime.jsx("p", { style: { ...emptyBodyStyle2, color: BODY_COLORS.muted }, children: "What do you want to talk about?" }),
1882
1969
  showActions ? /* @__PURE__ */ jsxRuntime.jsx(EngagementBar, {}) : null
1883
1970
  ] });
1884
1971
  }
1972
+ var emptyBodyStyle2 = {
1973
+ margin: 0,
1974
+ padding: "8px 16px 4px",
1975
+ fontSize: 14,
1976
+ lineHeight: 1.4
1977
+ };
1885
1978
  function renderContent(variant, media) {
1886
1979
  const settings = variant.settings;
1887
1980
  const kind = settings?.content_kind;
@@ -1911,14 +2004,15 @@ var IG_VAR = {
1911
2004
  function varRef2(name) {
1912
2005
  return `var(${name})`;
1913
2006
  }
2007
+ var PALETTE_VARS2 = {
2008
+ [IG_VAR.bg]: "light-dark(#ffffff, #000000)",
2009
+ [IG_VAR.text]: "light-dark(#000000, #f5f5f5)",
2010
+ [IG_VAR.muted]: "light-dark(#737373, #a8a8a8)",
2011
+ [IG_VAR.border]: "light-dark(#dbdbdb, #262626)",
2012
+ [IG_VAR.accent]: "light-dark(#00376b, #e0f1ff)"
2013
+ };
1914
2014
  function paletteVars2() {
1915
- return {
1916
- [IG_VAR.bg]: "light-dark(#ffffff, #000000)",
1917
- [IG_VAR.text]: "light-dark(#000000, #f5f5f5)",
1918
- [IG_VAR.muted]: "light-dark(#737373, #a8a8a8)",
1919
- [IG_VAR.border]: "light-dark(#dbdbdb, #262626)",
1920
- [IG_VAR.accent]: "light-dark(#00376b, #e0f1ff)"
1921
- };
2015
+ return PALETTE_VARS2;
1922
2016
  }
1923
2017
  function colorSchemeFor2(theme) {
1924
2018
  return theme === "auto" ? "light dark" : theme;
@@ -1959,7 +2053,14 @@ function ExpandableText({
1959
2053
  if (!el || expanded) {
1960
2054
  return;
1961
2055
  }
1962
- setOverflowing(el.scrollHeight > el.clientHeight + 1);
2056
+ const measure = () => setOverflowing(el.scrollHeight > el.clientHeight + 1);
2057
+ measure();
2058
+ if (typeof ResizeObserver === "undefined") {
2059
+ return;
2060
+ }
2061
+ const ro = new ResizeObserver(measure);
2062
+ ro.observe(el);
2063
+ return () => ro.disconnect();
1963
2064
  }, [expanded, children]);
1964
2065
  const textStyle = expanded ? FULL : { ...FULL, ...clampStyle(lines) };
1965
2066
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style, children: [
@@ -2013,10 +2114,10 @@ function Caption({
2013
2114
  toggleColor: varRef2(IG_VAR.muted),
2014
2115
  style: wrapStyle3,
2015
2116
  children: [
2016
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 600 }, children: [
2117
+ username ? /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 600 }, children: [
2017
2118
  username,
2018
2119
  " "
2019
- ] }),
2120
+ ] }) : null,
2020
2121
  parts.map(
2021
2122
  (part, i) => IS_TOKEN.test(part) ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: varRef2(IG_VAR.accent) }, children: part }, i) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: part }, i)
2022
2123
  )
@@ -2030,9 +2131,57 @@ var wrapStyle3 = {
2030
2131
  lineHeight: 1.4,
2031
2132
  color: varRef2(IG_VAR.text)
2032
2133
  };
2033
- function FeedMedia({ media }) {
2134
+ function MediaPlaceholder({
2135
+ label,
2136
+ color,
2137
+ background,
2138
+ shimmer = false
2139
+ }) {
2140
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...fillStyle, background, color }, children: [
2141
+ shimmer ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2142
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: SHIMMER_KEYFRAME }),
2143
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: shimmerStyle })
2144
+ ] }) : null,
2145
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: labelStyle, children: label })
2146
+ ] });
2147
+ }
2148
+ var SHIMMER_KEYFRAME = "@keyframes pr-media-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}";
2149
+ var fillStyle = {
2150
+ position: "absolute",
2151
+ inset: 0,
2152
+ display: "flex",
2153
+ alignItems: "center",
2154
+ justifyContent: "center",
2155
+ overflow: "hidden"
2156
+ };
2157
+ var labelStyle = {
2158
+ position: "relative",
2159
+ zIndex: 1,
2160
+ fontSize: 13,
2161
+ letterSpacing: 0.1
2162
+ };
2163
+ var shimmerStyle = {
2164
+ position: "absolute",
2165
+ inset: 0,
2166
+ background: "linear-gradient(90deg, transparent 0%, color-mix(in srgb, currentColor 12%, transparent) 50%, transparent 100%)",
2167
+ backgroundSize: "200% 100%",
2168
+ animation: "pr-media-shimmer 1.4s ease-in-out infinite"
2169
+ };
2170
+ function FeedMedia({
2171
+ media,
2172
+ pending = false
2173
+ }) {
2034
2174
  if (media.length <= 1) {
2035
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: frameStyle, children: media[0] ? /* @__PURE__ */ jsxRuntime.jsx(Tile2, { item: media[0] }) : null });
2175
+ const item = media[0];
2176
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: frameStyle, children: item ? /* @__PURE__ */ jsxRuntime.jsx(Tile2, { item }) : /* @__PURE__ */ jsxRuntime.jsx(
2177
+ MediaPlaceholder,
2178
+ {
2179
+ label: pending ? "Processing media\u2026" : "No media yet",
2180
+ color: varRef2(IG_VAR.muted),
2181
+ background: EMPTY_BG,
2182
+ shimmer: pending
2183
+ }
2184
+ ) });
2036
2185
  }
2037
2186
  return /* @__PURE__ */ jsxRuntime.jsx(Carousel, { media });
2038
2187
  }
@@ -2059,7 +2208,7 @@ function Carousel({ media }) {
2059
2208
  };
2060
2209
  }, [emblaApi, sync]);
2061
2210
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: frameStyle, children: [
2062
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: emblaRef, style: { position: "absolute", inset: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", height: "100%" }, children: media.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: slideStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Tile2, { item }) }, `${item.src}-${i}`)) }) }),
2211
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: emblaRef, style: { position: "absolute", inset: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", height: "100%" }, children: media.map((item) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: slideStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Tile2, { item }) }, item.src)) }) }),
2063
2212
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: pillStyle, children: [
2064
2213
  selected + 1,
2065
2214
  "/",
@@ -2070,7 +2219,7 @@ function Carousel({ media }) {
2070
2219
  {
2071
2220
  style: { ...dotStyle, opacity: i === selected ? 1 : 0.5 }
2072
2221
  },
2073
- `dot-${item.src}-${i}`
2222
+ `dot-${item.src}`
2074
2223
  )) })
2075
2224
  ] });
2076
2225
  }
@@ -2081,6 +2230,7 @@ var frameStyle = {
2081
2230
  background: "#000",
2082
2231
  overflow: "hidden"
2083
2232
  };
2233
+ var EMPTY_BG = "light-dark(#efefef, #1c1c1c)";
2084
2234
  var mediaStyle = {
2085
2235
  position: "absolute",
2086
2236
  inset: 0,
@@ -2195,12 +2345,13 @@ function ReelPreview({
2195
2345
  author,
2196
2346
  body,
2197
2347
  media,
2348
+ pending = false,
2198
2349
  audioName,
2199
2350
  className,
2200
2351
  style
2201
2352
  }) {
2202
2353
  const first = media[0];
2203
- const username = author.username ?? "";
2354
+ const username = author.username;
2204
2355
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...cardStyle3, ...style }, children: [
2205
2356
  first ? first.kind === "video" ? /* @__PURE__ */ jsxRuntime.jsx(
2206
2357
  "video",
@@ -2213,14 +2364,30 @@ function ReelPreview({
2213
2364
  playsInline: true,
2214
2365
  style: coverStyle2
2215
2366
  }
2216
- ) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: first.src, alt: first.alt ?? "", style: coverStyle2 }) : null,
2367
+ ) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: first.src, alt: first.alt ?? "", style: coverStyle2 }) : /* @__PURE__ */ jsxRuntime.jsx(
2368
+ MediaPlaceholder,
2369
+ {
2370
+ label: pending ? "Processing media\u2026" : "No media yet",
2371
+ color: "rgba(255,255,255,0.55)",
2372
+ background: "transparent",
2373
+ shimmer: pending
2374
+ }
2375
+ ),
2217
2376
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: scrimStyle }),
2218
2377
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: infoStyle, children: [
2219
2378
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: authorRowStyle, children: [
2220
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 600, fontSize: 14 }, children: username }),
2379
+ username ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 600, fontSize: 14 }, children: username }) : null,
2221
2380
  author.verified ? /* @__PURE__ */ jsxRuntime.jsx(md.MdVerified, { size: 13, "aria-label": "Verified", role: "img", style: { color: "#fff" } }) : null
2222
2381
  ] }),
2223
- body ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: captionStyle, children: body }) : null,
2382
+ body ? /* @__PURE__ */ jsxRuntime.jsx(
2383
+ ExpandableText,
2384
+ {
2385
+ lines: 2,
2386
+ toggleColor: "rgba(255,255,255,0.7)",
2387
+ style: captionStyle,
2388
+ children: body
2389
+ }
2390
+ ) : null,
2224
2391
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: audioStyle, children: [
2225
2392
  /* @__PURE__ */ jsxRuntime.jsx(md.MdMusicNote, { size: 14, "aria-hidden": true }),
2226
2393
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: audioName ?? "Original audio" })
@@ -2276,11 +2443,7 @@ var authorRowStyle = {
2276
2443
  };
2277
2444
  var captionStyle = {
2278
2445
  fontSize: 13,
2279
- lineHeight: 1.35,
2280
- display: "-webkit-box",
2281
- WebkitLineClamp: 2,
2282
- WebkitBoxOrient: "vertical",
2283
- overflow: "hidden"
2446
+ lineHeight: 1.35
2284
2447
  };
2285
2448
  var audioStyle = {
2286
2449
  display: "flex",
@@ -2313,7 +2476,8 @@ function InstagramPostPreviewImpl({
2313
2476
  altSignatureOf(variant.media)
2314
2477
  );
2315
2478
  const settings = variant.settings;
2316
- const isReel = variant.post_type === "reel" || settings?.media_type === "REELS";
2479
+ const isReel = variant.post_type === "reel";
2480
+ const pending = (media?.length ?? 0) > 0 && resolvedMedia.length === 0;
2317
2481
  if (isReel) {
2318
2482
  return /* @__PURE__ */ jsxRuntime.jsx(
2319
2483
  ReelPreview,
@@ -2321,6 +2485,7 @@ function InstagramPostPreviewImpl({
2321
2485
  author,
2322
2486
  body: variant.body ?? "",
2323
2487
  media: resolvedMedia,
2488
+ pending,
2324
2489
  audioName: settings?.audio_name,
2325
2490
  className,
2326
2491
  style
@@ -2335,16 +2500,22 @@ function InstagramPostPreviewImpl({
2335
2500
  color: varRef2(IG_VAR.text),
2336
2501
  border: `1px solid ${varRef2(IG_VAR.border)}`,
2337
2502
  borderRadius: 8,
2503
+ // Fill the container up to IG's feed width. WITHOUT an explicit width the card
2504
+ // shrinks to its content, so an empty post (no caption/media to widen it) would
2505
+ // collapse to a fraction of a populated card's width — a jarring size change as
2506
+ // you type. Pinning width keeps every state the SAME size (zero layout shift).
2507
+ width: "100%",
2338
2508
  maxWidth: 470,
2509
+ boxSizing: "border-box",
2339
2510
  overflow: "hidden",
2340
2511
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
2341
2512
  ...style
2342
2513
  };
2343
2514
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: cardStyle5, children: [
2344
2515
  /* @__PURE__ */ jsxRuntime.jsx(Header2, { author, collaborators }),
2345
- /* @__PURE__ */ jsxRuntime.jsx(FeedMedia, { media: resolvedMedia }),
2516
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMedia, { media: resolvedMedia, pending }),
2346
2517
  /* @__PURE__ */ jsxRuntime.jsx(Actions, {}),
2347
- variant.body ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBottom: 12 }, children: /* @__PURE__ */ jsxRuntime.jsx(Caption, { username: author.username ?? "", body: variant.body }) }) : null
2518
+ variant.body ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBottom: 12 }, children: /* @__PURE__ */ jsxRuntime.jsx(Caption, { username: author.username, body: variant.body }) }) : null
2348
2519
  ] });
2349
2520
  }
2350
2521
  var InstagramPostPreview = react.memo(InstagramPostPreviewImpl);
@@ -2367,7 +2538,15 @@ function Media2({
2367
2538
  }) {
2368
2539
  const [first] = media;
2369
2540
  if (!first) {
2370
- return pending ? /* @__PURE__ */ jsxRuntime.jsx(ProcessingMedia, {}) : /* @__PURE__ */ jsxRuntime.jsx(EmptyMedia, {});
2541
+ return /* @__PURE__ */ jsxRuntime.jsx(
2542
+ MediaPlaceholder,
2543
+ {
2544
+ label: pending ? "Processing media\u2026" : "No media yet",
2545
+ color: "rgba(255,255,255,0.55)",
2546
+ background: "linear-gradient(180deg,#1c1c20,#101013)",
2547
+ shimmer: pending
2548
+ }
2549
+ );
2371
2550
  }
2372
2551
  if (first.kind === "video") {
2373
2552
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2416,15 +2595,6 @@ function PhotoCarousel({ media }) {
2416
2595
  )) }) : null
2417
2596
  ] });
2418
2597
  }
2419
- function EmptyMedia() {
2420
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: emptyStyle, children: "No media yet" });
2421
- }
2422
- function ProcessingMedia() {
2423
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: emptyStyle, children: [
2424
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: shimmerStyle }),
2425
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { position: "relative", zIndex: 1 }, children: "Processing media\u2026" })
2426
- ] });
2427
- }
2428
2598
  var coverStyle3 = {
2429
2599
  position: "absolute",
2430
2600
  inset: 0,
@@ -2463,24 +2633,6 @@ var dotStyle2 = {
2463
2633
  background: "#fff",
2464
2634
  transition: "opacity 160ms ease"
2465
2635
  };
2466
- var emptyStyle = {
2467
- position: "absolute",
2468
- inset: 0,
2469
- display: "flex",
2470
- alignItems: "center",
2471
- justifyContent: "center",
2472
- color: "rgba(255,255,255,0.55)",
2473
- fontSize: 13,
2474
- background: "linear-gradient(180deg,#1c1c20,#101013)",
2475
- overflow: "hidden"
2476
- };
2477
- var shimmerStyle = {
2478
- position: "absolute",
2479
- inset: 0,
2480
- background: "linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0) 100%)",
2481
- backgroundSize: "200% 100%",
2482
- animation: "pr-tt-shimmer 1.4s ease-in-out infinite"
2483
- };
2484
2636
  var Icon = ({
2485
2637
  size = 24,
2486
2638
  viewBox,
@@ -2728,7 +2880,7 @@ var discImgStyle = {
2728
2880
  borderRadius: "50%",
2729
2881
  objectFit: "cover"
2730
2882
  };
2731
- var KEYFRAMES = `@keyframes pr-tt-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pr-tt-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}`;
2883
+ var KEYFRAMES = `@keyframes pr-tt-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`;
2732
2884
  var TikTokPostPreview = react.memo(TikTokPostPreviewImpl);
2733
2885
 
2734
2886
  // src/preview/tiktok/theme.ts
@@ -2742,14 +2894,15 @@ var TT_VAR = {
2742
2894
  function varRef3(name) {
2743
2895
  return `var(${name})`;
2744
2896
  }
2897
+ var PALETTE_VARS3 = {
2898
+ [TT_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.92))",
2899
+ [TT_VAR.muted]: "light-dark(rgba(0,0,0,0.55), #8a8a92)",
2900
+ [TT_VAR.border]: "light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.14))",
2901
+ [TT_VAR.accent]: "#fe2c55",
2902
+ [TT_VAR.switchOff]: "light-dark(rgba(0,0,0,0.18), rgba(255,255,255,0.22))"
2903
+ };
2745
2904
  function paletteVars3() {
2746
- return {
2747
- [TT_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.92))",
2748
- [TT_VAR.muted]: "light-dark(rgba(0,0,0,0.55), #8a8a92)",
2749
- [TT_VAR.border]: "light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.14))",
2750
- [TT_VAR.accent]: "#fe2c55",
2751
- [TT_VAR.switchOff]: "light-dark(rgba(0,0,0,0.18), rgba(255,255,255,0.22))"
2752
- };
2905
+ return PALETTE_VARS3;
2753
2906
  }
2754
2907
  function colorSchemeFor3(theme) {
2755
2908
  return theme === "auto" ? "light dark" : theme;
@@ -2765,11 +2918,17 @@ function Row({
2765
2918
  control,
2766
2919
  htmlFor
2767
2920
  }) {
2921
+ const text = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2922
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14 }, children: label }),
2923
+ sublabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: MUTED2 }, children: sublabel }) : null
2924
+ ] });
2925
+ const textStyle = {
2926
+ display: "flex",
2927
+ flexDirection: "column",
2928
+ gap: 2
2929
+ };
2768
2930
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: rowStyle3, children: [
2769
- /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor, style: { display: "flex", flexDirection: "column", gap: 2 }, children: [
2770
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14 }, children: label }),
2771
- sublabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: MUTED2 }, children: sublabel }) : null
2772
- ] }),
2931
+ htmlFor ? /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor, style: textStyle, children: text }) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: textStyle, children: text }),
2773
2932
  control
2774
2933
  ] });
2775
2934
  }
@@ -2891,7 +3050,6 @@ function AudienceSelect({
2891
3050
  value,
2892
3051
  onChange
2893
3052
  }) {
2894
- const id = react.useId();
2895
3053
  const options = js.audiencePrivacyOptions(creatorInfo.privacy_options, value);
2896
3054
  const unselected = js.audienceUnselected(value);
2897
3055
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
@@ -2899,7 +3057,7 @@ function AudienceSelect({
2899
3057
  /* @__PURE__ */ jsxRuntime.jsxs(
2900
3058
  "select",
2901
3059
  {
2902
- id,
3060
+ "aria-label": "Who can view this post",
2903
3061
  value: value.privacy_level ?? "",
2904
3062
  onChange: (e) => onChange({
2905
3063
  ...value,
@@ -2944,7 +3102,7 @@ function TikTokCaptionField({
2944
3102
  const over = count > max;
2945
3103
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...wrapStyle5, ...style }, children: [
2946
3104
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: labelRowStyle, children: [
2947
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, style: labelStyle, children: "Caption" }),
3105
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, style: labelStyle2, children: "Caption" }),
2948
3106
  /* @__PURE__ */ jsxRuntime.jsxs(
2949
3107
  "span",
2950
3108
  {
@@ -2987,7 +3145,7 @@ var labelRowStyle = {
2987
3145
  alignItems: "baseline",
2988
3146
  justifyContent: "space-between"
2989
3147
  };
2990
- var labelStyle = {
3148
+ var labelStyle2 = {
2991
3149
  fontSize: 13,
2992
3150
  fontWeight: 600,
2993
3151
  color: "inherit"