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,14 @@
|
|
|
1
|
+
import "./progress.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
|
|
4
|
+
export function ProgressView({ state }: ViewProps<any, { value: number; label: string }>) {
|
|
5
|
+
const pct = Math.round(Math.max(0, Math.min(1, state.value ?? 0)) * 100);
|
|
6
|
+
return (
|
|
7
|
+
<div className="pu-progress">
|
|
8
|
+
<div className="pu-progress-track">
|
|
9
|
+
<div className="pu-progress-fill" style={{ width: `${pct}%` }} />
|
|
10
|
+
</div>
|
|
11
|
+
<span className="pu-progress-label">{state.label || `${pct}%`}</span>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-progress {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: 10px;
|
|
6
|
+
}
|
|
7
|
+
.pu-progress-track {
|
|
8
|
+
flex: 1;
|
|
9
|
+
height: 8px;
|
|
10
|
+
border-radius: 999px;
|
|
11
|
+
background: var(--pu-border, #e3e6ea);
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
.pu-progress-fill {
|
|
15
|
+
height: 100%;
|
|
16
|
+
background: var(--pu-accent, #2563eb);
|
|
17
|
+
border-radius: 999px;
|
|
18
|
+
transition: width 0.2s ease;
|
|
19
|
+
}
|
|
20
|
+
.pu-progress-label {
|
|
21
|
+
color: var(--pu-muted, #6b7280);
|
|
22
|
+
font-variant-numeric: tabular-nums;
|
|
23
|
+
min-width: 3ch;
|
|
24
|
+
text-align: right;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "./radio.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ValueWidget } from "../../../lib/widgets/value.js";
|
|
4
|
+
|
|
5
|
+
export function RadioView({ widget, state, emit, set }: ViewProps<ValueWidget<string>, { value: string }>) {
|
|
6
|
+
const choices = (widget.choices as string[]) ?? [];
|
|
7
|
+
const update = (v: string) => {
|
|
8
|
+
set(["value"], v);
|
|
9
|
+
emit(widget.changed(v));
|
|
10
|
+
};
|
|
11
|
+
return (
|
|
12
|
+
<div className="pu-radio">
|
|
13
|
+
{choices.map((c) => (
|
|
14
|
+
<label key={c} className="pu-radio-opt">
|
|
15
|
+
<input type="radio" checked={state.value === c} onChange={() => update(c)} /> {c}
|
|
16
|
+
</label>
|
|
17
|
+
))}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-radio {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
}
|
|
7
|
+
.pu-radio-opt {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 8px;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
user-select: none;
|
|
13
|
+
}
|
|
14
|
+
.pu-radio-opt input {
|
|
15
|
+
width: 16px;
|
|
16
|
+
height: 16px;
|
|
17
|
+
accent-color: var(--pu-accent, #2563eb);
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { WidgetView } from "../view.js";
|
|
2
|
+
import { TextView } from "./text/index.js";
|
|
3
|
+
import { NumberView } from "./number/index.js";
|
|
4
|
+
import { SliderView } from "./slider/index.js";
|
|
5
|
+
import { CodeView } from "./code/lazy.js";
|
|
6
|
+
import { TableView } from "./table/lazy.js";
|
|
7
|
+
import { ChartView } from "./chart/lazy.js";
|
|
8
|
+
import { DropdownView } from "./dropdown/index.js";
|
|
9
|
+
import { CheckboxView } from "./checkbox/index.js";
|
|
10
|
+
import { CheckboxGroupView } from "./checkbox-group/index.js";
|
|
11
|
+
import { RadioView } from "./radio/index.js";
|
|
12
|
+
import { GalleryView } from "./gallery/index.js";
|
|
13
|
+
import { VideoView } from "./video/index.js";
|
|
14
|
+
import { LabelView } from "./label/index.js";
|
|
15
|
+
import { HighlightedTextView } from "./highlighted-text/index.js";
|
|
16
|
+
import { AnnotatedImageView } from "./annotated-image/index.js";
|
|
17
|
+
import { FileView } from "./file/index.js";
|
|
18
|
+
import { ProgressView } from "./progress/index.js";
|
|
19
|
+
import { HtmlView } from "./html/index.js";
|
|
20
|
+
import { FrameView } from "./frame/index.js";
|
|
21
|
+
import { UploadView } from "./upload/index.js";
|
|
22
|
+
import { ImageView } from "./image/index.js";
|
|
23
|
+
import { ButtonView } from "./button/index.js";
|
|
24
|
+
import { MicView } from "./mic/index.js";
|
|
25
|
+
import { ChatView } from "./chat/index.js";
|
|
26
|
+
import { OutputView } from "./output/index.js";
|
|
27
|
+
import { JsonView } from "./json/index.js";
|
|
28
|
+
import { LogView } from "./log/index.js";
|
|
29
|
+
import { AudioView } from "./audio/index.js";
|
|
30
|
+
import { WebcamView } from "./webcam/index.js";
|
|
31
|
+
import { LayoutView } from "./layout/index.js";
|
|
32
|
+
import { TabsView } from "./tabs/index.js";
|
|
33
|
+
import { AccordionView } from "./accordion/index.js";
|
|
34
|
+
|
|
35
|
+
// the only place that maps a widget's `type` to its React view
|
|
36
|
+
export const VIEWS: Record<string, WidgetView> = {
|
|
37
|
+
text: TextView,
|
|
38
|
+
number: NumberView,
|
|
39
|
+
slider: SliderView,
|
|
40
|
+
code: CodeView,
|
|
41
|
+
table: TableView,
|
|
42
|
+
chart: ChartView,
|
|
43
|
+
dropdown: DropdownView,
|
|
44
|
+
checkbox: CheckboxView,
|
|
45
|
+
checkboxGroup: CheckboxGroupView,
|
|
46
|
+
radio: RadioView,
|
|
47
|
+
gallery: GalleryView,
|
|
48
|
+
video: VideoView,
|
|
49
|
+
label: LabelView,
|
|
50
|
+
highlightedText: HighlightedTextView,
|
|
51
|
+
annotatedImage: AnnotatedImageView,
|
|
52
|
+
file: FileView,
|
|
53
|
+
progress: ProgressView,
|
|
54
|
+
html: HtmlView,
|
|
55
|
+
frame: FrameView,
|
|
56
|
+
upload: UploadView,
|
|
57
|
+
image: ImageView,
|
|
58
|
+
button: ButtonView,
|
|
59
|
+
mic: MicView,
|
|
60
|
+
chat: ChatView,
|
|
61
|
+
output: OutputView,
|
|
62
|
+
json: JsonView,
|
|
63
|
+
log: LogView,
|
|
64
|
+
audio: AudioView,
|
|
65
|
+
webcam: WebcamView,
|
|
66
|
+
row: LayoutView,
|
|
67
|
+
col: LayoutView,
|
|
68
|
+
grid: LayoutView,
|
|
69
|
+
tabs: TabsView,
|
|
70
|
+
accordion: AccordionView,
|
|
71
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "./slider.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ValueWidget } from "../../../lib/widgets/value.js";
|
|
4
|
+
|
|
5
|
+
export function SliderView({ widget, state, emit, set }: ViewProps<ValueWidget<number>, { value: number }>) {
|
|
6
|
+
const update = (v: number) => {
|
|
7
|
+
set(["value"], v); // local, so the track and read-back reflect the drag with no round-trip
|
|
8
|
+
emit(widget.changed(v));
|
|
9
|
+
};
|
|
10
|
+
return (
|
|
11
|
+
<div className="pu-slider">
|
|
12
|
+
<input
|
|
13
|
+
type="range"
|
|
14
|
+
min={widget.min as number}
|
|
15
|
+
max={widget.max as number}
|
|
16
|
+
step={widget.step as number}
|
|
17
|
+
value={state.value}
|
|
18
|
+
onChange={(e) => update(e.currentTarget.valueAsNumber)}
|
|
19
|
+
/>
|
|
20
|
+
<output className="pu-slider-val">{state.value}</output>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-slider {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: 12px;
|
|
6
|
+
}
|
|
7
|
+
.pu-slider input[type="range"] {
|
|
8
|
+
flex: 1;
|
|
9
|
+
accent-color: var(--pu-accent, #2563eb);
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
}
|
|
12
|
+
.pu-slider-val {
|
|
13
|
+
min-width: 3ch;
|
|
14
|
+
text-align: right;
|
|
15
|
+
font-variant-numeric: tabular-nums;
|
|
16
|
+
color: var(--pu-muted, #6b7280);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// table.css is imported by ./lazy.tsx (the static entry wrapper) so it lands in client.css, not a
|
|
2
|
+
// split css chunk serve() wouldn't route.
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { RevoGrid, type AfterEditEvent, type ColumnRegular, type DataType } from "@revolist/react-datagrid";
|
|
5
|
+
import type { ViewProps } from "../../view.js";
|
|
6
|
+
import type { TableWidget, Column, Row } from "../../../lib/widgets/table.js";
|
|
7
|
+
|
|
8
|
+
// "compact" theme metrics (from revo-grid-style.css): header row 45px, data row 32px. Used to fit the
|
|
9
|
+
// grid to its content up to maxHeight, then scroll — matching Gradio's max_height behavior.
|
|
10
|
+
const HEADER_PX = 45;
|
|
11
|
+
const ROW_PX = 32;
|
|
12
|
+
|
|
13
|
+
// A column with no explicit width defaults to one that fits its *header* — the header is usually the
|
|
14
|
+
// wider constraint for compact tables (e.g. "Δ from mean" over numeric cells), and unlike content-based
|
|
15
|
+
// auto-size it stays stable as data changes. Measured with canvas for proportional-font accuracy.
|
|
16
|
+
const HEADER_FONT = "600 14px Helvetica, Arial, sans-serif"; // compact theme header
|
|
17
|
+
const COL_PAD = 34; // header cell padding + room for the sort arrow
|
|
18
|
+
const MIN_COL = 80;
|
|
19
|
+
const MAX_COL = 240;
|
|
20
|
+
let measureCtx: CanvasRenderingContext2D | null | undefined;
|
|
21
|
+
function headerWidth(label: string): number {
|
|
22
|
+
if (measureCtx === undefined) measureCtx = document.createElement("canvas").getContext("2d");
|
|
23
|
+
let w: number;
|
|
24
|
+
if (measureCtx) {
|
|
25
|
+
measureCtx.font = HEADER_FONT;
|
|
26
|
+
w = measureCtx.measureText(label).width;
|
|
27
|
+
} else {
|
|
28
|
+
w = label.length * 8; // canvas unavailable: rough fallback
|
|
29
|
+
}
|
|
30
|
+
return Math.max(MIN_COL, Math.min(MAX_COL, Math.ceil(w) + COL_PAD));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function TableView({ widget, state, emit, set }: ViewProps<TableWidget, { columns: Column[]; rows: Row[] }>) {
|
|
34
|
+
const editable = (widget.editable as boolean) ?? false;
|
|
35
|
+
const maxHeight = (widget.maxHeight as number) ?? 500;
|
|
36
|
+
// fit to content (header + rows), capped at maxHeight; +2 leaves room for a horizontal scrollbar so
|
|
37
|
+
// a fitting grid doesn't sprout a vertical one
|
|
38
|
+
const height = Math.min(maxHeight, HEADER_PX + state.rows.length * ROW_PX + 2);
|
|
39
|
+
|
|
40
|
+
// RevoGrid warns that columns/source need stable references; the store hands us a fresh state object
|
|
41
|
+
// only on a real edit (edits are immutable), so memoizing on those references is both stable and correct.
|
|
42
|
+
const columns = useMemo<ColumnRegular[]>(
|
|
43
|
+
() =>
|
|
44
|
+
state.columns.map((c) => ({
|
|
45
|
+
prop: c.key,
|
|
46
|
+
name: c.label ?? c.key,
|
|
47
|
+
sortable: true,
|
|
48
|
+
readonly: !editable,
|
|
49
|
+
size: c.width ?? headerWidth(c.label ?? c.key),
|
|
50
|
+
})),
|
|
51
|
+
[state.columns, editable],
|
|
52
|
+
);
|
|
53
|
+
// clone each row: RevoGrid mutates its own source in place on edit — we must not let it touch the
|
|
54
|
+
// store's state objects. The real change is applied through the store via onAfteredit below.
|
|
55
|
+
const source = useMemo<DataType[]>(() => state.rows.map((r) => ({ ...r })), [state.rows]);
|
|
56
|
+
|
|
57
|
+
const apply = (row: number, key: string, value: unknown): void => {
|
|
58
|
+
// a range/paste commit reports every cell in the selection; skip the ones that didn't change so
|
|
59
|
+
// handlers don't see no-op edits
|
|
60
|
+
if (state.rows[row]?.[key] === value) return;
|
|
61
|
+
set(["rows", row, key], value); // local edit by path; reads reflect it immediately
|
|
62
|
+
emit(widget.edited({ row, key, value }));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onAfteredit = (e: CustomEvent<AfterEditEvent>): void => {
|
|
66
|
+
const d = e.detail as Record<string, unknown>;
|
|
67
|
+
if (d && "data" in d) {
|
|
68
|
+
// range / paste edit: { data: { [rowIndex]: changedRowModel } }
|
|
69
|
+
for (const [ri, model] of Object.entries(d.data as Record<string, Record<string, unknown>>)) {
|
|
70
|
+
for (const [key, value] of Object.entries(model)) apply(Number(ri), key, value);
|
|
71
|
+
}
|
|
72
|
+
} else if (d) {
|
|
73
|
+
// single cell: val is the editor's new value, not yet in the model
|
|
74
|
+
apply(d.rowIndex as number, d.prop as string, d.val);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="pu-table">
|
|
80
|
+
<RevoGrid
|
|
81
|
+
columns={columns}
|
|
82
|
+
source={source}
|
|
83
|
+
readonly={!editable}
|
|
84
|
+
range={true}
|
|
85
|
+
resize={true}
|
|
86
|
+
rowHeaders={true}
|
|
87
|
+
theme="compact"
|
|
88
|
+
stretch={true}
|
|
89
|
+
hideAttribution={true}
|
|
90
|
+
style={{ height }}
|
|
91
|
+
onAfteredit={onAfteredit}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "./table.css"; // kept in the entry bundle (client.css) so no /assets css chunk to serve
|
|
2
|
+
import { lazy, Suspense, type ComponentType } from "react";
|
|
3
|
+
import type { ViewProps } from "../../view.js";
|
|
4
|
+
|
|
5
|
+
// RevoGrid is a heavy web-component grid; load it only when an app actually renders a table widget.
|
|
6
|
+
// TableView narrows ViewProps internally; the registry hands it the generic shape, so widen here.
|
|
7
|
+
const Impl = lazy(async () => ({ default: (await import("./index.js")).TableView as unknown as ComponentType<ViewProps> }));
|
|
8
|
+
|
|
9
|
+
// mirror index.tsx's fit math so the loading placeholder is the grid's real height — no layout jump
|
|
10
|
+
// when the chunk resolves (can't import the constants from index.js without pulling it into the entry)
|
|
11
|
+
const fitHeight = (props: ViewProps): number => {
|
|
12
|
+
const rows = (props.state as { rows?: unknown[] }).rows ?? [];
|
|
13
|
+
const maxHeight = (props.widget as { maxHeight?: number }).maxHeight ?? 500;
|
|
14
|
+
return Math.min(maxHeight, 45 + rows.length * 32 + 2);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function TableView(props: ViewProps) {
|
|
18
|
+
return (
|
|
19
|
+
<Suspense fallback={<div className="pu-table" style={{ height: fitHeight(props) }} />}>
|
|
20
|
+
<Impl {...props} />
|
|
21
|
+
</Suspense>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-table {
|
|
3
|
+
border: 1px solid var(--pu-border, #e3e6ea);
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
}
|
|
7
|
+
.pu-table revo-grid {
|
|
8
|
+
display: block;
|
|
9
|
+
width: 100%;
|
|
10
|
+
/* RevoGrid floors the host at min-height:300px; we drive an exact fit height inline, so drop the
|
|
11
|
+
floor. !important is needed because RevoGrid injects that rule unlayered, which outranks our
|
|
12
|
+
@layer styles regardless of specificity. */
|
|
13
|
+
min-height: 0 !important;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "./tabs.css";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import type { ViewProps } from "../../view.js";
|
|
4
|
+
|
|
5
|
+
export function TabsView({ widget, state, slot }: ViewProps<any, { items: string[] }>) {
|
|
6
|
+
const items = state.items ?? [];
|
|
7
|
+
const labels = (widget.labels as string[]) ?? [];
|
|
8
|
+
const [active, setActive] = useState(0);
|
|
9
|
+
const current = Math.min(active, Math.max(0, items.length - 1));
|
|
10
|
+
return (
|
|
11
|
+
<div className="pu-tabs">
|
|
12
|
+
<div className="pu-tabs-bar" role="tablist">
|
|
13
|
+
{items.map((_key, i) => (
|
|
14
|
+
<button
|
|
15
|
+
key={i}
|
|
16
|
+
role="tab"
|
|
17
|
+
aria-selected={i === current}
|
|
18
|
+
className={`pu-tab${i === current ? " pu-tab-active" : ""}`}
|
|
19
|
+
onClick={() => setActive(i)}
|
|
20
|
+
>
|
|
21
|
+
{labels[i] ?? `Tab ${i + 1}`}
|
|
22
|
+
</button>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
<div className="pu-tabs-panel">{items[current] != null ? slot(items[current]) : null}</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-tabs {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 14px;
|
|
6
|
+
}
|
|
7
|
+
.pu-tabs-bar {
|
|
8
|
+
display: flex;
|
|
9
|
+
gap: 4px;
|
|
10
|
+
border-bottom: 1px solid var(--pu-border, #e3e6ea);
|
|
11
|
+
}
|
|
12
|
+
.pu-tab {
|
|
13
|
+
appearance: none;
|
|
14
|
+
background: none;
|
|
15
|
+
border: none;
|
|
16
|
+
padding: 8px 14px;
|
|
17
|
+
margin-bottom: -1px;
|
|
18
|
+
font: inherit;
|
|
19
|
+
color: var(--pu-muted, #6b7280);
|
|
20
|
+
border-bottom: 2px solid transparent;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
.pu-tab:hover {
|
|
24
|
+
color: var(--pu-text, #1c1f23);
|
|
25
|
+
}
|
|
26
|
+
.pu-tab-active {
|
|
27
|
+
color: var(--pu-accent, #2563eb);
|
|
28
|
+
border-bottom-color: var(--pu-accent, #2563eb);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "./text.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ValueWidget } from "../../../lib/widgets/value.js";
|
|
4
|
+
|
|
5
|
+
export function TextView({ widget, state, emit, set }: ViewProps<ValueWidget<string>, { value: string }>) {
|
|
6
|
+
const lines = (widget.lines as number) ?? 1;
|
|
7
|
+
const update = (v: string) => {
|
|
8
|
+
set(["value"], v); // local, so the controlled input reflects typing and reads see it
|
|
9
|
+
emit(widget.changed(v));
|
|
10
|
+
};
|
|
11
|
+
return lines > 1 ? (
|
|
12
|
+
<textarea className="pu-input" rows={lines} placeholder={widget.placeholder as string} value={state.value} onChange={(e) => update(e.currentTarget.value)} />
|
|
13
|
+
) : (
|
|
14
|
+
<input className="pu-input" placeholder={widget.placeholder as string} value={state.value} onChange={(e) => update(e.currentTarget.value)} />
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-input {
|
|
3
|
+
width: 100%;
|
|
4
|
+
padding: 9px 11px;
|
|
5
|
+
border: 1px solid var(--pu-border, #e3e6ea);
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
font: inherit;
|
|
8
|
+
color: inherit;
|
|
9
|
+
background: #fff;
|
|
10
|
+
outline: none;
|
|
11
|
+
transition: border-color 0.12s, box-shadow 0.12s;
|
|
12
|
+
}
|
|
13
|
+
.pu-input:focus {
|
|
14
|
+
border-color: var(--pu-accent, #2563eb);
|
|
15
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
|
16
|
+
}
|
|
17
|
+
textarea.pu-input {
|
|
18
|
+
resize: vertical;
|
|
19
|
+
line-height: 1.5;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "./upload.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
import type { ValueWidget } from "../../../lib/widgets/value.js";
|
|
4
|
+
|
|
5
|
+
export function UploadView({ widget, state, emit, set }: ViewProps<ValueWidget<string | null>, { value: string | null }>) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="pu-upload">
|
|
8
|
+
<label className="pu-upload-drop">
|
|
9
|
+
<input
|
|
10
|
+
type="file"
|
|
11
|
+
hidden
|
|
12
|
+
accept={widget.accept as string}
|
|
13
|
+
onChange={(e) => {
|
|
14
|
+
const f = e.currentTarget.files?.[0];
|
|
15
|
+
if (!f) return;
|
|
16
|
+
const r = new FileReader();
|
|
17
|
+
r.onload = () => {
|
|
18
|
+
const url = String(r.result);
|
|
19
|
+
set(["value"], url);
|
|
20
|
+
emit(widget.changed(url));
|
|
21
|
+
};
|
|
22
|
+
r.readAsDataURL(f);
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
<span>{state.value ? "Choose another image" : "Choose an image…"}</span>
|
|
26
|
+
</label>
|
|
27
|
+
{state.value && <img className="pu-image" src={state.value} alt="" />}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-upload {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 12px;
|
|
6
|
+
}
|
|
7
|
+
.pu-image {
|
|
8
|
+
display: block;
|
|
9
|
+
max-width: 100%;
|
|
10
|
+
border: 1px solid var(--pu-border, #e3e6ea);
|
|
11
|
+
border-radius: 8px;
|
|
12
|
+
}
|
|
13
|
+
.pu-upload-drop {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
padding: 22px 14px;
|
|
18
|
+
border: 1.5px dashed var(--pu-border, #e3e6ea);
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
color: var(--pu-muted, #6b7280);
|
|
21
|
+
background: #fafbfc;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
|
24
|
+
}
|
|
25
|
+
.pu-upload-drop:hover {
|
|
26
|
+
border-color: var(--pu-accent, #2563eb);
|
|
27
|
+
color: var(--pu-accent, #2563eb);
|
|
28
|
+
background: rgba(37, 99, 235, 0.04);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./video.css";
|
|
2
|
+
import type { ViewProps } from "../../view.js";
|
|
3
|
+
|
|
4
|
+
export function VideoView({ widget, state }: ViewProps<any, { src: string }>) {
|
|
5
|
+
return state.src ? (
|
|
6
|
+
<video src={state.src} controls autoPlay={Boolean(widget.autoplay)} loop={Boolean(widget.loop)} muted={Boolean(widget.autoplay)} />
|
|
7
|
+
) : (
|
|
8
|
+
<div className="pu-output">—</div>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import "./webcam.css";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import type { ViewProps } from "../../view.js";
|
|
4
|
+
import type { WebcamWidget } from "../../../lib/widgets/webcam.js";
|
|
5
|
+
|
|
6
|
+
export function WebcamView({ widget, state, enabled, emit, set }: ViewProps<WebcamWidget, { live: boolean }>) {
|
|
7
|
+
const streamRef = useRef<MediaStream | null>(null);
|
|
8
|
+
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
9
|
+
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
10
|
+
const startingRef = useRef(false);
|
|
11
|
+
|
|
12
|
+
// capture follows `live`; the view sets `live` locally so it starts/stops without a round-trip
|
|
13
|
+
const toggle = (live: boolean): void => {
|
|
14
|
+
set(["live"], live);
|
|
15
|
+
emit(widget.toggled(live));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
|
|
21
|
+
if (state.live && enabled && !streamRef.current && !startingRef.current && navigator.mediaDevices?.getUserMedia) {
|
|
22
|
+
startingRef.current = true;
|
|
23
|
+
void (async () => {
|
|
24
|
+
try {
|
|
25
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: widget.facing } });
|
|
26
|
+
if (cancelled) return stream.getTracks().forEach((t) => t.stop());
|
|
27
|
+
streamRef.current = stream;
|
|
28
|
+
const video = videoRef.current!;
|
|
29
|
+
video.srcObject = stream;
|
|
30
|
+
await video.play();
|
|
31
|
+
|
|
32
|
+
if (widget.every > 0) {
|
|
33
|
+
const canvas = document.createElement("canvas");
|
|
34
|
+
timerRef.current = setInterval(() => {
|
|
35
|
+
if (streamRef.current !== stream || video.readyState < 2 || !video.videoWidth) return;
|
|
36
|
+
canvas.width = video.videoWidth;
|
|
37
|
+
canvas.height = video.videoHeight;
|
|
38
|
+
canvas.getContext("2d")?.drawImage(video, 0, 0);
|
|
39
|
+
emit(widget.frame(canvas.toDataURL("image/jpeg", 0.8)));
|
|
40
|
+
}, widget.every);
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(err);
|
|
44
|
+
} finally {
|
|
45
|
+
startingRef.current = false;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
} else if ((!state.live || !enabled) && streamRef.current) {
|
|
49
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
50
|
+
timerRef.current = null;
|
|
51
|
+
streamRef.current.getTracks().forEach((t) => t.stop());
|
|
52
|
+
streamRef.current = null;
|
|
53
|
+
if (videoRef.current) videoRef.current.srcObject = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
cancelled = true;
|
|
58
|
+
};
|
|
59
|
+
}, [state.live, enabled]);
|
|
60
|
+
|
|
61
|
+
const mirror = widget.facing === "user" ? " pu-webcam-mirror" : "";
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="pu-webcam">
|
|
65
|
+
{widget.preview ? (
|
|
66
|
+
<div className="pu-webcam-stage">
|
|
67
|
+
<video ref={videoRef} className={`pu-webcam-video${mirror}`} muted playsInline />
|
|
68
|
+
{!state.live && <div className="pu-webcam-placeholder">Camera off</div>}
|
|
69
|
+
</div>
|
|
70
|
+
) : (
|
|
71
|
+
// kept alive off-screen so capture still has a frame source, but nothing renders
|
|
72
|
+
<video ref={videoRef} className="pu-webcam-offscreen" muted playsInline />
|
|
73
|
+
)}
|
|
74
|
+
{widget.control && (
|
|
75
|
+
<button className={`pu-submit${state.live ? " pu-rec" : ""}`} disabled={!enabled} onClick={() => toggle(!state.live)}>
|
|
76
|
+
{state.live ? "● Stop camera" : "Go live"}
|
|
77
|
+
</button>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pu-webcam {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
align-items: flex-start;
|
|
6
|
+
gap: 8px;
|
|
7
|
+
}
|
|
8
|
+
.pu-webcam-stage {
|
|
9
|
+
position: relative;
|
|
10
|
+
width: 480px;
|
|
11
|
+
max-width: 100%;
|
|
12
|
+
aspect-ratio: 4 / 3;
|
|
13
|
+
background: #111418;
|
|
14
|
+
border-radius: 12px;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
}
|
|
17
|
+
.pu-webcam-video {
|
|
18
|
+
position: absolute;
|
|
19
|
+
inset: 0;
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
|
+
object-fit: cover;
|
|
23
|
+
}
|
|
24
|
+
.pu-webcam-mirror {
|
|
25
|
+
transform: scaleX(-1);
|
|
26
|
+
}
|
|
27
|
+
.pu-webcam-offscreen {
|
|
28
|
+
position: fixed;
|
|
29
|
+
top: 0;
|
|
30
|
+
left: 0;
|
|
31
|
+
width: 1px;
|
|
32
|
+
height: 1px;
|
|
33
|
+
opacity: 0;
|
|
34
|
+
pointer-events: none;
|
|
35
|
+
}
|
|
36
|
+
.pu-webcam-placeholder {
|
|
37
|
+
position: absolute;
|
|
38
|
+
inset: 0;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
color: #6b7280;
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
letter-spacing: 0.02em;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/css.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|