From 68a5959d415eab22a19315b47334e7986d7bc08c Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Fri, 4 Sep 2026 13:26:03 +0900 Subject: [PATCH 1/6] Add Create a new instance link --- packages/web/src/routes/workspace/index.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/web/src/routes/workspace/index.tsx b/packages/web/src/routes/workspace/index.tsx index 74430f3..d264afc 100644 --- a/packages/web/src/routes/workspace/index.tsx +++ b/packages/web/src/routes/workspace/index.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 { Link } from "@kobalte/core/link"; import { graphql } from "relay-runtime"; import { For, Show } from "solid-js"; import { createLazyLoadQuery } from "solid-relay"; @@ -48,9 +49,14 @@ export default function WorkspacePage() {
You're not signed in.

}> {(viewer) => ( - - {(edge) => } - + <> + + {(edge) => } + + + Create A New Instance + + )}
From 55ce5bd6ffafa9c96cff4c777c90d2ce5143ee54 Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Fri, 4 Sep 2026 13:35:50 +0900 Subject: [PATCH 2/6] Make Basic Design of Workspace Used Skil: https://github.com/anthropics/claude-code/blob/main/plugins/frontend-design/skills/frontend-design/SKILL.md Used Prompt: ``` [$frontend-design](/Users/dodok/.codex/skills/frontend-design/SKILL.md) Using this skill make CSS Module, Frontend Markup, Design that aligns with other pages in workspace/index.tsx and InstanceSummary.tsx I think list design is better than gird design, it's not landing page so Your Instance should be small and focused on instance list and information ``` --- .../web/src/components/InstanceSummary.tsx | 25 +- packages/web/src/routes/workspace/index.tsx | 69 +++++- packages/web/src/styles/workspace.module.css | 221 ++++++++++++++++++ 3 files changed, 299 insertions(+), 16 deletions(-) create mode 100644 packages/web/src/styles/workspace.module.css diff --git a/packages/web/src/components/InstanceSummary.tsx b/packages/web/src/components/InstanceSummary.tsx index 6667096..8314684 100644 --- a/packages/web/src/components/InstanceSummary.tsx +++ b/packages/web/src/components/InstanceSummary.tsx @@ -16,10 +16,13 @@ import { Link } from "@kobalte/core/link"; import { graphql } from "relay-runtime"; +import { Show } from "solid-js"; import { createFragment } from "solid-relay"; import type { InstanceSummary_instance$key } from "./__generated__/InstanceSummary_instance.graphql.tsx"; +import styles from "~/styles/workspace.module.css"; + export default function InstanceSummary(props: { $instance: InstanceSummary_instance$key; }) { @@ -34,9 +37,23 @@ export default function InstanceSummary(props: { ); return ( -

- {data()?.host} - Create Actors -

+ + {(instance) => ( +
+
+ )} +
); } diff --git a/packages/web/src/routes/workspace/index.tsx b/packages/web/src/routes/workspace/index.tsx index d264afc..c6703d2 100644 --- a/packages/web/src/routes/workspace/index.tsx +++ b/packages/web/src/routes/workspace/index.tsx @@ -15,6 +15,7 @@ // along with this program. If not, see . import { Link } from "@kobalte/core/link"; +import { Title } from "@solidjs/meta"; import { graphql } from "relay-runtime"; import { For, Show } from "solid-js"; import { createLazyLoadQuery } from "solid-relay"; @@ -23,6 +24,8 @@ import InstanceSummary from "~/components/InstanceSummary.tsx"; import type { WorkspaceQuery } from "./__generated__/WorkspaceQuery.graphql.tsx"; +import styles from "~/styles/workspace.module.css"; + const workspaceQuery = graphql` query WorkspaceQuery { viewer { @@ -45,21 +48,63 @@ export default function WorkspacePage() { ); return ( -
-
- You're not signed in.

}> - {(viewer) => ( - <> +
+ Workspace — DrFed + +
+

Your instances

+ + + + New instance + + +
+ + +

Workspace unavailable

+

Sign in to manage your instances

+

Your workspace is linked to your DrFed account.

+ + Sign in + +
+ } + > + {(viewer) => ( + 0} + fallback={ +
+

No instances yet

+

Build your first test server

+

+ Start with an instance, then add actors to explore federation + flows. +

+ + Create an instance + +
+ } + > +
{(edge) => } - - Create A New Instance - - - )} - -
+ +
+ )} +
); } diff --git a/packages/web/src/styles/workspace.module.css b/packages/web/src/styles/workspace.module.css new file mode 100644 index 0000000..fe35e91 --- /dev/null +++ b/packages/web/src/styles/workspace.module.css @@ -0,0 +1,221 @@ +/* +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; +} + +.header { + align-items: center; + border-bottom: 1px solid var(--line); + display: flex; + gap: 1rem; + justify-content: space-between; + padding: 0.5rem 0 1.25rem; +} + +.header h1 { + font-size: 1.65rem; + margin: 0; +} + +.cardLabel, +.emptyLabel { + color: var(--accent-strong); + font-family: var(--font-mono); + font-size: var(--text-label); + font-weight: 600; + letter-spacing: var(--tracking-label); + text-transform: uppercase; +} + +.createButton, +.secondaryButton, +.cardAction { + align-items: center; + border-radius: var(--radius-sm); + display: inline-flex; + font-weight: 700; + justify-content: center; + text-decoration: none; +} + +.createButton { + background: var(--accent); + color: var(--on-accent); + flex: 0 0 auto; + gap: 0.55rem; + min-height: 2.6rem; + padding: 0.6rem 0.9rem; + transition: background var(--duration-fast) var(--ease-standard); +} + +.createButton span { + font-family: var(--font-mono); + font-size: 1.35rem; + font-weight: 400; + line-height: 1; +} + +.createButton:hover { + background: var(--accent-strong); +} + +.createButton:focus-visible, +.secondaryButton:focus-visible, +.cardAction:focus-visible { + outline: 3px solid var(--accent-soft); + outline-offset: 0.2rem; +} + +.instanceList { + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +.instanceCard { + align-items: center; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + padding: 1rem 1.25rem; + transition: background var(--duration-fast) var(--ease-standard); +} + +.instanceCard + .instanceCard { + border-top: 1px solid var(--line); +} + +.instanceCard:hover { + background: var(--surface-2); +} + +.instanceMarker { + background: var(--accent); + border: 0.3rem solid var(--accent-tint); + border-radius: 50%; + box-shadow: 0 0 0 1px var(--accent-soft); + height: 1rem; + margin-right: 0.9rem; + width: 1rem; +} + +.cardHeading { + min-width: 0; +} + +.cardLabel { + margin: 0 0 0.25rem; +} + +.cardHeading h2 { + font-family: var(--font-mono); + font-size: var(--text-heading-3); + line-height: 1.35; + margin: 0; + overflow-wrap: anywhere; +} + +.cardAction { + color: var(--accent-strong); + gap: 0.5rem; + margin-left: 1.5rem; + padding: 0.5rem; +} + +.cardAction span { + font-size: 1.2rem; + transition: transform var(--duration-fast) var(--ease-standard); +} + +.cardAction:hover span { + transform: translateX(0.25rem); +} + +.emptyState { + background: var(--card); + border: 1px dashed var(--line-strong); + border-radius: var(--radius); + margin: 0 auto; + max-width: var(--container-narrow); + padding: clamp(2rem, 7vw, 4rem); + text-align: center; + width: 100%; +} + +.emptyState h2 { + font-size: var(--text-heading-2); + margin: 0 0 0.75rem; +} + +.emptyState > p:not(.emptyLabel) { + margin: 0 auto 1.5rem; + max-width: 34rem; +} + +.emptyLabel { + margin-bottom: 0.75rem; +} + +.secondaryButton { + border: 1px solid var(--line-strong); + color: var(--ink); + min-height: 2.75rem; + padding: 0.65rem 1rem; + transition: + background var(--duration-fast) var(--ease-standard), + border-color var(--duration-fast) var(--ease-standard); +} + +.secondaryButton:hover { + background: var(--accent-tint); + border-color: var(--accent-soft); +} + +@media (max-width: 600px) { + .header { + padding-top: 0; + } + + .instanceCard { + align-items: start; + grid-template-columns: auto minmax(0, 1fr); + padding: 1rem; + } + + .cardAction { + grid-column: 2; + justify-self: start; + margin: 0.5rem 0 0 -0.5rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .createButton, + .secondaryButton, + .instanceCard, + .cardAction span { + transition: none; + } + .cardAction:hover span { + transform: none; + } +} From 31e50a2d32c749693dd3774ee31a6bf5e0bddd10 Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Fri, 4 Sep 2026 15:15:03 +0900 Subject: [PATCH 3/6] Make Basic Structure of instance detail page --- packages/web/src/routes/instance/[slug].tsx | 95 +++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 packages/web/src/routes/instance/[slug].tsx diff --git a/packages/web/src/routes/instance/[slug].tsx b/packages/web/src/routes/instance/[slug].tsx new file mode 100644 index 0000000..94c5ed8 --- /dev/null +++ b/packages/web/src/routes/instance/[slug].tsx @@ -0,0 +1,95 @@ +// 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 { Link } from "@kobalte/core/link"; +import { Title } from "@solidjs/meta"; +import { Show } from "solid-js"; + +// import type { InstanceDetailQuery } from "./__generated__/InstanceDetailQuery.graphql"; + +// const instanceDetailQuery = graphql` +// query InstanceDetailQuery($instanceSlug: String!) { +// instance: node(slug: $instanceSlug) { +// ... on Instance { +// host +// nodeInfoUrl +// } +// } +// } +// `; + +// const loadInstanceQuery = query( +// (instanceSlug: string) => +// loadQuery( +// useRelayEnvironment()(), +// instanceDetailQuery, +// { +// instanceSlug, +// }, +// ), +// "InstanceDetailQuery", +// ); + +// export const route = { +// preload({ params }) { +// if (params.slug === undefined) { +// throw new Error("Missing slug route parameter."); +// } +// return loadInstanceQuery(params.slug); +// }, +// } satisfies RouteDefinition; + +// type RouteData = ReturnType; + +export default function InstanceDetailPage() { + // const data = createPreloadedQuery( + // instanceDetailQuery, + // () => props.data, + // ); + // + + const data = () => ({ + instance: { + host: "hi", + nodeInfoUrl: "bye", + }, + }); + + return ( + Instance not found.

}> + {(instance) => ( +
+ {instance().host} — DrFed +

{instance().host}

+ + {(instance) => ( + + {(nodeInfoUrl) => ( +

+ NodeInfo: {nodeInfoUrl()} + WebFingerURL:{" "} + {`${instance().host}/.well-known/webfinger`} + SharedInbox: TBD +

+ )} +
+ )} +
+
+ )} +
+ ); +} From 08f8f90289a4e9295295e8749a93c6c6f926440b Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Fri, 4 Sep 2026 15:27:06 +0900 Subject: [PATCH 4/6] Fix lint error --- packages/web/src/routes/instance/[slug].tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/web/src/routes/instance/[slug].tsx b/packages/web/src/routes/instance/[slug].tsx index 94c5ed8..127e7bf 100644 --- a/packages/web/src/routes/instance/[slug].tsx +++ b/packages/web/src/routes/instance/[slug].tsx @@ -18,6 +18,7 @@ import { Link } from "@kobalte/core/link"; import { Title } from "@solidjs/meta"; import { Show } from "solid-js"; +// oxlint-disable-next-line capitalized-comments // import type { InstanceDetailQuery } from "./__generated__/InstanceDetailQuery.graphql"; // const instanceDetailQuery = graphql` @@ -55,12 +56,14 @@ import { Show } from "solid-js"; // type RouteData = ReturnType; export default function InstanceDetailPage() { + // oxlint-disable-next-line capitalized-comments // const data = createPreloadedQuery( // instanceDetailQuery, // () => props.data, // ); // + // oxlint-disable-next-line unicorn/consistent-function-scoping const data = () => ({ instance: { host: "hi", @@ -69,12 +72,14 @@ export default function InstanceDetailPage() { }); return ( + // oxlint-disable-next-line typescript/no-unnecessary-condition Instance not found.

}> {(instance) => (
{instance().host} — DrFed

{instance().host}

+ {/*oxlint-disable-next-line eslint/no-shadow */} {(instance) => ( {(nodeInfoUrl) => ( From 30983f61844551e1459e94d6fd947bc548998fbe Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Fri, 4 Sep 2026 15:38:01 +0900 Subject: [PATCH 5/6] Design instance details and actor list Used Skil: https://github.com/anthropics/claude-code/blob/main/plugins/frontend-design/skills/frontend-design/SKILL.md Used Prompt: ``` Make Design for /Users/dodok/repos/drfed/packages/web/src/routes/instance/[slug].tsx, Add Create Actor button to link create. Actor Page, Add dummy data for actors lists and make actors list also. It's too look like landing page, and we don't need hero section, and Actors doesn't need activity times, for now, just need handle ``` Assisted-by: Codex:gpt-5.6-sol --- packages/web/src/routes/instance/[slug].tsx | 145 +++++++------- packages/web/src/styles/instance.module.css | 198 ++++++++++++++++++++ 2 files changed, 271 insertions(+), 72 deletions(-) create mode 100644 packages/web/src/styles/instance.module.css diff --git a/packages/web/src/routes/instance/[slug].tsx b/packages/web/src/routes/instance/[slug].tsx index 127e7bf..49e2691 100644 --- a/packages/web/src/routes/instance/[slug].tsx +++ b/packages/web/src/routes/instance/[slug].tsx @@ -14,87 +14,88 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { Link } from "@kobalte/core/link"; import { Title } from "@solidjs/meta"; -import { Show } from "solid-js"; +import { A, useParams } from "@solidjs/router"; +import { For } from "solid-js"; -// oxlint-disable-next-line capitalized-comments -// import type { InstanceDetailQuery } from "./__generated__/InstanceDetailQuery.graphql"; +import styles from "~/styles/instance.module.css"; -// const instanceDetailQuery = graphql` -// query InstanceDetailQuery($instanceSlug: String!) { -// instance: node(slug: $instanceSlug) { -// ... on Instance { -// host -// nodeInfoUrl -// } -// } -// } -// `; +const actors = ["@sherry", "@newsroom", "@garden"] as const; -// const loadInstanceQuery = query( -// (instanceSlug: string) => -// loadQuery( -// useRelayEnvironment()(), -// instanceDetailQuery, -// { -// instanceSlug, -// }, -// ), -// "InstanceDetailQuery", -// ); +export default function InstanceDetailPage() { + const params = useParams(); + const host = () => params.slug ?? "social.example"; -// export const route = { -// preload({ params }) { -// if (params.slug === undefined) { -// throw new Error("Missing slug route parameter."); -// } -// return loadInstanceQuery(params.slug); -// }, -// } satisfies RouteDefinition; + return ( +
+ {host()} — DrFed -// type RouteData = ReturnType; + + All instances + -export default function InstanceDetailPage() { - // oxlint-disable-next-line capitalized-comments - // const data = createPreloadedQuery( - // instanceDetailQuery, - // () => props.data, - // ); - // +
+

{host()}

+ + + Create actor + +
- // oxlint-disable-next-line unicorn/consistent-function-scoping - const data = () => ({ - instance: { - host: "hi", - nodeInfoUrl: "bye", - }, - }); +
+
+

Connection record

+

Federation endpoints

+
+
+
+
NodeInfo
+
+ /nodeinfo/2.1 +
+
+
+
WebFinger
+
+ + /.well-known/webfinger + +
+
+
+
Shared inbox
+
+ /inbox +
+
+
+
- return ( - // oxlint-disable-next-line typescript/no-unnecessary-condition - Instance not found.

}> - {(instance) => ( -
- {instance().host} — DrFed -

{instance().host}

- - {/*oxlint-disable-next-line eslint/no-shadow */} - {(instance) => ( - - {(nodeInfoUrl) => ( -

- NodeInfo: {nodeInfoUrl()} - WebFingerURL:{" "} - {`${instance().host}/.well-known/webfinger`} - SharedInbox: TBD -

- )} -
+
+
+
+

Local identities

+

Actors

+
+

{actors.length} registered

+
+ +
+ + {(handle) => ( +
+
)} - -
- )} -
+ + + +
); } diff --git a/packages/web/src/styles/instance.module.css b/packages/web/src/styles/instance.module.css new file mode 100644 index 0000000..ba80e9a --- /dev/null +++ b/packages/web/src/styles/instance.module.css @@ -0,0 +1,198 @@ +/* +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 { + align-items: center; + color: var(--ink-soft); + display: inline-flex; + font-size: 0.9rem; + font-weight: 700; + gap: 0.45rem; + justify-self: start; + text-decoration: none; +} + +.backLink:hover { + color: var(--accent-strong); +} + +.header { + align-items: center; + border-bottom: 1px solid var(--line); + display: flex; + gap: 1rem; + justify-content: space-between; + padding: 0.25rem 0 1.25rem; +} + +.header h1 { + font-family: var(--font-mono); + font-size: clamp(1.5rem, 4vw, 2rem); + line-height: 1.2; + margin: 0; + overflow-wrap: anywhere; +} + +.createButton { + align-items: center; + background: var(--accent); + border-radius: var(--radius-sm); + color: var(--on-accent); + display: inline-flex; + flex: 0 0 auto; + font-weight: 800; + gap: 0.5rem; + min-height: 2.75rem; + padding: 0.65rem 0.95rem; + text-decoration: none; +} + +.createButton:hover { + background: var(--accent-strong); +} + +.details, +.actors { + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: clamp(1.25rem, 3vw, 1.75rem); +} + +.sectionLabel { + color: var(--accent-strong); + font-family: var(--font-mono); + font-size: var(--text-label); + font-weight: 600; + letter-spacing: var(--tracking-label); + margin: 0 0 0.35rem; + text-transform: uppercase; +} + +.details h2, +.sectionHeader h2 { + font-size: 1.3rem; + margin: 0; +} + +.endpointList { + margin: 1.25rem 0 0; +} + +.endpointList div { + align-items: baseline; + border-top: 1px solid var(--line); + display: grid; + gap: 1rem; + grid-template-columns: 8rem minmax(0, 1fr); + padding: 0.9rem 0; +} + +.endpointList div:last-child { + padding-bottom: 0; +} + +.endpointList dt { + color: var(--ink-soft); + font-size: 0.9rem; + font-weight: 700; +} + +.endpointList dd { + margin: 0; + min-width: 0; +} + +.endpointList a { + color: var(--ink); + font-family: var(--font-mono); + font-size: 0.85rem; + overflow-wrap: anywhere; + text-decoration-color: var(--accent-soft); + text-underline-offset: 0.2rem; +} + +.sectionHeader { + align-items: end; + display: flex; + justify-content: space-between; + margin-bottom: 1.25rem; +} + +.sectionHeader > p { + color: var(--ink-soft); + font-family: var(--font-mono); + font-size: 0.8rem; + margin: 0 0 0.15rem; +} + +.actorList { + border-top: 1px solid var(--line); +} + +.actorCard { + align-items: center; + display: flex; + gap: 0.75rem; + padding: 1rem 0; +} + +.actorCard + .actorCard { + border-top: 1px solid var(--line); +} + +.actorMarker { + background: var(--accent); + border: 0.25rem solid var(--accent-tint); + border-radius: 50%; + height: 0.85rem; + width: 0.85rem; +} + +.actorCard p { + font-family: var(--font-mono); + font-size: 0.9rem; + margin: 0; + overflow-wrap: anywhere; +} + +.backLink:focus-visible, +.createButton:focus-visible, +.endpointList a:focus-visible { + outline: 3px solid var(--accent-soft); + outline-offset: 0.2rem; +} + +@media (max-width: 520px) { + .header { + align-items: start; + flex-direction: column; + } + + .endpointList div { + gap: 0.25rem; + grid-template-columns: 1fr; + } +} From 6874460a21b6cc4b03e06d3e9559ea76bffe5683 Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Sun, 6 Sep 2026 14:08:24 +0900 Subject: [PATCH 6/6] Fix nullable value --- packages/web/src/routes/workspace/index.tsx | 59 ++++++++++++--------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/web/src/routes/workspace/index.tsx b/packages/web/src/routes/workspace/index.tsx index c6703d2..df52d22 100644 --- a/packages/web/src/routes/workspace/index.tsx +++ b/packages/web/src/routes/workspace/index.tsx @@ -76,32 +76,41 @@ export default function WorkspacePage() { > {(viewer) => ( 0} - fallback={ -
-

No instances yet

-

Build your first test server

-

- Start with an instance, then add actors to explore federation - flows. -

- - Create an instance - -
- } + when={viewer().instances} + fallback={

No Instance Available

} > -
- - {(edge) => } - -
+ {(instances) => ( + 0} + fallback={ +
+

No instances yet

+

+ Build your first test server +

+

+ Start with an instance, then add actors to explore + federation flows. +

+ + Create an instance + +
+ } + > +
+ + {(edge) => } + +
+
+ )}
)}