Organization
Teak provides multiple ways to organize and filter your cards: favorites for quick access, tags for categorization, and advanced filters for precise queries.
Favorites
Mark important cards as favorites for quick access and filtering.
Favoriting Cards
Cards have an isFavorited boolean flag:
// Mark as favorite
await ctx . db . patch ( cardId , {
isFavorited: true
});
// Remove from favorites
await ctx . db . patch ( cardId , {
isFavorited: false
});
Querying Favorites
Favorites are indexed for efficient queries:
// Get all favorites
const favorites = await ctx . db
. query ( "cards" )
. withIndex ( "by_user_favorites_deleted" , ( q ) =>
q . eq ( "userId" , userId )
. eq ( "isFavorited" , true )
. eq ( "isDeleted" , undefined )
)
. order ( "desc" )
. collect ();
Index definition:
. index ( "by_user_favorites" , [ "userId" , "isFavorited" ])
. index ( "by_user_favorites_deleted" , [ "userId" , "isFavorited" , "isDeleted" ])
Search Integration
All search indexes support favorites filtering:
// Search favorites only
const results = await searchCards ({
searchQuery: "design patterns" ,
favoritesOnly: true ,
limit: 50
});
Search indexes include isFavorited as a filter field:
. searchIndex ( "search_content" , {
searchField: "content" ,
filterFields: [ "userId" , "isDeleted" , "type" , "isFavorited" ],
})
Special Keywords
Quick access via search:
// Any of these keywords returns favorites
"fav"
"favs"
"favorites"
"favourite"
"favourites"
Implementation:
if ([ "fav" , "favs" , "favorites" , "favourite" , "favourites" ]. includes ( query )) {
const favorites = await ctx . db
. query ( "cards" )
. withIndex ( "by_user_favorites_deleted" , ( q ) =>
q . eq ( "userId" , userId )
. eq ( "isFavorited" , true )
. eq ( "isDeleted" , undefined )
)
. order ( "desc" )
. take ( limit );
return favorites ;
}
Favorites are user-specific. You can favorite unlimited cards without affecting storage limits.
Tags provide flexible categorization with both user-defined and AI-generated options.
Manually add tags to any card:
// Add tags
await ctx . db . patch ( cardId , {
tags: [ "react" , "tutorial" , "hooks" , "javascript" ]
});
// Update tags (merge new tags)
const card = await ctx . db . get ( cardId );
const existingTags = card . tags || [];
const newTags = [ "typescript" , "frontend" ];
await ctx . db . patch ( cardId , {
tags: [ ... new Set ([ ... existingTags , ... newTags ])]
});
// Remove a tag
const card = await ctx . db . get ( cardId );
await ctx . db . patch ( cardId , {
tags: card . tags ?. filter ( t => t !== "tutorial" )
});
Automatic topic extraction during AI processing:
// AI tags are generated automatically
aiTags : [ "web development" , "frontend" , "component architecture" , "react" ]
AI tags are:
Read-only : Generated by AI, not user-editable
Searchable : Indexed for full-text search
Complementary : Work alongside user tags
Tag Schema
Both user and AI tags are stored as string arrays:
{
tags : v . optional ( v . array ( v . string ())), // User-defined
aiTags : v . optional ( v . array ( v . string ())), // AI-generated
}
Tag Search
Tags have dedicated search indexes:
. searchIndex ( "search_tags" , {
searchField: "tags" ,
filterFields: [ "userId" , "isDeleted" , "type" , "isFavorited" ],
})
. searchIndex ( "search_ai_tags" , {
searchField: "aiTags" ,
filterFields: [ "userId" , "isDeleted" , "type" , "isFavorited" ],
})
Partial Tag Matching
Tag search supports partial matching:
// Search for "java" matches:
// - "javascript"
// - "java"
// - "Java Programming"
const searchTerms = "java" . toLowerCase (). split ( / \s + / );
const matches = allCards . filter (( card ) =>
card . tags ?. some (( tag ) =>
searchTerms . some (( term ) => tag . toLowerCase (). includes ( term ))
)
);
Tag Best Practices
Choose a consistent format:
lowercase : javascript, react, tutorial
kebab-case : web-development, ui-design
camelCase : webDevelopment, uiDesign
Stick to one convention for easier filtering.
Use prefixes for hierarchical organization:
project:teak, project:blog
topic:design, topic:development
status:todo, status:done
Filters
Advanced filtering options for precise card queries.
Type Filters
Filter by one or more card types:
// Single type
const images = await searchCards ({
types: [ "image" ],
limit: 50
});
// Multiple types
const media = await searchCards ({
types: [ "image" , "video" , "audio" ],
limit: 50
});
Implementation uses indexes:
// Single type: uses index
if ( types && types . length === 1 ) {
query = ctx . db . query ( "cards" )
. withIndex ( "by_user_type_deleted" , ( q ) =>
q . eq ( "userId" , userId )
. eq ( "type" , types [ 0 ])
. eq ( "isDeleted" , undefined )
);
}
// Multiple types: uses OR filter
else if ( types && types . length > 1 ) {
query = query . filter (( q ) => {
const typeConditions = types . map (( type ) => q . eq ( q . field ( "type" ), type ));
return typeConditions . reduce (( acc , condition ) => q . or ( acc , condition ));
});
}
Single type filters use indexes for optimal performance. Multiple type filters require post-index OR filtering.
Date Range Filters
Filter by creation date:
// Last 7 days
const sevenDaysAgo = Date . now () - ( 7 * 24 * 60 * 60 * 1000 );
const recent = await searchCards ({
createdAtRange: {
start: sevenDaysAgo ,
end: Date . now ()
},
limit: 50
});
// Specific date range
const startOfMonth = new Date ( 2024 , 0 , 1 ). getTime ();
const endOfMonth = new Date ( 2024 , 0 , 31 ). getTime ();
const january = await searchCards ({
createdAtRange: {
start: startOfMonth ,
end: endOfMonth
},
limit: 50
});
Uses dedicated date index:
. index ( "by_created" , [ "userId" , "createdAt" ])
query = ctx . db
. query ( "cards" )
. withIndex ( "by_created" , ( q ) =>
q . eq ( "userId" , userId )
. gte ( "createdAt" , range . start )
. lt ( "createdAt" , range . end )
);
Visual Style Filters
Filter images by aesthetic style:
const minimal = await searchCards ({
types: [ "image" ],
styleFilters: [ "minimal" , "monochrome" ],
limit: 50
});
Available Visual Styles
const VISUAL_STYLE_TAXONOMY = [
"abstract" , // Abstract art, patterns
"cinematic" , // Film-like, dramatic shots
"dark" , // Low-key lighting, dark tones
"illustrative" , // Drawings, cartoons, illustrations
"minimal" , // Simple, clean, negative space
"monochrome" , // Black and white, grayscale
"moody" , // Atmospheric, dramatic lighting
"pastel" , // Soft, muted colors
"photographic" , // Realistic photography
"retro" , // Vintage aesthetic, synthwave
"surreal" , // Dreamlike, unusual
"vintage" , // Old, aged appearance
"vibrant" , // Bright, saturated colors
] as const ;
Style Normalization
Aliases are automatically mapped to canonical styles:
// "minimalist" → "minimal"
// "b&w" → "monochrome"
// "colorful" → "vibrant"
// "photo" → "photographic"
const normalized = normalizeVisualStyle ( "colorful" );
// Returns: "vibrant"
Color Hue Filters
Filter by color palette hues:
const blueImages = await searchCards ({
types: [ "image" , "palette" ],
hueFilters: [ "blue" , "cyan" ],
limit: 50
});
Available Hue Buckets
const COLOR_HUE_BUCKETS = [
"red" ,
"orange" ,
"yellow" ,
"green" ,
"teal" ,
"cyan" ,
"blue" ,
"purple" ,
"pink" ,
"brown" ,
"neutral" , // Grays, blacks, whites
] as const ;
Hue Normalization
Aliases map to canonical hues:
// "violet" → "purple"
// "indigo" → "purple"
// "magenta" → "pink"
// "gray" → "neutral"
// "monochrome" → "neutral"
const normalized = normalizeColorHueBucket ( "magenta" );
// Returns: "pink"
Hex Color Filters
Find exact color matches:
const teakColors = await searchCards ({
types: [ "image" , "palette" ],
hexFilters: [ "#8B7355" , "#D4A574" ],
limit: 50
});
Searches the colorHexes field:
colorHexes : [ "#8B7355" , "#D4A574" , "#5C4A3A" , ... ]
Trash Filters
View deleted cards:
// Show trash only
const trash = await searchCards ({
showTrashOnly: true ,
limit: 50
});
Special keywords also work:
"trash"
"deleted"
"bin"
"recycle"
"trashed"
Implementation:
if ([ "trash" , "deleted" , "bin" , "recycle" , "trashed" ]. includes ( query )) {
const trashed = await ctx . db
. query ( "cards" )
. withIndex ( "by_user_deleted" , ( q ) =>
q . eq ( "userId" , userId ). eq ( "isDeleted" , true )
)
. order ( "desc" )
. take ( limit );
return trashed ;
}
Combined Filters
All filters can be combined:
// Complex query: Recent favorite minimal blue images
const results = await searchCards ({
types: [ "image" ],
favoritesOnly: true ,
styleFilters: [ "minimal" ],
hueFilters: [ "blue" ],
createdAtRange: {
start: Date . now () - ( 30 * 24 * 60 * 60 * 1000 ),
end: Date . now ()
},
limit: 50
});
Index-backed (fast):
User ID scoping
Deletion status
Single type filter
Favorites filter
Date range filter
In-memory (slower for large sets):
Multiple type filters (OR condition)
Visual style filters
Color hue/hex filters
Combined visual filters
Visual filters (styles, hues, hexes) require in-memory filtering. For best performance, combine with type or date filters to reduce the result set first.
Filter Helpers
Reusable filter logic:
Visual Filter Normalization
import {
normalizeVisualFilterArgs ,
applyCardLevelFilters ,
doesCardMatchVisualFilters
} from "./visualFilters" ;
const visualFilters = normalizeVisualFilterArgs ({
styleFilters: [ "minimal" , "vibrant" ],
hueFilters: [ "blue" , "cyan" ],
hexFilters: [ "#8B7355" ]
});
// Returns:
{
hasVisualFilters : true ,
styles : [ "minimal" , "vibrant" ],
hues : [ "blue" , "cyan" ],
hexes : [ "#8B7355" ]
}
Card-Level Filtering
// Apply all filters to a card array
const filtered = applyCardLevelFilters ( cards , {
types: [ "image" ],
favoritesOnly: true ,
createdAtRange: { start: timestamp , end: now },
visualFilters
});
Visual Filter Matching
// Check if card matches visual filters
const matches = doesCardMatchVisualFilters ( card , visualFilters );
Organization Examples
Next Steps
Search Master full-text search across all fields
Card Types Understand type-specific organization
Organizing Guide Best practices and workflows
Color Palettes Deep dive into color filtering