-
+
+
{:else if view === "search"}
@@ -946,57 +965,119 @@
border-color: var(--red);
}
+ /* X-style settings page: sectioned cards with label-left /
+ value-right rows, then a list of clickable action rows, then
+ a danger zone at the bottom. Stays monospace + terminal-
+ commented, but the structure is the same as X's. */
+ .settings {
+ padding: 0 var(--s-3) var(--s-6);
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-4);
+ }
+ .settings__group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-2);
+ }
.settings__h3 {
font-family: var(--font-mono);
font-size: var(--fs-50);
- color: var(--text-dim);
- letter-spacing: 0.04em;
- margin: var(--s-4) 0 var(--s-2);
- font-weight: 400;
+ color: var(--orange);
+ letter-spacing: var(--tracking-label);
+ margin: 0;
+ font-weight: 700;
}
-
- .did-cell {
- word-break: break-all;
- font-size: var(--fs-50);
- }
-
- .settings {
- padding: 0 var(--s-3);
+ .settings__list {
display: flex;
flex-direction: column;
- gap: var(--s-2);
+ background: var(--bg-elev);
+ border: 1px solid var(--line);
+ border-radius: var(--r-md);
+ overflow: hidden;
}
- .settings__rows {
+ /* Each row is a label-left / value-right flex line, separated
+ by a hairline (X uses a single border on each row except the
+ last). */
+ .settings__row {
display: flex;
- flex-direction: column;
- gap: var(--s-1);
- padding: var(--s-2) var(--s-4);
- margin: 0 0 var(--s-4);
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--s-3);
+ padding: var(--s-3) var(--s-4);
+ border-bottom: 1px solid var(--line);
font-family: var(--font-mono);
font-size: var(--fs-50);
}
- .settings__rows > div {
- display: flex;
- gap: var(--s-3);
+ .settings__list .settings__row:last-child {
+ border-bottom: 0;
}
- .settings__rows dt {
+ .settings__label {
color: var(--text-dim);
- letter-spacing: 0.04em;
- min-width: 9rem;
+ letter-spacing: var(--tracking-label);
+ flex: 0 0 auto;
}
- .settings__rows dd {
- margin: 0;
+ .settings__value {
color: var(--text);
+ text-align: right;
+ word-break: break-all;
+ min-width: 0;
}
+ .settings__value--mono {
+ font-size: var(--fs-50);
+ }
+ /* Actions live in their own list — same border-radius but each
+ item is a full-width clickable button. The hint on the right
+ (e.g. "atproto", "↗ bsky.app") is a dim secondary label, the
+ same way X shows the destination on follow / open-in-app
+ rows. */
.settings__actions {
display: flex;
- flex-wrap: wrap;
- gap: var(--s-2);
- margin: 0 0 var(--s-4);
+ flex-direction: column;
+ background: var(--bg-elev);
+ border: 1px solid var(--line);
+ border-radius: var(--r-md);
+ overflow: hidden;
}
- .settings__signout {
- margin-top: var(--s-4);
- padding-top: var(--s-4);
- border-top: 1px dashed var(--line);
+ .settings__action {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--s-3);
+ padding: var(--s-3) var(--s-4);
+ background: transparent;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ color: var(--text);
+ font-family: var(--font-mono);
+ font-size: var(--fs-100);
+ text-align: left;
+ cursor: pointer;
+ transition: background-color var(--dur) var(--ease),
+ color var(--dur) var(--ease);
+ }
+ .settings__actions .settings__action:last-child {
+ border-bottom: 0;
+ }
+ .settings__action:hover {
+ background: var(--orange-8);
+ color: var(--orange);
+ }
+ .settings__action-hint {
+ color: var(--text-dim);
+ font-size: var(--fs-50);
+ }
+ .settings__action:hover .settings__action-hint {
+ color: var(--orange);
+ }
+ .settings__group--danger .settings__action {
+ color: var(--red);
+ }
+ .settings__group--danger .settings__action:hover {
+ background: rgba(255, 59, 48, 0.08);
+ color: var(--red);
+ }
+ .settings__group--danger {
+ margin-top: var(--s-3);
}
diff --git a/crates/tauri-app/src/lib/components/PostCard.svelte b/crates/tauri-app/src/lib/components/PostCard.svelte
index e6da4ab..937f48b 100644
--- a/crates/tauri-app/src/lib/components/PostCard.svelte
+++ b/crates/tauri-app/src/lib/components/PostCard.svelte
@@ -124,36 +124,27 @@
> | null = $state(null);
$effect.pre(() => {
+ // Restore like/repost state from localStorage on mount and on
+ // every post-prop change (so navigating between posts restores
+ // the right per-post state). The writes are inside `untrack` so
+ // the effect's reactive dep set is just `[post.did, post.rkey]`
+ // — without untrack, each write to `liked` / `reposted` / etc.
+ // would re-enter the effect and the depth tracker would abort
+ // with `effect_update_depth_exceeded` as soon as the user
+ // clicks the heart or repost button.
const likeKey = localStorageKey(`liked:${post.did}:${post.rkey}`);
const repostKey = localStorageKey(`reposted:${post.did}:${post.rkey}`);
- likedBox = useLocalStorage(likeKey, { liked: false, uri: null });
- const storedLike = likedBox.get();
- liked = storedLike.liked;
- likedUri = storedLike.uri;
+ untrack(() => {
+ likedBox = useLocalStorage(likeKey, { liked: false, uri: null });
+ const storedLike = likedBox.get();
+ liked = storedLike.liked;
+ likedUri = storedLike.uri;
- repostedBox = useLocalStorage(repostKey, { reposted: false, uri: null });
- const storedRepost = repostedBox.get();
- reposted = storedRepost.reposted;
- repostUri = storedRepost.uri;
- });
-
- $effect(() => {
- // Skip the write when the box's stored value already matches the
- // current state — on first mount, `$effect.pre` reads the stored
- // value into `liked` / `likedUri`, and without this guard the very
- // first reactive pass would re-write the same bytes and trigger
- // an empty `notify()` to subscribers.
- if (!likedBox) return;
- const current = likedBox.get();
- if (current.liked === liked && current.uri === likedUri) return;
- likedBox.set({ liked, uri: likedUri });
- });
-
- $effect(() => {
- if (!repostedBox) return;
- const current = repostedBox.get();
- if (current.reposted === reposted && current.uri === repostUri) return;
- repostedBox.set({ reposted, uri: repostUri });
+ repostedBox = useLocalStorage(repostKey, { reposted: false, uri: null });
+ const storedRepost = repostedBox.get();
+ reposted = storedRepost.reposted;
+ repostUri = storedRepost.uri;
+ });
});
async function onLikeClick(event: MouseEvent) {
@@ -168,11 +159,16 @@
const previousDelta = likeDelta;
liked = !wasLiked;
likeDelta += wasLiked ? -1 : 1;
+ // Persist the optimistic state — the localStorage write is
+ // synchronous so a page reload reflects the in-flight like even
+ // if the server roundtrip is still pending.
+ likedBox?.set({ liked, uri: likedUri });
try {
if (wasLiked) {
if (!likedUri) {
liked = wasLiked;
likeDelta = previousDelta;
+ likedBox?.set({ liked, uri: likedUri });
showError("can't unlike: missing like URI");
return;
}
@@ -182,9 +178,11 @@
const response = await likePost(post.uri, post.cid);
likedUri = response.uri;
}
+ likedBox?.set({ liked, uri: likedUri });
} catch (error) {
liked = wasLiked;
likeDelta = previousDelta;
+ likedBox?.set({ liked, uri: likedUri });
showError(`like failed: ${error}`);
} finally {
likeBusy = false;
@@ -203,11 +201,13 @@
const previousDelta = repostDelta;
reposted = !wasReposted;
repostDelta += wasReposted ? -1 : 1;
+ repostedBox?.set({ reposted, uri: repostUri });
try {
if (wasReposted) {
if (!repostUri) {
reposted = wasReposted;
repostDelta = previousDelta;
+ repostedBox?.set({ reposted, uri: repostUri });
showError("can't unrepost: missing repost URI");
return;
}
@@ -217,9 +217,11 @@
const response = await repostPost(post.uri, post.cid);
repostUri = response.uri;
}
+ repostedBox?.set({ reposted, uri: repostUri });
} catch (error) {
reposted = wasReposted;
repostDelta = previousDelta;
+ repostedBox?.set({ reposted, uri: repostUri });
showError(`repost failed: ${error}`);
} finally {
repostBusy = false;