The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
    ├── FUNDING.yml
    └── workflows
    │   └── ci.yml
├── .gitignore
├── .npmrc
├── LICENSE.txt
├── README.md
├── package.json
├── packages
    ├── core
    │   ├── .npmignore
    │   ├── .npmrc
    │   ├── lib
    │   │   └── sass
    │   │   │   ├── _plugin-test.scss
    │   │   │   ├── _tp.scss
    │   │   │   ├── common
    │   │   │       └── _defs.scss
    │   │   │   └── view
    │   │   │       ├── _button.scss
    │   │   │       ├── _checkbox.scss
    │   │   │       ├── _color-picker.scss
    │   │   │       ├── _color-swatch.scss
    │   │   │       ├── _color-text.scss
    │   │   │       ├── _color.scss
    │   │   │       ├── _default-wrapper.scss
    │   │   │       ├── _folder.scss
    │   │   │       ├── _graph-log.scss
    │   │   │       ├── _label.scss
    │   │   │       ├── _list.scss
    │   │   │       ├── _log.scss
    │   │   │       ├── _point-2d-picker.scss
    │   │   │       ├── _point-2d.scss
    │   │   │       ├── _point-nd-text.scss
    │   │   │       ├── _popup.scss
    │   │   │       ├── _slider.scss
    │   │   │       ├── _tab.scss
    │   │   │       ├── _text.scss
    │   │   │       ├── _tooltip.scss
    │   │   │       ├── _views.scss
    │   │   │       ├── common
    │   │   │           └── _color.scss
    │   │   │       └── placeholder
    │   │   │           ├── _button.scss
    │   │   │           ├── _container.scss
    │   │   │           ├── _folder.scss
    │   │   │           ├── _input.scss
    │   │   │           ├── _list.scss
    │   │   │           ├── _monitor.scss
    │   │   │           ├── _texts.scss
    │   │   │           └── _theme.scss
    │   ├── package.json
    │   ├── scripts
    │   │   └── replace-version.js
    │   └── src
    │   │   ├── blade
    │   │       ├── binding
    │   │       │   ├── api
    │   │       │   │   ├── binding-test.ts
    │   │       │   │   ├── binding.ts
    │   │       │   │   ├── input-binding-test.ts
    │   │       │   │   ├── input-binding.ts
    │   │       │   │   ├── monitor-binding-test.ts
    │   │       │   │   └── monitor-binding.ts
    │   │       │   └── controller
    │   │       │   │   ├── binding-test.ts
    │   │       │   │   ├── binding.ts
    │   │       │   │   ├── input-binding-test.ts
    │   │       │   │   ├── input-binding.ts
    │   │       │   │   ├── monitor-binding-test.ts
    │   │       │   │   └── monitor-binding.ts
    │   │       ├── button
    │   │       │   ├── api
    │   │       │   │   ├── button-test.ts
    │   │       │   │   └── button.ts
    │   │       │   ├── controller
    │   │       │   │   ├── button-blade-test.ts
    │   │       │   │   ├── button-blade.ts
    │   │       │   │   ├── button-test.ts
    │   │       │   │   └── button.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   ├── plugin.ts
    │   │       │   └── view
    │   │       │   │   └── button.ts
    │   │       ├── common
    │   │       │   ├── api
    │   │       │   │   ├── blade-test.ts
    │   │       │   │   ├── blade.ts
    │   │       │   │   ├── container-blade.ts
    │   │       │   │   ├── container-test.ts
    │   │       │   │   ├── container.ts
    │   │       │   │   ├── event-listenable.ts
    │   │       │   │   ├── events.ts
    │   │       │   │   ├── params.ts
    │   │       │   │   ├── rack-test.ts
    │   │       │   │   ├── rack.ts
    │   │       │   │   ├── refreshable-test.ts
    │   │       │   │   ├── refreshable.ts
    │   │       │   │   ├── test-util.ts
    │   │       │   │   └── tp-event.ts
    │   │       │   ├── controller
    │   │       │   │   ├── blade-state-test.ts
    │   │       │   │   ├── blade-state.ts
    │   │       │   │   ├── blade-test.ts
    │   │       │   │   ├── blade.ts
    │   │       │   │   ├── container-blade-test.ts
    │   │       │   │   ├── container-blade.ts
    │   │       │   │   ├── rack.ts
    │   │       │   │   └── value-blade.ts
    │   │       │   ├── model
    │   │       │   │   ├── blade-positions.ts
    │   │       │   │   ├── blade.ts
    │   │       │   │   ├── foldable.ts
    │   │       │   │   ├── nested-ordered-set-test.ts
    │   │       │   │   ├── nested-ordered-set.ts
    │   │       │   │   ├── rack-test.ts
    │   │       │   │   └── rack.ts
    │   │       │   └── view
    │   │       │   │   └── blade-container.ts
    │   │       ├── folder
    │   │       │   ├── api
    │   │       │   │   ├── folder-test.ts
    │   │       │   │   └── folder.ts
    │   │       │   ├── controller
    │   │       │   │   ├── folder-test.ts
    │   │       │   │   └── folder.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   ├── plugin.ts
    │   │       │   └── view
    │   │       │   │   └── folder.ts
    │   │       ├── label
    │   │       │   └── controller
    │   │       │   │   ├── value-test.ts
    │   │       │   │   └── value.ts
    │   │       ├── plugin.ts
    │   │       ├── tab
    │   │       │   ├── api
    │   │       │   │   ├── tab-page-test.ts
    │   │       │   │   ├── tab-page.ts
    │   │       │   │   ├── tab-test.ts
    │   │       │   │   └── tab.ts
    │   │       │   ├── controller
    │   │       │   │   ├── tab-item-test.ts
    │   │       │   │   ├── tab-item.ts
    │   │       │   │   ├── tab-page-test.ts
    │   │       │   │   ├── tab-page.ts
    │   │       │   │   ├── tab-test.ts
    │   │       │   │   └── tab.ts
    │   │       │   ├── model
    │   │       │   │   ├── tab-test.ts
    │   │       │   │   └── tab.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   ├── plugin.ts
    │   │       │   └── view
    │   │       │   │   ├── tab-item.ts
    │   │       │   │   ├── tab-page.ts
    │   │       │   │   └── tab.ts
    │   │       └── test-util.ts
    │   │   ├── common
    │   │       ├── api
    │   │       │   ├── list-test.ts
    │   │       │   └── list.ts
    │   │       ├── binding
    │   │       │   ├── binding.ts
    │   │       │   ├── read-write-test.ts
    │   │       │   ├── read-write.ts
    │   │       │   ├── readonly.ts
    │   │       │   ├── target-test.ts
    │   │       │   ├── target.ts
    │   │       │   ├── ticker
    │   │       │   │   ├── interval-test.ts
    │   │       │   │   ├── interval.ts
    │   │       │   │   ├── manual-test.ts
    │   │       │   │   ├── manual.ts
    │   │       │   │   └── ticker.ts
    │   │       │   └── value
    │   │       │   │   ├── binding.ts
    │   │       │   │   ├── input-binding-test.ts
    │   │       │   │   ├── input-binding.ts
    │   │       │   │   ├── monitor-binding-test.ts
    │   │       │   │   └── monitor-binding.ts
    │   │       ├── compat.ts
    │   │       ├── constraint
    │   │       │   ├── composite-test.ts
    │   │       │   ├── composite.ts
    │   │       │   ├── constraint.ts
    │   │       │   ├── definite-range-test.ts
    │   │       │   ├── definite-range.ts
    │   │       │   ├── list-test.ts
    │   │       │   ├── list.ts
    │   │       │   ├── range-test.ts
    │   │       │   ├── range.ts
    │   │       │   ├── step-test.ts
    │   │       │   └── step.ts
    │   │       ├── controller
    │   │       │   ├── controller.ts
    │   │       │   ├── list-test.ts
    │   │       │   ├── list.ts
    │   │       │   ├── popup.ts
    │   │       │   ├── text-test.ts
    │   │       │   ├── text.ts
    │   │       │   └── value.ts
    │   │       ├── converter
    │   │       │   ├── boolean-test.ts
    │   │       │   ├── boolean.ts
    │   │       │   ├── ecma
    │   │       │   │   ├── nodes-test.ts
    │   │       │   │   ├── nodes.ts
    │   │       │   │   ├── parser-test.ts
    │   │       │   │   ├── parser.ts
    │   │       │   │   └── reader.ts
    │   │       │   ├── formatter.ts
    │   │       │   ├── number-test.ts
    │   │       │   ├── number.ts
    │   │       │   ├── parser-test.ts
    │   │       │   ├── parser.ts
    │   │       │   ├── percentage-test.ts
    │   │       │   ├── percentage.ts
    │   │       │   └── string.ts
    │   │       ├── dom-util-test.ts
    │   │       ├── dom-util.ts
    │   │       ├── label
    │   │       │   ├── controller
    │   │       │   │   ├── label-test.ts
    │   │       │   │   └── label.ts
    │   │       │   └── view
    │   │       │   │   └── label.ts
    │   │       ├── list-util.ts
    │   │       ├── micro-parsers-test.ts
    │   │       ├── micro-parsers.ts
    │   │       ├── model
    │   │       │   ├── buffered-value-test.ts
    │   │       │   ├── buffered-value.ts
    │   │       │   ├── complex-value.ts
    │   │       │   ├── emitter-test.ts
    │   │       │   ├── emitter.ts
    │   │       │   ├── primitive-value-test.ts
    │   │       │   ├── primitive-value.ts
    │   │       │   ├── reactive.ts
    │   │       │   ├── readonly-primitive-value.ts
    │   │       │   ├── test-util.ts
    │   │       │   ├── value-map-test.ts
    │   │       │   ├── value-map.ts
    │   │       │   ├── value-sync-test.ts
    │   │       │   ├── value-sync.ts
    │   │       │   ├── value.ts
    │   │       │   ├── values-test.ts
    │   │       │   ├── values.ts
    │   │       │   ├── view-props-test.ts
    │   │       │   └── view-props.ts
    │   │       ├── number
    │   │       │   ├── controller
    │   │       │   │   ├── number-text-test.ts
    │   │       │   │   ├── number-text.ts
    │   │       │   │   ├── slider-text-test.ts
    │   │       │   │   ├── slider-text.ts
    │   │       │   │   └── slider.ts
    │   │       │   ├── util-test.ts
    │   │       │   ├── util.ts
    │   │       │   └── view
    │   │       │   │   ├── number-text.ts
    │   │       │   │   ├── slider-test.ts
    │   │       │   │   ├── slider-text.ts
    │   │       │   │   └── slider.ts
    │   │       ├── params.ts
    │   │       ├── picker-util.ts
    │   │       ├── point-nd
    │   │       │   ├── point-axis.ts
    │   │       │   ├── test-util.ts
    │   │       │   └── util.ts
    │   │       ├── primitive.ts
    │   │       ├── tp-error-test.ts
    │   │       ├── tp-error.ts
    │   │       ├── ui.ts
    │   │       └── view
    │   │       │   ├── class-name.ts
    │   │       │   ├── css-vars.ts
    │   │       │   ├── list-test.ts
    │   │       │   ├── list.ts
    │   │       │   ├── plain.ts
    │   │       │   ├── pointer-handler.ts
    │   │       │   ├── popup.ts
    │   │       │   ├── reactive.ts
    │   │       │   ├── text-test.ts
    │   │       │   ├── text.ts
    │   │       │   └── view.ts
    │   │   ├── index.ts
    │   │   ├── input-binding
    │   │       ├── boolean
    │   │       │   ├── controller
    │   │       │   │   └── checkbox.ts
    │   │       │   ├── plugin.ts
    │   │       │   └── view
    │   │       │   │   └── checkbox.ts
    │   │       ├── color
    │   │       │   ├── controller
    │   │       │   │   ├── a-palette.ts
    │   │       │   │   ├── color-picker-test.ts
    │   │       │   │   ├── color-picker.ts
    │   │       │   │   ├── color-swatch.ts
    │   │       │   │   ├── color-texts-test.ts
    │   │       │   │   ├── color-texts.ts
    │   │       │   │   ├── color.ts
    │   │       │   │   ├── h-palette.ts
    │   │       │   │   └── sv-palette.ts
    │   │       │   ├── converter
    │   │       │   │   ├── color-number-test.ts
    │   │       │   │   ├── color-number.ts
    │   │       │   │   ├── color-object-test.ts
    │   │       │   │   ├── color-object.ts
    │   │       │   │   ├── color-string-test.ts
    │   │       │   │   ├── color-string.ts
    │   │       │   │   ├── writer-test.ts
    │   │       │   │   └── writer.ts
    │   │       │   ├── model
    │   │       │   │   ├── color-model-test.ts
    │   │       │   │   ├── color-model.ts
    │   │       │   │   ├── color-test.ts
    │   │       │   │   ├── color.ts
    │   │       │   │   ├── colors-test.ts
    │   │       │   │   ├── colors.ts
    │   │       │   │   ├── float-color-test.ts
    │   │       │   │   ├── float-color.ts
    │   │       │   │   ├── int-color-test.ts
    │   │       │   │   └── int-color.ts
    │   │       │   ├── plugin-number-test.ts
    │   │       │   ├── plugin-number.ts
    │   │       │   ├── plugin-object-test.ts
    │   │       │   ├── plugin-object.ts
    │   │       │   ├── plugin-string-test.ts
    │   │       │   ├── plugin-string.ts
    │   │       │   ├── util.ts
    │   │       │   └── view
    │   │       │   │   ├── a-palette.ts
    │   │       │   │   ├── color-picker.ts
    │   │       │   │   ├── color-swatch.ts
    │   │       │   │   ├── color-texts-test.ts
    │   │       │   │   ├── color-texts.ts
    │   │       │   │   ├── color.ts
    │   │       │   │   ├── h-palette.ts
    │   │       │   │   └── sv-palette.ts
    │   │       ├── common
    │   │       │   ├── constraint
    │   │       │   │   ├── point-nd-test.ts
    │   │       │   │   └── point-nd.ts
    │   │       │   ├── controller
    │   │       │   │   ├── point-nd-text-test.ts
    │   │       │   │   └── point-nd-text.ts
    │   │       │   ├── model
    │   │       │   │   └── point-nd.ts
    │   │       │   └── view
    │   │       │   │   └── point-nd-text.ts
    │   │       ├── number
    │   │       │   ├── api
    │   │       │   │   ├── slider-test.ts
    │   │       │   │   └── slider.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   └── plugin.ts
    │   │       ├── plugin-test.ts
    │   │       ├── plugin.ts
    │   │       ├── point-2d
    │   │       │   ├── controller
    │   │       │   │   ├── point-2d-picker.ts
    │   │       │   │   └── point-2d.ts
    │   │       │   ├── converter
    │   │       │   │   ├── point-2d-test.ts
    │   │       │   │   └── point-2d.ts
    │   │       │   ├── model
    │   │       │   │   └── point-2d.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   ├── plugin.ts
    │   │       │   └── view
    │   │       │   │   ├── point-2d-picker.ts
    │   │       │   │   └── point-2d.ts
    │   │       ├── point-3d
    │   │       │   ├── converter
    │   │       │   │   ├── point-3d-test.ts
    │   │       │   │   └── point-3d.ts
    │   │       │   ├── model
    │   │       │   │   ├── point-3d-test.ts
    │   │       │   │   └── point-3d.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   └── plugin.ts
    │   │       ├── point-4d
    │   │       │   ├── converter
    │   │       │   │   ├── point-4d-test.ts
    │   │       │   │   └── point-4d.ts
    │   │       │   ├── model
    │   │       │   │   ├── point-4d-test.ts
    │   │       │   │   └── point-4d.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   └── plugin.ts
    │   │       └── string
    │   │       │   └── plugin.ts
    │   │   ├── misc
    │   │       ├── constants.ts
    │   │       ├── dom-test-util.ts
    │   │       ├── semver-test.ts
    │   │       ├── semver.ts
    │   │       ├── test-util.ts
    │   │       ├── type-util-test.ts
    │   │       └── type-util.ts
    │   │   ├── monitor-binding
    │   │       ├── boolean
    │   │       │   └── plugin.ts
    │   │       ├── common
    │   │       │   ├── controller
    │   │       │   │   ├── multi-log.ts
    │   │       │   │   └── single-log.ts
    │   │       │   └── view
    │   │       │   │   ├── multi-log.ts
    │   │       │   │   └── single-log.ts
    │   │       ├── number
    │   │       │   ├── api
    │   │       │   │   ├── graph-log-test.ts
    │   │       │   │   └── graph-log.ts
    │   │       │   ├── controller
    │   │       │   │   ├── graph-log-test.ts
    │   │       │   │   └── graph-log.ts
    │   │       │   ├── plugin-test.ts
    │   │       │   ├── plugin.ts
    │   │       │   └── view
    │   │       │   │   └── graph-log.ts
    │   │       ├── plugin-test.ts
    │   │       ├── plugin.ts
    │   │       └── string
    │   │       │   └── plugin.ts
    │   │   ├── plugin
    │   │       ├── blade-api-cache-test.ts
    │   │       ├── blade-api-cache.ts
    │   │       ├── plugin.ts
    │   │       ├── plugins.ts
    │   │       └── pool.ts
    │   │   ├── tsconfig.json
    │   │   └── version.ts
    └── tweakpane
    │   ├── .npmignore
    │   ├── .npmrc
    │   ├── package.json
    │   ├── rollup-doc.config.js
    │   ├── rollup.config.js
    │   ├── scripts
    │       ├── assets-version.js
    │       ├── doc-build-html.js
    │       └── main-test-ts-module-pre.js
    │   ├── src
    │       ├── doc
    │       │   ├── img
    │       │   │   └── og.png
    │       │   ├── sass
    │       │   │   ├── _base.scss
    │       │   │   ├── _defs.scss
    │       │   │   ├── _pages.scss
    │       │   │   ├── _reset.scss
    │       │   │   ├── bundle.scss
    │       │   │   └── components
    │       │   │   │   ├── _catalog.scss
    │       │   │   │   ├── _code-block.scss
    │       │   │   │   ├── _concept.scss
    │       │   │   │   ├── _demo.scss
    │       │   │   │   ├── _global-footer.scss
    │       │   │   │   ├── _global-header.scss
    │       │   │   │   ├── _global-nav.scss
    │       │   │   │   ├── _hljs.scss
    │       │   │   │   ├── _logo.scss
    │       │   │   │   ├── _main.scss
    │       │   │   │   ├── _migration.scss
    │       │   │   │   ├── _page-header.scss
    │       │   │   │   ├── _pane-container.scss
    │       │   │   │   ├── _photo-credit.scss
    │       │   │   │   ├── _rel.scss
    │       │   │   │   ├── _sponsor.scss
    │       │   │   │   └── _theme-builder.scss
    │       │   ├── template
    │       │   │   ├── _template.html
    │       │   │   ├── blades
    │       │   │   │   └── index.html
    │       │   │   ├── catalog.html
    │       │   │   ├── getting-started
    │       │   │   │   └── index.html
    │       │   │   ├── index.html
    │       │   │   ├── input-bindings
    │       │   │   │   └── index.html
    │       │   │   ├── migration
    │       │   │   │   ├── datgui
    │       │   │   │   │   └── index.html
    │       │   │   │   ├── index.html
    │       │   │   │   └── v4
    │       │   │   │   │   └── index.html
    │       │   │   ├── misc
    │       │   │   │   └── index.html
    │       │   │   ├── monitor-bindings
    │       │   │   │   └── index.html
    │       │   │   ├── partial
    │       │   │   │   ├── _global-footer.html
    │       │   │   │   ├── _global-header.html
    │       │   │   │   └── _global-nav.html
    │       │   │   ├── plugins
    │       │   │   │   ├── dev
    │       │   │   │   │   └── index.html
    │       │   │   │   └── index.html
    │       │   │   ├── quick-tour
    │       │   │   │   └── index.html
    │       │   │   ├── theming
    │       │   │   │   └── index.html
    │       │   │   └── ui-components
    │       │   │   │   └── index.html
    │       │   ├── ts
    │       │   │   ├── bundle.ts
    │       │   │   ├── panepaint.ts
    │       │   │   ├── plugins
    │       │   │   │   ├── counter
    │       │   │   │   │   ├── bundle.ts
    │       │   │   │   │   ├── controller.ts
    │       │   │   │   │   ├── plugin.ts
    │       │   │   │   │   └── view.ts
    │       │   │   │   └── placeholder
    │       │   │   │   │   └── bundle.ts
    │       │   │   ├── preset-test.ts
    │       │   │   ├── preset.ts
    │       │   │   ├── route
    │       │   │   │   ├── blades.ts
    │       │   │   │   ├── catalog.ts
    │       │   │   │   ├── getting-started.ts
    │       │   │   │   ├── index.ts
    │       │   │   │   ├── input-bindings.ts
    │       │   │   │   ├── migration-datgui.ts
    │       │   │   │   ├── migration-v4.ts
    │       │   │   │   ├── misc.ts
    │       │   │   │   ├── monitor-bindings.ts
    │       │   │   │   ├── plugins-dev.ts
    │       │   │   │   ├── plugins.ts
    │       │   │   │   ├── quick-tour.ts
    │       │   │   │   ├── theming.ts
    │       │   │   │   └── ui-components.ts
    │       │   │   ├── screw.ts
    │       │   │   ├── simple-router.ts
    │       │   │   ├── sketch.ts
    │       │   │   ├── sp-menu.ts
    │       │   │   ├── themes.ts
    │       │   │   └── util.ts
    │       │   ├── tsconfig.json
    │       │   └── typedoc.json
    │       └── main
    │       │   ├── sass
    │       │       ├── bundle.scss
    │       │       └── view
    │       │       │   ├── _root.scss
    │       │       │   └── _separator.scss
    │       │   ├── ts
    │       │       ├── blade
    │       │       │   ├── list
    │       │       │   │   ├── api
    │       │       │   │   │   ├── list-test.ts
    │       │       │   │   │   └── list.ts
    │       │       │   │   ├── plugin-test.ts
    │       │       │   │   └── plugin.ts
    │       │       │   ├── root
    │       │       │   │   ├── api
    │       │       │   │   │   ├── root-test.ts
    │       │       │   │   │   └── root.ts
    │       │       │   │   └── controller
    │       │       │   │   │   └── root.ts
    │       │       │   ├── separator
    │       │       │   │   ├── api
    │       │       │   │   │   ├── separator-test.ts
    │       │       │   │   │   └── separator.ts
    │       │       │   │   ├── controller
    │       │       │   │   │   └── separator.ts
    │       │       │   │   ├── plugin-test.ts
    │       │       │   │   ├── plugin.ts
    │       │       │   │   └── view
    │       │       │   │   │   └── separator.ts
    │       │       │   ├── slider
    │       │       │   │   ├── api
    │       │       │   │   │   ├── slider-test.ts
    │       │       │   │   │   └── slider.ts
    │       │       │   │   ├── plugin-test.ts
    │       │       │   │   └── plugin.ts
    │       │       │   └── text
    │       │       │   │   ├── api
    │       │       │   │       ├── text-test.ts
    │       │       │   │       └── text.ts
    │       │       │   │   ├── plugin-test.ts
    │       │       │   │   └── plugin.ts
    │       │       ├── index.ts
    │       │       ├── misc
    │       │       │   └── test-util.ts
    │       │       └── pane
    │       │       │   ├── blade-test.ts
    │       │       │   ├── event-test.ts
    │       │       │   ├── input-test.ts
    │       │       │   ├── monitor-test.ts
    │       │       │   ├── pane-config.ts
    │       │       │   ├── pane-test.ts
    │       │       │   ├── pane.ts
    │       │       │   └── ui-test.ts
    │       │   ├── tsconfig-dts.json
    │       │   └── tsconfig.json
    │   └── test-module
    │       ├── .npmrc
    │       ├── node
    │           └── index.js
    │       ├── package.json
    │       ├── plugin
    │           ├── rollup.config.js
    │           ├── src
    │           │   ├── index.ts
    │           │   ├── test-blade.ts
    │           │   ├── test-input.ts
    │           │   └── test-view.ts
    │           ├── test
    │           │   ├── browser.html
    │           │   └── node.js
    │           └── tsconfig.json
    │       └── tsc
    │           ├── index.ts
    │           └── tsconfig.json
├── prettier.config.js
└── tsconfig.json


/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*.{html}]
 4 | indent_size = 2
 5 | indent_style = tab
 6 | 
 7 | [*.{js,json,ts}]
 8 | indent_size = 2
 9 | indent_style = tab
10 | 
11 | [*.md]
12 | indent_size = 2
13 | indent_style = space
14 | 
15 | [*.scss]
16 | indent_size = 2
17 | indent_style = tab
18 | 
19 | [*.yml]
20 | indent_size = 2
21 | indent_style = space
22 | 
23 | [package.json]
24 | indent_size = 2
25 | indent_style = space
26 | 


--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules


--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 | 	extends: [
 3 | 		'eslint:recommended',
 4 | 		'plugin:@typescript-eslint/eslint-recommended',
 5 | 		'plugin:@typescript-eslint/recommended',
 6 | 		'plugin:prettier/recommended',
 7 | 	],
 8 | 	parser: '@typescript-eslint/parser',
 9 | 	plugins: ['@typescript-eslint', 'prettier', 'simple-import-sort'],
10 | 	root: true,
11 | 	rules: {
12 | 		camelcase: 'off',
13 | 		'no-console': ['warn', {allow: ['warn', 'error']}],
14 | 		'no-unused-vars': 'off',
15 | 		'sort-imports': 'off',
16 | 
17 | 		'prettier/prettier': 'error',
18 | 		'simple-import-sort/imports': 'error',
19 | 		'@typescript-eslint/naming-convention': [
20 | 			'error',
21 | 			{
22 | 				selector: 'variable',
23 | 				format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
24 | 				custom: {
25 | 					regex: '^opt_',
26 | 					match: false,
27 | 				},
28 | 			},
29 | 		],
30 | 		'@typescript-eslint/explicit-function-return-type': 'off',
31 | 		'@typescript-eslint/no-empty-function': 'off',
32 | 		'@typescript-eslint/no-explicit-any': 'off',
33 | 		'@typescript-eslint/no-unused-vars': [
34 | 			'error',
35 | 			{
36 | 				argsIgnorePattern: '^_',
37 | 			},
38 | 		],
39 | 
40 | 		// TODO: Resolve latest lint warnings
41 | 		'@typescript-eslint/explicit-module-boundary-types': 'off',
42 | 	},
43 | };
44 | 


--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: cocopon
2 | 


--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
 1 | name: CI
 2 | on:
 3 |   pull_request:
 4 |   release:
 5 |     types: [released]
 6 | jobs:
 7 |   test:
 8 |     runs-on: ubuntu-latest
 9 |     steps:
10 |       - uses: actions/checkout@v2
11 |       - uses: actions/setup-node@v1
12 |         with:
13 |           # https://github.com/npm/cli/issues/3847
14 |           node-version: '16.1.0'
15 |       - run: npm install
16 |       - run: npm run setup
17 |       - run: npm run test --workspaces
18 |       - run: npm run coverage
19 |       - uses: coverallsapp/github-action@master
20 |         continue-on-error: true
21 |         with:
22 |           github-token: ${{ secrets.GITHUB_TOKEN }}
23 |           path-to-lcov: ./coverage/lcov.info
24 | 


--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | 


--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
 1 | Copyright (c) 2016 cocopon <cocopon@me.com>
 2 | 
 3 | Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | of this software and associated documentation files (the "Software"), to deal
 5 | in the Software without restriction, including without limitation the rights
 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | copies of the Software, and to permit persons to whom the Software is
 8 | furnished to do so, subject to the following conditions:
 9 | 
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 | 
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 | 


--------------------------------------------------------------------------------
/packages/core/.npmignore:
--------------------------------------------------------------------------------
1 | .nyc_output/


--------------------------------------------------------------------------------
/packages/core/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/_plugin-test.scss:
--------------------------------------------------------------------------------
 1 | @use './tp';
 2 | 
 3 | .#{tp.$prefix}-test {
 4 | 	@extend %tp-button;
 5 | 	@extend %tp-input;
 6 | 	@extend %tp-monitor;
 7 | 
 8 | 	background-color: tp.cssVar('input-bg');
 9 | 	color: tp.cssVar('input-fg');
10 | 	transition-duration: tp.$fold-transition-duration;
11 | }
12 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/_tp.scss:
--------------------------------------------------------------------------------
 1 | // Entry point for plugins
 2 | 
 3 | @forward './common/defs';
 4 | 
 5 | @forward './view/placeholder/button';
 6 | @forward './view/placeholder/container';
 7 | @forward './view/placeholder/folder';
 8 | @forward './view/placeholder/input';
 9 | @forward './view/placeholder/list';
10 | @forward './view/placeholder/monitor';
11 | @forward './view/placeholder/texts';
12 | @forward './view/placeholder/theme';
13 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_button.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-btnv {
 4 | 	&_b {
 5 | 		@extend %tp-button;
 6 | 
 7 | 		width: 100%;
 8 | 	}
 9 | 	&_t {
10 | 		text-align: center;
11 | 	}
12 | }
13 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_checkbox.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-ckbv {
 4 | 	&_l {
 5 | 		display: block;
 6 | 		position: relative;
 7 | 	}
 8 | 	&_i {
 9 | 		@extend %tp-resetUserAgent;
10 | 
11 | 		left: 0;
12 | 		opacity: 0;
13 | 		position: absolute;
14 | 		top: 0;
15 | 	}
16 | 	&_w {
17 | 		background-color: tp.cssVar('input-bg');
18 | 		border-radius: tp.cssVar('blade-border-radius');
19 | 		cursor: pointer;
20 | 		display: block;
21 | 		height: tp.cssVar('container-unit-size');
22 | 		position: relative;
23 | 		width: tp.cssVar('container-unit-size');
24 | 
25 | 		svg {
26 | 			display: block;
27 | 			height: 16px;
28 | 			inset: 0;
29 | 			margin: auto;
30 | 			opacity: 0;
31 | 			position: absolute;
32 | 			width: 16px;
33 | 
34 | 			path {
35 | 				fill: none;
36 | 				stroke: tp.cssVar('input-fg');
37 | 				stroke-width: 2;
38 | 			}
39 | 		}
40 | 	}
41 | 	&_i:hover + &_w {
42 | 		background-color: tp.cssVar('input-bg-hover');
43 | 	}
44 | 	&_i:focus + &_w {
45 | 		background-color: tp.cssVar('input-bg-focus');
46 | 	}
47 | 	&_i:active + &_w {
48 | 		background-color: tp.cssVar('input-bg-active');
49 | 	}
50 | 	&_i:checked + &_w svg {
51 | 		opacity: 1;
52 | 	}
53 | 	&.#{tp.$disabled} &_w {
54 | 		opacity: 0.5;
55 | 	}
56 | }
57 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_color-swatch.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | @use './common/color';
 3 | 
 4 | .#{tp.$prefix}-colswv {
 5 | 	@include color.checkerboard(10px);
 6 | 
 7 | 	border-radius: tp.cssVar('blade-border-radius');
 8 | 	overflow: hidden;
 9 | 
10 | 	&.#{tp.$disabled} {
11 | 		opacity: 0.5;
12 | 	}
13 | 
14 | 	&_sw {
15 | 		@extend %tp-input;
16 | 
17 | 		border-radius: 0;
18 | 	}
19 | 	&_b {
20 | 		@extend %tp-resetUserAgent;
21 | 
22 | 		cursor: pointer;
23 | 		display: block;
24 | 		height: tp.cssVar('container-unit-size');
25 | 		left: 0;
26 | 		position: absolute;
27 | 		top: 0;
28 | 		width: tp.cssVar('container-unit-size');
29 | 
30 | 		&:focus::after {
31 | 			border: rgba(white, 0.75) solid 2px;
32 | 			border-radius: tp.cssVar('blade-border-radius');
33 | 			content: '';
34 | 			display: block;
35 | 			inset: 0;
36 | 			position: absolute;
37 | 		}
38 | 	}
39 | }
40 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_color-text.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-coltxtv {
 4 | 	display: flex;
 5 | 	width: 100%;
 6 | 
 7 | 	// Mode selector wrapper
 8 | 	&_m {
 9 | 		@extend %tp-list;
10 | 
11 | 		margin-right: 4px;
12 | 	}
13 | 	// Mode selector
14 | 	&_ms {
15 | 		@extend %tp-resetUserAgent;
16 | 
17 | 		border-radius: tp.cssVar('blade-border-radius');
18 | 		color: tp.cssVar('label-fg');
19 | 		cursor: pointer;
20 | 		height: tp.cssVar('container-unit-size');
21 | 		line-height: tp.cssVar('container-unit-size');
22 | 		padding: 0 18px 0 4px;
23 | 
24 | 		&:hover {
25 | 			background-color: tp.cssVar('input-bg-hover');
26 | 		}
27 | 		&:focus {
28 | 			background-color: tp.cssVar('input-bg-focus');
29 | 		}
30 | 		&:active {
31 | 			background-color: tp.cssVar('input-bg-active');
32 | 		}
33 | 	}
34 | 	// Mode selector mark
35 | 	&_mm {
36 | 		@extend %tp-list_arrow;
37 | 
38 | 		color: tp.cssVar('label-fg');
39 | 	}
40 | 	&.#{tp.$disabled} &_mm {
41 | 		opacity: 0.5;
42 | 	}
43 | 	// Text components wrapper
44 | 	&_w {
45 | 		@extend %tp-texts;
46 | 
47 | 		flex: 1;
48 | 	}
49 | 	// Text component
50 | 	&_c {
51 | 		@extend %tp-texts_item;
52 | 	}
53 | }
54 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_color.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-colv {
 4 | 	position: relative;
 5 | 
 6 | 	&_h {
 7 | 		display: flex;
 8 | 	}
 9 | 	&_s {
10 | 		flex-grow: 0;
11 | 		flex-shrink: 0;
12 | 		width: tp.cssVar('container-unit-size');
13 | 	}
14 | 	&_t {
15 | 		flex: 1;
16 | 		margin-left: 4px;
17 | 	}
18 | 	&_p {
19 | 		height: 0;
20 | 		margin-top: 0;
21 | 		opacity: 0;
22 | 		overflow: hidden;
23 | 		transition: height tp.$fold-transition-duration ease-in-out,
24 | 			opacity tp.$fold-transition-duration linear,
25 | 			margin tp.$fold-transition-duration ease-in-out;
26 | 	}
27 | 	&#{&}-expanded#{&}-cpl &_p {
28 | 		overflow: visible;
29 | 	}
30 | 	&#{&}-expanded &_p {
31 | 		margin-top: tp.cssVar('container-unit-spacing');
32 | 		opacity: 1;
33 | 	}
34 | 
35 | 	.#{tp.$prefix}-popv {
36 | 		left: calc(-1 * #{tp.cssVar('container-h-padding')});
37 | 		right: calc(-1 * #{tp.cssVar('container-h-padding')});
38 | 		top: tp.cssVar('container-unit-size');
39 | 	}
40 | }
41 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_default-wrapper.scss:
--------------------------------------------------------------------------------
1 | @use '../tp';
2 | 
3 | .#{tp.$prefix}-dfwv {
4 | 	position: absolute;
5 | 	top: 8px;
6 | 	right: 8px;
7 | 	width: 256px;
8 | }
9 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_folder.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-fldv {
 4 | 	position: relative;
 5 | 
 6 | 	// Title button
 7 | 	&_b {
 8 | 		@extend %tp-folder_title;
 9 | 	}
10 | 	// Title
11 | 	&_t {
12 | 		padding-left: 4px;
13 | 	}
14 | 	// Marker
15 | 	&_m {
16 | 		@extend %tp-folder_mark;
17 | 	}
18 | 	&_b:disabled &_m {
19 | 		display: none;
20 | 	}
21 | 	&#{&}-expanded > &_b > &_m {
22 | 		@extend %tp-folder_mark-expanded;
23 | 	}
24 | 	// Container
25 | 	&_c {
26 | 		@extend %tp-folder_container;
27 | 		@extend %tp-container_children;
28 | 		@extend %tp-container_subcontainers;
29 | 
30 | 		padding-left: 4px;
31 | 	}
32 | 	&#{&}-expanded > &_c {
33 | 		@extend %tp-folder_container-expanded;
34 | 	}
35 | 	&#{&}-cpl:not(#{&}-expanded) > &_c {
36 | 		@extend %tp-folder_container-shrinkedCompletely;
37 | 	}
38 | 	// Indent
39 | 	&_i {
40 | 		bottom: 0;
41 | 		color: tp.cssVar('container-bg');
42 | 		left: 0;
43 | 		overflow: hidden;
44 | 		position: absolute;
45 | 		top: calc(#{tp.cssVar('container-unit-size')} + 4px);
46 | 		width: max(tp.cssVar('base-border-radius'), 4px);
47 | 
48 | 		&::before {
49 | 			background-color: currentColor;
50 | 			bottom: 0;
51 | 			content: '';
52 | 			left: 0;
53 | 			position: absolute;
54 | 			top: 0;
55 | 			width: 4px;
56 | 		}
57 | 	}
58 | 	&_b:hover + &_i {
59 | 		color: tp.cssVar('container-bg-hover');
60 | 	}
61 | 	&_b:focus + &_i {
62 | 		color: tp.cssVar('container-bg-focus');
63 | 	}
64 | 	&_b:active + &_i {
65 | 		color: tp.cssVar('container-bg-active');
66 | 	}
67 | 	&.#{tp.$disabled} > &_i {
68 | 		opacity: 0.5;
69 | 	}
70 | }
71 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_graph-log.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-grlv {
 4 | 	position: relative;
 5 | 
 6 | 	&_g {
 7 | 		@extend %tp-monitor;
 8 | 
 9 | 		display: block;
10 | 		height: calc(#{tp.cssVar('container-unit-size')} * 3);
11 | 
12 | 		polyline {
13 | 			fill: none;
14 | 			stroke: tp.cssVar('monitor-fg');
15 | 			stroke-linejoin: round;
16 | 		}
17 | 	}
18 | 	&_t {
19 | 		margin-top: -4px;
20 | 		transition: left 0.05s, top 0.05s;
21 | 		visibility: hidden;
22 | 
23 | 		&#{&}-a {
24 | 			visibility: visible;
25 | 		}
26 | 		&#{&}-in {
27 | 			transition: none;
28 | 		}
29 | 	}
30 | 	&.#{tp.$disabled} &_g {
31 | 		opacity: 0.5;
32 | 	}
33 | 
34 | 	.#{tp.$prefix}-ttv {
35 | 		background-color: tp.cssVar('monitor-fg');
36 | 
37 | 		&::before {
38 | 			border-top-color: tp.cssVar('monitor-fg');
39 | 		}
40 | 	}
41 | }
42 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_label.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-lblv {
 4 | 	align-items: center;
 5 | 	display: flex;
 6 | 	line-height: 1.3;
 7 | 	padding-left: tp.cssVar('container-h-padding');
 8 | 	padding-right: tp.cssVar('container-h-padding');
 9 | 
10 | 	&#{&}-nol {
11 | 		display: block;
12 | 	}
13 | 
14 | 	&_l {
15 | 		color: tp.cssVar('label-fg');
16 | 		flex: 1;
17 | 		hyphens: auto;
18 | 		overflow: hidden;
19 | 		padding-left: 4px;
20 | 		padding-right: 16px;
21 | 	}
22 | 	&.#{tp.$disabled} &_l {
23 | 		opacity: 0.5;
24 | 	}
25 | 	&#{&}-nol &_l {
26 | 		display: none;
27 | 	}
28 | 	&_v {
29 | 		align-self: flex-start;
30 | 		flex-grow: 0;
31 | 		flex-shrink: 0;
32 | 		width: tp.cssVar('blade-value-width');
33 | 	}
34 | 	&#{&}-nol &_v {
35 | 		width: 100%;
36 | 	}
37 | }
38 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_list.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-lstv {
 4 | 	@extend %tp-list;
 5 | 
 6 | 	&_s {
 7 | 		@extend %tp-list_select;
 8 | 
 9 | 		padding: 0 (16px + 2px * 2) 0 tp.cssVar('blade-h-padding');
10 | 		width: 100%;
11 | 	}
12 | 	&_m {
13 | 		@extend %tp-list_arrow;
14 | 
15 | 		color: tp.cssVar('button-fg');
16 | 	}
17 | }
18 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_log.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-sglv {
 4 | 	&_i {
 5 | 		@extend %tp-monitor;
 6 | 
 7 | 		padding-left: tp.cssVar('blade-h-padding');
 8 | 		padding-right: tp.cssVar('blade-h-padding');
 9 | 	}
10 | 	&.#{tp.$disabled} &_i {
11 | 		opacity: 0.5;
12 | 	}
13 | }
14 | 
15 | .#{tp.$prefix}-mllv {
16 | 	&_i {
17 | 		@extend %tp-monitor;
18 | 
19 | 		display: block;
20 | 		height: calc(#{tp.cssVar('container-unit-size')} * 3);
21 | 		line-height: tp.cssVar('container-unit-size');
22 | 		padding-left: tp.cssVar('blade-h-padding');
23 | 		padding-right: tp.cssVar('blade-h-padding');
24 | 		resize: none;
25 | 		white-space: pre;
26 | 	}
27 | 	&.#{tp.$disabled} &_i {
28 | 		opacity: 0.5;
29 | 	}
30 | }
31 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_point-2d-picker.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-p2dpv {
 4 | 	padding-left: calc(#{tp.cssVar('container-unit-size')} + 4px);
 5 | 
 6 | 	// Pad
 7 | 	&_p {
 8 | 		@extend %tp-input;
 9 | 
10 | 		cursor: crosshair;
11 | 		height: 0;
12 | 		overflow: hidden;
13 | 		padding-bottom: 100%;
14 | 		position: relative;
15 | 	}
16 | 	&.#{tp.$disabled} &_p {
17 | 		opacity: 0.5;
18 | 	}
19 | 	// Graphics
20 | 	&_g {
21 | 		display: block;
22 | 		height: 100%;
23 | 		left: 0;
24 | 		pointer-events: none;
25 | 		position: absolute;
26 | 		top: 0;
27 | 		width: 100%;
28 | 	}
29 | 	// Axis
30 | 	&_ax {
31 | 		opacity: 0.1;
32 | 		stroke: tp.cssVar('input-fg');
33 | 		stroke-dasharray: 1;
34 | 	}
35 | 	// Line
36 | 	&_l {
37 | 		opacity: 0.5;
38 | 		stroke: tp.cssVar('input-fg');
39 | 		stroke-dasharray: 1;
40 | 	}
41 | 	// Marker
42 | 	&_m {
43 | 		border: tp.cssVar('input-fg') solid 1px;
44 | 		border-radius: 50%;
45 | 		box-sizing: border-box;
46 | 		height: 4px;
47 | 		margin-left: -2px;
48 | 		margin-top: -2px;
49 | 		position: absolute;
50 | 		width: 4px;
51 | 	}
52 | 	&_p:focus &_m {
53 | 		background-color: tp.cssVar('input-fg');
54 | 		border-width: 0;
55 | 	}
56 | }
57 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_point-2d.scss:
--------------------------------------------------------------------------------
 1 | @use 'sass:math';
 2 | @use '../tp';
 3 | 
 4 | .#{tp.$prefix}-p2dv {
 5 | 	position: relative;
 6 | 
 7 | 	// Head
 8 | 	&_h {
 9 | 		display: flex;
10 | 	}
11 | 	// Button
12 | 	&_b {
13 | 		@extend %tp-button;
14 | 
15 | 		height: tp.cssVar('container-unit-size');
16 | 		margin-right: 4px;
17 | 		position: relative;
18 | 		width: tp.cssVar('container-unit-size');
19 | 
20 | 		svg {
21 | 			display: block;
22 | 			height: 16px;
23 | 			left: 50%;
24 | 			margin-left: math.div(-16px, 2);
25 | 			margin-top: math.div(-16px, 2);
26 | 			position: absolute;
27 | 			top: 50%;
28 | 			width: 16px;
29 | 
30 | 			path {
31 | 				stroke: currentColor;
32 | 				stroke-width: 2;
33 | 			}
34 | 			circle {
35 | 				fill: currentColor;
36 | 			}
37 | 		}
38 | 	}
39 | 	// Text
40 | 	&_t {
41 | 		flex: 1;
42 | 	}
43 | 	// Inline picker
44 | 	&_p {
45 | 		height: 0;
46 | 		margin-top: 0;
47 | 		opacity: 0;
48 | 		overflow: hidden;
49 | 		transition: height tp.$fold-transition-duration ease-in-out,
50 | 			opacity tp.$fold-transition-duration linear,
51 | 			margin tp.$fold-transition-duration ease-in-out;
52 | 	}
53 | 	&#{&}-expanded &_p {
54 | 		margin-top: tp.cssVar('container-unit-spacing');
55 | 		opacity: 1;
56 | 	}
57 | 
58 | 	// Popup
59 | 	.#{tp.$prefix}-popv {
60 | 		left: calc(-1 * #{tp.cssVar('container-h-padding')});
61 | 		right: calc(-1 * #{tp.cssVar('container-h-padding')});
62 | 		top: tp.cssVar('container-unit-size');
63 | 	}
64 | }
65 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_point-nd-text.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-pndtxtv {
 4 | 	@extend %tp-texts;
 5 | 
 6 | 	&_a {
 7 | 		@extend %tp-texts_item;
 8 | 	}
 9 | }
10 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_popup.scss:
--------------------------------------------------------------------------------
 1 | @use '../tp';
 2 | 
 3 | .#{tp.$prefix}-popv {
 4 | 	background-color: tp.cssVar('base-bg');
 5 | 	border-radius: tp.cssVar('base-border-radius');
 6 | 	box-shadow: 0 2px 4px tp.cssVar('base-shadow');
 7 | 	display: none;
 8 | 	max-width: tp.cssVar('blade-value-width');
 9 | 	padding: tp.cssVar('container-v-padding') tp.cssVar('container-h-padding');
10 | 	position: absolute;
11 | 	visibility: hidden;
12 | 	z-index: tp.$z-index-picker;
13 | 
14 | 	&#{&}-v {
15 | 		display: block;
16 | 		visibility: visible;
17 | 	}
18 | }
19 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_tooltip.scss:
--------------------------------------------------------------------------------
 1 | @use 'sass:math';
 2 | @use '../tp';
 3 | 
 4 | .#{tp.$prefix}-ttv {
 5 | 	background-color: tp.cssVar('input-fg');
 6 | 	border-radius: tp.cssVar('blade-border-radius');
 7 | 	color: tp.cssVar('base-bg');
 8 | 	padding: 2px 4px;
 9 | 	pointer-events: none;
10 | 	position: absolute;
11 | 	transform: translate(-50%, -100%);
12 | 
13 | 	&::before {
14 | 		$size: 4px;
15 | 
16 | 		border-color: tp.cssVar('input-fg') transparent transparent transparent;
17 | 		border-style: solid;
18 | 		border-width: math.div($size, 2);
19 | 		box-sizing: border-box;
20 | 		content: '';
21 | 		font-size: 0.9em;
22 | 		height: $size;
23 | 		left: 50%;
24 | 		margin-left: math.div(-$size, 2);
25 | 		position: absolute;
26 | 		top: 100%;
27 | 		width: $size;
28 | 	}
29 | }
30 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/_views.scss:
--------------------------------------------------------------------------------
 1 | // Entry point for full features
 2 | 
 3 | @forward './button';
 4 | @forward './checkbox';
 5 | @forward './color';
 6 | @forward './color-picker';
 7 | @forward './color-swatch';
 8 | @forward './color-text';
 9 | @forward './default-wrapper';
10 | @forward './folder';
11 | @forward './graph-log';
12 | @forward './label';
13 | @forward './list';
14 | @forward './log';
15 | @forward './point-2d';
16 | @forward './point-2d-picker';
17 | @forward './point-nd-text';
18 | @forward './popup';
19 | @forward './slider';
20 | @forward './tab';
21 | @forward './text';
22 | @forward './tooltip';
23 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/common/_color.scss:
--------------------------------------------------------------------------------
 1 | @use 'sass:math';
 2 | 
 3 | @mixin checkerboard($size) {
 4 | 	$checkerboard-color-dark: #ddd;
 5 | 	$checkerboard-color-light: white;
 6 | 
 7 | 	background-color: $checkerboard-color-light;
 8 | 	background-image: linear-gradient(
 9 | 			to top right,
10 | 			$checkerboard-color-dark 25%,
11 | 			transparent 25%,
12 | 			transparent 75%,
13 | 			$checkerboard-color-dark 75%
14 | 		),
15 | 		linear-gradient(
16 | 			to top right,
17 | 			$checkerboard-color-dark 25%,
18 | 			transparent 25%,
19 | 			transparent 75%,
20 | 			$checkerboard-color-dark 75%
21 | 		);
22 | 	background-size: $size $size;
23 | 	background-position: 0 0, math.div($size, 2) math.div($size, 2);
24 | }
25 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/placeholder/_button.scss:
--------------------------------------------------------------------------------
 1 | @use '../../common/defs';
 2 | 
 3 | %tp-button {
 4 | 	@extend %tp-resetUserAgent;
 5 | 
 6 | 	background-color: defs.cssVar('button-bg');
 7 | 	border-radius: defs.cssVar('blade-border-radius');
 8 | 	color: defs.cssVar('button-fg');
 9 | 	cursor: pointer;
10 | 	display: block;
11 | 	font-weight: bold;
12 | 	height: defs.cssVar('container-unit-size');
13 | 	line-height: defs.cssVar('container-unit-size');
14 | 	overflow: hidden;
15 | 	text-overflow: ellipsis;
16 | 	white-space: nowrap;
17 | 
18 | 	&:hover {
19 | 		background-color: defs.cssVar('button-bg-hover');
20 | 	}
21 | 	&:focus {
22 | 		background-color: defs.cssVar('button-bg-focus');
23 | 	}
24 | 	&:active {
25 | 		background-color: defs.cssVar('button-bg-active');
26 | 	}
27 | 	&:disabled {
28 | 		opacity: 0.5;
29 | 	}
30 | }
31 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/placeholder/_input.scss:
--------------------------------------------------------------------------------
 1 | @use '../../common/defs';
 2 | 
 3 | %tp-input {
 4 | 	@extend %tp-resetUserAgent;
 5 | 
 6 | 	background-color: defs.cssVar('input-bg');
 7 | 	border-radius: defs.cssVar('blade-border-radius');
 8 | 	box-sizing: border-box;
 9 | 	color: defs.cssVar('input-fg');
10 | 	font-family: inherit;
11 | 	height: defs.cssVar('container-unit-size');
12 | 	line-height: defs.cssVar('container-unit-size');
13 | 	min-width: 0;
14 | 	width: 100%;
15 | 
16 | 	&:hover {
17 | 		background-color: defs.cssVar('input-bg-hover');
18 | 	}
19 | 	&:focus {
20 | 		background-color: defs.cssVar('input-bg-focus');
21 | 	}
22 | 	&:active {
23 | 		background-color: defs.cssVar('input-bg-active');
24 | 	}
25 | 	&:disabled {
26 | 		opacity: 0.5;
27 | 	}
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/placeholder/_list.scss:
--------------------------------------------------------------------------------
 1 | @use './button';
 2 | 
 3 | %tp-list {
 4 | 	position: relative;
 5 | }
 6 | 
 7 | %tp-list_select {
 8 | 	@extend %tp-button;
 9 | 
10 | 	padding: 0 (16px + 2px * 2) 0 4px;
11 | 	width: 100%;
12 | }
13 | 
14 | %tp-list_arrow {
15 | 	bottom: 0;
16 | 	margin: auto;
17 | 	pointer-events: none;
18 | 	position: absolute;
19 | 	right: 2px;
20 | 	top: 0;
21 | 
22 | 	svg {
23 | 		bottom: 0;
24 | 		height: 16px;
25 | 		margin: auto;
26 | 		position: absolute;
27 | 		right: 0;
28 | 		top: 0;
29 | 		width: 16px;
30 | 
31 | 		path {
32 | 			fill: currentColor;
33 | 		}
34 | 	}
35 | }
36 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/placeholder/_monitor.scss:
--------------------------------------------------------------------------------
 1 | @use '../../common/defs';
 2 | 
 3 | %tp-monitor {
 4 | 	@extend %tp-resetUserAgent;
 5 | 
 6 | 	background-color: defs.cssVar('monitor-bg');
 7 | 	border-radius: defs.cssVar('blade-border-radius');
 8 | 	box-sizing: border-box;
 9 | 	color: defs.cssVar('monitor-fg');
10 | 	height: defs.cssVar('container-unit-size');
11 | 	scrollbar-color: currentColor transparent;
12 | 	scrollbar-width: thin;
13 | 	width: 100%;
14 | 
15 | 	&::-webkit-scrollbar {
16 | 		height: 8px;
17 | 		width: 8px;
18 | 	}
19 | 	&::-webkit-scrollbar-corner {
20 | 		background-color: transparent;
21 | 	}
22 | 	&::-webkit-scrollbar-thumb {
23 | 		background-clip: padding-box;
24 | 		background-color: currentColor;
25 | 		border: transparent solid 2px;
26 | 		border-radius: 4px;
27 | 	}
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/core/lib/sass/view/placeholder/_texts.scss:
--------------------------------------------------------------------------------
 1 | %tp-texts {
 2 | 	display: flex;
 3 | }
 4 | 
 5 | %tp-texts_item {
 6 | 	width: 100%;
 7 | 
 8 | 	& + & {
 9 | 		margin-left: 2px;
10 | 	}
11 | }
12 | 


--------------------------------------------------------------------------------
/packages/core/scripts/replace-version.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable no-console */
 2 | /* eslint-env node */
 3 | 
 4 | import Fs from 'fs';
 5 | 
 6 | const Package = JSON.parse(
 7 | 	Fs.readFileSync(new URL('../package.json', import.meta.url)),
 8 | );
 9 | 
10 | const path = process.argv[2];
11 | const f = Fs.readFileSync(path, 'utf-8');
12 | Fs.writeFileSync(path, f.replace('0.0.0-core.0', Package.version));
13 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/binding/api/input-binding.ts:
--------------------------------------------------------------------------------
 1 | import {InputBindingController} from '../controller/input-binding.js';
 2 | import {BindingApi} from './binding.js';
 3 | 
 4 | /**
 5 |  * The API for input binding between the parameter and the pane.
 6 |  * @template In The internal type.
 7 |  * @template Ex The external type.
 8 |  */
 9 | export type InputBindingApi<In = unknown, Ex = unknown> = BindingApi<
10 | 	In,
11 | 	Ex,
12 | 	InputBindingController<In>
13 | >;
14 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/binding/api/monitor-binding.ts:
--------------------------------------------------------------------------------
 1 | import {TpBuffer} from '../../../common/model/buffered-value.js';
 2 | import {MonitorBindingController} from '../controller/monitor-binding.js';
 3 | import {BindingApi} from './binding.js';
 4 | 
 5 | /**
 6 |  * The API for the monitor binding between the parameter and the pane.
 7 |  * @template T
 8 |  */
 9 | export type MonitorBindingApi<T = unknown> = BindingApi<
10 | 	TpBuffer<T>,
11 | 	T,
12 | 	MonitorBindingController<T>
13 | >;
14 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/binding/controller/input-binding.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	InputBindingValue,
 3 | 	isInputBindingValue,
 4 | } from '../../../common/binding/value/input-binding.js';
 5 | import {ValueController} from '../../../common/controller/value.js';
 6 | import {BladeController} from '../../common/controller/blade.js';
 7 | import {
 8 | 	BladeState,
 9 | 	importBladeState,
10 | } from '../../common/controller/blade-state.js';
11 | import {isValueBladeController} from '../../common/controller/value-blade.js';
12 | import {BindingController} from './binding.js';
13 | 
14 | /**
15 |  * @hidden
16 |  */
17 | export class InputBindingController<
18 | 	In = unknown,
19 | 	Vc extends ValueController<In> = ValueController<In>,
20 | > extends BindingController<In, Vc, InputBindingValue<In>> {
21 | 	override importState(state: BladeState): boolean {
22 | 		return importBladeState(
23 | 			state,
24 | 			(s) => super.importState(s),
25 | 			(p) => ({
26 | 				binding: p.required.object({
27 | 					value: p.required.raw,
28 | 				}),
29 | 			}),
30 | 			(result) => {
31 | 				this.value.binding.inject(result.binding.value);
32 | 				this.value.fetch();
33 | 				return true;
34 | 			},
35 | 		);
36 | 	}
37 | }
38 | 
39 | export function isInputBindingController(
40 | 	bc: BladeController,
41 | ): bc is InputBindingController {
42 | 	return isValueBladeController(bc) && isInputBindingValue(bc.value);
43 | }
44 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/binding/controller/monitor-binding.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	isMonitorBindingValue,
 3 | 	MonitorBindingValue,
 4 | } from '../../../common/binding/value/monitor-binding.js';
 5 | import {ValueController} from '../../../common/controller/value.js';
 6 | import {BufferedValue, TpBuffer} from '../../../common/model/buffered-value.js';
 7 | import {View} from '../../../common/view/view.js';
 8 | import {BladeController} from '../../common/controller/blade.js';
 9 | import {
10 | 	BladeState,
11 | 	exportBladeState,
12 | } from '../../common/controller/blade-state.js';
13 | import {isValueBladeController} from '../../common/controller/value-blade.js';
14 | import {BindingController} from './binding.js';
15 | 
16 | export type BufferedValueController<
17 | 	T,
18 | 	Vw extends View = View,
19 | > = ValueController<TpBuffer<T>, Vw>;
20 | 
21 | /**
22 |  * @hidden
23 |  */
24 | export class MonitorBindingController<
25 | 	T = unknown,
26 | 	Vc extends BufferedValueController<T> = BufferedValueController<T>,
27 | > extends BindingController<TpBuffer<T>, Vc, MonitorBindingValue<T>> {
28 | 	override exportState(): BladeState {
29 | 		return exportBladeState(() => super.exportState(), {
30 | 			binding: {
31 | 				readonly: true,
32 | 			},
33 | 		});
34 | 	}
35 | }
36 | 
37 | export function isMonitorBindingController(
38 | 	bc: BladeController,
39 | ): bc is MonitorBindingController {
40 | 	return (
41 | 		isValueBladeController(bc) &&
42 | 		isMonitorBindingValue(bc.value as BufferedValue<unknown>)
43 | 	);
44 | }
45 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/button/api/button.ts:
--------------------------------------------------------------------------------
 1 | import {BladeApi} from '../../common/api/blade.js';
 2 | import {EventListenable} from '../../common/api/event-listenable.js';
 3 | import {TpMouseEvent} from '../../common/api/tp-event.js';
 4 | import {ButtonBladeController} from '../controller/button-blade.js';
 5 | 
 6 | export interface ButtonApiEvents {
 7 | 	click: TpMouseEvent<ButtonApi>;
 8 | }
 9 | 
10 | export class ButtonApi
11 | 	extends BladeApi<ButtonBladeController>
12 | 	implements EventListenable<ButtonApiEvents>
13 | {
14 | 	get label(): string | null | undefined {
15 | 		return this.controller.labelController.props.get('label');
16 | 	}
17 | 
18 | 	set label(label: string | null | undefined) {
19 | 		this.controller.labelController.props.set('label', label);
20 | 	}
21 | 
22 | 	get title(): string {
23 | 		return this.controller.buttonController.props.get('title') ?? '';
24 | 	}
25 | 
26 | 	set title(title: string) {
27 | 		this.controller.buttonController.props.set('title', title);
28 | 	}
29 | 
30 | 	public on<EventName extends keyof ButtonApiEvents>(
31 | 		eventName: EventName,
32 | 		handler: (ev: ButtonApiEvents[EventName]) => void,
33 | 	): this {
34 | 		const bh = handler.bind(this);
35 | 		const emitter = this.controller.buttonController.emitter;
36 | 		emitter.on(
37 | 			eventName,
38 | 			(ev) => {
39 | 				bh(new TpMouseEvent(this, ev.nativeEvent));
40 | 			},
41 | 			{key: handler},
42 | 		);
43 | 		return this;
44 | 	}
45 | 
46 | 	public off<EventName extends keyof ButtonApiEvents>(
47 | 		eventName: EventName,
48 | 		handler: (ev: ButtonApiEvents[EventName]) => void,
49 | 	): this {
50 | 		const emitter = this.controller.buttonController.emitter;
51 | 		emitter.off(eventName, handler);
52 | 		return this;
53 | 	}
54 | }
55 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/button/controller/button-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {ValueMap} from '../../../common/model/value-map.js';
 5 | import {ViewProps} from '../../../common/model/view-props.js';
 6 | import {createTestWindow} from '../../../misc/dom-test-util.js';
 7 | import {ButtonPropsObject} from '../view/button.js';
 8 | import {ButtonController} from './button.js';
 9 | 
10 | function createController(doc: Document, title: string) {
11 | 	return new ButtonController(doc, {
12 | 		props: ValueMap.fromObject<ButtonPropsObject>({
13 | 			title: title,
14 | 		}),
15 | 		viewProps: ViewProps.create(),
16 | 	});
17 | }
18 | 
19 | describe(ButtonController.name, () => {
20 | 	it('should emit click event', () => {
21 | 		const doc = createTestWindow().document;
22 | 		const c = createController(doc, 'Push');
23 | 
24 | 		let count = 0;
25 | 		c.emitter.on('click', () => {
26 | 			count += 1;
27 | 		});
28 | 
29 | 		c.view.buttonElement.click();
30 | 		assert.strictEqual(count, 1);
31 | 	});
32 | 
33 | 	it('should export state', () => {
34 | 		const doc = createTestWindow().document;
35 | 		const c = createController(doc, 'foo');
36 | 
37 | 		assert.deepStrictEqual(c.exportProps(), {
38 | 			title: 'foo',
39 | 		});
40 | 	});
41 | 
42 | 	it('should import state', () => {
43 | 		const doc = createTestWindow().document;
44 | 		const c = createController(doc, 'foo');
45 | 
46 | 		assert.strictEqual(
47 | 			c.importProps({
48 | 				title: 'bar',
49 | 			}),
50 | 			true,
51 | 		);
52 | 		assert.strictEqual(c.props.get('title'), 'bar');
53 | 	});
54 | });
55 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/button/plugin.ts:
--------------------------------------------------------------------------------
 1 | import {LabelPropsObject} from '../../common/label/view/label.js';
 2 | import {parseRecord} from '../../common/micro-parsers.js';
 3 | import {ValueMap} from '../../common/model/value-map.js';
 4 | import {BaseBladeParams} from '../../common/params.js';
 5 | import {createPlugin} from '../../plugin/plugin.js';
 6 | import {BladePlugin} from '../plugin.js';
 7 | import {ButtonApi} from './api/button.js';
 8 | import {ButtonBladeController} from './controller/button-blade.js';
 9 | import {ButtonPropsObject} from './view/button.js';
10 | 
11 | export interface ButtonBladeParams extends BaseBladeParams {
12 | 	title: string;
13 | 	view: 'button';
14 | 
15 | 	label?: string;
16 | }
17 | 
18 | export const ButtonBladePlugin: BladePlugin<ButtonBladeParams> = createPlugin({
19 | 	id: 'button',
20 | 	type: 'blade',
21 | 	accept(params) {
22 | 		const result = parseRecord<ButtonBladeParams>(params, (p) => ({
23 | 			title: p.required.string,
24 | 			view: p.required.constant('button'),
25 | 
26 | 			label: p.optional.string,
27 | 		}));
28 | 		return result ? {params: result} : null;
29 | 	},
30 | 	controller(args) {
31 | 		return new ButtonBladeController(args.document, {
32 | 			blade: args.blade,
33 | 			buttonProps: ValueMap.fromObject<ButtonPropsObject>({
34 | 				title: args.params.title,
35 | 			}),
36 | 			labelProps: ValueMap.fromObject<LabelPropsObject>({
37 | 				label: args.params.label,
38 | 			}),
39 | 			viewProps: args.viewProps,
40 | 		});
41 | 	},
42 | 	api(args) {
43 | 		if (args.controller instanceof ButtonBladeController) {
44 | 			return new ButtonApi(args.controller);
45 | 		}
46 | 		return null;
47 | 	},
48 | });
49 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/button/view/button.ts:
--------------------------------------------------------------------------------
 1 | import {ValueMap} from '../../../common/model/value-map.js';
 2 | import {ViewProps} from '../../../common/model/view-props.js';
 3 | import {ClassName} from '../../../common/view/class-name.js';
 4 | import {bindValueToTextContent} from '../../../common/view/reactive.js';
 5 | import {View} from '../../../common/view/view.js';
 6 | 
 7 | /**
 8 |  * @hidden
 9 |  */
10 | export type ButtonPropsObject = {
11 | 	title: string | undefined;
12 | };
13 | 
14 | /**
15 |  * @hidden
16 |  */
17 | export type ButtonProps = ValueMap<ButtonPropsObject>;
18 | 
19 | /**
20 |  * @hidden
21 |  */
22 | interface Config {
23 | 	props: ButtonProps;
24 | 	viewProps: ViewProps;
25 | }
26 | 
27 | const cn = ClassName('btn');
28 | 
29 | /**
30 |  * @hidden
31 |  */
32 | export class ButtonView implements View {
33 | 	public readonly element: HTMLElement;
34 | 	public readonly buttonElement: HTMLButtonElement;
35 | 
36 | 	constructor(doc: Document, config: Config) {
37 | 		this.element = doc.createElement('div');
38 | 		this.element.classList.add(cn());
39 | 		config.viewProps.bindClassModifiers(this.element);
40 | 
41 | 		const buttonElem = doc.createElement('button');
42 | 		buttonElem.classList.add(cn('b'));
43 | 		config.viewProps.bindDisabled(buttonElem);
44 | 		this.element.appendChild(buttonElem);
45 | 		this.buttonElement = buttonElem;
46 | 
47 | 		const titleElem = doc.createElement('div');
48 | 		titleElem.classList.add(cn('t'));
49 | 		bindValueToTextContent(config.props.value('title'), titleElem);
50 | 		this.buttonElement.appendChild(titleElem);
51 | 	}
52 | }
53 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/blade-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {ViewProps} from '../../../common/model/view-props.js';
 5 | import {View} from '../../../common/view/view.js';
 6 | import {createTestWindow} from '../../../misc/dom-test-util.js';
 7 | import {BladeController} from '../controller/blade.js';
 8 | import {createBlade} from '../model/blade.js';
 9 | import {BladeApi} from './blade.js';
10 | 
11 | class TestView implements View {
12 | 	readonly element: HTMLElement;
13 | 
14 | 	constructor(doc: Document) {
15 | 		this.element = doc.createElement('div');
16 | 	}
17 | }
18 | 
19 | describe(BladeApi.name, () => {
20 | 	it('should get element', () => {
21 | 		const doc = createTestWindow().document;
22 | 		const v = new TestView(doc);
23 | 		const c = new BladeController({
24 | 			blade: createBlade(),
25 | 			view: v,
26 | 			viewProps: ViewProps.create(),
27 | 		});
28 | 		const api = new BladeApi(c);
29 | 		assert.strictEqual(api.element, v.element);
30 | 	});
31 | });
32 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/blade.ts:
--------------------------------------------------------------------------------
 1 | import {BladeController} from '../controller/blade.js';
 2 | import {BladeState} from '../controller/blade-state.js';
 3 | 
 4 | export class BladeApi<C extends BladeController = BladeController> {
 5 | 	/**
 6 | 	 * @hidden
 7 | 	 */
 8 | 	public readonly controller: C;
 9 | 
10 | 	/**
11 | 	 * @hidden
12 | 	 */
13 | 	constructor(controller: C) {
14 | 		this.controller = controller;
15 | 	}
16 | 
17 | 	get element(): HTMLElement {
18 | 		return this.controller.view.element;
19 | 	}
20 | 
21 | 	get disabled(): boolean {
22 | 		return this.controller.viewProps.get('disabled');
23 | 	}
24 | 
25 | 	set disabled(disabled: boolean) {
26 | 		this.controller.viewProps.set('disabled', disabled);
27 | 	}
28 | 
29 | 	get hidden(): boolean {
30 | 		return this.controller.viewProps.get('hidden');
31 | 	}
32 | 
33 | 	set hidden(hidden: boolean) {
34 | 		this.controller.viewProps.set('hidden', hidden);
35 | 	}
36 | 
37 | 	public dispose(): void {
38 | 		this.controller.viewProps.set('disposed', true);
39 | 	}
40 | 
41 | 	public importState(state: BladeState): boolean {
42 | 		return this.controller.importState(state);
43 | 	}
44 | 
45 | 	public exportState(): BladeState {
46 | 		return this.controller.exportState();
47 | 	}
48 | }
49 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/container-blade.ts:
--------------------------------------------------------------------------------
 1 | import {PluginPool} from '../../../plugin/pool.js';
 2 | import {ContainerBladeController} from '../controller/container-blade.js';
 3 | import {BladeApi} from './blade.js';
 4 | import {RackApi} from './rack.js';
 5 | import {Refreshable} from './refreshable.js';
 6 | 
 7 | export class ContainerBladeApi<C extends ContainerBladeController>
 8 | 	extends BladeApi<C>
 9 | 	implements Refreshable
10 | {
11 | 	/**
12 | 	 * @hidden
13 | 	 */
14 | 	protected readonly rackApi_: RackApi;
15 | 
16 | 	/**
17 | 	 * @hidden
18 | 	 */
19 | 	constructor(controller: C, pool: PluginPool) {
20 | 		super(controller);
21 | 
22 | 		this.rackApi_ = new RackApi(controller.rackController, pool);
23 | 	}
24 | 
25 | 	public refresh(): void {
26 | 		this.rackApi_.refresh();
27 | 	}
28 | }
29 | 
30 | export function isContainerBladeApi(
31 | 	api: BladeApi,
32 | ): api is ContainerBladeApi<ContainerBladeController> {
33 | 	return 'rackApi_' in api;
34 | }
35 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/event-listenable.ts:
--------------------------------------------------------------------------------
 1 | export interface EventListenable<E> {
 2 | 	off<EventName extends keyof E>(
 3 | 		eventName: EventName,
 4 | 		handler: (ev: E[EventName]) => void,
 5 | 	): this;
 6 | 	on<EventName extends keyof E>(
 7 | 		eventName: EventName,
 8 | 		handler: (ev: E[EventName]) => void,
 9 | 	): this;
10 | }
11 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/events.ts:
--------------------------------------------------------------------------------
1 | import {BladeApi} from './blade.js';
2 | import {TpChangeEvent} from './tp-event.js';
3 | 
4 | export interface ApiChangeEvents<T> {
5 | 	change: TpChangeEvent<T, BladeApi>;
6 | }
7 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/refreshable-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {isRefreshable} from './refreshable.js';
 5 | 
 6 | describe(isRefreshable.name, () => {
 7 | 	it('should determine Refreshable', () => {
 8 | 		assert.strictEqual(
 9 | 			isRefreshable({
10 | 				refresh: () => {},
11 | 			}),
12 | 			true,
13 | 		);
14 | 	});
15 | 
16 | 	it('should not determine variable as Refreshable', () => {
17 | 		assert.strictEqual(
18 | 			isRefreshable({
19 | 				refresh: 1,
20 | 			}),
21 | 			false,
22 | 		);
23 | 	});
24 | });
25 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/refreshable.ts:
--------------------------------------------------------------------------------
 1 | import {isObject} from '../../../misc/type-util.js';
 2 | 
 3 | export interface Refreshable {
 4 | 	/**
 5 | 	 * Refreshes the target.
 6 | 	 */
 7 | 	refresh(): void;
 8 | }
 9 | 
10 | export function isRefreshable(value: unknown): value is Refreshable {
11 | 	if (!isObject(value)) {
12 | 		return false;
13 | 	}
14 | 	return 'refresh' in value && typeof value.refresh === 'function';
15 | }
16 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/api/test-util.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | 
 3 | import {BladeApi} from './blade.js';
 4 | 
 5 | export function assertInitialState(api: BladeApi) {
 6 | 	assert.strictEqual(api.disabled, false);
 7 | 	assert.strictEqual(api.hidden, false);
 8 | 	assert.strictEqual(api.controller.viewProps.get('disposed'), false);
 9 | }
10 | 
11 | export function assertDisposes(api: BladeApi) {
12 | 	api.dispose();
13 | 	assert.strictEqual(api.controller.viewProps.get('disposed'), true);
14 | }
15 | 
16 | export function assertUpdates(api: BladeApi) {
17 | 	api.disabled = true;
18 | 	assert.strictEqual(api.disabled, true);
19 | 	assert.strictEqual(
20 | 		api.controller.view.element.classList.contains('tp-v-disabled'),
21 | 		true,
22 | 	);
23 | 
24 | 	api.hidden = true;
25 | 	assert.strictEqual(api.hidden, true);
26 | 	assert.strictEqual(
27 | 		api.controller.view.element.classList.contains('tp-v-hidden'),
28 | 		true,
29 | 	);
30 | }
31 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/controller/container-blade.ts:
--------------------------------------------------------------------------------
 1 | import {View} from '../../../common/view/view.js';
 2 | import {Blade} from '../model/blade.js';
 3 | import {BladeController} from './blade.js';
 4 | import {BladeState, exportBladeState, importBladeState} from './blade-state.js';
 5 | import {RackController} from './rack.js';
 6 | 
 7 | /**
 8 |  * @hidden
 9 |  */
10 | interface Config<V extends View> {
11 | 	blade: Blade;
12 | 	rackController: RackController;
13 | 	view: V;
14 | }
15 | 
16 | /**
17 |  * @hidden
18 |  */
19 | export class ContainerBladeController<
20 | 	V extends View = View,
21 | > extends BladeController<V> {
22 | 	public readonly rackController: RackController;
23 | 
24 | 	constructor(config: Config<V>) {
25 | 		super({
26 | 			blade: config.blade,
27 | 			view: config.view,
28 | 			viewProps: config.rackController.viewProps,
29 | 		});
30 | 		this.rackController = config.rackController;
31 | 	}
32 | 
33 | 	public importState(state: BladeState): boolean {
34 | 		return importBladeState(
35 | 			state,
36 | 			(s) => super.importState(s),
37 | 			(p) => ({
38 | 				children: p.required.array(p.required.raw),
39 | 			}),
40 | 			(result) => {
41 | 				return this.rackController.rack.children.every((c, index) => {
42 | 					return c.importState(result.children[index] as BladeState);
43 | 				});
44 | 			},
45 | 		);
46 | 	}
47 | 
48 | 	public exportState(): BladeState {
49 | 		return exportBladeState(() => super.exportState(), {
50 | 			children: this.rackController.rack.children.map((c) => c.exportState()),
51 | 		});
52 | 	}
53 | }
54 | 
55 | export function isContainerBladeController(
56 | 	bc: BladeController,
57 | ): bc is ContainerBladeController {
58 | 	return 'rackController' in bc;
59 | }
60 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/controller/rack.ts:
--------------------------------------------------------------------------------
 1 | import {insertElementAt, removeElement} from '../../../common/dom-util.js';
 2 | import {ViewProps} from '../../../common/model/view-props.js';
 3 | import {Blade} from '../model/blade.js';
 4 | import {Rack, RackEvents} from '../model/rack.js';
 5 | 
 6 | /**
 7 |  * @hidden
 8 |  */
 9 | interface Config {
10 | 	blade: Blade;
11 | 	element: HTMLElement;
12 | 	viewProps: ViewProps;
13 | 
14 | 	root?: boolean;
15 | }
16 | 
17 | /**
18 |  * @hidden
19 |  */
20 | export class RackController {
21 | 	public readonly element: HTMLElement;
22 | 	public readonly rack: Rack;
23 | 	public readonly viewProps: ViewProps;
24 | 
25 | 	constructor(config: Config) {
26 | 		this.onRackAdd_ = this.onRackAdd_.bind(this);
27 | 		this.onRackRemove_ = this.onRackRemove_.bind(this);
28 | 
29 | 		this.element = config.element;
30 | 		this.viewProps = config.viewProps;
31 | 
32 | 		const rack = new Rack({
33 | 			blade: config.root ? undefined : config.blade,
34 | 			viewProps: config.viewProps,
35 | 		});
36 | 		rack.emitter.on('add', this.onRackAdd_);
37 | 		rack.emitter.on('remove', this.onRackRemove_);
38 | 		this.rack = rack;
39 | 
40 | 		this.viewProps.handleDispose(() => {
41 | 			for (let i = this.rack.children.length - 1; i >= 0; i--) {
42 | 				const bc = this.rack.children[i];
43 | 				bc.viewProps.set('disposed', true);
44 | 			}
45 | 		});
46 | 	}
47 | 
48 | 	private onRackAdd_(ev: RackEvents['add']): void {
49 | 		if (!ev.root) {
50 | 			return;
51 | 		}
52 | 		insertElementAt(this.element, ev.bladeController.view.element, ev.index);
53 | 	}
54 | 
55 | 	private onRackRemove_(ev: RackEvents['remove']): void {
56 | 		if (!ev.root) {
57 | 			return;
58 | 		}
59 | 		removeElement(ev.bladeController.view.element);
60 | 	}
61 | }
62 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/controller/value-blade.ts:
--------------------------------------------------------------------------------
1 | import {ValueController} from '../../../common/controller/value.js';
2 | import {BladeController} from './blade.js';
3 | 
4 | export function isValueBladeController(
5 | 	bc: BladeController,
6 | ): bc is BladeController & ValueController<unknown> {
7 | 	return 'value' in bc;
8 | }
9 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/model/blade-positions.ts:
--------------------------------------------------------------------------------
1 | export type BladePosition = 'veryfirst' | 'first' | 'last' | 'verylast';
2 | 
3 | export function getAllBladePositions(): BladePosition[] {
4 | 	return ['veryfirst', 'first', 'last', 'verylast'];
5 | }
6 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/model/blade.ts:
--------------------------------------------------------------------------------
 1 | import {ValueMap} from '../../../common/model/value-map.js';
 2 | import {createValue} from '../../../common/model/values.js';
 3 | import {deepEqualsArray} from '../../../misc/type-util.js';
 4 | import {BladePosition} from './blade-positions.js';
 5 | 
 6 | export type Blade = ValueMap<{
 7 | 	positions: BladePosition[];
 8 | }>;
 9 | 
10 | export function createBlade(): Blade {
11 | 	return new ValueMap({
12 | 		positions: createValue<BladePosition[]>([], {
13 | 			equals: deepEqualsArray,
14 | 		}),
15 | 	});
16 | }
17 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/common/view/blade-container.ts:
--------------------------------------------------------------------------------
1 | import {ClassName} from '../../../common/view/class-name.js';
2 | 
3 | export const bladeContainerClassName = ClassName('cnt');
4 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/folder/plugin-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe} from 'mocha';
 3 | 
 4 | import {createTestWindow} from '../../misc/dom-test-util.js';
 5 | import {createDefaultPluginPool} from '../../plugin/plugins.js';
 6 | import {createEmptyBladeController} from '../test-util.js';
 7 | import {FolderBladePlugin} from './plugin.js';
 8 | 
 9 | describe(FolderBladePlugin.id, () => {
10 | 	[(doc: Document) => createEmptyBladeController(doc)].forEach(
11 | 		(createController) => {
12 | 			it('should not create API', () => {
13 | 				const doc = createTestWindow().document;
14 | 				const c = createController(doc);
15 | 				const api = FolderBladePlugin.api({
16 | 					controller: c,
17 | 					pool: createDefaultPluginPool(),
18 | 				});
19 | 				assert.strictEqual(api, null);
20 | 			});
21 | 		},
22 | 	);
23 | });
24 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/folder/plugin.ts:
--------------------------------------------------------------------------------
 1 | import {parseRecord} from '../../common/micro-parsers.js';
 2 | import {ValueMap} from '../../common/model/value-map.js';
 3 | import {BaseBladeParams} from '../../common/params.js';
 4 | import {createPlugin} from '../../plugin/plugin.js';
 5 | import {BladePlugin} from '../plugin.js';
 6 | import {FolderApi} from './api/folder.js';
 7 | import {FolderController} from './controller/folder.js';
 8 | import {FolderPropsObject} from './view/folder.js';
 9 | 
10 | export interface FolderBladeParams extends BaseBladeParams {
11 | 	title: string;
12 | 	view: 'folder';
13 | 
14 | 	expanded?: boolean;
15 | }
16 | 
17 | export const FolderBladePlugin: BladePlugin<FolderBladeParams> = createPlugin({
18 | 	id: 'folder',
19 | 	type: 'blade',
20 | 	accept(params) {
21 | 		const result = parseRecord<FolderBladeParams>(params, (p) => ({
22 | 			title: p.required.string,
23 | 			view: p.required.constant('folder'),
24 | 
25 | 			expanded: p.optional.boolean,
26 | 		}));
27 | 		return result ? {params: result} : null;
28 | 	},
29 | 	controller(args) {
30 | 		return new FolderController(args.document, {
31 | 			blade: args.blade,
32 | 			expanded: args.params.expanded,
33 | 			props: ValueMap.fromObject<FolderPropsObject>({
34 | 				title: args.params.title,
35 | 			}),
36 | 			viewProps: args.viewProps,
37 | 		});
38 | 	},
39 | 	api(args) {
40 | 		if (!(args.controller instanceof FolderController)) {
41 | 			return null;
42 | 		}
43 | 		return new FolderApi(args.controller, args.pool);
44 | 	},
45 | });
46 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/tab/controller/tab-item.ts:
--------------------------------------------------------------------------------
 1 | import {Controller} from '../../../common/controller/controller.js';
 2 | import {Emitter} from '../../../common/model/emitter.js';
 3 | import {ViewProps} from '../../../common/model/view-props.js';
 4 | import {TabItemProps, TabItemView} from '../view/tab-item.js';
 5 | 
 6 | /**
 7 |  * @hidden
 8 |  */
 9 | export interface TabItemEvents {
10 | 	click: {
11 | 		sender: TabItemController;
12 | 	};
13 | }
14 | 
15 | /**
16 |  * @hidden
17 |  */
18 | interface Config {
19 | 	props: TabItemProps;
20 | 	viewProps: ViewProps;
21 | }
22 | 
23 | /**
24 |  * @hidden
25 |  */
26 | export class TabItemController implements Controller<TabItemView> {
27 | 	public readonly emitter: Emitter<TabItemEvents> = new Emitter();
28 | 	public readonly props: TabItemProps;
29 | 	public readonly view: TabItemView;
30 | 	public readonly viewProps: ViewProps;
31 | 
32 | 	constructor(doc: Document, config: Config) {
33 | 		this.onClick_ = this.onClick_.bind(this);
34 | 
35 | 		this.props = config.props;
36 | 		this.viewProps = config.viewProps;
37 | 
38 | 		this.view = new TabItemView(doc, {
39 | 			props: config.props,
40 | 			viewProps: config.viewProps,
41 | 		});
42 | 		this.view.buttonElement.addEventListener('click', this.onClick_);
43 | 	}
44 | 
45 | 	private onClick_(): void {
46 | 		this.emitter.emit('click', {
47 | 			sender: this,
48 | 		});
49 | 	}
50 | }
51 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/tab/view/tab-page.ts:
--------------------------------------------------------------------------------
 1 | import {ViewProps} from '../../../common/model/view-props.js';
 2 | import {ClassName} from '../../../common/view/class-name.js';
 3 | import {View} from '../../../common/view/view.js';
 4 | 
 5 | interface Config {
 6 | 	viewProps: ViewProps;
 7 | }
 8 | 
 9 | const cn = ClassName('tbp');
10 | 
11 | /**
12 |  * @hidden
13 |  */
14 | export class TabPageView implements View {
15 | 	public readonly element: HTMLElement;
16 | 	public readonly containerElement: HTMLElement;
17 | 
18 | 	constructor(doc: Document, config: Config) {
19 | 		this.element = doc.createElement('div');
20 | 		this.element.classList.add(cn());
21 | 		config.viewProps.bindClassModifiers(this.element);
22 | 
23 | 		const containerElem = doc.createElement('div');
24 | 		containerElem.classList.add(cn('c'));
25 | 		this.element.appendChild(containerElem);
26 | 		this.containerElement = containerElem;
27 | 	}
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/core/src/blade/tab/view/tab.ts:
--------------------------------------------------------------------------------
 1 | import {bindValue} from '../../../common/model/reactive.js';
 2 | import {Value} from '../../../common/model/value.js';
 3 | import {ViewProps} from '../../../common/model/view-props.js';
 4 | import {ClassName} from '../../../common/view/class-name.js';
 5 | import {valueToClassName} from '../../../common/view/reactive.js';
 6 | import {View} from '../../../common/view/view.js';
 7 | import {bladeContainerClassName} from '../../common/view/blade-container.js';
 8 | 
 9 | /**
10 |  * @hidden
11 |  */
12 | interface Config {
13 | 	empty: Value<boolean>;
14 | 	viewProps: ViewProps;
15 | }
16 | 
17 | const cn = ClassName('tab');
18 | 
19 | /**
20 |  * @hidden
21 |  */
22 | export class TabView implements View {
23 | 	public readonly element: HTMLElement;
24 | 	public readonly itemsElement: HTMLElement;
25 | 	public readonly contentsElement: HTMLElement;
26 | 
27 | 	constructor(doc: Document, config: Config) {
28 | 		this.element = doc.createElement('div');
29 | 		this.element.classList.add(cn(), bladeContainerClassName());
30 | 		config.viewProps.bindClassModifiers(this.element);
31 | 		bindValue(
32 | 			config.empty,
33 | 			valueToClassName(this.element, cn(undefined, 'nop')),
34 | 		);
35 | 
36 | 		const titleElem = doc.createElement('div');
37 | 		titleElem.classList.add(cn('t'));
38 | 		this.element.appendChild(titleElem);
39 | 		this.itemsElement = titleElem;
40 | 
41 | 		const indentElem = doc.createElement('div');
42 | 		indentElem.classList.add(cn('i'));
43 | 		this.element.appendChild(indentElem);
44 | 
45 | 		const contentsElem = doc.createElement('div');
46 | 		contentsElem.classList.add(cn('c'));
47 | 		this.element.appendChild(contentsElem);
48 | 		this.contentsElement = contentsElem;
49 | 	}
50 | }
51 | 


--------------------------------------------------------------------------------
/packages/core/src/common/api/list.ts:
--------------------------------------------------------------------------------
 1 | import {BindingApi} from '../../blade/binding/api/binding.js';
 2 | import {InputBindingApi} from '../../blade/binding/api/input-binding.js';
 3 | import {InputBindingController} from '../../blade/binding/controller/input-binding.js';
 4 | import {ListItem} from '../constraint/list.js';
 5 | import {ListController} from '../controller/list.js';
 6 | 
 7 | export class ListInputBindingApi<T>
 8 | 	extends BindingApi<T, T, InputBindingController<T, ListController<T>>>
 9 | 	implements InputBindingApi<T, T>
10 | {
11 | 	get options(): ListItem<T>[] {
12 | 		return this.controller.valueController.props.get('options');
13 | 	}
14 | 
15 | 	set options(options: ListItem<T>[]) {
16 | 		this.controller.valueController.props.set('options', options);
17 | 	}
18 | }
19 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/binding.ts:
--------------------------------------------------------------------------------
 1 | import {isObject} from '../../misc/type-util.js';
 2 | import {BindingTarget} from './target.js';
 3 | 
 4 | /**
 5 |  * Converts an external unknown value into the internal value.
 6 |  * @template In The type of the internal value.
 7 |  */
 8 | export interface BindingReader<In> {
 9 | 	/**
10 | 	 * @param exValue The bound value.
11 | 	 * @return A converted value.
12 | 	 */
13 | 	(exValue: unknown): In;
14 | }
15 | 
16 | /**
17 |  * Writes the internal value to the bound target.
18 |  * @template In The type of the internal value.
19 |  */
20 | export interface BindingWriter<In> {
21 | 	/**
22 | 	 * @param target The target to be written.
23 | 	 * @param inValue The value to write.
24 | 	 */
25 | 	(target: BindingTarget, inValue: In): void;
26 | }
27 | 
28 | /**
29 |  * @hidden
30 |  */
31 | export interface Binding {
32 | 	readonly target: BindingTarget;
33 | }
34 | 
35 | export function isBinding(value: unknown): value is Binding {
36 | 	if (!isObject(value)) {
37 | 		return false;
38 | 	}
39 | 	return 'target' in value;
40 | }
41 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/read-write-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {numberFromUnknown} from '../converter/number.js';
 5 | import {writePrimitive} from '../primitive.js';
 6 | import {ReadWriteBinding} from './read-write.js';
 7 | import {BindingTarget} from './target.js';
 8 | 
 9 | describe(ReadWriteBinding.name, () => {
10 | 	it('should read value', () => {
11 | 		const obj = {
12 | 			foo: 123,
13 | 		};
14 | 		const target = new BindingTarget(obj, 'foo');
15 | 		const b = new ReadWriteBinding({
16 | 			reader: numberFromUnknown,
17 | 			target: target,
18 | 			writer: (v) => v,
19 | 		});
20 | 
21 | 		assert.strictEqual(b.read(), 123);
22 | 	});
23 | 
24 | 	it('should write value', () => {
25 | 		const obj = {
26 | 			foo: 123,
27 | 		};
28 | 		const target = new BindingTarget(obj, 'foo');
29 | 		const b = new ReadWriteBinding({
30 | 			reader: numberFromUnknown,
31 | 			target: target,
32 | 			writer: writePrimitive,
33 | 		});
34 | 		b.write(456);
35 | 
36 | 		assert.strictEqual(obj.foo, 456);
37 | 	});
38 | });
39 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/read-write.ts:
--------------------------------------------------------------------------------
 1 | import {Binding, BindingReader, BindingWriter} from './binding.js';
 2 | import {BindingTarget} from './target.js';
 3 | 
 4 | /**
 5 |  * @hidden
 6 |  */
 7 | interface Config<T> {
 8 | 	reader: BindingReader<T>;
 9 | 	target: BindingTarget;
10 | 	writer: BindingWriter<T>;
11 | }
12 | 
13 | /**
14 |  * A binding that can read and write the target.
15 |  * @hidden
16 |  * @template In The type of the internal value.
17 |  */
18 | export class ReadWriteBinding<In> implements Binding {
19 | 	public readonly target: BindingTarget;
20 | 	private readonly reader_: BindingReader<In>;
21 | 	private readonly writer_: BindingWriter<In>;
22 | 
23 | 	constructor(config: Config<In>) {
24 | 		this.target = config.target;
25 | 		this.reader_ = config.reader;
26 | 		this.writer_ = config.writer;
27 | 	}
28 | 
29 | 	public read(): In {
30 | 		return this.reader_(this.target.read());
31 | 	}
32 | 
33 | 	public write(value: In): void {
34 | 		this.writer_(this.target, value);
35 | 	}
36 | 
37 | 	public inject(value: unknown): void {
38 | 		this.write(this.reader_(value));
39 | 	}
40 | }
41 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/readonly.ts:
--------------------------------------------------------------------------------
 1 | import {Binding, BindingReader} from './binding.js';
 2 | import {BindingTarget} from './target.js';
 3 | 
 4 | /**
 5 |  * @hidden
 6 |  */
 7 | interface Config<T> {
 8 | 	reader: BindingReader<T>;
 9 | 	target: BindingTarget;
10 | }
11 | 
12 | /**
13 |  * A binding that can read the target.
14 |  * @hidden
15 |  * @template In The type of the internal value.
16 |  */
17 | export class ReadonlyBinding<In> implements Binding {
18 | 	public readonly target: BindingTarget;
19 | 	private readonly reader_: BindingReader<In>;
20 | 
21 | 	constructor(config: Config<In>) {
22 | 		this.target = config.target;
23 | 		this.reader_ = config.reader;
24 | 	}
25 | 
26 | 	public read(): In {
27 | 		return this.reader_(this.target.read());
28 | 	}
29 | }
30 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/target-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {BindingTarget} from './target.js';
 5 | 
 6 | describe(BindingTarget.name, () => {
 7 | 	it('should get properties', () => {
 8 | 		const obj = {foo: 'bar'};
 9 | 		const target = new BindingTarget(obj, 'foo');
10 | 		assert.strictEqual(target.key, 'foo');
11 | 	});
12 | 
13 | 	it('should read value', () => {
14 | 		const obj = {foo: 'bar'};
15 | 		const target = new BindingTarget(obj, 'foo');
16 | 		assert.strictEqual(target.read(), 'bar');
17 | 	});
18 | 
19 | 	it('should write value', () => {
20 | 		const obj = {foo: 'bar'};
21 | 		const target = new BindingTarget(obj, 'foo');
22 | 		target.write('wrote');
23 | 		assert.strictEqual(obj.foo, 'wrote');
24 | 	});
25 | 
26 | 	it('should bind static class field', () => {
27 | 		class Test {
28 | 			static foo = 1;
29 | 		}
30 | 
31 | 		assert.doesNotThrow(() => {
32 | 			new BindingTarget(Test, 'foo');
33 | 		});
34 | 	});
35 | 
36 | 	it('should determine class is bindable', () => {
37 | 		class Test {
38 | 			static foo = 1;
39 | 		}
40 | 
41 | 		assert.strictEqual(BindingTarget.isBindable(Test), true);
42 | 	});
43 | });
44 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/target.ts:
--------------------------------------------------------------------------------
 1 | import {TpError} from '../tp-error.js';
 2 | 
 3 | export type Bindable = Record<string, any>;
 4 | 
 5 | /**
 6 |  * A binding target.
 7 |  */
 8 | export class BindingTarget {
 9 | 	/**
10 | 	 * The property name of the binding.
11 | 	 */
12 | 	public readonly key: string;
13 | 	private readonly obj_: Bindable;
14 | 
15 | 	/**
16 | 	 * @hidden
17 | 	 */
18 | 	constructor(obj: Bindable, key: string) {
19 | 		this.obj_ = obj;
20 | 		this.key = key;
21 | 	}
22 | 
23 | 	public static isBindable(obj: unknown): obj is Bindable {
24 | 		if (obj === null) {
25 | 			return false;
26 | 		}
27 | 		if (typeof obj !== 'object' && typeof obj !== 'function') {
28 | 			return false;
29 | 		}
30 | 		return true;
31 | 	}
32 | 
33 | 	/**
34 | 	 * Read a bound value.
35 | 	 * @return A bound value
36 | 	 */
37 | 	public read(): unknown {
38 | 		return this.obj_[this.key];
39 | 	}
40 | 
41 | 	/**
42 | 	 * Write a value.
43 | 	 * @param value The value to write to the target.
44 | 	 */
45 | 	public write(value: unknown): void {
46 | 		this.obj_[this.key] = value;
47 | 	}
48 | 
49 | 	/**
50 | 	 * Write a value to the target property.
51 | 	 * @param name The property name.
52 | 	 * @param value The value to write to the target.
53 | 	 */
54 | 	public writeProperty(name: string, value: unknown): void {
55 | 		const valueObj = this.read();
56 | 
57 | 		if (!BindingTarget.isBindable(valueObj)) {
58 | 			throw TpError.notBindable();
59 | 		}
60 | 		if (!(name in valueObj)) {
61 | 			throw TpError.propertyNotFound(name);
62 | 		}
63 | 		valueObj[name] = value;
64 | 	}
65 | }
66 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/ticker/interval-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {createTestWindow} from '../../../misc/dom-test-util.js';
 5 | import {IntervalTicker} from './interval.js';
 6 | 
 7 | describe(IntervalTicker.name, () => {
 8 | 	it('should not create timer for negative interval', (done) => {
 9 | 		const doc = createTestWindow().document;
10 | 
11 | 		const t0 = new IntervalTicker(doc, 0);
12 | 		t0.emitter.on('tick', () => {
13 | 			assert.fail('should not be called');
14 | 		});
15 | 		const tn = new IntervalTicker(doc, -100);
16 | 		tn.emitter.on('tick', () => {
17 | 			assert.fail('should not be called');
18 | 		});
19 | 
20 | 		setTimeout(() => {
21 | 			done();
22 | 		}, 10);
23 | 	});
24 | 
25 | 	it('should tick', (done) => {
26 | 		const doc = createTestWindow().document;
27 | 		const t = new IntervalTicker(doc, 1);
28 | 
29 | 		t.emitter.on('tick', () => {
30 | 			t.dispose();
31 | 			done();
32 | 		});
33 | 	});
34 | 
35 | 	it('should be enabled by default', () => {
36 | 		const doc = createTestWindow().document;
37 | 		const t = new IntervalTicker(doc, 0);
38 | 
39 | 		assert.strictEqual(t.disabled, false);
40 | 	});
41 | 
42 | 	it('should not tick if disabled', (done) => {
43 | 		const doc = createTestWindow().document;
44 | 		const t = new IntervalTicker(doc, 1);
45 | 		t.disabled = true;
46 | 		t.emitter.on('tick', () => {
47 | 			assert.fail('should not called');
48 | 		});
49 | 
50 | 		setTimeout(done, 10);
51 | 	});
52 | });
53 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/ticker/manual-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {ManualTicker} from './manual.js';
 5 | 
 6 | describe(ManualTicker.name, () => {
 7 | 	it('should be enabled by default', () => {
 8 | 		const t = new ManualTicker();
 9 | 		assert.strictEqual(t.disabled, false);
10 | 	});
11 | 
12 | 	it('should fire tick event', () => {
13 | 		const t = new ManualTicker();
14 | 		let count = 0;
15 | 		t.emitter.on('tick', () => {
16 | 			count += 1;
17 | 		});
18 | 
19 | 		assert.strictEqual(count, 0);
20 | 		t.tick();
21 | 		assert.strictEqual(count, 1);
22 | 	});
23 | 
24 | 	it('should not fire tick event from disabled ticker', () => {
25 | 		const t = new ManualTicker();
26 | 		t.emitter.on('tick', () => {
27 | 			assert.fail('should not be called');
28 | 		});
29 | 
30 | 		t.disabled = true;
31 | 		assert.doesNotThrow(() => {
32 | 			t.tick();
33 | 		});
34 | 	});
35 | });
36 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/ticker/manual.ts:
--------------------------------------------------------------------------------
 1 | import {Emitter} from '../../model/emitter.js';
 2 | import {Ticker, TickerEvents} from './ticker.js';
 3 | 
 4 | /**
 5 |  * @hidden
 6 |  */
 7 | export class ManualTicker implements Ticker {
 8 | 	public readonly emitter: Emitter<TickerEvents>;
 9 | 	public disabled = false;
10 | 
11 | 	constructor() {
12 | 		this.emitter = new Emitter();
13 | 	}
14 | 
15 | 	public dispose(): void {}
16 | 
17 | 	public tick(): void {
18 | 		if (this.disabled) {
19 | 			return;
20 | 		}
21 | 
22 | 		this.emitter.emit('tick', {
23 | 			sender: this,
24 | 		});
25 | 	}
26 | }
27 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/ticker/ticker.ts:
--------------------------------------------------------------------------------
 1 | import {Emitter} from '../../model/emitter.js';
 2 | 
 3 | /**
 4 |  * @hidden
 5 |  */
 6 | export interface TickerEvents {
 7 | 	tick: {
 8 | 		sender: Ticker;
 9 | 	};
10 | }
11 | 
12 | /**
13 |  * @hidden
14 |  */
15 | export interface Ticker {
16 | 	readonly emitter: Emitter<TickerEvents>;
17 | 	disabled: boolean;
18 | 
19 | 	dispose(): void;
20 | }
21 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/value/binding.ts:
--------------------------------------------------------------------------------
 1 | import {isObject} from '../../../misc/type-util.js';
 2 | import {Value} from '../../model/value.js';
 3 | import {Binding, isBinding} from '../binding.js';
 4 | 
 5 | /**
 6 |  * @hidden
 7 |  */
 8 | export interface BindingValue<T> extends Value<T> {
 9 | 	readonly binding: Binding;
10 | 	fetch(): void;
11 | }
12 | 
13 | export function isBindingValue(v: unknown): v is BindingValue<unknown> {
14 | 	if (!isObject(v) || !('binding' in v)) {
15 | 		return false;
16 | 	}
17 | 	const b = (v as {binding: unknown}).binding;
18 | 	return isBinding(b);
19 | }
20 | 


--------------------------------------------------------------------------------
/packages/core/src/common/binding/value/input-binding-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {PrimitiveValue} from '../../model/primitive-value.js';
 5 | import {ReadWriteBinding} from '../read-write.js';
 6 | import {BindingTarget} from '../target.js';
 7 | import {InputBindingValue} from './input-binding.js';
 8 | 
 9 | describe(InputBindingValue.name, () => {
10 | 	it('should apply rawValue to target', () => {
11 | 		const iv = new PrimitiveValue(0);
12 | 		const target = new BindingTarget({foo: 0}, 'foo');
13 | 		const bv = new InputBindingValue(
14 | 			iv,
15 | 			new ReadWriteBinding({
16 | 				reader: (v: unknown) => Number(v),
17 | 				writer: (t, v) => t.write(v),
18 | 				target: target,
19 | 			}),
20 | 		);
21 | 
22 | 		bv.rawValue = 1;
23 | 		assert.strictEqual(target.read(), 1);
24 | 	});
25 | 
26 | 	it('should have its own sender', (done) => {
27 | 		const iv = new PrimitiveValue(0);
28 | 		const bv = new InputBindingValue(
29 | 			iv,
30 | 			new ReadWriteBinding({
31 | 				reader: (v: unknown) => Number(v),
32 | 				writer: (t, v) => t.write(v),
33 | 				target: new BindingTarget({foo: 0}, 'foo'),
34 | 			}),
35 | 		);
36 | 		bv.emitter.on('change', (ev) => {
37 | 			assert.strictEqual(ev.sender, bv);
38 | 			done();
39 | 		});
40 | 		iv.rawValue = 1;
41 | 	});
42 | });
43 | 


--------------------------------------------------------------------------------
/packages/core/src/common/compat.ts:
--------------------------------------------------------------------------------
 1 | import {Semver} from '../misc/semver.js';
 2 | import {VERSION} from '../version.js';
 3 | 
 4 | export function warnDeprecation(info: {
 5 | 	name: string;
 6 | 	alternative?: string;
 7 | 	postscript?: string;
 8 | }) {
 9 | 	console.warn(
10 | 		[
11 | 			`${info.name} is deprecated.`,
12 | 			info.alternative ? `use ${info.alternative} instead.` : '',
13 | 			info.postscript ?? '',
14 | 		].join(' '),
15 | 	);
16 | }
17 | 
18 | export function warnMissing(info: {
19 | 	key: string;
20 | 	target: string;
21 | 	place: string;
22 | }) {
23 | 	console.warn(
24 | 		[
25 | 			`Missing '${info.key}' of ${info.target} in ${info.place}.`,
26 | 			'Please rebuild plugins with the latest core package.',
27 | 		].join(' '),
28 | 	);
29 | }
30 | 
31 | export function isCompatible(ver: Semver | undefined): boolean {
32 | 	if (!ver) {
33 | 		// Version 1.x
34 | 		return false;
35 | 	}
36 | 	return ver.major === VERSION.major;
37 | }
38 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/composite.ts:
--------------------------------------------------------------------------------
 1 | import {Class} from '../../misc/type-util.js';
 2 | import {Constraint} from './constraint.js';
 3 | 
 4 | /**
 5 |  * A constraint to combine multiple constraints.
 6 |  * @template T The type of the value.
 7 |  */
 8 | export class CompositeConstraint<T> implements Constraint<T> {
 9 | 	public readonly constraints: Constraint<T>[];
10 | 
11 | 	constructor(constraints: Constraint<T>[]) {
12 | 		this.constraints = constraints;
13 | 	}
14 | 
15 | 	public constrain(value: T): T {
16 | 		return this.constraints.reduce((result, c) => {
17 | 			return c.constrain(result);
18 | 		}, value);
19 | 	}
20 | }
21 | 
22 | export function findConstraint<C>(
23 | 	c: Constraint<unknown>,
24 | 	constraintClass: Class<C>,
25 | ): C | null {
26 | 	if (c instanceof constraintClass) {
27 | 		return c;
28 | 	}
29 | 
30 | 	if (c instanceof CompositeConstraint) {
31 | 		const result = c.constraints.reduce((tmpResult: C | null, sc) => {
32 | 			if (tmpResult) {
33 | 				return tmpResult;
34 | 			}
35 | 
36 | 			return sc instanceof constraintClass ? sc : null;
37 | 		}, null);
38 | 		if (result) {
39 | 			return result;
40 | 		}
41 | 	}
42 | 
43 | 	return null;
44 | }
45 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/constraint.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * A constraint for the value.
 3 |  * @template T The type of the value.
 4 |  */
 5 | export interface Constraint<T> {
 6 | 	/**
 7 | 	 * Constrains the value.
 8 | 	 * @param value The value.
 9 | 	 * @return A constarined value.
10 | 	 */
11 | 	constrain(value: T): T;
12 | }
13 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/definite-range-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {DefiniteRangeConstraint} from './definite-range.js';
 5 | 
 6 | describe(DefiniteRangeConstraint.name, () => {
 7 | 	it('should constrain value with minimun and maximum value', () => {
 8 | 		const c = new DefiniteRangeConstraint({
 9 | 			max: 123,
10 | 			min: -123,
11 | 		});
12 | 		assert.strictEqual(c.constrain(-124), -123);
13 | 		assert.strictEqual(c.constrain(0), 0);
14 | 		assert.strictEqual(c.constrain(124), 123);
15 | 	});
16 | 
17 | 	it('should constrain value with updated range', () => {
18 | 		const c = new DefiniteRangeConstraint({
19 | 			max: 1,
20 | 			min: 0,
21 | 		});
22 | 		c.values.set('min', -10);
23 | 		c.values.set('max', 10);
24 | 
25 | 		assert.strictEqual(c.constrain(-100), -10);
26 | 		assert.strictEqual(c.constrain(100), 10);
27 | 	});
28 | });
29 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/definite-range.ts:
--------------------------------------------------------------------------------
 1 | import {ValueMap} from '../model/value-map.js';
 2 | import {Constraint} from './constraint.js';
 3 | 
 4 | interface Config {
 5 | 	max: number;
 6 | 	min: number;
 7 | }
 8 | 
 9 | /**
10 |  * A number range constraint that cannot be undefined. Used for slider control.
11 |  */
12 | export class DefiniteRangeConstraint implements Constraint<number> {
13 | 	public readonly values: ValueMap<{
14 | 		max: number;
15 | 		min: number;
16 | 	}>;
17 | 
18 | 	constructor(config: Config) {
19 | 		this.values = ValueMap.fromObject({
20 | 			max: config.max,
21 | 			min: config.min,
22 | 		});
23 | 	}
24 | 
25 | 	public constrain(value: number): number {
26 | 		const max = this.values.get('max');
27 | 		const min = this.values.get('min');
28 | 		return Math.min(Math.max(value, min), max);
29 | 	}
30 | }
31 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/list-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {ListConstraint} from './list.js';
 5 | 
 6 | describe(ListConstraint.name, () => {
 7 | 	it('should get list options', () => {
 8 | 		const options = [
 9 | 			{text: 'foo', value: 1.41},
10 | 			{text: 'bar', value: 2.72},
11 | 			{text: 'baz', value: 3.14},
12 | 		];
13 | 		const c = new ListConstraint(options);
14 | 		assert.deepStrictEqual(options, c.values.get('options'));
15 | 	});
16 | 
17 | 	it('should constrain value with list options', () => {
18 | 		const c = new ListConstraint([
19 | 			{text: 'foo', value: 1.41},
20 | 			{text: 'bar', value: 2.72},
21 | 			{text: 'baz', value: 3.14},
22 | 		]);
23 | 		assert.strictEqual(c.constrain(2.72), 2.72);
24 | 	});
25 | 
26 | 	it('should not constrain value without list options', () => {
27 | 		const c = new ListConstraint([]);
28 | 		assert.strictEqual(c.constrain(3.14), 3.14);
29 | 	});
30 | 
31 | 	it('should constrain an invalid value with list options', () => {
32 | 		const c = new ListConstraint([
33 | 			{text: 'foo', value: 1.41},
34 | 			{text: 'bar', value: 2.72},
35 | 			{text: 'baz', value: 3.14},
36 | 		]);
37 | 		assert.strictEqual(c.constrain(9.81), 1.41);
38 | 	});
39 | });
40 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/list.ts:
--------------------------------------------------------------------------------
 1 | import {ValueMap} from '../model/value-map.js';
 2 | import {Constraint} from './constraint.js';
 3 | 
 4 | export interface ListItem<T> {
 5 | 	text: string;
 6 | 	value: T;
 7 | }
 8 | 
 9 | /**
10 |  * A list constranit.
11 |  * @template T The type of the value.
12 |  */
13 | export class ListConstraint<T> implements Constraint<T> {
14 | 	public readonly values: ValueMap<{
15 | 		options: ListItem<T>[];
16 | 	}>;
17 | 
18 | 	constructor(options: ListItem<T>[]) {
19 | 		this.values = ValueMap.fromObject({
20 | 			options: options,
21 | 		});
22 | 	}
23 | 
24 | 	public constrain(value: T): T {
25 | 		const opts = this.values.get('options');
26 | 
27 | 		if (opts.length === 0) {
28 | 			return value;
29 | 		}
30 | 
31 | 		const matched =
32 | 			opts.filter((item) => {
33 | 				return item.value === value;
34 | 			}).length > 0;
35 | 
36 | 		return matched ? value : opts[0].value;
37 | 	}
38 | }
39 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/range-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {RangeConstraint} from './range.js';
 5 | 
 6 | describe(RangeConstraint.name, () => {
 7 | 	it('should constrain value with minimum value', () => {
 8 | 		const c = new RangeConstraint({
 9 | 			min: -10,
10 | 		});
11 | 		assert.strictEqual(c.values.get('min'), -10);
12 | 		assert.strictEqual(c.constrain(-11), -10);
13 | 		assert.strictEqual(c.constrain(-10), -10);
14 | 		assert.strictEqual(c.constrain(-9), -9);
15 | 	});
16 | 
17 | 	it('should constrain value with maximum value', () => {
18 | 		const c = new RangeConstraint({
19 | 			max: 123,
20 | 		});
21 | 		assert.strictEqual(c.values.get('max'), 123);
22 | 		assert.strictEqual(c.constrain(122), 122);
23 | 		assert.strictEqual(c.constrain(123), 123);
24 | 		assert.strictEqual(c.constrain(123.5), 123);
25 | 	});
26 | 
27 | 	it('should constrain value with minimun and maximum value', () => {
28 | 		const c = new RangeConstraint({
29 | 			max: 123,
30 | 			min: -123,
31 | 		});
32 | 		assert.strictEqual(c.constrain(-124), -123);
33 | 		assert.strictEqual(c.constrain(0), 0);
34 | 		assert.strictEqual(c.constrain(124), 123);
35 | 	});
36 | });
37 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/range.ts:
--------------------------------------------------------------------------------
 1 | import {isEmpty} from '../../misc/type-util.js';
 2 | import {ValueMap} from '../model/value-map.js';
 3 | import {Constraint} from './constraint.js';
 4 | 
 5 | interface Config {
 6 | 	max?: number;
 7 | 	min?: number;
 8 | }
 9 | 
10 | /**
11 |  * A number range constraint.
12 |  */
13 | export class RangeConstraint implements Constraint<number> {
14 | 	public readonly values: ValueMap<{
15 | 		max: number | undefined;
16 | 		min: number | undefined;
17 | 	}>;
18 | 
19 | 	constructor(config: Config) {
20 | 		this.values = ValueMap.fromObject({
21 | 			max: config.max,
22 | 			min: config.min,
23 | 		});
24 | 	}
25 | 
26 | 	public constrain(value: number): number {
27 | 		const max = this.values.get('max');
28 | 		const min = this.values.get('min');
29 | 
30 | 		let result = value;
31 | 		if (!isEmpty(min)) {
32 | 			result = Math.max(result, min);
33 | 		}
34 | 		if (!isEmpty(max)) {
35 | 			result = Math.min(result, max);
36 | 		}
37 | 		return result;
38 | 	}
39 | }
40 | 


--------------------------------------------------------------------------------
/packages/core/src/common/constraint/step.ts:
--------------------------------------------------------------------------------
 1 | import {Constraint} from './constraint.js';
 2 | 
 3 | /**
 4 |  * A number step range constraint.
 5 |  */
 6 | export class StepConstraint implements Constraint<number> {
 7 | 	public readonly step: number;
 8 | 	public readonly origin: number;
 9 | 
10 | 	constructor(step: number, origin = 0) {
11 | 		this.step = step;
12 | 		this.origin = origin;
13 | 	}
14 | 
15 | 	public constrain(value: number): number {
16 | 		const o = this.origin % this.step;
17 | 		const r = Math.round((value - o) / this.step);
18 | 		return o + r * this.step;
19 | 	}
20 | }
21 | 


--------------------------------------------------------------------------------
/packages/core/src/common/controller/controller.ts:
--------------------------------------------------------------------------------
 1 | import {ViewProps} from '../model/view-props.js';
 2 | import {View} from '../view/view.js';
 3 | 
 4 | /**
 5 |  * A controller that has a view to control.
 6 |  */
 7 | export interface Controller<V extends View = View> {
 8 | 	readonly view: V;
 9 | 	readonly viewProps: ViewProps;
10 | }
11 | 


--------------------------------------------------------------------------------
/packages/core/src/common/controller/popup.ts:
--------------------------------------------------------------------------------
 1 | import {Value} from '../model/value.js';
 2 | import {createValue} from '../model/values.js';
 3 | import {ViewProps} from '../model/view-props.js';
 4 | import {PopupView} from '../view/popup.js';
 5 | import {Controller} from './controller.js';
 6 | 
 7 | interface Config {
 8 | 	viewProps: ViewProps;
 9 | }
10 | 
11 | export class PopupController implements Controller<PopupView> {
12 | 	public readonly shows: Value<boolean> = createValue<boolean>(false);
13 | 	public readonly view: PopupView;
14 | 	public readonly viewProps: ViewProps;
15 | 
16 | 	constructor(doc: Document, config: Config) {
17 | 		this.viewProps = config.viewProps;
18 | 		this.view = new PopupView(doc, {
19 | 			shows: this.shows,
20 | 			viewProps: this.viewProps,
21 | 		});
22 | 	}
23 | }
24 | 


--------------------------------------------------------------------------------
/packages/core/src/common/controller/text.ts:
--------------------------------------------------------------------------------
 1 | import {forceCast, isEmpty} from '../../misc/type-util.js';
 2 | import {Parser} from '../converter/parser.js';
 3 | import {Value} from '../model/value.js';
 4 | import {ViewProps} from '../model/view-props.js';
 5 | import {TextProps, TextView} from '../view/text.js';
 6 | import {ValueController} from './value.js';
 7 | 
 8 | /**
 9 |  * @hidden
10 |  */
11 | export interface Config<T> {
12 | 	props: TextProps<T>;
13 | 	parser: Parser<T>;
14 | 	value: Value<T>;
15 | 	viewProps: ViewProps;
16 | }
17 | 
18 | /**
19 |  * @hidden
20 |  */
21 | export class TextController<T> implements ValueController<T, TextView<T>> {
22 | 	public readonly props: TextProps<T>;
23 | 	public readonly value: Value<T>;
24 | 	public readonly view: TextView<T>;
25 | 	public readonly viewProps: ViewProps;
26 | 	private readonly parser_: Parser<T>;
27 | 
28 | 	constructor(doc: Document, config: Config<T>) {
29 | 		this.onInputChange_ = this.onInputChange_.bind(this);
30 | 
31 | 		this.parser_ = config.parser;
32 | 		this.props = config.props;
33 | 		this.value = config.value;
34 | 		this.viewProps = config.viewProps;
35 | 
36 | 		this.view = new TextView(doc, {
37 | 			props: config.props,
38 | 			value: this.value,
39 | 			viewProps: this.viewProps,
40 | 		});
41 | 		this.view.inputElement.addEventListener('change', this.onInputChange_);
42 | 	}
43 | 
44 | 	private onInputChange_(e: Event): void {
45 | 		const inputElem: HTMLInputElement = forceCast(e.currentTarget);
46 | 		const value = inputElem.value;
47 | 
48 | 		const parsedValue = this.parser_(value);
49 | 		if (!isEmpty(parsedValue)) {
50 | 			this.value.rawValue = parsedValue;
51 | 		}
52 | 		this.view.refresh();
53 | 	}
54 | }
55 | 


--------------------------------------------------------------------------------
/packages/core/src/common/controller/value.ts:
--------------------------------------------------------------------------------
 1 | import {Value} from '../model/value.js';
 2 | import {View} from '../view/view.js';
 3 | import {Controller} from './controller.js';
 4 | 
 5 | export interface ValueController<
 6 | 	T,
 7 | 	Vw extends View = View,
 8 | 	Va extends Value<T> = Value<T>,
 9 | > extends Controller<Vw> {
10 | 	readonly value: Va;
11 | }
12 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/boolean-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {boolFromUnknown, boolToString} from './boolean.js';
 5 | 
 6 | describe('booleanConverter', () => {
 7 | 	it('should convert boolean to string', () => {
 8 | 		assert.strictEqual(boolToString(true), 'true');
 9 | 		assert.strictEqual(boolToString(false), 'false');
10 | 	});
11 | 
12 | 	[
13 | 		{
14 | 			arg: true,
15 | 			expected: true,
16 | 		},
17 | 		{
18 | 			arg: false,
19 | 			expected: false,
20 | 		},
21 | 		{
22 | 			arg: 'false',
23 | 			expected: false,
24 | 		},
25 | 	].forEach((testCase) => {
26 | 		context(`when ${JSON.stringify(testCase.arg)}`, () => {
27 | 			it(`should convert to ${String(testCase.expected)}`, () => {
28 | 				assert.strictEqual(boolFromUnknown(testCase.arg), testCase.expected);
29 | 			});
30 | 		});
31 | 	});
32 | });
33 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/boolean.ts:
--------------------------------------------------------------------------------
 1 | export function boolToString(value: boolean): string {
 2 | 	return String(value);
 3 | }
 4 | 
 5 | export function boolFromUnknown(value: unknown): boolean {
 6 | 	if (value === 'false') {
 7 | 		return false;
 8 | 	}
 9 | 	return !!value;
10 | }
11 | 
12 | export function BooleanFormatter(value: boolean): string {
13 | 	return boolToString(value);
14 | }
15 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/formatter.ts:
--------------------------------------------------------------------------------
1 | export type Formatter<T> = (value: T) => string;
2 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/number.ts:
--------------------------------------------------------------------------------
 1 | import {isEmpty} from '../../misc/type-util.js';
 2 | import {parseEcmaNumberExpression} from './ecma/parser.js';
 3 | import {Formatter} from './formatter.js';
 4 | 
 5 | export function parseNumber(text: string): number | null {
 6 | 	const r = parseEcmaNumberExpression(text);
 7 | 	return r?.evaluate() ?? null;
 8 | }
 9 | 
10 | export function numberFromUnknown(value: unknown): number {
11 | 	if (typeof value === 'number') {
12 | 		return value;
13 | 	}
14 | 
15 | 	if (typeof value === 'string') {
16 | 		const pv = parseNumber(value);
17 | 		if (!isEmpty(pv)) {
18 | 			return pv;
19 | 		}
20 | 	}
21 | 
22 | 	return 0;
23 | }
24 | 
25 | export function numberToString(value: number): string {
26 | 	return String(value);
27 | }
28 | 
29 | export function createNumberFormatter(digits: number): Formatter<number> {
30 | 	return (value: number): string => {
31 | 		// toFixed() of Safari doesn't support digits greater than 20
32 | 		// https://github.com/cocopon/tweakpane/pull/19
33 | 		return value.toFixed(Math.max(Math.min(digits, 20), 0));
34 | 	};
35 | }
36 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/parser-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {composeParsers} from './parser.js';
 5 | 
 6 | describe(composeParsers.name, () => {
 7 | 	it('should use the first parser', () => {
 8 | 		const p = composeParsers<number>([
 9 | 			(t) => parseInt(t) * 10,
10 | 			(t) => parseInt(t) * 100,
11 | 		]);
12 | 		assert.strictEqual(p('123'), 1230);
13 | 	});
14 | 
15 | 	it('should delegate a value to the next parser', () => {
16 | 		const p = composeParsers<number>([(_) => null, (t) => parseInt(t) * 100]);
17 | 		assert.strictEqual(p('123'), 12300);
18 | 	});
19 | });
20 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/parser.ts:
--------------------------------------------------------------------------------
 1 | export type Parser<T> = (text: string) => T | null;
 2 | 
 3 | export function composeParsers<T>(parsers: Parser<T>[]): Parser<T> {
 4 | 	return (text) => {
 5 | 		return parsers.reduce((result: T | null, parser) => {
 6 | 			if (result !== null) {
 7 | 				return result;
 8 | 			}
 9 | 			return parser(text);
10 | 		}, null);
11 | 	};
12 | }
13 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/percentage-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {formatPercentage} from './percentage.js';
 5 | 
 6 | describe('converter/percentage', () => {
 7 | 	[
 8 | 		{
 9 | 			expected: '0%',
10 | 			params: {
11 | 				value: 0,
12 | 			},
13 | 		},
14 | 		{
15 | 			expected: '12%',
16 | 			params: {
17 | 				value: 12,
18 | 			},
19 | 		},
20 | 		{
21 | 			expected: '100%',
22 | 			params: {
23 | 				value: 100,
24 | 			},
25 | 		},
26 | 	].forEach((testCase) => {
27 | 		context(`when ${JSON.stringify(testCase.params)}`, () => {
28 | 			it(`it should format to ${JSON.stringify(testCase.expected)}`, () => {
29 | 				assert.strictEqual(
30 | 					formatPercentage(testCase.params.value),
31 | 					testCase.expected,
32 | 				);
33 | 			});
34 | 		});
35 | 	});
36 | });
37 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/percentage.ts:
--------------------------------------------------------------------------------
1 | import {createNumberFormatter} from './number.js';
2 | 
3 | const innerFormatter = createNumberFormatter(0);
4 | 
5 | export function formatPercentage(value: number): string {
6 | 	return innerFormatter(value) + '%';
7 | }
8 | 


--------------------------------------------------------------------------------
/packages/core/src/common/converter/string.ts:
--------------------------------------------------------------------------------
1 | export function stringFromUnknown(value: unknown): string {
2 | 	return String(value);
3 | }
4 | 
5 | export function formatString(value: string): string {
6 | 	return value;
7 | }
8 | 


--------------------------------------------------------------------------------
/packages/core/src/common/dom-util-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {createTestWindow} from '../misc/dom-test-util.js';
 5 | import {indexOfChildElement, removeElement} from './dom-util.js';
 6 | 
 7 | describe('DomUtil', () => {
 8 | 	it('should get index of child element', () => {
 9 | 		const w = createTestWindow();
10 | 		const parent = w.document.createElement('div');
11 | 		const child = w.document.createElement('div');
12 | 		parent.appendChild(child);
13 | 
14 | 		removeElement(child);
15 | 		assert.strictEqual(child.parentElement, null);
16 | 	});
17 | 	it('should get index of child element', () => {
18 | 		const w = createTestWindow();
19 | 		const parent = w.document.createElement('div');
20 | 		parent.appendChild(w.document.createElement('div'));
21 | 		parent.appendChild(w.document.createElement('div'));
22 | 		const child = w.document.createElement('div');
23 | 		parent.appendChild(child);
24 | 		parent.appendChild(w.document.createElement('div'));
25 | 
26 | 		assert.strictEqual(indexOfChildElement(child), 2);
27 | 	});
28 | 
29 | 	it('should return negative index if not found', () => {
30 | 		const w = createTestWindow();
31 | 		const parent = w.document.createElement('div');
32 | 		parent.appendChild(w.document.createElement('div'));
33 | 		parent.appendChild(w.document.createElement('div'));
34 | 
35 | 		const elem = w.document.createElement('div');
36 | 		assert.strictEqual(indexOfChildElement(elem), -1);
37 | 	});
38 | });
39 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/buffered-value-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe as context, describe, it} from 'mocha';
 3 | 
 4 | import {createPushedBuffer, initializeBuffer} from './buffered-value.js';
 5 | 
 6 | describe(initializeBuffer.name, () => {
 7 | 	[
 8 | 		{
 9 | 			expected: [undefined],
10 | 			params: {
11 | 				bufferSize: 1,
12 | 			},
13 | 		},
14 | 		{
15 | 			expected: [undefined, undefined, undefined, undefined],
16 | 			params: {
17 | 				bufferSize: 4,
18 | 			},
19 | 		},
20 | 	].forEach(({expected, params}) => {
21 | 		context(`when ${JSON.stringify(params)}`, () => {
22 | 			it('should initialize buffer', () => {
23 | 				assert.deepStrictEqual(initializeBuffer(params.bufferSize), expected);
24 | 			});
25 | 		});
26 | 	});
27 | });
28 | 
29 | describe(createPushedBuffer.name, () => {
30 | 	[
31 | 		{
32 | 			expected: [0, 1, undefined],
33 | 			params: {
34 | 				buffer: [0, undefined, undefined],
35 | 				newValue: 1,
36 | 			},
37 | 		},
38 | 		{
39 | 			expected: [1, 2, 3, 4],
40 | 			params: {
41 | 				buffer: [0, 1, 2, 3],
42 | 				newValue: 4,
43 | 			},
44 | 		},
45 | 	].forEach(({expected, params}) => {
46 | 		context(`when ${JSON.stringify(params)}`, () => {
47 | 			it('should create pushed buffer', () => {
48 | 				assert.deepStrictEqual(
49 | 					createPushedBuffer(params.buffer, params.newValue),
50 | 					expected,
51 | 				);
52 | 			});
53 | 		});
54 | 	});
55 | });
56 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/buffered-value.ts:
--------------------------------------------------------------------------------
 1 | import {forceCast} from '../../misc/type-util.js';
 2 | import {Value, ValueEvents} from './value.js';
 3 | 
 4 | /**
 5 |  * A buffer. Prefixed to avoid conflicts with the Node.js built-in class.
 6 |  * @template T
 7 |  */
 8 | export type TpBuffer<T> = (T | undefined)[];
 9 | export type BufferedValue<T> = Value<TpBuffer<T>>;
10 | export type BufferedValueEvents<T> = ValueEvents<TpBuffer<T>>;
11 | 
12 | function fillBuffer<T>(buffer: TpBuffer<T>, bufferSize: number) {
13 | 	while (buffer.length < bufferSize) {
14 | 		buffer.push(undefined);
15 | 	}
16 | }
17 | 
18 | export function initializeBuffer<T>(bufferSize: number): TpBuffer<T> {
19 | 	const buffer: TpBuffer<T> = [];
20 | 	fillBuffer(buffer, bufferSize);
21 | 	return buffer;
22 | }
23 | 
24 | function createTrimmedBuffer<T>(buffer: TpBuffer<T>): T[] {
25 | 	const index = buffer.indexOf(undefined);
26 | 	return forceCast(index < 0 ? buffer : buffer.slice(0, index));
27 | }
28 | 
29 | export function createPushedBuffer<T>(
30 | 	buffer: TpBuffer<T>,
31 | 	newValue: T,
32 | ): TpBuffer<T> {
33 | 	const newBuffer = [...createTrimmedBuffer(buffer), newValue];
34 | 
35 | 	if (newBuffer.length > buffer.length) {
36 | 		newBuffer.splice(0, newBuffer.length - buffer.length);
37 | 	} else {
38 | 		fillBuffer(newBuffer, buffer.length);
39 | 	}
40 | 
41 | 	return newBuffer;
42 | }
43 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/primitive-value-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {PrimitiveValue} from './primitive-value.js';
 5 | 
 6 | describe(PrimitiveValue.name, () => {
 7 | 	it('should get raw value', () => {
 8 | 		const v = new PrimitiveValue(123);
 9 | 		assert.strictEqual(v.rawValue, 123);
10 | 	});
11 | 
12 | 	it('should set raw value', () => {
13 | 		const v = new PrimitiveValue(0);
14 | 		v.rawValue = 456;
15 | 		assert.strictEqual(v.rawValue, 456);
16 | 	});
17 | 
18 | 	it('should emit change event', (done) => {
19 | 		const v = new PrimitiveValue(1);
20 | 		v.emitter.on('change', (ev) => {
21 | 			assert.strictEqual(v.rawValue, 2);
22 | 			assert.strictEqual(ev.previousRawValue, 1);
23 | 			assert.strictEqual(ev.rawValue, 2);
24 | 			assert.strictEqual(ev.sender, v);
25 | 			done();
26 | 		});
27 | 		v.rawValue = 2;
28 | 	});
29 | 
30 | 	it('should emit beforechange event', (done) => {
31 | 		const v = new PrimitiveValue(1);
32 | 		let count = 0;
33 | 
34 | 		v.emitter.on('beforechange', (ev) => {
35 | 			assert.strictEqual(v.rawValue, 1);
36 | 			assert.strictEqual(ev.sender, v);
37 | 			assert.strictEqual(count, 0);
38 | 			count += 1;
39 | 		});
40 | 		v.emitter.on('change', () => {
41 | 			assert.strictEqual(count, 1);
42 | 			done();
43 | 		});
44 | 
45 | 		v.rawValue = 2;
46 | 	});
47 | });
48 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/primitive-value.ts:
--------------------------------------------------------------------------------
 1 | import {Emitter} from './emitter.js';
 2 | import {Value, ValueChangeOptions, ValueEvents} from './value.js';
 3 | 
 4 | /**
 5 |  * A value that has a primitive raw value.
 6 |  * @template T the type of the raw value.
 7 |  */
 8 | export class PrimitiveValue<T> implements Value<T> {
 9 | 	public readonly emitter: Emitter<ValueEvents<T>>;
10 | 	private value_: T;
11 | 
12 | 	constructor(initialValue: T) {
13 | 		this.emitter = new Emitter();
14 | 		this.value_ = initialValue;
15 | 	}
16 | 
17 | 	get rawValue(): T {
18 | 		return this.value_;
19 | 	}
20 | 
21 | 	set rawValue(value: T) {
22 | 		this.setRawValue(value, {
23 | 			forceEmit: false,
24 | 			last: true,
25 | 		});
26 | 	}
27 | 
28 | 	public setRawValue(value: T, options?: ValueChangeOptions): void {
29 | 		const opts = options ?? {
30 | 			forceEmit: false,
31 | 			last: true,
32 | 		};
33 | 
34 | 		const prevValue = this.value_;
35 | 		if (prevValue === value && !opts.forceEmit) {
36 | 			return;
37 | 		}
38 | 
39 | 		this.emitter.emit('beforechange', {
40 | 			sender: this,
41 | 		});
42 | 
43 | 		this.value_ = value;
44 | 
45 | 		this.emitter.emit('change', {
46 | 			options: opts,
47 | 			previousRawValue: prevValue,
48 | 			rawValue: this.value_,
49 | 			sender: this,
50 | 		});
51 | 	}
52 | }
53 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/reactive.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	ReadonlyValue,
 3 | 	ReadonlyValueEvents,
 4 | 	Value,
 5 | 	ValueEvents,
 6 | } from '../model/value.js';
 7 | import {ValueMap} from '../model/value-map.js';
 8 | 
 9 | export function bindValue<T>(
10 | 	value: Value<T> | ReadonlyValue<T>,
11 | 	applyValue: (value: T) => void,
12 | ) {
13 | 	value.emitter.on(
14 | 		'change',
15 | 		(ev: ValueEvents<T>['change'] | ReadonlyValueEvents<T>['change']) => {
16 | 			applyValue(ev.rawValue);
17 | 		},
18 | 	);
19 | 	applyValue(value.rawValue);
20 | }
21 | 
22 | export function bindValueMap<
23 | 	O extends Record<string, unknown>,
24 | 	Key extends keyof O,
25 | >(valueMap: ValueMap<O>, key: Key, applyValue: (value: O[Key]) => void) {
26 | 	bindValue(valueMap.value(key), applyValue);
27 | }
28 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/readonly-primitive-value.ts:
--------------------------------------------------------------------------------
 1 | import {Emitter} from './emitter.js';
 2 | import {
 3 | 	ReadonlyValue,
 4 | 	ReadonlyValueEvents,
 5 | 	Value,
 6 | 	ValueEvents,
 7 | } from './value.js';
 8 | 
 9 | /**
10 |  * @hidden
11 |  */
12 | export class ReadonlyPrimitiveValue<T> implements ReadonlyValue<T> {
13 | 	/**
14 | 	 * The event emitter for value changes.
15 | 	 */
16 | 	public readonly emitter: Emitter<ReadonlyValueEvents<T>> = new Emitter();
17 | 	private value_: Value<T>;
18 | 
19 | 	constructor(value: Value<T>) {
20 | 		this.onValueBeforeChange_ = this.onValueBeforeChange_.bind(this);
21 | 		this.onValueChange_ = this.onValueChange_.bind(this);
22 | 
23 | 		this.value_ = value;
24 | 		this.value_.emitter.on('beforechange', this.onValueBeforeChange_);
25 | 		this.value_.emitter.on('change', this.onValueChange_);
26 | 	}
27 | 
28 | 	/**
29 | 	 * The raw value of the model.
30 | 	 */
31 | 	get rawValue(): T {
32 | 		return this.value_.rawValue;
33 | 	}
34 | 
35 | 	private onValueBeforeChange_(ev: ValueEvents<T>['beforechange']): void {
36 | 		this.emitter.emit('beforechange', {
37 | 			...ev,
38 | 			sender: this,
39 | 		});
40 | 	}
41 | 
42 | 	private onValueChange_(ev: ValueEvents<T>['change']): void {
43 | 		this.emitter.emit('change', {
44 | 			...ev,
45 | 			sender: this,
46 | 		});
47 | 	}
48 | }
49 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/test-util.ts:
--------------------------------------------------------------------------------
1 | import {InputBindingValue} from '../binding/value/input-binding.js';
2 | import {Value} from './value.js';
3 | 
4 | export function getBoundValue<T>(v: InputBindingValue<T>): Value<T> {
5 | 	return (v as any).value_;
6 | }
7 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/value-map-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe} from 'mocha';
 3 | 
 4 | import {ValueMap} from './value-map.js';
 5 | 
 6 | describe(ValueMap.name, () => {
 7 | 	it('should get initial value', () => {
 8 | 		const m = ValueMap.fromObject({
 9 | 			foo: 'bar',
10 | 			baz: 'qux',
11 | 		});
12 | 		assert.strictEqual(m.get('foo'), 'bar');
13 | 		assert.strictEqual(m.get('baz'), 'qux');
14 | 	});
15 | 
16 | 	it('should set value', () => {
17 | 		const m = ValueMap.fromObject({
18 | 			foo: 'bar',
19 | 		});
20 | 		m.set('foo', 'baz');
21 | 		assert.strictEqual(m.get('foo'), 'baz');
22 | 	});
23 | 
24 | 	it('should fire change event', (done) => {
25 | 		const m = ValueMap.fromObject({
26 | 			foo: 'bar',
27 | 			baz: 'qux',
28 | 		});
29 | 
30 | 		m.emitter.on('change', (ev) => {
31 | 			assert.strictEqual(ev.key, 'baz');
32 | 			assert.strictEqual(m.get('baz'), 'changed');
33 | 			done();
34 | 		});
35 | 		m.set('baz', 'changed');
36 | 	});
37 | 
38 | 	it('should not fire change event when setting the same value', () => {
39 | 		const m = ValueMap.fromObject({
40 | 			foo: 'bar',
41 | 		});
42 | 
43 | 		m.emitter.on('change', () => {
44 | 			assert.fail('should not be called');
45 | 		});
46 | 
47 | 		assert.doesNotThrow(() => {
48 | 			m.set('foo', 'bar');
49 | 		});
50 | 	});
51 | 
52 | 	it('should return signle value emitter', (done) => {
53 | 		const m = ValueMap.fromObject({
54 | 			foo: 'bar',
55 | 			baz: 'qux',
56 | 		});
57 | 
58 | 		m.value('baz').emitter.on('change', (ev) => {
59 | 			assert.strictEqual(ev.rawValue, 'changed');
60 | 			assert.strictEqual(m.get('baz'), 'changed');
61 | 			done();
62 | 		});
63 | 		m.set('baz', 'changed');
64 | 	});
65 | });
66 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/value-sync-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {Point2d} from '../../input-binding/point-2d/model/point-2d.js';
 5 | import {Constraint} from '../constraint/constraint.js';
 6 | import {connectValues} from './value-sync.js';
 7 | import {createValue} from './values.js';
 8 | 
 9 | describe(connectValues.name, () => {
10 | 	it('should set initial value', () => {
11 | 		const pv = createValue(new Point2d(1, 2));
12 | 		const sv = createValue(0);
13 | 		connectValues({
14 | 			primary: pv,
15 | 			secondary: sv,
16 | 
17 | 			forward: (p) => {
18 | 				return p.x;
19 | 			},
20 | 			backward: (p, s) => {
21 | 				const comps = p.getComponents();
22 | 				comps[0] = s;
23 | 				return new Point2d(...comps);
24 | 			},
25 | 		});
26 | 		assert.strictEqual(sv.rawValue, 1);
27 | 	});
28 | 
29 | 	it('should apply constraint of primary value', () => {
30 | 		class TestConstraint implements Constraint<Point2d> {
31 | 			constrain(): Point2d {
32 | 				// Secondary value will be changed by constraint of primary value
33 | 				return new Point2d(-1, -1);
34 | 			}
35 | 		}
36 | 
37 | 		const pv = createValue(new Point2d(1, 2), {
38 | 			constraint: new TestConstraint(),
39 | 		});
40 | 
41 | 		const sv = createValue(0);
42 | 		connectValues({
43 | 			primary: pv,
44 | 			secondary: sv,
45 | 
46 | 			forward: (p) => p.x,
47 | 			backward: (p, s) => {
48 | 				const comps = p.getComponents();
49 | 				comps[0] = s;
50 | 				return new Point2d(...comps);
51 | 			},
52 | 		});
53 | 
54 | 		// Try to change secondary value
55 | 		sv.rawValue = 10;
56 | 		// ...and it should be updated by primary constraint
57 | 
58 | 		assert.strictEqual(sv.rawValue, -1);
59 | 	});
60 | });
61 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/value-sync.ts:
--------------------------------------------------------------------------------
 1 | import {Value} from './value.js';
 2 | 
 3 | /**
 4 |  * Synchronizes two values.
 5 |  */
 6 | export function connectValues<T1, T2>({
 7 | 	primary,
 8 | 	secondary,
 9 | 	forward,
10 | 	backward,
11 | }: {
12 | 	primary: Value<T1>;
13 | 	secondary: Value<T2>;
14 | 	forward: (primary: T1, secondary: T2) => T2;
15 | 	backward: (primary: T1, secondary: T2) => T1;
16 | }) {
17 | 	// Prevents an event firing loop
18 | 	// e.g.
19 | 	// primary changed
20 | 	// -> applies changes to secondary
21 | 	// -> secondary changed
22 | 	// -> applies changes to primary
23 | 	// -> ...
24 | 	let changing = false;
25 | 	function preventFeedback(callback: () => void) {
26 | 		if (changing) {
27 | 			return;
28 | 		}
29 | 		changing = true;
30 | 		callback();
31 | 		changing = false;
32 | 	}
33 | 
34 | 	primary.emitter.on('change', (ev) => {
35 | 		preventFeedback(() => {
36 | 			secondary.setRawValue(
37 | 				forward(primary.rawValue, secondary.rawValue),
38 | 				ev.options,
39 | 			);
40 | 		});
41 | 	});
42 | 	secondary.emitter.on('change', (ev) => {
43 | 		preventFeedback(() => {
44 | 			primary.setRawValue(
45 | 				backward(primary.rawValue, secondary.rawValue),
46 | 				ev.options,
47 | 			);
48 | 		});
49 | 
50 | 		// Re-update secondary value
51 | 		// to apply change from constraint of primary value
52 | 		preventFeedback(() => {
53 | 			secondary.setRawValue(
54 | 				forward(primary.rawValue, secondary.rawValue),
55 | 				ev.options,
56 | 			);
57 | 		});
58 | 	});
59 | 
60 | 	preventFeedback(() => {
61 | 		secondary.setRawValue(forward(primary.rawValue, secondary.rawValue), {
62 | 			forceEmit: false,
63 | 			last: true,
64 | 		});
65 | 	});
66 | }
67 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/value.ts:
--------------------------------------------------------------------------------
 1 | import {Emitter} from './emitter.js';
 2 | 
 3 | export interface ValueChangeOptions {
 4 | 	/**
 5 | 	 * The flag indicating whether an event should be fired even if the value doesn't change.
 6 | 	 */
 7 | 	forceEmit: boolean;
 8 | 	/**
 9 | 	 * The flag indicating whether the event is for the last change.
10 | 	 */
11 | 	last: boolean;
12 | }
13 | 
14 | export interface ValueEvents<T, V = Value<T>> {
15 | 	beforechange: {
16 | 		sender: V;
17 | 	};
18 | 	change: {
19 | 		options: ValueChangeOptions;
20 | 		previousRawValue: T;
21 | 		rawValue: T;
22 | 		sender: V;
23 | 	};
24 | }
25 | 
26 | /**
27 |  * A value that handles changes.
28 |  * @template T The type of the raw value.
29 |  */
30 | export interface Value<T> {
31 | 	/**
32 | 	 * The event emitter for value changes.
33 | 	 */
34 | 	readonly emitter: Emitter<ValueEvents<T>>;
35 | 	/**
36 | 	 * The raw value of the model.
37 | 	 */
38 | 	rawValue: T;
39 | 
40 | 	setRawValue(rawValue: T, options?: ValueChangeOptions): void;
41 | }
42 | 
43 | export type ReadonlyValueEvents<T> = ValueEvents<T, ReadonlyValue<T>>;
44 | 
45 | /**
46 |  * A readonly value that can be changed elsewhere.
47 |  * @template T The type of the raw value.
48 |  */
49 | export interface ReadonlyValue<T> {
50 | 	/**
51 | 	 * The event emitter for value changes.
52 | 	 */
53 | 	readonly emitter: Emitter<ReadonlyValueEvents<T>>;
54 | 	/**
55 | 	 * The raw value of the model.
56 | 	 */
57 | 	readonly rawValue: T;
58 | }
59 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/values-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {createReadonlyValue, createValue} from './values.js';
 5 | 
 6 | describe(createReadonlyValue.name, () => {
 7 | 	it('should get raw value', () => {
 8 | 		const v = createValue(123);
 9 | 		const [rv] = createReadonlyValue(v);
10 | 		assert.strictEqual(rv.rawValue, v.rawValue);
11 | 
12 | 		v.rawValue = 456;
13 | 		assert.strictEqual(rv.rawValue, v.rawValue);
14 | 	});
15 | 
16 | 	it('should set raw value', () => {
17 | 		const v = createValue(123);
18 | 		const [rv, setRawValue] = createReadonlyValue(v);
19 | 		assert.strictEqual(rv.rawValue, v.rawValue);
20 | 
21 | 		setRawValue(456);
22 | 		assert.strictEqual(rv.rawValue, v.rawValue);
23 | 	});
24 | 
25 | 	it('should emit beforechange event', (done) => {
26 | 		const v = createValue(123);
27 | 		const [rv, setRawValue] = createReadonlyValue(v);
28 | 
29 | 		rv.emitter.on('beforechange', (ev) => {
30 | 			assert.strictEqual(ev.sender, rv);
31 | 			done();
32 | 		});
33 | 		setRawValue(456);
34 | 	});
35 | 
36 | 	it('should emit change event', (done) => {
37 | 		const v = createValue(123);
38 | 		const [rv, setRawValue] = createReadonlyValue(v);
39 | 
40 | 		rv.emitter.on('change', (ev) => {
41 | 			assert.strictEqual(ev.previousRawValue, 123);
42 | 			assert.strictEqual(ev.rawValue, 456);
43 | 			assert.strictEqual(ev.sender, rv);
44 | 			done();
45 | 		});
46 | 		setRawValue(456);
47 | 	});
48 | });
49 | 


--------------------------------------------------------------------------------
/packages/core/src/common/model/values.ts:
--------------------------------------------------------------------------------
 1 | import {Constraint} from '../constraint/constraint.js';
 2 | import {ComplexValue} from './complex-value.js';
 3 | import {PrimitiveValue} from './primitive-value.js';
 4 | import {ReadonlyPrimitiveValue} from './readonly-primitive-value.js';
 5 | import {ReadonlyValue, Value, ValueChangeOptions} from './value.js';
 6 | 
 7 | interface Config<T> {
 8 | 	constraint?: Constraint<T>;
 9 | 	equals?: (v1: T, v2: T) => boolean;
10 | }
11 | 
12 | export function createValue<T>(initialValue: T, config?: Config<T>): Value<T> {
13 | 	const constraint = config?.constraint;
14 | 	const equals = config?.equals;
15 | 	if (!constraint && !equals) {
16 | 		return new PrimitiveValue(initialValue);
17 | 	}
18 | 	return new ComplexValue(initialValue, config);
19 | }
20 | 
21 | export type SetRawValue<T> = (
22 | 	rawValue: T,
23 | 	options?: ValueChangeOptions | undefined,
24 | ) => void;
25 | 
26 | export function createReadonlyValue<T>(
27 | 	value: Value<T>,
28 | ): [ReadonlyValue<T>, SetRawValue<T>] {
29 | 	return [
30 | 		new ReadonlyPrimitiveValue(value),
31 | 		(rawValue: T, options: ValueChangeOptions | undefined) => {
32 | 			value.setRawValue(rawValue, options);
33 | 		},
34 | 	];
35 | }
36 | 


--------------------------------------------------------------------------------
/packages/core/src/common/number/view/slider-text.ts:
--------------------------------------------------------------------------------
 1 | import {ClassName} from '../../../common/view/class-name.js';
 2 | import {View} from '../../../common/view/view.js';
 3 | import {NumberTextView} from './number-text.js';
 4 | import {SliderView} from './slider.js';
 5 | 
 6 | /**
 7 |  * @hidden
 8 |  */
 9 | interface Config {
10 | 	sliderView: SliderView;
11 | 	textView: NumberTextView;
12 | }
13 | 
14 | const cn = ClassName('sldtxt');
15 | 
16 | /**
17 |  * @hidden
18 |  */
19 | export class SliderTextView implements View {
20 | 	public readonly element: HTMLElement;
21 | 	private readonly sliderView_: SliderView;
22 | 	private readonly textView_: NumberTextView;
23 | 
24 | 	constructor(doc: Document, config: Config) {
25 | 		this.element = doc.createElement('div');
26 | 		this.element.classList.add(cn());
27 | 
28 | 		const sliderElem = doc.createElement('div');
29 | 		sliderElem.classList.add(cn('s'));
30 | 		this.sliderView_ = config.sliderView;
31 | 		sliderElem.appendChild(this.sliderView_.element);
32 | 		this.element.appendChild(sliderElem);
33 | 
34 | 		const textElem = doc.createElement('div');
35 | 		textElem.classList.add(cn('t'));
36 | 		this.textView_ = config.textView;
37 | 		textElem.appendChild(this.textView_.element);
38 | 		this.element.appendChild(textElem);
39 | 	}
40 | }
41 | 


--------------------------------------------------------------------------------
/packages/core/src/common/params.ts:
--------------------------------------------------------------------------------
 1 | import {Formatter} from './converter/formatter.js';
 2 | 
 3 | export interface BaseParams {
 4 | 	disabled?: boolean;
 5 | 	hidden?: boolean;
 6 | 	index?: number;
 7 | }
 8 | 
 9 | export type ArrayStyleListOptions<T> = {text: string; value: T}[];
10 | export type ObjectStyleListOptions<T> = {[text: string]: T};
11 | export type ListParamsOptions<T> =
12 | 	| ArrayStyleListOptions<T>
13 | 	| ObjectStyleListOptions<T>;
14 | 
15 | export type PickerLayout = 'inline' | 'popup';
16 | 
17 | interface BindingParams extends BaseParams {
18 | 	label?: string;
19 | 	tag?: string | undefined;
20 | 	view?: string;
21 | }
22 | 
23 | export interface BaseInputParams
24 | 	extends BindingParams,
25 | 		Record<string, unknown> {
26 | 	readonly?: false;
27 | }
28 | 
29 | export interface BaseMonitorParams
30 | 	extends BindingParams,
31 | 		Record<string, unknown> {
32 | 	bufferSize?: number;
33 | 	interval?: number;
34 | 	readonly: true;
35 | }
36 | 
37 | export interface BaseBladeParams extends BaseParams, Record<string, unknown> {}
38 | 
39 | export interface NumberTextInputParams {
40 | 	format?: Formatter<number>;
41 | 	/**
42 | 	 * The unit scale for key input.
43 | 	 */
44 | 	keyScale?: number;
45 | 	max?: number;
46 | 	min?: number;
47 | 	/**
48 | 	 * The unit scale for pointer input.
49 | 	 */
50 | 	pointerScale?: number;
51 | 	step?: number;
52 | }
53 | 
54 | export type PointDimensionParams = NumberTextInputParams;
55 | 


--------------------------------------------------------------------------------
/packages/core/src/common/picker-util.ts:
--------------------------------------------------------------------------------
1 | import {PickerLayout} from './params.js';
2 | 
3 | export function parsePickerLayout(value: unknown): PickerLayout | undefined {
4 | 	if (value === 'inline' || value === 'popup') {
5 | 		return value;
6 | 	}
7 | 	return undefined;
8 | }
9 | 


--------------------------------------------------------------------------------
/packages/core/src/common/point-nd/point-axis.ts:
--------------------------------------------------------------------------------
 1 | import {Constraint} from '../constraint/constraint.js';
 2 | import {ValueMap} from '../model/value-map.js';
 3 | import {createNumberTextPropsObject} from '../number/util.js';
 4 | import {NumberTextProps} from '../number/view/number-text.js';
 5 | import {PointDimensionParams} from '../params.js';
 6 | 
 7 | export interface PointAxis {
 8 | 	constraint: Constraint<number> | undefined;
 9 | 	textProps: NumberTextProps;
10 | }
11 | 
12 | export function createPointAxis(config: {
13 | 	constraint: Constraint<number> | undefined;
14 | 	initialValue: number;
15 | 	params: PointDimensionParams;
16 | }): PointAxis {
17 | 	return {
18 | 		constraint: config.constraint,
19 | 		textProps: ValueMap.fromObject(
20 | 			createNumberTextPropsObject(config.params, config.initialValue),
21 | 		),
22 | 	};
23 | }
24 | 


--------------------------------------------------------------------------------
/packages/core/src/common/point-nd/test-util.ts:
--------------------------------------------------------------------------------
 1 | import {findConstraint} from '../constraint/composite.js';
 2 | import {Constraint} from '../constraint/constraint.js';
 3 | import {DefiniteRangeConstraint} from '../constraint/definite-range.js';
 4 | import {RangeConstraint} from '../constraint/range.js';
 5 | import {StepConstraint} from '../constraint/step.js';
 6 | 
 7 | /**
 8 |  * Finds a range from number constraint.
 9 |  * @param c The number constraint.
10 |  * @return A list that contains a minimum value and a max value.
11 |  */
12 | function findNumberRange(
13 | 	c: Constraint<number>,
14 | ): [number | undefined, number | undefined] {
15 | 	const drc = findConstraint(c, DefiniteRangeConstraint);
16 | 	if (drc) {
17 | 		return [drc.values.get('min'), drc.values.get('max')];
18 | 	}
19 | 	const rc = findConstraint(c, RangeConstraint);
20 | 	if (rc) {
21 | 		return [rc.values.get('min'), rc.values.get('max')];
22 | 	}
23 | 	return [undefined, undefined];
24 | }
25 | 
26 | export function getDimensionProps(c: Constraint<number>) {
27 | 	const [min, max] = findNumberRange(c);
28 | 	const sc = findConstraint(c, StepConstraint);
29 | 	return {
30 | 		max: max,
31 | 		min: min,
32 | 		step: sc?.step,
33 | 	};
34 | }
35 | 


--------------------------------------------------------------------------------
/packages/core/src/common/point-nd/util.ts:
--------------------------------------------------------------------------------
 1 | import {isRecord} from '../../misc/type-util.js';
 2 | import {CompositeConstraint} from '../constraint/composite.js';
 3 | import {Constraint} from '../constraint/constraint.js';
 4 | import {MicroParsers, parseRecord} from '../micro-parsers.js';
 5 | import {
 6 | 	createNumberTextInputParamsParser,
 7 | 	createRangeConstraint,
 8 | 	createStepConstraint,
 9 | } from '../number/util.js';
10 | import {PointDimensionParams} from '../params.js';
11 | 
12 | export function createPointDimensionParser(p: typeof MicroParsers) {
13 | 	return createNumberTextInputParamsParser(p);
14 | }
15 | 
16 | export function parsePointDimensionParams(
17 | 	value: unknown,
18 | ): PointDimensionParams | undefined {
19 | 	if (!isRecord(value)) {
20 | 		return undefined;
21 | 	}
22 | 	return parseRecord(value, createPointDimensionParser);
23 | }
24 | 
25 | export function createDimensionConstraint(
26 | 	params: PointDimensionParams | undefined,
27 | 	initialValue: number,
28 | ): Constraint<number> | undefined {
29 | 	if (!params) {
30 | 		return undefined;
31 | 	}
32 | 
33 | 	const constraints: Constraint<number>[] = [];
34 | 	const cs = createStepConstraint(params, initialValue);
35 | 	if (cs) {
36 | 		constraints.push(cs);
37 | 	}
38 | 	const rs = createRangeConstraint(params);
39 | 	if (rs) {
40 | 		constraints.push(rs);
41 | 	}
42 | 	return new CompositeConstraint(constraints);
43 | }
44 | 


--------------------------------------------------------------------------------
/packages/core/src/common/primitive.ts:
--------------------------------------------------------------------------------
 1 | import {BindingTarget} from './binding/target.js';
 2 | 
 3 | /**
 4 |  * The union of primitive types.
 5 |  */
 6 | export type Primitive = boolean | number | string;
 7 | 
 8 | /**
 9 |  * Writes the primitive value.
10 |  * @param target The target to be written.
11 |  * @param value The value to write.
12 |  */
13 | export function writePrimitive<T extends Primitive>(
14 | 	target: BindingTarget,
15 | 	value: T,
16 | ): void {
17 | 	target.write(value);
18 | }
19 | 


--------------------------------------------------------------------------------
/packages/core/src/common/tp-error-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {TpError} from './tp-error.js';
 5 | 
 6 | describe(TpError.name, () => {
 7 | 	it('should instanciate for invalid parameters', () => {
 8 | 		const e = new TpError({
 9 | 			context: {
10 | 				name: 'foo',
11 | 			},
12 | 			type: 'invalidparams',
13 | 		});
14 | 
15 | 		assert.strictEqual(e.type, 'invalidparams');
16 | 	});
17 | 
18 | 	it('should use message for toString()', () => {
19 | 		const e = TpError.shouldNeverHappen();
20 | 		assert.strictEqual(e.message, e.toString());
21 | 	});
22 | });
23 | 


--------------------------------------------------------------------------------
/packages/core/src/common/ui.ts:
--------------------------------------------------------------------------------
 1 | interface StepKeys {
 2 | 	altKey: boolean;
 3 | 	downKey: boolean;
 4 | 	shiftKey: boolean;
 5 | 	upKey: boolean;
 6 | }
 7 | 
 8 | export function getStepForKey(keyScale: number, keys: StepKeys): number {
 9 | 	const step = keyScale * (keys.altKey ? 0.1 : 1) * (keys.shiftKey ? 10 : 1);
10 | 
11 | 	if (keys.upKey) {
12 | 		return +step;
13 | 	} else if (keys.downKey) {
14 | 		return -step;
15 | 	}
16 | 	return 0;
17 | }
18 | 
19 | export function getVerticalStepKeys(ev: KeyboardEvent): StepKeys {
20 | 	return {
21 | 		altKey: ev.altKey,
22 | 		downKey: ev.key === 'ArrowDown',
23 | 		shiftKey: ev.shiftKey,
24 | 		upKey: ev.key === 'ArrowUp',
25 | 	};
26 | }
27 | 
28 | export function getHorizontalStepKeys(ev: KeyboardEvent): StepKeys {
29 | 	return {
30 | 		altKey: ev.altKey,
31 | 		downKey: ev.key === 'ArrowLeft',
32 | 		shiftKey: ev.shiftKey,
33 | 		upKey: ev.key === 'ArrowRight',
34 | 	};
35 | }
36 | 
37 | export function isVerticalArrowKey(key: string): boolean {
38 | 	return key === 'ArrowUp' || key === 'ArrowDown';
39 | }
40 | 
41 | export function isArrowKey(key: string): boolean {
42 | 	return isVerticalArrowKey(key) || key === 'ArrowLeft' || key === 'ArrowRight';
43 | }
44 | 


--------------------------------------------------------------------------------
/packages/core/src/common/view/class-name.ts:
--------------------------------------------------------------------------------
 1 | const PREFIX = 'tp';
 2 | 
 3 | /**
 4 |  * A utility function for generating BEM-like class name.
 5 |  * @param viewName The name of the view. Used as part of the block name.
 6 |  * @return A class name generator function.
 7 |  */
 8 | export function ClassName(viewName: string) {
 9 | 	/**
10 | 	 * Generates a class name.
11 | 	 * @param [opt_elementName] The name of the element.
12 | 	 * @param [opt_modifier] The name of the modifier.
13 | 	 * @return A class name.
14 | 	 */
15 | 	const fn = (opt_elementName?: string, opt_modifier?: string): string => {
16 | 		return [
17 | 			PREFIX,
18 | 			'-',
19 | 			viewName,
20 | 			'v',
21 | 			opt_elementName ? `_${opt_elementName}` : '',
22 | 			opt_modifier ? `-${opt_modifier}` : '',
23 | 		].join('');
24 | 	};
25 | 	return fn;
26 | }
27 | 


--------------------------------------------------------------------------------
/packages/core/src/common/view/css-vars.ts:
--------------------------------------------------------------------------------
 1 | const CSS_VAR_MAP = {
 2 | 	containerUnitSize: 'cnt-usz',
 3 | };
 4 | 
 5 | /**
 6 |  * Gets a name of the internal CSS variable.
 7 |  * @param key The key for the CSS variable.
 8 |  * @return A name of the internal CSS variable.
 9 |  */
10 | export function getCssVar(key: keyof typeof CSS_VAR_MAP): string {
11 | 	return `--${CSS_VAR_MAP[key]}`;
12 | }
13 | 


--------------------------------------------------------------------------------
/packages/core/src/common/view/plain.ts:
--------------------------------------------------------------------------------
 1 | import {ViewProps} from '../model/view-props.js';
 2 | import {ClassName} from './class-name.js';
 3 | import {View} from './view.js';
 4 | 
 5 | /**
 6 |  * @hidden
 7 |  */
 8 | interface Config {
 9 | 	viewName: string;
10 | 	viewProps: ViewProps;
11 | }
12 | 
13 | /**
14 |  * @hidden
15 |  */
16 | export class PlainView implements View {
17 | 	public readonly element: HTMLElement;
18 | 
19 | 	/**
20 | 	 * @hidden
21 | 	 */
22 | 	constructor(doc: Document, config: Config) {
23 | 		const cn = ClassName(config.viewName);
24 | 		this.element = doc.createElement('div');
25 | 		this.element.classList.add(cn());
26 | 		config.viewProps.bindClassModifiers(this.element);
27 | 	}
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/core/src/common/view/popup.ts:
--------------------------------------------------------------------------------
 1 | import {bindValue} from '../model/reactive.js';
 2 | import {Value} from '../model/value.js';
 3 | import {ViewProps} from '../model/view-props.js';
 4 | import {ClassName} from './class-name.js';
 5 | import {valueToClassName} from './reactive.js';
 6 | import {View} from './view.js';
 7 | 
 8 | interface Config {
 9 | 	shows: Value<boolean>;
10 | 	viewProps: ViewProps;
11 | }
12 | 
13 | const cn = ClassName('pop');
14 | 
15 | /**
16 |  * @hidden
17 |  */
18 | export class PopupView implements View {
19 | 	public readonly element: HTMLElement;
20 | 
21 | 	constructor(doc: Document, config: Config) {
22 | 		this.element = doc.createElement('div');
23 | 		this.element.classList.add(cn());
24 | 		config.viewProps.bindClassModifiers(this.element);
25 | 		bindValue(config.shows, valueToClassName(this.element, cn(undefined, 'v')));
26 | 	}
27 | }
28 | 


--------------------------------------------------------------------------------
/packages/core/src/common/view/reactive.ts:
--------------------------------------------------------------------------------
 1 | import {bindValue} from '../model/reactive.js';
 2 | import {Value} from '../model/value.js';
 3 | 
 4 | function applyClass(elem: HTMLElement, className: string, active: boolean) {
 5 | 	if (active) {
 6 | 		elem.classList.add(className);
 7 | 	} else {
 8 | 		elem.classList.remove(className);
 9 | 	}
10 | }
11 | 
12 | export function valueToClassName(
13 | 	elem: HTMLElement,
14 | 	className: string,
15 | ): (value: boolean) => void {
16 | 	return (value) => {
17 | 		applyClass(elem, className, value);
18 | 	};
19 | }
20 | 
21 | export function bindValueToTextContent(
22 | 	value: Value<string | undefined>,
23 | 	elem: HTMLElement,
24 | ) {
25 | 	bindValue(value, (text) => {
26 | 		elem.textContent = text ?? '';
27 | 	});
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/core/src/common/view/view.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * A view interface.
 3 |  */
 4 | export interface View {
 5 | 	/**
 6 | 	 * A root element of the view.
 7 | 	 */
 8 | 	readonly element: HTMLElement;
 9 | }
10 | 
11 | /**
12 |  * @hidden
13 |  */
14 | export interface InputView extends View {
15 | 	readonly inputElement: HTMLInputElement;
16 | }
17 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/controller/color-swatch.ts:
--------------------------------------------------------------------------------
 1 | import {ValueController} from '../../../common/controller/value.js';
 2 | import {Value} from '../../../common/model/value.js';
 3 | import {ViewProps} from '../../../common/model/view-props.js';
 4 | import {IntColor} from '../model/int-color.js';
 5 | import {ColorSwatchView} from '../view/color-swatch.js';
 6 | 
 7 | interface Config {
 8 | 	value: Value<IntColor>;
 9 | 	viewProps: ViewProps;
10 | }
11 | 
12 | /**
13 |  * @hidden
14 |  */
15 | export class ColorSwatchController
16 | 	implements ValueController<IntColor, ColorSwatchView>
17 | {
18 | 	public readonly value: Value<IntColor>;
19 | 	public readonly view: ColorSwatchView;
20 | 	public readonly viewProps: ViewProps;
21 | 
22 | 	constructor(doc: Document, config: Config) {
23 | 		this.value = config.value;
24 | 		this.viewProps = config.viewProps;
25 | 
26 | 		this.view = new ColorSwatchView(doc, {
27 | 			value: this.value,
28 | 			viewProps: this.viewProps,
29 | 		});
30 | 	}
31 | }
32 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/converter/color-number.ts:
--------------------------------------------------------------------------------
 1 | import {mapRange} from '../../../common/number/util.js';
 2 | import {removeAlphaComponent} from '../model/color-model.js';
 3 | import {IntColor} from '../model/int-color.js';
 4 | 
 5 | export function colorToRgbNumber(value: IntColor): number {
 6 | 	return removeAlphaComponent(value.getComponents('rgb')).reduce(
 7 | 		(result, comp) => {
 8 | 			return (result << 8) | (Math.floor(comp) & 0xff);
 9 | 		},
10 | 		0,
11 | 	);
12 | }
13 | 
14 | export function colorToRgbaNumber(value: IntColor): number {
15 | 	return (
16 | 		value.getComponents('rgb').reduce((result, comp, index) => {
17 | 			const hex = Math.floor(index === 3 ? comp * 255 : comp) & 0xff;
18 | 			return (result << 8) | hex;
19 | 		}, 0) >>> 0
20 | 	);
21 | }
22 | 
23 | export function numberToRgbColor(num: number): IntColor {
24 | 	return new IntColor(
25 | 		[(num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff],
26 | 		'rgb',
27 | 	);
28 | }
29 | 
30 | export function numberToRgbaColor(num: number): IntColor {
31 | 	return new IntColor(
32 | 		[
33 | 			(num >> 24) & 0xff,
34 | 			(num >> 16) & 0xff,
35 | 			(num >> 8) & 0xff,
36 | 			mapRange(num & 0xff, 0, 255, 0, 1),
37 | 		],
38 | 		'rgb',
39 | 	);
40 | }
41 | 
42 | export function colorFromRgbNumber(value: unknown): IntColor {
43 | 	if (typeof value !== 'number') {
44 | 		return IntColor.black();
45 | 	}
46 | 	return numberToRgbColor(value);
47 | }
48 | 
49 | export function colorFromRgbaNumber(value: unknown): IntColor {
50 | 	if (typeof value !== 'number') {
51 | 		return IntColor.black();
52 | 	}
53 | 	return numberToRgbaColor(value);
54 | }
55 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/converter/color-object.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	Color,
 3 | 	createColorComponentsFromRgbObject,
 4 | 	isColorObject,
 5 | } from '../model/color.js';
 6 | import {ColorType} from '../model/color-model.js';
 7 | import {mapColorType} from '../model/colors.js';
 8 | import {FloatColor} from '../model/float-color.js';
 9 | import {IntColor} from '../model/int-color.js';
10 | 
11 | export function colorFromObject(value: unknown, type: 'int'): IntColor;
12 | export function colorFromObject(value: unknown, type: 'float'): FloatColor;
13 | export function colorFromObject(value: unknown, type: ColorType): Color;
14 | export function colorFromObject(value: unknown, type: ColorType): Color {
15 | 	if (!isColorObject(value)) {
16 | 		return mapColorType(IntColor.black(), type);
17 | 	}
18 | 	if (type === 'int') {
19 | 		const comps = createColorComponentsFromRgbObject(value);
20 | 		return new IntColor(comps, 'rgb');
21 | 	}
22 | 	if (type === 'float') {
23 | 		const comps = createColorComponentsFromRgbObject(value);
24 | 		return new FloatColor(comps, 'rgb');
25 | 	}
26 | 	return mapColorType(IntColor.black(), 'int');
27 | }
28 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/converter/writer-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {BindingTarget} from '../../../common/binding/target.js';
 5 | import {IntColor} from '../model/int-color.js';
 6 | import {writeRgbaColorObject, writeRgbColorObject} from './writer.js';
 7 | 
 8 | describe('writer/color', () => {
 9 | 	it('should write RGBA color object value without destruction', () => {
10 | 		const obj = {
11 | 			foo: {r: 0, g: 127, b: 255, a: 0.5},
12 | 		};
13 | 		const objFoo = obj.foo;
14 | 		const t = new BindingTarget(obj, 'foo');
15 | 		writeRgbaColorObject(t, new IntColor([128, 255, 0, 0.7], 'rgb'), 'int');
16 | 
17 | 		assert.strictEqual(obj.foo, objFoo, 'instance');
18 | 		assert.strictEqual(obj.foo.r, 128, 'r');
19 | 		assert.strictEqual(obj.foo.g, 255, 'g');
20 | 		assert.strictEqual(obj.foo.b, 0, 'b');
21 | 		assert.strictEqual(obj.foo.a, 0.7, 'a');
22 | 	});
23 | 
24 | 	it('should write RGB color object value without destruction', () => {
25 | 		const obj = {
26 | 			foo: {r: 0, g: 127, b: 255, a: 0.5},
27 | 		};
28 | 		const objFoo = obj.foo;
29 | 		const t = new BindingTarget(obj, 'foo');
30 | 		writeRgbColorObject(t, new IntColor([128, 255, 0, 0.7], 'rgb'), 'int');
31 | 
32 | 		assert.strictEqual(obj.foo, objFoo, 'instance');
33 | 		assert.strictEqual(obj.foo.r, 128, 'r');
34 | 		assert.strictEqual(obj.foo.g, 255, 'g');
35 | 		assert.strictEqual(obj.foo.b, 0, 'b');
36 | 		// should not overwrite alpha component
37 | 		assert.strictEqual(obj.foo.a, 0.5, 'a');
38 | 	});
39 | });
40 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/model/float-color.ts:
--------------------------------------------------------------------------------
 1 | import {Color, RgbaColorObject} from './color.js';
 2 | import {
 3 | 	appendAlphaComponent,
 4 | 	ColorComponents3,
 5 | 	ColorComponents4,
 6 | 	ColorMode,
 7 | 	ColorType,
 8 | 	constrainColorComponents,
 9 | 	convertColor,
10 | 	removeAlphaComponent,
11 | } from './color-model.js';
12 | 
13 | export class FloatColor implements Color {
14 | 	private readonly comps_: ColorComponents4;
15 | 	public readonly mode: ColorMode;
16 | 	public readonly type: ColorType = 'float';
17 | 
18 | 	constructor(comps: ColorComponents3 | ColorComponents4, mode: ColorMode) {
19 | 		this.mode = mode;
20 | 		this.comps_ = constrainColorComponents(comps, mode, this.type);
21 | 	}
22 | 
23 | 	public getComponents(opt_mode?: ColorMode): ColorComponents4 {
24 | 		return appendAlphaComponent(
25 | 			convertColor(
26 | 				removeAlphaComponent(this.comps_),
27 | 				{mode: this.mode, type: this.type},
28 | 				{mode: opt_mode ?? this.mode, type: this.type},
29 | 			),
30 | 			this.comps_[3],
31 | 		);
32 | 	}
33 | 
34 | 	public toRgbaObject(): RgbaColorObject {
35 | 		const rgbComps = this.getComponents('rgb');
36 | 		return {
37 | 			r: rgbComps[0],
38 | 			g: rgbComps[1],
39 | 			b: rgbComps[2],
40 | 			a: rgbComps[3],
41 | 		};
42 | 	}
43 | }
44 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/model/int-color.ts:
--------------------------------------------------------------------------------
 1 | import {Color, RgbaColorObject} from './color.js';
 2 | import {
 3 | 	appendAlphaComponent,
 4 | 	ColorComponents3,
 5 | 	ColorComponents4,
 6 | 	ColorMode,
 7 | 	ColorType,
 8 | 	constrainColorComponents,
 9 | 	convertColor,
10 | 	removeAlphaComponent,
11 | } from './color-model.js';
12 | 
13 | export class IntColor implements Color {
14 | 	public static black(): IntColor {
15 | 		return new IntColor([0, 0, 0], 'rgb');
16 | 	}
17 | 
18 | 	private readonly comps_: ColorComponents4;
19 | 	public readonly mode: ColorMode;
20 | 	public readonly type: ColorType = 'int';
21 | 
22 | 	constructor(comps: ColorComponents3 | ColorComponents4, mode: ColorMode) {
23 | 		this.mode = mode;
24 | 		this.comps_ = constrainColorComponents(comps, mode, this.type);
25 | 	}
26 | 
27 | 	public getComponents(opt_mode?: ColorMode): ColorComponents4 {
28 | 		return appendAlphaComponent(
29 | 			convertColor(
30 | 				removeAlphaComponent(this.comps_),
31 | 				{mode: this.mode, type: this.type},
32 | 				{mode: opt_mode ?? this.mode, type: this.type},
33 | 			),
34 | 			this.comps_[3],
35 | 		);
36 | 	}
37 | 
38 | 	public toRgbaObject(): RgbaColorObject {
39 | 		const rgbComps = this.getComponents('rgb');
40 | 		return {
41 | 			r: rgbComps[0],
42 | 			g: rgbComps[1],
43 | 			b: rgbComps[2],
44 | 			a: rgbComps[3],
45 | 		};
46 | 	}
47 | }
48 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/plugin-number-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe} from 'mocha';
 3 | 
 4 | import {BindingTarget} from '../../common/binding/target.js';
 5 | import {NumberColorInputPlugin} from './plugin-number.js';
 6 | 
 7 | describe('NumberColorInputPlugin', () => {
 8 | 	[
 9 | 		{
10 | 			view: 'color',
11 | 		},
12 | 		{
13 | 			color: {},
14 | 		},
15 | 		{
16 | 			color: {type: 'float'},
17 | 		},
18 | 	].forEach((params) => {
19 | 		context(`when params=${JSON.stringify(params)}`, () => {
20 | 			const input = {
21 | 				color: 0x00000000,
22 | 			};
23 | 			const result = NumberColorInputPlugin.accept(input.color, params);
24 | 
25 | 			it('should accept params', () => {
26 | 				assert.ok(result !== null);
27 | 			});
28 | 		});
29 | 	});
30 | 
31 | 	[
32 | 		{
33 | 			params: {
34 | 				view: 'color',
35 | 			},
36 | 			expected: 1,
37 | 		},
38 | 		{
39 | 			params: {
40 | 				color: {
41 | 					alpha: true,
42 | 				},
43 | 			},
44 | 			expected: 0,
45 | 		},
46 | 	].forEach(({params, expected}) => {
47 | 		context(`when params=${JSON.stringify(params)}`, () => {
48 | 			const input = {
49 | 				color: 0xffffff00,
50 | 			};
51 | 			const result = NumberColorInputPlugin.accept(input.color, params);
52 | 			if (!result) {
53 | 				throw new Error('unexpected result');
54 | 			}
55 | 			const reader = NumberColorInputPlugin.binding.reader({
56 | 				initialValue: input.color,
57 | 				params: result.params,
58 | 				target: new BindingTarget(input, 'color'),
59 | 			});
60 | 
61 | 			it('should apply alpha', () => {
62 | 				const c = reader(input.color);
63 | 				assert.strictEqual(c.getComponents('rgb')[3], expected);
64 | 			});
65 | 		});
66 | 	});
67 | });
68 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/util.ts:
--------------------------------------------------------------------------------
 1 | import {ColorInputParams} from '../../blade/common/api/params.js';
 2 | import {parseRecord} from '../../common/micro-parsers.js';
 3 | import {parsePickerLayout} from '../../common/picker-util.js';
 4 | import {ColorType} from './model/color-model.js';
 5 | 
 6 | function parseColorType(value: unknown): ColorType | undefined {
 7 | 	return value === 'int' ? 'int' : value === 'float' ? 'float' : undefined;
 8 | }
 9 | 
10 | export function parseColorInputParams(
11 | 	params: Record<string, unknown>,
12 | ): ColorInputParams | undefined {
13 | 	return parseRecord<ColorInputParams>(params, (p) => ({
14 | 		color: p.optional.object({
15 | 			alpha: p.optional.boolean,
16 | 			type: p.optional.custom(parseColorType),
17 | 		}),
18 | 		expanded: p.optional.boolean,
19 | 		picker: p.optional.custom(parsePickerLayout),
20 | 		readonly: p.optional.constant(false),
21 | 	}));
22 | }
23 | 
24 | export function getKeyScaleForColor(forAlpha: boolean): number {
25 | 	return forAlpha ? 0.1 : 1;
26 | }
27 | 
28 | export function extractColorType(
29 | 	params: ColorInputParams,
30 | ): ColorType | undefined {
31 | 	return params.color?.type;
32 | }
33 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/color/view/color-texts-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe} from 'mocha';
 3 | 
 4 | import {ValueMap} from '../../../common/model/value-map.js';
 5 | import {createValue} from '../../../common/model/values.js';
 6 | import {ViewProps} from '../../../common/model/view-props.js';
 7 | import {NumberTextView} from '../../../common/number/view/number-text.js';
 8 | import {createTestWindow} from '../../../misc/dom-test-util.js';
 9 | import {Tuple3} from '../../../misc/type-util.js';
10 | import {ColorTextsMode, ColorTextsView} from './color-texts.js';
11 | 
12 | function createTextViews(
13 | 	doc: Document,
14 | 	viewProps: ViewProps,
15 | ): Tuple3<NumberTextView> {
16 | 	return [0, 1, 2].map(
17 | 		() =>
18 | 			new NumberTextView(doc, {
19 | 				dragging: createValue<number | null>(0),
20 | 				props: ValueMap.fromObject({
21 | 					formatter: (v) => String(v),
22 | 					keyScale: 1,
23 | 					pointerScale: 1,
24 | 				}),
25 | 				value: createValue<number>(0),
26 | 				viewProps: viewProps,
27 | 			}),
28 | 	) as Tuple3<NumberTextView>;
29 | }
30 | 
31 | describe(ColorTextsView.name, () => {
32 | 	it('should bind disabled', () => {
33 | 		const doc = createTestWindow().document;
34 | 		const viewProps = ViewProps.create();
35 | 		const v = new ColorTextsView(doc, {
36 | 			mode: createValue<ColorTextsMode>('rgb'),
37 | 			inputViews: createTextViews(doc, viewProps),
38 | 			viewProps: viewProps,
39 | 		});
40 | 		assert.strictEqual(v.modeSelectElement.disabled, false);
41 | 
42 | 		viewProps.set('disabled', true);
43 | 		assert.strictEqual(v.modeSelectElement.disabled, true);
44 | 	});
45 | });
46 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/common/constraint/point-nd-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe as context, describe, it} from 'mocha';
 3 | 
 4 | import {Constraint} from '../../../common/constraint/constraint.js';
 5 | import {RangeConstraint} from '../../../common/constraint/range.js';
 6 | import {
 7 | 	Point2d,
 8 | 	Point2dAssembly,
 9 | 	Point2dObject,
10 | } from '../../point-2d/model/point-2d.js';
11 | import {PointNdConstraint} from './point-nd.js';
12 | 
13 | interface TestCase {
14 | 	expected: Point2dObject;
15 | 	params: {
16 | 		config: {
17 | 			x?: Constraint<number>;
18 | 			y?: Constraint<number>;
19 | 		};
20 | 		value: Point2dObject;
21 | 	};
22 | }
23 | 
24 | describe(PointNdConstraint.name, () => {
25 | 	[
26 | 		{
27 | 			expected: {x: 123, y: -123},
28 | 			params: {
29 | 				config: {},
30 | 				value: {x: 123, y: -123},
31 | 			},
32 | 		},
33 | 		{
34 | 			expected: {x: 0, y: -50},
35 | 			params: {
36 | 				config: {
37 | 					x: new RangeConstraint({min: 0}),
38 | 					y: new RangeConstraint({min: -50}),
39 | 				},
40 | 				value: {x: -100, y: -100},
41 | 			},
42 | 		},
43 | 	].forEach((testCase: TestCase) => {
44 | 		context(`when params = ${JSON.stringify(testCase.params)}`, () => {
45 | 			it(`should constrain value to ${JSON.stringify(
46 | 				testCase.expected,
47 | 			)}`, () => {
48 | 				const c = new PointNdConstraint({
49 | 					assembly: Point2dAssembly,
50 | 					components: [testCase.params.config.x, testCase.params.config.y],
51 | 				});
52 | 				const v = c.constrain(
53 | 					new Point2d(testCase.params.value.x, testCase.params.value.y),
54 | 				);
55 | 				assert.deepStrictEqual(v.toObject(), testCase.expected);
56 | 			});
57 | 		});
58 | 	});
59 | });
60 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/common/constraint/point-nd.ts:
--------------------------------------------------------------------------------
 1 | import {Constraint} from '../../../common/constraint/constraint.js';
 2 | import {PointNdAssembly} from '../model/point-nd.js';
 3 | 
 4 | interface Config<PointNd> {
 5 | 	assembly: PointNdAssembly<PointNd>;
 6 | 	components: (Constraint<number> | undefined)[];
 7 | }
 8 | 
 9 | /**
10 |  * @hidden
11 |  */
12 | export class PointNdConstraint<PointNd> implements Constraint<PointNd> {
13 | 	public readonly components: (Constraint<number> | undefined)[];
14 | 	private readonly asm_: PointNdAssembly<PointNd>;
15 | 
16 | 	constructor(config: Config<PointNd>) {
17 | 		this.components = config.components;
18 | 		this.asm_ = config.assembly;
19 | 	}
20 | 
21 | 	public constrain(value: PointNd): PointNd {
22 | 		const comps = this.asm_
23 | 			.toComponents(value)
24 | 			.map((comp, index) => this.components[index]?.constrain(comp) ?? comp);
25 | 		return this.asm_.fromComponents(comps);
26 | 	}
27 | }
28 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/common/model/point-nd.ts:
--------------------------------------------------------------------------------
1 | export interface PointNdAssembly<PointNd> {
2 | 	toComponents: (p: PointNd) => number[];
3 | 	fromComponents: (comps: number[]) => PointNd;
4 | }
5 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/common/view/point-nd-text.ts:
--------------------------------------------------------------------------------
 1 | import {NumberTextView} from '../../../common/number/view/number-text.js';
 2 | import {ClassName} from '../../../common/view/class-name.js';
 3 | import {View} from '../../../common/view/view.js';
 4 | 
 5 | interface Config {
 6 | 	textViews: NumberTextView[];
 7 | }
 8 | 
 9 | const cn = ClassName('pndtxt');
10 | 
11 | /**
12 |  * @hidden
13 |  */
14 | export class PointNdTextView implements View {
15 | 	public readonly element: HTMLElement;
16 | 	public readonly textViews: NumberTextView[];
17 | 
18 | 	constructor(doc: Document, config: Config) {
19 | 		this.textViews = config.textViews;
20 | 
21 | 		this.element = doc.createElement('div');
22 | 		this.element.classList.add(cn());
23 | 
24 | 		this.textViews.forEach((v) => {
25 | 			const axisElem = doc.createElement('div');
26 | 			axisElem.classList.add(cn('a'));
27 | 			axisElem.appendChild(v.element);
28 | 			this.element.appendChild(axisElem);
29 | 		});
30 | 	}
31 | }
32 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/number/api/slider.ts:
--------------------------------------------------------------------------------
 1 | import {BindingApi} from '../../../blade/binding/api/binding.js';
 2 | import {InputBindingApi} from '../../../blade/binding/api/input-binding.js';
 3 | import {InputBindingController} from '../../../blade/binding/controller/input-binding.js';
 4 | import {SliderTextController} from '../../../common/number/controller/slider-text.js';
 5 | 
 6 | export class SliderInputBindingApi
 7 | 	extends BindingApi<
 8 | 		number,
 9 | 		number,
10 | 		InputBindingController<number, SliderTextController>
11 | 	>
12 | 	implements InputBindingApi<number, number>
13 | {
14 | 	get max(): number {
15 | 		return this.controller.valueController.sliderController.props.get('max');
16 | 	}
17 | 
18 | 	set max(max: number) {
19 | 		this.controller.valueController.sliderController.props.set('max', max);
20 | 	}
21 | 
22 | 	get min(): number {
23 | 		return this.controller.valueController.sliderController.props.get('min');
24 | 	}
25 | 
26 | 	set min(max: number) {
27 | 		this.controller.valueController.sliderController.props.set('min', max);
28 | 	}
29 | }
30 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-2d/converter/point-2d.ts:
--------------------------------------------------------------------------------
 1 | import {BindingTarget} from '../../../common/binding/target.js';
 2 | import {Point2d} from '../model/point-2d.js';
 3 | 
 4 | export function point2dFromUnknown(value: unknown): Point2d {
 5 | 	return Point2d.isObject(value)
 6 | 		? new Point2d(value.x, value.y)
 7 | 		: new Point2d();
 8 | }
 9 | 
10 | export function writePoint2d(target: BindingTarget, value: Point2d) {
11 | 	target.writeProperty('x', value.x);
12 | 	target.writeProperty('y', value.y);
13 | }
14 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-2d/model/point-2d.ts:
--------------------------------------------------------------------------------
 1 | import {isEmpty} from '../../../misc/type-util.js';
 2 | import {PointNdAssembly} from '../../common/model/point-nd.js';
 3 | 
 4 | export interface Point2dObject {
 5 | 	x: number;
 6 | 	y: number;
 7 | }
 8 | 
 9 | export class Point2d {
10 | 	public x: number;
11 | 	public y: number;
12 | 
13 | 	constructor(x = 0, y = 0) {
14 | 		this.x = x;
15 | 		this.y = y;
16 | 	}
17 | 
18 | 	public getComponents(): [number, number] {
19 | 		return [this.x, this.y];
20 | 	}
21 | 
22 | 	public static isObject(obj: any): obj is Point2dObject {
23 | 		if (isEmpty(obj)) {
24 | 			return false;
25 | 		}
26 | 
27 | 		const x = obj.x;
28 | 		const y = obj.y;
29 | 		if (typeof x !== 'number' || typeof y !== 'number') {
30 | 			return false;
31 | 		}
32 | 
33 | 		return true;
34 | 	}
35 | 
36 | 	public static equals(v1: Point2d, v2: Point2d): boolean {
37 | 		return v1.x === v2.x && v1.y === v2.y;
38 | 	}
39 | 
40 | 	public toObject(): Point2dObject {
41 | 		return {
42 | 			x: this.x,
43 | 			y: this.y,
44 | 		};
45 | 	}
46 | }
47 | 
48 | export const Point2dAssembly: PointNdAssembly<Point2d> = {
49 | 	toComponents: (p: Point2d) => p.getComponents(),
50 | 	fromComponents: (comps: number[]) => new Point2d(...comps),
51 | };
52 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-3d/converter/point-3d-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {BindingTarget} from '../../../common/binding/target.js';
 5 | import {Point3d} from '../model/point-3d.js';
 6 | import {writePoint3d} from './point-3d.js';
 7 | 
 8 | describe(writePoint3d.name, () => {
 9 | 	it('should write value without destruction', () => {
10 | 		const obj = {
11 | 			foo: {x: 12, y: 34, z: -56},
12 | 		};
13 | 		const objFoo = obj.foo;
14 | 		const t = new BindingTarget(obj, 'foo');
15 | 		writePoint3d(t, new Point3d(56, 78, 901));
16 | 
17 | 		assert.strictEqual(obj.foo, objFoo);
18 | 		assert.strictEqual(obj.foo.x, 56);
19 | 		assert.strictEqual(obj.foo.y, 78);
20 | 		assert.strictEqual(obj.foo.z, 901);
21 | 	});
22 | });
23 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-3d/converter/point-3d.ts:
--------------------------------------------------------------------------------
 1 | import {BindingTarget} from '../../../common/binding/target.js';
 2 | import {Point3d} from '../model/point-3d.js';
 3 | 
 4 | export function point3dFromUnknown(value: unknown): Point3d {
 5 | 	return Point3d.isObject(value)
 6 | 		? new Point3d(value.x, value.y, value.z)
 7 | 		: new Point3d();
 8 | }
 9 | 
10 | export function writePoint3d(target: BindingTarget, value: Point3d) {
11 | 	target.writeProperty('x', value.x);
12 | 	target.writeProperty('y', value.y);
13 | 	target.writeProperty('z', value.z);
14 | }
15 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-3d/model/point-3d-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe as context, describe, it} from 'mocha';
 3 | 
 4 | import {Point3d} from './point-3d.js';
 5 | 
 6 | describe(Point3d.name, () => {
 7 | 	[
 8 | 		{
 9 | 			object: null,
10 | 			expected: false,
11 | 		},
12 | 		{
13 | 			object: undefined,
14 | 			expected: false,
15 | 		},
16 | 		{
17 | 			object: {x: 0, y: 1},
18 | 			expected: false,
19 | 		},
20 | 		{
21 | 			object: {x: -1, y: 0, z: 1},
22 | 			expected: true,
23 | 		},
24 | 		{
25 | 			object: {x: 1, y: 2, z: '3'},
26 | 			expected: false,
27 | 		},
28 | 	].forEach((testCase) => {
29 | 		context(`when object = ${JSON.stringify(testCase.object)}`, () => {
30 | 			it(`should regard input as object: ${testCase.expected}`, () => {
31 | 				assert.strictEqual(
32 | 					Point3d.isObject(testCase.object),
33 | 					testCase.expected,
34 | 				);
35 | 			});
36 | 		});
37 | 	});
38 | 
39 | 	[
40 | 		{
41 | 			object: new Point3d(0, 1, 2),
42 | 			expected: {x: 0, y: 1, z: 2},
43 | 		},
44 | 		{
45 | 			object: new Point3d(),
46 | 			expected: {x: 0, y: 0, z: 0},
47 | 		},
48 | 		{
49 | 			object: new Point3d(-100),
50 | 			expected: {x: -100, y: 0, z: 0},
51 | 		},
52 | 	].forEach((testCase) => {
53 | 		context(`when Point3d = ${JSON.stringify(testCase.object)}`, () => {
54 | 			it(`should convert into object: ${testCase.expected}`, () => {
55 | 				assert.deepStrictEqual(testCase.object.toObject(), testCase.expected);
56 | 			});
57 | 		});
58 | 	});
59 | });
60 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-3d/model/point-3d.ts:
--------------------------------------------------------------------------------
 1 | import {isEmpty} from '../../../misc/type-util.js';
 2 | import {PointNdAssembly} from '../../common/model/point-nd.js';
 3 | 
 4 | export interface Point3dObject {
 5 | 	x: number;
 6 | 	y: number;
 7 | 	z: number;
 8 | }
 9 | 
10 | export class Point3d {
11 | 	public x: number;
12 | 	public y: number;
13 | 	public z: number;
14 | 
15 | 	constructor(x = 0, y = 0, z = 0) {
16 | 		this.x = x;
17 | 		this.y = y;
18 | 		this.z = z;
19 | 	}
20 | 
21 | 	public getComponents(): [number, number, number] {
22 | 		return [this.x, this.y, this.z];
23 | 	}
24 | 
25 | 	public static isObject(obj: any): obj is Point3dObject {
26 | 		if (isEmpty(obj)) {
27 | 			return false;
28 | 		}
29 | 
30 | 		const x = obj.x;
31 | 		const y = obj.y;
32 | 		const z = obj.z;
33 | 		if (
34 | 			typeof x !== 'number' ||
35 | 			typeof y !== 'number' ||
36 | 			typeof z !== 'number'
37 | 		) {
38 | 			return false;
39 | 		}
40 | 
41 | 		return true;
42 | 	}
43 | 
44 | 	public static equals(v1: Point3d, v2: Point3d): boolean {
45 | 		return v1.x === v2.x && v1.y === v2.y && v1.z === v2.z;
46 | 	}
47 | 
48 | 	public toObject(): Point3dObject {
49 | 		return {
50 | 			x: this.x,
51 | 			y: this.y,
52 | 			z: this.z,
53 | 		};
54 | 	}
55 | }
56 | 
57 | export const Point3dAssembly: PointNdAssembly<Point3d> = {
58 | 	toComponents: (p: Point3d) => p.getComponents(),
59 | 	fromComponents: (comps: number[]) => new Point3d(...comps),
60 | };
61 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-4d/converter/point-4d-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {BindingTarget} from '../../../common/binding/target.js';
 5 | import {Point4d} from '../model/point-4d.js';
 6 | import {writePoint4d} from './point-4d.js';
 7 | 
 8 | describe(writePoint4d.name, () => {
 9 | 	it('should write value without destruction', () => {
10 | 		const obj = {
11 | 			foo: {x: 12, y: 34, z: -56, w: 78},
12 | 		};
13 | 		const objFoo = obj.foo;
14 | 		const t = new BindingTarget(obj, 'foo');
15 | 		writePoint4d(t, new Point4d(56, 78, 901, 23));
16 | 
17 | 		assert.strictEqual(obj.foo, objFoo);
18 | 		assert.strictEqual(obj.foo.x, 56);
19 | 		assert.strictEqual(obj.foo.y, 78);
20 | 		assert.strictEqual(obj.foo.z, 901);
21 | 		assert.strictEqual(obj.foo.w, 23);
22 | 	});
23 | });
24 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-4d/converter/point-4d.ts:
--------------------------------------------------------------------------------
 1 | import {BindingTarget} from '../../../common/binding/target.js';
 2 | import {Point4d} from '../model/point-4d.js';
 3 | 
 4 | export function point4dFromUnknown(value: unknown): Point4d {
 5 | 	return Point4d.isObject(value)
 6 | 		? new Point4d(value.x, value.y, value.z, value.w)
 7 | 		: new Point4d();
 8 | }
 9 | 
10 | export function writePoint4d(target: BindingTarget, value: Point4d) {
11 | 	target.writeProperty('x', value.x);
12 | 	target.writeProperty('y', value.y);
13 | 	target.writeProperty('z', value.z);
14 | 	target.writeProperty('w', value.w);
15 | }
16 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-4d/model/point-4d-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe as context, describe, it} from 'mocha';
 3 | 
 4 | import {Point4d} from './point-4d.js';
 5 | 
 6 | describe(Point4d.name, () => {
 7 | 	[
 8 | 		{
 9 | 			object: null,
10 | 			expected: false,
11 | 		},
12 | 		{
13 | 			object: undefined,
14 | 			expected: false,
15 | 		},
16 | 		{
17 | 			object: {x: 0, y: 1},
18 | 			expected: false,
19 | 		},
20 | 		{
21 | 			object: {x: -1, y: 0, z: 1},
22 | 			expected: false,
23 | 		},
24 | 		{
25 | 			object: {x: -1, y: 0, z: 1, w: 0},
26 | 			expected: true,
27 | 		},
28 | 		{
29 | 			object: {x: 1, y: 2, z: 3, w: '4'},
30 | 			expected: false,
31 | 		},
32 | 	].forEach((testCase) => {
33 | 		context(`when object = ${JSON.stringify(testCase.object)}`, () => {
34 | 			it(`should regard input as object: ${testCase.expected}`, () => {
35 | 				assert.strictEqual(
36 | 					Point4d.isObject(testCase.object),
37 | 					testCase.expected,
38 | 				);
39 | 			});
40 | 		});
41 | 	});
42 | 
43 | 	[
44 | 		{
45 | 			object: new Point4d(0, 1, 2),
46 | 			expected: {x: 0, y: 1, z: 2, w: 0},
47 | 		},
48 | 		{
49 | 			object: new Point4d(),
50 | 			expected: {x: 0, y: 0, z: 0, w: 0},
51 | 		},
52 | 		{
53 | 			object: new Point4d(-100),
54 | 			expected: {x: -100, y: 0, z: 0, w: 0},
55 | 		},
56 | 		{
57 | 			object: new Point4d(1, 2, 3, 4),
58 | 			expected: {x: 1, y: 2, z: 3, w: 4},
59 | 		},
60 | 	].forEach((testCase) => {
61 | 		context(`when Point3d = ${JSON.stringify(testCase.object)}`, () => {
62 | 			it(`should convert into object: ${testCase.expected}`, () => {
63 | 				assert.deepStrictEqual(testCase.object.toObject(), testCase.expected);
64 | 			});
65 | 		});
66 | 	});
67 | });
68 | 


--------------------------------------------------------------------------------
/packages/core/src/input-binding/point-4d/model/point-4d.ts:
--------------------------------------------------------------------------------
 1 | import {isEmpty, Tuple4} from '../../../misc/type-util.js';
 2 | import {PointNdAssembly} from '../../common/model/point-nd.js';
 3 | 
 4 | export interface Point4dObject {
 5 | 	x: number;
 6 | 	y: number;
 7 | 	z: number;
 8 | 	w: number;
 9 | }
10 | 
11 | export class Point4d {
12 | 	public x: number;
13 | 	public y: number;
14 | 	public z: number;
15 | 	public w: number;
16 | 
17 | 	constructor(x = 0, y = 0, z = 0, w = 0) {
18 | 		this.x = x;
19 | 		this.y = y;
20 | 		this.z = z;
21 | 		this.w = w;
22 | 	}
23 | 
24 | 	public getComponents(): Tuple4<number> {
25 | 		return [this.x, this.y, this.z, this.w];
26 | 	}
27 | 
28 | 	public static isObject(obj: any): obj is Point4dObject {
29 | 		if (isEmpty(obj)) {
30 | 			return false;
31 | 		}
32 | 
33 | 		const x = obj.x;
34 | 		const y = obj.y;
35 | 		const z = obj.z;
36 | 		const w = obj.w;
37 | 		if (
38 | 			typeof x !== 'number' ||
39 | 			typeof y !== 'number' ||
40 | 			typeof z !== 'number' ||
41 | 			typeof w !== 'number'
42 | 		) {
43 | 			return false;
44 | 		}
45 | 
46 | 		return true;
47 | 	}
48 | 
49 | 	public static equals(v1: Point4d, v2: Point4d): boolean {
50 | 		return v1.x === v2.x && v1.y === v2.y && v1.z === v2.z && v1.w === v2.w;
51 | 	}
52 | 
53 | 	public toObject(): Point4dObject {
54 | 		return {
55 | 			x: this.x,
56 | 			y: this.y,
57 | 			z: this.z,
58 | 			w: this.w,
59 | 		};
60 | 	}
61 | }
62 | 
63 | export const Point4dAssembly: PointNdAssembly<Point4d> = {
64 | 	toComponents: (p: Point4d) => p.getComponents(),
65 | 	fromComponents: (comps: number[]) => new Point4d(...comps),
66 | };
67 | 


--------------------------------------------------------------------------------
/packages/core/src/misc/constants.ts:
--------------------------------------------------------------------------------
1 | export const Constants = {
2 | 	monitor: {
3 | 		defaultInterval: 200,
4 | 		defaultRows: 3,
5 | 	},
6 | };
7 | 


--------------------------------------------------------------------------------
/packages/core/src/misc/dom-test-util.ts:
--------------------------------------------------------------------------------
1 | import {JSDOM} from 'jsdom';
2 | 
3 | import {forceCast} from './type-util.js';
4 | 
5 | export function createTestWindow(): Window {
6 | 	return forceCast(new JSDOM('').window);
7 | }
8 | 


--------------------------------------------------------------------------------
/packages/core/src/misc/semver-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe as context, describe, it} from 'mocha';
 3 | 
 4 | import {Semver} from './semver.js';
 5 | 
 6 | describe(Semver.name, () => {
 7 | 	[
 8 | 		{
 9 | 			expected: {
10 | 				major: 0,
11 | 				minor: 0,
12 | 				patch: 0,
13 | 				prerelease: null,
14 | 				text: '0.0.0',
15 | 			},
16 | 			text: '0.0.0',
17 | 		},
18 | 		{
19 | 			expected: {
20 | 				major: 3,
21 | 				minor: 14,
22 | 				patch: 16,
23 | 				prerelease: null,
24 | 				text: '3.14.16',
25 | 			},
26 | 			text: '3.14.16',
27 | 		},
28 | 		{
29 | 			expected: {
30 | 				major: 0,
31 | 				minor: 1,
32 | 				patch: 100,
33 | 				prerelease: null,
34 | 				text: '0.1.100',
35 | 			},
36 | 			text: '0.01.0100',
37 | 		},
38 | 		{
39 | 			expected: {
40 | 				major: 1,
41 | 				minor: 2,
42 | 				patch: 3,
43 | 				prerelease: 'beta.0',
44 | 				text: '1.2.3-beta.0',
45 | 			},
46 | 			text: '1.2.3-beta.0',
47 | 		},
48 | 		{
49 | 			expected: {
50 | 				major: 1,
51 | 				minor: 1,
52 | 				patch: 5,
53 | 				prerelease: '0',
54 | 				text: '1.1.5-0',
55 | 			},
56 | 			text: '1.1.5-0',
57 | 		},
58 | 	].forEach(({expected, text}) => {
59 | 		context(`when ${JSON.stringify(text)}`, () => {
60 | 			it('should compare array deeply', () => {
61 | 				const semver = new Semver(text);
62 | 				assert.strictEqual(semver.major, expected.major);
63 | 				assert.strictEqual(semver.minor, expected.minor);
64 | 				assert.strictEqual(semver.patch, expected.patch);
65 | 				assert.strictEqual(semver.prerelease, expected.prerelease);
66 | 				assert.strictEqual(semver.toString(), expected.text);
67 | 			});
68 | 		});
69 | 	});
70 | });
71 | 


--------------------------------------------------------------------------------
/packages/core/src/misc/semver.ts:
--------------------------------------------------------------------------------
 1 | /***
 2 |  * A simple semantic versioning perser.
 3 |  */
 4 | export class Semver {
 5 | 	public readonly major: number;
 6 | 	public readonly minor: number;
 7 | 	public readonly patch: number;
 8 | 	public readonly prerelease: string | null;
 9 | 
10 | 	/**
11 | 	 * @hidden
12 | 	 */
13 | 	constructor(text: string) {
14 | 		const [core, prerelease] = text.split('-');
15 | 		const coreComps = core.split('.');
16 | 		this.major = parseInt(coreComps[0], 10);
17 | 		this.minor = parseInt(coreComps[1], 10);
18 | 		this.patch = parseInt(coreComps[2], 10);
19 | 		this.prerelease = prerelease ?? null;
20 | 	}
21 | 
22 | 	public toString(): string {
23 | 		const core = [this.major, this.minor, this.patch].join('.');
24 | 		return this.prerelease !== null ? [core, this.prerelease].join('-') : core;
25 | 	}
26 | }
27 | 


--------------------------------------------------------------------------------
/packages/core/src/misc/test-util.ts:
--------------------------------------------------------------------------------
 1 | export const TestUtil = {
 2 | 	createEvent: (
 3 | 		win: Window,
 4 | 		type: string,
 5 | 		options?: Record<string, unknown>,
 6 | 	): Event => {
 7 | 		return options
 8 | 			? new (win as any).Event(type, options)
 9 | 			: new (win as any).Event(type);
10 | 	},
11 | 
12 | 	createKeyboardEvent: (
13 | 		win: Window,
14 | 		type: string,
15 | 		options: Record<string, unknown>,
16 | 	): Event => {
17 | 		return new (win as any).KeyboardEvent(type, options);
18 | 	},
19 | 
20 | 	closeTo: (actual: number, expected: number, delta: number): boolean => {
21 | 		return Math.abs(actual - expected) < delta;
22 | 	},
23 | };
24 | 


--------------------------------------------------------------------------------
/packages/core/src/monitor-binding/common/controller/multi-log.ts:
--------------------------------------------------------------------------------
 1 | import {BufferedValueController} from '../../../blade/binding/controller/monitor-binding.js';
 2 | import {Formatter} from '../../../common/converter/formatter.js';
 3 | import {BufferedValue} from '../../../common/model/buffered-value.js';
 4 | import {ViewProps} from '../../../common/model/view-props.js';
 5 | import {MultiLogView} from '../view/multi-log.js';
 6 | 
 7 | interface Config<T> {
 8 | 	formatter: Formatter<T>;
 9 | 	rows: number;
10 | 	value: BufferedValue<T>;
11 | 	viewProps: ViewProps;
12 | }
13 | 
14 | /**
15 |  * @hidden
16 |  */
17 | export class MultiLogController<T>
18 | 	implements BufferedValueController<T, MultiLogView<T>>
19 | {
20 | 	public readonly value: BufferedValue<T>;
21 | 	public readonly view: MultiLogView<T>;
22 | 	public readonly viewProps: ViewProps;
23 | 
24 | 	constructor(doc: Document, config: Config<T>) {
25 | 		this.value = config.value;
26 | 		this.viewProps = config.viewProps;
27 | 
28 | 		this.view = new MultiLogView(doc, {
29 | 			formatter: config.formatter,
30 | 			rows: config.rows,
31 | 			value: this.value,
32 | 			viewProps: this.viewProps,
33 | 		});
34 | 	}
35 | }
36 | 


--------------------------------------------------------------------------------
/packages/core/src/monitor-binding/common/controller/single-log.ts:
--------------------------------------------------------------------------------
 1 | import {BufferedValueController} from '../../../blade/binding/controller/monitor-binding.js';
 2 | import {Formatter} from '../../../common/converter/formatter.js';
 3 | import {BufferedValue} from '../../../common/model/buffered-value.js';
 4 | import {ViewProps} from '../../../common/model/view-props.js';
 5 | import {SingleLogView} from '../view/single-log.js';
 6 | 
 7 | interface Config<T> {
 8 | 	formatter: Formatter<T>;
 9 | 	value: BufferedValue<T>;
10 | 	viewProps: ViewProps;
11 | }
12 | 
13 | /**
14 |  * @hidden
15 |  */
16 | export class SingleLogController<T>
17 | 	implements BufferedValueController<T, SingleLogView<T>>
18 | {
19 | 	public readonly value: BufferedValue<T>;
20 | 	public readonly view: SingleLogView<T>;
21 | 	public readonly viewProps: ViewProps;
22 | 
23 | 	constructor(doc: Document, config: Config<T>) {
24 | 		this.value = config.value;
25 | 		this.viewProps = config.viewProps;
26 | 
27 | 		this.view = new SingleLogView(doc, {
28 | 			formatter: config.formatter,
29 | 			value: this.value,
30 | 			viewProps: this.viewProps,
31 | 		});
32 | 	}
33 | }
34 | 


--------------------------------------------------------------------------------
/packages/core/src/monitor-binding/number/api/graph-log.ts:
--------------------------------------------------------------------------------
 1 | import {BindingApi} from '../../../blade/binding/api/binding.js';
 2 | import {MonitorBindingApi} from '../../../blade/binding/api/monitor-binding.js';
 3 | import {MonitorBindingController} from '../../../blade/binding/controller/monitor-binding.js';
 4 | import {TpBuffer} from '../../../common/model/buffered-value.js';
 5 | import {GraphLogController} from '../controller/graph-log.js';
 6 | 
 7 | export class GraphLogMonitorBindingApi
 8 | 	extends BindingApi<
 9 | 		TpBuffer<number>,
10 | 		number,
11 | 		MonitorBindingController<number, GraphLogController>
12 | 	>
13 | 	implements MonitorBindingApi<number>
14 | {
15 | 	get max(): number {
16 | 		return this.controller.valueController.props.get('max');
17 | 	}
18 | 
19 | 	set max(max: number) {
20 | 		this.controller.valueController.props.set('max', max);
21 | 	}
22 | 
23 | 	get min(): number {
24 | 		return this.controller.valueController.props.get('min');
25 | 	}
26 | 
27 | 	set min(min: number) {
28 | 		this.controller.valueController.props.set('min', min);
29 | 	}
30 | }
31 | 


--------------------------------------------------------------------------------
/packages/core/src/monitor-binding/number/plugin-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe} from 'mocha';
 3 | 
 4 | import {MonitorBindingController} from '../../blade/binding/controller/monitor-binding.js';
 5 | import {BindingTarget} from '../../common/binding/target.js';
 6 | import {createTestWindow} from '../../misc/dom-test-util.js';
 7 | import {SingleLogController} from '../common/controller/single-log.js';
 8 | import {createMonitorBindingController} from '../plugin.js';
 9 | import {NumberMonitorPlugin} from './plugin.js';
10 | 
11 | describe(NumberMonitorPlugin.id, () => {
12 | 	it('should apply `format`', () => {
13 | 		const doc = createTestWindow().document;
14 | 		const obj = {
15 | 			foo: 1,
16 | 		};
17 | 		const bc = createMonitorBindingController(NumberMonitorPlugin, {
18 | 			document: doc,
19 | 			params: {
20 | 				format: () => 'formatted',
21 | 				interval: 0,
22 | 				readonly: true,
23 | 			},
24 | 			target: new BindingTarget(obj, 'foo'),
25 | 		}) as MonitorBindingController<number>;
26 | 
27 | 		const c = bc.valueController as SingleLogController<number>;
28 | 		assert.strictEqual(c.view.inputElement.value, 'formatted');
29 | 	});
30 | });
31 | 


--------------------------------------------------------------------------------
/packages/core/src/plugin/blade-api-cache-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {
 5 | 	createAppropriateBladeApi,
 6 | 	createAppropriateBladeController,
 7 | } from '../blade/test-util.js';
 8 | import {createTestWindow} from '../misc/dom-test-util.js';
 9 | import {BladeApiCache} from './blade-api-cache.js';
10 | 
11 | describe(BladeApiCache.name, () => {
12 | 	it('should add cache', () => {
13 | 		const doc = createTestWindow().document;
14 | 		const bc = createAppropriateBladeController(doc);
15 | 		const api = createAppropriateBladeApi(doc);
16 | 		const cache = new BladeApiCache();
17 | 
18 | 		assert.strictEqual(cache.get(bc), null);
19 | 		cache.add(bc, api);
20 | 		assert.strictEqual(cache.get(bc), api);
21 | 	});
22 | 
23 | 	it('should get existance', () => {
24 | 		const doc = createTestWindow().document;
25 | 		const bc = createAppropriateBladeController(doc);
26 | 		const api = createAppropriateBladeApi(doc);
27 | 		const cache = new BladeApiCache();
28 | 
29 | 		assert.strictEqual(cache.has(bc), false);
30 | 		cache.add(bc, api);
31 | 		assert.strictEqual(cache.has(bc), true);
32 | 	});
33 | 
34 | 	it('should remove disposed API', () => {
35 | 		const doc = createTestWindow().document;
36 | 		const bc = createAppropriateBladeController(doc);
37 | 		const api = createAppropriateBladeApi(doc);
38 | 		const cache = new BladeApiCache();
39 | 
40 | 		api.dispose();
41 | 		assert.strictEqual(cache.has(bc), false);
42 | 	});
43 | });
44 | 


--------------------------------------------------------------------------------
/packages/core/src/plugin/blade-api-cache.ts:
--------------------------------------------------------------------------------
 1 | import {BladeApi} from '../blade/common/api/blade.js';
 2 | import {BladeController} from '../blade/common/controller/blade.js';
 3 | 
 4 | /**
 5 |  * A cache that maps blade controllers and APIs.
 6 |  * @hidden
 7 |  */
 8 | export class BladeApiCache {
 9 | 	private map_: Map<BladeController, BladeApi> = new Map();
10 | 
11 | 	public get(bc: BladeController): BladeApi | null {
12 | 		return this.map_.get(bc) ?? null;
13 | 	}
14 | 
15 | 	public has(bc: BladeController): boolean {
16 | 		return this.map_.has(bc);
17 | 	}
18 | 
19 | 	public add(bc: BladeController, api: BladeApi): typeof api {
20 | 		this.map_.set(bc, api);
21 | 		bc.viewProps.handleDispose(() => {
22 | 			this.map_.delete(bc);
23 | 		});
24 | 		return api;
25 | 	}
26 | }
27 | 


--------------------------------------------------------------------------------
/packages/core/src/plugin/plugin.ts:
--------------------------------------------------------------------------------
 1 | import {Semver} from '../misc/semver.js';
 2 | import {VERSION} from '../version.js';
 3 | 
 4 | export type PluginType = 'blade' | 'input' | 'monitor';
 5 | 
 6 | /**
 7 |  * A base interface of the plugin.
 8 |  */
 9 | export interface BasePlugin {
10 | 	/**
11 | 	 * The identifier of the plugin.
12 | 	 */
13 | 	id: string;
14 | 
15 | 	/**
16 | 	 * The type of the plugin.
17 | 	 */
18 | 	type: PluginType;
19 | 
20 | 	/**
21 | 	 * The version of the core used for this plugin.
22 | 	 */
23 | 	core?: Semver;
24 | }
25 | 
26 | /**
27 |  * Creates a plugin with the current core.
28 |  * @param plugin The plugin without the core version.
29 |  * @return A plugin with the core version.
30 |  */
31 | export function createPlugin<P extends BasePlugin>(plugin: Omit<P, 'core'>): P {
32 | 	return {
33 | 		core: VERSION,
34 | 		...plugin,
35 | 	} as P;
36 | }
37 | 


--------------------------------------------------------------------------------
/packages/core/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": "../../../tsconfig.json",
3 | 	"compilerOptions": {
4 | 		"declaration": true,
5 | 		"outDir": "../dist"
6 | 	},
7 | 	"exclude": ["**/*-test.ts"]
8 | }
9 | 


--------------------------------------------------------------------------------
/packages/core/src/version.ts:
--------------------------------------------------------------------------------
1 | import {Semver} from './misc/semver.js';
2 | 
3 | export const VERSION = new Semver('0.0.0-core.0');
4 | 


--------------------------------------------------------------------------------
/packages/tweakpane/.npmignore:
--------------------------------------------------------------------------------
1 | .circleci/
2 | .nyc_output/
3 | docs/
4 | pages/
5 | test/
6 | 
7 | .editorconfig
8 | 


--------------------------------------------------------------------------------
/packages/tweakpane/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | 


--------------------------------------------------------------------------------
/packages/tweakpane/rollup-doc.config.js:
--------------------------------------------------------------------------------
 1 | import Alias from '@rollup/plugin-alias';
 2 | import {nodeResolve} from '@rollup/plugin-node-resolve';
 3 | import Typescript from '@rollup/plugin-typescript';
 4 | import Cleanup from 'rollup-plugin-cleanup';
 5 | import {terser as Terser} from 'rollup-plugin-terser';
 6 | 
 7 | export default async () => {
 8 | 	return {
 9 | 		input: 'src/doc/ts/bundle.ts',
10 | 		external: ['dat.gui', 'tweakpane'],
11 | 		output: {
12 | 			file: `docs/assets/bundle.js`,
13 | 			format: 'umd',
14 | 			globals: {
15 | 				'dat.gui': 'dat',
16 | 				tweakpane: 'Tweakpane',
17 | 			},
18 | 		},
19 | 		plugins: [
20 | 			Alias({
21 | 				entries: [
22 | 					{
23 | 						find: '@tweakpane/core',
24 | 						replacement: '../../node_modules/@tweakpane/core/dist/index.js',
25 | 					},
26 | 				],
27 | 			}),
28 | 			Typescript({
29 | 				tsconfig: 'src/doc/tsconfig.json',
30 | 			}),
31 | 			nodeResolve({
32 | 				preferBuiltins: false,
33 | 			}),
34 | 			Terser(),
35 | 			// https://github.com/microsoft/tslib/issues/47
36 | 			Cleanup({
37 | 				comments: 'none',
38 | 			}),
39 | 		],
40 | 
41 | 		onwarn(warning, warn) {
42 | 			if (warning.code === 'CIRCULAR_DEPENDENCY') {
43 | 				return;
44 | 			}
45 | 			warn(warning);
46 | 		},
47 | 	};
48 | };
49 | 


--------------------------------------------------------------------------------
/packages/tweakpane/scripts/assets-version.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable no-console */
 2 | /* eslint-env node */
 3 | 
 4 | import Fs from 'fs';
 5 | import Glob from 'glob';
 6 | import Path from 'path';
 7 | 
 8 | const Package = JSON.parse(
 9 | 	Fs.readFileSync(new URL('../package.json', import.meta.url)),
10 | );
11 | 
12 | const PATTERN = 'dist/*';
13 | 
14 | const paths = Glob.sync(PATTERN);
15 | paths.forEach((path) => {
16 | 	const fileName = Path.basename(path);
17 | 	if (Fs.statSync(path).isDirectory()) {
18 | 		return;
19 | 	}
20 | 
21 | 	const ext = fileName.match(/(\..+)$/)[1];
22 | 	const base = Path.basename(fileName, ext);
23 | 	const versionedPath = Path.join(
24 | 		Path.dirname(path),
25 | 		`${base}-${Package.version}${ext}`,
26 | 	);
27 | 	Fs.renameSync(path, versionedPath);
28 | });
29 | 


--------------------------------------------------------------------------------
/packages/tweakpane/scripts/doc-build-html.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable no-console */
 2 | /* eslint-env node */
 3 | 
 4 | import Fs from 'fs-extra';
 5 | import Glob from 'glob';
 6 | import Nunjucks from 'nunjucks';
 7 | import Path from 'path';
 8 | 
 9 | const Package = JSON.parse(
10 | 	Fs.readFileSync(new URL('../package.json', import.meta.url)),
11 | );
12 | 
13 | const context = {
14 | 	description: Package.description,
15 | 	version: Package.version,
16 | };
17 | 
18 | const SRC_DIR = 'src/doc/template';
19 | const SRC_PATTERN = 'src/doc/template/**/*.html';
20 | const DST_DIR = 'docs';
21 | 
22 | Fs.mkdirsSync(DST_DIR);
23 | 
24 | const srcPaths = Glob.sync(SRC_PATTERN).filter((path) => {
25 | 	return Path.basename(path).match(/^_.+$/) === null;
26 | });
27 | console.log('Found sources:');
28 | console.log(srcPaths.map((path) => `  ${path}`).join('\n'));
29 | 
30 | const env = new Nunjucks.Environment(new Nunjucks.FileSystemLoader(SRC_DIR));
31 | console.log('Compiling...');
32 | srcPaths.forEach((srcPath) => {
33 | 	const srcBase = Path.relative(SRC_DIR, srcPath);
34 | 	const dstPath = Path.join(DST_DIR, srcBase);
35 | 
36 | 	const dstDir = Path.dirname(dstPath);
37 | 	if (!Fs.existsSync(dstDir)) {
38 | 		Fs.mkdirsSync(dstDir);
39 | 	}
40 | 
41 | 	console.log(`  ${dstPath}`);
42 | 	Fs.writeFileSync(dstPath, env.render(srcBase, context));
43 | });
44 | 
45 | console.log('done.');
46 | 


--------------------------------------------------------------------------------
/packages/tweakpane/scripts/main-test-ts-module-pre.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable no-console */
 2 | /* eslint-env node */
 3 | 
 4 | import Fs from 'fs';
 5 | 
 6 | const corePackage = JSON.parse(
 7 | 	Fs.readFileSync(new URL('../../core/package.json', import.meta.url)),
 8 | );
 9 | const panePackage = JSON.parse(
10 | 	Fs.readFileSync(new URL('../package.json', import.meta.url)),
11 | );
12 | 
13 | // Remove version of core tgz file
14 | process.chdir('../core');
15 | 
16 | const coreTgz = `tweakpane-core-${corePackage.version}.tgz`;
17 | if (Fs.existsSync(coreTgz)) {
18 | 	Fs.renameSync(coreTgz, 'tweakpane-core.tgz');
19 | }
20 | 
21 | // Remove version of tweakpane tgz file
22 | process.chdir('../tweakpane');
23 | 
24 | const paneTgz = `tweakpane-${panePackage.version}.tgz`;
25 | if (Fs.existsSync(paneTgz)) {
26 | 	Fs.renameSync(paneTgz, 'tweakpane.tgz');
27 | }
28 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/img/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cocopon/tweakpane/c7c99a0e6c9779483c6c9dffa83e9aab255cbed9/packages/tweakpane/src/doc/img/og.png


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/_pages.scss:
--------------------------------------------------------------------------------
 1 | @use './defs';
 2 | 
 3 | :root.index {
 4 | 	.pageHeader {
 5 | 		background-image: none;
 6 | 	}
 7 | 	.pageHeader_inner {
 8 | 		position: static;
 9 | 
10 | 		@include defs.wide() {
11 | 			padding-bottom: 128px;
12 | 			padding-top: 128px;
13 | 		}
14 | 	}
15 | 	.pageHeader_text {
16 | 		mix-blend-mode: difference;
17 | 	}
18 | 	.pageHeader_title {
19 | 		color: white;
20 | 	}
21 | 	.pageHeader_text p {
22 | 		color: white;
23 | 
24 | 		@include defs.nonwide() {
25 | 			hyphens: auto;
26 | 		}
27 | 	}
28 | 	.pageHeader_pane {
29 | 		position: relative;
30 | 	}
31 | }
32 | 
33 | :root.theming {
34 | 	.pageHeader {
35 | 		background-color: #21292a;
36 | 		background-image: url(https://source.unsplash.com/u27Rrbs9Dwc/1600x900);
37 | 		background-position: center;
38 | 		background-repeat: no-repeat;
39 | 		background-size: cover;
40 | 
41 | 		&::before {
42 | 			background-image: radial-gradient(
43 | 				rgba(255, 255, 255, 0.1) 1px,
44 | 				transparent 0
45 | 			);
46 | 			background-position: center;
47 | 			background-size: 16px 16px;
48 | 			content: '';
49 | 			inset: 0;
50 | 			position: absolute;
51 | 		}
52 | 
53 | 		.paneContainer {
54 | 			-webkit-backdrop-filter: blur(4px);
55 | 			backdrop-filter: blur(4px);
56 | 		}
57 | 	}
58 | 	.pageHeader_title {
59 | 		h1,
60 | 		p {
61 | 			color: rgba(white, 0.8);
62 | 		}
63 | 	}
64 | }
65 | 
66 | :root.catalog {
67 | 	&.readme {
68 | 		.main_menu {
69 | 			display: none;
70 | 		}
71 | 		.main_wrap {
72 | 			max-width: 1088px;
73 | 		}
74 | 		.paneCatalog {
75 | 			border-radius: 0;
76 | 			padding: 64px;
77 | 		}
78 | 	}
79 | 
80 | 	.tp-dfwv {
81 | 		position: fixed;
82 | 	}
83 | 	&.readme .tp-dfwv {
84 | 		display: none;
85 | 	}
86 | }
87 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/_reset.scss:
--------------------------------------------------------------------------------
 1 | html,
 2 | body,
 3 | pre {
 4 | 	margin: 0;
 5 | 	padding: 0;
 6 | }
 7 | h1,
 8 | h2,
 9 | h3,
10 | h4,
11 | h5,
12 | h6 {
13 | 	font-size: 1rem;
14 | 	margin: 0;
15 | 	padding: 0;
16 | }
17 | p {
18 | 	margin: 0;
19 | 	padding: 0;
20 | }
21 | pre {
22 | 	display: block;
23 | }
24 | dl,
25 | ol,
26 | ul {
27 | 	list-style-type: none;
28 | 	margin: 0;
29 | 	padding: 0;
30 | }
31 | li {
32 | 	margin: 0;
33 | 	padding: 0;
34 | }
35 | button {
36 | 	appearance: none;
37 | 	background-color: transparent;
38 | 	border-width: 0;
39 | 	cursor: pointer;
40 | 	font-family: inherit;
41 | 	font-size: inherit;
42 | 	padding: 0;
43 | }
44 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/bundle.scss:
--------------------------------------------------------------------------------
 1 | @use './reset';
 2 | @use './base';
 3 | 
 4 | @use './components/catalog';
 5 | @use './components/code-block';
 6 | @use './components/concept';
 7 | @use './components/demo';
 8 | @use './components/global-footer';
 9 | @use './components/global-header';
10 | @use './components/global-nav';
11 | @use './components/hljs';
12 | @use './components/logo';
13 | @use './components/main';
14 | @use './components/migration';
15 | @use './components/page-header';
16 | @use './components/pane-container';
17 | @use './components/photo-credit';
18 | @use './components/rel';
19 | @use './components/sponsor';
20 | @use './components/theme-builder';
21 | 
22 | @use './pages';
23 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_catalog.scss:
--------------------------------------------------------------------------------
 1 | .paneCatalog {
 2 | 	background-color: var(--bg-color-secondary);
 3 | 	border-radius: 6px;
 4 | 	display: grid;
 5 | 	gap: 32px;
 6 | 	grid-template-columns: repeat(auto-fit, 256px);
 7 | 	justify-content: center;
 8 | 	padding: 32px;
 9 | 
10 | 	&#{&}-theming {
11 | 		background-color: hsl(230, 7%, 57%);
12 | 	}
13 | }
14 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_code-block.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .codeBlock {
 4 | 	position: relative;
 5 | 
 6 | 	&#{&}-compact {
 7 | 		@media screen {
 8 | 			max-height: 256px;
 9 | 		}
10 | 	}
11 | 
12 | 	pre {
13 | 		overflow: auto;
14 | 		padding: 32px;
15 | 
16 | 		@include defs.nonwide() {
17 | 			padding: 24px;
18 | 		}
19 | 
20 | 		code {
21 | 			font-size: 0.9rem;
22 | 			font-weight: 500;
23 | 		}
24 | 	}
25 | 	&#{&}-fig {
26 | 		pre {
27 | 			line-height: 0.9;
28 | 		}
29 | 		pre code {
30 | 			font-size: 0.8rem;
31 | 		}
32 | 	}
33 | }
34 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_concept.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .concepts {
 4 | 	display: grid;
 5 | 	margin-bottom: 1em;
 6 | 	margin-top: 1em;
 7 | 
 8 | 	@include defs.wide() {
 9 | 		gap: 16px;
10 | 		grid-template-columns: repeat(3, 1fr);
11 | 	}
12 | 	@include defs.nonwide() {
13 | 		gap: 8px;
14 | 		grid-template-columns: repeat(1, 1fr);
15 | 	}
16 | }
17 | 
18 | .concept {
19 | 	border: var(--bg-color-action) solid 2px;
20 | 	border-radius: 6px;
21 | 	padding: 24px 32px;
22 | 
23 | 	&_title {
24 | 		font-weight: bold;
25 | 	}
26 | 	&_detail {
27 | 		color: var(--fg-color-secondary);
28 | 		margin-top: 16px;
29 | 	}
30 | }
31 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_demo.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .demo {
 4 | 	background-color: var(--bg-color-secondary);
 5 | 	border-radius: 6px;
 6 | 	position: relative;
 7 | 
 8 | 	@include defs.wide() {
 9 | 		display: flex;
10 | 	}
11 | 
12 | 	&_code {
13 | 		display: flex;
14 | 		flex: 3;
15 | 		overflow: auto;
16 | 
17 | 		.codeBlock {
18 | 			flex: 1;
19 | 		}
20 | 	}
21 | 	&_result {
22 | 		@include defs.dotGrid();
23 | 
24 | 		flex: 2;
25 | 		padding: 32px;
26 | 		text-align: center;
27 | 	}
28 | 	&_chip {
29 | 		background-color: var(--hl-comment);
30 | 		border-bottom-left-radius: 1px;
31 | 		color: var(--bg-color-secondary);
32 | 		font-family: defs.$font-family-mono;
33 | 		font-size: 0.7rem;
34 | 		font-weight: 500;
35 | 		padding: 0.2em 0.5em;
36 | 		position: absolute;
37 | 		right: 0;
38 | 		top: 0;
39 | 	}
40 | }
41 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_global-footer.scss:
--------------------------------------------------------------------------------
 1 | .globalFooter {
 2 | 	text-align: center;
 3 | 
 4 | 	&_inner {
 5 | 		box-sizing: border-box;
 6 | 		margin-left: auto;
 7 | 		margin-right: auto;
 8 | 		padding-bottom: 16px;
 9 | 		padding-top: 16px;
10 | 	}
11 | 	&_copy {
12 | 		color: var(--fg-color-secondary);
13 | 		font-size: 0.8rem;
14 | 
15 | 		a {
16 | 			color: inherit;
17 | 		}
18 | 	}
19 | }
20 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_global-header.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .globalHeader {
 4 | 	-webkit-backdrop-filter: blur(2px);
 5 | 	backdrop-filter: blur(2px);
 6 | 	background-color: var(--bg-color-translucent);
 7 | 	box-shadow: 0 2px 4px rgba(black, 0.05);
 8 | 	color: inherit;
 9 | 	height: defs.$global-header-height;
10 | 	left: 0;
11 | 	line-height: defs.$global-header-height;
12 | 	position: fixed;
13 | 	right: 0;
14 | 	text-align: center;
15 | 	top: 0;
16 | 	z-index: defs.$z-index-global-header;
17 | 
18 | 	&_inner {
19 | 		@include defs.responsiveContainer();
20 | 
21 | 		box-sizing: border-box;
22 | 		margin-left: auto;
23 | 		margin-right: auto;
24 | 		position: relative;
25 | 	}
26 | 	&_logo {
27 | 		display: inline-block;
28 | 	}
29 | 	&_spMenuButton {
30 | 		left: 0;
31 | 		position: absolute;
32 | 		top: 0;
33 | 	}
34 | }
35 | 
36 | .spMenuButton {
37 | 	align-items: center;
38 | 	color: inherit;
39 | 	display: flex;
40 | 	height: defs.$global-header-height;
41 | 	justify-content: center;
42 | 	width: defs.$global-header-height;
43 | }
44 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_hljs.scss:
--------------------------------------------------------------------------------
 1 | $prefix: 'hljs';
 2 | 
 3 | .#{$prefix}-attr {
 4 | 	color: var(--fg-color);
 5 | }
 6 | .#{$prefix}-comment {
 7 | 	color: var(--hl-comment);
 8 | }
 9 | .#{$prefix}-keyword {
10 | 	color: var(--hl-keyword);
11 | }
12 | .#{$prefix}-literal,
13 | .#{$prefix}-number {
14 | 	color: var(--hl-constant);
15 | }
16 | .#{$prefix}-string {
17 | 	color: var(--hl-string);
18 | }
19 | .#{$prefix}-tag {
20 | 	color: var(--hl-keyword);
21 | }
22 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_main.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .main {
 4 | 	overflow: hidden;
 5 | 
 6 | 	&_globalHeader {
 7 | 		@include defs.wide() {
 8 | 			display: none;
 9 | 		}
10 | 		@include defs.nonwide() {
11 | 			height: defs.$global-header-height;
12 | 		}
13 | 		@media print {
14 | 			display: none;
15 | 		}
16 | 	}
17 | 	&_wrap {
18 | 		@include defs.responsiveContainer();
19 | 
20 | 		@include defs.wide() {
21 | 			display: flex;
22 | 			margin-top: 32px;
23 | 		}
24 | 	}
25 | 	&_menu {
26 | 		@include defs.wide() {
27 | 			margin-right: 32px;
28 | 			width: defs.$global-nav-width;
29 | 		}
30 | 		@include defs.nonwide() {
31 | 			margin-right: 0;
32 | 			width: 0;
33 | 		}
34 | 		@media print {
35 | 			display: none;
36 | 		}
37 | 	}
38 | 	&_main {
39 | 		flex: 1;
40 | 
41 | 		@include defs.wide() {
42 | 			left: defs.$global-nav-width;
43 | 			overflow: hidden;
44 | 			top: 0;
45 | 		}
46 | 		@include defs.nonwide() {
47 | 			left: 0;
48 | 			top: defs.$global-header-height;
49 | 		}
50 | 	}
51 | 	&_pageHeader,
52 | 	&_inner {
53 | 		@include defs.document();
54 | 
55 | 		flex: 1;
56 | 
57 | 		h1 + p {
58 | 			margin-top: 1em;
59 | 			padding-left: 0.1em;
60 | 			padding-right: 0.1em;
61 | 		}
62 | 	}
63 | 	&_pageHeader {
64 | 		@include defs.middle() {
65 | 			margin-left: -32px;
66 | 			margin-right: -32px;
67 | 		}
68 | 		@include defs.narrow() {
69 | 			margin-left: -16px;
70 | 			margin-right: -16px;
71 | 		}
72 | 	}
73 | 	&_inner {
74 | 		@include defs.wide() {
75 | 			padding-bottom: 64px;
76 | 			padding-top: 64px;
77 | 		}
78 | 		@include defs.nonwide() {
79 | 			padding-bottom: 32px;
80 | 			padding-top: 32px;
81 | 		}
82 | 	}
83 | 	&_media {
84 | 		margin-bottom: 1.5em;
85 | 		margin-top: 1.5em;
86 | 	}
87 | }
88 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_migration.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .comparison {
 4 | 	& > *:first-child {
 5 | 		border-top-left-radius: 6px;
 6 | 		border-top-right-radius: 6px;
 7 | 	}
 8 | 	& > *:last-child {
 9 | 		border-bottom-left-radius: 6px;
10 | 		border-bottom-right-radius: 6px;
11 | 	}
12 | 
13 | 	@include defs.nonwide() {
14 | 		overflow-x: auto;
15 | 	}
16 | 
17 | 	&_codes,
18 | 	&_results,
19 | 	&_texts {
20 | 		background-color: var(--bg-color-secondary);
21 | 		display: flex;
22 | 
23 | 		@include defs.nonwide() {
24 | 			min-width: 640px;
25 | 		}
26 | 	}
27 | 	&_codes {
28 | 		align-items: stretch;
29 | 	}
30 | 	&_code {
31 | 		display: flex;
32 | 		flex: 1;
33 | 		overflow-x: auto;
34 | 	}
35 | 	&_result {
36 | 		@include defs.dotGrid();
37 | 
38 | 		flex: 1;
39 | 		padding-bottom: 32px;
40 | 		padding-top: 32px;
41 | 	}
42 | 	&_text {
43 | 		display: flex;
44 | 		flex: 1;
45 | 		justify-content: center;
46 | 		padding: 32px;
47 | 	}
48 | 	&_code + &_code,
49 | 	&_result + &_result,
50 | 	&_text + &_text {
51 | 		border-left: var(--bg-color) solid 1px;
52 | 	}
53 | }
54 | 
55 | // dat.GUI
56 | .datgui {
57 | 	.dg {
58 | 		li + li {
59 | 			margin-top: 0;
60 | 		}
61 | 		li.save-row .button {
62 | 			margin-left: 4px;
63 | 		}
64 | 	}
65 | }
66 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_photo-credit.scss:
--------------------------------------------------------------------------------
 1 | .photoCredit {
 2 | 	bottom: 8px;
 3 | 	color: rgba(white, 0.5);
 4 | 	font-size: 0.6rem;
 5 | 	mix-blend-mode: exclusion;
 6 | 	position: absolute;
 7 | 	right: 8px;
 8 | 
 9 | 	a {
10 | 		color: currentColor;
11 | 	}
12 | }
13 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_rel.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .rel {
 4 | 	display: grid;
 5 | 	margin-bottom: 1em;
 6 | 	margin-top: 1em;
 7 | 
 8 | 	@include defs.wide() {
 9 | 		gap: 16px;
10 | 		grid-template-columns: repeat(2, 1fr);
11 | 	}
12 | 	@include defs.nonwide() {
13 | 		gap: 8px;
14 | 		grid-template-columns: repeat(1, 1fr);
15 | 	}
16 | }
17 | .relItem {
18 | 	align-items: stretch;
19 | 	display: flex;
20 | 
21 | 	a#{&}_anchor {
22 | 		align-items: center;
23 | 		background-color: var(--bg-color-action);
24 | 		border-radius: 6px;
25 | 		color: var(--fg-color);
26 | 		display: flex;
27 | 		flex: 1;
28 | 		padding: 24px 32px;
29 | 		text-decoration: none;
30 | 
31 | 		&:focus,
32 | 		&:hover {
33 | 			background-color: var(--bg-color-action-active);
34 | 		}
35 | 	}
36 | 	&_icon {
37 | 		align-items: center;
38 | 		border: transparent solid 2px;
39 | 		border-radius: 50%;
40 | 		display: flex;
41 | 		height: 2em;
42 | 		justify-content: center;
43 | 		opacity: 0.5;
44 | 		width: 2em;
45 | 
46 | 		&#{&}-circle {
47 | 			border-color: currentColor;
48 | 		}
49 | 	}
50 | 	&_text {
51 | 		display: flex;
52 | 		flex: 1;
53 | 		flex-direction: column;
54 | 	}
55 | 	&_icon + &_text,
56 | 	&_text + &_icon {
57 | 		margin-left: 32px;
58 | 	}
59 | 	&#{&}-oneline a#{&}_anchor {
60 | 		justify-content: center;
61 | 	}
62 | 	&_title {
63 | 		font-weight: 500;
64 | 	}
65 | 	&_detail {
66 | 		color: var(--fg-color-secondary);
67 | 		line-height: 1.3;
68 | 		margin-top: 8px;
69 | 	}
70 | }
71 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_sponsor.scss:
--------------------------------------------------------------------------------
 1 | ul.sponsors {
 2 | 	display: grid;
 3 | 	grid-template-columns: repeat(auto-fill, 128px);
 4 | 	gap: 32px;
 5 | 	padding-left: 0;
 6 | 
 7 | 	li {
 8 | 		list-style-type: none;
 9 | 	}
10 | 	li + li {
11 | 		margin-top: 0;
12 | 	}
13 | }
14 | 
15 | .sponsor {
16 | 	a#{&}_anchor {
17 | 		color: inherit;
18 | 		display: block;
19 | 		text-decoration: none;
20 | 
21 | 		&:hover {
22 | 			text-decoration: underline;
23 | 		}
24 | 	}
25 | 	&_image {
26 | 		background-color: var(--bg-color-action);
27 | 		border-radius: 50%;
28 | 		height: 80px;
29 | 		margin-left: auto;
30 | 		margin-right: auto;
31 | 		overflow: hidden;
32 | 		position: relative;
33 | 		width: 80px;
34 | 
35 | 		img {
36 | 			display: block;
37 | 			height: 100%;
38 | 			left: 0;
39 | 			position: absolute;
40 | 			top: 0;
41 | 			width: 100%;
42 | 		}
43 | 	}
44 | 	a#{&}_anchor:hover &_image {
45 | 		background-color: var(--bg-color-action-active);
46 | 
47 | 		img {
48 | 			filter: brightness(1.1) saturate(1.1);
49 | 		}
50 | 	}
51 | 	&_placeholder {
52 | 		left: 50%;
53 | 		margin-left: -12px;
54 | 		margin-top: -12px;
55 | 		position: absolute;
56 | 		top: 50%;
57 | 	}
58 | 	&_name {
59 | 		margin-top: 16px;
60 | 		text-align: center;
61 | 	}
62 | }
63 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/sass/components/_theme-builder.scss:
--------------------------------------------------------------------------------
 1 | @use '../defs';
 2 | 
 3 | .paint {
 4 | 	background-color: var(--bg-color-secondary);
 5 | 	border-radius: 6px;
 6 | 	overflow: hidden;
 7 | 
 8 | 	&_gui {
 9 | 		border-bottom: var(--bg-color) solid 2px;
10 | 
11 | 		@include defs.wide() {
12 | 			display: flex;
13 | 		}
14 | 	}
15 | 	&_controller {
16 | 		--tp-base-border-radius: 0px;
17 | 		--tp-base-shadow-color: transparent;
18 | 		background-color: var(--bg-color-secondary);
19 | 
20 | 		@include defs.nonwide() {
21 | 			--tp-blade-value-width: 60vw;
22 | 
23 | 			.paneContainer {
24 | 				width: 100%;
25 | 			}
26 | 		}
27 | 	}
28 | 	&_preview {
29 | 		align-items: center;
30 | 		display: flex;
31 | 		flex: 1;
32 | 		justify-content: center;
33 | 		overflow: hidden;
34 | 		padding: 32px;
35 | 		position: relative;
36 | 
37 | 		.paneContainer {
38 | 			position: static;
39 | 		}
40 | 		.tp-rotv {
41 | 			position: relative;
42 | 		}
43 | 	}
44 | 	&_bgImage {
45 | 		background-color: black;
46 | 		background: url(https://source.unsplash.com/74ft3aciAY0/900x1600) no-repeat
47 | 			center;
48 | 		background-size: cover;
49 | 		inset: -16px;
50 | 		position: absolute;
51 | 
52 | 		&::before {
53 | 			background-image: radial-gradient(
54 | 				rgba(255, 255, 255, 0.1) 1px,
55 | 				transparent 0
56 | 			);
57 | 			background-position: center;
58 | 			background-size: 16px 16px;
59 | 			content: '';
60 | 			inset: 0;
61 | 			position: absolute;
62 | 		}
63 | 	}
64 | }
65 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/template/migration/index.html:
--------------------------------------------------------------------------------
 1 | {% set pageId = 'migration' %}
 2 | {% set root = '../' %}
 3 | {% set title = 'Migration' %}
 4 | {% extends "_template.html" %}
 5 | 
 6 | 
 7 | {% block pageHeader %}
 8 | <div class="pageHeader">
 9 | 	<div class="pageHeader_inner">
10 | 		<div class="pageHeader_text">
11 | 			<h1 class="pageHeader_title">{{ title }}</h1>
12 | 			<p>List of the quick migration guides.</p>
13 | 		</div>
14 | 	</div>
15 | </div>
16 | {% endblock %}
17 | 
18 | 
19 | {% block content %}
20 | <h2>Major versions</h2>
21 | <ul>
22 | 	<li><a href="./v4/">Version 3.x to 4.x</a></li>
23 | </ul>
24 | 
25 | <h2>Other libraries</h2>
26 | <ul>
27 | 	<li><a href="./datgui/">From dat.GUI</a></li>
28 | </ul>
29 | {% endblock %}
30 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/template/partial/_global-footer.html:
--------------------------------------------------------------------------------
1 | <div class="globalFooter">
2 | 	<div class="globalFooter_inner">
3 | 		<div class="globalFooter_copy">
4 | 			&copy; 2018 <a href="https://github.com/cocopon">cocopon</a>.
5 | 		</div>
6 | 	</div>
7 | </div>
8 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/template/partial/_global-header.html:
--------------------------------------------------------------------------------
 1 | <div class="globalHeader">
 2 | 	<div class="globalHeader_inner">
 3 | 		<div class="globalHeader_logo">
 4 | 			<div class="logo">
 5 | 				<a class="logo_main" href="{{ root }}">
 6 | 					<div class="logo_symbol"></div>
 7 | 					<div class="logo_text">Tweakpane</div>
 8 | 				</a>
 9 | 			</div>
10 | 		</div>
11 | 		<div class="globalHeader_spMenuButton">
12 | 			<button id="spMenuButton" class="spMenuButton">
13 | 				<span class="material-icons">menu</span>
14 | 			</button>
15 | 		</div>
16 | 	</div>
17 | </div>
18 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/plugins/counter/bundle.ts:
--------------------------------------------------------------------------------
 1 | import {TpPluginBundle} from '@tweakpane/core';
 2 | 
 3 | import {CounterInputPlugin} from './plugin';
 4 | 
 5 | export const CounterPluginBundle: TpPluginBundle = {
 6 | 	// Identifier of the plugin bundle
 7 | 	id: 'counter',
 8 | 	// Plugins that should be registered
 9 | 	plugins: [CounterInputPlugin],
10 | 
11 | 	// Additional CSS for this bundle
12 | 	css: `.tp-counter {
13 | 	align-items: center;
14 | 	display: flex;
15 | }
16 | .tp-counter div {
17 | 	color: #00ffd680;
18 | 	flex: 1;
19 | }
20 | .tp-counter button {
21 | 	background-color: #00ffd6c0;
22 | 	border-radius: 2px;
23 | 	color: black;
24 | 	height: 20px;
25 | 	width: 20px;
26 | }`,
27 | };
28 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/plugins/counter/controller.ts:
--------------------------------------------------------------------------------
 1 | import {Value, ValueController, ViewProps} from '@tweakpane/core';
 2 | 
 3 | import {CounterView} from './view';
 4 | 
 5 | interface Config {
 6 | 	value: Value<number>;
 7 | 	viewProps: ViewProps;
 8 | }
 9 | 
10 | export class CounterController implements ValueController<number, CounterView> {
11 | 	public readonly value: Value<number>;
12 | 	public readonly view: CounterView;
13 | 	public readonly viewProps: ViewProps;
14 | 
15 | 	constructor(doc: Document, config: Config) {
16 | 		// Models
17 | 		this.value = config.value;
18 | 		this.viewProps = config.viewProps;
19 | 
20 | 		// Create a view
21 | 		this.view = new CounterView(doc, {
22 | 			value: config.value,
23 | 			viewProps: this.viewProps,
24 | 		});
25 | 
26 | 		// Handle user interaction
27 | 		this.view.buttonElement.addEventListener('click', () => {
28 | 			// Update a model
29 | 			this.value.rawValue += 1;
30 | 		});
31 | 	}
32 | }
33 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/plugins/counter/plugin.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	BaseInputParams,
 3 | 	BindingTarget,
 4 | 	createPlugin,
 5 | 	InputBindingPlugin,
 6 | } from '@tweakpane/core';
 7 | 
 8 | import {CounterController} from './controller';
 9 | 
10 | type CounterParams = BaseInputParams;
11 | 
12 | export const CounterInputPlugin: InputBindingPlugin<
13 | 	number,
14 | 	number,
15 | 	CounterParams
16 | > = createPlugin({
17 | 	id: 'counter',
18 | 	type: 'input',
19 | 	accept(value: unknown, params: Record<string, unknown>) {
20 | 		if (typeof value !== 'number') {
21 | 			return null;
22 | 		}
23 | 		if (params.view !== 'counter') {
24 | 			return null;
25 | 		}
26 | 		return {
27 | 			initialValue: value,
28 | 			params: params,
29 | 		};
30 | 	},
31 | 	binding: {
32 | 		reader: () => (value: unknown) => Number(value),
33 | 		writer: () => (target: BindingTarget, value: number) => {
34 | 			target.write(value);
35 | 		},
36 | 	},
37 | 	controller(args) {
38 | 		return new CounterController(args.document, {
39 | 			value: args.value,
40 | 			viewProps: args.viewProps,
41 | 		});
42 | 	},
43 | });
44 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/plugins/counter/view.ts:
--------------------------------------------------------------------------------
 1 | import {Value, View, ViewProps} from '@tweakpane/core';
 2 | 
 3 | interface Config {
 4 | 	value: Value<number>;
 5 | 	viewProps: ViewProps;
 6 | }
 7 | 
 8 | export class CounterView implements View {
 9 | 	public readonly element: HTMLElement;
10 | 	public readonly buttonElement: HTMLButtonElement;
11 | 
12 | 	constructor(doc: Document, config: Config) {
13 | 		// Create view elements
14 | 		this.element = doc.createElement('div');
15 | 		this.element.classList.add('tp-counter');
16 | 
17 | 		// Apply value changes to the preview element
18 | 		const previewElem = doc.createElement('div');
19 | 		const value = config.value;
20 | 		value.emitter.on('change', () => {
21 | 			previewElem.textContent = String(value.rawValue);
22 | 		});
23 | 		previewElem.textContent = String(value.rawValue);
24 | 		this.element.appendChild(previewElem);
25 | 
26 | 		// Create a button element for user interaction
27 | 		const buttonElem = doc.createElement('button');
28 | 		buttonElem.textContent = '+';
29 | 		this.element.appendChild(buttonElem);
30 | 		this.buttonElement = buttonElem;
31 | 	}
32 | }
33 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/preset-test.ts:
--------------------------------------------------------------------------------
 1 | import * as assert from 'assert';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {presetToState, stateToPreset} from './preset.js';
 5 | 
 6 | describe(stateToPreset.name, () => {
 7 | 	it('should convert state into preset', () => {
 8 | 		const preset = stateToPreset({
 9 | 			disabled: false,
10 | 			children: [
11 | 				{binding: {key: 'foo', value: 1}, disabled: false},
12 | 				{disabled: false},
13 | 				{binding: {key: 'bar', value: 'text'}, disabled: false},
14 | 				{
15 | 					children: [{binding: {key: 'baz', value: true}, disabled: false}],
16 | 				},
17 | 			],
18 | 		});
19 | 		assert.deepStrictEqual(preset, {
20 | 			foo: 1,
21 | 			bar: 'text',
22 | 			baz: true,
23 | 		});
24 | 	});
25 | });
26 | 
27 | describe(presetToState.name, () => {
28 | 	it('should convert preset into state', () => {
29 | 		const state = presetToState(
30 | 			{
31 | 				disabled: false,
32 | 				children: [
33 | 					{binding: {key: 'foo', value: 0}, disabled: false},
34 | 					{disabled: false},
35 | 					{binding: {key: 'bar', value: ''}, disabled: false},
36 | 					{
37 | 						children: [{binding: {key: 'baz', value: false}, disabled: false}],
38 | 					},
39 | 				],
40 | 			},
41 | 			{
42 | 				foo: 1,
43 | 				bar: 'text',
44 | 				baz: true,
45 | 			},
46 | 		);
47 | 		assert.deepStrictEqual(state, {
48 | 			disabled: false,
49 | 			children: [
50 | 				{binding: {key: 'foo', value: 1}, disabled: false},
51 | 				{disabled: false},
52 | 				{binding: {key: 'bar', value: 'text'}, disabled: false},
53 | 				{
54 | 					children: [{binding: {key: 'baz', value: true}, disabled: false}],
55 | 				},
56 | 			],
57 | 		});
58 | 	});
59 | });
60 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/preset.ts:
--------------------------------------------------------------------------------
 1 | import {BladeState} from '@tweakpane/core';
 2 | 
 3 | export interface PresetObject {
 4 | 	[key: string]: unknown;
 5 | }
 6 | 
 7 | export function stateToPreset(state: BladeState): PresetObject {
 8 | 	// Container
 9 | 	if ('children' in state && Array.isArray(state.children)) {
10 | 		return state.children.reduce((tmp: PresetObject, substate) => {
11 | 			return {
12 | 				...tmp,
13 | 				...stateToPreset(substate),
14 | 			};
15 | 		}, {});
16 | 	}
17 | 	// Binding
18 | 	const binding = (state.binding ?? {}) as Record<string, unknown>;
19 | 	if (
20 | 		'key' in binding &&
21 | 		typeof binding.key === 'string' &&
22 | 		'value' in binding
23 | 	) {
24 | 		return {
25 | 			[binding.key]: binding.value,
26 | 		};
27 | 	}
28 | 	return {};
29 | }
30 | 
31 | export function presetToState(
32 | 	state: BladeState,
33 | 	preset: PresetObject,
34 | ): BladeState {
35 | 	// Container
36 | 	if ('children' in state && Array.isArray(state.children)) {
37 | 		return {
38 | 			...state,
39 | 			children: state.children.map((substate) =>
40 | 				presetToState(substate, preset),
41 | 			),
42 | 		};
43 | 	}
44 | 	// Binding
45 | 	const binding = (state.binding ?? {}) as Record<string, unknown>;
46 | 	if (
47 | 		'key' in binding &&
48 | 		typeof binding.key === 'string' &&
49 | 		'value' in binding
50 | 	) {
51 | 		return {
52 | 			...state,
53 | 			binding: {
54 | 				...binding,
55 | 				value: preset[binding.key] ?? state.value,
56 | 			},
57 | 		};
58 | 	}
59 | 	return state;
60 | }
61 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/route/getting-started.ts:
--------------------------------------------------------------------------------
 1 | import {Pane} from 'tweakpane';
 2 | 
 3 | import {selectContainer} from '../util.js';
 4 | 
 5 | export function initGettingStarted() {
 6 | 	const markerToFnMap: {
 7 | 		[key: string]: (container: HTMLElement) => void;
 8 | 	} = {
 9 | 		hello: (container) => {
10 | 			new Pane({
11 | 				container: container,
12 | 			});
13 | 		},
14 | 	};
15 | 	Object.keys(markerToFnMap).forEach((marker) => {
16 | 		const initFn = markerToFnMap[marker];
17 | 		const container = selectContainer(marker);
18 | 		initFn(container);
19 | 	});
20 | }
21 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/screw.ts:
--------------------------------------------------------------------------------
 1 | export class Screw {
 2 | 	elem_: HTMLElement;
 3 | 
 4 | 	constructor(elem: HTMLElement) {
 5 | 		this.onWindowScroll_ = this.onWindowScroll_.bind(this);
 6 | 
 7 | 		this.elem_ = elem;
 8 | 
 9 | 		window.addEventListener('scroll', this.onWindowScroll_);
10 | 	}
11 | 
12 | 	onWindowScroll_() {
13 | 		const angle = window.scrollY * 0.5;
14 | 		this.elem_.style.transform = `rotate(${angle}deg)`;
15 | 	}
16 | }
17 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/simple-router.ts:
--------------------------------------------------------------------------------
 1 | type Matcher = (pathname: string) => boolean;
 2 | 
 3 | interface Route {
 4 | 	init: () => void;
 5 | 	matcher: Matcher;
 6 | }
 7 | 
 8 | export class SimpleRouter {
 9 | 	private routes_: Route[];
10 | 
11 | 	constructor() {
12 | 		this.routes_ = [];
13 | 	}
14 | 
15 | 	public add(matcher: RegExp | Matcher, callback: () => void): void {
16 | 		this.routes_.push({
17 | 			init: callback,
18 | 			matcher:
19 | 				matcher instanceof RegExp
20 | 					? (pathname: string): boolean => {
21 | 							return matcher.test(pathname);
22 | 					  }
23 | 					: matcher,
24 | 		});
25 | 	}
26 | 
27 | 	public route(pathname: string): void {
28 | 		this.routes_.forEach((route) => {
29 | 			if (route.matcher(pathname)) {
30 | 				route.init();
31 | 			}
32 | 		});
33 | 	}
34 | }
35 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/ts/util.ts:
--------------------------------------------------------------------------------
 1 | export function selectContainer(marker: string, console = false): HTMLElement {
 2 | 	const postfix = marker + (console ? 'console' : '');
 3 | 	const selector = `*[data-pane-${postfix}]`;
 4 | 	const elem = document.querySelector(selector);
 5 | 	if (!elem) {
 6 | 		throw Error(`container not found: ${selector}`);
 7 | 	}
 8 | 	return elem as HTMLElement;
 9 | }
10 | 
11 | export function wave(t: number): number {
12 | 	const p = t * 0.02;
13 | 	return (
14 | 		((3 * 4) / Math.PI) *
15 | 		(Math.sin(p * 1 * Math.PI) +
16 | 			Math.sin(p * 3 * Math.PI) / 3 +
17 | 			Math.sin(p * 5 * Math.PI) / 5) *
18 | 		0.25
19 | 	);
20 | }
21 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"extends": "../../../../tsconfig.json",
 3 | 	"compilerOptions": {
 4 | 		"baseUrl": ".",
 5 | 		"moduleResolution": "Node",
 6 | 		"paths": {
 7 | 			"@tweakpane/core": ["../../../core/src/index"],
 8 | 			"tweakpane": ["../main/ts/index"]
 9 | 		}
10 | 	}
11 | }
12 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/doc/typedoc.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"entryPoints": [
 3 | 		"../../",
 4 | 	],
 5 | 	"entryPointStrategy": "packages",
 6 | 	"exclude": [
 7 | 		"**/*.d.ts",
 8 | 		"**/*-test.ts"
 9 | 	],
10 | 	"excludePrivate": true,
11 | 	"intentionallyNotExported": [
12 | 		"BindingController",
13 | 		"BindingValue",
14 | 		"BladeController",
15 | 		"InputBindingController",
16 | 		"MonitorBindingController"
17 | 	],
18 | 	"out": "../../docs/api",
19 | 	"plugin": ["typedoc-plugin-missing-exports"],
20 | 	"readme": "none"
21 | }


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/sass/bundle.scss:
--------------------------------------------------------------------------------
1 | @use '../../../../../node_modules/@tweakpane/core/lib/sass/view/views';
2 | 
3 | @use './view/root';
4 | @use './view/separator';
5 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/sass/view/_separator.scss:
--------------------------------------------------------------------------------
 1 | @use '../../../../../../node_modules/@tweakpane/core/lib/sass/tp';
 2 | 
 3 | .#{tp.$prefix}-sprv {
 4 | 	&_r {
 5 | 		background-color: tp.cssVar('groove-fg');
 6 | 		border-width: 0;
 7 | 		display: block;
 8 | 		height: tp.$separator-width;
 9 | 		margin: 0;
10 | 		width: 100%;
11 | 	}
12 | 	&.#{tp.$disabled} &_r {
13 | 		opacity: 0.5;
14 | 	}
15 | }
16 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/root/api/root.ts:
--------------------------------------------------------------------------------
 1 | import {FolderApi, PluginPool} from '@tweakpane/core';
 2 | 
 3 | import {RootController} from '../controller/root.js';
 4 | 
 5 | export class RootApi extends FolderApi {
 6 | 	/**
 7 | 	 * @hidden
 8 | 	 */
 9 | 	constructor(controller: RootController, pool: PluginPool) {
10 | 		super(controller, pool);
11 | 	}
12 | 
13 | 	get element(): HTMLElement {
14 | 		return this.controller.view.element;
15 | 	}
16 | }
17 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/root/controller/root.ts:
--------------------------------------------------------------------------------
 1 | import {Blade, FolderController, FolderProps, ViewProps} from '@tweakpane/core';
 2 | 
 3 | /**
 4 |  * @hidden
 5 |  */
 6 | interface Config {
 7 | 	blade: Blade;
 8 | 	props: FolderProps;
 9 | 	viewProps: ViewProps;
10 | 
11 | 	expanded?: boolean;
12 | 	title?: string;
13 | }
14 | 
15 | /**
16 |  * @hidden
17 |  */
18 | export class RootController extends FolderController {
19 | 	constructor(doc: Document, config: Config) {
20 | 		super(doc, {
21 | 			expanded: config.expanded,
22 | 			blade: config.blade,
23 | 			props: config.props,
24 | 			root: true,
25 | 			viewProps: config.viewProps,
26 | 		});
27 | 	}
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/separator/api/separator-test.ts:
--------------------------------------------------------------------------------
 1 | import {createBlade, ViewProps} from '@tweakpane/core';
 2 | import {describe, it} from 'mocha';
 3 | 
 4 | import {
 5 | 	assertInitialState,
 6 | 	assertUpdates,
 7 | 	createTestWindow,
 8 | } from '../../../misc/test-util.js';
 9 | import {SeparatorController} from '../controller/separator.js';
10 | import {SeparatorBladeApi} from './separator.js';
11 | 
12 | describe(SeparatorBladeApi.name, () => {
13 | 	it('should have initial state', () => {
14 | 		const doc = createTestWindow().document;
15 | 		const c = new SeparatorController(doc, {
16 | 			blade: createBlade(),
17 | 			viewProps: ViewProps.create(),
18 | 		});
19 | 		const api = new SeparatorBladeApi(c);
20 | 		assertInitialState(api);
21 | 	});
22 | 
23 | 	it('should update properties', () => {
24 | 		const doc = createTestWindow().document;
25 | 		const c = new SeparatorController(doc, {
26 | 			blade: createBlade(),
27 | 			viewProps: ViewProps.create(),
28 | 		});
29 | 		const api = new SeparatorBladeApi(c);
30 | 		assertUpdates(api);
31 | 	});
32 | });
33 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/separator/api/separator.ts:
--------------------------------------------------------------------------------
1 | import {BladeApi} from '@tweakpane/core';
2 | 
3 | import {SeparatorController} from '../controller/separator.js';
4 | 
5 | export class SeparatorBladeApi extends BladeApi<SeparatorController> {}
6 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/separator/controller/separator.ts:
--------------------------------------------------------------------------------
 1 | import {Blade, BladeController, ViewProps} from '@tweakpane/core';
 2 | 
 3 | import {SeparatorView} from '../view/separator.js';
 4 | 
 5 | /**
 6 |  * @hidden
 7 |  */
 8 | interface Config {
 9 | 	blade: Blade;
10 | 	viewProps: ViewProps;
11 | }
12 | 
13 | /**
14 |  * @hidden
15 |  */
16 | export class SeparatorController extends BladeController<SeparatorView> {
17 | 	/**
18 | 	 * @hidden
19 | 	 */
20 | 	constructor(doc: Document, config: Config) {
21 | 		super({
22 | 			...config,
23 | 			view: new SeparatorView(doc, {
24 | 				viewProps: config.viewProps,
25 | 			}),
26 | 		});
27 | 	}
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/separator/plugin-test.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	BladeController,
 3 | 	createBladeController,
 4 | 	createDefaultPluginPool,
 5 | } from '@tweakpane/core';
 6 | import * as assert from 'assert';
 7 | import {describe as context, describe, it} from 'mocha';
 8 | 
 9 | import {
10 | 	createEmptyBladeController,
11 | 	createTestWindow,
12 | } from '../../misc/test-util.js';
13 | import {SeparatorBladePlugin} from './plugin.js';
14 | 
15 | describe(SeparatorBladePlugin.id, () => {
16 | 	[{}].forEach((params) => {
17 | 		context(`when ${JSON.stringify(params)}`, () => {
18 | 			it('should not create API', () => {
19 | 				const doc = createTestWindow().document;
20 | 				const api = createBladeController(SeparatorBladePlugin, {
21 | 					document: doc,
22 | 					params: params,
23 | 				});
24 | 				assert.strictEqual(api, null);
25 | 			});
26 | 		});
27 | 	});
28 | 
29 | 	it('should be created', () => {
30 | 		const doc = createTestWindow().document;
31 | 		const bc = createBladeController(SeparatorBladePlugin, {
32 | 			document: doc,
33 | 			params: {
34 | 				view: 'separator',
35 | 			},
36 | 		}) as BladeController;
37 | 		const api = SeparatorBladePlugin.api({
38 | 			controller: bc,
39 | 			pool: createDefaultPluginPool(),
40 | 		});
41 | 		assert.notStrictEqual(api, null);
42 | 	});
43 | 
44 | 	[(doc: Document) => createEmptyBladeController(doc)].forEach(
45 | 		(createController) => {
46 | 			it('should not create API', () => {
47 | 				const doc = createTestWindow().document;
48 | 				const c = createController(doc);
49 | 				const api = SeparatorBladePlugin.api({
50 | 					controller: c,
51 | 					pool: createDefaultPluginPool(),
52 | 				});
53 | 				assert.strictEqual(api, null);
54 | 			});
55 | 		},
56 | 	);
57 | });
58 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/separator/plugin.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	BaseBladeParams,
 3 | 	BladePlugin,
 4 | 	parseRecord,
 5 | 	VERSION,
 6 | } from '@tweakpane/core';
 7 | 
 8 | import {SeparatorBladeApi} from './api/separator.js';
 9 | import {SeparatorController} from './controller/separator.js';
10 | 
11 | export interface SeparatorBladeParams extends BaseBladeParams {
12 | 	view: 'separator';
13 | }
14 | 
15 | export const SeparatorBladePlugin: BladePlugin<SeparatorBladeParams> = {
16 | 	id: 'separator',
17 | 	type: 'blade',
18 | 	core: VERSION,
19 | 	accept(params) {
20 | 		const result = parseRecord<SeparatorBladeParams>(params, (p) => ({
21 | 			view: p.required.constant('separator'),
22 | 		}));
23 | 		return result ? {params: result} : null;
24 | 	},
25 | 	controller(args) {
26 | 		return new SeparatorController(args.document, {
27 | 			blade: args.blade,
28 | 			viewProps: args.viewProps,
29 | 		});
30 | 	},
31 | 	api(args) {
32 | 		if (!(args.controller instanceof SeparatorController)) {
33 | 			return null;
34 | 		}
35 | 		return new SeparatorBladeApi(args.controller);
36 | 	},
37 | };
38 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/blade/separator/view/separator.ts:
--------------------------------------------------------------------------------
 1 | import {ClassName, View, ViewProps} from '@tweakpane/core';
 2 | 
 3 | const cn = ClassName('spr');
 4 | 
 5 | /**
 6 |  * @hidden
 7 |  */
 8 | interface Config {
 9 | 	viewProps: ViewProps;
10 | }
11 | 
12 | /**
13 |  * @hidden
14 |  */
15 | export class SeparatorView implements View {
16 | 	public readonly element: HTMLElement;
17 | 
18 | 	constructor(doc: Document, config: Config) {
19 | 		this.element = doc.createElement('div');
20 | 		this.element.classList.add(cn());
21 | 		config.viewProps.bindClassModifiers(this.element);
22 | 
23 | 		const hrElem = doc.createElement('hr');
24 | 		hrElem.classList.add(cn('r'));
25 | 		this.element.appendChild(hrElem);
26 | 	}
27 | }
28 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/index.ts:
--------------------------------------------------------------------------------
 1 | import {Semver} from '@tweakpane/core';
 2 | 
 3 | export {
 4 | 	ArrayStyleListOptions,
 5 | 	BaseParams,
 6 | 	BaseBladeParams,
 7 | 	BindingApiEvents,
 8 | 	BindingParams,
 9 | 	BladeApi,
10 | 	BooleanInputParams,
11 | 	BooleanMonitorParams,
12 | 	ButtonApi,
13 | 	ButtonParams,
14 | 	ColorInputParams,
15 | 	FolderApi,
16 | 	FolderParams,
17 | 	InputBindingApi,
18 | 	ListInputBindingApi,
19 | 	ListParamsOptions,
20 | 	MonitorBindingApi,
21 | 	NumberInputParams,
22 | 	NumberMonitorParams,
23 | 	ObjectStyleListOptions,
24 | 	Point2dInputParams,
25 | 	Point3dInputParams,
26 | 	Point4dInputParams,
27 | 	Semver,
28 | 	SliderInputBindingApi,
29 | 	StringInputParams,
30 | 	StringMonitorParams,
31 | 	TabApi,
32 | 	TabPageApi,
33 | 	TabPageParams,
34 | 	TabParams,
35 | 	TpChangeEvent,
36 | 	TpPlugin,
37 | 	TpPluginBundle,
38 | } from '@tweakpane/core';
39 | 
40 | export {ListBladeApi} from './blade/list/api/list.js';
41 | export {ListBladeParams} from './blade/list/plugin.js';
42 | export {SeparatorBladeApi} from './blade/separator/api/separator.js';
43 | export {SeparatorBladeParams} from './blade/separator/plugin.js';
44 | export {SliderBladeApi} from './blade/slider/api/slider.js';
45 | export {SliderBladeParams} from './blade/slider/plugin.js';
46 | export {TextBladeApi} from './blade/text/api/text.js';
47 | export {TextBladeParams} from './blade/text/plugin.js';
48 | 
49 | export {Pane} from './pane/pane.js';
50 | 
51 | export const VERSION = new Semver('0.0.0-tweakpane.0');
52 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/pane/blade-test.ts:
--------------------------------------------------------------------------------
 1 | import {TpError} from '@tweakpane/core';
 2 | import * as assert from 'assert';
 3 | 
 4 | import {SliderBladeApi} from '../blade/slider/api/slider.js';
 5 | import {createTestWindow} from '../misc/test-util.js';
 6 | import {Pane} from './pane.js';
 7 | 
 8 | describe(Pane.name, () => {
 9 | 	it('should apply initial view properties', () => {
10 | 		const doc = createTestWindow().document;
11 | 		const pane = new Pane({
12 | 			document: doc,
13 | 		});
14 | 
15 | 		const i1 = pane.addBlade({
16 | 			title: '',
17 | 			view: 'button',
18 | 		});
19 | 		assert.strictEqual(i1.disabled, false);
20 | 		assert.strictEqual(i1.hidden, false);
21 | 
22 | 		const i2 = pane.addBlade({
23 | 			disabled: true,
24 | 			hidden: true,
25 | 			title: '',
26 | 			view: 'button',
27 | 		});
28 | 		assert.strictEqual(i2.disabled, true);
29 | 		assert.strictEqual(i2.hidden, true);
30 | 	});
31 | 
32 | 	it('should throw `alreadydisposed` error when calling dispose() inside blade change event', (done) => {
33 | 		const doc = createTestWindow().document;
34 | 		const pane = new Pane({
35 | 			document: doc,
36 | 		});
37 | 		const b = pane.addBlade({
38 | 			max: 100,
39 | 			min: 0,
40 | 			view: 'slider',
41 | 		}) as SliderBladeApi;
42 | 
43 | 		try {
44 | 			b.on('change', () => {
45 | 				b.dispose();
46 | 			});
47 | 			b.controller.value.rawValue = 1;
48 | 		} catch (err) {
49 | 			assert.strictEqual((err as TpError<any>).type, 'alreadydisposed');
50 | 			done();
51 | 		}
52 | 	});
53 | });
54 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/pane/event-test.ts:
--------------------------------------------------------------------------------
 1 | import {describe, it} from 'mocha';
 2 | 
 3 | import {createTestWindow} from '../misc/test-util.js';
 4 | import {Pane} from './pane.js';
 5 | 
 6 | function createPane(): Pane {
 7 | 	return new Pane({
 8 | 		document: createTestWindow().document,
 9 | 		title: 'Title',
10 | 	});
11 | }
12 | 
13 | describe(Pane.name, () => {
14 | 	it('should listen fold event', (done) => {
15 | 		const pane = createPane();
16 | 		pane.on('fold', () => {
17 | 			done();
18 | 		});
19 | 
20 | 		const folder = pane.controller.foldable;
21 | 		if (folder) {
22 | 			folder.set('expanded', false);
23 | 		}
24 | 	});
25 | });
26 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/ts/pane/pane-config.ts:
--------------------------------------------------------------------------------
 1 | export interface PaneConfig {
 2 | 	/**
 3 | 	 * The custom container element of the pane.
 4 | 	 */
 5 | 	container?: HTMLElement;
 6 | 	/**
 7 | 	 * The default expansion of the pane.
 8 | 	 */
 9 | 	expanded?: boolean;
10 | 	/**
11 | 	 * The pane title that can expand/collapse the entire pane.
12 | 	 */
13 | 	title?: string;
14 | 
15 | 	/**
16 | 	 * @hidden
17 | 	 */
18 | 	document?: Document;
19 | }
20 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/tsconfig-dts.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": "./tsconfig.json",
3 | 	"compilerOptions": {
4 | 		"declaration": true,
5 | 		"emitDeclarationOnly": true,
6 | 		"outDir": "../../dist/types"
7 | 	}
8 | }
9 | 


--------------------------------------------------------------------------------
/packages/tweakpane/src/main/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": "../../../../tsconfig.json"
3 | }
4 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/node/index.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable no-console */
 2 | /* eslint-env node */
 3 | 
 4 | import Fs from 'fs';
 5 | import {JSDOM as Jsdom} from 'jsdom';
 6 | // Require default module
 7 | import {Pane, VERSION} from 'tweakpane';
 8 | 
 9 | const Package = JSON.parse(
10 | 	Fs.readFileSync(new URL('../../package.json', import.meta.url)),
11 | );
12 | 
13 | // Check version
14 | if (VERSION.toString() !== Package.version) {
15 | 	throw new Error('invalid version');
16 | }
17 | 
18 | const PARAMS = {
19 | 	foo: 1,
20 | };
21 | 
22 | // Create pane
23 | const doc = new Jsdom('').window.document;
24 | const pane = new Pane({
25 | 	document: doc,
26 | });
27 | 
28 | // Add input
29 | const input = pane.addBinding(PARAMS, 'foo', {
30 | 	max: 1,
31 | 	min: 0,
32 | 	step: 1,
33 | });
34 | console.log(input);
35 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "tweakpane-test-module",
 3 |   "version": "1.0.0",
 4 |   "description": "",
 5 |   "type": "module",
 6 |   "private": true,
 7 |   "scripts": {
 8 |     "test": " run-p test:*",
 9 |     "test:node": "node node/index.js",
10 |     "test:sass": "test -f node_modules/@tweakpane/core/lib/sass/_tp.scss",
11 |     "test:tsc": "tsc --build --clean tsc/tsconfig.json && tsc --build tsc/tsconfig.json && node tsc/dist/index.js",
12 |     "test:plugin": "rollup --config plugin/rollup.config.js && node plugin/test/node.js"
13 |   },
14 |   "author": "cocopon",
15 |   "license": "MIT",
16 |   "devDependencies": {
17 |     "@rollup/plugin-alias": "^3.1.2",
18 |     "@rollup/plugin-node-resolve": "^13.0.0",
19 |     "@rollup/plugin-typescript": "^8.2.0",
20 |     "@tweakpane/core": "file:../../core/tweakpane-core.tgz",
21 |     "@types/jsdom": "^16.2.13",
22 |     "jsdom": "^16.7.0",
23 |     "npm-run-all": "^4.1.5",
24 |     "rollup": "^2.39.0",
25 |     "tweakpane": "file:../tweakpane.tgz",
26 |     "typescript": "^4.1.5"
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/plugin/rollup.config.js:
--------------------------------------------------------------------------------
 1 | import Alias from '@rollup/plugin-alias';
 2 | import {nodeResolve} from '@rollup/plugin-node-resolve';
 3 | import Typescript from '@rollup/plugin-typescript';
 4 | 
 5 | export default () => {
 6 | 	return {
 7 | 		input: 'plugin/src/index.ts',
 8 | 		external: ['tweakpane'],
 9 | 		output: {
10 | 			file: 'plugin/dist/bundle.js',
11 | 			format: 'esm',
12 | 			globals: {
13 | 				tweakpane: 'Tweakpane',
14 | 			},
15 | 			name: 'TweakpanePluginExample',
16 | 		},
17 | 		plugins: [
18 | 			Alias({
19 | 				entries: [
20 | 					{
21 | 						find: '@tweakpane/core',
22 | 						replacement: './node_modules/@tweakpane/core/dist/index.js',
23 | 					},
24 | 				],
25 | 			}),
26 | 			Typescript({
27 | 				tsconfig: 'plugin/tsconfig.json',
28 | 			}),
29 | 			nodeResolve(),
30 | 		],
31 | 
32 | 		onwarn(warning, warn) {
33 | 			if (warning.code === 'CIRCULAR_DEPENDENCY') {
34 | 				return;
35 | 			}
36 | 			warn(warning);
37 | 		},
38 | 	};
39 | };
40 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/plugin/src/index.ts:
--------------------------------------------------------------------------------
1 | import {TestBladePlugin} from './test-blade';
2 | import {TestInputPlugin} from './test-input';
3 | 
4 | export const plugins = [TestInputPlugin, TestBladePlugin];
5 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/plugin/src/test-input.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	BaseInputParams,
 3 | 	InputBindingPlugin,
 4 | 	parseRecord,
 5 | 	stringFromUnknown,
 6 | 	Value,
 7 | 	ValueController,
 8 | 	VERSION,
 9 | 	ViewProps,
10 | 	writePrimitive,
11 | } from '@tweakpane/core';
12 | 
13 | import {TestView} from './test-view';
14 | 
15 | class TestController implements ValueController<string, TestView> {
16 | 	public readonly value: Value<string>;
17 | 	public readonly view: TestView;
18 | 	public readonly viewProps: ViewProps;
19 | 
20 | 	constructor(
21 | 		doc: Document,
22 | 		config: {
23 | 			value: Value<string>;
24 | 			viewProps: ViewProps;
25 | 		},
26 | 	) {
27 | 		this.value = config.value;
28 | 		this.viewProps = config.viewProps;
29 | 
30 | 		this.view = new TestView(doc, {
31 | 			value: this.value,
32 | 		});
33 | 	}
34 | }
35 | 
36 | export const TestInputPlugin: InputBindingPlugin<
37 | 	string,
38 | 	string,
39 | 	BaseInputParams
40 | > = {
41 | 	id: 'input-test',
42 | 	type: 'input',
43 | 	core: VERSION,
44 | 	accept(value, params) {
45 | 		if (typeof value !== 'string') {
46 | 			return null;
47 | 		}
48 | 		const result = parseRecord(params, (p) => ({
49 | 			view: p.required.constant('test'),
50 | 		}));
51 | 		return result
52 | 			? {
53 | 					initialValue: value,
54 | 					params: result,
55 | 			  }
56 | 			: null;
57 | 	},
58 | 	binding: {
59 | 		reader: () => stringFromUnknown,
60 | 		writer: () => writePrimitive,
61 | 	},
62 | 	controller(args) {
63 | 		return new TestController(args.document, {
64 | 			value: args.value,
65 | 			viewProps: args.viewProps,
66 | 		});
67 | 	},
68 | };
69 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/plugin/src/test-view.ts:
--------------------------------------------------------------------------------
 1 | import {ClassName, Value, View} from '@tweakpane/core';
 2 | 
 3 | export class TestView implements View {
 4 | 	public readonly element: HTMLElement;
 5 | 
 6 | 	constructor(
 7 | 		doc: Document,
 8 | 		config: {
 9 | 			value: Value<string>;
10 | 		},
11 | 	) {
12 | 		this.element = doc.createElement('div');
13 | 		this.element.classList.add(ClassName('tst')());
14 | 		config.value.emitter.on('change', (ev) => {
15 | 			this.element.textContent = ev.rawValue;
16 | 		});
17 | 		this.element.textContent = config.value.rawValue;
18 | 	}
19 | }
20 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/plugin/test/browser.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 | <head>
 4 | 	<meta charset="UTF-8">
 5 | 	<title></title>
 6 | </head>
 7 | <body>
 8 | 	<script src="../../node_modules/tweakpane/dist/tweakpane.js"></script>
 9 | 	<script src="../dist/bundle.js"></script>
10 | 	<script>
11 | 		const params = {
12 | 			foo: 'hello, world',
13 | 		};
14 | 		const pane = new Tweakpane.Pane();
15 | 		pane.addBinding(params, 'foo', {
16 | 			view: 'test',
17 | 		});
18 | 	</script>
19 | </body>
20 | </html>
21 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/plugin/test/node.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable no-console */
 2 | /* eslint-env node */
 3 | import {JSDOM as Jsdom} from 'jsdom';
 4 | import {Pane} from 'tweakpane';
 5 | 
 6 | import * as TestPlugin from '../dist/bundle.js';
 7 | 
 8 | const doc = new Jsdom('').window.document;
 9 | const params = {foo: 'hello, world'};
10 | const pane = new Pane({
11 | 	document: doc,
12 | });
13 | pane.registerPlugin(TestPlugin);
14 | 
15 | // Create binding
16 | ((b) => {
17 | 	const elem = b.element.querySelector('.tp-tstv');
18 | 	if (!elem) {
19 | 		throw new Error('custom view not found');
20 | 	}
21 | 
22 | 	if (elem.textContent !== params.foo) {
23 | 		throw new Error('invalid display value');
24 | 	}
25 | })(
26 | 	pane.addBinding(params, 'foo', {
27 | 		view: 'test',
28 | 	}),
29 | );
30 | 
31 | // Create blade
32 | ((b) => {
33 | 	if (!b.element.classList.contains('tp-tstv')) {
34 | 		throw new Error('custom view not found');
35 | 	}
36 | })(
37 | 	pane.addBlade({
38 | 		value: 'foo',
39 | 		view: 'test',
40 | 	}),
41 | );
42 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/plugin/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"compilerOptions": {
 3 | 		"baseUrl": ".",
 4 | 		"lib": ["DOM", "ES2015"],
 5 | 		"strict": true,
 6 | 		"target": "ES6"
 7 | 	},
 8 | 	"include": ["src/*.ts"]
 9 | }
10 | 


--------------------------------------------------------------------------------
/packages/tweakpane/test-module/tsc/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"compilerOptions": {
 3 | 		"esModuleInterop": true,
 4 | 		"lib": ["DOM", "ES2015"],
 5 | 		"module": "node16",
 6 | 		"outDir": "dist",
 7 | 		"strict": true,
 8 | 		"target": "ES5"
 9 | 	}
10 | }
11 | 


--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 	arrowParens: 'always',
3 | 	bracketSpacing: false,
4 | 	singleQuote: true,
5 | 	trailingComma: 'all',
6 | 	useTabs: true,
7 | };
8 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"compilerOptions": {
 3 | 		"lib": ["DOM", "ES2015"],
 4 | 		"module": "node16",
 5 | 		"noImplicitAny": true,
 6 | 		"noUnusedLocals": true,
 7 | 		"noUnusedParameters": true,
 8 | 		"strict": true,
 9 | 		"target": "ES6"
10 | 	}
11 | }


--------------------------------------------------------------------------------