👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

start on lexicon

+656 -24
+85
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Project Overview 6 + 7 + **deckbelcher** is a TanStack Start application with: 8 + - React 19 + TanStack Router (file-based routing) 9 + - TanStack Query for data fetching 10 + - Tailwind CSS v4 for styling 11 + - TypeSpec/Typelex for lexicon schema generation 12 + - Vitest for testing 13 + - Biome for linting/formatting 14 + 15 + ## Development Commands 16 + 17 + ```bash 18 + # Development server (runs on port 3000) 19 + npm run dev 20 + 21 + # Build for production 22 + npm run build 23 + 24 + # Preview production build 25 + npm run serve 26 + 27 + # Testing 28 + npm run test # Run all tests 29 + vitest run <file> # Run specific test file 30 + 31 + # Linting & Formatting 32 + npm run lint # Lint code 33 + npm run format # Format code 34 + npm run check # Check both linting and formatting 35 + 36 + # Typelex (schema generation) 37 + npm run build:typelex # Compile lexicons from typelex/*.tsp to lexicons/ 38 + ``` 39 + 40 + ## Architecture 41 + 42 + ### File-Based Routing 43 + 44 + Routes live in `src/routes/` and are managed by TanStack Router: 45 + - `__root.tsx` - Root layout with header, devtools, and HTML shell 46 + - `index.tsx` - Homepage route 47 + - `demo/*` - Demo routes showcasing various TanStack Start features 48 + 49 + Route files are auto-generated into `src/routeTree.gen.ts` (excluded from linting). 50 + 51 + ### Router Setup 52 + 53 + Router initialization happens in `src/router.tsx`: 54 + - Integrates TanStack Query via `setupRouterSsrQueryIntegration` 55 + - Wraps router with TanStack Query provider from `src/integrations/tanstack-query/` 56 + - Uses context pattern for dependency injection 57 + 58 + ### Path Aliases 59 + 60 + TypeScript paths are configured with `@/*` alias pointing to `src/*` (tsconfig.json + vite-tsconfig-paths plugin). 61 + 62 + ### Typelex/Lexicons 63 + 64 + - **Source**: `typelex/*.tsp` - TypeSpec definitions for AT Protocol lexicons 65 + - **Generated**: `lexicons/com/deckbelcher/**/*.json` - Compiled lexicon schemas 66 + - Run `npm run build:typelex` after modifying `.tsp` files 67 + - Lexicons follow AT Protocol conventions (used for ATProto/Bluesky integrations) 68 + 69 + ### Styling 70 + 71 + Tailwind CSS v4 is integrated via `@tailwindcss/vite` plugin. Global styles in `src/styles.css`. 72 + 73 + ### Development Tooling 74 + 75 + - **Nix**: flake.nix provides Node.js 22, TypeSpec, and language servers 76 + - **Biome**: Uses tabs for indentation, double quotes, excludes generated files 77 + - **Devtools**: Integrated TanStack Router + Query + React devtools in root layout 78 + 79 + ## Important Notes 80 + 81 + - `src/routeTree.gen.ts` is auto-generated - never edit manually 82 + - Demo files (prefixed with `demo`) are safe to delete 83 + - Biome only lints files in `src/`, `.vscode/`, and root config files 84 + - Router uses "intent" preloading by default 85 + - SSR is configured via TanStack Start plugin
+121
PROJECT.md
··· 1 + # DeckBelcher Project Overview 2 + 3 + ## Core Concept 4 + 5 + Magic: The Gathering decklist tool built on AT Protocol, differentiating from Moxfield/Archidekt through genuine social features and ATProto data portability rather than bolted-on comments. 6 + 7 + ## Domain & Namespace 8 + 9 + - **Domain**: deckbelcher.com 10 + - **Namespace**: com.deckbelcher.* 11 + 12 + ## Lexicon Structure 13 + 14 + ### com.deckbelcher.actor.profile 15 + User profile for DeckBelcher. Fields: 16 + - `displayName` - User's display name 17 + - `description` - Bio/description text 18 + - `pronouns` - User pronouns 19 + - `createdAt` - Account creation timestamp 20 + 21 + Future additions may include featured decklist/card. 22 + 23 + ### com.deckbelcher.list (planned) 24 + Main decklist record: 25 + ```typescript 26 + { 27 + cards: [{ 28 + scryfallId: string, // per-printing ID 29 + quantity: number, 30 + section: "mainboard" | "sideboard" | "maybeboard", 31 + tags: string[] // user metadata like "removal", "wincon" 32 + }], 33 + primer: string, // inline, use atproto facets for rich text/card mentions 34 + tags: string[] // global list tags like "competitive", "budget" 35 + } 36 + ``` 37 + 38 + **Key decisions:** 39 + - Scryfall ID is per-printing (handles multiple printings naturally) 40 + - `section` as structured field, not special tags (easier queries) 41 + - Primer inline in record, NOT separate (avoid complexity, <50kb even with novel) 42 + - Facets over markdown for rich text (can link cards with Scryfall IDs) 43 + - No explicit order field - arrays preserve order, sorts are UI concern 44 + 45 + ### com.deckbelcher.reply (planned) 46 + Replies to lists/cards/tags: 47 + ```typescript 48 + { 49 + target: { 50 + type: "card" | "tag" | "list", 51 + scryfallId?: string, // if type=card 52 + tagName?: string, // if type=tag 53 + listUri: string // always include 54 + }, 55 + text: string, 56 + // ... standard reply fields 57 + } 58 + ``` 59 + 60 + Replies to removed cards still exist, just surface them lower (below maybeboard). Include CID in target if you want "this was about an old version" support. 61 + 62 + ### com.deckbelcher.like (planned) 63 + Likes for lists. 64 + 65 + ## Target Formats 66 + 67 + **Priority:** 68 + - Commander (primary) 69 + - Cube (high value - curators love discussing every card choice) 70 + - Pauper/PDH 71 + 72 + **Skip:** Standard, Vintage 73 + 74 + ## Technical Architecture 75 + 76 + ### Frontend 77 + - Load Scryfall JSON dump (~100MB, gzips well) for instant search/typeahead 78 + - No network latency for adding cards = huge UX win 79 + - Can lazy load by set if needed but probably unnecessary 80 + 81 + ### Backend 82 + - Constellation indexes social records (likes, replies) 83 + - Use existing Bluesky social graph for follows/discoverability 84 + - Basic PDS for hosting records 85 + 86 + ### Version History 87 + **SKIP FOR V1:** 88 + - ATProto repos don't keep history by default 89 + - Mutate records in place when edited 90 + - If needed later, Constellation can index CID changes from firehose 91 + - Verdverm pattern ($orig + $hist array) is too complex for v1 92 + - Client-side undo with localStorage if you want it, but not critical 93 + 94 + ## What Makes This Better Than Moxfield 95 + 96 + 1. **Reply-to-specific-cards** - Threaded discussions on individual card choices, portable across ATProto 97 + 2. **ATProto portability** - Your decklists aren't locked in 98 + 3. **Bluesky social graph** - Friends are already there if they're on Bsky 99 + 4. **Actually social** - Not afterthought comments 100 + 101 + ## Target Audience 102 + 103 + - Commander/Cube brewers who want feedback 104 + - Content creators doing primers 105 + - Bluesky-native crowd who cares about data ownership 106 + 107 + ## Explicitly Out of Scope for V1 108 + 109 + - Version history / change tracking 110 + - Default view settings for lists 111 + - Auto-tagging via ML (cool idea with Scryfall otags as training data, but ship without it first) 112 + - Undo/redo (maybe client-side later) 113 + 114 + ## Lexicon Scoping Patterns 115 + 116 + Reference from ecosystem research: 117 + - Bluesky uses `app.bsky.actor.profile` with `actor` scope 118 + - Many indie lexicons skip scoping (e.g., `app.popsky.profile`) 119 + - Teal.fm (respected devs) uses `fm.teal.alpha.actor.profile` with `actor` scope 120 + 121 + **Decision:** Use `com.deckbelcher.actor.profile` following Bluesky/Teal.fm pattern for consistency.
+68
SCRYFALL.md
··· 1 + # Scryfall Card Model Reference 2 + 3 + ## Key Identifiers 4 + 5 + **For DeckBelcher:** Use `id` (Scryfall's UUID) as the canonical identifier per-printing. 6 + 7 + - **`id`**: Unique UUID for each card printing in Scryfall's database 8 + - This is what we store in `com.deckbelcher.list` as `scryfallId` 9 + - Different for each printing/version of the same card 10 + - **`oracle_id`**: Consistent ID across all reprints of the same card 11 + - Useful for "print-agnostic" searches (e.g., "all Lightning Bolts regardless of printing") 12 + - **`multiverse_ids`**: Gatherer identifiers (can be null) 13 + - **Platform IDs**: `arena_id`, `mtgo_id`, `tcgplayer_id` 14 + 15 + ## Card Object Structure 16 + 17 + ### Core Fields 18 + - `name`: Card name 19 + - `mana_cost`: Mana cost string (e.g., `"{2}{U}"`) 20 + - `cmc`: Converted mana cost (numeric) 21 + - `type_line`: Full type line 22 + - `oracle_text`: Rules text 23 + - `keywords`: Array of keyword abilities 24 + 25 + ### Deckbuilding Fields 26 + - `color_identity`: Array of colors for Commander legality 27 + - `legalities`: Object with format legalities (`"commander": "legal"`, etc.) 28 + - `colors`: Actual card colors 29 + - `power`, `toughness`, `loyalty`: Creature/planeswalker stats 30 + 31 + ### Multi-Face Cards 32 + - `card_faces`: Array of faces for split/flip/transform/MDFC cards 33 + - Each face has independent attributes (name, mana_cost, type_line, etc.) 34 + - The main object contains shared metadata 35 + 36 + ## Image & Display Fields 37 + 38 + ### Images 39 + - `image_uris`: Object with card image URLs at different sizes 40 + - Keys: `small`, `normal`, `large`, `png`, `art_crop`, `border_crop` 41 + - `image_status`: Quality indicator (`missing`, `placeholder`, `lowres`, `highres_scan`) 42 + - `highres_image`: Boolean for high-res availability 43 + - `illustration_id`: Unique artwork identifier 44 + 45 + ### Card Appearance 46 + - `layout`: Layout code (e.g., `"normal"`, `"split"`, `"transform"`, `"modal_dfc"`) 47 + - `border_color`: Border type (`"black"`, `"white"`, `"borderless"`, `"silver"`, `"gold"`) 48 + - `frame`: Frame layout version 49 + - `frame_effects`: Array of frame effects (e.g., `"showcase"`, `"extendedart"`) 50 + - `full_art`: Boolean for oversized artwork 51 + - `finishes`: Available finishes array (`["foil", "nonfoil", "etched"]`) 52 + - `promo`: Boolean for promotional prints 53 + - `digital`: Boolean for digital-only cards 54 + 55 + ### Printing Metadata 56 + - `set`: Set code (e.g., `"2x2"`, `"cmm"`) 57 + - `set_name`: Full set name 58 + - `collector_number`: Print number within set 59 + - `rarity`: `"common"`, `"uncommon"`, `"rare"`, `"mythic"`, `"special"`, `"bonus"` 60 + - `released_at`: Release date (ISO date string) 61 + - `games`: Array of platforms (`["paper", "arena", "mtgo"]`) 62 + 63 + ## Important Notes 64 + 65 + 1. **Per-Printing IDs**: Scryfall's `id` is unique to each printing, which matches our lexicon design 66 + 2. **Oracle Names**: Use `oracle_id` to group printings of the same card 67 + 3. **Multi-Face Complexity**: Check `layout` field and `card_faces` array for split/transform/MDFC cards 68 + 4. **Bulk Data**: Scryfall provides bulk JSON downloads (~100MB gzipped) for offline search
+50
lexicons/com/deckbelcher/actor/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.deckbelcher.actor.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "displayName": { 12 + "type": "string", 13 + "maxLength": 640, 14 + "maxGraphemes": 64, 15 + "description": "User's display name." 16 + }, 17 + "description": { 18 + "type": "string", 19 + "maxLength": 2560, 20 + "maxGraphemes": 256, 21 + "description": "Free-form profile description." 22 + }, 23 + "descriptionFacets": { 24 + "type": "array", 25 + "items": { 26 + "type": "ref", 27 + "ref": "com.deckbelcher.richtext.facet" 28 + }, 29 + "description": "Annotations of text in the profile description (mentions, URLs, hashtags, etc)." 30 + }, 31 + "pronouns": { 32 + "type": "string", 33 + "maxLength": 200, 34 + "maxGraphemes": 20, 35 + "description": "Free-form pronouns text." 36 + }, 37 + "createdAt": { 38 + "type": "string", 39 + "format": "datetime", 40 + "description": "Timestamp when the profile was created." 41 + } 42 + }, 43 + "required": [ 44 + "createdAt" 45 + ] 46 + }, 47 + "description": "A DeckBelcher user profile." 48 + } 49 + } 50 + }
+102
lexicons/com/deckbelcher/deck/list.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.deckbelcher.deck.list", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "name": { 12 + "type": "string", 13 + "maxLength": 1280, 14 + "maxGraphemes": 128, 15 + "description": "Name of the decklist." 16 + }, 17 + "format": { 18 + "type": "string", 19 + "maxLength": 320, 20 + "maxGraphemes": 32, 21 + "description": "Format of the deck (e.g., \"commander\", \"cube\", \"pauper\")." 22 + }, 23 + "cards": { 24 + "type": "array", 25 + "items": { 26 + "type": "ref", 27 + "ref": "#card" 28 + }, 29 + "description": "Array of cards in the decklist." 30 + }, 31 + "primer": { 32 + "type": "string", 33 + "maxLength": 100000, 34 + "maxGraphemes": 10000, 35 + "description": "Deck primer with strategy, combos, and card choices." 36 + }, 37 + "primerFacets": { 38 + "type": "array", 39 + "items": { 40 + "type": "ref", 41 + "ref": "com.deckbelcher.richtext.facet" 42 + }, 43 + "description": "Annotations of text in the primer (mentions, URLs, hashtags, card references, etc)." 44 + }, 45 + "createdAt": { 46 + "type": "string", 47 + "format": "datetime", 48 + "description": "Timestamp when the decklist was created." 49 + }, 50 + "updatedAt": { 51 + "type": "string", 52 + "format": "datetime", 53 + "description": "Timestamp when the decklist was last updated." 54 + } 55 + }, 56 + "required": [ 57 + "name", 58 + "cards", 59 + "createdAt" 60 + ] 61 + }, 62 + "description": "A Magic: The Gathering decklist." 63 + }, 64 + "card": { 65 + "type": "object", 66 + "properties": { 67 + "scryfallId": { 68 + "type": "string", 69 + "description": "Scryfall UUID for the specific printing." 70 + }, 71 + "quantity": { 72 + "type": "integer", 73 + "minimum": 1, 74 + "description": "Number of copies in the deck." 75 + }, 76 + "section": { 77 + "type": "string", 78 + "knownValues": [ 79 + "mainboard", 80 + "sideboard", 81 + "maybeboard", 82 + "commander" 83 + ], 84 + "description": "Which section of the deck this card belongs to. Extensible to support format-specific sections." 85 + }, 86 + "tags": { 87 + "type": "array", 88 + "items": { 89 + "type": "string" 90 + }, 91 + "description": "User annotations for this card in this deck (e.g., \"removal\", \"wincon\", \"ramp\")." 92 + } 93 + }, 94 + "description": "A card entry in a decklist.", 95 + "required": [ 96 + "scryfallId", 97 + "quantity", 98 + "section" 99 + ] 100 + } 101 + } 102 + }
-21
lexicons/com/deckbelcher/example/profile.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "com.deckbelcher.example.profile", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "key": "literal:self", 8 - "record": { 9 - "type": "object", 10 - "properties": { 11 - "description": { 12 - "type": "string", 13 - "maxGraphemes": 256, 14 - "description": "Free-form profile description." 15 - } 16 - } 17 - }, 18 - "description": "My profile." 19 - } 20 - } 21 - }
+89
lexicons/com/deckbelcher/richtext/facet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.deckbelcher.richtext.facet", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "index": { 9 + "type": "ref", 10 + "ref": "#byteSlice" 11 + }, 12 + "features": { 13 + "type": "array", 14 + "items": { 15 + "type": "union", 16 + "refs": [ 17 + "#mention", 18 + "#link", 19 + "#tag" 20 + ] 21 + } 22 + } 23 + }, 24 + "description": "Annotation of a sub-string within rich text.\nExtends Bluesky's facet system to support DeckBelcher-specific features.", 25 + "required": [ 26 + "index", 27 + "features" 28 + ] 29 + }, 30 + "byteSlice": { 31 + "type": "object", 32 + "properties": { 33 + "byteStart": { 34 + "type": "integer", 35 + "minimum": 0 36 + }, 37 + "byteEnd": { 38 + "type": "integer", 39 + "minimum": 0 40 + } 41 + }, 42 + "description": "Specifies the sub-string range a facet feature applies to.\nStart index is inclusive, end index is exclusive.\nIndices are zero-indexed, counting bytes of the UTF-8 encoded text.", 43 + "required": [ 44 + "byteStart", 45 + "byteEnd" 46 + ] 47 + }, 48 + "mention": { 49 + "type": "object", 50 + "properties": { 51 + "did": { 52 + "type": "string", 53 + "format": "did" 54 + } 55 + }, 56 + "description": "Facet feature for mention of another account.\nThe text is usually a handle, including an `@` prefix, but the facet reference is a DID.", 57 + "required": [ 58 + "did" 59 + ] 60 + }, 61 + "link": { 62 + "type": "object", 63 + "properties": { 64 + "uri": { 65 + "type": "string", 66 + "format": "uri" 67 + } 68 + }, 69 + "description": "Facet feature for a URL.\nThe text URL may have been simplified or truncated, but the facet reference should be a complete URL.", 70 + "required": [ 71 + "uri" 72 + ] 73 + }, 74 + "tag": { 75 + "type": "object", 76 + "properties": { 77 + "tag": { 78 + "type": "string", 79 + "maxLength": 640, 80 + "maxGraphemes": 64 81 + } 82 + }, 83 + "description": "Facet feature for a hashtag.\nThe text usually includes a '#' prefix, but the facet reference should not.", 84 + "required": [ 85 + "tag" 86 + ] 87 + } 88 + } 89 + }
+58
typelex/deck-list.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + import "./richtext-facet.tsp"; 4 + 5 + namespace com.deckbelcher.deck.list { 6 + /** A Magic: The Gathering decklist. */ 7 + @rec("tid") 8 + model Main { 9 + /** Name of the decklist. */ 10 + @required 11 + @maxGraphemes(128) 12 + @maxLength(1280) 13 + name: string; 14 + 15 + /** Format of the deck (e.g., "commander", "cube", "pauper"). */ 16 + @maxGraphemes(32) 17 + @maxLength(320) 18 + format?: string; 19 + 20 + /** Array of cards in the decklist. */ 21 + @required 22 + cards: Card[]; 23 + 24 + /** Deck primer with strategy, combos, and card choices. */ 25 + @maxGraphemes(10000) 26 + @maxLength(100000) 27 + primer?: string; 28 + 29 + /** Annotations of text in the primer (mentions, URLs, hashtags, card references, etc). */ 30 + primerFacets?: com.deckbelcher.richtext.facet.Main[]; 31 + 32 + /** Timestamp when the decklist was created. */ 33 + @required 34 + createdAt: datetime; 35 + 36 + /** Timestamp when the decklist was last updated. */ 37 + updatedAt?: datetime; 38 + } 39 + 40 + /** A card entry in a decklist. */ 41 + model Card { 42 + /** Scryfall UUID for the specific printing. */ 43 + @required 44 + scryfallId: string; 45 + 46 + /** Number of copies in the deck. */ 47 + @required 48 + @minValue(1) 49 + quantity: integer; 50 + 51 + /** Which section of the deck this card belongs to. Extensible to support format-specific sections. */ 52 + @required 53 + section: "mainboard" | "sideboard" | "maybeboard" | "commander" | string; 54 + 55 + /** User annotations for this card in this deck (e.g., "removal", "wincon", "ramp"). */ 56 + tags?: string[]; 57 + } 58 + }
+23 -3
typelex/main.tsp
··· 1 1 import "@typelex/emitter"; 2 2 import "./externals.tsp"; 3 + import "./richtext-facet.tsp"; 4 + import "./deck-list.tsp"; 3 5 4 - namespace com.deckbelcher.profile { 5 - /** My profile. */ 6 + namespace com.deckbelcher.actor.profile { 7 + /** A DeckBelcher user profile. */ 6 8 @rec("literal:self") 7 9 model Main { 8 - /** Free-form profile description.*/ 10 + /** User's display name. */ 11 + @maxGraphemes(64) 12 + @maxLength(640) 13 + displayName?: string; 14 + 15 + /** Free-form profile description. */ 9 16 @maxGraphemes(256) 17 + @maxLength(2560) 10 18 description?: string; 19 + 20 + /** Annotations of text in the profile description (mentions, URLs, hashtags, etc). */ 21 + descriptionFacets?: com.deckbelcher.richtext.facet.Main[]; 22 + 23 + /** Free-form pronouns text. */ 24 + @maxGraphemes(20) 25 + @maxLength(200) 26 + pronouns?: string; 27 + 28 + /** Timestamp when the profile was created. */ 29 + @required 30 + createdAt: datetime; 11 31 } 12 32 }
+60
typelex/richtext-facet.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.deckbelcher.richtext.facet { 5 + /** 6 + * Annotation of a sub-string within rich text. 7 + * Extends Bluesky's facet system to support DeckBelcher-specific features. 8 + */ 9 + model Main { 10 + @required 11 + index: ByteSlice; 12 + 13 + @required 14 + features: (Mention | Link | Tag | unknown)[]; 15 + } 16 + 17 + /** 18 + * Specifies the sub-string range a facet feature applies to. 19 + * Start index is inclusive, end index is exclusive. 20 + * Indices are zero-indexed, counting bytes of the UTF-8 encoded text. 21 + */ 22 + model ByteSlice { 23 + @required 24 + @minValue(0) 25 + byteStart: integer; 26 + 27 + @required 28 + @minValue(0) 29 + byteEnd: integer; 30 + } 31 + 32 + /** 33 + * Facet feature for mention of another account. 34 + * The text is usually a handle, including an `@` prefix, but the facet reference is a DID. 35 + */ 36 + model Mention { 37 + @required 38 + did: did; 39 + } 40 + 41 + /** 42 + * Facet feature for a URL. 43 + * The text URL may have been simplified or truncated, but the facet reference should be a complete URL. 44 + */ 45 + model Link { 46 + @required 47 + uri: uri; 48 + } 49 + 50 + /** 51 + * Facet feature for a hashtag. 52 + * The text usually includes a '#' prefix, but the facet reference should not. 53 + */ 54 + model Tag { 55 + @required 56 + @maxGraphemes(64) 57 + @maxLength(640) 58 + tag: string; 59 + } 60 + }