diff --git a/src/app/globals.css b/src/app/globals.css index 2f22998..8d5f07e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1902,6 +1902,35 @@ html { Atmosphere Explorer Styles ============================================ */ +/* RecordPreview field rows. Each row is `LABEL value` — a fixed-width label + column beside a flexible value column. On a roomy card that reads well, but + when the preview is narrow (mobile, or embedded in a split pane) the value + column gets starved and prose / long AT-URIs / CIDs wrap mid-word into a thin + ribbon. `.record-fields` is a container-query host (it measures the card's + own width, not the viewport's); below the breakpoint the row flips to a + column so the label sits on top and the value spans the row's full width. */ +.record-fields { + container-type: inline-size; +} + +.field-row-label { + min-width: 140px; +} + +@container (max-width: 460px) { + .record-fields .field-row-main { + flex-direction: column; + gap: 0.25rem; + } + .record-fields .field-row-label { + min-width: 0; + /* Drop the baseline-alignment pad once stacked — beside the value it + lines the label up with the first text line; on top it'd just add a + stray gap above the label. */ + padding-top: 0; + } +} + .explore-small-caps { font-family: var(--font-serif); font-size: 0.75rem; diff --git a/src/components/RecordPreview.tsx b/src/components/RecordPreview.tsx index 8b2e371..edd8e74 100644 --- a/src/components/RecordPreview.tsx +++ b/src/components/RecordPreview.tsx @@ -158,7 +158,12 @@ export default function RecordPreview({ (not into the right column), so nested content gets the full container width instead of compounding indentation. */}
-
+ {/* container-query host: field rows below read this wrapper's + width (not the viewport's) to decide whether the value sits + beside its label or drops onto its own line — see .record-fields + in globals.css. Lets the card stack correctly inside narrow + split panes, not just on small screens. */} +
{previewFields.map(([key, val]) => ( // Inside the explorer (hideExplorerCtas) this card IS the // canonical record view, so top-level fields start expanded — @@ -341,6 +346,7 @@ function FieldRow({ }} >
-
{label}
+
+ {label} +
{isOpen ? : } - - {s.collection} - - - {s.path} - + + {s.collection} + + + {s.path} + + {/* Count over distinct-accounts, stacked and abbreviated (2.4k) so the metrics take one narrow right-hand column instead of two — leaving the NSID/path columns more room -- 2.51.2 From 754df209b4746c48b1791133b28aed64beba3735 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 15:31:06 +0000 Subject: [PATCH 2/2] Show all record fields; add rich-preview/raw-JSON view toggles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rich record preview now renders every field in the record value — including $type, createdAt, and updatedAt — instead of silently dropping them. The preview is meant to faithfully represent the record (and, in the explorer, mirror the raw JSON), so omitting fields hid real data and made the two views disagree. On explorer record pages, the always-present "Raw record JSON" disclosure is replaced with two inline, persisted view switches beneath the preview: - "Hide rich preview" collapses the rich card. - "Show raw JSON" swaps in the linkified record JSON in its place. Together they let a visitor read a record as JSON instead of (or alongside) the rich rendering. Both are backed by new preferences (hideRichPreview, showRawRecordJson) that sync to the PDS and are also exposed as toggles in account settings, mirroring the existing minimal-profile / minimal-post prefs. The generic-record Edit affordance now falls back to a standalone chip when the rich preview (which normally hosts it) is collapsed. --- src/components/RecordPreview.tsx | 12 +- src/components/account/tabs/GeneralTab.tsx | 16 +++ src/components/explore/RecordExplorer.tsx | 147 +++++++++++++-------- src/utils/atproto/preferencesPds.ts | 4 + src/utils/preferences.ts | 23 ++++ 5 files changed, 140 insertions(+), 62 deletions(-) diff --git a/src/components/RecordPreview.tsx b/src/components/RecordPreview.tsx index edd8e74..7db4299 100644 --- a/src/components/RecordPreview.tsx +++ b/src/components/RecordPreview.tsx @@ -50,12 +50,12 @@ export default function RecordPreview({ const recordType = value.$type || collection; const displayType = recordType.replace('app.bsky.', '').replace('com.atproto.', '').replace('net.anisota.', ''); - // Get a few key interesting fields to preview (limit to 5-6 on universal - // link pages, no cap inside the explorer where the page is the canonical - // record view). - const allFields = Object.entries(value).filter( - ([key]) => !key.startsWith('$') && key !== 'createdAt' && key !== 'updatedAt' - ); + // Show every field in the record value — including $type, createdAt, and + // updatedAt. The preview is meant to be a faithful, readable rendering of + // the record (and, inside the explorer, the rich counterpart to the raw + // JSON view), so dropping fields would make the two disagree and hide real + // data. Universal-link pages still cap the count for a compact teaser. + const allFields = Object.entries(value); const previewFields = hideExplorerCtas ? allFields : allFields.slice(0, 6); const hasMoreFields = !hideExplorerCtas && allFields.length > 6; diff --git a/src/components/account/tabs/GeneralTab.tsx b/src/components/account/tabs/GeneralTab.tsx index 4c37a2b..307118c 100644 --- a/src/components/account/tabs/GeneralTab.tsx +++ b/src/components/account/tabs/GeneralTab.tsx @@ -122,6 +122,22 @@ function AppearanceCard() { checked={prefs.minimalPostPreview} onChange={(next) => update((p) => ({ ...p, minimalPostPreview: next }))} /> + + update((p) => ({ ...p, hideRichPreview: next }))} + /> + + update((p) => ({ ...p, showRawRecordJson: next }))} + /> ); } diff --git a/src/components/explore/RecordExplorer.tsx b/src/components/explore/RecordExplorer.tsx index 64f5372..35dd791 100644 --- a/src/components/explore/RecordExplorer.tsx +++ b/src/components/explore/RecordExplorer.tsx @@ -111,6 +111,12 @@ export default function RecordExplorer({ repo, collection, rkey }: Props) { const isPost = collection === 'app.bsky.feed.post'; const showPostViewSwitch = !prefsLoading && isPost; const minimalPost = showPostViewSwitch && prefs.minimalPostPreview; + // Rich-preview vs raw-JSON view state. Both gated on prefs having settled so + // first paint matches the defaults (rich shown, raw hidden) and doesn't + // flicker. The two are independent: a visitor can collapse the rich card, + // surface the raw JSON, do both, or neither. + const hideRich = !prefsLoading && prefs.hideRichPreview; + const showRawJson = !prefsLoading && prefs.showRawRecordJson; // Universal link uses the canonical `/profile/` path; bare-form // `///` still works as a fallback route but // shareable copies should point at the canonical one. @@ -177,44 +183,80 @@ export default function RecordExplorer({ repo, collection, rkey }: Props) { {!record && !editing && (

Loading record…

)} - - {editing && canEdit && agent ? ( - { - setRecord((prev) => (prev ? { ...prev, value: next } : prev)); - }} - onDeleted={() => { - setEditing(false); - router.push(`/explore/${repoSeg}/${collection}`); - }} - onCancel={() => setEditing(false)} + {(editing || !hideRich) && ( + + {editing && canEdit && agent ? ( + { + setRecord((prev) => (prev ? { ...prev, value: next } : prev)); + }} + onDeleted={() => { + setEditing(false); + router.push(`/explore/${repoSeg}/${collection}`); + }} + onCancel={() => setEditing(false)} + /> + ) : ( + + )} + + )} + {/* View switches beneath the preview. "Hide rich preview" collapses + the rich card; "Show raw JSON" swaps in the linkified record JSON + (replacing the old standalone disclosure at the page foot). The + minimal/full post switch only applies to the rich post card, so + it's hidden once the rich preview is collapsed. All flip persisted + prefs so the choice sticks and syncs across devices. */} + {!editing && record && ( +
+ + updatePrefs((p) => ({ ...p, hideRichPreview: !p.hideRichPreview })) + } /> - ) : ( - + updatePrefs((p) => ({ ...p, minimalPostPreview: !p.minimalPostPreview })) + } + /> + )} + + updatePrefs((p) => ({ ...p, showRawRecordJson: !p.showRawRecordJson })) + } /> - )} - - {/* Inline switch between the rich post card and the minimal - record-only view — mirrors the repo page's profile switch so - users can flip without round-tripping through settings. */} - {!editing && showPostViewSwitch && ( - - updatePrefs((p) => ({ ...p, minimalPostPreview: !p.minimalPostPreview })) - } - /> +
+ )} + {/* Raw record JSON, surfaced right under the preview area so it + reads as the rich preview's counterpart rather than a footnote. */} + {!editing && record && showRawJson && ( + + + )} )} @@ -273,11 +315,11 @@ export default function RecordExplorer({ repo, collection, rkey }: Props) {
)} - {/* Standalone Edit chip — only for post / margin previews that - don't have a place to slot the button into their layout, and - only in read mode. Sits with the sign-in prompt below as the - page's editing affordances. */} - {!editsInPreviewFooter && !editing && editButton && ( + {/* Standalone Edit chip — for post / margin previews that don't have a + place to slot the button into their layout, and as the fallback when + the rich preview (which would host the generic record's footer Edit + button) is collapsed. Read mode only. */} + {(!editsInPreviewFooter || hideRich) && !editing && editButton && (
)} - {record && ( -
- Raw record JSON - -
- )}
); } /** - * Quiet text toggle between the rich post card and the minimal - * record-only view, mirroring the repo page's ProfileViewSwitch. Flips the - * persisted `minimalPostPreview` pref so the choice sticks and syncs. + * Quiet text toggle used for the record page's view switches (hide/show the + * rich preview, minimal/full post, show/hide raw JSON). Mirrors the repo + * page's ProfileViewSwitch styling; callers own the persisted pref each one + * flips and supply a state-dependent label. */ -function PostViewSwitch({ - minimal, +function ViewSwitchButton({ + label, onToggle, }: { - minimal: boolean; + label: string; onToggle: () => void; }) { return ( @@ -337,8 +374,6 @@ function PostViewSwitch({ type="button" onClick={onToggle} style={{ - alignSelf: 'flex-start', - marginTop: '-0.5rem', padding: 0, background: 'transparent', border: 0, @@ -356,7 +391,7 @@ function PostViewSwitch({ e.currentTarget.style.color = 'var(--text-tertiary)'; }} > - {minimal ? 'Show full post preview' : 'Use minimal view'} + {label} ); } diff --git a/src/utils/atproto/preferencesPds.ts b/src/utils/atproto/preferencesPds.ts index 1514a53..1adbcdf 100644 --- a/src/utils/atproto/preferencesPds.ts +++ b/src/utils/atproto/preferencesPds.ts @@ -25,6 +25,8 @@ * hideRepoGlance?: boolean // hide the "Repo at a glance" section * repoGlanceCollapsedByDefault?: boolean // start that section collapsed * minimalProfile?: boolean // minimal profile on repo pages + * hideRichPreview?: boolean // collapse the rich preview on record pages + * showRawRecordJson?: boolean // show raw record JSON on record pages * updatedAt: datetime * * // Legacy — still written for back-compat with older Aturi clients @@ -101,6 +103,8 @@ export async function writePreferencesToPds( hideRepoGlance: prefs.hideRepoGlance, repoGlanceCollapsedByDefault: prefs.repoGlanceCollapsedByDefault, minimalProfile: prefs.minimalProfile, + hideRichPreview: prefs.hideRichPreview, + showRawRecordJson: prefs.showRawRecordJson, // Legacy fields kept for back-compat with older clients. hiddenWaypoints: prefs.hiddenWaypoints, waypointOrder: prefs.waypointOrder, diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index 7cd140a..4f591ce 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -140,6 +140,19 @@ export type Preferences = { * offers an inline switch to flip back to the full preview. */ minimalPostPreview: boolean; + /** + * Collapse the rich preview card on explorer record pages. Paired with + * `showRawRecordJson` this lets a visitor read the record as raw JSON + * instead of (or alongside) the rich rendering. Flipped by the inline + * "Hide rich preview" switch beneath the preview. + */ + hideRichPreview: boolean; + /** + * Show the raw (linkified) record JSON on explorer record pages. Replaces + * the old always-present "Raw record JSON" disclosure with a persisted + * toggle, surfaced by the inline "Show raw JSON" switch. + */ + showRawRecordJson: boolean; /** * ISO timestamp of last local change. Used to break ties when local and * PDS prefs both exist on sign-in. @@ -165,6 +178,8 @@ export const DEFAULT_PREFERENCES: Preferences = { repoGlanceCollapsedByDefault: false, minimalProfile: false, minimalPostPreview: false, + hideRichPreview: false, + showRawRecordJson: false, updatedAt: new Date(0).toISOString(), }; @@ -266,6 +281,10 @@ export function mergeWithDefaults(input: Partial | null | undefined typeof input.minimalProfile === 'boolean' ? input.minimalProfile : false; const minimalPostPreview = typeof input.minimalPostPreview === 'boolean' ? input.minimalPostPreview : false; + const hideRichPreview = + typeof input.hideRichPreview === 'boolean' ? input.hideRichPreview : false; + const showRawRecordJson = + typeof input.showRawRecordJson === 'boolean' ? input.showRawRecordJson : false; return { waypointGroups, hiddenWaypoints, @@ -281,6 +300,8 @@ export function mergeWithDefaults(input: Partial | null | undefined repoGlanceCollapsedByDefault, minimalProfile, minimalPostPreview, + hideRichPreview, + showRawRecordJson, updatedAt: typeof input.updatedAt === 'string' ? input.updatedAt : new Date(0).toISOString(), }; @@ -350,6 +371,8 @@ export function preferencesAreEqual(a: Preferences, b: Preferences): boolean { a.repoGlanceCollapsedByDefault === b.repoGlanceCollapsedByDefault && a.minimalProfile === b.minimalProfile && a.minimalPostPreview === b.minimalPostPreview && + a.hideRichPreview === b.hideRichPreview && + a.showRawRecordJson === b.showRawRecordJson && JSON.stringify(a.waypointGroups) === JSON.stringify(b.waypointGroups) && JSON.stringify(a.customWaypoints) === JSON.stringify(b.customWaypoints) && JSON.stringify(a.knownWaypointIds) === JSON.stringify(b.knownWaypointIds) &&