Compare commits
15 Commits
achalmers/
...
extrude-ma
Author | SHA1 | Date | |
---|---|---|---|
2d350a93ed | |||
16dd5aab96 | |||
bf68a87897 | |||
c6e97e729a | |||
d2535bb8c2 | |||
b01357b49e | |||
793e3510cc | |||
04ae8141c3 | |||
3ae5393dd7 | |||
38119d5a3b | |||
b453b4b453 | |||
3972431cb4 | |||
884545fcde | |||
6deb242eb5 | |||
77fa9af71e |
9
.github/workflows/ci.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
- name: Install codespell
|
||||
run: |
|
||||
python -m pip install codespell
|
||||
@ -181,6 +181,9 @@ jobs:
|
||||
cd ../../
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
|
||||
- name: Run vite build (build:both)
|
||||
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||
|
||||
- name: Fix format
|
||||
run: yarn fmt
|
||||
|
||||
@ -250,10 +253,12 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
cargo install tauri-driver
|
||||
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||
export VITE_KC_API_BASE_URL
|
||||
xvfb-run yarn test:e2e:tauri
|
||||
env:
|
||||
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
|
||||
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
|
@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
## KittyCAD Modeling App
|
||||
|
||||
|
@ -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
|
||||
|
@ -4,8 +4,7 @@ import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import fsp from 'fs/promises'
|
||||
import { Themes } from '../../src/lib/theme'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
@ -86,26 +85,26 @@ test('Basic sketch', async ({ page }) => {
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[10.97, -14.79]'
|
||||
const tenish = '11.07'
|
||||
const startAt = '[18.26, -24.63]'
|
||||
const num = '18.43'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)`)
|
||||
|> line([${num}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)`)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line([-22.04, 0], %)`)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-36.69, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
@ -133,8 +132,8 @@ test('Basic sketch', async ({ page }) => {
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line({ to: [${tenish}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${num}], %)
|
||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||
})
|
||||
|
||||
@ -183,6 +182,26 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
|
||||
// wait for .cm-lint-marker-error not to be visible
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// let's check we get an error when defining the same variable twice
|
||||
await page.getByText('const bottomAng = 25').click()
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type("// Let's define the same thing twice")
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const topAng = 42')
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible()
|
||||
|
||||
await page.locator('.cm-lintRange.cm-lintRange-error').hover()
|
||||
await expect(page.locator('.cm-diagnosticText')).toBeVisible()
|
||||
await expect(page.getByText('Cannot redefine topAng')).toBeVisible()
|
||||
|
||||
const secondTopAng = await page.getByText('topAng').first()
|
||||
await secondTopAng?.dblclick()
|
||||
await page.keyboard.type('otherAng')
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('executes on load', async ({ page, context }) => {
|
||||
@ -488,27 +507,27 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[10.97, -14.79]'
|
||||
const tenish = '11.07'
|
||||
const twentyish = '22.04'
|
||||
const startAt = '[18.26, -24.63]'
|
||||
const num = '18.43'
|
||||
const num2 = '36.69'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)`)
|
||||
|> line([${num}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)`)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line([-${twentyish}, 0], %)`)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-${num2}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
@ -561,7 +580,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
// check the same selection again by putting cursor in code first then selecting axis
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
|
||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
@ -576,7 +595,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
// select segment in editor than another segment in scene and check there are two cursors
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
|
||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
@ -613,3 +632,106 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// hover again and check it works
|
||||
await selectionSequence()
|
||||
})
|
||||
|
||||
test('Command bar works and can change a setting', async ({ page }) => {
|
||||
// Brief boilerplate
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
|
||||
// First try opening the command bar and closing it
|
||||
// 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()
|
||||
|
||||
// Now try the same, but with the keyboard shortcut, check focus
|
||||
await page.keyboard.press('Meta+K')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
await expect(cmdSearchBar).toBeFocused()
|
||||
|
||||
// Try typing in the command bar
|
||||
await page.keyboard.type('theme')
|
||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||
await expect(themeOption).toBeVisible()
|
||||
await themeOption.click()
|
||||
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('ArrowDown')
|
||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active'
|
||||
)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Check the toast appeared
|
||||
await expect(page.getByText(`Set Theme to "${Themes.Dark}"`)).toBeVisible()
|
||||
// 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('|> startProfileAt([-6.95, 4.98], %)').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, %)`
|
||||
)
|
||||
})
|
||||
|
@ -45,7 +45,7 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
vantage: { x: 0, y: 50, z: 50 },
|
||||
vantage: { x: 0, y: 85, z: 85 },
|
||||
},
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
@ -29,13 +29,14 @@ describe('KCMA (Tauri, Linux)', () => {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
const verifyUrl = `https://api.kittycad.io/oauth2/device/verify?user_code=${userCode}`
|
||||
const apiBaseUrl = process.env.VITE_KC_API_BASE_URL
|
||||
const verifyUrl = `${apiBaseUrl}/oauth2/device/verify?user_code=${userCode}`
|
||||
console.log(`GET ${verifyUrl}`)
|
||||
const vr = await fetch(verifyUrl, { headers })
|
||||
console.log(vr.status)
|
||||
|
||||
// Device flow: confirm
|
||||
const confirmUrl = 'https://api.kittycad.io/oauth2/device/confirm'
|
||||
const confirmUrl = `${apiBaseUrl}/oauth2/device/confirm`
|
||||
const data = JSON.stringify({ user_code: userCode })
|
||||
console.log(`POST ${confirmUrl} ${data}`)
|
||||
const cr = await fetch(confirmUrl, {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.10.2",
|
||||
@ -60,6 +60,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"start:prod": "vite preview --port=3000",
|
||||
"serve": "vite serve --port=3000",
|
||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||
"build:local": "vite build",
|
||||
@ -134,7 +135,7 @@
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.0",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
|
@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 475 B |
@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 469 B |
@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5" stroke="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 200 B |
BIN
public/kcma-logomark-outlined.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -1,14 +1,13 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"build": {
|
||||
"beforeBuildCommand": "yarn build:both",
|
||||
"beforeDevCommand": "yarn start",
|
||||
"devPath": "http://localhost:3000",
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": {
|
||||
"productName": "kittycad-modeling",
|
||||
"version": "0.12.0"
|
||||
"version": "0.13.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -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'
|
||||
|
||||
|
17
src/App.tsx
@ -172,11 +172,8 @@ export function App() {
|
||||
<ModalContainer />
|
||||
<Resizable
|
||||
className={
|
||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||
(buttonDownInStream || onboardingStatus === 'camera'
|
||||
? ' pointer-events-none '
|
||||
: ' ') +
|
||||
paneOpacity
|
||||
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||
+paneOpacity
|
||||
}
|
||||
defaultSize={{
|
||||
width: '550px',
|
||||
@ -188,10 +185,16 @@ export function App() {
|
||||
maxHeight={'auto'}
|
||||
handleClasses={{
|
||||
right:
|
||||
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
||||
'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
|
||||
(buttonDownInStream || onboardingStatus === 'camera'
|
||||
? 'pointer-events-none '
|
||||
: 'pointer-events-auto'),
|
||||
}}
|
||||
>
|
||||
<div id="code-pane" className="h-full flex flex-col justify-between">
|
||||
<div
|
||||
id="code-pane"
|
||||
className="h-full flex flex-col justify-between pointer-events-none"
|
||||
>
|
||||
<CollapsiblePanel
|
||||
title="Code"
|
||||
icon={faCode}
|
||||
|
@ -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'
|
||||
|
@ -1,106 +0,0 @@
|
||||
.toolbarWrapper {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
@apply flex gap-4 items-center rounded-full;
|
||||
@apply border border-cool-20/30 bg-cool-10/50;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbar {
|
||||
@apply border-cool-100/50 bg-cool-120/50;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbar {
|
||||
@apply border-fern-20/20 bg-fern-10/20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbar {
|
||||
@apply border-fern-120/50 bg-fern-100/30;
|
||||
}
|
||||
|
||||
.toolbarCap {
|
||||
@apply text-sm font-bold;
|
||||
@apply bg-cool-20/50 text-cool-100;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbarCap {
|
||||
@apply bg-cool-90/50 text-cool-30;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbarCap {
|
||||
@apply bg-fern-20/50 text-fern-100;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbarCap {
|
||||
@apply bg-fern-90/50 text-fern-30;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply self-stretch flex items-center px-4 py-1;
|
||||
@apply rounded-l-full;
|
||||
}
|
||||
|
||||
.popoverToggle {
|
||||
@apply self-stretch m-0 flex items-center px-4 py-1;
|
||||
@apply rounded-r-full border-none;
|
||||
@apply hover:bg-cool-20;
|
||||
}
|
||||
|
||||
.toolbarButtons::-webkit-scrollbar {
|
||||
@apply h-0.5;
|
||||
}
|
||||
|
||||
.toolbarButtons {
|
||||
@apply flex items-center overflow-x-auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.toolbarButtons button {
|
||||
@apply text-chalkboard-90 bg-chalkboard-10/50 border-chalkboard-50 whitespace-nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@apply gap-1.5 p-0.5 pr-1;
|
||||
@apply rounded-sm;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button {
|
||||
@apply text-chalkboard-30 bg-chalkboard-90/50 border-chalkboard-50;
|
||||
}
|
||||
.toolbarButtons button:hover {
|
||||
@apply text-cool-90 bg-cool-10;
|
||||
}
|
||||
:global(.sketch) .toolbarButtons button:hover {
|
||||
@apply text-fern-90 bg-fern-10;
|
||||
}
|
||||
.toolbarButtons button:disabled {
|
||||
@apply text-chalkboard-70 bg-chalkboard-30;
|
||||
}
|
||||
.toolbarButtons button:disabled:hover {
|
||||
@apply !bg-inherit !text-inherit cursor-not-allowed;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbarButtons button {
|
||||
@apply text-chalkboard-20 border-chalkboard-50;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button:hover {
|
||||
@apply text-cool-10 border-chalkboard-50 bg-cool-90;
|
||||
}
|
||||
:global(.dark .sketch) .toolbarButtons button:hover {
|
||||
@apply text-fern-10 border-chalkboard-50 bg-fern-90;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button:disabled {
|
||||
@apply text-chalkboard-40 bg-chalkboard-80;
|
||||
}
|
||||
|
||||
:global(.dark) .popoverToggle {
|
||||
@apply hover:bg-cool-90;
|
||||
}
|
||||
|
||||
:global(.sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-90;
|
||||
}
|
305
src/Toolbar.tsx
@ -1,22 +1,18 @@
|
||||
import { Fragment, WheelEvent, useRef, useMemo } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import styles from './Toolbar.module.css'
|
||||
import { WheelEvent, useRef, useMemo } from 'react'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
export const sketchButtonClassnames = {
|
||||
background:
|
||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-fern-20 dark:group-hover:bg-fern-10 dark:hover:bg-fern-10 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50',
|
||||
icon: 'text-fern-20 h-auto group-hover:text-fern-10 hover:text-fern-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-60 hover:group-disabled:text-inherit',
|
||||
}
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const platform = usePlatform()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { state, send, context } = useModelingContext()
|
||||
const toolbarButtonsRef = useRef<HTMLSpanElement>(null)
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
const bgClassName =
|
||||
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
|
||||
const pathId = useMemo(
|
||||
() =>
|
||||
isCursorInSketchCommandRange(
|
||||
@ -35,72 +31,102 @@ export const Toolbar = () => {
|
||||
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||
}
|
||||
|
||||
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
||||
function ToolbarButtons({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
return (
|
||||
<span
|
||||
<ul
|
||||
{...props}
|
||||
ref={toolbarButtonsRef}
|
||||
onWheel={handleToolbarButtonsWheelEvent}
|
||||
className={styles.toolbarButtons + ' ' + className}
|
||||
className={
|
||||
'm-0 py-1 rounded-l-sm flex gap-2 items-center overflow-x-auto ' +
|
||||
className
|
||||
}
|
||||
style={{ scrollbarWidth: 'thin' }}
|
||||
>
|
||||
{state.nextEvents.includes('Enter sketch') && (
|
||||
<button
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
</button>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
icon={{
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||
<button
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Edit Sketch
|
||||
</button>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
icon={{
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Edit Sketch
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
<button onClick={() => send({ type: 'Cancel' })} className="group">
|
||||
<ActionIcon icon="exit" className="!p-0.5" size="md" />
|
||||
Exit Sketch
|
||||
</button>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Cancel' })}
|
||||
icon={{
|
||||
icon: 'arrowLeft',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Exit Sketch
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.matches('Sketch') && !state.matches('idle') && (
|
||||
<button
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Line Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip tool')
|
||||
}
|
||||
className={
|
||||
'group ' +
|
||||
(state.matches('Sketch.Line Tool')
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="line" className="!p-0.5" size="md" />
|
||||
Line
|
||||
</button>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Line Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip tool')
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Line Tool')}
|
||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||
icon={{
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Line
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.matches('Sketch') && (
|
||||
<button
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Move Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip move tool')
|
||||
}
|
||||
className={
|
||||
'group ' +
|
||||
(state.matches('Sketch.Move Tool')
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="move" className="!p-0.5" size="md" />
|
||||
Move
|
||||
</button>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Move Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip move tool')
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Move Tool')}
|
||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||
icon={{
|
||||
icon: 'move',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Move
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents
|
||||
@ -125,102 +151,71 @@ export const Toolbar = () => {
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => (
|
||||
<button
|
||||
key={eventName}
|
||||
onClick={() => send(eventName)}
|
||||
className="group"
|
||||
disabled={
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName)
|
||||
}
|
||||
title={eventName}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={'line'} // TODO
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
{eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', '')}
|
||||
</button>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className="text-sm"
|
||||
key={eventName}
|
||||
onClick={() => send(eventName)}
|
||||
disabled={
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName)
|
||||
}
|
||||
title={eventName}
|
||||
icon={{
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
{eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', '')}
|
||||
</ActionButton>
|
||||
</li>
|
||||
))}
|
||||
{state.matches('idle') && (
|
||||
<button
|
||||
onClick={() => send('extrude intent')}
|
||||
disabled={!state.can('extrude intent')}
|
||||
className="group"
|
||||
title={
|
||||
state.can('extrude intent')
|
||||
? 'extrude'
|
||||
: 'sketches need to be closed, or not already extruded'
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||
Extrude
|
||||
</button>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className="text-sm"
|
||||
onClick={() =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||
})
|
||||
}
|
||||
disabled={!state.can('Extrude')}
|
||||
title={
|
||||
state.can('Extrude')
|
||||
? 'extrude'
|
||||
: 'sketches need to be closed, or not already extruded'
|
||||
}
|
||||
icon={{
|
||||
icon: 'extrude',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Extrude
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
</span>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className={
|
||||
styles.toolbarWrapper + state.matches('Sketch') ? ' sketch' : ''
|
||||
}
|
||||
>
|
||||
<div className={styles.toolbar}>
|
||||
<span className={styles.toolbarCap + ' ' + styles.label}>
|
||||
{state.matches('Sketch') ? '2D' : '3D'}
|
||||
</span>
|
||||
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
<Popover.Button
|
||||
className={styles.toolbarCap + ' ' + styles.popoverToggle}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-out duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10 dark:bg-chalkboard-100 relative">
|
||||
<menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap bg-chalkboard-10 dark:bg-chalkboard-100 border-solid border border-energy-10 dark:border-chalkboard-90 border-r-0">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
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"
|
||||
>
|
||||
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 translate-y-1 scale-95"
|
||||
enterTo="opacity-100 translate-y-0 scale-100"
|
||||
leave="transition ease-out duration-75"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-2"
|
||||
>
|
||||
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
|
||||
<section className="flex justify-between items-center">
|
||||
<p
|
||||
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
||||
>
|
||||
You're in {state.matches('Sketch') ? '2D' : '3D'}
|
||||
</p>
|
||||
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
||||
</Popover.Button>
|
||||
</section>
|
||||
<section>
|
||||
<ToolbarButtons className="flex-wrap" />
|
||||
</section>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -39,16 +39,16 @@ type ActionButtonProps =
|
||||
| ActionButtonAsElement
|
||||
|
||||
export const ActionButton = (props: ActionButtonProps) => {
|
||||
const classNames = `group mono text-base flex items-center gap-2 rounded-sm border border-chalkboard-40 dark:border-chalkboard-60 hover:border-liquid-40 dark:hover:bg-chalkboard-90 p-[3px] text-chalkboard-110 dark:text-chalkboard-10 hover:text-chalkboard-110 hover:dark:text-chalkboard-10 ${
|
||||
const classNames = `action-button m-0 group mono text-sm flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 p-[3px] text-chalkboard-100 dark:text-chalkboard-10 ${
|
||||
props.icon ? 'pr-2' : 'px-2'
|
||||
} ${props.className || ''}`
|
||||
} ${props.className ? props.className : ''}`
|
||||
|
||||
switch (props.Element) {
|
||||
case 'button': {
|
||||
// Note we have to destructure 'className' and 'Element' out of props
|
||||
// because we don't want to pass them to the button element;
|
||||
// the same is true for the other cases below.
|
||||
const { Element, icon, children, className, ...rest } = props
|
||||
const { Element, icon, children, className: _className, ...rest } = props
|
||||
return (
|
||||
<button className={classNames} {...rest}>
|
||||
{props.icon && <ActionIcon {...icon} />}
|
||||
@ -57,7 +57,14 @@ export const ActionButton = (props: ActionButtonProps) => {
|
||||
)
|
||||
}
|
||||
case 'link': {
|
||||
const { Element, to, icon, children, className, ...rest } = props
|
||||
const {
|
||||
Element,
|
||||
to,
|
||||
icon,
|
||||
children,
|
||||
className: _className,
|
||||
...rest
|
||||
} = props
|
||||
return (
|
||||
<Link to={to || paths.INDEX} className={classNames} {...rest}>
|
||||
{icon && <ActionIcon {...icon} />}
|
||||
@ -66,7 +73,14 @@ export const ActionButton = (props: ActionButtonProps) => {
|
||||
)
|
||||
}
|
||||
case 'externalLink': {
|
||||
const { Element, to, icon, children, className, ...rest } = props
|
||||
const {
|
||||
Element,
|
||||
to,
|
||||
icon,
|
||||
children,
|
||||
className: _className,
|
||||
...rest
|
||||
} = props
|
||||
return (
|
||||
<Link
|
||||
to={to || paths.INDEX}
|
||||
@ -80,7 +94,7 @@ export const ActionButton = (props: ActionButtonProps) => {
|
||||
)
|
||||
}
|
||||
default: {
|
||||
const { Element, icon, children, className, ...rest } = props
|
||||
const { Element, icon, children, className: _className, ...rest } = props
|
||||
if (!Element) throw new Error('Element is required')
|
||||
|
||||
return (
|
||||
|
@ -7,10 +7,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { CustomIcon, CustomIconName } from './CustomIcon'
|
||||
|
||||
const iconSizes = {
|
||||
sm: 12,
|
||||
md: 14.4,
|
||||
lg: 20,
|
||||
xl: 28,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 20,
|
||||
lg: 24,
|
||||
}
|
||||
|
||||
export interface ActionIconProps extends React.PropsWithChildren {
|
||||
@ -30,20 +30,14 @@ export const ActionIcon = ({
|
||||
children,
|
||||
}: ActionIconProps) => {
|
||||
// By default, we reverse the icon color and background color in dark mode
|
||||
const computedIconClassName =
|
||||
iconClassName ||
|
||||
`text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50`
|
||||
const computedIconClassName = `h-auto dark:text-energy-10 !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
|
||||
|
||||
const computedBgClassName =
|
||||
bgClassName ||
|
||||
`bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10 group-disabled:bg-chalkboard-80 dark:group-disabled:bg-chalkboard-80`
|
||||
const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`p-${
|
||||
size === 'xl' ? '2' : '1'
|
||||
} w-fit inline-grid place-content-center ${className} ` +
|
||||
`w-fit inline-grid place-content-center ${className} ` +
|
||||
computedBgClassName
|
||||
}
|
||||
>
|
||||
|
@ -5,6 +5,9 @@ import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
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
|
||||
@ -20,13 +23,15 @@ export const AppHeader = ({
|
||||
className = '',
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const platform = usePlatform()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { auth } = useGlobalStateContext()
|
||||
const user = auth?.context?.user
|
||||
|
||||
return (
|
||||
<header
|
||||
className={
|
||||
(showToolbar ? 'w-full grid ' : 'flex justify-between ') +
|
||||
'w-full grid ' +
|
||||
styles.header +
|
||||
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
|
||||
className
|
||||
@ -38,18 +43,31 @@ export const AppHeader = ({
|
||||
file={project?.file}
|
||||
/>
|
||||
{/* Toolbar if the context deems it */}
|
||||
{showToolbar && (
|
||||
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||
<div className="flex-grow flex justify-center max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||
{showToolbar ? (
|
||||
<Toolbar />
|
||||
</div>
|
||||
)}
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || (
|
||||
<div className="flex items-center gap-1 ml-auto">
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<ActionButton
|
||||
Element="button"
|
||||
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">
|
||||
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||
</kbd>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-auto">
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || (
|
||||
<>
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
.button {
|
||||
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
|
||||
@apply ui-active:bg-energy-10/50 ui-active:text-inherit;
|
||||
@apply transition-colors ease-out;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
@apply text-chalkboard-30;
|
||||
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
|
||||
@apply ui-active:bg-chalkboard-80 ui-active:text-energy-10;
|
||||
}
|
||||
|
||||
.button small {
|
||||
|
@ -30,8 +30,10 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
<Menu.Button className="p-0 border-none relative">
|
||||
<ActionIcon
|
||||
icon={faEllipsis}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
bgClassName={
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-energy-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded-sm'
|
||||
}
|
||||
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
|
||||
/>
|
||||
|
@ -24,16 +24,17 @@ export const PanelHeader = ({
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<summary className={styles.header}>
|
||||
<div className="flex gap-2 align-center flex-1">
|
||||
<div className="flex gap-2 items-center flex-1">
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
bgClassName={
|
||||
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
||||
'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={
|
||||
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
||||
(iconClassNames?.icon || '')
|
||||
'group-open:text-energy-10 ' + (iconClassNames?.icon || '')
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
@ -59,7 +60,9 @@ export const CollapsiblePanel = ({
|
||||
<details
|
||||
{...props}
|
||||
data-testid={detailsTestId}
|
||||
className={styles.panel + ' group ' + (className || '')}
|
||||
className={
|
||||
styles.panel + ' pointer-events-auto group ' + (className || '')
|
||||
}
|
||||
>
|
||||
<PanelHeader
|
||||
title={title}
|
||||
|
@ -1,290 +0,0 @@
|
||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
||||
import {
|
||||
Dispatch,
|
||||
Fragment,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import Fuse from 'fuse.js'
|
||||
import { Command, SubCommand } from '../lib/commands'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
|
||||
export type SortedCommand = {
|
||||
item: Partial<Command | SubCommand> & { name: string }
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const addCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
||||
}
|
||||
const removeCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) =>
|
||||
prevCommands.filter((command) => !newCommands.includes(command))
|
||||
)
|
||||
}
|
||||
|
||||
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<SortedCommand | null>(
|
||||
null
|
||||
)
|
||||
// keep track of the current subcommand index
|
||||
const [subCommandIndex, setSubCommandIndex] = useState<number>()
|
||||
const [subCommandData, setSubCommandData] = useState<{
|
||||
[key: string]: string
|
||||
}>({})
|
||||
|
||||
// if the subcommand index is null, we're not in a subcommand
|
||||
const inSubCommand =
|
||||
selectedCommand &&
|
||||
'meta' in selectedCommand.item &&
|
||||
selectedCommand.item.meta?.args !== undefined &&
|
||||
subCommandIndex !== undefined
|
||||
const currentSubCommand =
|
||||
inSubCommand && 'meta' in selectedCommand.item
|
||||
? selectedCommand.item.meta?.args[subCommandIndex]
|
||||
: undefined
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const availableCommands =
|
||||
inSubCommand && currentSubCommand
|
||||
? currentSubCommand.type === 'string'
|
||||
? query
|
||||
? [{ name: query }]
|
||||
: currentSubCommand.options
|
||||
: currentSubCommand.options
|
||||
: commands
|
||||
|
||||
const fuse = new Fuse(availableCommands || [], {
|
||||
keys: ['name', 'description'],
|
||||
})
|
||||
|
||||
const filteredCommands = query
|
||||
? fuse.search(query)
|
||||
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
|
||||
|
||||
function clearState() {
|
||||
setQuery('')
|
||||
setCommandBarOpen(false)
|
||||
setSelectedCommand(null)
|
||||
setSubCommandIndex(undefined)
|
||||
setSubCommandData({})
|
||||
}
|
||||
|
||||
function handleCommandSelection(entry: SortedCommand) {
|
||||
// If we have subcommands and have not yet gathered all the
|
||||
// data required from them, set the selected command to the
|
||||
// current command and increment the subcommand index
|
||||
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
|
||||
setSelectedCommand(entry)
|
||||
setSubCommandIndex(0)
|
||||
setQuery('')
|
||||
return
|
||||
}
|
||||
|
||||
const { item } = entry
|
||||
// If we have just selected a command with no subcommands, run it
|
||||
const isCommandWithoutSubcommands =
|
||||
'callback' in item && !('meta' in item && item.meta)
|
||||
if (isCommandWithoutSubcommands) {
|
||||
if (item.callback === undefined) return
|
||||
item.callback()
|
||||
setCommandBarOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have subcommands and have not yet gathered all the
|
||||
// data required from them, set the selected command to the
|
||||
// current command and increment the subcommand index
|
||||
if (
|
||||
selectedCommand &&
|
||||
subCommandIndex !== undefined &&
|
||||
'meta' in selectedCommand.item
|
||||
) {
|
||||
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
|
||||
|
||||
if (subCommand) {
|
||||
const newSubCommandData = {
|
||||
...subCommandData,
|
||||
[subCommand.name]: item.name,
|
||||
}
|
||||
const newSubCommandIndex = subCommandIndex + 1
|
||||
|
||||
// If we have subcommands and have gathered all the data required
|
||||
// from them, run the command with the gathered data
|
||||
if (
|
||||
selectedCommand.item.callback &&
|
||||
selectedCommand.item.meta?.args.length === newSubCommandIndex
|
||||
) {
|
||||
selectedCommand.item.callback(newSubCommandData)
|
||||
setCommandBarOpen(false)
|
||||
} else {
|
||||
// Otherwise, set the subcommand data and increment the subcommand index
|
||||
setSubCommandData(newSubCommandData)
|
||||
setSubCommandIndex(newSubCommandIndex)
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayValue(command: Command) {
|
||||
if (command.meta?.displayValue === undefined || !command.meta.args)
|
||||
return command.name
|
||||
return command.meta?.displayValue(
|
||||
command.meta.args.map((c) =>
|
||||
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={
|
||||
commandBarOpen &&
|
||||
availableCommands?.length !== undefined &&
|
||||
availableCommands.length > 0
|
||||
}
|
||||
as={Fragment}
|
||||
afterLeave={() => clearState()}
|
||||
>
|
||||
<Dialog
|
||||
onClose={() => {
|
||||
setCommandBarOpen(false)
|
||||
clearState()
|
||||
}}
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
<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"
|
||||
as={Fragment}
|
||||
>
|
||||
<Combobox
|
||||
value={selectedCommand}
|
||||
onChange={handleCommandSelection}
|
||||
className="relative w-full max-w-xl p-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||
as="div"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
||||
<div>
|
||||
{inSubCommand && (
|
||||
<p className="text-liquid-70 dark:text-liquid-30">
|
||||
{selectedCommand.item &&
|
||||
getDisplayValue(selectedCommand.item as Command)}
|
||||
</p>
|
||||
)}
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="w-full bg-transparent focus:outline-none"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k')
|
||||
setCommandBarOpen(false)
|
||||
if (
|
||||
inSubCommand &&
|
||||
event.key === 'Backspace' &&
|
||||
!event.currentTarget.value
|
||||
) {
|
||||
setSubCommandIndex(subCommandIndex - 1)
|
||||
setSelectedCommand(null)
|
||||
}
|
||||
}}
|
||||
displayValue={(command: SortedCommand) =>
|
||||
command !== null ? command.item.name : ''
|
||||
}
|
||||
placeholder={
|
||||
inSubCommand
|
||||
? `Enter <${currentSubCommand?.name}>`
|
||||
: 'Search for a command'
|
||||
}
|
||||
value={query}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Combobox.Options static className="overflow-y-auto max-h-96">
|
||||
{filteredCommands?.map((commandResult) => (
|
||||
<Combobox.Option
|
||||
key={commandResult.item.name}
|
||||
value={commandResult}
|
||||
className="px-2 py-1 my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90"
|
||||
>
|
||||
<p>{commandResult.item.name}</p>
|
||||
{(commandResult.item as SubCommand).description && (
|
||||
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
||||
{(commandResult.item as SubCommand).description}
|
||||
</p>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommandBarProvider
|
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
@ -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
@ -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
@ -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
|
194
src/components/CommandBar/CommandBarHeader.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
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 || {})
|
||||
.filter(([argName, _]) =>
|
||||
selectedCommand?.args
|
||||
? selectedCommand?.args[argName]?.required ||
|
||||
(argName in argumentsToSubmit && argumentsToSubmit[argName])
|
||||
: false
|
||||
)
|
||||
.map(([argName, arg], i) => (
|
||||
<div className="relative group" key={argName}>
|
||||
<button
|
||||
disabled={!isReviewing && currentArgument?.name === argName}
|
||||
onClick={() => {
|
||||
commandBarSend({
|
||||
type: isReviewing
|
||||
? 'Edit argument'
|
||||
: 'Change current argument',
|
||||
data: { arg: { ...arg, name: 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>
|
||||
{!arg.required && (
|
||||
<button
|
||||
onClick={() => {
|
||||
commandBarSend({
|
||||
type: 'Remove argument',
|
||||
data: { [argName]: { ...arg, name: argName } },
|
||||
})
|
||||
}}
|
||||
className="invisible group-hover:visible absolute top-0 right-0 -translate-y-1/2 !p-0 flex items-center justify-center rounded-sm border-none bg-none"
|
||||
>
|
||||
<CustomIcon
|
||||
name="close"
|
||||
className="w-4 h-4 bg-destroy-80 dark:bg-destroy-30 hover:bg-destroy-70 dark:hover:bg-destroy-0 text-destroy-10 dark:text-destroy-80"
|
||||
/>
|
||||
<span className="sr-only">Remove argument</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</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
|
125
src/components/CommandBar/CommandBarReview.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import CommandBarHeader from './CommandBarHeader'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
|
||||
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const {
|
||||
context: { argumentsToSubmit, selectedCommand },
|
||||
} = commandBarState
|
||||
const optionalArgsNotAdded = Object.entries(
|
||||
selectedCommand?.args || {}
|
||||
).filter(
|
||||
([key, val]) =>
|
||||
selectedCommand?.args &&
|
||||
!selectedCommand.args[key].required &&
|
||||
!argumentsToSubmit[key]
|
||||
)
|
||||
|
||||
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 py-1">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>
|
||||
{optionalArgsNotAdded.length > 0 && (
|
||||
<>
|
||||
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
|
||||
<div className="flex flex-wrap px-4 gap-2 items-center">
|
||||
{optionalArgsNotAdded.map(([key, _]) => {
|
||||
const arg = selectedCommand?.args
|
||||
? selectedCommand?.args[key]
|
||||
: undefined
|
||||
if (!arg) return null
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
Element="button"
|
||||
key={key}
|
||||
className="text-xs [&:not(:hover)]:border-transparent gap-0.5"
|
||||
onClick={() => {
|
||||
commandBarSend({
|
||||
type: 'Edit argument',
|
||||
data: { arg: { ...arg, name: key } },
|
||||
})
|
||||
}}
|
||||
icon={{
|
||||
icon: 'plus',
|
||||
bgClassName: '!bg-transparent',
|
||||
iconClassName:
|
||||
'text-chalkboard-10 dark:text-chalkboard-100',
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</ActionButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CommandBarHeader>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommandBarReview
|
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
@ -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
|
@ -1,14 +1,23 @@
|
||||
export type CustomIconName =
|
||||
| 'createFile'
|
||||
| 'createFolder'
|
||||
| 'arrowDown'
|
||||
| 'arrowLeft'
|
||||
| 'arrowRight'
|
||||
| 'arrowUp'
|
||||
| 'checkmark'
|
||||
| 'close'
|
||||
| 'equal'
|
||||
| 'exit'
|
||||
| 'extrude'
|
||||
| 'file'
|
||||
| 'filePlus'
|
||||
| 'folder'
|
||||
| 'folderPlus'
|
||||
| 'gear'
|
||||
| 'horizontal'
|
||||
| 'line'
|
||||
| 'move'
|
||||
| 'parallel'
|
||||
| 'plus'
|
||||
| 'search'
|
||||
| 'sketch'
|
||||
| 'vertical'
|
||||
|
||||
@ -19,7 +28,7 @@ export const CustomIcon = ({
|
||||
name: CustomIconName
|
||||
} & React.SVGProps<SVGSVGElement>) => {
|
||||
switch (name) {
|
||||
case 'createFile':
|
||||
case 'arrowDown':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
@ -30,12 +39,12 @@ export const CustomIcon = ({
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
|
||||
d="M10 17.7071L9.64648 17.3535L6.14648 13.8535L6.85359 13.1464L9.50004 15.7929V2.99997H10.5V15.7929L13.1465 13.1464L13.8536 13.8535L10.3536 17.3535L10 17.7071Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'createFolder':
|
||||
case 'arrowLeft':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
@ -46,7 +55,71 @@ export const CustomIcon = ({
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
||||
d="M2.29291 10L2.64646 9.64645L6.14646 6.14645L6.85357 6.85356L4.20712 9.50001L17 9.50001V10.5L4.20712 10.5L6.85357 13.1465L6.14646 13.8536L2.64646 10.3536L2.29291 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'arrowRight':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.7071 10L17.3536 10.3536L13.8536 13.8536L13.1464 13.1465L15.7929 10.5H3V9.50001H15.7929L13.1464 6.85356L13.8536 6.14645L17.3536 9.64645L17.7071 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'arrowUp':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 2.29288L10.3536 2.64643L13.8536 6.14643L13.1465 6.85354L10.5 4.20709V17H9.50004V4.20709L6.85359 6.85354L6.14648 6.14643L9.64648 2.64643L10 2.29288Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</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
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.2929 10L6.46448 7.17158L7.17158 6.46448L10 9.2929L12.8284 6.46448L13.5355 7.17158L10.7071 10L13.5355 12.8284L12.8284 13.5355L10 10.7071L7.17158 13.5355L6.46448 12.8284L9.2929 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@ -65,21 +138,6 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'exit':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17 10L3 10M3 10L6.5 6.5M3 10L6.5 13.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'extrude':
|
||||
return (
|
||||
<svg
|
||||
@ -105,8 +163,74 @@ export const CustomIcon = ({
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5"
|
||||
stroke="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V16.5V17H15.5H4.5H4V16.5V3.5V3ZM5 4V16H15V8.50001H11H10.5V8.00001V4H5ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'filePlus':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'folder':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V16V16.5H16H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM15.5 8H4.5V15.5H15.5V8Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'folderPlus':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'gear':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.61477 3.0884L5.87402 4.67077L6.50004 5.75505L5.25004 7.92011H4.0047V11.07H5.25004L6.50004 13.2351L5.86973 14.3268L8.62776 15.9191L9.24503 14.85H11.745L12.3647 15.9234L15.1416 14.3202L14.5151 13.2351L15.7651 11.07H16.9951V7.92011H15.7651L14.5151 5.75505L15.1373 4.67741L12.3778 3.08423L11.7451 4.18012H9.24508L8.61477 3.0884ZM10.4999 13C12.4329 13 13.9999 11.433 13.9999 9.50003C13.9999 7.56703 12.4329 6.00003 10.4999 6.00003C8.56687 6.00003 6.99986 7.56703 6.99986 9.50003C6.99986 11.433 8.56687 13 10.4999 13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@ -174,6 +298,38 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'plus':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'search':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.016 9.00482C14.016 10.662 12.6731 12.0048 11.0172 12.0048C9.3613 12.0048 8.01841 10.662 8.01841 9.00482C8.01841 7.34768 9.3613 6.00482 11.0172 6.00482C12.6731 6.00482 14.016 7.34768 14.016 9.00482ZM15.016 9.00482C15.016 11.214 13.2257 13.0048 11.0172 13.0048C10.082 13.0048 9.22178 12.6837 8.54074 12.1456L5.6912 14.9952L4.98409 14.2881L7.83921 11.433C7.32431 10.7597 7.01841 9.91799 7.01841 9.00482C7.01841 6.79568 8.80873 5.00482 11.0172 5.00482C13.2257 5.00482 15.016 6.79568 15.016 9.00482Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'sketch':
|
||||
return (
|
||||
<svg
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useStore } from '../useStore'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const DownloadAppBanner = () => {
|
||||
const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({
|
||||
@ -11,44 +10,48 @@ const DownloadAppBanner = () => {
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="fixed inset-0 top-auto z-50 bg-warn-20 text-warn-80 px-8 py-4"
|
||||
className="fixed inset-0 z-50"
|
||||
open={!isBannerDismissed}
|
||||
onClose={() => ({})}
|
||||
>
|
||||
<Dialog.Panel className="max-w-3xl mx-auto">
|
||||
<div className="flex gap-2 justify-between items-start">
|
||||
<h2 className="text-xl font-bold mb-4">
|
||||
KittyCAD Modeling App is better as a desktop app!
|
||||
</h2>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => setBannerDismissed(true)}
|
||||
icon={{
|
||||
icon: faX,
|
||||
bgClassName:
|
||||
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
||||
iconClassName:
|
||||
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
|
||||
}}
|
||||
className="!p-0 !bg-transparent !border-transparent"
|
||||
/>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-100/50" />
|
||||
<Dialog.Panel className="absolute inset-0 top-auto bg-warn-20 text-warn-80 px-8 py-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="flex gap-2 justify-between items-start">
|
||||
<h2 className="text-xl font-bold mb-4">
|
||||
KittyCAD Modeling App is better as a desktop app!
|
||||
</h2>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => setBannerDismissed(true)}
|
||||
icon={{
|
||||
icon: 'close',
|
||||
className: 'p-1',
|
||||
bgClassName:
|
||||
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
||||
iconClassName:
|
||||
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
|
||||
}}
|
||||
className="!p-0 !bg-transparent !border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
The browser version of the app only saves your data temporarily in{' '}
|
||||
<code className="text-base inline-block px-0.5 bg-warn-30/50 rounded">
|
||||
localStorage
|
||||
</code>
|
||||
, and isn't backed up anywhere! Visit{' '}
|
||||
<a
|
||||
href="https://kittycad.io/modeling-app/download"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
|
||||
>
|
||||
our website
|
||||
</a>{' '}
|
||||
to download the app for the best experience.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
The browser version of the app only saves your data temporarily in{' '}
|
||||
<code className="text-base inline-block px-0.5 bg-warn-30/50 rounded">
|
||||
localStorage
|
||||
</code>
|
||||
, and isn't backed up anywhere! Visit{' '}
|
||||
<a
|
||||
href="https://kittycad.io/modeling-app/download"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
|
||||
>
|
||||
our website
|
||||
</a>{' '}
|
||||
to download the app for the best experience.
|
||||
</p>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)
|
||||
|
@ -118,6 +118,8 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: faFileExport,
|
||||
className: 'p-1',
|
||||
size: 'sm',
|
||||
iconClassName: className?.icon,
|
||||
bgClassName: className?.bg,
|
||||
}}
|
||||
@ -212,6 +214,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
onClick={closeModal}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
className: 'p-1',
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
@ -223,7 +226,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
icon={{ icon: faFileExport }}
|
||||
icon={{ icon: faFileExport, className: 'p-1' }}
|
||||
>
|
||||
Export
|
||||
</ActionButton>
|
||||
|
@ -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
|
||||
|
@ -325,16 +325,17 @@ export const FileTree = ({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-30/50 dark:bg-chalkboard-70/50">
|
||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'createFile',
|
||||
icon: 'filePlus',
|
||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
bgClassName:
|
||||
'bg-chalkboard-20/50 hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
}}
|
||||
className="!p-0 border-none bg-transparent !outline-none"
|
||||
className="!p-0 bg-transparent !outline-none"
|
||||
onClick={createFile}
|
||||
>
|
||||
<Tooltip position="inlineStart" delay={750}>
|
||||
@ -345,11 +346,12 @@ export const FileTree = ({
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'createFolder',
|
||||
icon: 'folderPlus',
|
||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
bgClassName:
|
||||
'bg-chalkboard-20/50 hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
}}
|
||||
className="!p-0 border-none bg-transparent !outline-none"
|
||||
className="!p-0 bg-transparent !outline-none"
|
||||
onClick={createFolder}
|
||||
>
|
||||
<Tooltip position="inlineStart" delay={750}>
|
||||
|
@ -1,19 +1,11 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from '../Router'
|
||||
import {
|
||||
authCommandBarMeta,
|
||||
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,
|
||||
settingsCommandBarMeta,
|
||||
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,11 +73,10 @@ export const GlobalStateProvider = ({
|
||||
})
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'settings',
|
||||
state: settingsState,
|
||||
send: settingsSend,
|
||||
commands,
|
||||
owner: 'settings',
|
||||
commandBarMeta: settingsCommandBarMeta,
|
||||
commandBarConfig: settingsCommandBarConfig,
|
||||
})
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
@ -121,11 +112,10 @@ export const GlobalStateProvider = ({
|
||||
})
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'auth',
|
||||
state: authState,
|
||||
send: authSend,
|
||||
commands,
|
||||
commandBarMeta: authCommandBarMeta,
|
||||
owner: 'auth',
|
||||
commandBarConfig: authCommandBarConfig,
|
||||
})
|
||||
|
||||
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,8 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
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'
|
||||
@ -46,7 +42,7 @@ export const NetworkHealthIndicator = () => {
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className={
|
||||
'p-0 border-none relative ' +
|
||||
'p-0 border-none bg-transparent dark:bg-transparent relative ' +
|
||||
(hasIssues
|
||||
? 'focus-visible:outline-destroy-80'
|
||||
: 'focus-visible:outline-succeed-80')
|
||||
@ -56,15 +52,17 @@ export const NetworkHealthIndicator = () => {
|
||||
<span className="sr-only">Network Health</span>
|
||||
<ActionIcon
|
||||
icon={faWifi}
|
||||
className="p-1"
|
||||
iconClassName={
|
||||
hasIssues
|
||||
? 'text-destroy-80 dark:text-destroy-30'
|
||||
: 'text-succeed-80 dark:text-succeed-30'
|
||||
}
|
||||
bgClassName={
|
||||
hasIssues
|
||||
'bg-transparent dark:bg-transparent ' +
|
||||
(hasIssues
|
||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded')
|
||||
}
|
||||
/>
|
||||
</Popover.Button>
|
||||
@ -75,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}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||
import { type ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ActionButton } from './ActionButton'
|
||||
@ -31,9 +31,11 @@ function ProjectCard({
|
||||
const [numberOfParts, setNumberOfParts] = useState(1)
|
||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||
|
||||
let inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
function handleSave(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||
}
|
||||
|
||||
function getDisplayedTime(date: Date) {
|
||||
@ -52,36 +54,48 @@ function ProjectCard({
|
||||
setNumberOfParts(kclFileCount)
|
||||
setNumberOfFolders(kclDirCount)
|
||||
}
|
||||
getNumberOfParts()
|
||||
void getNumberOfParts()
|
||||
}, [project.path])
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [inputRef])
|
||||
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-90 hover:border-chalkboard-30 dark:hover:border-chalkboard-80"
|
||||
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-90 hover:border-energy-10 dark:hover:border-chalkboard-70 hover:bg-energy-10/20 dark:hover:bg-chalkboard-90"
|
||||
>
|
||||
{isEditing ? (
|
||||
<form onSubmit={handleSave} className="flex gap-2 items-center">
|
||||
<input
|
||||
className="dark:bg-chalkboard-80 dark:border-chalkboard-40 min-w-0 p-1"
|
||||
className="dark:bg-chalkboard-80 dark:border-chalkboard-40 min-w-0 p-1 selection:bg-energy-10/20 focus:outline-none"
|
||||
type="text"
|
||||
id="newProjectName"
|
||||
name="newProjectName"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
defaultValue={project.name}
|
||||
autoFocus={true}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<div className="flex gap-1 items-center">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
icon={{ icon: faCheck, size: 'sm' }}
|
||||
icon={{ icon: faCheck, size: 'sm', className: 'p-1' }}
|
||||
className="!p-0"
|
||||
></ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faX, size: 'sm' }}
|
||||
icon={{
|
||||
icon: faX,
|
||||
size: 'sm',
|
||||
iconClassName: 'dark:!text-chalkboard-20',
|
||||
className: 'p-1',
|
||||
}}
|
||||
className="!p-0"
|
||||
onClick={() => setIsEditing(false)}
|
||||
/>
|
||||
@ -91,8 +105,8 @@ function ProjectCard({
|
||||
<>
|
||||
<div className="p-1 flex flex-col h-full gap-2">
|
||||
<Link
|
||||
className="flex-1 text-liquid-100 after:content-[''] after:absolute after:inset-0"
|
||||
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
|
||||
className="flex-1 text-liquid-100"
|
||||
>
|
||||
{project.name?.replace(FILE_EXT, '')}
|
||||
</Link>
|
||||
@ -106,24 +120,37 @@ function ProjectCard({
|
||||
<span className="text-chalkboard-60 text-xs">
|
||||
Edited {getDisplayedTime(project.entrypointMetadata.modifiedAt)}
|
||||
</span>
|
||||
<div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faPenAlt, size: 'sm' }}
|
||||
onClick={() => setIsEditing(true)}
|
||||
icon={{
|
||||
icon: faPenAlt,
|
||||
className: 'p-1',
|
||||
iconClassName: 'dark:!text-chalkboard-20',
|
||||
size: 'xs',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopPropagation()
|
||||
setIsEditing(true)
|
||||
}}
|
||||
className="!p-0"
|
||||
/>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: faTrashAlt,
|
||||
size: 'sm',
|
||||
bgClassName: 'bg-destroy-80 hover:bg-destroy-70',
|
||||
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',
|
||||
className: 'p-1',
|
||||
size: 'xs',
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName: '!text-destroy-20 dark:!text-destroy-40',
|
||||
}}
|
||||
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||
onClick={() => setIsConfirmingDelete(true)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopPropagation()
|
||||
setIsConfirmingDelete(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -156,8 +183,9 @@ function ProjectCard({
|
||||
icon={{
|
||||
icon: faTrashAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
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',
|
||||
className: 'p-1',
|
||||
size: 'sm',
|
||||
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 = {
|
||||
|
@ -21,7 +21,7 @@ const ProjectSidebarMenu = ({
|
||||
return renderAsLink ? (
|
||||
<Link
|
||||
to={paths.HOME}
|
||||
className="h-9 max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
|
||||
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
|
||||
data-testid="project-sidebar-link"
|
||||
>
|
||||
<img
|
||||
@ -39,7 +39,7 @@ const ProjectSidebarMenu = ({
|
||||
) : (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className="h-9 max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
|
||||
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
|
||||
data-testid="project-sidebar-toggle"
|
||||
>
|
||||
<img
|
||||
@ -82,12 +82,12 @@ const ProjectSidebarMenu = ({
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Panel
|
||||
className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-lg shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-energy-100 dark:border-energy-100/50"
|
||||
className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-md shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-chalkboard-40 dark:border-chalkboard-80"
|
||||
style={{ gridTemplateRows: 'auto 1fr auto' }}
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-10/25 dark:bg-energy-110">
|
||||
<div className="flex items-center gap-4 px-4 py-3">
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
@ -115,19 +115,16 @@ const ProjectSidebarMenu = ({
|
||||
{isTauri() ? (
|
||||
<FileTree
|
||||
file={file}
|
||||
className="overflow-hidden border-0 border-y border-energy-40 dark:border-energy-70"
|
||||
className="overflow-hidden border-0 border-y border-chalkboard-30 dark:border-chalkboard-80"
|
||||
closePanel={close}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 overflow-hidden" />
|
||||
)}
|
||||
<div className="flex flex-col gap-2 p-4 bg-energy-10/25 dark:bg-energy-110">
|
||||
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
|
||||
<ExportButton
|
||||
className={{
|
||||
button:
|
||||
'border-transparent dark:border-transparent hover:border-energy-60',
|
||||
icon: 'text-energy-10 dark:text-energy-120',
|
||||
bg: 'bg-energy-120 dark:bg-energy-10',
|
||||
button: 'border-transparent dark:border-transparent',
|
||||
}}
|
||||
>
|
||||
Export Model
|
||||
@ -138,10 +135,10 @@ const ProjectSidebarMenu = ({
|
||||
to={paths.HOME}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
iconClassName: 'text-energy-10 dark:text-energy-120',
|
||||
bgClassName: 'bg-energy-120 dark:bg-energy-10',
|
||||
className: 'p-1',
|
||||
size: 'sm',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent hover:border-energy-60"
|
||||
className="border-transparent dark:border-transparent hover:bg-energy-10/20 dark:hover:bg-chalkboard-90"
|
||||
>
|
||||
Go to Home
|
||||
</ActionButton>
|
||||
|
@ -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,11 +1,6 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import {
|
||||
faBars,
|
||||
faBug,
|
||||
faGear,
|
||||
faSignOutAlt,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faBars, faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useState } from 'react'
|
||||
@ -43,14 +38,14 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
<Popover className="relative">
|
||||
{user?.image && !imageLoadFailed ? (
|
||||
<Popover.Button
|
||||
className="border-0 rounded-full w-fit min-w-max p-0 focus:outline-none group"
|
||||
className="border-0 rounded-full w-fit min-w-max p-0 group"
|
||||
data-testid="user-sidebar-toggle"
|
||||
>
|
||||
<div className="rounded-full border border-chalkboard-70/50 hover:border-liquid-50 group-focus:border-liquid-50 overflow-hidden">
|
||||
<div className="rounded-full border overflow-hidden">
|
||||
<img
|
||||
src={user?.image || ''}
|
||||
alt={user?.name || ''}
|
||||
className="h-8 w-8"
|
||||
className="h-8 w-8 rounded-full"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setImageLoadFailed(true)}
|
||||
/>
|
||||
@ -87,11 +82,11 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
leaveTo="opacity-0 translate-x-4"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 dark:border-liquid-100/50 shadow-md rounded-l-lg overflow-hidden">
|
||||
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-30 dark:border-chalkboard-80 shadow-md rounded-l-md overflow-hidden">
|
||||
{({ close }) => (
|
||||
<>
|
||||
{user && (
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||
{user.image && !imageLoadFailed && (
|
||||
<div className="rounded-full shadow-inner overflow-hidden">
|
||||
<img
|
||||
@ -105,15 +100,12 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-liquid-10 text-mono"
|
||||
data-testid="username"
|
||||
>
|
||||
<p className="m-0 text-mono" data-testid="username">
|
||||
{displayedName || ''}
|
||||
</p>
|
||||
{displayedName !== user.email && (
|
||||
<p
|
||||
className="m-0 text-liquid-40 text-xs"
|
||||
className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs"
|
||||
data-testid="email"
|
||||
>
|
||||
{user.email}
|
||||
@ -125,8 +117,8 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faGear }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
icon={{ icon: 'gear' }}
|
||||
className="border-transparent dark:border-transparent hover:bg-transparent"
|
||||
onClick={() => {
|
||||
// since /settings is a nested route the sidebar doesn't close
|
||||
// automatically when navigating to it
|
||||
@ -142,16 +134,16 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
<ActionButton
|
||||
Element="externalLink"
|
||||
to="https://github.com/KittyCAD/modeling-app/discussions"
|
||||
icon={{ icon: faGithub }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
icon={{ icon: faGithub, className: 'p-1', size: 'sm' }}
|
||||
className="border-transparent dark:border-transparent"
|
||||
>
|
||||
Request a feature
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="externalLink"
|
||||
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
||||
icon={{ icon: faBug }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
icon={{ icon: faBug, className: 'p-1', size: 'sm' }}
|
||||
className="border-transparent dark:border-transparent"
|
||||
>
|
||||
Report a bug
|
||||
</ActionButton>
|
||||
@ -160,11 +152,13 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
onClick={() => send('Log out')}
|
||||
icon={{
|
||||
icon: faSignOutAlt,
|
||||
className: 'p-1',
|
||||
bgClassName: 'bg-destroy-80',
|
||||
size: 'sm',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
|
||||
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
|
||||
data-testid="user-sidebar-sign-out"
|
||||
>
|
||||
Sign out
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useState } from 'react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useKclContext } from 'lang/KclSinglton'
|
||||
|
||||
export function WasmErrBanner() {
|
||||
@ -26,7 +25,8 @@ export function WasmErrBanner() {
|
||||
Element="button"
|
||||
onClick={() => setBannerDismissed(true)}
|
||||
icon={{
|
||||
icon: faX,
|
||||
icon: 'close',
|
||||
className: 'p-1',
|
||||
bgClassName:
|
||||
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
||||
iconClassName:
|
||||
|
@ -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
@ -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,42 +1,68 @@
|
||||
import { useEffect } from 'react'
|
||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||
import { Command, CommandBarMeta, 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
|
||||
commandBarMeta?: CommandBarMeta
|
||||
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,
|
||||
commandBarMeta,
|
||||
owner,
|
||||
}: UseStateMachineCommandsArgs<T>) {
|
||||
const { addCommands, removeCommands } = useCommandsContext()
|
||||
actor,
|
||||
commandBarConfig,
|
||||
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,
|
||||
commandBarMeta,
|
||||
owner,
|
||||
actor,
|
||||
commandBarConfig,
|
||||
onCancel,
|
||||
})
|
||||
)
|
||||
.filter((c) => c !== null) as Command[]
|
||||
.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,27 +57,43 @@ select {
|
||||
}
|
||||
|
||||
button {
|
||||
@apply border border-chalkboard-100 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 {
|
||||
@apply border-chalkboard-40 bg-energy-10/20;
|
||||
}
|
||||
|
||||
.dark button {
|
||||
@apply border-chalkboard-20 hover:border-chalkboard-10 hover:bg-chalkboard-90;
|
||||
@apply border-chalkboard-70 focus-visible:ring-energy-10/50;
|
||||
}
|
||||
|
||||
.dark button:hover {
|
||||
@apply border-chalkboard-60;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@apply bg-chalkboard-20 text-chalkboard-60 border-chalkboard-20;
|
||||
@apply cursor-not-allowed bg-chalkboard-20 text-chalkboard-60 border-chalkboard-20;
|
||||
}
|
||||
|
||||
.dark button:disabled {
|
||||
@apply bg-chalkboard-90 text-chalkboard-40 border-chalkboard-70;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-liquid-80 hover:text-liquid-70;
|
||||
a:not(.action-button) {
|
||||
@apply text-energy-70 hover:text-energy-60;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
@apply text-liquid-20 hover:text-liquid-10;
|
||||
.dark 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 {
|
||||
|
@ -4,8 +4,6 @@ import reportWebVitals from './reportWebVitals'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { Router } from './Router'
|
||||
import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||
import { inspect } from '@xstate/inspect'
|
||||
import { DEV } from 'env'
|
||||
|
||||
// uncomment for xstate inspector
|
||||
// if (DEV)
|
||||
@ -19,10 +17,20 @@ root.render(
|
||||
<HotkeysProvider>
|
||||
<Router />
|
||||
<Toaster
|
||||
position="bottom-center"
|
||||
position="top-center"
|
||||
toastOptions={{
|
||||
style: {
|
||||
borderRadius: '0.25rem',
|
||||
},
|
||||
className:
|
||||
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10',
|
||||
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
|
||||
success: {
|
||||
iconTheme: {
|
||||
primary: 'oklch(93.31% 0.227 122.3deg)',
|
||||
secondary: 'oklch(24.49% 0.01405 158.7deg)',
|
||||
},
|
||||
duration: 1500,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</HotkeysProvider>
|
||||
|
@ -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()
|
||||
: {
|
||||
|
@ -1216,7 +1216,7 @@ export class EngineCommandManager {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'make_plane',
|
||||
size: 60,
|
||||
size: 100,
|
||||
origin: { x: 0, y: 0, z: 0 },
|
||||
x_axis,
|
||||
y_axis,
|
||||
|
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
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
62
src/lib/commandBarConfigs/modelingCommandConfig.ts
Normal file
@ -0,0 +1,62 @@
|
||||
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
|
||||
makeVariable?: string
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
makeVariable: {
|
||||
inputType: 'string',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
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
@ -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,124 +0,0 @@
|
||||
import { AnyStateMachine, EventFrom, StateFrom } from 'xstate'
|
||||
import { isTauri } from './isTauri'
|
||||
|
||||
type InitialCommandBarMetaArg = {
|
||||
name: string
|
||||
type: 'string' | 'select'
|
||||
description?: string
|
||||
defaultValue?: string
|
||||
options: string | Array<{ name: string }>
|
||||
}
|
||||
|
||||
type Platform = 'both' | 'web' | 'desktop'
|
||||
|
||||
export type CommandBarMeta = {
|
||||
[key: string]:
|
||||
| {
|
||||
displayValue: (args: string[]) => string
|
||||
args: InitialCommandBarMetaArg[]
|
||||
hide?: Platform
|
||||
}
|
||||
| {
|
||||
hide?: Platform
|
||||
}
|
||||
}
|
||||
|
||||
export type Command = {
|
||||
owner: string
|
||||
name: string
|
||||
callback: Function
|
||||
meta?: {
|
||||
displayValue(args: string[]): string | string
|
||||
args: SubCommand[]
|
||||
}
|
||||
}
|
||||
|
||||
export type SubCommand = {
|
||||
name: string
|
||||
type: 'select' | 'string'
|
||||
description?: string
|
||||
options?: Partial<{ name: string }>[]
|
||||
}
|
||||
|
||||
interface CommandBarArgs<T extends AnyStateMachine> {
|
||||
type: EventFrom<T>['type']
|
||||
state: StateFrom<T>
|
||||
commandBarMeta?: CommandBarMeta
|
||||
send: Function
|
||||
owner: string
|
||||
}
|
||||
|
||||
export function createMachineCommand<T extends AnyStateMachine>({
|
||||
type,
|
||||
state,
|
||||
commandBarMeta,
|
||||
send,
|
||||
owner,
|
||||
}: CommandBarArgs<T>): Command | null {
|
||||
const lookedUpMeta = commandBarMeta && commandBarMeta[type]
|
||||
if (lookedUpMeta && '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
|
||||
}
|
||||
let replacedArgs
|
||||
|
||||
if (lookedUpMeta && 'args' in lookedUpMeta) {
|
||||
replacedArgs = lookedUpMeta.args.map((arg) => {
|
||||
const optionsFromContext = state.context[
|
||||
arg.options as keyof typeof state.context
|
||||
] as { name: string }[] | string | undefined
|
||||
const defaultValueFromContext = state.context[
|
||||
arg.defaultValue as keyof typeof state.context
|
||||
] as string | undefined
|
||||
|
||||
const options =
|
||||
arg.options instanceof Array
|
||||
? arg.options.map((o) => ({
|
||||
...o,
|
||||
description:
|
||||
defaultValueFromContext === o.name ? '(current)' : '',
|
||||
}))
|
||||
: !optionsFromContext || typeof optionsFromContext === 'string'
|
||||
? [
|
||||
{
|
||||
name: optionsFromContext,
|
||||
description: arg.description || '',
|
||||
},
|
||||
]
|
||||
: optionsFromContext.map((o) => ({
|
||||
name: o.name || '',
|
||||
description: arg.description || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
...arg,
|
||||
options,
|
||||
}
|
||||
}) as any[]
|
||||
}
|
||||
|
||||
// We have to recreate this object every time,
|
||||
// otherwise we'll have stale state in the CommandBar
|
||||
// after completing our first action
|
||||
const meta = lookedUpMeta
|
||||
? {
|
||||
...lookedUpMeta,
|
||||
args: replacedArgs,
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
name: type,
|
||||
owner,
|
||||
callback: (data: EventFrom<T, typeof type>) => {
|
||||
if (data !== undefined && data !== null) {
|
||||
send(type, { data })
|
||||
} else {
|
||||
send(type)
|
||||
}
|
||||
},
|
||||
meta: meta as any,
|
||||
}
|
||||
}
|
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,7 @@
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCircleDot,
|
||||
faCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
|
||||
@ -13,7 +13,7 @@ export function getSortIcon(currentSort: string, newSort: string) {
|
||||
} else if (currentSort === newSort + DESC) {
|
||||
return faArrowDown
|
||||
}
|
||||
return faCircleDot
|
||||
return faCircle
|
||||
}
|
||||
|
||||
export function getNextSearchParams(currentSort: string, newSort: string) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { createMachine, assign } from 'xstate'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import withBaseURL from '../lib/withBaseURL'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
@ -40,12 +39,6 @@ export type Events =
|
||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||
|
||||
export const authCommandBarMeta: CommandBarMeta = {
|
||||
'Log in': {
|
||||
hide: 'both',
|
||||
},
|
||||
}
|
||||
|
||||
export const authMachine = createMachine<UserContext, Events>(
|
||||
{
|
||||
id: 'Auth',
|
||||
|
447
src/machines/commandBarMachine.ts
Normal file
@ -0,0 +1,447 @@
|
||||
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-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoibilJU1SVnaRFqA6JEIAe4IevIua2BKLhqBptZaa+OmsfB7FIbAkgAGpYJQnAQK+EZkBA-BgJIDwaqgADW5UanlBWvmAJLpYZHljGhCSZOUMxmjK2Q6FJ+4Iny3bujYmyqVkxz3vWmnaSxlkGRxOUNYVvB6WQn4kKggZsJQr4AGa7egkj1fl63Na17mCeuHVbvuhEKTyokuBOWRCpsVhZEsE02LkiUEqqGqcGAJn2U5LkJr8t3Jp5qboQgljTNoUnSpsb0uKFhTuiK6jpPUyi6C6gMdMDoPg2SBDLUh7Wbh24qlNCAovWabj5GFAqlBoGZIqkTh2qTgbk2DZCquqWquRlyGw22CNdQwrJlGkehuq4eYjVyo4+jy3WVg0OKzY+ZNgCDosAKIQC5NMy2ucP3R2mjYTY2gSp6rKgpraja3U4qzDYkLiELnQfm81Xfi1bmZSVZUVTgVW1Wda1NZH0v6nbcudYg0Uo1JJTKE49SyWFegWCFMqYToZg5J4Rv+klyCh+Hlmp4ZIGlTg5WVTVdXJ82rccXQq6GvDWcIFi2x2qCizWC40XlAiLhnskejuO6NGbEHdc1oqTcR9d0fbbtkj7UdJ1JxdKcH8BdNeYjx5bPsORjukqmwiNGgzPUwriiU7hb80euz4UqLX0tfEC4tNTahtrfeWpg7DTAlMeQ8XIOSrDCrsdk4gqjImRD6TCIVg4LV0mAqOwExZqigVLNqw8hKjwepoUo7g4SzE0EFQsGDpTbFcG6AhgUlheHvDgVAEA4CCDmrWEeDtvKiAWBaRWVobQaHtI6aYylzDFGUIpSsCVt5zjrESSAUj6YyOKBaPM1pUj1E9NKdBhQPTTBCq4SsGx9hs2DrGJs35oZ4GMXfBIswmabG9FJZGdRZBFlUqiZxmESjpFkLowBO95z10bB8IxdDpGIxntrZE2QsTSGsBw+xX1tDXkwioM0-JQREJASQ6hHE-FwMRAkqQUUiZ5KkuoIslZ2QuBlFaGiWQEm1OYvUm2WVTLmQ2pGaytkmlj2KGydpthOnJDUEKDYkh3CpBovjSomxRmpSWuA1al8ZkLIYUscEEo1AunsJYd0lghTOG2E4aoeRgniUSQ+IBJszYmUuY7TI4I7n9lUHIdIxEcZRPKFFbI0UMyGySfokO7xm6RgHplIF3laivMZgRNm7NsaIACgpV2SxajYMhDNLwQA */
|
||||
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,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
'Remove argument': [
|
||||
{
|
||||
target: 'Review',
|
||||
cond: 'Is current argument',
|
||||
actions: 'Remove argument',
|
||||
},
|
||||
{
|
||||
target: 'Gathering arguments',
|
||||
internal: true,
|
||||
actions: 'Remove argument',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
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: ['Remove argument'],
|
||||
},
|
||||
|
||||
'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
|
||||
},
|
||||
}),
|
||||
'Remove argument': assign({
|
||||
argumentsToSubmit: (context, event) => {
|
||||
if (event.type !== 'Remove argument') return context.argumentsToSubmit
|
||||
const argName = Object.keys(event.data)[0]
|
||||
const { argumentsToSubmit } = context
|
||||
const newArgumentsToSubmit = { ...argumentsToSubmit }
|
||||
delete newArgumentsToSubmit[argName]
|
||||
return newArgumentsToSubmit
|
||||
},
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
'Command needs review': (context, _) =>
|
||||
context.selectedCommand?.needsReview || false,
|
||||
'Is current argument': (context, event) => {
|
||||
if (event.type !== 'Remove argument') return false
|
||||
const argName = Object.keys(event.data)[0]
|
||||
return argName === context.currentArgument?.name
|
||||
},
|
||||
},
|
||||
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
|
||||
).filter(([argName, _]) =>
|
||||
context.selectedCommand?.args
|
||||
? context.selectedCommand?.args[argName]?.required
|
||||
: false
|
||||
)) {
|
||||
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
@ -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,60 +1,6 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
|
||||
export const homeCommandMeta: CommandBarMeta = {
|
||||
'Create project': {
|
||||
displayValue: (args: string[]) => `Create project "${args[0]}"`,
|
||||
args: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
description: '(default)',
|
||||
options: 'defaultProjectName',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Open project': {
|
||||
displayValue: (args: string[]) => `Open project "${args[0]}"`,
|
||||
args: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'select',
|
||||
options: 'projects',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Delete project': {
|
||||
displayValue: (args: string[]) => `Delete project "${args[0]}"`,
|
||||
args: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'select',
|
||||
options: 'projects',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Rename project': {
|
||||
displayValue: (args: string[]) =>
|
||||
`Rename project "${args[0]}" to "${args[1]}"`,
|
||||
args: [
|
||||
{
|
||||
name: 'oldName',
|
||||
type: 'select',
|
||||
options: 'projects',
|
||||
},
|
||||
{
|
||||
name: 'newName',
|
||||
type: 'string',
|
||||
description: '(default)',
|
||||
options: 'defaultProjectName',
|
||||
},
|
||||
],
|
||||
},
|
||||
assign: {
|
||||
hide: 'both',
|
||||
},
|
||||
}
|
||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||
|
||||
export const homeMachine = createMachine(
|
||||
{
|
||||
@ -192,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'
|
||||
|
@ -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 { CommandBarMeta } 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 settingsCommandBarMeta: CommandBarMeta = {
|
||||
'Set Base Unit': {
|
||||
displayValue: (args: string[]) => 'Set your default base unit',
|
||||
args: [
|
||||
{
|
||||
name: 'baseUnit',
|
||||
type: 'select',
|
||||
defaultValue: 'baseUnit',
|
||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Camera Controls': {
|
||||
displayValue: (args: string[]) => 'Set your camera controls',
|
||||
args: [
|
||||
{
|
||||
name: 'cameraControls',
|
||||
type: 'select',
|
||||
defaultValue: 'cameraControls',
|
||||
options: Object.values(cameraSystems).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Default Directory': {
|
||||
hide: 'both',
|
||||
},
|
||||
'Set Default Project Name': {
|
||||
displayValue: (args: string[]) => 'Set a new default project name',
|
||||
hide: 'web',
|
||||
args: [
|
||||
{
|
||||
name: 'defaultProjectName',
|
||||
type: 'string',
|
||||
description: '(default)',
|
||||
defaultValue: 'defaultProjectName',
|
||||
options: 'defaultProjectName',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Onboarding Status': {
|
||||
hide: 'both',
|
||||
},
|
||||
'Set Text Wrapping': {
|
||||
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
|
||||
args: [
|
||||
{
|
||||
name: 'textWrapping',
|
||||
type: 'select',
|
||||
defaultValue: 'textWrapping',
|
||||
options: [{ name: 'On' }, { name: 'Off' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Theme': {
|
||||
displayValue: (args: string[]) => 'Change the app theme',
|
||||
args: [
|
||||
{
|
||||
name: 'theme',
|
||||
type: 'select',
|
||||
defaultValue: 'theme',
|
||||
options: Object.values(Themes).map((v): { name: string } => ({
|
||||
name: v,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Unit System': {
|
||||
displayValue: (args: string[]) => 'Set your default unit system',
|
||||
args: [
|
||||
{
|
||||
name: 'unitSystem',
|
||||
type: 'select',
|
||||
defaultValue: '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 */
|
||||
@ -126,7 +46,12 @@ export const settingsMachine = createMachine(
|
||||
on: {
|
||||
'Set Base Unit': {
|
||||
actions: [
|
||||
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||
assign({
|
||||
baseUnit: (_, event) => {
|
||||
console.log('event', event)
|
||||
return event.data.baseUnit
|
||||
},
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
],
|
||||
|
@ -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 { homeCommandMeta, 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,
|
||||
commandBarMeta: homeCommandMeta,
|
||||
owner: 'home',
|
||||
commandBarConfig: homeCommandBarConfig,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@ -178,23 +178,24 @@ const Home = () => {
|
||||
<div className="w-full max-w-5xl px-4 mx-auto my-24 overflow-y-auto lg:px-0">
|
||||
<section className="flex justify-between">
|
||||
<h1 className="text-3xl text-bold">Your Projects</h1>
|
||||
<div className="flex">
|
||||
<div className="flex gap-2 items-center">
|
||||
<small>Sort by</small>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className={
|
||||
!sort.includes('name')
|
||||
'text-sm ' +
|
||||
(!sort.includes('name')
|
||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||
: ''
|
||||
: '')
|
||||
}
|
||||
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
|
||||
icon={{
|
||||
icon: getSortIcon(sort, 'name'),
|
||||
bgClassName: !sort?.includes('name')
|
||||
? 'bg-liquid-50 dark:bg-liquid-70'
|
||||
: '',
|
||||
iconClassName: !sort?.includes('name')
|
||||
? 'text-liquid-80 dark:text-liquid-30'
|
||||
className: 'p-1.5',
|
||||
iconClassName: !sort.includes('name')
|
||||
? '!text-chalkboard-40'
|
||||
: '',
|
||||
size: 'sm',
|
||||
}}
|
||||
>
|
||||
Name
|
||||
@ -202,21 +203,19 @@ const Home = () => {
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className={
|
||||
!isSortByModified
|
||||
'text-sm ' +
|
||||
(!isSortByModified
|
||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||
: ''
|
||||
: '')
|
||||
}
|
||||
onClick={() =>
|
||||
setSearchParams(getNextSearchParams(sort, 'modified'))
|
||||
}
|
||||
icon={{
|
||||
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
|
||||
bgClassName: !isSortByModified
|
||||
? 'bg-liquid-50 dark:bg-liquid-70'
|
||||
: '',
|
||||
iconClassName: !isSortByModified
|
||||
? 'text-liquid-80 dark:text-liquid-30'
|
||||
: '',
|
||||
className: 'p-1.5',
|
||||
iconClassName: !isSortByModified ? '!text-chalkboard-40' : '',
|
||||
size: 'sm',
|
||||
}}
|
||||
>
|
||||
Last Modified
|
||||
@ -225,11 +224,15 @@ const Home = () => {
|
||||
</section>
|
||||
<section>
|
||||
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
||||
Are being saved at{' '}
|
||||
<code className="text-liquid-80 dark:text-liquid-30">
|
||||
Loaded from{' '}
|
||||
<span className="text-energy-70 dark:text-energy-40">
|
||||
{defaultDirectory}
|
||||
</code>
|
||||
, which you can change in your <Link to="settings">Settings</Link>.
|
||||
</span>
|
||||
.{' '}
|
||||
<Link to="settings" className="underline underline-offset-2">
|
||||
Edit in settings
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
{state.matches('Reading projects') ? (
|
||||
<Loading>Loading your Projects...</Loading>
|
||||
@ -254,7 +257,7 @@ const Home = () => {
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send('Create project')}
|
||||
icon={{ icon: faPlus }}
|
||||
icon={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
|
||||
data-testid="home-new-file"
|
||||
>
|
||||
New file
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
import { SettingsSection } from 'routes/Settings'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
@ -70,28 +68,11 @@ export default function Units() {
|
||||
</li>
|
||||
</ul>
|
||||
</SettingsSection>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: Streaming
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Streaming"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
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) => ({
|
||||
@ -11,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())
|
||||
}
|
||||
getPlatform()
|
||||
}, [setPlatformName])
|
||||
const platformName = usePlatform()
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
|
||||
@ -31,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.
|
||||
@ -57,28 +47,11 @@ export default function CmdK() {
|
||||
management from the command bar, but we will be powering modeling
|
||||
commands with it soon.
|
||||
</p>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: User Menu
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: User Menu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
import { useBackdropHighlight } from 'hooks/useBackdropHighlight'
|
||||
|
||||
@ -57,28 +55,11 @@ export default function CodeEditor() {
|
||||
<kbd>Shift</kbd> + <kbd>C</kbd>.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: Parametric Modeling
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Parametric Modeling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
|
||||
export default function Export() {
|
||||
@ -44,29 +42,11 @@ export default function Export() {
|
||||
export to almost any CAD software.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
data-testid="onboarding-next"
|
||||
>
|
||||
Next: Sketching
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
next={next}
|
||||
dismiss={dismiss}
|
||||
nextText="Next: Sketching"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { useDismiss } from '.'
|
||||
import { OnboardingButtons, useDismiss } from '.'
|
||||
import { useEffect } from 'react'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
@ -38,28 +36,12 @@ export default function FutureWork() {
|
||||
hardware design with us 💚.
|
||||
</p>
|
||||
<p className="my-4">— The KittyCAD Team</p>
|
||||
<div className="flex justify-between mt-6">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Finish
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={dismiss}
|
||||
nextText="Finish"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
import { useBackdropHighlight } from 'hooks/useBackdropHighlight'
|
||||
|
||||
@ -97,28 +95,11 @@ export default function InteractiveNumbers() {
|
||||
we'd love to hear your ideas for how to make it better.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: Command Bar
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Command Bar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import {
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
OnboardingButtons,
|
||||
onboardingPaths,
|
||||
useDismiss,
|
||||
useNextClick,
|
||||
@ -65,31 +64,15 @@ function OnboardingWithNewFile() {
|
||||
We see you have some of your own code written in this project.
|
||||
Please save it somewhere else before continuing the onboarding.
|
||||
</p>
|
||||
<div className="flex justify-between mt-6">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
kclManager.setCodeAndExecute(bracket)
|
||||
next()
|
||||
}}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Overwrite code and continue
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={() => {
|
||||
kclManager.setCodeAndExecute(bracket)
|
||||
next()
|
||||
}}
|
||||
nextText="Overwrite code and continue"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -103,32 +86,16 @@ function OnboardingWithNewFile() {
|
||||
click the button below.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between mt-6">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
createAndOpenNewProject()
|
||||
kclManager.setCode(bracket, false)
|
||||
dismiss()
|
||||
}}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Make a new project
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={() => {
|
||||
void createAndOpenNewProject()
|
||||
kclManager.setCode(bracket, false)
|
||||
dismiss()
|
||||
}}
|
||||
nextText="Make a new project"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -192,28 +159,12 @@ export default function Introduction() {
|
||||
release as early as possible to get feedback from users like you.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between mt-6">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Get Started
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Camera"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
import { useBackdropHighlight } from 'hooks/useBackdropHighlight'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
@ -57,28 +55,11 @@ export default function ParametricModeling() {
|
||||
on the width of the bracket to meet a set safety factor on line 6.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: Interactive Numbers
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Interactive Numbers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
|
||||
@ -28,28 +26,11 @@ export default function ProjectMenu() {
|
||||
we add support for multi-file assemblies.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: Export
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
next={next}
|
||||
dismiss={dismiss}
|
||||
nextText="Next: Export"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from 'useStore'
|
||||
import { useEffect } from 'react'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
@ -39,29 +37,12 @@ export default function Sketching() {
|
||||
Watch the code pane as you click. Point-and-click interactions are
|
||||
always just modifying and generating code in KittyCAD Modeling App.
|
||||
</p>
|
||||
<div className="flex justify-between mt-6">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
data-testid="onboarding-next"
|
||||
>
|
||||
Next: Future Work
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
next={next}
|
||||
dismiss={dismiss}
|
||||
nextText="Next: Future Work"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
|
||||
export default function Streaming() {
|
||||
@ -38,28 +36,11 @@ export default function Streaming() {
|
||||
and you won't have to worry about the performance of the device.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: Code Editing
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Code Editor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
|
||||
export default function UserMenu() {
|
||||
@ -25,28 +23,11 @@ export default function UserMenu() {
|
||||
change your settings, sign out, or request a feature.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: faArrowRight }}
|
||||
>
|
||||
Next: Project Menu
|
||||
</ActionButton>
|
||||
</div>
|
||||
<OnboardingButtons
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Project Menu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -17,6 +17,7 @@ import Export from './Export'
|
||||
import FutureWork from './FutureWork'
|
||||
import { paths } from 'Router'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
|
||||
export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
|
||||
|
||||
@ -120,6 +121,45 @@ export function useDismiss() {
|
||||
}, [send, navigate, filePath])
|
||||
}
|
||||
|
||||
export function OnboardingButtons({
|
||||
next,
|
||||
nextText,
|
||||
dismiss,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
next: () => void
|
||||
nextText?: string
|
||||
dismiss: () => void
|
||||
className?: string
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div className={'flex justify-between ' + (className ?? '')} {...props}>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={dismiss}
|
||||
icon={{
|
||||
icon: 'close',
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName: 'text-destroy-20 group-hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={next}
|
||||
icon={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }}
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="onboarding-next"
|
||||
>
|
||||
{nextText ?? 'Next'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Onboarding = () => {
|
||||
const dismiss = useDismiss()
|
||||
useHotkeys('esc', dismiss)
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {
|
||||
faArrowRotateBack,
|
||||
faFolder,
|
||||
faXmark,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faArrowRotateBack, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../components/ActionButton'
|
||||
import { AppHeader } from '../components/AppHeader'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
@ -185,14 +181,9 @@ export const Settings = () => {
|
||||
/>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className="bg-chalkboard-100 dark:bg-chalkboard-90 hover:bg-chalkboard-90 dark:hover:bg-chalkboard-80 !text-chalkboard-10 border-chalkboard-100 hover:border-chalkboard-70"
|
||||
onClick={handleDirectorySelection}
|
||||
icon={{
|
||||
icon: faFolder,
|
||||
bgClassName:
|
||||
'bg-liquid-20 group-hover:bg-liquid-10 hover:bg-liquid-10',
|
||||
iconClassName:
|
||||
'text-liquid-90 group-hover:text-liquid-90 hover:text-liquid-90',
|
||||
icon: 'folder',
|
||||
}}
|
||||
>
|
||||
Choose a folder
|
||||
@ -305,7 +296,7 @@ export const Settings = () => {
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={restartOnboarding}
|
||||
icon={{ icon: faArrowRotateBack }}
|
||||
icon={{ icon: faArrowRotateBack, size: 'sm', className: 'p-1' }}
|
||||
>
|
||||
Replay Onboarding
|
||||
</ActionButton>
|
||||
|
@ -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
|
||||
|
14
src/wasm-lib/Cargo.lock
generated
@ -677,9 +677,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "databake"
|
||||
@ -822,16 +822,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execution-plan"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"kittycad",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "expectorate"
|
||||
version = "1.1.0"
|
||||
|
@ -54,7 +54,6 @@ members = [
|
||||
"derive-docs",
|
||||
"kcl",
|
||||
"kcl-macros",
|
||||
"execution-plan",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "execution-plan"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
rust-version = "1.73"
|
||||
description = "A DSL for composing KittyCAD API queries"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.5"
|
||||
kittycad = { workspace = true, features = ["requests"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|