@tinyvolt/pie 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/.nvmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/functions/midpoint.ts +29 -0
- package/functions/square.ts +20 -0
- package/init/index.js +87 -0
- package/init/site-agents.md +253 -0
- package/init/site-gitignore +28 -0
- package/init/site-tsconfig.json +20 -0
- package/init/site-vite.config.ts +6 -0
- package/layouts/header.tsx +16 -0
- package/layouts/index.tsx +10 -0
- package/nodes/square.tsx +36 -0
- package/package.json +59 -4
- package/pages/example.md +7 -0
- package/pages/python-editor.md +29 -0
- package/public/favicon.ico +0 -0
- package/public/logo.svg +5 -0
- package/site.yml +1 -0
- package/src/app.css +343 -0
- package/src/app.tsx +30 -0
- package/src/authorStyles.ts +3 -0
- package/src/components/Header.tsx +16 -0
- package/src/components/PageTitle.tsx +10 -0
- package/src/components/SplitPane.tsx +170 -0
- package/src/components/icons.tsx +22 -0
- package/src/entry-client.tsx +4 -0
- package/src/entry-server.tsx +21 -0
- package/src/global.d.ts +7 -0
- package/src/lib/appDir.ts +4 -0
- package/src/lib/autodiff/index.ts +49 -0
- package/src/lib/layouts/layout.ts +20 -0
- package/src/lib/layouts/vitePlugin.ts +39 -0
- package/src/lib/markdown/components/CanvasPage.tsx +25 -0
- package/src/lib/markdown/components/MarkdownBody.tsx +41 -0
- package/src/lib/markdown/components/PythonEditor.tsx +166 -0
- package/src/lib/markdown/frontmatter.ts +81 -0
- package/src/lib/markdown/pages.ts +23 -0
- package/src/lib/markdown/render.ts +54 -0
- package/src/lib/markdown/vitePlugin.ts +94 -0
- package/src/lib/nodes/components/CenteredGrid.tsx +59 -0
- package/src/lib/nodes/components/NodeCanvas.tsx +58 -0
- package/src/lib/nodes/execute.ts +166 -0
- package/src/lib/nodes/functions/Node2NodeFunc.ts +307 -0
- package/src/lib/nodes/functions/array.ts +36 -0
- package/src/lib/nodes/functions/autodiff.ts +64 -0
- package/src/lib/nodes/functions/basic.ts +298 -0
- package/src/lib/nodes/functions/registry.ts +46 -0
- package/src/lib/nodes/functions/sideEffects.ts +193 -0
- package/src/lib/nodes/functions/vitePlugin.ts +40 -0
- package/src/lib/nodes/hooks/useCanvasResize.ts +38 -0
- package/src/lib/nodes/jax.ts +191 -0
- package/src/lib/nodes/render/renderLogic.tsx +306 -0
- package/src/lib/nodes/state/NameGenerator.ts +31 -0
- package/src/lib/nodes/state/Node.ts +211 -0
- package/src/lib/nodes/state/NodeStore.ts +110 -0
- package/src/lib/nodes/tensors/broadcast.ts +101 -0
- package/src/lib/nodes/tensors/tensor.ts +42 -0
- package/src/lib/python/lift.ts +105 -0
- package/src/lib/python/py/__pycache__/pie.cpython-314.pyc +0 -0
- package/src/lib/python/py/pie.py +231 -0
- package/src/lib/python/py/pie_mpl.py +15 -0
- package/src/lib/python/py/show_figures.py +5 -0
- package/src/lib/python/runner.ts +202 -0
- package/src/lib/python/worker.ts +109 -0
- package/src/lib/site/vitePlugin.ts +43 -0
- package/src/routes/[...slug].tsx +38 -0
- package/src/routes/about.tsx +10 -0
- package/src/routes/index.tsx +14 -0
- package/src/routes/pages/index.tsx +20 -0
- package/styles/theme.css +27 -0
- package/vite.ts +61 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Python editor
|
|
3
|
+
canvas: left
|
|
4
|
+
---
|
|
5
|
+
# Python editor
|
|
6
|
+
|
|
7
|
+
This is plain python code created using \`\`\`python ... \`\`\` code fence:
|
|
8
|
+
```python
|
|
9
|
+
A = pie.point(1, 2)
|
|
10
|
+
print(A)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Below are python editors created using \`\`\`python-editor ... \`\`\` code fence:
|
|
16
|
+
|
|
17
|
+
```python-editor
|
|
18
|
+
a = pie.point(1, 2, out='a')
|
|
19
|
+
r = pie.scalar(1, out='r')
|
|
20
|
+
c = pie.circle(a, r)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python-editor
|
|
24
|
+
a = pie.point(0, 0, out='a')
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python-editor
|
|
28
|
+
pie.scalar(5, out='r')
|
|
29
|
+
```
|
|
Binary file
|
package/public/logo.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 10.5 64 64" width="256" height="256">
|
|
2
|
+
<path d="M32 29 L8 29 A24 24 0 0 0 20 49.78 Z" fill="#41DBC9"/>
|
|
3
|
+
<path d="M32 29 L44 49.78 A24 24 0 0 0 56 29 Z" fill="#41DBC9"/>
|
|
4
|
+
<path d="M32 34 L20 54.78 A24 24 0 0 0 44 54.78 Z" fill="#0B6B73"/>
|
|
5
|
+
</svg>
|
package/site.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
title: pie-ssg
|
package/src/app.css
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
color: var(--color-text, #1a1a1a);
|
|
4
|
+
background: var(--color-bg, #fff);
|
|
5
|
+
font-family: var(--font-ui, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
6
|
+
line-height: var(--line-height, 1.6);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
a {
|
|
10
|
+
color: inherit;
|
|
11
|
+
text-underline-offset: 0.15em;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#app {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
min-height: 100dvh;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.site-header {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: baseline;
|
|
23
|
+
gap: 1.5rem;
|
|
24
|
+
padding: 0.75rem 1.5rem;
|
|
25
|
+
border-bottom: 1px solid var(--color-border, #ddd);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.site-header-title {
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
text-decoration: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.site-header-nav {
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.site-header-nav a {
|
|
39
|
+
color: var(--color-muted, #555);
|
|
40
|
+
text-decoration: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.site-header-nav a:hover {
|
|
44
|
+
color: var(--color-hover, #000);
|
|
45
|
+
text-decoration: underline;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Page text: a centered column about 70 characters wide. */
|
|
49
|
+
main {
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
width: 100%;
|
|
52
|
+
max-width: var(--content-width, 40rem);
|
|
53
|
+
margin: 0 auto;
|
|
54
|
+
padding: 2rem 1.5rem 4rem;
|
|
55
|
+
font-family: var(--font-body, Georgia, "Times New Roman", serif);
|
|
56
|
+
font-size: var(--font-size-body, 1.125rem);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
main h1,
|
|
60
|
+
main h2,
|
|
61
|
+
main h3,
|
|
62
|
+
main h4 {
|
|
63
|
+
line-height: 1.25;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main h1 {
|
|
67
|
+
font-size: 2rem;
|
|
68
|
+
margin: 0 0 1rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main h2 {
|
|
72
|
+
font-size: 1.4rem;
|
|
73
|
+
margin: 2rem 0 0.75rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
main h3 {
|
|
77
|
+
font-size: 1.15rem;
|
|
78
|
+
margin: 1.5rem 0 0.5rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main p,
|
|
82
|
+
main ul,
|
|
83
|
+
main ol,
|
|
84
|
+
main blockquote,
|
|
85
|
+
main table {
|
|
86
|
+
margin: 0 0 1rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main li + li {
|
|
90
|
+
margin-top: 0.25rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main hr {
|
|
94
|
+
margin: 2rem 0;
|
|
95
|
+
border: 0;
|
|
96
|
+
border-top: 1px solid var(--color-border, #ddd);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main blockquote {
|
|
100
|
+
padding-left: 1rem;
|
|
101
|
+
border-left: 3px solid var(--color-border, #ddd);
|
|
102
|
+
color: var(--color-muted, #555);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main img {
|
|
106
|
+
max-width: 100%;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main code {
|
|
110
|
+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
111
|
+
font-size: 0.85em;
|
|
112
|
+
padding: 0.1em 0.3em;
|
|
113
|
+
background: var(--color-code-bg, #f4f4f4);
|
|
114
|
+
border-radius: 3px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main > pre {
|
|
118
|
+
margin: 0 0 1rem;
|
|
119
|
+
padding: 0.75rem 1rem;
|
|
120
|
+
overflow-x: auto;
|
|
121
|
+
background: var(--color-code-bg, #f4f4f4);
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
line-height: 1.5;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main pre code {
|
|
127
|
+
padding: 0;
|
|
128
|
+
background: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main table {
|
|
132
|
+
border-collapse: collapse;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main th,
|
|
136
|
+
main td {
|
|
137
|
+
padding: 0.3rem 0.75rem;
|
|
138
|
+
border-bottom: 1px solid var(--color-border, #ddd);
|
|
139
|
+
text-align: left;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main .katex-display {
|
|
143
|
+
overflow-x: auto;
|
|
144
|
+
overflow-y: hidden;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.split-pane {
|
|
148
|
+
display: flex;
|
|
149
|
+
min-width: 0;
|
|
150
|
+
min-height: 0;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.split-pane--left {
|
|
155
|
+
flex-direction: row;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.split-pane--right {
|
|
159
|
+
flex-direction: row-reverse;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.split-pane--top {
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.split-pane--bottom {
|
|
167
|
+
flex-direction: column-reverse;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Each pane scrolls on its own. */
|
|
171
|
+
.split-pane-pane {
|
|
172
|
+
position: relative;
|
|
173
|
+
min-width: 0;
|
|
174
|
+
min-height: 0;
|
|
175
|
+
overflow: auto;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Draggable divider: a thin bar along whichever axis the split uses. */
|
|
179
|
+
.split-pane-divider {
|
|
180
|
+
position: relative;
|
|
181
|
+
flex: 0 0 6px;
|
|
182
|
+
align-self: stretch;
|
|
183
|
+
background: var(--color-border, #ddd);
|
|
184
|
+
cursor: col-resize;
|
|
185
|
+
z-index: 5;
|
|
186
|
+
transition: background 0.15s ease;
|
|
187
|
+
touch-action: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.split-pane-divider:hover,
|
|
191
|
+
.split-pane-divider:active {
|
|
192
|
+
background: var(--color-border-hover, #999);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.split-pane-divider--horizontal {
|
|
196
|
+
cursor: row-resize;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.split-pane-buttons {
|
|
200
|
+
position: absolute;
|
|
201
|
+
top: 50%;
|
|
202
|
+
left: 50%;
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-direction: column;
|
|
205
|
+
gap: 0.3rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.split-pane-divider--horizontal .split-pane-buttons {
|
|
209
|
+
flex-direction: row;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.split-pane-button {
|
|
213
|
+
display: inline-flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
justify-content: center;
|
|
216
|
+
width: 1.25rem;
|
|
217
|
+
height: 1.25rem;
|
|
218
|
+
padding: 0;
|
|
219
|
+
background: var(--color-bg, #fff);
|
|
220
|
+
border: 1px solid var(--color-border, #ddd);
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
color: var(--color-subtle, #666);
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.split-pane-button:hover {
|
|
227
|
+
color: var(--color-hover, #000);
|
|
228
|
+
border-color: var(--color-border-hover, #999);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* A canvas page fills the screen below the header. */
|
|
232
|
+
#app:has(.canvas-page) {
|
|
233
|
+
height: 100dvh;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.canvas-page {
|
|
237
|
+
flex: 1 1 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.canvas-page-canvas {
|
|
241
|
+
height: 100%;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* NodeCanvas measures this box to size its grid. */
|
|
245
|
+
.node-canvas {
|
|
246
|
+
width: 100%;
|
|
247
|
+
height: 100%;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.canvas-page-canvas svg {
|
|
251
|
+
display: block;
|
|
252
|
+
width: 100%;
|
|
253
|
+
height: 100%;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Hidden until MarkdownBody swaps it for the editor; keeps its space so the text below doesn't jump. */
|
|
257
|
+
pre:has(> code.language-python-editor) {
|
|
258
|
+
visibility: hidden;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.python-editor {
|
|
262
|
+
margin: 1rem 0;
|
|
263
|
+
border: 1px solid var(--color-border, #ddd);
|
|
264
|
+
border-radius: 4px;
|
|
265
|
+
font-family: var(--font-ui, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.python-editor .cm-editor {
|
|
269
|
+
font-size: 0.875rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.python-editor-toolbar {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
gap: 0.5rem;
|
|
276
|
+
padding: 0.25rem 0.5rem;
|
|
277
|
+
border-top: 1px solid var(--color-border, #ddd);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.python-editor-button {
|
|
281
|
+
width: 1.75rem;
|
|
282
|
+
height: 1.75rem;
|
|
283
|
+
padding: 0;
|
|
284
|
+
background: var(--color-bg, #fff);
|
|
285
|
+
border: 1px solid var(--color-border, #ddd);
|
|
286
|
+
border-radius: 4px;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.python-editor-button:hover {
|
|
291
|
+
border-color: var(--color-border-hover, #999);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.python-editor-status {
|
|
295
|
+
color: var(--color-subtle, #666);
|
|
296
|
+
font-size: 0.875rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ↺ draws small in most fonts. */
|
|
300
|
+
.python-editor-reset {
|
|
301
|
+
font-size: 1.125rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.python-editor-collapse {
|
|
305
|
+
margin-left: auto;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Collapsed: only the toolbar shows. Hidden with CSS so the editor keeps its code. */
|
|
309
|
+
.python-editor-collapsed .python-editor-code,
|
|
310
|
+
.python-editor-collapsed .python-editor-output {
|
|
311
|
+
display: none;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.python-editor-collapsed .python-editor-toolbar {
|
|
315
|
+
border-top: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.python-editor-output {
|
|
319
|
+
padding: 0.5rem;
|
|
320
|
+
border-top: 1px solid var(--color-border, #ddd);
|
|
321
|
+
font-size: 0.875rem;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.python-editor-text {
|
|
325
|
+
margin: 0;
|
|
326
|
+
max-height: 20rem;
|
|
327
|
+
overflow: auto;
|
|
328
|
+
white-space: pre-wrap;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.python-editor-plot {
|
|
332
|
+
display: block;
|
|
333
|
+
max-width: 100%;
|
|
334
|
+
height: auto;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.python-editor-error {
|
|
338
|
+
color: var(--color-error, #c00);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.python-editor-note {
|
|
342
|
+
color: var(--color-subtle, #666);
|
|
343
|
+
}
|
package/src/app.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MetaProvider } from "@solidjs/meta";
|
|
2
|
+
import { Router } from "@solidjs/router";
|
|
3
|
+
import { FileRoutes } from "@solidjs/start/router";
|
|
4
|
+
import { Suspense } from "solid-js";
|
|
5
|
+
import { Header as BuiltInHeader } from "./components/Header";
|
|
6
|
+
import { PageTitle } from "./components/PageTitle";
|
|
7
|
+
import { layout, type LayoutModules } from "./lib/layouts/layout";
|
|
8
|
+
import "./app.css";
|
|
9
|
+
// Author styles load after `app.css`, so they override it.
|
|
10
|
+
import "./authorStyles";
|
|
11
|
+
|
|
12
|
+
// The site's own `layouts/header.tsx`, or the built-in header.
|
|
13
|
+
const Header = layout(import.meta.glob<LayoutModules[string]>("/layouts/header.tsx", { eager: true }), BuiltInHeader);
|
|
14
|
+
|
|
15
|
+
export default function App() {
|
|
16
|
+
return (
|
|
17
|
+
<Router
|
|
18
|
+
root={props => (
|
|
19
|
+
<MetaProvider>
|
|
20
|
+
{/* Default; a page's own PageTitle overrides it. */}
|
|
21
|
+
<PageTitle />
|
|
22
|
+
<Header />
|
|
23
|
+
<Suspense>{props.children}</Suspense>
|
|
24
|
+
</MetaProvider>
|
|
25
|
+
)}
|
|
26
|
+
>
|
|
27
|
+
<FileRoutes />
|
|
28
|
+
</Router>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import site from "/site.yml";
|
|
2
|
+
|
|
3
|
+
/** The site name and nav links, shown at the top of every page when the site has no `layouts/header.tsx`. */
|
|
4
|
+
export function Header() {
|
|
5
|
+
return (
|
|
6
|
+
<header class="site-header">
|
|
7
|
+
<a class="site-header-title" href="/">
|
|
8
|
+
{site.title}
|
|
9
|
+
</a>
|
|
10
|
+
<nav class="site-header-nav">
|
|
11
|
+
<a href="/about">About</a>
|
|
12
|
+
<a href="/pages">Pages</a>
|
|
13
|
+
</nav>
|
|
14
|
+
</header>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Title } from "@solidjs/meta";
|
|
2
|
+
import site from "/site.yml";
|
|
3
|
+
|
|
4
|
+
/** Sets the browser tab title to "<page> – <site title>", or the site title alone when no page title is given. */
|
|
5
|
+
export function PageTitle(props: { children?: string }) {
|
|
6
|
+
// A ternary written inside the JSX gets wrapped in a memo, which `@solidjs/meta` reads
|
|
7
|
+
// outside any owner when it removes the tag, causing a "never be disposed" warning.
|
|
8
|
+
const title = () => (props.children ? `${props.children} – ${site.title}` : site.title);
|
|
9
|
+
return <Title>{title()}</Title>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createSignal, Show, type JSX } from 'solid-js';
|
|
2
|
+
import { Chevron, type Direction } from './icons';
|
|
3
|
+
|
|
4
|
+
/** The edge the first pane sits against. */
|
|
5
|
+
export type SplitSide = 'left' | 'right' | 'top' | 'bottom';
|
|
6
|
+
type Collapsed = 'none' | 'first' | 'second';
|
|
7
|
+
|
|
8
|
+
// Limits on the first pane's share, in %, while dragging.
|
|
9
|
+
const MIN_PERCENT = 20;
|
|
10
|
+
const MAX_PERCENT = 85;
|
|
11
|
+
/** Dragging a pane below this share, in %, collapses it. */
|
|
12
|
+
const SNAP_PERCENT = 8;
|
|
13
|
+
|
|
14
|
+
const SIDE_DIRECTION: Record<SplitSide, Direction> = { left: 'left', right: 'right', top: 'up', bottom: 'down' };
|
|
15
|
+
const OPPOSITE: Record<Direction, Direction> = { left: 'right', right: 'left', up: 'down', down: 'up' };
|
|
16
|
+
const BUTTONS_TRANSFORM: Record<Direction | 'center', string> = {
|
|
17
|
+
center: 'translate(-50%, -50%)',
|
|
18
|
+
left: 'translate(-100%, -50%)',
|
|
19
|
+
right: 'translate(0, -50%)',
|
|
20
|
+
up: 'translate(-50%, -100%)',
|
|
21
|
+
down: 'translate(-50%, 0)',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const HIDDEN_PANE: JSX.CSSProperties = { flex: '0 0 0%', overflow: 'hidden', visibility: 'hidden' };
|
|
25
|
+
|
|
26
|
+
/** The first pane's share of `rect`, in %, with the pointer at (`x`, `y`). */
|
|
27
|
+
function firstPercentAt(side: SplitSide, rect: DOMRect, x: number, y: number): number {
|
|
28
|
+
const fraction = {
|
|
29
|
+
left: (x - rect.left) / rect.width,
|
|
30
|
+
right: (rect.right - x) / rect.width,
|
|
31
|
+
top: (y - rect.top) / rect.height,
|
|
32
|
+
bottom: (rect.bottom - y) / rect.height,
|
|
33
|
+
}[side];
|
|
34
|
+
return fraction * 100;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calls `onMove` on every pointer move until the pointer is released.
|
|
39
|
+
* Listens on the window, so the drag goes on after the pointer leaves the divider.
|
|
40
|
+
* Text selection is off meanwhile, so dragging doesn't highlight page text.
|
|
41
|
+
*/
|
|
42
|
+
function trackDrag(onMove: (ev: PointerEvent) => void) {
|
|
43
|
+
document.body.style.userSelect = 'none';
|
|
44
|
+
const stop = () => {
|
|
45
|
+
document.body.style.userSelect = '';
|
|
46
|
+
window.removeEventListener('pointermove', onMove);
|
|
47
|
+
window.removeEventListener('pointerup', stop);
|
|
48
|
+
};
|
|
49
|
+
window.addEventListener('pointermove', onMove);
|
|
50
|
+
window.addEventListener('pointerup', stop);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The draggable bar between the panes, with a button to collapse each one. */
|
|
54
|
+
function SplitDivider(props: {
|
|
55
|
+
side: SplitSide;
|
|
56
|
+
collapsed: Collapsed;
|
|
57
|
+
setCollapsed: (c: Collapsed) => void;
|
|
58
|
+
firstLabel: string;
|
|
59
|
+
secondLabel: string;
|
|
60
|
+
onPointerDown: (e: PointerEvent) => void;
|
|
61
|
+
}) {
|
|
62
|
+
const isStacked = () => props.side === 'top' || props.side === 'bottom';
|
|
63
|
+
const firstDirection = () => SIDE_DIRECTION[props.side];
|
|
64
|
+
const secondDirection = () => OPPOSITE[firstDirection()];
|
|
65
|
+
// A collapsed pane's buttons sit beside the bar, on the open pane's side.
|
|
66
|
+
const buttonsTransform = () => {
|
|
67
|
+
const c = props.collapsed;
|
|
68
|
+
return BUTTONS_TRANSFORM[c === 'first' ? secondDirection() : c === 'second' ? firstDirection() : 'center'];
|
|
69
|
+
};
|
|
70
|
+
const firstButtonLabel = () => (props.collapsed === 'second' ? `Show ${props.secondLabel}` : `Hide ${props.firstLabel}`);
|
|
71
|
+
const secondButtonLabel = () => (props.collapsed === 'first' ? `Show ${props.firstLabel}` : `Hide ${props.secondLabel}`);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
class="split-pane-divider"
|
|
76
|
+
classList={{ 'split-pane-divider--horizontal': isStacked() }}
|
|
77
|
+
onPointerDown={props.onPointerDown}
|
|
78
|
+
role="separator"
|
|
79
|
+
aria-orientation={isStacked() ? 'horizontal' : 'vertical'}
|
|
80
|
+
>
|
|
81
|
+
<div class="split-pane-buttons" style={{ transform: buttonsTransform() }}>
|
|
82
|
+
<Show when={props.collapsed !== 'first'}>
|
|
83
|
+
<button
|
|
84
|
+
class="split-pane-button"
|
|
85
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
86
|
+
onClick={() => props.setCollapsed(props.collapsed === 'second' ? 'none' : 'first')}
|
|
87
|
+
aria-label={firstButtonLabel()}
|
|
88
|
+
title={firstButtonLabel()}
|
|
89
|
+
>
|
|
90
|
+
<Chevron direction={firstDirection()} />
|
|
91
|
+
</button>
|
|
92
|
+
</Show>
|
|
93
|
+
<Show when={props.collapsed !== 'second'}>
|
|
94
|
+
<button
|
|
95
|
+
class="split-pane-button"
|
|
96
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
97
|
+
onClick={() => props.setCollapsed(props.collapsed === 'first' ? 'none' : 'second')}
|
|
98
|
+
aria-label={secondButtonLabel()}
|
|
99
|
+
title={secondButtonLabel()}
|
|
100
|
+
>
|
|
101
|
+
<Chevron direction={secondDirection()} />
|
|
102
|
+
</button>
|
|
103
|
+
</Show>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SplitPaneProps {
|
|
110
|
+
side: SplitSide;
|
|
111
|
+
first: JSX.Element;
|
|
112
|
+
second: JSX.Element;
|
|
113
|
+
/** Names used on the collapse buttons, e.g. "Hide canvas". */
|
|
114
|
+
firstLabel: string;
|
|
115
|
+
secondLabel: string;
|
|
116
|
+
class?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Two panes split by a draggable divider: `first` against `side`, `second` in the rest.
|
|
121
|
+
* Each pane can be collapsed with a button on the divider, or by dragging it nearly shut.
|
|
122
|
+
*/
|
|
123
|
+
export function SplitPane(props: SplitPaneProps) {
|
|
124
|
+
let ref: HTMLDivElement | undefined;
|
|
125
|
+
const [firstPercent, setFirstPercent] = createSignal(50);
|
|
126
|
+
const [collapsed, setCollapsed] = createSignal<Collapsed>('none');
|
|
127
|
+
|
|
128
|
+
const startResize = (e: PointerEvent) => {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
trackDrag((ev) => {
|
|
131
|
+
if (!ref) return;
|
|
132
|
+
const pct = firstPercentAt(props.side, ref.getBoundingClientRect(), ev.clientX, ev.clientY);
|
|
133
|
+
if (pct < SNAP_PERCENT) {
|
|
134
|
+
setCollapsed('first');
|
|
135
|
+
} else if (pct > 100 - SNAP_PERCENT) {
|
|
136
|
+
setCollapsed('second');
|
|
137
|
+
} else {
|
|
138
|
+
setCollapsed('none');
|
|
139
|
+
setFirstPercent(Math.min(MAX_PERCENT, Math.max(MIN_PERCENT, pct)));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const firstStyle = (): JSX.CSSProperties => {
|
|
145
|
+
const c = collapsed();
|
|
146
|
+
if (c === 'first') return HIDDEN_PANE;
|
|
147
|
+
if (c === 'second') return { flex: '1 1 0%' };
|
|
148
|
+
return { flex: `0 0 ${firstPercent()}%` };
|
|
149
|
+
};
|
|
150
|
+
const secondStyle = (): JSX.CSSProperties => (collapsed() === 'second' ? HIDDEN_PANE : { flex: '1 1 0%' });
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div class={`split-pane split-pane--${props.side} ${props.class ?? ''}`} ref={ref}>
|
|
154
|
+
<div class="split-pane-pane" style={firstStyle()}>
|
|
155
|
+
{props.first}
|
|
156
|
+
</div>
|
|
157
|
+
<SplitDivider
|
|
158
|
+
side={props.side}
|
|
159
|
+
collapsed={collapsed()}
|
|
160
|
+
setCollapsed={setCollapsed}
|
|
161
|
+
firstLabel={props.firstLabel}
|
|
162
|
+
secondLabel={props.secondLabel}
|
|
163
|
+
onPointerDown={startResize}
|
|
164
|
+
/>
|
|
165
|
+
<div class="split-pane-pane" style={secondStyle()}>
|
|
166
|
+
{props.second}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Direction = 'left' | 'right' | 'up' | 'down';
|
|
2
|
+
|
|
3
|
+
const CHEVRON_TURN: Record<Direction, number> = { left: 0, up: 90, right: 180, down: 270 };
|
|
4
|
+
|
|
5
|
+
/** A small arrow pointing `direction`. */
|
|
6
|
+
export function Chevron(props: { direction: Direction }) {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
width="12"
|
|
10
|
+
height="12"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="2"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
style={{ transform: `rotate(${CHEVRON_TURN[props.direction]}deg)` }}
|
|
18
|
+
>
|
|
19
|
+
<path d="M15 18l-6-6 6-6" />
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @refresh reload
|
|
2
|
+
import { createHandler, StartServer } from "@solidjs/start/server";
|
|
3
|
+
|
|
4
|
+
export default createHandler(() => (
|
|
5
|
+
<StartServer
|
|
6
|
+
document={({ assets, children, scripts }) => (
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8" />
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
11
|
+
<link rel="icon" href="/favicon.ico" />
|
|
12
|
+
{assets}
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="app">{children}</div>
|
|
16
|
+
{scripts}
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
19
|
+
)}
|
|
20
|
+
/>
|
|
21
|
+
));
|
package/src/global.d.ts
ADDED