cadcode 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -2
- package/client/index.html +51 -0
- package/client/viewer.css +216 -0
- package/client/viewer.js +308 -0
- package/package.json +34 -4
- package/src/cli.js +96 -0
- package/src/core/mesh.js +69 -0
- package/src/core/program.js +118 -0
- package/src/dev/server.js +172 -0
- package/src/dev/worker.js +39 -0
- package/src/export/png.js +52 -0
- package/src/export/raster.js +207 -0
- package/src/export/stl.js +28 -0
- package/src/lib/colors.js +56 -0
- package/src/lib/index.js +25 -0
- package/src/lib/shapes.js +489 -0
- package/src/lib/util.js +24 -0
package/README.md
CHANGED
|
@@ -1,3 +1,215 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cadcode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Code-first CAD. Write a model in JavaScript, see it live in the browser while you edit, and export STL for printing and PNG for previews. There is no modeling UI: your editor is the CAD system.
|
|
4
|
+
|
|
5
|
+
The geometry runs on [manifold](https://github.com/elalish/manifold), the same boolean engine recent OpenSCAD uses, so solids always come out watertight. The API keeps OpenSCAD's ideas (cube, cylinder, hull, difference, `$fn`), so OpenSCAD models port line by line.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { cube, cylinder, difference } from 'cadcode';
|
|
9
|
+
|
|
10
|
+
export default difference(
|
|
11
|
+
cube([20, 20, 5], { center: true }),
|
|
12
|
+
cylinder({ d: 6, h: 10, center: true }),
|
|
13
|
+
);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx cadcode dev plate.js
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install cadcode
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Node 20 or newer.
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cadcode dev <file> # live preview in the browser, reloads on save
|
|
32
|
+
cadcode stl <file> # export a part to binary STL
|
|
33
|
+
cadcode png <file> # render a part to a PNG preview
|
|
34
|
+
cadcode parts <file> # list the parts a program defines
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Option | Applies to | Meaning |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `-p, --part <name>` | all | Part to use. The default is the first one. |
|
|
40
|
+
| `-a, --all` | stl, png | Export every part, one file each. |
|
|
41
|
+
| `-s, --set <key=value>` | all | Override a parameter. Repeatable. Values are parsed as JSON, falling back to strings. |
|
|
42
|
+
| `-o, --out <path>` | stl, png | Output file, or a directory with `--all`. The default is `<name>.stl` or `<name>-<part>.stl` in the current directory. |
|
|
43
|
+
| `--view <name>` | png | `iso` (default), `iso-back`, `front`, `back`, `left`, `right`, `top`, `bottom`, or `sheet` for iso, front, right and top in one image. |
|
|
44
|
+
| `--size <WxH>` | png | Image size. The default is `800x600`. |
|
|
45
|
+
| `--port <n>` | dev | The default is `5173`. |
|
|
46
|
+
| `--host <addr>` | dev | The default is `127.0.0.1`. |
|
|
47
|
+
| `--open` | dev | Open the browser. |
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cadcode stl case.js --part box --set wall=2.4
|
|
51
|
+
cadcode stl case.js --all -o out/
|
|
52
|
+
cadcode png case.js --part assembly --view sheet --size 1600x1200
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### The dev viewer
|
|
56
|
+
|
|
57
|
+
`cadcode dev` watches the program and every file it imports. On each save it rebuilds the model and swaps it into the page without touching the camera, so you keep your angle, position and zoom while you iterate. The camera is also remembered per file across browser refreshes and server restarts.
|
|
58
|
+
|
|
59
|
+
- **Rotate**: drag
|
|
60
|
+
- **Pan**: shift+drag or right-drag
|
|
61
|
+
- **Zoom**: mouse wheel
|
|
62
|
+
- **Fit the model to the view**: F, or the Fit button
|
|
63
|
+
- **Switch parts**: the part dropdown
|
|
64
|
+
|
|
65
|
+
Errors appear in the page and in the terminal, with the line in your file. The last good model stays on screen until you fix them. Whatever the program prints with `console.log` appears in the corner of the page.
|
|
66
|
+
|
|
67
|
+
## Writing a program
|
|
68
|
+
|
|
69
|
+
A program is an ES module. Its default export is one of these:
|
|
70
|
+
|
|
71
|
+
- **A model**: a solid, or an array of solids. Arrays may nest.
|
|
72
|
+
- **An object of named parts**: each value is a model, or a function that returns one. Functions are only called for the part being shown or exported.
|
|
73
|
+
- **A function of `params`**: it returns either of the above.
|
|
74
|
+
|
|
75
|
+
Parameters you might want to change from the command line go in `export const params`. `--set` can only override a parameter declared there, so a typo is an error instead of being silently ignored.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import { cube, cylinder, hull, product, setResolution } from 'cadcode';
|
|
79
|
+
|
|
80
|
+
setResolution({ fn: 48 });
|
|
81
|
+
|
|
82
|
+
export const params = {
|
|
83
|
+
width: 40,
|
|
84
|
+
depth: 30,
|
|
85
|
+
height: 12,
|
|
86
|
+
wall: 2,
|
|
87
|
+
radius: 3,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default (p) => {
|
|
91
|
+
const roundedBox = (w, d, h, r) =>
|
|
92
|
+
hull(product([r, w - r], [r, d - r]).map(([x, y]) => cylinder({ r, h }).translate([x, y, 0])));
|
|
93
|
+
|
|
94
|
+
const tray = () =>
|
|
95
|
+
roundedBox(p.width, p.depth, p.height, p.radius).subtract(
|
|
96
|
+
roundedBox(p.width - 2 * p.wall, p.depth - 2 * p.wall, p.height, p.radius - p.wall).translate([p.wall, p.wall, p.wall]),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
assembly: () => [tray().color('steelblue'), cube([10, 10, 4]).translate([5, 5, p.wall]).color('orange', 0.6)],
|
|
101
|
+
tray,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The first part is the default one. The STL of a part merges all its solids into one body. In the viewer and in PNGs each solid keeps its own color and transparency, which is what assembly views are for.
|
|
107
|
+
|
|
108
|
+
A program can import its own helper files and npm packages like any module. TypeScript files work too.
|
|
109
|
+
|
|
110
|
+
[`examples/sensor-case.js`](examples/sensor-case.js) is a complete project: a snap-fit case for a board, a battery and a sensor, with `assembly`, `box`, `lid` and `print` parts.
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
Every shape is immutable: methods return a new shape. Units are whatever your slicer assumes, usually millimeters. Angles are in degrees.
|
|
115
|
+
|
|
116
|
+
Wherever a list of shapes is accepted, it may contain nested arrays and falsy values. That makes loops and conditionals read naturally:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
difference(base, holes.map(([x, y]) => cylinder({ d: 3, h: 10 }).translate([x, y, -1])), withSlot && slot);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3D primitives
|
|
123
|
+
|
|
124
|
+
| Function | Notes |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `cube(size, { center })` | `size` is a number or `[x, y, z]`. A corner sits at the origin unless `center: true`. |
|
|
127
|
+
| `cylinder({ h, r \| d, r1, r2, d1, d2, center, fn })` | Stands on XY along +Z. Give `r1`/`r2` for a cone. |
|
|
128
|
+
| `sphere(r)` or `sphere({ r \| d, fn })` | Centered on the origin. |
|
|
129
|
+
| `polyhedron(points, faces)` | Faces list point indices clockwise when seen from outside, as in OpenSCAD. |
|
|
130
|
+
|
|
131
|
+
### 2D primitives
|
|
132
|
+
|
|
133
|
+
| Function | Notes |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `square(size, { center })` | `size` is a number or `[x, y]`. |
|
|
136
|
+
| `circle(r)` or `circle({ r \| d, fn })` | |
|
|
137
|
+
| `polygon(points, paths?)` | Paths inside other paths become holes. |
|
|
138
|
+
|
|
139
|
+
### Solid methods
|
|
140
|
+
|
|
141
|
+
| Method | Notes |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `.translate([x, y, z])` | |
|
|
144
|
+
| `.rotate([x, y, z])` | Around X, then Y, then Z, like OpenSCAD. `.rotate(a)` turns around Z, and `.rotate(a, [x, y, z])` around any axis. |
|
|
145
|
+
| `.scale(s)` or `.scale([x, y, z])` | |
|
|
146
|
+
| `.mirror([nx, ny, nz])` | Across the plane through the origin with this normal. |
|
|
147
|
+
| `.transform(matrix)` | A column-major 4×4 matrix, as 16 numbers. |
|
|
148
|
+
| `.color(name, alpha?)` | A CSS color name, `#rrggbb`, or `[r, g, b, a]` in 0..1. Colors survive booleans. |
|
|
149
|
+
| `.union(...shapes)` | |
|
|
150
|
+
| `.subtract(...shapes)` | |
|
|
151
|
+
| `.intersect(...shapes)` | |
|
|
152
|
+
| `.hull(...shapes)` | Convex hull of this shape and the others. |
|
|
153
|
+
| `.slice(z)` | The 2D cross-section at height `z`. |
|
|
154
|
+
| `.project()` | The 2D outline seen from above. |
|
|
155
|
+
| `.volume()`, `.surfaceArea()`, `.bounds()`, `.isEmpty()` | `bounds()` returns `{ min: [x, y, z], max: [x, y, z] }`. |
|
|
156
|
+
|
|
157
|
+
### 2D shape methods
|
|
158
|
+
|
|
159
|
+
| Method | Notes |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `.translate([x, y])`, `.rotate(a)`, `.scale(s)`, `.mirror([nx, ny])` | |
|
|
162
|
+
| `.offset(r)` | Grow (or shrink, when negative) with rounded corners. `.offset({ delta })` keeps corners sharp, and `.offset({ delta, chamfer: true })` cuts them. |
|
|
163
|
+
| `.union()`, `.subtract()`, `.intersect()`, `.hull()` | Same as for solids. |
|
|
164
|
+
| `.extrude(h, { center, twist, scale, divisions })` | Extrudes along +Z, like `linear_extrude`. |
|
|
165
|
+
| `.revolve({ angle, fn })` | Revolves the shape around the Y axis into a solid around Z, like `rotate_extrude`. |
|
|
166
|
+
| `.area()`, `.bounds()`, `.isEmpty()`, `.toPolygons()` | |
|
|
167
|
+
|
|
168
|
+
### Functions
|
|
169
|
+
|
|
170
|
+
These take any mix of shapes and nested arrays, so OpenSCAD's nesting style works as well as method chaining:
|
|
171
|
+
|
|
172
|
+
| Function | Equivalent |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `union(...shapes)` | |
|
|
175
|
+
| `difference(base, ...cutters)` | `base.subtract(...cutters)` |
|
|
176
|
+
| `intersection(...shapes)` | |
|
|
177
|
+
| `hull(...shapes)` | |
|
|
178
|
+
| `translate(v, ...shapes)` | `union(...shapes).translate(v)` |
|
|
179
|
+
| `rotate(a, ...shapes)` | `union(...shapes).rotate(a)` |
|
|
180
|
+
| `scale(v, ...shapes)` | `union(...shapes).scale(v)` |
|
|
181
|
+
| `mirror(n, ...shapes)` | `union(...shapes).mirror(n)` |
|
|
182
|
+
| `color(c, alpha?, ...shapes)` | `union(...shapes).color(c, alpha)` |
|
|
183
|
+
| `offset(r, ...shapes)` | `union(...shapes).offset(r)` |
|
|
184
|
+
| `linearExtrude(h, options, ...shapes)` | `union(...shapes).extrude(h, options)` |
|
|
185
|
+
|
|
186
|
+
### Helpers
|
|
187
|
+
|
|
188
|
+
| Function | Notes |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `range(start, end, step = 1)` | Includes `end`, like OpenSCAD's `[start : step : end]`. |
|
|
191
|
+
| `product(...lists)` | Every combination, like `for (x = xs, y = ys)`: `product([1, 2], ['a', 'b'])` gives `[[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]`. |
|
|
192
|
+
| `setResolution({ fn, fa, fs })` | Default segment count for curves, with the same rules as OpenSCAD's `$fn`, `$fa` and `$fs`. Any primitive also takes its own `fn`. |
|
|
193
|
+
|
|
194
|
+
## Porting from OpenSCAD
|
|
195
|
+
|
|
196
|
+
| OpenSCAD | cadcode |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| `$fn = 40;` | `setResolution({ fn: 40 });` |
|
|
199
|
+
| `cylinder(d = 5, h = 2, center = true);` | `cylinder({ d: 5, h: 2, center: true })` |
|
|
200
|
+
| `translate([x, y, 0]) rotate([0, 90, 0]) cube(s);` | `cube(s).rotate([0, 90, 0]).translate([x, y, 0])` |
|
|
201
|
+
| `difference() { a(); b(); c(); }` | `difference(a(), b(), c())` |
|
|
202
|
+
| `for (x = xs, y = ys) translate([x, y]) c();` | `product(xs, ys).map(([x, y]) => c().translate([x, y]))` |
|
|
203
|
+
| `for (i = [0 : n - 1])` | `range(0, n - 1).map((i) => ...)` |
|
|
204
|
+
| `linear_extrude(h) offset(r = 1) square(s, center = true);` | `square(s, { center: true }).offset(1).extrude(h)` |
|
|
205
|
+
| `color("orange", 0.35) lid();` | `lid().color('orange', 0.35)` |
|
|
206
|
+
| `module box() { ... }` | `const box = () => ...` |
|
|
207
|
+
| `part = "box";` with `-D 'part="box"'` | Named parts and `--part box` |
|
|
208
|
+
| `wall = 2;` with `-D wall=2.4` | `export const params = { wall: 2 }` and `--set wall=2.4` |
|
|
209
|
+
| `echo(...)` | `console.log(...)` |
|
|
210
|
+
|
|
211
|
+
When chaining, read OpenSCAD's transforms from the innermost one out. `translate(t) rotate(r) child` becomes `child.rotate(r).translate(t)`.
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>cadcode</title>
|
|
7
|
+
<link rel="stylesheet" href="/viewer.css" />
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"three": "/vendor/three/build/three.module.js",
|
|
12
|
+
"three/addons/": "/vendor/three/examples/jsm/"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<canvas id="view"></canvas>
|
|
19
|
+
|
|
20
|
+
<div class="top">
|
|
21
|
+
<header class="bar">
|
|
22
|
+
<span class="brand">cadcode</span>
|
|
23
|
+
<span id="file" class="file"
|
|
24
|
+
><span class="dir"></span><span class="name"></span
|
|
25
|
+
></span>
|
|
26
|
+
<label class="part">
|
|
27
|
+
<span>part</span>
|
|
28
|
+
<select id="part"></select>
|
|
29
|
+
</label>
|
|
30
|
+
<span id="status" class="status" data-state="connecting"
|
|
31
|
+
>connecting</span
|
|
32
|
+
>
|
|
33
|
+
<button id="fit" type="button" title="Fit model to view (F)">
|
|
34
|
+
Fit
|
|
35
|
+
</button>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<section id="logs" class="logs" hidden></section>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<section id="error" class="error" hidden>
|
|
42
|
+
<pre id="error-text"></pre>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<footer class="hint">
|
|
46
|
+
drag: rotate · shift+drag or right-drag: pan · wheel: zoom · F: fit
|
|
47
|
+
</footer>
|
|
48
|
+
|
|
49
|
+
<script type="module" src="/viewer.js"></script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #eef0f3;
|
|
3
|
+
--panel: rgba(255, 255, 255, 0.86);
|
|
4
|
+
--text: #1d2228;
|
|
5
|
+
--muted: #6b7480;
|
|
6
|
+
--border: rgba(20, 30, 40, 0.12);
|
|
7
|
+
--ok: #2f9e5b;
|
|
8
|
+
--busy: #c98a12;
|
|
9
|
+
--bad: #c8372d;
|
|
10
|
+
font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
11
|
+
color: var(--text);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
* {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
html,
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
height: 100%;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: var(--bg);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#view {
|
|
27
|
+
position: fixed;
|
|
28
|
+
inset: 0;
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
display: block;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Toolbar on the left, program output on the right; they share one row so they never overlap. */
|
|
35
|
+
.top {
|
|
36
|
+
position: fixed;
|
|
37
|
+
top: 12px;
|
|
38
|
+
left: 12px;
|
|
39
|
+
right: 12px;
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
align-items: flex-start;
|
|
43
|
+
gap: 12px;
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.top > * {
|
|
48
|
+
pointer-events: auto;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.bar {
|
|
52
|
+
flex: 0 1 auto;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 12px;
|
|
57
|
+
padding: 6px 8px 6px 12px;
|
|
58
|
+
background: var(--panel);
|
|
59
|
+
backdrop-filter: blur(8px);
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
border-radius: 10px;
|
|
62
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.bar > :not(.file) {
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.brand {
|
|
70
|
+
font-weight: 650;
|
|
71
|
+
letter-spacing: 0.02em;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.file {
|
|
75
|
+
display: flex;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
color: var(--muted);
|
|
78
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
79
|
+
font-size: 12px;
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* A long path gives up its directory first; the file name always stays. */
|
|
84
|
+
.file .dir {
|
|
85
|
+
min-width: 0;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
text-overflow: ellipsis;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.file .name {
|
|
91
|
+
flex-shrink: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.part {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 6px;
|
|
98
|
+
color: var(--muted);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
select,
|
|
102
|
+
button {
|
|
103
|
+
font: inherit;
|
|
104
|
+
color: var(--text);
|
|
105
|
+
background: #fff;
|
|
106
|
+
border: 1px solid var(--border);
|
|
107
|
+
border-radius: 6px;
|
|
108
|
+
padding: 3px 8px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
button {
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
button:hover,
|
|
116
|
+
select:hover {
|
|
117
|
+
border-color: rgba(20, 30, 40, 0.28);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.status {
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 6px;
|
|
124
|
+
color: var(--muted);
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
font-variant-numeric: tabular-nums;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.status::before {
|
|
130
|
+
content: '';
|
|
131
|
+
width: 8px;
|
|
132
|
+
height: 8px;
|
|
133
|
+
border-radius: 50%;
|
|
134
|
+
background: var(--muted);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.status[data-state='ready']::before {
|
|
138
|
+
background: var(--ok);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.status[data-state='building']::before,
|
|
142
|
+
.status[data-state='evaluating']::before {
|
|
143
|
+
background: var(--busy);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.status[data-state='error']::before,
|
|
147
|
+
.status[data-state='disconnected']::before {
|
|
148
|
+
background: var(--bad);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.error {
|
|
152
|
+
position: fixed;
|
|
153
|
+
left: 12px;
|
|
154
|
+
right: 12px;
|
|
155
|
+
bottom: 40px;
|
|
156
|
+
max-height: 45vh;
|
|
157
|
+
overflow: auto;
|
|
158
|
+
background: #fff6f5;
|
|
159
|
+
border: 1px solid rgba(200, 55, 45, 0.35);
|
|
160
|
+
border-left: 4px solid var(--bad);
|
|
161
|
+
border-radius: 8px;
|
|
162
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.error pre {
|
|
166
|
+
margin: 0;
|
|
167
|
+
padding: 12px 14px;
|
|
168
|
+
font: 12px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
|
|
169
|
+
white-space: pre-wrap;
|
|
170
|
+
color: #7a1d16;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.logs {
|
|
174
|
+
flex: 0 1 auto;
|
|
175
|
+
min-width: 0;
|
|
176
|
+
max-width: min(420px, 40vw);
|
|
177
|
+
max-height: 40vh;
|
|
178
|
+
overflow: auto;
|
|
179
|
+
padding: 8px 10px;
|
|
180
|
+
background: var(--panel);
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 10px;
|
|
183
|
+
font: 12px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
|
|
184
|
+
white-space: pre-wrap;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.logs .log-warn {
|
|
188
|
+
color: var(--busy);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.logs .log-error {
|
|
192
|
+
color: var(--bad);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.hint {
|
|
196
|
+
position: fixed;
|
|
197
|
+
left: 12px;
|
|
198
|
+
bottom: 12px;
|
|
199
|
+
color: var(--muted);
|
|
200
|
+
font-size: 12px;
|
|
201
|
+
pointer-events: none;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@media (max-width: 720px) {
|
|
205
|
+
.file,
|
|
206
|
+
.hint {
|
|
207
|
+
display: none;
|
|
208
|
+
}
|
|
209
|
+
.logs {
|
|
210
|
+
position: fixed;
|
|
211
|
+
left: 12px;
|
|
212
|
+
right: 12px;
|
|
213
|
+
bottom: 40px;
|
|
214
|
+
max-width: none;
|
|
215
|
+
}
|
|
216
|
+
}
|