From b9e3444bac9341dca76a7910829de09bd749d610 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:32:38 +0200 Subject: [PATCH] fix: gitignore docs/superpowers --- .../plans/2026-04-14-i18n-en-de.md | 526 ------------------ .../2026-03-30-syndication-dialog-design.md | 217 -------- 2 files changed, 743 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-14-i18n-en-de.md delete mode 100644 docs/superpowers/specs/2026-03-30-syndication-dialog-design.md diff --git a/docs/superpowers/plans/2026-04-14-i18n-en-de.md b/docs/superpowers/plans/2026-04-14-i18n-en-de.md deleted file mode 100644 index 05fb8af..0000000 --- a/docs/superpowers/plans/2026-04-14-i18n-en-de.md +++ /dev/null @@ -1,526 +0,0 @@ -# i18n (en + de) Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add English and German translations to obsidian-micropub by extracting all hardcoded UI strings into locale files and wiring them through a tiny `t()` helper. - -**Architecture:** A `src/i18n.ts` module exports a single `t(key, vars?)` function that reads `window.moment.locale()` (Obsidian's own locale) at call time, looks up the key in the matching locale map (falling back to `en`), and does `{var}` substitution. Locale maps live in `src/lang/en.ts` and `src/lang/de.ts`. No external dependencies, no build changes. - -**Tech Stack:** TypeScript, Obsidian API (`window.moment.locale()`), esbuild (existing) - ---- - -## File Map - -| Action | Path | Responsibility | -|--------|------|---------------| -| Create | `src/lang/en.ts` | English string map (source of truth) | -| Create | `src/lang/de.ts` | German string map | -| Create | `src/i18n.ts` | `t()` helper — locale detection + lookup + interpolation | -| Modify | `src/main.ts` | Replace hardcoded strings with `t()` calls | -| Modify | `src/SettingsTab.ts` | Replace hardcoded strings with `t()` calls | -| Modify | `src/SyndicationDialog.ts` | Replace hardcoded strings with `t()` calls | -| Modify | `src/IndieAuth.ts` | Replace one hardcoded error string with `t()` call | -| Modify | `README.md` | Note i18n support under Configuration | - ---- - -## Task 1: English locale file - -**Files:** -- Create: `src/lang/en.ts` - -- [ ] **Step 1: Create `src/lang/en.ts` with all UI strings** - -```typescript -// src/lang/en.ts -export const en: Record = { - // Commands & ribbon - cmdPublish: "Publish to Micropub", - cmdUpdate: "Update existing Micropub post", - - // Notices — main.ts - noticeOpenNote: "Open a Markdown note to publish.", - noticeNoEndpoint: "⚠️ Micropub endpoint not configured. Open plugin settings to add it.", - noticeNoToken: "⚠️ Access token not configured. Open plugin settings to add it.", - noticePublishing: "Publishing…", - noticePublished: "✅ Published!", - noticePublishFailed: "❌ Publish failed: {error}", - noticeError: "❌ Error: {error}", - noticeNoSyndTargets: "⚠️ Could not fetch syndication targets. Publishing without dialog.", - - // Settings headings - settingsTitle: "Micropub Publisher", - settingsAccount: "Account", - settingsEndpoints: "Endpoints", - settingsEndpointsHint: "These are filled automatically when you sign in. Only edit them manually if your server uses non-standard paths.", - settingsPublishBehaviour:"Publish Behaviour", - settingsDigitalGarden: "Digital Garden", - - // Settings — endpoints - settingMicropubEndpoint: "Micropub endpoint", - settingMicropubEndpointDesc: "e.g. https://example.com/micropub", // intentional: replaces personal domain in source - settingMediaEndpoint: "Media endpoint", - settingMediaEndpointDesc:"For image uploads. Auto-discovered if blank.", - - // Settings — publish behaviour - settingVisibility: "Default visibility", - settingVisibilityDesc: "Applies when the note has no explicit visibility property.", - visibilityPublic: "Public", - visibilityUnlisted: "Unlisted", - visibilityPrivate: "Private", - - settingWriteUrl: "Write URL back to note", - settingWriteUrlDesc: "After publishing, store the post URL as `mp-url` in frontmatter. Subsequent publishes will update the existing post instead of creating a new one.", - - settingSyndDialog: "Syndication dialog", - settingSyndDialogDesc: "When to show the cross-posting dialog before publishing. 'When needed' shows it only if the note has no mp-syndicate-to frontmatter.", - syndDialogWhenNeeded: "When needed", - syndDialogAlways: "Always", - syndDialogNever: "Never", - - settingSyndDefaults: "Default syndication targets", - settingSyndDefaultsNone: "None configured. Targets checked by default in the publish dialog.", - btnClearDefaults: "Clear defaults", - - // Settings — digital garden - settingGardenTags: "Map #garden/* tags to gardenStage", - settingGardenTagsDesc: "Obsidian tags like #garden/plant become a `garden-stage: plant` Micropub property. The blog renders these as growth stage badges at /garden/.", - settingGardenStages: "Stages: plant 🌱 · cultivate 🌿 · question ❓ · repot 🪴 · revitalize ✨ · revisit 🔄", - - // Settings — sign-in / sign-out - settingSiteUrl: "Site URL", - settingSiteUrlDesc: "Your site's home page. Clicking Sign in opens your blog's login page in the browser — the same flow iA Writer uses.", - settingSiteUrlPlaceholder: "https://example.com", // intentional: replaces personal domain in source - btnSignIn: "Sign in", - btnOpeningBrowser: "Opening browser…", - noticeEnterSiteUrl: "Enter your site URL first.", - noticeSignedInAs: "✅ Signed in as {me}", - noticeSignInFailed: "Sign-in failed: {error}", - lblSignedIn: "Signed in", - btnSignOut: "Sign out", - manualTokenSummary: "Or paste a token manually", - settingAccessToken: "Access token", - settingAccessTokenDesc: "Bearer token from your Indiekit admin panel.", - btnVerify: "Verify", - noticeSetEndpointFirst: "Set the Micropub endpoint and token first.", - noticeTokenValid: "✅ Token is valid!", - noticeTokenCheckFailed: "Token check failed: {error}", - - // Syndication dialog - syndDialogTitle: "Syndication targets", - syndDialogSubtitle: "Choose where to cross-post this note.", - btnCancel: "Cancel", - btnPublish: "Publish", - - // IndieAuth - errSignInTimeout: "Sign-in timed out (5 min). Please try again.", -}; -``` - -- [ ] **Step 2: Build to confirm no TypeScript errors** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build -``` -Expected: exits 0. - -- [ ] **Step 3: Commit** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" -git add src/lang/en.ts -git commit -m "feat(i18n): add English locale file with all UI strings" -``` - ---- - -## Task 2: German locale file - -**Files:** -- Create: `src/lang/de.ts` - -- [ ] **Step 1: Create `src/lang/de.ts`** - -```typescript -// src/lang/de.ts -export const de: Record = { - // Commands & ribbon - cmdPublish: "An Micropub veröffentlichen", - cmdUpdate: "Bestehenden Micropub-Beitrag aktualisieren", - - // Notices — main.ts - noticeOpenNote: "Öffne eine Markdown-Notiz zum Veröffentlichen.", - noticeNoEndpoint: "⚠️ Micropub-Endpunkt nicht konfiguriert. Bitte in den Plugin-Einstellungen eintragen.", - noticeNoToken: "⚠️ Zugriffstoken nicht konfiguriert. Bitte in den Plugin-Einstellungen eintragen.", - noticePublishing: "Wird veröffentlicht…", - noticePublished: "✅ Veröffentlicht!", - noticePublishFailed: "❌ Veröffentlichung fehlgeschlagen: {error}", - noticeError: "❌ Fehler: {error}", - noticeNoSyndTargets: "⚠️ Syndizierungsziele konnten nicht abgerufen werden. Veröffentlichung ohne Dialog.", - - // Settings headings - settingsTitle: "Micropub Publisher", - settingsAccount: "Konto", - settingsEndpoints: "Endpunkte", - settingsEndpointsHint: "Diese werden beim Anmelden automatisch ausgefüllt. Nur manuell bearbeiten, wenn der Server nicht standardmäßige Pfade verwendet.", - settingsPublishBehaviour:"Veröffentlichungsverhalten", - settingsDigitalGarden: "Digitaler Garten", - - // Settings — endpoints - settingMicropubEndpoint: "Micropub-Endpunkt", - settingMicropubEndpointDesc: "z. B. https://example.com/micropub", - settingMediaEndpoint: "Medien-Endpunkt", - settingMediaEndpointDesc:"Für Bild-Uploads. Wird automatisch ermittelt, wenn leer.", - - // Settings — publish behaviour - settingVisibility: "Standard-Sichtbarkeit", - settingVisibilityDesc: "Gilt, wenn die Notiz keine explizite Sichtbarkeits-Eigenschaft hat.", - visibilityPublic: "Öffentlich", - visibilityUnlisted: "Nicht gelistet", - visibilityPrivate: "Privat", - - settingWriteUrl: "URL zurück in Notiz schreiben", - settingWriteUrlDesc: "Nach der Veröffentlichung wird die Beitrags-URL als `mp-url` im Frontmatter gespeichert. Spätere Veröffentlichungen aktualisieren den Beitrag statt einen neuen zu erstellen.", - - settingSyndDialog: "Syndizierungsdialog", - settingSyndDialogDesc: "Wann der Dialog zum Querverweis vor der Veröffentlichung angezeigt wird. 'Bei Bedarf' zeigt ihn nur, wenn kein mp-syndicate-to im Frontmatter vorhanden ist.", - syndDialogWhenNeeded: "Bei Bedarf", - syndDialogAlways: "Immer", - syndDialogNever: "Nie", - - settingSyndDefaults: "Standard-Syndizierungsziele", - settingSyndDefaultsNone: "Keine konfiguriert. Im Veröffentlichungsdialog standardmäßig aktivierte Ziele.", - btnClearDefaults: "Standards löschen", - - // Settings — digital garden - settingGardenTags: "#garden/*-Tags zu gardenStage zuordnen", - settingGardenTagsDesc: "Obsidian-Tags wie #garden/plant werden zur Micropub-Eigenschaft `garden-stage: plant`. Der Blog zeigt diese als Wachstumsstufen-Abzeichen unter /garden/ an.", - settingGardenStages: "Stufen: plant 🌱 · cultivate 🌿 · question ❓ · repot 🪴 · revitalize ✨ · revisit 🔄", - - // Settings — sign-in / sign-out - settingSiteUrl: "Website-URL", - settingSiteUrlDesc: "Startseite deiner Website. Klick auf Anmelden öffnet die Login-Seite deines Blogs im Browser.", - settingSiteUrlPlaceholder: "https://example.com", - btnSignIn: "Anmelden", - btnOpeningBrowser: "Browser wird geöffnet…", - noticeEnterSiteUrl: "Bitte zuerst die Website-URL eingeben.", - noticeSignedInAs: "✅ Angemeldet als {me}", - noticeSignInFailed: "Anmeldung fehlgeschlagen: {error}", - lblSignedIn: "Angemeldet", - btnSignOut: "Abmelden", - manualTokenSummary: "Oder Token manuell einfügen", - settingAccessToken: "Zugriffstoken", - settingAccessTokenDesc: "Bearer-Token aus deinem Indiekit-Adminbereich.", - btnVerify: "Prüfen", - noticeSetEndpointFirst: "Bitte zuerst Micropub-Endpunkt und Token eingeben.", - noticeTokenValid: "✅ Token ist gültig!", - noticeTokenCheckFailed: "Token-Prüfung fehlgeschlagen: {error}", - - // Syndication dialog - syndDialogTitle: "Syndizierungsziele", - syndDialogSubtitle: "Wo soll diese Notiz gleichzeitig veröffentlicht werden?", - btnCancel: "Abbrechen", - btnPublish: "Veröffentlichen", - - // IndieAuth - errSignInTimeout: "Anmeldung abgelaufen (5 Min.). Bitte erneut versuchen.", -}; -``` - -- [ ] **Step 2: Build** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build -``` -Expected: exits 0. - -- [ ] **Step 3: Commit** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" -git add src/lang/de.ts -git commit -m "feat(i18n): add German locale file" -``` - ---- - -## Task 3: i18n helper - -**Files:** -- Create: `src/i18n.ts` - -- [ ] **Step 1: Create `src/i18n.ts`** - -```typescript -// src/i18n.ts -import { en } from "./lang/en"; -import { de } from "./lang/de"; - -const locales: Record> = { en, de }; - -/** - * Returns the translated string for `key` in the active Obsidian locale. - * Falls back to English if the locale or key is missing. - * - * Supports `{var}` interpolation: - * t("noticePublishFailed", { error: "500" }) - * → "❌ Publish failed: 500" - */ -export function t(key: string, vars?: Record): string { - const lang = (window.moment?.locale() ?? "en").split("-")[0]; - const map = locales[lang] ?? locales["en"]; - let str = map[key] ?? locales["en"][key] ?? key; - - if (vars) { - for (const [k, v] of Object.entries(vars)) { - str = str.split(`{${k}}`).join(v); // replaceAll not available in ES2018 - } - } - return str; -} -``` - -- [ ] **Step 2: Build** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build -``` -Expected: exits 0. - -- [ ] **Step 3: Commit** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" -git add src/i18n.ts -git commit -m "feat(i18n): add t() helper with locale detection and interpolation" -``` - ---- - -## Task 4: Wire `t()` into `main.ts` - -**Files:** -- Modify: `src/main.ts` - -- [ ] **Step 1: Add import and replace all strings** - -At the top of `main.ts`, add: -```typescript -import { t } from "./i18n"; -``` - -Then replace each hardcoded string: - -| Old | New | -|-----|-----| -| `"Publish to Micropub"` (command name, line 36) | `t("cmdPublish")` | -| `"Update existing Micropub post"` (line 49) | `t("cmdUpdate")` | -| `"Publish to Micropub"` (ribbon tooltip, line 75) | `t("cmdPublish")` | -| `"Open a Markdown note to publish."` (line 78) | `t("noticeOpenNote")` | -| `"⚠️ Micropub endpoint not configured…"` (line 93) | `t("noticeNoEndpoint")` | -| `"⚠️ Access token not configured…"` (line 100) | `t("noticeNoToken")` | -| `"Publishing…"` (line 114) | `t("noticePublishing")` | -| `` `✅ Published!${urlDisplay}` `` (line 126) | `` `${t("noticePublished")}${urlDisplay}` `` | -| `` `❌ Publish failed: ${result.error}` `` (line 128) | `t("noticePublishFailed", { error: result.error ?? "" })` | -| `` `❌ Error: ${msg}` `` (line 134) | `t("noticeError", { error: msg })` | -| `"⚠️ Could not fetch syndication targets…"` (line 167) | `t("noticeNoSyndTargets")` | - -- [ ] **Step 2: Build** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build -``` -Expected: exits 0. - -- [ ] **Step 3: Commit** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" -git add src/main.ts -git commit -m "feat(i18n): wire t() into main.ts" -``` - ---- - -## Task 5: Wire `t()` into `SettingsTab.ts` - -**Files:** -- Modify: `src/SettingsTab.ts` - -- [ ] **Step 1: Add import** - -```typescript -import { t } from "./i18n"; -``` - -- [ ] **Step 2: Replace strings in `display()`** - -| Old | New | -|-----|-----| -| `"Micropub Publisher"` (h2) | `t("settingsTitle")` | -| `"Account"` (h3) | `t("settingsAccount")` | -| `"Endpoints"` (h3) | `t("settingsEndpoints")` | -| `"These are filled automatically…"` (p) | `t("settingsEndpointsHint")` | -| `"Micropub endpoint"` (setName) | `t("settingMicropubEndpoint")` | -| `"e.g. https://blog.giersig.eu/micropub"` (setDesc) | `t("settingMicropubEndpointDesc")` | -| `"Media endpoint"` (setName) | `t("settingMediaEndpoint")` | -| `"For image uploads…"` (setDesc) | `t("settingMediaEndpointDesc")` | -| `"Publish Behaviour"` (h3) | `t("settingsPublishBehaviour")` | -| `"Default visibility"` (setName) | `t("settingVisibility")` | -| `"Applies when the note…"` (setDesc) | `t("settingVisibilityDesc")` | -| `"Public"` | `t("visibilityPublic")` | -| `"Unlisted"` | `t("visibilityUnlisted")` | -| `"Private"` | `t("visibilityPrivate")` | -| `"Write URL back to note"` (setName) | `t("settingWriteUrl")` | -| `"After publishing, store…"` (setDesc) | `t("settingWriteUrlDesc")` | -| `"Syndication dialog"` (setName) | `t("settingSyndDialog")` | -| `"When to show the cross-posting…"` (setDesc) | `t("settingSyndDialogDesc")` | -| `"When needed"` | `t("syndDialogWhenNeeded")` | -| `"Always"` | `t("syndDialogAlways")` | -| `"Never"` | `t("syndDialogNever")` | -| `"Default syndication targets"` (setName) | `t("settingSyndDefaults")` | -| `"None configured…"` (false branch of setDesc — when `defaults.length === 0`) | `t("settingSyndDefaultsNone")` | -| `"Clear defaults"` (btn) | `t("btnClearDefaults")` | -| `"Digital Garden"` (h3) | `t("settingsDigitalGarden")` | -| `"Map #garden/* tags…"` (setName) | `t("settingGardenTags")` | -| `"Obsidian tags like…"` (setDesc) | `t("settingGardenTagsDesc")` | -| `"Stages: plant 🌱…"` (p) | `t("settingGardenStages")` | - -- [ ] **Step 3: Replace strings in `renderSignedOut()`** - -| Old | New | -|-----|-----| -| `"Site URL"` (setName) | `t("settingSiteUrl")` | -| `"Your site's home page…"` (setDesc) | `t("settingSiteUrlDesc")` | -| `"https://blog.giersig.eu"` (placeholder) | `t("settingSiteUrlPlaceholder")` | -| `"Sign in"` (btn) | `t("btnSignIn")` | -| `"Enter your site URL first."` (Notice) | `t("noticeEnterSiteUrl")` | -| `"Opening browser…"` (btn) | `t("btnOpeningBrowser")` | -| `` `✅ Signed in as ${result.me}` `` | `t("noticeSignedInAs", { me: result.me })` | -| `` `Sign-in failed: ${String(err)}` `` | `t("noticeSignInFailed", { error: String(err) })` | -| `"Sign in"` (catch btn reset) | `t("btnSignIn")` | -| `"Or paste a token manually"` (summary) | `t("manualTokenSummary")` | -| `"Access token"` (setName) | `t("settingAccessToken")` | -| `"Bearer token from your Indiekit admin panel."` (setDesc) | `t("settingAccessTokenDesc")` | -| `"Verify"` (btn) | `t("btnVerify")` | -| `"Set the Micropub endpoint and token first."` (Notice) | `t("noticeSetEndpointFirst")` | -| `"✅ Token is valid!"` (Notice) | `t("noticeTokenValid")` | -| `` `Token check failed: ${String(err)}` `` | `t("noticeTokenCheckFailed", { error: String(err) })` | - -- [ ] **Step 4: Replace strings in `renderSignedIn()`** - -| Old | New | -|-----|-----| -| `"Signed in"` (label) | `t("lblSignedIn")` | -| `"Site URL"` (setName) | `t("settingSiteUrl")` | -| `"Sign out"` (btn) | `t("btnSignOut")` | - -- [ ] **Step 5: Build** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build -``` -Expected: exits 0. - -- [ ] **Step 6: Commit** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" -git add src/SettingsTab.ts -git commit -m "feat(i18n): wire t() into SettingsTab.ts" -``` - ---- - -## Task 6: Wire `t()` into `SyndicationDialog.ts` and `IndieAuth.ts` - -**Files:** -- Modify: `src/SyndicationDialog.ts` -- Modify: `src/IndieAuth.ts` - -- [ ] **Step 1: Update `SyndicationDialog.ts`** - -Add import: -```typescript -import { t } from "./i18n"; -``` - -Replace: - -| Old | New | -|-----|-----| -| `"Syndication targets"` (h2) | `t("syndDialogTitle")` | -| `"Choose where to cross-post this note."` (p) | `t("syndDialogSubtitle")` | -| `"Cancel"` (btn) | `t("btnCancel")` | -| `"Publish"` (btn) | `t("btnPublish")` | - -- [ ] **Step 2: Update `IndieAuth.ts`** - -Add import at top: -```typescript -import { t } from "./i18n"; -``` - -Replace: - -| Old | New | -|-----|-----| -| `"Sign-in timed out (5 min). Please try again."` | `t("errSignInTimeout")` | - -- [ ] **Step 3: Build** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build -``` -Expected: exits 0. - -- [ ] **Step 4: Commit** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" -git add src/SyndicationDialog.ts src/IndieAuth.ts -git commit -m "feat(i18n): wire t() into SyndicationDialog and IndieAuth" -``` - ---- - -## Task 7: README update - -**Files:** -- Modify: `README.md` - -- [ ] **Step 1: Add i18n note to Configuration section** - -After the Settings reference table, add: - -```markdown -### Language - -The plugin follows Obsidian's display language. Set it in **Settings → About → Language**. Currently supported: English (`en`), German (`de`). -``` - -- [ ] **Step 2: Add `SyndicationDialog.ts` already done in prior session (verify)** - -Architecture table should already show `SyndicationDialog.ts` from the prior README update. - -- [ ] **Step 3: Commit** - -```bash -cd "/Users/sven/PARA/1. Projects/obsidian-micropub" -git add README.md -git commit -m "docs: note i18n support (en/de) in README" -``` - ---- - -## Manual verification checklist - -After all tasks, load the plugin in Obsidian and verify: - -- [ ] With Obsidian in English: all UI labels, notices, and dialog text appear in English -- [ ] With Obsidian in German (`de`): all UI labels, notices, and dialog text appear in German -- [ ] Publishing a note shows the correct locale notice (`Publishing…` / `Wird veröffentlicht…`) -- [ ] The syndication dialog title and buttons are translated -- [ ] Sign-in flow button text and error notices are translated -- [ ] Switching locale and restarting Obsidian picks up the new language diff --git a/docs/superpowers/specs/2026-03-30-syndication-dialog-design.md b/docs/superpowers/specs/2026-03-30-syndication-dialog-design.md deleted file mode 100644 index e98b720..0000000 --- a/docs/superpowers/specs/2026-03-30-syndication-dialog-design.md +++ /dev/null @@ -1,217 +0,0 @@ -# Syndication Dialog Design - -**Date:** 2026-03-30 -**Status:** Approved -**Scope:** obsidian-micropub plugin feature - ---- - -## 1. Overview - -Add a dialog that appears when publishing to Micropub, allowing users to select which syndication targets (e.g., Twitter, Mastodon) to cross-post to. The dialog integrates with the existing `?q=config` Micropub endpoint to fetch available targets. - ---- - -## 2. User Flow - -1. User clicks "Publish to Micropub" -2. Plugin fetches `?q=config` to get available syndication targets -3. Plugin checks frontmatter for `mp-syndicate-to`: - - **Has values** → use those, skip dialog, publish - - **Empty array `[]`** → force dialog - - **Absent** → show dialog with defaults pre-checked -4. Dialog displays checkboxes for each target from server -5. User confirms → publish with selected targets -6. Successful publish writes `mp-syndicate-to` to frontmatter - ---- - -## 3. Configuration - -### New Settings - -| Setting | Type | Default | Description | -|---------|------|---------|-------------| -| `showSyndicationDialog` | enum | `"when-needed"` | When to show the dialog | -| `defaultSyndicateTo` | string[] | `[]` | Targets checked by default | - -### `showSyndicationDialog` Options - -- `"when-needed"` — Show only if `mp-syndicate-to` is absent from frontmatter -- `"always"` — Show every time user publishes -- `"never"` — Use defaults, never show dialog - -### Frontmatter Support - -Users can bypass the dialog per-note using frontmatter: - -```yaml ---- -# Skip dialog, auto-syndicate to these targets -mp-syndicate-to: [twitter, mastodon] - -# Force dialog even with defaults set -mp-syndicate-to: [] ---- -``` - ---- - -## 4. Components - -### 4.1 SyndicationDialog (New) - -**Location:** `src/SyndicationDialog.ts` - -**Responsibilities:** -- Render modal with checkbox list of targets -- Pre-check targets from `defaultSyndicateTo` setting -- Handle OK/Cancel actions -- Return selected target UIDs via promise - -**Interface:** -```typescript -export class SyndicationDialog extends Modal { - constructor( - app: App, - targets: SyndicationTarget[], - defaultSelected: string[] - ); - - /** - * Opens the dialog and waits for user selection. - * @returns Selected target UIDs, or null if cancelled. - */ - async awaitSelection(): Promise; -} -``` - -### 4.2 Publisher (Modified) - -**Changes:** -- Accept optional `syndicateToOverride?: string[]` parameter -- Merge override with frontmatter values (override wins) -- Write `mp-syndicate-to` to frontmatter on successful publish - -### 4.3 SettingsTab (Modified) - -**Changes:** -- Add dropdown for `showSyndicationDialog` behavior -- Display currently configured default targets (read-only list) -- Add button to clear defaults - -### 4.4 main.ts (Modified) - -**Changes:** -- Before calling `publishActiveNote`: - 1. Fetch `?q=config` for syndication targets - 2. Check frontmatter for `mp-syndicate-to` - 3. Decide whether to show dialog based on setting + frontmatter - 4. If showing dialog, wait for user selection - 5. Call `publisher.publish()` with selected targets - ---- - -## 5. Data Flow - -``` -User clicks "Publish" - │ - ▼ -Fetch ?q=config ──► Check frontmatter mp-syndicate-to - │ │ - │ ┌─────────────┼─────────────┐ - │ │ │ │ - │ Has values Absent Empty [] - │ (skip dialog) (show dialog) (show dialog) - │ │ │ │ - │ └─────────────┴─────────────┘ - │ │ - ▼ ▼ -SyndicationDialog (if needed) - │ - ▼ -Publisher.publish(selectedTargets?) - │ - ▼ -Write mp-syndicate-to to frontmatter -``` - ---- - -## 6. Error Handling - -| Scenario | Behavior | -|----------|----------| -| `?q=config` fails | Warn user, offer to publish without syndication or cancel | -| Dialog canceled | Abort publish, no changes | -| Micropub POST fails | Don't write `mp-syndicate-to` to frontmatter | -| No targets returned from server | Skip dialog, publish normally (backward compatible) | - ---- - -## 7. UI/UX Details - -### Dialog Layout - -``` -┌─────────────────────────────────────────┐ -│ Publish to Syndication Targets │ -├─────────────────────────────────────────┤ -│ │ -│ [✓] Twitter (@username) │ -│ [✓] Mastodon (@user@instance) │ -│ [ ] LinkedIn │ -│ │ -├─────────────────────────────────────────┤ -│ [Cancel] [Publish] │ -└─────────────────────────────────────────┘ -``` - -### Settings UI Addition - -``` -Publish Behaviour -├── Default visibility: [public ▼] -├── Write URL back to note: [✓] -├── Syndication dialog: [when-needed ▼] -│ └── when-needed: Show only if no mp-syndicate-to -├── Default syndication targets: -│ └── twitter, mastodon [Clear defaults] -└── ... -``` - ---- - -## 8. Edge Cases - -1. **User has no syndication targets configured on server** — Skip dialog, publish normally -2. **User cancels dialog** — Abort publish entirely, no state changes -3. **Micropub server returns targets but some are invalid** — Show all, let server reject invalid ones -4. **User changes targets in settings after publishing** — Affects future publishes only, doesn't retroactively change existing `mp-syndicate-to` frontmatter - ---- - -## 9. Backward Compatibility - -- Default `showSyndicationDialog: "when-needed"` means existing behavior unchanged for notes without frontmatter -- Existing `mp-syndicate-to` frontmatter values continue to work -- Plugin remains compatible with servers that don't return syndication targets - ---- - -## 10. Testing Considerations - -- Unit test: `SyndicationDialog` renders checkboxes correctly -- Unit test: Frontmatter parsing handles `mp-syndicate-to` array -- Unit test: Setting `"never"` skips dialog -- Integration test: Full flow from click to publish with targets -- Edge case: Server returns empty targets array -- Edge case: User cancels dialog - ---- - -## Approval - -**Approved by:** @svemagie -**Date:** 2026-03-30