an app to share curated trails sidetrail.app
atproto nextjs react rsc
51
fork

Configure Feed

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

better focus management

+143 -100
+5
app/EditButtons.css
··· 41 41 transition-duration: 0.05s; 42 42 } 43 43 44 + .edit-btn-utility:focus-visible { 45 + outline: 2px solid var(--accent-color); 46 + outline-offset: 2px; 47 + } 48 + 44 49 .edit-btn-add { 45 50 font-size: 1.25rem; 46 51 line-height: 0.9;
+14 -2
app/EditButtons.tsx
··· 19 19 className = "", 20 20 }: UtilityButtonProps & { className?: string }) { 21 21 return ( 22 - <button onClick={onClick} className={`edit-btn-utility ${className}`} title={title}> 22 + <button 23 + type="button" 24 + onClick={onClick} 25 + className={`edit-btn-utility ${className}`} 26 + title={title} 27 + aria-label={title} 28 + > 23 29 {children} 24 30 </button> 25 31 ); ··· 43 49 44 50 export function DeleteButton({ onClick, title }: DeleteButtonProps) { 45 51 return ( 46 - <button onClick={onClick} className="edit-btn-delete" title={title}> 52 + <button 53 + type="button" 54 + onClick={onClick} 55 + className="edit-btn-delete" 56 + title={title} 57 + aria-label={title} 58 + > 47 59 × 48 60 </button> 49 61 );
+11 -3
app/at/(trail)/[handle]/trail/[rkey]/TrailProgress.css
··· 50 50 box-shadow 0.2s ease; 51 51 position: relative; 52 52 flex-shrink: 0; 53 + /* Reset button styles */ 54 + padding: 0; 55 + border: none; 56 + background: none; 57 + font: inherit; 53 58 } 54 59 55 60 .TrailProgress-node--clickable { ··· 75 80 transition-duration: 0.1s; 76 81 } 77 82 83 + .TrailProgress-node:focus-visible { 84 + outline: 2px solid var(--accent-color); 85 + outline-offset: 2px; 86 + } 87 + 78 88 .TrailProgress-node--upcoming { 79 89 background-color: rgba(0, 0, 0, 0.08); 80 - border: none; 81 90 } 82 91 83 92 @media (prefers-color-scheme: dark) { 84 93 .TrailProgress-node--upcoming { 85 - background-color: transparent; 86 - border: 1px solid rgba(255, 255, 255, 0.2); 94 + background-color: rgba(255, 255, 255, 0.15); 87 95 } 88 96 } 89 97
+9 -3
app/at/(trail)/[handle]/trail/[rkey]/TrailProgress.tsx
··· 47 47 const isCurrent = i === currentStep; 48 48 const isVisited = i > currentStep && i <= maxReached; 49 49 50 + const canNavigate = isUnlocked && onStepClick; 51 + 50 52 return ( 51 53 <div key={i} className="TrailProgress-nodeWrapper"> 52 - <div 54 + <button 55 + type="button" 53 56 className={`TrailProgress-node ${ 54 57 isCompleted 55 58 ? "TrailProgress-node--completed" ··· 58 61 : isVisited 59 62 ? "TrailProgress-node--visited" 60 63 : "TrailProgress-node--upcoming" 61 - } ${isUnlocked && onStepClick ? "TrailProgress-node--clickable" : ""}`} 62 - onClick={() => isUnlocked && onStepClick?.(i)} 64 + } ${canNavigate ? "TrailProgress-node--clickable" : ""}`} 65 + onClick={() => canNavigate && onStepClick(i)} 66 + disabled={!canNavigate} 67 + aria-label={`Go to stop ${i + 1}${isCurrent ? " (current)" : isCompleted ? " (completed)" : isVisited ? " (visited)" : ""}`} 68 + aria-current={isCurrent ? "step" : undefined} 63 69 /> 64 70 {i < totalSteps - 1 && <div className="TrailProgress-line" />} 65 71 </div>
+2 -1
app/at/(trail)/[handle]/trail/[rkey]/TrailRegister.css
··· 42 42 padding-bottom: 2px; 43 43 } 44 44 45 - .TrailRegister-entry--deletable:hover .TrailRegister-deleteButton { 45 + .TrailRegister-entry--deletable:hover .TrailRegister-deleteButton, 46 + .TrailRegister-entry--deletable:has(:focus-visible) .TrailRegister-deleteButton { 46 47 opacity: 1; 47 48 pointer-events: auto; 48 49 }
+12 -14
app/at/(trail)/[handle]/trail/[rkey]/TrailStop.css
··· 49 49 transition-duration: 0.05s; 50 50 } 51 51 52 + .TrailStop:focus-visible { 53 + outline: 2px solid var(--accent-color); 54 + outline-offset: 2px; 55 + } 56 + 52 57 .TrailStop--current { 53 58 border: 2px solid var(--accent-color); 54 59 } ··· 127 132 pointer-events: none; 128 133 } 129 134 130 - .TrailStop--reorderActive .TrailStop-reorderControls { 135 + .TrailStop--reorderActive .TrailStop-reorderControls, 136 + .TrailStop-reorderControls:focus-within { 131 137 opacity: 1; 132 138 } 133 139 ··· 155 161 z-index: 5; 156 162 } 157 163 158 - .TrailStop-insertButton--before { 159 - top: -2rem; 160 - opacity: 0.3; 161 - transition: opacity 0.2s ease; 162 - } 163 - 164 - @media (hover: hover) { 165 - .TrailStop-insertButton--before:hover { 166 - opacity: 1; 167 - } 168 - } 169 - 170 164 .TrailStop-insertButton--between, 171 165 .TrailStop-insertButton--final { 172 166 top: calc(100% + 2rem); 173 167 } 174 168 175 - /* Between-stops buttons are subtle by default, show fully on hover */ 169 + /* Between-stops buttons are subtle by default, show fully on hover/focus */ 176 170 .TrailStop-insertButton--between { 177 171 opacity: 0.3; 178 172 transition: opacity 0.2s ease; 173 + } 174 + 175 + .TrailStop-insertButton--between:focus-within { 176 + opacity: 1; 179 177 } 180 178 181 179 /* Extend hover zone below the stop to include the gap area */
+89 -77
app/at/(trail)/[handle]/trail/[rkey]/TrailStop.tsx
··· 11 11 totalStops: number; 12 12 isStepCompleted: boolean; 13 13 isCurrent: boolean; 14 + isNextCurrent: boolean; 14 15 isVisited: boolean; 15 16 isClickable: boolean; 16 17 isReorderActive?: boolean; ··· 24 25 totalStops, 25 26 isStepCompleted, 26 27 isCurrent, 28 + isNextCurrent, 27 29 isVisited, 28 30 isClickable, 29 31 isReorderActive = false, ··· 36 38 const handleGoToStop = (newIndex: number) => { 37 39 onGoToStop(newIndex); 38 40 }; 41 + 42 + const stopLabel = `Stop ${index + 1}${stop.title ? `: ${stop.title}` : ""}${ 43 + isCurrent ? " (current)" : isStepCompleted ? " (completed)" : isVisited ? " (visited)" : "" 44 + }`; 39 45 40 46 return ( 41 47 <div> ··· 50 56 : "" 51 57 } ${isReorderActive && isEditing ? "TrailStop--reorderActive" : ""} ${isEditing ? "TrailStop--editing" : ""}`} 52 58 onClick={() => isClickable && onGoToStop(index)} 59 + onKeyDown={(e) => { 60 + if (isClickable && (e.key === "Enter" || e.key === " ") && e.target === e.currentTarget) { 61 + e.preventDefault(); 62 + onGoToStop(index); 63 + } 64 + }} 65 + onFocus={() => { 66 + if (!isCurrent) { 67 + onGoToStop(index); 68 + } 69 + }} 53 70 style={{ cursor: isClickable ? "pointer" : "default" }} 71 + tabIndex={isClickable && !isEditing ? 0 : undefined} 72 + role={isClickable && !isEditing ? "button" : undefined} 73 + aria-label={isClickable && !isEditing ? stopLabel : undefined} 54 74 > 55 75 {/* Opaque background - matches page background */} 56 76 <div className="TrailStop-bg" /> 57 77 58 78 <div className="TrailStop-content"> 59 - {/* Edit controls */} 79 + {/* Stop content first for logical tab order */} 80 + <div> 81 + <TrailStopCard 82 + stop={stop} 83 + stepNumber={index + 1} 84 + totalStops={totalStops} 85 + isCurrent={isCurrent} 86 + /> 87 + </div> 88 + 89 + {/* View mode: done button */} 90 + {isCurrent && !isEditing && ( 91 + <div className="TrailStop-actions"> 92 + <AccentButton 93 + onClick={() => { 94 + onContinue(); 95 + }} 96 + size="large" 97 + > 98 + {stop.buttonText || "done that"} 99 + </AccentButton> 100 + </div> 101 + )} 102 + 103 + {/* Edit mode controls - after content for tab order */} 60 104 {isEditing && editContext && ( 61 105 <> 62 - {/* Reorder controls - only show on current step */} 106 + {/* Delete control - visually top right */} 107 + <div className="TrailStop-deleteButton"> 108 + <DeleteButton 109 + onClick={(e) => { 110 + e.stopPropagation(); 111 + if (totalStops === 1) { 112 + alert("you need at least one stop"); 113 + return; 114 + } 115 + const isEmpty = !stop.title.trim() && !stop.content.trim(); 116 + const shouldDelete = isEmpty || confirm("delete this stop?"); 117 + 118 + if (shouldDelete) { 119 + if (isCurrent && index > 0) { 120 + handleGoToStop(index - 1); 121 + } 122 + editContext.deleteStop(stop.tid); 123 + } 124 + }} 125 + title="delete stop" 126 + /> 127 + </div> 128 + 129 + {/* Reorder controls - only on current stop */} 63 130 {isCurrent && ( 64 131 <div className="TrailStop-reorderControls"> 65 132 {index > 0 && ( ··· 88 155 )} 89 156 </div> 90 157 )} 91 - {/* Delete control - top right subtle × */} 92 - <div className="TrailStop-deleteButton"> 93 - <DeleteButton 94 - onClick={(e) => { 95 - e.stopPropagation(); 96 - if (totalStops === 1) { 97 - alert("you need at least one stop"); 98 - return; 99 - } 100 - const isEmpty = !stop.title.trim() && !stop.content.trim(); 101 - const shouldDelete = isEmpty || confirm("delete this stop?"); 102 158 103 - if (shouldDelete) { 104 - if (isCurrent && index > 0) { 105 - handleGoToStop(index - 1); 106 - } 107 - editContext.deleteStop(stop.tid); 108 - } 109 - }} 110 - title="delete stop" 111 - /> 112 - </div> 159 + {/* Insert after button - visually between this stop and next. 160 + Show if this stop is current, next stop is current (so it appears above current), 161 + or it's the last stop with content */} 162 + {(isCurrent || 163 + isNextCurrent || 164 + (index === totalStops - 1 && (stop.title.trim() || stop.content.trim()))) && 165 + totalStops < 12 && ( 166 + <div 167 + className={`TrailStop-insertButton ${index === totalStops - 1 ? "TrailStop-insertButton--final" : "TrailStop-insertButton--between"}`} 168 + > 169 + <AddButton 170 + onClick={(e) => { 171 + e.stopPropagation(); 172 + editContext.addStop(index); 173 + handleGoToStop(index + 1); 174 + }} 175 + title="add stop after" 176 + /> 177 + </div> 178 + )} 113 179 </> 114 180 )} 115 - 116 - <div> 117 - <TrailStopCard 118 - stop={stop} 119 - stepNumber={index + 1} 120 - totalStops={totalStops} 121 - isCurrent={isCurrent} 122 - /> 123 - </div> 124 - 125 - {isCurrent && !isEditing && ( 126 - <div className="TrailStop-actions"> 127 - <AccentButton 128 - onClick={() => { 129 - onContinue(); 130 - }} 131 - size="large" 132 - > 133 - {stop.buttonText || "done that"} 134 - </AccentButton> 135 - </div> 136 - )} 137 - 138 - {isEditing && editContext && isCurrent && index > 0 && totalStops < 12 && ( 139 - <div className="TrailStop-insertButton TrailStop-insertButton--before"> 140 - <AddButton 141 - onClick={(e) => { 142 - e.stopPropagation(); 143 - editContext.addStop(index - 1); 144 - handleGoToStop(index); 145 - }} 146 - title="insert stop before" 147 - /> 148 - </div> 149 - )} 150 - 151 - {isEditing && 152 - editContext && 153 - (isCurrent || 154 - (index === totalStops - 1 && (stop.title.trim() || stop.content.trim()))) && 155 - totalStops < 12 && ( 156 - <div 157 - className={`TrailStop-insertButton ${index === totalStops - 1 ? "TrailStop-insertButton--final" : "TrailStop-insertButton--between"}`} 158 - > 159 - <AddButton 160 - onClick={(e) => { 161 - e.stopPropagation(); 162 - editContext.addStop(index); 163 - handleGoToStop(index + 1); 164 - }} 165 - title="insert stop after" 166 - /> 167 - </div> 168 - )} 169 181 </div> 170 182 </div> 171 183 </div>
+1
app/at/(trail)/[handle]/trail/[rkey]/TrailWalk.tsx
··· 270 270 totalStops={stops.length} 271 271 isStepCompleted={isStepCompleted} 272 272 isCurrent={isCurrent} 273 + isNextCurrent={index + 1 === currentStopIndex} 273 274 isVisited={isVisited} 274 275 isClickable={isClickable} 275 276 isReorderActive={isReorderActive}