Command bar: add extrude command, nonlinear editing, etc (#1204)
* Tweak toaster look and feel * Add icons, tweak plus icon names * Rename commandBarMeta to commandBarConfig * Refactor command bar, add support for icons * Create a tailwind plugin for aria-pressed button state * Remove overlay from behind command bar * Clean up toolbar * Button and other style tweaks * Icon tweaks follow-up: make old icons work with new sizing * Delete unused static icons * More CSS tweaks * Small CSS tweak to project sidebar * Add command bar E2E test * fumpt * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * fix typo in a comment * Fix icon padding (built version only) * Update onboarding and warning banner icons padding * Misc minor style fixes * Get Extrude opening and canceling from command bar * Iconography tweaks * Get extrude kind of working * Refactor command bar config types and organization * Move command bar configs to be co-located with each other * Start building a state machine for the command bar * Start converting command bar to state machine * Add support for multiple args, confirmation step * Submission behavior, hotkeys, code organization * Add new test for extruding from command bar * Polish step back and selection hotkeys, CSS tweaks * Loading style tweaks * Validate selection inputs, polish UX of args re-editing * Prevent submission with multiple selection on singlular arg * Remove stray console logs * Tweak test, CSS nit, remove extrude "result" argument * Fix linting warnings * Show Ctrl+/ instead of ⌘K on all platforms but Mac * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Add "Enter sketch" to command bar * fix command bar test * Fix flaky cmd bar extrude test by waiting for engine select response * Cover both button labels '⌘K' and 'Ctrl+/' in test --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
		
										
											Binary file not shown.
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -8,275 +8,275 @@ property float z
 | 
			
		||||
element face 68
 | 
			
		||||
property list uchar uint vertex_indices
 | 
			
		||||
end_header
 | 
			
		||||
0 0 4 
 | 
			
		||||
0 0 0 
 | 
			
		||||
0 -1 4 
 | 
			
		||||
0 -1 4 
 | 
			
		||||
0 0 0 
 | 
			
		||||
0 -1 0 
 | 
			
		||||
0 -1 4 
 | 
			
		||||
0 -1 0 
 | 
			
		||||
3.0950184 -1 4 
 | 
			
		||||
3.0950184 -1 4 
 | 
			
		||||
0 -1 0 
 | 
			
		||||
3.0950184 -1 0 
 | 
			
		||||
3.0950184 -1 4 
 | 
			
		||||
3.0950184 -1 0 
 | 
			
		||||
5.9513144 -3 4 
 | 
			
		||||
5.9513144 -3 4 
 | 
			
		||||
3.0950184 -1 0 
 | 
			
		||||
5.9513144 -3 0 
 | 
			
		||||
5.9513144 -3 4 
 | 
			
		||||
5.9513144 -3 0 
 | 
			
		||||
9.5 -3 4 
 | 
			
		||||
9.5 -3 4 
 | 
			
		||||
5.9513144 -3 0 
 | 
			
		||||
9.5 -3 0 
 | 
			
		||||
9.5 -3 4 
 | 
			
		||||
9.5 -3 0 
 | 
			
		||||
9.5 -2.5 4 
 | 
			
		||||
9.5 -2.5 4 
 | 
			
		||||
9.5 -3 0 
 | 
			
		||||
9.5 -2.5 0 
 | 
			
		||||
9.5 -2.5 4 
 | 
			
		||||
9.5 -2.5 0 
 | 
			
		||||
6.108964 -2.5 4 
 | 
			
		||||
6.108964 -2.5 4 
 | 
			
		||||
9.5 -2.5 0 
 | 
			
		||||
6.108964 -2.5 0 
 | 
			
		||||
3.4311862 -0.625 4 
 | 
			
		||||
4.323779 -1.25 4 
 | 
			
		||||
4.323779 -1.25 0 
 | 
			
		||||
4.323779 -1.25 4 
 | 
			
		||||
6.108964 -2.5 4 
 | 
			
		||||
6.108964 -2.5 0 
 | 
			
		||||
3.4311862 -0.625 0 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
3.4311862 -0.625 4 
 | 
			
		||||
3.4311862 -0.625 0 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
4.323779 -1.25 4 
 | 
			
		||||
6.108964 -2.5 0 
 | 
			
		||||
4.323779 -1.25 0 
 | 
			
		||||
3.4311862 -0.625 0 
 | 
			
		||||
3.4311862 -0.625 4 
 | 
			
		||||
4.323779 -1.25 0 
 | 
			
		||||
3.342784 0.375 4 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
4.146974 0.75 4 
 | 
			
		||||
3.342784 0.375 4 
 | 
			
		||||
3.342784 0.375 0 
 | 
			
		||||
3.342784 0.375 0 
 | 
			
		||||
4.146974 0.75 0 
 | 
			
		||||
4.146974 0.75 4 
 | 
			
		||||
4.146974 0.75 0 
 | 
			
		||||
5.755354 1.5 0 
 | 
			
		||||
5.755354 1.5 4 
 | 
			
		||||
3.342784 0.375 4 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
3.342784 0.375 0 
 | 
			
		||||
5.755354 1.5 4 
 | 
			
		||||
4.146974 0.75 4 
 | 
			
		||||
4.146974 0.75 0 
 | 
			
		||||
5.755354 1.5 4 
 | 
			
		||||
5.755354 1.5 0 
 | 
			
		||||
9.5 1.5 4 
 | 
			
		||||
9.5 1.5 4 
 | 
			
		||||
5.755354 1.5 0 
 | 
			
		||||
9.5 1.5 0 
 | 
			
		||||
9.5 1.5 4 
 | 
			
		||||
9.5 1.5 0 
 | 
			
		||||
9.5 2 4 
 | 
			
		||||
9.5 2 4 
 | 
			
		||||
9.5 1.5 0 
 | 
			
		||||
9.5 2 0 
 | 
			
		||||
9.5 2 4 
 | 
			
		||||
9.5 2 0 
 | 
			
		||||
5.644507 2 4 
 | 
			
		||||
5.644507 2 4 
 | 
			
		||||
9.5 2 0 
 | 
			
		||||
5.644507 2 0 
 | 
			
		||||
5.644507 2 4 
 | 
			
		||||
5.644507 2 0 
 | 
			
		||||
3.5 1 4 
 | 
			
		||||
3.5 1 4 
 | 
			
		||||
5.644507 2 0 
 | 
			
		||||
3.5 1 0 
 | 
			
		||||
3.5 1 4 
 | 
			
		||||
3.5 1 0 
 | 
			
		||||
0 1 4 
 | 
			
		||||
0 1 4 
 | 
			
		||||
3.5 1 0 
 | 
			
		||||
0 1 0 
 | 
			
		||||
0 1 4 
 | 
			
		||||
0 1 0 
 | 
			
		||||
0 0 4 
 | 
			
		||||
0 0 4 
 | 
			
		||||
0 1 0 
 | 
			
		||||
0 0 0 
 | 
			
		||||
3.342784 0.375 0 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
3.5 1 0 
 | 
			
		||||
3.4311862 -0.625 0 
 | 
			
		||||
4.323779 -1.25 0 
 | 
			
		||||
3.0950184 -1 0 
 | 
			
		||||
3.342784 0.375 0 
 | 
			
		||||
3.5 1 0 
 | 
			
		||||
4.146974 0.75 0 
 | 
			
		||||
4.323779 -1.25 0 
 | 
			
		||||
5.9513144 -3 0 
 | 
			
		||||
3.0950184 -1 0 
 | 
			
		||||
0 -1 0 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
3.0950184 -1 0 
 | 
			
		||||
0 -1 0 
 | 
			
		||||
0 0 0 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
9.5 -3 0 
 | 
			
		||||
6.108964 -2.5 0 
 | 
			
		||||
9.5 -2.5 0 
 | 
			
		||||
9.5 -3 0 
 | 
			
		||||
5.9513144 -3 0 
 | 
			
		||||
6.108964 -2.5 0 
 | 
			
		||||
5.9513144 -3 0 
 | 
			
		||||
4.323779 -1.25 0 
 | 
			
		||||
6.108964 -2.5 0 
 | 
			
		||||
5.644507 2 0 
 | 
			
		||||
5.755354 1.5 0 
 | 
			
		||||
4.146974 0.75 0 
 | 
			
		||||
3.0950184 -1 0 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
3.4311862 -0.625 0 
 | 
			
		||||
4.146974 0.75 0 
 | 
			
		||||
3.5 1 0 
 | 
			
		||||
5.644507 2 0 
 | 
			
		||||
9.5 1.5 0 
 | 
			
		||||
5.755354 1.5 0 
 | 
			
		||||
9.5 2 0 
 | 
			
		||||
5.755354 1.5 0 
 | 
			
		||||
5.644507 2 0 
 | 
			
		||||
9.5 2 0 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
0 0 0 
 | 
			
		||||
0 1 0 
 | 
			
		||||
3.5 1 0 
 | 
			
		||||
2.5385938 0 0 
 | 
			
		||||
0 1 0 
 | 
			
		||||
3.342784 0.375 4 
 | 
			
		||||
3.5 1 4 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
4.146974 0.75 4 
 | 
			
		||||
3.5 1 4 
 | 
			
		||||
3.342784 0.375 4 
 | 
			
		||||
3.4311862 -0.625 4 
 | 
			
		||||
3.0950184 -1 4 
 | 
			
		||||
4.323779 -1.25 4 
 | 
			
		||||
4.146974 0.75 4 
 | 
			
		||||
5.755354 1.5 4 
 | 
			
		||||
5.644507 2 4 
 | 
			
		||||
0 1 4 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
3.5 1 4 
 | 
			
		||||
0 1 4 
 | 
			
		||||
0 0 4 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
5.644507 2 4 
 | 
			
		||||
5.755354 1.5 4 
 | 
			
		||||
9.5 2 4 
 | 
			
		||||
9.5 2 4 
 | 
			
		||||
5.755354 1.5 4 
 | 
			
		||||
9.5 1.5 4 
 | 
			
		||||
4.146974 0.75 4 
 | 
			
		||||
5.644507 2 4 
 | 
			
		||||
3.5 1 4 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
3.0950184 -1 4 
 | 
			
		||||
3.4311862 -0.625 4 
 | 
			
		||||
4.323779 -1.25 4 
 | 
			
		||||
3.0950184 -1 4 
 | 
			
		||||
5.9513144 -3 4 
 | 
			
		||||
6.108964 -2.5 4 
 | 
			
		||||
4.323779 -1.25 4 
 | 
			
		||||
5.9513144 -3 4 
 | 
			
		||||
9.5 -2.5 4 
 | 
			
		||||
6.108964 -2.5 4 
 | 
			
		||||
9.5 -3 4 
 | 
			
		||||
6.108964 -2.5 4 
 | 
			
		||||
5.9513144 -3 4 
 | 
			
		||||
9.5 -3 4 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
0 -1 4 
 | 
			
		||||
3.0950184 -1 4 
 | 
			
		||||
0 -1 4 
 | 
			
		||||
2.5385938 0 4 
 | 
			
		||||
0 0 4 
 | 
			
		||||
3 0 1 2 
 | 
			
		||||
3 3 4 5 
 | 
			
		||||
3 6 7 8 
 | 
			
		||||
3 9 10 11 
 | 
			
		||||
3 12 13 14 
 | 
			
		||||
3 15 16 17 
 | 
			
		||||
3 18 19 20 
 | 
			
		||||
3 21 22 23 
 | 
			
		||||
3 24 25 26 
 | 
			
		||||
3 27 28 29 
 | 
			
		||||
3 30 31 32 
 | 
			
		||||
3 33 34 35 
 | 
			
		||||
3 36 37 38 
 | 
			
		||||
3 39 40 41 
 | 
			
		||||
3 42 43 44 
 | 
			
		||||
3 45 46 47 
 | 
			
		||||
3 48 49 50 
 | 
			
		||||
3 51 52 53 
 | 
			
		||||
3 54 55 56 
 | 
			
		||||
3 57 58 59 
 | 
			
		||||
3 60 61 62 
 | 
			
		||||
3 63 64 65 
 | 
			
		||||
3 66 67 68 
 | 
			
		||||
3 69 70 71 
 | 
			
		||||
3 72 73 74 
 | 
			
		||||
3 75 76 77 
 | 
			
		||||
3 78 79 80 
 | 
			
		||||
3 81 82 83 
 | 
			
		||||
3 84 85 86 
 | 
			
		||||
3 87 88 89 
 | 
			
		||||
3 90 91 92 
 | 
			
		||||
3 93 94 95 
 | 
			
		||||
3 96 97 98 
 | 
			
		||||
3 99 100 101 
 | 
			
		||||
3 102 103 104 
 | 
			
		||||
3 105 106 107 
 | 
			
		||||
3 108 109 110 
 | 
			
		||||
3 111 112 113 
 | 
			
		||||
3 114 115 116 
 | 
			
		||||
3 117 118 119 
 | 
			
		||||
3 120 121 122 
 | 
			
		||||
3 123 124 125 
 | 
			
		||||
3 126 127 128 
 | 
			
		||||
3 129 130 131 
 | 
			
		||||
3 132 133 134 
 | 
			
		||||
3 135 136 137 
 | 
			
		||||
3 138 139 140 
 | 
			
		||||
3 141 142 143 
 | 
			
		||||
3 144 145 146 
 | 
			
		||||
3 147 148 149 
 | 
			
		||||
3 150 151 152 
 | 
			
		||||
3 153 154 155 
 | 
			
		||||
3 156 157 158 
 | 
			
		||||
3 159 160 161 
 | 
			
		||||
3 162 163 164 
 | 
			
		||||
3 165 166 167 
 | 
			
		||||
3 168 169 170 
 | 
			
		||||
3 171 172 173 
 | 
			
		||||
3 174 175 176 
 | 
			
		||||
3 177 178 179 
 | 
			
		||||
3 180 181 182 
 | 
			
		||||
3 183 184 185 
 | 
			
		||||
3 186 187 188 
 | 
			
		||||
3 189 190 191 
 | 
			
		||||
3 192 193 194 
 | 
			
		||||
3 195 196 197 
 | 
			
		||||
3 198 199 200 
 | 
			
		||||
3 201 202 203 
 | 
			
		||||
0 0 4
 | 
			
		||||
0 0 0
 | 
			
		||||
0 -1 4
 | 
			
		||||
0 -1 4
 | 
			
		||||
0 0 0
 | 
			
		||||
0 -1 0
 | 
			
		||||
0 -1 4
 | 
			
		||||
0 -1 0
 | 
			
		||||
3.0950184 -1 4
 | 
			
		||||
3.0950184 -1 4
 | 
			
		||||
0 -1 0
 | 
			
		||||
3.0950184 -1 0
 | 
			
		||||
3.0950184 -1 4
 | 
			
		||||
3.0950184 -1 0
 | 
			
		||||
5.9513144 -3 4
 | 
			
		||||
5.9513144 -3 4
 | 
			
		||||
3.0950184 -1 0
 | 
			
		||||
5.9513144 -3 0
 | 
			
		||||
5.9513144 -3 4
 | 
			
		||||
5.9513144 -3 0
 | 
			
		||||
9.5 -3 4
 | 
			
		||||
9.5 -3 4
 | 
			
		||||
5.9513144 -3 0
 | 
			
		||||
9.5 -3 0
 | 
			
		||||
9.5 -3 4
 | 
			
		||||
9.5 -3 0
 | 
			
		||||
9.5 -2.5 4
 | 
			
		||||
9.5 -2.5 4
 | 
			
		||||
9.5 -3 0
 | 
			
		||||
9.5 -2.5 0
 | 
			
		||||
9.5 -2.5 4
 | 
			
		||||
9.5 -2.5 0
 | 
			
		||||
6.108964 -2.5 4
 | 
			
		||||
6.108964 -2.5 4
 | 
			
		||||
9.5 -2.5 0
 | 
			
		||||
6.108964 -2.5 0
 | 
			
		||||
3.4311862 -0.625 4
 | 
			
		||||
4.323779 -1.25 4
 | 
			
		||||
4.323779 -1.25 0
 | 
			
		||||
4.323779 -1.25 4
 | 
			
		||||
6.108964 -2.5 4
 | 
			
		||||
6.108964 -2.5 0
 | 
			
		||||
3.4311862 -0.625 0
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
3.4311862 -0.625 4
 | 
			
		||||
3.4311862 -0.625 0
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
4.323779 -1.25 4
 | 
			
		||||
6.108964 -2.5 0
 | 
			
		||||
4.323779 -1.25 0
 | 
			
		||||
3.4311862 -0.625 0
 | 
			
		||||
3.4311862 -0.625 4
 | 
			
		||||
4.323779 -1.25 0
 | 
			
		||||
3.342784 0.375 4
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
4.146974 0.75 4
 | 
			
		||||
3.342784 0.375 4
 | 
			
		||||
3.342784 0.375 0
 | 
			
		||||
3.342784 0.375 0
 | 
			
		||||
4.146974 0.75 0
 | 
			
		||||
4.146974 0.75 4
 | 
			
		||||
4.146974 0.75 0
 | 
			
		||||
5.755354 1.5 0
 | 
			
		||||
5.755354 1.5 4
 | 
			
		||||
3.342784 0.375 4
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
3.342784 0.375 0
 | 
			
		||||
5.755354 1.5 4
 | 
			
		||||
4.146974 0.75 4
 | 
			
		||||
4.146974 0.75 0
 | 
			
		||||
5.755354 1.5 4
 | 
			
		||||
5.755354 1.5 0
 | 
			
		||||
9.5 1.5 4
 | 
			
		||||
9.5 1.5 4
 | 
			
		||||
5.755354 1.5 0
 | 
			
		||||
9.5 1.5 0
 | 
			
		||||
9.5 1.5 4
 | 
			
		||||
9.5 1.5 0
 | 
			
		||||
9.5 2 4
 | 
			
		||||
9.5 2 4
 | 
			
		||||
9.5 1.5 0
 | 
			
		||||
9.5 2 0
 | 
			
		||||
9.5 2 4
 | 
			
		||||
9.5 2 0
 | 
			
		||||
5.644507 2 4
 | 
			
		||||
5.644507 2 4
 | 
			
		||||
9.5 2 0
 | 
			
		||||
5.644507 2 0
 | 
			
		||||
5.644507 2 4
 | 
			
		||||
5.644507 2 0
 | 
			
		||||
3.5 1 4
 | 
			
		||||
3.5 1 4
 | 
			
		||||
5.644507 2 0
 | 
			
		||||
3.5 1 0
 | 
			
		||||
3.5 1 4
 | 
			
		||||
3.5 1 0
 | 
			
		||||
0 1 4
 | 
			
		||||
0 1 4
 | 
			
		||||
3.5 1 0
 | 
			
		||||
0 1 0
 | 
			
		||||
0 1 4
 | 
			
		||||
0 1 0
 | 
			
		||||
0 0 4
 | 
			
		||||
0 0 4
 | 
			
		||||
0 1 0
 | 
			
		||||
0 0 0
 | 
			
		||||
3.342784 0.375 0
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
3.5 1 0
 | 
			
		||||
3.4311862 -0.625 0
 | 
			
		||||
4.323779 -1.25 0
 | 
			
		||||
3.0950184 -1 0
 | 
			
		||||
3.342784 0.375 0
 | 
			
		||||
3.5 1 0
 | 
			
		||||
4.146974 0.75 0
 | 
			
		||||
4.323779 -1.25 0
 | 
			
		||||
5.9513144 -3 0
 | 
			
		||||
3.0950184 -1 0
 | 
			
		||||
0 -1 0
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
3.0950184 -1 0
 | 
			
		||||
0 -1 0
 | 
			
		||||
0 0 0
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
9.5 -3 0
 | 
			
		||||
6.108964 -2.5 0
 | 
			
		||||
9.5 -2.5 0
 | 
			
		||||
9.5 -3 0
 | 
			
		||||
5.9513144 -3 0
 | 
			
		||||
6.108964 -2.5 0
 | 
			
		||||
5.9513144 -3 0
 | 
			
		||||
4.323779 -1.25 0
 | 
			
		||||
6.108964 -2.5 0
 | 
			
		||||
5.644507 2 0
 | 
			
		||||
5.755354 1.5 0
 | 
			
		||||
4.146974 0.75 0
 | 
			
		||||
3.0950184 -1 0
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
3.4311862 -0.625 0
 | 
			
		||||
4.146974 0.75 0
 | 
			
		||||
3.5 1 0
 | 
			
		||||
5.644507 2 0
 | 
			
		||||
9.5 1.5 0
 | 
			
		||||
5.755354 1.5 0
 | 
			
		||||
9.5 2 0
 | 
			
		||||
5.755354 1.5 0
 | 
			
		||||
5.644507 2 0
 | 
			
		||||
9.5 2 0
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
0 0 0
 | 
			
		||||
0 1 0
 | 
			
		||||
3.5 1 0
 | 
			
		||||
2.5385938 0 0
 | 
			
		||||
0 1 0
 | 
			
		||||
3.342784 0.375 4
 | 
			
		||||
3.5 1 4
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
4.146974 0.75 4
 | 
			
		||||
3.5 1 4
 | 
			
		||||
3.342784 0.375 4
 | 
			
		||||
3.4311862 -0.625 4
 | 
			
		||||
3.0950184 -1 4
 | 
			
		||||
4.323779 -1.25 4
 | 
			
		||||
4.146974 0.75 4
 | 
			
		||||
5.755354 1.5 4
 | 
			
		||||
5.644507 2 4
 | 
			
		||||
0 1 4
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
3.5 1 4
 | 
			
		||||
0 1 4
 | 
			
		||||
0 0 4
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
5.644507 2 4
 | 
			
		||||
5.755354 1.5 4
 | 
			
		||||
9.5 2 4
 | 
			
		||||
9.5 2 4
 | 
			
		||||
5.755354 1.5 4
 | 
			
		||||
9.5 1.5 4
 | 
			
		||||
4.146974 0.75 4
 | 
			
		||||
5.644507 2 4
 | 
			
		||||
3.5 1 4
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
3.0950184 -1 4
 | 
			
		||||
3.4311862 -0.625 4
 | 
			
		||||
4.323779 -1.25 4
 | 
			
		||||
3.0950184 -1 4
 | 
			
		||||
5.9513144 -3 4
 | 
			
		||||
6.108964 -2.5 4
 | 
			
		||||
4.323779 -1.25 4
 | 
			
		||||
5.9513144 -3 4
 | 
			
		||||
9.5 -2.5 4
 | 
			
		||||
6.108964 -2.5 4
 | 
			
		||||
9.5 -3 4
 | 
			
		||||
6.108964 -2.5 4
 | 
			
		||||
5.9513144 -3 4
 | 
			
		||||
9.5 -3 4
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
0 -1 4
 | 
			
		||||
3.0950184 -1 4
 | 
			
		||||
0 -1 4
 | 
			
		||||
2.5385938 0 4
 | 
			
		||||
0 0 4
 | 
			
		||||
3 0 1 2
 | 
			
		||||
3 3 4 5
 | 
			
		||||
3 6 7 8
 | 
			
		||||
3 9 10 11
 | 
			
		||||
3 12 13 14
 | 
			
		||||
3 15 16 17
 | 
			
		||||
3 18 19 20
 | 
			
		||||
3 21 22 23
 | 
			
		||||
3 24 25 26
 | 
			
		||||
3 27 28 29
 | 
			
		||||
3 30 31 32
 | 
			
		||||
3 33 34 35
 | 
			
		||||
3 36 37 38
 | 
			
		||||
3 39 40 41
 | 
			
		||||
3 42 43 44
 | 
			
		||||
3 45 46 47
 | 
			
		||||
3 48 49 50
 | 
			
		||||
3 51 52 53
 | 
			
		||||
3 54 55 56
 | 
			
		||||
3 57 58 59
 | 
			
		||||
3 60 61 62
 | 
			
		||||
3 63 64 65
 | 
			
		||||
3 66 67 68
 | 
			
		||||
3 69 70 71
 | 
			
		||||
3 72 73 74
 | 
			
		||||
3 75 76 77
 | 
			
		||||
3 78 79 80
 | 
			
		||||
3 81 82 83
 | 
			
		||||
3 84 85 86
 | 
			
		||||
3 87 88 89
 | 
			
		||||
3 90 91 92
 | 
			
		||||
3 93 94 95
 | 
			
		||||
3 96 97 98
 | 
			
		||||
3 99 100 101
 | 
			
		||||
3 102 103 104
 | 
			
		||||
3 105 106 107
 | 
			
		||||
3 108 109 110
 | 
			
		||||
3 111 112 113
 | 
			
		||||
3 114 115 116
 | 
			
		||||
3 117 118 119
 | 
			
		||||
3 120 121 122
 | 
			
		||||
3 123 124 125
 | 
			
		||||
3 126 127 128
 | 
			
		||||
3 129 130 131
 | 
			
		||||
3 132 133 134
 | 
			
		||||
3 135 136 137
 | 
			
		||||
3 138 139 140
 | 
			
		||||
3 141 142 143
 | 
			
		||||
3 144 145 146
 | 
			
		||||
3 147 148 149
 | 
			
		||||
3 150 151 152
 | 
			
		||||
3 153 154 155
 | 
			
		||||
3 156 157 158
 | 
			
		||||
3 159 160 161
 | 
			
		||||
3 162 163 164
 | 
			
		||||
3 165 166 167
 | 
			
		||||
3 168 169 170
 | 
			
		||||
3 171 172 173
 | 
			
		||||
3 174 175 176
 | 
			
		||||
3 177 178 179
 | 
			
		||||
3 180 181 182
 | 
			
		||||
3 183 184 185
 | 
			
		||||
3 186 187 188
 | 
			
		||||
3 189 190 191
 | 
			
		||||
3 192 193 194
 | 
			
		||||
3 195 196 197
 | 
			
		||||
3 198 199 200
 | 
			
		||||
3 201 202 203
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { getUtils } from './test-utils'
 | 
			
		||||
import waitOn from 'wait-on'
 | 
			
		||||
import { Themes } from '../../src/lib/theme'
 | 
			
		||||
import { platform } from '@tauri-apps/api/os'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
 | 
			
		||||
@ -643,7 +644,11 @@ test('Command bar works and can change a setting', async ({ page }) => {
 | 
			
		||||
  let cmdSearchBar = page.getByPlaceholder('Search commands')
 | 
			
		||||
 | 
			
		||||
  // First try opening the command bar and closing it
 | 
			
		||||
  await page.getByRole('button', { name: '⌘K' }).click()
 | 
			
		||||
  // It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
 | 
			
		||||
  await page
 | 
			
		||||
    .getByRole('button', { name: 'Ctrl+/' })
 | 
			
		||||
    .or(page.getByRole('button', { name: '⌘K' }))
 | 
			
		||||
    .click()
 | 
			
		||||
  await expect(cmdSearchBar).toBeVisible()
 | 
			
		||||
  await page.keyboard.press('Escape')
 | 
			
		||||
  await expect(cmdSearchBar).not.toBeVisible()
 | 
			
		||||
@ -658,12 +663,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
 | 
			
		||||
  const themeOption = page.getByRole('option', { name: 'Set Theme' })
 | 
			
		||||
  await expect(themeOption).toBeVisible()
 | 
			
		||||
  await themeOption.click()
 | 
			
		||||
  const themeInput = page.getByPlaceholder(Themes.System)
 | 
			
		||||
  const themeInput = page.getByPlaceholder('Select an option')
 | 
			
		||||
  await expect(themeInput).toBeVisible()
 | 
			
		||||
  await expect(themeInput).toBeFocused()
 | 
			
		||||
  // Select dark theme
 | 
			
		||||
  await page.keyboard.press('ArrowDown')
 | 
			
		||||
  await page.keyboard.press('ArrowUp')
 | 
			
		||||
  await page.keyboard.press('ArrowDown')
 | 
			
		||||
  await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
 | 
			
		||||
    'data-headlessui-state',
 | 
			
		||||
    'active'
 | 
			
		||||
@ -675,3 +680,59 @@ test('Command bar works and can change a setting', async ({ page }) => {
 | 
			
		||||
  // Check that the theme changed
 | 
			
		||||
  await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('Can extrude from the command bar', async ({ page, context }) => {
 | 
			
		||||
  await context.addInitScript(async (token) => {
 | 
			
		||||
    localStorage.setItem(
 | 
			
		||||
      'persistCode',
 | 
			
		||||
      `const part001 = startSketchOn('-XZ')
 | 
			
		||||
    |> startProfileAt([-6.95, 4.98], %)
 | 
			
		||||
    |> line([25.1, 0.41], %)
 | 
			
		||||
    |> line([0.73, -14.93], %)
 | 
			
		||||
    |> line([-23.44, 0.52], %)
 | 
			
		||||
    |> close(%)`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const u = getUtils(page)
 | 
			
		||||
  await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
  await page.goto('/')
 | 
			
		||||
  await u.waitForAuthSkipAppStart()
 | 
			
		||||
 | 
			
		||||
  let cmdSearchBar = page.getByPlaceholder('Search commands')
 | 
			
		||||
  await page.keyboard.press('Meta+K')
 | 
			
		||||
  await expect(cmdSearchBar).toBeVisible()
 | 
			
		||||
 | 
			
		||||
  // Search for extrude command and choose it
 | 
			
		||||
  await page.getByRole('option', { name: 'Extrude' }).click()
 | 
			
		||||
  await expect(page.locator('#arg-form > label')).toContainText(
 | 
			
		||||
    'Please select one face'
 | 
			
		||||
  )
 | 
			
		||||
  await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
 | 
			
		||||
 | 
			
		||||
  // Click to select face and set distance
 | 
			
		||||
  await u.openAndClearDebugPanel()
 | 
			
		||||
  await page.getByText('|> line([25.1, 0.41], %)').click()
 | 
			
		||||
  await u.waitForCmdReceive('select_add')
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.getByRole('button', { name: 'Continue' }).click()
 | 
			
		||||
  await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
 | 
			
		||||
  await page.keyboard.press('Enter')
 | 
			
		||||
 | 
			
		||||
  // Review step and argument hotkeys
 | 
			
		||||
  await page.keyboard.press('2')
 | 
			
		||||
  await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
 | 
			
		||||
  await page.keyboard.press('Enter')
 | 
			
		||||
 | 
			
		||||
  // Check that the code was updated
 | 
			
		||||
  await page.keyboard.press('Enter')
 | 
			
		||||
  await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
    `const part001 = startSketchOn('-XZ')
 | 
			
		||||
    |> startProfileAt([-6.95, 4.98], %)
 | 
			
		||||
    |> line([25.1, 0.41], %)
 | 
			
		||||
    |> line([0.73, -14.93], %)
 | 
			
		||||
    |> line([-23.44, 0.52], %)
 | 
			
		||||
    |> close(%)
 | 
			
		||||
    |> extrude(5, %)`
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 113 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB  | 
@ -8,7 +8,7 @@ import {
 | 
			
		||||
  createRoutesFromElements,
 | 
			
		||||
} from 'react-router-dom'
 | 
			
		||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
 | 
			
		||||
import CommandBarProvider from 'components/CommandBar'
 | 
			
		||||
import CommandBarProvider from 'components/CommandBar/CommandBar'
 | 
			
		||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
 | 
			
		||||
import { BROWSER_FILE_NAME } from 'Router'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ import {
 | 
			
		||||
  settingsMachine,
 | 
			
		||||
} from './machines/settingsMachine'
 | 
			
		||||
import { ContextFrom } from 'xstate'
 | 
			
		||||
import CommandBarProvider from 'components/CommandBar'
 | 
			
		||||
import CommandBarProvider from 'components/CommandBar/CommandBar'
 | 
			
		||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
 | 
			
		||||
import * as Sentry from '@sentry/react'
 | 
			
		||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
 | 
			
		||||
 | 
			
		||||
@ -4,9 +4,11 @@ import { engineCommandManager } from './lang/std/engineConnection'
 | 
			
		||||
import { useModelingContext } from 'hooks/useModelingContext'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { ActionButton } from 'components/ActionButton'
 | 
			
		||||
import usePlatform from 'hooks/usePlatform'
 | 
			
		||||
 | 
			
		||||
export const Toolbar = () => {
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const platform = usePlatform()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { state, send, context } = useModelingContext()
 | 
			
		||||
  const toolbarButtonsRef = useRef<HTMLUListElement>(null)
 | 
			
		||||
  const bgClassName =
 | 
			
		||||
@ -177,10 +179,15 @@ export const Toolbar = () => {
 | 
			
		||||
            <ActionButton
 | 
			
		||||
              Element="button"
 | 
			
		||||
              className="text-sm"
 | 
			
		||||
              onClick={() => send('extrude intent')}
 | 
			
		||||
              disabled={!state.can('extrude intent')}
 | 
			
		||||
              onClick={() =>
 | 
			
		||||
                commandBarSend({
 | 
			
		||||
                  type: 'Find and select command',
 | 
			
		||||
                  data: { name: 'Extrude', ownerMachine: 'modeling' },
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
              disabled={!state.can('Extrude')}
 | 
			
		||||
              title={
 | 
			
		||||
                state.can('extrude intent')
 | 
			
		||||
                state.can('Extrude')
 | 
			
		||||
                  ? 'extrude'
 | 
			
		||||
                  : 'sketches need to be closed, or not already extruded'
 | 
			
		||||
              }
 | 
			
		||||
@ -204,10 +211,10 @@ export const Toolbar = () => {
 | 
			
		||||
      </menu>
 | 
			
		||||
      <ActionButton
 | 
			
		||||
        Element="button"
 | 
			
		||||
        onClick={() => setCommandBarOpen(true)}
 | 
			
		||||
        onClick={() => commandBarSend({ type: 'Open' })}
 | 
			
		||||
        className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10"
 | 
			
		||||
      >
 | 
			
		||||
        ⌘K
 | 
			
		||||
        {platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
 | 
			
		||||
      </ActionButton>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import styles from './AppHeader.module.css'
 | 
			
		||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { ActionButton } from './ActionButton'
 | 
			
		||||
import usePlatform from 'hooks/usePlatform'
 | 
			
		||||
 | 
			
		||||
interface AppHeaderProps extends React.PropsWithChildren {
 | 
			
		||||
  showToolbar?: boolean
 | 
			
		||||
@ -22,7 +23,8 @@ export const AppHeader = ({
 | 
			
		||||
  className = '',
 | 
			
		||||
  enableMenu = false,
 | 
			
		||||
}: AppHeaderProps) => {
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const platform = usePlatform()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { auth } = useGlobalStateContext()
 | 
			
		||||
  const user = auth?.context?.user
 | 
			
		||||
 | 
			
		||||
@ -47,12 +49,12 @@ export const AppHeader = ({
 | 
			
		||||
        ) : (
 | 
			
		||||
          <ActionButton
 | 
			
		||||
            Element="button"
 | 
			
		||||
            onClick={() => setCommandBarOpen(true)}
 | 
			
		||||
            onClick={() => commandBarSend({ type: 'Open' })}
 | 
			
		||||
            className="text-sm self-center flex items-center w-fit gap-3"
 | 
			
		||||
          >
 | 
			
		||||
            Command Palette{' '}
 | 
			
		||||
            <kbd className="bg-energy-10/50 dark:bg-chalkboard-100 dark:text-energy-10 inline-block px-1 py-0.5 border-energy-10 dark:border-chalkboard-90">
 | 
			
		||||
              ⌘K
 | 
			
		||||
              {platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
 | 
			
		||||
            </kbd>
 | 
			
		||||
          </ActionButton>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
@ -24,13 +24,13 @@ export const PanelHeader = ({
 | 
			
		||||
}: CollapsiblePanelProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <summary className={styles.header}>
 | 
			
		||||
      <div className="flex gap-2 align-center items-center flex-1">
 | 
			
		||||
      <div className="flex gap-2 items-center flex-1">
 | 
			
		||||
        <ActionIcon
 | 
			
		||||
          icon={icon}
 | 
			
		||||
          className="p-1"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          bgClassName={
 | 
			
		||||
            'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 group-open:border dark:group-open:border-chalkboard-60 rounded-sm ' +
 | 
			
		||||
            'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 border border-transparent dark:group-open:border-chalkboard-60 rounded-sm ' +
 | 
			
		||||
            (iconClassNames?.bg || '')
 | 
			
		||||
          }
 | 
			
		||||
          iconClassName={
 | 
			
		||||
 | 
			
		||||
@ -1,404 +0,0 @@
 | 
			
		||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
 | 
			
		||||
import {
 | 
			
		||||
  Dispatch,
 | 
			
		||||
  Fragment,
 | 
			
		||||
  SetStateAction,
 | 
			
		||||
  createContext,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useRef,
 | 
			
		||||
  useState,
 | 
			
		||||
} from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
  CommandArgumentOption,
 | 
			
		||||
} from '../lib/commands'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CustomIcon } from './CustomIcon'
 | 
			
		||||
 | 
			
		||||
type ComboboxOption = Command | CommandArgumentOption
 | 
			
		||||
type CommandArgumentData = [string, any]
 | 
			
		||||
 | 
			
		||||
export const CommandsContext = createContext(
 | 
			
		||||
  {} as {
 | 
			
		||||
    commands: Command[]
 | 
			
		||||
    addCommands: (commands: Command[]) => void
 | 
			
		||||
    removeCommands: (commands: Command[]) => void
 | 
			
		||||
    commandBarOpen: boolean
 | 
			
		||||
    setCommandBarOpen: Dispatch<SetStateAction<boolean>>
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export const CommandBarProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const [commands, internalSetCommands] = useState([] as Command[])
 | 
			
		||||
  const [commandBarOpen, setCommandBarOpen] = useState(false)
 | 
			
		||||
 | 
			
		||||
  function sortCommands(a: Command, b: Command) {
 | 
			
		||||
    if (b.owner === 'auth') return -1
 | 
			
		||||
    if (a.owner === 'auth') return 1
 | 
			
		||||
    return a.name.localeCompare(b.name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => console.log('commands updated', commands), [commands])
 | 
			
		||||
 | 
			
		||||
  const addCommands = (newCommands: Command[]) => {
 | 
			
		||||
    internalSetCommands((prevCommands) =>
 | 
			
		||||
      [...newCommands, ...prevCommands].sort(sortCommands)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  const removeCommands = (newCommands: Command[]) => {
 | 
			
		||||
    internalSetCommands((prevCommands) =>
 | 
			
		||||
      prevCommands
 | 
			
		||||
        .filter((command) => !newCommands.includes(command))
 | 
			
		||||
        .sort(sortCommands)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CommandsContext.Provider
 | 
			
		||||
      value={{
 | 
			
		||||
        commands,
 | 
			
		||||
        addCommands,
 | 
			
		||||
        removeCommands,
 | 
			
		||||
        commandBarOpen,
 | 
			
		||||
        setCommandBarOpen,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <CommandBar />
 | 
			
		||||
    </CommandsContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CommandBar = () => {
 | 
			
		||||
  const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  useHotkeys(['meta+k', 'meta+/'], () => {
 | 
			
		||||
    if (commands?.length === 0) return
 | 
			
		||||
    setCommandBarOpen(!commandBarOpen)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const [selectedCommand, setSelectedCommand] = useState<Command>()
 | 
			
		||||
  const [commandArguments, setCommandArguments] = useState<CommandArgument[]>(
 | 
			
		||||
    []
 | 
			
		||||
  )
 | 
			
		||||
  const [commandArgumentData, setCommandArgumentData] = useState<
 | 
			
		||||
    CommandArgumentData[]
 | 
			
		||||
  >([])
 | 
			
		||||
  const [commandArgumentIndex, setCommandArgumentIndex] = useState<number>(0)
 | 
			
		||||
 | 
			
		||||
  function clearState() {
 | 
			
		||||
    setCommandBarOpen(false)
 | 
			
		||||
    setSelectedCommand(undefined)
 | 
			
		||||
    setCommandArguments([])
 | 
			
		||||
    setCommandArgumentData([])
 | 
			
		||||
    setCommandArgumentIndex(0)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function selectCommand(command: Command) {
 | 
			
		||||
    console.log('selecting command', command)
 | 
			
		||||
    if (!('args' in command && command.args?.length)) {
 | 
			
		||||
      submitCommand({ command })
 | 
			
		||||
    } else {
 | 
			
		||||
      setCommandArguments(command.args)
 | 
			
		||||
      setSelectedCommand(command)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function stepBack() {
 | 
			
		||||
    if (!selectedCommand) {
 | 
			
		||||
      clearState()
 | 
			
		||||
    } else {
 | 
			
		||||
      if (commandArgumentIndex === 0) {
 | 
			
		||||
        setSelectedCommand(undefined)
 | 
			
		||||
      } else {
 | 
			
		||||
        setCommandArgumentIndex((prevIndex) => Math.max(0, prevIndex - 1))
 | 
			
		||||
      }
 | 
			
		||||
      if (commandArgumentData.length > 0) {
 | 
			
		||||
        setCommandArgumentData((prevData) => prevData.slice(0, -1))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function appendCommandArgumentData(data: { name: any }) {
 | 
			
		||||
    const transformedData = [
 | 
			
		||||
      commandArguments[commandArgumentIndex].name,
 | 
			
		||||
      data.name,
 | 
			
		||||
    ]
 | 
			
		||||
    if (commandArgumentIndex + 1 === commandArguments.length) {
 | 
			
		||||
      submitCommand({
 | 
			
		||||
        dataArr: [
 | 
			
		||||
          ...commandArgumentData,
 | 
			
		||||
          transformedData,
 | 
			
		||||
        ] as CommandArgumentData[],
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      setCommandArgumentData(
 | 
			
		||||
        (prevData) => [...prevData, transformedData] as CommandArgumentData[]
 | 
			
		||||
      )
 | 
			
		||||
      setCommandArgumentIndex((prevIndex) => prevIndex + 1)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function submitCommand({
 | 
			
		||||
    command = selectedCommand,
 | 
			
		||||
    dataArr = commandArgumentData,
 | 
			
		||||
  }) {
 | 
			
		||||
    console.log('submitting command', command, dataArr)
 | 
			
		||||
    if (dataArr.length === 0) {
 | 
			
		||||
      command?.callback()
 | 
			
		||||
    } else {
 | 
			
		||||
      const data = Object.fromEntries(dataArr)
 | 
			
		||||
      console.log('submitting data', data)
 | 
			
		||||
      command?.callback(data)
 | 
			
		||||
    }
 | 
			
		||||
    setCommandBarOpen(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getDisplayValue(command: Command) {
 | 
			
		||||
    if (
 | 
			
		||||
      'args' in command &&
 | 
			
		||||
      command.args &&
 | 
			
		||||
      command.args?.length > 0 &&
 | 
			
		||||
      'formatFunction' in command &&
 | 
			
		||||
      command.formatFunction
 | 
			
		||||
    ) {
 | 
			
		||||
      command.formatFunction(
 | 
			
		||||
        command.args.map((c, i) =>
 | 
			
		||||
          commandArgumentData[i] ? commandArgumentData[i][0] : `<${c.name}>`
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return command.name
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Transition.Root
 | 
			
		||||
      show={commandBarOpen || false}
 | 
			
		||||
      afterLeave={() => clearState()}
 | 
			
		||||
      as={Fragment}
 | 
			
		||||
    >
 | 
			
		||||
      <Dialog
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setCommandBarOpen(false)
 | 
			
		||||
        }}
 | 
			
		||||
        className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1"
 | 
			
		||||
      >
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          enter="duration-100 ease-out"
 | 
			
		||||
          enterFrom="opacity-0 scale-95"
 | 
			
		||||
          enterTo="opacity-100 scale-100"
 | 
			
		||||
          leave="duration-75 ease-in"
 | 
			
		||||
          leaveFrom="opacity-100 scale-100"
 | 
			
		||||
          leaveTo="opacity-0 scale-95"
 | 
			
		||||
        >
 | 
			
		||||
          <Dialog.Panel
 | 
			
		||||
            className="relative w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
 | 
			
		||||
            as="div"
 | 
			
		||||
          >
 | 
			
		||||
            {!(
 | 
			
		||||
              commandArguments &&
 | 
			
		||||
              commandArguments.length &&
 | 
			
		||||
              selectedCommand
 | 
			
		||||
            ) ? (
 | 
			
		||||
              <CommandComboBox
 | 
			
		||||
                options={commands}
 | 
			
		||||
                handleSelection={selectCommand}
 | 
			
		||||
                stepBack={stepBack}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <div className="px-4 text-sm flex flex-wrap gap-2">
 | 
			
		||||
                  <p className="pr-4 flex gap-2 items-center">
 | 
			
		||||
                    {selectedCommand &&
 | 
			
		||||
                      'icon' in selectedCommand &&
 | 
			
		||||
                      selectedCommand.icon && (
 | 
			
		||||
                        <CustomIcon
 | 
			
		||||
                          name={selectedCommand.icon}
 | 
			
		||||
                          className="w-5 h-5"
 | 
			
		||||
                        />
 | 
			
		||||
                      )}
 | 
			
		||||
                    {getDisplayValue(selectedCommand)}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  {commandArguments.map((arg, i) => (
 | 
			
		||||
                    <p
 | 
			
		||||
                      key={arg.name}
 | 
			
		||||
                      className={`w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
 | 
			
		||||
                        i === commandArgumentIndex
 | 
			
		||||
                          ? 'bg-energy-10/50 dark:bg-energy-10/20 border-energy-10 dark:border-energy-10'
 | 
			
		||||
                          : 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
 | 
			
		||||
                      }`}
 | 
			
		||||
                    >
 | 
			
		||||
                      {commandArgumentIndex >= i && commandArgumentData[i] ? (
 | 
			
		||||
                        commandArgumentData[i][1]
 | 
			
		||||
                      ) : arg.defaultValue ? (
 | 
			
		||||
                        arg.defaultValue
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <em>{arg.name}</em>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </p>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
 | 
			
		||||
                <Argument
 | 
			
		||||
                  arg={commandArguments[commandArgumentIndex]}
 | 
			
		||||
                  appendCommandArgumentData={appendCommandArgumentData}
 | 
			
		||||
                  stepBack={stepBack}
 | 
			
		||||
                />
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Dialog.Panel>
 | 
			
		||||
        </Transition.Child>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </Transition.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Argument({
 | 
			
		||||
  arg,
 | 
			
		||||
  appendCommandArgumentData,
 | 
			
		||||
  stepBack,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument
 | 
			
		||||
  appendCommandArgumentData: Dispatch<SetStateAction<any>>
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
}) {
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (inputRef.current) {
 | 
			
		||||
      inputRef.current.focus()
 | 
			
		||||
      inputRef.current.select()
 | 
			
		||||
    }
 | 
			
		||||
  }, [arg, inputRef])
 | 
			
		||||
 | 
			
		||||
  return arg.type === 'select' ? (
 | 
			
		||||
    <CommandComboBox
 | 
			
		||||
      options={arg.options}
 | 
			
		||||
      handleSelection={appendCommandArgumentData}
 | 
			
		||||
      stepBack={stepBack}
 | 
			
		||||
      placeholder="Select an option"
 | 
			
		||||
    />
 | 
			
		||||
  ) : (
 | 
			
		||||
    <form
 | 
			
		||||
      onSubmit={(event) => {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
 | 
			
		||||
        appendCommandArgumentData({ name: inputRef.current?.value })
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <label className="flex items-center mx-4 my-4">
 | 
			
		||||
        <span className="px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
 | 
			
		||||
          {arg.name}
 | 
			
		||||
        </span>
 | 
			
		||||
        <input
 | 
			
		||||
          ref={inputRef}
 | 
			
		||||
          className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
 | 
			
		||||
          placeholder="Enter a value"
 | 
			
		||||
          defaultValue={arg.defaultValue}
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
 | 
			
		||||
            if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </label>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarProvider
 | 
			
		||||
 | 
			
		||||
function CommandComboBox({
 | 
			
		||||
  options,
 | 
			
		||||
  handleSelection,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  placeholder,
 | 
			
		||||
}: {
 | 
			
		||||
  options: ComboboxOption[]
 | 
			
		||||
  handleSelection: Dispatch<SetStateAction<any>>
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  placeholder?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const [query, setQuery] = useState('')
 | 
			
		||||
  const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>()
 | 
			
		||||
 | 
			
		||||
  const defaultOption =
 | 
			
		||||
    options.find((o) => 'isCurrent' in o && o.isCurrent) || null
 | 
			
		||||
 | 
			
		||||
  const fuse = new Fuse(options, {
 | 
			
		||||
    keys: ['name', 'description'],
 | 
			
		||||
    threshold: 0.3,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const results = fuse.search(query).map((result) => result.item)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : options)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Combobox defaultValue={defaultOption} onChange={handleSelection}>
 | 
			
		||||
      <div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
 | 
			
		||||
        <CustomIcon
 | 
			
		||||
          name="search"
 | 
			
		||||
          className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
 | 
			
		||||
        />
 | 
			
		||||
        <Combobox.Input
 | 
			
		||||
          onChange={(event) => setQuery(event.target.value)}
 | 
			
		||||
          className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
 | 
			
		||||
            if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          placeholder={
 | 
			
		||||
            (defaultOption && defaultOption.name) ||
 | 
			
		||||
            placeholder ||
 | 
			
		||||
            'Search commands'
 | 
			
		||||
          }
 | 
			
		||||
          autoCapitalize="off"
 | 
			
		||||
          autoComplete="off"
 | 
			
		||||
          autoCorrect="off"
 | 
			
		||||
          spellCheck="false"
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <Combobox.Options
 | 
			
		||||
        static
 | 
			
		||||
        className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
      >
 | 
			
		||||
        {filteredOptions?.map((option) => (
 | 
			
		||||
          <Combobox.Option
 | 
			
		||||
            key={option.name}
 | 
			
		||||
            value={option}
 | 
			
		||||
            className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
 | 
			
		||||
          >
 | 
			
		||||
            {'icon' in option && option.icon && (
 | 
			
		||||
              <CustomIcon
 | 
			
		||||
                name={option.icon}
 | 
			
		||||
                className="w-5 h-5 dark:text-energy-10"
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <p className="flex-grow">{option.name} </p>
 | 
			
		||||
            {'isCurrent' in option && option.isCurrent && (
 | 
			
		||||
              <small className="text-chalkboard-70 dark:text-chalkboard-50">
 | 
			
		||||
                current
 | 
			
		||||
              </small>
 | 
			
		||||
            )}
 | 
			
		||||
          </Combobox.Option>
 | 
			
		||||
        ))}
 | 
			
		||||
      </Combobox.Options>
 | 
			
		||||
    </Combobox>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								src/components/CommandBar/CommandArgOptionInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/components/CommandBar/CommandArgOptionInput.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,114 @@
 | 
			
		||||
import { Combobox } from '@headlessui/react'
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CommandArgumentOption } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useRef, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
function CommandArgOptionInput({
 | 
			
		||||
  options,
 | 
			
		||||
  argName,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
  placeholder,
 | 
			
		||||
}: {
 | 
			
		||||
  options: CommandArgumentOption<unknown>[]
 | 
			
		||||
  argName: string
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (data: unknown) => void
 | 
			
		||||
  placeholder?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { commandBarSend, commandBarState } = useCommandsContext()
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
  const formRef = useRef<HTMLFormElement>(null)
 | 
			
		||||
  const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
 | 
			
		||||
    options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
 | 
			
		||||
      commandBarState.context.argumentsToSubmit[argName] ||
 | 
			
		||||
      options[0].value
 | 
			
		||||
  )
 | 
			
		||||
  const [query, setQuery] = useState('')
 | 
			
		||||
  const [filteredOptions, setFilteredOptions] = useState<typeof options>()
 | 
			
		||||
 | 
			
		||||
  const fuse = new Fuse(options, {
 | 
			
		||||
    keys: ['name', 'description'],
 | 
			
		||||
    threshold: 0.3,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
    inputRef.current?.select()
 | 
			
		||||
  }, [inputRef])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const results = fuse.search(query).map((result) => result.item)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : options)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  function handleSelectOption(option: CommandArgumentOption<unknown>) {
 | 
			
		||||
    setArgValue(option)
 | 
			
		||||
    onSubmit(option.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    onSubmit(argValue)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
 | 
			
		||||
      <Combobox value={argValue} onChange={handleSelectOption} name="options">
 | 
			
		||||
        <div className="flex items-center mx-4 mt-4 mb-2">
 | 
			
		||||
          <label
 | 
			
		||||
            htmlFor="option-input"
 | 
			
		||||
            className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
 | 
			
		||||
          >
 | 
			
		||||
            {argName}
 | 
			
		||||
          </label>
 | 
			
		||||
          <Combobox.Input
 | 
			
		||||
            id="option-input"
 | 
			
		||||
            ref={inputRef}
 | 
			
		||||
            onChange={(event) => setQuery(event.target.value)}
 | 
			
		||||
            className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
 | 
			
		||||
            onKeyDown={(event) => {
 | 
			
		||||
              if (event.metaKey && event.key === 'k')
 | 
			
		||||
                commandBarSend({ type: 'Close' })
 | 
			
		||||
              if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
                stepBack()
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            placeholder={
 | 
			
		||||
              (argValue as CommandArgumentOption<unknown>)?.name ||
 | 
			
		||||
              placeholder ||
 | 
			
		||||
              'Select an option for ' + argName
 | 
			
		||||
            }
 | 
			
		||||
            autoCapitalize="off"
 | 
			
		||||
            autoComplete="off"
 | 
			
		||||
            autoCorrect="off"
 | 
			
		||||
            spellCheck="false"
 | 
			
		||||
            autoFocus
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <Combobox.Options
 | 
			
		||||
          static
 | 
			
		||||
          className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
        >
 | 
			
		||||
          {filteredOptions?.map((option) => (
 | 
			
		||||
            <Combobox.Option
 | 
			
		||||
              key={option.name}
 | 
			
		||||
              value={option}
 | 
			
		||||
              className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
 | 
			
		||||
            >
 | 
			
		||||
              <p className="flex-grow">{option.name} </p>
 | 
			
		||||
              {'isCurrent' in option && option.isCurrent && (
 | 
			
		||||
                <small className="text-chalkboard-70 dark:text-chalkboard-50">
 | 
			
		||||
                  current
 | 
			
		||||
                </small>
 | 
			
		||||
              )}
 | 
			
		||||
            </Combobox.Option>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Combobox.Options>
 | 
			
		||||
      </Combobox>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandArgOptionInput
 | 
			
		||||
							
								
								
									
										166
									
								
								src/components/CommandBar/CommandBar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/components/CommandBar/CommandBar.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,166 @@
 | 
			
		||||
import { Dialog, Popover, Transition } from '@headlessui/react'
 | 
			
		||||
import { Fragment, createContext, useEffect } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useMachine } from '@xstate/react'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
import { EventFrom, StateFrom } from 'xstate'
 | 
			
		||||
import CommandBarArgument from './CommandBarArgument'
 | 
			
		||||
import CommandComboBox from '../CommandComboBox'
 | 
			
		||||
import { useLocation } from 'react-router-dom'
 | 
			
		||||
import CommandBarReview from './CommandBarReview'
 | 
			
		||||
 | 
			
		||||
type CommandsContextType = {
 | 
			
		||||
  commandBarState: StateFrom<typeof commandBarMachine>
 | 
			
		||||
  commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CommandsContext = createContext<CommandsContextType>({
 | 
			
		||||
  commandBarState: commandBarMachine.initialState,
 | 
			
		||||
  commandBarSend: () => {},
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const CommandBarProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const { pathname } = useLocation()
 | 
			
		||||
  const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Arguments are ready': (context, _) => {
 | 
			
		||||
        return context.selectedCommand?.args
 | 
			
		||||
          ? context.argumentsToSubmit.length ===
 | 
			
		||||
              Object.keys(context.selectedCommand.args)?.length
 | 
			
		||||
          : false
 | 
			
		||||
      },
 | 
			
		||||
      'Command has no arguments': (context, _event) => {
 | 
			
		||||
        return (
 | 
			
		||||
          !context.selectedCommand?.args ||
 | 
			
		||||
          Object.keys(context.selectedCommand?.args).length === 0
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Close the command bar when navigating
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    commandBarSend({ type: 'Close' })
 | 
			
		||||
  }, [pathname])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CommandsContext.Provider
 | 
			
		||||
      value={{
 | 
			
		||||
        commandBarState,
 | 
			
		||||
        commandBarSend,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <CommandBar />
 | 
			
		||||
    </CommandsContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CommandBar = () => {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { selectedCommand, currentArgument, commands },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
  const isSelectionArgument = currentArgument?.inputType === 'selection'
 | 
			
		||||
  const WrapperComponent = isSelectionArgument ? Popover : Dialog
 | 
			
		||||
 | 
			
		||||
  useHotkeys(['mod+k', 'mod+/'], () => {
 | 
			
		||||
    if (commandBarState.context.commands.length === 0) return
 | 
			
		||||
    if (commandBarState.matches('Closed')) {
 | 
			
		||||
      commandBarSend({ type: 'Open' })
 | 
			
		||||
    } else {
 | 
			
		||||
      commandBarSend({ type: 'Close' })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  function stepBack() {
 | 
			
		||||
    if (!currentArgument) {
 | 
			
		||||
      if (commandBarState.matches('Review')) {
 | 
			
		||||
        const entries = Object.entries(selectedCommand?.args || {})
 | 
			
		||||
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: commandBarState.matches('Review')
 | 
			
		||||
            ? 'Edit argument'
 | 
			
		||||
            : 'Change current argument',
 | 
			
		||||
          data: {
 | 
			
		||||
            arg: {
 | 
			
		||||
              name: entries[entries.length - 1][0],
 | 
			
		||||
              ...entries[entries.length - 1][1],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        commandBarSend({ type: 'Deselect command' })
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const entries = Object.entries(selectedCommand?.args || {})
 | 
			
		||||
      const index = entries.findIndex(
 | 
			
		||||
        ([key, _]) => key === currentArgument.name
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      if (index === 0) {
 | 
			
		||||
        commandBarSend({ type: 'Deselect command' })
 | 
			
		||||
      } else {
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: 'Change current argument',
 | 
			
		||||
          data: {
 | 
			
		||||
            arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Transition.Root
 | 
			
		||||
      show={!commandBarState.matches('Closed') || false}
 | 
			
		||||
      afterLeave={() => {
 | 
			
		||||
        if (selectedCommand?.onCancel) selectedCommand.onCancel()
 | 
			
		||||
        commandBarSend({ type: 'Clear' })
 | 
			
		||||
      }}
 | 
			
		||||
      as={Fragment}
 | 
			
		||||
    >
 | 
			
		||||
      <WrapperComponent
 | 
			
		||||
        open={!commandBarState.matches('Closed') || isSelectionArgument}
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          commandBarSend({ type: 'Close' })
 | 
			
		||||
        }}
 | 
			
		||||
        className={
 | 
			
		||||
          'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
 | 
			
		||||
          (isSelectionArgument ? 'pointer-events-none' : '')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          enter="duration-100 ease-out"
 | 
			
		||||
          enterFrom="opacity-0 scale-95"
 | 
			
		||||
          enterTo="opacity-100 scale-100"
 | 
			
		||||
          leave="duration-75 ease-in"
 | 
			
		||||
          leaveFrom="opacity-100 scale-100"
 | 
			
		||||
          leaveTo="opacity-0 scale-95"
 | 
			
		||||
        >
 | 
			
		||||
          <WrapperComponent.Panel
 | 
			
		||||
            className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
 | 
			
		||||
            as="div"
 | 
			
		||||
          >
 | 
			
		||||
            {commandBarState.matches('Selecting command') ? (
 | 
			
		||||
              <CommandComboBox options={commands} />
 | 
			
		||||
            ) : commandBarState.matches('Gathering arguments') ? (
 | 
			
		||||
              <CommandBarArgument stepBack={stepBack} />
 | 
			
		||||
            ) : (
 | 
			
		||||
              commandBarState.matches('Review') && (
 | 
			
		||||
                <CommandBarReview stepBack={stepBack} />
 | 
			
		||||
              )
 | 
			
		||||
            )}
 | 
			
		||||
          </WrapperComponent.Panel>
 | 
			
		||||
        </Transition.Child>
 | 
			
		||||
      </WrapperComponent>
 | 
			
		||||
    </Transition.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarProvider
 | 
			
		||||
							
								
								
									
										80
									
								
								src/components/CommandBar/CommandBarArgument.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/components/CommandBar/CommandBarArgument.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
import CommandArgOptionInput from './CommandArgOptionInput'
 | 
			
		||||
import CommandBarBasicInput from './CommandBarBasicInput'
 | 
			
		||||
import CommandBarSelectionInput from './CommandBarSelectionInput'
 | 
			
		||||
import { CommandArgument } from 'lib/commandTypes'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import CommandBarHeader from './CommandBarHeader'
 | 
			
		||||
 | 
			
		||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { currentArgument },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
 | 
			
		||||
  function onSubmit(data: unknown) {
 | 
			
		||||
    if (!currentArgument) return
 | 
			
		||||
 | 
			
		||||
    commandBarSend({
 | 
			
		||||
      type: 'Submit argument',
 | 
			
		||||
      data: {
 | 
			
		||||
        [currentArgument.name]:
 | 
			
		||||
          currentArgument.inputType === 'number'
 | 
			
		||||
            ? parseFloat((data as string) || '0')
 | 
			
		||||
            : data,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    currentArgument && (
 | 
			
		||||
      <CommandBarHeader>
 | 
			
		||||
        <ArgumentInput
 | 
			
		||||
          arg={currentArgument}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
        />
 | 
			
		||||
      </CommandBarHeader>
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarArgument
 | 
			
		||||
 | 
			
		||||
function ArgumentInput({
 | 
			
		||||
  arg,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument<unknown> & { name: string }
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (event: any) => void
 | 
			
		||||
}) {
 | 
			
		||||
  switch (arg.inputType) {
 | 
			
		||||
    case 'options':
 | 
			
		||||
      return (
 | 
			
		||||
        <CommandArgOptionInput
 | 
			
		||||
          options={arg.options}
 | 
			
		||||
          argName={arg.name}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
          placeholder="Select an option"
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    case 'selection':
 | 
			
		||||
      return (
 | 
			
		||||
        <CommandBarSelectionInput
 | 
			
		||||
          arg={arg}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    default:
 | 
			
		||||
      return (
 | 
			
		||||
        <CommandBarBasicInput
 | 
			
		||||
          arg={arg}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/components/CommandBar/CommandBarBasicInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/components/CommandBar/CommandBarBasicInput.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CommandArgument } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useRef } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
function CommandBarBasicInput({
 | 
			
		||||
  arg,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument<unknown> & {
 | 
			
		||||
    inputType: 'number' | 'string'
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (event: unknown) => void
 | 
			
		||||
}) {
 | 
			
		||||
  const { commandBarSend, commandBarState } = useCommandsContext()
 | 
			
		||||
  useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
  const inputType = arg.inputType === 'number' ? 'number' : 'text'
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (inputRef.current) {
 | 
			
		||||
      inputRef.current.focus()
 | 
			
		||||
      inputRef.current.select()
 | 
			
		||||
    }
 | 
			
		||||
  }, [arg, inputRef])
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    onSubmit(inputRef.current?.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form id="arg-form" onSubmit={handleSubmit}>
 | 
			
		||||
      <label className="flex items-center mx-4 my-4">
 | 
			
		||||
        <span className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
 | 
			
		||||
          {arg.name}
 | 
			
		||||
        </span>
 | 
			
		||||
        <input
 | 
			
		||||
          id="arg-form"
 | 
			
		||||
          name={inputType}
 | 
			
		||||
          ref={inputRef}
 | 
			
		||||
          type={inputType}
 | 
			
		||||
          required
 | 
			
		||||
          className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
 | 
			
		||||
          placeholder="Enter a value"
 | 
			
		||||
          defaultValue={
 | 
			
		||||
            (commandBarState.context.argumentsToSubmit[arg.name] as
 | 
			
		||||
              | string
 | 
			
		||||
              | undefined) || (arg.defaultValue as string)
 | 
			
		||||
          }
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </label>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarBasicInput
 | 
			
		||||
							
								
								
									
										171
									
								
								src/components/CommandBar/CommandBarHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								src/components/CommandBar/CommandBarHeader.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,171 @@
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CustomIcon } from '../CustomIcon'
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import { ActionButton } from '../ActionButton'
 | 
			
		||||
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { selectedCommand, currentArgument, argumentsToSubmit },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
  const isReviewing = commandBarState.matches('Review')
 | 
			
		||||
  const [showShortcuts, setShowShortcuts] = useState(false)
 | 
			
		||||
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    'alt',
 | 
			
		||||
    () => setShowShortcuts(true),
 | 
			
		||||
    { enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [showShortcuts]
 | 
			
		||||
  )
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    'alt',
 | 
			
		||||
    () => setShowShortcuts(false),
 | 
			
		||||
    { keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [showShortcuts]
 | 
			
		||||
  )
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    [
 | 
			
		||||
      'alt+1',
 | 
			
		||||
      'alt+2',
 | 
			
		||||
      'alt+3',
 | 
			
		||||
      'alt+4',
 | 
			
		||||
      'alt+5',
 | 
			
		||||
      'alt+6',
 | 
			
		||||
      'alt+7',
 | 
			
		||||
      'alt+8',
 | 
			
		||||
      'alt+9',
 | 
			
		||||
      'alt+0',
 | 
			
		||||
    ],
 | 
			
		||||
    (_, b) => {
 | 
			
		||||
      if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
 | 
			
		||||
        if (!selectedCommand?.args) return
 | 
			
		||||
        const argName = Object.keys(selectedCommand.args)[
 | 
			
		||||
          parseInt(b.keys[0], 10) - 1
 | 
			
		||||
        ]
 | 
			
		||||
        const arg = selectedCommand?.args[argName]
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: 'Change current argument',
 | 
			
		||||
          data: { arg: { ...arg, name: argName } },
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [argumentsToSubmit, selectedCommand]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    selectedCommand &&
 | 
			
		||||
    argumentsToSubmit && (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className="px-4 text-sm flex gap-4 items-start">
 | 
			
		||||
          <div className="flex flex-1 flex-wrap gap-2">
 | 
			
		||||
            <p
 | 
			
		||||
              data-command-name={selectedCommand?.name}
 | 
			
		||||
              className="pr-4 flex gap-2 items-center"
 | 
			
		||||
            >
 | 
			
		||||
              {selectedCommand &&
 | 
			
		||||
                'icon' in selectedCommand &&
 | 
			
		||||
                selectedCommand.icon && (
 | 
			
		||||
                  <CustomIcon name={selectedCommand.icon} className="w-5 h-5" />
 | 
			
		||||
                )}
 | 
			
		||||
              {selectedCommand?.name}
 | 
			
		||||
            </p>
 | 
			
		||||
            {Object.entries(selectedCommand?.args || {}).map(
 | 
			
		||||
              ([argName, arg], i) => (
 | 
			
		||||
                <button
 | 
			
		||||
                  disabled={!isReviewing && currentArgument?.name === argName}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    commandBarSend({
 | 
			
		||||
                      type: isReviewing
 | 
			
		||||
                        ? 'Edit argument'
 | 
			
		||||
                        : 'Change current argument',
 | 
			
		||||
                      data: { arg: { ...arg, name: argName } },
 | 
			
		||||
                    })
 | 
			
		||||
                  }}
 | 
			
		||||
                  key={argName}
 | 
			
		||||
                  className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
 | 
			
		||||
                    argName === currentArgument?.name
 | 
			
		||||
                      ? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
 | 
			
		||||
                      : 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
 | 
			
		||||
                  }`}
 | 
			
		||||
                >
 | 
			
		||||
                  {argumentsToSubmit[argName] ? (
 | 
			
		||||
                    arg.inputType === 'selection' ? (
 | 
			
		||||
                      getSelectionTypeDisplayText(
 | 
			
		||||
                        argumentsToSubmit[argName] as Selections
 | 
			
		||||
                      )
 | 
			
		||||
                    ) : typeof argumentsToSubmit[argName] === 'object' ? (
 | 
			
		||||
                      JSON.stringify(argumentsToSubmit[argName])
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      argumentsToSubmit[argName]
 | 
			
		||||
                    )
 | 
			
		||||
                  ) : arg.payload ? (
 | 
			
		||||
                    arg.inputType === 'selection' ? (
 | 
			
		||||
                      getSelectionTypeDisplayText(arg.payload as Selections)
 | 
			
		||||
                    ) : typeof arg.payload === 'object' ? (
 | 
			
		||||
                      JSON.stringify(arg.payload)
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      arg.payload
 | 
			
		||||
                    )
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <em>{argName}</em>
 | 
			
		||||
                  )}
 | 
			
		||||
                  {showShortcuts && (
 | 
			
		||||
                    <small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
 | 
			
		||||
                      <span className="sr-only">Hotkey: </span>
 | 
			
		||||
                      {i + 1}
 | 
			
		||||
                    </small>
 | 
			
		||||
                  )}
 | 
			
		||||
                </button>
 | 
			
		||||
              )
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          {isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
 | 
			
		||||
        {children}
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ReviewingButton() {
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionButton
 | 
			
		||||
      Element="button"
 | 
			
		||||
      autoFocus
 | 
			
		||||
      type="submit"
 | 
			
		||||
      form="review-form"
 | 
			
		||||
      className="w-fit !p-0 rounded-sm border !border-chalkboard-100 dark:!border-energy-10 hover:shadow"
 | 
			
		||||
      icon={{
 | 
			
		||||
        icon: 'checkmark',
 | 
			
		||||
        bgClassName:
 | 
			
		||||
          'p-1 rounded-sm !bg-chalkboard-100 hover:!bg-chalkboard-110 dark:!bg-energy-20 dark:hover:!bg-energy-10',
 | 
			
		||||
        iconClassName: '!text-energy-10 dark:!text-chalkboard-100',
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <span className="sr-only">Submit command</span>
 | 
			
		||||
    </ActionButton>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function GatheringArgsButton() {
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionButton
 | 
			
		||||
      Element="button"
 | 
			
		||||
      type="submit"
 | 
			
		||||
      form="arg-form"
 | 
			
		||||
      className="w-fit !p-0 rounded-sm"
 | 
			
		||||
      icon={{
 | 
			
		||||
        icon: 'arrowRight',
 | 
			
		||||
        bgClassName: 'p-1 rounded-sm',
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <span className="sr-only">Continue</span>
 | 
			
		||||
    </ActionButton>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarHeader
 | 
			
		||||
							
								
								
									
										81
									
								
								src/components/CommandBar/CommandBarReview.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/components/CommandBar/CommandBarReview.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import CommandBarHeader from './CommandBarHeader'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { argumentsToSubmit, selectedCommand },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
 | 
			
		||||
  useHotkeys('backspace', stepBack, {
 | 
			
		||||
    enableOnFormTags: true,
 | 
			
		||||
    enableOnContentEditable: true,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    '1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
 | 
			
		||||
    (_, b) => {
 | 
			
		||||
      if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
 | 
			
		||||
        if (!selectedCommand?.args) return
 | 
			
		||||
        const argName = Object.keys(selectedCommand.args)[
 | 
			
		||||
          parseInt(b.keys[0], 10) - 1
 | 
			
		||||
        ]
 | 
			
		||||
        const arg = selectedCommand?.args[argName]
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: 'Edit argument',
 | 
			
		||||
          data: { arg: { ...arg, name: argName } },
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [argumentsToSubmit, selectedCommand]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  Object.keys(argumentsToSubmit).forEach((key, i) => {
 | 
			
		||||
    const arg = selectedCommand?.args ? selectedCommand?.args[key] : undefined
 | 
			
		||||
    if (!arg) return
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  function submitCommand() {
 | 
			
		||||
    commandBarSend({
 | 
			
		||||
      type: 'Submit command',
 | 
			
		||||
      data: argumentsToSubmit,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CommandBarHeader>
 | 
			
		||||
      <p className="px-4">Confirm {selectedCommand?.name}</p>
 | 
			
		||||
      <form
 | 
			
		||||
        id="review-form"
 | 
			
		||||
        className="absolute opacity-0 inset-0 pointer-events-none"
 | 
			
		||||
        onSubmit={submitCommand}
 | 
			
		||||
      >
 | 
			
		||||
        {Object.entries(argumentsToSubmit).map(([key, value], i) => {
 | 
			
		||||
          const arg = selectedCommand?.args
 | 
			
		||||
            ? selectedCommand?.args[key]
 | 
			
		||||
            : undefined
 | 
			
		||||
          if (!arg) return null
 | 
			
		||||
 | 
			
		||||
          return (
 | 
			
		||||
            <input
 | 
			
		||||
              id={key}
 | 
			
		||||
              name={key}
 | 
			
		||||
              key={key}
 | 
			
		||||
              type="text"
 | 
			
		||||
              defaultValue={
 | 
			
		||||
                typeof value === 'object'
 | 
			
		||||
                  ? JSON.stringify(value)
 | 
			
		||||
                  : (value as string)
 | 
			
		||||
              }
 | 
			
		||||
              hidden
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </form>
 | 
			
		||||
    </CommandBarHeader>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarReview
 | 
			
		||||
							
								
								
									
										114
									
								
								src/components/CommandBar/CommandBarSelectionInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/components/CommandBar/CommandBarSelectionInput.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,114 @@
 | 
			
		||||
import { useSelector } from '@xstate/react'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useKclContext } from 'lang/KclSinglton'
 | 
			
		||||
import { CommandArgument } from 'lib/commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  ResolvedSelectionType,
 | 
			
		||||
  canSubmitSelectionArg,
 | 
			
		||||
  getSelectionType,
 | 
			
		||||
  getSelectionTypeDisplayText,
 | 
			
		||||
} from 'lib/selections'
 | 
			
		||||
import { modelingMachine } from 'machines/modelingMachine'
 | 
			
		||||
import { useEffect, useRef, useState } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import { StateFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
 | 
			
		||||
  snapshot.context.selectionRanges
 | 
			
		||||
 | 
			
		||||
function CommandBarSelectionInput({
 | 
			
		||||
  arg,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument<unknown> & { inputType: 'selection'; name: string }
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (data: unknown) => void
 | 
			
		||||
}) {
 | 
			
		||||
  const { code } = useKclContext()
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const [hasSubmitted, setHasSubmitted] = useState(false)
 | 
			
		||||
  const selection = useSelector(arg.actor, selectionSelector)
 | 
			
		||||
  const [selectionsByType, setSelectionsByType] = useState<
 | 
			
		||||
    'none' | ResolvedSelectionType[]
 | 
			
		||||
  >(
 | 
			
		||||
    selection.codeBasedSelections[0]?.range[1] === code.length
 | 
			
		||||
      ? 'none'
 | 
			
		||||
      : getSelectionType(selection)
 | 
			
		||||
  )
 | 
			
		||||
  const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
 | 
			
		||||
    canSubmitSelectionArg(selectionsByType, arg)
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useHotkeys('tab', () => onSubmit(selection), {
 | 
			
		||||
    enableOnFormTags: true,
 | 
			
		||||
    enableOnContentEditable: true,
 | 
			
		||||
    keyup: true,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
  }, [selection, inputRef])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setSelectionsByType(
 | 
			
		||||
      selection.codeBasedSelections[0]?.range[1] === code.length
 | 
			
		||||
        ? 'none'
 | 
			
		||||
        : getSelectionType(selection)
 | 
			
		||||
    )
 | 
			
		||||
  }, [selection])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
 | 
			
		||||
  }, [selectionsByType, arg])
 | 
			
		||||
 | 
			
		||||
  function handleChange() {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
 | 
			
		||||
    if (!canSubmitSelection) {
 | 
			
		||||
      setHasSubmitted(true)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onSubmit(selection)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form id="arg-form" onSubmit={handleSubmit}>
 | 
			
		||||
      <label
 | 
			
		||||
        className={
 | 
			
		||||
          'relative flex items-center mx-4 my-4 ' +
 | 
			
		||||
          (!hasSubmitted || canSubmitSelection || 'text-destroy-50')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        {canSubmitSelection
 | 
			
		||||
          ? getSelectionTypeDisplayText(selection) + ' selected'
 | 
			
		||||
          : `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`}
 | 
			
		||||
        <input
 | 
			
		||||
          id="selection"
 | 
			
		||||
          name="selection"
 | 
			
		||||
          ref={inputRef}
 | 
			
		||||
          required
 | 
			
		||||
          placeholder="Select an entity with your mouse"
 | 
			
		||||
          className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.key === 'Backspace') {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            } else if (event.key === 'Escape') {
 | 
			
		||||
              commandBarSend({ type: 'Close' })
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          onChange={handleChange}
 | 
			
		||||
          value={JSON.stringify(selection || {})}
 | 
			
		||||
        />
 | 
			
		||||
      </label>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarSelectionInput
 | 
			
		||||
							
								
								
									
										90
									
								
								src/components/CommandComboBox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/components/CommandComboBox.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
import { Combobox } from '@headlessui/react'
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { Command } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { CustomIcon } from './CustomIcon'
 | 
			
		||||
 | 
			
		||||
function CommandComboBox({
 | 
			
		||||
  options,
 | 
			
		||||
  placeholder,
 | 
			
		||||
}: {
 | 
			
		||||
  options: Command[]
 | 
			
		||||
  placeholder?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const [query, setQuery] = useState('')
 | 
			
		||||
  const [filteredOptions, setFilteredOptions] = useState<typeof options>()
 | 
			
		||||
 | 
			
		||||
  const defaultOption =
 | 
			
		||||
    options.find((o) => 'isCurrent' in o && o.isCurrent) || null
 | 
			
		||||
 | 
			
		||||
  const fuse = new Fuse(options, {
 | 
			
		||||
    keys: ['name', 'description'],
 | 
			
		||||
    threshold: 0.3,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const results = fuse.search(query).map((result) => result.item)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : options)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  function handleSelection(command: Command) {
 | 
			
		||||
    commandBarSend({ type: 'Select command', data: { command } })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Combobox defaultValue={defaultOption} onChange={handleSelection}>
 | 
			
		||||
      <div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
 | 
			
		||||
        <CustomIcon
 | 
			
		||||
          name="search"
 | 
			
		||||
          className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
 | 
			
		||||
        />
 | 
			
		||||
        <Combobox.Input
 | 
			
		||||
          onChange={(event) => setQuery(event.target.value)}
 | 
			
		||||
          className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (
 | 
			
		||||
              (event.metaKey && event.key === 'k') ||
 | 
			
		||||
              (event.key === 'Backspace' && !event.currentTarget.value)
 | 
			
		||||
            ) {
 | 
			
		||||
              commandBarSend({ type: 'Close' })
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          placeholder={
 | 
			
		||||
            (defaultOption && defaultOption.name) ||
 | 
			
		||||
            placeholder ||
 | 
			
		||||
            'Search commands'
 | 
			
		||||
          }
 | 
			
		||||
          autoCapitalize="off"
 | 
			
		||||
          autoComplete="off"
 | 
			
		||||
          autoCorrect="off"
 | 
			
		||||
          spellCheck="false"
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <Combobox.Options
 | 
			
		||||
        static
 | 
			
		||||
        className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
      >
 | 
			
		||||
        {filteredOptions?.map((option) => (
 | 
			
		||||
          <Combobox.Option
 | 
			
		||||
            key={option.name}
 | 
			
		||||
            value={option}
 | 
			
		||||
            className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
 | 
			
		||||
          >
 | 
			
		||||
            {'icon' in option && option.icon && (
 | 
			
		||||
              <CustomIcon
 | 
			
		||||
                name={option.icon}
 | 
			
		||||
                className="w-5 h-5 dark:text-energy-10"
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <p className="flex-grow">{option.name} </p>
 | 
			
		||||
          </Combobox.Option>
 | 
			
		||||
        ))}
 | 
			
		||||
      </Combobox.Options>
 | 
			
		||||
    </Combobox>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandComboBox
 | 
			
		||||
@ -3,6 +3,7 @@ export type CustomIconName =
 | 
			
		||||
  | 'arrowLeft'
 | 
			
		||||
  | 'arrowRight'
 | 
			
		||||
  | 'arrowUp'
 | 
			
		||||
  | 'checkmark'
 | 
			
		||||
  | 'close'
 | 
			
		||||
  | 'equal'
 | 
			
		||||
  | 'extrude'
 | 
			
		||||
@ -90,6 +91,22 @@ export const CustomIcon = ({
 | 
			
		||||
          />
 | 
			
		||||
        </svg>
 | 
			
		||||
      )
 | 
			
		||||
    case 'checkmark':
 | 
			
		||||
      return (
 | 
			
		||||
        <svg
 | 
			
		||||
          {...props}
 | 
			
		||||
          viewBox="0 0 20 20"
 | 
			
		||||
          fill="none"
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
        >
 | 
			
		||||
          <path
 | 
			
		||||
            fillRule="evenodd"
 | 
			
		||||
            clipRule="evenodd"
 | 
			
		||||
            d="M8.29956 13.5388L13.9537 6L14.7537 6.6L8.75367 14.6L8.00012 14.6536L5 11.6536L5.70709 10.9465L8.29956 13.5388Z"
 | 
			
		||||
            fill="currentColor"
 | 
			
		||||
          />
 | 
			
		||||
        </svg>
 | 
			
		||||
      )
 | 
			
		||||
    case 'close':
 | 
			
		||||
      return (
 | 
			
		||||
        <svg
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ export const FileMachineProvider = ({
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
 | 
			
		||||
 | 
			
		||||
  const [state, send] = useMachine(fileMachine, {
 | 
			
		||||
@ -54,7 +54,7 @@ export const FileMachineProvider = ({
 | 
			
		||||
        event: EventFrom<typeof fileMachine>
 | 
			
		||||
      ) => {
 | 
			
		||||
        if (event.data && 'name' in event.data) {
 | 
			
		||||
          setCommandBarOpen(false)
 | 
			
		||||
          commandBarSend({ type: 'Close' })
 | 
			
		||||
          navigate(
 | 
			
		||||
            `${paths.FILE}/${encodeURIComponent(
 | 
			
		||||
              context.selectedDirectory + sep + event.data.name
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,11 @@
 | 
			
		||||
import { useMachine } from '@xstate/react'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { paths } from '../Router'
 | 
			
		||||
import {
 | 
			
		||||
  authCommandBarConfig,
 | 
			
		||||
  authMachine,
 | 
			
		||||
  TOKEN_PERSIST_KEY,
 | 
			
		||||
} from '../machines/authMachine'
 | 
			
		||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
 | 
			
		||||
import withBaseUrl from '../lib/withBaseURL'
 | 
			
		||||
import React, { createContext, useEffect, useRef } from 'react'
 | 
			
		||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
 | 
			
		||||
import {
 | 
			
		||||
  SETTINGS_PERSIST_KEY,
 | 
			
		||||
  settingsCommandBarConfig,
 | 
			
		||||
  settingsMachine,
 | 
			
		||||
} from 'machines/settingsMachine'
 | 
			
		||||
import { SETTINGS_PERSIST_KEY, settingsMachine } from 'machines/settingsMachine'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
import { setThemeClass, Themes } from 'lib/theme'
 | 
			
		||||
import {
 | 
			
		||||
@ -23,8 +15,9 @@ import {
 | 
			
		||||
  Prop,
 | 
			
		||||
  StateFrom,
 | 
			
		||||
} from 'xstate'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { isTauri } from 'lib/isTauri'
 | 
			
		||||
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
 | 
			
		||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
 | 
			
		||||
 | 
			
		||||
type MachineContext<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
@ -45,7 +38,6 @@ export const GlobalStateProvider = ({
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const { commands } = useCommandsContext()
 | 
			
		||||
 | 
			
		||||
  // Settings machine setup
 | 
			
		||||
  const retrievedSettings = useRef(
 | 
			
		||||
@ -81,10 +73,9 @@ export const GlobalStateProvider = ({
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useStateMachineCommands({
 | 
			
		||||
    machineId: 'settings',
 | 
			
		||||
    state: settingsState,
 | 
			
		||||
    send: settingsSend,
 | 
			
		||||
    commands,
 | 
			
		||||
    owner: 'settings',
 | 
			
		||||
    commandBarConfig: settingsCommandBarConfig,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@ -121,11 +112,10 @@ export const GlobalStateProvider = ({
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useStateMachineCommands({
 | 
			
		||||
    machineId: 'auth',
 | 
			
		||||
    state: authState,
 | 
			
		||||
    send: authSend,
 | 
			
		||||
    commands,
 | 
			
		||||
    commandBarConfig: authCommandBarConfig,
 | 
			
		||||
    owner: 'auth',
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -15,23 +15,23 @@ const Loading = ({ children }: React.PropsWithChildren) => {
 | 
			
		||||
      data-testid="loading"
 | 
			
		||||
    >
 | 
			
		||||
      <svg viewBox="0 0 10 10" className="w-8 h-8">
 | 
			
		||||
        <circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
 | 
			
		||||
        <circle cx="5" cy="5" r="4" stroke="var(--energy-50)" fill="none" />
 | 
			
		||||
        <circle
 | 
			
		||||
          cx="5"
 | 
			
		||||
          cy="5"
 | 
			
		||||
          r="4"
 | 
			
		||||
          stroke="var(--liquid-10)"
 | 
			
		||||
          stroke="var(--energy-10)"
 | 
			
		||||
          fill="none"
 | 
			
		||||
          strokeDasharray="4, 4"
 | 
			
		||||
          className="animate-spin origin-center"
 | 
			
		||||
        />
 | 
			
		||||
      </svg>
 | 
			
		||||
      <p className="text-base mt-4 text-liquid-80 dark:text-liquid-20">
 | 
			
		||||
      <p className="text-base mt-4 text-energy-80 dark:text-energy-30">
 | 
			
		||||
        {children || 'Loading'}
 | 
			
		||||
      </p>
 | 
			
		||||
      <p
 | 
			
		||||
        className={
 | 
			
		||||
          'text-sm mt-4 text-liquid-90 dark:text-liquid-10 transition-opacity duration-500' +
 | 
			
		||||
          'text-sm mt-4 text-energy-70 dark:text-energy-50 transition-opacity duration-500' +
 | 
			
		||||
          (hasLongLoadTime ? ' opacity-100' : ' opacity-0')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
 | 
			
		||||
@ -29,19 +29,26 @@ import {
 | 
			
		||||
  addNewSketchLn,
 | 
			
		||||
  compareVec2Epsilon,
 | 
			
		||||
} from 'lang/std/sketch'
 | 
			
		||||
import { kclManager } from 'lang/KclSinglton'
 | 
			
		||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
 | 
			
		||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
 | 
			
		||||
import {
 | 
			
		||||
  angleBetweenInfo,
 | 
			
		||||
  applyConstraintAngleBetween,
 | 
			
		||||
} from './Toolbar/SetAngleBetween'
 | 
			
		||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
import { pathMapToSelections } from 'lang/util'
 | 
			
		||||
import { useStore } from 'useStore'
 | 
			
		||||
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
 | 
			
		||||
import {
 | 
			
		||||
  canExtrudeSelection,
 | 
			
		||||
  handleSelectionBatch,
 | 
			
		||||
  handleSelectionWithShift,
 | 
			
		||||
  isSelectionLastLine,
 | 
			
		||||
  isSketchPipe,
 | 
			
		||||
} from 'lib/selections'
 | 
			
		||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
 | 
			
		||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
 | 
			
		||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
 | 
			
		||||
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
 | 
			
		||||
 | 
			
		||||
type MachineContext<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
@ -59,6 +66,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const { auth } = useGlobalStateContext()
 | 
			
		||||
  const { code } = useKclContext()
 | 
			
		||||
  const token = auth?.context?.token
 | 
			
		||||
  const streamRef = useRef<HTMLDivElement>(null)
 | 
			
		||||
  useSetupEngineManager(streamRef, token)
 | 
			
		||||
@ -68,8 +76,6 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
    editorView: s.editorView,
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  // const { commands } = useCommandsContext()
 | 
			
		||||
 | 
			
		||||
  // Settings machine setup
 | 
			
		||||
  // const retrievedSettings = useRef(
 | 
			
		||||
  // localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
 | 
			
		||||
@ -83,148 +89,85 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
  //   >
 | 
			
		||||
  // )
 | 
			
		||||
 | 
			
		||||
  const [modelingState, modelingSend] = useMachine(modelingMachine, {
 | 
			
		||||
    // context: persistedSettings,
 | 
			
		||||
    actions: {
 | 
			
		||||
      'Modify AST': () => {},
 | 
			
		||||
      'Update code selection cursors': () => {},
 | 
			
		||||
      'show default planes': () => {
 | 
			
		||||
        kclManager.showPlanes()
 | 
			
		||||
      },
 | 
			
		||||
      'create path': assign({
 | 
			
		||||
        sketchEnginePathId: () => {
 | 
			
		||||
          const sketchUuid = uuidv4()
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
            cmd_id: sketchUuid,
 | 
			
		||||
            cmd: {
 | 
			
		||||
              type: 'start_path',
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            cmd: {
 | 
			
		||||
              type: 'edit_mode_enter',
 | 
			
		||||
              target: sketchUuid,
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
          return sketchUuid
 | 
			
		||||
  const [modelingState, modelingSend, modelingActor] = useMachine(
 | 
			
		||||
    modelingMachine,
 | 
			
		||||
    {
 | 
			
		||||
      // context: persistedSettings,
 | 
			
		||||
      actions: {
 | 
			
		||||
        'Modify AST': () => {},
 | 
			
		||||
        'Update code selection cursors': () => {},
 | 
			
		||||
        'show default planes': () => {
 | 
			
		||||
          kclManager.showPlanes()
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'AST start new sketch': assign(
 | 
			
		||||
        ({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
 | 
			
		||||
          if (!axis) {
 | 
			
		||||
            // Something really weird must have happened for this to happen.
 | 
			
		||||
            console.error('axis is undefined for starting a new sketch')
 | 
			
		||||
            return {}
 | 
			
		||||
          }
 | 
			
		||||
          if (!segmentId) {
 | 
			
		||||
            // Something really weird must have happened for this to happen.
 | 
			
		||||
            console.error('segmentId is undefined for starting a new sketch')
 | 
			
		||||
            return {}
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const _addStartSketch = addStartSketch(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            axis,
 | 
			
		||||
            [roundOff(coords[0].x), roundOff(coords[0].y)],
 | 
			
		||||
            [
 | 
			
		||||
              roundOff(coords[1].x - coords[0].x),
 | 
			
		||||
              roundOff(coords[1].y - coords[0].y),
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
          const _modifiedAst = _addStartSketch.modifiedAst
 | 
			
		||||
          const _pathToNode = _addStartSketch.pathToNode
 | 
			
		||||
          const newCode = recast(_modifiedAst)
 | 
			
		||||
          const astWithUpdatedSource = parse(newCode)
 | 
			
		||||
          const updatedPipeNode = getNodeFromPath<PipeExpression>(
 | 
			
		||||
            astWithUpdatedSource,
 | 
			
		||||
            _pathToNode
 | 
			
		||||
          ).node
 | 
			
		||||
          const startProfileAtCallExp = updatedPipeNode.body.find(
 | 
			
		||||
            (exp) =>
 | 
			
		||||
              exp.type === 'CallExpression' &&
 | 
			
		||||
              exp.callee.name === 'startProfileAt'
 | 
			
		||||
          )
 | 
			
		||||
          if (startProfileAtCallExp)
 | 
			
		||||
            engineCommandManager.artifactMap[sketchEnginePathId] = {
 | 
			
		||||
              type: 'result',
 | 
			
		||||
              range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
 | 
			
		||||
              commandType: 'start_path',
 | 
			
		||||
              data: null,
 | 
			
		||||
              raw: {} as any,
 | 
			
		||||
        'create path': assign({
 | 
			
		||||
          sketchEnginePathId: () => {
 | 
			
		||||
            const sketchUuid = uuidv4()
 | 
			
		||||
            engineCommandManager.sendSceneCommand({
 | 
			
		||||
              type: 'modeling_cmd_req',
 | 
			
		||||
              cmd_id: sketchUuid,
 | 
			
		||||
              cmd: {
 | 
			
		||||
                type: 'start_path',
 | 
			
		||||
              },
 | 
			
		||||
            })
 | 
			
		||||
            engineCommandManager.sendSceneCommand({
 | 
			
		||||
              type: 'modeling_cmd_req',
 | 
			
		||||
              cmd_id: uuidv4(),
 | 
			
		||||
              cmd: {
 | 
			
		||||
                type: 'edit_mode_enter',
 | 
			
		||||
                target: sketchUuid,
 | 
			
		||||
              },
 | 
			
		||||
            })
 | 
			
		||||
            return sketchUuid
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
        'AST start new sketch': assign(
 | 
			
		||||
          ({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
 | 
			
		||||
            if (!axis) {
 | 
			
		||||
              // Something really weird must have happened for this to happen.
 | 
			
		||||
              console.error('axis is undefined for starting a new sketch')
 | 
			
		||||
              return {}
 | 
			
		||||
            }
 | 
			
		||||
          const lineCallExp = updatedPipeNode.body.find(
 | 
			
		||||
            (exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
 | 
			
		||||
          )
 | 
			
		||||
          if (lineCallExp)
 | 
			
		||||
            engineCommandManager.artifactMap[segmentId] = {
 | 
			
		||||
              type: 'result',
 | 
			
		||||
              range: [lineCallExp.start, lineCallExp.end],
 | 
			
		||||
              commandType: 'extend_path',
 | 
			
		||||
              parentId: sketchEnginePathId,
 | 
			
		||||
              data: null,
 | 
			
		||||
              raw: {} as any,
 | 
			
		||||
            if (!segmentId) {
 | 
			
		||||
              // Something really weird must have happened for this to happen.
 | 
			
		||||
              console.error('segmentId is undefined for starting a new sketch')
 | 
			
		||||
              return {}
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
          kclManager.executeAstMock(astWithUpdatedSource, true)
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            sketchPathToNode: _pathToNode,
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ),
 | 
			
		||||
      'AST add line segment': async (
 | 
			
		||||
        { sketchPathToNode, sketchEnginePathId },
 | 
			
		||||
        { data: { coords, segmentId } }
 | 
			
		||||
      ) => {
 | 
			
		||||
        if (!sketchPathToNode) return
 | 
			
		||||
        const lastCoord = coords[coords.length - 1]
 | 
			
		||||
 | 
			
		||||
        const pathInfo = await engineCommandManager.sendSceneCommand({
 | 
			
		||||
          type: 'modeling_cmd_req',
 | 
			
		||||
          cmd_id: uuidv4(),
 | 
			
		||||
          cmd: {
 | 
			
		||||
            type: 'path_get_info',
 | 
			
		||||
            path_id: sketchEnginePathId,
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
        const firstSegment = pathInfo?.data?.data?.segments.find(
 | 
			
		||||
          (seg: any) => seg.command === 'line_to'
 | 
			
		||||
        )
 | 
			
		||||
        const firstSegCoords = await engineCommandManager.sendSceneCommand({
 | 
			
		||||
          type: 'modeling_cmd_req',
 | 
			
		||||
          cmd_id: uuidv4(),
 | 
			
		||||
          cmd: {
 | 
			
		||||
            type: 'curve_get_control_points',
 | 
			
		||||
            curve_id: firstSegment.command_id,
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
        const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
 | 
			
		||||
 | 
			
		||||
        const isClose = compareVec2Epsilon(
 | 
			
		||||
          [startPathCoord.x, startPathCoord.y],
 | 
			
		||||
          [lastCoord.x, lastCoord.y]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        let _modifiedAst: Program
 | 
			
		||||
        if (!isClose) {
 | 
			
		||||
          const newSketchLn = addNewSketchLn({
 | 
			
		||||
            node: kclManager.ast,
 | 
			
		||||
            programMemory: kclManager.programMemory,
 | 
			
		||||
            to: [lastCoord.x, lastCoord.y],
 | 
			
		||||
            from: [coords[0].x, coords[0].y],
 | 
			
		||||
            fnName: 'line',
 | 
			
		||||
            pathToNode: sketchPathToNode,
 | 
			
		||||
          })
 | 
			
		||||
          const _modifiedAst = newSketchLn.modifiedAst
 | 
			
		||||
          kclManager.executeAstMock(_modifiedAst, true).then(() => {
 | 
			
		||||
            const lineCallExp = getNodeFromPath<CallExpression>(
 | 
			
		||||
            const _addStartSketch = addStartSketch(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              newSketchLn.pathToNode
 | 
			
		||||
              axis,
 | 
			
		||||
              [roundOff(coords[0].x), roundOff(coords[0].y)],
 | 
			
		||||
              [
 | 
			
		||||
                roundOff(coords[1].x - coords[0].x),
 | 
			
		||||
                roundOff(coords[1].y - coords[0].y),
 | 
			
		||||
              ]
 | 
			
		||||
            )
 | 
			
		||||
            const _modifiedAst = _addStartSketch.modifiedAst
 | 
			
		||||
            const _pathToNode = _addStartSketch.pathToNode
 | 
			
		||||
            const newCode = recast(_modifiedAst)
 | 
			
		||||
            const astWithUpdatedSource = parse(newCode)
 | 
			
		||||
            const updatedPipeNode = getNodeFromPath<PipeExpression>(
 | 
			
		||||
              astWithUpdatedSource,
 | 
			
		||||
              _pathToNode
 | 
			
		||||
            ).node
 | 
			
		||||
            if (segmentId)
 | 
			
		||||
            const startProfileAtCallExp = updatedPipeNode.body.find(
 | 
			
		||||
              (exp) =>
 | 
			
		||||
                exp.type === 'CallExpression' &&
 | 
			
		||||
                exp.callee.name === 'startProfileAt'
 | 
			
		||||
            )
 | 
			
		||||
            if (startProfileAtCallExp)
 | 
			
		||||
              engineCommandManager.artifactMap[sketchEnginePathId] = {
 | 
			
		||||
                type: 'result',
 | 
			
		||||
                range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
 | 
			
		||||
                commandType: 'start_path',
 | 
			
		||||
                data: null,
 | 
			
		||||
                raw: {} as any,
 | 
			
		||||
              }
 | 
			
		||||
            const lineCallExp = updatedPipeNode.body.find(
 | 
			
		||||
              (exp) =>
 | 
			
		||||
                exp.type === 'CallExpression' && exp.callee.name === 'line'
 | 
			
		||||
            )
 | 
			
		||||
            if (lineCallExp)
 | 
			
		||||
              engineCommandManager.artifactMap[segmentId] = {
 | 
			
		||||
                type: 'result',
 | 
			
		||||
                range: [lineCallExp.start, lineCallExp.end],
 | 
			
		||||
@ -233,120 +176,189 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
                data: null,
 | 
			
		||||
                raw: {} as any,
 | 
			
		||||
              }
 | 
			
		||||
          })
 | 
			
		||||
        } else {
 | 
			
		||||
          _modifiedAst = addCloseToPipe({
 | 
			
		||||
            node: kclManager.ast,
 | 
			
		||||
            programMemory: kclManager.programMemory,
 | 
			
		||||
            pathToNode: sketchPathToNode,
 | 
			
		||||
          })
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            cmd: { type: 'edit_mode_exit' },
 | 
			
		||||
          })
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            cmd: { type: 'default_camera_disable_sketch_mode' },
 | 
			
		||||
          })
 | 
			
		||||
          kclManager.executeAstMock(_modifiedAst, true)
 | 
			
		||||
          // updateAst(_modifiedAst, true)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'sketch exit execute': () => {
 | 
			
		||||
        kclManager.executeAst()
 | 
			
		||||
      },
 | 
			
		||||
      'set tool': () => {}, // TODO
 | 
			
		||||
      'toast extrude failed': () => {
 | 
			
		||||
        toast.error(
 | 
			
		||||
          'Extrude failed, sketches need to be closed, or not already extruded'
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Set selection': assign(({ selectionRanges }, event) => {
 | 
			
		||||
        if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
 | 
			
		||||
        const setSelections = event.data
 | 
			
		||||
        if (!editorView) return {}
 | 
			
		||||
        if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
 | 
			
		||||
          return { selectionRanges: setSelections.selection }
 | 
			
		||||
        else if (setSelections.selectionType === 'otherSelection') {
 | 
			
		||||
          // TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
 | 
			
		||||
          // const axisAddCmd: EngineCommand = {
 | 
			
		||||
          //   type: 'modeling_cmd_req',
 | 
			
		||||
          //   cmd: {
 | 
			
		||||
          //     type: 'highlight_set_entities',
 | 
			
		||||
          //     entities: [
 | 
			
		||||
          //       setSelections.selection === 'x-axis'
 | 
			
		||||
          //         ? X_AXIS_UUID
 | 
			
		||||
          //         : Y_AXIS_UUID,
 | 
			
		||||
          //     ],
 | 
			
		||||
          //   },
 | 
			
		||||
          //   cmd_id: uuidv4(),
 | 
			
		||||
          // }
 | 
			
		||||
 | 
			
		||||
          // if (!isShiftDown) {
 | 
			
		||||
          //   engineCommandManager
 | 
			
		||||
          //     .sendSceneCommand({
 | 
			
		||||
          //       type: 'modeling_cmd_req',
 | 
			
		||||
          //       cmd: {
 | 
			
		||||
          //         type: 'select_clear',
 | 
			
		||||
          //       },
 | 
			
		||||
          //       cmd_id: uuidv4(),
 | 
			
		||||
          //     })
 | 
			
		||||
          //     .then(() => {
 | 
			
		||||
          //       engineCommandManager.sendSceneCommand(axisAddCmd)
 | 
			
		||||
          //     })
 | 
			
		||||
          // } else {
 | 
			
		||||
          //   engineCommandManager.sendSceneCommand(axisAddCmd)
 | 
			
		||||
          // }
 | 
			
		||||
            kclManager.executeAstMock(astWithUpdatedSource, true)
 | 
			
		||||
 | 
			
		||||
          const {
 | 
			
		||||
            codeMirrorSelection,
 | 
			
		||||
            selectionRangeTypeMap,
 | 
			
		||||
            otherSelections,
 | 
			
		||||
          } = handleSelectionWithShift({
 | 
			
		||||
            otherSelection: setSelections.selection,
 | 
			
		||||
            currentSelections: selectionRanges,
 | 
			
		||||
            isShiftDown,
 | 
			
		||||
          })
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            editorView.dispatch({
 | 
			
		||||
              selection: codeMirrorSelection,
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
          return {
 | 
			
		||||
            selectionRangeTypeMap,
 | 
			
		||||
            selectionRanges: {
 | 
			
		||||
              codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
              otherSelections,
 | 
			
		||||
            },
 | 
			
		||||
            return {
 | 
			
		||||
              sketchPathToNode: _pathToNode,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } else if (setSelections.selectionType === 'singleCodeCursor') {
 | 
			
		||||
          // This DOES NOT set the `selectionRanges` in xstate context
 | 
			
		||||
          // instead it updates/dispatches to the editor, which in turn updates the xstate context
 | 
			
		||||
          // I've found this the best way to deal with the editor without causing an infinite loop
 | 
			
		||||
          // and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
 | 
			
		||||
          // because we want to respect the user manually placing the cursor too.
 | 
			
		||||
        ),
 | 
			
		||||
        'AST add line segment': async (
 | 
			
		||||
          { sketchPathToNode, sketchEnginePathId },
 | 
			
		||||
          { data: { coords, segmentId } }
 | 
			
		||||
        ) => {
 | 
			
		||||
          if (!sketchPathToNode) return
 | 
			
		||||
          const lastCoord = coords[coords.length - 1]
 | 
			
		||||
 | 
			
		||||
          // for more details on how selections see `src/lib/selections.ts`.
 | 
			
		||||
 | 
			
		||||
          const {
 | 
			
		||||
            codeMirrorSelection,
 | 
			
		||||
            selectionRangeTypeMap,
 | 
			
		||||
            otherSelections,
 | 
			
		||||
          } = handleSelectionWithShift({
 | 
			
		||||
            codeSelection: setSelections.selection,
 | 
			
		||||
            currentSelections: selectionRanges,
 | 
			
		||||
            isShiftDown,
 | 
			
		||||
          const pathInfo = await engineCommandManager.sendSceneCommand({
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            cmd: {
 | 
			
		||||
              type: 'path_get_info',
 | 
			
		||||
              path_id: sketchEnginePathId,
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
          if (codeMirrorSelection) {
 | 
			
		||||
          const firstSegment = pathInfo?.data?.data?.segments.find(
 | 
			
		||||
            (seg: any) => seg.command === 'line_to'
 | 
			
		||||
          )
 | 
			
		||||
          const firstSegCoords = await engineCommandManager.sendSceneCommand({
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            cmd: {
 | 
			
		||||
              type: 'curve_get_control_points',
 | 
			
		||||
              curve_id: firstSegment.command_id,
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
          const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
 | 
			
		||||
 | 
			
		||||
          const isClose = compareVec2Epsilon(
 | 
			
		||||
            [startPathCoord.x, startPathCoord.y],
 | 
			
		||||
            [lastCoord.x, lastCoord.y]
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          let _modifiedAst: Program
 | 
			
		||||
          if (!isClose) {
 | 
			
		||||
            const newSketchLn = addNewSketchLn({
 | 
			
		||||
              node: kclManager.ast,
 | 
			
		||||
              programMemory: kclManager.programMemory,
 | 
			
		||||
              to: [lastCoord.x, lastCoord.y],
 | 
			
		||||
              from: [coords[0].x, coords[0].y],
 | 
			
		||||
              fnName: 'line',
 | 
			
		||||
              pathToNode: sketchPathToNode,
 | 
			
		||||
            })
 | 
			
		||||
            const _modifiedAst = newSketchLn.modifiedAst
 | 
			
		||||
            kclManager.executeAstMock(_modifiedAst, true).then(() => {
 | 
			
		||||
              const lineCallExp = getNodeFromPath<CallExpression>(
 | 
			
		||||
                kclManager.ast,
 | 
			
		||||
                newSketchLn.pathToNode
 | 
			
		||||
              ).node
 | 
			
		||||
              if (segmentId)
 | 
			
		||||
                engineCommandManager.artifactMap[segmentId] = {
 | 
			
		||||
                  type: 'result',
 | 
			
		||||
                  range: [lineCallExp.start, lineCallExp.end],
 | 
			
		||||
                  commandType: 'extend_path',
 | 
			
		||||
                  parentId: sketchEnginePathId,
 | 
			
		||||
                  data: null,
 | 
			
		||||
                  raw: {} as any,
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
          } else {
 | 
			
		||||
            _modifiedAst = addCloseToPipe({
 | 
			
		||||
              node: kclManager.ast,
 | 
			
		||||
              programMemory: kclManager.programMemory,
 | 
			
		||||
              pathToNode: sketchPathToNode,
 | 
			
		||||
            })
 | 
			
		||||
            engineCommandManager.sendSceneCommand({
 | 
			
		||||
              type: 'modeling_cmd_req',
 | 
			
		||||
              cmd_id: uuidv4(),
 | 
			
		||||
              cmd: { type: 'edit_mode_exit' },
 | 
			
		||||
            })
 | 
			
		||||
            engineCommandManager.sendSceneCommand({
 | 
			
		||||
              type: 'modeling_cmd_req',
 | 
			
		||||
              cmd_id: uuidv4(),
 | 
			
		||||
              cmd: { type: 'default_camera_disable_sketch_mode' },
 | 
			
		||||
            })
 | 
			
		||||
            kclManager.executeAstMock(_modifiedAst, true)
 | 
			
		||||
            // updateAst(_modifiedAst, true)
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'sketch exit execute': () => {
 | 
			
		||||
          kclManager.executeAst()
 | 
			
		||||
        },
 | 
			
		||||
        'set tool': () => {}, // TODO
 | 
			
		||||
        'Set selection': assign(({ selectionRanges }, event) => {
 | 
			
		||||
          if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
 | 
			
		||||
          const setSelections = event.data
 | 
			
		||||
          if (!editorView) return {}
 | 
			
		||||
          if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
 | 
			
		||||
            return { selectionRanges: setSelections.selection }
 | 
			
		||||
          else if (setSelections.selectionType === 'otherSelection') {
 | 
			
		||||
            // TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
 | 
			
		||||
            // const axisAddCmd: EngineCommand = {
 | 
			
		||||
            //   type: 'modeling_cmd_req',
 | 
			
		||||
            //   cmd: {
 | 
			
		||||
            //     type: 'highlight_set_entities',
 | 
			
		||||
            //     entities: [
 | 
			
		||||
            //       setSelections.selection === 'x-axis'
 | 
			
		||||
            //         ? X_AXIS_UUID
 | 
			
		||||
            //         : Y_AXIS_UUID,
 | 
			
		||||
            //     ],
 | 
			
		||||
            //   },
 | 
			
		||||
            //   cmd_id: uuidv4(),
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            // if (!isShiftDown) {
 | 
			
		||||
            //   engineCommandManager
 | 
			
		||||
            //     .sendSceneCommand({
 | 
			
		||||
            //       type: 'modeling_cmd_req',
 | 
			
		||||
            //       cmd: {
 | 
			
		||||
            //         type: 'select_clear',
 | 
			
		||||
            //       },
 | 
			
		||||
            //       cmd_id: uuidv4(),
 | 
			
		||||
            //     })
 | 
			
		||||
            //     .then(() => {
 | 
			
		||||
            //       engineCommandManager.sendSceneCommand(axisAddCmd)
 | 
			
		||||
            //     })
 | 
			
		||||
            // } else {
 | 
			
		||||
            //   engineCommandManager.sendSceneCommand(axisAddCmd)
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              codeMirrorSelection,
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              otherSelections,
 | 
			
		||||
            } = handleSelectionWithShift({
 | 
			
		||||
              otherSelection: setSelections.selection,
 | 
			
		||||
              currentSelections: selectionRanges,
 | 
			
		||||
              isShiftDown,
 | 
			
		||||
            })
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              editorView.dispatch({
 | 
			
		||||
                selection: codeMirrorSelection,
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          if (!setSelections.selection) {
 | 
			
		||||
            return {
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              selectionRanges: {
 | 
			
		||||
                codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
                otherSelections,
 | 
			
		||||
              },
 | 
			
		||||
            }
 | 
			
		||||
          } else if (setSelections.selectionType === 'singleCodeCursor') {
 | 
			
		||||
            // This DOES NOT set the `selectionRanges` in xstate context
 | 
			
		||||
            // instead it updates/dispatches to the editor, which in turn updates the xstate context
 | 
			
		||||
            // I've found this the best way to deal with the editor without causing an infinite loop
 | 
			
		||||
            // and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
 | 
			
		||||
            // because we want to respect the user manually placing the cursor too.
 | 
			
		||||
 | 
			
		||||
            // for more details on how selections see `src/lib/selections.ts`.
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              codeMirrorSelection,
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              otherSelections,
 | 
			
		||||
            } = handleSelectionWithShift({
 | 
			
		||||
              codeSelection: setSelections.selection,
 | 
			
		||||
              currentSelections: selectionRanges,
 | 
			
		||||
              isShiftDown,
 | 
			
		||||
            })
 | 
			
		||||
            if (codeMirrorSelection) {
 | 
			
		||||
              setTimeout(() => {
 | 
			
		||||
                editorView.dispatch({
 | 
			
		||||
                  selection: codeMirrorSelection,
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
            if (!setSelections.selection) {
 | 
			
		||||
              return {
 | 
			
		||||
                selectionRangeTypeMap,
 | 
			
		||||
                selectionRanges: {
 | 
			
		||||
                  codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
                  otherSelections,
 | 
			
		||||
                },
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            return {
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              selectionRanges: {
 | 
			
		||||
@ -355,171 +367,180 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              },
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          // This DOES NOT set the `selectionRanges` in xstate context
 | 
			
		||||
          // same as comment above
 | 
			
		||||
          const { codeMirrorSelection, selectionRangeTypeMap } =
 | 
			
		||||
            handleSelectionBatch({
 | 
			
		||||
              selections: setSelections.selection,
 | 
			
		||||
            })
 | 
			
		||||
          if (codeMirrorSelection) {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              editorView.dispatch({
 | 
			
		||||
                selection: codeMirrorSelection,
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          return { selectionRangeTypeMap }
 | 
			
		||||
        }),
 | 
			
		||||
      },
 | 
			
		||||
      guards: {
 | 
			
		||||
        'Selection contains axis': () => true,
 | 
			
		||||
        'Selection contains edge': () => true,
 | 
			
		||||
        'Selection contains face': () => true,
 | 
			
		||||
        'Selection contains line': () => true,
 | 
			
		||||
        'Selection contains point': () => true,
 | 
			
		||||
        'Selection is not empty': () => true,
 | 
			
		||||
        'has valid extrude selection': ({ selectionRanges }) => {
 | 
			
		||||
          // A user can begin extruding if they either have 1+ faces selected or nothing selected
 | 
			
		||||
          // TODO: I believe this guard only allows for extruding a single face at a time
 | 
			
		||||
          if (selectionRanges.codeBasedSelections.length < 1) return false
 | 
			
		||||
          const isPipe = isSketchPipe(selectionRanges)
 | 
			
		||||
 | 
			
		||||
          if (isSelectionLastLine(selectionRanges, code)) return true
 | 
			
		||||
          if (!isPipe) return false
 | 
			
		||||
 | 
			
		||||
          return canExtrudeSelection(selectionRanges)
 | 
			
		||||
        },
 | 
			
		||||
        'Selection is one face': ({ selectionRanges }) => {
 | 
			
		||||
          return !!isCursorInSketchCommandRange(
 | 
			
		||||
            engineCommandManager.artifactMap,
 | 
			
		||||
            selectionRanges
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      services: {
 | 
			
		||||
        'Get horizontal info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintHorzVertDistance({
 | 
			
		||||
              constraint: 'setHorzDistance',
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionRangeTypeMap,
 | 
			
		||||
            selectionRanges: {
 | 
			
		||||
              codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
              otherSelections,
 | 
			
		||||
            },
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        // This DOES NOT set the `selectionRanges` in xstate context
 | 
			
		||||
        // same as comment above
 | 
			
		||||
        const { codeMirrorSelection, selectionRangeTypeMap } =
 | 
			
		||||
          handleSelectionBatch({
 | 
			
		||||
            selections: setSelections.selection,
 | 
			
		||||
          })
 | 
			
		||||
        if (codeMirrorSelection) {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            editorView.dispatch({
 | 
			
		||||
              selection: codeMirrorSelection,
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        return { selectionRangeTypeMap }
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Selection contains axis': () => true,
 | 
			
		||||
      'Selection contains edge': () => true,
 | 
			
		||||
      'Selection contains face': () => true,
 | 
			
		||||
      'Selection contains line': () => true,
 | 
			
		||||
      'Selection contains point': () => true,
 | 
			
		||||
      'Selection is not empty': () => true,
 | 
			
		||||
      'Selection is one face': ({ selectionRanges }) => {
 | 
			
		||||
        return !!isCursorInSketchCommandRange(
 | 
			
		||||
          engineCommandManager.artifactMap,
 | 
			
		||||
          selectionRanges
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    services: {
 | 
			
		||||
      'Get horizontal info': async ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
      }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
          await applyConstraintHorzVertDistance({
 | 
			
		||||
            constraint: 'setHorzDistance',
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
          })
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get vertical info': async ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
      }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
          await applyConstraintHorzVertDistance({
 | 
			
		||||
            constraint: 'setVertDistance',
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
          })
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
 | 
			
		||||
        },
 | 
			
		||||
        'Get vertical info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }).enabled
 | 
			
		||||
          ? applyConstraintAngleBetween({
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintHorzVertDistance({
 | 
			
		||||
              constraint: 'setVertDistance',
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
          : applyConstraintAngleLength({
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              angleOrLength: 'setAngle',
 | 
			
		||||
            }))
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get length info': async ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
      }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
 | 
			
		||||
          { selectionRanges }
 | 
			
		||||
        )
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get perpendicular distance info': async ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
      }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'Get angle info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        })
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get ABS X info': async ({ selectionRanges }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
 | 
			
		||||
          {
 | 
			
		||||
            constraint: 'xAbs',
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
          }).enabled
 | 
			
		||||
            ? applyConstraintAngleBetween({
 | 
			
		||||
                selectionRanges,
 | 
			
		||||
              })
 | 
			
		||||
            : applyConstraintAngleLength({
 | 
			
		||||
                selectionRanges,
 | 
			
		||||
                angleOrLength: 'setAngle',
 | 
			
		||||
              }))
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get ABS Y info': async ({ selectionRanges }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
 | 
			
		||||
          {
 | 
			
		||||
            constraint: 'yAbs',
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
        },
 | 
			
		||||
        'Get length info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintAngleLength({ selectionRanges })
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
        'Get perpendicular distance info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
 | 
			
		||||
            {
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'Get ABS X info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintAbsDistance({
 | 
			
		||||
              constraint: 'xAbs',
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'Get ABS Y info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintAbsDistance({
 | 
			
		||||
              constraint: 'yAbs',
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    devTools: true,
 | 
			
		||||
  })
 | 
			
		||||
      devTools: true,
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    engineCommandManager.onPlaneSelected((plane_id: string) => {
 | 
			
		||||
@ -538,13 +559,17 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
    })
 | 
			
		||||
  }, [modelingSend])
 | 
			
		||||
 | 
			
		||||
  // useStateMachineCommands({
 | 
			
		||||
  //   state: settingsState,
 | 
			
		||||
  //   send: settingsSend,
 | 
			
		||||
  //   commands,
 | 
			
		||||
  //   owner: 'settings',
 | 
			
		||||
  //   commandBarMeta: settingsCommandBarMeta,
 | 
			
		||||
  // })
 | 
			
		||||
  useStateMachineCommands({
 | 
			
		||||
    machineId: 'modeling',
 | 
			
		||||
    state: modelingState,
 | 
			
		||||
    send: modelingSend,
 | 
			
		||||
    actor: modelingActor,
 | 
			
		||||
    commandBarConfig: modelingMachineConfig,
 | 
			
		||||
    onCancel: () => {
 | 
			
		||||
      console.log('firing onCancel!!')
 | 
			
		||||
      modelingSend({ type: 'Cancel' })
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ModelingMachineContext.Provider
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { fireEvent, render, screen } from '@testing-library/react'
 | 
			
		||||
import { BrowserRouter } from 'react-router-dom'
 | 
			
		||||
import { GlobalStateProvider } from './GlobalStateProvider'
 | 
			
		||||
import CommandBarProvider from './CommandBar'
 | 
			
		||||
import CommandBarProvider from './CommandBar/CommandBar'
 | 
			
		||||
import {
 | 
			
		||||
  NETWORK_CONTENT,
 | 
			
		||||
  NetworkHealthIndicator,
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,4 @@
 | 
			
		||||
import {
 | 
			
		||||
  faCheck,
 | 
			
		||||
  faExclamation,
 | 
			
		||||
  faWifi,
 | 
			
		||||
} from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { faExclamation, faWifi } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { Popover } from '@headlessui/react'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { ActionIcon } from './ActionIcon'
 | 
			
		||||
@ -77,8 +73,8 @@ export const NetworkHealthIndicator = () => {
 | 
			
		||||
            data-testid="network-good"
 | 
			
		||||
          >
 | 
			
		||||
            <ActionIcon
 | 
			
		||||
              icon={faCheck}
 | 
			
		||||
              bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
 | 
			
		||||
              icon="checkmark"
 | 
			
		||||
              bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
 | 
			
		||||
              iconClassName={'text-succeed-80 dark:text-succeed-30'}
 | 
			
		||||
            />
 | 
			
		||||
            {NETWORK_CONTENT.good}
 | 
			
		||||
 | 
			
		||||
@ -143,7 +143,7 @@ function ProjectCard({
 | 
			
		||||
                  className: 'p-1',
 | 
			
		||||
                  size: 'xs',
 | 
			
		||||
                  bgClassName: 'bg-destroy-80',
 | 
			
		||||
                  iconClassName: 'text-destroy-20 dark:text-destroy-40',
 | 
			
		||||
                  iconClassName: '!text-destroy-20 dark:!text-destroy-40',
 | 
			
		||||
                }}
 | 
			
		||||
                className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
@ -185,8 +185,7 @@ function ProjectCard({
 | 
			
		||||
                      bgClassName: 'bg-destroy-80',
 | 
			
		||||
                      className: 'p-1',
 | 
			
		||||
                      size: 'sm',
 | 
			
		||||
                      iconClassName:
 | 
			
		||||
                        'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
 | 
			
		||||
                      iconClassName: '!text-destroy-70 dark:!text-destroy-40',
 | 
			
		||||
                    }}
 | 
			
		||||
                    className="hover:border-destroy-40 dark:hover:border-destroy-40"
 | 
			
		||||
                  >
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
 | 
			
		||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
 | 
			
		||||
import { ProjectWithEntryPointMetadata } from '../Router'
 | 
			
		||||
import { GlobalStateProvider } from './GlobalStateProvider'
 | 
			
		||||
import CommandBarProvider from './CommandBar'
 | 
			
		||||
import CommandBarProvider from './CommandBar/CommandBar'
 | 
			
		||||
 | 
			
		||||
const now = new Date()
 | 
			
		||||
const projectWellFormed = {
 | 
			
		||||
 | 
			
		||||
@ -243,15 +243,14 @@ export const Stream = ({ className = '' }) => {
 | 
			
		||||
      })
 | 
			
		||||
    } else if (
 | 
			
		||||
      !didDragInStream &&
 | 
			
		||||
      (state.matches('Sketch.SketchIdle') ||
 | 
			
		||||
        state.matches('idle') ||
 | 
			
		||||
        state.matches('awaiting selection'))
 | 
			
		||||
      (state.matches('Sketch.SketchIdle') || state.matches('idle'))
 | 
			
		||||
    ) {
 | 
			
		||||
      command.cmd = {
 | 
			
		||||
        type: 'select_with_point',
 | 
			
		||||
        selected_at_window: { x, y },
 | 
			
		||||
        selection_type: 'add',
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      engineCommandManager.sendSceneCommand(command)
 | 
			
		||||
    } else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
 | 
			
		||||
      command.cmd = {
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export const TextEditor = ({
 | 
			
		||||
 | 
			
		||||
  const { settings: { context: { textWrapping } = {} } = {} } =
 | 
			
		||||
    useGlobalStateContext()
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { enable: convertEnabled, handleClick: convertCallback } =
 | 
			
		||||
    useConvertToVariable()
 | 
			
		||||
 | 
			
		||||
@ -136,7 +136,7 @@ export const TextEditor = ({
 | 
			
		||||
        {
 | 
			
		||||
          key: 'Meta-k',
 | 
			
		||||
          run: () => {
 | 
			
		||||
            setCommandBarOpen(true)
 | 
			
		||||
            commandBarSend({ type: 'Open' })
 | 
			
		||||
            return false
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ import {
 | 
			
		||||
} from 'react-router-dom'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { GlobalStateProvider } from './GlobalStateProvider'
 | 
			
		||||
import CommandBarProvider from './CommandBar'
 | 
			
		||||
import CommandBarProvider from './CommandBar/CommandBar'
 | 
			
		||||
 | 
			
		||||
type User = Models['User_type']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { CommandsContext } from 'components/CommandBar'
 | 
			
		||||
import { CommandsContext } from 'components/CommandBar/CommandBar'
 | 
			
		||||
import { useContext } from 'react'
 | 
			
		||||
 | 
			
		||||
export const useCommandsContext = () => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/hooks/usePlatform.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/hooks/usePlatform.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { Platform, platform } from '@tauri-apps/api/os'
 | 
			
		||||
import { isTauri } from 'lib/isTauri'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export default function usePlatform() {
 | 
			
		||||
  const [platformName, setPlatformName] = useState<Platform | ''>('')
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    async function getPlatform() {
 | 
			
		||||
      setPlatformName(await platform())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isTauri()) {
 | 
			
		||||
      void getPlatform()
 | 
			
		||||
    } else {
 | 
			
		||||
      if (navigator.userAgent.indexOf('Mac') !== -1) {
 | 
			
		||||
        setPlatformName('darwin')
 | 
			
		||||
      } else if (navigator.userAgent.indexOf('Win') !== -1) {
 | 
			
		||||
        setPlatformName('win32')
 | 
			
		||||
      } else if (navigator.userAgent.indexOf('Linux') !== -1) {
 | 
			
		||||
        setPlatformName('linux')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [setPlatformName])
 | 
			
		||||
 | 
			
		||||
  return platformName
 | 
			
		||||
}
 | 
			
		||||
@ -1,46 +1,68 @@
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import { AnyStateMachine, StateFrom } from 'xstate'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandBarConfig,
 | 
			
		||||
  createMachineCommand,
 | 
			
		||||
} from '../lib/commands'
 | 
			
		||||
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate'
 | 
			
		||||
import { createMachineCommand } from '../lib/createMachineCommand'
 | 
			
		||||
import { useCommandsContext } from './useCommandsContext'
 | 
			
		||||
import { modelingMachine } from 'machines/modelingMachine'
 | 
			
		||||
import { authMachine } from 'machines/authMachine'
 | 
			
		||||
import { settingsMachine } from 'machines/settingsMachine'
 | 
			
		||||
import { homeMachine } from 'machines/homeMachine'
 | 
			
		||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
 | 
			
		||||
 | 
			
		||||
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
 | 
			
		||||
// This might not be necessary, AnyStateMachine from xstate is working
 | 
			
		||||
export type AllMachines =
 | 
			
		||||
  | typeof modelingMachine
 | 
			
		||||
  | typeof settingsMachine
 | 
			
		||||
  | typeof authMachine
 | 
			
		||||
  | typeof homeMachine
 | 
			
		||||
 | 
			
		||||
interface UseStateMachineCommandsArgs<
 | 
			
		||||
  T extends AllMachines,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
> {
 | 
			
		||||
  machineId: T['id']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  commandBarConfig?: CommandBarConfig<T>
 | 
			
		||||
  commands: Command[]
 | 
			
		||||
  owner: string
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
  commandBarConfig?: CommandSetConfig<T, S>
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useStateMachineCommands<T extends AnyStateMachine>({
 | 
			
		||||
export default function useStateMachineCommands<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
>({
 | 
			
		||||
  machineId,
 | 
			
		||||
  state,
 | 
			
		||||
  send,
 | 
			
		||||
  actor,
 | 
			
		||||
  commandBarConfig,
 | 
			
		||||
  owner,
 | 
			
		||||
}: UseStateMachineCommandsArgs<T>) {
 | 
			
		||||
  const { addCommands, removeCommands } = useCommandsContext()
 | 
			
		||||
  onCancel,
 | 
			
		||||
}: UseStateMachineCommandsArgs<T, S>) {
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const newCommands = state.nextEvents
 | 
			
		||||
      .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
 | 
			
		||||
      .map((type) =>
 | 
			
		||||
        createMachineCommand<T>({
 | 
			
		||||
        createMachineCommand<T, S>({
 | 
			
		||||
          ownerMachine: machineId,
 | 
			
		||||
          type,
 | 
			
		||||
          state,
 | 
			
		||||
          send,
 | 
			
		||||
          actor,
 | 
			
		||||
          commandBarConfig,
 | 
			
		||||
          owner,
 | 
			
		||||
          onCancel,
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      .filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
 | 
			
		||||
 | 
			
		||||
    addCommands(newCommands)
 | 
			
		||||
    commandBarSend({ type: 'Add commands', data: { commands: newCommands } })
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      removeCommands(newCommands)
 | 
			
		||||
      commandBarSend({
 | 
			
		||||
        type: 'Remove commands',
 | 
			
		||||
        data: { commands: newCommands },
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [state])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -57,7 +57,7 @@ select {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
  @apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs;
 | 
			
		||||
  @apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs focus-visible:ring-energy-10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button:hover {
 | 
			
		||||
@ -65,7 +65,7 @@ button:hover {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark button {
 | 
			
		||||
  @apply border-chalkboard-70;
 | 
			
		||||
  @apply border-chalkboard-70 focus-visible:ring-energy-10/50;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark button:hover {
 | 
			
		||||
@ -88,6 +88,14 @@ a:not(.action-button) {
 | 
			
		||||
  @apply text-chalkboard-20 hover:text-energy-10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input {
 | 
			
		||||
  @apply selection:bg-energy-10/50;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark input {
 | 
			
		||||
  @apply selection:bg-energy-10/40;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mono {
 | 
			
		||||
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
 | 
			
		||||
    monospace;
 | 
			
		||||
 | 
			
		||||
@ -248,7 +248,8 @@ export function mutateObjExpProp(
 | 
			
		||||
export function extrudeSketch(
 | 
			
		||||
  node: Program,
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
  shouldPipe = true
 | 
			
		||||
  shouldPipe = true,
 | 
			
		||||
  distance = 4
 | 
			
		||||
): {
 | 
			
		||||
  modifiedAst: Program
 | 
			
		||||
  pathToNode: PathToNode
 | 
			
		||||
@ -274,7 +275,7 @@ export function extrudeSketch(
 | 
			
		||||
    getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
 | 
			
		||||
 | 
			
		||||
  const extrudeCall = createCallExpressionStdLib('extrude', [
 | 
			
		||||
    createLiteral(4),
 | 
			
		||||
    createLiteral(distance),
 | 
			
		||||
    shouldPipe
 | 
			
		||||
      ? createPipeSubstitution()
 | 
			
		||||
      : {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								src/lib/commandBarConfigs/authCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/lib/commandBarConfigs/authCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
import { CommandSetConfig } from 'lib/commandTypes'
 | 
			
		||||
import { authMachine } from 'machines/authMachine'
 | 
			
		||||
 | 
			
		||||
type AuthCommandSchema = {}
 | 
			
		||||
 | 
			
		||||
export const authCommandBarConfig: CommandSetConfig<
 | 
			
		||||
  typeof authMachine,
 | 
			
		||||
  AuthCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Log in': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Log out': {
 | 
			
		||||
    args: [],
 | 
			
		||||
    icon: 'arrowLeft',
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								src/lib/commandBarConfigs/homeCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/lib/commandBarConfigs/homeCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
import { CommandSetConfig } from 'lib/commandTypes'
 | 
			
		||||
import { homeMachine } from 'machines/homeMachine'
 | 
			
		||||
 | 
			
		||||
export type HomeCommandSchema = {
 | 
			
		||||
  'Create project': {
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  'Open project': {
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  'Delete project': {
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  'Rename project': {
 | 
			
		||||
    oldName: string
 | 
			
		||||
    newName: string
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const homeCommandBarConfig: CommandSetConfig<
 | 
			
		||||
  typeof homeMachine,
 | 
			
		||||
  HomeCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Open project': {
 | 
			
		||||
    icon: 'arrowRight',
 | 
			
		||||
    description: 'Open a project',
 | 
			
		||||
    args: {
 | 
			
		||||
      name: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          context.projects.map((p) => ({
 | 
			
		||||
            name: p.name!,
 | 
			
		||||
            value: p.name!,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Create project': {
 | 
			
		||||
    icon: 'folderPlus',
 | 
			
		||||
    description: 'Create a project',
 | 
			
		||||
    args: {
 | 
			
		||||
      name: {
 | 
			
		||||
        inputType: 'string',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.defaultProjectName,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Delete project': {
 | 
			
		||||
    icon: 'close',
 | 
			
		||||
    description: 'Delete a project',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      name: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          context.projects.map((p) => ({
 | 
			
		||||
            name: p.name!,
 | 
			
		||||
            value: p.name!,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Rename project': {
 | 
			
		||||
    icon: 'folder',
 | 
			
		||||
    description: 'Rename a project',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      oldName: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          context.projects.map((p) => ({
 | 
			
		||||
            name: p.name!,
 | 
			
		||||
            value: p.name!,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
      newName: {
 | 
			
		||||
        inputType: 'string',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.defaultProjectName,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								src/lib/commandBarConfigs/modelingCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/lib/commandBarConfigs/modelingCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
import { CommandSetConfig } from 'lib/commandTypes'
 | 
			
		||||
import { Selections } from 'lib/selections'
 | 
			
		||||
import { modelingMachine } from 'machines/modelingMachine'
 | 
			
		||||
 | 
			
		||||
export const EXTRUSION_RESULTS = [
 | 
			
		||||
  'new',
 | 
			
		||||
  'add',
 | 
			
		||||
  'subtract',
 | 
			
		||||
  'intersect',
 | 
			
		||||
] as const
 | 
			
		||||
 | 
			
		||||
export type ModelingCommandSchema = {
 | 
			
		||||
  'Enter sketch': {}
 | 
			
		||||
  Extrude: {
 | 
			
		||||
    selection: Selections // & { type: 'face' } would be cool to lock that down
 | 
			
		||||
    // result: (typeof EXTRUSION_RESULTS)[number]
 | 
			
		||||
    distance: number
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const modelingMachineConfig: CommandSetConfig<
 | 
			
		||||
  typeof modelingMachine,
 | 
			
		||||
  ModelingCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Enter sketch': {
 | 
			
		||||
    description: 'Enter sketch mode.',
 | 
			
		||||
    icon: 'sketch',
 | 
			
		||||
  },
 | 
			
		||||
  Extrude: {
 | 
			
		||||
    description: 'Pull a sketch into 3D along its normal or perpendicular.',
 | 
			
		||||
    icon: 'extrude',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      selection: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['face'],
 | 
			
		||||
        multiple: false, // TODO: multiple selection
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
      // result: {
 | 
			
		||||
      //   inputType: 'options',
 | 
			
		||||
      //   payload: 'add',
 | 
			
		||||
      //   required: true,
 | 
			
		||||
      //   options: EXTRUSION_RESULTS.map((r) => ({
 | 
			
		||||
      //     name: r,
 | 
			
		||||
      //     isCurrent: r === 'add',
 | 
			
		||||
      //     value: r,
 | 
			
		||||
      //   })),
 | 
			
		||||
      // },
 | 
			
		||||
      distance: {
 | 
			
		||||
        inputType: 'number',
 | 
			
		||||
        defaultValue: 5,
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								src/lib/commandBarConfigs/settingsCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/lib/commandBarConfigs/settingsCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,141 @@
 | 
			
		||||
import { CommandSetConfig } from '../commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  BaseUnit,
 | 
			
		||||
  Toggle,
 | 
			
		||||
  UnitSystem,
 | 
			
		||||
  baseUnitsUnion,
 | 
			
		||||
  settingsMachine,
 | 
			
		||||
} from 'machines/settingsMachine'
 | 
			
		||||
import { CameraSystem, cameraSystems } from '../cameraControls'
 | 
			
		||||
import { Themes } from '../theme'
 | 
			
		||||
 | 
			
		||||
// SETTINGS MACHINE
 | 
			
		||||
export type SettingsCommandSchema = {
 | 
			
		||||
  'Set Base Unit': {
 | 
			
		||||
    baseUnit: BaseUnit
 | 
			
		||||
  }
 | 
			
		||||
  'Set Camera Controls': {
 | 
			
		||||
    cameraControls: CameraSystem
 | 
			
		||||
  }
 | 
			
		||||
  'Set Default Project Name': {
 | 
			
		||||
    defaultProjectName: string
 | 
			
		||||
  }
 | 
			
		||||
  'Set Text Wrapping': {
 | 
			
		||||
    textWrapping: Toggle
 | 
			
		||||
  }
 | 
			
		||||
  'Set Theme': {
 | 
			
		||||
    theme: Themes
 | 
			
		||||
  }
 | 
			
		||||
  'Set Unit System': {
 | 
			
		||||
    unitSystem: UnitSystem
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const settingsCommandBarConfig: CommandSetConfig<
 | 
			
		||||
  typeof settingsMachine,
 | 
			
		||||
  SettingsCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Set Base Unit': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      baseUnit: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.baseUnit,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          Object.values(baseUnitsUnion).map((v) => ({
 | 
			
		||||
            name: v,
 | 
			
		||||
            value: v,
 | 
			
		||||
            isCurrent: v === context.baseUnit,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Camera Controls': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      cameraControls: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.cameraControls,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          Object.values(cameraSystems).map((v) => ({
 | 
			
		||||
            name: v,
 | 
			
		||||
            value: v,
 | 
			
		||||
            isCurrent: v === context.cameraControls,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Default Project Name': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    hide: 'web',
 | 
			
		||||
    args: {
 | 
			
		||||
      defaultProjectName: {
 | 
			
		||||
        inputType: 'string',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.defaultProjectName,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Text Wrapping': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      textWrapping: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.textWrapping,
 | 
			
		||||
        options: (context) => [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'On',
 | 
			
		||||
            value: 'On' as Toggle,
 | 
			
		||||
            isCurrent: context.textWrapping === 'On',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Off',
 | 
			
		||||
            value: 'Off' as Toggle,
 | 
			
		||||
            isCurrent: context.textWrapping === 'Off',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Theme': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      theme: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.theme,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          Object.values(Themes).map((v) => ({
 | 
			
		||||
            name: v,
 | 
			
		||||
            value: v,
 | 
			
		||||
            isCurrent: v === context.theme,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Unit System': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      unitSystem: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.unitSystem,
 | 
			
		||||
        options: (context) => [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Imperial',
 | 
			
		||||
            value: 'imperial' as UnitSystem,
 | 
			
		||||
            isCurrent: context.unitSystem === 'imperial',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Metric',
 | 
			
		||||
            value: 'metric' as UnitSystem,
 | 
			
		||||
            isCurrent: context.unitSystem === 'metric',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										136
									
								
								src/lib/commandTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/lib/commandTypes.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,136 @@
 | 
			
		||||
import { CustomIconName } from 'components/CustomIcon'
 | 
			
		||||
import { AllMachines } from 'hooks/useStateMachineCommands'
 | 
			
		||||
import {
 | 
			
		||||
  AnyStateMachine,
 | 
			
		||||
  ContextFrom,
 | 
			
		||||
  EventFrom,
 | 
			
		||||
  InterpreterFrom,
 | 
			
		||||
} from 'xstate'
 | 
			
		||||
import { Selection } from './selections'
 | 
			
		||||
 | 
			
		||||
type Icon = CustomIconName
 | 
			
		||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
 | 
			
		||||
const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
 | 
			
		||||
export type CommandInputType = (typeof INPUT_TYPES)[number]
 | 
			
		||||
 | 
			
		||||
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]: Record<string, any>
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type CommandSet<
 | 
			
		||||
  T extends AllMachines,
 | 
			
		||||
  Schema extends CommandSetSchema<T>
 | 
			
		||||
> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]: Command<
 | 
			
		||||
    T,
 | 
			
		||||
    EventFrom<T>['type'],
 | 
			
		||||
    Schema[EventType]
 | 
			
		||||
  >
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type CommandSetConfig<
 | 
			
		||||
  T extends AllMachines,
 | 
			
		||||
  Schema extends CommandSetSchema<T>
 | 
			
		||||
> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]: CommandConfig<
 | 
			
		||||
    T,
 | 
			
		||||
    EventFrom<T>['type'],
 | 
			
		||||
    Schema[EventType]
 | 
			
		||||
  >
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type Command<
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine,
 | 
			
		||||
  CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
 | 
			
		||||
  CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
 | 
			
		||||
> = {
 | 
			
		||||
  name: CommandName
 | 
			
		||||
  ownerMachine: T['id']
 | 
			
		||||
  needsReview: boolean
 | 
			
		||||
  onSubmit: (data?: CommandSchema) => void
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
  args?: {
 | 
			
		||||
    [ArgName in keyof CommandSchema]: CommandArgument<CommandSchema[ArgName], T>
 | 
			
		||||
  }
 | 
			
		||||
  description?: string
 | 
			
		||||
  icon?: Icon
 | 
			
		||||
  hide?: (typeof PLATFORMS)[number]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandConfig<
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine,
 | 
			
		||||
  CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
 | 
			
		||||
  CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
 | 
			
		||||
> = Omit<
 | 
			
		||||
  Command<T, CommandName, CommandSchema>,
 | 
			
		||||
  'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
 | 
			
		||||
> & {
 | 
			
		||||
  needsReview?: true
 | 
			
		||||
  args?: {
 | 
			
		||||
    [ArgName in keyof CommandSchema]: CommandArgumentConfig<
 | 
			
		||||
      CommandSchema[ArgName],
 | 
			
		||||
      T
 | 
			
		||||
    >
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandArgumentConfig<
 | 
			
		||||
  OutputType,
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine
 | 
			
		||||
> =
 | 
			
		||||
  | {
 | 
			
		||||
      description?: string
 | 
			
		||||
      required: boolean
 | 
			
		||||
      skip?: true
 | 
			
		||||
      defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
 | 
			
		||||
      payload?: OutputType
 | 
			
		||||
    } & (
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'options'>
 | 
			
		||||
          options:
 | 
			
		||||
            | CommandArgumentOption<OutputType>[]
 | 
			
		||||
            | ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
 | 
			
		||||
        }
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'selection'>
 | 
			
		||||
          selectionTypes: Selection['type'][]
 | 
			
		||||
          multiple: boolean
 | 
			
		||||
        }
 | 
			
		||||
      | { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
export type CommandArgument<
 | 
			
		||||
  OutputType,
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine
 | 
			
		||||
> =
 | 
			
		||||
  | {
 | 
			
		||||
      description?: string
 | 
			
		||||
      required: boolean
 | 
			
		||||
      payload?: OutputType // Payload sets the initialized value and more importantly its type
 | 
			
		||||
      defaultValue?: OutputType // Default value is used as the starting value for the input on this argument
 | 
			
		||||
    } & (
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'options'>
 | 
			
		||||
          options: CommandArgumentOption<OutputType>[]
 | 
			
		||||
        }
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'selection'>
 | 
			
		||||
          selectionTypes: Selection['type'][]
 | 
			
		||||
          actor: InterpreterFrom<T>
 | 
			
		||||
          multiple: boolean
 | 
			
		||||
        }
 | 
			
		||||
      | { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
export type CommandArgumentWithName<
 | 
			
		||||
  OutputType,
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine
 | 
			
		||||
> = CommandArgument<OutputType, T> & {
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandArgumentOption<A> = {
 | 
			
		||||
  name: string
 | 
			
		||||
  isCurrent?: boolean
 | 
			
		||||
  value: A
 | 
			
		||||
}
 | 
			
		||||
@ -1,177 +0,0 @@
 | 
			
		||||
import { AnyStateMachine, ContextFrom, EventFrom, StateFrom } from 'xstate'
 | 
			
		||||
import { isTauri } from './isTauri'
 | 
			
		||||
import { CustomIconName } from 'components/CustomIcon'
 | 
			
		||||
 | 
			
		||||
type Icon = CustomIconName
 | 
			
		||||
type Platform = 'both' | 'web' | 'desktop'
 | 
			
		||||
type InputType = 'select' | 'string' | 'interaction'
 | 
			
		||||
export type CommandArgumentOption = { name: string; isCurrent?: boolean }
 | 
			
		||||
 | 
			
		||||
// Command arguments can either be defined manually
 | 
			
		||||
// or flagged as needing to be looked up from the context.
 | 
			
		||||
// This is useful for things like settings, where
 | 
			
		||||
// we want to show the current setting value as the default.
 | 
			
		||||
// The lookup is done in createMachineCommand.
 | 
			
		||||
type CommandArgumentConfig<T extends AnyStateMachine> = {
 | 
			
		||||
  name: string // TODO: I would love for this to be strongly-typed so we could guarantee it's a valid data payload key on the event type.
 | 
			
		||||
  type: InputType
 | 
			
		||||
  description?: string
 | 
			
		||||
} & (
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'select'
 | 
			
		||||
      options?: CommandArgumentOption[]
 | 
			
		||||
      getOptionsFromContext?: keyof ContextFrom<T>
 | 
			
		||||
      defaultValue?: string
 | 
			
		||||
      getDefaultValueFromContext?: keyof ContextFrom<T>
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'string'
 | 
			
		||||
      defaultValue?: string
 | 
			
		||||
      getDefaultValueFromContext?: keyof ContextFrom<T>
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'interaction' }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export type CommandBarConfig<T extends AnyStateMachine> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]:
 | 
			
		||||
    | {
 | 
			
		||||
        args: CommandArgumentConfig<T>[]
 | 
			
		||||
        formatFunction?: (args: string[]) => string
 | 
			
		||||
        icon?: Icon
 | 
			
		||||
        hide?: Platform
 | 
			
		||||
      }
 | 
			
		||||
    | {
 | 
			
		||||
        hide?: Platform
 | 
			
		||||
      }
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type Command = {
 | 
			
		||||
  owner: string
 | 
			
		||||
  name: string
 | 
			
		||||
  callback: Function
 | 
			
		||||
  icon?: Icon
 | 
			
		||||
  args?: CommandArgument[]
 | 
			
		||||
  formatFunction?: (args: string[]) => string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandArgument = {
 | 
			
		||||
  name: string
 | 
			
		||||
  defaultValue?: string
 | 
			
		||||
} & (
 | 
			
		||||
  | {
 | 
			
		||||
      type: Extract<InputType, 'select'>
 | 
			
		||||
      options: CommandArgumentOption[]
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: Exclude<InputType, 'select'>
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
interface CreateMachineCommandProps<T extends AnyStateMachine> {
 | 
			
		||||
  type: EventFrom<T>['type']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  commandBarConfig?: CommandBarConfig<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  owner: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Creates a command with subcommands, ready for use in the CommandBar component,
 | 
			
		||||
// from a more terse Command Bar Meta definition.
 | 
			
		||||
export function createMachineCommand<T extends AnyStateMachine>({
 | 
			
		||||
  type,
 | 
			
		||||
  state,
 | 
			
		||||
  commandBarConfig,
 | 
			
		||||
  send,
 | 
			
		||||
  owner,
 | 
			
		||||
}: CreateMachineCommandProps<T>): Command | null {
 | 
			
		||||
  const lookedUpMeta = commandBarConfig && commandBarConfig[type]
 | 
			
		||||
  if (!lookedUpMeta) return null
 | 
			
		||||
 | 
			
		||||
  // Hide commands based on platform by returning `null`
 | 
			
		||||
  // so the consumer can filter them out
 | 
			
		||||
  if ('hide' in lookedUpMeta) {
 | 
			
		||||
    const { hide } = lookedUpMeta
 | 
			
		||||
    if (hide === 'both') return null
 | 
			
		||||
    else if (hide === 'desktop' && isTauri()) return null
 | 
			
		||||
    else if (hide === 'web' && !isTauri()) return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const icon = ('icon' in lookedUpMeta && lookedUpMeta.icon) || undefined
 | 
			
		||||
  const formatFunction =
 | 
			
		||||
    ('formatFunction' in lookedUpMeta && lookedUpMeta.formatFunction) ||
 | 
			
		||||
    undefined
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    name: type,
 | 
			
		||||
    owner,
 | 
			
		||||
    icon,
 | 
			
		||||
    callback: (data: EventFrom<T, typeof type>) => {
 | 
			
		||||
      if (data !== undefined && data !== null) {
 | 
			
		||||
        send(type, { data })
 | 
			
		||||
      } else {
 | 
			
		||||
        send(type)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    ...('args' in lookedUpMeta
 | 
			
		||||
      ? {
 | 
			
		||||
          args: getCommandArgumentValuesFromContext(state, lookedUpMeta.args),
 | 
			
		||||
          formatFunction,
 | 
			
		||||
        }
 | 
			
		||||
      : {}),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCommandArgumentValuesFromContext<T extends AnyStateMachine>(
 | 
			
		||||
  state: StateFrom<T>,
 | 
			
		||||
  args: CommandArgumentConfig<T>[]
 | 
			
		||||
): CommandArgument[] {
 | 
			
		||||
  function getDefaultValue(
 | 
			
		||||
    arg: CommandArgumentConfig<T> & { type: 'string' | 'select' }
 | 
			
		||||
  ) {
 | 
			
		||||
    if (
 | 
			
		||||
      arg.type === 'select' ||
 | 
			
		||||
      ('getDefaultValueFromContext' in arg && arg.getDefaultValueFromContext)
 | 
			
		||||
    ) {
 | 
			
		||||
      return state.context[arg.getDefaultValueFromContext]
 | 
			
		||||
    } else {
 | 
			
		||||
      return arg.defaultValue
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return args.map((arg) => {
 | 
			
		||||
    switch (arg.type) {
 | 
			
		||||
      case 'interaction':
 | 
			
		||||
        return {
 | 
			
		||||
          name: arg.name,
 | 
			
		||||
          type: 'interaction',
 | 
			
		||||
        }
 | 
			
		||||
      case 'string':
 | 
			
		||||
        return {
 | 
			
		||||
          name: arg.name,
 | 
			
		||||
          type: arg.type,
 | 
			
		||||
          defaultValue: arg.getDefaultValueFromContext
 | 
			
		||||
            ? state.context[arg.getDefaultValueFromContext]
 | 
			
		||||
            : arg.defaultValue,
 | 
			
		||||
        }
 | 
			
		||||
      default:
 | 
			
		||||
        return {
 | 
			
		||||
          name: arg.name,
 | 
			
		||||
          type: arg.type,
 | 
			
		||||
          defaultValue: getDefaultValue(arg),
 | 
			
		||||
          options: arg.getOptionsFromContext
 | 
			
		||||
            ? state.context[arg.getOptionsFromContext].map(
 | 
			
		||||
                (v: string | { name: string }) => ({
 | 
			
		||||
                  name: typeof v === 'string' ? v : v.name,
 | 
			
		||||
                  isCurrent: v === getDefaultValue(arg),
 | 
			
		||||
                })
 | 
			
		||||
              )
 | 
			
		||||
            : arg.getDefaultValueFromContext
 | 
			
		||||
            ? arg.options?.map((v) => ({
 | 
			
		||||
                ...v,
 | 
			
		||||
                isCurrent: v.name === getDefaultValue(arg),
 | 
			
		||||
              }))
 | 
			
		||||
            : arg.options,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										158
									
								
								src/lib/createMachineCommand.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								src/lib/createMachineCommand.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,158 @@
 | 
			
		||||
import { AnyStateMachine, EventFrom, InterpreterFrom, StateFrom } from 'xstate'
 | 
			
		||||
import { isTauri } from './isTauri'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
  CommandArgumentConfig,
 | 
			
		||||
  CommandConfig,
 | 
			
		||||
  CommandSetConfig,
 | 
			
		||||
  CommandSetSchema,
 | 
			
		||||
} from './commandTypes'
 | 
			
		||||
 | 
			
		||||
interface CreateMachineCommandProps<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
> {
 | 
			
		||||
  type: EventFrom<T>['type']
 | 
			
		||||
  ownerMachine: T['id']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
  commandBarConfig?: CommandSetConfig<T, S>
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Creates a command with subcommands, ready for use in the CommandBar component,
 | 
			
		||||
// from a more terse Command Bar Meta definition.
 | 
			
		||||
export function createMachineCommand<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
>({
 | 
			
		||||
  ownerMachine,
 | 
			
		||||
  type,
 | 
			
		||||
  state,
 | 
			
		||||
  send,
 | 
			
		||||
  actor,
 | 
			
		||||
  commandBarConfig,
 | 
			
		||||
  onCancel,
 | 
			
		||||
}: CreateMachineCommandProps<T, S>): Command<
 | 
			
		||||
  T,
 | 
			
		||||
  typeof type,
 | 
			
		||||
  S[typeof type]
 | 
			
		||||
> | null {
 | 
			
		||||
  const commandConfig = commandBarConfig && commandBarConfig[type]
 | 
			
		||||
  if (!commandConfig) return null
 | 
			
		||||
 | 
			
		||||
  // Hide commands based on platform by returning `null`
 | 
			
		||||
  // so the consumer can filter them out
 | 
			
		||||
  if ('hide' in commandConfig) {
 | 
			
		||||
    const { hide } = commandConfig
 | 
			
		||||
    if (hide === 'both') return null
 | 
			
		||||
    else if (hide === 'desktop' && isTauri()) return null
 | 
			
		||||
    else if (hide === 'web' && !isTauri()) return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const icon = ('icon' in commandConfig && commandConfig.icon) || undefined
 | 
			
		||||
 | 
			
		||||
  const command: Command<T, typeof type, S[typeof type]> = {
 | 
			
		||||
    name: type,
 | 
			
		||||
    ownerMachine: ownerMachine,
 | 
			
		||||
    icon,
 | 
			
		||||
    needsReview: commandConfig.needsReview || false,
 | 
			
		||||
    onSubmit: (data?: S[typeof type]) => {
 | 
			
		||||
      if (data !== undefined && data !== null) {
 | 
			
		||||
        send(type, { data })
 | 
			
		||||
      } else {
 | 
			
		||||
        send(type)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (commandConfig.args) {
 | 
			
		||||
    const newArgs = buildCommandArguments(state, commandConfig.args, actor)
 | 
			
		||||
 | 
			
		||||
    command.args = newArgs
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (onCancel) {
 | 
			
		||||
    command.onCancel = onCancel
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return command
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Takes the args from a CommandConfig and creates
 | 
			
		||||
// a finalized CommandArgument object for each one,
 | 
			
		||||
// bundled together into the args for a Command.
 | 
			
		||||
function buildCommandArguments<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>,
 | 
			
		||||
  CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
 | 
			
		||||
>(
 | 
			
		||||
  state: StateFrom<T>,
 | 
			
		||||
  args: CommandConfig<T, CommandName, S>['args'],
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
): NonNullable<Command<T, CommandName, S>['args']> {
 | 
			
		||||
  const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
 | 
			
		||||
 | 
			
		||||
  for (const arg in args) {
 | 
			
		||||
    const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
 | 
			
		||||
    const newArg = buildCommandArgument(argConfig, state, actor)
 | 
			
		||||
    newArgs[arg] = newArg
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return newArgs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildCommandArgument<
 | 
			
		||||
  O extends CommandSetSchema<T>,
 | 
			
		||||
  T extends AnyStateMachine
 | 
			
		||||
>(
 | 
			
		||||
  arg: CommandArgumentConfig<O, T>,
 | 
			
		||||
  state: StateFrom<T>,
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
 | 
			
		||||
  const baseCommandArgument = {
 | 
			
		||||
    description: arg.description,
 | 
			
		||||
    required: arg.required,
 | 
			
		||||
    payload: arg.payload,
 | 
			
		||||
    defaultValue:
 | 
			
		||||
      arg.defaultValue instanceof Function
 | 
			
		||||
        ? arg.defaultValue(state.context)
 | 
			
		||||
        : arg.defaultValue,
 | 
			
		||||
  } satisfies Omit<CommandArgument<O, T>, 'inputType'>
 | 
			
		||||
 | 
			
		||||
  if (arg.inputType === 'options') {
 | 
			
		||||
    const options = arg.options
 | 
			
		||||
      ? arg.options instanceof Function
 | 
			
		||||
        ? arg.options(state.context)
 | 
			
		||||
        : arg.options
 | 
			
		||||
      : undefined
 | 
			
		||||
 | 
			
		||||
    if (!options) {
 | 
			
		||||
      throw new Error('Options must be provided for options input type')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      inputType: arg.inputType,
 | 
			
		||||
      ...baseCommandArgument,
 | 
			
		||||
      options,
 | 
			
		||||
    } satisfies CommandArgument<O, T> & { inputType: 'options' }
 | 
			
		||||
  } else if (arg.inputType === 'selection') {
 | 
			
		||||
    if (!actor)
 | 
			
		||||
      throw new Error('Actor must be provided for selection input type')
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      inputType: arg.inputType,
 | 
			
		||||
      ...baseCommandArgument,
 | 
			
		||||
      multiple: arg.multiple,
 | 
			
		||||
      selectionTypes: arg.selectionTypes,
 | 
			
		||||
      actor,
 | 
			
		||||
    } satisfies CommandArgument<O, T> & { inputType: 'selection' }
 | 
			
		||||
  } else {
 | 
			
		||||
    return {
 | 
			
		||||
      inputType: arg.inputType,
 | 
			
		||||
      ...baseCommandArgument,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -7,6 +7,10 @@ import { EditorSelection } from '@codemirror/state'
 | 
			
		||||
import { kclManager } from 'lang/KclSinglton'
 | 
			
		||||
import { SelectionRange } from '@uiw/react-codemirror'
 | 
			
		||||
import { isOverlap } from 'lib/utils'
 | 
			
		||||
import { isCursorInSketchCommandRange } from 'lang/util'
 | 
			
		||||
import { Program } from 'lang/wasm'
 | 
			
		||||
import { doesPipeHaveCallExp } from 'lang/queryAst'
 | 
			
		||||
import { CommandArgument } from './commandTypes'
 | 
			
		||||
 | 
			
		||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
 | 
			
		||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
 | 
			
		||||
@ -371,3 +375,128 @@ function resetAndSetEngineEntitySelectionCmds(
 | 
			
		||||
    },
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isSketchPipe(selectionRanges: Selections) {
 | 
			
		||||
  return isCursorInSketchCommandRange(
 | 
			
		||||
    engineCommandManager.artifactMap,
 | 
			
		||||
    selectionRanges
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isSelectionLastLine(
 | 
			
		||||
  selectionRanges: Selections,
 | 
			
		||||
  code: string,
 | 
			
		||||
  i = 0
 | 
			
		||||
) {
 | 
			
		||||
  return selectionRanges.codeBasedSelections[i].range[1] === code.length
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommonASTNode = {
 | 
			
		||||
  selection: Selection
 | 
			
		||||
  ast: Program
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function buildCommonNodeFromSelection(
 | 
			
		||||
  selectionRanges: Selections,
 | 
			
		||||
  i: number
 | 
			
		||||
) {
 | 
			
		||||
  return {
 | 
			
		||||
    selection: selectionRanges.codeBasedSelections[i],
 | 
			
		||||
    ast: kclManager.ast,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function nodeHasExtrude(node: CommonASTNode) {
 | 
			
		||||
  return doesPipeHaveCallExp({
 | 
			
		||||
    calleeName: 'extrude',
 | 
			
		||||
    ...node,
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function nodeHasClose(node: CommonASTNode) {
 | 
			
		||||
  return doesPipeHaveCallExp({
 | 
			
		||||
    calleeName: 'close',
 | 
			
		||||
    ...node,
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canExtrudeSelection(selection: Selections) {
 | 
			
		||||
  const commonNodes = selection.codeBasedSelections.map((_, i) =>
 | 
			
		||||
    buildCommonNodeFromSelection(selection, i)
 | 
			
		||||
  )
 | 
			
		||||
  return (
 | 
			
		||||
    !!isSketchPipe(selection) &&
 | 
			
		||||
    commonNodes.every((n) => nodeHasClose(n)) &&
 | 
			
		||||
    commonNodes.every((n) => !nodeHasExtrude(n))
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canExtrudeSelectionItem(selection: Selections, i: number) {
 | 
			
		||||
  const commonNode = buildCommonNodeFromSelection(selection, i)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    !!isSketchPipe(selection) &&
 | 
			
		||||
    nodeHasClose(commonNode) &&
 | 
			
		||||
    !nodeHasExtrude(commonNode)
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This accounts for non-geometry selections under "other"
 | 
			
		||||
export type ResolvedSelectionType = [Selection['type'] | 'other', number]
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * In the future, I'd like this function to properly return the type of each selected entity based on
 | 
			
		||||
 * its code source range, so that we can show something like "0 objects" or "1 face" or "1 line, 2 edges",
 | 
			
		||||
 * and then validate the selection in CommandBarSelectionInput.tsx and show the proper label.
 | 
			
		||||
 * @param selection
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export function getSelectionType(
 | 
			
		||||
  selection: Selections
 | 
			
		||||
): ResolvedSelectionType[] {
 | 
			
		||||
  return selection.codeBasedSelections
 | 
			
		||||
    .map((s, i) => {
 | 
			
		||||
      if (canExtrudeSelectionItem(selection, i)) {
 | 
			
		||||
        return ['face', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad
 | 
			
		||||
      } else {
 | 
			
		||||
        return ['other', 1] as ResolvedSelectionType
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .reduce((acc, [type, count]) => {
 | 
			
		||||
      const foundIndex = acc.findIndex((item) => item && item[0] === type)
 | 
			
		||||
 | 
			
		||||
      if (foundIndex === -1) {
 | 
			
		||||
        return [...acc, [type, count]]
 | 
			
		||||
      } else {
 | 
			
		||||
        const temp = [...acc]
 | 
			
		||||
        temp[foundIndex][1] += count
 | 
			
		||||
        return temp
 | 
			
		||||
      }
 | 
			
		||||
    }, [] as ResolvedSelectionType[])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getSelectionTypeDisplayText(
 | 
			
		||||
  selection: Selections
 | 
			
		||||
): string | null {
 | 
			
		||||
  const selectionsByType = getSelectionType(selection)
 | 
			
		||||
 | 
			
		||||
  return (selectionsByType as Exclude<typeof selectionsByType, 'none'>)
 | 
			
		||||
    .map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`)
 | 
			
		||||
    .join(', ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canSubmitSelectionArg(
 | 
			
		||||
  selectionsByType: 'none' | ResolvedSelectionType[],
 | 
			
		||||
  argument: CommandArgument<unknown> & { inputType: 'selection' }
 | 
			
		||||
) {
 | 
			
		||||
  return (
 | 
			
		||||
    selectionsByType !== 'none' &&
 | 
			
		||||
    selectionsByType.every(([type, count]) => {
 | 
			
		||||
      const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
 | 
			
		||||
      return (
 | 
			
		||||
        foundIndex !== -1 &&
 | 
			
		||||
        (!argument.multiple ? count < 2 && count > 0 : count > 0)
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { createMachine, assign } from 'xstate'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import withBaseURL from '../lib/withBaseURL'
 | 
			
		||||
import { CommandBarConfig } from '../lib/commands'
 | 
			
		||||
import { isTauri } from 'lib/isTauri'
 | 
			
		||||
import { invoke } from '@tauri-apps/api'
 | 
			
		||||
import { VITE_KC_API_BASE_URL } from 'env'
 | 
			
		||||
@ -40,16 +39,6 @@ export type Events =
 | 
			
		||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
 | 
			
		||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
 | 
			
		||||
 | 
			
		||||
export const authCommandBarConfig: CommandBarConfig<typeof authMachine> = {
 | 
			
		||||
  'Log in': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Log out': {
 | 
			
		||||
    args: [],
 | 
			
		||||
    icon: 'arrowLeft',
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const authMachine = createMachine<UserContext, Events>(
 | 
			
		||||
  {
 | 
			
		||||
    id: 'Auth',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										425
									
								
								src/machines/commandBarMachine.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										425
									
								
								src/machines/commandBarMachine.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,425 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
  CommandArgumentWithName,
 | 
			
		||||
} from 'lib/commandTypes'
 | 
			
		||||
import { Selections } from 'lib/selections'
 | 
			
		||||
 | 
			
		||||
export const commandBarMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oGAOJYbgACzAJAj+FIUAAruhBtxYGQACJwUPveO6pMA43AhA5UqWXOzBjS-nOR3LySqXNODTyZwONTV-GXXXPUcfHqTlOM4TrSublb5-lLewlIVySRaOrmGw-jLSI8FRPf0zzjS8HHCOlogZE1ERUEUSgPa05yQ1ZjEQeZP2-VwDzUN1zEabxTlPAkG2bVt207Hs+wHZAm1wGAvi7EgSD7DsSG7XscG4K9oOnNNEVUeQZGSOxc0qaQ7HhdDBJFeRc35extGkNI+UAgNJDIls2xwSMqK4-tJBJAB3LAYl0-AHjYLtuBjLsACN0B4djOL7XixhvBIDxUT8qj5VIkTwtQHRk8oUQcD15LMXQclcdTa00xttMojjqO42BJAANSwShOAgRsIzICB+DASQHg1VAAGtSo1HK8sbMASVSgz3JgmdMnKGYzRlbIdGXA8EX8zd3RsTY9yyY4iLxID6ySiiLP0misrq-LeF0shWxIVBAzYShGwAM229BJFq3LVsa5q3INf5r1gg9JIfXqqh0TQsiFTYrCyJZRpsXJ4oJVUNU4MBjLsxznITX5rqgjzYMsaZtGXaVNhcZFKgRd0RXUdJ6mUXQXX+jpAeB0GyQIRbuNa-jbwQcVSmhAUeTkWQ3HyGSBVKDQMyRAKAsJwNiZBshVXVLUXLSnjoeTPjUxpnmpFtcVs1cPNBq5T8fR5DrKwaHEppIomwCBoWAFEIGcinJcg6XYZnTRhJsbQJU9VlQTVtQNbqcVZhsSFxH5zoWzeSr2ya1z0qKkqypwCrqpOlaGrDiX9WtqdZYSeSEeXEplCceo1xkvQLGCmUIp0Mwck8fWQMVIOQ4spODIHYqcFK8qqpqhPuAu8P+zoCDDRlzzTCkCVWWlSxrBceTygRFxZHZNJbE2VQ5AFAO6PeevI0bmiNpY7bJF2g6jvjs7E8u9KqfTxAkK2fZYuRvdYUGjQZnqYVxRKdx-ZOHBUAgHAQQ00AyD1tgJUQCwLQMEyHUG0Gh7SOmmDuGB6gOTyVBMkDeRJIBgLahA4oFo8zWlSPUT00o0KFA9NMYKrhKwbH2GaQ81cawEljGOdskM8B4OpgkWY9MV5ZjqLUN6MlqGojoRFEo6QWYb1PC8McuCbpD1gosLYrhkTZCxNIawhYxEfW0HhCKKgzT8lBAHLS809KX37Dwm+iIWZSEinjTRy51BFnvNofM7hGZfgXBYuaOlrG9wyiZMya1IxWRsnY4eDi2TONsK45IaghQbEkO4VIKlsaVE2AE8iQTxZN2WufCJMS7o6JSBKNQLp7CT35EKZw2wnDVDyCvBczDmg10NsbYyZS7aZHBNU58q8sTQgxnud+kVsjyQzHrTprDLh11DjY+AyjwFy00DQ2EaQBSLAPLIDGWRNw2BUiXHkwVCJeCAA */
 | 
			
		||||
    context: {
 | 
			
		||||
      commands: [] as Command[],
 | 
			
		||||
      selectedCommand: undefined as Command | undefined,
 | 
			
		||||
      currentArgument: undefined as
 | 
			
		||||
        | (CommandArgument<unknown> & { name: string })
 | 
			
		||||
        | undefined,
 | 
			
		||||
      selectionRanges: {
 | 
			
		||||
        otherSelections: [],
 | 
			
		||||
        codeBasedSelections: [],
 | 
			
		||||
      } as Selections,
 | 
			
		||||
      argumentsToSubmit: {} as { [x: string]: unknown },
 | 
			
		||||
    },
 | 
			
		||||
    id: 'Command Bar',
 | 
			
		||||
    initial: 'Closed',
 | 
			
		||||
    states: {
 | 
			
		||||
      Closed: {
 | 
			
		||||
        on: {
 | 
			
		||||
          Open: {
 | 
			
		||||
            target: 'Selecting command',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Find and select command': {
 | 
			
		||||
            target: 'Command selected',
 | 
			
		||||
            actions: [
 | 
			
		||||
              'Find and select command',
 | 
			
		||||
              'Initialize arguments to submit',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Add commands': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                commands: (context, event) =>
 | 
			
		||||
                  [...context.commands, ...event.data.commands].sort(
 | 
			
		||||
                    sortCommands
 | 
			
		||||
                  ),
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Remove commands': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                commands: (context, event) =>
 | 
			
		||||
                  context.commands.filter(
 | 
			
		||||
                    (c) =>
 | 
			
		||||
                      !event.data.commands.some(
 | 
			
		||||
                        (c2) =>
 | 
			
		||||
                          c2.name === c.name &&
 | 
			
		||||
                          c2.ownerMachine === c.ownerMachine
 | 
			
		||||
                      )
 | 
			
		||||
                  ),
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Selecting command': {
 | 
			
		||||
        on: {
 | 
			
		||||
          'Select command': {
 | 
			
		||||
            target: 'Command selected',
 | 
			
		||||
            actions: ['Set selected command', 'Initialize arguments to submit'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Command selected': {
 | 
			
		||||
        always: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
            cond: 'Command has no arguments',
 | 
			
		||||
            actions: ['Execute command'],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                currentArgument: (context, event) => {
 | 
			
		||||
                  const { selectedCommand } = context
 | 
			
		||||
                  if (!(selectedCommand && selectedCommand.args))
 | 
			
		||||
                    return undefined
 | 
			
		||||
                  const argName = Object.keys(selectedCommand.args)[0]
 | 
			
		||||
                  return {
 | 
			
		||||
                    ...selectedCommand.args[argName],
 | 
			
		||||
                    name: argName,
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Gathering arguments': {
 | 
			
		||||
        states: {
 | 
			
		||||
          'Awaiting input': {
 | 
			
		||||
            on: {
 | 
			
		||||
              'Submit argument': {
 | 
			
		||||
                target: 'Validating',
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          Validating: {
 | 
			
		||||
            invoke: {
 | 
			
		||||
              src: 'Validate argument',
 | 
			
		||||
              id: 'validateArgument',
 | 
			
		||||
              onDone: {
 | 
			
		||||
                target: '#Command Bar.Checking Arguments',
 | 
			
		||||
                actions: [
 | 
			
		||||
                  assign({
 | 
			
		||||
                    argumentsToSubmit: (context, event) => {
 | 
			
		||||
                      const [argName, argData] = Object.entries(event.data)[0]
 | 
			
		||||
                      const { currentArgument } = context
 | 
			
		||||
                      if (!currentArgument) return {}
 | 
			
		||||
                      return {
 | 
			
		||||
                        ...context.argumentsToSubmit,
 | 
			
		||||
                        [argName]: argData,
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  }),
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              onError: [
 | 
			
		||||
                {
 | 
			
		||||
                  target: 'Awaiting input',
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        initial: 'Awaiting input',
 | 
			
		||||
 | 
			
		||||
        on: {
 | 
			
		||||
          'Change current argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            internal: true,
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Deselect command': {
 | 
			
		||||
            target: 'Selecting command',
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                selectedCommand: (_c, _e) => undefined,
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      Review: {
 | 
			
		||||
        entry: ['Clear current argument'],
 | 
			
		||||
        on: {
 | 
			
		||||
          'Submit command': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
            actions: ['Execute command'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Add argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Remove argument': {
 | 
			
		||||
            target: 'Review',
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                argumentsToSubmit: (context, event) => {
 | 
			
		||||
                  const argName = Object.keys(event.data)[0]
 | 
			
		||||
                  const { argumentsToSubmit } = context
 | 
			
		||||
                  const newArgumentsToSubmit = { ...argumentsToSubmit }
 | 
			
		||||
                  newArgumentsToSubmit[argName] = undefined
 | 
			
		||||
                  return newArgumentsToSubmit
 | 
			
		||||
                },
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Edit argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Checking Arguments': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          src: 'Validate all arguments',
 | 
			
		||||
          id: 'validateArguments',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Review',
 | 
			
		||||
              cond: 'Command needs review',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Closed',
 | 
			
		||||
              actions: 'Execute command',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Gathering arguments',
 | 
			
		||||
              actions: ['Set current argument'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    on: {
 | 
			
		||||
      Close: {
 | 
			
		||||
        target: '.Closed',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      Clear: {
 | 
			
		||||
        target: '#Command Bar',
 | 
			
		||||
        internal: true,
 | 
			
		||||
        actions: ['Clear argument data'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as
 | 
			
		||||
        | { type: 'Open' }
 | 
			
		||||
        | { type: 'Close' }
 | 
			
		||||
        | { type: 'Clear' }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Select command'
 | 
			
		||||
            data: { command: Command }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Deselect command' }
 | 
			
		||||
        | { type: 'Submit command'; data: { [x: string]: unknown } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Add argument'
 | 
			
		||||
            data: { argument: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Remove argument'
 | 
			
		||||
            data: { [x: string]: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Edit argument'
 | 
			
		||||
            data: { arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Add commands'
 | 
			
		||||
            data: { commands: Command[] }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Remove commands'
 | 
			
		||||
            data: { commands: Command[] }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Submit argument'; data: { [x: string]: unknown } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.validateArguments'
 | 
			
		||||
            data: { [x: string]: unknown }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'error.platform.validateArguments'
 | 
			
		||||
            data: { message: string; arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Find and select command'
 | 
			
		||||
            data: { name: string; ownerMachine: string }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Change current argument'
 | 
			
		||||
            data: { arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
          },
 | 
			
		||||
    },
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
    preserveActionOrder: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    actions: {
 | 
			
		||||
      'Execute command': (context, event) => {
 | 
			
		||||
        const { selectedCommand } = context
 | 
			
		||||
        if (!selectedCommand) return
 | 
			
		||||
        if (selectedCommand?.args) {
 | 
			
		||||
          selectedCommand?.onSubmit(
 | 
			
		||||
            event.type === 'Submit command' ||
 | 
			
		||||
              event.type === 'done.invoke.validateArguments'
 | 
			
		||||
              ? event.data
 | 
			
		||||
              : undefined
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          selectedCommand?.onSubmit()
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Clear current argument': assign({
 | 
			
		||||
        currentArgument: undefined,
 | 
			
		||||
      }),
 | 
			
		||||
      'Set current argument': assign({
 | 
			
		||||
        currentArgument: (context, event) => {
 | 
			
		||||
          switch (event.type) {
 | 
			
		||||
            case 'error.platform.validateArguments':
 | 
			
		||||
              return event.data.arg
 | 
			
		||||
            case 'Edit argument':
 | 
			
		||||
              return event.data.arg
 | 
			
		||||
            case 'Change current argument':
 | 
			
		||||
              return event.data.arg
 | 
			
		||||
            default:
 | 
			
		||||
              return context.currentArgument
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Clear argument data': assign({
 | 
			
		||||
        selectedCommand: undefined,
 | 
			
		||||
        currentArgument: undefined,
 | 
			
		||||
        argumentsToSubmit: {},
 | 
			
		||||
      }),
 | 
			
		||||
      'Set selected command': assign({
 | 
			
		||||
        selectedCommand: (c, e) =>
 | 
			
		||||
          e.type === 'Select command' ? e.data.command : c.selectedCommand,
 | 
			
		||||
      }),
 | 
			
		||||
      'Find and select command': assign({
 | 
			
		||||
        selectedCommand: (c, e) => {
 | 
			
		||||
          if (e.type !== 'Find and select command') return c.selectedCommand
 | 
			
		||||
          const found = c.commands.find(
 | 
			
		||||
            (cmd) =>
 | 
			
		||||
              cmd.name === e.data.name &&
 | 
			
		||||
              cmd.ownerMachine === e.data.ownerMachine
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          return !!found ? found : c.selectedCommand
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Initialize arguments to submit': assign({
 | 
			
		||||
        argumentsToSubmit: (c, e) => {
 | 
			
		||||
          if (
 | 
			
		||||
            e.type !== 'Select command' &&
 | 
			
		||||
            e.type !== 'Find and select command'
 | 
			
		||||
          )
 | 
			
		||||
            return c.argumentsToSubmit
 | 
			
		||||
          const command =
 | 
			
		||||
            'command' in e.data ? e.data.command : c.selectedCommand!
 | 
			
		||||
          if (!command.args) return {}
 | 
			
		||||
          const args: { [x: string]: unknown } = {}
 | 
			
		||||
          for (const [argName, arg] of Object.entries(command.args)) {
 | 
			
		||||
            args[argName] = arg.payload
 | 
			
		||||
          }
 | 
			
		||||
          return args
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Command needs review': (context, _) =>
 | 
			
		||||
        context.selectedCommand?.needsReview || false,
 | 
			
		||||
    },
 | 
			
		||||
    services: {
 | 
			
		||||
      'Validate argument': (context, event) => {
 | 
			
		||||
        if (event.type !== 'Submit argument') return Promise.reject()
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
          // TODO: figure out if we should validate argument data here or in the form itself,
 | 
			
		||||
          // and if we should support people configuring a argument's validation function
 | 
			
		||||
 | 
			
		||||
          resolve(event.data)
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
      'Validate all arguments': (context, _) => {
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
          for (const [argName, arg] of Object.entries(
 | 
			
		||||
            context.argumentsToSubmit
 | 
			
		||||
          )) {
 | 
			
		||||
            let argConfig = context.selectedCommand!.args![argName]
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
              typeof arg !== typeof argConfig.payload &&
 | 
			
		||||
              typeof arg !== typeof argConfig.defaultValue &&
 | 
			
		||||
              'options' in argConfig &&
 | 
			
		||||
              typeof arg !== typeof argConfig.options[0].value
 | 
			
		||||
            ) {
 | 
			
		||||
              return reject({
 | 
			
		||||
                message: 'Argument payload is of the wrong type',
 | 
			
		||||
                arg: {
 | 
			
		||||
                  ...argConfig,
 | 
			
		||||
                  name: argName,
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!arg && argConfig.required) {
 | 
			
		||||
              return reject({
 | 
			
		||||
                message: 'Argument payload is falsy but is required',
 | 
			
		||||
                arg: {
 | 
			
		||||
                  ...argConfig,
 | 
			
		||||
                  name: argName,
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return resolve(context.argumentsToSubmit)
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    delays: {},
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function sortCommands(a: Command, b: Command) {
 | 
			
		||||
  if (b.ownerMachine === 'auth') return -1
 | 
			
		||||
  if (a.ownerMachine === 'auth') return 1
 | 
			
		||||
  return a.name.localeCompare(b.name)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								src/machines/commandBarMachine.typegen.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/machines/commandBarMachine.typegen.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
			
		||||
// This file was automatically generated. Edits will be overwritten
 | 
			
		||||
 | 
			
		||||
export interface Typegen0 {
 | 
			
		||||
  '@@xstate/typegen': true
 | 
			
		||||
  internalEvents: {
 | 
			
		||||
    '': { type: '' }
 | 
			
		||||
    'done.invoke.validateArgument': {
 | 
			
		||||
      type: 'done.invoke.validateArgument'
 | 
			
		||||
      data: unknown
 | 
			
		||||
      __tip: 'See the XState TS docs to learn how to strongly type this.'
 | 
			
		||||
    }
 | 
			
		||||
    'done.invoke.validateArguments': {
 | 
			
		||||
      type: 'done.invoke.validateArguments'
 | 
			
		||||
      data: unknown
 | 
			
		||||
      __tip: 'See the XState TS docs to learn how to strongly type this.'
 | 
			
		||||
    }
 | 
			
		||||
    'error.platform.validateArgument': {
 | 
			
		||||
      type: 'error.platform.validateArgument'
 | 
			
		||||
      data: unknown
 | 
			
		||||
    }
 | 
			
		||||
    'error.platform.validateArguments': {
 | 
			
		||||
      type: 'error.platform.validateArguments'
 | 
			
		||||
      data: unknown
 | 
			
		||||
    }
 | 
			
		||||
    'xstate.init': { type: 'xstate.init' }
 | 
			
		||||
  }
 | 
			
		||||
  invokeSrcNameMap: {
 | 
			
		||||
    'Validate all arguments': 'done.invoke.validateArguments'
 | 
			
		||||
    'Validate argument': 'done.invoke.validateArgument'
 | 
			
		||||
  }
 | 
			
		||||
  missingImplementations: {
 | 
			
		||||
    actions:
 | 
			
		||||
      | 'Add arguments'
 | 
			
		||||
      | 'Close dialog'
 | 
			
		||||
      | 'Execute command'
 | 
			
		||||
      | 'Open dialog'
 | 
			
		||||
    delays: never
 | 
			
		||||
    guards: never
 | 
			
		||||
    services: never
 | 
			
		||||
  }
 | 
			
		||||
  eventsCausingActions: {
 | 
			
		||||
    'Add arguments': 'done.invoke.validateArguments'
 | 
			
		||||
    'Add commands': 'Add commands'
 | 
			
		||||
    'Close dialog': 'Close'
 | 
			
		||||
    'Execute command': '' | 'Submit'
 | 
			
		||||
    'Open dialog': 'Open'
 | 
			
		||||
    'Remove argument': 'Remove argument'
 | 
			
		||||
    'Remove commands': 'Remove commands'
 | 
			
		||||
    'Set current argument':
 | 
			
		||||
      | 'Add argument'
 | 
			
		||||
      | 'Edit argument'
 | 
			
		||||
      | 'error.platform.validateArguments'
 | 
			
		||||
  }
 | 
			
		||||
  eventsCausingDelays: {}
 | 
			
		||||
  eventsCausingGuards: {
 | 
			
		||||
    'Arguments are ready': 'done.invoke.validateArguments'
 | 
			
		||||
    'Command has no arguments': ''
 | 
			
		||||
  }
 | 
			
		||||
  eventsCausingServices: {
 | 
			
		||||
    'Validate all arguments': 'done.invoke.validateArgument'
 | 
			
		||||
    'Validate argument': 'Submit'
 | 
			
		||||
  }
 | 
			
		||||
  matchesStates:
 | 
			
		||||
    | 'Checking Arguments'
 | 
			
		||||
    | 'Closed'
 | 
			
		||||
    | 'Command selected'
 | 
			
		||||
    | 'Gathering arguments'
 | 
			
		||||
    | 'Gathering arguments.Awaiting input'
 | 
			
		||||
    | 'Gathering arguments.Validating'
 | 
			
		||||
    | 'Review'
 | 
			
		||||
    | 'Selecting command'
 | 
			
		||||
    | { 'Gathering arguments'?: 'Awaiting input' | 'Validating' }
 | 
			
		||||
  tags: never
 | 
			
		||||
}
 | 
			
		||||
@ -1,56 +1,6 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { ProjectWithEntryPointMetadata } from '../Router'
 | 
			
		||||
import { CommandBarConfig } from '../lib/commands'
 | 
			
		||||
 | 
			
		||||
export const homeCommandConfig: CommandBarConfig<typeof homeMachine> = {
 | 
			
		||||
  'Create project': {
 | 
			
		||||
    icon: 'folderPlus',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        getDefaultValueFromContext: 'defaultProjectName',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Open project': {
 | 
			
		||||
    icon: 'arrowRight',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getOptionsFromContext: 'projects',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Delete project': {
 | 
			
		||||
    icon: 'close',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getOptionsFromContext: 'projects',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Rename project': {
 | 
			
		||||
    icon: 'folder',
 | 
			
		||||
    formatFunction: (args: string[]) =>
 | 
			
		||||
      `Rename project "${args[0]}" to "${args[1]}"`,
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'oldName',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getOptionsFromContext: 'projects',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'newName',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        getDefaultValueFromContext: 'defaultProjectName',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
 | 
			
		||||
 | 
			
		||||
export const homeMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
@ -188,10 +138,10 @@ export const homeMachine = createMachine(
 | 
			
		||||
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as
 | 
			
		||||
        | { type: 'Open project'; data: { name: string } }
 | 
			
		||||
        | { type: 'Rename project'; data: { oldName: string; newName: string } }
 | 
			
		||||
        | { type: 'Create project'; data: { name: string } }
 | 
			
		||||
        | { type: 'Delete project'; data: { name: string } }
 | 
			
		||||
        | { type: 'Open project'; data: HomeCommandSchema['Open project'] }
 | 
			
		||||
        | { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
 | 
			
		||||
        | { type: 'Create project'; data: HomeCommandSchema['Create project'] }
 | 
			
		||||
        | { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
 | 
			
		||||
        | { type: 'navigate'; data: { name: string } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.read-projects'
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -32,14 +32,14 @@
 | 
			
		||||
"Get vertical info": "done.invoke.get-vertical-info";
 | 
			
		||||
        };
 | 
			
		||||
        missingImplementations: {
 | 
			
		||||
          actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed";
 | 
			
		||||
          actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute";
 | 
			
		||||
          delays: never;
 | 
			
		||||
          guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face";
 | 
			
		||||
          guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face" | "has valid extrude selection";
 | 
			
		||||
          services: "Get ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
 | 
			
		||||
        };
 | 
			
		||||
        eventsCausingActions: {
 | 
			
		||||
          "AST add line segment": "Add point";
 | 
			
		||||
"AST extrude": "" | "extrude intent";
 | 
			
		||||
"AST extrude": "Extrude";
 | 
			
		||||
"AST start new sketch": "Add point";
 | 
			
		||||
"Add to code-based selection": "Deselect point" | "Deselect segment" | "Select all" | "Select edge" | "Select face" | "Select point" | "Select segment";
 | 
			
		||||
"Add to other selection": "Select axis";
 | 
			
		||||
@ -63,7 +63,7 @@
 | 
			
		||||
"edit mode enter": "Enter sketch" | "Re-execute";
 | 
			
		||||
"edit_mode_exit": "Cancel";
 | 
			
		||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain snap to X" | "Constrain snap to Y" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-abs-x-info" | "error.platform.get-abs-y-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
 | 
			
		||||
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
 | 
			
		||||
"hide default planes": "Cancel" | "Select default plane" | "Set selection" | "xstate.stop";
 | 
			
		||||
"reset sketch metadata": "Cancel" | "Select default plane";
 | 
			
		||||
"set default plane id": "Select default plane";
 | 
			
		||||
"set sketch metadata": "Enter sketch";
 | 
			
		||||
@ -72,9 +72,8 @@
 | 
			
		||||
"set tool line": "Equip tool";
 | 
			
		||||
"set tool move": "Equip move tool" | "Re-execute" | "Set selection";
 | 
			
		||||
"show default planes": "Enter sketch";
 | 
			
		||||
"sketch exit execute": "Cancel" | "Complete line" | "xstate.stop";
 | 
			
		||||
"sketch exit execute": "Cancel" | "Complete line" | "Set selection" | "xstate.stop";
 | 
			
		||||
"sketch mode enabled": "Enter sketch" | "Re-execute" | "Select default plane";
 | 
			
		||||
"toast extrude failed": "";
 | 
			
		||||
        };
 | 
			
		||||
        eventsCausingDelays: {
 | 
			
		||||
          
 | 
			
		||||
@ -105,8 +104,7 @@
 | 
			
		||||
"Selection is one face": "Enter sketch";
 | 
			
		||||
"can move": "";
 | 
			
		||||
"can move with execute": "";
 | 
			
		||||
"has no selection": "extrude intent";
 | 
			
		||||
"has valid extrude selection": "" | "extrude intent";
 | 
			
		||||
"has valid extrude selection": "Extrude";
 | 
			
		||||
"is editing existing sketch": "";
 | 
			
		||||
        };
 | 
			
		||||
        eventsCausingServices: {
 | 
			
		||||
@ -118,7 +116,7 @@
 | 
			
		||||
"Get perpendicular distance info": "Constrain perpendicular distance";
 | 
			
		||||
"Get vertical info": "Constrain vertical distance";
 | 
			
		||||
        };
 | 
			
		||||
        matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
 | 
			
		||||
        matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
 | 
			
		||||
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
 | 
			
		||||
        tags: never;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { CommandBarConfig } from '../lib/commands'
 | 
			
		||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
 | 
			
		||||
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
 | 
			
		||||
import { CameraSystem } from 'lib/cameraControls'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
 | 
			
		||||
@ -24,85 +23,6 @@ export type Toggle = 'On' | 'Off'
 | 
			
		||||
 | 
			
		||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
 | 
			
		||||
 | 
			
		||||
export const settingsCommandBarConfig: CommandBarConfig<
 | 
			
		||||
  typeof settingsMachine
 | 
			
		||||
> = {
 | 
			
		||||
  'Set Base Unit': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'baseUnit',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getDefaultValueFromContext: 'baseUnit',
 | 
			
		||||
        options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Camera Controls': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'cameraControls',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getDefaultValueFromContext: 'cameraControls',
 | 
			
		||||
        options: Object.values(cameraSystems).map((v) => ({ name: v })),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Default Directory': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Set Default Project Name': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    hide: 'web',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'defaultProjectName',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        getDefaultValueFromContext: 'defaultProjectName',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Onboarding Status': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Set Text Wrapping': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'textWrapping',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getDefaultValueFromContext: 'textWrapping',
 | 
			
		||||
        options: [{ name: 'On' }, { name: 'Off' }],
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Theme': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'theme',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getDefaultValueFromContext: 'theme',
 | 
			
		||||
        options: Object.values(Themes).map((v): { name: string } => ({
 | 
			
		||||
          name: v,
 | 
			
		||||
        })),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Unit System': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'unitSystem',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getDefaultValueFromContext: 'unitSystem',
 | 
			
		||||
        options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const settingsMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import { Link } from 'react-router-dom'
 | 
			
		||||
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
 | 
			
		||||
import Loading from '../components/Loading'
 | 
			
		||||
import { useMachine } from '@xstate/react'
 | 
			
		||||
import { homeCommandConfig, homeMachine } from '../machines/homeMachine'
 | 
			
		||||
import { homeMachine } from '../machines/homeMachine'
 | 
			
		||||
import { ContextFrom, EventFrom } from 'xstate'
 | 
			
		||||
import { paths } from '../Router'
 | 
			
		||||
import {
 | 
			
		||||
@ -30,11 +30,12 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
 | 
			
		||||
import { sep } from '@tauri-apps/api/path'
 | 
			
		||||
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
 | 
			
		||||
 | 
			
		||||
// This route only opens in the Tauri desktop context for now,
 | 
			
		||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
 | 
			
		||||
const Home = () => {
 | 
			
		||||
  const { commands, setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
 | 
			
		||||
  const {
 | 
			
		||||
@ -56,7 +57,7 @@ const Home = () => {
 | 
			
		||||
        event: EventFrom<typeof homeMachine>
 | 
			
		||||
      ) => {
 | 
			
		||||
        if (event.data && 'name' in event.data) {
 | 
			
		||||
          setCommandBarOpen(false)
 | 
			
		||||
          commandBarSend({ type: 'Close' })
 | 
			
		||||
          navigate(
 | 
			
		||||
            `${paths.FILE}/${encodeURIComponent(
 | 
			
		||||
              context.defaultDirectory + sep + event.data.name
 | 
			
		||||
@ -143,12 +144,11 @@ const Home = () => {
 | 
			
		||||
 | 
			
		||||
  const isSortByModified = sort?.includes('modified') || !sort || sort === null
 | 
			
		||||
 | 
			
		||||
  useStateMachineCommands<typeof homeMachine>({
 | 
			
		||||
    commands,
 | 
			
		||||
  useStateMachineCommands({
 | 
			
		||||
    machineId: 'home',
 | 
			
		||||
    send,
 | 
			
		||||
    state,
 | 
			
		||||
    commandBarConfig: homeCommandConfig,
 | 
			
		||||
    owner: 'home',
 | 
			
		||||
    commandBarConfig: homeCommandBarConfig,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import usePlatform from 'hooks/usePlatform'
 | 
			
		||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
 | 
			
		||||
import { useStore } from '../../useStore'
 | 
			
		||||
import { Platform, platform } from '@tauri-apps/api/os'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export default function CmdK() {
 | 
			
		||||
  const { buttonDownInStream } = useStore((s) => ({
 | 
			
		||||
@ -9,14 +8,7 @@ export default function CmdK() {
 | 
			
		||||
  }))
 | 
			
		||||
  const dismiss = useDismiss()
 | 
			
		||||
  const next = useNextClick(onboardingPaths.USER_MENU)
 | 
			
		||||
  const [platformName, setPlatformName] = useState<Platform | ''>('')
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    async function getPlatform() {
 | 
			
		||||
      setPlatformName(await platform())
 | 
			
		||||
    }
 | 
			
		||||
    void getPlatform()
 | 
			
		||||
  }, [setPlatformName])
 | 
			
		||||
  const platformName = usePlatform()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
 | 
			
		||||
@ -29,13 +21,13 @@ export default function CmdK() {
 | 
			
		||||
        <h2 className="text-2xl">Command Bar</h2>
 | 
			
		||||
        <p className="my-4">
 | 
			
		||||
          Press{' '}
 | 
			
		||||
          {platformName === 'win32' ? (
 | 
			
		||||
          {platformName === 'darwin' ? (
 | 
			
		||||
            <>
 | 
			
		||||
              <kbd>Win</kbd> + <kbd>/</kbd>
 | 
			
		||||
              <kbd>⌘</kbd> + <kbd>K</kbd>
 | 
			
		||||
            </>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <>
 | 
			
		||||
              <kbd>OS</kbd> + <kbd>K</kbd>
 | 
			
		||||
              <kbd>Ctrl</kbd> + <kbd>/</kbd>
 | 
			
		||||
            </>
 | 
			
		||||
          )}{' '}
 | 
			
		||||
          to open the command bar. Try changing your theme with it.
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { ActionButton } from '../components/ActionButton'
 | 
			
		||||
import { isTauri } from '../lib/isTauri'
 | 
			
		||||
import { invoke } from '@tauri-apps/api/tauri'
 | 
			
		||||
@ -65,7 +64,7 @@ const SignIn = () => {
 | 
			
		||||
          <ActionButton
 | 
			
		||||
            Element="button"
 | 
			
		||||
            onClick={signInTauri}
 | 
			
		||||
            icon={{ icon: faSignInAlt }}
 | 
			
		||||
            icon={{ icon: 'arrowRight' }}
 | 
			
		||||
            className="w-fit mt-4"
 | 
			
		||||
            data-testid="sign-in-button"
 | 
			
		||||
          >
 | 
			
		||||
@ -80,7 +79,7 @@ const SignIn = () => {
 | 
			
		||||
              typeof window !== 'undefined' &&
 | 
			
		||||
                window.location.href.replace('signin', '')
 | 
			
		||||
            )}`}
 | 
			
		||||
            icon={{ icon: faSignInAlt }}
 | 
			
		||||
            icon={{ icon: 'arrowRight' }}
 | 
			
		||||
            className="w-fit mt-4"
 | 
			
		||||
          >
 | 
			
		||||
            Sign in
 | 
			
		||||
 | 
			
		||||
@ -8497,9 +8497,9 @@ ws@^8.8.0:
 | 
			
		||||
  integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
 | 
			
		||||
 | 
			
		||||
xstate@^4.38.2:
 | 
			
		||||
  version "4.38.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
 | 
			
		||||
  integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==
 | 
			
		||||
  version "4.38.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
 | 
			
		||||
  integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
 | 
			
		||||
 | 
			
		||||
y18n@^5.0.5:
 | 
			
		||||
  version "5.0.8"
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user