|
5 | 5 |
|
6 | 6 | // Bluesky budget: 300 chars total. ~80 chars for change desc + stats + feed name + links |
7 | 7 | const BSKY_LIMIT = 300; |
8 | | - const BSKY_OVERHEAD = 120; // approx: change desc, stats, feed name, URLs |
9 | | - let prefixInput = $state(data.profile.postPrefix || ''); |
10 | | - let suffixInput = $state(data.profile.postSuffix || ''); |
11 | | - let bskyBudget = $derived(BSKY_LIMIT - BSKY_OVERHEAD - prefixInput.length - suffixInput.length); |
| 8 | + const AP_LIMIT = 500; |
| 9 | + const OVERHEAD = 120; |
| 10 | +
|
| 11 | + let apPrefixInput = $state(data.profile.postPrefix || ''); |
| 12 | + let apSuffixInput = $state(data.profile.postSuffix || ''); |
| 13 | + let bskyPrefixInput = $state(data.profile.bskyPostPrefix || ''); |
| 14 | + let bskySuffixInput = $state(data.profile.bskyPostSuffix || ''); |
| 15 | +
|
| 16 | + let previewTab = $state('ap'); |
12 | 17 |
|
13 | | - // Live preview using a real diff |
14 | 18 | const preview = data.previewData; |
15 | | - let previewText = $derived(() => { |
| 19 | +
|
| 20 | + function buildPreview(prefix: string, suffix: string): string { |
16 | 21 | if (!preview) return ''; |
17 | 22 | const changes = [ |
18 | 23 | preview.titleChanged ? 'Headline changed' : '', |
19 | 24 | preview.contentChanged ? 'Content changed' : '' |
20 | 25 | ].filter(Boolean).join(' & ') || 'Article updated'; |
21 | 26 | const stats = `+${preview.charsAdded} / -${preview.charsRemoved} chars`; |
22 | | - const prefix = prefixInput ? `${prefixInput} ` : ''; |
23 | | - const suffix = suffixInput ? `\n\n${suffixInput}` : ''; |
24 | | - return `${prefix}${changes} in "${preview.title}" (${preview.feedName})\n${stats}\n\nhttps://diff.example.com/diff/${preview.diffId}\nhttps://example.com/article${suffix}`; |
25 | | - }); |
26 | | - let previewCharCount = $derived(previewText().length); |
| 27 | + const pfx = prefix ? `${prefix} ` : ''; |
| 28 | + const sfx = suffix ? `\n\n${suffix}` : ''; |
| 29 | + return `${pfx}${changes} in "${preview.title}" (${preview.feedName})\n${stats}\n\nhttps://diff.example.com/diff/${preview.diffId}\nhttps://example.com/article${sfx}`; |
| 30 | + } |
| 31 | +
|
| 32 | + let apPreview = $derived(buildPreview(apPrefixInput, apSuffixInput)); |
| 33 | + let bskyPreview = $derived(buildPreview(bskyPrefixInput || apPrefixInput, bskySuffixInput || apSuffixInput)); |
| 34 | + let apBudget = $derived(AP_LIMIT - apPreview.length); |
| 35 | + let bskyBudget = $derived(BSKY_LIMIT - bskyPreview.length); |
27 | 36 |
|
28 | 37 | let fields = $derived((() => { |
29 | 38 | const f = [...(profile.fields || [])]; |
|
116 | 125 |
|
117 | 126 | <section> |
118 | 127 | <h2>Post template</h2> |
119 | | - <p class="hint">Customize how syndicated posts are constructed on ActivityPub and Bluesky. The default format is: <code>{'{change}'} in "{'{title}'}" ({'{feed}'})</code></p> |
120 | | - <div class="field"> |
121 | | - <label for="postPrefix">Prefix</label> |
122 | | - <input type="text" id="postPrefix" name="postPrefix" bind:value={prefixInput} placeholder="e.g. 📝 Edit detected:" /> |
123 | | - <p class="hint">Added before the change description. Leave empty for default.</p> |
124 | | - </div> |
125 | | - <div class="field"> |
126 | | - <label for="postSuffix">Suffix</label> |
127 | | - <input type="text" id="postSuffix" name="postSuffix" bind:value={suffixInput} placeholder="e.g. #newsdiff #transparency" /> |
128 | | - <p class="hint">Added at the end of every post. Useful for hashtags.</p> |
129 | | - </div> |
130 | | - <div class="budget" class:budget-warn={bskyBudget < 30} class:budget-over={bskyBudget < 0}> |
131 | | - Bluesky budget: ~{bskyBudget} chars remaining for title |
132 | | - {#if bskyBudget < 0} |
133 | | - — title will be truncated |
134 | | - {:else if bskyBudget < 30} |
135 | | - — title may be truncated |
136 | | - {/if} |
| 128 | + <p class="hint">Customize syndicated posts per network. Bluesky fields fall back to ActivityPub values if left empty.</p> |
| 129 | + |
| 130 | + <div class="template-tabs"> |
| 131 | + <button class:active={previewTab === 'ap'} onclick={() => previewTab = 'ap'}> |
| 132 | + <span class="tab-icon">🐘</span> ActivityPub |
| 133 | + </button> |
| 134 | + <button class:active={previewTab === 'bsky'} onclick={() => previewTab = 'bsky'}> |
| 135 | + <span class="tab-icon">🦋</span> Bluesky |
| 136 | + </button> |
137 | 137 | </div> |
138 | 138 |
|
139 | | - {#if preview} |
140 | | - <div class="preview-cards"> |
141 | | - <div class="preview-card preview-mastodon"> |
142 | | - <div class="preview-header"> |
143 | | - <span class="preview-icon">🐘</span> |
144 | | - <span class="preview-label">ActivityPub preview</span> |
145 | | - </div> |
146 | | - <div class="preview-body">{previewText()}</div> |
147 | | - <div class="preview-meta">{previewCharCount} chars (no limit)</div> |
| 139 | + {#if previewTab === 'ap'} |
| 140 | + <div class="template-fields"> |
| 141 | + <div class="field"> |
| 142 | + <label for="postPrefix">Prefix</label> |
| 143 | + <input type="text" id="postPrefix" name="postPrefix" bind:value={apPrefixInput} placeholder="e.g. 📝 Edit detected:" /> |
148 | 144 | </div> |
149 | | - <div class="preview-card preview-bluesky"> |
150 | | - <div class="preview-header"> |
151 | | - <span class="preview-icon">🦋</span> |
152 | | - <span class="preview-label">Bluesky preview</span> |
153 | | - </div> |
154 | | - <div class="preview-body" class:preview-truncated={previewCharCount > 300}>{previewText().slice(0, 300)}{#if previewCharCount > 300}...{/if}</div> |
155 | | - <div class="preview-meta" class:budget-over={previewCharCount > 300}>{Math.min(previewCharCount, 300)}/300 chars</div> |
| 145 | + <div class="field"> |
| 146 | + <label for="postSuffix">Suffix</label> |
| 147 | + <input type="text" id="postSuffix" name="postSuffix" bind:value={apSuffixInput} placeholder="e.g. #newsdiff #transparency" /> |
| 148 | + </div> |
| 149 | + <div class="budget" class:budget-warn={apBudget < 50} class:budget-over={apBudget < 0}> |
| 150 | + {apPreview.length}/500 chars |
| 151 | + </div> |
| 152 | + </div> |
| 153 | + {:else} |
| 154 | + <div class="template-fields"> |
| 155 | + <div class="field"> |
| 156 | + <label for="bskyPostPrefix">Prefix</label> |
| 157 | + <input type="text" id="bskyPostPrefix" name="bskyPostPrefix" bind:value={bskyPrefixInput} placeholder={apPrefixInput || 'Same as ActivityPub'} /> |
| 158 | + </div> |
| 159 | + <div class="field"> |
| 160 | + <label for="bskyPostSuffix">Suffix</label> |
| 161 | + <input type="text" id="bskyPostSuffix" name="bskyPostSuffix" bind:value={bskySuffixInput} placeholder={apSuffixInput || 'Same as ActivityPub'} /> |
| 162 | + </div> |
| 163 | + <div class="budget" class:budget-warn={bskyBudget < 30} class:budget-over={bskyBudget < 0}> |
| 164 | + {bskyPreview.length}/300 chars |
| 165 | + {#if bskyBudget < 0} — title will be truncated{/if} |
156 | 166 | </div> |
157 | 167 | </div> |
158 | 168 | {/if} |
| 169 | + |
| 170 | + {#if preview} |
| 171 | + <div class="preview-card" class:preview-mastodon={previewTab === 'ap'} class:preview-bluesky={previewTab === 'bsky'}> |
| 172 | + <div class="preview-header"> |
| 173 | + <span class="preview-icon">{previewTab === 'ap' ? '🐘' : '🦋'}</span> |
| 174 | + <span class="preview-label">{previewTab === 'ap' ? 'ActivityPub' : 'Bluesky'} preview</span> |
| 175 | + </div> |
| 176 | + {#if previewTab === 'ap'} |
| 177 | + <div class="preview-body">{apPreview}</div> |
| 178 | + {:else} |
| 179 | + <div class="preview-body" class:preview-truncated={bskyPreview.length > 300}>{bskyPreview.slice(0, 300)}{#if bskyPreview.length > 300}...{/if}</div> |
| 180 | + {/if} |
| 181 | + </div> |
| 182 | + {/if} |
159 | 183 | </section> |
160 | 184 |
|
161 | 185 | <div class="actions"> |
|
226 | 250 | .budget-warn { color: #92400e; background: #fef3c7; } |
227 | 251 | .budget-over { color: var(--color-del-text); background: var(--color-del-bg); } |
228 | 252 |
|
229 | | - .preview-cards { display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap; } |
230 | | - .preview-card { flex: 1; min-width: 260px; border: 1px solid var(--color-border); border-radius: 0.5rem; overflow: hidden; background: white; } |
| 253 | + .template-tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: 2px solid var(--color-border); } |
| 254 | + .template-tabs button { padding: 0.5rem 1rem; border: none; background: none; cursor: pointer; font-size: 0.85rem; color: var(--color-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; display: flex; align-items: center; gap: 0.3rem; } |
| 255 | + .template-tabs button.active { color: var(--color-text); border-bottom-color: var(--color-primary); font-weight: 600; } |
| 256 | + .template-tabs button:hover { color: var(--color-text); } |
| 257 | + .template-fields { margin-bottom: 0.75rem; } |
| 258 | +
|
| 259 | + .preview-card { border: 1px solid var(--color-border); border-radius: 0.5rem; overflow: hidden; background: white; margin-top: 1rem; } |
231 | 260 | .preview-header { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.75rem; background: #f8f8f8; border-bottom: 1px solid var(--color-border); font-size: 0.8rem; font-weight: 600; color: var(--color-muted); } |
232 | 261 | .preview-icon { font-size: 1rem; } |
233 | 262 | .preview-body { padding: 0.75rem; font-size: 0.85rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; color: var(--color-text); } |
|
0 commit comments