Compare commits
14 Commits
achalmers/
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
c530a19719 | |||
c5e1119752 | |||
3e22e3a115 | |||
5b8ad29e7d | |||
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:
|
||||
|
@ -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,46 @@ 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
|
||||
await 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(Themes.System)
|
||||
await expect(themeInput).toBeVisible()
|
||||
await expect(themeInput).toBeFocused()
|
||||
// Select dark theme
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
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}`)
|
||||
})
|
||||
|
@ -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: 115 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 55 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",
|
||||
@ -134,7 +134,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 |
@ -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": {
|
||||
|
@ -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;
|
||||
}
|
298
src/Toolbar.tsx
@ -1,22 +1,16 @@
|
||||
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'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const { setCommandBarOpen } = 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 +29,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 +149,66 @@ 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={() => send('extrude intent')}
|
||||
disabled={!state.can('extrude intent')}
|
||||
title={
|
||||
state.can('extrude intent')
|
||||
? '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={() => setCommandBarOpen(true)}
|
||||
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>
|
||||
⌘K
|
||||
</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,8 @@ 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'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
@ -20,13 +22,14 @@ export const AppHeader = ({
|
||||
className = '',
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const { setCommandBarOpen } = 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 +41,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={() => setCommandBarOpen(true)}
|
||||
className="text-sm self-center flex items-center w-fit gap-3"
|
||||
>
|
||||
Command Palette{' '}
|
||||
<kbd className="bg-energy-10/50 dark:bg-chalkboard-100 dark:text-energy-10 inline-block px-1 py-0.5 border-energy-10 dark:border-chalkboard-90">
|
||||
⌘K
|
||||
</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 align-center 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 group-open:border 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}
|
||||
|
@ -4,18 +4,22 @@ import {
|
||||
Fragment,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
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 {
|
||||
Command,
|
||||
CommandArgument,
|
||||
CommandArgumentOption,
|
||||
} from '../lib/commands'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
export type SortedCommand = {
|
||||
item: Partial<Command | SubCommand> & { name: string }
|
||||
}
|
||||
type ComboboxOption = Command | CommandArgumentOption
|
||||
type CommandArgumentData = [string, any]
|
||||
|
||||
export const CommandsContext = createContext(
|
||||
{} as {
|
||||
@ -35,12 +39,24 @@ export const CommandBarProvider = ({
|
||||
const [commands, internalSetCommands] = useState([] as Command[])
|
||||
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
||||
|
||||
function sortCommands(a: Command, b: Command) {
|
||||
if (b.owner === 'auth') return -1
|
||||
if (a.owner === 'auth') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
useEffect(() => console.log('commands updated', commands), [commands])
|
||||
|
||||
const addCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
||||
internalSetCommands((prevCommands) =>
|
||||
[...newCommands, ...prevCommands].sort(sortCommands)
|
||||
)
|
||||
}
|
||||
const removeCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) =>
|
||||
prevCommands.filter((command) => !newCommands.includes(command))
|
||||
prevCommands
|
||||
.filter((command) => !newCommands.includes(command))
|
||||
.sort(sortCommands)
|
||||
)
|
||||
}
|
||||
|
||||
@ -63,152 +79,117 @@ export const CommandBarProvider = ({
|
||||
const CommandBar = () => {
|
||||
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
||||
useHotkeys(['meta+k', 'meta+/'], () => {
|
||||
if (commands.length === 0) return
|
||||
if (commands?.length === 0) return
|
||||
setCommandBarOpen(!commandBarOpen)
|
||||
})
|
||||
|
||||
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
|
||||
null
|
||||
const [selectedCommand, setSelectedCommand] = useState<Command>()
|
||||
const [commandArguments, setCommandArguments] = useState<CommandArgument[]>(
|
||||
[]
|
||||
)
|
||||
// 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))
|
||||
const [commandArgumentData, setCommandArgumentData] = useState<
|
||||
CommandArgumentData[]
|
||||
>([])
|
||||
const [commandArgumentIndex, setCommandArgumentIndex] = useState<number>(0)
|
||||
|
||||
function clearState() {
|
||||
setQuery('')
|
||||
setCommandBarOpen(false)
|
||||
setSelectedCommand(null)
|
||||
setSubCommandIndex(undefined)
|
||||
setSubCommandData({})
|
||||
setSelectedCommand(undefined)
|
||||
setCommandArguments([])
|
||||
setCommandArgumentData([])
|
||||
setCommandArgumentIndex(0)
|
||||
}
|
||||
|
||||
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
|
||||
function selectCommand(command: Command) {
|
||||
console.log('selecting command', command)
|
||||
if (!('args' in command && command.args?.length)) {
|
||||
submitCommand({ command })
|
||||
} else {
|
||||
setCommandArguments(command.args)
|
||||
setSelectedCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
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 stepBack() {
|
||||
if (!selectedCommand) {
|
||||
clearState()
|
||||
} else {
|
||||
if (commandArgumentIndex === 0) {
|
||||
setSelectedCommand(undefined)
|
||||
} else {
|
||||
setCommandArgumentIndex((prevIndex) => Math.max(0, prevIndex - 1))
|
||||
}
|
||||
if (commandArgumentData.length > 0) {
|
||||
setCommandArgumentData((prevData) => prevData.slice(0, -1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function 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}>`
|
||||
function appendCommandArgumentData(data: { name: any }) {
|
||||
const transformedData = [
|
||||
commandArguments[commandArgumentIndex].name,
|
||||
data.name,
|
||||
]
|
||||
if (commandArgumentIndex + 1 === commandArguments.length) {
|
||||
submitCommand({
|
||||
dataArr: [
|
||||
...commandArgumentData,
|
||||
transformedData,
|
||||
] as CommandArgumentData[],
|
||||
})
|
||||
} else {
|
||||
setCommandArgumentData(
|
||||
(prevData) => [...prevData, transformedData] as CommandArgumentData[]
|
||||
)
|
||||
)
|
||||
setCommandArgumentIndex((prevIndex) => prevIndex + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function submitCommand({
|
||||
command = selectedCommand,
|
||||
dataArr = commandArgumentData,
|
||||
}) {
|
||||
console.log('submitting command', command, dataArr)
|
||||
if (dataArr.length === 0) {
|
||||
command?.callback()
|
||||
} else {
|
||||
const data = Object.fromEntries(dataArr)
|
||||
console.log('submitting data', data)
|
||||
command?.callback(data)
|
||||
}
|
||||
setCommandBarOpen(false)
|
||||
}
|
||||
|
||||
function getDisplayValue(command: Command) {
|
||||
if (
|
||||
'args' in command &&
|
||||
command.args &&
|
||||
command.args?.length > 0 &&
|
||||
'formatFunction' in command &&
|
||||
command.formatFunction
|
||||
) {
|
||||
command.formatFunction(
|
||||
command.args.map((c, i) =>
|
||||
commandArgumentData[i] ? commandArgumentData[i][0] : `<${c.name}>`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return command.name
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={
|
||||
commandBarOpen &&
|
||||
availableCommands?.length !== undefined &&
|
||||
availableCommands.length > 0
|
||||
}
|
||||
as={Fragment}
|
||||
show={commandBarOpen || false}
|
||||
afterLeave={() => clearState()}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
onClose={() => {
|
||||
setCommandBarOpen(false)
|
||||
clearState()
|
||||
}}
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1"
|
||||
>
|
||||
<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"
|
||||
@ -216,75 +197,208 @@ const CommandBar = () => {
|
||||
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"
|
||||
<Dialog.Panel
|
||||
className="relative w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||
as="div"
|
||||
>
|
||||
<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)}
|
||||
{!(
|
||||
commandArguments &&
|
||||
commandArguments.length &&
|
||||
selectedCommand
|
||||
) ? (
|
||||
<CommandComboBox
|
||||
options={commands}
|
||||
handleSelection={selectCommand}
|
||||
stepBack={stepBack}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="px-4 text-sm flex flex-wrap gap-2">
|
||||
<p className="pr-4 flex gap-2 items-center">
|
||||
{selectedCommand &&
|
||||
'icon' in selectedCommand &&
|
||||
selectedCommand.icon && (
|
||||
<CustomIcon
|
||||
name={selectedCommand.icon}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
)}
|
||||
{getDisplayValue(selectedCommand)}
|
||||
</p>
|
||||
)}
|
||||
<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}
|
||||
{commandArguments.map((arg, i) => (
|
||||
<p
|
||||
key={arg.name}
|
||||
className={`w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
||||
i === commandArgumentIndex
|
||||
? 'bg-energy-10/50 dark:bg-energy-10/20 border-energy-10 dark:border-energy-10'
|
||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||
}`}
|
||||
>
|
||||
{commandArgumentIndex >= i && commandArgumentData[i] ? (
|
||||
commandArgumentData[i][1]
|
||||
) : arg.defaultValue ? (
|
||||
arg.defaultValue
|
||||
) : (
|
||||
<em>{arg.name}</em>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
))}
|
||||
</div>
|
||||
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
|
||||
<Argument
|
||||
arg={commandArguments[commandArgumentIndex]}
|
||||
appendCommandArgumentData={appendCommandArgumentData}
|
||||
stepBack={stepBack}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function Argument({
|
||||
arg,
|
||||
appendCommandArgumentData,
|
||||
stepBack,
|
||||
}: {
|
||||
arg: CommandArgument
|
||||
appendCommandArgumentData: Dispatch<SetStateAction<any>>
|
||||
stepBack: () => void
|
||||
}) {
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [arg, inputRef])
|
||||
|
||||
return arg.type === 'select' ? (
|
||||
<CommandComboBox
|
||||
options={arg.options}
|
||||
handleSelection={appendCommandArgumentData}
|
||||
stepBack={stepBack}
|
||||
placeholder="Select an option"
|
||||
/>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
appendCommandArgumentData({ name: inputRef.current?.value })
|
||||
}}
|
||||
>
|
||||
<label className="flex items-center mx-4 my-4">
|
||||
<span className="px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
|
||||
{arg.name}
|
||||
</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||
placeholder="Enter a value"
|
||||
defaultValue={arg.defaultValue}
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||
stepBack()
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommandBarProvider
|
||||
|
||||
function CommandComboBox({
|
||||
options,
|
||||
handleSelection,
|
||||
stepBack,
|
||||
placeholder,
|
||||
}: {
|
||||
options: ComboboxOption[]
|
||||
handleSelection: Dispatch<SetStateAction<any>>
|
||||
stepBack: () => void
|
||||
placeholder?: string
|
||||
}) {
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const [query, setQuery] = useState('')
|
||||
const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>()
|
||||
|
||||
const defaultOption =
|
||||
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
|
||||
|
||||
const fuse = new Fuse(options, {
|
||||
keys: ['name', 'description'],
|
||||
threshold: 0.3,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const results = fuse.search(query).map((result) => result.item)
|
||||
setFilteredOptions(query.length > 0 ? results : options)
|
||||
}, [query])
|
||||
|
||||
return (
|
||||
<Combobox defaultValue={defaultOption} onChange={handleSelection}>
|
||||
<div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
|
||||
<CustomIcon
|
||||
name="search"
|
||||
className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
|
||||
/>
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||
stepBack()
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
(defaultOption && defaultOption.name) ||
|
||||
placeholder ||
|
||||
'Search commands'
|
||||
}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<Combobox.Options
|
||||
static
|
||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||
>
|
||||
{filteredOptions?.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.name}
|
||||
value={option}
|
||||
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
||||
>
|
||||
{'icon' in option && option.icon && (
|
||||
<CustomIcon
|
||||
name={option.icon}
|
||||
className="w-5 h-5 dark:text-energy-10"
|
||||
/>
|
||||
)}
|
||||
<p className="flex-grow">{option.name} </p>
|
||||
{'isCurrent' in option && option.isCurrent && (
|
||||
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
||||
current
|
||||
</small>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,21 @@
|
||||
export type CustomIconName =
|
||||
| 'createFile'
|
||||
| 'createFolder'
|
||||
| 'arrowDown'
|
||||
| 'arrowLeft'
|
||||
| 'arrowRight'
|
||||
| 'arrowUp'
|
||||
| 'close'
|
||||
| 'equal'
|
||||
| 'exit'
|
||||
| 'extrude'
|
||||
| 'file'
|
||||
| 'filePlus'
|
||||
| 'folder'
|
||||
| 'folderPlus'
|
||||
| 'gear'
|
||||
| 'horizontal'
|
||||
| 'line'
|
||||
| 'move'
|
||||
| 'parallel'
|
||||
| 'search'
|
||||
| 'sketch'
|
||||
| 'vertical'
|
||||
|
||||
@ -19,7 +26,7 @@ export const CustomIcon = ({
|
||||
name: CustomIconName
|
||||
} & React.SVGProps<SVGSVGElement>) => {
|
||||
switch (name) {
|
||||
case 'createFile':
|
||||
case 'arrowDown':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
@ -30,12 +37,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 +53,55 @@ 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 '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 +120,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 +145,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 +280,22 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</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) => ({
|
||||
@ -24,7 +23,8 @@ const DownloadAppBanner = () => {
|
||||
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:
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -2,7 +2,7 @@ import { useMachine } from '@xstate/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from '../Router'
|
||||
import {
|
||||
authCommandBarMeta,
|
||||
authCommandBarConfig,
|
||||
authMachine,
|
||||
TOKEN_PERSIST_KEY,
|
||||
} from '../machines/authMachine'
|
||||
@ -11,7 +11,7 @@ import React, { createContext, useEffect, useRef } from 'react'
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import {
|
||||
SETTINGS_PERSIST_KEY,
|
||||
settingsCommandBarMeta,
|
||||
settingsCommandBarConfig,
|
||||
settingsMachine,
|
||||
} from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
@ -85,7 +85,7 @@ export const GlobalStateProvider = ({
|
||||
send: settingsSend,
|
||||
commands,
|
||||
owner: 'settings',
|
||||
commandBarMeta: settingsCommandBarMeta,
|
||||
commandBarConfig: settingsCommandBarConfig,
|
||||
})
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
@ -124,7 +124,7 @@ export const GlobalStateProvider = ({
|
||||
state: authState,
|
||||
send: authSend,
|
||||
commands,
|
||||
commandBarMeta: authCommandBarMeta,
|
||||
commandBarConfig: authCommandBarConfig,
|
||||
owner: 'auth',
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
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'
|
||||
|
@ -46,7 +46,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 +56,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>
|
||||
|
@ -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,6 +183,8 @@ function ProjectCard({
|
||||
icon={{
|
||||
icon: faTrashAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
className: 'p-1',
|
||||
size: 'sm',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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,12 +1,16 @@
|
||||
import { useEffect } from 'react'
|
||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
|
||||
import {
|
||||
Command,
|
||||
CommandBarConfig,
|
||||
createMachineCommand,
|
||||
} from '../lib/commands'
|
||||
import { useCommandsContext } from './useCommandsContext'
|
||||
|
||||
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
||||
state: StateFrom<T>
|
||||
send: Function
|
||||
commandBarMeta?: CommandBarMeta
|
||||
commandBarConfig?: CommandBarConfig<T>
|
||||
commands: Command[]
|
||||
owner: string
|
||||
}
|
||||
@ -14,7 +18,7 @@ interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
||||
export default function useStateMachineCommands<T extends AnyStateMachine>({
|
||||
state,
|
||||
send,
|
||||
commandBarMeta,
|
||||
commandBarConfig,
|
||||
owner,
|
||||
}: UseStateMachineCommandsArgs<T>) {
|
||||
const { addCommands, removeCommands } = useCommandsContext()
|
||||
@ -27,11 +31,11 @@ export default function useStateMachineCommands<T extends AnyStateMachine>({
|
||||
type,
|
||||
state,
|
||||
send,
|
||||
commandBarMeta,
|
||||
commandBarConfig,
|
||||
owner,
|
||||
})
|
||||
)
|
||||
.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)
|
||||
|
||||
|
@ -57,27 +57,35 @@ 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
@ -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,
|
||||
|
@ -1,117 +1,110 @@
|
||||
import { AnyStateMachine, EventFrom, StateFrom } from 'xstate'
|
||||
import { AnyStateMachine, ContextFrom, EventFrom, StateFrom } from 'xstate'
|
||||
import { isTauri } from './isTauri'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
|
||||
type InitialCommandBarMetaArg = {
|
||||
name: string
|
||||
type: 'string' | 'select'
|
||||
description?: string
|
||||
defaultValue?: string
|
||||
options: string | Array<{ name: string }>
|
||||
}
|
||||
|
||||
type Icon = CustomIconName
|
||||
type Platform = 'both' | 'web' | 'desktop'
|
||||
type InputType = 'select' | 'string' | 'interaction'
|
||||
export type CommandArgumentOption = { name: string; isCurrent?: boolean }
|
||||
|
||||
export type CommandBarMeta = {
|
||||
[key: string]:
|
||||
// Command arguments can either be defined manually
|
||||
// or flagged as needing to be looked up from the context.
|
||||
// This is useful for things like settings, where
|
||||
// we want to show the current setting value as the default.
|
||||
// The lookup is done in createMachineCommand.
|
||||
type CommandArgumentConfig<T extends AnyStateMachine> = {
|
||||
name: string // TODO: I would love for this to be strongly-typed so we could guarantee it's a valid data payload key on the event type.
|
||||
type: InputType
|
||||
description?: string
|
||||
} & (
|
||||
| {
|
||||
type: 'select'
|
||||
options?: CommandArgumentOption[]
|
||||
getOptionsFromContext?: keyof ContextFrom<T>
|
||||
defaultValue?: string
|
||||
getDefaultValueFromContext?: keyof ContextFrom<T>
|
||||
}
|
||||
| {
|
||||
type: 'string'
|
||||
defaultValue?: string
|
||||
getDefaultValueFromContext?: keyof ContextFrom<T>
|
||||
}
|
||||
| { type: 'interaction' }
|
||||
)
|
||||
|
||||
export type CommandBarConfig<T extends AnyStateMachine> = Partial<{
|
||||
[EventType in EventFrom<T>['type']]:
|
||||
| {
|
||||
displayValue: (args: string[]) => string
|
||||
args: InitialCommandBarMetaArg[]
|
||||
args: CommandArgumentConfig<T>[]
|
||||
formatFunction?: (args: string[]) => string
|
||||
icon?: Icon
|
||||
hide?: Platform
|
||||
}
|
||||
| {
|
||||
hide?: Platform
|
||||
}
|
||||
}
|
||||
}>
|
||||
|
||||
export type Command = {
|
||||
owner: string
|
||||
name: string
|
||||
callback: Function
|
||||
meta?: {
|
||||
displayValue(args: string[]): string | string
|
||||
args: SubCommand[]
|
||||
}
|
||||
icon?: Icon
|
||||
args?: CommandArgument[]
|
||||
formatFunction?: (args: string[]) => string
|
||||
}
|
||||
|
||||
export type SubCommand = {
|
||||
export type CommandArgument = {
|
||||
name: string
|
||||
type: 'select' | 'string'
|
||||
description?: string
|
||||
options?: Partial<{ name: string }>[]
|
||||
}
|
||||
defaultValue?: string
|
||||
} & (
|
||||
| {
|
||||
type: Extract<InputType, 'select'>
|
||||
options: CommandArgumentOption[]
|
||||
}
|
||||
| {
|
||||
type: Exclude<InputType, 'select'>
|
||||
}
|
||||
)
|
||||
|
||||
interface CommandBarArgs<T extends AnyStateMachine> {
|
||||
interface CreateMachineCommandProps<T extends AnyStateMachine> {
|
||||
type: EventFrom<T>['type']
|
||||
state: StateFrom<T>
|
||||
commandBarMeta?: CommandBarMeta
|
||||
commandBarConfig?: CommandBarConfig<T>
|
||||
send: Function
|
||||
owner: string
|
||||
}
|
||||
|
||||
// Creates a command with subcommands, ready for use in the CommandBar component,
|
||||
// from a more terse Command Bar Meta definition.
|
||||
export function createMachineCommand<T extends AnyStateMachine>({
|
||||
type,
|
||||
state,
|
||||
commandBarMeta,
|
||||
commandBarConfig,
|
||||
send,
|
||||
owner,
|
||||
}: CommandBarArgs<T>): Command | null {
|
||||
const lookedUpMeta = commandBarMeta && commandBarMeta[type]
|
||||
if (lookedUpMeta && 'hide' in lookedUpMeta) {
|
||||
}: CreateMachineCommandProps<T>): Command | null {
|
||||
const lookedUpMeta = commandBarConfig && commandBarConfig[type]
|
||||
if (!lookedUpMeta) return null
|
||||
|
||||
// Hide commands based on platform by returning `null`
|
||||
// so the consumer can filter them out
|
||||
if ('hide' in lookedUpMeta) {
|
||||
const { hide } = lookedUpMeta
|
||||
if (hide === 'both') return null
|
||||
else if (hide === 'desktop' && isTauri()) return null
|
||||
else if (hide === 'web' && !isTauri()) return null
|
||||
}
|
||||
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
|
||||
const icon = ('icon' in lookedUpMeta && lookedUpMeta.icon) || undefined
|
||||
const formatFunction =
|
||||
('formatFunction' in lookedUpMeta && lookedUpMeta.formatFunction) ||
|
||||
undefined
|
||||
|
||||
return {
|
||||
name: type,
|
||||
owner,
|
||||
icon,
|
||||
callback: (data: EventFrom<T, typeof type>) => {
|
||||
if (data !== undefined && data !== null) {
|
||||
send(type, { data })
|
||||
@ -119,6 +112,66 @@ export function createMachineCommand<T extends AnyStateMachine>({
|
||||
send(type)
|
||||
}
|
||||
},
|
||||
meta: meta as any,
|
||||
...('args' in lookedUpMeta
|
||||
? {
|
||||
args: getCommandArgumentValuesFromContext(state, lookedUpMeta.args),
|
||||
formatFunction,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
function getCommandArgumentValuesFromContext<T extends AnyStateMachine>(
|
||||
state: StateFrom<T>,
|
||||
args: CommandArgumentConfig<T>[]
|
||||
): CommandArgument[] {
|
||||
function getDefaultValue(
|
||||
arg: CommandArgumentConfig<T> & { type: 'string' | 'select' }
|
||||
) {
|
||||
if (
|
||||
arg.type === 'select' ||
|
||||
('getDefaultValueFromContext' in arg && arg.getDefaultValueFromContext)
|
||||
) {
|
||||
return state.context[arg.getDefaultValueFromContext]
|
||||
} else {
|
||||
return arg.defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
return args.map((arg) => {
|
||||
switch (arg.type) {
|
||||
case 'interaction':
|
||||
return {
|
||||
name: arg.name,
|
||||
type: 'interaction',
|
||||
}
|
||||
case 'string':
|
||||
return {
|
||||
name: arg.name,
|
||||
type: arg.type,
|
||||
defaultValue: arg.getDefaultValueFromContext
|
||||
? state.context[arg.getDefaultValueFromContext]
|
||||
: arg.defaultValue,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
name: arg.name,
|
||||
type: arg.type,
|
||||
defaultValue: getDefaultValue(arg),
|
||||
options: arg.getOptionsFromContext
|
||||
? state.context[arg.getOptionsFromContext].map(
|
||||
(v: string | { name: string }) => ({
|
||||
name: typeof v === 'string' ? v : v.name,
|
||||
isCurrent: v === getDefaultValue(arg),
|
||||
})
|
||||
)
|
||||
: arg.getDefaultValueFromContext
|
||||
? arg.options?.map((v) => ({
|
||||
...v,
|
||||
isCurrent: v.name === getDefaultValue(arg),
|
||||
}))
|
||||
: arg.options,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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,7 @@
|
||||
import { createMachine, assign } from 'xstate'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import withBaseURL from '../lib/withBaseURL'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
import { CommandBarConfig } from '../lib/commands'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
@ -40,10 +40,14 @@ export type Events =
|
||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||
|
||||
export const authCommandBarMeta: CommandBarMeta = {
|
||||
export const authCommandBarConfig: CommandBarConfig<typeof authMachine> = {
|
||||
'Log in': {
|
||||
hide: 'both',
|
||||
},
|
||||
'Log out': {
|
||||
args: [],
|
||||
icon: 'arrowLeft',
|
||||
},
|
||||
}
|
||||
|
||||
export const authMachine = createMachine<UserContext, Events>(
|
||||
|
@ -1,59 +1,55 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
import { CommandBarConfig } from '../lib/commands'
|
||||
|
||||
export const homeCommandMeta: CommandBarMeta = {
|
||||
export const homeCommandConfig: CommandBarConfig<typeof homeMachine> = {
|
||||
'Create project': {
|
||||
displayValue: (args: string[]) => `Create project "${args[0]}"`,
|
||||
icon: 'folderPlus',
|
||||
args: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
description: '(default)',
|
||||
options: 'defaultProjectName',
|
||||
getDefaultValueFromContext: 'defaultProjectName',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Open project': {
|
||||
displayValue: (args: string[]) => `Open project "${args[0]}"`,
|
||||
icon: 'arrowRight',
|
||||
args: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'select',
|
||||
options: 'projects',
|
||||
getOptionsFromContext: 'projects',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Delete project': {
|
||||
displayValue: (args: string[]) => `Delete project "${args[0]}"`,
|
||||
icon: 'close',
|
||||
args: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'select',
|
||||
options: 'projects',
|
||||
getOptionsFromContext: 'projects',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Rename project': {
|
||||
displayValue: (args: string[]) =>
|
||||
icon: 'folder',
|
||||
formatFunction: (args: string[]) =>
|
||||
`Rename project "${args[0]}" to "${args[1]}"`,
|
||||
args: [
|
||||
{
|
||||
name: 'oldName',
|
||||
type: 'select',
|
||||
options: 'projects',
|
||||
getOptionsFromContext: 'projects',
|
||||
},
|
||||
{
|
||||
name: 'newName',
|
||||
type: 'string',
|
||||
description: '(default)',
|
||||
options: 'defaultProjectName',
|
||||
getDefaultValueFromContext: 'defaultProjectName',
|
||||
},
|
||||
],
|
||||
},
|
||||
assign: {
|
||||
hide: 'both',
|
||||
},
|
||||
}
|
||||
|
||||
export const homeMachine = createMachine(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
import { CommandBarConfig } from '../lib/commands'
|
||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
||||
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
|
||||
import { Models } from '@kittycad/lib'
|
||||
@ -24,25 +24,27 @@ export type Toggle = 'On' | 'Off'
|
||||
|
||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||
|
||||
export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
export const settingsCommandBarConfig: CommandBarConfig<
|
||||
typeof settingsMachine
|
||||
> = {
|
||||
'Set Base Unit': {
|
||||
displayValue: (args: string[]) => 'Set your default base unit',
|
||||
icon: 'gear',
|
||||
args: [
|
||||
{
|
||||
name: 'baseUnit',
|
||||
type: 'select',
|
||||
defaultValue: 'baseUnit',
|
||||
getDefaultValueFromContext: 'baseUnit',
|
||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Camera Controls': {
|
||||
displayValue: (args: string[]) => 'Set your camera controls',
|
||||
icon: 'gear',
|
||||
args: [
|
||||
{
|
||||
name: 'cameraControls',
|
||||
type: 'select',
|
||||
defaultValue: 'cameraControls',
|
||||
getDefaultValueFromContext: 'cameraControls',
|
||||
options: Object.values(cameraSystems).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
@ -51,15 +53,13 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
hide: 'both',
|
||||
},
|
||||
'Set Default Project Name': {
|
||||
displayValue: (args: string[]) => 'Set a new default project name',
|
||||
icon: 'gear',
|
||||
hide: 'web',
|
||||
args: [
|
||||
{
|
||||
name: 'defaultProjectName',
|
||||
type: 'string',
|
||||
description: '(default)',
|
||||
defaultValue: 'defaultProjectName',
|
||||
options: 'defaultProjectName',
|
||||
getDefaultValueFromContext: 'defaultProjectName',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -67,23 +67,23 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
hide: 'both',
|
||||
},
|
||||
'Set Text Wrapping': {
|
||||
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
|
||||
icon: 'gear',
|
||||
args: [
|
||||
{
|
||||
name: 'textWrapping',
|
||||
type: 'select',
|
||||
defaultValue: 'textWrapping',
|
||||
getDefaultValueFromContext: 'textWrapping',
|
||||
options: [{ name: 'On' }, { name: 'Off' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Theme': {
|
||||
displayValue: (args: string[]) => 'Change the app theme',
|
||||
icon: 'gear',
|
||||
args: [
|
||||
{
|
||||
name: 'theme',
|
||||
type: 'select',
|
||||
defaultValue: 'theme',
|
||||
getDefaultValueFromContext: 'theme',
|
||||
options: Object.values(Themes).map((v): { name: string } => ({
|
||||
name: v,
|
||||
})),
|
||||
@ -91,12 +91,12 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
],
|
||||
},
|
||||
'Set Unit System': {
|
||||
displayValue: (args: string[]) => 'Set your default unit system',
|
||||
icon: 'gear',
|
||||
args: [
|
||||
{
|
||||
name: 'unitSystem',
|
||||
type: 'select',
|
||||
defaultValue: 'unitSystem',
|
||||
getDefaultValueFromContext: 'unitSystem',
|
||||
options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
|
||||
},
|
||||
],
|
||||
@ -126,7 +126,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 { homeCommandConfig, homeMachine } from '../machines/homeMachine'
|
||||
import { ContextFrom, EventFrom } from 'xstate'
|
||||
import { paths } from '../Router'
|
||||
import {
|
||||
@ -147,7 +147,7 @@ const Home = () => {
|
||||
commands,
|
||||
send,
|
||||
state,
|
||||
commandBarMeta: homeCommandMeta,
|
||||
commandBarConfig: homeCommandConfig,
|
||||
owner: 'home',
|
||||
})
|
||||
|
||||
@ -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,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 { Platform, platform } from '@tauri-apps/api/os'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -17,7 +15,7 @@ export default function CmdK() {
|
||||
async function getPlatform() {
|
||||
setPlatformName(await platform())
|
||||
}
|
||||
getPlatform()
|
||||
void getPlatform()
|
||||
}, [setPlatformName])
|
||||
|
||||
return (
|
||||
@ -57,28 +55,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>
|
||||
|
270
src/wasm-lib/Cargo.lock
generated
@ -127,6 +127,12 @@ dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08abcc3b4e9339e33a3d0a5ed15d84a687350c05689d825e0f6655eef9e76a94"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.6.0"
|
||||
@ -243,7 +249,7 @@ dependencies = [
|
||||
"libm",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.17",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -329,7 +335,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
@ -394,6 +400,18 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cgmath"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64a4b57c8f4e3a2e9ac07e0f6abc9c24b6fc9e1b54c3478cfb598f3d0023e51c"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"mint",
|
||||
"num-traits 0.1.43",
|
||||
"rand 0.4.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.31"
|
||||
@ -402,8 +420,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"js-sys",
|
||||
"num-traits 0.2.17",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
@ -580,7 +600,7 @@ dependencies = [
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"num-traits 0.2.17",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
@ -677,9 +697,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"
|
||||
@ -742,6 +762,49 @@ dependencies = [
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel"
|
||||
version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8"
|
||||
dependencies = [
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.1",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"diesel_derives",
|
||||
"mysqlclient-sys",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits 0.2.17",
|
||||
"percent-encoding",
|
||||
"r2d2",
|
||||
"serde_json",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_derives"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44"
|
||||
dependencies = [
|
||||
"diesel_table_macro_syntax",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_table_macro_syntax"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
|
||||
dependencies = [
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@ -806,6 +869,26 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7add3873b5dd076766ee79c8e406ad1a472c385476b9e38849f8eec24f1be689"
|
||||
dependencies = [
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -822,14 +905,26 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "euler"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f19d11568a4a46aee488bdab3a2963e5e2c3cfd6091aa0abceaddcea82c0bc1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"cgmath",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execution-plan"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"kittycad",
|
||||
"kittycad-modeling-cmds",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -946,6 +1041,12 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@ -1299,7 +1400,7 @@ dependencies = [
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"num-traits 0.2.17",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
@ -1338,6 +1439,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inflections"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.34.0"
|
||||
@ -1504,7 +1611,7 @@ dependencies = [
|
||||
"log",
|
||||
"parse-display",
|
||||
"phonenumber",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"reqwest-conditional-middleware",
|
||||
"reqwest-middleware",
|
||||
@ -1521,6 +1628,42 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3217f9ffe9dce4b16303eeef539e7e27b743bc1c46eff8ce64657745a2b75ca"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"data-encoding",
|
||||
"diesel",
|
||||
"diesel_derives",
|
||||
"enum-iterator",
|
||||
"enum-iterator-derive",
|
||||
"euler",
|
||||
"http",
|
||||
"kittycad-unit-conversion-derive",
|
||||
"measurements",
|
||||
"parse-display",
|
||||
"parse-display-derive",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-unit-conversion-derive"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7001c46a92c1edce6722a3900539b198230980799035f02d92b4e7df3fc08738"
|
||||
dependencies = [
|
||||
"inflections",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@ -1620,6 +1763,15 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "measurements"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5b734b4e8187ea5777bc29c086f0970a27d8de42061b48f5af32cafc0ca904b"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
@ -1667,6 +1819,12 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mint"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.9"
|
||||
@ -1678,6 +1836,16 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mysqlclient-sys"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f61b381528ba293005c42a409dd73d034508e273bf90481f17ec2e964a6e969b"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "newline-converter"
|
||||
version = "0.3.0"
|
||||
@ -1705,7 +1873,7 @@ checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1715,7 +1883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
"num-traits 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1726,7 +1894,16 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||
dependencies = [
|
||||
"num-traits 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1812,7 +1989,7 @@ dependencies = [
|
||||
"phonenumber",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
@ -1864,7 +2041,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@ -2019,7 +2196,7 @@ version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"num-traits 0.2.17",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
@ -2136,12 +2313,36 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
||||
dependencies = [
|
||||
"log",
|
||||
"parking_lot 0.12.1",
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
||||
dependencies = [
|
||||
"fuchsia-cprng",
|
||||
"libc",
|
||||
"rand_core 0.3.1",
|
||||
"rdrand",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@ -2150,7 +2351,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2160,9 +2361,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
@ -2192,6 +2408,15 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@ -2400,7 +2625,7 @@ checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2528,6 +2753,15 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
|
||||
dependencies = [
|
||||
"parking_lot 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.16"
|
||||
@ -3444,7 +3678,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"rustls",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
|
@ -9,5 +9,7 @@ description = "A DSL for composing KittyCAD API queries"
|
||||
[dependencies]
|
||||
bytes = "1.5"
|
||||
kittycad = { workspace = true, features = ["requests"] }
|
||||
kittycad-modeling-cmds = "0.1.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
uuid = "1.6.1"
|
||||
|
69
src/wasm-lib/execution-plan/src/arithmetic.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use crate::primitive::{NumericPrimitive, Primitive};
|
||||
use crate::{ExecutionError, Memory, Operand, Operation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Instruction to perform arithmetic on values in memory.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Arithmetic {
|
||||
/// Apply this operation
|
||||
pub operation: Operation,
|
||||
/// First operand for the operation
|
||||
pub operand0: Operand,
|
||||
/// Second operand for the operation
|
||||
pub operand1: Operand,
|
||||
}
|
||||
|
||||
macro_rules! arithmetic_body {
|
||||
($arith:ident, $mem:ident, $method:ident) => {
|
||||
match (
|
||||
$arith.operand0.eval(&$mem)?.clone(),
|
||||
$arith.operand1.eval(&$mem)?.clone(),
|
||||
) {
|
||||
// If both operands are numeric, then do the arithmetic operation.
|
||||
(Primitive::NumericValue(x), Primitive::NumericValue(y)) => {
|
||||
let num = match (x, y) {
|
||||
(NumericPrimitive::Integer(x), NumericPrimitive::Integer(y)) => {
|
||||
NumericPrimitive::Integer(x.$method(y))
|
||||
}
|
||||
(NumericPrimitive::Integer(x), NumericPrimitive::Float(y)) => {
|
||||
NumericPrimitive::Float((x as f64).$method(y))
|
||||
}
|
||||
(NumericPrimitive::Float(x), NumericPrimitive::Integer(y)) => {
|
||||
NumericPrimitive::Float(x.$method(y as f64))
|
||||
}
|
||||
(NumericPrimitive::Float(x), NumericPrimitive::Float(y)) => NumericPrimitive::Float(x.$method(y)),
|
||||
};
|
||||
Ok(Primitive::NumericValue(num))
|
||||
}
|
||||
// This operation can only be done on numeric types.
|
||||
_ => Err(ExecutionError::CannotApplyOperation {
|
||||
op: $arith.operation,
|
||||
operands: vec![
|
||||
$arith.operand0.eval(&$mem)?.clone().to_owned(),
|
||||
$arith.operand1.eval(&$mem)?.clone().to_owned(),
|
||||
],
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
impl Arithmetic {
|
||||
/// Calculate the the arithmetic equation.
|
||||
/// May read values from the given memory.
|
||||
pub fn calculate(self, mem: &Memory) -> Result<Primitive, ExecutionError> {
|
||||
use std::ops::{Add, Div, Mul, Sub};
|
||||
match self.operation {
|
||||
Operation::Add => {
|
||||
arithmetic_body!(self, mem, add)
|
||||
}
|
||||
Operation::Mul => {
|
||||
arithmetic_body!(self, mem, mul)
|
||||
}
|
||||
Operation::Sub => {
|
||||
arithmetic_body!(self, mem, sub)
|
||||
}
|
||||
Operation::Div => {
|
||||
arithmetic_body!(self, mem, div)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
use crate::{ExecutionError, Value};
|
||||
|
||||
/// Types that can be written to or read from KCEP program memory,
|
||||
/// but require multiple values to store.
|
||||
/// They get laid out into multiple consecutive memory addresses.
|
||||
pub trait Composite: Sized {
|
||||
/// How many memory addresses are required to store this value?
|
||||
const SIZE: usize;
|
||||
/// Store the value in memory.
|
||||
fn into_parts(self) -> Vec<Value>;
|
||||
/// Read the value from memory.
|
||||
fn from_parts(values: Vec<Value>) -> Result<Self, ExecutionError>;
|
||||
}
|
||||
|
||||
impl Composite for kittycad::types::Point3D {
|
||||
fn into_parts(self) -> Vec<Value> {
|
||||
let points = [self.x, self.y, self.z];
|
||||
points
|
||||
.into_iter()
|
||||
.map(|x| Value::NumericValue(crate::NumericValue::Float(x)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
const SIZE: usize = 3;
|
||||
|
||||
fn from_parts(values: Vec<Value>) -> Result<Self, ExecutionError> {
|
||||
let n = values.len();
|
||||
let Ok([x, y, z]): Result<[Value; 3], _> = values.try_into() else {
|
||||
return Err(ExecutionError::MemoryWrongSize {
|
||||
actual: n,
|
||||
expected: Self::SIZE,
|
||||
});
|
||||
};
|
||||
let x = x.try_into()?;
|
||||
let y = y.try_into()?;
|
||||
let z = z.try_into()?;
|
||||
Ok(Self { x, y, z })
|
||||
}
|
||||
}
|
@ -6,18 +6,28 @@
|
||||
//! You can think of it as a domain-specific language for making KittyCAD API calls and using
|
||||
//! the results to make other API calls.
|
||||
|
||||
use composite::Composite;
|
||||
use self::arithmetic::Arithmetic;
|
||||
use self::primitive::Primitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fmt};
|
||||
use std::fmt;
|
||||
use value::Value;
|
||||
|
||||
mod composite;
|
||||
mod arithmetic;
|
||||
mod primitive;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod value;
|
||||
|
||||
/// KCEP's program memory. A flat, linear list of values.
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Memory(HashMap<usize, Value>);
|
||||
pub struct Memory(Vec<Option<Primitive>>);
|
||||
|
||||
impl Default for Memory {
|
||||
fn default() -> Self {
|
||||
Self(vec![None; 1024])
|
||||
}
|
||||
}
|
||||
|
||||
/// An address in KCEP's program memory.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
@ -37,90 +47,43 @@ impl From<usize> for Address {
|
||||
|
||||
impl Memory {
|
||||
/// Get a value from KCEP's program memory.
|
||||
pub fn get(&self, addr: &Address) -> Option<&Value> {
|
||||
self.0.get(&addr.0)
|
||||
pub fn get(&self, Address(addr): &Address) -> Option<&Primitive> {
|
||||
self.0[*addr].as_ref()
|
||||
}
|
||||
|
||||
/// Store a value in KCEP's program memory.
|
||||
pub fn set(&mut self, addr: Address, value: Value) {
|
||||
self.0.insert(addr.0, value);
|
||||
pub fn set(&mut self, Address(addr): Address, value: Primitive) {
|
||||
// If isn't big enough for this value, double the size of memory until it is.
|
||||
while addr > self.0.len() {
|
||||
self.0.extend(vec![None; self.0.len()]);
|
||||
}
|
||||
self.0[addr] = Some(value);
|
||||
}
|
||||
|
||||
/// Store a composite value (i.e. a value which takes up multiple addresses in memory).
|
||||
/// Store a value value (i.e. a value which takes up multiple addresses in memory).
|
||||
/// Store its parts in consecutive memory addresses starting at `start`.
|
||||
pub fn set_composite<T: Composite>(&mut self, composite_value: T, start: Address) {
|
||||
pub fn set_composite<T: Value>(&mut self, composite_value: T, start: Address) {
|
||||
let parts = composite_value.into_parts().into_iter();
|
||||
for (value, addr) in parts.zip(start.0..) {
|
||||
self.0.insert(addr, value);
|
||||
self.0[addr] = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a composite value (i.e. a value which takes up multiple addresses in memory).
|
||||
/// Get a value value (i.e. a value which takes up multiple addresses in memory).
|
||||
/// Its parts are stored in consecutive memory addresses starting at `start`.
|
||||
pub fn get_composite<T: Composite>(&self, start: Address) -> Result<T, ExecutionError> {
|
||||
let addrs = start.0..start.0 + T::SIZE;
|
||||
let values: Vec<Value> = addrs
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
let addr = Address(a);
|
||||
self.get(&addr)
|
||||
.map(|x| x.to_owned())
|
||||
.ok_or(ExecutionError::MemoryEmpty { addr })
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
pub fn get_composite<T: Value>(&self, start: Address) -> Result<T, ExecutionError> {
|
||||
let values = &self.0[start.0..];
|
||||
T::from_parts(values)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value stored in KCEP program memory.
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum Value {
|
||||
String(String),
|
||||
NumericValue(NumericValue),
|
||||
}
|
||||
|
||||
impl TryFrom<Value> for f64 {
|
||||
type Error = ExecutionError;
|
||||
|
||||
fn try_from(value: Value) -> Result<Self, Self::Error> {
|
||||
if let Value::NumericValue(NumericValue::Float(x)) = value {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err(ExecutionError::MemoryWrongType {
|
||||
expected: "float",
|
||||
actual: format!("{value:?}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<f64> for Value {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::NumericValue(NumericValue::Float(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<usize> for Value {
|
||||
fn from(value: usize) -> Self {
|
||||
Self::NumericValue(NumericValue::Integer(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum NumericValue {
|
||||
Integer(usize),
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
/// One step of the execution plan.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Call the KittyCAD API.
|
||||
ApiRequest {
|
||||
/// Which ModelingCmd to call.
|
||||
/// It's a composite value starting at the given address.
|
||||
/// It's a value value starting at the given address.
|
||||
endpoint: Address,
|
||||
/// Which address should the response be stored in?
|
||||
store_response: Option<usize>,
|
||||
@ -132,7 +95,7 @@ pub enum Instruction {
|
||||
/// Which memory address to set.
|
||||
address: Address,
|
||||
/// What value to set the memory address to.
|
||||
value: Value,
|
||||
value: Primitive,
|
||||
},
|
||||
/// Perform arithmetic on values in memory.
|
||||
Arithmetic {
|
||||
@ -143,68 +106,8 @@ pub enum Instruction {
|
||||
},
|
||||
}
|
||||
|
||||
/// Instruction to perform arithmetic on values in memory.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Arithmetic {
|
||||
/// Apply this operation
|
||||
pub operation: Operation,
|
||||
/// First operand for the operation
|
||||
pub operand0: Operand,
|
||||
/// Second operand for the operation
|
||||
pub operand1: Operand,
|
||||
}
|
||||
|
||||
macro_rules! arithmetic_body {
|
||||
($arith:ident, $mem:ident, $method:ident) => {
|
||||
match (
|
||||
$arith.operand0.eval(&$mem)?.clone(),
|
||||
$arith.operand1.eval(&$mem)?.clone(),
|
||||
) {
|
||||
// If both operands are numeric, then do the arithmetic operation.
|
||||
(Value::NumericValue(x), Value::NumericValue(y)) => {
|
||||
let num = match (x, y) {
|
||||
(NumericValue::Integer(x), NumericValue::Integer(y)) => NumericValue::Integer(x.$method(y)),
|
||||
(NumericValue::Integer(x), NumericValue::Float(y)) => NumericValue::Float((x as f64).$method(y)),
|
||||
(NumericValue::Float(x), NumericValue::Integer(y)) => NumericValue::Float(x.$method(y as f64)),
|
||||
(NumericValue::Float(x), NumericValue::Float(y)) => NumericValue::Float(x.$method(y)),
|
||||
};
|
||||
Ok(Value::NumericValue(num))
|
||||
}
|
||||
// This operation can only be done on numeric types.
|
||||
_ => Err(ExecutionError::CannotApplyOperation {
|
||||
op: $arith.operation,
|
||||
operands: vec![
|
||||
$arith.operand0.eval(&$mem)?.clone().to_owned(),
|
||||
$arith.operand1.eval(&$mem)?.clone().to_owned(),
|
||||
],
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
impl Arithmetic {
|
||||
/// Calculate the the arithmetic equation.
|
||||
/// May read values from the given memory.
|
||||
fn calculate(self, mem: &Memory) -> Result<Value, ExecutionError> {
|
||||
use std::ops::{Add, Div, Mul, Sub};
|
||||
match self.operation {
|
||||
Operation::Add => {
|
||||
arithmetic_body!(self, mem, add)
|
||||
}
|
||||
Operation::Mul => {
|
||||
arithmetic_body!(self, mem, mul)
|
||||
}
|
||||
Operation::Sub => {
|
||||
arithmetic_body!(self, mem, sub)
|
||||
}
|
||||
Operation::Div => {
|
||||
arithmetic_body!(self, mem, div)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Operations that can be applied to values in memory.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
|
||||
pub enum Operation {
|
||||
Add,
|
||||
Mul,
|
||||
@ -227,13 +130,13 @@ impl fmt::Display for Operation {
|
||||
/// Argument to an operation.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum Operand {
|
||||
Literal(Value),
|
||||
Literal(Primitive),
|
||||
Reference(Address),
|
||||
}
|
||||
|
||||
impl Operand {
|
||||
/// Evaluate the operand, getting its value.
|
||||
fn eval(&self, mem: &Memory) -> Result<Value, ExecutionError> {
|
||||
fn eval(&self, mem: &Memory) -> Result<Primitive, ExecutionError> {
|
||||
match self {
|
||||
Operand::Literal(v) => Ok(v.to_owned()),
|
||||
Operand::Reference(addr) => match mem.get(addr) {
|
||||
@ -265,14 +168,16 @@ pub fn execute(mem: &mut Memory, plan: Vec<Instruction>) -> Result<(), Execution
|
||||
}
|
||||
|
||||
/// Errors that could occur when executing a KittyCAD execution plan.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, Clone)]
|
||||
pub enum ExecutionError {
|
||||
#[error("Memory address {addr} was not set")]
|
||||
MemoryEmpty { addr: Address },
|
||||
#[error("Cannot apply operation {op} to operands {operands:?}")]
|
||||
CannotApplyOperation { op: Operation, operands: Vec<Value> },
|
||||
CannotApplyOperation { op: Operation, operands: Vec<Primitive> },
|
||||
#[error("Tried to read a '{expected}' from KCEP program memory, found an '{actual}' instead")]
|
||||
MemoryWrongType { expected: &'static str, actual: String },
|
||||
#[error("Wrong size of memory trying to read value from KCEP program memory: got {actual} but wanted {expected}")]
|
||||
MemoryWrongSize { expected: usize, actual: usize },
|
||||
#[error("Wanted {expected} values but did not get enough")]
|
||||
MemoryWrongSize { expected: usize },
|
||||
#[error("No endpoint {0} recognized")]
|
||||
UnrecognizedEndpoint(String),
|
||||
}
|
||||
|
98
src/wasm-lib/execution-plan/src/primitive.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use crate::ExecutionError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A value stored in KCEP program memory.
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum Primitive {
|
||||
String(String),
|
||||
NumericValue(NumericPrimitive),
|
||||
Uuid(Uuid),
|
||||
}
|
||||
|
||||
impl From<Uuid> for Primitive {
|
||||
fn from(u: Uuid) -> Self {
|
||||
Self::Uuid(u)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Primitive {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Primitive {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::NumericValue(NumericPrimitive::Float(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Primitive> for String {
|
||||
type Error = ExecutionError;
|
||||
|
||||
fn try_from(value: Primitive) -> Result<Self, Self::Error> {
|
||||
if let Primitive::String(s) = value {
|
||||
Ok(s)
|
||||
} else {
|
||||
Err(ExecutionError::MemoryWrongType {
|
||||
expected: "string",
|
||||
actual: format!("{value:?}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Primitive> for Uuid {
|
||||
type Error = ExecutionError;
|
||||
|
||||
fn try_from(value: Primitive) -> Result<Self, Self::Error> {
|
||||
if let Primitive::Uuid(u) = value {
|
||||
Ok(u)
|
||||
} else {
|
||||
Err(ExecutionError::MemoryWrongType {
|
||||
expected: "uuid",
|
||||
actual: format!("{value:?}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Primitive> for f64 {
|
||||
type Error = ExecutionError;
|
||||
|
||||
fn try_from(value: Primitive) -> Result<Self, Self::Error> {
|
||||
if let Primitive::NumericValue(NumericPrimitive::Float(x)) = value {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err(ExecutionError::MemoryWrongType {
|
||||
expected: "float",
|
||||
actual: format!("{value:?}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<usize> for Primitive {
|
||||
fn from(value: usize) -> Self {
|
||||
Self::NumericValue(NumericPrimitive::Integer(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum NumericPrimitive {
|
||||
Integer(usize),
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
impl crate::value::Value for Primitive {
|
||||
fn into_parts(self) -> Vec<Primitive> {
|
||||
vec![self]
|
||||
}
|
||||
|
||||
fn from_parts(values: &[Option<Primitive>]) -> Result<Self, ExecutionError> {
|
||||
let v = values.get(0).ok_or(ExecutionError::MemoryWrongSize { expected: 1 })?;
|
||||
v.to_owned().ok_or(ExecutionError::MemoryWrongSize { expected: 1 })
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
use kittycad::types::Point3D;
|
||||
use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, ModelingCmd, MovePathPen};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -57,15 +58,16 @@ fn add_to_composite_value() {
|
||||
let mut mem = Memory::default();
|
||||
|
||||
// Write a point to memory.
|
||||
let point_before = Point3D { x: 2.0, y: 3.0, z: 4.0 };
|
||||
let start_addr = Address(100);
|
||||
let point_before = Point3d {
|
||||
x: 2.0f64,
|
||||
y: 3.0,
|
||||
z: 4.0,
|
||||
};
|
||||
let start_addr = Address(0);
|
||||
mem.set_composite(point_before, start_addr);
|
||||
assert_eq!(
|
||||
mem,
|
||||
Memory(HashMap::from(
|
||||
[(100, 2.0.into()), (101, 3.0.into()), (102, 4.0.into()),]
|
||||
))
|
||||
);
|
||||
assert_eq!(mem.0[0], Some(2.0.into()));
|
||||
assert_eq!(mem.0[1], Some(3.0.into()));
|
||||
assert_eq!(mem.0[2], Some(4.0.into()));
|
||||
|
||||
// Update the point's x-value in memory.
|
||||
execute(
|
||||
@ -82,13 +84,35 @@ fn add_to_composite_value() {
|
||||
.unwrap();
|
||||
|
||||
// Read the point out of memory, validate it.
|
||||
let point_after: Point3D = mem.get_composite(start_addr).unwrap();
|
||||
let point_after: Point3d<f64> = mem.get_composite(start_addr).unwrap();
|
||||
assert_eq!(
|
||||
point_after,
|
||||
Point3D {
|
||||
Point3d {
|
||||
x: 42.0,
|
||||
y: 3.0,
|
||||
z: 4.0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_types() {
|
||||
let mut mem = Memory::default();
|
||||
let start_addr = Address(0);
|
||||
let id = ModelingCmdId(Uuid::parse_str("6306afa2-3999-4b03-af30-1efad7cdc6fc").unwrap());
|
||||
let p = Point3d {
|
||||
x: 2.0f64,
|
||||
y: 3.0,
|
||||
z: 4.0,
|
||||
};
|
||||
let val_in = ModelingCmd::MovePathPen(MovePathPen { path: id, to: p });
|
||||
mem.set_composite(val_in, start_addr);
|
||||
let val_out: ModelingCmd = mem.get_composite(start_addr).unwrap();
|
||||
match val_out {
|
||||
ModelingCmd::MovePathPen(params) => {
|
||||
assert_eq!(params.to, p);
|
||||
assert_eq!(params.path, id);
|
||||
}
|
||||
_ => panic!("unexpected ModelingCmd variant"),
|
||||
}
|
||||
}
|
||||
|
13
src/wasm-lib/execution-plan/src/value.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use crate::{ExecutionError, Primitive};
|
||||
|
||||
mod impls;
|
||||
|
||||
/// Types that can be written to or read from KCEP program memory.
|
||||
/// If they require multiple memory addresses, they will be laid out
|
||||
/// into multiple consecutive memory addresses.
|
||||
pub trait Value: Sized {
|
||||
/// Store the value in memory.
|
||||
fn into_parts(self) -> Vec<Primitive>;
|
||||
/// Read the value from memory.
|
||||
fn from_parts(values: &[Option<Primitive>]) -> Result<Self, ExecutionError>;
|
||||
}
|
98
src/wasm-lib/execution-plan/src/value/impls.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, MovePathPen};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Address, ExecutionError, Primitive};
|
||||
|
||||
use super::Value;
|
||||
|
||||
impl<T> Value for kittycad_modeling_cmds::shared::Point3d<T>
|
||||
where
|
||||
Primitive: From<T>,
|
||||
T: TryFrom<Primitive, Error = ExecutionError>,
|
||||
{
|
||||
fn into_parts(self) -> Vec<Primitive> {
|
||||
let points = [self.x, self.y, self.z];
|
||||
points.into_iter().map(|component| component.into()).collect()
|
||||
}
|
||||
|
||||
fn from_parts(values: &[Option<Primitive>]) -> Result<Self, ExecutionError> {
|
||||
let err = ExecutionError::MemoryWrongSize { expected: 3 };
|
||||
let [x, y, z] = [0, 1, 2].map(|n| values.get(n).ok_or(err.clone()));
|
||||
let x = x?.to_owned().ok_or(err.clone())?.try_into()?;
|
||||
let y = y?.to_owned().ok_or(err.clone())?.try_into()?;
|
||||
let z = z?.to_owned().ok_or(err.clone())?.try_into()?;
|
||||
Ok(Self { x, y, z })
|
||||
}
|
||||
}
|
||||
|
||||
const START_PATH: &str = "StartPath";
|
||||
const MOVE_PATH_PEN: &str = "MovePathPen";
|
||||
|
||||
impl Value for MovePathPen {
|
||||
fn into_parts(self) -> Vec<Primitive> {
|
||||
let MovePathPen { path, to } = self;
|
||||
let to = to.into_parts();
|
||||
let mut vals = Vec::with_capacity(1 + to.len());
|
||||
vals.push(Primitive::Uuid(path.into()));
|
||||
vals.extend(to);
|
||||
vals
|
||||
}
|
||||
|
||||
fn from_parts(values: &[Option<Primitive>]) -> Result<Self, ExecutionError> {
|
||||
let path = get_some(values, 0)?;
|
||||
let path = Uuid::try_from(path)?;
|
||||
let path = ModelingCmdId::from(path);
|
||||
let to = Point3d::from_parts(&values[1..])?;
|
||||
let params = MovePathPen { path, to };
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory layout for modeling commands:
|
||||
/// Field 0 is the command's name.
|
||||
/// Fields 1 onwards are the command's fields.
|
||||
impl Value for kittycad_modeling_cmds::ModelingCmd {
|
||||
fn into_parts(self) -> Vec<Primitive> {
|
||||
let (endpoint_name, params) = match self {
|
||||
kittycad_modeling_cmds::ModelingCmd::StartPath => (START_PATH, vec![]),
|
||||
kittycad_modeling_cmds::ModelingCmd::MovePathPen(MovePathPen { path, to }) => {
|
||||
let to = to.into_parts();
|
||||
let mut vals = Vec::with_capacity(1 + to.len());
|
||||
vals.push(Primitive::Uuid(path.into()));
|
||||
vals.extend(to);
|
||||
(MOVE_PATH_PEN, vals)
|
||||
}
|
||||
_ => todo!(),
|
||||
};
|
||||
let mut out = Vec::with_capacity(params.len() + 1);
|
||||
out.push(endpoint_name.to_owned().into());
|
||||
out.extend(params);
|
||||
out
|
||||
}
|
||||
|
||||
fn from_parts(values: &[Option<Primitive>]) -> Result<Self, ExecutionError> {
|
||||
// Check the array has an element at index 0
|
||||
let first_memory = values
|
||||
.get(0)
|
||||
.ok_or(ExecutionError::MemoryWrongSize { expected: 1 })?
|
||||
.to_owned();
|
||||
// The element should be Some
|
||||
let first_memory = first_memory.ok_or(ExecutionError::MemoryWrongSize { expected: 1 })?;
|
||||
let endpoint_name: String = first_memory.try_into()?;
|
||||
match endpoint_name.as_str() {
|
||||
START_PATH => Ok(Self::StartPath),
|
||||
MOVE_PATH_PEN => {
|
||||
let params = MovePathPen::from_parts(&values[1..])?;
|
||||
Ok(Self::MovePathPen(params))
|
||||
}
|
||||
other => Err(ExecutionError::UnrecognizedEndpoint(other.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_some(values: &[Option<Primitive>], i: usize) -> Result<Primitive, ExecutionError> {
|
||||
let addr = Address(0); // TODO: pass the `start` addr in
|
||||
let v = values.get(i).ok_or(ExecutionError::MemoryEmpty { addr })?.to_owned();
|
||||
let v = v.ok_or(ExecutionError::MemoryEmpty { addr })?.to_owned();
|
||||
Ok(v)
|
||||
}
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 93 KiB |
@ -1,3 +1,5 @@
|
||||
const plugin = require('tailwindcss/plugin')
|
||||
|
||||
const themeColorRamps = [
|
||||
{ name: 'chalkboard', stops: 12 },
|
||||
{ name: 'energy', stops: 12 },
|
||||
@ -10,27 +12,23 @@ const themeColorRamps = [
|
||||
{ name: 'warn', stops: 8 },
|
||||
{ name: 'succeed', stops: 8 },
|
||||
]
|
||||
const toOKLCHVar = val => `oklch(var(${val}) / <alpha-value>) `
|
||||
const toOKLCHVar = (val) => `oklch(var(${val}) / <alpha-value>) `
|
||||
|
||||
const themeColors = Object.fromEntries(
|
||||
themeColorRamps.map(({name, stops}) => [
|
||||
name,
|
||||
Object.fromEntries(
|
||||
new Array(stops)
|
||||
.fill(0)
|
||||
.map((_, i) => [
|
||||
(i + 1) * 10,
|
||||
toOKLCHVar(`--_${name}-${(i + 1) * 10}`),
|
||||
])
|
||||
),
|
||||
themeColorRamps.map(({ name, stops }) => [
|
||||
name,
|
||||
Object.fromEntries(
|
||||
new Array(stops)
|
||||
.fill(0)
|
||||
.map((_, i) => [(i + 1) * 10, toOKLCHVar(`--_${name}-${(i + 1) * 10}`)])
|
||||
),
|
||||
])
|
||||
)
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
mode: 'jit',
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
@ -41,5 +39,22 @@ module.exports = {
|
||||
darkMode: 'class',
|
||||
plugins: [
|
||||
require('@headlessui/tailwindcss'),
|
||||
// custom plugin to add variants for aria-pressed
|
||||
// To use, just add a class of 'group-pressed:<some-tailwind-class>' or 'pressed:<some-tailwind-class>'
|
||||
// to your element. Based on https://dev.to/philw_/tying-tailwind-styling-to-aria-attributes-502f
|
||||
plugin(function ({ addVariant, e }) {
|
||||
addVariant('group-pressed', ({ modifySelectors, separator }) => {
|
||||
modifySelectors(({ className }) => {
|
||||
return `.group[aria-pressed='true'] .${e(
|
||||
`group-pressed${separator}${className}`
|
||||
)}`
|
||||
})
|
||||
})
|
||||
addVariant('pressed', ({ modifySelectors, separator }) => {
|
||||
modifySelectors(({ className }) => {
|
||||
return `.${e(`pressed${separator}${className}`)}[aria-pressed='true']`
|
||||
})
|
||||
})
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
@ -7698,10 +7698,10 @@ swr@^2.2.2:
|
||||
client-only "^0.0.1"
|
||||
use-sync-external-store "^1.2.0"
|
||||
|
||||
tailwindcss@^3.3.5:
|
||||
version "3.3.5"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.5.tgz#22a59e2fbe0ecb6660809d9cc5f3976b077be3b8"
|
||||
integrity sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==
|
||||
tailwindcss@^3.3.6:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.6.tgz#4dd7986bf4902ad385d90d45fd4b2fa5fab26d5f"
|
||||
integrity sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==
|
||||
dependencies:
|
||||
"@alloc/quick-lru" "^5.2.0"
|
||||
arg "^5.0.2"
|
||||
|