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