diff --git a/src/components/KindInfoButton.tsx b/src/components/KindInfoButton.tsx
new file mode 100644
index 00000000..fc204ff9
--- /dev/null
+++ b/src/components/KindInfoButton.tsx
@@ -0,0 +1,75 @@
+import { ExternalLink, Info } from 'lucide-react';
+
+import type { ExtraKindDef } from '@/lib/extraKinds';
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { ExternalFavicon } from '@/components/ExternalFavicon';
+
+interface KindInfoButtonProps {
+ kindDef: ExtraKindDef;
+ icon?: React.ReactNode;
+}
+
+/** Info button that opens a modal with a blurb and external site links for an extra kind. */
+export function KindInfoButton({ kindDef, icon }: KindInfoButtonProps) {
+ const { label, blurb, sites } = kindDef;
+
+ if (!sites?.length && !blurb) return null;
+
+ return (
+
+ );
+}
diff --git a/src/lib/extraKinds.ts b/src/lib/extraKinds.ts
index f2e99031..d477adfc 100644
--- a/src/lib/extraKinds.ts
+++ b/src/lib/extraKinds.ts
@@ -15,6 +15,14 @@ export interface SubKindDef {
addressable: boolean;
}
+/** An external site where users can create or participate in a specific kind. */
+export interface ExtraKindSite {
+ /** Full URL to the site. */
+ url: string;
+ /** Display name override. Defaults to the first segment of the hostname, capitalized. */
+ name?: string;
+}
+
/** Section labels for grouping extra kinds in settings UI. */
export type ExtraKindSection = 'feed' | 'media' | 'social' | 'whimsy';
@@ -50,6 +58,10 @@ export interface ExtraKindDef {
section: ExtraKindSection;
/** If true, this entry only has a feed toggle (no sidebar toggle). */
feedOnly?: boolean;
+ /** Longer, whimsical blurb shown in the info modal. */
+ blurb?: string;
+ /** External sites where users can create or participate in this kind of content. */
+ sites?: ExtraKindSite[];
}
/** All supported extra content kinds, ordered by section (feed → media → social → whimsy). */
@@ -91,6 +103,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'articles',
addressable: true,
section: 'feed',
+ blurb: 'Blog posts, essays, and guides. Write and publish from a dedicated editor.',
+ sites: [{ url: 'https://inkwell.shakespeare.wtf' }],
},
// Media
{
@@ -102,6 +116,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'vines',
addressable: true,
section: 'media',
+ blurb: 'Short video clips. Record and share from a dedicated app.',
+ sites: [{ url: 'https://divine.video' }],
},
{
kind: 30311,
@@ -112,6 +128,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'streams',
addressable: true,
section: 'media',
+ blurb: 'Watch and broadcast live video. Start a stream from a streaming app.',
+ sites: [{ url: 'https://zap.stream', name: 'zap.stream' }],
},
// Social
{
@@ -123,6 +141,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'polls',
addressable: false,
section: 'social',
+ blurb: 'Ask a question, let people vote. Create polls from a polling app.',
+ sites: [{ url: 'https://pollerama.fun' }],
},
{
kind: 39089,
@@ -133,6 +153,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'packs',
addressable: true,
section: 'social',
+ blurb: 'Curated lists of people to follow. Browse or create your own.',
+ sites: [{ url: 'https://following.space', name: 'following.space' }, { url: 'https://following.party', name: 'following.party' }],
},
// Whimsy
{
@@ -144,6 +166,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'colors',
addressable: false,
section: 'whimsy',
+ blurb: 'Share your mood as a color palette. Pick colors that match the moment.',
+ sites: [{ url: 'https://espy.you' }],
},
{
kind: 37381,
@@ -154,6 +178,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'decks',
addressable: true,
section: 'whimsy',
+ blurb: 'Magic: The Gathering deck lists. Build and share decks with other players.',
+ sites: [{ url: 'https://surveil.cards' }],
},
{
kind: 37516,
@@ -163,6 +189,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
route: 'treasures',
addressable: true,
section: 'whimsy',
+ blurb: 'Real-world geocaching. Hide treasures outside, find others, and log your discoveries.',
+ sites: [{ url: 'https://treasures.to' }],
subKinds: [
{
kind: 37516,
diff --git a/src/pages/KindFeedPage.tsx b/src/pages/KindFeedPage.tsx
index eabde8ce..ad716417 100644
--- a/src/pages/KindFeedPage.tsx
+++ b/src/pages/KindFeedPage.tsx
@@ -1,19 +1,29 @@
+import { useMemo } from 'react';
import { useSeoMeta } from '@unhead/react';
import { ArrowLeft } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Feed } from '@/components/Feed';
+import { KindInfoButton } from '@/components/KindInfoButton';
import { useLayoutOptions } from '@/contexts/LayoutContext';
+import { EXTRA_KINDS, type ExtraKindDef } from '@/lib/extraKinds';
interface KindFeedPageProps {
kind: number | number[];
title: string;
icon?: React.ReactNode;
emptyMessage?: string;
+ /** Override the auto-detected ExtraKindDef (useful for pages with sub-kinds like Treasures). */
+ kindDef?: ExtraKindDef;
}
-export function KindFeedPage({ kind, title, icon, emptyMessage }: KindFeedPageProps) {
+export function KindFeedPage({ kind, title, icon, emptyMessage, kindDef }: KindFeedPageProps) {
const primaryKind = Array.isArray(kind) ? kind[0] : kind;
+ const resolvedDef = useMemo(
+ () => kindDef ?? EXTRA_KINDS.find((def) => def.kind === primaryKind),
+ [kindDef, primaryKind],
+ );
+
useSeoMeta({
title: `${title} | Ditto`,
description: `${title} on Nostr`,
@@ -33,10 +43,11 @@ export function KindFeedPage({ kind, title, icon, emptyMessage }: KindFeedPagePr