├── .github └── workflows │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MANIFEST.in ├── docs ├── README.md ├── favicon.png ├── logo.svg ├── process_task.md ├── save_load_widget.md ├── state_model.md ├── styling.md ├── templating.md └── widgets.md ├── examples ├── __init__.py └── save_load_example.py ├── js ├── build-widgets.ps1 ├── build-widgets.sh ├── eslint.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── scripts │ └── build-css.js ├── src │ ├── __mocks__ │ │ ├── @anywidget │ │ │ └── react.ts │ │ └── styleMock.js │ ├── components │ │ ├── ui │ │ │ ├── Accordion.tsx │ │ │ ├── Button.tsx │ │ │ ├── Chart.tsx │ │ │ ├── Chat.tsx │ │ │ ├── CheckBox.tsx │ │ │ ├── DateTimePicker.tsx │ │ │ ├── DateTimeRangePicker.tsx │ │ │ ├── DropDown.tsx │ │ │ ├── FileLoader.tsx │ │ │ ├── LoadSave.tsx │ │ │ ├── MapSelector.tsx │ │ │ ├── MarkdownDisplay.tsx │ │ │ ├── MarkdownDrawer.tsx │ │ │ ├── ModalDialog.tsx │ │ │ ├── NumberInput.tsx │ │ │ ├── Plot.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── RadioButtons.tsx │ │ │ ├── Slider.tsx │ │ │ ├── String.tsx │ │ │ ├── Table.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── TaskButton.tsx │ │ │ ├── TaskModal.tsx │ │ │ ├── TimelineChart.tsx │ │ │ ├── Timer.tsx │ │ │ ├── Toast.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── TreeBrowser.tsx │ │ │ ├── Workflow.tsx │ │ │ ├── chartjs-registration.ts │ │ │ ├── dialog.tsx │ │ │ └── projects │ │ │ │ ├── ItemDetails.tsx │ │ │ │ ├── ItemsList.tsx │ │ │ │ ├── ProjectBrowser.tsx │ │ │ │ ├── ProjectDetails.tsx │ │ │ │ ├── ProjectList.tsx │ │ │ │ ├── ProjectMenu.tsx │ │ │ │ ├── ScenarioDetails.tsx │ │ │ │ └── ScenarioList.tsx │ │ └── widgets │ │ │ ├── AccordionWidget.tsx │ │ │ ├── ButtonWidget.tsx │ │ │ ├── ChartWidget.tsx │ │ │ ├── ChatWidget.tsx │ │ │ ├── CheckBoxWidget.tsx │ │ │ ├── DOMElementMapWidget.tsx │ │ │ ├── DateTimePickerWidget.tsx │ │ │ ├── DateTimeRangePickerWidget.tsx │ │ │ ├── DropDownWidget.tsx │ │ │ ├── FileLoaderWidget.tsx │ │ │ ├── GroupComponent.tsx │ │ │ ├── HTMLTemplateWidget.tsx │ │ │ ├── InputSelectorWidget.tsx │ │ │ ├── LoadSaveContext.tsx │ │ │ ├── LoadSaveWidget.tsx │ │ │ ├── MapSelectorWidget.tsx │ │ │ ├── MarkdownDisplayWidget.tsx │ │ │ ├── MarkdownDrawerWidget.tsx │ │ │ ├── MarkdownRender.tsx │ │ │ ├── ModalDialogWidget.tsx │ │ │ ├── NumberInputWidget.tsx │ │ │ ├── PlotWidget.tsx │ │ │ ├── ProgressBarWidget.tsx │ │ │ ├── ProjectMenuWidget.tsx │ │ │ ├── RadioButtonsWidget.tsx │ │ │ ├── SaveLoadWidget.tsx │ │ │ ├── SliderWidget.tsx │ │ │ ├── StringInputWidget.tsx │ │ │ ├── TableWidget.tsx │ │ │ ├── TabsWidget.tsx │ │ │ ├── TaskWidget.tsx │ │ │ ├── TimelineChartWidget.tsx │ │ │ ├── TimerWidget.tsx │ │ │ ├── ToastWidget.tsx │ │ │ ├── ToggleButtonWidget.tsx │ │ │ ├── TreeBrowserWidget.tsx │ │ │ ├── URLParamsWidget.tsx │ │ │ ├── WeightedAssessmentSurveyWidget.tsx │ │ │ └── __tests__ │ │ │ ├── ButtonWidget.test.tsx │ │ │ ├── CheckBoxWidget.test.tsx │ │ │ ├── LoadSaveWidget.test.tsx │ │ │ └── TimerWidget.test.tsx │ ├── css │ │ ├── _inputs.scss │ │ ├── _variables.scss │ │ ├── components │ │ │ ├── Chat.scss │ │ │ ├── DropDown.scss │ │ │ ├── LoadSave.scss │ │ │ ├── NumberInput.scss │ │ │ ├── ProgressBar.scss │ │ │ ├── Slider.scss │ │ │ ├── TimelineChart.scss │ │ │ ├── Toast.scss │ │ │ ├── WeightedAssessmentSurvey.scss │ │ │ └── Workflow.scss │ │ ├── styles.css │ │ └── styles.scss │ ├── index.html │ ├── main.tsx │ ├── public │ │ └── logo.svg │ ├── setupTests.ts │ └── types │ │ └── global.d.ts ├── tsconfig.json ├── vite.config.mts └── widget-config.json ├── mkdocs.yml ├── pyproject.toml ├── python ├── examples │ ├── marimo │ │ └── numerous │ │ │ ├── app.py │ │ │ ├── app_cover.jpg │ │ │ ├── app_loadsave.py │ │ │ ├── logo.svg │ │ │ ├── numerous.png │ │ │ ├── numerous.toml │ │ │ ├── page.html.j2 │ │ │ ├── page.py │ │ │ ├── requirements.txt │ │ │ ├── timeline_data.h5 │ │ │ └── timeline_data.json │ ├── panel │ │ └── app.py │ └── timeline_example.py ├── package-lock.json ├── src │ └── numerous │ │ └── widgets │ │ ├── __init__.py │ │ ├── advanced │ │ ├── __init__.py │ │ └── weighted_assessment_survey.py │ │ ├── base │ │ ├── __init__.py │ │ ├── accordion.py │ │ ├── button.py │ │ ├── card.py │ │ ├── chartjs.py │ │ ├── chat.py │ │ ├── checkbox.py │ │ ├── config.py │ │ ├── container.py │ │ ├── datetime_picker.py │ │ ├── datetime_range_picker.py │ │ ├── dom_element_map.py │ │ ├── drop_down.py │ │ ├── html_template.py │ │ ├── map_selector.py │ │ ├── markdown_display.py │ │ ├── markdown_drawer.py │ │ ├── modal_dialog.py │ │ ├── number.py │ │ ├── plotly.py │ │ ├── progress_bar.py │ │ ├── radio_buttons.py │ │ ├── slider.py │ │ ├── string.py │ │ ├── table.py │ │ ├── tabs.py │ │ ├── task.py │ │ ├── timer.py │ │ ├── toast.py │ │ ├── toggle_button.py │ │ ├── tree.py │ │ └── url_params.py │ │ ├── cli.py │ │ ├── files │ │ ├── __init__.py │ │ └── load_save_from_local.py │ │ ├── loadsave.py │ │ ├── numerous │ │ ├── __init__.py │ │ ├── project.py │ │ ├── projects.py │ │ └── stress_test_projects.py │ │ ├── py.typed │ │ ├── state │ │ ├── __init__.py │ │ └── model.py │ │ ├── task │ │ ├── __init__.py │ │ └── process_task.py │ │ ├── templating │ │ └── __init__.py │ │ └── timeline.py └── tests │ ├── test_cli.py │ └── test_mockup.py └── scripts └── gen_ref_pages.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | 3 | .venv 4 | dist 5 | .DS_Store 6 | 7 | # Python 8 | __pycache__ 9 | .ipynb_checkpoints 10 | 11 | python/src/collections 12 | node_modules 13 | 14 | # Environment files 15 | .env 16 | .env.local 17 | 18 | *collections/ 19 | 20 | *.ruff_cache 21 | *.mypy_cache 22 | 23 | python/src/numerous/widgets/static 24 | 25 | *.mdc 26 | 27 | js/coverage 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: ["pre-commit", "pre-push"] 2 | default_install_hook_types: [pre-commit, pre-push] 3 | repos: 4 | - repo: https://github.com/astral-sh/ruff-pre-commit 5 | # Ruff version. 6 | rev: v0.6.4 7 | hooks: 8 | # Run the linter. 9 | - id: ruff 10 | args: [--fix] 11 | # Run the formatter. 12 | - id: ruff-format 13 | - repo: https://github.com/pre-commit/mirrors-mypy 14 | rev: v1.11.2 15 | hooks: 16 | - id: mypy 17 | entry: "mypy --strict ./python" 18 | pass_filenames: false 19 | additional_dependencies: 20 | - "types-requests" 21 | - "pytest-asyncio" 22 | - "pydantic" 23 | - "marimo" 24 | - repo: local 25 | hooks: 26 | - id: pytest-check 27 | stages: [pre-push] 28 | types: [python] 29 | name: pytest-check 30 | entry: python -m pytest -v python/tests/ 31 | language: system 32 | pass_filenames: false 33 | always_run: true 34 | - id: jest-tests 35 | stages: [pre-push] 36 | types: [javascript, jsx, ts, tsx] 37 | name: jest-tests 38 | entry: powershell -Command "Set-Location js; npm test" 39 | language: system 40 | pass_filenames: false 41 | always_run: true -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | version: 2 3 | 4 | mkdocs: 5 | configuration: mkdocs.yml 6 | 7 | build: 8 | os: "ubuntu-22.04" 9 | tools: 10 | python: "3.12" 11 | 12 | python: 13 | install: 14 | - method: pip 15 | path: . 16 | extra_requirements: 17 | - dev # Install dev dependencies as well 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Numerous Widgets 2 | 3 | Thank you for your interest in contributing to Numerous Widgets! This document provides guidelines and instructions for contributing to this project. 4 | 5 | We will be back soon with more information. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Numerous ApS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-include py.typed 2 | include python/src/numerous/widgets/static/* -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/numerous-com/numerous-widgets/4caca6a44a37521bda66bb954962df7da3dca32f/docs/favicon.png -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/process_task.md: -------------------------------------------------------------------------------- 1 | # ProcessTasks and Control 2 | 3 | The task classes are made to help with running long-running tasks in a separate process with progress tracking. 4 | 5 | The `ProcessTask` class is a base class for controlling the task which can be extended to with a run method to execute the task in a separate process. 6 | 7 | The `SubprocessTask` class is a subclass of `ProcessTask` that uses a subprocess to run the task. Use this if you need to run a task that is not a python script. 8 | 9 | For a simple way to combine a `ProcessTask` with a `Task` widget, see the `process_task_control` function. This will automatically create a `Task` widget and connect it to the `ProcessTask` so you can monitor the progress of the task and control it. 10 | 11 | Example: 12 | 13 | ```python 14 | # Basic usage 15 | from numerous.widgets import SubprocessTask, process_task_control 16 | 17 | # Create a subprocess task with progress tracking 18 | subprocess_task = SubprocessTask( 19 | progress_parser=my_progress_parser # Function to parse progress updates 20 | ) 21 | 22 | # Define command and startup function 23 | cmd = ["executable", "-arg1", "value1", "-arg2", "value2"] 24 | 25 | def on_start_cmd(): 26 | subprocess_task.start(cmd, cwd="working_directory") 27 | 28 | # Create and display the process control widget 29 | process_task = process_task_control( 30 | subprocess_task, 31 | on_start=on_start_cmd 32 | ) 33 | 34 | ``` 35 | 36 | This example shows how to: 37 | 1. Create a `SubprocessTask` with progress tracking 38 | 2. Define a command to run 39 | 3. Create a startup function 40 | 4. Connect everything with `process_task_control` 41 | 42 | ## ::: widgets.ProcessTask 43 | options: 44 | show_root_heading: true 45 | 46 | ## ::: widgets.SubprocessTask 47 | options: 48 | show_root_heading: true 49 | 50 | ## ::: widgets.process_task_control 51 | options: 52 | show_root_heading: true 53 | 54 | -------------------------------------------------------------------------------- /docs/state_model.md: -------------------------------------------------------------------------------- 1 | # State Model 2 | 3 | The StateModel is a pydantic model that can be used to generate a ui from a pydantic model and sync the ui with the model. 4 | 5 | ## Example Usage 6 | 7 | ```python 8 | from numerous.widgets.state import StateModel, number_field 9 | 10 | class MyModel(StateModel): 11 | number: int = number_field(label="Number", tooltip="A number", start=0, stop=100, default=0, multiple_of=1) 12 | 13 | model = MyModel() 14 | number_widget = model.get_widget("number") 15 | 16 | model.number = 10 17 | 18 | model.changed() 19 | # True 20 | 21 | model.update_widgets() 22 | model.changed() 23 | # False 24 | 25 | model.validate("number", 10) 26 | # True 27 | 28 | model.validate("number", 101) 29 | # False 30 | 31 | model_widget.val = 55 32 | model.revert() 33 | # model.number = 10 34 | 35 | new_model = MyModel(number=150) 36 | # raises ValidationError 37 | 38 | new_model.number = 20 39 | model.apply_values(new_model) 40 | # model.number = 20 41 | # number_widget.val = 20 42 | ``` 43 | 44 | ## ::: widgets.state.StateModel 45 | options: 46 | show_root_heading: true 47 | 48 | ## ::: widgets.state.number_field 49 | options: 50 | show_root_heading: true 51 | -------------------------------------------------------------------------------- /docs/styling.md: -------------------------------------------------------------------------------- 1 | # Styling Numerous Widgets 2 | 3 | Numerous Widgets provides a flexible way to customize the appearance of widgets through CSS. The package includes a default stylesheet, but you can easily export and modify it to match your application's design. 4 | 5 | ## Command Line Interface 6 | 7 | The `numerous-widgets` command line tool provides several commands for managing widget styles: 8 | 9 | ### View Available Commands 10 | 11 | To see all available commands and options: 12 | 13 | # Styling Numerous Widgets 14 | 15 | Numerous Widgets provides a flexible way to customize the appearance of widgets through CSS. The package includes a default stylesheet, but you can easily customize it to match your application's design. 16 | 17 | ## Quick Start 18 | 19 | The fastest way to start customizing widget styles is to export the default CSS: 20 | 21 | ```bash 22 | export NUMEROUS_WIDGETS_CSS=/path/to/your/custom.css 23 | ``` 24 | 25 | Or in Python, before importing numerous widgets: 26 | 27 | ```python 28 | import os 29 | os.environ["NUMEROUS_WIDGETS_CSS"] = "/path/to/your/custom.css" 30 | import numerous.widgets 31 | ``` 32 | 33 | ### Export Default CSS 34 | 35 | To export the default CSS to a file: 36 | 37 | ```bash 38 | numerous-widgets export-css 39 | ``` 40 | 41 | Or specify a custom path 42 | 43 | ```bash 44 | numerous-widgets export-css -o ~/my-custom-widgets.css 45 | ``` 46 | 47 | ## CSS Structure 48 | 49 | The stylesheet is organized by widget type. Each widget has its own class namespace to prevent style conflicts: 50 | 51 | ```css 52 | .numerous-number-input { 53 | / Your custom styles here / 54 | border: 1px solid #your-color; 55 | background-color: #your-background; 56 | } 57 | ``` 58 | 59 | ## Best Practices 60 | 61 | 1. **Start with Default Styles**: Always begin by exporting the default CSS to understand the existing structure 62 | 2. **Use CSS Variables**: Leverage CSS variables for consistent theming across widgets 63 | 3. **Maintain Specificity**: Keep the original class structure to ensure proper styling 64 | 4. **Test Thoroughly**: Verify your customizations across different widgets and states (hover, focus, disabled) 65 | 5. **Keep a Backup**: Save the original CSS file before making modifications 66 | 67 | ## Troubleshooting 68 | 69 | If your custom CSS isn't being applied: 70 | 71 | 1. Verify the path in `NUMEROUS_WIDGETS_CSS` is correct and absolute 72 | 2. Check file permissions 73 | 3. Ensure the CSS file exists before importing numerous widgets 74 | 4. Look for warning messages in your application logs 75 | 76 | ## Additional Resources 77 | 78 | - [CSS Variables Reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) 79 | - [CSS Selectors Guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) 80 | -------------------------------------------------------------------------------- /docs/templating.md: -------------------------------------------------------------------------------- 1 | # Templating 2 | 3 | ## ::: widgets.templating.render_template 4 | options: 5 | show_root_heading: true 6 | -------------------------------------------------------------------------------- /docs/widgets.md: -------------------------------------------------------------------------------- 1 | # Widgets 2 | 3 | The widgets are available in the `widgets` module. 4 | 5 | ## ::: widgets.Number 6 | options: 7 | show_root_heading: true 8 | 9 | ## ::: widgets.CheckBox 10 | options: 11 | show_root_heading: true 12 | 13 | ## ::: widgets.DropDown 14 | options: 15 | show_root_heading: true 16 | 17 | ## ::: widgets.Button 18 | options: 19 | show_root_heading: true 20 | 21 | ## ::: widgets.ProgressBar 22 | options: 23 | show_root_heading: true 24 | 25 | ## ::: widgets.Tabs 26 | options: 27 | show_root_heading: true 28 | 29 | ## ::: widgets.LoadSaveWidget 30 | options: 31 | show_root_heading: true 32 | 33 | ## ::: widgets.LoadSaveManager 34 | options: 35 | show_root_heading: true 36 | 37 | ## ::: widgets.Timer 38 | options: 39 | show_root_heading: true 40 | 41 | ## ::: widgets.Task 42 | options: 43 | show_root_heading: true 44 | 45 | ## ::: widgets.MapSelector 46 | options: 47 | show_root_heading: true 48 | 49 | ## ::: widgets.Slider 50 | options: 51 | show_root_heading: true 52 | 53 | ## ::: widgets.RadioButtons 54 | options: 55 | show_root_heading: true 56 | 57 | ## ::: widgets.String 58 | options: 59 | show_root_heading: true 60 | 61 | ## ::: widgets.MarkdownDisplay 62 | options: 63 | show_root_heading: true 64 | 65 | ## ::: widgets.Accordion 66 | options: 67 | show_root_heading: true 68 | 69 | ## ::: widgets.Chat 70 | options: 71 | show_root_heading: true 72 | 73 | ## ::: widgets.Table 74 | options: 75 | show_root_heading: true 76 | 77 | ## ::: widgets.Task 78 | options: 79 | show_root_heading: true 80 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | """Examples package for numerous-widgets.""" 2 | -------------------------------------------------------------------------------- /js/build-widgets.ps1: -------------------------------------------------------------------------------- 1 | # Ensure we can run scripts 2 | $currentPolicy = Get-ExecutionPolicy 3 | if ($currentPolicy -eq "Restricted") { 4 | Write-Host "Current execution policy is Restricted. Attempting to set to RemoteSigned for current process..." 5 | try { 6 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force 7 | } catch { 8 | Write-Error "Failed to set execution policy. Please run PowerShell as Administrator and set execution policy manually." 9 | exit 1 10 | } 11 | } 12 | 13 | # Read widgets from config file 14 | $widgetConfig = Get-Content 'widget-config.json' | ConvertFrom-Json 15 | $widgets = $widgetConfig.widgets 16 | 17 | # Create output directory if it doesn't exist 18 | $outputDir = "../python/src/widgets/static" 19 | if (-not (Test-Path $outputDir)) { 20 | New-Item -ItemType Directory -Path $outputDir -Force 21 | } 22 | 23 | # Build each widget 24 | foreach ($widget in $widgets) { 25 | Write-Host "Building $widget..." -ForegroundColor Green 26 | try { 27 | $env:WIDGET_NAME = $widget 28 | npm run build 29 | if ($LASTEXITCODE -ne 0) { 30 | throw "Build failed for $widget" 31 | } 32 | 33 | # Rename the output file to match the widget name 34 | $outputFile = Join-Path $outputDir "index.mjs" 35 | $targetFile = Join-Path $outputDir "$widget.mjs" 36 | if (Test-Path $outputFile) { 37 | Move-Item -Path $outputFile -Destination $targetFile -Force 38 | } 39 | } catch { 40 | Write-Error ("Error building " + $widget + ": " + $_.Exception.Message) 41 | exit 1 42 | } 43 | } 44 | 45 | Write-Host "`nAll widgets built successfully!" -ForegroundColor Green -------------------------------------------------------------------------------- /js/build-widgets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure the script is run with bash 4 | if [ -z "$BASH_VERSION" ]; then 5 | echo "This script must be run with bash" >&2 6 | exit 1 7 | fi 8 | 9 | # Read widgets from config file 10 | widgets=($(cat widget-config.json | jq -r '.widgets[]')) 11 | 12 | 13 | # Create output directory if it doesn't exist 14 | outputDir="../python/src/numerous/widgets/static" 15 | mkdir -p "$outputDir" 16 | 17 | # Copy styles.css if it exists 18 | if [ -f "dist/styles.css" ]; then 19 | cp dist/styles.css "$outputDir/" 20 | fi 21 | 22 | # Build each widget 23 | for widget in "${widgets[@]}"; do 24 | echo "Building $widget..." 25 | export WIDGET_NAME="$widget" 26 | npm run build 27 | if [ $? -ne 0 ]; then 28 | echo "Build failed for $widget" >&2 29 | exit 1 30 | fi 31 | 32 | # Rename the output file to match the widget name 33 | outputFile="$outputDir/index.mjs" 34 | targetFile="$outputDir/$widget.mjs" 35 | if [ -f "$outputFile" ]; then 36 | mv -f "$outputFile" "$targetFile" 37 | fi 38 | done 39 | 40 | echo -e "\nAll widgets built successfully!" -------------------------------------------------------------------------------- /js/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import reactPlugin from 'eslint-plugin-react'; 4 | 5 | export default [ 6 | js.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | { 9 | files: ['**/*.{ts,tsx}'], 10 | languageOptions: { 11 | parserOptions: { 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | }, 16 | }, 17 | plugins: { 18 | react: reactPlugin, 19 | }, 20 | rules: { 21 | 'react/react-in-jsx-scope': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/no-explicit-any': 'warn', 24 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 25 | }, 26 | settings: { 27 | react: { 28 | version: 'detect', 29 | }, 30 | }, 31 | }, 32 | { 33 | files: ['**/*.{test,spec}.{ts,tsx}'], 34 | languageOptions: { 35 | globals: { 36 | jest: true, 37 | describe: true, 38 | it: true, 39 | expect: true, 40 | beforeEach: true, 41 | afterEach: true, 42 | }, 43 | }, 44 | }, 45 | ]; -------------------------------------------------------------------------------- /js/jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | moduleNameMapper: { 5 | '\\.(css|less|scss|sass)$': '/src/__mocks__/styleMock.js', 6 | }, 7 | setupFilesAfterEnv: ['/src/setupTests.ts'], 8 | transform: { 9 | '^.+\\.tsx?$': ['ts-jest', { 10 | tsconfig: 'tsconfig.json', 11 | useESM: true, 12 | }], 13 | }, 14 | extensionsToTreatAsEsm: ['.ts', '.tsx'], 15 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 16 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 17 | }; -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build", 6 | "typecheck": "tsc --noEmit", 7 | "css": "node scripts/build-css.js", 8 | "css:watch": "node scripts/build-css.js --watch", 9 | "test": "jest", 10 | "test:watch": "jest --watch", 11 | "test:coverage": "jest --coverage", 12 | "lint": "eslint src/**/*.{ts,tsx}", 13 | "lint:fix": "eslint src/**/*.{ts,tsx} --fix" 14 | }, 15 | "dependencies": { 16 | "@anywidget/react": "^0.0.8", 17 | "@headlessui/react": "^2.2.0", 18 | "@minoru/react-dnd-treeview": "^3.4.4", 19 | "@radix-ui/react-dialog": "^1.1.2", 20 | "@tanstack/react-table": "^8.11.7", 21 | "@types/plotly.js": "^2.35.1", 22 | "chart.js": "^4.4.7", 23 | "chartjs-adapter-date-fns": "^3.0.0", 24 | "date-fns": "^4.1.0", 25 | "highlight.js": "^11.10.0", 26 | "katex": "^0.16.11", 27 | "lucide-react": "^0.473.0", 28 | "ol": "^10.2.1", 29 | "plotly.js": "^2.35.2", 30 | "react": "^18.3.1", 31 | "react-chartjs-2": "^5.2.0", 32 | "react-dnd": "^16.0.1", 33 | "react-dnd-html5-backend": "^16.0.1", 34 | "react-dom": "^18.3.1", 35 | "react-markdown": "^9.0.1", 36 | "react-plotly.js": "^2.6.0", 37 | "react-table": "^7.8.0", 38 | "rehype-highlight": "^7.0.1", 39 | "rehype-katex": "^7.0.1", 40 | "remark-gfm": "^4.0.0", 41 | "remark-math": "^6.0.0" 42 | }, 43 | "devDependencies": { 44 | "@anywidget/vite": "^0.2.0", 45 | "@eslint/js": "^9.23.0", 46 | "@testing-library/jest-dom": "^6.6.3", 47 | "@testing-library/react": "^16.2.0", 48 | "@types/jest": "^29.5.14", 49 | "@types/node": "^22.10.0", 50 | "@types/react": "^18.3.18", 51 | "@types/react-dom": "^18.3.5", 52 | "@typescript-eslint/eslint-plugin": "^8.28.0", 53 | "@typescript-eslint/parser": "^8.28.0", 54 | "@vitejs/plugin-react": "^4.3.3", 55 | "chokidar": "^3.5.3", 56 | "esbuild": "^0.24.0", 57 | "eslint": "^9.23.0", 58 | "eslint-plugin-react": "^7.37.4", 59 | "jest": "^29.7.0", 60 | "jest-environment-jsdom": "^29.7.0", 61 | "postcss-css-variables": "^0.19.0", 62 | "postcss-import": "^16.1.0", 63 | "postcss-purgecss": "^5.0.0", 64 | "postcss-url": "^10.1.3", 65 | "sass": "^1.83.4", 66 | "ts-jest": "^29.3.0", 67 | "typescript": "^5.6.3", 68 | "typescript-eslint": "^8.28.0", 69 | "vite": "^5.4.11" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /js/scripts/build-css.js: -------------------------------------------------------------------------------- 1 | const sass = require('sass'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const chokidar = require('chokidar'); 5 | 6 | const config = { 7 | input: 'src/css/styles.scss', 8 | output: 'src/css/styles.css', 9 | watchDir: 'src/css' 10 | }; 11 | 12 | function compileSass() { 13 | try { 14 | console.log('Compiling SASS...'); 15 | const result = sass.compile(config.input, { 16 | style: process.env.NODE_ENV === 'production' ? 'compressed' : 'expanded' 17 | }); 18 | 19 | // Ensure output directory exists 20 | const outputDir = path.dirname(config.output); 21 | if (!fs.existsSync(outputDir)) { 22 | fs.mkdirSync(outputDir, { recursive: true }); 23 | } 24 | 25 | fs.writeFileSync(config.output, result.css); 26 | console.log(`✓ CSS compiled to ${config.output}`); 27 | } catch (error) { 28 | console.error('× SASS compilation failed:', error.message); 29 | } 30 | } 31 | 32 | if (process.argv.includes('--watch')) { 33 | console.log(`Watching ${config.watchDir} for changes...`); 34 | chokidar.watch(config.watchDir).on('all', (event, path) => { 35 | if (path.endsWith('.scss')) { 36 | console.log(`File ${path} changed. Recompiling...`); 37 | compileSass(); 38 | } 39 | }); 40 | } else { 41 | compileSass(); 42 | } -------------------------------------------------------------------------------- /js/src/__mocks__/@anywidget/react.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useModelState = jest.fn().mockImplementation( 4 | (model: any, attribute: string, defaultValue: any) => { 5 | const [value, setValue] = useState(defaultValue); 6 | return [value, setValue]; 7 | } 8 | ); -------------------------------------------------------------------------------- /js/src/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // Mock file for CSS imports 2 | export default {}; -------------------------------------------------------------------------------- /js/src/components/ui/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface AccordionProps { 5 | title: string; 6 | isExpanded: boolean; 7 | uiLabel?: string; 8 | uiTooltip?: string; 9 | onChange: () => void; 10 | } 11 | 12 | export function Accordion({ 13 | title, 14 | isExpanded, 15 | uiLabel, 16 | uiTooltip, 17 | onChange, 18 | }: AccordionProps) { 19 | return ( 20 |
21 | {uiLabel && ( 22 | 26 | )} 27 | 28 |
32 | {title} 33 | 42 | 43 | 44 |
45 |
46 | ); 47 | } -------------------------------------------------------------------------------- /js/src/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface ButtonProps { 4 | label: string; 5 | tooltip: string; 6 | onClick: () => void; 7 | disabled?: boolean; 8 | value?: boolean; 9 | className?: string; 10 | icon?: React.ReactNode; 11 | variant?: 'default' | 'process-step'; 12 | fitToContent?: boolean; 13 | } 14 | 15 | export function Button({ 16 | label, 17 | tooltip, 18 | onClick, 19 | disabled = false, 20 | value = false, 21 | className = '', 22 | icon, 23 | variant = 'default', 24 | fitToContent = false 25 | }: ButtonProps) { 26 | const baseStyles = className ? className : ( 27 | variant === 'process-step' 28 | ? "relative w-16 h-16 rounded-full bg-white/20 border border-white/10 transition-all group" 29 | : `widget-button${fitToContent ? ' fit-to-content' : ''}` 30 | ); 31 | 32 | return ( 33 |
34 | 51 |
52 | ); 53 | } -------------------------------------------------------------------------------- /js/src/components/ui/Chart.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Chart } from 'react-chartjs-2'; 3 | import type { ChartType, ChartData, ChartOptions } from 'chart.js'; 4 | 5 | interface ChartProps { 6 | type: ChartType; 7 | data: ChartData; 8 | options?: ChartOptions; 9 | } 10 | 11 | export function ChartComponent({ type, data, options = {} }: ChartProps) { 12 | const defaultOptions: ChartOptions = { 13 | maintainAspectRatio: false, 14 | responsive: true, 15 | devicePixelRatio: 2, 16 | plugins: { 17 | title: { 18 | font: { 19 | size: 20, 20 | weight: '400' // Lighter weight (normal) 21 | } 22 | } 23 | } 24 | }; 25 | 26 | const mergedOptions = { ...defaultOptions, ...options }; 27 | 28 | return ( 29 |
30 | 31 |
32 | ); 33 | } -------------------------------------------------------------------------------- /js/src/components/ui/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface CheckBoxProps { 5 | value: boolean; 6 | uiLabel: string; 7 | uiTooltip: string; 8 | onChange: (value: boolean) => void; 9 | } 10 | 11 | export function CheckBox({ 12 | value, 13 | uiLabel, 14 | uiTooltip, 15 | onChange 16 | }: CheckBoxProps) { 17 | return ( 18 |
19 | 29 |
30 | ); 31 | } -------------------------------------------------------------------------------- /js/src/components/ui/DateTimePicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface DateTimePickerProps { 5 | value: string; 6 | minDate: string; 7 | maxDate: string; 8 | uiLabel: string; 9 | uiTooltip: string; 10 | onChange: (value: string) => void; 11 | } 12 | 13 | export function DateTimePicker({ 14 | value, 15 | minDate, 16 | maxDate, 17 | uiLabel, 18 | uiTooltip, 19 | onChange 20 | }: DateTimePickerProps) { 21 | const dateValue = value.split('T')[0]; 22 | const timeValue = value.split('T')[1].slice(0, 5); // HH:mm format 23 | 24 | const handleDateChange = (e: React.ChangeEvent) => { 25 | const newDate = e.target.value; 26 | const newValue = `${newDate}T${timeValue}:00`; 27 | onChange(newValue); 28 | }; 29 | 30 | const handleTimeChange = (e: React.ChangeEvent) => { 31 | const newTime = e.target.value; 32 | const newValue = `${dateValue}T${newTime}:00`; 33 | onChange(newValue); 34 | }; 35 | 36 | return ( 37 |
38 |
39 | {uiLabel} 40 | {uiTooltip && } 41 |
42 |
43 | 51 | 57 |
58 |
59 | ); 60 | } -------------------------------------------------------------------------------- /js/src/components/ui/DropDown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface DropDownProps { 5 | selected_key: string; 6 | options: string[]; 7 | uiLabel: string; 8 | uiTooltip: string; 9 | onChange: (selected_key: string) => void; 10 | fitToContent: boolean; 11 | labelInline: boolean; 12 | } 13 | 14 | export function DropDown({ 15 | selected_key, 16 | options, 17 | uiLabel, 18 | uiTooltip, 19 | onChange, 20 | fitToContent, 21 | labelInline 22 | }: DropDownProps) { 23 | const [isOpen, setIsOpen] = React.useState(false); 24 | const containerRef = React.useRef(null); 25 | 26 | // Click outside handler 27 | /*React.useEffect(() => { 28 | const handleClickOutside = (event: MouseEvent) => { 29 | if (containerRef.current && !containerRef.current.contains(event.target as Node)) { 30 | setIsOpen(false); 31 | } 32 | }; 33 | 34 | document.addEventListener('mousedown', handleClickOutside); 35 | return () => { 36 | document.removeEventListener('mousedown', handleClickOutside); 37 | }; 38 | }, []);*/ 39 | 40 | const handleOptionClick = (option: string) => { 41 | onChange(option); 42 | setIsOpen(false); 43 | }; 44 | 45 | const toggleDropdown = () => { 46 | setIsOpen(!isOpen); 47 | }; 48 | 49 | return ( 50 |
51 | {!labelInline && ( 52 | 56 | )} 57 |
58 | {labelInline && ( 59 | 63 | )} 64 |
69 |
{selected_key}
70 |
71 |
72 | {isOpen && ( 73 |
74 | {options.map(option => ( 75 |
handleOptionClick(option)} 79 | > 80 | {option} 81 |
82 | ))} 83 |
84 | )} 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /js/src/components/ui/FileLoader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface FileLoaderProps { 5 | uiLabel: string; 6 | uiTooltip: string; 7 | accept: string; 8 | onFileLoad: (content: Uint8Array, filename: string, encoding: string) => void; 9 | encoding: string; 10 | } 11 | 12 | export function FileLoader({ uiLabel, uiTooltip, accept, onFileLoad }: FileLoaderProps) { 13 | const fileInputRef = React.useRef(null); 14 | 15 | const handleClick = () => { 16 | fileInputRef.current?.click(); 17 | }; 18 | 19 | const handleFileChange = async (event: React.ChangeEvent) => { 20 | const file = event.target.files?.[0]; 21 | if (!file) return; 22 | 23 | try { 24 | const arrayBuffer = await file.arrayBuffer(); 25 | const uint8Array = new Uint8Array(arrayBuffer); 26 | 27 | // Detect encoding from the file content 28 | const detectedEncoding = detectEncoding(uint8Array); 29 | console.log(file.name); 30 | console.log(detectedEncoding); 31 | 32 | // Pass both content and detected encoding back 33 | onFileLoad(uint8Array, file.name, detectedEncoding); 34 | } catch (error) { 35 | console.error('Error loading file:', error); 36 | } 37 | }; 38 | 39 | // Simple encoding detection function 40 | const detectEncoding = (data: Uint8Array): string => { 41 | // Check for UTF-8 BOM 42 | if (data.length >= 3 && data[0] === 0xEF && data[1] === 0xBB && data[2] === 0xBF) { 43 | return 'utf-8'; 44 | } 45 | // Check for UTF-16 LE BOM 46 | if (data.length >= 2 && data[0] === 0xFF && data[1] === 0xFE) { 47 | return 'utf-16le'; 48 | } 49 | // Check for UTF-16 BE BOM 50 | if (data.length >= 2 && data[0] === 0xFE && data[1] === 0xFF) { 51 | return 'utf-16be'; 52 | } 53 | // Default to UTF-8 54 | return 'utf-8'; 55 | }; 56 | 57 | return ( 58 |
59 | 66 | 70 |
71 | ); 72 | } -------------------------------------------------------------------------------- /js/src/components/ui/MarkdownDisplay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactMarkdown from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | import remarkMath from 'remark-math'; 5 | import rehypeKatex from 'rehype-katex'; 6 | import rehypeHighlight from 'rehype-highlight'; 7 | import 'katex/dist/katex.min.css'; 8 | import 'highlight.js/styles/github.css'; 9 | 10 | interface MarkdownDisplayProps { 11 | content: string; 12 | className?: string; 13 | } 14 | 15 | export function MarkdownDisplay({ 16 | content, 17 | className = "" 18 | }: MarkdownDisplayProps) { 19 | return ( 20 | 37 | ); 38 | } -------------------------------------------------------------------------------- /js/src/components/ui/MarkdownDrawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactMarkdown from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | import remarkMath from 'remark-math'; 5 | import rehypeKatex from 'rehype-katex'; 6 | import rehypeHighlight from 'rehype-highlight'; 7 | import { Info } from 'lucide-react'; 8 | import 'katex/dist/katex.min.css'; 9 | import 'highlight.js/styles/github.css'; 10 | 11 | interface MarkdownDrawerProps { 12 | title: string; 13 | content: string; 14 | isOpen: boolean; 15 | onToggle: (isOpen: boolean) => void; 16 | } 17 | 18 | export function MarkdownDrawer({ 19 | title, 20 | content, 21 | isOpen, 22 | onToggle 23 | }: MarkdownDrawerProps) { 24 | const drawerRef = React.useRef(null); 25 | 26 | React.useEffect(() => { 27 | function handleClickOutside(event: MouseEvent) { 28 | if (isOpen && drawerRef.current && !drawerRef.current.contains(event.target as Node)) { 29 | onToggle(false); 30 | } 31 | } 32 | 33 | document.addEventListener('mousedown', handleClickOutside); 34 | return () => { 35 | document.removeEventListener('mousedown', handleClickOutside); 36 | }; 37 | }, [isOpen, onToggle]); 38 | 39 | return ( 40 | <> 41 | {isOpen && ( 42 | 70 | )} 71 | {!isOpen && ( 72 | 82 | )} 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /js/src/components/ui/ModalDialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactMarkdown from 'react-markdown'; 3 | 4 | interface ModalDialogProps { 5 | isOpen: boolean; 6 | title: string; 7 | message: string; 8 | showCancel: boolean; 9 | okLabel: string; 10 | cancelLabel: string; 11 | className?: string; 12 | onResult: (result: 'ok' | 'cancel') => void; 13 | } 14 | 15 | export function ModalDialog({ 16 | isOpen, 17 | title, 18 | message, 19 | showCancel, 20 | okLabel, 21 | cancelLabel, 22 | className = "", 23 | onResult, 24 | }: ModalDialogProps) { 25 | if (!isOpen) return null; 26 | 27 | const handleOk = () => { 28 | onResult('ok'); 29 | }; 30 | 31 | const handleCancel = () => { 32 | onResult('cancel'); 33 | }; 34 | 35 | // Prevent clicks inside the modal from closing it 36 | const handleModalClick = (e: React.MouseEvent) => { 37 | e.stopPropagation(); 38 | }; 39 | 40 | return ( 41 |
42 |
46 |
47 | {title &&

{title}

} 48 |
49 |
50 | 51 | {message} 52 | 53 |
54 |
55 | 61 | {showCancel && ( 62 | 68 | )} 69 |
70 |
71 |
72 | ); 73 | } -------------------------------------------------------------------------------- /js/src/components/ui/Plot.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Plot from 'react-plotly.js'; 3 | import type { Data, Layout, Config } from 'plotly.js'; 4 | 5 | interface PlotProps { 6 | data: Data[]; 7 | layout?: Partial; 8 | config?: Partial; 9 | } 10 | 11 | export function PlotComponent({ data, layout = {}, config = {} }: PlotProps) { 12 | const [isVisible, setIsVisible] = React.useState(false); 13 | const containerRef = React.useRef(null); 14 | 15 | React.useEffect(() => { 16 | if (containerRef.current) { 17 | setIsVisible(true); 18 | } 19 | }, []); 20 | 21 | const defaultLayout = { 22 | autosize: true, 23 | margin: { t: 40, r: 10, l: 50, b: 50 }, 24 | ...layout, 25 | }; 26 | 27 | const defaultConfig = { 28 | responsive: true, 29 | displayModeBar: false, 30 | ...config, 31 | }; 32 | 33 | return ( 34 |
35 | {isVisible && ( 36 | 43 | )} 44 |
45 | ); 46 | } -------------------------------------------------------------------------------- /js/src/components/ui/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | import '../../css/components/ProgressBar.scss'; 4 | 5 | interface ProgressBarProps { 6 | value: number; 7 | uiLabel?: string; 8 | uiTooltip?: string; 9 | fitToContent?: boolean; 10 | labelInline?: boolean; 11 | } 12 | 13 | export function ProgressBar({ 14 | value, 15 | uiLabel, 16 | uiTooltip, 17 | fitToContent = false, 18 | labelInline = true 19 | }: ProgressBarProps) { 20 | // Ensure value is between 0 and 100 21 | const clampedValue = Math.min(Math.max(value, 0), 100); 22 | 23 | return ( 24 |
25 | {uiLabel && !labelInline && ( 26 | 30 | )} 31 |
32 | {uiLabel && labelInline && ( 33 | 37 | )} 38 |
45 |
49 |
50 |
{Math.round(clampedValue)}%
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /js/src/components/ui/RadioButtons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface RadioButtonsProps { 5 | value: string; 6 | options: string[]; 7 | uiLabel: string; 8 | uiTooltip: string; 9 | onChange: (value: string) => void; 10 | fitToContent?: boolean; 11 | labelInline?: boolean; 12 | } 13 | 14 | export function RadioButtons({ 15 | value, 16 | options, 17 | uiLabel, 18 | uiTooltip, 19 | onChange, 20 | fitToContent = false, 21 | labelInline = true, 22 | }: RadioButtonsProps) { 23 | return ( 24 |
25 | {!labelInline && ( 26 |
27 | {uiLabel} 28 | {uiTooltip && } 29 |
30 | )} 31 |
32 | {labelInline && ( 33 |
34 | {uiLabel} 35 | {uiTooltip && } 36 |
37 | )} 38 |
39 | {options.map((option) => ( 40 | 49 | ))} 50 |
51 |
52 |
53 | ); 54 | } -------------------------------------------------------------------------------- /js/src/components/ui/Slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface SliderProps { 5 | value: number; 6 | minValue: number; 7 | maxValue: number; 8 | step: number; 9 | uiLabel: string; 10 | uiTooltip: string; 11 | onChange: (value: number) => void; 12 | fitToContent?: boolean; 13 | labelInline?: boolean; 14 | } 15 | 16 | export function Slider({ 17 | value, 18 | minValue, 19 | maxValue, 20 | step, 21 | uiLabel, 22 | uiTooltip, 23 | onChange, 24 | fitToContent = false, 25 | labelInline = true 26 | }: SliderProps) { 27 | const percentage = ((value - minValue) / (maxValue - minValue)) * 100; 28 | 29 | return ( 30 |
31 | {!labelInline && ( 32 | 36 | )} 37 |
38 | {labelInline && ( 39 | 43 | )} 44 |
{value}
45 |
46 | onChange(parseFloat(e.target.value))} 53 | className="slider-input" 54 | style={{ 55 | background: `linear-gradient(to right, 56 | var(--ui-widget-focus-border) 0%, 57 | var(--ui-widget-focus-border) ${percentage}%, 58 | var(--ui-widget-secondary-background) ${percentage}%, 59 | var(--ui-widget-secondary-background) 100%)` 60 | }} 61 | /> 62 |
63 |
64 |
65 | ); 66 | } -------------------------------------------------------------------------------- /js/src/components/ui/String.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface StringInputProps { 5 | value: string; 6 | uiLabel: string; 7 | uiTooltip: string; 8 | placeholder?: string; 9 | onChange: (value: string) => void; 10 | fitToContent: boolean; 11 | isValid: boolean; 12 | isPassword?: boolean; 13 | validationMessage: string; 14 | labelInline: boolean; 15 | } 16 | 17 | export function StringInput({ 18 | value, 19 | uiLabel, 20 | uiTooltip, 21 | placeholder, 22 | onChange, 23 | fitToContent, 24 | isValid, 25 | isPassword, 26 | validationMessage, 27 | labelInline 28 | }: StringInputProps) { 29 | 30 | return ( 31 |
32 | {!labelInline && ( 33 | 37 | )} 38 |
39 | {labelInline && ( 40 | 44 | )} 45 | onChange(e.target.value)} 50 | /> 51 | {!isValid && validationMessage && ( 52 |
53 | {validationMessage} 54 |
55 | )} 56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /js/src/components/ui/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | interface TabsProps { 5 | value: string; 6 | tabs: string[]; 7 | uiLabel: string; 8 | uiTooltip: string; 9 | contentUpdated: boolean; 10 | onChange: (value: string) => void; 11 | setContentUpdated?: (updated: boolean) => void; 12 | } 13 | 14 | export function Tabs({ 15 | value, 16 | tabs, 17 | uiLabel, 18 | uiTooltip, 19 | contentUpdated, 20 | onChange, 21 | setContentUpdated, 22 | }: TabsProps) { 23 | React.useEffect(() => { 24 | if (contentUpdated && setContentUpdated) { 25 | setContentUpdated(false); 26 | } 27 | }, [contentUpdated, setContentUpdated]); 28 | 29 | return ( 30 |
31 | {uiLabel && ( 32 | 36 | )} 37 | 38 |
39 |
40 | {tabs.map(tab => ( 41 |
onChange(tab)} 45 | > 46 | {tab} 47 |
48 | ))} 49 |
50 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /js/src/components/ui/Timer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface TimerProps { 4 | interval: number; 5 | isActive: boolean; 6 | uiLabel: string; 7 | onTick: () => void; 8 | onToggle: (active: boolean) => void; 9 | } 10 | 11 | export function Timer({ interval, isActive, uiLabel, onTick, onToggle }: TimerProps) { 12 | const [isBlinking, setIsBlinking] = React.useState(false); 13 | const [showReset, setShowReset] = React.useState(false); 14 | const intervalRef = React.useRef(null); 15 | 16 | React.useEffect(() => { 17 | if (isActive) { 18 | setShowReset(false); 19 | 20 | const startTime = Date.now(); 21 | 22 | const tick = () => { 23 | const elapsed = (Date.now() - startTime) % (interval * 1000); 24 | 25 | if (elapsed < 50) { 26 | 27 | onTick(); 28 | setIsBlinking(true); 29 | setTimeout(() => setIsBlinking(false), 250); 30 | } 31 | }; 32 | 33 | 34 | onTick(); 35 | setIsBlinking(true); 36 | setTimeout(() => setIsBlinking(false), 250); 37 | 38 | intervalRef.current = setInterval(tick, 50); 39 | 40 | return () => { 41 | 42 | if (intervalRef.current) { 43 | clearInterval(intervalRef.current); 44 | intervalRef.current = null; 45 | } 46 | setShowReset(true); // Show reset when timer stops 47 | }; 48 | } else { 49 | 50 | if (intervalRef.current) { 51 | clearInterval(intervalRef.current); 52 | intervalRef.current = null; 53 | } 54 | setIsBlinking(false); 55 | setShowReset(true); // Show reset when timer is inactive 56 | } 57 | }, [isActive, interval, onTick]); 58 | 59 | const handleClick = () => { 60 | if (showReset) { 61 | // Reset action 62 | setShowReset(false); 63 | onToggle(true); 64 | } else { 65 | // Normal toggle 66 | onToggle(!isActive); 67 | } 68 | }; 69 | 70 | return ( 71 |
{ 75 | if (!isActive) setShowReset(true); 76 | }} 77 | style={{ 78 | width: '20px', 79 | height: '20px', 80 | borderRadius: '50%', 81 | backgroundColor: showReset ? '#2196F3' : (isBlinking ? '#4CAF50' : '#ddd'), 82 | cursor: 'pointer', 83 | transition: 'background-color 0.1s ease-in-out', 84 | display: 'flex', 85 | alignItems: 'center', 86 | justifyContent: 'center', 87 | ...(showReset && { 88 | '&::after': { 89 | content: '↺', 90 | color: 'white', 91 | fontSize: '14px' 92 | } 93 | }) 94 | }} 95 | > 96 | {showReset && "↺"} 97 |
98 | ); 99 | } -------------------------------------------------------------------------------- /js/src/components/ui/Toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface ToastProps { 4 | message: string; 5 | duration?: number; 6 | onDismiss: () => void; 7 | visible: boolean; 8 | } 9 | 10 | export function Toast({ 11 | message, 12 | duration = 3000, 13 | onDismiss, 14 | visible 15 | }: ToastProps) { 16 | const [progress, setProgress] = React.useState(100); 17 | 18 | React.useEffect(() => { 19 | if (!visible) { 20 | setProgress(100); 21 | return; 22 | } 23 | 24 | const startTime = Date.now(); 25 | const endTime = startTime + duration; 26 | 27 | const updateProgress = () => { 28 | const now = Date.now(); 29 | const remaining = Math.max(0, endTime - now); 30 | const newProgress = (remaining / duration) * 100; 31 | 32 | if (newProgress <= 0) { 33 | onDismiss(); 34 | } else { 35 | setProgress(newProgress); 36 | requestAnimationFrame(updateProgress); 37 | } 38 | }; 39 | 40 | const animationFrame = requestAnimationFrame(updateProgress); 41 | return () => cancelAnimationFrame(animationFrame); 42 | }, [visible, duration, onDismiss]); 43 | 44 | if (!visible) return null; 45 | 46 | return ( 47 |
48 |
49 | 58 |

{message}

59 |
60 |
64 |
65 |
66 |
67 | ); 68 | } -------------------------------------------------------------------------------- /js/src/components/ui/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface TooltipProps { 4 | tooltip: string; 5 | } 6 | 7 | export function Tooltip({ tooltip }: TooltipProps) { 8 | return ( 9 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } -------------------------------------------------------------------------------- /js/src/components/ui/Workflow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ChevronLeft, ChevronRight, Check } from "lucide-react"; 3 | 4 | interface WorkflowProps { 5 | steps: string[]; 6 | activeStep: number; 7 | completedSteps: number[]; 8 | onStepChange: (step: number) => void; 9 | } 10 | 11 | export function Workflow({ steps, activeStep, completedSteps, onStepChange }: WorkflowProps) { 12 | const canGoToStep = (stepIndex: number) => { 13 | // Can go to completed steps or the first uncompleted step 14 | return completedSteps.includes(stepIndex) || 15 | (stepIndex === 0 || completedSteps.length >= stepIndex); 16 | }; 17 | 18 | return ( 19 |
20 | 27 | 28 |
29 | {steps.map((step, index) => ( 30 |
canGoToStep(index) && onStepChange(index)} 36 | > 37 |
38 | {completedSteps.includes(index) ? ( 39 | 40 | ) : ( 41 | index + 1 42 | )} 43 |
44 |
{step}
45 |
46 | ))} 47 |
48 | 49 | 56 |
57 | ); 58 | } -------------------------------------------------------------------------------- /js/src/components/ui/chartjs-registration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chart as ChartJS, 3 | CategoryScale, 4 | LinearScale, 5 | PointElement, 6 | LineElement, 7 | BarElement, 8 | ArcElement, 9 | Title, 10 | Tooltip, 11 | Legend, 12 | LineController, 13 | BarController, 14 | PieController, 15 | DoughnutController, 16 | TimeScale 17 | } from 'chart.js'; 18 | import 'chartjs-adapter-date-fns'; 19 | 20 | // Register Chart.js components and controllers 21 | ChartJS.register( 22 | CategoryScale, 23 | LinearScale, 24 | TimeScale, 25 | PointElement, 26 | LineElement, 27 | BarElement, 28 | ArcElement, 29 | Title, 30 | Tooltip, 31 | Legend, 32 | LineController, 33 | BarController, 34 | PieController, 35 | DoughnutController 36 | ); 37 | 38 | // Set global font defaults 39 | ChartJS.defaults.font.family = '"Nunito Sans", sans-serif'; 40 | ChartJS.defaults.font.size = 12; -------------------------------------------------------------------------------- /js/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as DialogPrimitive from '@radix-ui/react-dialog' 3 | 4 | const Dialog = DialogPrimitive.Root 5 | const DialogTrigger = DialogPrimitive.Trigger 6 | const DialogPortal = DialogPrimitive.Portal 7 | const DialogClose = DialogPrimitive.Close 8 | const DialogTitle = DialogPrimitive.Title 9 | const DialogDescription = DialogPrimitive.Description 10 | 11 | const DialogOverlay = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 22 | 23 | const DialogContent = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | 29 | 34 | {children} 35 | 36 | 37 | )) 38 | DialogContent.displayName = DialogPrimitive.Content.displayName 39 | 40 | export { 41 | Dialog, 42 | DialogTrigger, 43 | DialogContent, 44 | DialogClose, 45 | DialogTitle, 46 | DialogDescription, 47 | } -------------------------------------------------------------------------------- /js/src/components/ui/projects/ItemDetails.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | interface ItemDetailsProps { 4 | name: string; 5 | description: string; 6 | title: string; 7 | onNameChange: (name: string) => void; 8 | onDescriptionChange: (description: string) => void; 9 | } 10 | 11 | export const ItemDetails: React.FC = ({ 12 | name, 13 | description, 14 | title, 15 | onNameChange, 16 | onDescriptionChange, 17 | children, 18 | }) => { 19 | return ( 20 |
21 |
22 |

{title}

23 |
24 | 25 | onNameChange(e.target.value)} 29 | className="w-full border rounded p-2" 30 | /> 31 |
32 |
33 | 34 |