This is page 93 of 186. Use http://codebase.md/xmlui-org/xmlui/tools/vscode/resources/assets/img/bg-iphone-14-pro.jpg?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ └── config.json
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog-optimized.yml
│ ├── deploy-blog-swa.yml
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── an-advanced-codefence.gif
│ │ │ │ ├── an-advanced-codefence.mp4
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── codefence-runner.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ ├── lorem-ipsum.png
│ │ │ │ ├── playground-checkbox-source.png
│ │ │ │ ├── playground.png
│ │ │ │ ├── use-xmlui-mcp-to-find-a-howto.png
│ │ │ │ └── xmlui-demo-gallery.png
│ │ │ ├── introducing-xmlui.md
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ ├── xmlui-playground.md
│ │ │ └── xmlui-powered-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ ├── staticwebapp.config.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ └── PageNotFound.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── blog-theme.ts
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── FancyButton.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── control-cache-invalidation.md
│ │ │ │ ├── debounce-user-input-for-api-calls.md
│ │ │ │ ├── debounce-with-changelistener.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── github.svg
│ │ │ ├── images
│ │ │ │ ├── apiaction-tutorial
│ │ │ │ │ ├── add-success.png
│ │ │ │ │ ├── apiaction-param.png
│ │ │ │ │ ├── change-completed.png
│ │ │ │ │ ├── change-in-progress.png
│ │ │ │ │ ├── confirm-delete.png
│ │ │ │ │ ├── data-error.png
│ │ │ │ │ ├── data-progress.png
│ │ │ │ │ ├── data-success.png
│ │ │ │ │ ├── display-1.png
│ │ │ │ │ ├── item-deleted.png
│ │ │ │ │ ├── item-updated.png
│ │ │ │ │ ├── missing-api-key.png
│ │ │ │ │ ├── new-item-added.png
│ │ │ │ │ └── test-message.png
│ │ │ │ ├── chat-api
│ │ │ │ │ └── domain-model.svg
│ │ │ │ ├── components
│ │ │ │ │ ├── image
│ │ │ │ │ │ └── breakfast.jpg
│ │ │ │ │ ├── markdown
│ │ │ │ │ │ └── colors.png
│ │ │ │ │ └── modal
│ │ │ │ │ ├── deep_link_dialog_1.jpg
│ │ │ │ │ └── deep_link_dialog_2.jpg
│ │ │ │ ├── create-apps
│ │ │ │ │ ├── collapsed-vertical.png
│ │ │ │ │ ├── using-forms-warning-dialog.png
│ │ │ │ │ └── using-forms.png
│ │ │ │ ├── datasource-tutorial
│ │ │ │ │ ├── data-with-header.png
│ │ │ │ │ ├── filtered-data.png
│ │ │ │ │ ├── filtered-items.png
│ │ │ │ │ ├── initial-page-items.png
│ │ │ │ │ ├── list-items.png
│ │ │ │ │ ├── next-page-items.png
│ │ │ │ │ ├── no-data.png
│ │ │ │ │ ├── pagination-1.jpg
│ │ │ │ │ ├── pagination-1.png
│ │ │ │ │ ├── polling-1.png
│ │ │ │ │ ├── refetch-data.png
│ │ │ │ │ ├── slow-loading.png
│ │ │ │ │ ├── test-message.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── unconventional-data.png
│ │ │ │ │ └── unfiltered-items.png
│ │ │ │ ├── flower.jpg
│ │ │ │ ├── get-started
│ │ │ │ │ ├── add-new-contact.png
│ │ │ │ │ ├── app-modified.png
│ │ │ │ │ ├── app-start.png
│ │ │ │ │ ├── app-with-boxes.png
│ │ │ │ │ ├── app-with-toast.png
│ │ │ │ │ ├── boilerplate-structure.png
│ │ │ │ │ ├── cl-initial.png
│ │ │ │ │ ├── cl-start.png
│ │ │ │ │ ├── contact-counts.png
│ │ │ │ │ ├── contact-dialog-title.png
│ │ │ │ │ ├── contact-dialog.png
│ │ │ │ │ ├── contact-menus.png
│ │ │ │ │ ├── contact-predicates.png
│ │ │ │ │ ├── context-menu.png
│ │ │ │ │ ├── dashboard-numbers.png
│ │ │ │ │ ├── default-contact-list.png
│ │ │ │ │ ├── delete-contact.png
│ │ │ │ │ ├── delete-task.png
│ │ │ │ │ ├── detailed-template.png
│ │ │ │ │ ├── edit-contact-details.png
│ │ │ │ │ ├── edited-contact-saved.png
│ │ │ │ │ ├── empty-sections.png
│ │ │ │ │ ├── filter-completed.png
│ │ │ │ │ ├── fullwidth-desktop.png
│ │ │ │ │ ├── fullwidth-mobile.png
│ │ │ │ │ ├── initial-table.png
│ │ │ │ │ ├── items-and-badges.png
│ │ │ │ │ ├── loading-message.png
│ │ │ │ │ ├── new-contact-button.png
│ │ │ │ │ ├── new-contact-saved.png
│ │ │ │ │ ├── no-empty-sections.png
│ │ │ │ │ ├── personal-todo-initial.png
│ │ │ │ │ ├── piechart.png
│ │ │ │ │ ├── review-today.png
│ │ │ │ │ ├── rudimentary-dashboard.png
│ │ │ │ │ ├── section-collapsed.png
│ │ │ │ │ ├── sectioned-items.png
│ │ │ │ │ ├── sections-ordered.png
│ │ │ │ │ ├── spacex-list-with-links.png
│ │ │ │ │ ├── spacex-list.png
│ │ │ │ │ ├── start-personal-todo-1.png
│ │ │ │ │ ├── submit-new-contact.png
│ │ │ │ │ ├── submit-new-task.png
│ │ │ │ │ ├── syntax-highlighting.png
│ │ │ │ │ ├── table-with-badge.png
│ │ │ │ │ ├── template-with-card.png
│ │ │ │ │ ├── test-emulated-api.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── todo-logo.png
│ │ │ │ │ └── xmlui-tools.png
│ │ │ │ ├── HelloApp.png
│ │ │ │ ├── HelloApp2.png
│ │ │ │ ├── logos
│ │ │ │ │ ├── xmlui1.svg
│ │ │ │ │ ├── xmlui2.svg
│ │ │ │ │ ├── xmlui3.svg
│ │ │ │ │ ├── xmlui4.svg
│ │ │ │ │ ├── xmlui5.svg
│ │ │ │ │ ├── xmlui6.svg
│ │ │ │ │ └── xmlui7.svg
│ │ │ │ ├── pdf
│ │ │ │ │ └── dummy-pdf.jpg
│ │ │ │ ├── rendering-engine
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ ├── Component.svg
│ │ │ │ │ ├── CompoundComponent.svg
│ │ │ │ │ ├── RootComponent.svg
│ │ │ │ │ └── tree-with-containers.svg
│ │ │ │ ├── reviewers-guide
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ └── incbutton-in-action.png
│ │ │ │ ├── tools
│ │ │ │ │ └── boilerplate-structure.png
│ │ │ │ ├── try.svg
│ │ │ │ ├── tutorial
│ │ │ │ │ ├── app-chat-history.png
│ │ │ │ │ ├── app-content-placeholder.png
│ │ │ │ │ ├── app-header-and-content.png
│ │ │ │ │ ├── app-links-channel-selected.png
│ │ │ │ │ ├── app-links-click.png
│ │ │ │ │ ├── app-navigation.png
│ │ │ │ │ ├── finished-ex01.png
│ │ │ │ │ ├── finished-ex02.png
│ │ │ │ │ ├── hello.png
│ │ │ │ │ ├── splash-screen-advanced.png
│ │ │ │ │ ├── splash-screen-after-click.png
│ │ │ │ │ ├── splash-screen-centered.png
│ │ │ │ │ ├── splash-screen-events.png
│ │ │ │ │ ├── splash-screen-expression.png
│ │ │ │ │ ├── splash-screen-reuse-after.png
│ │ │ │ │ ├── splash-screen-reuse-before.png
│ │ │ │ │ └── splash-screen.png
│ │ │ │ └── tutorial-01.png
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── Boxes.xmlui
│ │ │ ├── Breadcrumb.xmlui
│ │ │ ├── ChangeLog.xmlui
│ │ │ ├── ColorPalette.xmlui
│ │ │ ├── DocumentLinks.xmlui
│ │ │ ├── DocumentPage.xmlui
│ │ │ ├── DocumentPageNoTOC.xmlui
│ │ │ ├── Icons.xmlui
│ │ │ ├── IncButton.xmlui
│ │ │ ├── IncButton2.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── docs-theme.ts
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── Animation.tsx
│ │ ├── AnimationNative.tsx
│ │ ├── FadeAnimation.tsx
│ │ ├── FadeInAnimation.tsx
│ │ ├── FadeOutAnimation.tsx
│ │ ├── index.tsx
│ │ ├── ScaleAnimation.tsx
│ │ └── SlideInAnimation.tsx
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── HelloWorld.module.scss
│ │ ├── HelloWorld.tsx
│ │ ├── HelloWorldNative.tsx
│ │ └── index.tsx
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── IPhoneFrame.module.scss
│ │ ├── IPhoneFrame.tsx
│ │ ├── MacOSAppFrame.module.scss
│ │ ├── MacOSAppFrame.tsx
│ │ ├── WindowsAppFrame.module.scss
│ │ └── WindowsAppFrame.tsx
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── LazyPdfNative.tsx
│ │ ├── Pdf.module.scss
│ │ └── Pdf.tsx
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── hooks
│ │ │ ├── usePlayground.ts
│ │ │ └── useToast.ts
│ │ ├── index.tsx
│ │ ├── playground
│ │ │ ├── Box.module.scss
│ │ │ ├── Box.tsx
│ │ │ ├── CodeSelector.tsx
│ │ │ ├── ConfirmationDialog.module.scss
│ │ │ ├── ConfirmationDialog.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── Header.module.scss
│ │ │ ├── Header.tsx
│ │ │ ├── Playground.tsx
│ │ │ ├── PlaygroundContent.module.scss
│ │ │ ├── PlaygroundContent.tsx
│ │ │ ├── PlaygroundNative.module.scss
│ │ │ ├── PlaygroundNative.tsx
│ │ │ ├── Preview.module.scss
│ │ │ ├── Preview.tsx
│ │ │ ├── Select.module.scss
│ │ │ ├── StandalonePlayground.tsx
│ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ ├── ThemeSwitcher.module.scss
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ ├── ToneSwitcher.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── providers
│ │ │ ├── Toast.module.scss
│ │ │ └── ToastProvider.tsx
│ │ ├── state
│ │ │ └── store.ts
│ │ ├── themes
│ │ │ └── theme.ts
│ │ └── utils
│ │ └── helpers.ts
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Search.module.scss
│ │ └── Search.tsx
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Spreadsheet.tsx
│ │ └── SpreadsheetNative.tsx
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ └── src
│ ├── Carousel
│ │ ├── Carousel.module.scss
│ │ ├── Carousel.tsx
│ │ ├── CarouselContext.tsx
│ │ └── CarouselNative.tsx
│ ├── FancyButton
│ │ ├── FancyButton.module.scss
│ │ ├── FancyButton.tsx
│ │ └── FancyButton.xmlui
│ ├── Hello
│ │ ├── Hello.tsx
│ │ ├── Hello.xmlui
│ │ └── Hello.xmlui.xs
│ ├── HeroSection
│ │ ├── HeroSection.module.scss
│ │ ├── HeroSection.spec.ts
│ │ ├── HeroSection.tsx
│ │ └── HeroSectionNative.tsx
│ ├── index.tsx
│ ├── ScrollToTop
│ │ ├── ScrollToTop.module.scss
│ │ ├── ScrollToTop.tsx
│ │ └── ScrollToTopNative.tsx
│ └── vite-env.d.ts
├── playwright.config.ts
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.cjs
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── component-metadata.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── theme-variables-refactoring.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── extract-component-metadata.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── generate-metadata-markdown.js
│ ├── get-langserver-metadata.js
│ ├── inline-links.mjs
│ └── README-e2e-summary.md
├── src
│ ├── abstractions
│ │ ├── _conventions.md
│ │ ├── ActionDefs.ts
│ │ ├── AppContextDefs.ts
│ │ ├── ComponentDefs.ts
│ │ ├── ContainerDefs.ts
│ │ ├── ExtensionDefs.ts
│ │ ├── FunctionDefs.ts
│ │ ├── RendererDefs.ts
│ │ ├── scripting
│ │ │ ├── BlockScope.ts
│ │ │ ├── Compilation.ts
│ │ │ ├── LogicalThread.ts
│ │ │ ├── LoopScope.ts
│ │ │ ├── modules.ts
│ │ │ ├── ScriptParserError.ts
│ │ │ ├── Token.ts
│ │ │ ├── TryScope.ts
│ │ │ └── TryScopeExp.ts
│ │ └── ThemingDefs.ts
│ ├── components
│ │ ├── _conventions.md
│ │ ├── abstractions.ts
│ │ ├── Accordion
│ │ │ ├── Accordion.md
│ │ │ ├── Accordion.module.scss
│ │ │ ├── Accordion.spec.ts
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── AccordionItem.tsx
│ │ │ ├── AccordionItemNative.tsx
│ │ │ └── AccordionNative.tsx
│ │ ├── Animation
│ │ │ └── AnimationNative.tsx
│ │ ├── APICall
│ │ │ ├── APICall.md
│ │ │ ├── APICall.spec.ts
│ │ │ ├── APICall.tsx
│ │ │ └── APICallNative.tsx
│ │ ├── App
│ │ │ ├── App.md
│ │ │ ├── App.module.scss
│ │ │ ├── App.spec.ts
│ │ │ ├── App.tsx
│ │ │ ├── AppLayoutContext.ts
│ │ │ ├── AppNative.tsx
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── AppHeader
│ │ │ ├── AppHeader.md
│ │ │ ├── AppHeader.module.scss
│ │ │ ├── AppHeader.spec.ts
│ │ │ ├── AppHeader.tsx
│ │ │ └── AppHeaderNative.tsx
│ │ ├── AppState
│ │ │ ├── AppState.md
│ │ │ ├── AppState.spec.ts
│ │ │ ├── AppState.tsx
│ │ │ └── AppStateNative.tsx
│ │ ├── AutoComplete
│ │ │ ├── AutoComplete.md
│ │ │ ├── AutoComplete.module.scss
│ │ │ ├── AutoComplete.spec.ts
│ │ │ ├── AutoComplete.tsx
│ │ │ ├── AutoCompleteContext.tsx
│ │ │ └── AutoCompleteNative.tsx
│ │ ├── Avatar
│ │ │ ├── Avatar.md
│ │ │ ├── Avatar.module.scss
│ │ │ ├── Avatar.spec.ts
│ │ │ ├── Avatar.tsx
│ │ │ └── AvatarNative.tsx
│ │ ├── Backdrop
│ │ │ ├── Backdrop.md
│ │ │ ├── Backdrop.module.scss
│ │ │ ├── Backdrop.spec.ts
│ │ │ ├── Backdrop.tsx
│ │ │ └── BackdropNative.tsx
│ │ ├── Badge
│ │ │ ├── Badge.md
│ │ │ ├── Badge.module.scss
│ │ │ ├── Badge.spec.ts
│ │ │ ├── Badge.tsx
│ │ │ └── BadgeNative.tsx
│ │ ├── Bookmark
│ │ │ ├── Bookmark.md
│ │ │ ├── Bookmark.module.scss
│ │ │ ├── Bookmark.spec.ts
│ │ │ ├── Bookmark.tsx
│ │ │ └── BookmarkNative.tsx
│ │ ├── Breakout
│ │ │ ├── Breakout.module.scss
│ │ │ ├── Breakout.spec.ts
│ │ │ ├── Breakout.tsx
│ │ │ └── BreakoutNative.tsx
│ │ ├── Button
│ │ │ ├── Button-style.spec.ts
│ │ │ ├── Button.md
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.spec.ts
│ │ │ ├── Button.tsx
│ │ │ └── ButtonNative.tsx
│ │ ├── Card
│ │ │ ├── Card.md
│ │ │ ├── Card.module.scss
│ │ │ ├── Card.spec.ts
│ │ │ ├── Card.tsx
│ │ │ └── CardNative.tsx
│ │ ├── Carousel
│ │ │ ├── Carousel.md
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.spec.ts
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ ├── CarouselItem.tsx
│ │ │ ├── CarouselItemNative.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── ChangeListener
│ │ │ ├── ChangeListener.md
│ │ │ ├── ChangeListener.spec.ts
│ │ │ ├── ChangeListener.tsx
│ │ │ └── ChangeListenerNative.tsx
│ │ ├── chart-color-schemes.ts
│ │ ├── Charts
│ │ │ ├── AreaChart
│ │ │ │ ├── AreaChart.md
│ │ │ │ ├── AreaChart.spec.ts
│ │ │ │ ├── AreaChart.tsx
│ │ │ │ └── AreaChartNative.tsx
│ │ │ ├── BarChart
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── BarChart.module.scss
│ │ │ │ ├── BarChart.spec.ts
│ │ │ │ ├── BarChart.tsx
│ │ │ │ └── BarChartNative.tsx
│ │ │ ├── DonutChart
│ │ │ │ ├── DonutChart.spec.ts
│ │ │ │ └── DonutChart.tsx
│ │ │ ├── LabelList
│ │ │ │ ├── LabelList.module.scss
│ │ │ │ ├── LabelList.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ └── ContentSeparatorNative.tsx
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.js
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── index.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── index.ts
│ │ ├── infrastructure
│ │ │ ├── index.html
│ │ │ ├── main.tsx
│ │ │ ├── public
│ │ │ │ ├── mockServiceWorker.js
│ │ │ │ ├── resources
│ │ │ │ │ ├── bell.svg
│ │ │ │ │ ├── box.svg
│ │ │ │ │ ├── doc.svg
│ │ │ │ │ ├── eye.svg
│ │ │ │ │ ├── flower-640x480.jpg
│ │ │ │ │ ├── sun.svg
│ │ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ │ └── txt.svg
│ │ │ │ └── serve.json
│ │ │ └── TestBed.tsx
│ │ └── themed-app-test-helpers.ts
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.bin.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/docs/content/components/FormItem.md:
--------------------------------------------------------------------------------
```markdown
1 | # FormItem [#formitem]
2 |
3 | `FormItem` wraps individual input controls within a `Form`, providing data binding, validation, labeling, and layout functionality. It connects form controls to the parent form's data model and handles validation feedback automatically.
4 |
5 | > **Note:** `FormItem` must be used inside a `Form` component.
6 |
7 | **Key features:**
8 | - **Data binding**: Automatically syncs control values with form data using the `bindTo` property
9 | - **Validation**: Displays validation states and error messages for the associated input
10 | - **Flexible labeling**: Supports labels, helper text, and various label positioning options
11 | - **Layout management**: Handles consistent spacing and alignment of form elements
12 |
13 | See [this guide](/forms) for details.
14 |
15 | **Context variables available during execution:**
16 |
17 | - `$setValue`: Function to set the FormItem's value programmatically
18 | - `$validationResult`: Current validation state and error messages for this field
19 | - `$value`: Current value of the FormItem, accessible in expressions and code snippets
20 |
21 | ## Properties [#properties]
22 |
23 | ### `autoFocus` (default: false) [#autofocus-default-false]
24 |
25 | If this property is set to `true`, the component gets the focus automatically when displayed.
26 |
27 | ### `bindTo` [#bindto]
28 |
29 | This property binds a particular input field to one of the attributes of the `Form` data. It names the property of the form's `data` data to get the input's initial value.When the field is saved, its value will be stored in the `data` property with this name. If the property is not set, the input will be bound to an internal data field but not submitted.
30 |
31 | Try to enter some kind of text in the input field labelled `Lastname` and submit the form. Note how the submitted data looks like compared to the one set in `data`.
32 |
33 | ```xmlui-pg copy display name="Example: bindTo"
34 | <App>
35 | <Form
36 | data="{{ firstname: 'James', lastname: 'Clewell' }}"
37 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
38 | <FormItem label="Firstname" bindTo="firstname" />
39 | <FormItem label="Lastname" />
40 | </Form>
41 | </App>
42 | ```
43 |
44 | ### `customValidationsDebounce` (default: 0) [#customvalidationsdebounce-default-0]
45 |
46 | This optional number prop determines the time interval between two runs of a custom validation.
47 |
48 | Note how changing the input in the demo below will result in a slight delay of input checks noted by the appearance of a new "I" character.
49 |
50 | ```xmlui-pg copy display name="Example: customValidationsDebounce"
51 | <App>
52 | <Form
53 | var.validations="Validations: "
54 | data="{{ name: 'Joe' }}"
55 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
56 | <FormItem
57 | customValidationsDebounce="3000"
58 | onValidate="(value) => {
59 | validations += '| ';
60 | return value === value.toUpperCase();
61 | }"
62 | bindTo="name" />
63 | <Text value="{validations}" />
64 | </Form>
65 | </App>
66 | ```
67 |
68 | ### `enabled` (default: true) [#enabled-default-true]
69 |
70 | This boolean property value indicates whether the component responds to user events (`true`) or not (`false`).
71 |
72 | ```xmlui-pg copy display name="Example: enabled"
73 | <App>
74 | <Form>
75 | <FormItem label="Firstname" enabled="true" />
76 | <FormItem label="Lastname" enabled="false" />
77 | </Form>
78 | </App>
79 | ```
80 |
81 | ### `gap` (default: "0") [#gap-default-0]
82 |
83 | This property defines the gap between the adornments and the input area.
84 |
85 | ### `initialValue` [#initialvalue]
86 |
87 | This property sets the component's initial value.
88 |
89 | ```xmlui-pg copy display name="Example: initialValue"
90 | <App>
91 | <Form data="{{ firstname: 'Michael', lastname: undefined }}">
92 | <FormItem label="Firstname" bindTo="firstname" initialValue="James" />
93 | <FormItem label="Lastname" bindTo="lastname" initialValue="Jordan" />
94 | </Form>
95 | </App>
96 | ```
97 |
98 | ### `inputTemplate` [#inputtemplate]
99 |
100 | This property is used to define a custom input template.
101 |
102 | ### `label` [#label]
103 |
104 | This property sets the label of the component. If not set, the component will not display a label.
105 |
106 | ```xmlui-pg copy display name="Example: label"
107 | <App>
108 | <Form>
109 | <FormItem label="Firstname" />
110 | </Form>
111 | </App>
112 | ```
113 |
114 | ### `labelBreak` (default: true) [#labelbreak-default-true]
115 |
116 | This boolean value indicates if the label can be split into multiple lines if it would overflow the available label width.
117 |
118 | ### `labelPosition` (default: "top") [#labelposition-default-top]
119 |
120 | Places the label at the given position of the component.
121 |
122 | Available values:
123 |
124 | | Value | Description |
125 | | --- | --- |
126 | | `start` | The left side of the input (left-to-right) or the right side of the input (right-to-left) |
127 | | `end` | The right side of the input (left-to-right) or the left side of the input (right-to-left) |
128 | | `top` | The top of the input **(default)** |
129 | | `bottom` | The bottom of the input |
130 |
131 | Different input components have different layout methods
132 | (i.e. `TextBox` labels are positioned at the top, `Checkbox` labels are on the right side).
133 |
134 | ```xmlui-pg copy display name="Example: labelPosition"
135 | <App>
136 | <Form>
137 | <FormItem label="Start Label" labelPosition="start" />
138 | <FormItem label="Top Label" labelPosition="top" />
139 | <FormItem label="End Label" labelPosition="end" />
140 | <FormItem label="Bottom Label" labelPosition="bottom" />
141 | </Form>
142 | </App>
143 | ```
144 |
145 | ### `labelWidth` [#labelwidth]
146 |
147 | This property sets the width of the `FormItem` component's label. If not defined, the label's width will be determined by its content and the available space.
148 |
149 | ### `lengthInvalidMessage` [#lengthinvalidmessage]
150 |
151 | This optional string property is used to customize the message that is displayed on a failed length check: [minLength](#minlength) or [maxLength](#maxlength).
152 |
153 | In the app, type a name longer than four characters in both fields, then leave the edited field. The two fields will display different error messages; the second uses the customized one.
154 |
155 | ```xmlui-pg copy display name="Example: lengthInvalidMessage"
156 | <App>
157 | <Form
158 | data="{{ firstname: 'James', lastname: 'Clewell' }}"
159 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
160 | <FormItem maxLength="4" bindTo="firstname" />
161 | <FormItem lengthInvalidMessage="Name is too long!" maxLength="4" bindTo="lastname" />
162 | </Form>
163 | </App>
164 | ```
165 |
166 | ### `lengthInvalidSeverity` (default: "error") [#lengthinvalidseverity-default-error]
167 |
168 | This property sets the severity level of the length validation.
169 |
170 | Available values: `error` **(default)**, `warning`, `valid`
171 |
172 | In the app, type a name longer than four characters in both fields, then leave the edited field. The two fields will display different error messages; the second uses a warning instead of an error.
173 |
174 | ```xmlui-pg copy display name="Example: lengthInvalidSeverity"
175 | <App>
176 | <Form
177 | data="{{ firstname: 'James', lastname: 'Clewell' }}"
178 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
179 | <FormItem maxLength="4" bindTo="firstname" />
180 | <FormItem lengthInvalidSeverity="warning" maxLength="4" bindTo="lastname" />
181 | </Form>
182 | </App>
183 | ```
184 |
185 | ### `maxLength` [#maxlength]
186 |
187 | This property sets the maximum length of the input value. If the value is not set, no maximum length check is done.
188 |
189 | Note that it is not possible for the user to enter a string larger than the value of the `maxLength`,
190 | but setting such a value programmatically still results in a validation check.
191 |
192 | In the demo below, try to enter an input longer than 4 characters or submit the form as is.
193 |
194 | ```xmlui-pg copy display name="Example: maxLength"
195 | <App>
196 | <Form
197 | data="{{ firstname: 'James' }}"
198 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
199 | <FormItem maxLength="4" bindTo="firstname" />
200 | </Form>
201 | </App>
202 | ```
203 |
204 | ### `maxTextLength` [#maxtextlength]
205 |
206 | The maximum length of the text in the input field. If this value is not set, no maximum length constraint is set for the input field.
207 |
208 | ### `maxValue` [#maxvalue]
209 |
210 | The maximum value of the input. If this value is not specified, no maximum value check is done.
211 |
212 | Note that it is not possible for the user to enter a number larger than the value of the `maxValue`,
213 | but setting such a value programmatically still results in a validation check.
214 |
215 | In the demo below, enter an input greater than 99 or just submit the form as is.
216 |
217 | ```xmlui-pg copy display name="Example: maxValue"
218 | <App>
219 | <Form
220 | data="{{ age: 100 }}"
221 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
222 | <FormItem maxValue="99" bindTo="age" type="integer" />
223 | </Form>
224 | </App>
225 | ```
226 |
227 | ### `minLength` [#minlength]
228 |
229 | This property sets the minimum length of the input value. If the value is not set, no minimum length check is done.
230 |
231 | In the demo below, enter an input shorter than 4 characters or just submit the form as is.
232 |
233 | ```xmlui-pg copy display name="Example: minLength"
234 | <App>
235 | <Form
236 | data="{{ firstname: '' }}"
237 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
238 | <FormItem minLength="4" bindTo="firstname" />
239 | </Form>
240 | </App>
241 | ```
242 |
243 | ### `minValue` [#minvalue]
244 |
245 | The minimum value of the input. If this value is not specified, no minimum value check is done.
246 |
247 | Note that it is not possible for the user to enter a number smaller than the value of the `minValue`,
248 | but setting such a value programmatically still results in a validation check.
249 |
250 | In the demo below, enter an input smaller than 18 or just submit the form as is.
251 |
252 | ```xmlui-pg copy display name="Example: minValue"
253 | <App>
254 | <Form
255 | data="{{ age: 0 }}"
256 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
257 | <FormItem minValue="18" bindTo="age" type="integer" />
258 | </Form>
259 | </App>
260 | ```
261 |
262 | ### `noSubmit` (default: false) [#nosubmit-default-false]
263 |
264 | When set to `true`, the field will not be included in the form's submitted data. This is useful for fields that should be present in the form but not submitted, similar to hidden fields. If multiple FormItems reference the same `bindTo` value and any of them has `noSubmit` set to `true`, the field will NOT be submitted.
265 |
266 | ### `pattern` [#pattern]
267 |
268 | This value specifies a predefined regular expression to test the input value. If this value is not set, no pattern check is done.
269 |
270 | | Value | Description |
271 | | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
272 | | `email` | Accepts the `[username]@[second level domain].[top level domain]` format |
273 | | `phone` | Accepts a wide range of characters: numbers, upper- and lowercase letters and the following symbols: `#`, `*`, `)`, `(`, `+`, `.`, `\`, `-`, `_`, `&`, `'` |
274 | | `url` | Accepts URLs and URIs starting with either `http` or `https` |
275 |
276 | > **Note:** To define custom patterns and regular expressions, see the [regex section](#regex).
277 |
278 | In the demo below, enter an input that is not solely one lowercase string or just submit the form as is.
279 |
280 | ```xmlui-pg copy display name="Example: pattern"
281 | <App>
282 | <Form
283 | data="{{ userEmail: 'mailto' }}"
284 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
285 | <FormItem pattern="email" bindTo="userEmail" />
286 | </Form>
287 | </App>
288 | ```
289 |
290 | ### `patternInvalidMessage` [#patterninvalidmessage]
291 |
292 | This optional string property is used to customize the message that is displayed on a failed pattern test.
293 |
294 | In the demo below, enter anything that does not look like an email and click outside to see the regular and custom message.
295 |
296 | ```xmlui-pg copy display name="Example: patternInvalidMessage"
297 | <App>
298 | <Form
299 | data="{{ oldEmail: 'mailto', newEmail: 'mailto' }}"
300 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
301 | <FormItem pattern="email" bindTo="oldEmail" />
302 | <FormItem
303 | patternInvalidMessage="This does not look like an email"
304 | pattern="email" bindTo="newEmail" />
305 | </Form>
306 | </App>
307 | ```
308 |
309 | ### `patternInvalidSeverity` (default: "error") [#patterninvalidseverity-default-error]
310 |
311 | This property sets the severity level of the pattern validation.
312 |
313 | Available values: `error` **(default)**, `warning`, `valid`
314 |
315 | In the demo below, enter a string of characters that does not look like an email to see the difference in feedback.
316 |
317 | ```xmlui-pg copy display name="Example: patternInvalidSeverity"
318 | <App>
319 | <Form
320 | data="{{ oldEmail: 'mailto', newEmail: 'mailto' }}"
321 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
322 | <FormItem pattern="email" bindTo="oldEmail" />
323 | <FormItem patternInvalidSeverity="warning" pattern="email" bindTo="newEmail" />
324 | </Form>
325 | </App>
326 | ```
327 |
328 | ### `rangeInvalidMessage` [#rangeinvalidmessage]
329 |
330 | This optional string property is used to customize the message that is displayed when a value is out of range.
331 |
332 | In the demo below, enter any value that is out of range in the input fields and click outside to see the regular and custom message.
333 | Just submitting the form as is also produces the same error.
334 |
335 | ```xmlui-pg copy display name="Example: rangeInvalidMessage"
336 | <App>
337 | <Form
338 | data="{{ age: 100, customAge: 100 }}"
339 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
340 | <FormItem minValue="0" maxValue="99" bindTo="age" type="integer" />
341 | <FormItem
342 | minValue="0"
343 | maxValue="99"
344 | rangeInvalidMessage="Out of range!"
345 | bindTo="customAge"
346 | type="integer" />
347 | </Form>
348 | </App>
349 | ```
350 |
351 | ### `rangeInvalidSeverity` (default: "error") [#rangeinvalidseverity-default-error]
352 |
353 | This property sets the severity level of the value range validation.
354 |
355 | Available values: `error` **(default)**, `warning`, `valid`
356 |
357 | In the demo below, enter any value that is out of range in the input fields and click outside to see the regular and custom message.
358 | Just submitting the form as is also produces the same error.
359 |
360 | ```xmlui-pg copy display name="Example: rangeInvalidSeverity"
361 | <App>
362 | <Form
363 | data="{{ age: 100, customAge: 100 }}"
364 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
365 | <FormItem minValue="0" maxValue="99" bindTo="age" type="integer" />
366 | <FormItem
367 | minValue="0" maxValue="99"
368 | rangeInvalidSeverity="warning"
369 | bindTo="customAge"
370 | type="integer" />
371 | </Form>
372 | </App>
373 | ```
374 |
375 | ### `regex` [#regex]
376 |
377 | This value specifies a custom regular expression to test the input value. If this value is not set, no regular expression pattern check is done.
378 |
379 | In the demo below, enter an input that is not solely one lowercase string or just submit the form as is.
380 |
381 | ```xmlui-pg copy display name="Example: regex"
382 | <App>
383 | <Form
384 | data="{{ password: 'PASSWORD123' }}"
385 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
386 | <FormItem regex="^[a-z]+$" bindTo="password" />
387 | </Form>
388 | </App>
389 | ```
390 |
391 | ### `regexInvalidMessage` [#regexinvalidmessage]
392 |
393 | This optional string property is used to customize the message that is displayed on a failed regular expression test.
394 |
395 | In the demo below, enter a password that is not a lowercase string and click outside to see the regular and custom message.
396 |
397 | ```xmlui-pg copy display name="Example: regexInvalidMessage"
398 | <App>
399 | <Form
400 | data="{{ oldPassword: 'PASSWORD123', newPassword: 'PASSWORD123' }}"
401 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
402 | <FormItem regex="^[a-z]+$" bindTo="oldPassword" />
403 | <FormItem
404 | regexInvalidMessage="Password must be all lowercase"
405 | regex="^[a-z]+$" bindTo="newPassword" />
406 | </Form>
407 | </App>
408 | ```
409 |
410 | ### `regexInvalidSeverity` (default: "error") [#regexinvalidseverity-default-error]
411 |
412 | This property sets the severity level of regular expression validation.
413 |
414 | Available values: `error` **(default)**, `warning`, `valid`
415 |
416 | In the demo below, enter a password that is not a lowercase string and click outside to see the regular and custom message.
417 | Just submitting the form as is also produces the same error.
418 |
419 | ```xmlui-pg copy display name="Example: regexInvalidSeverity"
420 | <App>
421 | <Form
422 | data="{{ oldPassword: 'PASSWORD123', newPassword: 'PASSWORD123' }}"
423 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
424 | <FormItem regex="^[a-z]+$" bindTo="oldPassword" />
425 | <FormItem regexInvalidSeverity="warning" regex="^[a-z]+$" bindTo="newPassword" />
426 | </Form>
427 | </App>
428 | ```
429 |
430 | ### `required` (default: false) [#required-default-false]
431 |
432 | Set this property to `true` to indicate it must have a value before submitting the containing form.
433 |
434 | ```xmlui-pg copy display name="Example: required"
435 | <App>
436 | <Form
437 | data="{{ name: undefined }}"
438 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
439 | <FormItem required="true" label="Name" bindTo="name" />
440 | </Form>
441 | </App>
442 | ```
443 |
444 | ### `requiredInvalidMessage` [#requiredinvalidmessage]
445 |
446 | This optional string property is used to customize the message that is displayed if the field is not filled in. If not defined, the default message is used.
447 |
448 | In the demo below, leave the field empty and click outside to see the regular and custom message.
449 |
450 | ```xmlui-pg copy display name="Example: requiredInvalidMessage"
451 | <App>
452 | <Form
453 | data="{{ firstname: undefined, lastname: undefined }}"
454 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
455 | <FormItem required="true" label="First Name" bindTo="firstname" />
456 | <FormItem
457 | requiredInvalidMessage="Last Name is required!"
458 | required="true"
459 | label="Last Name"
460 | bindTo="lastname" />
461 | </Form>
462 | </App>
463 | ```
464 |
465 | ### `type` (default: "text") [#type-default-text]
466 |
467 | This property is used to determine the specific input control the FormItem will wrap around. Note that the control names start with a lowercase letter and map to input components found in XMLUI.
468 |
469 | Available values:
470 |
471 | | Value | Description |
472 | | --- | --- |
473 | | `text` | Renders TextBox **(default)** |
474 | | `password` | Renders TextBox with `password` type |
475 | | `textarea` | Renders Textarea |
476 | | `checkbox` | Renders Checkbox |
477 | | `number` | Renders NumberBox |
478 | | `integer` | Renders NumberBox with `integersOnly` set to true |
479 | | `file` | Renders FileInput |
480 | | `datePicker` | Renders DatePicker |
481 | | `radioGroup` | Renders RadioGroup |
482 | | `switch` | Renders Switch |
483 | | `select` | Renders Select |
484 | | `autocomplete` | Renders AutoComplete |
485 | | `slider` | Renders Slider |
486 | | `colorpicker` | Renders ColorPicker |
487 | | `items` | Renders Items |
488 | | `custom` | A custom control specified in children. If `type` is not specified but the `FormItem` has children, it considers the control a custom one. |
489 |
490 | >[!INFO]
491 | > For custom controls, there is no need to explicitly set the `type` to `custom`.
492 | > Omitting the type and providing child components implicitly sets it to custom.
493 |
494 | ### `validationMode` (default: "errorLate") [#validationmode-default-errorlate]
495 |
496 | This property sets what kind of validation mode or strategy to employ for a particular input field.
497 |
498 | Available values:
499 |
500 | | Value | Description |
501 | | --- | --- |
502 | | `errorLate` | Display the error when the field loses focus.If an error is already displayed, continue for every keystroke until input is accepted. **(default)** |
503 | | `onChanged` | Display error (if present) for every keystroke. |
504 | | `onLostFocus` | Show/hide error (if present) only if the field loses focus. |
505 |
506 | ## Events [#events]
507 |
508 | ### `validate` [#validate]
509 |
510 | This event is used to define a custom validation function.
511 |
512 | In the demo below, leave the field as is and submit the form or enter an input that is not all capital letters.
513 |
514 | ```xmlui-pg copy {7} display name="Example: validate"
515 | <App>
516 | <Form
517 | data="{{ name: 'James' }}"
518 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
519 | <FormItem
520 | bindTo="name"
521 | onValidate="(value) => value === value.toUpperCase()" />
522 | </Form>
523 | </App>
524 | ```
525 |
526 | ## Exposed Methods [#exposed-methods]
527 |
528 | ### `addItem` [#additem]
529 |
530 | This method adds a new item to the list held by the FormItem. The function has a single parameter, the data to add to the FormItem. The new item is appended to the end of the list.
531 |
532 | **Signature**: `addItem(data: any): void`
533 |
534 | - `data`: The data to add to the FormItem's list.
535 |
536 | ### `removeItem` [#removeitem]
537 |
538 | Removes the item specified by its index from the list held by the FormItem. The function has a single argument, the index to remove.
539 |
540 | **Signature**: `removeItem(index: number): void`
541 |
542 | - `index`: The index of the item to remove from the FormItem's list.
543 |
544 | ## Styling [#styling]
545 |
546 | ### Theme Variables [#theme-variables]
547 |
548 | | Variable | Default Value (Light) | Default Value (Dark) |
549 | | --- | --- | --- |
550 | | [fontFamily](../styles-and-themes/common-units/#fontFamily)-FormItemLabel | *none* | *none* |
551 | | [fontSize](../styles-and-themes/common-units/#size)-FormItemLabel | $fontSize-sm | $fontSize-sm |
552 | | [fontSize](../styles-and-themes/common-units/#size)-FormItemLabel-required | *none* | *none* |
553 | | [fontStyle](../styles-and-themes/common-units/#fontStyle)-FormItemLabel | normal | normal |
554 | | [fontStyle](../styles-and-themes/common-units/#fontStyle)-FormItemLabel-required | *none* | *none* |
555 | | [fontWeight](../styles-and-themes/common-units/#fontWeight)-FormItemLabel | $fontWeight-medium | $fontWeight-medium |
556 | | [fontWeight](../styles-and-themes/common-units/#fontWeight)-FormItemLabel-required | *none* | *none* |
557 | | [textColor](../styles-and-themes/common-units/#color)-FormItemLabel | $textColor-primary | $textColor-primary |
558 | | [textColor](../styles-and-themes/common-units/#color)-FormItemLabel-required | *none* | *none* |
559 | | [textColor](../styles-and-themes/common-units/#color)-FormItemLabel-requiredMark | $color-danger-400 | $color-danger-400 |
560 | | [textTransform](../styles-and-themes/common-units/#textTransform)-FormItemLabel | none | none |
561 | | [textTransform](../styles-and-themes/common-units/#textTransform)-FormItemLabel-required | *none* | *none* |
562 |
```
--------------------------------------------------------------------------------
/xmlui/tests/components-core/scripts-runner/process-statement-destruct.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it } from "vitest";
2 |
3 | import { processStatementQueueAsync } from "../../../src/components-core/script-runner/process-statement-async";
4 | import { createEvalContext, parseStatements } from "./test-helpers";
5 |
6 | describe("Process statements - destructure (exp)", () => {
7 | it("let array destructure #1", async () => {
8 | // --- Arrange
9 | const source = "let [a, b] = [3, 6]; x = a; y = b;";
10 | const evalContext = createEvalContext({
11 | localContext: {
12 | x: 0,
13 | y: 0
14 | }
15 | });
16 | const statements = parseStatements(source);
17 |
18 | // --- Act
19 | await processStatementQueueAsync(statements, evalContext);
20 |
21 | // --- Assert
22 | expect(evalContext.localContext.x).equal(3);
23 | expect(evalContext.localContext.y).equal(6);
24 |
25 | });
26 |
27 | it("let array destructure #2", async () => {
28 | // --- Arrange
29 | const source = "let [,a, b] = [3, 6, 8]; x = a; y = b;";
30 | const evalContext = createEvalContext({
31 | localContext: {
32 | x: 0,
33 | y: 0
34 | }
35 | });
36 | const statements = parseStatements(source);
37 |
38 | // --- Act
39 | await processStatementQueueAsync(statements, evalContext);
40 |
41 | // --- Assert
42 | expect(evalContext.localContext.x).equal(6);
43 | expect(evalContext.localContext.y).equal(8);
44 |
45 | });
46 |
47 | it("let array destructure #3", async () => {
48 | // --- Arrange
49 | const source = "let [a, [b, c]] = [3, [6, 8]]; x = a; y = b; z = c;";
50 | const evalContext = createEvalContext({
51 | localContext: {
52 | x: 0,
53 | y: 0,
54 | z: 0,
55 | }
56 | });
57 | const statements = parseStatements(source);
58 |
59 | // --- Act
60 | await processStatementQueueAsync(statements, evalContext);
61 |
62 | // --- Assert
63 | expect(evalContext.localContext.x).equal(3);
64 | expect(evalContext.localContext.y).equal(6);
65 | expect(evalContext.localContext.z).equal(8);
66 |
67 | });
68 |
69 | it("let array destructure #4", async () => {
70 | // --- Arrange
71 | const source = "let [a, , [, b, c]] = [3, -11, [-1, 6, 8]]; x = a; y = b; z = c;";
72 | const evalContext = createEvalContext({
73 | localContext: {
74 | x: 0,
75 | y: 0,
76 | z: 0,
77 | }
78 | });
79 | const statements = parseStatements(source);
80 |
81 | // --- Act
82 | await processStatementQueueAsync(statements, evalContext);
83 |
84 | // --- Assert
85 | expect(evalContext.localContext.x).equal(3);
86 | expect(evalContext.localContext.y).equal(6);
87 | expect(evalContext.localContext.z).equal(8);
88 |
89 | });
90 |
91 | it("let object destructure #1", async () => {
92 | // --- Arrange
93 | const source = "let {a, b} = {a: 3, b: 6}; x = a; y = b;";
94 | const evalContext = createEvalContext({
95 | localContext: {
96 | x: 0,
97 | y: 0
98 | }
99 | });
100 | const statements = parseStatements(source);
101 |
102 | // --- Act
103 | await processStatementQueueAsync(statements, evalContext);
104 |
105 | // --- Assert
106 | expect(evalContext.localContext.x).equal(3);
107 | expect(evalContext.localContext.y).equal(6);
108 |
109 | });
110 |
111 | it("let object destructure #2", async () => {
112 | // --- Arrange
113 | const source = "let {a, qqq:b } = {a: 3, qqq: 6}; x = a; y = b;";
114 | const evalContext = createEvalContext({
115 | localContext: {
116 | x: 0,
117 | y: 0
118 | }
119 | });
120 | const statements = parseStatements(source);
121 |
122 | // --- Act
123 | await processStatementQueueAsync(statements, evalContext);
124 |
125 | // --- Assert
126 | expect(evalContext.localContext.x).equal(3);
127 | expect(evalContext.localContext.y).equal(6);
128 |
129 | });
130 |
131 | it("let object destructure #3", async () => {
132 | // --- Arrange
133 | const source = "let {a, qqq: {b, c}} = {a: 3, qqq: {b: 6, c: 8}}; x = a; y = b; z = c";
134 | const evalContext = createEvalContext({
135 | localContext: {
136 | x: 0,
137 | y: 0,
138 | z: 0,
139 | }
140 | });
141 | const statements = parseStatements(source);
142 |
143 | // --- Act
144 | await processStatementQueueAsync(statements, evalContext);
145 |
146 | // --- Assert
147 | expect(evalContext.localContext.x).equal(3);
148 | expect(evalContext.localContext.y).equal(6);
149 | expect(evalContext.localContext.z).equal(8);
150 | });
151 |
152 | it("let object and array destructure #1", async () => {
153 | // --- Arrange
154 | const source = "let {a, qqq: [b, c]} = {a: 3, qqq: [6, 8] }; x = a; y = b; z = c";
155 | const evalContext = createEvalContext({
156 | localContext: {
157 | x: 0,
158 | y: 0,
159 | z: 0,
160 | }
161 | });
162 | const statements = parseStatements(source);
163 |
164 | // --- Act
165 | await processStatementQueueAsync(statements, evalContext);
166 |
167 | // --- Assert
168 | expect(evalContext.localContext.x).equal(3);
169 | expect(evalContext.localContext.y).equal(6);
170 | expect(evalContext.localContext.z).equal(8);
171 | });
172 |
173 | it("let object and array destructure #2", async () => {
174 | // --- Arrange
175 | const source = "let {a, qqq: [b,,c]} = {a: 3, qqq: [6, -1, 8] }; x = a; y = b; z = c";
176 | const evalContext = createEvalContext({
177 | localContext: {
178 | x: 0,
179 | y: 0,
180 | z: 0,
181 | }
182 | });
183 | const statements = parseStatements(source);
184 |
185 | // --- Act
186 | await processStatementQueueAsync(statements, evalContext);
187 |
188 | // --- Assert
189 | expect(evalContext.localContext.x).equal(3);
190 | expect(evalContext.localContext.y).equal(6);
191 | expect(evalContext.localContext.z).equal(8);
192 | });
193 |
194 | it("let object and array destructure #3", async () => {
195 | // --- Arrange
196 | const source = "let [a, {b, c}] = [3, {b: 6, c: 8}]; x = a; y = b; z = c";
197 | const evalContext = createEvalContext({
198 | localContext: {
199 | x: 0,
200 | y: 0,
201 | z: 0,
202 | }
203 | });
204 | const statements = parseStatements(source);
205 |
206 | // --- Act
207 | await processStatementQueueAsync(statements, evalContext);
208 |
209 | // --- Assert
210 | expect(evalContext.localContext.x).equal(3);
211 | expect(evalContext.localContext.y).equal(6);
212 | expect(evalContext.localContext.z).equal(8);
213 | });
214 |
215 | it("let object and array destructure #3", async () => {
216 | // --- Arrange
217 | const source = "let [a, , {b, c}] = [3, -1, {b: 6, c: 8}]; x = a; y = b; z = c";
218 | const evalContext = createEvalContext({
219 | localContext: {
220 | x: 0,
221 | y: 0,
222 | z: 0,
223 | }
224 | });
225 | const statements = parseStatements(source);
226 |
227 | // --- Act
228 | await processStatementQueueAsync(statements, evalContext);
229 |
230 | // --- Assert
231 | expect(evalContext.localContext.x).equal(3);
232 | expect(evalContext.localContext.y).equal(6);
233 | expect(evalContext.localContext.z).equal(8);
234 | });
235 |
236 | it("const array destructure #1", async () => {
237 | // --- Arrange
238 | const source = "const [a, b] = [3, 6]; x = a; y = b;";
239 | const evalContext = createEvalContext({
240 | localContext: {
241 | x: 0,
242 | y: 0
243 | }
244 | });
245 | const statements = parseStatements(source);
246 |
247 | // --- Act
248 | await processStatementQueueAsync(statements, evalContext);
249 |
250 | // --- Assert
251 | expect(evalContext.localContext.x).equal(3);
252 | expect(evalContext.localContext.y).equal(6);
253 | });
254 |
255 | it("const array destructure #2", async () => {
256 | // --- Arrange
257 | const source = "const [,a, b] = [3, 6, 8]; x = a; y = b;";
258 | const evalContext = createEvalContext({
259 | localContext: {
260 | x: 0,
261 | y: 0
262 | }
263 | });
264 | const statements = parseStatements(source);
265 |
266 | // --- Act
267 | await processStatementQueueAsync(statements, evalContext);
268 |
269 | // --- Assert
270 | expect(evalContext.localContext.x).equal(6);
271 | expect(evalContext.localContext.y).equal(8);
272 |
273 | });
274 |
275 | it("const array destructure #3", async () => {
276 | // --- Arrange
277 | const source = "const [a, [b, c]] = [3, [6, 8]]; x = a; y = b; z = c;";
278 | const evalContext = createEvalContext({
279 | localContext: {
280 | x: 0,
281 | y: 0,
282 | z: 0,
283 | }
284 | });
285 | const statements = parseStatements(source);
286 |
287 | // --- Act
288 | await processStatementQueueAsync(statements, evalContext);
289 |
290 | // --- Assert
291 | expect(evalContext.localContext.x).equal(3);
292 | expect(evalContext.localContext.y).equal(6);
293 | expect(evalContext.localContext.z).equal(8);
294 |
295 | });
296 |
297 | it("const array destructure #4", async () => {
298 | // --- Arrange
299 | const source = "const [a, , [, b, c]] = [3, -11, [-1, 6, 8]]; x = a; y = b; z = c;";
300 | const evalContext = createEvalContext({
301 | localContext: {
302 | x: 0,
303 | y: 0,
304 | z: 0,
305 | }
306 | });
307 | const statements = parseStatements(source);
308 |
309 | // --- Act
310 | await processStatementQueueAsync(statements, evalContext);
311 |
312 | // --- Assert
313 | expect(evalContext.localContext.x).equal(3);
314 | expect(evalContext.localContext.y).equal(6);
315 | expect(evalContext.localContext.z).equal(8);
316 |
317 | });
318 |
319 | it("const object destructure #1", async () => {
320 | // --- Arrange
321 | const source = "const {a, b} = {a: 3, b: 6}; x = a; y = b;";
322 | const evalContext = createEvalContext({
323 | localContext: {
324 | x: 0,
325 | y: 0
326 | }
327 | });
328 | const statements = parseStatements(source);
329 |
330 | // --- Act
331 | await processStatementQueueAsync(statements, evalContext);
332 |
333 | // --- Assert
334 | expect(evalContext.localContext.x).equal(3);
335 | expect(evalContext.localContext.y).equal(6);
336 |
337 | });
338 |
339 | it("const object destructure #2", async () => {
340 | // --- Arrange
341 | const source = "const {a, qqq:b } = {a: 3, qqq: 6}; x = a; y = b;";
342 | const evalContext = createEvalContext({
343 | localContext: {
344 | x: 0,
345 | y: 0
346 | }
347 | });
348 | const statements = parseStatements(source);
349 |
350 | // --- Act
351 | await processStatementQueueAsync(statements, evalContext);
352 |
353 | // --- Assert
354 | expect(evalContext.localContext.x).equal(3);
355 | expect(evalContext.localContext.y).equal(6);
356 |
357 | });
358 |
359 | it("const object destructure #3", async () => {
360 | // --- Arrange
361 | const source = "const {a, qqq: {b, c}} = {a: 3, qqq: {b: 6, c: 8}}; x = a; y = b; z = c";
362 | const evalContext = createEvalContext({
363 | localContext: {
364 | x: 0,
365 | y: 0,
366 | z: 0,
367 | }
368 | });
369 | const statements = parseStatements(source);
370 |
371 | // --- Act
372 | await processStatementQueueAsync(statements, evalContext);
373 |
374 | // --- Assert
375 | expect(evalContext.localContext.x).equal(3);
376 | expect(evalContext.localContext.y).equal(6);
377 | expect(evalContext.localContext.z).equal(8);
378 | });
379 |
380 | it("const object and array destructure #1", async () => {
381 | // --- Arrange
382 | const source = "const {a, qqq: [b, c]} = {a: 3, qqq: [6, 8] }; x = a; y = b; z = c";
383 | const evalContext = createEvalContext({
384 | localContext: {
385 | x: 0,
386 | y: 0,
387 | z: 0,
388 | }
389 | });
390 | const statements = parseStatements(source);
391 |
392 | // --- Act
393 | await processStatementQueueAsync(statements, evalContext);
394 |
395 | // --- Assert
396 | expect(evalContext.localContext.x).equal(3);
397 | expect(evalContext.localContext.y).equal(6);
398 | expect(evalContext.localContext.z).equal(8);
399 | });
400 |
401 | it("const object and array destructure #2", async () => {
402 | // --- Arrange
403 | const source = "const {a, qqq: [b,,c]} = {a: 3, qqq: [6, -1, 8] }; x = a; y = b; z = c";
404 | const evalContext = createEvalContext({
405 | localContext: {
406 | x: 0,
407 | y: 0,
408 | z: 0,
409 | }
410 | });
411 | const statements = parseStatements(source);
412 |
413 | // --- Act
414 | await processStatementQueueAsync(statements, evalContext);
415 |
416 | // --- Assert
417 | expect(evalContext.localContext.x).equal(3);
418 | expect(evalContext.localContext.y).equal(6);
419 | expect(evalContext.localContext.z).equal(8);
420 | });
421 |
422 | it("const object and array destructure #3", async () => {
423 | // --- Arrange
424 | const source = "const [a, {b, c}] = [3, {b: 6, c: 8}]; x = a; y = b; z = c";
425 | const evalContext = createEvalContext({
426 | localContext: {
427 | x: 0,
428 | y: 0,
429 | z: 0,
430 | }
431 | });
432 | const statements = parseStatements(source);
433 |
434 | // --- Act
435 | await processStatementQueueAsync(statements, evalContext);
436 |
437 | // --- Assert
438 | expect(evalContext.localContext.x).equal(3);
439 | expect(evalContext.localContext.y).equal(6);
440 | expect(evalContext.localContext.z).equal(8);
441 | });
442 |
443 | it("const object and array destructure #3", async () => {
444 | // --- Arrange
445 | const source = "const [a, , {b, c}] = [3, -1, {b: 6, c: 8}]; x = a; y = b; z = c";
446 | const evalContext = createEvalContext({
447 | localContext: {
448 | x: 0,
449 | y: 0,
450 | z: 0,
451 | }
452 | });
453 | const statements = parseStatements(source);
454 |
455 | // --- Act
456 | await processStatementQueueAsync(statements, evalContext);
457 |
458 | // --- Assert
459 | expect(evalContext.localContext.x).equal(3);
460 | expect(evalContext.localContext.y).equal(6);
461 | expect(evalContext.localContext.z).equal(8);
462 | });
463 |
464 | it("arrow destructure #1", async () => {
465 | // --- Arrange
466 | const source = "const fn = ([a, b]) => { x = a; y = b}; fn([3, 6, 8])";
467 | const evalContext = createEvalContext({
468 | localContext: {
469 | x: 0,
470 | y: 0,
471 | }
472 | });
473 | const statements = parseStatements(source);
474 |
475 | // --- Act
476 | await processStatementQueueAsync(statements, evalContext);
477 |
478 | // --- Assert
479 | expect(evalContext.localContext.x).equal(3);
480 | expect(evalContext.localContext.y).equal(6);
481 | });
482 |
483 | it("arrow destructure #2", async () => {
484 | // --- Arrange
485 | const source = "const fn = ([a, , b]) => { x = a; y = b}; fn([3, 6, 8])";
486 | const evalContext = createEvalContext({
487 | localContext: {
488 | x: 0,
489 | y: 0,
490 | }
491 | });
492 | const statements = parseStatements(source);
493 |
494 | // --- Act
495 | await processStatementQueueAsync(statements, evalContext);
496 |
497 | // --- Assert
498 | expect(evalContext.localContext.x).equal(3);
499 | expect(evalContext.localContext.y).equal(8);
500 | });
501 |
502 | it("arrow destructure #3", async () => {
503 | // --- Arrange
504 | const source = "const fn = ([a, b]) => { x = a; y = b}; fn([3])";
505 | const evalContext = createEvalContext({
506 | localContext: {
507 | x: 0,
508 | y: 0,
509 | }
510 | });
511 | const statements = parseStatements(source);
512 |
513 | // --- Act
514 | await processStatementQueueAsync(statements, evalContext);
515 |
516 | // --- Assert
517 | expect(evalContext.localContext.x).equal(3);
518 | expect(evalContext.localContext.y).equal(undefined);
519 | });
520 |
521 | it("arrow destructure #4", async () => {
522 | // --- Arrange
523 | const source = "const fn = ([a, , b]) => { x = a; y = b}; fn([3, 6])";
524 | const evalContext = createEvalContext({
525 | localContext: {
526 | x: 0,
527 | y: 0,
528 | }
529 | });
530 | const statements = parseStatements(source);
531 |
532 | // --- Act
533 | await processStatementQueueAsync(statements, evalContext);
534 |
535 | // --- Assert
536 | expect(evalContext.localContext.x).equal(3);
537 | expect(evalContext.localContext.y).equal(undefined);
538 | });
539 |
540 | it("arrow destructure #5", async () => {
541 | // --- Arrange
542 | const source = "const fn = ([a, [b, c]]) => { x = a; y = b; z = c }; fn([3, [6, 8]])";
543 | const evalContext = createEvalContext({
544 | localContext: {
545 | x: 0,
546 | y: 0,
547 | z: 0,
548 | }
549 | });
550 | const statements = parseStatements(source);
551 |
552 | // --- Act
553 | await processStatementQueueAsync(statements, evalContext);
554 |
555 | // --- Assert
556 | expect(evalContext.localContext.x).equal(3);
557 | expect(evalContext.localContext.y).equal(6);
558 | expect(evalContext.localContext.z).equal(8);
559 | });
560 |
561 | it("arrow destructure #6", async () => {
562 | // --- Arrange
563 | const source = "const fn = ({a, b}) => { x = a; y = b}; fn({a: 3, b: 6, v: 8})";
564 | const evalContext = createEvalContext({
565 | localContext: {
566 | x: 0,
567 | y: 0,
568 | }
569 | });
570 | const statements = parseStatements(source);
571 |
572 | // --- Act
573 | await processStatementQueueAsync(statements, evalContext);
574 |
575 | // --- Assert
576 | expect(evalContext.localContext.x).equal(3);
577 | expect(evalContext.localContext.y).equal(6);
578 | });
579 |
580 | it("arrow destructure #7", async () => {
581 | // --- Arrange
582 | const source = "const fn = ({a, b}) => { x = a; y = b}; fn({a: 3, v: 8})";
583 | const evalContext = createEvalContext({
584 | localContext: {
585 | x: 0,
586 | y: 0,
587 | }
588 | });
589 | const statements = parseStatements(source);
590 |
591 | // --- Act
592 | await processStatementQueueAsync(statements, evalContext);
593 |
594 | // --- Assert
595 | expect(evalContext.localContext.x).equal(3);
596 | expect(evalContext.localContext.y).equal(undefined);
597 | });
598 |
599 | it("arrow destructure #8", async () => {
600 | // --- Arrange
601 | const source = "const fn = ({a, q:b}) => { x = a; y = b}; fn({a: 3, q: 6, v: 8})";
602 | const evalContext = createEvalContext({
603 | localContext: {
604 | x: 0,
605 | y: 0,
606 | }
607 | });
608 | const statements = parseStatements(source);
609 |
610 | // --- Act
611 | await processStatementQueueAsync(statements, evalContext);
612 |
613 | // --- Assert
614 | expect(evalContext.localContext.x).equal(3);
615 | expect(evalContext.localContext.y).equal(6);
616 | });
617 |
618 | it("arrow destructure #9", async () => {
619 | // --- Arrange
620 | const source = "const fn = ({a, q: {b, c}}) => { x = a; y = b; z = c}; fn({a: 3, q: {b: 6, c: 8}})";
621 | const evalContext = createEvalContext({
622 | localContext: {
623 | x: 0,
624 | y: 0,
625 | z: 0,
626 | }
627 | });
628 | const statements = parseStatements(source);
629 |
630 | // --- Act
631 | await processStatementQueueAsync(statements, evalContext);
632 |
633 | // --- Assert
634 | expect(evalContext.localContext.x).equal(3);
635 | expect(evalContext.localContext.y).equal(6);
636 | expect(evalContext.localContext.z).equal(8);
637 | });
638 |
639 | it("arrow destructure #10", async () => {
640 | // --- Arrange
641 | const source = "const fn = ({a, q:[b, c]}) => { x = a; y = b; z = c}; fn({a: 3, q: [6, 8]})";
642 | const evalContext = createEvalContext({
643 | localContext: {
644 | x: 0,
645 | y: 0,
646 | z: 0,
647 | }
648 | });
649 | const statements = parseStatements(source);
650 |
651 | // --- Act
652 | await processStatementQueueAsync(statements, evalContext);
653 |
654 | // --- Assert
655 | expect(evalContext.localContext.x).equal(3);
656 | expect(evalContext.localContext.y).equal(6);
657 | expect(evalContext.localContext.z).equal(8);
658 | });
659 |
660 | it("arrow destructure #11", async () => {
661 | // --- Arrange
662 | const source = "const fn = ({a, q:[b, , c]}) => { x = a; y = b; z = c}; fn({a: 3, q: [6, -1, 8]})";
663 | const evalContext = createEvalContext({
664 | localContext: {
665 | x: 0,
666 | y: 0,
667 | z: 0,
668 | }
669 | });
670 | const statements = parseStatements(source);
671 |
672 | // --- Act
673 | await processStatementQueueAsync(statements, evalContext);
674 |
675 | // --- Assert
676 | expect(evalContext.localContext.x).equal(3);
677 | expect(evalContext.localContext.y).equal(6);
678 | expect(evalContext.localContext.z).equal(8);
679 | });
680 |
681 | it("arrow destructure #12", async () => {
682 | // --- Arrange
683 | const source = "const fn = ([a, {b, c}]) => { x = a; y = b; z = c}; fn([3, {b: 6, c: 8}])";
684 | const evalContext = createEvalContext({
685 | localContext: {
686 | x: 0,
687 | y: 0,
688 | z: 0,
689 | }
690 | });
691 | const statements = parseStatements(source);
692 |
693 | // --- Act
694 | await processStatementQueueAsync(statements, evalContext);
695 |
696 | // --- Assert
697 | expect(evalContext.localContext.x).equal(3);
698 | expect(evalContext.localContext.y).equal(6);
699 | expect(evalContext.localContext.z).equal(8);
700 | });
701 |
702 | });
```
--------------------------------------------------------------------------------
/xmlui/src/components/Table/useRowSelection.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import type { KeyboardEventHandler } from "react";
2 | import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3 | import { union, uniq } from "lodash-es";
4 |
5 | import { useEvent } from "../../components-core/utils/misc";
6 | import { EMPTY_ARRAY } from "../../components-core/constants";
7 | import { usePrevious } from "../../components-core/utils/hooks";
8 | import { useSelectionContext } from "../SelectionStore/SelectionStoreNative";
9 |
10 | /**
11 | * An interval of selected items
12 | */
13 | type SelectionInterval = {
14 | from: string;
15 | to: string;
16 | };
17 |
18 | /**
19 | * Represents an item that has an ID unique in its context
20 | */
21 | type Item = any;
22 |
23 | /**
24 | * This type defines the event options of a toggle event we consider to change the current selection
25 | */
26 | type ToggleOptions = {
27 | shiftKey?: boolean;
28 | metaKey?: boolean;
29 | ctrlKey?: boolean;
30 | singleItem?: boolean;
31 | };
32 |
33 | type SelectionApi = {
34 | getSelectedItems: () => any[];
35 | getSelectedIds: () => any[];
36 | clearSelection: () => void;
37 | selectAll: () => void;
38 | selectId: (id: any | Array<any>) => void;
39 | };
40 |
41 | /**
42 | * This type defines an object with properties and operations to manage the selection
43 | */
44 | type RowSelectionOperations = {
45 | /**
46 | * Operation to handle the keydown event
47 | */
48 | onKeyDown: KeyboardEventHandler;
49 |
50 | /**
51 | * The currently focused index (row number)
52 | */
53 | focusedIndex: number;
54 |
55 | /**
56 | * Operation to toggle the specified index
57 | * @param targetIndex Index to toggle
58 | * @param options Key options (state of SHIFT, CTRL, and META keys)
59 | */
60 | toggleRowIndex: (targetIndex: number, options: ToggleOptions | undefined) => void;
61 |
62 | /**
63 | * Operation to toggle the item with a particular ID
64 | * @param targetId Item identifier
65 | * @param options Key options (state of SHIFT, CTRL, and META keys)
66 | */
67 | toggleRow: (row: any, options: ToggleOptions | undefined) => void;
68 |
69 | /**
70 | * Operation to check or uncheck all rows
71 | * @param checked True to check, false to uncheck all rows
72 | */
73 | checkAllRows: (checked: boolean) => void;
74 |
75 | /**
76 | * A hash object that indicates if a particular row ID is selected or not
77 | */
78 | selectedRowIdMap: Record<string, boolean>;
79 |
80 | /**
81 | * The list of selected row IDs
82 | */
83 | selectedItems: any[];
84 |
85 | idKey: string;
86 |
87 | selectionApi: SelectionApi;
88 | };
89 |
90 | /**
91 | * Hook for managing table row selection with optional bidirectional AppState synchronization.
92 | *
93 | * ## AppState Synchronization Mechanism
94 | *
95 | * When `syncWithAppState` is provided, this hook implements a robust bidirectional synchronization
96 | * between the table's selection state and an AppState instance. The synchronization prevents
97 | * infinite loops using a state machine approach.
98 | *
99 | * ### State Machine Design
100 | *
101 | * The sync operates through three states:
102 | * - `idle`: Normal state, ready to respond to changes from either side
103 | * - `updating_to_appstate`: Currently propagating table selection → AppState (blocks AppState → table)
104 | * - `updating_from_appstate`: Currently propagating AppState → table selection (blocks table → AppState)
105 | *
106 | * ### Synchronization Flow
107 | *
108 | * **AppState → Table (External Updates)**:
109 | * 1. AppState.value.selectedIds changes externally (e.g., from another component)
110 | * 2. Effect detects change and validates it's not from our own update (using source tracking)
111 | * 3. Sets state to `updating_from_appstate` to block reverse sync
112 | * 4. Updates table selection via setSelectedRowIds()
113 | * 5. Uses requestAnimationFrame to reset state to `idle` after update completes
114 | *
115 | * **Table → AppState (User Interaction)**:
116 | * 1. User interacts with table (clicks, keyboard navigation)
117 | * 2. selectedItems changes through normal table selection logic
118 | * 3. Effect detects change and validates it's different from AppState
119 | * 4. Sets state to `updating_to_appstate` to block reverse sync
120 | * 5. Calls syncWithAppState.update({ selectedIds: [...] })
121 | * 6. Uses requestAnimationFrame to reset state to `idle` after update completes
122 | *
123 | * ### Loop Prevention Strategy
124 | *
125 | * Multiple mechanisms prevent infinite loops:
126 | * - **State Machine**: Directional blocking prevents simultaneous updates
127 | * - **Source Tracking**: lastUpdateSourceRef tracks whether the last change came from 'table' or 'appstate'
128 | * - **Value Tracking**: lastAppStateSelectionRef and lastTableSelectionRef track last known values
129 | * - **Change Detection**: Only triggers updates when values actually differ using JSON.stringify comparison
130 | * - **Frame-Based Reset**: Uses requestAnimationFrame instead of setTimeout for deterministic timing
131 | *
132 | * ### Usage with AppState
133 | *
134 | * ```typescript
135 | * // In your component
136 | * const appState = useAppState('myBucket');
137 | *
138 | * // Pass to Table
139 | * <Table
140 | * items={data}
141 | * syncWithAppState={appState}
142 | * // ... other props
143 | * />
144 | *
145 | * // AppState will contain: { selectedIds: ['id1', 'id2', ...] }
146 | * // Changes from either side are automatically synchronized
147 | * ```
148 | *
149 | * ### Precedence Rules
150 | *
151 | * - When both `initiallySelected` and `syncWithAppState` are provided, `syncWithAppState` takes precedence
152 | * - Multi-row selection limits are respected (single selection truncates to first ID)
153 | * - Only valid item IDs (present in current `items` array) are synchronized
154 | *
155 | * @param options Configuration object for row selection behavior
156 | * @returns Row selection operations and state management interface
157 | */
158 | export default function useRowSelection({
159 | items = EMPTY_ARRAY,
160 | visibleItems = items,
161 | rowsSelectable,
162 | enableMultiRowSelection,
163 | rowDisabledPredicate,
164 | onSelectionDidChange,
165 | initiallySelected = EMPTY_ARRAY,
166 | syncWithAppState,
167 | }: {
168 | items: Item[];
169 | visibleItems: Item[];
170 | rowsSelectable: boolean;
171 | enableMultiRowSelection: boolean;
172 | rowDisabledPredicate?: (item: any) => boolean;
173 | onSelectionDidChange?: (newSelection: Item[]) => Promise<void>;
174 | initiallySelected?: string[];
175 | syncWithAppState?: any;
176 | }): RowSelectionOperations {
177 | // --- The focused index in the row source (if there is any)
178 | const [focusedIndex, setFocusedIndex] = useState<number>(-1);
179 | // --- The current selection interval
180 | const [selectionInterval, setSelectionInterval] = useState<SelectionInterval | null>(null);
181 | // --- Access the selection context that stores the current state of selection
182 | const { selectedItems, setSelectedRowIds, refreshSelection, idKey } = useSelectionContext();
183 | // --- Refresh the list of item IDs whenever the items in the selection change
184 | const walkableList: string[] = useMemo(() => {
185 | return visibleItems.map((item) => item[idKey]);
186 | }, [idKey, visibleItems]);
187 |
188 | // --- Track if initial selection has been applied
189 | const [initialSelectionApplied, setInitialSelectionApplied] = useState(false);
190 |
191 | // --- If the items change, refresh the selectable items (if the rows are selectable)
192 | useEffect(() => {
193 | refreshSelection(rowsSelectable ? items : EMPTY_ARRAY);
194 | }, [refreshSelection, items, rowsSelectable]);
195 |
196 | // --- Handle AppState synchronization
197 | // This implements bidirectional sync between Table selection and AppState.
198 | // The approach uses React's useEffect pattern which is appropriate for React-to-React communication.
199 | // The new AppState didUpdate event is more useful for non-React integrations.
200 | const appStateSelection = syncWithAppState?.value?.selectedIds;
201 | const prevAppStateSelection = usePrevious(appStateSelection);
202 |
203 | // --- State machine for sync direction control
204 | const [syncState, setSyncState] = useState<
205 | "idle" | "updating_to_appstate" | "updating_from_appstate"
206 | >("idle");
207 |
208 | // --- Use refs to track the last known selections to prevent update loops
209 | const lastAppStateSelectionRef = useRef<any[]>();
210 | const lastTableSelectionRef = useRef<any[]>();
211 |
212 | // --- Track the source of the last update to prevent echoing
213 | const lastUpdateSourceRef = useRef<"table" | "appstate" | null>(null);
214 |
215 | // --- Sync from AppState to table selection (when AppState changes externally)
216 | useEffect(() => {
217 | // Skip if not selectable, no sync, no selection, or we're currently updating to AppState
218 | if (
219 | !rowsSelectable ||
220 | !syncWithAppState ||
221 | !appStateSelection ||
222 | syncState === "updating_to_appstate"
223 | ) {
224 | return;
225 | }
226 |
227 | // Only update if AppState selection actually changed and this wasn't caused by our own table update
228 | const appStateChanged = appStateSelection !== prevAppStateSelection;
229 | const isDifferentFromLastKnown =
230 | JSON.stringify([...(appStateSelection || [])].sort()) !==
231 | JSON.stringify([...(lastAppStateSelectionRef.current || [])].sort());
232 | const wasNotOurUpdate = lastUpdateSourceRef.current !== "table";
233 |
234 | if (appStateChanged && isDifferentFromLastKnown && wasNotOurUpdate && items.length > 0) {
235 | // Set state machine to indicate we're updating from AppState
236 | setSyncState("updating_from_appstate");
237 |
238 | const validIds = appStateSelection.filter((id: string) =>
239 | items.some((item) => item[idKey] === id),
240 | );
241 |
242 | const idsToSelect = enableMultiRowSelection ? validIds : validIds.slice(0, 1);
243 |
244 | // Track what we're setting to prevent loop
245 | lastAppStateSelectionRef.current = [...appStateSelection];
246 | lastTableSelectionRef.current = [...idsToSelect];
247 | lastUpdateSourceRef.current = "appstate";
248 |
249 | setSelectedRowIds(idsToSelect);
250 | setInitialSelectionApplied(true);
251 | }
252 | }, [
253 | appStateSelection,
254 | prevAppStateSelection,
255 | items,
256 | rowsSelectable,
257 | syncWithAppState,
258 | idKey,
259 | enableMultiRowSelection,
260 | setSelectedRowIds,
261 | syncState,
262 | ]);
263 |
264 | // --- Sync from table selection to AppState (when user interacts with table)
265 | useEffect(() => {
266 | // Skip if not selectable, no sync, or currently updating from AppState
267 | if (!rowsSelectable || !syncWithAppState || syncState === "updating_from_appstate") {
268 | return;
269 | }
270 |
271 | const currentSelectionIds = selectedItems.map((item) => item[idKey]);
272 | const appStateSelectionIds = appStateSelection || [];
273 |
274 | // Only update if table selection is different from AppState, different from our last update, and wasn't caused by AppState
275 | const tableChanged =
276 | JSON.stringify([...currentSelectionIds].sort()) !==
277 | JSON.stringify([...(lastTableSelectionRef.current || [])].sort());
278 | const isDifferentFromAppState =
279 | JSON.stringify([...currentSelectionIds].sort()) !==
280 | JSON.stringify([...appStateSelectionIds].sort());
281 | const wasNotAppStateUpdate = lastUpdateSourceRef.current !== "appstate";
282 |
283 | if (tableChanged && isDifferentFromAppState && wasNotAppStateUpdate) {
284 | // Set state machine to indicate we're updating to AppState
285 | setSyncState("updating_to_appstate");
286 |
287 | // Track what we're updating to prevent loop
288 | lastTableSelectionRef.current = [...currentSelectionIds];
289 | lastAppStateSelectionRef.current = [...currentSelectionIds];
290 | lastUpdateSourceRef.current = "table";
291 |
292 | syncWithAppState.update?.({ selectedIds: currentSelectionIds });
293 | }
294 | }, [selectedItems, syncWithAppState, appStateSelection, idKey, rowsSelectable, syncState]);
295 |
296 | // --- Reset sync state machine to idle when updates are complete
297 | useEffect(() => {
298 | if (syncState !== "idle") {
299 | // Reset to idle state in the next tick to allow the current update to complete
300 | const resetTimer = requestAnimationFrame(() => {
301 | setSyncState("idle");
302 | });
303 |
304 | return () => cancelAnimationFrame(resetTimer);
305 | }
306 | }, [syncState, appStateSelection, selectedItems]);
307 |
308 | // --- Clear update source when sync state becomes idle
309 | useEffect(() => {
310 | if (syncState === "idle") {
311 | // Use a separate frame to clear the source after the sync state is reset
312 | const clearTimer = requestAnimationFrame(() => {
313 | lastUpdateSourceRef.current = null;
314 | });
315 |
316 | return () => cancelAnimationFrame(clearTimer);
317 | }
318 | }, [syncState]);
319 |
320 | // --- Set initial selection when component mounts and items are available
321 | // Use a separate effect that runs after the refresh to ensure timing is correct
322 | useEffect(() => {
323 | // If we have AppState sync, don't use initiallySelected
324 | if (syncWithAppState) {
325 | return;
326 | }
327 |
328 | if (
329 | !rowsSelectable ||
330 | !initiallySelected ||
331 | initiallySelected.length === 0 ||
332 | initialSelectionApplied
333 | ) {
334 | return;
335 | }
336 |
337 | // Only set initial selection when items are available and we haven't applied it yet
338 | if (items.length > 0) {
339 | // Use requestAnimationFrame to ensure this runs after the refreshSelection effect
340 | const frameId = requestAnimationFrame(() => {
341 | // Filter initiallySelected to only include IDs that exist in current items
342 | const validIds = initiallySelected.filter((id) => items.some((item) => item[idKey] === id));
343 |
344 | if (validIds.length > 0) {
345 | // If multi-row selection is disabled, only select the first valid ID
346 | const idsToSelect = enableMultiRowSelection ? validIds : [validIds[0]];
347 | setSelectedRowIds(idsToSelect);
348 | setInitialSelectionApplied(true);
349 | }
350 | });
351 |
352 | return () => cancelAnimationFrame(frameId);
353 | }
354 | }, [
355 | items,
356 | initiallySelected,
357 | rowsSelectable,
358 | idKey,
359 | enableMultiRowSelection,
360 | setSelectedRowIds,
361 | initialSelectionApplied,
362 | selectedItems,
363 | syncWithAppState,
364 | ]);
365 |
366 | // --- If the multi-row selection switches to disabled, keep only the first selected item
367 | const prevEnableMultiRowSelection = usePrevious(enableMultiRowSelection);
368 | useEffect(() => {
369 | if (prevEnableMultiRowSelection && !enableMultiRowSelection) {
370 | if (selectedItems.length > 1) {
371 | setSelectedRowIds([selectedItems[0][idKey]]);
372 | }
373 | }
374 | }, [
375 | enableMultiRowSelection,
376 | idKey,
377 | prevEnableMultiRowSelection,
378 | selectedItems,
379 | setSelectedRowIds,
380 | ]);
381 |
382 | // --- If the focused item is not available set the focus to the first item
383 | useEffect(() => {
384 | if (!rowsSelectable) {
385 | return;
386 | }
387 | if (focusedIndex !== -1 && !walkableList[focusedIndex] && walkableList[0]) {
388 | setFocusedIndex(0);
389 | }
390 | }, [focusedIndex, rowsSelectable, setFocusedIndex, walkableList]);
391 |
392 | // --- Handle the user event to change the current selection. The event function handles the SHIFT, CTRL,
393 | // --- and META keys to decide how to change or extend the existing selection
394 | const toggleRowIndex = useEvent(
395 | // targetIndex: the item affected by an event
396 | // options: key event options
397 | (targetIndex: number, options: ToggleOptions = {}) => {
398 | if (!rowsSelectable) {
399 | return;
400 | }
401 |
402 | const targetId = walkableList[targetIndex];
403 | const { shiftKey, metaKey, ctrlKey } = options;
404 |
405 | const singleItem = !enableMultiRowSelection || (!shiftKey && !metaKey && !ctrlKey);
406 |
407 | // --- This variable will hold the newest selection interval
408 | let newSelectionInterval: SelectionInterval;
409 | let newSelectedRowsIdsInOrder = [...selectedItems.map((item) => item[idKey])];
410 |
411 | if (singleItem) {
412 | newSelectionInterval = {
413 | from: targetId,
414 | to: targetId,
415 | };
416 | newSelectedRowsIdsInOrder = [targetId];
417 | } else {
418 | if (shiftKey) {
419 | // --- SHIFT is pressed, extend the current selection
420 | let normalizedFromIdx: number;
421 | let normalizedToIdx: number;
422 | let from: string;
423 | let to: string;
424 |
425 | if (selectionInterval) {
426 | // --- Get the selection boundaries and normalize them (from is less than or equal than to)
427 | let oldFromIdx = walkableList.indexOf(selectionInterval.from);
428 | let oldToIdx = walkableList.indexOf(selectionInterval.to);
429 |
430 | let normalizedOldFromIdx = Math.min(oldFromIdx, oldToIdx);
431 | let normalizedOldToIdx = Math.max(oldFromIdx, oldToIdx);
432 |
433 | // --- Get the slice of selected IDs
434 | const slice = walkableList.slice(normalizedOldFromIdx, normalizedOldToIdx + 1);
435 | newSelectedRowsIdsInOrder = newSelectedRowsIdsInOrder.filter(
436 | (item) => !slice.includes(item),
437 | );
438 | from = selectionInterval.from;
439 | to = targetId;
440 | let fromIdx = walkableList.indexOf(from);
441 | let toIdx = walkableList.indexOf(to);
442 | normalizedFromIdx = Math.min(fromIdx, toIdx);
443 | normalizedToIdx = Math.max(fromIdx, toIdx);
444 | } else {
445 | from = targetId;
446 | to = targetId;
447 | normalizedFromIdx = targetIndex;
448 | normalizedToIdx = targetIndex;
449 | }
450 |
451 | const sl = walkableList.slice(normalizedFromIdx, normalizedToIdx + 1);
452 | newSelectedRowsIdsInOrder = union(newSelectedRowsIdsInOrder, sl);
453 | newSelectionInterval = {
454 | from: from,
455 | to: to,
456 | };
457 | } else {
458 | // --- SHIFT is not pressed, set the new interval to the newly focused item
459 | newSelectionInterval = {
460 | from: targetId,
461 | to: targetId,
462 | };
463 |
464 | if (metaKey || ctrlKey) {
465 | // --- If META key (Mac) or CTRL (Windows) is pressed, toggle the selection of the targeted item
466 | if (newSelectedRowsIdsInOrder.includes(targetId)) {
467 | newSelectedRowsIdsInOrder = newSelectedRowsIdsInOrder.filter(
468 | (item) => item !== targetId,
469 | );
470 | } else {
471 | newSelectedRowsIdsInOrder.push(targetId);
472 | }
473 | } else {
474 | // --- The targeted item remains the only selection
475 | newSelectedRowsIdsInOrder = [targetId];
476 | }
477 | }
478 | }
479 |
480 | // --- Update the state variables of the selection
481 | setFocusedIndex(targetIndex);
482 | setSelectedRowIds(uniq(newSelectedRowsIdsInOrder));
483 | setSelectionInterval(newSelectionInterval);
484 | },
485 | );
486 |
487 | // --- This function handles the user event to change the current selection according to the row ID
488 | // --- affected by the event
489 | const toggleRow = useEvent((item: any, options?: ToggleOptions) => {
490 | if (!rowsSelectable) {
491 | return;
492 | }
493 | const targetIndex = walkableList.indexOf(item[idKey]);
494 | toggleRowIndex(targetIndex, options);
495 | });
496 |
497 | // --- Handle the key events that may change the current selection
498 | const onKeyDown: KeyboardEventHandler = useEvent((event) => {
499 | if (!rowsSelectable) {
500 | return;
501 | }
502 | if (event.key === "ArrowDown") {
503 | // --- Move/extend the selection to the item below the focused one
504 | event.preventDefault();
505 | let newFocusIndex = Math.min(visibleItems.length - 1, focusedIndex + 1);
506 | if (focusedIndex !== visibleItems.length - 1) {
507 | toggleRowIndex(newFocusIndex, event);
508 | }
509 | }
510 | if (event.key === "PageDown") {
511 | // --- Move/extend the selection to the item 8 items below the focused one
512 | event.preventDefault();
513 | const newFocusIndex = Math.min(visibleItems.length - 1, focusedIndex + 8);
514 | toggleRowIndex(newFocusIndex, event);
515 | }
516 | if (event.key === "ArrowUp") {
517 | // --- Move/extend the selection to the item above the focused one
518 | event.preventDefault();
519 | let newFocusIndex = Math.max(0, focusedIndex - 1);
520 | if (focusedIndex >= 0) {
521 | toggleRowIndex(newFocusIndex, event);
522 | }
523 | }
524 | if (event.key === "PageUp") {
525 | // --- Move/extend the selection to the item 8 items above the focused one
526 | event.preventDefault();
527 | const newFocusIndex = Math.max(0, focusedIndex - 8);
528 | toggleRowIndex(newFocusIndex, event);
529 | }
530 | });
531 |
532 | useEffect(() => {
533 | // console.log("selection DID CHANGE?");
534 | void onSelectionDidChange?.(selectedItems);
535 | }, [selectedItems, onSelectionDidChange]);
536 |
537 | /**
538 | * This operation checks or clears all rows
539 | */
540 | const checkAllRows = useEvent((checked: boolean) => {
541 | if (!rowsSelectable) {
542 | return;
543 | }
544 | if (!enableMultiRowSelection && checked) {
545 | return;
546 | }
547 | setSelectedRowIds(
548 | checked
549 | ? items
550 | .filter((item) => (rowDisabledPredicate ? !rowDisabledPredicate(item) : true))
551 | .map((item) => item[idKey])
552 | : [],
553 | );
554 | });
555 |
556 | /**
557 | * This operation creates a hash object that indicates the selected status of selected row IDs
558 | */
559 | const selectedRowIdMap = useMemo(() => {
560 | let rows: Record<string, boolean> = {};
561 | selectedItems.forEach((item) => {
562 | rows[item[idKey]] = true;
563 | });
564 | return rows;
565 | }, [idKey, selectedItems]);
566 |
567 | const getSelectedItems = useCallback(() => {
568 | return selectedItems;
569 | }, [selectedItems]);
570 |
571 | const getSelectedIds = useCallback(() => {
572 | return selectedItems.map((item) => item[idKey]);
573 | }, [idKey, selectedItems]);
574 |
575 | const clearSelection = useCallback(() => {
576 | checkAllRows(false);
577 | }, [checkAllRows]);
578 |
579 | const selectAll = useCallback(() => {
580 | checkAllRows(true);
581 | }, [checkAllRows]);
582 |
583 | const selectId = useCallback(
584 | (id: any | Array<any>) => {
585 | if (!rowsSelectable) {
586 | return;
587 | }
588 | let ids = Array.isArray(id) ? id : [id];
589 | if (ids.length > 1 && !enableMultiRowSelection) {
590 | ids = [ids[0]];
591 | }
592 | setSelectedRowIds(ids);
593 | },
594 | [enableMultiRowSelection, rowsSelectable, setSelectedRowIds],
595 | );
596 |
597 | const api = useMemo(() => {
598 | return {
599 | getSelectedItems,
600 | getSelectedIds,
601 | clearSelection,
602 | selectAll,
603 | selectId,
604 | };
605 | }, [clearSelection, getSelectedIds, getSelectedItems, selectAll, selectId]);
606 |
607 | // --- Retrieve the selection management object
608 | return {
609 | onKeyDown,
610 | focusedIndex,
611 | toggleRowIndex,
612 | toggleRow,
613 | checkAllRows,
614 | selectedRowIdMap,
615 | selectedItems,
616 | idKey,
617 | selectionApi: api,
618 | };
619 | }
620 |
```