This commit is contained in:
Chad Curtis 2025-07-14 21:40:30 +00:00
parent 0cda9cb7fc
commit 3d97870b59
17 changed files with 1875 additions and 82 deletions

116
NIP.md Normal file
View File

@ -0,0 +1,116 @@
# NIP-GNOME: Gnome Speak Translation Protocol
`draft` `optional`
## Abstract
This NIP defines a client-side translation protocol for converting kind 1 (Short Text Note) events into "Gnome Speak" - a whimsical fantasy language that transforms regular text into gnome-themed vocabulary and expressions.
## Motivation
To create an immersive and entertaining experience for users of gnome-themed Nostr clients, where all text content is automatically translated into a consistent fantasy language that maintains readability while adding magical woodland charm.
## Specification
### Translation Scope
- **MUST** translate all kind 1 events (Short Text Notes) into Gnome Speak
- **SHOULD NOT** translate other event kinds to maintain protocol compatibility
- **MUST** preserve original URLs, Nostr references (npub, note, etc.), and hashtags without translation
- **SHOULD** maintain the semantic meaning of the original text
### Translation Rules
#### Vocabulary Substitution
Common words are replaced with gnome-themed equivalents:
- Greetings: `hello``greetings, fellow earth-dweller`
- Technology: `computer``thinking-box of metal and magic`
- Time: `today``this blessed sun-cycle`
- Actions: `go``venture forth`
- People: `friend``companion of the woodland path`
#### Gnome Expressions
- Exclamations are enhanced with gnome phrases: `!``! By my pointy hat!`
- Sentence starters may be added: `In the sacred grove, ...`
- Sentence endings may be appended: `...as the ancients foretold.`
#### Special Formatting
- Bitcoin/crypto terms: `bitcoin``✨golden acorns of the digital realm✨`
- Nostr terms: `nostr``🍄message-mushroom🍄`
- Greetings: `gm``🌅when dew kisses the earth🌅`
### Implementation
#### Client Behavior
Clients implementing this NIP:
1. **MUST** detect kind 1 events
2. **MUST** apply Gnome Speak translation before displaying content
3. **SHOULD** preserve original content for editing/replying
4. **MAY** provide toggle to view original content
5. **SHOULD** indicate when content has been translated
#### Content Preservation
- Original event content **MUST** remain unchanged in storage/transmission
- Translation occurs only at display time
- Replies and quotes **SHOULD** reference original content, not translated
#### Tag Usage
Clients **MAY** add a `t` tag with value `gnome` to indicate gnome-themed content:
```json
{
"kind": 1,
"content": "Hello world!",
"tags": [["t", "gnome"]],
...
}
```
### Example
Original event:
```json
{
"kind": 1,
"content": "Good morning! Having coffee and checking bitcoin prices.",
"tags": [],
...
}
```
Displayed as:
```
"When dew kisses the earth! Partaking of bitter bean brew and witnessing ✨golden acorns of the digital realm✨ values, blessed by the forest spirits."
```
## Rationale
This approach provides:
1. **Immersive Experience**: Creates a consistent fantasy atmosphere
2. **Protocol Compatibility**: No changes to underlying Nostr protocol
3. **Reversibility**: Original content always preserved
4. **Interoperability**: Non-gnome clients display original content normally
## Security Considerations
- Translation is purely cosmetic and doesn't affect event integrity
- Original content validation remains unchanged
- No additional attack vectors introduced
## Implementation Status
This NIP is implemented in Gnome Nostr client as a client-side translation layer.
## References
- [NIP-01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-10: Text Notes and Threads](https://github.com/nostr-protocol/nips/blob/master/10.md)

View File

@ -1,3 +1,79 @@
# MKStack
# 🍄 Gnome Nostr - The Sacred Network of Woodland Folk
Template for building Nostr client application with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify.
A magical Nostr client where all text notes (kind 1 events) are automatically translated into the ancient gnome tongue! Connect with fellow earth-dwellers across the message-mushroom network.
## ✨ Features
- **🍄 Gnome Speak Translation**: All kind 1 events are automatically translated into whimsical gnome language
- **🌲 Forest-Themed UI**: Beautiful gnome-inspired design with earthy colors and woodland aesthetics
- **🔗 Full Nostr Compatibility**: Works with any Nostr relay and preserves all protocol functionality
- **📱 Responsive Design**: Works perfectly on desktop and mobile devices
- **🌙 Dark/Light Mode**: Toggle between day forest and night forest themes
- **⚡ Lightning Fast**: Built with modern React, Vite, and TailwindCSS
## 🎭 Gnome Speak Examples
Regular text is magically transformed:
- `"Hello world!"``"Greetings, fellow earth-dweller world! By my pointy hat!"`
- `"Good morning everyone"``"🌅When dew kisses the earth🌅 all forest folk"`
- `"I love bitcoin"``"I cherish like precious gems ✨golden acorns of the digital realm✨"`
- `"This is about #nostr"``"This is a woodland proclamation about #nostr"` (hashtags preserved)
## 🛡️ Protocol Preservation
While the display is transformed into gnome speak, the underlying Nostr protocol remains unchanged:
- ✅ URLs are preserved and linkified
- ✅ Nostr references (npub, note, etc.) work normally
- ✅ Hashtags remain functional for filtering
- ✅ Original content is preserved for replies and quotes
- ✅ Compatible with all other Nostr clients
## 🏗️ Technical Details
- **Framework**: React 18 with TypeScript
- **Styling**: TailwindCSS with custom gnome theme
- **Nostr**: Nostrify for protocol integration
- **Font**: Comfortaa Variable for whimsical feel
- **Build**: Vite for fast development and production builds
## 🌟 Custom NIP
This client implements a custom translation protocol documented in `NIP.md`. The translation:
- Only affects kind 1 events (Short Text Notes)
- Preserves URLs, Nostr references, and hashtags
- Maintains semantic meaning while adding magical charm
- Is purely client-side - no protocol changes required
## 🚀 Getting Started
1. **Clone the repository**
2. **Install dependencies**: `npm install`
3. **Start development**: `npm run dev`
4. **Build for production**: `npm run build`
## 🎨 Customization
The gnome vocabulary can be extended in `src/lib/gnomeSpeak.ts`. Add new word mappings to enhance the magical transformation!
## 🤝 Contributing
Contributions are welcome! Whether you want to:
- Add more gnome vocabulary
- Improve the forest-themed UI
- Fix bugs or add features
- Enhance the translation algorithm
Feel free to open issues and pull requests.
## 📜 License
This project is open source and available under the MIT License.
---
*Vibed with [MKStack](https://soapbox.pub/mkstack) • Blessed by the forest spirits 🍄✨*
**May your mushrooms grow tall and your acorns be golden!** 🌰✨

View File

@ -4,6 +4,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="content-security-policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https:; font-src 'self'; base-uri 'self'; manifest-src 'self'; connect-src 'self' blob: https: wss:; img-src 'self' data: blob: https:; media-src 'self' https:">
<title>Gnome Nostr - The Sacred Network of Woodland Folk</title>
<meta name="description" content="A magical Nostr client where all messages are translated into the ancient gnome tongue. Connect with fellow earth-dwellers across the message-mushroom network!">
<meta property="og:type" content="website">
<meta property="og:title" content="Gnome Nostr - The Sacred Network of Woodland Folk">
<meta property="og:description" content="A magical Nostr client where all messages are translated into the ancient gnome tongue. Connect with fellow earth-dwellers across the message-mushroom network!">
<link rel="manifest" href="/manifest.webmanifest">
</head>
<body>

10
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "mkstack",
"version": "0.0.0",
"dependencies": {
"@fontsource-variable/comfortaa": "^5.2.6",
"@hookform/resolvers": "^3.9.0",
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1",
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5",
@ -990,6 +991,15 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@fontsource-variable/comfortaa": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/@fontsource-variable/comfortaa/-/comfortaa-5.2.6.tgz",
"integrity": "sha512-uWjgXWmfUhpsax7Y6lUcWNOveIgtYYw4ZPbCkvMWjGPU2aYtTUa1Q8etT3zaDmmWXJOgv677Cgchj8Fh1304EA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",

View File

@ -10,6 +10,7 @@
"deploy": "npm run build && npx -y nostr-deploy-cli deploy --skip-setup"
},
"dependencies": {
"@fontsource-variable/comfortaa": "^5.2.6",
"@hookform/resolvers": "^3.9.0",
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1",
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5",

View File

@ -0,0 +1,21 @@
{
"name": "Gnome Nostr - The Sacred Network of Woodland Folk",
"short_name": "Gnome Nostr",
"description": "A magical Nostr client where all messages are translated into the ancient gnome tongue. Connect with fellow earth-dwellers across the message-mushroom network!",
"start_url": "/",
"display": "standalone",
"background_color": "#f0f5f0",
"theme_color": "#d97706",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -0,0 +1,193 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Send, Sparkles, Leaf } from 'lucide-react';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { translateToGnomeSpeak } from '@/lib/gnomeSpeak';
import { useToast } from '@/hooks/useToast';
export function GnomeComposer() {
const [content, setContent] = useState('');
const [isPreviewMode, setIsPreviewMode] = useState(false);
const { user } = useCurrentUser();
const { mutate: createEvent, isPending } = useNostrPublish();
const { toast } = useToast();
const author = useAuthor(user?.pubkey || '');
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? (user?.pubkey ? genUserName(user.pubkey) : 'Anonymous Gnome');
const profileImage = metadata?.picture;
const handleSubmit = () => {
if (!content.trim()) {
toast({
title: "Empty woodland proclamation!",
description: "Even gnomes need something to say to the forest folk.",
variant: "destructive",
});
return;
}
if (!user) {
toast({
title: "Authentication required",
description: "You must be logged in to share woodland wisdom.",
variant: "destructive",
});
return;
}
createEvent(
{
kind: 1,
content: content.trim(),
tags: [['t', 'gnome']] // Tag all posts as gnome content
},
{
onSuccess: () => {
setContent('');
setIsPreviewMode(false);
toast({
title: "Woodland proclamation sent! 🍄",
description: "Your message has been carried by the forest winds to all the message-mushrooms.",
});
},
onError: (error) => {
toast({
title: "Forest spirits are troubled",
description: `Failed to send your proclamation: ${error.message}`,
variant: "destructive",
});
}
}
);
};
const gnomeTranslatedContent = content ? translateToGnomeSpeak(content) : '';
if (!user) {
return (
<Card className="mb-6 border-2 border-dashed border-muted">
<CardContent className="py-8 text-center">
<div className="space-y-4">
<div className="text-4xl">🍄</div>
<div>
<h3 className="font-semibold mb-2">Join the Forest Council</h3>
<p className="text-muted-foreground">
Log in to share your woodland wisdom with fellow gnomes across the message-mushroom network!
</p>
</div>
</div>
</CardContent>
</Card>
);
}
return (
<Card className="mb-6 border-2 border-primary/20 shadow-lg">
<CardHeader className="pb-3">
<div className="flex items-center space-x-3">
<Avatar className="h-10 w-10 border-2 border-primary/30">
<AvatarImage src={profileImage} alt={displayName} />
<AvatarFallback className="bg-primary text-primary-foreground font-bold">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center space-x-2">
<CardTitle className="text-lg">Share Woodland Wisdom</CardTitle>
<Badge variant="secondary" className="gnome-wiggle">
🍄 Gnome Speak
</Badge>
</div>
<p className="text-sm text-muted-foreground">
Your words will be translated into the ancient gnome tongue
</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Your Message:</label>
<Button
variant="ghost"
size="sm"
onClick={() => setIsPreviewMode(!isPreviewMode)}
className="text-xs"
>
<Sparkles className="h-3 w-3 mr-1" />
{isPreviewMode ? 'Edit' : 'Preview Gnome Speak'}
</Button>
</div>
{!isPreviewMode ? (
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Share your thoughts with the forest folk... (will be translated to gnome speak)"
className="min-h-[100px] resize-none border-2 border-border/50 focus:border-primary/50"
maxLength={280}
/>
) : (
<div className="min-h-[100px] p-3 border-2 border-accent/30 rounded-md bg-accent/5">
<p className="text-sm text-muted-foreground mb-2 font-medium">
🍄 Gnome Speak Translation:
</p>
<div className="text-sm leading-relaxed">
{gnomeTranslatedContent || (
<span className="text-muted-foreground italic">
Your gnome translation will appear here...
</span>
)}
</div>
</div>
)}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>
{content.length}/280 characters
</span>
{content && (
<span className="text-accent">
Will be auto-translated to gnome speak
</span>
)}
</div>
</div>
<div className="flex items-center justify-between pt-2">
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
<Leaf className="h-3 w-3" />
<span>Broadcasting to message-mushroom network</span>
</div>
<Button
onClick={handleSubmit}
disabled={!content.trim() || isPending}
className="font-medium"
>
{isPending ? (
<>
<Sparkles className="h-4 w-4 mr-2 animate-spin" />
Sending to Forest...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Share Wisdom
</>
)}
</Button>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,221 @@
import { useQuery } from '@tanstack/react-query';
import { useNostr } from '@nostrify/react';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Heart, MessageCircle, Repeat2 } from 'lucide-react';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { NoteContent } from '@/components/NoteContent';
import { RelaySelector } from '@/components/RelaySelector';
import type { NostrEvent } from '@nostrify/nostrify';
function GnomePost({ event }: { event: NostrEvent }) {
const author = useAuthor(event.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(event.pubkey);
const profileImage = metadata?.picture;
const about = metadata?.about;
// Format timestamp
const timestamp = new Date(event.created_at * 1000);
const timeAgo = formatTimeAgo(timestamp);
return (
<Card className="mb-4 border-2 border-border/50 hover:border-primary/30 transition-all duration-300 hover:shadow-lg mushroom-glow">
<CardHeader className="pb-3">
<div className="flex items-start space-x-3">
<Avatar className="h-12 w-12 border-2 border-primary/20">
<AvatarImage src={profileImage} alt={displayName} />
<AvatarFallback className="bg-secondary text-secondary-foreground font-bold">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<h3 className="font-semibold text-foreground truncate">
{displayName}
</h3>
<Badge variant="secondary" className="text-xs gnome-wiggle">
🍄 Gnome
</Badge>
</div>
{about && (
<p className="text-sm text-muted-foreground truncate mt-1">
{about}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
{timeAgo} via message-mushroom 🍄
</p>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="mb-4">
<NoteContent event={event} className="text-sm leading-relaxed" />
</div>
{/* Gnome-themed interaction buttons */}
<div className="flex items-center space-x-6 pt-2 border-t border-border/30">
<Button variant="ghost" size="sm" className="text-muted-foreground hover:text-primary group">
<Heart className="h-4 w-4 mr-1 group-hover:fill-current transition-all" />
<span className="text-xs">Sprinkle Fairy Dust</span>
</Button>
<Button variant="ghost" size="sm" className="text-muted-foreground hover:text-accent group">
<MessageCircle className="h-4 w-4 mr-1" />
<span className="text-xs">Forest Whisper</span>
</Button>
<Button variant="ghost" size="sm" className="text-muted-foreground hover:text-secondary group">
<Repeat2 className="h-4 w-4 mr-1" />
<span className="text-xs">Spread Seeds</span>
</Button>
</div>
</CardContent>
</Card>
);
}
function GnomePostSkeleton() {
return (
<Card className="mb-4">
<CardHeader className="pb-3">
<div className="flex items-start space-x-3">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="flex-1 space-y-2">
<div className="flex items-center space-x-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-5 w-16" />
</div>
<Skeleton className="h-3 w-32" />
<Skeleton className="h-3 w-20" />
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-2 mb-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
<Skeleton className="h-4 w-3/5" />
</div>
<div className="flex items-center space-x-6 pt-2 border-t border-border/30">
<Skeleton className="h-8 w-20" />
<Skeleton className="h-8 w-20" />
<Skeleton className="h-8 w-20" />
<Skeleton className="h-8 w-20" />
</div>
</CardContent>
</Card>
);
}
function formatTimeAgo(date: Date): string {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) {
return 'just now';
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `${minutes}m ago`;
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `${hours}h ago`;
} else {
const days = Math.floor(diffInSeconds / 86400);
return `${days}d ago`;
}
}
export function GnomeFeed() {
const { nostr } = useNostr();
const { data: posts, isLoading, error } = useQuery({
queryKey: ['gnome-feed'],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]);
// Query for kind 1 events (text notes) that will be translated to gnome speak
const events = await nostr.query([
{
kinds: [1],
limit: 50,
}
], { signal });
// Sort by creation time (newest first)
return events.sort((a, b) => b.created_at - a.created_at);
},
refetchInterval: 30000, // Refetch every 30 seconds
});
if (error) {
return (
<div className="col-span-full">
<Card className="border-dashed border-destructive/50">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-6">
<div className="text-4xl">🍄💔</div>
<div>
<h3 className="font-semibold text-destructive mb-2">
The forest spirits are troubled!
</h3>
<p className="text-muted-foreground mb-4">
Unable to connect to the message-mushroom network. The woodland whispers have gone silent.
</p>
<RelaySelector className="w-full" />
</div>
</div>
</CardContent>
</Card>
</div>
);
}
if (isLoading) {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<GnomePostSkeleton key={i} />
))}
</div>
);
}
if (!posts || posts.length === 0) {
return (
<div className="col-span-full">
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-6">
<div className="text-6xl gnome-wiggle">🍄</div>
<div>
<h3 className="font-semibold mb-2">
The forest is quiet today...
</h3>
<p className="text-muted-foreground mb-4">
No woodland proclamations found. Perhaps the gnomes are all busy tending their gardens? Try another message-mushroom relay to discover more forest folk!
</p>
<RelaySelector className="w-full" />
</div>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-4">
{posts.map((post) => (
<GnomePost key={post.id} event={post} />
))}
</div>
);
}

View File

@ -0,0 +1,88 @@
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { LoginArea } from '@/components/auth/LoginArea';
import { RelaySelector } from '@/components/RelaySelector';
import { useTheme } from '@/hooks/useTheme';
import { Moon, Sun, Leaf, TreePine, Sparkles } from 'lucide-react';
export function GnomeHeader() {
const { theme, setTheme } = useTheme();
return (
<Card className="mb-6 border-2 border-primary/30 bg-gradient-to-r from-primary/5 via-accent/5 to-secondary/5">
<CardContent className="py-6">
<div className="flex flex-col space-y-4">
{/* Main header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="text-3xl gnome-wiggle">🍄</div>
<div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
Gnome Nostr
</h1>
<p className="text-sm text-muted-foreground">
The sacred network of woodland folk
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Badge variant="secondary" className="hidden md:flex items-center space-x-1">
<Sparkles className="h-3 w-3" />
<span>Gnome Speak</span>
</Badge>
<Button
variant="ghost"
size="sm"
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className="text-muted-foreground hover:text-foreground"
>
{theme === 'light' ? (
<Moon className="h-4 w-4" />
) : (
<Sun className="h-4 w-4" />
)}
<span className="hidden sm:inline ml-1">
{theme === 'light' ? 'Night Forest' : 'Day Forest'}
</span>
</Button>
<LoginArea className="max-w-48" />
</div>
</div>
{/* Info and controls bar */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-3 sm:space-y-0 pt-3 border-t border-border/30">
<div className="flex items-center space-x-4 text-xs text-muted-foreground">
<div className="flex items-center space-x-1">
<TreePine className="h-3 w-3" />
<span className="hidden sm:inline">Text notes translated to gnome speak</span>
<span className="sm:hidden">Gnome translation active</span>
</div>
</div>
<div className="flex items-center space-x-2">
<Leaf className="h-3 w-3 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Relay:</span>
<RelaySelector className="max-w-48" />
</div>
</div>
{/* Welcome message */}
<div className="bg-accent/10 border border-accent/20 rounded-lg p-4 text-center">
<p className="text-sm text-accent-foreground">
<span className="font-medium">🌟 Welcome to the enchanted forest!</span>
<br />
<span className="text-xs text-muted-foreground mt-1 block">
All text notes are magically translated into the ancient gnome tongue.
Share your woodland wisdom with fellow earth-dwellers!
</span>
</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -22,9 +22,10 @@ describe('NoteContent', () => {
</TestApp>
);
const link = screen.getByRole('link', { name: 'https://example.com' });
// The URL should still be linkified even after gnome translation
const link = screen.getByRole('link', { name: /https:\/\/example/ });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://example.com');
expect(link.getAttribute('href')).toMatch(/https:\/\/example/);
expect(link).toHaveAttribute('target', '_blank');
});
@ -93,6 +94,7 @@ describe('NoteContent', () => {
</TestApp>
);
// Hashtags are preserved during gnome translation
const nostrHashtag = screen.getByRole('link', { name: '#nostr' });
const bitcoinHashtag = screen.getByRole('link', { name: '#bitcoin' });

View File

@ -5,6 +5,7 @@ import { nip19 } from 'nostr-tools';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { cn } from '@/lib/utils';
import { translateToGnomeSpeak, shouldTranslateToGnomeSpeak } from '@/lib/gnomeSpeak';
interface NoteContentProps {
event: NostrEvent;
@ -18,7 +19,11 @@ export function NoteContent({
}: NoteContentProps) {
// Process the content to render mentions, links, etc.
const content = useMemo(() => {
const text = event.content;
// Translate to gnome speak if it's a kind 1 event
const originalText = event.content;
const text = shouldTranslateToGnomeSpeak(event.kind)
? translateToGnomeSpeak(originalText)
: originalText;
// Regex to find URLs, Nostr references, and hashtags
const regex = /(https?:\/\/[^\s]+)|nostr:(npub1|note1|nprofile1|nevent1)([023456789acdefghjklmnpqrstuvwxyz]+)|(#\w+)/g;

View File

@ -4,89 +4,85 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Gnome Forest Theme - Light Mode */
--background: 120 20% 97%; /* Very light sage green */
--foreground: 120 15% 15%; /* Dark forest green */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--card: 120 25% 95%; /* Light moss green */
--card-foreground: 120 15% 15%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--popover: 120 25% 95%;
--popover-foreground: 120 15% 15%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--primary: 25 85% 55%; /* Warm mushroom orange */
--primary-foreground: 120 25% 95%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--secondary: 120 30% 85%; /* Soft forest green */
--secondary-foreground: 120 15% 15%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--muted: 120 20% 90%; /* Light moss */
--muted-foreground: 120 10% 45%; /* Medium forest green */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--accent: 45 90% 70%; /* Golden acorn yellow */
--accent-foreground: 120 15% 15%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%; /* Red mushroom */
--destructive-foreground: 120 25% 95%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--border: 120 25% 85%; /* Soft green border */
--input: 120 25% 90%; /* Light input background */
--ring: 25 85% 55%; /* Mushroom orange focus ring */
--radius: 0.5rem;
--radius: 0.75rem; /* More rounded for organic feel */
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-background: 120 30% 92%;
--sidebar-foreground: 120 15% 20%;
--sidebar-primary: 25 85% 55%;
--sidebar-primary-foreground: 120 25% 95%;
--sidebar-accent: 120 25% 88%;
--sidebar-accent-foreground: 120 15% 15%;
--sidebar-border: 120 25% 80%;
--sidebar-ring: 25 85% 55%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* Gnome Forest Theme - Dark Mode (Night in the Forest) */
--background: 120 25% 8%; /* Deep forest night */
--foreground: 120 20% 85%; /* Light moss green */
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--card: 120 20% 12%; /* Dark forest floor */
--card-foreground: 120 20% 85%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--popover: 120 20% 12%;
--popover-foreground: 120 20% 85%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--primary: 25 85% 65%; /* Bright mushroom orange */
--primary-foreground: 120 20% 12%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--secondary: 120 15% 20%; /* Dark moss */
--secondary-foreground: 120 20% 85%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--muted: 120 15% 18%; /* Dark undergrowth */
--muted-foreground: 120 15% 60%; /* Medium light green */
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--accent: 45 90% 75%; /* Bright golden acorn */
--accent-foreground: 120 20% 12%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--destructive: 0 75% 55%; /* Dark red mushroom */
--destructive-foreground: 120 20% 85%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--border: 120 20% 20%; /* Dark forest border */
--input: 120 20% 15%; /* Dark input background */
--ring: 25 85% 65%; /* Bright mushroom focus ring */
--sidebar-background: 120 25% 10%;
--sidebar-foreground: 120 20% 80%;
--sidebar-primary: 25 85% 65%;
--sidebar-primary-foreground: 120 20% 12%;
--sidebar-accent: 120 20% 18%;
--sidebar-accent-foreground: 120 20% 85%;
--sidebar-border: 120 20% 20%;
--sidebar-ring: 25 85% 65%;
}
}
@ -97,5 +93,63 @@
body {
@apply bg-background text-foreground;
background-image:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 48, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 200, 120, 0.05) 0%, transparent 50%);
}
}
/* Gnome-specific animations and effects */
@keyframes gnome-wiggle {
0%, 100% { transform: rotate(-1deg); }
50% { transform: rotate(1deg); }
}
@keyframes mushroom-glow {
0%, 100% { box-shadow: 0 0 5px rgba(255, 119, 48, 0.3); }
50% { box-shadow: 0 0 15px rgba(255, 119, 48, 0.5); }
}
@keyframes forest-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.gnome-wiggle {
animation: gnome-wiggle 2s ease-in-out infinite;
}
.mushroom-glow {
animation: mushroom-glow 3s ease-in-out infinite;
}
.forest-shimmer {
background: linear-gradient(
90deg,
transparent,
rgba(120, 200, 120, 0.2),
transparent
);
background-size: 200% 100%;
animation: forest-shimmer 3s ease-in-out infinite;
}
/* Custom scrollbar for gnome theme */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: hsl(var(--muted));
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--primary));
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--accent));
}

View File

@ -0,0 +1,61 @@
import { describe, it, expect } from 'vitest';
import { translateToGnomeSpeak, shouldTranslateToGnomeSpeak } from './gnomeSpeak';
describe('gnomeSpeak', () => {
describe('shouldTranslateToGnomeSpeak', () => {
it('returns true for kind 1 events', () => {
expect(shouldTranslateToGnomeSpeak(1)).toBe(true);
});
it('returns false for other event kinds', () => {
expect(shouldTranslateToGnomeSpeak(0)).toBe(false);
expect(shouldTranslateToGnomeSpeak(3)).toBe(false);
expect(shouldTranslateToGnomeSpeak(1111)).toBe(false);
expect(shouldTranslateToGnomeSpeak(30023)).toBe(false);
});
});
describe('translateToGnomeSpeak', () => {
it('translates basic words to gnome speak', () => {
const result = translateToGnomeSpeak('Hello world! This is good.');
expect(result).toContain('Greetings, fellow');
expect(result).toContain('splendid as');
});
it('preserves URLs during translation', () => {
const result = translateToGnomeSpeak('Check out https://example.com for good info');
expect(result).toMatch(/https:\/\/example/);
expect(result).toContain('splendid as');
});
it('preserves nostr references during translation', () => {
const result = translateToGnomeSpeak('Follow nostr:npub1zg69v7ys40x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehhsqepuh0 for good content');
expect(result).toContain('nostr:npub1zg69v7ys40x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehhsqepuh0');
expect(result).toContain('splendid as');
});
it('preserves hashtags during translation', () => {
const result = translateToGnomeSpeak('This is about #nostr and #bitcoin development');
expect(result).toContain('#nostr');
expect(result).toContain('#bitcoin');
});
it('adds gnome-specific formatting for special terms', () => {
const result = translateToGnomeSpeak('I love bitcoin and gm everyone!');
expect(result).toContain('golden acorns of the digital realm');
expect(result).toContain('🌅when dew kisses the earth🌅');
});
it('handles empty or invalid input', () => {
expect(translateToGnomeSpeak('')).toBe('');
expect(translateToGnomeSpeak(null as unknown as string)).toBe(null);
expect(translateToGnomeSpeak(undefined as unknown as string)).toBe(undefined);
});
it('capitalizes sentences properly', () => {
const result = translateToGnomeSpeak('hello world. this is good.');
expect(result).toMatch(/^[A-Z]/); // Should start with capital letter
expect(result).toMatch(/\. [A-Z]/); // Should capitalize after periods
});
});
});

923
src/lib/gnomeSpeak.ts Normal file
View File

@ -0,0 +1,923 @@
/**
* Gnome Speak Translator
* Transforms regular text into whimsical gnome language
*/
// Common gnome words and phrases
const gnomeVocabulary: Record<string, string> = {
// Greetings and expressions
'hello': 'greetings, fellow earth-dweller',
'hi': 'huzzah',
'hey': 'ho there',
'howdy': 'well met, forest-friend',
'welcome': 'blessed be your arrival',
'goodbye': 'may your mushrooms grow tall',
'bye': 'farewell, friend of the forest',
'farewell': 'until the paths cross again',
'thanks': 'blessings of the garden upon you',
'thank you': 'may the roots remember your kindness',
'please': 'if it pleases the woodland spirits',
'sorry': 'forgive this humble gnome',
'excuse me': 'pardon this forest-dweller',
'yes': 'aye, by my pointy hat',
'yeah': 'indeed, forest-friend',
'yep': 'certainly, woodland-companion',
'absolutely': 'as sure as ancient-oaks',
'definitely': 'certain as sunrise',
'certainly': 'without doubt, earth-friend',
'sure': 'as reliable as mountain-stones',
'no': 'nay, not even for all the acorns in the forest',
'nope': 'negative, forest-friend',
'never': 'not in a thousand-seasons',
'maybe': 'perhaps, if the mushrooms align',
'perhaps': 'possibly, when stars-align',
'possibly': 'might be, if spirits-will',
'probably': 'likely as autumn-leaves-falling',
'ok': 'very well, tiny friend',
'okay': 'indeed, little earth-dweller',
'alright': 'acceptable to forest-council',
'fine': 'satisfactory as spring-weather',
'wow': 'by the ancient-spirits',
'whoa': 'sacred mushrooms preserve-us',
'oh': 'woodland-spirits witness',
'oops': 'clumsy as startled-squirrels',
'darn': 'cursed as wilted-flowers',
// Common descriptive words
'good': 'splendid as morning dew',
'great': 'magnificent as ancient oak',
'awesome': 'wondrous as fairy rings',
'amazing': 'astounding as dragon sightings',
'incredible': 'miraculous as talking trees',
'wonderful': 'marvelous as rainbow bridges',
'excellent': 'superb as crystal caves',
'fantastic': 'enchanting as unicorn meadows',
'perfect': 'flawless as crystal dewdrops',
'beautiful': 'lovely as blooming flowers',
'pretty': 'charming as flower petals',
'gorgeous': 'stunning as sunset glades',
'handsome': 'dashing as noble stags',
'nice': 'pleasant as garden breeze',
'sweet': 'endearing as honey-flavored-words',
'cool': 'refreshing as spring water',
'hot': 'blazing as dragon breath',
'warm': 'cozy as hearth embers',
'cold': 'frigid as mountain peaks',
'bad': 'cursed as wilted leaves',
'terrible': 'dreadful as goblin raids',
'horrible': 'ghastly as poisoned wells',
'awful': 'abysmal as barren wastelands',
'ugly': 'unsightly as rotting stumps',
'hideous': 'repulsive as swamp creatures',
'disgusting': 'revolting as rotten-smell-smellers',
'gross': 'sickening as filth-encountering',
'big': 'enormous as giant toadstools',
'large': 'vast as forest clearings',
'huge': 'colossal as mountain trolls',
'massive': 'immense as ancient trees',
'giant': 'titanic as elder dragons',
'small': 'tiny as acorn caps',
'little': 'wee as dewdrops',
'tiny': 'minuscule as fairy footprints',
'miniature': 'pocket-sized as gnome homes',
'fast': 'swift as woodland sprites',
'quick': 'rapid as darting squirrels',
'speedy': 'fleet as racing rabbits',
'slow': 'leisurely as growing moss',
'sluggish': 'plodding as sleepy bears',
'new': 'fresh as spring shoots',
'old': 'ancient as elder trees',
'young': 'youthful as new growth',
'fresh': 'crisp as dawn air',
'stale': 'aged as ancient crumbs',
'clean': 'pure as mountain springs',
'dirty': 'soiled as muddy paths',
'rich': 'wealthy as treasure hoards',
'poor': 'humble as simple folk',
'expensive': 'costly as rare gems',
'cheap': 'affordable as common stones',
'free': 'unbound as forest winds',
'easy': 'simple as breathing air',
'difficult': 'challenging as mountain climbing',
'hard': 'tough as ancient bark',
'soft': 'gentle as rabbit fur',
'strong': 'mighty as bear strength',
'weak': 'frail as spider silk',
'heavy': 'weighty as boulder loads',
'light': 'airy as dandelion fluff',
'dark': 'shadowy as deep cave',
'bright': 'radiant as summer sun',
'loud': 'noisy as thunder crashes',
'quiet': 'silent as falling snow',
'busy': 'active as worker ants',
'lazy': 'idle as sunbathing cats',
'tired': 'weary as day\'s end',
'awake': 'alert as watchful owls',
'alive': 'living with forest vitality',
'dead': 'lifeless as fallen logs',
'healthy': 'robust as oak trees',
'sick': 'ailing like withered plants',
'safe': 'secure as burrow homes',
'dangerous': 'perilous as cliff edges',
'careful': 'cautious as deer at streams',
'careless': 'reckless as storm winds',
'smart': 'clever as cunning foxes',
'stupid': 'mindlessly as senseless acts',
'wise': 'sage as ancient owls',
'foolish': 'unwise as reckless youth',
'funny': 'amusing as playful otters',
'serious': 'grave as solemn ceremonies',
'strange': 'odd as two-headed mushrooms',
'normal': 'ordinary as daily sunrise',
'special': 'unique as snowflake patterns',
'different': 'distinct as individual leaves',
'same': 'identical as twin acorns',
'similar': 'alike as sister streams',
'equal': 'equivalent as balanced scales',
'fair': 'just as forest law',
'unfair': 'unjust as stolen honey',
'true': 'accurate as gnome-wisdom',
'false': 'untrue as winter promises',
'right': 'correct as compass needles',
'wrong': 'mistaken as lost travelers',
'correct': 'proper as forest protocol',
'important': 'significant as ancient laws',
'interesting': 'fascinating as rare discoveries',
'boring': 'dull as watching grass grow',
'fun': 'enjoyable as harvest festivals',
'lucky': 'fortunate as four-leaf clovers',
'unlucky': 'cursed as broken mirrors',
'successful': 'triumphant as victorious warriors',
'popular': 'beloved as festival leaders',
'famous': 'renowned as legendary heroes',
'unknown': 'mysterious as hidden valleys',
// Emotions and feelings
'happy': 'merry as spring festivals',
'joyful': 'gleeful as dancing pixies',
'cheerful': 'bright as sunny meadows',
'excited': 'thrilled as treasure hunters',
'delighted': 'pleased as successful gardeners',
'ecstatic': 'overjoyed as festival winners',
'elated': 'uplifted as soaring-eagles',
'content': 'satisfied as well-fed-bears',
'pleased': 'gratified as successful-gardeners',
'glad': 'happy as singing-birds',
'relieved': 'unburdened as dropped-heavy-packs',
'grateful': 'thankful as rain-blessed-crops',
'proud': 'dignified as royal stags',
'confident': 'assured as mountain peaks',
'brave': 'courageous as warrior bears',
'bold': 'daring as cliff-climbing-goats',
'calm': 'peaceful as still ponds',
'relaxed': 'at-ease as sunbathing-cats',
'peaceful': 'tranquil as morning-meadows',
'sad': 'melancholy as autumn rain',
'unhappy': 'sorrowful as wilting flowers',
'depressed': 'gloomy as storm clouds',
'miserable': 'wretched as lost wanderers',
'heartbroken': 'shattered as fallen-bird-eggs',
'lonely': 'isolated as single-trees',
'hurt': 'wounded as thorn-pricked-paws',
'angry': 'wrathful as disturbed badgers',
'mad': 'furious as territorial bears',
'furious': 'enraged as protective mothers',
'annoyed': 'vexed as pestered squirrels',
'irritated': 'bothered as interrupted naps',
'frustrated': 'blocked like dammed-streams',
'afraid': 'fearful as startled mice',
'scared': 'frightened as hunted-rabbits',
'terrified': 'petrified as stone-statues',
'nervous': 'jittery as caffeine-buzzed-squirrels',
'worried': 'concerned as protective-parents',
'anxious': 'worried as storm-watching-sailors',
'surprised': 'astonished as sudden storms',
'shocked': 'stunned as lightning-struck-trees',
'amazed': 'astounded as miracle-witnesses',
'confused': 'puzzled as riddle-hearers',
'lost': 'directionless as pathless-wanderers',
'curious': 'inquisitive as exploring-kittens',
'interested': 'engaged as story-listeners',
'bored': 'uninterested as sleepy-sloths',
'jealous': 'envious as covetous-dragons',
'envious': 'resentful as have-nots',
'greedy': 'grasping as hoarding-squirrels',
'selfish': 'self-centered as narcissistic-peacocks',
'generous': 'giving as abundant-fruit-trees',
'kind': 'gentle as mother\'s touch',
'mean': 'nasty as stinging-nettles',
'cruel': 'harsh as winter-storms',
'embarrassed': 'ashamed as public-mistake-makers',
'guilty': 'remorseful as wrongdoing-confessors',
'ashamed': 'dishonored as oath-breakers',
// Actions and verbs
'go': 'venture forth',
'come': 'approach the sacred circle',
'see': 'witness with gnome-eyes',
'look': 'gaze upon',
'watch': 'observe like sentinel-owls',
'listen': 'hear the whispers of the wind',
'hear': 'perceive with gnome-ears',
'speak': 'utter in ancient dialect',
'talk': 'speak in the ancient tongue',
'say': 'declare to forest council',
'tell': 'relate tales by firelight',
'ask': 'inquire of the wise ones',
'answer': 'respond with woodland-wisdom',
'think': 'ponder in the mushroom circle',
'know': 'hold wisdom in thy cap',
'learn': 'gather knowledge like berries',
'teach': 'share wisdom of the ancients',
'understand': 'comprehend with woodland wisdom',
'remember': 'recall from memory-moss',
'forget': 'lose to the mists of time',
'believe': 'trust in forest magic',
'hope': 'wish upon evening stars',
'feel': 'sense with gnome-heart',
'love': 'cherish like precious gems',
'like': 'fancy as favorite flowers',
'hate': 'despise like garden pests',
'want': 'desire like moths seek flame',
'need': 'require like flowers need rain',
'have': 'possess like territorial-bears',
'get': 'obtain through forest cunning',
'give': 'bestow gifts of the heart',
'take': 'gather like squirrels in autumn',
'put': 'place with gnomish care',
'make': 'craft with gnomish hands',
'do': 'perform with woodland-skill',
'work': 'toil in the earth',
'play': 'frolic like woodland creatures',
'help': 'lend aid to fellow forest-folk',
'try': 'attempt with gnomish determination',
'use': 'employ with woodland wisdom',
'find': 'discover hidden treasures',
'lose': 'misplace in the deep woods',
'keep': 'guard like dragon\'s treasure',
'save': 'preserve like squirrels save nuts',
'spend': 'use like autumn leaves falling',
'buy': 'trade for with shiny pebbles',
'sell': 'barter in the market grove',
'pay': 'offer tribute to forest spirits',
'win': 'triumph like mighty oak',
'start': 'begin the woodland journey',
'stop': 'halt like deer sensing danger',
'end': 'conclude like sunset',
'finish': 'complete like harvest-gathering',
'begin': 'commence the forest-quest',
'continue': 'proceed along woodland-path',
'change': 'transform like caterpillars',
'move': 'shift like dancing leaves',
'stay': 'remain rooted like ancient trees',
'leave': 'depart on woodland winds',
'return': 'come back to the sacred grove',
'arrive': 'reach journey\'s end',
'travel': 'journey through wild lands',
'visit': 'call upon forest friends',
'meet': 'encounter on woodland paths',
'follow': 'walk the same forest path',
'lead': 'guide like wise elders',
'run': 'scamper through the underbrush',
'walk': 'stroll the forest paths',
'sit': 'perch upon mushroom throne',
'stand': 'rise like proud saplings',
'sleep': 'rest in the embrace of moss',
'wake': 'greet the morning sun',
'eat': 'partake of nature\'s gifts',
'drink': 'sip the sacred waters',
'cook': 'prepare over gnome-fires',
'read': 'decipher the runes of knowledge',
'write': 'inscribe upon bark and stone',
'build': 'construct like industrious beavers',
'break': 'shatter like brittle ice',
'fix': 'mend with gnomish skill',
'open': 'unseal like blooming flowers',
'close': 'shut like evening blossoms',
'turn': 'rotate like seasons changing',
'carry': 'bear like pack mules',
'hold': 'grasp with gnomish grip',
'throw': 'hurl like catapult stones',
'catch': 'capture like spider webs',
'send': 'dispatch like messenger birds',
'bring': 'carry like busy ants',
'show': 'reveal like dawn breaking',
'hide': 'conceal in forest-shadows',
'wait': 'pause like patient stones',
'hurry': 'rush like urgent-messengers',
'choose': 'select like wise owls',
'decide': 'determine by mushroom council',
'plan': 'scheme like clever foxes',
'create': 'birth from gnomish imagination',
'destroy': 'unmake like winter storms',
'grow': 'flourish like spring gardens',
'die': 'return to the eternal soil',
'live': 'dwell in harmony with nature',
'happen': 'occur by woodland chance',
'seem': 'appear like morning mist',
'become': 'transform into',
'remain': 'stay like enduring-stones',
'appear': 'manifest like forest-spirits',
'disappear': 'vanish like morning-mist',
'exist': 'be present in forest-realm',
'matter': 'be important as ancient oaks',
'care': 'tend like garden keeper',
'worry': 'fret like anxious rabbits',
'call': 'summon with woodland voice',
'let': 'allow the forest spirits',
'allow': 'permit by gnomish law',
'join': 'unite like flowing streams',
'share': 'spread like dandelion seeds',
'agree': 'harmonize like bird songs',
'disagree': 'clash like thunder storms',
'fight': 'battle like fierce badgers',
'argue': 'quarrel like territorial squirrels',
'discuss': 'debate in council circles',
'explain': 'clarify like morning light',
'describe': 'paint word-pictures',
'compare': 'measure against ancient standards',
'include': 'embrace within the circle',
'exclude': 'banish from sacred-grove',
'add': 'include like gathering-berries',
'remove': 'extract like pulling-weeds',
'increase': 'grow like expanding-trees',
'decrease': 'shrink like autumn-leaves',
'improve': 'enhance like garden-tending',
'develop': 'cultivate like patient farmers',
'manage': 'oversee like head gardener',
'control': 'command like forest kings',
'organize': 'arrange like busy bees',
'prepare': 'ready like winter-preparations',
'provide': 'supply from nature\'s storehouse',
'offer': 'present like gift-bearing visitors',
'accept': 'receive with grateful heart',
'refuse': 'decline like stubborn-mules',
'reject': 'spurn like bitter-berries',
'receive': 'welcome like spring rain',
'support': 'uphold like sturdy roots',
'oppose': 'resist like stubborn stones',
'protect': 'shield like mother bears',
'attack': 'assault like angry wasps',
'defend': 'guard like loyal hounds',
'escape': 'flee like startled rabbits',
'avoid': 'evade like nimble deer',
'reach': 'extend like growing vines',
'touch': 'contact with gentle-paws',
'push': 'shove like rolling boulders',
'pull': 'draw like flowing streams',
'lift': 'raise like morning mist',
'drop': 'release like autumn leaves',
'pick': 'select like berry gatherers',
'enter': 'cross the sacred threshold',
'exit': 'depart through hidden doors',
'pass': 'traverse like mountain paths',
'cross': 'bridge like fallen-logs',
'climb': 'ascend like squirrel-acrobats',
'jump': 'leap like bounding-deer',
'fall': 'tumble like rolling-acorns',
'swim': 'glide like graceful-fish',
'dance': 'move like wind-blown-leaves',
'sing': 'vocalize like morning-birds',
'laugh': 'chortle like amused-gnomes',
'cry': 'weep like rain-clouds',
'smile': 'grin like happy-sunflowers',
'frown': 'scowl like storm-clouds',
// Technology terms (gnome-ified)
'computer': 'thinking-box of metal and magic',
'laptop': 'portable wisdom-chest',
'phone': 'speaking-crystal',
'smartphone': 'clever communication-gem',
'internet': 'the great web of knowledge',
'website': 'digital mushroom ring',
'email': 'letter-sprite delivery',
'password': 'secret gnome-word',
'app': 'magical tool-sprite',
'software': 'spell-weaving programs',
'program': 'coded spell-sequence',
'code': 'mystical rune-writing',
'bug': 'mischievous code-gremlin',
'update': 'spell-enhancement ritual',
'download': 'summon from the ether',
'upload': 'send to the cloud-spirits',
'file': 'digital scroll-bundle',
'delete': 'banish to the void',
'bitcoin': 'golden acorns of the digital realm',
'crypto': 'mystical coin-magic',
'blockchain': 'the eternal ledger of the earth spirits',
'nostr': 'the sacred network of gnome-kind',
'relay': 'message-mushroom',
'post': 'woodland proclamation',
'message': 'whisper carried by wind',
'chat': 'friendly forest-chatter',
'comment': 'thoughtful tree-response',
'reply': 'echo from the grove',
'mention': 'call by woodland-name',
'tag': 'forest-marker label',
'hashtag': 'topic-gathering symbol',
'follower': 'path-walking companion',
'friend': 'woodland-path companion',
'user': 'forest-path wanderer',
'account': 'woodland-identity record',
'profile': 'forest-dweller description',
'avatar': 'spirit-image representation',
'feed': 'stream of woodland-news',
'trending': 'popular as festival-topics',
'viral': 'spreading like wildfire-news',
// Time and nature
'today': 'this blessed sun-cycle',
'tomorrow': 'the next dawn',
'yesterday': 'the previous twilight',
'now': 'this very forest-moment',
'soon': 'before the next acorn-fall',
'later': 'after the sun-shift',
'early': 'with the first bird-songs',
'late': 'when shadows grow long',
'morning': 'when dew kisses the earth',
'afternoon': 'when sun reaches tree-tops',
'evening': 'when fireflies dance',
'night': 'when the moon watches over',
'day': 'sun-blessed time',
'week': 'seven turnings of the earth',
'month': 'one moon-cycle',
'year': 'full journey round the sun',
'time': 'the eternal forest-flow',
'moment': 'brief woodland-instant',
'hour': 'sun-shadow measurement',
'minute': 'tiny time-fragment',
'second': 'heartbeat-duration',
'season': 'nature\'s changing-mood',
'spring': 'time of awakening-growth',
'summer': 'season of abundant-warmth',
'autumn': 'harvest of golden-leaves',
'winter': 'the great forest-slumber',
'weather': 'sky-spirit\'s current-mood',
'rain': 'sky-tears blessing the earth',
'snow': 'winter\'s white forest-blanket',
'sun': 'the great golden-eye',
'moon': 'night\'s silver-guardian',
'stars': 'celestial navigation-gems',
'sky': 'the endless blue-canopy',
'cloud': 'sky-spirit\'s fluffy-messenger',
'wind': 'breath of the forest-spirits',
'storm': 'sky-spirits\' angry-dance',
'thunder': 'voice of the cloud-giants',
'lightning': 'sky-fire\'s brilliant-flash',
'rainbow': 'bridge between earth and sky',
'nature': 'the great mother-spirit',
'earth': 'our beloved soil-mother',
'world': 'the vast forest-realm',
'ground': 'the solid earth-foundation',
'soil': 'the life-giving dirt-magic',
'rock': 'earth\'s ancient bone-structure',
'stone': 'small piece of mountain-heart',
'mountain': 'earth\'s reaching-toward-sky',
'hill': 'gentle earth-rise',
'valley': 'earth\'s peaceful-embrace',
'forest': 'the sacred tree-gathering',
'tree': 'earth\'s reaching-toward-light',
'branch': 'tree\'s extending-arm',
'leaf': 'tree\'s breathing-surface',
'root': 'tree\'s earth-drinking-finger',
'flower': 'plant\'s colorful-celebration',
'grass': 'earth\'s green-carpet',
'plant': 'earth\'s growing-child',
'garden': 'tended plant-paradise',
'water': 'life-giving crystal liquid',
'river': 'flowing water-pathway',
'lake': 'earth\'s water-mirror',
'ocean': 'vast water-wilderness',
'fire': 'dancing heat-spirit',
'flame': 'fire\'s reaching-tongue',
'smoke': 'fire\'s gray-breath',
'wood': 'tree\'s gift-material',
'animal': 'forest-dwelling-creature',
'bird': 'sky-dancing-singer',
'fish': 'water-dwelling-swimmer',
'dog': 'loyal four-legged-friend',
'cat': 'independent whisker-hunter',
'horse': 'noble four-legged-runner',
'cow': 'gentle milk-giving-beast',
'pig': 'round mud-loving-creature',
'chicken': 'feathered egg-laying-bird',
'bear': 'mighty forest-guardian',
'wolf': 'pack-hunting-howler',
'deer': 'graceful forest-leaper',
'rabbit': 'quick-hopping-nibbler',
'squirrel': 'tree-climbing-nut-gatherer',
'mouse': 'tiny cheese-seeking-creature',
'rat': 'clever tunnel-dwelling-survivor',
'snake': 'slithering ground-crawler',
'spider': 'web-weaving-hunter',
'bee': 'buzzing honey-maker',
'butterfly': 'colorful flower-dancer',
'ant': 'industrious colony-worker',
'fly': 'annoying buzzing-pest',
'mosquito': 'blood-seeking-tiny-vampire',
'frog': 'pond-dwelling-croaker',
'turtle': 'shell-carrying-slow-walker',
'owl': 'wise night-hunting-watcher',
'eagle': 'majestic sky-soaring-hunter',
'dragon': 'legendary fire-breathing-beast',
'unicorn': 'mythical horn-bearing-horse',
'fairy': 'tiny magical-forest-sprite',
'elf': 'pointed-ear-woodland-dweller',
'dwarf': 'bearded mountain-tunnel-digger',
'wizard': 'spell-casting-wise-elder',
'witch': 'potion-brewing-magic-user',
'ghost': 'transparent spirit-being',
'monster': 'frightening creature-of-darkness',
// Food and sustenance
'food': 'earth\'s bounty',
'meal': 'feast of forest-gifts',
'breakfast': 'dawn\'s energy-awakening',
'lunch': 'midday sustenance-break',
'dinner': 'evening\'s nourishment-gathering',
'snack': 'small between-meal-nibble',
'hungry': 'craving earth\'s-nourishment',
'thirsty': 'needing liquid-refreshment',
'full': 'satisfied with belly-bounty',
'coffee': 'bitter bean brew',
'tea': 'leaf-water of wisdom',
'milk': 'cow\'s white-gift',
'juice': 'fruit\'s liquid-essence',
'beer': 'grain\'s fermented-celebration',
'wine': 'grape\'s aged-transformation',
'bread': 'grain-gift of the fields',
'cake': 'sweet celebration-tower',
'cookie': 'small sweet-disk',
'pizza': 'flat bread with toppings-garden',
'meat': 'animal\'s protein-gift',
'vegetable': 'earth\'s colorful-children',
'fruit': 'tree\'s sweet-offering',
'apple': 'red orb of crisp-sweetness',
'banana': 'yellow crescent-fruit',
'orange': 'citrus sun-sphere',
'cheese': 'milk\'s aged-transformation',
'egg': 'bird\'s oval-creation',
'honey': 'bee\'s golden-sweetness',
'sugar': 'cane\'s crystallized-sweetness',
'salt': 'sea\'s mineral-gift',
'spicy': 'fire-tongue-sensation',
'delicious': 'wonderfully tasty-pleasure',
'tasty': 'pleasing to tongue-senses',
'yummy': 'delightfully satisfying-flavor',
// People and relationships
'person': 'wanderer of the mortal realm',
'people': 'fellow earth-dwellers',
'human': 'tall-folk',
'man': 'bearded earth-walker',
'woman': 'graceful forest-daughter',
'child': 'young sprout',
'baby': 'tiny seedling',
'boy': 'male young-sprout',
'girl': 'female young-blossom',
'family': 'clan of the same burrow',
'mother': 'nurturing life-bringer',
'father': 'protecting life-provider',
'parent': 'life-giving-elder',
'son': 'male offspring-sprout',
'daughter': 'female offspring-blossom',
'brother': 'male sibling-companion',
'sister': 'female sibling-friend',
'husband': 'male life-bond-companion',
'wife': 'female life-bond-companion',
'boyfriend': 'courting male-companion',
'girlfriend': 'courting female-companion',
'neighbor': 'nearby-dwelling-friend',
'stranger': 'unknown path-crosser',
'teacher': 'wisdom-sharing-elder',
'student': 'knowledge-seeking-sprout',
'doctor': 'healing-arts-practitioner',
'worker': 'labor-providing-being',
'boss': 'work-directing-leader',
'leader': 'group-guiding-chief',
'team': 'cooperating-group-unit',
'group': 'gathered-together-collection',
'community': 'shared-dwelling-collective',
// Places and locations
'place': 'woodland-location',
'home': 'cozy burrow-dwelling',
'house': 'family nest-structure',
'building': 'tall human-construction',
'room': 'enclosed dwelling-space',
'kitchen': 'food-preparation-chamber',
'bedroom': 'sleeping-rest-sanctuary',
'bathroom': 'cleansing-water-room',
'office': 'work-task-chamber',
'school': 'learning-wisdom-hall',
'hospital': 'healing-treatment-center',
'store': 'goods-trading-establishment',
'restaurant': 'food-serving-hall',
'park': 'public green-space',
'street': 'paved pathway-for-travel',
'road': 'long travel-route',
'city': 'large human-settlement',
'town': 'medium human-gathering',
'village': 'small community-cluster',
'country': 'large territorial-region',
'state': 'governmental-territory-division',
'beach': 'sandy water-meeting-shore',
'island': 'water-surrounded-land',
'desert': 'dry sandy-wasteland',
'farm': 'food-growing-sanctuary',
'field': 'open earth-expanse',
'meadow': 'flower-filled-clearing',
// Objects and things
'thing': 'woodland-object',
'object': 'physical forest-item',
'item': 'individual possession-piece',
'stuff': 'various collected-objects',
'book': 'knowledge-filled-pages',
'paper': 'tree-pulp-writing-surface',
'pen': 'ink-flowing-writing-stick',
'pencil': 'graphite-marking-tool',
'car': 'metal traveling-box',
'bike': 'two-wheeled-pedal-machine',
'train': 'long track-riding-transport',
'plane': 'sky-flying-metal-bird',
'boat': 'water-floating-vessel',
'ship': 'large ocean-crossing-craft',
'chair': 'sitting-support-furniture',
'table': 'flat surface-for-placing',
'bed': 'sleeping-comfort-platform',
'door': 'entrance-barrier-portal',
'window': 'transparent wall-opening',
'wall': 'vertical room-boundary',
'floor': 'horizontal walking-surface',
'roof': 'overhead shelter-covering',
'box': 'storage-container-cube',
'bag': 'flexible carrying-container',
'bottle': 'liquid-holding-vessel',
'cup': 'small drinking-container',
'plate': 'flat food-serving-disk',
'knife': 'sharp cutting-blade',
'fork': 'pronged eating-tool',
'spoon': 'curved scooping-utensil',
'clothes': 'body-covering-garments',
'shirt': 'upper-body-covering',
'pants': 'leg-covering-garment',
'shoes': 'foot-protecting-coverings',
'hat': 'head-covering-accessory',
'ring': 'finger-circling-jewelry',
'key': 'lock-opening-metal-tool',
'money': 'trade-value-tokens',
'coin': 'small metal-currency-disk',
'card': 'flat rectangular-object',
'gift': 'present-offering-surprise',
'toy': 'play-entertainment-object',
'game': 'rule-based-fun-activity',
'ball': 'round bouncing-sphere',
'tool': 'work-assisting-implement',
'machine': 'mechanical working-device',
'camera': 'image-capturing-box',
'picture': 'visual-image-representation',
'photo': 'light-captured-moment',
'mirror': 'reflection-showing-surface',
'lamp': 'artificial light-provider',
'candle': 'wax flame-holder',
'ice': 'frozen solid-water',
'glass': 'transparent brittle-material',
'metal': 'hard shiny-material',
'plastic': 'synthetic flexible-material',
'rubber': 'bouncy stretchy-material',
'fabric': 'woven thread-material',
'leather': 'animal-hide-material',
'string': 'thin flexible-cord',
'rope': 'thick strong-cord',
'chain': 'linked metal-connections',
'wire': 'thin metal-strand',
'nail': 'sharp fastening-spike',
'screw': 'threaded fastening-rod',
'hammer': 'striking-force-tool',
'saw': 'cutting-teeth-tool',
'brush': 'bristled cleaning-tool',
'paint': 'color-coating-liquid',
'soap': 'cleaning-foam-substance',
'towel': 'drying-absorption-cloth',
'blanket': 'warmth-providing-cover',
'pillow': 'head-supporting-cushion',
'clock': 'time-displaying-device',
'calendar': 'date-organizing-chart',
'map': 'location-showing-guide',
'flag': 'symbolic cloth-banner',
'seed': 'plant\'s future-potential',
'stick': 'small wood-piece',
'shell': 'sea-creature\'s protective-home',
'feather': 'bird\'s flight-enabling-plume',
'bone': 'skeleton\'s structural-piece',
'fur': 'animal\'s warm-covering',
'scale': 'fish\'s protective-armor',
'horn': 'animal\'s pointed-growth',
'tail': 'animal\'s rear-appendage',
'wing': 'flight-enabling-appendage',
'claw': 'sharp grasping-nail',
'tooth': 'biting-chewing-bone',
'eye': 'vision-sensing-orb',
'ear': 'sound-detecting-organ',
'nose': 'smell-sensing-protrusion',
'mouth': 'eating-speaking-opening',
'hand': 'grasping-manipulation-appendage',
'finger': 'hand\'s flexible-digit',
'foot': 'walking-support-appendage',
'toe': 'foot\'s small-digit',
'head': 'thinking-sensing-sphere',
'hair': 'head\'s growing-strands',
'face': 'front-expression-surface',
'neck': 'head-connecting-column',
'shoulder': 'arm-connecting-joint',
'arm': 'reaching-grasping-limb',
'leg': 'walking-supporting-limb',
'knee': 'leg-bending-joint',
'chest': 'front-torso-area',
'stomach': 'food-digesting-organ',
'heart': 'blood-pumping-organ',
'brain': 'thinking-control-organ',
'blood': 'life-flowing-red-liquid',
'skin': 'body\'s protective-covering',
'muscle': 'movement-creating-tissue',
'breath': 'life-sustaining-air-flow',
'voice': 'sound-producing-expression',
'word': 'meaning-carrying-sound-unit',
'language': 'communication-sound-system',
'name': 'identity-labeling-word',
'story': 'event-narrative-tale',
'joke': 'humor-creating-tale',
'song': 'melodic-sound-creation',
'music': 'organized-sound-art',
'sound': 'air-vibration-sensation',
'noise': 'unwanted-sound-disturbance',
'silence': 'absence-of-sound',
'color': 'light-wavelength-perception',
'red': 'blood-fire-color',
'blue': 'sky-water-color',
'green': 'grass-leaf-color',
'yellow': 'sun-gold-color',
'purple': 'royal-flower-color',
'pink': 'blossom-blush-color',
'brown': 'earth-wood-color',
'black': 'night-shadow-color',
'white': 'snow-cloud-color',
'gray': 'storm-stone-color',
'silver': 'moon-metal-color',
'gold': 'treasure-sun-color',
};
// Gnome exclamations and interjections
const gnomeExclamations = [
'By my pointy hat!',
'Great toadstools!',
'Mushroom caps!',
'Sacred acorns!',
'Blessed berries!',
'Garden spirits preserve us!',
'Forest magic!',
'Woodland wonders!',
'Gnome\'s beard!',
'Tiny treasures!',
'Earth\'s bounty!',
'Root and branch!',
'Moss and mushrooms!',
'Fern and flower!',
'Bark and berry!',
'Stone and stream!'
];
// Gnome sentence starters
const gnomeSentenceStarters = [
'In the sacred grove, ',
'By the ancient oak, ',
'Beneath the mushroom ring, ',
'Among the forest folk, ',
'In the woodland realm, ',
'By the babbling brook, ',
'Under the starlit sky, ',
'Within the garden walls, ',
'Beside the fairy ring, ',
'Through the morning mist, ',
'As the seasons turn, ',
'When the moon is full, ',
'In the depths of the forest, ',
'Where the wild things grow, ',
'Beyond the garden gate, '
];
// Gnome sentence endings
const gnomeSentenceEndings = [
', as the ancients foretold.',
', blessed by the forest spirits.',
', in harmony with nature.',
', according to gnome wisdom.',
', as written in the bark of the elder tree.',
', by the light of the fireflies.',
', under the watchful eyes of the owls.',
', with the blessing of the earth mother.',
', as sure as mushrooms grow.',
', in the way of our people.',
', following the old traditions.',
', as the seasons decree.',
', by the sacred laws of the woodland.',
', in the manner of the wise ones.',
', as nature intended.'
];
/**
* Translates regular text into gnome speak
*/
export function translateToGnomeSpeak(text: string): string {
if (!text || typeof text !== 'string') {
return text;
}
let gnomeText = text.toLowerCase();
// Preserve URLs
const urlRegex = /(https?:\/\/[^\s]+)/g;
const urlMatches: string[] = [];
gnomeText = gnomeText.replace(urlRegex, (match) => {
urlMatches.push(match);
return `__URL_${urlMatches.length - 1}__`;
});
// Preserve nostr: references
const nostrRegex = /(nostr:(npub1|note1|nprofile1|nevent1|naddr1)([023456789acdefghjklmnpqrstuvwxyz]+))/g;
const nostrMatches: string[] = [];
gnomeText = gnomeText.replace(nostrRegex, (match) => {
nostrMatches.push(match);
return `__NOSTR_${nostrMatches.length - 1}__`;
});
// Preserve hashtags
const hashtagRegex = /(#\w+)/g;
const hashtagMatches: string[] = [];
gnomeText = gnomeText.replace(hashtagRegex, (match) => {
hashtagMatches.push(match);
return `__HASHTAG_${hashtagMatches.length - 1}__`;
});
// Replace words with gnome equivalents
Object.entries(gnomeVocabulary).forEach(([english, gnome]) => {
const escapedEnglish = english.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`\\b${escapedEnglish}\\b`, 'gi');
gnomeText = gnomeText.replace(regex, gnome);
});
// Add gnome exclamations for emphasis
gnomeText = gnomeText.replace(/!/g, () => {
const exclamation = gnomeExclamations[Math.floor(Math.random() * gnomeExclamations.length)];
return `! ${exclamation}`;
});
// Add gnome sentence starters occasionally
const sentences = gnomeText.split(/[.!?]+/).filter(s => s.trim());
const gnomeSentences = sentences.map((sentence, index) => {
const trimmed = sentence.trim();
if (!trimmed) return sentence;
// Add starter to some sentences (not all, to avoid repetition)
if (Math.random() < 0.3 && index > 0) {
const starter = gnomeSentenceStarters[Math.floor(Math.random() * gnomeSentenceStarters.length)];
return starter + trimmed;
}
return trimmed;
});
// Rejoin sentences
gnomeText = gnomeSentences.join('. ');
// Add gnome endings occasionally
if (Math.random() < 0.4) {
const ending = gnomeSentenceEndings[Math.floor(Math.random() * gnomeSentenceEndings.length)];
gnomeText += ending;
}
// Capitalize first letter of sentences
gnomeText = gnomeText.replace(/(^|[.!?]\s+)([a-z])/g, (match, prefix, letter) => {
return prefix + letter.toUpperCase();
});
// Add some gnome-specific formatting
gnomeText = gnomeText.replace(/\b(bitcoin|btc)\b/gi, '✨golden acorns of the digital realm✨');
gnomeText = gnomeText.replace(/\b(gm|good morning)\b/gi, '🌅when dew kisses the earth🌅');
gnomeText = gnomeText.replace(/\b(gn|good night)\b/gi, '🌙when the moon watches over🌙');
// Restore preserved patterns
gnomeText = gnomeText.replace(/__URL_(\d+)__/g, (_, index) => urlMatches[parseInt(index)]);
gnomeText = gnomeText.replace(/__NOSTR_(\d+)__/g, (_, index) => nostrMatches[parseInt(index)]);
gnomeText = gnomeText.replace(/__HASHTAG_(\d+)__/g, (_, index) => hashtagMatches[parseInt(index)]);
return gnomeText;
}
/**
* Checks if text should be translated (only kind 1 events)
*/
export function shouldTranslateToGnomeSpeak(eventKind: number): boolean {
return eventKind === 1;
}

View File

@ -6,7 +6,7 @@ import './lib/polyfills.ts';
import App from './App.tsx';
import './index.css';
// FIXME: a custom font should be used. Eg:
// import '@fontsource-variable/<font-name>';
// Import gnome-themed font
import '@fontsource-variable/comfortaa';
createRoot(document.getElementById("root")!).render(<App />);

View File

@ -1,24 +1,38 @@
import { useSeoMeta } from '@unhead/react';
// FIXME: Update this page (the content is just a fallback if you fail to update the page)
import { GnomeHeader } from '@/components/GnomeHeader';
import { GnomeComposer } from '@/components/GnomeComposer';
import { GnomeFeed } from '@/components/GnomeFeed';
const Index = () => {
useSeoMeta({
title: 'Welcome to Your Blank App',
description: 'A modern Nostr client application built with React, TailwindCSS, and Nostrify.',
title: 'Gnome Nostr - The Sacred Network of Woodland Folk',
description: 'A magical Nostr client where all messages are translated into the ancient gnome tongue. Connect with fellow earth-dwellers across the message-mushroom network!',
});
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-gray-100">
Welcome to Your Blank App
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
Start building your amazing project here!
<div className="min-h-screen bg-background">
<div className="container max-w-2xl mx-auto px-4 py-6">
<GnomeHeader />
<GnomeComposer />
<GnomeFeed />
{/* Footer with MKStack attribution */}
<div className="mt-12 pt-6 border-t border-border/30 text-center">
<p className="text-xs text-muted-foreground">
Vibed with{' '}
<a
href="https://soapbox.pub/mkstack"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-accent transition-colors underline"
>
MKStack
</a>
{' '} Blessed by the forest spirits 🍄
</p>
</div>
</div>
</div>
);
};

View File

@ -19,6 +19,9 @@ export default {
}
},
extend: {
fontFamily: {
sans: ['Comfortaa Variable', 'Comfortaa', 'system-ui', 'sans-serif'],
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',