diff --git a/.claude/skills/lexicons/SKILL.md b/.claude/skills/lexicons/SKILL.md new file mode 100644 index 00000000..3d04e88b --- /dev/null +++ b/.claude/skills/lexicons/SKILL.md @@ -0,0 +1,304 @@ +--- +name: lexicons +description: Reference for the AT Protocol lexicon system. Use when working with lexicons, adding new record types, or modifying AT Protocol schemas. +user-invocable: false +--- + +# Lexicon System + +## Overview + +Lexicons define the schema for AT Protocol records. This project has two namespaces: +- **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.) +- **`site.standard.*`** - Standard site lexicons for interoperability + +The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`. + +## Key Files + +- **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons +- **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained) +- **`lexicons/build.ts`** - Builds TypeScript sources to JSON +- **`lexicons/api/`** - Generated TypeScript types and client +- **`package.json`** - Contains `lexgen` script + +## Running Lexicon Generation + +```bash +npm run lexgen +``` + +This runs: +1. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript +2. `lex gen-api` - Generates TypeScript types from all JSON lexicons +3. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions + +## Adding a New pub.leaflet Lexicon + +### 1. Create the Source Definition + +Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`): + +```typescript +import { LexiconDoc } from "@atproto/lexicon"; + +export const PubLeafletMyLexicon: LexiconDoc = { + lexicon: 1, + id: "pub.leaflet.myLexicon", + defs: { + main: { + type: "record", // or "object" for non-record types + key: "tid", + record: { + type: "object", + required: ["field1"], + properties: { + field1: { type: "string", maxLength: 1000 }, + field2: { type: "integer", minimum: 0 }, + optionalRef: { type: "ref", ref: "other.lexicon#def" }, + }, + }, + }, + // Additional defs for sub-objects + subType: { + type: "object", + properties: { + nested: { type: "string" }, + }, + }, + }, +}; +``` + +### 2. Add to Build + +Update `lexicons/build.ts`: + +```typescript +import { PubLeafletMyLexicon } from "./src/myLexicon"; + +const lexicons = [ + // ... existing lexicons + PubLeafletMyLexicon, +]; +``` + +### 3. Update lexgen Command (if needed) + +If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`: + +```json +"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..." +``` + +Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included. + +### 4. Add to authFullPermissions (for record types) + +If your lexicon is a record type that users should be able to create/update/delete, add it to the `authFullPermissions` permission set in `lexicons/src/authFullPermissions.ts`: + +```typescript +import { PubLeafletMyLexicon } from "./myLexicon"; + +// In the permissions collection array: +collection: [ + // ... existing lexicons + PubLeafletMyLexicon.id, +], +``` + +### 5. Regenerate Types + +```bash +npm run lexgen +``` + +### 6. Use the Generated Types + +```typescript +import { PubLeafletMyLexicon } from "lexicons/api"; + +// Type for the record +type MyRecord = PubLeafletMyLexicon.Record; + +// Validation +const result = PubLeafletMyLexicon.validateRecord(data); +if (result.success) { + // result.value is typed +} + +// Type guard +if (PubLeafletMyLexicon.isRecord(data)) { + // data is typed as Record +} +``` + +## Adding a New site.standard Lexicon + +### 1. Create the JSON Definition + +Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`): + +```json +{ + "lexicon": 1, + "id": "site.standard.myType", + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["field1"], + "properties": { + "field1": { + "type": "string", + "maxLength": 1000 + } + } + } + } + } +} +``` + +### 2. Regenerate Types + +```bash +npm run lexgen +``` + +The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files. + +## Common Lexicon Patterns + +### Referencing Other Lexicons + +```typescript +// Reference another lexicon's main def +{ type: "ref", ref: "pub.leaflet.publication" } + +// Reference a specific def within a lexicon +{ type: "ref", ref: "pub.leaflet.publication#theme" } + +// Reference within the same lexicon +{ type: "ref", ref: "#myDef" } +``` + +### Union Types + +```typescript +{ + type: "union", + refs: [ + "pub.leaflet.pages.linearDocument", + "pub.leaflet.pages.canvas", + ], +} + +// Open union (allows unknown types) +{ + type: "union", + closed: false, // default is true + refs: ["pub.leaflet.content"], +} +``` + +### Blob Types (for images/files) + +```typescript +{ + type: "blob", + accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"] + maxSize: 1000000, // bytes +} +``` + +### Color Types + +The project has color types defined: +- `pub.leaflet.theme.color#rgb` / `#rgba` +- `site.standard.theme.color#rgb` / `#rgba` + +```typescript +// In lexicons/src/theme.ts +export const ColorUnion = { + type: "union", + refs: [ + "pub.leaflet.theme.color#rgba", + "pub.leaflet.theme.color#rgb", + ], +}; +``` + +## Normalization Between Formats + +Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats: + +```typescript +import { + normalizeDocument, + normalizePublication, + isLeafletDocument, + isStandardDocument, + getDocumentPages, +} from "lexicons/src/normalize"; + +// Normalize a document from either format +const normalized = normalizeDocument(record); +if (normalized) { + // normalized is always in site.standard.document format + console.log(normalized.title, normalized.site); + + // Get pages if content is pub.leaflet.content + const pages = getDocumentPages(normalized); +} + +// Normalize a publication +const pub = normalizePublication(record); +if (pub) { + console.log(pub.name, pub.url); +} +``` + +## Handling in Appview (Firehose Consumer) + +When processing records from the firehose in `appview/index.ts`: + +```typescript +import { ids } from "lexicons/api/lexicons"; +import { PubLeafletMyLexicon } from "lexicons/api"; + +// In filterCollections: +filterCollections: [ + ids.PubLeafletMyLexicon, + // ... +], + +// In handleEvent: +if (evt.collection === ids.PubLeafletMyLexicon) { + if (evt.event === "create" || evt.event === "update") { + let record = PubLeafletMyLexicon.validateRecord(evt.record); + if (!record.success) return; + + // Store in database + await supabase.from("my_table").upsert({ + uri: evt.uri.toString(), + data: record.value as Json, + }); + } + if (evt.event === "delete") { + await supabase.from("my_table").delete().eq("uri", evt.uri.toString()); + } +} +``` + +## Publishing Lexicons + +To publish lexicons to an AT Protocol PDS: + +```bash +npm run publish-lexicons +``` + +This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS. diff --git a/.claude/skills/notifications/SKILL.md b/.claude/skills/notifications/SKILL.md new file mode 100644 index 00000000..908953f5 --- /dev/null +++ b/.claude/skills/notifications/SKILL.md @@ -0,0 +1,150 @@ +--- +name: notifications +description: Reference for the notification system. Use when adding new notification types or modifying notification handling. +user-invocable: false +--- + +# Notification System + +## Overview + +Notifications are stored in the database and hydrated with related data before being rendered. The system supports multiple notification types (comments, subscriptions, etc.) that are processed in parallel. + +## Key Files + +- **`src/notifications.ts`** - Core notification types and hydration logic +- **`app/(home-pages)/notifications/NotificationList.tsx`** - Renders all notification types +- **`app/(home-pages)/notifications/Notification.tsx`** - Base notification component +- Individual notification components (e.g., `CommentNotification.tsx`, `FollowNotification.tsx`) + +## Adding a New Notification Type + +### 1. Update Notification Data Types (`src/notifications.ts`) + +Add your type to the `NotificationData` union: + +```typescript +export type NotificationData = + | { type: "comment"; comment_uri: string; parent_uri?: string } + | { type: "subscribe"; subscription_uri: string } + | { type: "your_type"; your_field: string }; // Add here +``` + +Add to the `HydratedNotification` union: + +```typescript +export type HydratedNotification = + | HydratedCommentNotification + | HydratedSubscribeNotification + | HydratedYourNotification; // Add here +``` + +### 2. Create Hydration Function (`src/notifications.ts`) + +```typescript +export type HydratedYourNotification = Awaited< + ReturnType +>[0]; + +async function hydrateYourNotifications(notifications: NotificationRow[]) { + const yourNotifications = notifications.filter( + (n): n is NotificationRow & { data: ExtractNotificationType<"your_type"> } => + (n.data as NotificationData)?.type === "your_type", + ); + + if (yourNotifications.length === 0) return []; + + // Fetch related data with joins + const { data } = await supabaseServerClient + .from("your_table") + .select("*, related_table(*)") + .in("uri", yourNotifications.map((n) => n.data.your_field)); + + return yourNotifications.map((notification) => ({ + id: notification.id, + recipient: notification.recipient, + created_at: notification.created_at, + type: "your_type" as const, + your_field: notification.data.your_field, + yourData: data?.find((d) => d.uri === notification.data.your_field)!, + })); +} +``` + +Add to `hydrateNotifications` parallel array: + +```typescript +const [commentNotifications, subscribeNotifications, yourNotifications] = await Promise.all([ + hydrateCommentNotifications(notifications), + hydrateSubscribeNotifications(notifications), + hydrateYourNotifications(notifications), // Add here +]); + +const allHydrated = [...commentNotifications, ...subscribeNotifications, ...yourNotifications]; +``` + +### 3. Trigger the Notification (in your action file) + +```typescript +import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; +import { v7 } from "uuid"; + +// When the event occurs: +const recipient = /* determine who should receive it */; +if (recipient !== currentUser) { + const notification: Notification = { + id: v7(), + recipient, + data: { + type: "your_type", + your_field: "value", + }, + }; + await supabaseServerClient.from("notifications").insert(notification); + await pingIdentityToUpdateNotification(recipient); +} +``` + +### 4. Create Notification Component + +Create a new component (e.g., `YourNotification.tsx`): + +```typescript +import { HydratedYourNotification } from "src/notifications"; +import { Notification } from "./Notification"; + +export const YourNotification = (props: HydratedYourNotification) => { + // Extract data from props.yourData + + return ( + Message to display} + content={/* optional additional content */} + /> + ); +}; +``` + +### 5. Update NotificationList (`NotificationList.tsx`) + +Import and render your notification type: + +```typescript +import { YourNotification } from "./YourNotification"; + +// In the map function: +if (n.type === "your_type") { + return ; +} +``` + +## Example: Subscribe Notifications + +See the implementation in: +- `src/notifications.ts:88-125` - Hydration logic +- `app/lish/subscribeToPublication.ts:55-68` - Trigger +- `app/(home-pages)/notifications/FollowNotification.tsx` - Component +- `app/(home-pages)/notifications/NotificationList.tsx:40-42` - Rendering diff --git a/.claude/skills/spec/SKILL.md b/.claude/skills/spec/SKILL.md new file mode 100644 index 00000000..dbc201e9 --- /dev/null +++ b/.claude/skills/spec/SKILL.md @@ -0,0 +1,85 @@ +--- +name: spec +description: Draft design documents and specs with research-informed questioning +disable-model-invocation: true +--- + +# Spec Writing + +## When to Use + +Use this skill when the user explicitly requests a spec, design document, or similar planning artifact. Do not auto-trigger for routine tasks. + +## Process + +### 1. Initial Scoping + +When starting a spec: +- Ask the user for a brief description of what they want to build/change +- Reason about which question categories matter for this particular work—derive these from the problem, not a checklist + +### 2. Interrogation Loop + +Conduct open-ended questioning until the designer signals completion: + +**Research-informed questions**: Before asking about an area, do just-in-time codebase research. Find relevant files, understand current patterns, identify constraints. Reference specific files and functions in questions when it adds clarity. + +**Adaptive scope**: When the designer marks something as out of scope, update your mental model and don't revisit. However, flag when you believe there are gaps that could cause problems: +- "This approach assumes X—is that intentional?" +- "The current implementation of Y in `src/foo.ts:42` handles Z differently. Should we align or diverge?" + +**Question style**: +- Ask one focused question at a time, or a small related cluster +- Ground questions in what you've learned from the codebase +- Avoid hypotheticals that don't apply to this codebase +- When presenting options, describe tradeoffs without recommending unless asked + +### 3. Spec Writing + +When the designer indicates readiness, produce the spec document. + +## Output + +Save to: `/specs/YYYY-MM-DD-short-name.md` + +### Required Elements + +**Title and status** (draft | approved | implemented) + +**Goal**: What this achieves and why. 1-3 sentences. + +**Design**: The substance of what will be built/changed. For each significant component or concern: +- Describe the approach +- State key decisions with their rationale inline +- Reference specific files, functions, and types where relevant +- Structure subsections to fit the problem—no fixed format + +**Implementation**: Ordered steps that an agent or developer can execute. Each step should: +- Be concrete and actionable +- Reference specific files/functions to modify or create +- Be sequenced correctly (dependencies before dependents) + +### Optional + +- Background context (only if necessary for understanding) +- Open questions (only if unresolved items remain) + +Add other sections if the problem demands it. + +## Writing Style + +- No filler, hedging, or preamble +- No "This document describes..." or "In this spec we will..." +- Start sections with substance, not meta-commentary +- Use precise technical language +- Keep decisions and rationale tight—one sentence each when possible +- Code references use `file/path.ts:lineNumber` or `functionName` in backticks +- Prefer concrete over abstract; specific over general + +## Status Lifecycle + +- **draft**: Work in progress. Should not be merged to main. +- **approved**: Designer has signed off. Ready for implementation. +- **implemented**: Work is complete. Spec is now historical record. + +Update status in the document as it progresses. Specs are point-in-time snapshots—do not update content after implementation begins except to change status.