From 7e9be094f917468aeafebc2d463390616800e90a Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Sat, 19 Sep 2026 15:21:48 +0900 Subject: [PATCH 1/4] Rename ActorDetails to ActorCard since it might be confusing with actor detail page. I asked Codex to rename ActorDetail.tsx to ActorCard.tsx and update [slug].tsx as well. Assisted-by: Codex:gpt-6-astra --- .../web/src/components/{ActorDetail.tsx => ActorCard.tsx} | 6 +++--- packages/web/src/routes/instance/[slug].tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) rename packages/web/src/components/{ActorDetail.tsx => ActorCard.tsx} (86%) diff --git a/packages/web/src/components/ActorDetail.tsx b/packages/web/src/components/ActorCard.tsx similarity index 86% rename from packages/web/src/components/ActorDetail.tsx rename to packages/web/src/components/ActorCard.tsx index af8be37..4d0fdcd 100644 --- a/packages/web/src/components/ActorDetail.tsx +++ b/packages/web/src/components/ActorCard.tsx @@ -18,14 +18,14 @@ import { graphql } from "relay-runtime"; import { Show } from "solid-js"; import { createFragment } from "solid-relay"; -import type { ActorDetail_actor$key } from "./__generated__/ActorDetail_actor.graphql.ts"; +import type { ActorCard_actor$key } from "./__generated__/ActorCard_actor.graphql.ts"; import styles from "~/styles/instance.module.css"; -export const ActorDetail = (props: { $actor: ActorDetail_actor$key }) => { +export const ActorCard = (props: { $actor: ActorCard_actor$key }) => { const actorData = createFragment( graphql` - fragment ActorDetail_actor on Actor { + fragment ActorCard_actor on Actor { handle } `, diff --git a/packages/web/src/routes/instance/[slug].tsx b/packages/web/src/routes/instance/[slug].tsx index 131ebd3..e16527b 100644 --- a/packages/web/src/routes/instance/[slug].tsx +++ b/packages/web/src/routes/instance/[slug].tsx @@ -29,7 +29,7 @@ import { useRelayEnvironment, } from "solid-relay"; -import { ActorDetail } from "~/components/ActorDetail.tsx"; +import { ActorCard } from "~/components/ActorCard.tsx"; import type { InstanceDetailQuery } from "./__generated__/InstanceDetailQuery.graphql.ts"; @@ -47,7 +47,7 @@ const instanceDetailQuery = graphql` totalCount edges { node { - ...ActorDetail_actor + ...ActorCard_actor } } } @@ -165,7 +165,7 @@ export default function InstanceDetailPage(
- {(edge) => } + {(edge) => }
From 5042f78d851bed86ca52a7f552e076dee4f7b55d Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Tue, 22 Sep 2026 00:27:33 +0900 Subject: [PATCH 2/4] Add actor detail page with identity and federation endpoints Link actor cards to a Relay ID route with preloaded actor details, loading and error states, and reusable copy buttons. Add Relay scalar mappings and styles for the detail page and buttons. AI provenance: I selected ID-based routing and asked Codex to implement the page, then requested the buttons stylesheet rename. Codex implemented the route, fragment component, copy controls, scalar mappings, and styles while preserving the card link I added. Codex ran the build and repository checks successfully before the final stylesheet rename, then checked the renamed import and diff whitespace. A subsequent AI review reported no actionable defects. Human browser verification was not recorded. Assisted-by: Codex:gpt-6 --- packages/web/relay.config.json | 5 + packages/web/src/components/ActorCard.tsx | 6 +- packages/web/src/components/ActorDetail.tsx | 149 ++++++++++++++++++++ packages/web/src/components/CopyButton.tsx | 49 +++++++ packages/web/src/routes/actor/[id].tsx | 105 ++++++++++++++ packages/web/src/styles/actor.module.css | 101 +++++++++++++ packages/web/src/styles/buttons.module.css | 37 +++++ packages/web/src/styles/instance.module.css | 5 +- 8 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 packages/web/src/components/ActorDetail.tsx create mode 100644 packages/web/src/components/CopyButton.tsx create mode 100644 packages/web/src/routes/actor/[id].tsx create mode 100644 packages/web/src/styles/actor.module.css create mode 100644 packages/web/src/styles/buttons.module.css diff --git a/packages/web/relay.config.json b/packages/web/relay.config.json index 89f9e7e..1f531fa 100644 --- a/packages/web/relay.config.json +++ b/packages/web/relay.config.json @@ -2,5 +2,10 @@ "src": "./src", "schema": "./schema.graphql", "language": "typescript", + "customScalarTypes": { + "DateTime": "string", + "URL": "string", + "UUID": "string" + }, "eagerEsModules": true } diff --git a/packages/web/src/components/ActorCard.tsx b/packages/web/src/components/ActorCard.tsx index 4d0fdcd..c7913a0 100644 --- a/packages/web/src/components/ActorCard.tsx +++ b/packages/web/src/components/ActorCard.tsx @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +import { A } from "@solidjs/router"; import { graphql } from "relay-runtime"; import { Show } from "solid-js"; import { createFragment } from "solid-relay"; @@ -27,6 +28,7 @@ export const ActorCard = (props: { $actor: ActorCard_actor$key }) => { graphql` fragment ActorCard_actor on Actor { handle + id } `, () => props.$actor, @@ -37,7 +39,9 @@ export const ActorCard = (props: { $actor: ActorCard_actor$key }) => { {(actor) => ( )} diff --git a/packages/web/src/components/ActorDetail.tsx b/packages/web/src/components/ActorDetail.tsx new file mode 100644 index 0000000..9c6186a --- /dev/null +++ b/packages/web/src/components/ActorDetail.tsx @@ -0,0 +1,149 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { Title } from "@solidjs/meta"; +import { graphql } from "relay-runtime"; +import { For, Show } from "solid-js"; +import { createFragment } from "solid-relay"; + +import type { ActorDetail_actor$key } from "./__generated__/ActorDetail_actor.graphql.ts"; +import { CopyButton } from "./CopyButton.tsx"; + +import styles from "~/styles/actor.module.css"; + +/** + * Read-only identity and federation information for an actor. + * @returns The actor identity and endpoint sections. + */ +export function ActorDetail(props: { $actor: ActorDetail_actor$key }) { + const data = createFragment( + graphql` + fragment ActorDetail_actor on Actor @throwOnFieldError { + handle + username + uuid + type + created + iri + avatarUrl + inboxUrl + outboxUrl + followersUrl + followingUrl + featuredUrl + profileUrl + instance { + host + } + } + `, + () => props.$actor, + ); + return ( + + {(actor) => { + const endpoints = () => [ + { label: "Actor IRI", url: actor().iri }, + { label: "Inbox", url: actor().inboxUrl }, + { label: "Outbox", url: actor().outboxUrl }, + { label: "Followers", url: actor().followersUrl }, + { label: "Following", url: actor().followingUrl }, + { label: "Featured", url: actor().featuredUrl }, + { label: "Profile", url: actor().profileUrl }, + ]; + return ( + <> + {actor().handle} — DrFed +
+ + {(url) => ( + + )} + +
+

{actor().type}

+

{actor().handle}

+ +
+
+
+

Identity

+
+
+
Username
+
{actor().username}
+
+
+
Instance
+
{actor().instance.host}
+
+
+
UUID
+
{actor().uuid}
+
+
+
Created
+
+ +
+
+
+
+
+

Federation endpoints

+
+ + {(endpoint) => ( + + {(url) => ( +
+
{endpoint.label}
+
+ {url()} + +
+
+ )} +
+ )} +
+
+
+ + ); + }} +
+ ); +} + +// Stored remote URLs are data, not trusted navigation targets. +function httpUrl(value: string): string | undefined { + try { + const url = new URL(value); + return url.protocol === "https:" || url.protocol === "http:" + ? url.href + : undefined; + } catch { + return undefined; + } +} diff --git a/packages/web/src/components/CopyButton.tsx b/packages/web/src/components/CopyButton.tsx new file mode 100644 index 0000000..19210a2 --- /dev/null +++ b/packages/web/src/components/CopyButton.tsx @@ -0,0 +1,49 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { createSignal } from "solid-js"; + +import styles from "~/styles/buttons.module.css"; + +/** + * Copy a value to the clipboard and announce the result. + * @returns A copy button with accessible feedback. + */ +export function CopyButton(props: { value: string; label: string }) { + const [message, setMessage] = createSignal(""); + async function copy() { + try { + await navigator.clipboard.writeText(props.value); + setMessage("Copied."); + } catch { + setMessage("Could not copy. Select and copy the text manually."); + } + } + return ( + + + {message()} + + ); +} diff --git a/packages/web/src/routes/actor/[id].tsx b/packages/web/src/routes/actor/[id].tsx new file mode 100644 index 0000000..3c1949a --- /dev/null +++ b/packages/web/src/routes/actor/[id].tsx @@ -0,0 +1,105 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { Title } from "@solidjs/meta"; +import { + A, + type RouteDefinition, + type RouteSectionProps, + query, +} from "@solidjs/router"; +import { graphql } from "relay-runtime"; +import { ErrorBoundary, Show, Suspense } from "solid-js"; +import { + createPreloadedQuery, + loadQuery, + useRelayEnvironment, +} from "solid-relay"; + +import { ActorDetail } from "~/components/ActorDetail.tsx"; + +import type { ActorDetailQuery } from "./__generated__/ActorDetailQuery.graphql.ts"; + +import styles from "~/styles/actor.module.css"; + +const actorDetailQuery = graphql` + query ActorDetailQuery($id: ID!) @throwOnFieldError { + node(id: $id) { + ... on Actor @alias(as: "actor") { + ...ActorDetail_actor + } + } + } +`; + +const loadActorDetailQuery = query( + (id: string) => + loadQuery(useRelayEnvironment()(), actorDetailQuery, { + id, + }), + "ActorDetailQuery", +); + +export const route = { + preload({ params }) { + if (params.id === undefined) { + throw new Error("Missing actor ID route parameter."); + } + return loadActorDetailQuery(params.id); + }, +} satisfies RouteDefinition; + +type RouteData = ReturnType; + +export default function ActorDetailPage(props: RouteSectionProps) { + return ( +
+ Actor — DrFed + + ← All instances + + + Unable to load this actor. Please reload the page to try again. +

+ } + > + Loading actor…}> + + +
+
+ ); +} + +function ActorDetailContent(props: { data: RouteData }) { + const data = createPreloadedQuery( + actorDetailQuery, + () => props.data, + ); + const actor = () => { + const node = data()?.node; + return node?.actor; + }; + return ( + + Actor not found.

}> + {(value) => } +
+
+ ); +} diff --git a/packages/web/src/styles/actor.module.css b/packages/web/src/styles/actor.module.css new file mode 100644 index 0000000..b1bd11c --- /dev/null +++ b/packages/web/src/styles/actor.module.css @@ -0,0 +1,101 @@ +/* +DrFed: A web-based platform for developing and debugging ActivityPub apps +Copyright (C) 2026 DrFed team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +.page { + display: grid; + gap: 1.5rem; + padding-bottom: 3rem; +} +.backLink { + color: var(--ink-soft); + justify-self: start; + font-weight: 700; +} +.header { + display: flex; + align-items: center; + gap: 1rem; + border-bottom: 1px solid var(--line); + padding-bottom: 1.25rem; +} +.header > div { + min-width: 0; +} +.header h1 { + margin: 0; + font-family: var(--font-mono); + font-size: clamp(1.25rem, 4vw, 2rem); + overflow-wrap: anywhere; +} +.avatar { + border-radius: var(--radius-sm); + object-fit: cover; + flex-shrink: 0; +} +.label { + color: var(--accent-strong); + font-weight: 700; + margin: 0 0 0.5rem; +} +.panel { + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: clamp(1rem, 3vw, 1.75rem); +} +.panel h2 { + margin: 0; + font-size: 1.3rem; +} +.fields { + margin: 1.25rem 0 0; +} +.fields > div { + display: grid; + grid-template-columns: 8rem minmax(0, 1fr); + gap: 1rem; + padding: 1rem 0; + border-top: 1px solid var(--line); +} +.fields dt { + color: var(--ink-soft); + font-weight: 700; +} +.fields dd { + margin: 0; + font-family: var(--font-mono); + font-size: 0.9rem; + overflow-wrap: anywhere; +} +.fields a { + color: var(--ink); + text-underline-offset: 0.2rem; +} +.page a:focus-visible { + outline: 3px solid var(--accent-soft); + outline-offset: 0.2rem; +} +@media (max-width: 520px) { + .fields > div { + grid-template-columns: 1fr; + gap: 0.5rem; + } + .header { + align-items: start; + } +} diff --git a/packages/web/src/styles/buttons.module.css b/packages/web/src/styles/buttons.module.css new file mode 100644 index 0000000..d82ea84 --- /dev/null +++ b/packages/web/src/styles/buttons.module.css @@ -0,0 +1,37 @@ +/* +DrFed: A web-based platform for developing and debugging ActivityPub apps +Copyright (C) 2026 DrFed team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +.copyControl { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.5rem; + font-size: 0.8rem; +} +.copyControl button { + background: var(--card); + color: var(--ink); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 0.35rem 0.65rem; + cursor: pointer; +} +.copyControl button:focus-visible { + outline: 3px solid var(--accent-soft); + outline-offset: 0.2rem; +} diff --git a/packages/web/src/styles/instance.module.css b/packages/web/src/styles/instance.module.css index ba80e9a..3372baf 100644 --- a/packages/web/src/styles/instance.module.css +++ b/packages/web/src/styles/instance.module.css @@ -171,7 +171,7 @@ along with this program. If not, see . width: 0.85rem; } -.actorCard p { +.actorCard a { font-family: var(--font-mono); font-size: 0.9rem; margin: 0; @@ -180,7 +180,8 @@ along with this program. If not, see . .backLink:focus-visible, .createButton:focus-visible, -.endpointList a:focus-visible { +.endpointList a:focus-visible, +.actorCard a:focus-visible { outline: 3px solid var(--accent-soft); outline-offset: 0.2rem; } From 15d9b587d52ad2fe9ddb1fefd344c5c081ca1842 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 23 Sep 2026 00:46:25 +0900 Subject: [PATCH 3/4] Fix actor endpoint copy button alignment and back link styling Place copy buttons to the right of endpoint URLs while allowing long URLs to wrap, and remove the underline from the All instances link. AI provenance: I requested right-aligned copy buttons and an un-underlined All instances link. Codex updated the endpoint markup and CSS and verified repository checks. Human browser verification was not recorded. Assisted-by: Codex:gpt-6-astra --- packages/web/src/components/ActorDetail.tsx | 2 +- packages/web/src/styles/actor.module.css | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/web/src/components/ActorDetail.tsx b/packages/web/src/components/ActorDetail.tsx index 9c6186a..084e577 100644 --- a/packages/web/src/components/ActorDetail.tsx +++ b/packages/web/src/components/ActorDetail.tsx @@ -118,7 +118,7 @@ export function ActorDetail(props: { $actor: ActorDetail_actor$key }) { {(url) => (
{endpoint.label}
-
+
{url()}
diff --git a/packages/web/src/styles/actor.module.css b/packages/web/src/styles/actor.module.css index b1bd11c..77a87e5 100644 --- a/packages/web/src/styles/actor.module.css +++ b/packages/web/src/styles/actor.module.css @@ -25,6 +25,7 @@ along with this program. If not, see . color: var(--ink-soft); justify-self: start; font-weight: 700; + text-decoration: none; } .header { display: flex; @@ -86,6 +87,21 @@ along with this program. If not, see . color: var(--ink); text-underline-offset: 0.2rem; } +.endpoint { + display: flex; + align-items: baseline; + gap: 0.75rem; +} +.endpoint > a { + flex: 1; + min-width: 0; +} +.endpoint > :last-child { + flex-shrink: 0; + flex-wrap: wrap; + max-width: 40%; + margin-top: 0; +} .page a:focus-visible { outline: 3px solid var(--accent-soft); outline-offset: 0.2rem; From 51a3e24ef079756ce513ed673032ce6a13b4be4d Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 23 Sep 2026 01:03:11 +0900 Subject: [PATCH 4/4] Use actor collection IRIs in the detail page Read outbox, followers, following, and featured URLs from the nullable collection fields introduced on main after the rebase. AI provenance: I asked Codex to update the actor detail page after rebasing and prepare the PR. Codex updated the Relay fragment and endpoint accessors, regenerated artifacts, and verified the build and repository checks. Automated Chrome checks covered actor rendering, endpoint links, clipboard copying, desktop/mobile layout, and error states against a temporary database. Human verification of this compatibility update has not been recorded. Assisted-by: Codex:gpt-6-astra --- packages/web/src/components/ActorDetail.tsx | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/web/src/components/ActorDetail.tsx b/packages/web/src/components/ActorDetail.tsx index 084e577..52a5ee1 100644 --- a/packages/web/src/components/ActorDetail.tsx +++ b/packages/web/src/components/ActorDetail.tsx @@ -40,10 +40,18 @@ export function ActorDetail(props: { $actor: ActorDetail_actor$key }) { iri avatarUrl inboxUrl - outboxUrl - followersUrl - followingUrl - featuredUrl + outbox { + iri + } + followers { + iri + } + following { + iri + } + featured { + iri + } profileUrl instance { host @@ -58,10 +66,10 @@ export function ActorDetail(props: { $actor: ActorDetail_actor$key }) { const endpoints = () => [ { label: "Actor IRI", url: actor().iri }, { label: "Inbox", url: actor().inboxUrl }, - { label: "Outbox", url: actor().outboxUrl }, - { label: "Followers", url: actor().followersUrl }, - { label: "Following", url: actor().followingUrl }, - { label: "Featured", url: actor().featuredUrl }, + { label: "Outbox", url: actor().outbox?.iri }, + { label: "Followers", url: actor().followers?.iri }, + { label: "Following", url: actor().following?.iri }, + { label: "Featured", url: actor().featured?.iri }, { label: "Profile", url: actor().profileUrl }, ]; return (