@lettercamp/cli 0.1.7 → 0.1.8

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.
Files changed (58) hide show
  1. package/dist/account.js +20 -8
  2. package/dist/build.js +21 -5
  3. package/dist/cli.js +15 -6
  4. package/dist/code-block.js +50 -0
  5. package/dist/code-detection.js +24 -0
  6. package/dist/code-highlight.js +54 -0
  7. package/dist/custom-layout.js +32 -0
  8. package/dist/deploy.js +131 -0
  9. package/dist/dev.js +27 -10
  10. package/dist/domain.js +190 -0
  11. package/dist/hosting.js +132 -0
  12. package/dist/lettercamp.js +1 -4
  13. package/dist/media-dimensions.js +154 -0
  14. package/dist/new.js +1 -1
  15. package/dist/preview.js +1 -1
  16. package/dist/prompts.js +31 -0
  17. package/dist/publication-links.js +23 -0
  18. package/dist/react.js +15 -0
  19. package/dist/upload.js +40 -10
  20. package/dist/workspace.js +5 -3
  21. package/package.json +11 -2
  22. package/renderer/src/components/AuthorByline.astro +20 -11
  23. package/renderer/src/components/BlogHome.astro +14 -57
  24. package/renderer/src/components/Footer.astro +12 -9
  25. package/renderer/src/components/Header.astro +38 -0
  26. package/renderer/src/components/MobileMenuIcons.astro +7 -0
  27. package/renderer/src/components/PostCard.astro +12 -5
  28. package/renderer/src/components/SocialLink.astro +4 -4
  29. package/renderer/src/layouts/SiteLayout.astro +70 -51
  30. package/renderer/src/lib/analytics.ts +21 -0
  31. package/renderer/src/lib/author-dialog.ts +102 -0
  32. package/renderer/src/lib/authors.js +18 -0
  33. package/renderer/src/lib/block-widths.ts +30 -0
  34. package/renderer/src/lib/branding.js +15 -0
  35. package/renderer/src/lib/button.js +10 -0
  36. package/renderer/src/lib/category-routes.js +19 -0
  37. package/renderer/src/lib/code-block.ts +46 -0
  38. package/renderer/src/lib/fonts.js +33 -0
  39. package/renderer/src/lib/home-layout.ts +5 -0
  40. package/renderer/src/lib/image-radius.ts +5 -0
  41. package/renderer/src/lib/image-rounding.ts +28 -0
  42. package/renderer/src/lib/image-widths.ts +13 -0
  43. package/renderer/src/lib/metadata.js +47 -0
  44. package/renderer/src/lib/mobile-navigation.ts +76 -0
  45. package/renderer/src/lib/post-layout.ts +6 -0
  46. package/renderer/src/lib/post-share.ts +264 -0
  47. package/renderer/src/lib/post-toc.ts +69 -0
  48. package/renderer/src/lib/posts.js +14 -6
  49. package/renderer/src/lib/publication-url.js +14 -0
  50. package/renderer/src/lib/table-styles.ts +12 -0
  51. package/renderer/src/lib/urls.js +3 -0
  52. package/renderer/src/pages/[...slug].astro +21 -11
  53. package/renderer/src/pages/category/[tag].astro +16 -0
  54. package/renderer/src/pages/category/index.astro +4 -0
  55. package/renderer/src/pages/index.astro +1 -1
  56. package/renderer/src/styles/post-share.css +57 -0
  57. package/renderer/src/styles/site.css +261 -91
  58. package/dist/public-assets.js +0 -29
@@ -0,0 +1,69 @@
1
+ // A string lets both the static renderer and preview inject the same standalone script.
2
+ export const postTocScript = `
3
+ (() => {
4
+ let cleanup = () => {};
5
+ const mount = () => {
6
+ cleanup();
7
+ const sidebar = document.querySelector('.article-sidebar');
8
+ const nav = sidebar?.querySelector('.article-toc');
9
+ if (!sidebar || !nav) return;
10
+ sidebar.querySelector('.toc-trigger')?.remove();
11
+ const links = [...nav.querySelectorAll('a[href^="#"]')];
12
+ if (!links.length) return;
13
+ const controller = new AbortController();
14
+ const signal = controller.signal;
15
+ const trigger = document.createElement('button');
16
+ trigger.type = 'button';
17
+ trigger.className = 'toc-trigger';
18
+ trigger.setAttribute('aria-label', 'Table of contents');
19
+ trigger.setAttribute('aria-controls', 'post-contents');
20
+ trigger.setAttribute('aria-expanded', 'false');
21
+ nav.id = 'post-contents';
22
+ nav.querySelector('h2').textContent = 'Contents';
23
+ const markers = links.map(() => {
24
+ const marker = document.createElement('span');
25
+ marker.setAttribute('aria-hidden', 'true');
26
+ trigger.append(marker);
27
+ return marker;
28
+ });
29
+ sidebar.prepend(trigger);
30
+ sidebar.setAttribute('data-floating-toc', '');
31
+ const setOpen = open => {
32
+ sidebar.toggleAttribute('data-toc-open', open);
33
+ trigger.setAttribute('aria-expanded', String(open));
34
+ };
35
+ sidebar.addEventListener('pointerenter', event => { if (event.pointerType === 'mouse') setOpen(true); }, { signal });
36
+ sidebar.addEventListener('pointerleave', () => { if (!sidebar.contains(document.activeElement)) setOpen(false); }, { signal });
37
+ sidebar.addEventListener('focusin', () => setOpen(true), { signal });
38
+ sidebar.addEventListener('focusout', event => { if (!sidebar.contains(event.relatedTarget)) setOpen(false); }, { signal });
39
+ trigger.addEventListener('click', () => setOpen(true), { signal });
40
+ document.addEventListener('pointerdown', event => { if (!sidebar.contains(event.target)) setOpen(false); }, { signal });
41
+ sidebar.addEventListener('keydown', event => {
42
+ if (event.key !== 'Escape') return;
43
+ event.preventDefault(); trigger.focus(); setOpen(false);
44
+ }, { signal });
45
+ nav.addEventListener('click', event => {
46
+ if (event.target.closest('a')) { trigger.focus(); setOpen(false); }
47
+ }, { signal });
48
+ const headings = links.map(link => document.getElementById(decodeURIComponent(link.hash.slice(1))));
49
+ let frame = 0;
50
+ const update = () => {
51
+ frame = 0;
52
+ let active = -1;
53
+ headings.forEach((heading, index) => { if (heading && heading.getBoundingClientRect().top <= 120) active = index; });
54
+ links.forEach((link, index) => {
55
+ if (index === active) link.setAttribute('aria-current', 'location');
56
+ else link.removeAttribute('aria-current');
57
+ markers[index].toggleAttribute('data-active', index === active);
58
+ });
59
+ };
60
+ const schedule = () => { if (!frame) frame = requestAnimationFrame(update); };
61
+ window.addEventListener('scroll', schedule, { passive: true, signal });
62
+ window.addEventListener('resize', schedule, { passive: true, signal });
63
+ update();
64
+ cleanup = () => { controller.abort(); cancelAnimationFrame(frame); setOpen(false); };
65
+ };
66
+ document.addEventListener('lettercamp:post-updated', mount);
67
+ mount();
68
+ })();
69
+ `;
@@ -1,21 +1,29 @@
1
+ import { sitePath } from './urls.js';
1
2
  import { pages, contentPrefix } from '../content.js';
3
+ import { categoryRoutes } from './category-routes.js';
4
+
5
+ export function getCategories() {
6
+ return categoryRoutes(getPosts().map(post => post.category)).map(category => ({ ...category, href: sitePath(category.href) }));
7
+ }
2
8
 
3
9
  export function pageSlug(filename) {
4
10
  return filename.slice(contentPrefix.length).replace(/\.(md|mdx)$/, '');
5
11
  }
6
12
 
7
- export function postDate(value, language = 'en') {
13
+ export function postDate(value, language = 'en', timeZone = 'UTC') {
8
14
  if (!value) return { iso: '', label: '', timestamp: -Infinity };
9
15
  const date = value instanceof Date ? value : new Date(String(value));
10
16
  if (Number.isNaN(date.valueOf())) return { iso: '', label: '', timestamp: -Infinity };
17
+ // A calendar date has no time zone; shifting midnight changes its intended day.
18
+ const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(String(value));
11
19
  return {
12
- iso: date.toISOString().slice(0, 10),
13
- label: new Intl.DateTimeFormat(language, { month: 'long', day: 'numeric', year: 'numeric', timeZone: 'UTC' }).format(date),
20
+ iso: dateOnly ? String(value) : date.toISOString(),
21
+ label: new Intl.DateTimeFormat(language, { month: 'short', day: 'numeric', year: 'numeric', timeZone: dateOnly ? 'UTC' : timeZone }).format(date),
14
22
  timestamp: date.valueOf(),
15
23
  };
16
24
  }
17
25
 
18
- export function getPosts(language = 'en') {
26
+ export function getPosts(language = 'en', timeZone = 'UTC') {
19
27
  return Object.entries(pages)
20
28
  .map(([filename, page]) => {
21
29
  const frontmatter = page.frontmatter ?? {};
@@ -24,8 +32,8 @@ export function getPosts(language = 'en') {
24
32
  ...frontmatter,
25
33
  title: frontmatter.title ?? slug,
26
34
  category: typeof frontmatter.category === 'string' ? frontmatter.category.trim() : '',
27
- href: `/${slug.split('/').map(encodeURIComponent).join('/')}/`,
28
- date: postDate(frontmatter.date, language),
35
+ href: sitePath(`/${slug.split('/').map(encodeURIComponent).join('/')}/`),
36
+ date: postDate(frontmatter.date, language, timeZone),
29
37
  };
30
38
  })
31
39
  .sort((a, b) => b.date.timestamp - a.date.timestamp || a.href.localeCompare(b.href));
@@ -0,0 +1,14 @@
1
+ // Generated from lettercamp/lib/publication-url.ts. Run pnpm sync:renderer in the app repository.
2
+ // Publication paths are relative to the blog; absolute URLs can link to the parent website.
3
+ export function publicationBasePath(url) {
4
+ return new URL(url).pathname.replace(/\/+$/, "");
5
+ }
6
+ export function publicationPath(path, basePath = "") {
7
+ if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//"))
8
+ return path;
9
+ const base = basePath.replace(/\/+$/, "");
10
+ return base + path;
11
+ }
12
+ export function publicationUrl(path, siteUrl) {
13
+ return new URL(publicationPath(path, publicationBasePath(siteUrl)), siteUrl);
14
+ }
@@ -0,0 +1,12 @@
1
+ // Generated from lettercamp/lib/table-styles.ts. Run pnpm sync:renderer in the app repository.
2
+ // Shared by the editable table, safe preview, and published renderer.
3
+ export const tableStyles = `
4
+ [data-lettercamp-table] { position: relative; margin-block: 24px 32px; color: #303034; font: inherit; }
5
+ [data-lettercamp-table] > table { width: 100%; border-collapse: collapse; table-layout: fixed; border-spacing: 0; font: inherit; color: inherit; }
6
+ [data-lettercamp-table] > table > tbody > tr > :is(td, th) { position: relative; border: 1px solid #f0f0f0; padding: 0; min-width: 0; vertical-align: top; font: inherit; text-align: left; }
7
+ [data-lettercamp-table] [data-table-content] { display: block; box-sizing: border-box; min-height: calc(1.8em + 16px); padding: 8px 12px; line-height: inherit; overflow-wrap: anywhere; white-space: pre-wrap; outline: none; }
8
+ [data-lettercamp-table] [data-table-cell-height] [data-table-content] { height: var(--table-cell-height); min-height: 0; overflow: auto; }
9
+ [data-lettercamp-table] :is(td, th) > p, [data-lettercamp-table] [data-table-content] > p { margin: 0; padding: 0; min-height: 0; }
10
+ [data-lettercamp-table] [data-table-content] code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85em; padding: .18em .35em; background: #f3f3f3; border-radius: 3px; }
11
+ [data-lettercamp-table] [data-table-content] a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
12
+ `;
@@ -0,0 +1,3 @@
1
+ import { publicationPath } from './publication-url.js';
2
+ export const basePath = import.meta.env.BASE_URL.replace(/\/+$/, '');
3
+ export const sitePath = path => publicationPath(path, basePath);
@@ -1,9 +1,11 @@
1
1
  ---
2
2
  import SiteLayout from '../layouts/SiteLayout.astro';
3
3
  import AuthorByline from '../components/AuthorByline.astro';
4
- import { pageSlug, postDate } from '../lib/posts.js';
4
+ import { pageSlug, postDate, getCategories } from '../lib/posts.js';
5
5
  import site from '../site.js';
6
+ import { readContentWidthEm } from '../lib/post-layout';
6
7
  import { pages } from '../content.js';
8
+ import { postShareButton } from '../lib/post-share';
7
9
 
8
10
  export function getStaticPaths() {
9
11
  return Object.entries(pages).map(([filename, page]) => {
@@ -15,28 +17,36 @@ export function getStaticPaths() {
15
17
  const { page } = Astro.props;
16
18
  const { Content, frontmatter = {} } = page;
17
19
  const coverImage = frontmatter.image ?? frontmatter.ogImage;
20
+ const coverImageWidth = Number(frontmatter.coverImageWidth ?? (frontmatter.image ? frontmatter.imageWidth : undefined));
21
+ const coverImageHeight = Number(frontmatter.coverImageHeight ?? (frontmatter.image ? frontmatter.imageHeight : undefined));
22
+ const coverSizeKnown = Number.isFinite(coverImageWidth) && coverImageWidth > 0 && Number.isFinite(coverImageHeight) && coverImageHeight > 0;
18
23
  const headings = (page.getHeadings?.() ?? []).filter(({ depth }) => depth === 2 || depth === 3);
19
24
  const category = typeof frontmatter.category === 'string' ? frontmatter.category.trim() : '';
20
- const date = postDate(frontmatter.date, site.language);
21
- const postLayout = site.postLayout ?? {};
25
+ const categoryHref = getCategories().find(item => item.name === category)?.href;
26
+ const date = postDate(frontmatter.date, site.language, site.timeZone);
27
+ const postLayout = site.post ?? {};
28
+ const coverWidthMode = ['regular', 'wide', 'full'].includes(frontmatter.coverWidthMode) ? frontmatter.coverWidthMode : 'wide';
29
+ const coverWidth = /^[0-9]+$/.test(String(frontmatter.coverWidth)) && Number(frontmatter.coverWidth) > 0 ? `${frontmatter.coverWidth}px` : undefined;
22
30
  ---
23
31
 
24
32
  <SiteLayout site={site} page={frontmatter} article>
25
- <article class="publication publication-post" data-post-width={postLayout.contentWidth ?? 'standard'} data-post-hero={postLayout.heroLayout ?? 'split'}
33
+ <article class="publication publication-post" style={{ '--post-content-width': `calc(var(--text-base) * ${readContentWidthEm(postLayout)})` }} data-post-hero={postLayout.heroLayout ?? 'stacked'}
34
+ data-reading-minutes={frontmatter.readingMinutes}
26
35
  data-post-cover={postLayout.showCover === false ? 'hidden' : 'visible'} data-post-toc={postLayout.showTableOfContents === false ? 'hidden' : 'visible'}>
27
- <a class="back-link" href="/">← All posts</a>
28
36
  <header class:list={['article-header', 'article-hero', { 'article-hero-with-image': coverImage }]}>
29
37
  <div class="article-intro">
30
- <p class="eyebrow">{category ? <a href={`/?category=${encodeURIComponent(category)}`}>{category}</a> : site.name}</p>
38
+ {category && <p class="article-category">
39
+ <a href={categoryHref}>{category}</a>
40
+ </p>}
31
41
  <h1>{frontmatter.title ?? site.name}</h1>
32
42
  {frontmatter.description && <p class="description">{frontmatter.description}</p>}
33
43
  <div class="byline">
34
- {date.label && <time datetime={date.iso}>{date.label}</time>}
35
- <AuthorByline post={frontmatter} site={site} />
44
+ <AuthorByline post={frontmatter} site={site} date={date} />
45
+ <Fragment set:html={postShareButton} />
36
46
  </div>
37
47
  </div>
38
- {coverImage && <figure class="article-hero-image">
39
- <img src={coverImage} alt={frontmatter.imageAlt ?? ''} width="1200" height="630" fetchpriority="high" />
48
+ {coverImage && <figure class="article-hero-image" data-image-width={coverWidthMode} style={{ '--cover-image-width': coverWidth }}>
49
+ <img src={coverImage} alt={frontmatter.imageAlt ?? ''} width={coverSizeKnown ? coverImageWidth : 1600} height={coverSizeKnown ? coverImageHeight : 900} fetchpriority="high" />
40
50
  </figure>}
41
51
  </header>
42
52
  <div class:list={['article-body', { 'article-body-with-toc': headings.length > 0 }]}>
@@ -45,7 +55,7 @@ const postLayout = site.postLayout ?? {};
45
55
  <h2>On this page</h2>
46
56
  <ol>
47
57
  {headings.map(({ depth, slug, text }) => <li class:list={{ 'toc-subheading': depth === 3 }}>
48
- <a href={`#${slug}`}>{text}</a>
58
+ <a href={`#${slug}`} title={text}><span class="toc-label">{text}</span></a>
49
59
  </li>)}
50
60
  </ol>
51
61
  </nav>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import SiteLayout from '../../layouts/SiteLayout.astro';
3
+ import BlogHome from '../../components/BlogHome.astro';
4
+ import { getCategories } from '../../lib/posts.js';
5
+ import site from '../../site.js';
6
+
7
+ export function getStaticPaths() {
8
+ return getCategories().map(category => ({ params: { tag: category.tag }, props: { category: category.name } }));
9
+ }
10
+
11
+ const { category } = Astro.props;
12
+ ---
13
+
14
+ <SiteLayout site={site} page={{ title: category }}>
15
+ <BlogHome site={site} category={category} />
16
+ </SiteLayout>
@@ -0,0 +1,4 @@
1
+ ---
2
+ import { sitePath } from '../../lib/urls.js';
3
+ return Astro.redirect(sitePath('/'), 308);
4
+ ---
@@ -2,7 +2,7 @@
2
2
  import SiteLayout from '../layouts/SiteLayout.astro';
3
3
  import BlogHome from '../components/BlogHome.astro';
4
4
  import site from '../site.js';
5
- const title = site.homeTitle ?? `${site.name} Blog`;
5
+ const title = site.home.title ?? `${site.name} Blog`;
6
6
  ---
7
7
 
8
8
  <SiteLayout site={site} page={{ title }}>
@@ -0,0 +1,57 @@
1
+ .byline:has(.post-share-button) { display: grid; grid-template-columns: minmax(0, 1fr) auto; width: 100%; }
2
+ .byline:has(.post-share-button) .post-byline { grid-column: 1; grid-row: 1; min-width: 0; }
3
+ .byline > .post-share-button { grid-column: 2; grid-row: 1; justify-self: end; }
4
+ .post-share-button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; flex-shrink: 0; margin-left: auto; padding: 5px 11px; border: 0; border-radius: 999px; background: #f5f5f5; color: #555; font-size: 13px; line-height: 1.4; }
5
+ .post-share-button:hover { background: #ededed; color: #111; }
6
+ .post-share-button svg { width: 15px; height: 15px; }
7
+ html:has(.post-share-dialog[open]) { overflow: hidden; }
8
+ .post-share-dialog { position: fixed; width: min(380px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); margin: auto; padding: 16px; overflow: auto; overscroll-behavior: contain; border: 0; border-radius: 14px; background: var(--site-background, #fff); color: #222; box-shadow: 0 20px 80px #0003; font: 13px/1.5 var(--font-family); }
9
+ .post-share-dialog::backdrop { background: #0006; }
10
+ .post-share-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
11
+ .post-share-heading h2 { margin: 0; font: 600 15px/1.4 var(--font-family); letter-spacing: -.3px; }
12
+ .post-share-close { display: grid; place-items: center; flex-shrink: 0; width: 28px; height: 28px; margin: -4px -4px -4px 0; padding: 0; border: 0; background: transparent; color: #777; }
13
+ .post-share-close:hover { color: #111; }
14
+ .post-share-close svg { width: 19px; height: 19px; }
15
+ .post-share-preview { overflow: hidden; border: 1px solid #e5e5e5; border-radius: 8px; background: #f7f7f7; color: #222; font-family: Arial, Helvetica, sans-serif; }
16
+ .post-share-image { display: block; width: 100%; height: auto; aspect-ratio: 1200 / 630; object-fit: cover; }
17
+ .post-share-preview-copy { padding: 9px 10px; }
18
+ .post-share-domain { margin: 0 0 4px; color: #777; font-size: 10px; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
19
+ .post-share-preview h3 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin: 0; font: 600 14px/1.3 Arial, Helvetica, sans-serif; letter-spacing: 0; text-wrap: auto; }
20
+ .post-share-description { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin: 4px 0 0; color: #666; font-size: 11px; line-height: 1.4; overflow-wrap: anywhere; }
21
+ .post-share-description:empty { display: none; }
22
+ .post-share-actions { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 6px; margin-top: 22px; }
23
+ .post-share-action { display: flex; flex-direction: column; align-items: center; gap: 5px; min-width: 0; padding: 0; border: 0; background: transparent; color: #555; font-size: 11px; line-height: 1.4; text-decoration: none; }
24
+ .post-share-action-icon { display: grid; place-items: center; width: 100%; height: 40px; border-radius: 10px; background: #00000005; }
25
+ .post-share-action:hover, .post-share-action[aria-expanded='true'] { color: #111; }
26
+ .post-share-action:hover .post-share-action-icon, .post-share-action[aria-expanded='true'] .post-share-action-icon { background: #0000000a; }
27
+ .post-share-action svg { width: 18px; height: 18px; }
28
+ .post-share-menu { position: fixed; inset: auto; width: 184px; max-width: calc(100vw - 24px); max-height: calc(100dvh - 24px); margin: 0; padding: 6px; overflow: auto; overscroll-behavior: contain; border: 1px solid #eaeaea; border-radius: 10px; background: #fafafa; color: #444; box-shadow: 0 6px 24px #0002; font: 12px/1.4 var(--font-family); }
29
+ .post-share-menu::backdrop { background: transparent; }
30
+ .post-share-menu-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 7px 9px; border: 0; border-radius: 6px; background: transparent; color: inherit; text-align: left; font: inherit; text-decoration: none; }
31
+ .post-share-menu-item:is(:hover, :focus-visible) { background: #efefef; color: #111; outline: none; }
32
+ .post-share-menu-item svg { width: 15px; height: 15px; color: #888; }
33
+ .post-share-menu-item[data-share='native'] { margin-top: 4px; border-top: 1px solid #e8e8e8; border-radius: 0; }
34
+ .post-share-summaries { margin-top: 14px; padding-top: 12px; border-top: 1px solid #0000000d; }
35
+ .post-share-summaries h3 { margin: 0; font: 600 13px/1.5 var(--font-family); }
36
+ .post-share-summaries > p { margin: 3px 0 9px; color: #777; font-size: 12px; }
37
+ .post-share-providers { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
38
+ .post-share-provider { display: flex; align-items: center; justify-content: space-between; gap: 5px; padding: 7px 9px; border-radius: 7px; background: #00000005; font-size: 12px; text-decoration: none; }
39
+ .post-share-provider:hover { background: #0000000a; }
40
+ .post-share-provider svg { width: 13px; height: 13px; flex-shrink: 0; color: #888; }
41
+ .post-share-status { margin: 12px 0 0; color: #666; font-size: 12px; text-align: center; }
42
+ .post-share-status:empty { display: none; }
43
+ .post-share-copy-fallback { margin-top: 12px; }
44
+ .post-share-copy-fallback label { display: block; margin-bottom: 4px; color: #666; font-size: 12px; }
45
+ .post-share-copy-fallback input { width: 100%; min-width: 0; padding: 8px 10px; border: 1px solid #ddd; border-radius: 8px; background: #fff; color: #333; font: inherit; }
46
+ @media (max-width: 640px) {
47
+ .byline:has(.post-share-button) .post-byline { flex-wrap: wrap; gap: 6px 12px; overflow: visible; white-space: normal; }
48
+ .byline:has(.post-share-button) .post-authors { max-width: 100%; white-space: normal; }
49
+ }
50
+ @media (max-width: 400px) {
51
+ .post-share-provider { padding: 7px; font-size: 11px; }
52
+ .byline:has(.post-share-button) { column-gap: 10px; }
53
+ }
54
+ @media (prefers-reduced-motion: no-preference) {
55
+ .post-share-dialog[open] { animation: post-share-enter 140ms ease-out; }
56
+ @keyframes post-share-enter { from { opacity: 0; transform: translateY(6px) scale(.98); } to { opacity: 1; transform: none; } }
57
+ }