knobkit 0.0.1
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/LICENSE +21 -0
- package/dist/assets/cell-renderer-CLTRlCa5-DIlwS99c.js +1 -0
- package/dist/assets/chart-D8ctp-_1.js +36 -0
- package/dist/assets/code-BMuLQBYq.js +2 -0
- package/dist/assets/column.service-C6hByxPy-XG9X0y3N.js +1 -0
- package/dist/assets/debounce-PCRWZliA-BjJpj_P7.js +32 -0
- package/dist/assets/dimension.helpers-CGKwSvw6-D_czicbS.js +1 -0
- package/dist/assets/dist-1hsZpGRf.js +23 -0
- package/dist/assets/dist-B-y4Etc5.js +1 -0
- package/dist/assets/dist-B8BXgMDk.js +10 -0
- package/dist/assets/dist-BJlXPLNt.js +1 -0
- package/dist/assets/dist-ByhR2UY_.js +1 -0
- package/dist/assets/dist-C0bxYHYH.js +2 -0
- package/dist/assets/dist-C8dagUDy.js +6 -0
- package/dist/assets/dist-CtLpohkg.js +1 -0
- package/dist/assets/dist-D00mNtIr.js +1 -0
- package/dist/assets/dist-Dh1Dvy3h.js +1 -0
- package/dist/assets/dist-DlwQ1Qqm.js +1 -0
- package/dist/assets/dist-DtZDI7jp.js +1 -0
- package/dist/assets/dist-thZFs69d.js +9 -0
- package/dist/assets/edit.utils-Dnnbd0xG-OAxDw8WC.js +1 -0
- package/dist/assets/events-BvSmBueA-4kqQ57iN.js +1 -0
- package/dist/assets/filter.button-BFwo1uvz-CyvQhOO5.js +1 -0
- package/dist/assets/header-cell-renderer-BMmXRsd_-BHbC7fao.js +1 -0
- package/dist/assets/index-Db3qZoW5-peeY7EGw.js +1 -0
- package/dist/assets/markdown-dDCgur7g.js +29 -0
- package/dist/assets/revo-grid.entry-CfI6s-uT.js +1 -0
- package/dist/assets/revogr-attribution_7.entry-6fUjzImt.js +1 -0
- package/dist/assets/revogr-clipboard_3.entry-DmI7LkER.js +2 -0
- package/dist/assets/revogr-data_4.entry-CYZIiXNw.js +1 -0
- package/dist/assets/revogr-filter-panel.entry-TmQHTQxw.js +1 -0
- package/dist/assets/table-Zn7rpfG-.js +1 -0
- package/dist/assets/text-editor-C3RUSwH5-DuDr9wKc.js +1 -0
- package/dist/assets/theme.service-BmnDvr6P-DftEgmbe.js +3 -0
- package/dist/assets/throttle-CaUDyxyU-Djj__DCp.js +1 -0
- package/dist/assets/viewport.helpers-CoCAvmZs-ByVRjjkF.js +1 -0
- package/dist/assets/viewport.store-_c579YyM-B_ZSqqka.js +1 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +82 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +77 -0
- package/dist/cli/mount.d.ts +2 -0
- package/dist/cli/mount.js +26 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +21 -0
- package/dist/client/app.d.ts +8 -0
- package/dist/client/app.js +40 -0
- package/dist/client/context.d.ts +2 -0
- package/dist/client/context.js +16 -0
- package/dist/client/mount.d.ts +3 -0
- package/dist/client/mount.js +42 -0
- package/dist/client/runtime.d.ts +19 -0
- package/dist/client/runtime.js +88 -0
- package/dist/client/view.d.ts +12 -0
- package/dist/client/view.js +1 -0
- package/dist/client/widgets/accordion/index.d.ts +5 -0
- package/dist/client/widgets/accordion/index.js +8 -0
- package/dist/client/widgets/annotated-image/index.d.ts +8 -0
- package/dist/client/widgets/annotated-image/index.js +34 -0
- package/dist/client/widgets/audio/index.d.ts +5 -0
- package/dist/client/widgets/audio/index.js +5 -0
- package/dist/client/widgets/button/index.d.ts +4 -0
- package/dist/client/widgets/button/index.js +5 -0
- package/dist/client/widgets/chart/index.d.ts +5 -0
- package/dist/client/widgets/chart/index.js +23 -0
- package/dist/client/widgets/chart/lazy.d.ts +3 -0
- package/dist/client/widgets/chart/lazy.js +9 -0
- package/dist/client/widgets/chat/index.d.ts +6 -0
- package/dist/client/widgets/chat/index.js +77 -0
- package/dist/client/widgets/checkbox/index.d.ts +6 -0
- package/dist/client/widgets/checkbox/index.js +9 -0
- package/dist/client/widgets/checkbox-group/index.d.ts +6 -0
- package/dist/client/widgets/checkbox-group/index.js +12 -0
- package/dist/client/widgets/code/index.d.ts +5 -0
- package/dist/client/widgets/code/index.js +101 -0
- package/dist/client/widgets/code/lazy.d.ts +3 -0
- package/dist/client/widgets/code/lazy.js +10 -0
- package/dist/client/widgets/dropdown/index.d.ts +5 -0
- package/dist/client/widgets/dropdown/index.js +9 -0
- package/dist/client/widgets/file/index.d.ts +6 -0
- package/dist/client/widgets/file/index.js +7 -0
- package/dist/client/widgets/frame/index.d.ts +6 -0
- package/dist/client/widgets/frame/index.js +11 -0
- package/dist/client/widgets/gallery/index.d.ts +6 -0
- package/dist/client/widgets/gallery/index.js +8 -0
- package/dist/client/widgets/highlighted-text/index.d.ts +7 -0
- package/dist/client/widgets/highlighted-text/index.js +20 -0
- package/dist/client/widgets/html/index.d.ts +4 -0
- package/dist/client/widgets/html/index.js +8 -0
- package/dist/client/widgets/image/index.d.ts +4 -0
- package/dist/client/widgets/image/index.js +4 -0
- package/dist/client/widgets/json/index.d.ts +4 -0
- package/dist/client/widgets/json/index.js +4 -0
- package/dist/client/widgets/label/index.d.ts +7 -0
- package/dist/client/widgets/label/index.js +8 -0
- package/dist/client/widgets/layout/index.d.ts +4 -0
- package/dist/client/widgets/layout/index.js +7 -0
- package/dist/client/widgets/log/index.d.ts +4 -0
- package/dist/client/widgets/log/index.js +4 -0
- package/dist/client/widgets/mic/index.d.ts +6 -0
- package/dist/client/widgets/mic/index.js +70 -0
- package/dist/client/widgets/number/index.d.ts +5 -0
- package/dist/client/widgets/number/index.js +8 -0
- package/dist/client/widgets/output/index.d.ts +6 -0
- package/dist/client/widgets/output/index.js +13 -0
- package/dist/client/widgets/output/markdown.d.ts +3 -0
- package/dist/client/widgets/output/markdown.js +8 -0
- package/dist/client/widgets/progress/index.d.ts +6 -0
- package/dist/client/widgets/progress/index.js +6 -0
- package/dist/client/widgets/radio/index.d.ts +6 -0
- package/dist/client/widgets/radio/index.js +10 -0
- package/dist/client/widgets/registry.d.ts +2 -0
- package/dist/client/widgets/registry.js +69 -0
- package/dist/client/widgets/slider/index.d.ts +6 -0
- package/dist/client/widgets/slider/index.js +9 -0
- package/dist/client/widgets/table/index.d.ts +6 -0
- package/dist/client/widgets/table/index.js +72 -0
- package/dist/client/widgets/table/lazy.d.ts +3 -0
- package/dist/client/widgets/table/lazy.js +16 -0
- package/dist/client/widgets/tabs/index.d.ts +5 -0
- package/dist/client/widgets/tabs/index.js +10 -0
- package/dist/client/widgets/text/index.d.ts +6 -0
- package/dist/client/widgets/text/index.js +10 -0
- package/dist/client/widgets/upload/index.d.ts +6 -0
- package/dist/client/widgets/upload/index.js +16 -0
- package/dist/client/widgets/video/index.d.ts +5 -0
- package/dist/client/widgets/video/index.js +5 -0
- package/dist/client/widgets/webcam/index.d.ts +6 -0
- package/dist/client/widgets/webcam/index.js +62 -0
- package/dist/client.css +1 -0
- package/dist/client.js +11 -0
- package/dist/knobkit.browser.css +2 -0
- package/dist/knobkit.browser.js +151 -0
- package/dist/lib/bound.d.ts +12 -0
- package/dist/lib/bound.js +10 -0
- package/dist/lib/controls.d.ts +9 -0
- package/dist/lib/controls.js +35 -0
- package/dist/lib/ctx.d.ts +9 -0
- package/dist/lib/ctx.js +22 -0
- package/dist/lib/declare.d.ts +18 -0
- package/dist/lib/declare.js +43 -0
- package/dist/lib/event.d.ts +2 -0
- package/dist/lib/event.js +9 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.js +6 -0
- package/dist/lib/knobkit.d.ts +19 -0
- package/dist/lib/knobkit.js +48 -0
- package/dist/lib/on.d.ts +2 -0
- package/dist/lib/on.js +1 -0
- package/dist/lib/stream.d.ts +1 -0
- package/dist/lib/stream.js +33 -0
- package/dist/lib/types.d.ts +48 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/widget.d.ts +7 -0
- package/dist/lib/widget.js +4 -0
- package/dist/lib/widgets/annotated-image.d.ts +14 -0
- package/dist/lib/widgets/annotated-image.js +16 -0
- package/dist/lib/widgets/audio.d.ts +10 -0
- package/dist/lib/widgets/audio.js +13 -0
- package/dist/lib/widgets/button.d.ts +11 -0
- package/dist/lib/widgets/button.js +17 -0
- package/dist/lib/widgets/chart.d.ts +20 -0
- package/dist/lib/widgets/chart.js +24 -0
- package/dist/lib/widgets/chat.d.ts +26 -0
- package/dist/lib/widgets/chat.js +24 -0
- package/dist/lib/widgets/checkbox-group.d.ts +4 -0
- package/dist/lib/widgets/checkbox-group.js +5 -0
- package/dist/lib/widgets/checkbox.d.ts +4 -0
- package/dist/lib/widgets/checkbox.js +4 -0
- package/dist/lib/widgets/code.d.ts +5 -0
- package/dist/lib/widgets/code.js +10 -0
- package/dist/lib/widgets/dropdown.d.ts +4 -0
- package/dist/lib/widgets/dropdown.js +4 -0
- package/dist/lib/widgets/embed.d.ts +5 -0
- package/dist/lib/widgets/embed.js +23 -0
- package/dist/lib/widgets/file.d.ts +11 -0
- package/dist/lib/widgets/file.js +15 -0
- package/dist/lib/widgets/frame.d.ts +18 -0
- package/dist/lib/widgets/frame.js +25 -0
- package/dist/lib/widgets/gallery.d.ts +12 -0
- package/dist/lib/widgets/gallery.js +17 -0
- package/dist/lib/widgets/highlighted-text.d.ts +12 -0
- package/dist/lib/widgets/highlighted-text.js +15 -0
- package/dist/lib/widgets/html.d.ts +9 -0
- package/dist/lib/widgets/html.js +12 -0
- package/dist/lib/widgets/image.d.ts +7 -0
- package/dist/lib/widgets/image.js +12 -0
- package/dist/lib/widgets/index.d.ts +31 -0
- package/dist/lib/widgets/index.js +31 -0
- package/dist/lib/widgets/json.d.ts +7 -0
- package/dist/lib/widgets/json.js +12 -0
- package/dist/lib/widgets/label.d.ts +15 -0
- package/dist/lib/widgets/label.js +18 -0
- package/dist/lib/widgets/layout.d.ts +21 -0
- package/dist/lib/widgets/layout.js +29 -0
- package/dist/lib/widgets/log.d.ts +8 -0
- package/dist/lib/widgets/log.js +15 -0
- package/dist/lib/widgets/mic.d.ts +19 -0
- package/dist/lib/widgets/mic.js +28 -0
- package/dist/lib/widgets/number.d.ts +5 -0
- package/dist/lib/widgets/number.js +4 -0
- package/dist/lib/widgets/output.d.ts +10 -0
- package/dist/lib/widgets/output.js +13 -0
- package/dist/lib/widgets/progress.d.ts +10 -0
- package/dist/lib/widgets/progress.js +15 -0
- package/dist/lib/widgets/radio.d.ts +4 -0
- package/dist/lib/widgets/radio.js +5 -0
- package/dist/lib/widgets/slider.d.ts +6 -0
- package/dist/lib/widgets/slider.js +6 -0
- package/dist/lib/widgets/table.d.ts +32 -0
- package/dist/lib/widgets/table.js +36 -0
- package/dist/lib/widgets/text.d.ts +4 -0
- package/dist/lib/widgets/text.js +4 -0
- package/dist/lib/widgets/upload.d.ts +3 -0
- package/dist/lib/widgets/upload.js +5 -0
- package/dist/lib/widgets/value.d.ts +9 -0
- package/dist/lib/widgets/value.js +20 -0
- package/dist/lib/widgets/video.d.ts +12 -0
- package/dist/lib/widgets/video.js +14 -0
- package/dist/lib/widgets/webcam.d.ts +21 -0
- package/dist/lib/widgets/webcam.js +29 -0
- package/dist/server/context.d.ts +2 -0
- package/dist/server/context.js +10 -0
- package/dist/server/serve.d.ts +5 -0
- package/dist/server/serve.js +131 -0
- package/package.json +71 -0
- package/src/cli/config.ts +83 -0
- package/src/cli/index.ts +82 -0
- package/src/cli/mount.ts +25 -0
- package/src/cli/serve.ts +22 -0
- package/src/client/app.test.tsx +70 -0
- package/src/client/app.tsx +62 -0
- package/src/client/browser-runtime.test.ts +22 -0
- package/src/client/browser.ts +3 -0
- package/src/client/context.ts +17 -0
- package/src/client/embed.test.tsx +58 -0
- package/src/client/entry.tsx +25 -0
- package/src/client/mount.test.tsx +36 -0
- package/src/client/mount.tsx +48 -0
- package/src/client/runtime.test.ts +64 -0
- package/src/client/runtime.ts +112 -0
- package/src/client/serve-stub.ts +3 -0
- package/src/client/styles.css +131 -0
- package/src/client/view.ts +16 -0
- package/src/client/widgets/accordion/accordion.css +35 -0
- package/src/client/widgets/accordion/index.tsx +17 -0
- package/src/client/widgets/annotated-image/annotated-image.css +62 -0
- package/src/client/widgets/annotated-image/index.tsx +73 -0
- package/src/client/widgets/audio/audio.css +6 -0
- package/src/client/widgets/audio/index.tsx +6 -0
- package/src/client/widgets/button/button.css +25 -0
- package/src/client/widgets/button/index.tsx +11 -0
- package/src/client/widgets/chart/chart.css +12 -0
- package/src/client/widgets/chart/index.tsx +63 -0
- package/src/client/widgets/chart/lazy.tsx +15 -0
- package/src/client/widgets/chat/chat.css +97 -0
- package/src/client/widgets/chat/index.tsx +121 -0
- package/src/client/widgets/checkbox/checkbox.css +15 -0
- package/src/client/widgets/checkbox/index.tsx +15 -0
- package/src/client/widgets/checkbox-group/checkbox-group.css +20 -0
- package/src/client/widgets/checkbox-group/index.tsx +22 -0
- package/src/client/widgets/code/code.css +31 -0
- package/src/client/widgets/code/index.tsx +108 -0
- package/src/client/widgets/code/lazy.tsx +16 -0
- package/src/client/widgets/dropdown/dropdown.css +0 -0
- package/src/client/widgets/dropdown/index.tsx +19 -0
- package/src/client/widgets/file/file.css +26 -0
- package/src/client/widgets/file/index.tsx +12 -0
- package/src/client/widgets/frame/frame.css +17 -0
- package/src/client/widgets/frame/index.tsx +15 -0
- package/src/client/widgets/gallery/gallery.css +26 -0
- package/src/client/widgets/gallery/index.tsx +18 -0
- package/src/client/widgets/highlighted-text/highlighted-text.css +21 -0
- package/src/client/widgets/highlighted-text/index.tsx +42 -0
- package/src/client/widgets/html/index.tsx +8 -0
- package/src/client/widgets/image/index.tsx +5 -0
- package/src/client/widgets/json/index.tsx +5 -0
- package/src/client/widgets/json/json.css +0 -0
- package/src/client/widgets/label/index.tsx +20 -0
- package/src/client/widgets/label/label.css +39 -0
- package/src/client/widgets/layout/index.tsx +14 -0
- package/src/client/widgets/log/index.tsx +5 -0
- package/src/client/widgets/log/log.css +0 -0
- package/src/client/widgets/mic/index.tsx +85 -0
- package/src/client/widgets/mic/mic.css +8 -0
- package/src/client/widgets/number/index.tsx +10 -0
- package/src/client/widgets/number/number.css +0 -0
- package/src/client/widgets/output/index.tsx +19 -0
- package/src/client/widgets/output/markdown.tsx +12 -0
- package/src/client/widgets/output/output.css +75 -0
- package/src/client/widgets/progress/index.tsx +14 -0
- package/src/client/widgets/progress/progress.css +26 -0
- package/src/client/widgets/radio/index.tsx +20 -0
- package/src/client/widgets/radio/radio.css +20 -0
- package/src/client/widgets/registry.tsx +71 -0
- package/src/client/widgets/slider/index.tsx +23 -0
- package/src/client/widgets/slider/slider.css +18 -0
- package/src/client/widgets/table/index.tsx +95 -0
- package/src/client/widgets/table/lazy.tsx +23 -0
- package/src/client/widgets/table/table.css +15 -0
- package/src/client/widgets/tabs/index.tsx +28 -0
- package/src/client/widgets/tabs/tabs.css +30 -0
- package/src/client/widgets/text/index.tsx +16 -0
- package/src/client/widgets/text/text.css +21 -0
- package/src/client/widgets/upload/index.tsx +30 -0
- package/src/client/widgets/upload/upload.css +30 -0
- package/src/client/widgets/video/index.tsx +10 -0
- package/src/client/widgets/video/video.css +8 -0
- package/src/client/widgets/webcam/index.tsx +81 -0
- package/src/client/widgets/webcam/webcam.css +46 -0
- package/src/css.d.ts +1 -0
- package/src/env.d.ts +1 -0
- package/src/lib/bound.ts +30 -0
- package/src/lib/controls.ts +36 -0
- package/src/lib/ctx.ts +31 -0
- package/src/lib/declare.test.ts +46 -0
- package/src/lib/declare.ts +74 -0
- package/src/lib/event.ts +12 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/knobkit.ts +57 -0
- package/src/lib/on.ts +3 -0
- package/src/lib/stream.ts +38 -0
- package/src/lib/types.ts +63 -0
- package/src/lib/widget.ts +11 -0
- package/src/lib/widgets/annotated-image.ts +34 -0
- package/src/lib/widgets/audio.ts +20 -0
- package/src/lib/widgets/button.ts +27 -0
- package/src/lib/widgets/chart.ts +44 -0
- package/src/lib/widgets/chat.ts +43 -0
- package/src/lib/widgets/checkbox-group.ts +6 -0
- package/src/lib/widgets/checkbox.ts +5 -0
- package/src/lib/widgets/code.ts +11 -0
- package/src/lib/widgets/dropdown.ts +5 -0
- package/src/lib/widgets/embed.ts +26 -0
- package/src/lib/widgets/file.ts +23 -0
- package/src/lib/widgets/frame.ts +36 -0
- package/src/lib/widgets/gallery.ts +29 -0
- package/src/lib/widgets/highlighted-text.ts +29 -0
- package/src/lib/widgets/html.ts +18 -0
- package/src/lib/widgets/image.ts +18 -0
- package/src/lib/widgets/index.ts +31 -0
- package/src/lib/widgets/json.ts +18 -0
- package/src/lib/widgets/label.ts +29 -0
- package/src/lib/widgets/layout.ts +47 -0
- package/src/lib/widgets/log.ts +22 -0
- package/src/lib/widgets/mic.ts +42 -0
- package/src/lib/widgets/number.ts +5 -0
- package/src/lib/widgets/output.ts +20 -0
- package/src/lib/widgets/progress.ts +21 -0
- package/src/lib/widgets/radio.ts +6 -0
- package/src/lib/widgets/slider.ts +7 -0
- package/src/lib/widgets/table.ts +58 -0
- package/src/lib/widgets/text.ts +5 -0
- package/src/lib/widgets/upload.ts +6 -0
- package/src/lib/widgets/value.ts +28 -0
- package/src/lib/widgets/video.ts +22 -0
- package/src/lib/widgets/webcam.ts +46 -0
- package/src/server/context.ts +12 -0
- package/src/server/serve.test.ts +121 -0
- package/src/server/serve.ts +130 -0
- package/tsconfig.base.json +14 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { ensureTsconfig } from "./config.js";
|
|
5
|
+
import { buildMount, devMount } from "./mount.js";
|
|
6
|
+
import { runServe } from "./serve.js";
|
|
7
|
+
const HELP = `knobkit — build a web app from widgets and event handlers
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
knobkit dev [file] Start a dev server (auto-detects mount vs serve)
|
|
11
|
+
knobkit build [file] Build a browser (mount) app to dist/
|
|
12
|
+
knobkit serve [file] Run a server (serve) app (same as: knobkit dev --serve)
|
|
13
|
+
|
|
14
|
+
file defaults to demo.tsx
|
|
15
|
+
|
|
16
|
+
Flags:
|
|
17
|
+
--mount Force browser (mount) mode
|
|
18
|
+
--serve Force server (serve) mode
|
|
19
|
+
--port <n> Dev server port (mount)
|
|
20
|
+
`;
|
|
21
|
+
function parse(argv) {
|
|
22
|
+
const out = { file: "demo.tsx", mount: false, serve: false };
|
|
23
|
+
let sawFile = false;
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === "--mount")
|
|
27
|
+
out.mount = true;
|
|
28
|
+
else if (a === "--serve")
|
|
29
|
+
out.serve = true;
|
|
30
|
+
else if (a === "--port")
|
|
31
|
+
out.port = Number(argv[++i]);
|
|
32
|
+
else if (a.startsWith("--port="))
|
|
33
|
+
out.port = Number(a.slice("--port=".length));
|
|
34
|
+
else if (!a.startsWith("-") && !sawFile) {
|
|
35
|
+
out.file = a;
|
|
36
|
+
sawFile = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function mode(file, args) {
|
|
42
|
+
if (args.serve)
|
|
43
|
+
return "serve";
|
|
44
|
+
if (args.mount)
|
|
45
|
+
return "mount";
|
|
46
|
+
return /\.serve\s*\(/.test(readFileSync(file, "utf8")) ? "serve" : "mount";
|
|
47
|
+
}
|
|
48
|
+
async function main() {
|
|
49
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
50
|
+
if (!cmd || cmd === "help" || cmd === "-h" || cmd === "--help") {
|
|
51
|
+
process.stdout.write(HELP);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (cmd !== "dev" && cmd !== "build" && cmd !== "serve") {
|
|
55
|
+
process.stderr.write(`knobkit: unknown command "${cmd}"\n\n${HELP}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const args = parse(rest);
|
|
59
|
+
const root = process.cwd();
|
|
60
|
+
const file = resolve(root, args.file);
|
|
61
|
+
if (!existsSync(file)) {
|
|
62
|
+
process.stderr.write(`knobkit: no such file: ${args.file}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
ensureTsconfig(root);
|
|
66
|
+
if (cmd === "build")
|
|
67
|
+
return buildMount(root, file);
|
|
68
|
+
if (cmd === "serve")
|
|
69
|
+
return runServe(file);
|
|
70
|
+
if (mode(file, args) === "serve")
|
|
71
|
+
return runServe(file);
|
|
72
|
+
return devMount(root, file, args.port);
|
|
73
|
+
}
|
|
74
|
+
main().catch((err) => {
|
|
75
|
+
console.error(err);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
|
+
import { build as viteBuild, createServer } from "vite";
|
|
4
|
+
import { indexHtml, mountConfig } from "./config.js";
|
|
5
|
+
export async function devMount(root, entry, port) {
|
|
6
|
+
const ownHtml = existsSync(resolve(root, "index.html"));
|
|
7
|
+
const server = await createServer({ ...mountConfig(root, entry, ownHtml), server: { port } });
|
|
8
|
+
await server.listen();
|
|
9
|
+
server.printUrls();
|
|
10
|
+
}
|
|
11
|
+
export async function buildMount(root, entry) {
|
|
12
|
+
const htmlPath = resolve(root, "index.html");
|
|
13
|
+
const created = !existsSync(htmlPath);
|
|
14
|
+
if (created)
|
|
15
|
+
writeFileSync(htmlPath, indexHtml(relative(root, entry)));
|
|
16
|
+
try {
|
|
17
|
+
await viteBuild({
|
|
18
|
+
...mountConfig(root, entry, true),
|
|
19
|
+
build: { outDir: "dist", emptyOutDir: true },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
if (created)
|
|
24
|
+
rmSync(htmlPath, { force: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runServe(file: string): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
function tsxBin() {
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const pkgPath = require.resolve("tsx/package.json");
|
|
8
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
9
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin.tsx;
|
|
10
|
+
return resolve(dirname(pkgPath), bin);
|
|
11
|
+
}
|
|
12
|
+
export function runServe(file) {
|
|
13
|
+
const child = spawn(process.execPath, [tsxBin(), "watch", file], { stdio: "inherit" });
|
|
14
|
+
return new Promise((res) => {
|
|
15
|
+
child.on("exit", (code) => {
|
|
16
|
+
if (code)
|
|
17
|
+
process.exitCode = code;
|
|
18
|
+
res();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./styles.css";
|
|
2
|
+
import type { AppDecl } from "../lib/declare.js";
|
|
3
|
+
import type { Store } from "./runtime.js";
|
|
4
|
+
export declare function App({ decl, store }: {
|
|
5
|
+
decl: AppDecl;
|
|
6
|
+
store: Store;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function render(decl: AppDecl, store: Store, el: Element): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./styles.css";
|
|
3
|
+
import { createElement, useCallback, useMemo, useSyncExternalStore } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { VIEWS } from "./widgets/registry.js";
|
|
6
|
+
// rebuild a widget's event constructors from the `type` strings in the decl, so a view's
|
|
7
|
+
// `emit(widget.changed(v))` produces the right `{ type, payload }`.
|
|
8
|
+
function rebuild(decl) {
|
|
9
|
+
const widget = { type: decl.type, ...decl.props };
|
|
10
|
+
for (const [name, type] of Object.entries(decl.events)) {
|
|
11
|
+
widget[name] = (payload) => ({ type, payload });
|
|
12
|
+
}
|
|
13
|
+
return widget;
|
|
14
|
+
}
|
|
15
|
+
// One widget. It subscribes to its own key, so only the widget whose state (or enabled) changed
|
|
16
|
+
// re-renders. The view gets `emit` (events onto the local bus) and `set` (a local state edit for its
|
|
17
|
+
// own widget, e.g. an input reflecting typing — no server round-trip).
|
|
18
|
+
function Field({ wd, byKey, store, emit }) {
|
|
19
|
+
const subscribe = useCallback((cb) => store.subscribe(wd.key, cb), [store, wd.key]);
|
|
20
|
+
const cell = useSyncExternalStore(subscribe, () => store.cell(wd.key));
|
|
21
|
+
const widget = useMemo(() => rebuild(wd), [wd]);
|
|
22
|
+
const set = useCallback((path, value) => store.applyEdit(wd.key, "set", path, value), [store, wd.key]);
|
|
23
|
+
const slot = useCallback((key) => {
|
|
24
|
+
const child = byKey[key];
|
|
25
|
+
return child ? _jsx(Field, { wd: child, byKey: byKey, store: store, emit: emit }, key) : null;
|
|
26
|
+
}, [byKey, store, emit]);
|
|
27
|
+
const View = VIEWS[wd.type];
|
|
28
|
+
if (!View)
|
|
29
|
+
return null;
|
|
30
|
+
return (_jsxs("div", { className: `pu-field${cell.enabled ? "" : " pu-disabled"}${cell.busy ? " pu-busy" : ""}`, children: [cell.busy && _jsx("div", { className: "pu-busy-bar", role: "status", "aria-label": "Loading" }), createElement(View, { widget, state: cell.state, enabled: cell.enabled, emit, set, slot })] }));
|
|
31
|
+
}
|
|
32
|
+
export function App({ decl, store }) {
|
|
33
|
+
const emit = (e) => store.emit(e.type, e.payload);
|
|
34
|
+
const byKey = useMemo(() => Object.fromEntries(decl.widgets.map((wd) => [wd.key, wd])), [decl]);
|
|
35
|
+
const rootWd = byKey[decl.root];
|
|
36
|
+
return (_jsxs("div", { className: "pu-page", children: [decl.title && _jsx("h1", { children: decl.title }), decl.description && _jsx("p", { className: "pu-desc", children: decl.description }), rootWd && _jsx(Field, { wd: rootWd, byKey: byKey, store: store, emit: emit })] }));
|
|
37
|
+
}
|
|
38
|
+
export function render(decl, store, el) {
|
|
39
|
+
createRoot(el).render(_jsx(App, { decl: decl, store: store }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { setBoundResolver } from "../lib/bound.js";
|
|
2
|
+
// Browser context binding for mount mode, where handlers run in-process. There is no AsyncLocalStorage
|
|
3
|
+
// in the browser, so the current context is a module global set around each handler run. This is
|
|
4
|
+
// correct for one in-flight handler at a time (the common case); overlapping async handlers would
|
|
5
|
+
// share it.
|
|
6
|
+
let current;
|
|
7
|
+
setBoundResolver(() => current);
|
|
8
|
+
export async function run(bound, fn) {
|
|
9
|
+
current = bound;
|
|
10
|
+
try {
|
|
11
|
+
await fn();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
current = undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "./styles.css";
|
|
2
|
+
import { declare } from "../lib/declare.js";
|
|
3
|
+
import { makeBound } from "../lib/ctx.js";
|
|
4
|
+
import { createStore } from "./runtime.js";
|
|
5
|
+
import { render } from "./app.js";
|
|
6
|
+
import { run } from "./context.js";
|
|
7
|
+
const isEvent = (x) => x != null && typeof x.type === "string" && "payload" in x;
|
|
8
|
+
// DOM-only, no server: the browser owns state AND runs the `on(...)` handlers locally. Same store and
|
|
9
|
+
// views as serve mode; the transport invokes the knobkit's handlers in-place with a context that reads the
|
|
10
|
+
// local store and applies edits to it directly. Produced events go straight back onto the store.
|
|
11
|
+
export function mount(knobkit, selector) {
|
|
12
|
+
const el = document.querySelector(selector);
|
|
13
|
+
if (!el)
|
|
14
|
+
throw new Error(`knobkit: no element matches "${selector}"`);
|
|
15
|
+
const decl = declare(knobkit.config, knobkit.serverEvents());
|
|
16
|
+
let store;
|
|
17
|
+
const localBound = () => makeBound({
|
|
18
|
+
read: (key, path) => Promise.resolve(store.read(key, path)),
|
|
19
|
+
edit: (key, op, path, value) => store.applyEdit(key, op, path, value),
|
|
20
|
+
enable: (key, value) => store.setEnabled(key, value),
|
|
21
|
+
busy: (key, value) => store.setBusy(key, value),
|
|
22
|
+
keyFor: (w) => knobkit.keyFor(w),
|
|
23
|
+
});
|
|
24
|
+
const transport = (type, payload) => {
|
|
25
|
+
const bound = localBound();
|
|
26
|
+
void (async () => {
|
|
27
|
+
for (const handler of knobkit.handlers.get(type) ?? []) {
|
|
28
|
+
await run(bound, async () => {
|
|
29
|
+
const r = await handler(payload);
|
|
30
|
+
if (isEvent(r))
|
|
31
|
+
store.emit(r.type, r.payload);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
};
|
|
36
|
+
store = createStore(decl, transport);
|
|
37
|
+
render(decl, store, el);
|
|
38
|
+
void (async () => {
|
|
39
|
+
for (const fn of knobkit.setups)
|
|
40
|
+
await run(localBound(), fn);
|
|
41
|
+
})();
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AppDecl } from "../lib/declare.js";
|
|
2
|
+
import type { EditOp, Path } from "../lib/bound.js";
|
|
3
|
+
export type Transport = (type: string, payload: unknown) => void;
|
|
4
|
+
export interface Cell {
|
|
5
|
+
state: unknown;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
busy: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface Store {
|
|
10
|
+
emit(type: string, payload: unknown): void;
|
|
11
|
+
applyEdit(key: string, op: EditOp, path: Path, value: unknown): void;
|
|
12
|
+
setEnabled(key: string, value: boolean): void;
|
|
13
|
+
setBusy(key: string, value: boolean): void;
|
|
14
|
+
read(key: string, path: Path): unknown;
|
|
15
|
+
cell(key: string): Cell;
|
|
16
|
+
enabled(key: string): boolean;
|
|
17
|
+
subscribe(key: string, fn: () => void): () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createStore(decl: AppDecl, transport: Transport): Store;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function readAt(node, path) {
|
|
2
|
+
for (const p of path) {
|
|
3
|
+
if (node == null)
|
|
4
|
+
return undefined;
|
|
5
|
+
node = Array.isArray(node) && p === -1 ? node[node.length - 1] : node[p];
|
|
6
|
+
}
|
|
7
|
+
return node;
|
|
8
|
+
}
|
|
9
|
+
// Immutably apply one structured edit at a path. -1 addresses an array's last element.
|
|
10
|
+
function editAt(node, path, op, value) {
|
|
11
|
+
if (path.length === 0) {
|
|
12
|
+
if (op === "set")
|
|
13
|
+
return value;
|
|
14
|
+
if (op === "append")
|
|
15
|
+
return [...(node ?? []), value];
|
|
16
|
+
return (node ?? "") + value; // appendText
|
|
17
|
+
}
|
|
18
|
+
const [head, ...rest] = path;
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
const i = head === -1 ? node.length - 1 : head;
|
|
21
|
+
const copy = node.slice();
|
|
22
|
+
copy[i] = editAt(node[i], rest, op, value);
|
|
23
|
+
return copy;
|
|
24
|
+
}
|
|
25
|
+
return { ...node, [head]: editAt(node?.[head], rest, op, value) };
|
|
26
|
+
}
|
|
27
|
+
// The browser owns all state as structured JSON, one cell per widget. The store has no per-widget
|
|
28
|
+
// logic: events route to handlers (gated by enabled), edits apply generically by path, reads walk a
|
|
29
|
+
// path. A change notifies only that widget's subscribers — rendering is per-key.
|
|
30
|
+
export function createStore(decl, transport) {
|
|
31
|
+
const cells = new Map();
|
|
32
|
+
const listeners = new Map();
|
|
33
|
+
const owner = new Map(); // an event type -> the widget key that emits it
|
|
34
|
+
const serverEvents = new Set(decl.serverEvents);
|
|
35
|
+
for (const w of decl.widgets) {
|
|
36
|
+
cells.set(w.key, { state: w.state, enabled: w.enabled, busy: false });
|
|
37
|
+
for (const t of Object.values(w.events))
|
|
38
|
+
owner.set(t, w.key);
|
|
39
|
+
}
|
|
40
|
+
const notify = (key) => {
|
|
41
|
+
for (const fn of listeners.get(key) ?? [])
|
|
42
|
+
fn();
|
|
43
|
+
};
|
|
44
|
+
const applyEdit = (key, op, path, value) => {
|
|
45
|
+
const cur = cells.get(key);
|
|
46
|
+
if (!cur)
|
|
47
|
+
return;
|
|
48
|
+
cells.set(key, { ...cur, state: editAt(cur.state, path, op, value) });
|
|
49
|
+
notify(key);
|
|
50
|
+
};
|
|
51
|
+
const setEnabled = (key, value) => {
|
|
52
|
+
const cur = cells.get(key);
|
|
53
|
+
if (!cur || cur.enabled === value)
|
|
54
|
+
return;
|
|
55
|
+
cells.set(key, { ...cur, enabled: value });
|
|
56
|
+
notify(key);
|
|
57
|
+
};
|
|
58
|
+
const setBusy = (key, value) => {
|
|
59
|
+
const cur = cells.get(key);
|
|
60
|
+
if (!cur || cur.busy === value)
|
|
61
|
+
return;
|
|
62
|
+
cells.set(key, { ...cur, busy: value });
|
|
63
|
+
notify(key);
|
|
64
|
+
};
|
|
65
|
+
function emit(type, payload) {
|
|
66
|
+
const src = owner.get(type);
|
|
67
|
+
const c = src ? cells.get(src) : undefined;
|
|
68
|
+
if (c && (c.enabled === false || c.busy))
|
|
69
|
+
return; // a disabled or busy widget drops its own input events
|
|
70
|
+
if (serverEvents.has(type))
|
|
71
|
+
transport(type, payload);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
emit,
|
|
75
|
+
applyEdit,
|
|
76
|
+
setEnabled,
|
|
77
|
+
setBusy,
|
|
78
|
+
read: (key, path) => readAt(cells.get(key)?.state, path),
|
|
79
|
+
cell: (key) => cells.get(key),
|
|
80
|
+
enabled: (key) => cells.get(key)?.enabled !== false,
|
|
81
|
+
subscribe: (key, fn) => {
|
|
82
|
+
const subs = listeners.get(key) ?? new Set();
|
|
83
|
+
subs.add(fn);
|
|
84
|
+
listeners.set(key, subs);
|
|
85
|
+
return () => subs.delete(fn);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { Path } from "../lib/bound.js";
|
|
3
|
+
import type { Emit, Widget } from "../lib/types.js";
|
|
4
|
+
export interface ViewProps<W extends Widget<any> = Widget<any>, S = unknown> {
|
|
5
|
+
widget: W;
|
|
6
|
+
state: S;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
emit: Emit;
|
|
9
|
+
set: (path: Path, value: unknown) => void;
|
|
10
|
+
slot: (key: string) => ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export type WidgetView = (props: ViewProps<any, any>) => ReactNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./accordion.css";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
export function AccordionView({ widget, state, slot }) {
|
|
5
|
+
const items = state.items ?? [];
|
|
6
|
+
const [open, setOpen] = useState(widget.open !== false);
|
|
7
|
+
return (_jsxs("div", { className: `pu-accordion${open ? " pu-accordion-open" : ""}`, children: [_jsxs("button", { className: "pu-accordion-head", "aria-expanded": open, onClick: () => setOpen((o) => !o), children: [_jsx("span", { className: "pu-accordion-caret", children: "\u25B8" }), widget.label] }), open && _jsx("div", { className: "pu-accordion-body", children: items.map((key) => slot(key)) })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./annotated-image.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { Annotation } from "../../../lib/widgets/annotated-image.js";
|
|
4
|
+
export declare function AnnotatedImageView({ state, }: ViewProps<any, {
|
|
5
|
+
src: string;
|
|
6
|
+
annotations: Annotation[];
|
|
7
|
+
colorMap: Record<string, string>;
|
|
8
|
+
}>): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./annotated-image.css";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
const PALETTE = ["#2563eb", "#16a34a", "#dc2626", "#d97706", "#7c3aed", "#0891b2", "#db2777", "#65a30d"];
|
|
5
|
+
function colorFor(label, colorMap) {
|
|
6
|
+
if (colorMap[label])
|
|
7
|
+
return colorMap[label];
|
|
8
|
+
let h = 0;
|
|
9
|
+
for (let i = 0; i < label.length; i++)
|
|
10
|
+
h = (h * 31 + label.charCodeAt(i)) | 0;
|
|
11
|
+
return PALETTE[Math.abs(h) % PALETTE.length];
|
|
12
|
+
}
|
|
13
|
+
export function AnnotatedImageView({ state, }) {
|
|
14
|
+
// The image's natural pixel size — only known once the <img> loads — turns the author's pixel boxes
|
|
15
|
+
// into percentages of the (possibly resized) displayed image. Boxes wait for it.
|
|
16
|
+
const [nat, setNat] = useState(null);
|
|
17
|
+
if (!state.src)
|
|
18
|
+
return _jsx("div", { className: "pu-output", children: "\u2014" });
|
|
19
|
+
const annotations = state.annotations ?? [];
|
|
20
|
+
const colorMap = state.colorMap ?? {};
|
|
21
|
+
// Distinct labels for the legend, in first-seen order.
|
|
22
|
+
const labels = [...new Set(annotations.map((a) => a.label))];
|
|
23
|
+
return (_jsxs("div", { className: "pu-annimg", children: [_jsxs("div", { className: "pu-annimg-frame", children: [_jsx("img", { className: "pu-annimg-base", src: state.src, alt: "", onLoad: (e) => setNat({ w: e.currentTarget.naturalWidth, h: e.currentTarget.naturalHeight }) }), nat &&
|
|
24
|
+
annotations.map((a, i) => {
|
|
25
|
+
const color = colorFor(a.label, colorMap);
|
|
26
|
+
return (_jsxs("span", { children: [a.mask && _jsx("img", { className: "pu-annimg-mask", src: a.mask, alt: "" }), a.box && (_jsx("span", { className: "pu-annimg-box", style: {
|
|
27
|
+
left: `${(a.box[0] / nat.w) * 100}%`,
|
|
28
|
+
top: `${(a.box[1] / nat.h) * 100}%`,
|
|
29
|
+
width: `${((a.box[2] - a.box[0]) / nat.w) * 100}%`,
|
|
30
|
+
height: `${((a.box[3] - a.box[1]) / nat.h) * 100}%`,
|
|
31
|
+
borderColor: color,
|
|
32
|
+
}, children: _jsx("span", { className: "pu-annimg-tag", style: { background: color }, children: a.label }) }))] }, i));
|
|
33
|
+
})] }), labels.length > 0 && (_jsx("div", { className: "pu-annimg-legend", children: labels.map((l) => (_jsxs("span", { className: "pu-annimg-chip", children: [_jsx("span", { className: "pu-annimg-swatch", style: { background: colorFor(l, colorMap) } }), l] }, l))) }))] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "./button.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ButtonWidget, ButtonState } from "../../../lib/widgets/button.js";
|
|
4
|
+
export declare function ButtonView({ widget, state, enabled, emit }: ViewProps<ButtonWidget, ButtonState>): import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./button.css";
|
|
3
|
+
export function ButtonView({ widget, state, enabled, emit }) {
|
|
4
|
+
return (_jsx("button", { className: "pu-submit", disabled: !enabled, onClick: () => emit(widget.clicked()), children: state.label }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// chart.css is imported by ./lazy.tsx (the static entry wrapper) so it lands in client.css.
|
|
3
|
+
import { ResponsiveContainer, CartesianGrid, XAxis, YAxis, Tooltip, Legend, BarChart, Bar, LineChart, Line, AreaChart, Area } from "recharts";
|
|
4
|
+
const COLORS = ["#2563eb", "#16a34a", "#dc2626", "#d97706", "#7c3aed"]; // knobkit accent + a small palette
|
|
5
|
+
export function ChartView({ widget, state }) {
|
|
6
|
+
const kind = widget.kind ?? "bar";
|
|
7
|
+
const x = widget.x;
|
|
8
|
+
const series = Array.isArray(widget.y) ? widget.y : [widget.y];
|
|
9
|
+
const height = widget.maxHeight ?? 300;
|
|
10
|
+
const data = state.data;
|
|
11
|
+
const axes = (_jsxs(_Fragment, { children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--pu-border, #e3e6ea)" }), _jsx(XAxis, { dataKey: x, tick: { fontSize: 12 } }), _jsx(YAxis, { tick: { fontSize: 12 } }), _jsx(Tooltip, {}), series.length > 1 && _jsx(Legend, {})] }));
|
|
12
|
+
let inner;
|
|
13
|
+
if (kind === "line") {
|
|
14
|
+
inner = (_jsxs(LineChart, { data: data, children: [axes, series.map((k, i) => (_jsx(Line, { type: "monotone", dataKey: k, stroke: COLORS[i % COLORS.length], dot: false }, k)))] }));
|
|
15
|
+
}
|
|
16
|
+
else if (kind === "area") {
|
|
17
|
+
inner = (_jsxs(AreaChart, { data: data, children: [axes, series.map((k, i) => (_jsx(Area, { type: "monotone", dataKey: k, stroke: COLORS[i % COLORS.length], fill: COLORS[i % COLORS.length], fillOpacity: 0.2 }, k)))] }));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
inner = (_jsxs(BarChart, { data: data, children: [axes, series.map((k, i) => (_jsx(Bar, { dataKey: k, fill: COLORS[i % COLORS.length] }, k)))] }));
|
|
21
|
+
}
|
|
22
|
+
return (_jsx("div", { className: "pu-chart", children: _jsx(ResponsiveContainer, { width: "100%", height: height, children: inner }) }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./chart.css"; // kept in the entry bundle (client.css) so no /assets css chunk to serve
|
|
3
|
+
import { lazy, Suspense } from "react";
|
|
4
|
+
// Recharts is sizeable; load it only when an app actually renders a chart widget.
|
|
5
|
+
const Impl = lazy(async () => ({ default: (await import("./index.js")).ChartView }));
|
|
6
|
+
export function ChartView(props) {
|
|
7
|
+
const height = (props.widget.maxHeight ?? 300) + 16; // + container padding
|
|
8
|
+
return (_jsx(Suspense, { fallback: _jsx("div", { className: "pu-chart", style: { height } }), children: _jsx(Impl, { ...props }) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "./chat.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ChatWidget, Message } from "../../../lib/widgets/chat.js";
|
|
4
|
+
export declare function ChatView({ widget, state, emit }: ViewProps<ChatWidget, {
|
|
5
|
+
messages: Message[];
|
|
6
|
+
}>): import("react").JSX.Element;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import "./chat.css";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
// shrink a picked image so attachments (and later history reads) stay small
|
|
5
|
+
async function downscale(dataUrl, max = 768) {
|
|
6
|
+
const img = await new Promise((res, rej) => {
|
|
7
|
+
const i = new Image();
|
|
8
|
+
i.onload = () => res(i);
|
|
9
|
+
i.onerror = rej;
|
|
10
|
+
i.src = dataUrl;
|
|
11
|
+
});
|
|
12
|
+
const scale = Math.min(1, max / Math.max(img.width, img.height));
|
|
13
|
+
if (scale === 1)
|
|
14
|
+
return dataUrl;
|
|
15
|
+
const canvas = document.createElement("canvas");
|
|
16
|
+
canvas.width = Math.round(img.width * scale);
|
|
17
|
+
canvas.height = Math.round(img.height * scale);
|
|
18
|
+
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
19
|
+
return canvas.toDataURL("image/jpeg", 0.85);
|
|
20
|
+
}
|
|
21
|
+
export function ChatView({ widget, state, emit }) {
|
|
22
|
+
const streamRef = useRef(null);
|
|
23
|
+
const recRef = useRef(null);
|
|
24
|
+
const pressedRef = useRef(false);
|
|
25
|
+
const fileRef = useRef(null);
|
|
26
|
+
const [recording, setRecording] = useState(false);
|
|
27
|
+
const [pending, setPending] = useState(null);
|
|
28
|
+
const send = (input) => {
|
|
29
|
+
if (!input.value.trim() && !pending)
|
|
30
|
+
return;
|
|
31
|
+
emit(widget.sent({ text: input.value, image: pending ?? undefined }));
|
|
32
|
+
input.value = "";
|
|
33
|
+
setPending(null);
|
|
34
|
+
};
|
|
35
|
+
const pick = (e) => {
|
|
36
|
+
const f = e.currentTarget.files?.[0];
|
|
37
|
+
if (!f)
|
|
38
|
+
return;
|
|
39
|
+
const r = new FileReader();
|
|
40
|
+
r.onload = async () => setPending(await downscale(String(r.result)));
|
|
41
|
+
r.readAsDataURL(f);
|
|
42
|
+
e.currentTarget.value = "";
|
|
43
|
+
};
|
|
44
|
+
const start = async () => {
|
|
45
|
+
if (streamRef.current || pressedRef.current || !navigator.mediaDevices?.getUserMedia)
|
|
46
|
+
return;
|
|
47
|
+
pressedRef.current = true;
|
|
48
|
+
setRecording(true);
|
|
49
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
50
|
+
if (!pressedRef.current)
|
|
51
|
+
return stream.getTracks().forEach((t) => t.stop());
|
|
52
|
+
streamRef.current = stream;
|
|
53
|
+
const ac = new AudioContext({ sampleRate: 16000 });
|
|
54
|
+
const chunks = [];
|
|
55
|
+
const rec = new MediaRecorder(stream);
|
|
56
|
+
recRef.current = rec;
|
|
57
|
+
rec.ondataavailable = (e) => chunks.push(e.data);
|
|
58
|
+
rec.onstop = async () => {
|
|
59
|
+
const a = await ac.decodeAudioData(await new Blob(chunks).arrayBuffer());
|
|
60
|
+
emit(widget.recorded(a.getChannelData(0)));
|
|
61
|
+
};
|
|
62
|
+
rec.start();
|
|
63
|
+
};
|
|
64
|
+
const stop = () => {
|
|
65
|
+
pressedRef.current = false;
|
|
66
|
+
setRecording(false);
|
|
67
|
+
if (recRef.current?.state === "recording")
|
|
68
|
+
recRef.current.stop();
|
|
69
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
70
|
+
streamRef.current = null;
|
|
71
|
+
recRef.current = null;
|
|
72
|
+
};
|
|
73
|
+
return (_jsxs("div", { className: "pu-chat", children: [state.messages.map((m, i) => (_jsxs("p", { className: `pu-msg pu-${m.role}`, children: [_jsxs("b", { children: [m.role, ":"] }), " ", m.content, m.image && _jsx("img", { className: "pu-msg-image", src: m.image, alt: "" })] }, i))), pending && (_jsxs("div", { className: "pu-attachment", children: [_jsx("img", { src: pending, alt: "" }), _jsx("button", { className: "pu-attach-x", onClick: () => setPending(null), "aria-label": "Remove image", children: "\u2715" })] })), _jsxs("div", { className: "pu-composer", children: [widget.images && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileRef, type: "file", accept: "image/*", hidden: true, onChange: pick }), _jsx("button", { className: "pu-attach", onClick: () => fileRef.current?.click(), "aria-label": "Attach image", children: "+" })] })), _jsx("input", { className: "pu-input", placeholder: widget.placeholder, onKeyDown: (e) => {
|
|
74
|
+
if (e.key === "Enter")
|
|
75
|
+
send(e.currentTarget);
|
|
76
|
+
} }), widget.voice && (_jsx("button", { className: `pu-mic${recording ? " pu-rec" : ""}`, onPointerDown: start, onPointerUp: stop, onPointerLeave: () => pressedRef.current && stop(), "aria-label": "Hold to talk", children: recording ? "●" : "🎤" }))] })] }));
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "./checkbox.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ValueWidget } from "../../../lib/widgets/value.js";
|
|
4
|
+
export declare function CheckboxView({ widget, state, emit, set }: ViewProps<ValueWidget<boolean>, {
|
|
5
|
+
value: boolean;
|
|
6
|
+
}>): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./checkbox.css";
|
|
3
|
+
export function CheckboxView({ widget, state, emit, set }) {
|
|
4
|
+
const update = (v) => {
|
|
5
|
+
set(["value"], v);
|
|
6
|
+
emit(widget.changed(v));
|
|
7
|
+
};
|
|
8
|
+
return (_jsxs("label", { className: "pu-check", children: [_jsx("input", { type: "checkbox", checked: state.value, onChange: (e) => update(e.currentTarget.checked) }), " ", widget.label] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "./checkbox-group.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ValueWidget } from "../../../lib/widgets/value.js";
|
|
4
|
+
export declare function CheckboxGroupView({ widget, state, emit, set }: ViewProps<ValueWidget<string[]>, {
|
|
5
|
+
value: string[];
|
|
6
|
+
}>): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./checkbox-group.css";
|
|
3
|
+
export function CheckboxGroupView({ widget, state, emit, set }) {
|
|
4
|
+
const choices = widget.choices ?? [];
|
|
5
|
+
const selected = state.value ?? [];
|
|
6
|
+
const toggle = (c) => {
|
|
7
|
+
const next = selected.includes(c) ? selected.filter((x) => x !== c) : [...selected, c];
|
|
8
|
+
set(["value"], next);
|
|
9
|
+
emit(widget.changed(next));
|
|
10
|
+
};
|
|
11
|
+
return (_jsx("div", { className: "pu-checkgroup", children: choices.map((c) => (_jsxs("label", { className: "pu-checkgroup-opt", children: [_jsx("input", { type: "checkbox", checked: selected.includes(c), onChange: () => toggle(c) }), " ", c] }, c))) }));
|
|
12
|
+
}
|