@lettercamp/cli 0.1.12 → 0.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lettercamp/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "The repository for the Lettercamp CLI",
5
5
  "keywords": [
6
6
  "lettercamp",
@@ -6,6 +6,7 @@ const { posts } = Astro.props;
6
6
  // inert JSON script: article text may contain a literal closing script tag.
7
7
  const index = JSON.stringify(posts.map(post => ({
8
8
  title: String(post.title), href: post.href,
9
+ image: post.image ?? post.ogImage,
9
10
  summary: typeof post.description === 'string' ? post.description : '',
10
11
  text: typeof post.searchText === 'string' ? post.searchText : '',
11
12
  category: post.category, date: post.date.label,
@@ -14,7 +15,7 @@ const index = JSON.stringify(posts.map(post => ({
14
15
 
15
16
  <div class="post-search-trigger" hidden>
16
17
  <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
+ <button type="button" aria-label="Search articles" aria-haspopup="dialog" aria-controls="post-search-dialog">Search articles…</button>
18
19
  </div>
19
20
  <dialog id="post-search-dialog" class="post-search-dialog" aria-label="Search articles">
20
21
  <div class="post-search-header">
@@ -3,7 +3,7 @@ export const postSearchScript = String.raw`(() => {
3
3
  const dialog = document.querySelector('#post-search-dialog');
4
4
  const data = document.querySelector('[data-post-search-index]');
5
5
  if (!trigger || !dialog || !data || typeof dialog.showModal !== 'function') return;
6
- const opener = trigger.querySelector('input');
6
+ const opener = trigger.querySelector('button');
7
7
  const input = dialog.querySelector('input');
8
8
  const results = dialog.querySelector('.post-search-results');
9
9
  const status = dialog.querySelector('.post-search-status');
@@ -13,7 +13,6 @@ export const postSearchScript = String.raw`(() => {
13
13
  const normalized = [];
14
14
  let generation = 0;
15
15
  let timer;
16
- let restoringFocus = false;
17
16
  const normalize = text => text.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase().replace(/\s+/g, ' ').trim();
18
17
  const element = (tag, className, text) => {
19
18
  const node = document.createElement(tag);
@@ -80,11 +79,24 @@ export const postSearchScript = String.raw`(() => {
80
79
  const row = document.createElement('li');
81
80
  const link = element('a', 'post-search-result', '');
82
81
  link.href = post.href;
83
- link.append(highlighted('post-search-title', post.title, terms));
82
+ if (post.image) {
83
+ const image = element('img', 'post-search-image', '');
84
+ image.src = post.image;
85
+ image.alt = '';
86
+ image.width = 1200;
87
+ image.height = 630;
88
+ image.loading = 'lazy';
89
+ image.decoding = 'async';
90
+ image.addEventListener('error', () => image.remove(), { once: true });
91
+ link.append(image);
92
+ }
93
+ const content = element('span', 'post-search-content', '');
94
+ content.append(highlighted('post-search-title', post.title, terms));
84
95
  const meta = [post.category, post.date].filter(Boolean).join(' · ');
85
- if (meta) link.append(element('span', 'post-search-meta', meta));
96
+ if (meta) content.append(element('span', 'post-search-meta', meta));
86
97
  const excerpt = terms.length ? snippet(post, terms, normalized[index]) : post.summary;
87
- if (excerpt) link.append(highlighted('post-search-excerpt', excerpt, terms));
98
+ if (excerpt) content.append(highlighted('post-search-excerpt', excerpt, terms));
99
+ link.append(content);
88
100
  row.append(link);
89
101
  fragment.append(row);
90
102
  }
@@ -93,7 +105,7 @@ export const postSearchScript = String.raw`(() => {
93
105
  results.removeAttribute('aria-busy');
94
106
  empty.hidden = count > 0;
95
107
  empty.textContent = posts.length ? 'No articles found. Try a different word or title.' : 'No articles here yet.';
96
- status.textContent = !terms.length ? 'Recent articles' : count > limit
108
+ status.textContent = !terms.length ? '' : count > limit
97
109
  ? 'Showing ' + limit + ' of ' + count + ' matching articles. Keep typing to narrow your search.'
98
110
  : count + (count === 1 ? ' article found' : ' articles found');
99
111
  }
@@ -143,7 +155,7 @@ export const postSearchScript = String.raw`(() => {
143
155
  timer = setTimeout(step, 0);
144
156
  }
145
157
  function open() {
146
- if (restoringFocus || dialog.open) return;
158
+ if (dialog.open) return;
147
159
  if (!posts) {
148
160
  posts = JSON.parse(data.textContent);
149
161
  data.textContent = '';
@@ -153,19 +165,12 @@ export const postSearchScript = String.raw`(() => {
153
165
  search();
154
166
  }
155
167
  function close() {
156
- restoringFocus = true;
157
168
  ++generation;
158
169
  clearTimeout(timer);
159
170
  dialog.close();
160
171
  opener.focus({ preventScroll: true });
161
- // Native dialog focus restoration must not immediately reopen the search.
162
- queueMicrotask(() => { restoringFocus = false; });
163
172
  }
164
- opener.addEventListener('focus', open);
165
173
  opener.addEventListener('click', open);
166
- opener.addEventListener('keydown', event => {
167
- if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); open(); }
168
- });
169
174
  input.addEventListener('input', event => { if (!event.isComposing) search(); });
170
175
  input.addEventListener('compositionend', search);
171
176
  dialog.querySelector('.post-search-close').addEventListener('click', close);
@@ -1,22 +1,25 @@
1
1
  .post-search-trigger { position: relative; flex: 0 1 240px; min-width: 180px; margin-left: auto; }
2
2
  .post-search-trigger svg { position: absolute; left: 13px; top: 50%; width: 16px; height: 16px; transform: translateY(-50%); color: var(--site-text-muted); pointer-events: none; }
3
- .post-search-trigger input { width: 100%; height: 36px; padding: 0 14px 0 38px; border: 1px solid var(--site-border-strong); border-radius: 999px; background: var(--site-background); color: var(--site-foreground); font: 400 var(--text-label)/1.5 var(--font-family); cursor: text; }
4
- .post-search-trigger input::placeholder, .post-search-header input::placeholder { color: var(--site-text-secondary); opacity: 1; }
5
- .post-search-trigger input:focus-visible { outline: 2px solid var(--site-foreground); outline-offset: 3px; }
3
+ .post-search-trigger button { width: 100%; height: 36px; padding: 0 14px 0 38px; border: 0; border-radius: 999px; background: var(--site-surface-hover); color: var(--site-text-secondary); font: 400 var(--text-label)/1.5 var(--font-family); text-align: left; cursor: pointer; }
4
+ .post-search-trigger button:hover { background: var(--site-surface-active); color: var(--site-foreground); }
5
+ .post-search-header input::placeholder { color: var(--site-text-secondary); opacity: 1; }
6
+ .post-search-trigger button:focus-visible { outline: 1px solid var(--site-border-strong); outline-offset: 3px; border-radius: 999px; background: var(--site-surface-active); }
6
7
  html:has(.post-search-dialog[open]) { overflow: hidden; }
7
8
  .post-search-dialog { position: fixed; inset: 0; width: min(620px, calc(100vw - 32px)); max-width: none; max-height: min(440px, calc(100dvh - 48px)); margin: auto; padding: 0; overflow: hidden; border: 1px solid var(--site-border); border-radius: 14px; background: var(--site-background); color: var(--site-foreground); box-shadow: 0 20px 80px #0003; font: 400 var(--text-label)/1.5 var(--font-family); }
8
9
  .post-search-dialog[open] { display: flex; flex-direction: column; }
9
10
  .post-search-dialog::backdrop { background: color-mix(in srgb, var(--site-background) 65%, transparent); backdrop-filter: blur(3px); }
10
- .post-search-header { display: flex; flex-shrink: 0; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--site-border); }
11
+ .post-search-header { display: flex; flex-shrink: 0; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--site-surface, #fafafa); }
11
12
  .post-search-header > svg { width: 20px; height: 20px; flex-shrink: 0; color: var(--site-text-secondary); }
12
13
  .post-search-header input { min-width: 0; width: 100%; padding: 2px 0; border: 0; outline: none; background: transparent; color: inherit; font: 400 max(16px, var(--text-small))/1.5 var(--font-family); }
13
- .post-search-header:focus-within { border-bottom-color: var(--site-border-strong); }
14
14
  .post-search-close { display: grid; flex-shrink: 0; place-items: center; width: 28px; height: 28px; padding: 4px; border: 0; background: transparent; color: var(--site-text-secondary); }
15
15
  .post-search-close:hover { color: var(--site-foreground); }
16
16
  .post-search-close svg { width: 20px; height: 20px; }
17
17
  .post-search-status { flex-shrink: 0; margin: 0; padding: 8px 16px 4px; color: var(--site-text-secondary); font-size: var(--text-meta); }
18
+ .post-search-status:empty { padding: 0; }
18
19
  .post-search-results { flex: 0 1 auto; min-height: 0; margin: 0; padding: 0 6px 6px; overflow: auto; overscroll-behavior: contain; list-style: none; }
19
- .post-search-result { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 8px; color: inherit; text-decoration: none; overflow-wrap: anywhere; }
20
+ .post-search-result { display: flex; align-items: flex-start; gap: 12px; padding: 8px 10px; border-radius: 8px; color: inherit; text-decoration: none; overflow-wrap: anywhere; }
21
+ .post-search-image { flex: 0 0 64px; width: 64px; height: auto; aspect-ratio: 1200 / 630; object-fit: cover; border-radius: 4px; background: var(--site-surface); }
22
+ .post-search-content { display: flex; flex: 1; min-width: 0; flex-direction: column; gap: 2px; }
20
23
  .post-search-result:is(:hover, :focus-visible) { outline: none; background: var(--site-surface-active); border-radius: 8px; }
21
24
  .post-search-title { font-size: var(--text-label); font-weight: 500; }
22
25
  .post-search-meta { color: var(--site-text-secondary); font-size: var(--text-meta); }
@@ -27,6 +30,8 @@ html:has(.post-search-dialog[open]) { overflow: hidden; }
27
30
  .post-search-trigger { flex-basis: 100%; min-width: 0; }
28
31
  .post-search-dialog { width: calc(100vw - 24px); max-height: min(440px, calc(100dvh - 24px)); }
29
32
  .post-search-header { padding: 10px 12px; }
33
+ .post-search-result { gap: 10px; }
34
+ .post-search-image { flex-basis: 56px; width: 56px; }
30
35
  }
31
36
  @media (prefers-reduced-motion: no-preference) {
32
37
  .post-search-dialog[open] { animation: post-search-enter 140ms ease-out; }