this repo has no description
2
fork

Configure Feed

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

feat: colors and consistent components for project and tags

+337 -115
+88 -52
mast-react-vite/src/components/ui/action-parser.tsx
··· 3 3 import { Button } from "@/components/ui/button"; 4 4 import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; 5 5 import { Search, Plus, Pencil, Check, Calendar } from "lucide-react"; 6 + import { Tag } from "@/components/ui/tag"; 7 + import { Project } from "@/components/ui/project"; 6 8 7 9 export interface InputProps 8 10 extends React.InputHTMLAttributes<HTMLInputElement> { ··· 283 285 ref={suggestionsRef} 284 286 onScroll={handleSuggestionsScroll} 285 287 > 286 - {suggestions.map((suggestion, index) => ( 287 - <Button 288 - key={index} 289 - variant="ghost" 290 - size="sm" 291 - onClick={() => { 292 - const trimmedValue = (value as string || '').trim(); 293 - const words = trimmedValue.split(/\s+/); 294 - const lastWord = words[words.length - 1] || ''; 295 - 296 - // Replace just the last word with the suggestion 297 - if (lastWord.startsWith('#')) { 298 - words[words.length - 1] = `#${suggestion}`; 299 - } else if (lastWord.startsWith('+')) { 300 - words[words.length - 1] = `+${suggestion}`; 301 - } 302 - 303 - const newValue = words.join(' '); 304 - props.onChange?.({ target: { value: newValue } } as any); 305 - setTimeout(() => focusInput(), 0) 306 - }} 307 - className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 308 - > 309 - {suggestion} 310 - </Button> 311 - ))} 288 + {suggestions.map((suggestion, index) => { 289 + const trimmedValue = (value as string || '').trim(); 290 + const words = trimmedValue.split(/\s+/); 291 + const lastWord = words[words.length - 1] || ''; 292 + 293 + const handleClick = () => { 294 + // Replace just the last word with the suggestion 295 + if (lastWord.startsWith('#')) { 296 + words[words.length - 1] = `#${suggestion}`; 297 + } else if (lastWord.startsWith('+')) { 298 + words[words.length - 1] = `+${suggestion}`; 299 + } 300 + 301 + const newValue = words.join(' '); 302 + props.onChange?.({ target: { value: newValue } } as any); 303 + setTimeout(() => focusInput(), 0); 304 + }; 305 + 306 + // Return Tag or Project component based on input type 307 + return lastWord.startsWith('#') ? ( 308 + <Tag 309 + key={index} 310 + tag={suggestion} 311 + onClick={handleClick} 312 + /> 313 + ) : lastWord.startsWith('+') ? ( 314 + <Project 315 + key={index} 316 + name={suggestion} 317 + onClick={handleClick} 318 + /> 319 + ) : ( 320 + <Button 321 + key={index} 322 + variant="ghost" 323 + size="sm" 324 + onClick={handleClick} 325 + className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 326 + > 327 + {suggestion} 328 + </Button> 329 + ); 330 + })} 312 331 </div> 313 332 </> 314 333 )}</div> ··· 449 468 ref={mobileSuggestionsRef} 450 469 onScroll={handleSuggestionsScroll} 451 470 > 452 - {suggestions.map((suggestion, index) => ( 453 - <Button 454 - key={index} 455 - variant="ghost" 456 - size="sm" 457 - onClick={() => { 458 - const trimmedValue = (value as string || '').trim(); 459 - const words = trimmedValue.split(/\s+/); 460 - const lastWord = words[words.length - 1] || ''; 461 - 462 - // Replace just the last word with the suggestion 463 - if (lastWord.startsWith('#')) { 464 - words[words.length - 1] = `#${suggestion}`; 465 - } else if (lastWord.startsWith('+')) { 466 - words[words.length - 1] = `+${suggestion}`; 467 - } 468 - 469 - const newValue = words.join(' '); 470 - props.onChange?.({ target: { value: newValue } } as any); 471 - setTimeout(() => focusInput(), 0); 472 - }} 473 - className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 474 - > 475 - {suggestion} 476 - </Button> 477 - ))} 471 + {suggestions.map((suggestion, index) => { 472 + const trimmedValue = (value as string || '').trim(); 473 + const words = trimmedValue.split(/\s+/); 474 + const lastWord = words[words.length - 1] || ''; 475 + 476 + const handleClick = () => { 477 + // Replace just the last word with the suggestion 478 + if (lastWord.startsWith('#')) { 479 + words[words.length - 1] = `#${suggestion}`; 480 + } else if (lastWord.startsWith('+')) { 481 + words[words.length - 1] = `+${suggestion}`; 482 + } 483 + 484 + const newValue = words.join(' '); 485 + props.onChange?.({ target: { value: newValue } } as any); 486 + setTimeout(() => focusInput(), 0); 487 + }; 488 + 489 + // Return Tag or Project component based on input type 490 + return lastWord.startsWith('#') ? ( 491 + <Tag 492 + key={index} 493 + tag={suggestion} 494 + onClick={handleClick} 495 + /> 496 + ) : lastWord.startsWith('+') ? ( 497 + <Project 498 + key={index} 499 + name={suggestion} 500 + onClick={handleClick} 501 + /> 502 + ) : ( 503 + <Button 504 + key={index} 505 + variant="ghost" 506 + size="sm" 507 + onClick={handleClick} 508 + className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 509 + > 510 + {suggestion} 511 + </Button> 512 + ); 513 + })} 478 514 </div> 479 515 </> 480 516 )}</div>
+4 -2
mast-react-vite/src/components/ui/app-sidebar.tsx
··· 11 11 } from "@/components/ui/sidebar"; 12 12 import { useState, useEffect } from "react"; 13 13 import { useQuery } from "@vlcn.io/react"; 14 + import { Project } from "@/components/ui/project"; 15 + import { Tag } from "@/components/ui/tag"; 14 16 15 17 // Helper function to load all visited rooms 16 18 function loadAllRooms(): Set<string> { ··· 137 139 isActive={filterContext.filterProject === item.project} 138 140 onClick={() => handleProjectClick(item.project)} 139 141 > 140 - {item.project} 142 + <Project name={item.project} /> 141 143 </SidebarMenuButton> 142 144 </SidebarMenuItem> 143 145 ))} ··· 159 161 isActive={filterContext.filterTags && filterContext.filterTags.includes(item.tag)} 160 162 onClick={() => handleTagClick(item.tag)} 161 163 > 162 - {item.tag} 164 + <Tag tag={item.tag} /> 163 165 </SidebarMenuButton> 164 166 </SidebarMenuItem> 165 167 ))}
+93
mast-react-vite/src/components/ui/project.tsx
··· 1 + import { Button } from "@/components/ui/button"; 2 + 3 + interface ProjectProps { 4 + name: string; 5 + status?: number; 6 + onClick?: () => void; 7 + color?: string; 8 + selected?: boolean; 9 + isPreview?: boolean; 10 + } 11 + 12 + // Array of predefined colors 13 + const PROJECT_COLORS = [ 14 + { text: 'text-blue-600', bg: 'bg-blue-600', icon: 'text-blue-500' }, 15 + { text: 'text-red-600', bg: 'bg-red-600', icon: 'text-red-500' }, 16 + { text: 'text-green-600', bg: 'bg-green-600', icon: 'text-green-500' }, 17 + { text: 'text-purple-600', bg: 'bg-purple-600', icon: 'text-purple-500' }, 18 + { text: 'text-orange-600', bg: 'bg-orange-600', icon: 'text-orange-500' }, 19 + { text: 'text-teal-600', bg: 'bg-teal-600', icon: 'text-teal-500' }, 20 + { text: 'text-indigo-600', bg: 'bg-indigo-600', icon: 'text-indigo-500' }, 21 + { text: 'text-pink-600', bg: 'bg-pink-600', icon: 'text-pink-500' }, 22 + ]; 23 + 24 + export function Project({ name, status, onClick, color, selected = false, isPreview = false }: ProjectProps) { 25 + // Get random color if not provided 26 + const getRandomColor = () => { 27 + // Generate a hash from name string for consistent color 28 + const hashCode = name.split('').reduce((hash, char) => { 29 + return char.charCodeAt(0) + ((hash << 5) - hash); 30 + }, 0); 31 + 32 + // Use the hash to pick a consistent color 33 + const index = Math.abs(hashCode) % PROJECT_COLORS.length; 34 + return PROJECT_COLORS[index]; 35 + }; 36 + 37 + const projectColor = color ? { 38 + text: `text-${color}-600`, 39 + bg: `bg-${color}-600`, 40 + icon: `text-${color}-500` 41 + } : getRandomColor(); 42 + 43 + // Get variant based on status 44 + const getVariant = (status) => { 45 + switch (status) { 46 + case 2: // preview-add 47 + return 'outline'; 48 + case 3: // preview-done 49 + return 'ghost'; 50 + default: 51 + return 'ghost'; 52 + } 53 + }; 54 + 55 + // Get custom class based on status 56 + const getCustomClass = (status) => { 57 + // Handle special statuses first 58 + switch (status) { 59 + case 2: // preview-add 60 + return 'border border-yellow-700 text-yellow-700 bg-yellow-100'; 61 + case 3: // preview-done 62 + return 'border border-green-800 text-green-700 bg-green-100 opacity-50'; 63 + } 64 + 65 + // Handle preview state 66 + if (isPreview) { 67 + return 'border border-yellow-700 text-yellow-600 bg-yellow-100'; 68 + } 69 + 70 + // Handle selected state - same as hover state 71 + if (selected) { 72 + return `${projectColor.bg} text-white [&>svg]:text-white hover:${projectColor.bg} hover:text-white hover:[&>svg]:text-white`; 73 + } 74 + 75 + // Default state - transparent background with hover effect 76 + return `${projectColor.text} [&>svg]:${projectColor.icon} hover:${projectColor.bg} hover:text-white hover:[&>svg]:text-white`; 77 + }; 78 + 79 + return ( 80 + <Button 81 + variant={getVariant(status)} 82 + size="sm" 83 + className={`group h-6 px-1.5 whitespace-nowrap text-sm rounded-full ${getCustomClass(status)}`} 84 + onClick={onClick} 85 + > 86 + <svg className={`w-2 h-2 mr-0`} viewBox="0 0 6 6" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"> 87 + <line x1="3" y1="0.5" x2="3" y2="5.5"></line> 88 + <line x1="0.5" y1="3" x2="5.5" y2="3"></line> 89 + </svg> 90 + {name} 91 + </Button> 92 + ); 93 + }
+114
mast-react-vite/src/components/ui/tag.tsx
··· 1 + import { Button } from "@/components/ui/button"; 2 + 3 + interface TagProps { 4 + tag: string; 5 + status?: number; 6 + isPreview?: boolean; 7 + onClick?: () => void; 8 + color?: string; 9 + } 10 + 11 + // Array of predefined colors 12 + const TAG_COLORS = [ 13 + { text: 'text-blue-600', border: 'border-blue-700', icon: 'text-blue-500' }, 14 + { text: 'text-red-600', border: 'border-red-700', icon: 'text-red-500' }, 15 + { text: 'text-green-600', border: 'border-green-700', icon: 'text-green-500' }, 16 + { text: 'text-purple-600', border: 'border-purple-700', icon: 'text-purple-500' }, 17 + { text: 'text-orange-600', border: 'border-orange-700', icon: 'text-orange-500' }, 18 + { text: 'text-teal-600', border: 'border-teal-700', icon: 'text-teal-500' }, 19 + { text: 'text-indigo-600', border: 'border-indigo-700', icon: 'text-indigo-500' }, 20 + { text: 'text-pink-600', border: 'border-pink-700', icon: 'text-pink-500' }, 21 + ]; 22 + 23 + export function Tag({ tag, status, isPreview = false, onClick, color }: TagProps) { 24 + const getRandomColor = () => { 25 + const hashCode = tag.split('').reduce((hash, char) => { 26 + return char.charCodeAt(0) + ((hash << 5) - hash); 27 + }, 0); 28 + 29 + const index = Math.abs(hashCode) % TAG_COLORS.length; 30 + return TAG_COLORS[index]; 31 + }; 32 + 33 + const tagColor = color ? { text: `text-${color}-600`, border: `border-${color}-700`, icon: `text-${color}-500` } : getRandomColor(); 34 + 35 + // Get variant based on status 36 + const getVariant = (status) => { 37 + switch (status) { 38 + case 2: // preview-add 39 + return 'outline'; 40 + case 3: // preview-done 41 + return 'ghost'; 42 + default: 43 + return isPreview ? 'outline' : 'ghost'; 44 + } 45 + }; 46 + 47 + // Get custom class based on status 48 + const getCustomClass = (status) => { 49 + switch (status) { 50 + case 2: // preview-add 51 + return 'border border-yellow-700 text-yellow-600 bg-yellow-100'; 52 + case 3: // preview-done 53 + return 'border border-green-800 text-green-700 bg-green-100 opacity-50'; 54 + default: 55 + return isPreview 56 + ? 'border border-yellow-700 text-yellow-600 bg-yellow-100' 57 + : `border ${tagColor.border} ${tagColor.text} hover:border-white`; 58 + } 59 + }; 60 + 61 + return ( 62 + <Button 63 + variant={getVariant(status)} 64 + size="sm" 65 + className={`h-6 px-1.5 whitespace-nowrap text-sm ${getCustomClass(status)}`} 66 + onClick={onClick} 67 + > 68 + <svg className={`w-2 h-2 mr-0 ${status < 2 && !isPreview ? tagColor.icon : ''}`} viewBox="0 0 6 6" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"> 69 + <line x1="2" y1="0.5" x2="2" y2="5.5"></line> 70 + <line x1="4" y1="0.5" x2="4" y2="5.5"></line> 71 + <line x1="0.5" y1="2" x2="5.5" y2="2"></line> 72 + <line x1="0.5" y1="4" x2="5.5" y2="4"></line> 73 + </svg> 74 + {tag} 75 + </Button> 76 + ); 77 + } 78 + 79 + interface TagListProps { 80 + tags: string[]; 81 + previewTags?: string[]; 82 + status?: number; 83 + onTagClick?: (tag: string) => void; 84 + tagColors?: Record<string, string>; 85 + } 86 + 87 + export function TagList({ tags, previewTags = [], status, onTagClick, tagColors = {} }: TagListProps) { 88 + return ( 89 + <div className="flex flex-wrap gap-1"> 90 + {(tags && tags.length > 0) && 91 + tags.map((tag: string) => ( 92 + <Tag 93 + key={tag} 94 + tag={tag} 95 + status={status} 96 + onClick={onTagClick ? () => onTagClick(tag) : undefined} 97 + color={tagColors[tag]} 98 + /> 99 + )) 100 + } 101 + {(previewTags.length > 0) && 102 + previewTags.map((tag: string) => ( 103 + <Tag 104 + key={`preview-${tag}`} 105 + tag={tag} 106 + isPreview={true} 107 + onClick={onTagClick ? () => onTagClick(tag) : undefined} 108 + color={tagColors[tag]} 109 + /> 110 + )) 111 + } 112 + </div> 113 + ); 114 + }
+38 -61
mast-react-vite/src/components/ui/task.tsx
··· 1 + import * as React from "react"; 1 2 import { Separator } from "@/components/ui/separator"; 2 - import { Badge } from "@/components/ui/badge"; 3 3 import { useSelection } from "@/contexts/selection-context"; 4 + import { Project } from "@/components/ui/project"; 5 + import { TagList } from "@/components/ui/tag"; 4 6 5 7 interface TaskProps { 6 8 selected?: boolean; ··· 10 12 } 11 13 12 14 export function Task({ selected, onSelect, data }: TaskProps) { 13 - const { isSelected, toggleSelection } = useSelection(); 15 + const { isSelected, toggleSelection } = useSelection(); 14 16 const tagList = JSON.parse(data.tags); 15 - 17 + 16 18 // Handle preview changes 17 19 const hasPreview = data.previewMode && data.preview; 18 - 20 + 19 21 // Determine which description to display 20 - const displayDescription = hasPreview && data.preview.description.length > 0 21 - ? data.preview.description 22 - : data.description; 22 + const displayDescription = 23 + hasPreview && data.preview.description.length > 0 24 + ? data.preview.description 25 + : data.description; 23 26 24 - const previewTags = hasPreview && data.preview.tags 25 - ? JSON.parse(data.preview.tags) 26 - : [] 27 - 28 - // Determine which project to display 29 - const displayProject = hasPreview && data.preview.project 30 - ? data.preview.project 31 - : data.project; 27 + const previewTags = 28 + hasPreview && data.preview.tags ? JSON.parse(data.preview.tags) : []; 32 29 30 + // Handle project display with preview 31 + const hasPreviewProject = hasPreview && data.preview.project; 32 + const displayProject = hasPreviewProject ? data.preview.project : data.project || ''; 33 + const isPreviewProject = hasPreviewProject && data.project !== data.preview.project; 33 34 34 - const getStateStyles = (status) => { 35 + const getStateStyles = (status) => { 35 36 switch (status) { 36 37 case 2: // preview-add 37 - return 'bg-yellow-100 border-yellow-600 text-yellow-700 '; 38 + return "bg-yellow-100 border-yellow-600 text-yellow-700 "; 38 39 case 3: // preview-done 39 - return 'bg-green-100 text-green-700 border-green-600 opacity-50'; 40 + return "bg-green-100 text-green-700 border-green-600 opacity-50"; 40 41 default: 41 - return isSelected(data.working_id) ? 'bg-muted border-primary' : 'bg-card'; 42 - } 43 - }; 44 - 45 - const getTagStateStyles = (status) => { 46 - switch (status) { 47 - case 2: // preview-add 48 - return 'bg-yellow-100 border-yellow-600 text-yellow-700 '; 49 - case 3: // preview-done 50 - return 'bg-green-100 text-green-700 border-green-600 opacity-50'; 51 - default: 52 - return "bg-background border-muted-foreground text-muted-foreground text-xs" 42 + return isSelected(data.working_id) 43 + ? "bg-muted border-primary" 44 + : "bg-card"; 53 45 } 54 46 }; 55 47 56 48 return ( 57 49 <> 58 50 <div 59 - className={`p-4 ${getStateStyles(data.completed)} hover:bg-muted/50 transition-colors`} 51 + className={`p-4 ${getStateStyles(data.completed)} hover:bg-muted/50 transition-colors`} 60 52 onClick={() => toggleSelection(data.working_id)} 61 53 > 62 54 <div className="flex items-start gap-4"> 63 55 <div className="flex-1 min-w-0"> 64 - <div className="flex items-center relative"> 56 + <div className="flex items-center gap-1 relative ml-[-0.5rem]"> 65 57 {displayProject && ( 66 - <div className="flex items-center"> 67 - <div className="w-2 h-2 rounded-full bg-blue-500"></div> 68 - <span className={`text-xs ${getStateStyles(data.completed)} leading-6 ml-1`}> 69 - {displayProject} 70 - </span> 71 - </div> 58 + <Project 59 + name={displayProject} 60 + status={data.completed} 61 + selected={isSelected(data.working_id)} 62 + isPreview={isPreviewProject} 63 + /> 72 64 )} 73 65 <div className="absolute right-4 top-1 w-6 h-6 flex items-center justify-center rounded-full border border-gray-800 text-gray-700 text-xs"> 74 66 {data.working_id} 75 67 </div> 76 68 </div> 77 69 78 - <div className="mt-1 flex flex-wrap gap-2 items-center"> 70 + <div className="mt-3 flex flex-wrap gap-2 items-center"> 79 71 <p className="text-sm font-medium leading-none truncate min-h-6 leading-6"> 80 72 {displayDescription} 81 73 </p> 82 - <div className="flex gap-1"> 83 - {(tagList && tagList.length > 0) && ( 84 - tagList.map((tag: string) => ( 85 - <Badge 86 - key={tag} 87 - className={`items-center ${getTagStateStyles(data.completed)} -mt-2`} 88 - > 89 - # {tag} 90 - </Badge> 91 - )) 92 - )} 93 - {(previewTags.length > 0) && ( 94 - previewTags.map((tag: string) => ( 95 - <Badge 96 - key={`preview-${tag}`} 97 - className="items-center text-xs bg-yellow-100 border-yellow-600 text-yellow-700 -mt-2" 98 - > 99 - # {tag} 100 - </Badge> 101 - )) 102 - )} 103 - </div> 74 + <div className="-mt-2.5"> 75 + <TagList 76 + tags={tagList} 77 + previewTags={previewTags} 78 + status={data.completed} 79 + /> 80 + </div> 104 81 {data.dueDate && ( 105 82 <span className="text-xs text-muted-foreground"> 106 83 Due: {data.dueDate}