feat: brain colored folders/tags — color + icon fields, mobile pills
Backend: - Added color (hex) and icon (lucide name) columns to folders and tags - Default folders seeded with colors: Home=green, Work=indigo, Travel=blue, etc. - API returns color/icon in sidebar and CRUD responses - Create/update endpoints accept color and icon Frontend: - Mobile: horizontal scrollable pill tabs with colored dots - Desktop sidebar: colored dots next to folder names - Active pill gets tinted border matching folder color Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,11 +22,15 @@ router = APIRouter(prefix="/api/taxonomy", tags=["taxonomy"])
|
||||
|
||||
class FolderIn(BaseModel):
|
||||
name: str
|
||||
color: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
|
||||
class FolderOut(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
slug: str
|
||||
color: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
is_active: bool
|
||||
sort_order: int
|
||||
item_count: int = 0
|
||||
@@ -34,11 +38,15 @@ class FolderOut(BaseModel):
|
||||
|
||||
class TagIn(BaseModel):
|
||||
name: str
|
||||
color: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
|
||||
class TagOut(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
slug: str
|
||||
color: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
is_active: bool
|
||||
sort_order: int
|
||||
item_count: int = 0
|
||||
@@ -77,8 +85,8 @@ async def get_sidebar(
|
||||
folder_counts[row[0]] = row[1]
|
||||
|
||||
folders = [FolderOut(
|
||||
id=f.id, name=f.name, slug=f.slug, is_active=f.is_active,
|
||||
sort_order=f.sort_order, item_count=folder_counts.get(f.id, 0)
|
||||
id=f.id, name=f.name, slug=f.slug, color=f.color, icon=f.icon,
|
||||
is_active=f.is_active, sort_order=f.sort_order, item_count=folder_counts.get(f.id, 0)
|
||||
) for f in folder_rows]
|
||||
|
||||
# Tags with counts
|
||||
@@ -95,8 +103,8 @@ async def get_sidebar(
|
||||
tag_counts[row[0]] = row[1]
|
||||
|
||||
tags = [TagOut(
|
||||
id=t.id, name=t.name, slug=t.slug, is_active=t.is_active,
|
||||
sort_order=t.sort_order, item_count=tag_counts.get(t.id, 0)
|
||||
id=t.id, name=t.name, slug=t.slug, color=t.color, icon=t.icon,
|
||||
is_active=t.is_active, sort_order=t.sort_order, item_count=tag_counts.get(t.id, 0)
|
||||
) for t in tag_rows]
|
||||
|
||||
# Total items
|
||||
@@ -118,7 +126,7 @@ async def list_folders(
|
||||
rows = (await db.execute(
|
||||
select(Folder).where(Folder.user_id == user_id).order_by(Folder.sort_order, Folder.name)
|
||||
)).scalars().all()
|
||||
return [FolderOut(id=f.id, name=f.name, slug=f.slug, is_active=f.is_active, sort_order=f.sort_order) for f in rows]
|
||||
return [FolderOut(id=f.id, name=f.name, slug=f.slug, color=f.color, icon=f.icon, is_active=f.is_active, sort_order=f.sort_order) for f in rows]
|
||||
|
||||
|
||||
@router.post("/folders", response_model=FolderOut, status_code=201)
|
||||
@@ -138,7 +146,8 @@ async def create_folder(
|
||||
select(func.max(Folder.sort_order)).where(Folder.user_id == user_id)
|
||||
)).scalar() or 0
|
||||
|
||||
folder = Folder(id=str(uuid.uuid4()), user_id=user_id, name=body.name.strip(), slug=slug, sort_order=max_order + 1)
|
||||
folder = Folder(id=str(uuid.uuid4()), user_id=user_id, name=body.name.strip(), slug=slug,
|
||||
color=body.color, icon=body.icon, sort_order=max_order + 1)
|
||||
db.add(folder)
|
||||
await db.commit()
|
||||
return FolderOut(id=folder.id, name=folder.name, slug=folder.slug, is_active=folder.is_active, sort_order=folder.sort_order)
|
||||
@@ -159,6 +168,8 @@ async def update_folder(
|
||||
|
||||
folder.name = body.name.strip()
|
||||
folder.slug = slugify(body.name)
|
||||
if body.color is not None: folder.color = body.color
|
||||
if body.icon is not None: folder.icon = body.icon
|
||||
folder.updated_at = datetime.utcnow()
|
||||
|
||||
# Update denormalized folder name on items
|
||||
@@ -211,7 +222,7 @@ async def list_tags(
|
||||
rows = (await db.execute(
|
||||
select(Tag).where(Tag.user_id == user_id).order_by(Tag.sort_order, Tag.name)
|
||||
)).scalars().all()
|
||||
return [TagOut(id=t.id, name=t.name, slug=t.slug, is_active=t.is_active, sort_order=t.sort_order) for t in rows]
|
||||
return [TagOut(id=t.id, name=t.name, slug=t.slug, color=t.color, icon=t.icon, is_active=t.is_active, sort_order=t.sort_order) for t in rows]
|
||||
|
||||
|
||||
@router.post("/tags", response_model=TagOut, status_code=201)
|
||||
@@ -231,7 +242,8 @@ async def create_tag(
|
||||
select(func.max(Tag.sort_order)).where(Tag.user_id == user_id)
|
||||
)).scalar() or 0
|
||||
|
||||
tag = Tag(id=str(uuid.uuid4()), user_id=user_id, name=body.name.strip(), slug=slug, sort_order=max_order + 1)
|
||||
tag = Tag(id=str(uuid.uuid4()), user_id=user_id, name=body.name.strip(), slug=slug,
|
||||
color=body.color, icon=body.icon, sort_order=max_order + 1)
|
||||
db.add(tag)
|
||||
await db.commit()
|
||||
return TagOut(id=tag.id, name=tag.name, slug=tag.slug, is_active=tag.is_active, sort_order=tag.sort_order)
|
||||
@@ -253,6 +265,8 @@ async def update_tag(
|
||||
old_name = tag.name
|
||||
tag.name = body.name.strip()
|
||||
tag.slug = slugify(body.name)
|
||||
if body.color is not None: tag.color = body.color
|
||||
if body.icon is not None: tag.icon = body.icon
|
||||
tag.updated_at = datetime.utcnow()
|
||||
|
||||
# Update denormalized tags array on items that had the old name
|
||||
|
||||
Reference in New Issue
Block a user