initial version (alpha)

This commit is contained in:
Danny Morabito 2025-10-12 13:10:06 -05:00
commit d16d7a128f
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
57 changed files with 11087 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,979 @@
= Arxlets API Context
:description: Arxlets are secure, sandboxed JavaScript applications that extend Eve's functionality.
:doctype: book
:icons: font
:source-highlighter: highlight.js
:toc: left
:toclevels: 2
:sectlinks:
== Installing EveOS
sudo coreos-installer install /dev/sda --ignition-url https://arx-ccn.com/eveos.ign
// Overview Section
== Overview
Arxlets are secure, sandboxed JavaScript applications that extend Eve's functionality. They run in isolated iframes and are registered on your CCN (Closed Community Network) for member-only access. Arxlets provide a powerful way to build custom applications that interact with Nostr events and profiles through Eve.
=== Core Concepts
What are Arxlets?
- **Sandboxed Applications**: Run in isolated iframes for security
- **JavaScript-based**: Written in TypeScript/JavaScript with wasm support coming in the future
- **CCN Integration**: Registered on your Closed Community Network
- **Nostr-native**: Built-in access to Nostr protocol operations
- **Real-time**: Support for live event subscriptions and updates
NOTE: WASM support will be added in future releases for even more powerful applications.
=== CCN Local-First Architecture
CCNs (Closed Community Networks) are designed with a local-first approach that ensures data availability and functionality even when offline:
* **Local Data Storage**: All Nostr events and profiles are stored locally on your device, providing instant access without network dependencies
* **Offline Functionality**: Arxlets can read, display, and interact with locally cached data when disconnected from the internet
* **Sync When Connected**: When network connectivity is restored, the CCN automatically synchronizes with remote relays to fetch new events and propagate local changes
* **Resilient Operation**: Your applications continue to work seamlessly regardless of network conditions, making CCNs ideal for unreliable connectivity scenarios
* **Privacy by Design**: Local-first storage means your data remains on your device, reducing exposure to external services and improving privacy
=== Architecture
- **Frontend**: TypeScript applications with render functions
- **Backend**: Eve relay providing Nostr protocol access
- **Communication**: window.eve API or direct WebSocket connections
// API Reference Section
== API Reference
The primary interface for Arxlets to interact with Eve's Nostr relay. All methods return promises for async operations.
=== window.eve API
[source,typescript]
----
// Using window.eve API for Nostr operations
import type { Filter, NostrEvent } from "./types";
// Publish a new event
const event: NostrEvent = {
kind: 1,
content: "Hello from my Arxlet!",
tags: [],
created_at: Math.floor(Date.now() / 1000),
pubkey: "your-pubkey-here",
};
await window.eve.publish(event);
// Get a specific event by ID
const eventId = "event-id-here";
const event = await window.eve.getSingleEventById(eventId);
// Query events with a filter
const filter: Filter = {
kinds: [1],
authors: ["pubkey-here"],
limit: 10,
};
const singleEvent = await window.eve.getSingleEventWithFilter(filter);
const allEvents = await window.eve.getAllEventsWithFilter(filter);
// Real-time subscription with RxJS Observable
const subscription = window.eve.subscribeToEvents(filter).subscribe({
next: (event) => {
console.log("New event received:", event);
// Update your UI with the new event
},
error: (err) => console.error("Subscription error:", err),
complete: () => console.log("Subscription completed"),
});
// Subscribe to profile updates for a specific user
const profileSubscription = window.eve.subscribeToProfile(pubkey).subscribe({
next: (profile) => {
console.log("Profile updated:", profile);
// Update your UI with the new profile data
},
error: (err) => console.error("Profile subscription error:", err),
});
// Don't forget to unsubscribe when done
// subscription.unsubscribe();
// profileSubscription.unsubscribe();
// Get user profile and avatar
const pubkey = "user-pubkey-here";
const profile = await window.eve.getProfile(pubkey);
const avatarUrl = await window.eve.getAvatar(pubkey);
----
=== Real-time Subscriptions
[source,typescript]
----
// Real-time subscription examples
import { filter, map, takeUntil } from "rxjs/operators";
// Basic subscription
const subscription = window.eve
.subscribeToEvents({
kinds: [1], // Text notes
limit: 50,
})
.subscribe((event) => {
console.log("New text note:", event.content);
});
// Advanced filtering with RxJS operators
const filteredSubscription = window.eve
.subscribeToEvents({
kinds: [1, 6, 7], // Notes, reposts, reactions
authors: ["pubkey1", "pubkey2"],
})
.pipe(
filter((event) => event.content.includes("#arxlet")), // Only events mentioning arxlets
map((event) => ({
id: event.id,
author: event.pubkey,
content: event.content,
timestamp: new Date(event.created_at * 1000),
})),
)
.subscribe({
next: (processedEvent) => {
// Update your UI
updateEventsList(processedEvent);
},
error: (err) => {
console.error("Subscription error:", err);
showErrorMessage("Failed to receive real-time updates");
},
});
// Profile subscription example
const profileSubscription = window.eve.subscribeToProfile("user-pubkey-here").subscribe({
next: (profile) => {
console.log("Profile updated:", profile);
updateUserProfile(profile);
},
error: (err) => {
console.error("Profile subscription error:", err);
},
});
// Clean up subscriptions when component unmounts
// subscription.unsubscribe();
// filteredSubscription.unsubscribe();
// profileSubscription.unsubscribe();
----
=== WebSocket Alternative
For advanced use cases, connect directly to Eve's WebSocket relay, or use any nostr library. This is not recommended:
[source,typescript]
----
// Alternative: Direct WebSocket connection
const ws = new WebSocket("ws://localhost:6942");
ws.onopen = () => {
// Subscribe to events
ws.send(JSON.stringify(["REQ", "sub1", { kinds: [1], limit: 10 }]));
};
ws.onmessage = (event) => {
const [type, subId, data] = JSON.parse(event.data);
if (type === "EVENT") {
console.log("Received event:", data);
}
};
// Publish an event
const signedEvent = await window.nostr.signEvent(unsignedEvent);
ws.send(JSON.stringify(["EVENT", signedEvent]));
----
// Type Definitions Section
== Type Definitions
[source,typescript]
----
import type { Observable } from "rxjs";
export interface NostrEvent {
id?: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig?: string;
}
export interface Filter {
ids?: string[];
authors?: string[];
kinds?: number[];
since?: number;
until?: number;
limit?: number;
[key: string]: any;
}
export interface Profile {
name?: string;
about?: string;
picture?: string;
nip05?: string;
[key: string]: any;
}
export interface WindowEve {
publish(event: NostrEvent): Promise<void>;
getSingleEventById(id: string): Promise<NostrEvent | null>;
getSingleEventWithFilter(filter: Filter): Promise<NostrEvent | null>;
getAllEventsWithFilter(filter: Filter): Promise<NostrEvent[]>;
subscribeToEvents(filter: Filter): Observable<NostrEvent>;
subscribeToProfile(pubkey: string): Observable<Profile>;
getProfile(pubkey: string): Promise<Profile | null>;
getAvatar(pubkey: string): Promise<string | null>;
signEvent(event: NostrEvent): Promise<NostrEvent>;
get publicKey(): Promise<string>;
}
declare global {
interface Window {
eve: WindowEve;
}
}
----
// Registration Section
== Registration
Arxlets are registered using Nostr events with kind `30420`:
[source,json]
----
{
"kind": 30420,
"tags": [
["d", "my-calculator"],
["name", "Simple Calculator"],
["description", "A basic calculator for quick math"],
["script", "export function render(el) { /* your code */ }"],
["icon", "mdi:calculator", "#3b82f6"]
],
"content": "",
"created_at": 1735171200
}
----
=== Required Tags
* `d`: Unique identifier (alphanumeric, hyphens, underscores)
* `name`: Human-readable display name
* `script`: Complete JavaScript code with render export function
=== Optional Tags
* `description`: Brief description of functionality
* `icon`: Iconify icon name and hex color
// Development Patterns Section
== Development Patterns
=== Basic Arxlet Structure
[source,typescript]
----
/**
* Required export function - Entry point for your Arxlet
*/
export function render(container: HTMLElement): void {
// Initialize your application
container.innerHTML = `
<div class="p-6">
<h1 class="text-3xl font-bold mb-4">My Arxlet</h1>
<p class="text-lg">Hello from Eve!</p>
<button class="btn btn-primary mt-4" id="myButton">
Click me!
</button>
</div>
`;
// Add event listeners with proper typing
const button = container.querySelector<HTMLButtonElement>("#myButton");
button?.addEventListener("click", (): void => {
alert("Button clicked!");
});
// Your app logic here...
}
----
=== Real-time Updates
[source,typescript]
----
export function render(container: HTMLElement): void {
let subscription: Subscription;
// Set up UI
container.innerHTML = `<div id="events"></div>`;
const eventsContainer = container.querySelector("#events");
// Subscribe to real-time events
subscription = window.eve
.subscribeToEvents({
kinds: [1],
limit: 50,
})
.subscribe({
next: (event) => {
// Update UI with new event
const eventElement = document.createElement("div");
eventElement.textContent = event.content;
eventsContainer?.prepend(eventElement);
},
error: (err) => console.error("Subscription error:", err),
});
// Cleanup when arxlet is destroyed
window.addEventListener("beforeunload", () => {
subscription?.unsubscribe();
});
}
----
=== Publishing Events
[source,typescript]
----
import type { NostrEvent } from "./type-definitions.ts";
export async function render(container: HTMLElement): Promise<void> {
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
<div class="card-body">
<h2 class="card-title">📝 Publish a Note</h2>
<div class="form-control">
<label class="label">
<span class="label-text">What's on your mind?</span>
<span class="label-text-alt" id="charCount">0/280</span>
</label>
<textarea
class="textarea textarea-bordered h-32"
id="noteContent"
placeholder="Share your thoughts with your CCN..."
maxlength="280">
</textarea>
</div>
<div class="card-actions justify-between items-center">
<div id="status" class="flex-1"></div>
<button class="btn btn-primary" id="publishBtn" disabled>
Publish Note
</button>
</div>
</div>
</div>
`;
const textarea = container.querySelector<HTMLTextAreaElement>("#noteContent")!;
const publishBtn = container.querySelector<HTMLButtonElement>("#publishBtn")!;
const status = container.querySelector<HTMLDivElement>("#status")!;
const charCount = container.querySelector<HTMLSpanElement>("#charCount")!;
textarea.oninput = (): void => {
const length: number = textarea.value.length;
charCount.textContent = `${length}/280`;
publishBtn.disabled = length === 0 || length > 280;
};
publishBtn.onclick = async (e): Promise<void> => {
const content: string = textarea.value.trim();
if (!content) return;
publishBtn.disabled = true;
publishBtn.textContent = "Publishing...";
status.innerHTML = '<span class="loading loading-spinner loading-sm"></span>';
try {
const unsignedEvent: NostrEvent = {
kind: 1, // Text note
content: content,
tags: [["client", "arxlet-publisher"]],
created_at: Math.floor(Date.now() / 1000),
pubkey: await window.eve.publicKey,
};
const signedEvent: NostrEvent = await window.eve.signEvent(unsignedEvent);
await window.eve.publish(signedEvent);
status.innerHTML = `
<div class="alert alert-success">
<span>✅ Note published successfully!</span>
</div>
`;
textarea.value = "";
textarea.oninput?.(e);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.error("Publishing failed:", error);
status.innerHTML = `
<div class="alert alert-error">
<span>❌ Failed to publish: ${errorMessage}</span>
</div>
`;
} finally {
publishBtn.disabled = false;
publishBtn.textContent = "Publish Note";
}
};
}
----
// Best Practices Section
== Best Practices
=== Error Handling
- Always wrap API calls in try-catch blocks
- Check for null returns from query methods
- Provide user feedback for failed operations
=== Performance
- Use specific filters to limit result sets
- Cache profile data to avoid repeated lookups
- Unsubscribe from observables when done
- Debounce rapid API calls
- Consider pagination for large datasets
=== Security
- Validate all user inputs
- Sanitize content before displaying
- Use proper event signing for authenticity
- Follow principle of least privilege
=== Memory Management
- Always unsubscribe from RxJS observables
- Clean up event listeners on component destruction
- Avoid memory leaks in long-running subscriptions
- Use weak references where appropriate
// Common Use Cases Section
== Common Use Cases
=== Social Feed
- Subscribe to events from followed users
- Display real-time updates
- Handle profile information and avatars
- Implement engagement features
=== Publishing Tools
- Create and sign events
- Validate content before publishing
- Handle publishing errors gracefully
- Provide user feedback
=== Data Visualization
- Query historical events
- Process and aggregate data
- Create interactive charts and graphs
- Real-time data updates
=== Communication Apps
- Direct messaging interfaces
- Group chat functionality
- Notification systems
- Presence indicators
// Framework Integration Section
== Framework Integration
Arxlets support various JavaScript frameworks. All frameworks must export a `render` function that accepts a container element:
=== Vanilla JavaScript
[source,typescript]
----
export function render(container: HTMLElement) {
let count: number = 0;
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center">Counter App</h2>
<div class="text-6xl font-bold text-primary my-4" id="display">
${count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" id="decrement"></button>
<button class="btn btn-success" id="increment">+</button>
<button class="btn btn-ghost" id="reset">Reset</button>
</div>
</div>
</div>
`;
const display = container.querySelector<HTMLDivElement>("#display")!;
const incrementBtn = container.querySelector<HTMLButtonElement>("#increment")!;
const decrementBtn = container.querySelector<HTMLButtonElement>("#decrement")!;
const resetBtn = container.querySelector<HTMLButtonElement>("#reset")!;
const updateDisplay = (): void => {
display.textContent = count.toString();
display.className = `text-6xl font-bold my-4 ${
count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary"
}`;
};
incrementBtn.onclick = (): void => {
count++;
updateDisplay();
};
decrementBtn.onclick = (): void => {
count--;
updateDisplay();
};
resetBtn.onclick = (): void => {
count = 0;
updateDisplay();
};
}
----
=== Preact/React
[source,tsx]
----
// @jsx h
// @jsxImportSource preact
import { render as renderPreact } from "preact";
import { useState } from "preact/hooks";
const CounterApp = () => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("");
const increment = () => {
setCount((prev) => prev + 1);
setMessage(`Clicked ${count + 1} times!`);
};
const decrement = () => {
setCount((prev) => prev - 1);
setMessage(`Count decreased to ${count - 1}`);
};
const reset = () => {
setCount(0);
setMessage("Counter reset!");
};
return (
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center"> Preact Counter </h2>
<div
class={`text-6xl font-bold my-4 ${count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary"}`}
>
{count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" onClick={decrement}>
</button>
<button class="btn btn-success" onClick={increment}>
+
</button>
<button class="btn btn-ghost" onClick={reset}>
Reset
</button>
</div>
{message && (
<div class="alert alert-info mt-4">
<span>{message} </span>
</div>
)}
</div>
</div>
);
};
export function render(container: HTMLElement): void {
renderPreact(<CounterApp />, container);
}
----
=== Svelte
[source,svelte]
----
<script lang="ts">
let count = $state(0);
let message = $state("");
function increment() {
count += 1;
message = `Clicked ${count} times!`;
}
function decrement() {
count -= 1;
message = `Count decreased to ${count}`;
}
function reset() {
count = 0;
message = "Counter reset!";
}
const countColor = $derived(count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary");
</script>
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center">🔥 Svelte Counter</h2>
<div class="text-6xl font-bold my-4 {countColor}">
{count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" onclick={decrement}> </button>
<button class="btn btn-success" onclick={increment}> + </button>
<button class="btn btn-ghost" onclick={reset}> Reset </button>
</div>
{#if message}
<div class="alert alert-info mt-4">
<span>{message}</span>
</div>
{/if}
</div>
</div>
<style>
.card-title {
color: var(--primary);
}
</style>
----
=== Build Process
All frameworks require bundling into a single JavaScript file:
[source,bash]
----
# For TypeScript/JavaScript projects
bun build index.ts --outfile=build.js --minify --target=browser --production
# The resulting build.js content goes in your registration event's script tag
----
==== Svelte Build Requirements
IMPORTANT: The standard build command above will NOT work for Svelte projects. Svelte requires specific Vite configuration to compile properly.
For Svelte arxlets:
. Use the https://git.arx-ccn.com/Arx/arxlets-template[arxlets-template] which includes the correct Vite configuration
. Run `bun run build` instead of the standard build command
. Your compiled file will be available at `dist/bundle.js`
While the initial setup is more complex, Svelte provides an excellent development experience once configured, with features like:
- Built-in reactivity with runes (`$state()`, `$derived()`, etc.)
- Scoped CSS
- Compile-time optimizations
- No runtime overhead
// Debugging and Development Section
== Debugging and Development
=== Console Logging
- Use `console.log()` for debugging
- Events and errors are logged to browser console
=== Error Handling
- Catch and log API errors
- Display user-friendly error messages
- Implement retry mechanisms for transient failures
=== Testing
- Test with various event types and filters
- Verify subscription cleanup
- Test error scenarios and edge cases
- Validate event signing and publishing
// Limitations and Considerations Section
== Limitations and Considerations
=== Sandbox Restrictions
- Limited access to browser APIs
- No direct file system access
- Restricted network access (only to Eve relay)
- No access to parent window context
=== Performance Constraints
- Iframe overhead for each arxlet
- Memory usage for subscriptions
- Event processing limitations
=== Security Considerations
- All events are public on Nostr
- Private key management handled by Eve
- Content sanitization required
- XSS prevention necessary
// DaisyUI Components Section
== DaisyUI Components
Arxlets have access to DaisyUI 5, a comprehensive CSS component library. Use these pre-built components for consistent, accessible UI:
=== Essential Components
[source,html]
----
<!-- Cards for content containers -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Card Title</h2>
<p>Card content goes here</p>
<div class="card-actions justify-end">
<button class="btn btn-primary">Action</button>
</div>
</div>
</div>
<!-- Buttons with various styles -->
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-error">Error</button>
<button class="btn btn-ghost">Ghost</button>
<!-- Form controls -->
<div class="form-control">
<label class="label">
<span class="label-text">Input Label</span>
</label>
<input type="text" class="input input-bordered" placeholder="Enter text" />
</div>
<!-- Alerts for feedback -->
<div class="alert alert-success">
<span>✅ Success message</span>
</div>
<div class="alert alert-error">
<span>❌ Error message</span>
</div>
<!-- Loading states -->
<span class="loading loading-spinner loading-lg"></span>
<button class="btn btn-primary">
<span class="loading loading-spinner loading-sm"></span>
Loading...
</button>
<!-- Modals for dialogs -->
<dialog class="modal" id="my-modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Modal Title</h3>
<p class="py-4">Modal content</p>
<div class="modal-action">
<button class="btn" onclick="document.getElementById('my-modal').close()">
Close
</button>
</div>
</div>
</dialog>
----
=== Layout Utilities
[source,html]
----
<!-- Responsive grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="card">Content 1</div>
<div class="card">Content 2</div>
<div class="card">Content 3</div>
</div>
<!-- Flexbox utilities -->
<div class="flex justify-between items-center">
<span>Left content</span>
<button class="btn">Right button</button>
</div>
<!-- Spacing -->
<div class="p-4 m-2 space-y-4">
<!-- p-4 = padding, m-2 = margin, space-y-4 = vertical spacing -->
</div>
----
=== Color System
[source,html]
----
<!-- Background colors -->
<div class="bg-base-100">Default background</div>
<div class="bg-base-200">Slightly darker</div>
<div class="bg-primary">Primary color</div>
<div class="bg-secondary">Secondary color</div>
<!-- Text colors -->
<span class="text-primary">Primary text</span>
<span class="text-success">Success text</span>
<span class="text-error">Error text</span>
<span class="text-base-content">Default text</span>
----
// Complete Example Patterns Section
== Complete Example Patterns
=== Simple Counter Arxlet
[source,typescript]
----
export function render(container: HTMLElement) {
let count: number = 0;
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center">Counter App</h2>
<div class="text-6xl font-bold text-primary my-4" id="display">
${count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" id="decrement"></button>
<button class="btn btn-success" id="increment">+</button>
<button class="btn btn-ghost" id="reset">Reset</button>
</div>
</div>
</div>
`;
const display = container.querySelector<HTMLDivElement>("#display")!;
const incrementBtn = container.querySelector<HTMLButtonElement>("#increment")!;
const decrementBtn = container.querySelector<HTMLButtonElement>("#decrement")!;
const resetBtn = container.querySelector<HTMLButtonElement>("#reset")!;
const updateDisplay = (): void => {
display.textContent = count.toString();
display.className = `text-6xl font-bold my-4 ${
count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary"
}`;
};
incrementBtn.onclick = (): void => {
count++;
updateDisplay();
};
decrementBtn.onclick = (): void => {
count--;
updateDisplay();
};
resetBtn.onclick = (): void => {
count = 0;
updateDisplay();
};
}
----
=== Nostr Event Publisher
[source,typescript]
----
import type { NostrEvent } from "./type-definitions.ts";
export async function render(container: HTMLElement): Promise<void> {
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
<div class="card-body">
<h2 class="card-title">📝 Publish a Note</h2>
<div class="form-control">
<label class="label">
<span class="label-text">What's on your mind?</span>
<span class="label-text-alt" id="charCount">0/280</span>
</label>
<textarea
class="textarea textarea-bordered h-32"
id="noteContent"
placeholder="Share your thoughts with your CCN..."
maxlength="280">
</textarea>
</div>
<div class="card-actions justify-between items-center">
<div id="status" class="flex-1"></div>
<button class="btn btn-primary" id="publishBtn" disabled>
Publish Note
</button>
</div>
</div>
</div>
`;
const textarea = container.querySelector<HTMLTextAreaElement>("#noteContent")!;
const publishBtn = container.querySelector<HTMLButtonElement>("#publishBtn")!;
const status = container.querySelector<HTMLDivElement>("#status")!;
const charCount = container.querySelector<HTMLSpanElement>("#charCount")!;
textarea.oninput = (): void => {
const length: number = textarea.value.length;
charCount.textContent = `${length}/280`;
publishBtn.disabled = length === 0 || length > 280;
};
publishBtn.onclick = async (e): Promise<void> => {
const content: string = textarea.value.trim();
if (!content) return;
publishBtn.disabled = true;
publishBtn.textContent = "Publishing...";
status.innerHTML = '<span class="loading loading-spinner loading-sm"></span>';
try {
const unsignedEvent: NostrEvent = {
kind: 1, // Text note
content: content,
tags: [["client", "arxlet-publisher"]],
created_at: Math.floor(Date.now() / 1000),
pubkey: await window.eve.publicKey,
};
const signedEvent: NostrEvent = await window.eve.signEvent(unsignedEvent);
await window.eve.publish(signedEvent);
status.innerHTML = `
<div class="alert alert-success">
<span>✅ Note published successfully!</span>
</div>
`;
textarea.value = "";
textarea.oninput?.(e);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.error("Publishing failed:", error);
status.innerHTML = `
<div class="alert alert-error">
<span>❌ Failed to publish: ${errorMessage}</span>
</div>
`;
} finally {
publishBtn.disabled = false;
publishBtn.textContent = "Publish Note";
}
};
}
----

View file

@ -0,0 +1,746 @@
@import url("https://esm.sh/prismjs/themes/prism-tomorrow.css");
html {
scroll-behavior: smooth;
}
* {
transition: all 0.2s ease;
}
code[class*="language-"],
pre[class*="language-"] {
font-family: "Fira Code", "Monaco", "Cascadia Code", "Roboto Mono", monospace;
font-size: 0.875rem;
line-height: 1.5;
}
kbd {
border-radius: 25%;
color: white;
margin-right: 2px;
}
.mockup-code pre::before {
display: none;
}
/* Next-Level Sidebar Styling */
.sidebar-container {
background: linear-gradient(180deg, hsl(var(--b2)) 0%, hsl(var(--b1)) 100%);
border-right: 1px solid hsl(var(--b3));
position: relative;
overflow: hidden;
}
.sidebar-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, hsl(var(--p) / 0.5), transparent);
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
/* Global Progress Bar */
.global-progress-container {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 2px;
background: hsl(var(--b3) / 0.1);
z-index: 9999;
backdrop-filter: blur(10px);
}
.global-progress-bar {
height: 100%;
background: cyan;
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 0 12px hsl(var(--p) / 0.5),
0 1px 3px hsl(var(--p) / 0.3);
position: relative;
border-radius: 0 1px 1px 0;
}
.global-progress-bar::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, hsl(var(--p) / 0.8) 0%, hsl(var(--s) / 0.9) 50%, hsl(var(--a) / 0.8) 100%);
animation: shimmer-progress 2s ease-in-out infinite;
}
.global-progress-bar::after {
content: "";
position: absolute;
top: -1px;
right: -2px;
width: 4px;
height: 4px;
background: hsl(var(--pc));
border-radius: 50%;
box-shadow: 0 0 6px hsl(var(--pc) / 0.8);
animation: pulse-dot 1.5s ease-in-out infinite;
}
@keyframes shimmer-progress {
0%,
100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
@keyframes pulse-dot {
0%,
100% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.2);
opacity: 1;
}
}
/* Ensure content doesn't get hidden behind progress bar */
body {
padding-top: 2px;
}
/* Enhanced Header */
.sidebar-header {
background: hsl(var(--b1));
backdrop-filter: blur(10px);
border-bottom: 1px solid hsl(var(--b3) / 0.5);
position: relative;
}
.header-icon {
position: relative;
overflow: hidden;
}
.header-icon::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, hsl(var(--pc) / 0.1), transparent);
animation: rotate 3s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Navigation Container */
.navigation-container {
position: relative;
}
.navigation-container.scrolling {
background: hsl(var(--b2) / 0.95);
}
.drawer-side .menu {
padding: 1rem;
gap: 0.5rem;
}
/* Navigation Items */
.nav-item {
animation: slideInLeft 0.6s ease-out;
animation-delay: calc(var(--item-index) * 0.1s);
animation-fill-mode: both;
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Section Links */
.section-link {
font-weight: 600;
font-size: 0.95rem;
padding: 1rem;
border-radius: 0.75rem;
margin-bottom: 0.25rem;
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 0.75rem;
overflow: hidden;
backdrop-filter: blur(5px);
}
.section-link::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, hsl(var(--p) / 0.1), transparent);
transition: left 0.5s ease;
}
.section-link:hover::before {
left: 100%;
}
.section-link:hover {
background: hsl(var(--b3) / 0.7);
transform: translateX(4px) scale(1.02);
box-shadow: 0 8px 25px hsl(var(--b3) / 0.3);
}
.section-link.active {
background: linear-gradient(135deg, hsl(var(--p)) 0%, hsl(var(--s)) 100%);
color: hsl(var(--pc));
font-weight: 700;
box-shadow:
0 8px 25px hsl(var(--p) / 0.4),
0 0 0 1px hsl(var(--p) / 0.2),
inset 0 1px 0 hsl(var(--pc) / 0.1);
transform: translateX(6px);
}
.section-link.active::after {
content: "";
position: absolute;
left: -1rem;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 70%;
background: linear-gradient(180deg, hsl(var(--p)), hsl(var(--s)));
border-radius: 2px;
box-shadow: 0 0 10px hsl(var(--p) / 0.5);
}
/* Section Icon Container */
.section-icon-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
}
.section-icon {
font-size: 1.1rem;
transition: all 0.3s ease;
position: relative;
z-index: 2;
}
.icon-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
background: radial-gradient(circle, hsl(var(--p) / 0.2) 0%, transparent 70%);
border-radius: 50%;
opacity: 0;
transition: opacity 0.3s ease;
}
.section-link.active .icon-glow {
opacity: 1;
animation: pulse-glow 2s ease-in-out infinite;
}
@keyframes pulse-glow {
0%,
100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.2;
}
50% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0.4;
}
}
.section-link.active .section-icon {
transform: scale(1.1);
filter: drop-shadow(0 0 8px hsl(var(--pc) / 0.5));
}
/* Section Text */
.section-text {
flex: 1;
transition: all 0.3s ease;
}
.section-link.active .section-text {
text-shadow: 0 0 10px hsl(var(--pc) / 0.3);
}
/* Section Badge */
.section-badge {
display: flex;
align-items: center;
}
.subsection-count {
background: hsl(var(--b3) / 0.5);
color: hsl(var(--bc) / 0.7);
font-size: 0.75rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 1rem;
min-width: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.section-link.active .subsection-count {
background: hsl(var(--pc) / 0.2);
color: hsl(var(--pc));
box-shadow: 0 0 10px hsl(var(--pc) / 0.3);
}
/* Enhanced Subsection Styling */
.subsection-menu {
margin-left: 1rem;
margin-top: 0.75rem;
padding-left: 1.5rem;
border-left: 2px solid hsl(var(--b3) / 0.3);
gap: 0.25rem;
position: relative;
animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.subsection-menu::before {
content: "";
position: absolute;
left: -2px;
top: 0;
width: 2px;
height: 100%;
background: linear-gradient(180deg, hsl(var(--p) / 0.5), transparent);
transform: scaleY(0);
transform-origin: top;
animation: expandLine 0.6s ease-out 0.2s forwards;
}
@keyframes expandLine {
to {
transform: scaleY(1);
}
}
.subsection-menu li {
animation: slideInRight 0.4s ease-out;
animation-delay: calc(var(--sub-index) * 0.05s + 0.1s);
animation-fill-mode: both;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(-15px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.subsection-link {
font-size: 0.875rem;
font-weight: 500;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
color: hsl(var(--bc) / 0.7);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.125rem;
}
.subsection-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: hsl(var(--bc) / 0.3);
transition: all 0.3s ease;
position: relative;
}
.subsection-dot::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 0;
height: 0;
background: hsl(var(--p));
border-radius: 50%;
transition: all 0.3s ease;
}
.subsection-text {
flex: 1;
transition: all 0.3s ease;
}
.subsection-link:hover {
background: hsl(var(--b3) / 0.4);
color: hsl(var(--bc));
transform: translateX(4px);
box-shadow: 0 4px 12px hsl(var(--b3) / 0.2);
}
.subsection-link:hover .subsection-dot {
background: hsl(var(--p) / 0.7);
transform: scale(1.2);
}
.subsection-link:hover .subsection-dot::after {
width: 12px;
height: 12px;
}
.subsection-link.active {
background: linear-gradient(135deg, hsl(var(--p) / 0.15), hsl(var(--s) / 0.1));
color: hsl(var(--p));
font-weight: 600;
transform: translateX(6px);
box-shadow:
0 4px 15px hsl(var(--p) / 0.2),
inset 0 1px 0 hsl(var(--p) / 0.1);
border-left: 3px solid hsl(var(--p));
padding-left: calc(1rem - 3px);
}
.subsection-link.active .subsection-dot {
background: hsl(var(--p));
transform: scale(1.3);
box-shadow: 0 0 10px hsl(var(--p) / 0.5);
}
.subsection-link.active .subsection-dot::after {
width: 16px;
height: 16px;
background: hsl(var(--p) / 0.3);
animation: ripple 1.5s ease-out infinite;
}
@keyframes ripple {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(2);
opacity: 0;
}
}
.subsection-link.active .subsection-text {
text-shadow: 0 0 8px hsl(var(--p) / 0.3);
}
/* Smooth animations for section changes */
.drawer-side .menu ul {
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Enhanced scrollbar for sidebar */
.drawer-side .menu {
scrollbar-width: thin;
scrollbar-color: hsl(var(--p) / 0.3) transparent;
}
.drawer-side .menu::-webkit-scrollbar {
width: 6px;
}
.drawer-side .menu::-webkit-scrollbar-track {
background: transparent;
}
.drawer-side .menu::-webkit-scrollbar-thumb {
background: hsl(var(--p) / 0.3);
border-radius: 3px;
}
.drawer-side .menu::-webkit-scrollbar-thumb:hover {
background: hsl(var(--p) / 0.5);
}
/* Enhance
d Sidebar Structure */
.drawer-side aside {
background: hsl(var(--b2));
border-right: 1px solid hsl(var(--b3));
}
/* Section Links with Icons */
.section-link {
display: flex;
align-items: center;
gap: 0.75rem;
}
.section-icon {
font-size: 1.1rem;
opacity: 0.8;
transition: all 0.2s ease;
}
.section-link.active .section-icon {
opacity: 1;
transform: scale(1.1);
}
/* Subsection Menu */
.subsection-menu {
margin-left: 0.5rem;
margin-top: 0.5rem;
padding-left: 1rem;
border-left: 2px solid hsl(var(--b3));
gap: 0.125rem;
animation: slideDown 0.3s ease-out;
}
.subsection-link {
font-size: 0.875rem;
font-weight: 500;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
color: hsl(var(--bc) / 0.7);
transition: all 0.2s ease;
position: relative;
display: block;
}
.subsection-link:hover {
background: hsl(var(--b3) / 0.5);
color: hsl(var(--bc));
transform: translateX(2px);
}
.subsection-link.active {
background: hsl(var(--p) / 0.1);
color: hsl(var(--p));
font-weight: 600;
border-left: 3px solid hsl(var(--p));
padding-left: calc(0.75rem - 3px);
}
.subsection-link.active::before {
content: "▶";
position: absolute;
left: -1.25rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.625rem;
color: hsl(var(--p));
}
/* Enhanced scrollbar for navigation */
.drawer-side nav {
scrollbar-width: thin;
scrollbar-color: hsl(var(--p) / 0.3) transparent;
}
.drawer-side nav::-webkit-scrollbar {
width: 6px;
}
.drawer-side nav::-webkit-scrollbar-track {
background: transparent;
}
.drawer-side nav::-webkit-scrollbar-thumb {
background: hsl(var(--p) / 0.3);
border-radius: 3px;
}
.drawer-side nav::-webkit-scrollbar-thumb:hover {
background: hsl(var(--p) / 0.5);
}
/* Enhanced Footer */
.sidebar-footer {
background: linear-gradient(180deg, hsl(var(--b1)) 0%, hsl(var(--b2)) 100%);
border-top: 1px solid hsl(var(--b3) / 0.5);
backdrop-filter: blur(10px);
}
/* Progress Dots */
.progress-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: hsl(var(--bc) / 0.2);
transition: all 0.3s ease;
position: relative;
}
.progress-dot.active {
background: hsl(var(--p));
box-shadow: 0 0 10px hsl(var(--p) / 0.5);
transform: scale(1.2);
}
.progress-dot.active::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border: 2px solid hsl(var(--p) / 0.3);
border-radius: 50%;
animation: expand-ring 2s ease-out infinite;
}
@keyframes expand-ring {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0;
}
}
/* Enhanced Animations */
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
/* Smooth Transitions */
* {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Glassmorphism Effects */
.sidebar-header,
.sidebar-footer {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
/* Hover Glow Effects */
.section-link:hover,
.subsection-link:hover {
position: relative;
}
.section-link:hover::after {
content: "";
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, hsl(var(--p) / 0.1), hsl(var(--s) / 0.1));
border-radius: inherit;
z-index: -1;
filter: blur(4px);
}
/* Responsive Enhancements */
@media (max-width: 768px) {
.sidebar-container {
width: 100%;
}
.section-link {
padding: 0.75rem;
}
.subsection-link {
padding: 0.5rem 0.75rem;
}
}
/* Dark Mode Optimizations */
@media (prefers-color-scheme: dark) {
.sidebar-container {
background: linear-gradient(180deg, hsl(var(--b2)) 0%, hsl(220 13% 9%) 100%);
}
.progress-bar {
box-shadow: 0 0 20px hsl(var(--p) / 0.4);
}
}

View file

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arxlets Documentation - Eve</title>
<link rel="stylesheet" href="./arxlet-docs.css" />
<link
href="https://cdn.jsdelivr.net/npm/daisyui@5"
rel="stylesheet"
type="text/css"
/>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link
href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css"
rel="stylesheet"
type="text/css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
</head>
<body data-theme="dark" class="bg-base-100 text-base-content">
<script src="./arxlet-docs.jsx"></script>
</body>
</html>

View file

@ -0,0 +1,343 @@
// @jsx h
// @jsxImportSource preact
import { render } from "preact";
import { useEffect, useState } from "preact/hooks";
import "./arxlet-docs.css";
import { APISection } from "./components/APISection.jsx";
import { BestPracticesSection } from "./components/BestPracticesSection.jsx";
import { DevelopmentSection } from "./components/DevelopmentSection.jsx";
import { ExamplesSection } from "./components/ExamplesSection.jsx";
import { LLMsSection } from "./components/LLMsSection.jsx";
import { OverviewSection } from "./components/OverviewSection.jsx";
import { RegistrationSection } from "./components/RegistrationSection.jsx";
import { useSyntaxHighlighting } from "./hooks/useSyntaxHighlighting.js";
const SECTIONS = {
Overview: {
component: <OverviewSection />,
subsections: {},
},
Registration: {
component: <RegistrationSection />,
subsections: {
"nostr-event-structure": "Nostr Event Structure",
"tag-reference": "Tag Reference",
},
},
Development: {
component: <DevelopmentSection />,
subsections: {
"understanding-arxlets": "Understanding the Arxlet Environment",
"nostr-vs-arxlets": "Nostr Apps vs Arxlets",
"available-apis": "Available APIs",
"security-restrictions": "Security Restrictions",
"typescript-development": "TypeScript Development",
"required-export-function": "Required Export Function",
},
},
"Best Practices": {
component: <BestPracticesSection />,
subsections: {
"error-handling": "Error Handling & Reliability",
performance: "Performance & Efficiency",
subscriptions: "Subscription Management",
"user-experience": "User Experience Excellence",
security: "Security & Privacy",
"production-checklist": "Production Readiness",
},
},
"API Reference": {
component: <APISection />,
subsections: {
"window-eve-api": "window.eve API",
"real-time-subscriptions": "Real-time Subscriptions",
"websocket-alternative": "WebSocket Alternative",
"best-practices": "Best Practices",
},
},
Examples: {
component: <ExamplesSection />,
subsections: {
vanilla: "Vanilla JS",
svelte: "Svelte",
preact: "Preact + JSX",
nostr: "Nostr Publisher",
},
},
LLMs: {
component: <LLMsSection />,
subsections: {},
},
};
const ArxletDocs = () => {
useSyntaxHighlighting();
const [activeSection, setActiveSection] = useState("Overview");
const [activeExample, setActiveExample] = useState("vanilla");
const [activeSubsection, setActiveSubsection] = useState("");
const [scrollProgress, setScrollProgress] = useState(0);
const [isScrolling, setIsScrolling] = useState(false);
useEffect(() => {
const handleHashChange = () => {
const hash = window.location.hash.substring(1);
if (hash) {
setActiveExample(hash);
}
};
window.addEventListener("hashchange", handleHashChange);
handleHashChange();
return () => {
window.removeEventListener("hashchange", handleHashChange);
};
}, []);
// Enhanced Intersection Observer to track which subsection is currently visible
useEffect(() => {
const observerOptions = {
root: null,
rootMargin: "-10% 0px -60% 0px", // More responsive triggering
threshold: [0, 0.1, 0.5, 1.0], // Multiple thresholds for better detection
};
const visibleSections = new Set();
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
visibleSections.add(entry.target.id);
} else {
visibleSections.delete(entry.target.id);
}
});
// Set the active subsection to the first visible one (topmost)
if (visibleSections.size > 0) {
const currentSectionData = SECTIONS[activeSection];
if (currentSectionData?.subsections) {
const subsectionIds = Object.keys(currentSectionData.subsections);
const firstVisible = subsectionIds.find((id) => visibleSections.has(id));
if (firstVisible) {
setActiveSubsection(firstVisible);
}
}
}
}, observerOptions);
// Get all subsection IDs from the current section
const currentSectionData = SECTIONS[activeSection];
if (currentSectionData?.subsections) {
const subsectionIds = Object.keys(currentSectionData.subsections);
// Clear previous observations
visibleSections.clear();
// Observe elements with these IDs
subsectionIds.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.observe(element);
}
});
return () => {
subsectionIds.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.unobserve(element);
}
});
visibleSections.clear();
};
}
}, [activeSection]);
// Scroll progress tracking
useEffect(() => {
let scrollTimeout;
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = Math.min((scrollTop / docHeight) * 100, 100);
setScrollProgress(progress);
setIsScrolling(true);
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
setIsScrolling(false);
}, 150);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
clearTimeout(scrollTimeout);
};
}, []);
useEffect(() => {
window.scrollTo(0, 0);
}, [activeSection, activeExample]);
const renderContent = () => {
const section = SECTIONS[activeSection];
if (!section) return <div>Section not found</div>;
if (activeSection === "Examples") {
return <ExamplesSection activeExample={activeExample} />;
}
return section.component;
};
const handleNavClick = (e, section) => {
e.preventDefault();
setActiveSection(section);
window.location.hash = "";
};
const handleSubNavClick = (e, subsectionId) => {
e.preventDefault();
if (activeSection === "Examples") {
window.location.hash = subsectionId;
} else {
document.getElementById(subsectionId)?.scrollIntoView({ behavior: "smooth" });
}
};
return (
<div class="drawer drawer-open" data-theme="dark">
{/* Global Progress Bar */}
<div class="global-progress-container">
<div class="global-progress-bar" style={`width: ${scrollProgress}%`}></div>
</div>
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col p-8">
{/* Header */}
<header class="mb-12">
<div class="flex items-center gap-4 mb-6">
<div class="w-16 h-16 bg-gradient-to-br from-primary to-secondary rounded-xl flex items-center justify-center shadow-lg">
<span class="text-2xl">🚀</span>
</div>
<div>
<h1 class="text-5xl font-bold">Arxlets</h1>
<p class="text-xl text-base-content/70 mt-2">Secure Applications for Eve</p>
</div>
</div>
<div class="divider"></div>
</header>
{renderContent()}
{/* Footer */}
<footer class="mt-16 pt-8 border-t border-base-300">
<div class="text-center text-base-content/60">
<p class="text-lg">Arxlets Documentation Eve</p>
<p class="text-sm mt-2">Build secure, sandboxed applications for your CCN</p>
</div>
</footer>
</div>
<div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<aside class="sidebar-container w-80 min-h-full bg-base-200 flex flex-col">
{/* Sidebar Header */}
<div class="sidebar-header p-4 border-b border-base-300">
<div class="flex items-center gap-3">
<div class="header-icon w-8 h-8 bg-gradient-to-br from-primary to-secondary rounded-lg flex items-center justify-center">
<span class="text-sm">📚</span>
</div>
<div>
<h2 class="font-bold text-lg">Documentation</h2>
<p class="text-xs text-base-content/60">Navigate sections</p>
</div>
</div>
</div>
{/* Navigation Menu */}
<nav class={`navigation-container flex-1 overflow-y-auto ${isScrolling ? "scrolling" : ""}`}>
<ul class="menu p-4 w-full text-base-content">
{Object.entries(SECTIONS).map(([section, { subsections }], index) => (
<li key={section} class="nav-item" style={`--item-index: ${index}`}>
<button
type="button"
class={`${activeSection === section ? "active" : ""} section-link`}
onClick={(e) => handleNavClick(e, section)}
>
<span class="section-icon-container">
<span class="section-icon">
{section === "Overview" && "🏠"}
{section === "Registration" && "📝"}
{section === "Development" && "⚡"}
{section === "Best Practices" && "✨"}
{section === "API Reference" && "🔧"}
{section === "Examples" && "💡"}
{section === "LLMs" && "🤖"}
</span>
<span class="icon-glow"></span>
</span>
<span class="section-text">{section}</span>
<span class="section-badge">
{Object.keys(subsections).length > 0 && (
<span class="subsection-count">{Object.keys(subsections).length}</span>
)}
</span>
</button>
{activeSection === section && Object.keys(subsections).length > 0 && (
<ul class="subsection-menu">
{Object.entries(subsections).map(([subsectionId, subsectionLabel], subIndex) => (
<li key={subsectionId} style={`--sub-index: ${subIndex}`}>
<a
href={`#${subsectionId}`}
class={`subsection-link ${
activeSection === "Examples"
? activeExample === subsectionId
? "active"
: ""
: activeSubsection === subsectionId
? "active"
: ""
}`}
onClick={(e) => handleSubNavClick(e, subsectionId)}
>
<span class="subsection-dot"></span>
<span class="subsection-text">{subsectionLabel}</span>
</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
{/* Sidebar Footer */}
<div class="sidebar-footer p-4 border-t border-base-300">
<div class="text-center space-y-2">
<div class="flex items-center justify-center gap-2">
<div class="w-2 h-2 rounded-full bg-success animate-pulse"></div>
<p class="text-xs text-base-content/50">Arxlets v0.1b</p>
</div>
<div class="flex justify-center gap-1">
{Object.keys(SECTIONS).map((_, index) => (
<div
class={`progress-dot ${index <= Object.keys(SECTIONS).findIndex(([key]) => key === activeSection) ? "active" : ""}`}
></div>
))}
</div>
</div>
</div>
</aside>
</div>
</div>
);
};
render(<ArxletDocs />, document.body);

View file

@ -0,0 +1,678 @@
// @jsx h
// @jsxImportSource preact
import eveApiExample from "../highlight/eve-api-example.ts" with { type: "text" };
import subscriptionExamples from "../highlight/subscription-examples.ts" with { type: "text" };
import websocketExample from "../highlight/websocket-example.ts" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
import { CodeBlock } from "./CodeBlock.jsx";
/**
* API Section - Comprehensive guide to available APIs
* Covers window.eve API and WebSocket alternatives
*/
export const APISection = () => {
useSyntaxHighlighting();
return (
<div class="space-y-6">
<h2 class="text-3xl font-bold">API Reference</h2>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-info mb-4">Understanding Arxlet APIs</h3>
<div class="space-y-4">
<p>
Your Arxlet has access to powerful APIs that let you interact with Nostr data, manage user profiles, and
create real-time applications. Think of these APIs as your toolkit for building social, decentralized
applications within the CCN ecosystem.
</p>
<p>
<strong>Two approaches available:</strong> You can use the convenient <code>window.eve</code> API
(recommended for most cases) or connect directly via WebSocket for advanced scenarios. Both give you full
access to Nostr events and CCN features.
</p>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-primary mb-4">🎯 Which API Should You Use?</h3>
<div class="grid md:grid-cols-2 gap-6">
<div class="border-2 border-primary rounded-lg p-4">
<h4 class="font-bold text-primary mb-3"> window.eve API (Recommended)</h4>
<p class="text-sm mb-3">
<strong>Best for most Arxlets.</strong> This high-level API handles all the complex Nostr protocol
details for you.
</p>
<div class="space-y-2 text-sm">
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Simple promise-based functions</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Automatic error handling</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Built-in RxJS observables for real-time data</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Profile and avatar helpers</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Perfect for beginners</span>
</div>
</div>
</div>
<div class="border-2 border-accent rounded-lg p-4">
<h4 class="font-bold text-accent mb-3"> Direct WebSocket</h4>
<p class="text-sm mb-3">
<strong>For advanced use cases.</strong> Direct connection to the Nostr relay with full protocol
control.
</p>
<div class="space-y-2 text-sm">
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Maximum performance and control</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Custom subscription management</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Raw Nostr protocol access</span>
</div>
<div class="flex items-start gap-2">
<span class="text-warning">!</span>
<span>Requires Nostr protocol knowledge</span>
</div>
<div class="flex items-start gap-2">
<span class="text-warning">!</span>
<span>More complex error handling</span>
</div>
</div>
</div>
</div>
<div class="alert alert-info mt-6">
<span>
💡 <strong>Our Recommendation:</strong> Start with <code>window.eve</code> for your first Arxlet. You can
always switch to WebSocket later if you need more control or performance.
</span>
</div>
</div>
</div>
{/* window.eve API */}
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="window-eve-api" class="card-title text-primary mb-4">
🚀 window.eve API - Your Main Toolkit
</h3>
<div class="space-y-4 mb-6">
<p>
The <code>window.eve</code> API is your primary interface for working with Nostr data in Arxlets. It
provides simple, promise-based functions that handle all the complex protocol details behind the scenes.
</p>
<p>
<strong>How it works:</strong> Each function communicates with the local Nostr relay, processes the
results, and returns clean JavaScript objects. No need to understand Nostr protocol internals - just call
the functions and get your data.
</p>
</div>
<div class="space-y-6">
<div>
<h4 class="font-bold text-lg mb-4 text-success">📤 Publishing & Writing Data</h4>
<div class="space-y-4">
<div class="border-l-4 border-success pl-4">
<h5 class="font-bold">publish(event)</h5>
<p class="text-sm opacity-80 mb-2">
Publishes a Nostr event to the relay. This is how you save data, post messages, or create any
content.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Posting messages, saving user preferences, creating notes, updating
profiles
</p>
<div class="badge badge-outline">Promise&lt;void&gt;</div>
</div>
<div class="border-l-4 border-success pl-4">
<h5 class="font-bold">signEvent(event)</h5>
<p class="text-sm opacity-80 mb-2">
Signs an unsigned Nostr event with the user's private key. Required before publishing most events.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Preparing events for publication, authenticating user actions
</p>
<div class="badge badge-outline">Promise&lt;NostrEvent&gt;</div>
</div>
</div>
</div>
<div>
<h4 class="font-bold text-lg mb-4 text-info">🔍 Reading & Querying Data</h4>
<div class="space-y-4">
<div class="border-l-4 border-info pl-4">
<h5 class="font-bold">getSingleEventById(id)</h5>
<p class="text-sm opacity-80 mb-2">
Retrieves a specific event when you know its exact ID. Perfect for loading specific posts or data.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Loading a specific message, fetching referenced content, getting event
details
</p>
<div class="badge badge-outline">Promise&lt;NostrEvent | null&gt;</div>
</div>
<div class="border-l-4 border-info pl-4">
<h5 class="font-bold">getSingleEventWithFilter(filter)</h5>
<p class="text-sm opacity-80 mb-2">
Gets the first event matching your criteria. Useful when you expect only one result or want the most
recent.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Getting a user's latest profile, finding the most recent post, checking
if something exists
</p>
<div class="badge badge-outline">Promise&lt;NostrEvent | null&gt;</div>
</div>
<div class="border-l-4 border-info pl-4">
<h5 class="font-bold">getAllEventsWithFilter(filter)</h5>
<p class="text-sm opacity-80 mb-2">
Gets all events matching your criteria. Use this for lists, feeds, or when you need multiple
results.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Building feeds, loading message history, getting all posts by a user
</p>
<div class="badge badge-outline">Promise&lt;NostrEvent[]&gt;</div>
</div>
</div>
</div>
<div>
<h4 class="font-bold text-lg mb-4 text-accent">🔄 Real-time Subscriptions</h4>
<div class="space-y-4">
<div class="border-l-4 border-accent pl-4">
<h5 class="font-bold">subscribeToEvents(filter)</h5>
<p class="text-sm opacity-80 mb-2">
Creates a live stream of events matching your filter. Your app updates automatically when new events
arrive.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Live chat, real-time feeds, notifications, collaborative features
</p>
<div class="badge badge-outline">Observable&lt;NostrEvent&gt;</div>
</div>
<div class="border-l-4 border-accent pl-4">
<h5 class="font-bold">subscribeToProfile(pubkey)</h5>
<p class="text-sm opacity-80 mb-2">
Watches for profile changes for a specific user. Updates automatically when they change their name,
bio, avatar, etc.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> User profile displays, contact lists, member directories
</p>
<div class="badge badge-outline">Observable&lt;Profile&gt;</div>
</div>
</div>
</div>
<div>
<h4 class="font-bold text-lg mb-4 text-warning">👤 User & Profile Helpers</h4>
<div class="space-y-4">
<div class="border-l-4 border-warning pl-4">
<h5 class="font-bold">getProfile(pubkey)</h5>
<p class="text-sm opacity-80 mb-2">
Retrieves user profile information (name, bio, avatar, etc.) for any user by their public key.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Displaying user info, building contact lists, showing message authors
</p>
<div class="badge badge-outline">Promise&lt;Profile | null&gt;</div>
</div>
<div class="border-l-4 border-warning pl-4">
<h5 class="font-bold">getAvatar(pubkey)</h5>
<p class="text-sm opacity-80 mb-2">
Quick helper to get just the avatar URL from a user's profile. Saves you from parsing the full
profile.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Profile pictures, user avatars in lists, message author images
</p>
<div class="badge badge-outline">Promise&lt;string | null&gt;</div>
</div>
<div class="border-l-4 border-warning pl-4">
<h5 class="font-bold">publicKey</h5>
<p class="text-sm opacity-80 mb-2">
Gets the current user's public key. This identifies the user and is needed for many operations.
</p>
<p class="text-sm mb-2">
<strong>Use cases:</strong> Identifying the current user, filtering their content, permission checks
</p>
<div class="badge badge-outline">Promise&lt;string&gt;</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<h4 class="font-semibold mb-3 text-lg">Practical Example:</h4>
<p class="mb-3 text-sm">
Here's how these functions work together in a real Arxlet. This example shows fetching events, displaying
user profiles, and handling real-time updates:
</p>
<CodeBlock language="typescript" code={eveApiExample} />
</div>
</div>
</div>
{/* Real-time Subscriptions */}
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="real-time-subscriptions" class="card-title text-accent mb-4">
🔄 Understanding Real-time Subscriptions
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>What are subscriptions?</strong> Think of subscriptions as "live feeds" that automatically notify
your Arxlet when new data arrives. Instead of repeatedly asking "is there new data?", subscriptions push
updates to you instantly.
</p>
<p>
<strong>How they work:</strong> When you subscribe to events or profiles, you get an RxJS Observable - a
stream of data that flows over time. Your Arxlet can "listen" to this stream and update the UI whenever
new data arrives.
</p>
<p>
<strong>Why use them?</strong> Subscriptions make your Arxlet feel alive and responsive. Users see new
messages instantly, profile changes update immediately, and collaborative features work in real-time.
</p>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div class="border-2 border-accent rounded-lg p-4">
<h4 class="font-bold text-accent mb-3">🎯 Event Subscriptions</h4>
<p class="text-sm mb-3">
<code>subscribeToEvents(filter)</code> gives you a live stream of events matching your criteria.
</p>
<div class="space-y-2 text-sm">
<div>
<strong>Perfect for:</strong>
</div>
<ul class="list-disc list-inside space-y-1 ml-2">
<li>Live chat applications</li>
<li>Real-time feeds and timelines</li>
<li>Notification systems</li>
<li>Collaborative tools</li>
<li>Activity monitoring</li>
</ul>
</div>
</div>
<div class="border-2 border-warning rounded-lg p-4">
<h4 class="font-bold text-warning mb-3">👤 Profile Subscriptions</h4>
<p class="text-sm mb-3">
<code>subscribeToProfile(pubkey)</code> watches for changes to a specific user's profile.
</p>
<div class="space-y-2 text-sm">
<div>
<strong>Perfect for:</strong>
</div>
<ul class="list-disc list-inside space-y-1 ml-2">
<li>User profile displays</li>
<li>Contact lists that stay current</li>
<li>Member directories</li>
<li>Avatar/name displays</li>
<li>User status indicators</li>
</ul>
</div>
</div>
</div>
<div class="mb-6">
<h4 class="font-semibold mb-3 text-lg">How to Use Subscriptions:</h4>
<p class="mb-3 text-sm">
Here's a complete example showing how to set up subscriptions, handle incoming data, and clean up
properly:
</p>
<CodeBlock language="typescript" code={subscriptionExamples} />
</div>
<div class="grid md:grid-cols-2 gap-4">
<div class="alert alert-warning">
<div>
<h4 class="font-bold mb-2">! Memory Management</h4>
<div class="text-sm space-y-1">
<p>
Always call <code>unsubscribe()</code> when:
</p>
<ul class="list-disc list-inside ml-2">
<li>Your component unmounts</li>
<li>User navigates away</li>
<li>You no longer need the data</li>
<li>Your Arxlet is closing</li>
</ul>
<p class="mt-2">
<strong>Why?</strong> Prevents memory leaks and unnecessary disk i/o.
</p>
</div>
</div>
</div>
<div class="alert alert-success">
<div>
<h4 class="font-bold mb-2"> Pro Tips</h4>
<div class="text-sm space-y-1">
<ul class="list-disc list-inside">
<li>Use specific filters to reduce data volume</li>
<li>Debounce rapid updates for better UX</li>
<li>Cache data to avoid duplicate processing</li>
<li>
Handle errors gracefully with <code>catchError</code>
</li>
<li>
Consider using <code>takeUntil</code> for automatic cleanup
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{/* WebSocket Alternative */}
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="websocket-alternative" class="card-title text-accent mb-4">
🔌 Direct WebSocket Connection - Advanced Usage
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>What is the WebSocket approach?</strong> Instead of using the convenient <code>window.eve</code>{" "}
API, you can connect directly to the Nostr relay at <code>ws://localhost:6942</code> and speak the raw
Nostr protocol.
</p>
<p>
<strong>Why would you use this?</strong> Direct WebSocket gives you maximum control and performance. You
can implement custom subscription logic, handle multiple concurrent subscriptions efficiently, or
integrate with existing Nostr libraries.
</p>
<p>
<strong>The trade-off:</strong> You'll need to understand the Nostr protocol, handle JSON message parsing,
manage connection states, and implement your own error handling. It's more work but gives you complete
flexibility.
</p>
</div>
<div class="mb-6">
<h4 class="font-semibold mb-3 text-lg">WebSocket Implementation Example:</h4>
<p class="mb-3 text-sm">
Here's how to establish a WebSocket connection and communicate using standard Nostr protocol messages:
</p>
<CodeBlock language="typescript" code={websocketExample} />
</div>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div class="border-2 border-success rounded-lg p-4">
<h4 class="font-bold text-success mb-3"> Use window.eve When:</h4>
<div class="space-y-2 text-sm">
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Building your first Arxlet</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>You want simple, clean code</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Standard CRUD operations are enough</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>You prefer promise-based APIs</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>Built-in RxJS observables work for you</span>
</div>
<div class="flex items-start gap-2">
<span class="text-success"></span>
<span>You don't need custom protocol handling</span>
</div>
</div>
</div>
<div class="border-2 border-accent rounded-lg p-4">
<h4 class="font-bold text-accent mb-3"> Use WebSocket When:</h4>
<div class="space-y-2 text-sm">
<div class="flex items-start gap-2">
<span class="text-accent"></span>
<span>You need maximum performance</span>
</div>
<div class="flex items-start gap-2">
<span class="text-accent"></span>
<span>Custom subscription management required</span>
</div>
<div class="flex items-start gap-2">
<span class="text-accent"></span>
<span>Integrating existing Nostr libraries</span>
</div>
<div class="flex items-start gap-2">
<span class="text-accent"></span>
<span>You understand the Nostr protocol</span>
</div>
<div class="flex items-start gap-2">
<span class="text-accent"></span>
<span>Need fine-grained connection control</span>
</div>
<div class="flex items-start gap-2">
<span class="text-accent"></span>
<span>Building high-frequency applications</span>
</div>
</div>
</div>
</div>
<div class="alert alert-info">
<div>
<h4 class="font-bold mb-2">🎯 Choosing the Right Approach</h4>
<div class="text-sm space-y-2">
<p>
<strong>Start with window.eve:</strong> Even if you think you might need WebSocket later, begin with
the high-level API. You can always refactor specific parts to use WebSocket once you understand your
performance requirements.
</p>
<p>
<strong>Hybrid approach:</strong> Many successful Arxlets use <code>window.eve</code> for most
operations and WebSocket only for specific high-performance features like real-time chat or live
collaboration.
</p>
<p>
<strong>Migration path:</strong> The data structures are the same, so you can gradually migrate from{" "}
<code>window.eve</code>
to WebSocket for specific features without rewriting your entire application.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Best Practices */}
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="best-practices" class="card-title text-warning mb-4">
💡 Best Practices for Robust Arxlets
</h3>
<div class="space-y-6">
<div class="border-l-4 border-error pl-4">
<h4 class="font-bold text-lg text-error mb-3">🛡 Error Handling & Reliability</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Always use try-catch blocks:</strong>
<p>
Network requests can fail, relays can be down, or data might be malformed. Wrap all API calls to
prevent crashes.
</p>
</div>
<div>
<strong>Check for null/undefined returns:</strong>
<p>
Query methods return <code>null</code> when no data is found. Always check before using the result.
</p>
</div>
<div>
<strong>Provide meaningful user feedback:</strong>
<p>
Show loading states, error messages, and success confirmations. Users should know what's happening.
</p>
</div>
<div>
<strong>Implement retry logic for critical operations:</strong>
<p>Publishing events or loading essential data should retry on failure with exponential backoff.</p>
</div>
</div>
</div>
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg text-success mb-3"> Performance & Efficiency</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Use specific, narrow filters:</strong>
<p>
Instead of fetching all events and filtering in JavaScript, use precise Nostr filters to reduce data
transfer.
</p>
</div>
<div>
<strong>Cache frequently accessed data:</strong>
<p>Profile information, avatars, and static content should be cached to avoid repeated API calls.</p>
</div>
<div>
<strong>Implement pagination for large datasets:</strong>
<p>
Don't load thousands of events at once. Use <code>limit</code> and <code>until</code> parameters for
pagination.
</p>
</div>
<div>
<strong>Debounce rapid user actions:</strong>
<p>
If users can trigger API calls quickly (like typing in search), debounce to avoid overwhelming the
relay.
</p>
</div>
<div>
<strong>Unsubscribe from observables:</strong>
<p>Always clean up subscriptions to prevent memory leaks and unnecessary network traffic.</p>
</div>
</div>
</div>
<div class="border-l-4 border-info pl-4">
<h4 class="font-bold text-lg text-info mb-3">🎯 User Experience</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Show loading states:</strong>
<p>Use spinners, skeletons, or progress indicators while data loads. Empty screens feel broken.</p>
</div>
<div>
<strong>Handle empty states gracefully:</strong>
<p>When no data is found, show helpful messages or suggestions rather than blank areas.</p>
</div>
<div>
<strong>Implement optimistic updates:</strong>
<p>
Update the UI immediately when users take actions, then sync with the server. Makes apps feel
faster.
</p>
</div>
<div>
<strong>Provide offline indicators:</strong>
<p>Let users know when they're disconnected or when operations might not work.</p>
</div>
</div>
</div>
<div class="border-l-4 border-warning pl-4">
<h4 class="font-bold text-lg text-warning mb-3">🔒 Security & Privacy</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Validate all user inputs:</strong>
<p>Never trust user input. Validate, sanitize, and escape data before using it in events or UI.</p>
</div>
<div>
<strong>Be mindful of public data:</strong>
<p>
Remember that events are visible to everyone in your CCN by default. Don't accidentally expose
private information.
</p>
</div>
<div>
<strong>Handle signing errors gracefully:</strong>
<p>Users might reject signing requests. Always have fallbacks and clear error messages.</p>
</div>
<div>
<strong>Respect user privacy preferences:</strong>
<p>Some users prefer pseudonymous usage. Don't force real names or personal information.</p>
</div>
</div>
</div>
</div>
<div class="alert alert-success mt-6">
<div>
<h4 class="font-bold mb-2">🚀 Quick Checklist for Production Arxlets</h4>
<div class="grid md:grid-cols-2 gap-4 text-sm">
<div>
<strong>Code Quality:</strong>
<ul class="list-disc list-inside mt-1 space-y-1">
<li>All API calls wrapped in try-catch</li>
<li>Null checks before using data</li>
<li>Subscriptions properly cleaned up</li>
<li>Input validation implemented</li>
</ul>
</div>
<div>
<strong>User Experience:</strong>
<ul class="list-disc list-inside mt-1 space-y-1">
<li>Loading states for all async operations</li>
<li>Error messages are user-friendly</li>
<li>Empty states handled gracefully</li>
<li>Performance tested with large datasets</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,628 @@
// @jsx h
// @jsxImportSource preact
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
import { CodeBlock } from "./CodeBlock.jsx";
/**
* Best Practices Section - Comprehensive development guidelines for Arxlets
*/
export const BestPracticesSection = () => {
useSyntaxHighlighting();
const errorHandlingExample = `// Always wrap API calls in try-catch blocks
async function loadUserData() {
try {
const events = await window.eve.getAllEventsWithFilter({
kinds: [0], // Profile events
limit: 10
});
if (events.length === 0) {
showEmptyState("No profiles found");
return;
}
displayProfiles(events);
} catch (error) {
console.error("Failed to load profiles:", error);
showErrorMessage("Unable to load profiles. Please try again.");
}
}
// Provide user feedback for all states
function showErrorMessage(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-error';
alert.innerHTML = \`<span>\${message}</span>\`;
container.appendChild(alert);
}`;
const performanceExample = `// Use specific filters to reduce data transfer
const efficientFilter = {
kinds: [1], // Only text notes
authors: [userPubkey], // Only from specific user
since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
limit: 20 // Reasonable limit
};
// Cache frequently accessed data
const profileCache = new Map();
async function getCachedProfile(pubkey) {
if (profileCache.has(pubkey)) {
return profileCache.get(pubkey);
}
const profile = await window.eve.getProfile(pubkey);
if (profile) {
profileCache.set(pubkey, profile);
}
return profile;
}
// Debounce rapid user actions
let searchTimeout;
function handleSearchInput(query) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
performSearch(query);
}, 300); // Wait 300ms after user stops typing
}`;
const subscriptionExample = `// Proper subscription management
let eventSubscription;
function startListening() {
eventSubscription = window.eve.subscribeToEvents({
kinds: [1],
limit: 50
}).subscribe({
next: (event) => {
addEventToUI(event);
},
error: (error) => {
console.error("Subscription error:", error);
showErrorMessage("Lost connection. Reconnecting...");
// Implement retry logic
setTimeout(startListening, 5000);
}
});
}
// CRITICAL: Always clean up subscriptions
function cleanup() {
if (eventSubscription) {
eventSubscription.unsubscribe();
eventSubscription = null;
}
}
// Clean up when Arxlet is closed or user navigates away
window.addEventListener('beforeunload', cleanup);`;
const uiExample = `// Use DaisyUI components for consistency
function createLoadingState() {
return \`
<div class="flex justify-center items-center p-8">
<span class="loading loading-spinner loading-lg"></span>
<span class="ml-4">Loading profiles...</span>
</div>
\`;
}
function createEmptyState() {
return \`
<div class="text-center p-8">
<div class="text-6xl mb-4">📭</div>
<h3 class="text-lg font-semibold mb-2">No messages yet</h3>
<p class="text-base-content/70">Be the first to start a conversation!</p>
<button class="btn btn-primary mt-4" onclick="openComposer()">
Write a message
</button>
</div>
\`;
}
// Implement optimistic updates for better UX
async function publishMessage(content) {
// Show message immediately (optimistic)
const tempId = 'temp-' + Date.now();
addMessageToUI({ id: tempId, content, pending: true });
try {
const event = await window.eve.publish({
kind: 1,
content: content,
tags: []
});
// Replace temp message with real one
replaceMessageInUI(tempId, event);
} catch (error) {
// Remove temp message and show error
removeMessageFromUI(tempId);
showErrorMessage("Failed to send message");
}
}`;
return (
<div class="space-y-8">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4"> Best Practices</h2>
<p class="text-xl text-base-content/70 max-w-3xl mx-auto">
Master the art of building production-ready Arxlets with these comprehensive development guidelines
</p>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-info mb-4">Building Production-Ready Arxlets</h3>
<div class="space-y-4">
<p>
Creating a great Arxlet goes beyond just making it work - it needs to be reliable, performant, and provide
an excellent user experience. These best practices will help you build Arxlets that users love and that
work consistently in the CCN environment.
</p>
<p>
<strong>Why these practices matter:</strong> Arxlets run in a shared environment where performance issues
can affect other applications, and users expect the same level of polish they get from native apps.
Following these guidelines ensures your Arxlet integrates seamlessly with the CCN ecosystem.
</p>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="error-handling" class="card-title text-error mb-4">
🛡 Error Handling & Reliability
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>User experience first:</strong> When something goes wrong, users should know what happened and
what they can do about it. Silent failures are frustrating and make your app feel broken.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-error pl-4">
<h4 class="font-bold text-lg mb-3">Essential Error Handling Patterns</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Wrap all API calls in try-catch blocks:</strong>
<p>
Every call to <code>window.eve</code> functions can potentially fail. Always handle exceptions.
</p>
</div>
<div>
<strong>Check for null/undefined returns:</strong>
<p>
Query methods return <code>null</code> when no data is found. Verify results before using them.
</p>
</div>
<div>
<strong>Provide meaningful user feedback:</strong>
<p>Show specific error messages that help users understand what went wrong and how to fix it.</p>
</div>
<div>
<strong>Implement retry logic for critical operations:</strong>
<p>
Publishing events or loading essential data should retry automatically with exponential backoff.
</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">Practical Error Handling Example:</h4>
<CodeBlock language="typescript" code={errorHandlingExample} />
</div>
</div>
<div class="grid md:grid-cols-2 gap-4 mt-6">
<div class="alert alert-error">
<div>
<h4 class="font-bold mb-2"> Common Mistakes</h4>
<ul class="text-sm list-disc list-inside space-y-1">
<li>Not handling API failures</li>
<li>Assuming data will always exist</li>
<li>Silent failures with no user feedback</li>
<li>Generic "Something went wrong" messages</li>
<li>No retry mechanisms for critical operations</li>
</ul>
</div>
</div>
<div class="alert alert-success">
<div>
<h4 class="font-bold mb-2"> Best Practices</h4>
<ul class="text-sm list-disc list-inside space-y-1">
<li>Specific, actionable error messages</li>
<li>Graceful degradation when features fail</li>
<li>Loading states for all async operations</li>
<li>Retry buttons for failed operations</li>
<li>Offline indicators when appropriate</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="performance" class="card-title text-success mb-4">
Performance & Efficiency
</h3>
<div class="space-y-6">
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg mb-3">Performance Optimization Strategies</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Use specific, narrow filters:</strong>
<p>
Instead of fetching all events and filtering in JavaScript, use precise Nostr filters to reduce data
transfer.
</p>
</div>
<div>
<strong>Implement intelligent caching:</strong>
<p>Cache profile information, avatars, and other static content to avoid repeated API calls.</p>
</div>
<div>
<strong>Paginate large datasets:</strong>
<p>
Don't load thousands of events at once. Use <code>limit</code> and <code>until</code> parameters for
pagination.
</p>
</div>
<div>
<strong>Debounce rapid user actions:</strong>
<p>
If users can trigger API calls quickly (like typing in search), debounce to avoid overwhelming the
relay.
</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">Performance Optimization Example:</h4>
<CodeBlock language="typescript" code={performanceExample} />
</div>
</div>
<div class="alert alert-warning mt-6">
<div>
<h4 class="font-bold mb-2">! Performance Pitfalls to Avoid</h4>
<div class="text-sm space-y-2">
<p>
<strong>Overly broad filters:</strong> Fetching all events and filtering client-side wastes bandwidth.
</p>
<p>
<strong>No pagination:</strong> Loading thousands of items at once can freeze the interface.
</p>
<p>
<strong>Repeated API calls:</strong> Fetching the same profile data multiple times is inefficient.
</p>
<p>
<strong>Unthrottled user input:</strong> Search-as-you-type without debouncing can overwhelm the
relay.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="subscriptions" class="card-title text-accent mb-4">
🔄 Subscription Management
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>Subscriptions power real-time features.</strong> They make your Arxlet feel alive by automatically
updating when new data arrives. However, they need careful management to prevent memory leaks.
</p>
<p>
<strong>Clean up is critical.</strong> Forgetting to unsubscribe from observables can cause memory leaks,
unnecessary disk i/o, and performance degradation over time.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-accent pl-4">
<h4 class="font-bold text-lg mb-3">Subscription Best Practices</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Always store subscription references:</strong>
<p>Keep references to all subscriptions so you can unsubscribe when needed.</p>
</div>
<div>
<strong>Implement proper cleanup:</strong>
<p>Unsubscribe when components unmount, users navigate away, or the Arxlet closes.</p>
</div>
<div>
<strong>Use specific filters:</strong>
<p>Narrow subscription filters reduce unnecessary data and improve performance.</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">Proper Subscription Management:</h4>
<CodeBlock language="typescript" code={subscriptionExample} />
</div>
</div>
<div class="alert alert-error mt-6">
<div>
<h4 class="font-bold mb-2">🚨 Memory Leak Prevention</h4>
<div class="text-sm space-y-2">
<p>
<strong>Always unsubscribe:</strong> Every <code>subscribe()</code> call must have a corresponding{" "}
<code>unsubscribe()</code>.
</p>
<p>
<strong>Clean up on navigation:</strong> Users might navigate away without properly closing your
Arxlet.
</p>
<p>
<strong>Handle page refresh:</strong> Use <code>beforeunload</code> event to clean up subscriptions.
</p>
<p>
<strong>Monitor subscription count:</strong> Too many active subscriptions can impact performance.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="user-experience" class="card-title text-info mb-4">
🎯 User Experience Excellence
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>Great UX makes the difference.</strong> Users expect responsive, intuitive interfaces that provide
clear feedback. Small details like loading states and empty state messages significantly impact user
satisfaction.
</p>
<p>
<strong>Consistency with CCN design.</strong> Using DaisyUI components ensures your Arxlet feels
integrated with the rest of the platform while saving you development time.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-info pl-4">
<h4 class="font-bold text-lg mb-3">UX Best Practices</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Show loading states for all async operations:</strong>
<p>Users should never see blank screens or wonder if something is happening.</p>
</div>
<div>
<strong>Handle empty states gracefully:</strong>
<p>When no data is available, provide helpful messages or suggestions for next steps.</p>
</div>
<div>
<strong>Implement optimistic updates:</strong>
<p>Update the UI immediately when users take actions, then sync with the server.</p>
</div>
<div>
<strong>Use consistent DaisyUI components:</strong>
<p>Leverage the pre-built component library for consistent styling and behavior.</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">UI/UX Implementation Examples:</h4>
<CodeBlock language="typescript" code={uiExample} />
</div>
</div>
<div class="grid md:grid-cols-2 gap-4 mt-6">
<div class="border-2 border-success rounded-lg p-4">
<h4 class="font-bold text-success mb-3"> Excellent UX Includes</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Loading spinners for async operations</li>
<li>Helpful empty state messages</li>
<li>Immediate feedback for user actions</li>
<li>Clear error messages with solutions</li>
<li>Consistent visual design</li>
<li>Accessible keyboard navigation</li>
<li>Responsive layout for different screen sizes</li>
</ul>
</div>
<div class="border-2 border-warning rounded-lg p-4">
<h4 class="font-bold text-warning mb-3">! UX Anti-patterns</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Blank screens during loading</li>
<li>No feedback for user actions</li>
<li>Generic or confusing error messages</li>
<li>Inconsistent styling with CCN</li>
<li>Broken layouts on mobile devices</li>
<li>Inaccessible interface elements</li>
<li>Slow or unresponsive interactions</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="security" class="card-title text-warning mb-4">
🔒 Security & Privacy Considerations
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>Security is everyone's responsibility.</strong> Even though Arxlets run in a sandboxed
environment, you still need to validate inputs, handle user data responsibly, and follow security best
practices.
</p>
<p>
<strong>Privacy by design.</strong> Remember that Nostr events are public by default. Be mindful of what
data you're storing and how you're handling user information.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-warning pl-4">
<h4 class="font-bold text-lg mb-3">Security Best Practices</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Validate all user inputs:</strong>
<p>Never trust user input. Validate, sanitize, and escape data before using it in events or UI.</p>
</div>
<div>
<strong>Be mindful of public data:</strong>
<p>Nostr events are public by default. Don't accidentally expose private information.</p>
</div>
<div>
<strong>Handle signing errors gracefully:</strong>
<p>Users might reject signing requests. Always have fallbacks and clear error messages.</p>
</div>
<div>
<strong>Respect user privacy preferences:</strong>
<p>Some users prefer pseudonymous usage. Don't force real names or personal information.</p>
</div>
<div>
<strong>Sanitize HTML content:</strong>
<p>If displaying user-generated content, sanitize it to prevent XSS attacks.</p>
</div>
</div>
</div>
</div>
<div class="alert alert-error mt-6">
<div>
<h4 class="font-bold mb-2">🚨 Security Checklist</h4>
<div class="grid md:grid-cols-2 gap-4 text-sm">
<div>
<strong>Input Validation:</strong>
<ul class="list-disc list-inside mt-1 space-y-1">
<li>Validate all form inputs</li>
<li>Sanitize user-generated content</li>
<li>Check data types and ranges</li>
<li>Escape HTML when displaying content</li>
</ul>
</div>
<div>
<strong>Privacy Protection:</strong>
<ul class="list-disc list-inside mt-1 space-y-1">
<li>Don't store sensitive data in events</li>
<li>Respect user anonymity preferences</li>
<li>Handle signing rejections gracefully</li>
<li>Be transparent about data usage</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 id="production-checklist" class="card-title text-success mb-4">
🚀 Production Readiness Checklist
</h3>
<div class="space-y-4 mb-6">
<p>
Before publishing your Arxlet, run through this comprehensive checklist to ensure it meets production
quality standards. A well-tested Arxlet provides a better user experience and reflects positively on the
entire CCN ecosystem.
</p>
</div>
<div class="grid md:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-success mb-3"> Code Quality</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>All API calls wrapped in try-catch blocks</li>
<li>Null/undefined checks before using data</li>
<li>Subscriptions properly cleaned up</li>
<li>Input validation implemented</li>
<li>Error handling with user feedback</li>
<li>Performance optimizations applied</li>
<li>Code is well-commented and organized</li>
</ul>
</div>
<div class="border-l-4 border-info pl-4">
<h4 class="font-bold text-info mb-3">🎯 User Experience</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Loading states for all async operations</li>
<li>Error messages are user-friendly</li>
<li>Empty states handled gracefully</li>
<li>Consistent DaisyUI styling</li>
<li>Responsive design for mobile</li>
<li>Keyboard navigation works</li>
<li>Accessibility features implemented</li>
</ul>
</div>
</div>
<div class="space-y-4">
<div class="border-l-4 border-warning pl-4">
<h4 class="font-bold text-warning mb-3">🔒 Security & Privacy</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>User inputs are validated and sanitized</li>
<li>No sensitive data in public events</li>
<li>Signing errors handled gracefully</li>
<li>Privacy preferences respected</li>
<li>HTML content properly escaped</li>
<li>No hardcoded secrets or keys</li>
<li>Data usage is transparent</li>
</ul>
</div>
<div class="border-l-4 border-accent pl-4">
<h4 class="font-bold text-accent mb-3"> Performance</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Efficient Nostr filters used</li>
<li>Data caching implemented</li>
<li>Pagination for large datasets</li>
<li>User actions are debounced</li>
<li>Memory leaks prevented</li>
<li>Bundle size optimized</li>
<li>Performance tested with large datasets</li>
</ul>
</div>
</div>
</div>
<div class="alert alert-success mt-6">
<div>
<h4 class="font-bold mb-2">🎉 Ready for Production!</h4>
<p class="text-sm">
Once you've checked off all these items, your Arxlet is ready to provide an excellent experience for CCN
users. Remember that you can always iterate and improve based on user feedback and changing
requirements.
</p>
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,53 @@
// @jsx h
// @jsxImportSource preact
import { useState } from "preact/hooks";
/**
* Reusable code block component with syntax highlighting and a copy button.
*/
export const CodeBlock = ({ language = "javascript", code }) => {
const [isCopied, setIsCopied] = useState(false);
const handleCopy = () => {
if (code) {
navigator.clipboard.writeText(code.trim());
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
}
};
return (
<div class="mockup-code relative">
<button
class="absolute top-2 right-2 btn btn-ghost btn-sm"
type="button"
onClick={handleCopy}
aria-label="Copy code"
>
{isCopied ? (
<span class="text-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</span>
) : (
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
)}
</button>
<pre>
<code class={`language-${language}`}>{code?.trim()}</code>
</pre>
</div>
);
};

View file

@ -0,0 +1,471 @@
// @jsx h
// @jsxImportSource preact
import buildCommand from "../highlight/build-command.sh" with { type: "text" };
import renderFunction from "../highlight/render-function.ts" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
import { CodeBlock } from "./CodeBlock.jsx";
/**
* Development Section - Guide for building Arxlets
* Covers APIs, restrictions, and the required render function
*/
export const DevelopmentSection = () => {
useSyntaxHighlighting();
return (
<div class="space-y-6">
<h2 class="text-3xl font-bold">Development Guide</h2>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 id="understanding-arxlets" class="card-title text-info mb-4">
Understanding the Arxlet Environment
</h3>
<p class="mb-6">
When you build an Arxlet, you're creating a web application that runs inside the CCN. Think of it like
building a mini-website that has access to special CCN features and Nostr data. Here's what you have
available and what the limitations are:
</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 id="nostr-vs-arxlets" class="card-title text-purple-600 mb-4">
🔄 Nostr Apps vs Arxlets: What's the Difference?
</h3>
<div class="space-y-4 mb-6">
<p>
If you're coming from the broader Nostr ecosystem, you might be wondering how Arxlets relate to regular
Nostr applications. Here's the key relationship to understand:
</p>
<div class="grid md:grid-cols-2 gap-6">
<div class="border-2 border-success rounded-lg p-4">
<h4 class="font-bold text-success mb-3"> Nostr App Arxlet</h4>
<p class="text-sm mb-3">
<strong>Most Nostr apps CAN become Arxlets</strong> with some modifications:
</p>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>
Replace external API calls with <code>window.eve</code> or local relay
</li>
<li>Adapt the UI to work within a container element</li>
<li>Remove routing if it conflicts with CCN navigation</li>
<li>
Use the provided <code>window.nostr</code> for signing
</li>
<li>Bundle everything into a single JavaScript file</li>
</ul>
</div>
<div class="border-2 border-warning rounded-lg p-4">
<h4 class="font-bold text-warning mb-3">! Arxlet Nostr App</h4>
<p class="text-sm mb-3">
<strong>Not every Arxlet works as a standalone Nostr app</strong> because:
</p>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>
May depend on CCN-specific APIs (<code>window.eve</code>)
</li>
<li>Designed for the sandboxed environment</li>
<li>Might rely on CCN member data or community features</li>
<li>UI optimized for container-based rendering</li>
<li>No independent relay connections</li>
</ul>
</div>
</div>
</div>
<div class="alert alert-info">
<div>
<h4 class="font-bold mb-2">💡 Practical Examples:</h4>
<div class="text-sm space-y-2">
<p>
<strong>Easy to Port:</strong> A simple note-taking app, image gallery, or profile viewer can usually
be adapted to work as both.
</p>
<p>
<strong>CCN-Specific:</strong> A CCN member directory, community chat, or collaborative workspace
might only make sense as an Arxlet.
</p>
<p>
<strong>Hybrid Approach:</strong> Many developers create a core library that works in both
environments, then build different interfaces for standalone vs Arxlet deployment.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="available-apis" class="card-title text-success mb-4">
What You Can Use
</h3>
<div class="space-y-6">
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg">window.eve - Your CCN Toolkit</h4>
<p class="text-sm opacity-80 mb-2">
This is your main interface to the CCN. It provides functions to read and write Nostr events, manage
data, and interact with other CCN members.
</p>
<p class="text-sm">
<strong>What it does:</strong> Lets you fetch events, publish new ones, manage user data, and access
CCN-specific features like member directories.
</p>
</div>
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg">window.nostr - Cryptographic Signing (NIP-07)</h4>
<p class="text-sm opacity-80 mb-2">
This is the standard Nostr extension API that lets your Arxlet create and sign events using the user's
private key.
</p>
<p class="text-sm">
<strong>What it does:</strong> Allows your app to publish events on behalf of the user, like posting
messages, creating profiles, or any other Nostr activity that requires authentication.
</p>
</div>
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg">DaisyUI 5 - Pre-built UI Components</h4>
<p class="text-sm opacity-80 mb-2">
A complete CSS framework with beautiful, accessible components already loaded and ready to use.
</p>
<p class="text-sm">
<strong>What it does:</strong> Provides buttons, cards, modals, forms, and dozens of other UI components
with consistent styling. No need to write CSS from scratch.
</p>
</div>
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg">Local Relay Connection</h4>
<p class="text-sm opacity-80 mb-2">
Direct WebSocket connection to <code>ws://localhost:6942</code> for real-time Nostr event streaming.
</p>
<p class="text-sm">
<strong>What it does:</strong> Lets you subscribe to live event feeds, get real-time updates, and
implement features like live chat or notifications.
</p>
</div>
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg">CCN Member Events</h4>
<p class="text-sm opacity-80 mb-2">Access to events from other members of your current CCN community.</p>
<p class="text-sm">
<strong>What it does:</strong> Enables community features like member directories, shared content, group
discussions, and collaborative tools.
</p>
</div>
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg">Standard Web APIs</h4>
<p class="text-sm opacity-80 mb-2">
Full access to modern browser APIs like localStorage, fetch (for local requests), DOM manipulation, and
more.
</p>
<p class="text-sm">
<strong>What it does:</strong> Everything you'd expect in a web app - store data locally, manipulate the
page, handle user interactions, etc.
</p>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="security-restrictions" class="card-title text-warning mb-4">
🔒 Security & Limitations
</h3>
<div class="space-y-4">
<div class="border-l-4 border-warning pl-4">
<h4 class="font-bold">No External Network Access</h4>
<p class="text-sm">
You can't make HTTP requests to external websites or APIs. All data must come through the CCN. This
prevents data leaks and ensures all communication goes through Nostr protocols.
</p>
</div>
<div class="border-l-4 border-warning pl-4">
<h4 class="font-bold">CCN-Scoped Data</h4>
<p class="text-sm">
You only have access to events and data from your current CCN community. You can't see events from other
CCNs or the broader Nostr network unless they're specifically shared with your community.
</p>
</div>
</div>
<div class="alert alert-info mt-6">
<span>
💡 <strong>Why These Restrictions?</strong> These limitations ensure your Arxlet is secure, respects user
privacy, and works reliably within the CCN ecosystem. They also make your app more predictable and easier
to debug.
</span>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="alert alert-info">
<span>
📚 <strong>Need More Details?</strong> Check the{" "}
<button
type="button"
class="link link-primary font-semibold underline"
onClick={() => {
// Find and click the API Reference tab
const tabs = document.querySelectorAll(".menu > li > a");
const apiTab = Array.from(tabs).find((tab) => tab.textContent.trim() === "API Reference");
if (apiTab) {
apiTab.click();
// Scroll to top after tab switch
setTimeout(() => {
window.scrollTo({ top: 0, behavior: "smooth" });
}, 100);
}
}}
>
API Reference
</button>{" "}
tab for comprehensive documentation of the <code>window.eve</code> API, code examples, and WebSocket usage
patterns.
</span>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="typescript-development" class="card-title text-info mb-4">
🚀 Building Your Arxlet with TypeScript
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>What is TypeScript?</strong> TypeScript is JavaScript with type checking. It helps catch errors
before your code runs and provides better autocomplete in your editor. While you can write Arxlets in
plain JavaScript, TypeScript makes development much smoother.
</p>
<p>
<strong>Why use it for Arxlets?</strong> Eve provides TypeScript definitions for all APIs, so you'll get
autocomplete for <code>window.eve</code> functions, proper error checking, and better documentation right
in your editor.
</p>
</div>
<div class="mb-6">
<h4 class="font-semibold mb-3 text-lg">Building Your Code</h4>
<p class="mb-3">
Since Arxlets need to be a single JavaScript file, you'll use <strong>Bun</strong> (a fast JavaScript
runtime and bundler) to compile your TypeScript code. Here's the command that does everything:
</p>
<CodeBlock language="bash" code={buildCommand} />
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mt-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-amber-700">
<strong> Svelte Exception:</strong> The above build command will NOT work for Svelte projects.
Svelte requires specific Vite configuration to compile properly. Instead, use our{" "}
<a
href="https://git.arx-ccn.com/Arx/arxlets-template"
class="link link-primary"
target="_blank"
rel="noopener noreferrer"
>
arxlets-template
</a>{" "}
and simply run <code class="bg-amber-100 px-1 rounded">bun run build</code>. Your compiled file will
be available at <code class="bg-amber-100 px-1 rounded">dist/bundle.js</code> once built.
</p>
</div>
</div>
</div>
</div>
<div class="grid md:grid-cols-2 gap-6 mt-6">
<div class="space-y-3">
<h4 class="font-semibold text-lg">What Each Build Option Does:</h4>
<div class="space-y-3">
<div class="border-l-4 border-info pl-3">
<code class="font-bold">--minify</code>
<p class="text-sm">
Removes whitespace and shortens variable names to make your file smaller. Smaller files load faster.
</p>
</div>
<div class="border-l-4 border-info pl-3">
<code class="font-bold">--target=browser</code>
<p class="text-sm">Tells Bun to optimize the code for web browsers instead of server environments.</p>
</div>
<div class="border-l-4 border-info pl-3">
<code class="font-bold">--production</code>
<p class="text-sm">
Enables all optimizations and removes development-only code for better performance.
</p>
</div>
</div>
</div>
<div class="space-y-3">
<h4 class="font-semibold text-lg">Why TypeScript Helps:</h4>
<div class="space-y-3">
<div class="border-l-4 border-success pl-3">
<strong>Catch Errors Early</strong>
<p class="text-sm">
TypeScript finds mistakes like typos in function names or wrong parameter types before you run your
code.
</p>
</div>
<div class="border-l-4 border-success pl-3">
<strong>Better Autocomplete</strong>
<p class="text-sm">
Your editor will suggest available functions and show you what parameters they expect.
</p>
</div>
<div class="border-l-4 border-success pl-3">
<strong>Easier Refactoring</strong>
<p class="text-sm">
When you rename functions or change interfaces, TypeScript helps update all the places that use
them.
</p>
</div>
<div class="border-l-4 border-success pl-3">
<strong>Self-Documenting Code</strong>
<p class="text-sm">
Type annotations serve as inline documentation, making your code easier to understand later.
</p>
</div>
</div>
</div>
</div>
<div class="alert alert-success mt-6">
<div>
<h4 class="font-bold mb-2">Recommended Development Workflow:</h4>
<ol class="list-decimal list-inside space-y-1 text-sm">
<li>
Create your main file as <kbd class="kbd">index.ts</kbd> (TypeScript)
</li>
<li>Write your Arxlet code with full TypeScript features</li>
<li>
Run the build command to create <kbd class="kbd">build.js</kbd>
</li>
<li>
Copy the contents of <kbd class="kbd">build.js</kbd> into your Nostr registration event
</li>
<li>Repeat steps 2-4 as you develop and test</li>
</ol>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="required-export-function" class="card-title mb-4">
The Heart of Your Arxlet: The Render Function
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>What is the render function?</strong> This is the main entry point of your Arxlet - think of it as
the <code>main()</code>
function in other programming languages. When someone opens your Arxlet, Eve calls this function and gives
it a container element where your app should display itself.
</p>
<p>
<strong>How it works:</strong> The platform creates an empty <code>&lt;div&gt;</code> element and passes
it to your render function. Your job is to fill that container with your app's interface - buttons, forms,
content, whatever your Arxlet does.
</p>
<p>
<strong>Why this pattern?</strong> This approach gives you complete control over your app's interface
while keeping it isolated from other Arxlets and the main CCN interface.
</p>
</div>
<h4 class="font-semibold mb-3 text-lg">Basic Structure:</h4>
<CodeBlock language="typescript" code={renderFunction} />
<div class="mt-6 space-y-4">
<h4 class="font-semibold text-lg">Breaking Down the Example:</h4>
<div class="grid gap-4">
<div class="border-l-4 border-primary pl-4">
<code class="font-bold">export function render(container: HTMLElement)</code>
<p class="text-sm mt-1">
This declares your main function. The <code>container</code> parameter is the DOM element where your
app will live. The <code>export</code> keyword makes it available to the CCN.
</p>
</div>
<div class="border-l-4 border-primary pl-4">
<code class="font-bold">container.innerHTML = '...'</code>
<p class="text-sm mt-1">
This is the simplest way to add content - just set the HTML directly. For simple Arxlets, this might
be all you need.
</p>
</div>
<div class="border-l-4 border-primary pl-4">
<code class="font-bold">const button = container.querySelector('button')</code>
<p class="text-sm mt-1">
After adding HTML, you can find elements and attach event listeners to make your app interactive.
</p>
</div>
<div class="border-l-4 border-primary pl-4">
<code class="font-bold">window.eve.getEvents(...)</code>
<p class="text-sm mt-1">
This shows how to use the CCN API to fetch Nostr events. Most Arxlets will interact with Nostr data in
some way.
</p>
</div>
</div>
</div>
<div class="alert alert-success mt-6">
<div>
<h4 class="font-bold mb-2">Development Tips:</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>
<strong>Start Simple:</strong> Begin with basic HTML and gradually add interactivity
</li>
<li>
<strong>Use Modern JavaScript:</strong> async/await, destructuring, arrow functions - it all works
</li>
<li>
<strong>Leverage DaisyUI:</strong> Use pre-built components instead of writing CSS from scratch
</li>
<li>
<strong>Handle Errors:</strong> Wrap API calls in try/catch blocks for better user experience
</li>
<li>
<strong>Think Reactive:</strong> Update the UI when data changes, don't just set it once
</li>
</ul>
</div>
</div>
<div class="alert alert-info mt-4">
<span>
💡 <strong>Advanced Patterns:</strong> You can use any frontend framework (React, Vue, Svelte) by
rendering it into the container, or build complex apps with routing, state management, and real-time
updates. The render function is just your starting point!
</span>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,60 @@
// @jsx h
// @jsxImportSource preact
import { CounterExample } from "../examples/CounterExample.jsx";
import { NostrPublisherExample } from "../examples/NostrPublisherExample.jsx";
import { PreactCounterExample } from "../examples/PreactCounterExample.jsx";
import { SvelteCounterExample } from "../examples/SvelteCounterExample.jsx";
const examples = {
vanilla: <CounterExample />,
preact: <PreactCounterExample />,
svelte: <SvelteCounterExample />,
nostr: <NostrPublisherExample />,
};
/**
* Examples Section - Practical Arxlet implementations
* Shows real-world examples with detailed explanations
*/
export const ExamplesSection = ({ activeExample }) => {
const ActiveComponent = examples[activeExample] || <div>Example not found</div>;
return (
<div class="space-y-6">
<h2 class="text-3xl font-bold">Example Arxlets</h2>
<div class="bg-info border-l-4 border-info/50 p-4 mb-6">
<div class="flex">
<div class="ml-3 text-info-content">
<p class="text-sm text-info-content">
<strong>Framework Freedom:</strong> These examples show basic implementations, but you're not limited to
vanilla JavaScript! You can use any framework you prefer - React with JSX, Preact, Vue, Svelte, or any
other modern framework. Bun's powerful plugin system supports transpilation and bundling for virtually any
JavaScript ecosystem.
</p>
<p class="text-sm t-2">
Check out{" "}
<a
href="https://bun.com/docs/runtime/plugins"
target="_blank"
rel="noopener noreferrer"
class="underline hover:text-blue-900"
>
Bun's plugin documentation
</a>{" "}
to see how you can integrate your preferred tools and frameworks.
</p>
<p class="text-sm mt-2">
If you have any questions or run into problems, feel free to reach out to the team @ Arx (builders of Eve)
on Nostr: <kbd class="kbd">npub1ven4zk8xxw873876gx8y9g9l9fazkye9qnwnglcptgvfwxmygscqsxddfhif</kbd>
</p>
</div>
</div>
</div>
{/* Tab Content */}
<div class="mt-6">{ActiveComponent}</div>
</div>
);
};

View file

@ -0,0 +1,15 @@
// @jsx h
// @jsxImportSource preact
import contextMd from "../highlight/context.md" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
import { CodeBlock } from "./CodeBlock.jsx";
export const LLMsSection = () => {
useSyntaxHighlighting();
return (
<section id="llms" className="arxlet-docs-section">
<CodeBlock code={contextMd} />
</section>
);
};

View file

@ -0,0 +1,50 @@
// @jsx h
// @jsxImportSource preact
import typeDefinitions from "../highlight/type-definitions.ts" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
import { CodeBlock } from "./CodeBlock.jsx";
/**
* Overview Section - Introduction to Arxlets
* Explains what Arxlets are and their key features
*/
export const OverviewSection = () => {
useSyntaxHighlighting();
return (
<div class="space-y-8">
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">What are Arxlets?</h2>
<p class="text-lg mb-4">
Arxlets are secure, sandboxed JavaScript applications that extend Eve's functionality. They run inside Eve
and are registered on your CCN for member-only access.
</p>
</div>
</div>
<div class="alert alert-info">
<span>
<strong>Coming Soon:</strong> WASM support will be added in future releases for even more powerful
applications.
</span>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 class="card-title text-secondary">📝 TypeScript Definitions</h3>
<p class="mb-4">Use these type definitions for full TypeScript support in your Arxlets:</p>
<CodeBlock language="typescript" code={typeDefinitions} />
<div class="alert alert-info mt-4">
<span>
<strong>Pro Tip:</strong> Save these types in a <code>types.ts</code> file and import them throughout your
Arxlet for better development experience and type safety.
</span>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,147 @@
// @jsx h
// @jsxImportSource preact
import registrationEvent from "../highlight/registration-event.json" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
import { CodeBlock } from "./CodeBlock.jsx";
/**
* Registration Section - How to register Arxlets on CCN
* Covers the Nostr event structure and required fields
*/
export const RegistrationSection = () => {
useSyntaxHighlighting();
return (
<div class="space-y-6">
<h2 class="text-3xl font-bold">Arxlet Registration</h2>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-info">What are Nostr Events?</h3>
<div class="space-y-3">
<p>
<strong>Nostr</strong> (Notes and Other Stuff Transmitted by Relays) is a decentralized protocol where all
data is stored as <strong>events</strong>. Think of events as structured messages that contain information
and are cryptographically signed by their authors.
</p>
<p>Each event has:</p>
<ul class="list-disc list-inside space-y-1 ml-4">
<li>
<strong>Kind:</strong> A number that defines what type of data the event contains (like a category)
</li>
<li>
<strong>Content:</strong> The main data or message
</li>
<li>
<strong>Tags:</strong> Additional metadata organized as key-value pairs
</li>
<li>
<strong>Signature:</strong> Cryptographic proof that the author created this event
</li>
</ul>
<p>
To register your Arxlet, you create a special event (kind 30420) that tells the CCN about your
application. This event acts like a "business card" for your Arxlet, containing its code, name, and other
details. If you publish this event publicly outside your CCN, this will be available in the arxlet store,
and any CCN will be able to install it.
</p>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="nostr-event-structure" class="card-title">
Nostr Event Structure
</h3>
<p class="mb-4">
Register your Arxlet using a replaceable Nostr event with kind{" "}
<code class="badge badge-primary">30420</code>:
</p>
<CodeBlock language="json" code={registrationEvent} />
</div>
</div>
<div id="tag-reference" class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 class="card-title">Tag Reference</h3>
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>Tag</th>
<th>Required</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>d</code>
</td>
<td>
<span class="badge badge-error">Required</span>
</td>
<td>Unique identifier (alphanumeric, hyphens, underscores)</td>
<td>
<code>my-todo-app</code>
</td>
</tr>
<tr>
<td>
<code>name</code>
</td>
<td>
<span class="badge badge-error">Required</span>
</td>
<td>Human-readable display name</td>
<td>
<code>Todo Manager</code>
</td>
</tr>
<tr>
<td>
<code>description</code>
</td>
<td>
<span class="badge badge-warning">Optional</span>
</td>
<td>Brief description of functionality</td>
<td>
<code>Manage your tasks</code>
</td>
</tr>
<tr>
<td>
<code>script</code>
</td>
<td>
<span class="badge badge-error">Required</span>
</td>
<td>Complete JavaScript code with render export</td>
<td>
<code>export function render...</code>
</td>
</tr>
<tr>
<td>
<code>icon</code>
</td>
<td>
<span class="badge badge-warning">Optional</span>
</td>
<td>Iconify icon name and hex color</td>
<td>
<code>mdi:check-circle, #10b981</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,20 @@
// @jsx h
// @jsxImportSource preact
import { CodeBlock } from "../components/CodeBlock.jsx";
import code from "../highlight/counter.ts" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
export const CounterExample = () => {
useSyntaxHighlighting();
return (
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 class="card-title">🔢 Interactive Counter</h3>
<p class="mb-4">A simple counter demonstrating state management and event handling:</p>
<CodeBlock language="typescript" code={code} />
</div>
</div>
);
};

View file

@ -0,0 +1,623 @@
// @jsx h
// @jsxImportSource preact
import { CodeBlock } from "../components/CodeBlock.jsx";
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
/**
* Development Tips Component - Comprehensive best practices for Arxlet development
*/
export const DevelopmentTips = () => {
useSyntaxHighlighting();
const errorHandlingExample = `// Always wrap API calls in try-catch blocks
async function loadUserData() {
try {
const events = await window.eve.getAllEventsWithFilter({
kinds: [0], // Profile events
limit: 10
});
if (events.length === 0) {
showEmptyState("No profiles found");
return;
}
displayProfiles(events);
} catch (error) {
console.error("Failed to load profiles:", error);
showErrorMessage("Unable to load profiles. Please try again.");
}
}
// Provide user feedback for all states
function showErrorMessage(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-error';
alert.innerHTML = \`<span>\${message}</span>\`;
container.appendChild(alert);
}`;
const performanceExample = `// Use specific filters to reduce data transfer
const efficientFilter = {
kinds: [1], // Only text notes
authors: [userPubkey], // Only from specific user
since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
limit: 20 // Reasonable limit
};
// Cache frequently accessed data
const profileCache = new Map();
async function getCachedProfile(pubkey) {
if (profileCache.has(pubkey)) {
return profileCache.get(pubkey);
}
const profile = await window.eve.getProfile(pubkey);
if (profile) {
profileCache.set(pubkey, profile);
}
return profile;
}
// Debounce rapid user actions
let searchTimeout;
function handleSearchInput(query) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
performSearch(query);
}, 300); // Wait 300ms after user stops typing
}`;
const subscriptionExample = `// Proper subscription management
let eventSubscription;
function startListening() {
eventSubscription = window.eve.subscribeToEvents({
kinds: [1],
limit: 50
}).subscribe({
next: (event) => {
addEventToUI(event);
},
error: (error) => {
console.error("Subscription error:", error);
showErrorMessage("Lost connection. Reconnecting...");
// Implement retry logic
setTimeout(startListening, 5000);
}
});
}
// CRITICAL: Always clean up subscriptions
function cleanup() {
if (eventSubscription) {
eventSubscription.unsubscribe();
eventSubscription = null;
}
}
// Clean up when Arxlet is closed or user navigates away
window.addEventListener('beforeunload', cleanup);`;
const uiExample = `// Use DaisyUI components for consistency
function createLoadingState() {
return \`
<div class="flex justify-center items-center p-8">
<span class="loading loading-spinner loading-lg"></span>
<span class="ml-4">Loading profiles...</span>
</div>
\`;
}
function createEmptyState() {
return \`
<div class="text-center p-8">
<div class="text-6xl mb-4">📭</div>
<h3 class="text-lg font-semibold mb-2">No messages yet</h3>
<p class="text-base-content/70">Be the first to start a conversation!</p>
<button class="btn btn-primary mt-4" onclick="openComposer()">
Write a message
</button>
</div>
\`;
}
// Implement optimistic updates for better UX
async function publishMessage(content) {
// Show message immediately (optimistic)
const tempId = 'temp-' + Date.now();
addMessageToUI({ id: tempId, content, pending: true });
try {
const event = await window.eve.publish({
kind: 1,
content: content,
tags: []
});
// Replace temp message with real one
replaceMessageInUI(tempId, event);
} catch (error) {
// Remove temp message and show error
removeMessageFromUI(tempId);
showErrorMessage("Failed to send message");
}
}`;
return (
<div class="space-y-6">
<h2 class="text-3xl font-bold">💡 Development Best Practices</h2>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-info mb-4">Building Production-Ready Arxlets</h3>
<div class="space-y-4">
<p>
Creating a great Arxlet goes beyond just making it work - it needs to be reliable, performant, and provide
an excellent user experience. These best practices will help you build Arxlets that users love and that
work consistently in the CCN environment.
</p>
<p>
<strong>Why these practices matter:</strong> Arxlets run in a shared environment where performance issues
can affect other applications, and users expect the same level of polish they get from native apps.
Following these guidelines ensures your Arxlet integrates seamlessly with the CCN ecosystem.
</p>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="error-handling" class="card-title text-error mb-4">
🛡 Error Handling & Reliability
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>User experience first:</strong> When something goes wrong, users should know what happened and
what they can do about it. Silent failures are frustrating and make your app feel broken.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-error pl-4">
<h4 class="font-bold text-lg mb-3">Essential Error Handling Patterns</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Wrap all API calls in try-catch blocks:</strong>
<p>
Every call to <code>window.eve</code> functions can potentially fail. Always handle exceptions.
</p>
</div>
<div>
<strong>Check for null/undefined returns:</strong>
<p>
Query methods return <code>null</code> when no data is found. Verify results before using them.
</p>
</div>
<div>
<strong>Provide meaningful user feedback:</strong>
<p>Show specific error messages that help users understand what went wrong and how to fix it.</p>
</div>
<div>
<strong>Implement retry logic for critical operations:</strong>
<p>
Publishing events or loading essential data should retry automatically with exponential backoff.
</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">Practical Error Handling Example:</h4>
<CodeBlock language="typescript" code={errorHandlingExample} />
</div>
</div>
<div class="grid md:grid-cols-2 gap-4 mt-6">
<div class="alert alert-error">
<div>
<h4 class="font-bold mb-2"> Common Mistakes</h4>
<ul class="text-sm list-disc list-inside space-y-1">
<li>Not handling API failures</li>
<li>Assuming data will always exist</li>
<li>Silent failures with no user feedback</li>
<li>Generic "Something went wrong" messages</li>
<li>No retry mechanisms for critical operations</li>
</ul>
</div>
</div>
<div class="alert alert-success">
<div>
<h4 class="font-bold mb-2"> Best Practices</h4>
<ul class="text-sm list-disc list-inside space-y-1">
<li>Specific, actionable error messages</li>
<li>Graceful degradation when features fail</li>
<li>Loading states for all async operations</li>
<li>Retry buttons for failed operations</li>
<li>Offline indicators when appropriate</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="performance" class="card-title text-success mb-4">
Performance & Efficiency
</h3>
<div class="space-y-6">
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-lg mb-3">Performance Optimization Strategies</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Use specific, narrow filters:</strong>
<p>
Instead of fetching all events and filtering in JavaScript, use precise Nostr filters to reduce data
transfer.
</p>
</div>
<div>
<strong>Implement intelligent caching:</strong>
<p>Cache profile information, avatars, and other static content to avoid repeated API calls.</p>
</div>
<div>
<strong>Paginate large datasets:</strong>
<p>
Don't load thousands of events at once. Use <code>limit</code> and <code>until</code> parameters for
pagination.
</p>
</div>
<div>
<strong>Debounce rapid user actions:</strong>
<p>
If users can trigger API calls quickly (like typing in search), debounce to avoid overwhelming the
relay.
</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">Performance Optimization Example:</h4>
<CodeBlock language="typescript" code={performanceExample} />
</div>
</div>
<div class="alert alert-warning mt-6">
<div>
<h4 class="font-bold mb-2">! Performance Pitfalls to Avoid</h4>
<div class="text-sm space-y-2">
<p>
<strong>Overly broad filters:</strong> Fetching all events and filtering client-side wastes bandwidth.
</p>
<p>
<strong>No pagination:</strong> Loading thousands of items at once can freeze the interface.
</p>
<p>
<strong>Repeated API calls:</strong> Fetching the same profile data multiple times is inefficient.
</p>
<p>
<strong>Unthrottled user input:</strong> Search-as-you-type without debouncing can overwhelm the
relay.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="subscriptions" class="card-title text-accent mb-4">
🔄 Subscription Management
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>Subscriptions power real-time features.</strong> They make your Arxlet feel alive by automatically
updating when new data arrives. However, they need careful management to prevent memory leaks.
</p>
<p>
<strong>Clean up is critical.</strong> Forgetting to unsubscribe from observables can cause memory leaks,
unnecessary i/o usage, and performance degradation over time.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-accent pl-4">
<h4 class="font-bold text-lg mb-3">Subscription Best Practices</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Always store subscription references:</strong>
<p>Keep references to all subscriptions so you can unsubscribe when needed.</p>
</div>
<div>
<strong>Implement proper cleanup:</strong>
<p>Unsubscribe when components unmount, users navigate away, or the Arxlet closes.</p>
</div>
<div>
<strong>Use specific filters:</strong>
<p>Narrow subscription filters reduce unnecessary data and improve performance.</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">Proper Subscription Management:</h4>
<CodeBlock language="typescript" code={subscriptionExample} />
</div>
</div>
<div class="alert alert-error mt-6">
<div>
<h4 class="font-bold mb-2">🚨 Memory Leak Prevention</h4>
<div class="text-sm space-y-2">
<p>
<strong>Always unsubscribe:</strong> Every <code>subscribe()</code> call must have a corresponding{" "}
<code>unsubscribe()</code>.
</p>
<p>
<strong>Clean up on navigation:</strong> Users might navigate away without properly closing your
Arxlet.
</p>
<p>
<strong>Handle page refresh:</strong> Use <code>beforeunload</code> event to clean up subscriptions.
</p>
<p>
<strong>Monitor subscription count:</strong> Too many active subscriptions can impact performance.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="user-experience" class="card-title text-info mb-4">
🎯 User Experience Excellence
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>Great UX makes the difference.</strong> Users expect responsive, intuitive interfaces that provide
clear feedback. Small details like loading states and empty state messages significantly impact user
satisfaction.
</p>
<p>
<strong>Consistency with CCN design.</strong> Using DaisyUI components ensures your Arxlet feels
integrated with the rest of the platform while saving you development time.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-info pl-4">
<h4 class="font-bold text-lg mb-3">UX Best Practices</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Show loading states for all async operations:</strong>
<p>Users should never see blank screens or wonder if something is happening.</p>
</div>
<div>
<strong>Handle empty states gracefully:</strong>
<p>When no data is available, provide helpful messages or suggestions for next steps.</p>
</div>
<div>
<strong>Implement optimistic updates:</strong>
<p>Update the UI immediately when users take actions, then sync with the server.</p>
</div>
<div>
<strong>Use consistent DaisyUI components:</strong>
<p>Leverage the pre-built component library for consistent styling and behavior.</p>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold mb-3 text-lg">UI/UX Implementation Examples:</h4>
<CodeBlock language="typescript" code={uiExample} />
</div>
</div>
<div class="grid md:grid-cols-2 gap-4 mt-6">
<div class="border-2 border-success rounded-lg p-4">
<h4 class="font-bold text-success mb-3"> Excellent UX Includes</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Loading spinners for async operations</li>
<li>Helpful empty state messages</li>
<li>Immediate feedback for user actions</li>
<li>Clear error messages with solutions</li>
<li>Consistent visual design</li>
<li>Accessible keyboard navigation</li>
<li>Responsive layout for different screen sizes</li>
</ul>
</div>
<div class="border-2 border-warning rounded-lg p-4">
<h4 class="font-bold text-warning mb-3">! UX Anti-patterns</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Blank screens during loading</li>
<li>No feedback for user actions</li>
<li>Generic or confusing error messages</li>
<li>Inconsistent styling with CCN</li>
<li>Broken layouts on mobile devices</li>
<li>Inaccessible interface elements</li>
<li>Slow or unresponsive interactions</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 id="security" class="card-title text-warning mb-4">
🔒 Security & Privacy Considerations
</h3>
<div class="space-y-4 mb-6">
<p>
<strong>Security is everyone's responsibility.</strong> Even though Arxlets run in a sandboxed
environment, you still need to validate inputs, handle user data responsibly, and follow security best
practices.
</p>
<p>
<strong>Privacy by design.</strong> Remember that events are visible to everyone in your CCN by default.
Be mindful of what data you're storing and how you're handling user information.
</p>
</div>
<div class="space-y-6">
<div class="border-l-4 border-warning pl-4">
<h4 class="font-bold text-lg mb-3">Security Best Practices</h4>
<div class="space-y-3 text-sm">
<div>
<strong>Validate all user inputs:</strong>
<p>Never trust user input. Validate, sanitize, and escape data before using it in events or UI.</p>
</div>
<div>
<strong>Be mindful of public data:</strong>
<p>Nostr events are public by default. Don't accidentally expose private information.</p>
</div>
<div>
<strong>Handle signing errors gracefully:</strong>
<p>Users might reject signing requests. Always have fallbacks and clear error messages.</p>
</div>
<div>
<strong>Respect user privacy preferences:</strong>
<p>Some users prefer pseudonymous usage. Don't force real names or personal information.</p>
</div>
<div>
<strong>Sanitize HTML content:</strong>
<p>If displaying user-generated content, sanitize it to prevent XSS attacks.</p>
</div>
</div>
</div>
</div>
<div class="alert alert-error mt-6">
<div>
<h4 class="font-bold mb-2">🚨 Security Checklist</h4>
<div class="grid md:grid-cols-2 gap-4 text-sm">
<div>
<strong>Input Validation:</strong>
<ul class="list-disc list-inside mt-1 space-y-1">
<li>Validate all form inputs</li>
<li>Sanitize user-generated content</li>
<li>Check data types and ranges</li>
<li>Escape HTML when displaying content</li>
</ul>
</div>
<div>
<strong>Privacy Protection:</strong>
<ul class="list-disc list-inside mt-1 space-y-1">
<li>Don't store sensitive data in events</li>
<li>Respect user anonymity preferences</li>
<li>Handle signing rejections gracefully</li>
<li>Be transparent about data usage</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 id="production-checklist" class="card-title text-success mb-4">
🚀 Production Readiness Checklist
</h3>
<div class="space-y-4 mb-6">
<p>
Before publishing your Arxlet, run through this comprehensive checklist to ensure it meets production
quality standards. A well-tested Arxlet provides a better user experience and reflects positively on the
entire CCN ecosystem.
</p>
</div>
<div class="grid md:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="border-l-4 border-success pl-4">
<h4 class="font-bold text-success mb-3"> Code Quality</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>All API calls wrapped in try-catch blocks</li>
<li>Null/undefined checks before using data</li>
<li>Subscriptions properly cleaned up</li>
<li>Input validation implemented</li>
<li>Error handling with user feedback</li>
<li>Performance optimizations applied</li>
<li>Code is well-commented and organized</li>
</ul>
</div>
<div class="border-l-4 border-info pl-4">
<h4 class="font-bold text-info mb-3">🎯 User Experience</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Loading states for all async operations</li>
<li>Error messages are user-friendly</li>
<li>Empty states handled gracefully</li>
<li>Consistent DaisyUI styling</li>
<li>Responsive design for mobile</li>
<li>Keyboard navigation works</li>
<li>Accessibility features implemented</li>
</ul>
</div>
</div>
<div class="space-y-4">
<div class="border-l-4 border-warning pl-4">
<h4 class="font-bold text-warning mb-3">🔒 Security & Privacy</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>User inputs are validated and sanitized</li>
<li>No sensitive data in public events</li>
<li>Signing errors handled gracefully</li>
<li>Privacy preferences respected</li>
<li>HTML content properly escaped</li>
<li>No hardcoded secrets or keys</li>
<li>Data usage is transparent</li>
</ul>
</div>
<div class="border-l-4 border-accent pl-4">
<h4 class="font-bold text-accent mb-3"> Performance</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Efficient Nostr filters used</li>
<li>Data caching implemented</li>
<li>Pagination for large datasets</li>
<li>User actions are debounced</li>
<li>Memory leaks prevented</li>
<li>Bundle size optimized</li>
<li>Performance tested with large datasets</li>
</ul>
</div>
</div>
</div>
<div class="alert alert-success mt-6">
<div>
<h4 class="font-bold mb-2">🎉 Ready for Production!</h4>
<p class="text-sm">
Once you've checked off all these items, your Arxlet is ready to provide an excellent experience for CCN
users. Remember that you can always iterate and improve based on user feedback and changing
requirements.
</p>
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,20 @@
// @jsx h
// @jsxImportSource preact
import { CodeBlock } from "../components/CodeBlock.jsx";
import code from "../highlight/nostr-publisher.ts" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
export const NostrPublisherExample = () => {
useSyntaxHighlighting();
return (
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 class="card-title">📝 Nostr Note Publisher</h3>
<p class="mb-4">Publish notes to your CCN using the window.eve API:</p>
<CodeBlock language="typescript" code={code} />
</div>
</div>
);
};

View file

@ -0,0 +1,20 @@
// @jsx h
// @jsxImportSource preact
import { CodeBlock } from "../components/CodeBlock.jsx";
import code from "../highlight/preact-counter.tsx" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
export const PreactCounterExample = () => {
useSyntaxHighlighting();
return (
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 class="card-title"> Preact Counter with JSX</h3>
<p class="mb-4">A modern counter using Preact hooks and JSX syntax:</p>
<CodeBlock language="typescript" code={code} />
</div>
</div>
);
};

View file

@ -0,0 +1,55 @@
// @jsx h
// @jsxImportSource preact
import { CodeBlock } from "../components/CodeBlock.jsx";
import code from "../highlight/svelte-counter.svelte" with { type: "text" };
import { useSyntaxHighlighting } from "../hooks/useSyntaxHighlighting.js";
export const SvelteCounterExample = () => {
useSyntaxHighlighting();
return (
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h3 class="card-title">🔥 Svelte Counter</h3>
<p class="mb-4">A reactive counter built with Svelte's elegant syntax and built-in reactivity:</p>
<div class="bg-green-50 border-l-4 border-green-400 p-4 mb-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-green-700">
<strong>Why Svelte?</strong> Svelte compiles to vanilla JavaScript with no runtime overhead, making it
perfect for arxlets. Features like runes (<kbd class="kbd">$state()</kbd>,{" "}
<kbd class="kbd">$derived()</kbd>, etc), scoped CSS and intuitive event handling make development a
breeze.
</p>
</div>
</div>
</div>
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-amber-700">
<strong>Build Setup Note:</strong> Building Svelte for arxlets requires specific Vite configuration to
handle the compilation properly. While this adds some initial complexity, we've created a ready-to-use
template at{" "}
<a
href="https://git.arx-ccn.com/Arx/arxlets-template"
class="link link-primary"
target="_blank"
rel="noopener noreferrer"
>
arxlets-template
</a>{" "}
with all the correct configurations. We still highly recommend Svelte because once set up, the
development experience is incredibly smooth and optimal.
</p>
</div>
</div>
</div>
<CodeBlock language="svelte" code={code} />
</div>
</div>
);
};

View file

@ -0,0 +1 @@
bun build --minify --outfile=build.js --target=browser --production index.ts

View file

@ -0,0 +1,718 @@
# Arxlets API Context
## Overview
Arxlets are secure, sandboxed JavaScript applications that extend Eve's functionality. They run in isolated iframes and are registered on your CCN (Closed Community Network) for member-only access. Arxlets provide a powerful way to build custom applications that interact with Nostr events and profiles through Eve.
## Core Concepts
### What are Arxlets?
- **Sandboxed Applications**: Run in isolated iframes for security
- **JavaScript-based**: Written in TypeScript/JavaScript with wasm support coming in the future
- **CCN Integration**: Registered on your Closed Community Network
- **Nostr-native**: Built-in access to Nostr protocol operations
- **Real-time**: Support for live event subscriptions and updates
### CCN Local-First Architecture
CCNs (Closed Community Networks) are designed with a local-first approach that ensures data availability and functionality even when offline:
- **Local Data Storage**: All Nostr events and profiles are stored locally on your device, providing instant access without network dependencies
- **Offline Functionality**: Arxlets can read, display, and interact with locally cached data when disconnected from the internet
- **Sync When Connected**: When network connectivity is restored, the CCN automatically synchronizes with remote relays to fetch new events and propagate local changes
- **Resilient Operation**: Your applications continue to work seamlessly regardless of network conditions, making CCNs ideal for unreliable connectivity scenarios
- **Privacy by Design**: Local-first storage means your data remains on your device, reducing exposure to external services and improving privacy
### Architecture
- **Frontend**: TypeScript applications with render functions
- **Backend**: Eve relay providing Nostr protocol access
- **Communication**: window.eve API or direct WebSocket connections
## API Reference
### window.eve API
The primary interface for Arxlets to interact with Eve's Nostr relay. All methods return promises for async operations.
#### Event Operations
```typescript
// Publish a Nostr event
await window.eve.publish(event: NostrEvent): Promise<void>
// Get a specific event by ID
const event = await window.eve.getSingleEventById(id: string): Promise<NostrEvent | null>
// Get first event matching filter
const event = await window.eve.getSingleEventWithFilter(filter: Filter): Promise<NostrEvent | null>
// Get all events matching filter
const events = await window.eve.getAllEventsWithFilter(filter: Filter): Promise<NostrEvent[]>
```
#### Real-time Subscriptions
```typescript
// Subscribe to events with RxJS Observable
const subscription = window.eve.subscribeToEvents(filter: Filter): Observable<NostrEvent>
// Subscribe to profile updates
const profileSub = window.eve.subscribeToProfile(pubkey: string): Observable<Profile>
// Always unsubscribe when done
subscription.unsubscribe()
```
#### Profile Operations
```typescript
// Get user profile
const profile = await window.eve.getProfile(pubkey: string): Promise<Profile | null>
// Get user avatar URL
const avatarUrl = await window.eve.getAvatar(pubkey: string): Promise<string | null>
```
#### Cryptographic Operations
```typescript
// Sign an unsigned event
const signedEvent = await window.eve.signEvent(event: NostrEvent): Promise<NostrEvent>
// Get current user's public key
const pubkey = await window.eve.publicKey: Promise<string>
```
### WebSocket Alternative
For advanced use cases, connect directly to Eve's WebSocket relay, or use any nostr library. This is not recommended:
```typescript
const ws = new WebSocket("ws://localhost:6942");
// Send Nostr protocol messages
ws.send(JSON.stringify(["REQ", subscriptionId, filter]));
ws.send(JSON.stringify(["EVENT", event]));
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
```
## Type Definitions
```typescript
import { Observable } from "rxjs";
interface NostrEvent {
id?: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig?: string;
}
interface Filter {
ids?: string[];
authors?: string[];
kinds?: number[];
since?: number;
until?: number;
limit?: number;
[key: string]: any;
}
interface Profile {
name?: string;
about?: string;
picture?: string;
nip05?: string;
[key: string]: any;
}
interface WindowEve {
publish(event: NostrEvent): Promise<void>;
getSingleEventById(id: string): Promise<NostrEvent | null>;
getSingleEventWithFilter(filter: Filter): Promise<NostrEvent | null>;
getAllEventsWithFilter(filter: Filter): Promise<NostrEvent[]>;
subscribeToEvents(filter: Filter): Observable<NostrEvent>;
subscribeToProfile(pubkey: string): Observable<Profile>;
getProfile(pubkey: string): Promise<Profile | null>;
getAvatar(pubkey: string): Promise<string | null>;
signEvent(event: NostrEvent): Promise<NostrEvent>;
get publicKey(): Promise<string>;
}
// Global declarations for TypeScript
declare global {
interface Window {
eve: WindowEve;
nostr?: {
getPublicKey(): Promise<string>;
signEvent(event: NostrEvent): Promise<NostrEvent>;
getRelays?(): Promise<{
"ws://localhost:6942": { read: true; write: true };
}>;
};
}
}
```
## Registration
Arxlets are registered using Nostr events with kind `30420`:
```json
{
"kind": 30420,
"content": "",
"tags": [
["d", "unique-arxlet-id"],
["name", "Display Name"],
["description", "Brief description"],
["script", "export function render(container) { /* implementation */ }"],
["icon", "mdi:icon-name, #hexcolor"]
]
}
```
### Required Tags
- `d`: Unique identifier (alphanumeric, hyphens, underscores)
- `name`: Human-readable display name
- `script`: Complete JavaScript code with render export function
### Optional Tags
- `description`: Brief description of functionality
- `icon`: Iconify icon name and hex color
## Development Patterns
### Basic Arxlet Structure
```typescript
export function render(container: HTMLElement): void {
// Set up your UI
container.innerHTML = `
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">My Arxlet</h2>
<!-- Your content here -->
</div>
</div>
`;
// Add event listeners and logic
const button = container.querySelector("#myButton");
button?.addEventListener("click", handleClick);
}
async function handleClick() {
try {
// Use window.eve API
const events = await window.eve.getAllEventsWithFilter({
kinds: [1],
limit: 10,
});
// Update UI with events
} catch (error) {
console.error("Failed to fetch events:", error);
}
}
```
### Real-time Updates
```typescript
export function render(container: HTMLElement): void {
let subscription: Subscription;
// Set up UI
container.innerHTML = `<div id="events"></div>`;
const eventsContainer = container.querySelector("#events");
// Subscribe to real-time events
subscription = window.eve
.subscribeToEvents({
kinds: [1],
limit: 50,
})
.subscribe({
next: (event) => {
// Update UI with new event
const eventElement = document.createElement("div");
eventElement.textContent = event.content;
eventsContainer?.prepend(eventElement);
},
error: (err) => console.error("Subscription error:", err),
});
// Cleanup when arxlet is destroyed
window.addEventListener("beforeunload", () => {
subscription?.unsubscribe();
});
}
```
### Publishing Events
```typescript
async function publishNote(content: string): Promise<void> {
try {
const unsignedEvent: NostrEvent = {
kind: 1,
content: content,
tags: [["client", "my-arxlet"]],
created_at: Math.floor(Date.now() / 1000),
pubkey: await window.eve.publicKey,
};
const signedEvent = await window.eve.signEvent(unsignedEvent);
await window.eve.publish(signedEvent);
console.log("Event published successfully");
} catch (error) {
console.error("Failed to publish event:", error);
throw error;
}
}
```
## Best Practices
### Error Handling
- Always wrap API calls in try-catch blocks
- Check for null returns from query methods
- Provide user feedback for failed operations
### Performance
- Use specific filters to limit result sets
- Cache profile data to avoid repeated lookups
- Unsubscribe from observables when done
- Debounce rapid API calls
- Consider pagination for large datasets
### Security
- Validate all user inputs
- Sanitize content before displaying
- Use proper event signing for authenticity
- Follow principle of least privilege
### Memory Management
- Always unsubscribe from RxJS observables
- Clean up event listeners on component destruction
- Avoid memory leaks in long-running subscriptions
- Use weak references where appropriate
## Common Use Cases
### Social Feed
- Subscribe to events from followed users
- Display real-time updates
- Handle profile information and avatars
- Implement engagement features
### Publishing Tools
- Create and sign events
- Validate content before publishing
- Handle publishing errors gracefully
- Provide user feedback
### Data Visualization
- Query historical events
- Process and aggregate data
- Create interactive charts and graphs
- Real-time data updates
### Communication Apps
- Direct messaging interfaces
- Group chat functionality
- Notification systems
- Presence indicators
## Framework Integration
Arxlets support various JavaScript frameworks. All frameworks must export a `render` function that accepts a container element:
### Vanilla JavaScript
```typescript
export function render(container: HTMLElement): void {
// Set up your UI with direct DOM manipulation
container.innerHTML = `
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">My App</h2>
<button id="myButton" class="btn btn-primary">Click me</button>
</div>
</div>
`;
// Add event listeners
const button = container.querySelector("#myButton");
button?.addEventListener("click", () => {
console.log("Button clicked!");
});
}
```
### Preact/React
```typescript
// @jsx h
// @jsxImportSource preact
import { render } from 'preact';
import { useState } from 'preact/hooks';
const App = () => {
const [count, setCount] = useState(0);
return (
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Counter: {count}</h2>
<button
class="btn btn-primary"
onClick={() => setCount(count + 1)}
>
Increment
</button>
</div>
</div>
);
};
export function render(container: HTMLElement): void {
render(<App />, container);
}
```
### Svelte
```typescript
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
export function render(container: HTMLElement) {
return mount(App, {
target: container,
});
}
```
### Build Process
All frameworks require bundling into a single JavaScript file:
```bash
# For TypeScript/JavaScript projects
bun build index.ts --outfile=build.js --minify --target=browser --production
# The resulting build.js content goes in your registration event's script tag
```
#### Svelte Build Requirements
**Important:** The standard build command above will NOT work for Svelte projects. Svelte requires specific Vite configuration to compile properly.
For Svelte arxlets:
1. Use the [arxlets-template](https://git.arx-ccn.com/Arx/arxlets-template) which includes the correct Vite configuration
2. Run `bun run build` instead of the standard build command
3. Your compiled file will be available at `dist/bundle.js`
While the initial setup is more complex, Svelte provides an excellent development experience once configured, with features like:
- Built-in reactivity with runes (`$state()`, `$derived()`, etc.)
- Scoped CSS
- Compile-time optimizations
- No runtime overhead
## Debugging and Development
### Console Logging
- Use `console.log()` for debugging
- Events and errors are logged to browser console
### Error Handling
- Catch and log API errors
- Display user-friendly error messages
- Implement retry mechanisms for transient failures
### Testing
- Test with various event types and filters
- Verify subscription cleanup
- Test error scenarios and edge cases
- Validate event signing and publishing
## Limitations and Considerations
### Sandbox Restrictions
- Limited access to browser APIs
- No direct file system access
- Restricted network access (only to Eve relay)
- No access to parent window context
### Performance Constraints
- Iframe overhead for each arxlet
- Memory usage for subscriptions
- Event processing limitations
### Security Considerations
- All events are public on Nostr
- Private key management handled by Eve
- Content sanitization required
- XSS prevention necessary
## DaisyUI Components
Arxlets have access to DaisyUI 5, a comprehensive CSS component library. Use these pre-built components for consistent, accessible UI:
### Essential Components
```html
<!-- Cards for content containers -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Card Title</h2>
<p>Card content goes here</p>
<div class="card-actions justify-end">
<button class="btn btn-primary">Action</button>
</div>
</div>
</div>
<!-- Buttons with various styles -->
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-error">Error</button>
<button class="btn btn-ghost">Ghost</button>
<!-- Form controls -->
<div class="form-control">
<label class="label">
<span class="label-text">Input Label</span>
</label>
<input type="text" class="input input-bordered" placeholder="Enter text" />
</div>
<!-- Alerts for feedback -->
<div class="alert alert-success">
<span>✅ Success message</span>
</div>
<div class="alert alert-error">
<span>❌ Error message</span>
</div>
<!-- Loading states -->
<span class="loading loading-spinner loading-lg"></span>
<button class="btn btn-primary">
<span class="loading loading-spinner loading-sm"></span>
Loading...
</button>
<!-- Modals for dialogs -->
<dialog class="modal" id="my-modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Modal Title</h3>
<p class="py-4">Modal content</p>
<div class="modal-action">
<button class="btn" onclick="document.getElementById('my-modal').close()">
Close
</button>
</div>
</div>
</dialog>
```
### Layout Utilities
```html
<!-- Responsive grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="card">Content 1</div>
<div class="card">Content 2</div>
<div class="card">Content 3</div>
</div>
<!-- Flexbox utilities -->
<div class="flex justify-between items-center">
<span>Left content</span>
<button class="btn">Right button</button>
</div>
<!-- Spacing -->
<div class="p-4 m-2 space-y-4">
<!-- p-4 = padding, m-2 = margin, space-y-4 = vertical spacing -->
</div>
```
### Color System
```html
<!-- Background colors -->
<div class="bg-base-100">Default background</div>
<div class="bg-base-200">Slightly darker</div>
<div class="bg-primary">Primary color</div>
<div class="bg-secondary">Secondary color</div>
<!-- Text colors -->
<span class="text-primary">Primary text</span>
<span class="text-success">Success text</span>
<span class="text-error">Error text</span>
<span class="text-base-content">Default text</span>
```
## Complete Example Patterns
### Simple Counter Arxlet
```typescript
export function render(container: HTMLElement): void {
let count = 0;
function updateUI() {
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center">Counter</h2>
<div class="text-6xl font-bold my-4 ${count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary"}">
${count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" id="decrement"></button>
<button class="btn btn-success" id="increment">+</button>
<button class="btn btn-ghost" id="reset">Reset</button>
</div>
</div>
</div>
`;
// Attach event listeners
container.querySelector("#increment")?.addEventListener("click", () => {
count++;
updateUI();
});
container.querySelector("#decrement")?.addEventListener("click", () => {
count--;
updateUI();
});
container.querySelector("#reset")?.addEventListener("click", () => {
count = 0;
updateUI();
});
}
updateUI();
}
```
### Nostr Event Publisher
```typescript
export async function render(container: HTMLElement): Promise<void> {
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
<div class="card-body">
<h2 class="card-title">📝 Publish a Note</h2>
<div class="form-control">
<label class="label">
<span class="label-text">What's on your mind?</span>
<span class="label-text-alt" id="charCount">0/280</span>
</label>
<textarea
class="textarea textarea-bordered h-32"
id="noteContent"
placeholder="Share your thoughts..."
maxlength="280">
</textarea>
</div>
<div class="card-actions justify-between items-center">
<div id="status" class="flex-1"></div>
<button class="btn btn-primary" id="publishBtn" disabled>
Publish Note
</button>
</div>
</div>
</div>
`;
const textarea =
container.querySelector<HTMLTextAreaElement>("#noteContent")!;
const publishBtn = container.querySelector<HTMLButtonElement>("#publishBtn")!;
const status = container.querySelector<HTMLDivElement>("#status")!;
const charCount = container.querySelector<HTMLSpanElement>("#charCount")!;
textarea.oninput = () => {
const length = textarea.value.length;
charCount.textContent = `${length}/280`;
publishBtn.disabled = length === 0 || length > 280;
};
publishBtn.onclick = async () => {
const content = textarea.value.trim();
if (!content) return;
publishBtn.disabled = true;
publishBtn.textContent = "Publishing...";
status.innerHTML =
'<span class="loading loading-spinner loading-sm"></span>';
try {
const unsignedEvent: NostrEvent = {
kind: 1,
content: content,
tags: [["client", "my-arxlet"]],
created_at: Math.floor(Date.now() / 1000),
pubkey: await window.eve.publicKey,
};
const signedEvent = await window.eve.signEvent(unsignedEvent);
await window.eve.publish(signedEvent);
status.innerHTML = `
<div class="alert alert-success">
<span>✅ Note published successfully!</span>
</div>
`;
textarea.value = "";
textarea.oninput?.();
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("Publishing failed:", error);
status.innerHTML = `
<div class="alert alert-error">
<span>❌ Failed to publish: ${errorMessage}</span>
</div>
`;
} finally {
publishBtn.disabled = false;
publishBtn.textContent = "Publish Note";
}
};
}
```
This context provides comprehensive information about the Arxlets API, enabling LLMs to understand and work with the system effectively.

View file

@ -0,0 +1,44 @@
export function render(container: HTMLElement) {
let count: number = 0;
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center">Counter App</h2>
<div class="text-6xl font-bold text-primary my-4" id="display">
${count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" id="decrement"></button>
<button class="btn btn-success" id="increment">+</button>
<button class="btn btn-ghost" id="reset">Reset</button>
</div>
</div>
</div>
`;
const display = container.querySelector<HTMLDivElement>("#display")!;
const incrementBtn = container.querySelector<HTMLButtonElement>("#increment")!;
const decrementBtn = container.querySelector<HTMLButtonElement>("#decrement")!;
const resetBtn = container.querySelector<HTMLButtonElement>("#reset")!;
const updateDisplay = (): void => {
display.textContent = count.toString();
display.className = `text-6xl font-bold my-4 ${
count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary"
}`;
};
incrementBtn.onclick = (): void => {
count++;
updateDisplay();
};
decrementBtn.onclick = (): void => {
count--;
updateDisplay();
};
resetBtn.onclick = (): void => {
count = 0;
updateDisplay();
};
}

View file

@ -0,0 +1,55 @@
// Using window.eve API for Nostr operations
import type { Filter, NostrEvent } from "./types";
// Publish a new event
const event: NostrEvent = {
kind: 1,
content: "Hello from my Arxlet!",
tags: [],
created_at: Math.floor(Date.now() / 1000),
pubkey: "your-pubkey-here",
};
await window.eve.publish(event);
// Get a specific event by ID
const eventId = "event-id-here";
const event = await window.eve.getSingleEventById(eventId);
// Query events with a filter
const filter: Filter = {
kinds: [1],
authors: ["pubkey-here"],
limit: 10,
};
const singleEvent = await window.eve.getSingleEventWithFilter(filter);
const allEvents = await window.eve.getAllEventsWithFilter(filter);
// Real-time subscription with RxJS Observable
const subscription = window.eve.subscribeToEvents(filter).subscribe({
next: (event) => {
console.log("New event received:", event);
// Update your UI with the new event
},
error: (err) => console.error("Subscription error:", err),
complete: () => console.log("Subscription completed"),
});
// Subscribe to profile updates for a specific user
const profileSubscription = window.eve.subscribeToProfile(pubkey).subscribe({
next: (profile) => {
console.log("Profile updated:", profile);
// Update your UI with the new profile data
},
error: (err) => console.error("Profile subscription error:", err),
});
// Don't forget to unsubscribe when done
// subscription.unsubscribe();
// profileSubscription.unsubscribe();
// Get user profile and avatar
const pubkey = "user-pubkey-here";
const profile = await window.eve.getProfile(pubkey);
const avatarUrl = await window.eve.getAvatar(pubkey);

View file

@ -0,0 +1,85 @@
import type { NostrEvent } from "./type-definitions.ts";
export async function render(container: HTMLElement): Promise<void> {
container.innerHTML = `
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
<div class="card-body">
<h2 class="card-title">📝 Publish a Note</h2>
<div class="form-control">
<label class="label">
<span class="label-text">What's on your mind?</span>
<span class="label-text-alt" id="charCount">0/280</span>
</label>
<textarea
class="textarea textarea-bordered h-32"
id="noteContent"
placeholder="Share your thoughts with your CCN..."
maxlength="280">
</textarea>
</div>
<div class="card-actions justify-between items-center">
<div id="status" class="flex-1"></div>
<button class="btn btn-primary" id="publishBtn" disabled>
Publish Note
</button>
</div>
</div>
</div>
`;
const textarea = container.querySelector<HTMLTextAreaElement>("#noteContent")!;
const publishBtn = container.querySelector<HTMLButtonElement>("#publishBtn")!;
const status = container.querySelector<HTMLDivElement>("#status")!;
const charCount = container.querySelector<HTMLSpanElement>("#charCount")!;
textarea.oninput = (): void => {
const length: number = textarea.value.length;
charCount.textContent = `${length}/280`;
publishBtn.disabled = length === 0 || length > 280;
};
publishBtn.onclick = async (e): Promise<void> => {
const content: string = textarea.value.trim();
if (!content) return;
publishBtn.disabled = true;
publishBtn.textContent = "Publishing...";
status.innerHTML = '<span class="loading loading-spinner loading-sm"></span>';
try {
const unsignedEvent: NostrEvent = {
kind: 1, // Text note
content: content,
tags: [["client", "arxlet-publisher"]],
created_at: Math.floor(Date.now() / 1000),
pubkey: await window.eve.publicKey,
};
const signedEvent: NostrEvent = await window.eve.signEvent(unsignedEvent);
await window.eve.publish(signedEvent);
status.innerHTML = `
<div class="alert alert-success">
<span> Note published successfully!</span>
</div>
`;
textarea.value = "";
textarea.oninput?.(e);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.error("Publishing failed:", error);
status.innerHTML = `
<div class="alert alert-error">
<span> Failed to publish: ${errorMessage}</span>
</div>
`;
} finally {
publishBtn.disabled = false;
publishBtn.textContent = "Publish Note";
}
};
}

View file

@ -0,0 +1,61 @@
// @jsx h
// @jsxImportSource preact
import { render as renderPreact } from "preact";
import { useState } from "preact/hooks";
const CounterApp = () => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("");
const increment = () => {
setCount((prev) => prev + 1);
setMessage(`Clicked ${count + 1} times!`);
};
const decrement = () => {
setCount((prev) => prev - 1);
setMessage(`Count decreased to ${count - 1}`);
};
const reset = () => {
setCount(0);
setMessage("Counter reset!");
};
return (
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center"> Preact Counter </h2>
<div
class={`text-6xl font-bold my-4 ${count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary"}`}
>
{count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" onClick={decrement}>
</button>
<button class="btn btn-success" onClick={increment}>
+
</button>
<button class="btn btn-ghost" onClick={reset}>
Reset
</button>
</div>
{message && (
<div class="alert alert-info mt-4">
<span>{message} </span>
</div>
)}
</div>
</div>
);
};
export function render(container: HTMLElement): void {
renderPreact(<CounterApp />, container);
}

View file

@ -0,0 +1,12 @@
{
"kind": 30420,
"tags": [
["d", "my-calculator"],
["name", "Simple Calculator"],
["description", "A basic calculator for quick math"],
["script", "export function render(el) { /* your code */ }"],
["icon", "mdi:calculator", "#3b82f6"]
],
"content": "",
"created_at": 1735171200
}

View file

@ -0,0 +1,23 @@
/**
* Required export function - Entry point for your Arxlet
*/
export function render(container: HTMLElement): void {
// Initialize your application
container.innerHTML = `
<div class="p-6">
<h1 class="text-3xl font-bold mb-4">My Arxlet</h1>
<p class="text-lg">Hello from Eve!</p>
<button class="btn btn-primary mt-4" id="myButton">
Click me!
</button>
</div>
`;
// Add event listeners with proper typing
const button = container.querySelector<HTMLButtonElement>("#myButton");
button?.addEventListener("click", (): void => {
alert("Button clicked!");
});
// Your app logic here...
}

View file

@ -0,0 +1,54 @@
// Real-time subscription examples
import { filter, map, takeUntil } from "rxjs/operators";
// Basic subscription
const subscription = window.eve
.subscribeToEvents({
kinds: [1], // Text notes
limit: 50,
})
.subscribe((event) => {
console.log("New text note:", event.content);
});
// Advanced filtering with RxJS operators
const filteredSubscription = window.eve
.subscribeToEvents({
kinds: [1, 6, 7], // Notes, reposts, reactions
authors: ["pubkey1", "pubkey2"],
})
.pipe(
filter((event) => event.content.includes("#arxlet")), // Only events mentioning arxlets
map((event) => ({
id: event.id,
author: event.pubkey,
content: event.content,
timestamp: new Date(event.created_at * 1000),
})),
)
.subscribe({
next: (processedEvent) => {
// Update your UI
updateEventsList(processedEvent);
},
error: (err) => {
console.error("Subscription error:", err);
showErrorMessage("Failed to receive real-time updates");
},
});
// Profile subscription example
const profileSubscription = window.eve.subscribeToProfile("user-pubkey-here").subscribe({
next: (profile) => {
console.log("Profile updated:", profile);
updateUserProfile(profile);
},
error: (err) => {
console.error("Profile subscription error:", err);
},
});
// Clean up subscriptions when component unmounts
// subscription.unsubscribe();
// filteredSubscription.unsubscribe();
// profileSubscription.unsubscribe();

View file

@ -0,0 +1,49 @@
<script lang="ts">
let count = $state(0);
let message = $state("");
function increment() {
count += 1;
message = `Clicked ${count} times!`;
}
function decrement() {
count -= 1;
message = `Count decreased to ${count}`;
}
function reset() {
count = 0;
message = "Counter reset!";
}
const countColor = $derived(count > 0 ? "text-success" : count < 0 ? "text-error" : "text-primary");
</script>
<div class="card bg-base-100 shadow-xl max-w-sm mx-auto">
<div class="card-body text-center">
<h2 class="card-title justify-center">🔥 Svelte Counter</h2>
<div class="text-6xl font-bold my-4 {countColor}">
{count}
</div>
<div class="card-actions justify-center gap-4">
<button class="btn btn-error" onclick={decrement}> </button>
<button class="btn btn-success" onclick={increment}> + </button>
<button class="btn btn-ghost" onclick={reset}> Reset </button>
</div>
{#if message}
<div class="alert alert-info mt-4">
<span>{message}</span>
</div>
{/if}
</div>
</div>
<style>
.card-title {
color: var(--primary);
}
</style>

View file

@ -0,0 +1,48 @@
import type { Observable } from "rxjs";
export interface NostrEvent {
id?: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig?: string;
}
export interface Filter {
ids?: string[];
authors?: string[];
kinds?: number[];
since?: number;
until?: number;
limit?: number;
[key: string]: any;
}
export interface Profile {
name?: string;
about?: string;
picture?: string;
nip05?: string;
[key: string]: any;
}
export interface WindowEve {
publish(event: NostrEvent): Promise<void>;
getSingleEventById(id: string): Promise<NostrEvent | null>;
getSingleEventWithFilter(filter: Filter): Promise<NostrEvent | null>;
getAllEventsWithFilter(filter: Filter): Promise<NostrEvent[]>;
subscribeToEvents(filter: Filter): Observable<NostrEvent>;
subscribeToProfile(pubkey: string): Observable<Profile>;
getProfile(pubkey: string): Promise<Profile | null>;
getAvatar(pubkey: string): Promise<string | null>;
signEvent(event: NostrEvent): Promise<NostrEvent>;
get publicKey(): Promise<string>;
}
declare global {
interface Window {
eve: WindowEve;
}
}

View file

@ -0,0 +1,18 @@
// Alternative: Direct WebSocket connection
const ws = new WebSocket("ws://localhost:6942");
ws.onopen = () => {
// Subscribe to events
ws.send(JSON.stringify(["REQ", "sub1", { kinds: [1], limit: 10 }]));
};
ws.onmessage = (event) => {
const [type, subId, data] = JSON.parse(event.data);
if (type === "EVENT") {
console.log("Received event:", data);
}
};
// Publish an event
const signedEvent = await window.nostr.signEvent(unsignedEvent);
ws.send(JSON.stringify(["EVENT", signedEvent]));

View file

@ -0,0 +1,30 @@
import { useEffect } from "preact/hooks";
import Prism from "prismjs";
import "prismjs/components/prism-json";
import "prismjs/components/prism-javascript";
import "prismjs/components/prism-typescript";
import "prismjs/components/prism-bash";
import "prism-svelte";
/**
* Custom hook for managing syntax highlighting
* Handles initialization and tab change events
*/
export const useSyntaxHighlighting = () => {
useEffect(() => {
const highlightCode = () => setTimeout(() => Prism.highlightAll(), 100);
highlightCode();
const tabInputs = document.querySelectorAll('input[name="arxlet_tabs"]');
tabInputs.forEach((input) => {
input.addEventListener("change", highlightCode);
});
return () => {
tabInputs.forEach((input) => {
input.removeEventListener("change", highlightCode);
});
};
}, []);
};