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
package/src/lib/bound.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Widget } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// A path into a widget's structured state, e.g. ["messages"] or ["messages", -1, "content"].
|
|
4
|
+
// -1 addresses the last element of an array.
|
|
5
|
+
export type Path = (string | number)[];
|
|
6
|
+
export type EditOp = "set" | "append" | "appendText";
|
|
7
|
+
|
|
8
|
+
// A widget method like `convo.history()` or `convo.append(t)` resolves its data through `bound(w)`.
|
|
9
|
+
// The resolver is set by whichever runtime is executing a handler (server per-request, or mount). It
|
|
10
|
+
// never holds state: reads are pulled from the client on demand (one attribute at a time); writes are
|
|
11
|
+
// generic structured edits the client applies.
|
|
12
|
+
export interface Bound {
|
|
13
|
+
read<T>(widget: Widget<any>, path: Path): Promise<T>;
|
|
14
|
+
edit(widget: Widget<any>, op: EditOp, path: Path, value: unknown): void;
|
|
15
|
+
enable(widget: Widget<any>, value: boolean): void;
|
|
16
|
+
busy(widget: Widget<any>, value: boolean): void;
|
|
17
|
+
key(widget: Widget<any>): string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let resolve: () => Bound | undefined = () => undefined;
|
|
21
|
+
|
|
22
|
+
export function setBoundResolver(fn: () => Bound | undefined): void {
|
|
23
|
+
resolve = fn;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function bound(_widget: Widget<any>): Bound {
|
|
27
|
+
const b = resolve();
|
|
28
|
+
if (!b) throw new Error("widget method called outside a knobkit handler");
|
|
29
|
+
return b;
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { bound } from "./bound.js";
|
|
2
|
+
import type { Widget } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Spread into every widget factory so enable()/disable()/busy() are uniform. `enabled` and `busy` are
|
|
5
|
+
// two orthogonal runtime flags (both separate from state, both drop the widget's input events): enabled
|
|
6
|
+
// is a persistent "can't use this" (dimmed); busy is a transient "this is working" (a loading bar).
|
|
7
|
+
// `busyStart`/`busyEnd` mark a busy span by hand (e.g. across a setup() load); `busy(fn)` wraps an
|
|
8
|
+
// async fn in the same span. `this` is the widget at call time.
|
|
9
|
+
export const controls = {
|
|
10
|
+
enable(this: Widget): void {
|
|
11
|
+
bound(this).enable(this, true);
|
|
12
|
+
},
|
|
13
|
+
disable(this: Widget): void {
|
|
14
|
+
bound(this).enable(this, false);
|
|
15
|
+
},
|
|
16
|
+
setEnabled(this: Widget, value: boolean): void {
|
|
17
|
+
bound(this).enable(this, value);
|
|
18
|
+
},
|
|
19
|
+
busyStart(this: Widget): void {
|
|
20
|
+
bound(this).busy(this, true);
|
|
21
|
+
},
|
|
22
|
+
busyEnd(this: Widget): void {
|
|
23
|
+
bound(this).busy(this, false);
|
|
24
|
+
},
|
|
25
|
+
busy(this: Widget, run: (payload: any) => any): (payload: any) => Promise<any> {
|
|
26
|
+
const self = this;
|
|
27
|
+
return async (payload: any) => {
|
|
28
|
+
self.busyStart();
|
|
29
|
+
try {
|
|
30
|
+
return await run(payload);
|
|
31
|
+
} finally {
|
|
32
|
+
self.busyEnd();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
package/src/lib/ctx.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Bound, EditOp, Path } from "./bound.js";
|
|
2
|
+
import type { Widget } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Builds a handler-time Bound from low-level transport functions. Reads proxy to wherever the state
|
|
5
|
+
// lives (the client, over the socket); edits/enables are sent for the client to apply. Translates the
|
|
6
|
+
// widget object to its key; the handler never sees keys.
|
|
7
|
+
export function makeBound(opts: {
|
|
8
|
+
read: (key: string, path: Path) => Promise<unknown>;
|
|
9
|
+
edit: (key: string, op: EditOp, path: Path, value: unknown) => void;
|
|
10
|
+
enable: (key: string, value: boolean) => void;
|
|
11
|
+
busy: (key: string, value: boolean) => void;
|
|
12
|
+
keyFor: (widget: Widget<any>) => string;
|
|
13
|
+
}): Bound {
|
|
14
|
+
return {
|
|
15
|
+
read<T>(widget: Widget<any>, path: Path): Promise<T> {
|
|
16
|
+
return opts.read(opts.keyFor(widget), path) as Promise<T>;
|
|
17
|
+
},
|
|
18
|
+
edit(widget: Widget<any>, op: EditOp, path: Path, value: unknown): void {
|
|
19
|
+
opts.edit(opts.keyFor(widget), op, path, value);
|
|
20
|
+
},
|
|
21
|
+
enable(widget: Widget<any>, value: boolean): void {
|
|
22
|
+
opts.enable(opts.keyFor(widget), value);
|
|
23
|
+
},
|
|
24
|
+
busy(widget: Widget<any>, value: boolean): void {
|
|
25
|
+
opts.busy(opts.keyFor(widget), value);
|
|
26
|
+
},
|
|
27
|
+
key(widget: Widget<any>): string {
|
|
28
|
+
return opts.keyFor(widget);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { knobkit } from "./knobkit.js";
|
|
3
|
+
import { declare } from "./declare.js";
|
|
4
|
+
import { text, button, output, row, col, grid } from "./widgets/index.js";
|
|
5
|
+
|
|
6
|
+
test("an authored array becomes an implicit col whose items reference the children", () => {
|
|
7
|
+
const a = text();
|
|
8
|
+
const b = output();
|
|
9
|
+
const app = knobkit({ widgets: [a, b] });
|
|
10
|
+
const decl = declare(app.config);
|
|
11
|
+
|
|
12
|
+
const root = decl.widgets.find((w) => w.key === decl.root)!;
|
|
13
|
+
expect(root.type).toBe("col");
|
|
14
|
+
expect((root.state as { items: string[] }).items).toEqual([app.keyFor(a), app.keyFor(b)]);
|
|
15
|
+
expect(decl.widgets.find((w) => w.key === app.keyFor(a))!.state).toEqual({ value: "" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("nested containers lower to a flat decl with items refs at each level", () => {
|
|
19
|
+
const size = text();
|
|
20
|
+
const go = button({ label: "Go" });
|
|
21
|
+
const caption = output();
|
|
22
|
+
const app = knobkit({ widgets: col(row(size, go), caption) });
|
|
23
|
+
const decl = declare(app.config);
|
|
24
|
+
|
|
25
|
+
const root = decl.widgets.find((w) => w.key === decl.root)!;
|
|
26
|
+
expect(root.type).toBe("col");
|
|
27
|
+
const [rowKey, captionKey] = (root.state as { items: string[] }).items;
|
|
28
|
+
expect(captionKey).toBe(app.keyFor(caption));
|
|
29
|
+
|
|
30
|
+
const rowDecl = decl.widgets.find((w) => w.key === rowKey)!;
|
|
31
|
+
expect(rowDecl.type).toBe("row");
|
|
32
|
+
expect((rowDecl.state as { items: string[] }).items).toEqual([app.keyFor(size), app.keyFor(go)]);
|
|
33
|
+
|
|
34
|
+
expect(decl.widgets).toHaveLength(5);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("grid carries its cols as a prop and items refs", () => {
|
|
38
|
+
const a = text();
|
|
39
|
+
const b = text();
|
|
40
|
+
const app = knobkit({ widgets: grid([a, b], { cols: 3 }) });
|
|
41
|
+
const decl = declare(app.config);
|
|
42
|
+
const root = decl.widgets.find((w) => w.key === decl.root)!;
|
|
43
|
+
expect(root.type).toBe("grid");
|
|
44
|
+
expect(root.props.cols).toBe(3);
|
|
45
|
+
expect((root.state as { items: string[] }).items).toEqual([app.keyFor(a), app.keyFor(b)]);
|
|
46
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AppConfig, Widget } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// What the browser needs to render and interact, without the server-side functions: each widget's
|
|
4
|
+
// key, its `type` (which view), its serializable props, and the `type` string of each event it
|
|
5
|
+
// exposes (so the browser can reconstruct emit-able event constructors). `serverEvents` lists the
|
|
6
|
+
// event types that have an `on(...)` handler the browser must route off-device.
|
|
7
|
+
export interface WidgetDecl {
|
|
8
|
+
key: string;
|
|
9
|
+
type: string;
|
|
10
|
+
state: unknown;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
props: Record<string, unknown>;
|
|
13
|
+
events: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AppDecl {
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
widgets: WidgetDecl[];
|
|
20
|
+
root: string;
|
|
21
|
+
serverEvents: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SKIP = new Set(["state", "behavior", "view", "fold", "type", "children", "__subapp"]);
|
|
25
|
+
|
|
26
|
+
interface TreeNode {
|
|
27
|
+
widget: Widget<any>;
|
|
28
|
+
key: string;
|
|
29
|
+
children: TreeNode[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildTree(widgets: Widget<any> | Widget<any>[]): TreeNode {
|
|
33
|
+
let n = 0;
|
|
34
|
+
const visit = (w: Widget<any>): TreeNode => ({
|
|
35
|
+
widget: w,
|
|
36
|
+
key: `${(w.type as string) || "widget"}-${n++}`,
|
|
37
|
+
children: (w.children ?? []).map(visit),
|
|
38
|
+
});
|
|
39
|
+
const root: Widget<any> = Array.isArray(widgets) ? ({ type: "col", state: {}, children: widgets } as Widget<any>) : widgets;
|
|
40
|
+
return visit(root);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function flatten(node: TreeNode): TreeNode[] {
|
|
44
|
+
return [node, ...node.children.flatMap(flatten)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function widgetKeys(widgets: Widget<any> | Widget<any>[]): Map<Widget<any>, string> {
|
|
48
|
+
const map = new Map<Widget<any>, string>();
|
|
49
|
+
for (const node of flatten(buildTree(widgets))) map.set(node.widget, node.key);
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toDecl(node: TreeNode): WidgetDecl {
|
|
54
|
+
const w = node.widget;
|
|
55
|
+
const props: Record<string, unknown> = {};
|
|
56
|
+
const events: Record<string, string> = {};
|
|
57
|
+
for (const [k, v] of Object.entries(w)) {
|
|
58
|
+
if (SKIP.has(k)) continue;
|
|
59
|
+
if (typeof v === "function" && typeof (v as unknown as { type?: unknown }).type === "string") {
|
|
60
|
+
events[k] = (v as unknown as { type: string }).type;
|
|
61
|
+
} else if (typeof v !== "function") {
|
|
62
|
+
props[k] = v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const isContainer = Array.isArray(w.children);
|
|
66
|
+
const state = isContainer ? { items: node.children.map((c) => c.key) } : w.state;
|
|
67
|
+
return { key: node.key, type: (w.type as string | undefined) ?? "", state, enabled: true, props, events };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function declare(config: AppConfig, serverEvents: string[] = []): AppDecl {
|
|
71
|
+
const root = buildTree(config.widgets);
|
|
72
|
+
const widgets = flatten(root).map(toDecl);
|
|
73
|
+
return { title: config.title, description: config.description, widgets, root: root.key, serverEvents };
|
|
74
|
+
}
|
package/src/lib/event.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Event, EventCtor } from "./types.js";
|
|
2
|
+
|
|
3
|
+
let counter = 0;
|
|
4
|
+
|
|
5
|
+
// Declare an event. Each call is a distinct event with a unique `type`; the optional name is only
|
|
6
|
+
// for readability. The returned constructor makes instances: `Token("hi") -> { type, payload }`.
|
|
7
|
+
export function event<P = void>(name?: string): EventCtor<P> {
|
|
8
|
+
const type = `${name ?? "event"}#${counter++}`;
|
|
9
|
+
const ctor = ((payload: P): Event<P> => ({ type, payload })) as EventCtor<P>;
|
|
10
|
+
ctor.type = type;
|
|
11
|
+
return ctor;
|
|
12
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
Event,
|
|
3
|
+
EventCtor,
|
|
4
|
+
Emit,
|
|
5
|
+
On,
|
|
6
|
+
Widget,
|
|
7
|
+
SelfListen,
|
|
8
|
+
Produce,
|
|
9
|
+
Listen,
|
|
10
|
+
AppConfig,
|
|
11
|
+
KnobkitServer,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
export type { Handler } from "./on.js";
|
|
14
|
+
export type { Bound } from "./bound.js";
|
|
15
|
+
export { event } from "./event.js";
|
|
16
|
+
export { widget } from "./widget.js";
|
|
17
|
+
export { bound } from "./bound.js";
|
|
18
|
+
export { stream } from "./stream.js";
|
|
19
|
+
export { Knobkit, knobkit } from "./knobkit.js";
|
|
20
|
+
export type { KnobkitApp } from "./knobkit.js";
|
|
21
|
+
export * from "./widgets/index.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AppConfig, KnobkitServer, EventCtor, Widget } from "./types.js";
|
|
2
|
+
import type { Handler } from "./on.js";
|
|
3
|
+
import { widgetKeys } from "./declare.js";
|
|
4
|
+
import { collectSubapps } from "./widgets/embed.js";
|
|
5
|
+
|
|
6
|
+
// The knobkit is the authored app: a set of widgets plus the `on(event, handler)` handlers registered
|
|
7
|
+
// against their events. It holds no state — the browser owns that. Handlers run on the server (serve)
|
|
8
|
+
// or in-browser (mount); both resolve a handler by the event's `type`.
|
|
9
|
+
export class Knobkit {
|
|
10
|
+
readonly handlers = new Map<string, Handler<any>[]>();
|
|
11
|
+
readonly setups: Array<() => void | Promise<void>> = [];
|
|
12
|
+
private readonly keys: Map<Widget<any>, string>;
|
|
13
|
+
|
|
14
|
+
constructor(public config: AppConfig) {
|
|
15
|
+
this.keys = widgetKeys(config.widgets);
|
|
16
|
+
for (const sub of collectSubapps(config.widgets)) {
|
|
17
|
+
for (const [type, hs] of sub.handlers) this.handlers.set(type, [...(this.handlers.get(type) ?? []), ...hs]);
|
|
18
|
+
this.setups.push(...sub.setups);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
keyFor(widget: Widget<any>): string {
|
|
23
|
+
const key = this.keys.get(widget);
|
|
24
|
+
if (!key) throw new Error("widget is not part of this knobkit — pass it to knobkit({ widgets })");
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
on<P>(source: EventCtor<P>, handler: Handler<P>): this {
|
|
29
|
+
const list = this.handlers.get(source.type) ?? [];
|
|
30
|
+
list.push(handler as Handler<any>);
|
|
31
|
+
this.handlers.set(source.type, list);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setup(fn: () => void | Promise<void>): this {
|
|
36
|
+
this.setups.push(fn);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
serverEvents(): string[] {
|
|
41
|
+
return [...this.handlers.keys()];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
mount(selector: string): void {
|
|
45
|
+
void import("../client/mount.js").then(({ mount }) => mount(this, selector));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
serve(opts?: { port?: number }): Promise<KnobkitServer> {
|
|
49
|
+
return import("../server/serve.js").then(({ serve }) => serve(this, opts));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type KnobkitApp = Knobkit;
|
|
54
|
+
|
|
55
|
+
export function knobkit(config: AppConfig): Knobkit {
|
|
56
|
+
return new Knobkit(config);
|
|
57
|
+
}
|
package/src/lib/on.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export async function* stream<T>(run: (push: (value: T) => void) => Promise<void> | void): AsyncGenerator<T> {
|
|
2
|
+
const buffer: T[] = [];
|
|
3
|
+
let wake: (() => void) | null = null;
|
|
4
|
+
let done = false;
|
|
5
|
+
let failure: unknown = null;
|
|
6
|
+
|
|
7
|
+
const push = (value: T): void => {
|
|
8
|
+
buffer.push(value);
|
|
9
|
+
wake?.();
|
|
10
|
+
wake = null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
Promise.resolve(run(push)).then(
|
|
14
|
+
() => {
|
|
15
|
+
done = true;
|
|
16
|
+
wake?.();
|
|
17
|
+
},
|
|
18
|
+
(err) => {
|
|
19
|
+
failure = err;
|
|
20
|
+
done = true;
|
|
21
|
+
wake?.();
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
while (true) {
|
|
26
|
+
if (buffer.length) {
|
|
27
|
+
yield buffer.shift()!;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (done) {
|
|
31
|
+
if (failure) throw failure;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await new Promise<void>((resolve) => {
|
|
35
|
+
wake = resolve;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface Event<P = unknown> {
|
|
2
|
+
type: string;
|
|
3
|
+
payload: P;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface EventCtor<P = void> {
|
|
7
|
+
(payload: P): Event<P>;
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Emit = <P>(event: Event<P>) => void;
|
|
12
|
+
|
|
13
|
+
export type On<P = any> = EventCtor<P> | Widget<any>;
|
|
14
|
+
|
|
15
|
+
export type Produce<P = any, I = any, V = any> = (
|
|
16
|
+
payload: P,
|
|
17
|
+
inputs: I,
|
|
18
|
+
) => V | Promise<V> | AsyncIterable<V>;
|
|
19
|
+
|
|
20
|
+
export interface Widget<S = unknown> {
|
|
21
|
+
type?: string;
|
|
22
|
+
state: S;
|
|
23
|
+
children?: Widget<any>[];
|
|
24
|
+
fold?: (state: any, value: any) => S;
|
|
25
|
+
behavior?: SelfListen<any, any>[];
|
|
26
|
+
view?: (state: S, emit: Emit) => unknown;
|
|
27
|
+
// Uniform control over whether the widget is interactive — see controls.ts. Provided by every widget
|
|
28
|
+
// factory. enabled is a persistent disable (dimmed); busy is a transient working state (a loading
|
|
29
|
+
// bar). Both drop the input events the widget emits.
|
|
30
|
+
enable(): void;
|
|
31
|
+
disable(): void;
|
|
32
|
+
setEnabled(value: boolean): void;
|
|
33
|
+
busyStart(): void;
|
|
34
|
+
busyEnd(): void;
|
|
35
|
+
busy<P = unknown>(run: (payload: P) => void | Event | Promise<void | Event>): (payload: P) => Promise<void | Event>;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SelfListen<P = any, V = any> {
|
|
40
|
+
on: EventCtor<P>;
|
|
41
|
+
from?: Record<string, Widget<any>>;
|
|
42
|
+
respond: Produce<P, any, V>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Listen<P = any, I = any, V = any> {
|
|
46
|
+
on: On<P>;
|
|
47
|
+
in: Widget<any>;
|
|
48
|
+
from?: Record<string, Widget<any>>;
|
|
49
|
+
respond: Produce<P, I, V>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AppConfig {
|
|
53
|
+
title?: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
events?: EventCtor<any>[];
|
|
56
|
+
widgets: Widget<any> | Widget<any>[];
|
|
57
|
+
loading?: string; // raw HTML placed in #root before mount, for serve() (mount apps own index.html)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface KnobkitServer {
|
|
61
|
+
url: string;
|
|
62
|
+
stop(): Promise<void>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { controls } from "./controls.js";
|
|
2
|
+
import type { Emit, SelfListen, Widget } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function widget<S>(init: {
|
|
5
|
+
state: S;
|
|
6
|
+
fold?: (state: S, value: any) => S;
|
|
7
|
+
view?: (state: S, emit: Emit) => unknown;
|
|
8
|
+
behavior?: SelfListen<any, any>[];
|
|
9
|
+
}): Widget<S> {
|
|
10
|
+
return { state: init.state, fold: init.fold, view: init.view, behavior: init.behavior, ...controls };
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { bound } from "../bound.js";
|
|
2
|
+
import { controls } from "../controls.js";
|
|
3
|
+
import type { Widget } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export interface Annotation {
|
|
6
|
+
label: string;
|
|
7
|
+
// A bounding box as [xmin, ymin, xmax, ymax] in pixels of the image's natural size — the format
|
|
8
|
+
// detection models emit (and what gr.AnnotatedImage takes). The view divides by the loaded image's
|
|
9
|
+
// natural width/height to place it, so any displayed size works.
|
|
10
|
+
box?: [number, number, number, number];
|
|
11
|
+
// Optionally, a full-size mask image (URL or data URL) overlaid for this region (segmentation).
|
|
12
|
+
mask?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AnnotatedImageWidget
|
|
16
|
+
extends Widget<{ src: string; annotations: Annotation[]; colorMap: Record<string, string> }> {
|
|
17
|
+
// A base image with labeled regions on top: the standard output for detection / segmentation.
|
|
18
|
+
// `colorMap` pins a color per label; unmapped labels get a stable auto color in the view.
|
|
19
|
+
set(src: string, annotations?: Annotation[], colorMap?: Record<string, string>): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function annotatedImage(): AnnotatedImageWidget {
|
|
23
|
+
return {
|
|
24
|
+
type: "annotatedImage",
|
|
25
|
+
state: { src: "", annotations: [], colorMap: {} },
|
|
26
|
+
...controls,
|
|
27
|
+
set(src: string, annotations?: Annotation[], colorMap?: Record<string, string>): void {
|
|
28
|
+
const b = bound(this);
|
|
29
|
+
if (colorMap !== undefined) b.edit(this, "set", ["colorMap"], colorMap);
|
|
30
|
+
b.edit(this, "set", ["annotations"], annotations ?? []);
|
|
31
|
+
b.edit(this, "set", ["src"], src);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { bound } from "../bound.js";
|
|
2
|
+
import { controls } from "../controls.js";
|
|
3
|
+
import type { Widget } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export interface AudioWidget extends Widget<{ src: string }> {
|
|
6
|
+
autoplay: boolean;
|
|
7
|
+
set(value: string): void; // set the audio source (URL or data URL)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function audio(opts: { autoplay?: boolean } = {}): AudioWidget {
|
|
11
|
+
return {
|
|
12
|
+
type: "audio",
|
|
13
|
+
state: { src: "" },
|
|
14
|
+
autoplay: opts.autoplay ?? false,
|
|
15
|
+
...controls,
|
|
16
|
+
set(value: string): void {
|
|
17
|
+
bound(this).edit(this, "set", ["src"], value);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { event } from "../event.js";
|
|
2
|
+
import { bound } from "../bound.js";
|
|
3
|
+
import { controls } from "../controls.js";
|
|
4
|
+
import type { EventCtor, Widget } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export interface ButtonState {
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ButtonWidget extends Widget<ButtonState> {
|
|
11
|
+
clicked: EventCtor;
|
|
12
|
+
set(patch: Partial<ButtonState>): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// state is the label it shows; whether it's interactive is the framework-level enabled flag (controls)
|
|
16
|
+
export function button(opts: { label: string }): ButtonWidget {
|
|
17
|
+
return {
|
|
18
|
+
type: "button",
|
|
19
|
+
state: { label: opts.label },
|
|
20
|
+
clicked: event("button.clicked"),
|
|
21
|
+
...controls,
|
|
22
|
+
set(patch: Partial<ButtonState>): void {
|
|
23
|
+
const rt = bound(this);
|
|
24
|
+
for (const [k, v] of Object.entries(patch)) rt.edit(this, "set", [k], v);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { bound } from "../bound.js";
|
|
2
|
+
import { controls } from "../controls.js";
|
|
3
|
+
import type { Widget } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export type Point = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export interface ChartWidget extends Widget<{ data: Point[] }> {
|
|
8
|
+
kind: "bar" | "line" | "area";
|
|
9
|
+
x: string; // key for the category/x axis
|
|
10
|
+
y: string | string[]; // one or more series keys to plot
|
|
11
|
+
maxHeight: number;
|
|
12
|
+
data(): Promise<Point[]>;
|
|
13
|
+
setData(data: Point[]): void; // replace the plotted data
|
|
14
|
+
push(point: Point): void; // append one data point (append ["data"])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A read-only chart rendered by Recharts. Pure output: state is `{ data }` (an array of row objects)
|
|
18
|
+
// and the view reads it; nothing rounds-trips back. `x`/`y` name which keys map to the axes/series.
|
|
19
|
+
export function chart(opts: {
|
|
20
|
+
x: string;
|
|
21
|
+
y: string | string[];
|
|
22
|
+
kind?: "bar" | "line" | "area";
|
|
23
|
+
data?: Point[];
|
|
24
|
+
maxHeight?: number;
|
|
25
|
+
}): ChartWidget {
|
|
26
|
+
return {
|
|
27
|
+
type: "chart",
|
|
28
|
+
state: { data: opts.data ?? [] },
|
|
29
|
+
kind: opts.kind ?? "bar",
|
|
30
|
+
x: opts.x,
|
|
31
|
+
y: opts.y,
|
|
32
|
+
maxHeight: opts.maxHeight ?? 300,
|
|
33
|
+
...controls,
|
|
34
|
+
data(): Promise<Point[]> {
|
|
35
|
+
return bound(this).read<Point[]>(this, ["data"]);
|
|
36
|
+
},
|
|
37
|
+
setData(data: Point[]): void {
|
|
38
|
+
bound(this).edit(this, "set", ["data"], data);
|
|
39
|
+
},
|
|
40
|
+
push(point: Point): void {
|
|
41
|
+
bound(this).edit(this, "append", ["data"], point);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { event } from "../event.js";
|
|
2
|
+
import { bound } from "../bound.js";
|
|
3
|
+
import { controls } from "../controls.js";
|
|
4
|
+
import type { EventCtor, Widget } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export interface Message {
|
|
7
|
+
role: "user" | "assistant" | "system";
|
|
8
|
+
content: string;
|
|
9
|
+
image?: string; // optional attached image, as a data URL
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ChatWidget extends Widget<{ messages: Message[] }> {
|
|
13
|
+
sent: EventCtor<{ text: string; image?: string }>;
|
|
14
|
+
recorded: EventCtor<Float32Array>;
|
|
15
|
+
placeholder: string;
|
|
16
|
+
voice: boolean;
|
|
17
|
+
images: boolean;
|
|
18
|
+
history(): Promise<Message[]>;
|
|
19
|
+
say(message: Message): void; // append a whole message
|
|
20
|
+
append(token: string): void; // stream a token into the last message's content
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function chat(opts: { placeholder?: string; voice?: boolean; images?: boolean } = {}): ChatWidget {
|
|
24
|
+
return {
|
|
25
|
+
type: "chat",
|
|
26
|
+
state: { messages: [] },
|
|
27
|
+
sent: event<{ text: string; image?: string }>("chat.sent"),
|
|
28
|
+
recorded: event<Float32Array>("chat.recorded"),
|
|
29
|
+
placeholder: opts.placeholder ?? "Say something…",
|
|
30
|
+
voice: opts.voice ?? false,
|
|
31
|
+
images: opts.images ?? false,
|
|
32
|
+
...controls,
|
|
33
|
+
history(): Promise<Message[]> {
|
|
34
|
+
return bound(this).read<Message[]>(this, ["messages"]);
|
|
35
|
+
},
|
|
36
|
+
say(message: Message): void {
|
|
37
|
+
bound(this).edit(this, "append", ["messages"], message);
|
|
38
|
+
},
|
|
39
|
+
append(token: string): void {
|
|
40
|
+
bound(this).edit(this, "appendText", ["messages", -1, "content"], token);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { value } from "./value.js";
|
|
2
|
+
|
|
3
|
+
// Multi-select from a fixed set; its `value` is the array of checked choices.
|
|
4
|
+
export function checkboxGroup(opts: { choices: string[]; value?: string[] }) {
|
|
5
|
+
return value<string[]>("checkboxGroup", opts.value ?? [], { choices: opts.choices });
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { value } from "./value.js";
|
|
2
|
+
|
|
3
|
+
// A code editor over the uniform `value` attribute: holds `{ value: string }`, plus `language` (syntax
|
|
4
|
+
// highlighting) and `editable` (false = a read-only, still-highlighted viewer) as static props. The
|
|
5
|
+
// CodeMirror instance lives in the view; this stays a plain value widget (changed/value()/set()).
|
|
6
|
+
export function code(opts: { value?: string; language?: string; editable?: boolean } = {}) {
|
|
7
|
+
return value("code", opts.value ?? "", {
|
|
8
|
+
language: opts.language ?? "",
|
|
9
|
+
editable: opts.editable ?? true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Knobkit } from "../knobkit.js";
|
|
2
|
+
import type { Widget } from "../types.js";
|
|
3
|
+
import { col } from "./layout.js";
|
|
4
|
+
|
|
5
|
+
export const SUBAPP = "__subapp";
|
|
6
|
+
|
|
7
|
+
export function embed(app: Knobkit): Widget {
|
|
8
|
+
const ws = app.config.widgets;
|
|
9
|
+
const node = col(...(Array.isArray(ws) ? ws : [ws])) as Widget;
|
|
10
|
+
node[SUBAPP] = app;
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function collectSubapps(widgets: Widget | Widget[]): Knobkit[] {
|
|
15
|
+
const out: Knobkit[] = [];
|
|
16
|
+
const visit = (w: Widget): void => {
|
|
17
|
+
const sub = w[SUBAPP] as Knobkit | undefined;
|
|
18
|
+
if (sub) {
|
|
19
|
+
out.push(sub);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const c of w.children ?? []) visit(c);
|
|
23
|
+
};
|
|
24
|
+
for (const w of Array.isArray(widgets) ? widgets : [widgets]) visit(w);
|
|
25
|
+
return out;
|
|
26
|
+
}
|