@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { createContext, memo, useMemo, useState, useEffect, Fragment as Fragment$1, useCallback, useRef, useLayoutEffect, createElement, useContext, useId } from 'react';
2
+ import { createContext, memo, useMemo, useState, useEffect, Fragment as Fragment$1, useRef, useLayoutEffect, useCallback, createElement, useContext, useId } from 'react';
3
3
  import { useInfiniteQuery, useQuery, useMutation, QueryClient } from '@tanstack/react-query';
4
4
  import { xPollDurationLabel, linkedinPollDurationLabel, createPostrunClient, profilesList, profilesGet, profilesCreate, profilesUpdate, profilesDelete, connectionsConnect, connectionsListByProfile, connectionsGet, connectionsListAccounts, connectionsSelect, connectionsDelete, mediaGet, mediaList, mediaUpdate, mediaDelete, postsList, postsGet, postsCreate, buildCreatePost, isPostPlatform, failedVariants, isPublished, postsValidate, postsUpdate, postsDelete, tiktokCreatorInfo, audiencePrivacyOptions, audienceUnselected, tiktokPrivacyLabel, parsePrivacyLevel, TIKTOK_AUDIENCE_REQUIRED_HINT, captionMaxFor, commercialLabelNotice, commercialDisclosureIncomplete, setCommercialDisclosure, setBrandKind, TIKTOK_DISCLOSURE_REQUIRED_HINT, TIKTOK_BRANDED_CONTENT_POLICY_URL, TIKTOK_MUSIC_CONFIRMATION_URL, interactionRows, interactionValueKey, toggleInteraction, tiktokOptionsReady, TIKTOK_PROCESSING_NOTICE, brandedContentDeclared, mediaCreate, PostrunError } from '@postrun/js';
5
5
  export { TIKTOK_CAPTION_MAX, captionMaxFor } from '@postrun/js';
@@ -1114,6 +1114,38 @@ var ACTIONS = [
1114
1114
  function XPreviewActions() {
1115
1115
  return /* @__PURE__ */ jsx("div", { style: ROW, children: ACTIONS.map(({ label, Icon: Icon2 }) => /* @__PURE__ */ jsx(Icon2, { size: 18, "aria-label": label, role: "img" }, label)) });
1116
1116
  }
1117
+ function twitterMediaUrl(src) {
1118
+ try {
1119
+ const url = new URL(src);
1120
+ const extension = url.pathname.split(".").pop();
1121
+ if (!extension) {
1122
+ return src;
1123
+ }
1124
+ url.pathname = url.pathname.replace(`.${extension}`, "");
1125
+ url.searchParams.set("format", extension);
1126
+ url.searchParams.set("name", "small");
1127
+ return url.toString();
1128
+ } catch {
1129
+ return src;
1130
+ }
1131
+ }
1132
+ function rawSrcLookup(...lists) {
1133
+ const map = /* @__PURE__ */ new Map();
1134
+ for (const list of lists) {
1135
+ for (const item of list) {
1136
+ const poster = item.posterSrc ?? item.src;
1137
+ map.set(twitterMediaUrl(item.src), item.src);
1138
+ map.set(twitterMediaUrl(poster), poster);
1139
+ }
1140
+ }
1141
+ return map;
1142
+ }
1143
+ function makeRawMediaImg(rawBySrc) {
1144
+ return function RawMediaImg(props) {
1145
+ const raw = typeof props.src === "string" ? rawBySrc.get(props.src) ?? props.src : props.src;
1146
+ return /* @__PURE__ */ jsx("img", { ...props, src: raw });
1147
+ };
1148
+ }
1117
1149
  var {
1118
1150
  extractCashtagsWithIndices,
1119
1151
  extractHashtagsWithIndices,
@@ -1164,9 +1196,12 @@ function buildUser(author) {
1164
1196
  return {
1165
1197
  id_str: "",
1166
1198
  name: author.name,
1167
- profile_image_url_https: author.avatarUrl ?? PLACEHOLDER_AVATAR,
1199
+ profile_image_url_https: author.avatar_url ?? PLACEHOLDER_AVATAR,
1168
1200
  profile_image_shape: "Circle",
1169
- screen_name: author.handle,
1201
+ // `username`/`avatar_url` derive from the SDK Connection and are nullable.
1202
+ screen_name: author.username ?? "",
1203
+ // Legacy (pre-X-era) verification flag — always false for organic posts; the
1204
+ // X Blue checkmark rides on `is_blue_verified`.
1170
1205
  verified: false,
1171
1206
  is_blue_verified: verified
1172
1207
  };
@@ -1229,7 +1264,11 @@ function buildQuoted(quote, hasQuoteId) {
1229
1264
  if (!quote && !hasQuoteId) {
1230
1265
  return void 0;
1231
1266
  }
1232
- const author = quote?.author ?? { name: "Quoted post", handle: "" };
1267
+ const author = quote?.author ?? {
1268
+ name: "Quoted post",
1269
+ username: "",
1270
+ avatar_url: null
1271
+ };
1233
1272
  const body = quote?.body ?? "";
1234
1273
  const media = quote?.media ?? [];
1235
1274
  return {
@@ -1295,7 +1334,7 @@ function XPostPreviewImpl({
1295
1334
  showActions = true,
1296
1335
  className,
1297
1336
  style,
1298
- components
1337
+ components: componentsProp
1299
1338
  }) {
1300
1339
  const resolvedMedia = useResolvedMedia(
1301
1340
  media,
@@ -1303,6 +1342,15 @@ function XPostPreviewImpl({
1303
1342
  altSignatureOf(variant.media)
1304
1343
  );
1305
1344
  const resolvedQuotedMedia = useResolvedMedia(quotedTweet?.media, void 0, "");
1345
+ const components = useMemo(
1346
+ () => ({
1347
+ MediaImg: makeRawMediaImg(
1348
+ rawSrcLookup(resolvedMedia, resolvedQuotedMedia)
1349
+ ),
1350
+ ...componentsProp
1351
+ }),
1352
+ [resolvedMedia, resolvedQuotedMedia, componentsProp]
1353
+ );
1306
1354
  const tweet = useMemo(() => {
1307
1355
  const resolvedQuoted = quotedTweet ? {
1308
1356
  author: quotedTweet.author,
@@ -1326,6 +1374,7 @@ function XPostPreviewImpl({
1326
1374
  resolvedQuotedMedia,
1327
1375
  replyToHandle
1328
1376
  ]);
1377
+ const isEmpty = !variant.body && resolvedMedia.length === 0 && !variant.settings?.poll && !tweet.quoted_tweet;
1329
1378
  return /* @__PURE__ */ jsx(
1330
1379
  "div",
1331
1380
  {
@@ -1335,7 +1384,7 @@ function XPostPreviewImpl({
1335
1384
  children: /* @__PURE__ */ jsxs(TweetContainer, { children: [
1336
1385
  /* @__PURE__ */ jsx(TweetHeader, { tweet, components }),
1337
1386
  tweet.in_reply_to_screen_name ? /* @__PURE__ */ jsx(TweetInReplyTo, { tweet }) : null,
1338
- /* @__PURE__ */ jsx(TweetBody, { tweet }),
1387
+ isEmpty ? /* @__PURE__ */ jsx(EmptyBody, {}) : /* @__PURE__ */ jsx(TweetBody, { tweet }),
1339
1388
  tweet.mediaDetails?.length ? /* @__PURE__ */ jsx(TweetMedia, { tweet, components }) : null,
1340
1389
  variant.settings?.poll ? /* @__PURE__ */ jsx(XPoll, { poll: variant.settings.poll }) : null,
1341
1390
  tweet.quoted_tweet ? /* @__PURE__ */ jsx(QuotedTweet, { tweet: tweet.quoted_tweet }) : null,
@@ -1344,6 +1393,18 @@ function XPostPreviewImpl({
1344
1393
  }
1345
1394
  );
1346
1395
  }
1396
+ function EmptyBody() {
1397
+ return /* @__PURE__ */ jsx("p", { style: emptyBodyStyle, children: "What's happening?" });
1398
+ }
1399
+ var emptyBodyStyle = {
1400
+ margin: "var(--tweet-body-margin)",
1401
+ fontSize: "var(--tweet-body-font-size)",
1402
+ fontWeight: "var(--tweet-body-font-weight)",
1403
+ lineHeight: "var(--tweet-body-line-height)",
1404
+ color: "var(--tweet-font-color-secondary)",
1405
+ whiteSpace: "pre-wrap",
1406
+ overflowWrap: "break-word"
1407
+ };
1347
1408
  var XPostPreview = memo(XPostPreviewImpl);
1348
1409
 
1349
1410
  // src/preview/linkedin/theme.ts
@@ -1357,14 +1418,15 @@ var LI_VAR = {
1357
1418
  function varRef(name) {
1358
1419
  return `var(${name})`;
1359
1420
  }
1421
+ var PALETTE_VARS = {
1422
+ [LI_VAR.bg]: "light-dark(#ffffff, #1b1f23)",
1423
+ [LI_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.9))",
1424
+ [LI_VAR.muted]: "light-dark(rgba(0,0,0,0.6), rgba(255,255,255,0.6))",
1425
+ [LI_VAR.border]: "light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.15))",
1426
+ [LI_VAR.accent]: "light-dark(rgb(10,102,194), rgb(112,181,249))"
1427
+ };
1360
1428
  function paletteVars() {
1361
- return {
1362
- [LI_VAR.bg]: "light-dark(#ffffff, #1b1f23)",
1363
- [LI_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.9))",
1364
- [LI_VAR.muted]: "light-dark(rgba(0,0,0,0.6), rgba(255,255,255,0.6))",
1365
- [LI_VAR.border]: "light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.15))",
1366
- [LI_VAR.accent]: "light-dark(rgb(10,102,194), rgb(112,181,249))"
1367
- };
1429
+ return PALETTE_VARS;
1368
1430
  }
1369
1431
  function colorSchemeFor(theme) {
1370
1432
  return theme === "auto" ? "light dark" : theme;
@@ -1510,7 +1572,7 @@ function Header({ author, visibility, time = "Now" }) {
1510
1572
  /* @__PURE__ */ jsx(
1511
1573
  "img",
1512
1574
  {
1513
- src: author.avatarUrl ?? PLACEHOLDER_AVATAR2,
1575
+ src: author.avatar_url ?? PLACEHOLDER_AVATAR2,
1514
1576
  alt: "",
1515
1577
  width: 48,
1516
1578
  height: 48,
@@ -1609,7 +1671,6 @@ function Tile({
1609
1671
  overlay ? /* @__PURE__ */ jsx(
1610
1672
  "div",
1611
1673
  {
1612
- "aria-hidden": false,
1613
1674
  style: {
1614
1675
  position: "absolute",
1615
1676
  inset: 0,
@@ -1632,12 +1693,13 @@ function ImageMosaic({ media }) {
1632
1693
  return null;
1633
1694
  }
1634
1695
  if (media.length === 1) {
1696
+ const ratio = only.width && only.height ? `${only.width} / ${only.height}` : void 0;
1635
1697
  return /* @__PURE__ */ jsx(
1636
1698
  "img",
1637
1699
  {
1638
1700
  src: only.src,
1639
1701
  alt: only.alt ?? "",
1640
- style: { width: "100%", height: "auto", display: "block" }
1702
+ style: { width: "100%", height: "auto", display: "block", aspectRatio: ratio }
1641
1703
  }
1642
1704
  );
1643
1705
  }
@@ -1780,7 +1842,18 @@ function linkify(text, colors, mentionNames) {
1780
1842
  );
1781
1843
  } else if (token.startsWith("#")) {
1782
1844
  nodes.push(
1783
- /* @__PURE__ */ jsx("a", { href: "#", style: linkStyle(colors.accent), children: token }, key++)
1845
+ // Preview-only hashtag: no real destination, so prevent the host SPA from
1846
+ // navigating / scrolling to top on click.
1847
+ /* @__PURE__ */ jsx(
1848
+ "a",
1849
+ {
1850
+ href: "#",
1851
+ style: linkStyle(colors.accent),
1852
+ onClick: (e) => e.preventDefault(),
1853
+ children: token
1854
+ },
1855
+ key++
1856
+ )
1784
1857
  );
1785
1858
  } else {
1786
1859
  nodes.push(
@@ -1798,32 +1871,41 @@ function PostBody({ text, mentionNames = [], colors }) {
1798
1871
  const [expanded, setExpanded] = useState(false);
1799
1872
  const isLong = text.length > FOLD_CHARS;
1800
1873
  const shown = isLong && !expanded ? truncateAtWord(text, FOLD_CHARS) : text;
1874
+ const nodes = useMemo(
1875
+ () => linkify(shown, colors, mentionNames),
1876
+ [shown, colors, mentionNames]
1877
+ );
1878
+ const toggleStyle2 = {
1879
+ background: "none",
1880
+ border: "none",
1881
+ padding: 0,
1882
+ cursor: "pointer",
1883
+ color: colors.muted,
1884
+ fontWeight: 600,
1885
+ fontSize: 14
1886
+ };
1801
1887
  return /* @__PURE__ */ jsxs("div", { style: BODY_STYLE, children: [
1802
- linkify(shown, colors, mentionNames),
1803
- isLong && !expanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
1804
- "\u2026",
1805
- " ",
1888
+ nodes,
1889
+ isLong ? /* @__PURE__ */ jsxs(Fragment, { children: [
1890
+ !expanded ? "\u2026 " : " ",
1806
1891
  /* @__PURE__ */ jsx(
1807
1892
  "button",
1808
1893
  {
1809
1894
  type: "button",
1810
- onClick: () => setExpanded(true),
1811
- style: {
1812
- background: "none",
1813
- border: "none",
1814
- padding: 0,
1815
- cursor: "pointer",
1816
- color: colors.muted,
1817
- fontWeight: 600,
1818
- fontSize: 14
1819
- },
1820
- children: "more"
1895
+ "aria-expanded": expanded,
1896
+ onClick: () => setExpanded((v) => !v),
1897
+ style: toggleStyle2,
1898
+ children: expanded ? "see less" : "more"
1821
1899
  }
1822
1900
  )
1823
1901
  ] }) : null
1824
1902
  ] });
1825
1903
  }
1826
1904
  var FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
1905
+ var BODY_COLORS = {
1906
+ accent: varRef(LI_VAR.accent),
1907
+ muted: varRef(LI_VAR.muted)
1908
+ };
1827
1909
  function LinkedInPostPreviewImpl({
1828
1910
  variant,
1829
1911
  author,
@@ -1840,11 +1922,10 @@ function LinkedInPostPreviewImpl({
1840
1922
  altSignatureOf(variant.media)
1841
1923
  );
1842
1924
  const visibility = variant.settings?.visibility ?? "PUBLIC";
1843
- const mentionNames = (variant.settings?.mentions ?? []).map((m) => m.name);
1844
- const bodyColors = {
1845
- accent: varRef(LI_VAR.accent),
1846
- muted: varRef(LI_VAR.muted)
1847
- };
1925
+ const mentionNames = useMemo(
1926
+ () => (variant.settings?.mentions ?? []).map((m) => m.name),
1927
+ [variant.settings?.mentions]
1928
+ );
1848
1929
  const cardStyle5 = {
1849
1930
  ...paletteVars(),
1850
1931
  colorScheme: colorSchemeFor(theme),
@@ -1852,11 +1933,16 @@ function LinkedInPostPreviewImpl({
1852
1933
  color: varRef(LI_VAR.text),
1853
1934
  border: `1px solid ${varRef(LI_VAR.border)}`,
1854
1935
  borderRadius: 10,
1936
+ // Pin the width (see InstagramPostPreview): without it the card shrinks to its
1937
+ // content, so an empty post collapses to a fraction of a populated card's width.
1938
+ width: "100%",
1855
1939
  maxWidth: 552,
1940
+ boxSizing: "border-box",
1856
1941
  overflow: "hidden",
1857
1942
  fontFamily: FONT_STACK,
1858
1943
  ...style
1859
1944
  };
1945
+ const hasContent = Boolean(variant.body) || resolvedMedia.length > 0 || Boolean(variant.settings?.content_kind && variant.settings.content_kind !== "text");
1860
1946
  return /* @__PURE__ */ jsxs("div", { className, style: cardStyle5, children: [
1861
1947
  /* @__PURE__ */ jsx(Header, { author, visibility, time }),
1862
1948
  variant.body ? /* @__PURE__ */ jsx("div", { style: { padding: "8px 16px 0" }, children: /* @__PURE__ */ jsx(
@@ -1864,13 +1950,20 @@ function LinkedInPostPreviewImpl({
1864
1950
  {
1865
1951
  text: variant.body,
1866
1952
  mentionNames,
1867
- colors: bodyColors
1953
+ colors: BODY_COLORS
1868
1954
  }
1869
1955
  ) }) : null,
1870
1956
  renderContent(variant, resolvedMedia),
1957
+ hasContent ? null : /* @__PURE__ */ jsx("p", { style: { ...emptyBodyStyle2, color: BODY_COLORS.muted }, children: "What do you want to talk about?" }),
1871
1958
  showActions ? /* @__PURE__ */ jsx(EngagementBar, {}) : null
1872
1959
  ] });
1873
1960
  }
1961
+ var emptyBodyStyle2 = {
1962
+ margin: 0,
1963
+ padding: "8px 16px 4px",
1964
+ fontSize: 14,
1965
+ lineHeight: 1.4
1966
+ };
1874
1967
  function renderContent(variant, media) {
1875
1968
  const settings = variant.settings;
1876
1969
  const kind = settings?.content_kind;
@@ -1900,14 +1993,15 @@ var IG_VAR = {
1900
1993
  function varRef2(name) {
1901
1994
  return `var(${name})`;
1902
1995
  }
1996
+ var PALETTE_VARS2 = {
1997
+ [IG_VAR.bg]: "light-dark(#ffffff, #000000)",
1998
+ [IG_VAR.text]: "light-dark(#000000, #f5f5f5)",
1999
+ [IG_VAR.muted]: "light-dark(#737373, #a8a8a8)",
2000
+ [IG_VAR.border]: "light-dark(#dbdbdb, #262626)",
2001
+ [IG_VAR.accent]: "light-dark(#00376b, #e0f1ff)"
2002
+ };
1903
2003
  function paletteVars2() {
1904
- return {
1905
- [IG_VAR.bg]: "light-dark(#ffffff, #000000)",
1906
- [IG_VAR.text]: "light-dark(#000000, #f5f5f5)",
1907
- [IG_VAR.muted]: "light-dark(#737373, #a8a8a8)",
1908
- [IG_VAR.border]: "light-dark(#dbdbdb, #262626)",
1909
- [IG_VAR.accent]: "light-dark(#00376b, #e0f1ff)"
1910
- };
2004
+ return PALETTE_VARS2;
1911
2005
  }
1912
2006
  function colorSchemeFor2(theme) {
1913
2007
  return theme === "auto" ? "light dark" : theme;
@@ -1948,7 +2042,14 @@ function ExpandableText({
1948
2042
  if (!el || expanded) {
1949
2043
  return;
1950
2044
  }
1951
- setOverflowing(el.scrollHeight > el.clientHeight + 1);
2045
+ const measure = () => setOverflowing(el.scrollHeight > el.clientHeight + 1);
2046
+ measure();
2047
+ if (typeof ResizeObserver === "undefined") {
2048
+ return;
2049
+ }
2050
+ const ro = new ResizeObserver(measure);
2051
+ ro.observe(el);
2052
+ return () => ro.disconnect();
1952
2053
  }, [expanded, children]);
1953
2054
  const textStyle = expanded ? FULL : { ...FULL, ...clampStyle(lines) };
1954
2055
  return /* @__PURE__ */ jsxs("div", { style, children: [
@@ -2002,10 +2103,10 @@ function Caption({
2002
2103
  toggleColor: varRef2(IG_VAR.muted),
2003
2104
  style: wrapStyle3,
2004
2105
  children: [
2005
- /* @__PURE__ */ jsxs("span", { style: { fontWeight: 600 }, children: [
2106
+ username ? /* @__PURE__ */ jsxs("span", { style: { fontWeight: 600 }, children: [
2006
2107
  username,
2007
2108
  " "
2008
- ] }),
2109
+ ] }) : null,
2009
2110
  parts.map(
2010
2111
  (part, i) => IS_TOKEN.test(part) ? /* @__PURE__ */ jsx("span", { style: { color: varRef2(IG_VAR.accent) }, children: part }, i) : /* @__PURE__ */ jsx("span", { children: part }, i)
2011
2112
  )
@@ -2019,9 +2120,57 @@ var wrapStyle3 = {
2019
2120
  lineHeight: 1.4,
2020
2121
  color: varRef2(IG_VAR.text)
2021
2122
  };
2022
- function FeedMedia({ media }) {
2123
+ function MediaPlaceholder({
2124
+ label,
2125
+ color,
2126
+ background,
2127
+ shimmer = false
2128
+ }) {
2129
+ return /* @__PURE__ */ jsxs("div", { style: { ...fillStyle, background, color }, children: [
2130
+ shimmer ? /* @__PURE__ */ jsxs(Fragment, { children: [
2131
+ /* @__PURE__ */ jsx("style", { children: SHIMMER_KEYFRAME }),
2132
+ /* @__PURE__ */ jsx("div", { style: shimmerStyle })
2133
+ ] }) : null,
2134
+ /* @__PURE__ */ jsx("span", { style: labelStyle, children: label })
2135
+ ] });
2136
+ }
2137
+ var SHIMMER_KEYFRAME = "@keyframes pr-media-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}";
2138
+ var fillStyle = {
2139
+ position: "absolute",
2140
+ inset: 0,
2141
+ display: "flex",
2142
+ alignItems: "center",
2143
+ justifyContent: "center",
2144
+ overflow: "hidden"
2145
+ };
2146
+ var labelStyle = {
2147
+ position: "relative",
2148
+ zIndex: 1,
2149
+ fontSize: 13,
2150
+ letterSpacing: 0.1
2151
+ };
2152
+ var shimmerStyle = {
2153
+ position: "absolute",
2154
+ inset: 0,
2155
+ background: "linear-gradient(90deg, transparent 0%, color-mix(in srgb, currentColor 12%, transparent) 50%, transparent 100%)",
2156
+ backgroundSize: "200% 100%",
2157
+ animation: "pr-media-shimmer 1.4s ease-in-out infinite"
2158
+ };
2159
+ function FeedMedia({
2160
+ media,
2161
+ pending = false
2162
+ }) {
2023
2163
  if (media.length <= 1) {
2024
- return /* @__PURE__ */ jsx("div", { style: frameStyle, children: media[0] ? /* @__PURE__ */ jsx(Tile2, { item: media[0] }) : null });
2164
+ const item = media[0];
2165
+ return /* @__PURE__ */ jsx("div", { style: frameStyle, children: item ? /* @__PURE__ */ jsx(Tile2, { item }) : /* @__PURE__ */ jsx(
2166
+ MediaPlaceholder,
2167
+ {
2168
+ label: pending ? "Processing media\u2026" : "No media yet",
2169
+ color: varRef2(IG_VAR.muted),
2170
+ background: EMPTY_BG,
2171
+ shimmer: pending
2172
+ }
2173
+ ) });
2025
2174
  }
2026
2175
  return /* @__PURE__ */ jsx(Carousel, { media });
2027
2176
  }
@@ -2048,7 +2197,7 @@ function Carousel({ media }) {
2048
2197
  };
2049
2198
  }, [emblaApi, sync]);
2050
2199
  return /* @__PURE__ */ jsxs("div", { style: frameStyle, children: [
2051
- /* @__PURE__ */ jsx("div", { ref: emblaRef, style: { position: "absolute", inset: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsx("div", { style: { display: "flex", height: "100%" }, children: media.map((item, i) => /* @__PURE__ */ jsx("div", { style: slideStyle, children: /* @__PURE__ */ jsx(Tile2, { item }) }, `${item.src}-${i}`)) }) }),
2200
+ /* @__PURE__ */ jsx("div", { ref: emblaRef, style: { position: "absolute", inset: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsx("div", { style: { display: "flex", height: "100%" }, children: media.map((item) => /* @__PURE__ */ jsx("div", { style: slideStyle, children: /* @__PURE__ */ jsx(Tile2, { item }) }, item.src)) }) }),
2052
2201
  /* @__PURE__ */ jsxs("div", { style: pillStyle, children: [
2053
2202
  selected + 1,
2054
2203
  "/",
@@ -2059,7 +2208,7 @@ function Carousel({ media }) {
2059
2208
  {
2060
2209
  style: { ...dotStyle, opacity: i === selected ? 1 : 0.5 }
2061
2210
  },
2062
- `dot-${item.src}-${i}`
2211
+ `dot-${item.src}`
2063
2212
  )) })
2064
2213
  ] });
2065
2214
  }
@@ -2070,6 +2219,7 @@ var frameStyle = {
2070
2219
  background: "#000",
2071
2220
  overflow: "hidden"
2072
2221
  };
2222
+ var EMPTY_BG = "light-dark(#efefef, #1c1c1c)";
2073
2223
  var mediaStyle = {
2074
2224
  position: "absolute",
2075
2225
  inset: 0,
@@ -2184,12 +2334,13 @@ function ReelPreview({
2184
2334
  author,
2185
2335
  body,
2186
2336
  media,
2337
+ pending = false,
2187
2338
  audioName,
2188
2339
  className,
2189
2340
  style
2190
2341
  }) {
2191
2342
  const first = media[0];
2192
- const username = author.username ?? "";
2343
+ const username = author.username;
2193
2344
  return /* @__PURE__ */ jsxs("div", { className, style: { ...cardStyle3, ...style }, children: [
2194
2345
  first ? first.kind === "video" ? /* @__PURE__ */ jsx(
2195
2346
  "video",
@@ -2202,14 +2353,30 @@ function ReelPreview({
2202
2353
  playsInline: true,
2203
2354
  style: coverStyle2
2204
2355
  }
2205
- ) : /* @__PURE__ */ jsx("img", { src: first.src, alt: first.alt ?? "", style: coverStyle2 }) : null,
2356
+ ) : /* @__PURE__ */ jsx("img", { src: first.src, alt: first.alt ?? "", style: coverStyle2 }) : /* @__PURE__ */ jsx(
2357
+ MediaPlaceholder,
2358
+ {
2359
+ label: pending ? "Processing media\u2026" : "No media yet",
2360
+ color: "rgba(255,255,255,0.55)",
2361
+ background: "transparent",
2362
+ shimmer: pending
2363
+ }
2364
+ ),
2206
2365
  /* @__PURE__ */ jsx("div", { style: scrimStyle }),
2207
2366
  /* @__PURE__ */ jsxs("div", { style: infoStyle, children: [
2208
2367
  /* @__PURE__ */ jsxs("div", { style: authorRowStyle, children: [
2209
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 14 }, children: username }),
2368
+ username ? /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 14 }, children: username }) : null,
2210
2369
  author.verified ? /* @__PURE__ */ jsx(MdVerified, { size: 13, "aria-label": "Verified", role: "img", style: { color: "#fff" } }) : null
2211
2370
  ] }),
2212
- body ? /* @__PURE__ */ jsx("div", { style: captionStyle, children: body }) : null,
2371
+ body ? /* @__PURE__ */ jsx(
2372
+ ExpandableText,
2373
+ {
2374
+ lines: 2,
2375
+ toggleColor: "rgba(255,255,255,0.7)",
2376
+ style: captionStyle,
2377
+ children: body
2378
+ }
2379
+ ) : null,
2213
2380
  /* @__PURE__ */ jsxs("div", { style: audioStyle, children: [
2214
2381
  /* @__PURE__ */ jsx(MdMusicNote, { size: 14, "aria-hidden": true }),
2215
2382
  /* @__PURE__ */ jsx("span", { children: audioName ?? "Original audio" })
@@ -2265,11 +2432,7 @@ var authorRowStyle = {
2265
2432
  };
2266
2433
  var captionStyle = {
2267
2434
  fontSize: 13,
2268
- lineHeight: 1.35,
2269
- display: "-webkit-box",
2270
- WebkitLineClamp: 2,
2271
- WebkitBoxOrient: "vertical",
2272
- overflow: "hidden"
2435
+ lineHeight: 1.35
2273
2436
  };
2274
2437
  var audioStyle = {
2275
2438
  display: "flex",
@@ -2302,7 +2465,8 @@ function InstagramPostPreviewImpl({
2302
2465
  altSignatureOf(variant.media)
2303
2466
  );
2304
2467
  const settings = variant.settings;
2305
- const isReel = variant.post_type === "reel" || settings?.media_type === "REELS";
2468
+ const isReel = variant.post_type === "reel";
2469
+ const pending = (media?.length ?? 0) > 0 && resolvedMedia.length === 0;
2306
2470
  if (isReel) {
2307
2471
  return /* @__PURE__ */ jsx(
2308
2472
  ReelPreview,
@@ -2310,6 +2474,7 @@ function InstagramPostPreviewImpl({
2310
2474
  author,
2311
2475
  body: variant.body ?? "",
2312
2476
  media: resolvedMedia,
2477
+ pending,
2313
2478
  audioName: settings?.audio_name,
2314
2479
  className,
2315
2480
  style
@@ -2324,16 +2489,22 @@ function InstagramPostPreviewImpl({
2324
2489
  color: varRef2(IG_VAR.text),
2325
2490
  border: `1px solid ${varRef2(IG_VAR.border)}`,
2326
2491
  borderRadius: 8,
2492
+ // Fill the container up to IG's feed width. WITHOUT an explicit width the card
2493
+ // shrinks to its content, so an empty post (no caption/media to widen it) would
2494
+ // collapse to a fraction of a populated card's width — a jarring size change as
2495
+ // you type. Pinning width keeps every state the SAME size (zero layout shift).
2496
+ width: "100%",
2327
2497
  maxWidth: 470,
2498
+ boxSizing: "border-box",
2328
2499
  overflow: "hidden",
2329
2500
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
2330
2501
  ...style
2331
2502
  };
2332
2503
  return /* @__PURE__ */ jsxs("div", { className, style: cardStyle5, children: [
2333
2504
  /* @__PURE__ */ jsx(Header2, { author, collaborators }),
2334
- /* @__PURE__ */ jsx(FeedMedia, { media: resolvedMedia }),
2505
+ /* @__PURE__ */ jsx(FeedMedia, { media: resolvedMedia, pending }),
2335
2506
  /* @__PURE__ */ jsx(Actions, {}),
2336
- variant.body ? /* @__PURE__ */ jsx("div", { style: { paddingBottom: 12 }, children: /* @__PURE__ */ jsx(Caption, { username: author.username ?? "", body: variant.body }) }) : null
2507
+ variant.body ? /* @__PURE__ */ jsx("div", { style: { paddingBottom: 12 }, children: /* @__PURE__ */ jsx(Caption, { username: author.username, body: variant.body }) }) : null
2337
2508
  ] });
2338
2509
  }
2339
2510
  var InstagramPostPreview = memo(InstagramPostPreviewImpl);
@@ -2356,7 +2527,15 @@ function Media2({
2356
2527
  }) {
2357
2528
  const [first] = media;
2358
2529
  if (!first) {
2359
- return pending ? /* @__PURE__ */ jsx(ProcessingMedia, {}) : /* @__PURE__ */ jsx(EmptyMedia, {});
2530
+ return /* @__PURE__ */ jsx(
2531
+ MediaPlaceholder,
2532
+ {
2533
+ label: pending ? "Processing media\u2026" : "No media yet",
2534
+ color: "rgba(255,255,255,0.55)",
2535
+ background: "linear-gradient(180deg,#1c1c20,#101013)",
2536
+ shimmer: pending
2537
+ }
2538
+ );
2360
2539
  }
2361
2540
  if (first.kind === "video") {
2362
2541
  return /* @__PURE__ */ jsx(
@@ -2405,15 +2584,6 @@ function PhotoCarousel({ media }) {
2405
2584
  )) }) : null
2406
2585
  ] });
2407
2586
  }
2408
- function EmptyMedia() {
2409
- return /* @__PURE__ */ jsx("div", { style: emptyStyle, children: "No media yet" });
2410
- }
2411
- function ProcessingMedia() {
2412
- return /* @__PURE__ */ jsxs("div", { style: emptyStyle, children: [
2413
- /* @__PURE__ */ jsx("div", { style: shimmerStyle }),
2414
- /* @__PURE__ */ jsx("span", { style: { position: "relative", zIndex: 1 }, children: "Processing media\u2026" })
2415
- ] });
2416
- }
2417
2587
  var coverStyle3 = {
2418
2588
  position: "absolute",
2419
2589
  inset: 0,
@@ -2452,24 +2622,6 @@ var dotStyle2 = {
2452
2622
  background: "#fff",
2453
2623
  transition: "opacity 160ms ease"
2454
2624
  };
2455
- var emptyStyle = {
2456
- position: "absolute",
2457
- inset: 0,
2458
- display: "flex",
2459
- alignItems: "center",
2460
- justifyContent: "center",
2461
- color: "rgba(255,255,255,0.55)",
2462
- fontSize: 13,
2463
- background: "linear-gradient(180deg,#1c1c20,#101013)",
2464
- overflow: "hidden"
2465
- };
2466
- var shimmerStyle = {
2467
- position: "absolute",
2468
- inset: 0,
2469
- background: "linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0) 100%)",
2470
- backgroundSize: "200% 100%",
2471
- animation: "pr-tt-shimmer 1.4s ease-in-out infinite"
2472
- };
2473
2625
  var Icon = ({
2474
2626
  size = 24,
2475
2627
  viewBox,
@@ -2717,7 +2869,7 @@ var discImgStyle = {
2717
2869
  borderRadius: "50%",
2718
2870
  objectFit: "cover"
2719
2871
  };
2720
- 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}}`;
2872
+ var KEYFRAMES = `@keyframes pr-tt-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`;
2721
2873
  var TikTokPostPreview = memo(TikTokPostPreviewImpl);
2722
2874
 
2723
2875
  // src/preview/tiktok/theme.ts
@@ -2731,14 +2883,15 @@ var TT_VAR = {
2731
2883
  function varRef3(name) {
2732
2884
  return `var(${name})`;
2733
2885
  }
2886
+ var PALETTE_VARS3 = {
2887
+ [TT_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.92))",
2888
+ [TT_VAR.muted]: "light-dark(rgba(0,0,0,0.55), #8a8a92)",
2889
+ [TT_VAR.border]: "light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.14))",
2890
+ [TT_VAR.accent]: "#fe2c55",
2891
+ [TT_VAR.switchOff]: "light-dark(rgba(0,0,0,0.18), rgba(255,255,255,0.22))"
2892
+ };
2734
2893
  function paletteVars3() {
2735
- return {
2736
- [TT_VAR.text]: "light-dark(rgba(0,0,0,0.9), rgba(255,255,255,0.92))",
2737
- [TT_VAR.muted]: "light-dark(rgba(0,0,0,0.55), #8a8a92)",
2738
- [TT_VAR.border]: "light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.14))",
2739
- [TT_VAR.accent]: "#fe2c55",
2740
- [TT_VAR.switchOff]: "light-dark(rgba(0,0,0,0.18), rgba(255,255,255,0.22))"
2741
- };
2894
+ return PALETTE_VARS3;
2742
2895
  }
2743
2896
  function colorSchemeFor3(theme) {
2744
2897
  return theme === "auto" ? "light dark" : theme;
@@ -2754,11 +2907,17 @@ function Row({
2754
2907
  control,
2755
2908
  htmlFor
2756
2909
  }) {
2910
+ const text = /* @__PURE__ */ jsxs(Fragment, { children: [
2911
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 14 }, children: label }),
2912
+ sublabel ? /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: MUTED2 }, children: sublabel }) : null
2913
+ ] });
2914
+ const textStyle = {
2915
+ display: "flex",
2916
+ flexDirection: "column",
2917
+ gap: 2
2918
+ };
2757
2919
  return /* @__PURE__ */ jsxs("div", { style: rowStyle3, children: [
2758
- /* @__PURE__ */ jsxs("label", { htmlFor, style: { display: "flex", flexDirection: "column", gap: 2 }, children: [
2759
- /* @__PURE__ */ jsx("span", { style: { fontSize: 14 }, children: label }),
2760
- sublabel ? /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: MUTED2 }, children: sublabel }) : null
2761
- ] }),
2920
+ htmlFor ? /* @__PURE__ */ jsx("label", { htmlFor, style: textStyle, children: text }) : /* @__PURE__ */ jsx("span", { style: textStyle, children: text }),
2762
2921
  control
2763
2922
  ] });
2764
2923
  }
@@ -2880,7 +3039,6 @@ function AudienceSelect({
2880
3039
  value,
2881
3040
  onChange
2882
3041
  }) {
2883
- const id = useId();
2884
3042
  const options = audiencePrivacyOptions(creatorInfo.privacy_options, value);
2885
3043
  const unselected = audienceUnselected(value);
2886
3044
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
@@ -2888,7 +3046,7 @@ function AudienceSelect({
2888
3046
  /* @__PURE__ */ jsxs(
2889
3047
  "select",
2890
3048
  {
2891
- id,
3049
+ "aria-label": "Who can view this post",
2892
3050
  value: value.privacy_level ?? "",
2893
3051
  onChange: (e) => onChange({
2894
3052
  ...value,
@@ -2933,7 +3091,7 @@ function TikTokCaptionField({
2933
3091
  const over = count > max;
2934
3092
  return /* @__PURE__ */ jsxs("div", { className, style: { ...wrapStyle5, ...style }, children: [
2935
3093
  /* @__PURE__ */ jsxs("div", { style: labelRowStyle, children: [
2936
- /* @__PURE__ */ jsx("label", { htmlFor: id, style: labelStyle, children: "Caption" }),
3094
+ /* @__PURE__ */ jsx("label", { htmlFor: id, style: labelStyle2, children: "Caption" }),
2937
3095
  /* @__PURE__ */ jsxs(
2938
3096
  "span",
2939
3097
  {
@@ -2976,7 +3134,7 @@ var labelRowStyle = {
2976
3134
  alignItems: "baseline",
2977
3135
  justifyContent: "space-between"
2978
3136
  };
2979
- var labelStyle = {
3137
+ var labelStyle2 = {
2980
3138
  fontSize: 13,
2981
3139
  fontWeight: 600,
2982
3140
  color: "inherit"