@lettercamp/cli 0.1.10 → 0.1.12
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/build.js +3 -1
- package/dist/code-block.js +8 -8
- package/dist/content-component-syntax.js +75 -0
- package/dist/content-components.js +244 -0
- package/dist/dev.js +20 -3
- package/dist/media-dimensions.js +4 -3
- package/dist/publication-links.js +7 -0
- package/dist/search-content.js +24 -0
- package/dist/text-styles.js +23 -0
- package/package.json +7 -1
- package/renderer/src/components/AuthorByline.astro +9 -2
- package/renderer/src/components/BlogHome.astro +8 -4
- package/renderer/src/components/Footer.astro +1 -1
- package/renderer/src/components/Header.astro +2 -2
- package/renderer/src/components/PostSearch.astro +30 -0
- package/renderer/src/components/RelatedPosts.astro +11 -0
- package/renderer/src/components/SocialLink.astro +1 -1
- package/renderer/src/layouts/SiteLayout.astro +11 -4
- package/renderer/src/lib/block-widths.ts +0 -6
- package/renderer/src/lib/branding.js +2 -0
- package/renderer/src/lib/button.js +11 -9
- package/renderer/src/lib/code-block.ts +8 -8
- package/renderer/src/lib/faq.ts +25 -0
- package/renderer/src/lib/font-catalog.js +1650 -0
- package/renderer/src/lib/font-fallbacks.js +36 -0
- package/renderer/src/lib/fonts.js +31 -8
- package/renderer/src/lib/image-widths.ts +0 -3
- package/renderer/src/lib/key-takeaways.ts +25 -0
- package/renderer/src/lib/post-search.ts +194 -0
- package/renderer/src/lib/post-share.ts +10 -0
- package/renderer/src/lib/related-posts.ts +50 -0
- package/renderer/src/lib/site-colors.ts +42 -0
- package/renderer/src/lib/table-styles.ts +3 -3
- package/renderer/src/pages/[...slug].astro +14 -3
- package/renderer/src/styles/post-search.css +37 -0
- package/renderer/src/styles/post-share.css +44 -27
- package/renderer/src/styles/site.css +95 -72
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../styles/post-search.css';
|
|
3
|
+
import { postSearchScript } from '../lib/post-search';
|
|
4
|
+
const { posts } = Astro.props;
|
|
5
|
+
// Only public post fields enter the browser. Escape HTML delimiters even in an
|
|
6
|
+
// inert JSON script: article text may contain a literal closing script tag.
|
|
7
|
+
const index = JSON.stringify(posts.map(post => ({
|
|
8
|
+
title: String(post.title), href: post.href,
|
|
9
|
+
summary: typeof post.description === 'string' ? post.description : '',
|
|
10
|
+
text: typeof post.searchText === 'string' ? post.searchText : '',
|
|
11
|
+
category: post.category, date: post.date.label,
|
|
12
|
+
}))).replace(/</g, '\\u003c');
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class="post-search-trigger" hidden>
|
|
16
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5" /><path d="m16 16 4.5 4.5" /></svg>
|
|
17
|
+
<input type="search" readonly placeholder="Search articles…" aria-label="Search articles" aria-haspopup="dialog" aria-controls="post-search-dialog" />
|
|
18
|
+
</div>
|
|
19
|
+
<dialog id="post-search-dialog" class="post-search-dialog" aria-label="Search articles">
|
|
20
|
+
<div class="post-search-header">
|
|
21
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5" /><path d="m16 16 4.5 4.5" /></svg>
|
|
22
|
+
<input type="text" role="searchbox" enterkeyhint="search" placeholder="Search articles…" aria-label="Search article titles and content" aria-controls="post-search-results" autocomplete="off" spellcheck="false" autofocus />
|
|
23
|
+
<button type="button" class="post-search-close" aria-label="Close search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18" /></svg></button>
|
|
24
|
+
</div>
|
|
25
|
+
<p class="post-search-status" role="status" aria-live="polite" aria-atomic="true"></p>
|
|
26
|
+
<ul id="post-search-results" class="post-search-results" aria-label="Matching articles"></ul>
|
|
27
|
+
<p class="post-search-empty" hidden>No articles found. Try a different word or title.</p>
|
|
28
|
+
</dialog>
|
|
29
|
+
<script is:inline type="application/json" data-post-search-index set:html={index} />
|
|
30
|
+
<script is:inline set:html={postSearchScript} />
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import PostCard from './PostCard.astro';
|
|
3
|
+
const { posts, site } = Astro.props;
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
{posts.length > 0 && <section class="related-posts" aria-labelledby="related-posts-heading" hidden={site.post?.showRelatedPosts === false}>
|
|
7
|
+
<h2 id="related-posts-heading">Related</h2>
|
|
8
|
+
<div class="post-list">
|
|
9
|
+
{posts.map(post => <PostCard post={post} site={site} />)}
|
|
10
|
+
</div>
|
|
11
|
+
</section>}
|
|
@@ -18,6 +18,6 @@ const hasIcon = icons[platform] || ['instagram', 'website', 'hacker-news'].inclu
|
|
|
18
18
|
{icons[platform] && <path d={icons[platform]} />}
|
|
19
19
|
{platform === 'instagram' && <g fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="18" height="18" rx="5" /><circle cx="12" cy="12" r="4" /><circle cx="17.5" cy="6.5" r=".8" fill="currentColor" stroke="none" /></g>}
|
|
20
20
|
{platform === 'website' && <g fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9" /><ellipse cx="12" cy="12" rx="4" ry="9" /><path d="M3 12h18" /></g>}
|
|
21
|
-
{platform === 'hacker-news' && <><rect x="2" y="2" width="20" height="20" rx="2" /><path d="m7 6 5 7 5-7m-5 7v5" fill="none" stroke="var(--background, #fff)" stroke-width="1.8" /></>}
|
|
21
|
+
{platform === 'hacker-news' && <><rect x="2" y="2" width="20" height="20" rx="2" /><path d="m7 6 5 7 5-7m-5 7v5" fill="none" stroke="var(--site-background, #fff)" stroke-width="1.8" /></>}
|
|
22
22
|
</svg> : <span>{label}</span>}
|
|
23
23
|
</a>
|
|
@@ -13,6 +13,9 @@ import { postShareScript } from '../lib/post-share';
|
|
|
13
13
|
import { mobileNavigationScript } from '../lib/mobile-navigation';
|
|
14
14
|
import { blockWidthStyles } from '../lib/block-widths';
|
|
15
15
|
import { tableStyles } from '../lib/table-styles';
|
|
16
|
+
import { keyTakeawaysStyles } from '../lib/key-takeaways';
|
|
17
|
+
import { faqStyles } from '../lib/faq';
|
|
18
|
+
import { siteColors } from '../lib/site-colors';
|
|
16
19
|
import { imageWidthStyles } from '../lib/image-widths';
|
|
17
20
|
import { googleAnalyticsScript } from '../lib/analytics';
|
|
18
21
|
import Header from '../components/HeaderSlot.astro';
|
|
@@ -21,19 +24,21 @@ import Footer from '../components/FooterSlot.astro';
|
|
|
21
24
|
const { site, page, article = false } = Astro.props;
|
|
22
25
|
const meta = pageMetadata(site, page, Astro.url, article, basePath);
|
|
23
26
|
const typography = site.design?.typography ?? {};
|
|
24
|
-
const fonts = fontResources(typography);
|
|
27
|
+
const fonts = fontResources(typography, article ? 700 : 500);
|
|
25
28
|
const fontSize = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
26
|
-
const
|
|
29
|
+
const headingBaseSize = fontSize(typography.headingBaseSizePx, 18);
|
|
30
|
+
const bodyBaseSize = fontSize(typography.bodyBaseSizePx, 16);
|
|
27
31
|
const bodyLineHeight = fontSize(typography.bodyLineHeight, 1.8);
|
|
28
|
-
const
|
|
32
|
+
const colors = siteColors(site.design);
|
|
29
33
|
const analytics = googleAnalyticsScript(site.analytics?.googleAnalyticsId);
|
|
30
34
|
---
|
|
31
35
|
|
|
32
36
|
<!doctype html>
|
|
33
|
-
<html data-base-path={basePath} lang={meta.language} style={{ '--image-radius': readImageRadius(site.design?.imageRadiusPercent) / 100, '--
|
|
37
|
+
<html data-base-path={basePath} data-site-accent={colors['--site-accent'] !== 'inherit' ? '' : undefined} lang={meta.language} style={{ ...colors, '--image-radius': readImageRadius(site.design?.imageRadiusPercent) / 100, '--font-family': fonts.bodyFamily, '--heading-font': fonts.headingFamily, '--heading-type-scale': headingBaseSize / 18, '--body-type-scale': bodyBaseSize / 16, '--body-line-height': bodyLineHeight }}>
|
|
34
38
|
<head>
|
|
35
39
|
<meta charset="utf-8" />
|
|
36
40
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
41
|
+
{fonts.preloads.map(href => <link data-lettercamp-font-preload rel="preload" href={href} as="font" type="font/woff2" crossorigin />)}
|
|
37
42
|
{fonts.googleUrl && <>
|
|
38
43
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
39
44
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
@@ -44,6 +49,8 @@ const analytics = googleAnalyticsScript(site.analytics?.googleAnalyticsId);
|
|
|
44
49
|
<style is:inline set:html={blockWidthStyles} />
|
|
45
50
|
<style is:inline set:html={codeBlockStyles} />
|
|
46
51
|
<style is:inline set:html={tableStyles} />
|
|
52
|
+
<style is:inline set:html={keyTakeawaysStyles} />
|
|
53
|
+
<style is:inline set:html={faqStyles} />
|
|
47
54
|
<title>{meta.documentTitle}</title>
|
|
48
55
|
<meta name="description" content={meta.description} />
|
|
49
56
|
{meta.keywords && <meta name="keywords" content={meta.keywords} />}
|
|
@@ -26,10 +26,4 @@ ${blocks}${full} {
|
|
|
26
26
|
max-width: none; margin-inline: calc((100% - var(--block-display-width)) / 2);
|
|
27
27
|
margin-left: calc((100% - var(--block-display-width)) / 2 - var(--block-column-offset, 0px));
|
|
28
28
|
}
|
|
29
|
-
/* The two-column TOC layout shifts the reading column 106px from center. */
|
|
30
|
-
@media (min-width: 761px) and (max-width: 1199px) {
|
|
31
|
-
.publication-post[data-post-hero="split"][data-post-toc="visible"] .article-body-with-toc .prose {
|
|
32
|
-
--block-column-offset: 106px;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
29
|
`;
|
|
@@ -7,6 +7,8 @@ export const defaultSiteBranding = {
|
|
|
7
7
|
export function withDefaultBranding(source) {
|
|
8
8
|
const branding = Object.fromEntries(Object.entries(defaultSiteBranding).map(([key, fallback]) =>
|
|
9
9
|
[key, typeof source[key] === 'string' && source[key].trim() ? source[key] : fallback]));
|
|
10
|
+
// An explicit empty logo disables the image while omitted logos keep the default.
|
|
11
|
+
if (typeof source.logo === 'string') branding.logo = source.logo.trim();
|
|
10
12
|
return {
|
|
11
13
|
...source,
|
|
12
14
|
...branding,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// Generated from lettercamp/lib/button-colors.ts.
|
|
2
|
+
// Shared by settings, the preview bridge, and the published renderer.
|
|
3
|
+
// Keep self-contained so the preview can embed this function.
|
|
4
|
+
export function buttonColors(value, accent) {
|
|
5
|
+
const background = [value, accent].find((color) => typeof color === "string" && /^#[0-9a-f]{6}$/i.test(color)) ?? "#111111";
|
|
6
|
+
const channels = [1, 3, 5].map(offset => {
|
|
7
|
+
const channel = parseInt(background.slice(offset, offset + 2), 16) / 255;
|
|
8
|
+
return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
|
|
9
|
+
});
|
|
10
|
+
const luminance = channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
|
11
|
+
return { background, text: (luminance + 0.05) / 0.05 >= 1.05 / (luminance + 0.05) ? "#000000" : "#ffffff" };
|
|
10
12
|
}
|
|
@@ -23,24 +23,24 @@ export function writeCodeSettings(meta: string, patch: Partial<CodeBlockSettings
|
|
|
23
23
|
|
|
24
24
|
// The editor and both publication renderers use the same token classes and palette.
|
|
25
25
|
export const codeBlockStyles = `
|
|
26
|
-
[data-lettercamp-code] { --code-keyword: #8250a0; --code-string: #3d7757; --code-number: #a15c32; --code-name: #326b99; --code-comment: #93939b; color: #414146; }
|
|
26
|
+
[data-lettercamp-code] { --code-keyword: color-mix(in srgb, #8250a0, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-string: color-mix(in srgb, #3d7757, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-number: color-mix(in srgb, #a15c32, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-name: color-mix(in srgb, #326b99, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-comment: var(--site-text-muted, #93939b); color: var(--site-text-strong, #414146); }
|
|
27
27
|
[data-lettercamp-code][data-code-width] { margin-block: 24px; }
|
|
28
|
-
[data-lettercamp-code][data-code-theme="ocean"] { --code-keyword: #3d62a8; --code-string: #277c85; --code-number: #98623e; --code-name: #6961a4; }
|
|
29
|
-
[data-lettercamp-code][data-code-theme="forest"] { --code-keyword: #47724a; --code-string: #827030; --code-number: #a25d47; --code-name: #3c7b79; }
|
|
30
|
-
[data-lettercamp-code][data-code-theme="mono"] { --code-keyword: #45454a; --code-string: #62626a; --code-number: #62626a; --code-name: #45454a; }
|
|
28
|
+
[data-lettercamp-code][data-code-theme="ocean"] { --code-keyword: color-mix(in srgb, #3d62a8, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-string: color-mix(in srgb, #277c85, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-number: color-mix(in srgb, #98623e, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-name: color-mix(in srgb, #6961a4, var(--site-foreground, #000000) var(--site-code-tint, 0%)); }
|
|
29
|
+
[data-lettercamp-code][data-code-theme="forest"] { --code-keyword: color-mix(in srgb, #47724a, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-string: color-mix(in srgb, #827030, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-number: color-mix(in srgb, #a25d47, var(--site-foreground, #000000) var(--site-code-tint, 0%)); --code-name: color-mix(in srgb, #3c7b79, var(--site-foreground, #000000) var(--site-code-tint, 0%)); }
|
|
30
|
+
[data-lettercamp-code][data-code-theme="mono"] { --code-keyword: var(--site-text-strong, #45454a); --code-string: var(--site-text-secondary, #62626a); --code-number: var(--site-text-secondary, #62626a); --code-name: var(--site-text-strong, #45454a); }
|
|
31
31
|
[data-lettercamp-code] .tok-keyword, [data-lettercamp-code] .tok-operator { color: var(--code-keyword); }
|
|
32
32
|
[data-lettercamp-code] .tok-string, [data-lettercamp-code] .tok-string2, [data-lettercamp-code] .tok-inserted { color: var(--code-string); }
|
|
33
33
|
[data-lettercamp-code] .tok-number, [data-lettercamp-code] .tok-bool, [data-lettercamp-code] .tok-atom, [data-lettercamp-code] .tok-literal { color: var(--code-number); }
|
|
34
34
|
[data-lettercamp-code] .tok-typeName, [data-lettercamp-code] .tok-className, [data-lettercamp-code] .tok-propertyName, [data-lettercamp-code] .tok-definition, [data-lettercamp-code] .tok-variableName2, [data-lettercamp-code] .tok-labelName { color: var(--code-name); }
|
|
35
35
|
[data-lettercamp-code] .tok-comment, [data-lettercamp-code] .tok-meta { color: var(--code-comment); }
|
|
36
|
-
[data-lettercamp-code] .tok-invalid, [data-lettercamp-code] .tok-deleted { color: #aa4747; }
|
|
36
|
+
[data-lettercamp-code] .tok-invalid, [data-lettercamp-code] .tok-deleted { color: color-mix(in srgb, #aa4747, var(--site-foreground, #000000) var(--site-code-tint, 0%)); }
|
|
37
37
|
[data-lettercamp-code] .tok-strong { font-weight: 600; }
|
|
38
38
|
[data-lettercamp-code] .tok-emphasis { font-style: italic; }
|
|
39
|
-
figure[data-lettercamp-code] > pre { box-sizing: border-box; margin: 0; padding: 16px 0; overflow-x: auto; border-radius: 8px; background: #fafafa; color: #414146; font: 13px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; tab-size: 4; }
|
|
39
|
+
figure[data-lettercamp-code] > pre { box-sizing: border-box; margin: 0; padding: 16px 0; overflow-x: auto; border-radius: 8px; background: var(--site-surface, #fafafa); color: var(--site-text-strong, #414146); font: 13px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; tab-size: 4; }
|
|
40
40
|
figure[data-lettercamp-code] > pre > code { display: block; width: max-content; min-width: 100%; padding: 0; background: transparent; color: inherit; font: inherit; white-space: normal; counter-reset: code-line; }
|
|
41
41
|
[data-lettercamp-code] .code-line { position: relative; display: block; box-sizing: border-box; min-height: 1.7em; padding: 0 18px 0 calc(var(--code-gutter, 3ch) + 24px); white-space: pre; }
|
|
42
|
-
[data-lettercamp-code] .code-line::before { position: absolute; left: 8px; width: var(--code-gutter, 3ch); text-align: right; content: counter(code-line); counter-increment: code-line; color: #b4b4bc; user-select: none; font-variant-numeric: tabular-nums; }
|
|
42
|
+
[data-lettercamp-code] .code-line::before { position: absolute; left: 8px; width: var(--code-gutter, 3ch); text-align: right; content: counter(code-line); counter-increment: code-line; color: var(--site-text-faint, #b4b4bc); user-select: none; font-variant-numeric: tabular-nums; }
|
|
43
43
|
[data-lettercamp-code][data-code-wrap="true"] > pre > code { width: 100%; }
|
|
44
44
|
[data-lettercamp-code][data-code-wrap="true"] .code-line { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
45
|
-
figure[data-lettercamp-code] > figcaption { margin: 8px 0 0; color: #888; font: 13px/1.5 var(--font-family, inherit); text-align: center; overflow-wrap: anywhere; }
|
|
45
|
+
figure[data-lettercamp-code] > figcaption { margin: 8px 0 0; color: var(--site-text-muted, #888); font: 13px/1.5 var(--font-family, inherit); text-align: center; overflow-wrap: anywhere; }
|
|
46
46
|
`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Generated from lettercamp/lib/faq.ts.
|
|
2
|
+
// Shared by the editor preview and published posts. Native disclosures work
|
|
3
|
+
// without hydration and let readers keep several answers open at once.
|
|
4
|
+
export const faqStyles = `
|
|
5
|
+
[data-lettercamp-faq] { margin-block: 24px; color: inherit; overflow-wrap: anywhere; }
|
|
6
|
+
[data-lettercamp-faq] > details { border-bottom: 1px solid var(--site-border, #e5e5e5); }
|
|
7
|
+
[data-lettercamp-faq] > details:first-child { border-top: 1px solid var(--site-border, #e5e5e5); }
|
|
8
|
+
[data-lettercamp-faq] > details > summary {
|
|
9
|
+
display: flex; align-items: center; justify-content: space-between; gap: 24px;
|
|
10
|
+
padding: 18px 0; cursor: pointer; list-style: none; font-weight: 600; line-height: 1.5;
|
|
11
|
+
}
|
|
12
|
+
[data-lettercamp-faq] > details > summary::-webkit-details-marker { display: none; }
|
|
13
|
+
[data-lettercamp-faq] > details > summary::after {
|
|
14
|
+
content: ''; width: 7px; height: 7px; flex: 0 0 7px; margin: 0 4px 4px;
|
|
15
|
+
border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
|
|
16
|
+
transform: rotate(45deg); transition: transform 150ms;
|
|
17
|
+
}
|
|
18
|
+
[data-lettercamp-faq] > details[open] > summary::after { transform: translateY(4px) rotate(225deg); }
|
|
19
|
+
[data-lettercamp-faq] > details > summary:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; border-radius: 2px; }
|
|
20
|
+
[data-lettercamp-faq] > details > [data-faq-answer] { padding: 0 28px 20px 0; }
|
|
21
|
+
[data-lettercamp-faq] > details > [data-faq-answer] > :first-child { margin-top: 0; }
|
|
22
|
+
[data-lettercamp-faq] > details > [data-faq-answer] > :last-child { margin-bottom: 0; }
|
|
23
|
+
@media (prefers-reduced-motion: reduce) { [data-lettercamp-faq] > details > summary::after { transition: none; } }
|
|
24
|
+
@media print { [data-lettercamp-faq] > details::details-content { content-visibility: visible; } }
|
|
25
|
+
`;
|