initial version (alpha)
This commit is contained in:
commit
d16d7a128f
57 changed files with 11087 additions and 0 deletions
1825
src/pages/docs/arxlets/arxlet-docs-out.html
Normal file
1825
src/pages/docs/arxlets/arxlet-docs-out.html
Normal file
File diff suppressed because it is too large
Load diff
979
src/pages/docs/arxlets/arxlet-docs.adoc
Normal file
979
src/pages/docs/arxlets/arxlet-docs.adoc
Normal 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";
|
||||
}
|
||||
};
|
||||
}
|
||||
----
|
746
src/pages/docs/arxlets/arxlet-docs.css
Normal file
746
src/pages/docs/arxlets/arxlet-docs.css
Normal 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);
|
||||
}
|
||||
}
|
26
src/pages/docs/arxlets/arxlet-docs.html
Normal file
26
src/pages/docs/arxlets/arxlet-docs.html
Normal 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>
|
343
src/pages/docs/arxlets/arxlet-docs.jsx
Normal file
343
src/pages/docs/arxlets/arxlet-docs.jsx
Normal 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);
|
678
src/pages/docs/arxlets/components/APISection.jsx
Normal file
678
src/pages/docs/arxlets/components/APISection.jsx
Normal 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<void></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<NostrEvent></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<NostrEvent | null></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<NostrEvent | null></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<NostrEvent[]></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<NostrEvent></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<Profile></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<Profile | null></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<string | null></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<string></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>
|
||||
);
|
||||
};
|
628
src/pages/docs/arxlets/components/BestPracticesSection.jsx
Normal file
628
src/pages/docs/arxlets/components/BestPracticesSection.jsx
Normal 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>
|
||||
);
|
||||
};
|
53
src/pages/docs/arxlets/components/CodeBlock.jsx
Normal file
53
src/pages/docs/arxlets/components/CodeBlock.jsx
Normal 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>
|
||||
);
|
||||
};
|
471
src/pages/docs/arxlets/components/DevelopmentSection.jsx
Normal file
471
src/pages/docs/arxlets/components/DevelopmentSection.jsx
Normal 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><div></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>
|
||||
);
|
||||
};
|
60
src/pages/docs/arxlets/components/ExamplesSection.jsx
Normal file
60
src/pages/docs/arxlets/components/ExamplesSection.jsx
Normal 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>
|
||||
);
|
||||
};
|
15
src/pages/docs/arxlets/components/LLMsSection.jsx
Normal file
15
src/pages/docs/arxlets/components/LLMsSection.jsx
Normal 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>
|
||||
);
|
||||
};
|
50
src/pages/docs/arxlets/components/OverviewSection.jsx
Normal file
50
src/pages/docs/arxlets/components/OverviewSection.jsx
Normal 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>
|
||||
);
|
||||
};
|
147
src/pages/docs/arxlets/components/RegistrationSection.jsx
Normal file
147
src/pages/docs/arxlets/components/RegistrationSection.jsx
Normal 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>
|
||||
);
|
||||
};
|
20
src/pages/docs/arxlets/examples/CounterExample.jsx
Normal file
20
src/pages/docs/arxlets/examples/CounterExample.jsx
Normal 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>
|
||||
);
|
||||
};
|
623
src/pages/docs/arxlets/examples/DevelopmentTips.jsx
Normal file
623
src/pages/docs/arxlets/examples/DevelopmentTips.jsx
Normal 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>
|
||||
);
|
||||
};
|
20
src/pages/docs/arxlets/examples/NostrPublisherExample.jsx
Normal file
20
src/pages/docs/arxlets/examples/NostrPublisherExample.jsx
Normal 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>
|
||||
);
|
||||
};
|
20
src/pages/docs/arxlets/examples/PreactCounterExample.jsx
Normal file
20
src/pages/docs/arxlets/examples/PreactCounterExample.jsx
Normal 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>
|
||||
);
|
||||
};
|
55
src/pages/docs/arxlets/examples/SvelteCounterExample.jsx
Normal file
55
src/pages/docs/arxlets/examples/SvelteCounterExample.jsx
Normal 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>
|
||||
);
|
||||
};
|
1
src/pages/docs/arxlets/highlight/build-command.sh
Normal file
1
src/pages/docs/arxlets/highlight/build-command.sh
Normal file
|
@ -0,0 +1 @@
|
|||
bun build --minify --outfile=build.js --target=browser --production index.ts
|
718
src/pages/docs/arxlets/highlight/context.md
Normal file
718
src/pages/docs/arxlets/highlight/context.md
Normal 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.
|
44
src/pages/docs/arxlets/highlight/counter.ts
Normal file
44
src/pages/docs/arxlets/highlight/counter.ts
Normal 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();
|
||||
};
|
||||
}
|
55
src/pages/docs/arxlets/highlight/eve-api-example.ts
Normal file
55
src/pages/docs/arxlets/highlight/eve-api-example.ts
Normal 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);
|
85
src/pages/docs/arxlets/highlight/nostr-publisher.ts
Normal file
85
src/pages/docs/arxlets/highlight/nostr-publisher.ts
Normal 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";
|
||||
}
|
||||
};
|
||||
}
|
61
src/pages/docs/arxlets/highlight/preact-counter.tsx
Normal file
61
src/pages/docs/arxlets/highlight/preact-counter.tsx
Normal 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);
|
||||
}
|
12
src/pages/docs/arxlets/highlight/registration-event.json
Normal file
12
src/pages/docs/arxlets/highlight/registration-event.json
Normal 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
|
||||
}
|
23
src/pages/docs/arxlets/highlight/render-function.ts
Normal file
23
src/pages/docs/arxlets/highlight/render-function.ts
Normal 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...
|
||||
}
|
54
src/pages/docs/arxlets/highlight/subscription-examples.ts
Normal file
54
src/pages/docs/arxlets/highlight/subscription-examples.ts
Normal 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();
|
49
src/pages/docs/arxlets/highlight/svelte-counter.svelte
Normal file
49
src/pages/docs/arxlets/highlight/svelte-counter.svelte
Normal 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>
|
48
src/pages/docs/arxlets/highlight/type-definitions.ts
Normal file
48
src/pages/docs/arxlets/highlight/type-definitions.ts
Normal 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;
|
||||
}
|
||||
}
|
18
src/pages/docs/arxlets/highlight/websocket-example.ts
Normal file
18
src/pages/docs/arxlets/highlight/websocket-example.ts
Normal 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]));
|
30
src/pages/docs/arxlets/hooks/useSyntaxHighlighting.js
Normal file
30
src/pages/docs/arxlets/hooks/useSyntaxHighlighting.js
Normal 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);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue