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/instance/[slug].tsx b/packages/web/src/routes/instance/[slug].tsx new file mode 100644 index 0000000..49e2691 --- /dev/null +++ b/packages/web/src/routes/instance/[slug].tsx @@ -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 . + +import { Title } from "@solidjs/meta"; +import { A, useParams } from "@solidjs/router"; +import { For } from "solid-js"; + +import styles from "~/styles/instance.module.css"; + +const actors = ["@sherry", "@newsroom", "@garden"] as const; + +export default function InstanceDetailPage() { + const params = useParams(); + const host = () => params.slug ?? "social.example"; + + return ( +
+ {host()} — DrFed + + + All instances + + +
+

{host()}

+ + + Create actor + +
+ +
+
+

Connection record

+

Federation endpoints

+
+
+
+
NodeInfo
+
+ /nodeinfo/2.1 +
+
+
+
WebFinger
+
+ + /.well-known/webfinger + +
+
+
+
Shared inbox
+
+ /inbox +
+
+
+
+ +
+
+
+

Local identities

+

Actors

+
+

{actors.length} registered

+
+ +
+ + {(handle) => ( +
+
+ )} +
+
+
+
+ ); +} diff --git a/packages/web/src/routes/workspace/index.tsx b/packages/web/src/routes/workspace/index.tsx index 74430f3..df52d22 100644 --- a/packages/web/src/routes/workspace/index.tsx +++ b/packages/web/src/routes/workspace/index.tsx @@ -14,6 +14,8 @@ // 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 { graphql } from "relay-runtime"; import { For, Show } from "solid-js"; import { createLazyLoadQuery } from "solid-relay"; @@ -22,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 { @@ -44,16 +48,72 @@ export default function WorkspacePage() { ); return ( -
-
- You're not signed in.

}> - {(viewer) => ( - - {(edge) => } - - )} +
+ 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) => ( + No Instance Available

} + > + {(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) => } + +
+
+ )} +
+ )} +
); } 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; + } +} 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; + } +}