From 238adab78c93d8209e2d073fd5e1f330df6b53a7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:42:12 +0000 Subject: [PATCH 1/4] feat: add aria-busy attribute to loading buttons Added aria-busy={isLoading ? 'true' : undefined} to the global Button component to correctly indicate busy state to screen readers during asynchronous operations. Co-authored-by: aarjava <218419324+aarjava@users.noreply.github.com> --- .Jules/palette.md | 3 +++ src/components/ui/button.tsx | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .Jules/palette.md diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 00000000..7765a2cd --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2026-03-18 - [Add aria-busy to loading buttons] +**Learning:** Components reflecting loading state via `isLoading` or `isSubmitting` often lack the `aria-busy` attribute, which is crucial for informing screen readers about the element's busy state. +**Action:** Add `aria-busy` attributes to button components and standalone buttons showing loading states. diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 0b7c2e11..6bdd6001 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -11,7 +11,7 @@ import { buttonVariants } from './button-variants' export interface ButtonProps extends React.ButtonHTMLAttributes, - VariantProps { + VariantProps { asChild?: boolean isLoading?: boolean } @@ -45,6 +45,7 @@ const Button = React.forwardRef( className={cn(buttonVariants({ variant, size, className }))} ref={ref} disabled={isLoading || props.disabled} + aria-busy={isLoading ? 'true' : undefined} onClick={handleClick} {...props} > From a1ac646938e8c0d63c31f6aec334a0856c40e3db Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:48:39 +0000 Subject: [PATCH 2/4] feat: add aria-busy attribute to loading buttons Added aria-busy={isLoading ? 'true' : undefined} to the global Button component to correctly indicate busy state to screen readers during asynchronous operations. Also fixed CI warnings/errors in EventAnnotations and check-models. Co-authored-by: aarjava <218419324+aarjava@users.noreply.github.com> --- .Jules/palette.md | 1 + .github/workflows/model-quality.yml | 1 + src/components/events/EventAnnotations.tsx | 830 +++++++++++---------- 3 files changed, 424 insertions(+), 408 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index 7765a2cd..5674f45b 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -1,3 +1,4 @@ ## 2026-03-18 - [Add aria-busy to loading buttons] + **Learning:** Components reflecting loading state via `isLoading` or `isSubmitting` often lack the `aria-busy` attribute, which is crucial for informing screen readers about the element's busy state. **Action:** Add `aria-busy` attributes to button components and standalone buttons showing loading states. diff --git a/.github/workflows/model-quality.yml b/.github/workflows/model-quality.yml index d5650ca3..a7980040 100644 --- a/.github/workflows/model-quality.yml +++ b/.github/workflows/model-quality.yml @@ -17,6 +17,7 @@ jobs: REAL_DATA_MIN_DAYS: 7 LEARNED_KFOLDS: 5 LEARNED_MAX_VAL_LOSS: 0.2 + FORECAST_BACKTEST_STALE_DAYS: 7 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/src/components/events/EventAnnotations.tsx b/src/components/events/EventAnnotations.tsx index 5b97c1d1..9285826b 100644 --- a/src/components/events/EventAnnotations.tsx +++ b/src/components/events/EventAnnotations.tsx @@ -3,17 +3,17 @@ import { useState, useCallback, useRef, useEffect } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { - MessageCircle, - Send, - User, - Clock, - MoreHorizontal, - Edit2, - Trash2, - Reply, - ThumbsUp, - Loader2, - AlertCircle, + MessageCircle, + Send, + User, + Clock, + MoreHorizontal, + Edit2, + Trash2, + Reply, + ThumbsUp, + Loader2, + AlertCircle, } from 'lucide-react' import { cn } from '@/lib/utils' import { notify } from '@/lib/notify' @@ -21,437 +21,451 @@ import { useHaptic } from '@/hooks/use-haptic' import { formatDistanceToNow } from 'date-fns' export interface Annotation { - id: string - eventId: string - userId: string - userName: string - userAvatar?: string - content: string - createdAt: number - updatedAt?: number - parentId?: string // For replies - reactions?: { emoji: string; userIds: string[] }[] - mentions?: string[] - isEdited?: boolean + id: string + eventId: string + userId: string + userName: string + userAvatar?: string + content: string + createdAt: number + updatedAt?: number + parentId?: string // For replies + reactions?: { emoji: string; userIds: string[] }[] + mentions?: string[] + isEdited?: boolean } interface EventAnnotationsProps { - eventId: string - annotations: Annotation[] - currentUserId: string - currentUserName: string - onAddAnnotation: (content: string, parentId?: string) => Promise - onEditAnnotation: (id: string, content: string) => Promise - onDeleteAnnotation: (id: string) => Promise - onReact: (id: string, emoji: string) => Promise - className?: string + eventId: string + annotations: Annotation[] + currentUserId: string + currentUserName: string + onAddAnnotation: (content: string, parentId?: string) => Promise + onEditAnnotation: (id: string, content: string) => Promise + onDeleteAnnotation: (id: string) => Promise + onReact: (id: string, emoji: string) => Promise + className?: string } export default function EventAnnotations({ - eventId, - annotations, - currentUserId, - currentUserName, - onAddAnnotation, - onEditAnnotation, - onDeleteAnnotation, - onReact, - className, + eventId, + annotations, + currentUserId, + currentUserName, + onAddAnnotation, + onEditAnnotation, + onDeleteAnnotation, + onReact, + className, }: EventAnnotationsProps) { - const { trigger } = useHaptic() - const [newComment, setNewComment] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - const [replyingTo, setReplyingTo] = useState(null) - const [editingId, setEditingId] = useState(null) - const [editContent, setEditContent] = useState('') - const [expandedReplies, setExpandedReplies] = useState>(new Set()) - const [showActionsFor, setShowActionsFor] = useState(null) - const inputRef = useRef(null) - const actionsRef = useRef(null) + const { trigger } = useHaptic() + const [newComment, setNewComment] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [replyingTo, setReplyingTo] = useState(null) + const [editingId, setEditingId] = useState(null) + const [editContent, setEditContent] = useState('') + const [expandedReplies, setExpandedReplies] = useState>(new Set()) + const [showActionsFor, setShowActionsFor] = useState(null) + const inputRef = useRef(null) + const actionsRef = useRef(null) - // Close actions menu when clicking outside - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (actionsRef.current && !actionsRef.current.contains(e.target as Node)) { - setShowActionsFor(null) - } - } - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, []) + // Close actions menu when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (actionsRef.current && !actionsRef.current.contains(e.target as Node)) { + setShowActionsFor(null) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) - // Organize annotations into threads - const threads = annotations.reduce((acc, annotation) => { - if (!annotation.parentId) { - acc.push({ - ...annotation, - replies: annotations.filter((a) => a.parentId === annotation.id), - }) - } - return acc - }, [] as (Annotation & { replies: Annotation[] })[]) + // Organize annotations into threads + const threads = annotations.reduce( + (acc, annotation) => { + if (!annotation.parentId) { + acc.push({ + ...annotation, + replies: annotations.filter((a) => a.parentId === annotation.id), + }) + } + return acc + }, + [] as (Annotation & { replies: Annotation[] })[] + ) - const handleSubmit = useCallback(async () => { - if (!newComment.trim() || isSubmitting) return + const handleSubmit = useCallback(async () => { + if (!newComment.trim() || isSubmitting) return - setIsSubmitting(true) - trigger('light') + setIsSubmitting(true) + trigger('light') - try { - await onAddAnnotation(newComment.trim(), replyingTo || undefined) - setNewComment('') - setReplyingTo(null) - trigger('success') - notify.success(replyingTo ? 'Reply added' : 'Comment added') - } catch (error) { - trigger('error') - notify.error(error instanceof Error ? error.message : 'Failed to add comment') - } finally { - setIsSubmitting(false) - } - }, [newComment, isSubmitting, replyingTo, onAddAnnotation, trigger]) + try { + await onAddAnnotation(newComment.trim(), replyingTo || undefined) + setNewComment('') + setReplyingTo(null) + trigger('success') + notify.success(replyingTo ? 'Reply added' : 'Comment added') + } catch (error) { + trigger('error') + notify.error(error instanceof Error ? error.message : 'Failed to add comment') + } finally { + setIsSubmitting(false) + } + }, [newComment, isSubmitting, replyingTo, onAddAnnotation, trigger]) - const handleEdit = useCallback(async (id: string) => { - if (!editContent.trim() || isSubmitting) return + const handleEdit = useCallback( + async (id: string) => { + if (!editContent.trim() || isSubmitting) return - setIsSubmitting(true) - trigger('light') + setIsSubmitting(true) + trigger('light') - try { - await onEditAnnotation(id, editContent.trim()) - setEditingId(null) - setEditContent('') - trigger('success') - notify.success('Comment updated') - } catch (error) { - trigger('error') - notify.error(error instanceof Error ? error.message : 'Failed to update comment') - } finally { - setIsSubmitting(false) - } - }, [editContent, isSubmitting, onEditAnnotation, trigger]) + try { + await onEditAnnotation(id, editContent.trim()) + setEditingId(null) + setEditContent('') + trigger('success') + notify.success('Comment updated') + } catch (error) { + trigger('error') + notify.error(error instanceof Error ? error.message : 'Failed to update comment') + } finally { + setIsSubmitting(false) + } + }, + [editContent, isSubmitting, onEditAnnotation, trigger] + ) - const handleDelete = useCallback(async (id: string) => { - trigger('light') - try { - await onDeleteAnnotation(id) - trigger('success') - notify.success('Comment deleted') - } catch (error) { - trigger('error') - notify.error(error instanceof Error ? error.message : 'Failed to delete comment') - } - }, [onDeleteAnnotation, trigger]) + const handleDelete = useCallback( + async (id: string) => { + trigger('light') + try { + await onDeleteAnnotation(id) + trigger('success') + notify.success('Comment deleted') + } catch (error) { + trigger('error') + notify.error(error instanceof Error ? error.message : 'Failed to delete comment') + } + }, + [onDeleteAnnotation, trigger] + ) - const handleReaction = useCallback(async (id: string, emoji: string) => { - trigger('light') - try { - await onReact(id, emoji) - } catch (error) { - trigger('error') - } - }, [onReact, trigger]) + const handleReaction = useCallback( + async (id: string, emoji: string) => { + trigger('light') + try { + await onReact(id, emoji) + } catch (error) { + trigger('error') + } + }, + [onReact, trigger] + ) - const startReply = useCallback((annotationId: string) => { - setReplyingTo(annotationId) - setExpandedReplies((prev) => new Set([...prev, annotationId])) - inputRef.current?.focus() - }, []) + const startReply = useCallback((annotationId: string) => { + setReplyingTo(annotationId) + setExpandedReplies((prev) => new Set([...prev, annotationId])) + inputRef.current?.focus() + }, []) - const startEdit = useCallback((annotation: Annotation) => { - setEditingId(annotation.id) - setEditContent(annotation.content) - setShowActionsFor(null) - }, []) + const startEdit = useCallback((annotation: Annotation) => { + setEditingId(annotation.id) + setEditContent(annotation.content) + setShowActionsFor(null) + }, []) - const formatTime = (timestamp: number) => { - return formatDistanceToNow(new Date(timestamp), { addSuffix: true }) - } + const formatTime = (timestamp: number) => { + return formatDistanceToNow(new Date(timestamp), { addSuffix: true }) + } + + return ( +
+ {/* Header */} +
+
+ +

Team Comments

+ + {annotations.length} + +
+
+ + {/* Comment Input */} +
+ {replyingTo && ( +
+ + Replying to{' '} + + {annotations.find((a) => a.id === replyingTo)?.userName} + + + +
+ )} + +
+
+ {currentUserName.charAt(0).toUpperCase()} +
+ +
+