learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs

refactor: Update lexicon namespaces

* add `card_type` database column

* put animations back in tutorial overlay

+22 -19
crates/core/src/at_uri.rs
··· 4 4 //! Format: `at://<authority>/<collection>/<rkey>` 5 5 //! 6 6 //! - authority: DID or handle 7 - //! - collection: NSID (e.g., "app.malfestio.deck") 7 + //! - collection: NSID (e.g., "org.stormlightlabs.malfestio.deck") 8 8 //! - rkey: Record key (usually a TID) 9 9 10 10 use std::fmt; ··· 14 14 pub struct AtUri { 15 15 /// The authority (DID or handle) 16 16 pub authority: String, 17 - /// The collection NSID (e.g., "app.malfestio.deck") 17 + /// The collection NSID (e.g., "org.stormlightlabs.malfestio.deck") 18 18 pub collection: String, 19 19 /// The record key 20 20 pub rkey: String, ··· 34 34 35 35 /// Create an AT-URI for a deck record. 36 36 pub fn deck(did: &str, rkey: &str) -> Self { 37 - Self::new(did, "app.malfestio.deck", rkey) 37 + Self::new(did, "org.stormlightlabs.malfestio.deck", rkey) 38 38 } 39 39 40 40 /// Create an AT-URI for a card record. 41 41 pub fn card(did: &str, rkey: &str) -> Self { 42 - Self::new(did, "app.malfestio.card", rkey) 42 + Self::new(did, "org.stormlightlabs.malfestio.card", rkey) 43 43 } 44 44 45 45 /// Create an AT-URI for a note record. 46 46 pub fn note(did: &str, rkey: &str) -> Self { 47 - Self::new(did, "app.malfestio.note", rkey) 47 + Self::new(did, "org.stormlightlabs.malfestio.note", rkey) 48 48 } 49 49 50 50 /// Parse an AT-URI string. ··· 126 126 127 127 #[test] 128 128 fn test_new_at_uri() { 129 - let uri = AtUri::new("did:plc:abc123", "app.malfestio.deck", "3k5abc123"); 129 + let uri = AtUri::new("did:plc:abc123", "org.stormlightlabs.malfestio.deck", "3k5abc123"); 130 130 assert_eq!(uri.authority, "did:plc:abc123"); 131 - assert_eq!(uri.collection, "app.malfestio.deck"); 131 + assert_eq!(uri.collection, "org.stormlightlabs.malfestio.deck"); 132 132 assert_eq!(uri.rkey, "3k5abc123"); 133 133 } 134 134 135 135 #[test] 136 136 fn test_display() { 137 - let uri = AtUri::new("did:plc:abc123", "app.malfestio.deck", "3k5abc123"); 138 - assert_eq!(uri.to_string(), "at://did:plc:abc123/app.malfestio.deck/3k5abc123"); 137 + let uri = AtUri::new("did:plc:abc123", "org.stormlightlabs.malfestio.deck", "3k5abc123"); 138 + assert_eq!( 139 + uri.to_string(), 140 + "at://did:plc:abc123/org.stormlightlabs.malfestio.deck/3k5abc123" 141 + ); 139 142 } 140 143 141 144 #[test] 142 145 fn test_parse_valid() { 143 - let uri = AtUri::parse("at://did:plc:abc123/app.malfestio.deck/3k5abc123").unwrap(); 146 + let uri = AtUri::parse("at://did:plc:abc123/org.stormlightlabs.malfestio.deck/3k5abc123").unwrap(); 144 147 assert_eq!(uri.authority, "did:plc:abc123"); 145 - assert_eq!(uri.collection, "app.malfestio.deck"); 148 + assert_eq!(uri.collection, "org.stormlightlabs.malfestio.deck"); 146 149 assert_eq!(uri.rkey, "3k5abc123"); 147 150 } 148 151 149 152 #[test] 150 153 fn test_parse_with_handle() { 151 - let uri = AtUri::parse("at://alice.bsky.social/app.malfestio.note/abc123").unwrap(); 154 + let uri = AtUri::parse("at://alice.bsky.social/org.stormlightlabs.malfestio.note/abc123").unwrap(); 152 155 assert_eq!(uri.authority, "alice.bsky.social"); 153 156 assert!(uri.is_handle()); 154 157 assert!(!uri.is_did()); ··· 156 159 157 160 #[test] 158 161 fn test_parse_missing_scheme() { 159 - let result = AtUri::parse("did:plc:abc123/app.malfestio.deck/3k5abc123"); 162 + let result = AtUri::parse("did:plc:abc123/org.stormlightlabs.malfestio.deck/3k5abc123"); 160 163 assert_eq!(result, Err(AtUriError::MissingScheme)); 161 164 } 162 165 163 166 #[test] 164 167 fn test_parse_invalid_format() { 165 - let result = AtUri::parse("at://did:plc:abc123/app.malfestio.deck"); 168 + let result = AtUri::parse("at://did:plc:abc123/org.stormlightlabs.malfestio.deck"); 166 169 assert_eq!(result, Err(AtUriError::InvalidFormat)); 167 170 } 168 171 169 172 #[test] 170 173 fn test_parse_empty_authority() { 171 - let result = AtUri::parse("at:///app.malfestio.deck/rkey"); 174 + let result = AtUri::parse("at:///org.stormlightlabs.malfestio.deck/rkey"); 172 175 assert_eq!(result, Err(AtUriError::EmptyAuthority)); 173 176 } 174 177 ··· 180 183 181 184 #[test] 182 185 fn test_roundtrip() { 183 - let original = "at://did:plc:abc123/app.malfestio.deck/3k5abc123"; 186 + let original = "at://did:plc:abc123/org.stormlightlabs.malfestio.deck/3k5abc123"; 184 187 let uri = AtUri::parse(original).unwrap(); 185 188 assert_eq!(uri.to_string(), original); 186 189 } ··· 188 191 #[test] 189 192 fn test_convenience_constructors() { 190 193 let deck = AtUri::deck("did:plc:abc", "tid123"); 191 - assert_eq!(deck.collection, "app.malfestio.deck"); 194 + assert_eq!(deck.collection, "org.stormlightlabs.malfestio.deck"); 192 195 193 196 let card = AtUri::card("did:plc:abc", "tid456"); 194 - assert_eq!(card.collection, "app.malfestio.card"); 197 + assert_eq!(card.collection, "org.stormlightlabs.malfestio.card"); 195 198 196 199 let note = AtUri::note("did:plc:abc", "tid789"); 197 - assert_eq!(note.collection, "app.malfestio.note"); 200 + assert_eq!(note.collection, "org.stormlightlabs.malfestio.note"); 198 201 } 199 202 200 203 #[test]
+14 -7
crates/server/src/repository/card.rs
··· 23 23 #[async_trait] 24 24 pub trait CardRepository: Send + Sync { 25 25 async fn create(&self, params: CreateCardParams) -> Result<Card, CardRepoError>; 26 - 27 26 async fn list_by_deck(&self, deck_id: &str) -> Result<Vec<Card>, CardRepoError>; 28 - 29 27 async fn verify_deck_ownership(&self, deck_id: &str, owner_did: &str) -> Result<bool, CardRepoError>; 30 - 31 28 async fn update_at_uri(&self, card_id: &str, at_uri: &str) -> Result<(), CardRepoError>; 32 29 } 33 30 ··· 67 64 } 68 65 69 66 let card_id = uuid::Uuid::new_v4(); 67 + let card_type_str = match params.card_type { 68 + CardType::Basic => "basic", 69 + CardType::Cloze => "cloze", 70 + }; 70 71 client 71 72 .execute( 72 - "INSERT INTO cards (id, owner_did, deck_id, front, back, media_url, hints) 73 - VALUES ($1, $2, $3, $4, $5, $6, $7)", 73 + "INSERT INTO cards (id, owner_did, deck_id, front, back, media_url, hints, card_type) 74 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", 74 75 &[ 75 76 &card_id, 76 77 &params.owner_did, ··· 79 80 &params.back, 80 81 &params.media_url, 81 82 &params.hints, 83 + &card_type_str, 82 84 ], 83 85 ) 84 86 .await ··· 118 120 119 121 let rows = client 120 122 .query( 121 - "SELECT id, owner_did, deck_id, front, back, media_url, hints 123 + "SELECT id, owner_did, deck_id, front, back, media_url, hints, card_type 122 124 FROM cards 123 125 WHERE deck_id = $1 124 126 ORDER BY created_at ASC", ··· 131 133 for row in rows { 132 134 let id: uuid::Uuid = row.get("id"); 133 135 let card_deck_id: uuid::Uuid = row.get("deck_id"); 136 + let card_type_str: Option<String> = row.get("card_type"); 137 + let card_type = match card_type_str.as_deref() { 138 + Some("cloze") => CardType::Cloze, 139 + _ => CardType::Basic, 140 + }; 134 141 135 142 cards.push(Card { 136 143 id: id.to_string(), ··· 139 146 front: row.get("front"), 140 147 back: row.get("back"), 141 148 media_url: row.get("media_url"), 142 - card_type: CardType::default(), 149 + card_type, 143 150 hints: row.get("hints"), 144 151 }); 145 152 }
+1 -1
docs/at-notes.md
··· 71 71 72 72 Format: `at://<did>/<collection>/<rkey>` 73 73 74 - Example: `at://did:plc:abc123/app.malfestio.deck/3k5abc123` 74 + Example: `at://did:plc:abc123/org.stormlightlabs.malfestio.deck/3k5abc123` 75 75 76 76 ## Firehose / Jetstream 77 77
+14 -14
docs/data-model-mapping.md
··· 13 13 14 14 ## Mapping Table 15 15 16 - | Public Lexicon | Internal DB Table(s) | Notes | 17 - | :----------------------------- | :------------------- | :------------------------------------------------------------- | 18 - | `app.malfestio.deck` | `decks` | Public metadata. | 19 - | `app.malfestio.card` | `cards` | Content. | 20 - | `app.malfestio.note` | `notes` | Standalone notes. | 21 - | `app.malfestio.source.*` | `sources` | Metadata for articles/lectures. | 22 - | `app.malfestio.collection` | `collections` | | 23 - | `app.malfestio.thread.comment` | `comments` | | 24 - | _(None)_ | `reviews` | **Private**. Logs of every review event. | 25 - | _(None)_ | `study_progress` | **Private**. Current SRS state for a card (box/interval/ease). | 26 - | _(None)_ | `user_settings` | **Private**. Daily goals, UI references. | 27 - | _(None)_ | `drafts` | **Private**. Content being authored before publishing. | 16 + | Public Lexicon | Internal DB Table(s) | Notes | 17 + | :-------------------------------------------- | :------------------- | :------------------------------------------------------------- | 18 + | `org.stormlightlabs.malfestio.deck` | `decks` | Public metadata. | 19 + | `org.stormlightlabs.malfestio.card` | `cards` | Content. | 20 + | `org.stormlightlabs.malfestio.note` | `notes` | Standalone notes. | 21 + | `org.stormlightlabs.malfestio.source.*` | `sources` | Metadata for articles/lectures. | 22 + | `org.stormlightlabs.malfestio.collection` | `collections` | | 23 + | `org.stormlightlabs.malfestio.thread.comment` | `comments` | | 24 + | _(None)_ | `reviews` | **Private**. Logs of every review event. | 25 + | _(None)_ | `study_progress` | **Private**. Current SRS state for a card (box/interval/ease). | 26 + | _(None)_ | `user_settings` | **Private**. Daily goals, UI references. | 27 + | _(None)_ | `drafts` | **Private**. Content being authored before publishing. | 28 28 29 29 ## Sync Strategy 30 30 31 31 - **Publishing**: 32 - Write to `drafts` -> User clicks "Publish" -> Sign record -> Push to PDS -> Move `drafts` content to `decks`/`cards` tables (or mark as synced). 32 + Write to `drafts` -> User clicks "Publish" -> Sign record -> Push to PDS -> Move `drafts` content to `decks`/`cards` tables (or mark as synced). 33 33 - **Consuming**: 34 - Pull from PDS (firehose or direct sync) -> Validate signature -> Upsert into local tables. 34 + Pull from PDS (firehose or direct sync) -> Validate signature -> Upsert into local tables.
+10 -10
docs/information-architecture.md
··· 44 44 45 45 Mapping screens to underlying data entities (Lexicon Records + Private State). 46 46 47 - | Screen / Component | Primary Data Entity | Secondary Entities | Private/Public | 48 - | :----------------- | :----------------------------- | :---------------------------------------- | :---------------------------- | 49 - | **Deck Overview** | `app.malfestio.deck` | `app.malfestio.card` (refs), User Profile | **Public** | 50 - | **Study Session** | N/A (Ephemeral) | `app.malfestio.card`, Private Review Log | **Private** | 51 - | **Card View** | `app.malfestio.card` | `app.malfestio.note`, Media Blobs | **Public** | 52 - | **Editor** | Draft State (Local) | Source (`article`), `note` | **Private (Draft) -> Public** | 53 - | **Source View** | `app.malfestio.source.article` | `app.malfestio.note` (linked) | **Public** | 54 - | **Note View** | `app.malfestio.note` | Backlinks (`card`/`deck`) | **Public** | 55 - | **Library** | `app.malfestio.collection` | Bookmarks, User Prefs | **Mixed** | 56 - | **Comments** | `app.malfestio.thread.comment` | User Profile | **Public** | 47 + | Screen / Component | Primary Data Entity | Secondary Entities | Private/Public | 48 + | :----------------- | :-------------------------------------------- | :------------------------------------------------------- | :---------------------------- | 49 + | **Deck Overview** | `org.stormlightlabs.malfestio.deck` | `org.stormlightlabs.malfestio.card` (refs), User Profile | **Public** | 50 + | **Study Session** | N/A (Ephemeral) | `org.stormlightlabs.malfestio.card`, Private Review Log | **Private** | 51 + | **Card View** | `org.stormlightlabs.malfestio.card` | `org.stormlightlabs.malfestio.note`, Media Blobs | **Public** | 52 + | **Editor** | Draft State (Local) | Source (`article`), `note` | **Private (Draft) -> Public** | 53 + | **Source View** | `org.stormlightlabs.malfestio.source.article` | `org.stormlightlabs.malfestio.note` (linked) | **Public** | 54 + | **Note View** | `org.stormlightlabs.malfestio.note` | Backlinks (`card`/`deck`) | **Public** | 55 + | **Library** | `org.stormlightlabs.malfestio.collection` | Bookmarks, User Prefs | **Mixed** | 56 + | **Comments** | `org.stormlightlabs.malfestio.thread.comment` | User Profile | **Public** | 57 57 58 58 ## URL Structure 59 59
+7 -7
docs/share-vs-private-rules.md
··· 15 15 16 16 These entities are visible to anyone with access to the PDS (essentially public). 17 17 18 - - Deck (`app.malfestio.deck`): The collection of cards/notes. 19 - - Card (`app.malfestio.card`): The flashcard content (Front/Back). 20 - - Note (`app.malfestio.note`): The source knowledge note. 21 - - Article (`app.malfestio.source.article`): Metadata/snapshot of an external article. 22 - - Lecture (`app.malfestio.source.lecture`): Metadata/outline of an external video/audio. 23 - - Collection (`app.malfestio.collection`): Curated lists of decks. 24 - - Comment (`app.malfestio.thread.comment`): Public discussion. 18 + - Deck (`org.stormlightlabs.malfestio.deck`): The collection of cards/notes. 19 + - Card (`org.stormlightlabs.malfestio.card`): The flashcard content (Front/Back). 20 + - Note (`org.stormlightlabs.malfestio.note`): The source knowledge note. 21 + - Article (`org.stormlightlabs.malfestio.source.article`): Metadata/snapshot of an external article. 22 + - Lecture (`org.stormlightlabs.malfestio.source.lecture`): Metadata/outline of an external video/audio. 23 + - Collection (`org.stormlightlabs.malfestio.collection`): Curated lists of decks. 24 + - Comment (`org.stormlightlabs.malfestio.thread.comment`): Public discussion. 25 25 26 26 > **Rule**: If a user puts sensitive information in a Card, they must be warned that 27 27 > publishing the Deck makes it public.
+1 -1
docs/todo.md
··· 131 131 132 132 **Indexing:** 133 133 134 - - [ ] Subscribe to `com.atproto.sync.subscribeRepos` (or Jetstream) for `app.malfestio.*` records 134 + - [ ] Subscribe to `com.atproto.sync.subscribeRepos` (or Jetstream) for `org.stormlightlabs.malfestio.*` records 135 135 - [ ] Index posts with compound cursor (timestamp::CID) for deterministic pagination 136 136 - [ ] Garbage collect indexed data older than 48 hours (except pinned content) 137 137
+8 -8
lexicons/README.md
··· 11 11 12 12 ### Namespace + NSID conventions 13 13 14 - - `app.malfestio.note` 15 - - `app.malfestio.card` 16 - - `app.malfestio.deck` 17 - - `app.malfestio.source.article` 18 - - `app.malfestio.source.lecture` 19 - - `app.malfestio.collection` 20 - - `app.malfestio.thread.comment` 14 + - `org.stormlightlabs.malfestio.note` 15 + - `org.stormlightlabs.malfestio.card` 16 + - `org.stormlightlabs.malfestio.deck` 17 + - `org.stormlightlabs.malfestio.source.article` 18 + - `org.stormlightlabs.malfestio.source.lecture` 19 + - `org.stormlightlabs.malfestio.collection` 20 + - `org.stormlightlabs.malfestio.thread.comment` 21 21 22 22 ### Lexicon basics 23 23 ··· 38 38 2. **No Renaming**: Do not rename fields. 39 39 If a semantic change is needed, add a new field and deprecate the old one. 40 40 3. **No Type Changes**: Once published, a field's type is fixed. 41 - 4. **Version by Copying**: If a breaking change is absolutely required, create a new Lexicon with a new major version or a new name (e.g., `app.malfestio.noteV2`). 41 + 4. **Version by Copying**: If a breaking change is absolutely required, create a new Lexicon with a new major version or a new name (e.g., `org.stormlightlabs.malfestio.noteV2`).
+1 -1
lexicons/app/malfestio/card.json lexicons/org/stormlightlabs/malfestio/card.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.malfestio.card", 3 + "id": "org.stormlightlabs.malfestio.card", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+1 -1
lexicons/app/malfestio/collection.json lexicons/org/stormlightlabs/malfestio/collection.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.malfestio.collection", 3 + "id": "org.stormlightlabs.malfestio.collection", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+1 -1
lexicons/app/malfestio/deck.json lexicons/org/stormlightlabs/malfestio/deck.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.malfestio.deck", 3 + "id": "org.stormlightlabs.malfestio.deck", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+1 -1
lexicons/app/malfestio/note.json lexicons/org/stormlightlabs/malfestio/note.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.malfestio.note", 3 + "id": "org.stormlightlabs.malfestio.note", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+1 -1
lexicons/app/malfestio/source/article.json lexicons/org/stormlightlabs/malfestio/source/article.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.malfestio.source.article", 3 + "id": "org.stormlightlabs.malfestio.source.article", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+1 -1
lexicons/app/malfestio/source/lecture.json lexicons/org/stormlightlabs/malfestio/source/lecture.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.malfestio.source.lecture", 3 + "id": "org.stormlightlabs.malfestio.source.lecture", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+1 -1
lexicons/app/malfestio/thread/comment.json lexicons/org/stormlightlabs/malfestio/thread/comment.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "app.malfestio.thread.comment", 3 + "id": "org.stormlightlabs.malfestio.thread.comment", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+5
migrations/012_2026_01_02_add_card_type.sql
··· 1 + -- Migration: Add card_type column to cards table 2 + -- Date: 2026-01-02 3 + -- Issue: card_type was not being persisted, always returning 'basic' 4 + 5 + ALTER TABLE cards ADD COLUMN IF NOT EXISTS card_type TEXT DEFAULT 'basic';
+22 -18
web/src/components/TutorialOverlay.tsx
··· 11 11 const tooltipWidth = 320; 12 12 13 13 switch (placement) { 14 - case "top": 14 + case "top": { 15 + const tooltipHeight = 240; 15 16 return { 16 - top: target.top - padding - 8, 17 + top: target.top - padding - 16 - tooltipHeight, 17 18 left: target.left + target.width / 2 - tooltipWidth / 2, 18 - transform: "translateY(-100%)", 19 19 }; 20 + } 20 21 case "bottom": 21 22 return { top: target.top + target.height + padding, left: target.left + target.width / 2 - tooltipWidth / 2 }; 22 23 case "left": ··· 94 95 exit={{ opacity: 0 }} 95 96 transition={{ duration: 0.2 }} 96 97 class="fixed inset-0 z-50 pointer-events-none"> 97 - <Show when={targetPos()}> 98 + <Show when={targetPos()} keyed> 98 99 {(pos) => ( 99 100 <> 100 101 <svg ··· 104 105 <mask id="spotlight-mask"> 105 106 <rect width="100%" height="100%" fill="white" /> 106 107 <rect 107 - x={pos().left - 8} 108 - y={pos().top - 8} 109 - width={pos().width + 16} 110 - height={pos().height + 16} 108 + x={pos.left - 8} 109 + y={pos.top - 8} 110 + width={pos.width + 16} 111 + height={pos.height + 16} 111 112 rx="8" 112 113 fill="black" /> 113 114 </mask> ··· 120 121 onClick={() => tutorial.skipTutorial()} /> 121 122 </svg> 122 123 123 - <div 124 + <Motion.div 124 125 class="absolute border-2 border-[#0F62FE] rounded-lg pointer-events-none" 125 126 style={{ 126 - top: `${pos().top - 8}px`, 127 - left: `${pos().left - 8}px`, 128 - width: `${pos().width + 16}px`, 129 - height: `${pos().height + 16}px`, 127 + top: `${pos.top - 8}px`, 128 + left: `${pos.left - 8}px`, 129 + width: `${pos.width + 16}px`, 130 + height: `${pos.height + 16}px`, 130 131 "box-shadow": "0 0 0 4px rgba(15, 98, 254, 0.3)", 131 132 }} /> 132 133 133 - <div 134 + <Motion.div 135 + initial={{ opacity: 0, scale: 0.95 }} 136 + animate={{ opacity: 1, scale: 1 }} 137 + transition={{ duration: 0.2 }} 134 138 class="absolute w-80 bg-[#262626] border border-[#393939] rounded-lg shadow-xl p-4 pointer-events-auto" 135 139 style={{ 136 - top: `${getTooltipPosition(pos(), tutorial.currentStep()!.placement).top}px`, 137 - left: `${getTooltipPosition(pos(), tutorial.currentStep()!.placement).left}px`, 138 - transform: getTooltipPosition(pos(), tutorial.currentStep()!.placement).transform, 140 + top: `${getTooltipPosition(pos, tutorial.currentStep()!.placement).top}px`, 141 + left: `${getTooltipPosition(pos, tutorial.currentStep()!.placement).left}px`, 142 + transform: getTooltipPosition(pos, tutorial.currentStep()!.placement).transform, 139 143 }}> 140 144 <div class="h-1 bg-[#393939] rounded-full mb-4 overflow-hidden"> 141 145 <div ··· 178 182 </Index> 179 183 </div> 180 184 <p class="text-xs text-[#525252] text-center mt-3">Use ← → arrow keys or Esc to skip</p> 181 - </div> 185 + </Motion.div> 182 186 </> 183 187 )} 184 188 </Show>
+2 -2
web/src/components/tests/DeckPreview.test.tsx
··· 9 9 vi.mock( 10 10 "@solidjs/router", 11 11 () => ({ 12 - useSearchParams: () => [{ uri: "at://did:plc:test/app.malfestio.deck/123" }], 12 + useSearchParams: () => [{ uri: "at://did:plc:test/org.stormlightlabs.malfestio.deck/123" }], 13 13 useNavigate: () => vi.fn(), 14 14 A: (props: { href: string; children: JSX.Element }) => <a href={props.href}>{props.children}</a>, 15 15 }), ··· 29 29 ok: true, 30 30 json: async () => ({ 31 31 deck: { 32 - id: "at://did:plc:test/app.malfestio.deck/123", 32 + id: "at://did:plc:test/org.stormlightlabs.malfestio.deck/123", 33 33 owner_did: "did:plc:test", 34 34 title: "Remote Deck", 35 35 description: "A test deck",
+1 -1
web/src/components/tests/Library.test.tsx
··· 32 32 title: "Federated Deck", 33 33 description: "From another server", 34 34 tags: ["remote"], 35 - at_uri: "at://did:plc:other/app.malfestio.deck/deck1", 35 + at_uri: "at://did:plc:other/org.stormlightlabs.malfestio.deck/deck1", 36 36 }, 37 37 rank: 1, 38 38 source: "remote",