This is page 161 of 181. Use http://codebase.md/xmlui-org/xmlui/tools/vscode/resources/assets/img/%7Bsrc%7D?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.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
│ ├── layout-changes.md
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ └── lorem-ipsum.png
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ └── welcome-to-the-xmlui-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
│ │ └── 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
│ │ ├── HelloMd.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
│ │ │ │ ├── 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
│ ├── 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
│ │ └── tsconfig.json
│ ├── 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
│ │ ├── tsconfig.json
│ │ └── 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
│ │ └── tsconfig.json
│ ├── 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
│ │ └── tsconfig.json
│ ├── 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
│ │ └── tsconfig.json
│ ├── 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
│ │ └── tsconfig.json
│ ├── 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
│ │ └── tsconfig.json
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── index.tsx
│ │ │ ├── Spreadsheet.tsx
│ │ │ └── SpreadsheetNative.tsx
│ │ └── tsconfig.json
│ └── 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.tsx
│ │ │ └── HeroSectionNative.tsx
│ │ ├── index.tsx
│ │ ├── ScrollToTop
│ │ │ ├── ScrollToTop.module.scss
│ │ │ ├── ScrollToTop.tsx
│ │ │ └── ScrollToTopNative.tsx
│ │ └── vite-env.d.ts
│ └── tsconfig.json
├── 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.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
│ ├── 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
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── playwright.config.ts
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.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
│ ├── get-langserver-metadata.mjs
│ ├── 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.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ ├── LabelListNative.module.scss
│ │ │ │ └── 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.mjs
│ ├── 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
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.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
--------------------------------------------------------------------------------
/xmlui/src/components/Tree/TreeNative.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { type ReactNode, memo, useCallback, useEffect, useMemo, useState, useRef } from "react";
2 | import { Virtualizer, type VirtualizerHandle } from "virtua";
3 | import classnames from "classnames";
4 | import Icon from "../Icon/IconNative";
5 | import { Spinner } from "../Spinner/SpinnerNative";
6 |
7 | import styles from "./TreeComponent.module.scss";
8 |
9 | import type {
10 | FlatTreeNode,
11 | TreeNode,
12 | UnPackedTreeData,
13 | TreeFieldConfig,
14 | TreeSelectionEvent,
15 | TreeDataFormat,
16 | DefaultExpansion,
17 | NodeLoadingState,
18 | FlatTreeNodeWithState,
19 | } from "../../components-core/abstractions/treeAbstractions";
20 | import { toFlatTree, flatToNative, hierarchyToNative } from "../../components-core/utils/treeUtils";
21 |
22 | /**
23 | * Describes the data attached to a particular tree row
24 | */
25 | interface RowContext {
26 | nodes: FlatTreeNode[];
27 | toggleNode: (node: FlatTreeNode) => void;
28 | selectedId: string | number | undefined;
29 | itemRenderer: (item: any) => ReactNode;
30 | itemClickExpands: boolean;
31 | onItemClick?: (item: FlatTreeNode) => void;
32 | onSelection: (node: FlatTreeNode) => void;
33 | focusedIndex: number;
34 | onKeyDown: (e: React.KeyboardEvent) => void;
35 | treeContainerRef: React.RefObject<HTMLDivElement>;
36 | iconCollapsed: string;
37 | iconExpanded: string;
38 | iconSize: string;
39 | animateExpand: boolean;
40 | expandRotation: number;
41 | }
42 |
43 | interface TreeRowProps {
44 | index: number;
45 | data: RowContext;
46 | }
47 |
48 | const TreeRow = memo(({ index, data }: TreeRowProps) => {
49 | const {
50 | nodes,
51 | toggleNode,
52 | selectedId,
53 | itemRenderer,
54 | itemClickExpands,
55 | onItemClick,
56 | onSelection,
57 | focusedIndex,
58 | treeContainerRef,
59 | iconCollapsed,
60 | iconExpanded,
61 | iconSize,
62 | animateExpand,
63 | expandRotation,
64 | } = data;
65 | const treeItem = nodes[index];
66 | const isFocused = focusedIndex === index && focusedIndex >= 0;
67 |
68 | // Use string comparison to handle type mismatches between selectedId and treeItem.key
69 | const isSelected = String(selectedId) === String(treeItem.key);
70 |
71 | const onToggleNode = useCallback(
72 | (e: React.MouseEvent) => {
73 | e.stopPropagation();
74 | // Prevent toggling if node is in loading state
75 | const nodeWithState = treeItem as FlatTreeNodeWithState;
76 | if (nodeWithState.loadingState === "loading") {
77 | return;
78 | }
79 | toggleNode(treeItem);
80 | },
81 | [toggleNode, treeItem],
82 | );
83 |
84 | const onItemMouseDownHandler = useCallback(
85 | (e: React.MouseEvent) => {
86 | // Handle selection immediately on mouse down for immediate visual feedback
87 | if (treeItem.selectable) {
88 | onSelection(treeItem);
89 | // Ensure tree container maintains focus after mouse selection
90 | setTimeout(() => {
91 | treeContainerRef.current?.focus();
92 | }, 0);
93 | }
94 | },
95 | [onSelection, treeItem, treeContainerRef],
96 | );
97 |
98 | const onItemClickHandler = useCallback(
99 | (e: React.MouseEvent) => {
100 | // Selection is already handled in onMouseDown, so we skip it here
101 |
102 | // Call optional onItemClick callback
103 | if (onItemClick) {
104 | onItemClick(treeItem);
105 | }
106 |
107 | // If itemClickExpands is enabled and item has children, also toggle
108 | // But prevent toggling if node is in loading state
109 | const nodeWithState = treeItem as FlatTreeNodeWithState;
110 | if (itemClickExpands && treeItem.hasChildren && nodeWithState.loadingState !== "loading") {
111 | toggleNode(treeItem);
112 | }
113 | },
114 | [onItemClick, itemClickExpands, treeItem, toggleNode],
115 | );
116 |
117 | // Get loading state for styling and interaction
118 | const nodeWithState = treeItem as FlatTreeNodeWithState;
119 | const isLoading = nodeWithState.loadingState === "loading";
120 |
121 | return (
122 | <div style={{ width: "100%", display: "flex" }}>
123 | <div
124 | className={classnames(styles.rowWrapper, {
125 | [styles.selected]: isSelected,
126 | [styles.focused]: isFocused,
127 | })}
128 | role="treeitem"
129 | aria-level={treeItem.depth + 1}
130 | aria-expanded={treeItem.hasChildren ? treeItem.isExpanded : undefined}
131 | aria-selected={isSelected}
132 | aria-label={treeItem.displayName}
133 | aria-busy={isLoading}
134 | tabIndex={isFocused ? 0 : -1}
135 | >
136 | <div
137 | onClick={onToggleNode}
138 | className={styles.gutter}
139 | style={{ cursor: isLoading ? "default" : "pointer" }}
140 | >
141 | <div style={{ width: treeItem.depth * 10 }} className={styles.depthPlaceholder} />
142 | <div
143 | className={classnames(styles.toggleWrapper, {
144 | [styles.expanded]: treeItem.isExpanded,
145 | [styles.hidden]: !treeItem.hasChildren,
146 | })}
147 | >
148 | {treeItem.hasChildren && (
149 | <>
150 | {/* Show loading spinner when node is loading */}
151 | {(treeItem as FlatTreeNodeWithState).loadingState === "loading" ? (
152 | <Spinner delay={0} style={{ width: 24, height: 24 }} />
153 | ) : (
154 | <Icon
155 | name={
156 | animateExpand
157 | ? treeItem.iconCollapsed || iconCollapsed
158 | : treeItem.isExpanded
159 | ? treeItem.iconExpanded || iconExpanded
160 | : treeItem.iconCollapsed || iconCollapsed
161 | }
162 | size={iconSize}
163 | className={classnames(styles.toggleIcon, {
164 | [styles.rotated]: animateExpand && treeItem.isExpanded,
165 | })}
166 | style={
167 | animateExpand && treeItem.isExpanded
168 | ? {
169 | transform: `rotate(${expandRotation}deg)`,
170 | }
171 | : undefined
172 | }
173 | />
174 | )}
175 | </>
176 | )}
177 | </div>
178 | </div>
179 | <div
180 | className={styles.labelWrapper}
181 | onMouseDown={onItemMouseDownHandler}
182 | onClick={onItemClickHandler}
183 | style={{ cursor: "pointer" }}
184 | >
185 | {itemRenderer(treeItem)}
186 | </div>
187 | </div>
188 | </div>
189 | );
190 | });
191 |
192 | const emptyTreeData: UnPackedTreeData = {
193 | treeData: [],
194 | treeItemsById: {},
195 | };
196 |
197 | /**
198 | * Find all parent node IDs for a given node ID by traversing up the tree structure
199 | * @param nodeId The target node ID to find parents for
200 | * @param treeItemsById Map of all tree nodes by their ID
201 | * @returns Array of parent node IDs from immediate parent to root
202 | */
203 | const findAllParentIds = (
204 | nodeId: string | number,
205 | treeItemsById: Record<string, TreeNode>,
206 | ): (string | number)[] => {
207 | const parentIds: (string | number)[] = [];
208 | const targetNode = treeItemsById[String(nodeId)];
209 |
210 | if (!targetNode) {
211 | return parentIds;
212 | }
213 |
214 | // Walk up the tree using parentIds property which contains the path from root to parent
215 | if (targetNode.parentIds && targetNode.parentIds.length > 0) {
216 | parentIds.push(...targetNode.parentIds);
217 | }
218 |
219 | return parentIds;
220 | };
221 |
222 | /**
223 | * Expand all parent paths for an array of node IDs to ensure they are visible
224 | * @param nodeIds Array of node IDs that should be expanded
225 | * @param treeItemsById Map of all tree nodes by their ID
226 | * @returns Array containing original node IDs plus all necessary parent IDs
227 | */
228 | const expandParentPaths = (
229 | nodeIds: (string | number)[],
230 | treeItemsById: Record<string, TreeNode>,
231 | ): (string | number)[] => {
232 | const allExpandedIds = new Set<string | number>(nodeIds);
233 |
234 | // For each target node, find and add all its parent IDs
235 | nodeIds.forEach((nodeId) => {
236 | const parentIds = findAllParentIds(nodeId, treeItemsById);
237 | parentIds.forEach((parentId) => allExpandedIds.add(parentId));
238 | });
239 |
240 | return Array.from(allExpandedIds);
241 | };
242 |
243 | // Default props following XMLUI conventions
244 | export const defaultProps = {
245 | dataFormat: "flat" as const,
246 | idField: "id",
247 | nameField: "name",
248 | iconField: "icon",
249 | iconExpandedField: "iconExpanded",
250 | iconCollapsedField: "iconCollapsed",
251 | parentIdField: "parentId",
252 | childrenField: "children",
253 | selectableField: "selectable",
254 | defaultExpanded: "none" as const,
255 | autoExpandToSelection: true,
256 | itemClickExpands: false,
257 | dynamicField: "dynamic",
258 | iconCollapsed: "chevronright",
259 | iconExpanded: "chevrondown",
260 | iconSize: "16",
261 | itemHeight: 32,
262 | animateExpand: false,
263 | expandRotation: 90,
264 | };
265 |
266 | interface TreeComponentProps {
267 | registerComponentApi?: (api: any) => void;
268 | data?: any; // Raw data in flat array or hierarchy format
269 | dataFormat?: TreeDataFormat;
270 | idField?: string;
271 | nameField?: string;
272 | iconField?: string;
273 | iconExpandedField?: string;
274 | iconCollapsedField?: string;
275 | parentIdField?: string;
276 | childrenField?: string;
277 | selectableField?: string;
278 | selectedValue?: string | number;
279 | selectedId?: string | number;
280 | defaultExpanded?: DefaultExpansion;
281 | autoExpandToSelection?: boolean;
282 | itemClickExpands?: boolean;
283 | dynamicField?: string;
284 | iconCollapsed?: string;
285 | iconExpanded?: string;
286 | iconSize?: string;
287 | itemHeight?: number;
288 | animateExpand?: boolean;
289 | expandRotation?: number;
290 | onItemClick?: (node: FlatTreeNode) => void;
291 | onSelectionChanged?: (event: TreeSelectionEvent) => void;
292 | onNodeExpanded?: (node: FlatTreeNode) => void;
293 | onNodeCollapsed?: (node: FlatTreeNode) => void;
294 | loadChildren?: (node: FlatTreeNode) => Promise<any[]>;
295 | itemRenderer: (item: any) => ReactNode;
296 | className?: string;
297 | }
298 |
299 | export const TreeComponent = memo((props: TreeComponentProps) => {
300 | const {
301 | registerComponentApi,
302 | data = emptyTreeData,
303 | dataFormat = defaultProps.dataFormat,
304 | idField = defaultProps.idField,
305 | nameField = defaultProps.nameField,
306 | iconField = defaultProps.iconField,
307 | iconExpandedField = defaultProps.iconExpandedField,
308 | iconCollapsedField = defaultProps.iconCollapsedField,
309 | parentIdField = defaultProps.parentIdField,
310 | childrenField = defaultProps.childrenField,
311 | selectableField = defaultProps.selectableField,
312 | selectedValue,
313 | selectedId,
314 | defaultExpanded = defaultProps.defaultExpanded,
315 | autoExpandToSelection = defaultProps.autoExpandToSelection,
316 | itemClickExpands = defaultProps.itemClickExpands,
317 | dynamicField = defaultProps.dynamicField,
318 | iconCollapsed = defaultProps.iconCollapsed,
319 | iconExpanded = defaultProps.iconExpanded,
320 | iconSize = defaultProps.iconSize,
321 | itemHeight = defaultProps.itemHeight,
322 | animateExpand = defaultProps.animateExpand,
323 | expandRotation = defaultProps.expandRotation,
324 | onItemClick,
325 | onSelectionChanged,
326 | onNodeExpanded,
327 | onNodeCollapsed,
328 | loadChildren,
329 | itemRenderer,
330 | className,
331 | } = props;
332 | // Internal selection state for uncontrolled usage
333 | // Initialize with selectedValue if provided and no onSelectionChanged handler (uncontrolled mode)
334 | const [internalSelectedId, setInternalSelectedId] = useState<string | number | undefined>(() => {
335 | return !onSelectionChanged && selectedValue ? selectedValue : undefined;
336 | });
337 |
338 | // Internal data state for API methods that modify the tree structure
339 | const [internalData, setInternalData] = useState<any>(undefined);
340 | const [dataRevision, setDataRevision] = useState(0);
341 |
342 | // Use internal data if available, otherwise use props data
343 | const effectiveData = internalData ?? data;
344 |
345 | // Helper function to update internal data and force re-render
346 | const updateInternalData = useCallback((updater: (prevData: any) => any) => {
347 | setInternalData(updater);
348 | setDataRevision((prev) => prev + 1);
349 | }, []);
350 |
351 | // Build field configuration
352 | const fieldConfig = useMemo<TreeFieldConfig>(
353 | () => ({
354 | idField: idField || "id",
355 | labelField: nameField || "name",
356 | iconField,
357 | iconExpandedField,
358 | iconCollapsedField,
359 | parentField: parentIdField,
360 | childrenField,
361 | selectableField,
362 | dynamicField,
363 | }),
364 | [
365 | idField,
366 | nameField,
367 | iconField,
368 | iconExpandedField,
369 | iconCollapsedField,
370 | parentIdField,
371 | childrenField,
372 | selectableField,
373 | dynamicField,
374 | ],
375 | );
376 |
377 | // Steps 3a & 3b: Transform data based on format
378 | // Enhanced data transformation pipeline with validation and error handling
379 | const transformedData = useMemo(() => {
380 | // Return empty data if no data provided
381 | if (!effectiveData) {
382 | return emptyTreeData;
383 | }
384 |
385 | try {
386 | if (dataFormat === "flat") {
387 | // Validation: Flat format requires array
388 | if (!Array.isArray(effectiveData)) {
389 | throw new Error(
390 | `TreeComponent: dataFormat='flat' requires array data, received: ${typeof effectiveData}`,
391 | );
392 | }
393 |
394 | // Validation: Check for required fields in sample data
395 | if (effectiveData.length > 0) {
396 | const sampleItem = effectiveData[0];
397 | if (typeof sampleItem !== "object" || sampleItem === null) {
398 | throw new Error("TreeComponent: Flat data items must be objects");
399 | }
400 | if (!(fieldConfig.idField in sampleItem)) {
401 | throw new Error(
402 | `TreeComponent: Required field '${fieldConfig.idField}' not found in flat data items`,
403 | );
404 | }
405 | if (!(fieldConfig.labelField in sampleItem)) {
406 | throw new Error(
407 | `TreeComponent: Required field '${fieldConfig.labelField}' not found in flat data items`,
408 | );
409 | }
410 | }
411 |
412 | return flatToNative(effectiveData, fieldConfig);
413 | } else if (dataFormat === "hierarchy") {
414 | // Validation: Hierarchy format requires object or array
415 | if (!effectiveData || typeof effectiveData !== "object") {
416 | throw new Error(
417 | `TreeComponent: dataFormat='hierarchy' requires object or array data, received: ${typeof effectiveData}`,
418 | );
419 | }
420 |
421 | // Validation: Check for required fields in hierarchy data
422 | const checkHierarchyData = (item: any): void => {
423 | if (typeof item !== "object" || item === null) {
424 | throw new Error("TreeComponent: Hierarchy data items must be objects");
425 | }
426 | if (!(fieldConfig.idField in item)) {
427 | throw new Error(
428 | `TreeComponent: Required field '${fieldConfig.idField}' not found in hierarchy data`,
429 | );
430 | }
431 | if (!(fieldConfig.labelField in item)) {
432 | throw new Error(
433 | `TreeComponent: Required field '${fieldConfig.labelField}' not found in hierarchy data`,
434 | );
435 | }
436 | };
437 |
438 | if (Array.isArray(effectiveData)) {
439 | if (effectiveData.length > 0) {
440 | checkHierarchyData(effectiveData[0]);
441 | }
442 | } else {
443 | checkHierarchyData(effectiveData);
444 | }
445 |
446 | return hierarchyToNative(effectiveData, fieldConfig);
447 | } else {
448 | throw new Error(
449 | `TreeComponent: Unsupported dataFormat '${dataFormat}'. Use 'flat' or 'hierarchy'.`,
450 | );
451 | }
452 | } catch (error) {
453 | // Return empty data on error to prevent crashes
454 | return emptyTreeData;
455 | }
456 | }, [effectiveData, dataFormat, fieldConfig, dataRevision]);
457 |
458 | const { treeData, treeItemsById } = transformedData;
459 |
460 | // Use selectedValue (source ID) directly since TreeNode.key is the source ID
461 | const mappedSelectedId = useMemo(() => {
462 | if (selectedValue) {
463 | // For flat/hierarchy formats, selectedValue is already the source ID (matches TreeNode.key)
464 | return selectedValue;
465 | }
466 | return selectedId;
467 | }, [selectedValue, selectedId]);
468 |
469 | // Determine if we're in controlled mode (has onSelectionChanged handler) or uncontrolled mode
470 | const isControlledMode = !!onSelectionChanged;
471 |
472 | // Use mapped selectedValue/selectedId if in controlled mode and provided,
473 | // otherwise use internal state (uncontrolled mode or controlled mode without selectedValue)
474 | const effectiveSelectedId =
475 | isControlledMode && mappedSelectedId !== undefined ? mappedSelectedId : internalSelectedId;
476 |
477 | // Initialize expanded IDs based on defaultExpanded prop
478 | const [expandedIds, setExpandedIds] = useState<(string | number)[]>(() => {
479 | // Helper function to check if a node is dynamic (should not be auto-expanded)
480 | const isDynamic = (node: TreeNode): boolean => {
481 | return !!(fieldConfig.dynamicField && node[fieldConfig.dynamicField]);
482 | };
483 |
484 | if (defaultExpanded === "first-level") {
485 | return treeData.filter((node) => !isDynamic(node)).map((node) => node.key);
486 | } else if (defaultExpanded === "all") {
487 | const allIds: (string | number)[] = [];
488 | const collectIds = (nodes: TreeNode[]) => {
489 | nodes.forEach((node) => {
490 | if (!isDynamic(node)) {
491 | allIds.push(node.key);
492 | }
493 | if (node.children) {
494 | collectIds(node.children);
495 | }
496 | });
497 | };
498 | collectIds(treeData);
499 | return allIds;
500 | } else if (Array.isArray(defaultExpanded)) {
501 | // Expand full paths to specified nodes by including all parent nodes
502 | // But exclude dynamic nodes from the expansion
503 | const expandedPaths = expandParentPaths(defaultExpanded, treeItemsById);
504 | return expandedPaths.filter((nodeId) => {
505 | const node = treeItemsById[String(nodeId)];
506 | return !node || !isDynamic(node);
507 | });
508 | }
509 | return [];
510 | });
511 |
512 | // Node loading states management for dynamic loading
513 | const [nodeStates, setNodeStates] = useState<Map<string | number, NodeLoadingState>>(new Map());
514 |
515 | // Helper functions for managing node loading states
516 | const getNodeState = useCallback(
517 | (nodeId: string | number): NodeLoadingState => {
518 | return nodeStates.get(nodeId) || "loaded";
519 | },
520 | [nodeStates],
521 | );
522 |
523 | const setNodeState = useCallback((nodeId: string | number, state: NodeLoadingState) => {
524 | setNodeStates((prev) => {
525 | const newStates = new Map(prev);
526 | newStates.set(nodeId, state);
527 | return newStates;
528 | });
529 | }, []);
530 |
531 | // Simplified focus management
532 | const [focusedIndex, setFocusedIndex] = useState<number>(-1);
533 | const treeContainerRef = useRef<HTMLDivElement>(null);
534 | const listRef = useRef<VirtualizerHandle>(null);
535 |
536 | const flatTreeData = useMemo(() => {
537 | return toFlatTree(treeData, expandedIds, fieldConfig.dynamicField, nodeStates);
538 | }, [expandedIds, treeData, fieldConfig.dynamicField, nodeStates]);
539 |
540 | // Tree node utilities for consistent ID mapping
541 | const findNodeById = useCallback(
542 | (nodeId: string | number): FlatTreeNode | null => {
543 | return flatTreeData.find((n) => String(n.key) === String(nodeId)) || null;
544 | },
545 | [flatTreeData],
546 | );
547 |
548 | const findNodeIndexById = useCallback(
549 | (nodeId: string | number): number => {
550 | return flatTreeData.findIndex((item) => String(item.key) === String(nodeId));
551 | },
552 | [flatTreeData],
553 | );
554 |
555 | // Tree validation utilities
556 | const nodeExists = useCallback(
557 | (nodeId: string | number): boolean => {
558 | return Object.values(treeItemsById).some((n) => String(n.key) === String(nodeId));
559 | },
560 | [treeItemsById],
561 | );
562 |
563 | /**
564 | * Centralized selection handler - handles all selection logic consistently
565 | * @param nodeId - The node key (source ID) to select, or undefined to clear selection
566 | */
567 | const setSelectedNodeById = useCallback(
568 | (nodeId: string | number | undefined) => {
569 | // Find the node if nodeId is provided
570 | const node = nodeId
571 | ? Object.values(treeItemsById).find((n) => String(n.key) === String(nodeId))
572 | : null;
573 |
574 | const nodeKey = node?.key;
575 |
576 | // Get previous selection for event
577 | const previousNode = effectiveSelectedId ? findNodeById(effectiveSelectedId) : null;
578 |
579 | // Always update internal state (this provides visual feedback)
580 | setInternalSelectedId(nodeKey);
581 |
582 | // Update focused index to match the selected item
583 | if (nodeKey) {
584 | const nodeIndex = flatTreeData.findIndex((item) => String(item.key) === String(nodeKey));
585 | if (nodeIndex >= 0) {
586 | setFocusedIndex(nodeIndex);
587 | }
588 | }
589 |
590 | // Fire selection event if handler is provided
591 | if (onSelectionChanged) {
592 | const newNode = node
593 | ? ({
594 | ...node,
595 | isExpanded: expandedIds.includes(node.key),
596 | depth: node.parentIds.length,
597 | hasChildren: !!(node.children && node.children.length > 0),
598 | } as FlatTreeNode)
599 | : null;
600 |
601 | onSelectionChanged({
602 | previousNode,
603 | newNode,
604 | });
605 | }
606 | },
607 | [
608 | treeItemsById,
609 | effectiveSelectedId,
610 | flatTreeData,
611 | expandedIds,
612 | onSelectionChanged,
613 | internalSelectedId,
614 | ],
615 | );
616 |
617 | // Simple tree API method implementations
618 | const getExpandedNodes = useCallback((): (string | number)[] => {
619 | return expandedIds;
620 | }, [expandedIds]);
621 |
622 | const getSelectedNode = useCallback(() => {
623 | if (!effectiveSelectedId) return null;
624 | return (
625 | Object.values(treeItemsById).find(
626 | (node) => String(node.key) === String(effectiveSelectedId),
627 | ) || null
628 | );
629 | }, [effectiveSelectedId, treeItemsById]);
630 |
631 | const getNodeById = useCallback(
632 | (nodeId: string | number) => {
633 | return Object.values(treeItemsById).find((n) => String(n.key) === String(nodeId)) || null;
634 | },
635 | [treeItemsById],
636 | );
637 |
638 | const clearSelection = useCallback(() => {
639 | setSelectedNodeById(undefined);
640 | }, [setSelectedNodeById]);
641 |
642 | // Initialize selection based on selectedValue prop - only on mount
643 | useEffect(() => {
644 | if (selectedValue !== undefined && !onSelectionChanged) {
645 | // Uncontrolled mode: set initial selection based on selectedValue
646 | setInternalSelectedId(selectedValue);
647 | }
648 | }, []); // Only run on mount
649 |
650 | /**
651 | * ensure the selected item's parents are expanded when selection changes
652 | */
653 | useEffect(() => {
654 | if (autoExpandToSelection && effectiveSelectedId) {
655 | // Find node by key (source ID) since treeItemsById is indexed by id
656 | const treeItem = Object.values(treeItemsById).find(
657 | (node) => node.key === effectiveSelectedId,
658 | );
659 | if (treeItem) {
660 | setExpandedIds((prev) => [...prev, ...treeItem.parentIds]);
661 | }
662 | }
663 | }, [autoExpandToSelection, effectiveSelectedId, treeItemsById]);
664 |
665 | const toggleNode = useCallback(
666 | async (node: FlatTreeNode) => {
667 | if (!node.isExpanded) {
668 | // Expanding the node
669 | setExpandedIds((prev) => [...prev, node.key]);
670 |
671 | // Always fire nodeDidExpand event
672 | if (onNodeExpanded) {
673 | onNodeExpanded({ ...node, isExpanded: true });
674 | }
675 |
676 | // Check if we need to load children dynamically
677 | const nodeWithState = node as FlatTreeNodeWithState;
678 | if (nodeWithState.loadingState === "unloaded" && loadChildren) {
679 | // Set loading state
680 | setNodeStates((prev) => new Map(prev).set(node.key, "loading"));
681 |
682 | // Immediately remove existing children so node appears empty while loading
683 | updateInternalData((prevData) => {
684 | const currentData = prevData ?? data;
685 |
686 | if (dataFormat === "flat" && Array.isArray(currentData)) {
687 | // Remove existing children of this node
688 | return currentData.filter(
689 | (item) => String(item[fieldConfig.parentField || "parentId"]) !== String(node.key),
690 | );
691 | } else if (dataFormat === "hierarchy" && Array.isArray(currentData)) {
692 | // For hierarchy format, clear children array
693 | const clearChildren = (nodes: any[]): any[] => {
694 | return nodes.map((n) => {
695 | if (String(n[fieldConfig.idField || "id"]) === String(node.key)) {
696 | return {
697 | ...n,
698 | [fieldConfig.childrenField || "children"]: [],
699 | };
700 | } else if (n[fieldConfig.childrenField || "children"]) {
701 | return {
702 | ...n,
703 | [fieldConfig.childrenField || "children"]: clearChildren(
704 | n[fieldConfig.childrenField || "children"],
705 | ),
706 | };
707 | }
708 | return n;
709 | });
710 | };
711 | return clearChildren(currentData);
712 | }
713 |
714 | return currentData;
715 | });
716 |
717 | try {
718 | // Load the children data
719 | const loadedData = await loadChildren({ ...node, isExpanded: true });
720 |
721 | // Update the tree data with loaded children
722 | if (loadedData && Array.isArray(loadedData) && loadedData.length > 0) {
723 | updateInternalData((prevData) => {
724 | const currentData = prevData ?? data;
725 |
726 | if (dataFormat === "flat" && Array.isArray(currentData)) {
727 | // Replace existing children with newly loaded data
728 |
729 | // Remove existing children of this node
730 | const filteredData = currentData.filter(
731 | (item) =>
732 | String(item[fieldConfig.parentField || "parentId"]) !== String(node.key),
733 | );
734 |
735 | // Add new children
736 | const newItems = loadedData.map((item) => ({
737 | ...item,
738 | [fieldConfig.parentField || "parentId"]: String(node.key),
739 | }));
740 |
741 | return [...filteredData, ...newItems];
742 | } else if (dataFormat === "hierarchy" && Array.isArray(currentData)) {
743 | // For hierarchy format, we need to find the node and add children
744 | const updateHierarchy = (nodes: any[]): any[] => {
745 | return nodes.map((n) => {
746 | if (String(n[fieldConfig.idField || "id"]) === String(node.key)) {
747 | return {
748 | ...n,
749 | [fieldConfig.childrenField || "children"]: loadedData,
750 | };
751 | } else if (n[fieldConfig.childrenField || "children"]) {
752 | return {
753 | ...n,
754 | [fieldConfig.childrenField || "children"]: updateHierarchy(
755 | n[fieldConfig.childrenField || "children"],
756 | ),
757 | };
758 | }
759 | return n;
760 | });
761 | };
762 | return updateHierarchy(currentData);
763 | }
764 |
765 | return currentData;
766 | });
767 | }
768 |
769 | // Set loaded state
770 | setNodeStates((prev) => new Map(prev).set(node.key, "loaded"));
771 | } catch (error) {
772 | console.error("Error loading tree node data:", error);
773 | // Set back to unloaded state on error
774 | setNodeStates((prev) => {
775 | const newMap = new Map(prev);
776 | newMap.delete(node.key);
777 | return newMap;
778 | });
779 | // Collapse the node since loading failed
780 | setExpandedIds((prev) => prev.filter((id) => id !== node.key));
781 | }
782 | }
783 | } else {
784 | // Collapsing the node
785 | setExpandedIds((prev) => prev.filter((id) => id !== node.key));
786 |
787 | // Fire nodeDidCollapse event
788 | if (onNodeCollapsed) {
789 | onNodeCollapsed({ ...node, isExpanded: false });
790 | }
791 | }
792 | },
793 | [onNodeExpanded, onNodeCollapsed, loadChildren, data, dataFormat, fieldConfig, setNodeStates],
794 | );
795 |
796 | // Simplified keyboard navigation handler
797 | const handleKeyDown = useCallback(
798 | (e: React.KeyboardEvent) => {
799 | if (flatTreeData.length === 0) return;
800 |
801 | const currentIndex = focusedIndex >= 0 ? focusedIndex : 0;
802 | let newIndex = currentIndex;
803 | let handled = false;
804 |
805 | switch (e.key) {
806 | case "ArrowDown":
807 | e.preventDefault();
808 | newIndex = Math.min(currentIndex + 1, flatTreeData.length - 1);
809 | handled = true;
810 | break;
811 |
812 | case "ArrowUp":
813 | e.preventDefault();
814 | newIndex = Math.max(currentIndex - 1, 0);
815 | handled = true;
816 | break;
817 |
818 | case "ArrowRight":
819 | e.preventDefault();
820 | if (currentIndex >= 0) {
821 | const currentNode = flatTreeData[currentIndex];
822 | if (currentNode.hasChildren && !currentNode.isExpanded) {
823 | // Expand node
824 | void toggleNode(currentNode);
825 | } else if (
826 | currentNode.hasChildren &&
827 | currentNode.isExpanded &&
828 | currentIndex + 1 < flatTreeData.length
829 | ) {
830 | // Move to first child
831 | newIndex = currentIndex + 1;
832 | }
833 | }
834 | handled = true;
835 | break;
836 |
837 | case "ArrowLeft":
838 | e.preventDefault();
839 | if (currentIndex >= 0) {
840 | const currentNode = flatTreeData[currentIndex];
841 | if (currentNode.hasChildren && currentNode.isExpanded) {
842 | // Collapse node
843 | void toggleNode(currentNode);
844 | } else if (currentNode.depth > 0) {
845 | // Move to parent - find previous node with smaller depth
846 | for (let i = currentIndex - 1; i >= 0; i--) {
847 | if (flatTreeData[i].depth < currentNode.depth) {
848 | newIndex = i;
849 | break;
850 | }
851 | }
852 | }
853 | }
854 | handled = true;
855 | break;
856 |
857 | case "Home":
858 | e.preventDefault();
859 | newIndex = 0;
860 | handled = true;
861 | break;
862 |
863 | case "End":
864 | e.preventDefault();
865 | newIndex = flatTreeData.length - 1;
866 | handled = true;
867 | break;
868 |
869 | case "Enter":
870 | case " ":
871 | e.preventDefault();
872 | if (currentIndex >= 0) {
873 | const currentNode = flatTreeData[currentIndex];
874 | // Handle selection
875 | if (currentNode.selectable) {
876 | setSelectedNodeById(currentNode.key);
877 | // Ensure focus stays on the current item after selection
878 | newIndex = currentIndex;
879 | }
880 | // Handle expansion for Enter key
881 | if (e.key === "Enter" && currentNode.hasChildren) {
882 | void toggleNode(currentNode);
883 | }
884 | }
885 | handled = true;
886 | break;
887 | }
888 |
889 | if (handled) {
890 | setFocusedIndex(newIndex);
891 | }
892 | },
893 | [focusedIndex, flatTreeData, toggleNode, setSelectedNodeById],
894 | );
895 |
896 | const itemData = useMemo(() => {
897 | return {
898 | nodes: flatTreeData,
899 | toggleNode,
900 | selectedId: effectiveSelectedId,
901 | itemRenderer,
902 | itemClickExpands,
903 | onItemClick,
904 | onSelection: (node: FlatTreeNode) => setSelectedNodeById(node.key),
905 | focusedIndex,
906 | onKeyDown: handleKeyDown,
907 | treeContainerRef,
908 | iconCollapsed,
909 | iconExpanded,
910 | iconSize,
911 | animateExpand,
912 | expandRotation,
913 | };
914 | }, [
915 | flatTreeData,
916 | toggleNode,
917 | effectiveSelectedId,
918 | itemRenderer,
919 | itemClickExpands,
920 | onItemClick,
921 | setSelectedNodeById,
922 | focusedIndex,
923 | handleKeyDown,
924 | iconCollapsed,
925 | iconExpanded,
926 | iconSize,
927 | animateExpand,
928 | expandRotation,
929 | ]);
930 |
931 | // Shared API implementation to avoid duplication between ref and component APIs
932 | const treeApiMethods = useMemo(() => {
933 | return {
934 | // Expansion methods
935 | expandAll: () => {
936 | const allIds: (string | number)[] = [];
937 | const collectIds = (nodes: TreeNode[]) => {
938 | nodes.forEach((node) => {
939 | allIds.push(node.key);
940 | if (node.children) {
941 | collectIds(node.children);
942 | }
943 | });
944 | };
945 | collectIds(treeData);
946 | setExpandedIds(allIds);
947 | },
948 |
949 | collapseAll: () => {
950 | setExpandedIds([]);
951 | },
952 |
953 | expandToLevel: (level: number) => {
954 | const levelIds: (string | number)[] = [];
955 | const collectIdsToLevel = (nodes: TreeNode[], currentLevel: number = 0) => {
956 | if (currentLevel >= level) return;
957 | nodes.forEach((node) => {
958 | levelIds.push(node.key);
959 | if (node.children && currentLevel < level - 1) {
960 | collectIdsToLevel(node.children, currentLevel + 1);
961 | }
962 | });
963 | };
964 | collectIdsToLevel(treeData);
965 | setExpandedIds(levelIds);
966 | },
967 |
968 | expandNode: async (nodeId: string | number) => {
969 | // nodeId is source ID, which matches TreeNode.key
970 | const wasExpanded = expandedIds.includes(nodeId);
971 | if (!wasExpanded) {
972 | setExpandedIds((prev) => [...prev, nodeId]);
973 |
974 | // Always fire nodeDidExpand event
975 | const node = getNodeById(nodeId);
976 | if (node && onNodeExpanded) {
977 | // Convert TreeNode to FlatTreeNode format for the event
978 | const flatNode: FlatTreeNode = {
979 | ...node,
980 | isExpanded: true,
981 | depth: node.parentIds.length,
982 | hasChildren: !!(node.children && node.children.length > 0),
983 | };
984 | onNodeExpanded(flatNode);
985 | }
986 |
987 | // Check if we need to load children dynamically
988 | if (node && loadChildren) {
989 | // Set loading state
990 | setNodeStates((prev) => new Map(prev).set(nodeId, "loading"));
991 |
992 | try {
993 | // Convert TreeNode to FlatTreeNode format for loadChildren
994 | const flatNode: FlatTreeNode = {
995 | ...node,
996 | isExpanded: true,
997 | depth: node.parentIds.length,
998 | hasChildren: !!(node.children && node.children.length > 0),
999 | };
1000 |
1001 | // Load the children data
1002 | const loadedData = await loadChildren(flatNode);
1003 |
1004 | // Update the tree data with loaded children
1005 | if (loadedData && Array.isArray(loadedData) && loadedData.length > 0) {
1006 | updateInternalData((prevData) => {
1007 | const currentData = prevData ?? data;
1008 |
1009 | if (dataFormat === "flat" && Array.isArray(currentData)) {
1010 | // Replace existing children with newly loaded data
1011 |
1012 | // Remove existing children of this node
1013 | const filteredData = currentData.filter(
1014 | (item) =>
1015 | String(item[fieldConfig.parentField || "parentId"]) !== String(nodeId),
1016 | );
1017 |
1018 | // Add new children
1019 | const newItems = loadedData.map((item) => ({
1020 | ...item,
1021 | [fieldConfig.parentField || "parentId"]: String(nodeId),
1022 | }));
1023 |
1024 | return [...filteredData, ...newItems];
1025 | } else if (dataFormat === "hierarchy" && Array.isArray(currentData)) {
1026 | // For hierarchy format, we need to find the node and add children
1027 | const updateHierarchy = (nodes: any[]): any[] => {
1028 | return nodes.map((n) => {
1029 | if (String(n[fieldConfig.idField || "id"]) === String(nodeId)) {
1030 | return {
1031 | ...n,
1032 | [fieldConfig.childrenField || "children"]: loadedData,
1033 | };
1034 | } else if (n[fieldConfig.childrenField || "children"]) {
1035 | return {
1036 | ...n,
1037 | [fieldConfig.childrenField || "children"]: updateHierarchy(
1038 | n[fieldConfig.childrenField || "children"],
1039 | ),
1040 | };
1041 | }
1042 | return n;
1043 | });
1044 | };
1045 | return updateHierarchy(currentData);
1046 | }
1047 |
1048 | return currentData;
1049 | });
1050 | }
1051 |
1052 | // Set loaded state
1053 | setNodeStates((prev) => new Map(prev).set(nodeId, "loaded"));
1054 | } catch (error) {
1055 | console.error("Error loading tree node data:", error);
1056 | // Set back to unloaded state on error
1057 | setNodeStates((prev) => {
1058 | const newMap = new Map(prev);
1059 | newMap.delete(nodeId);
1060 | return newMap;
1061 | });
1062 | // Collapse the node since loading failed
1063 | setExpandedIds((prev) => prev.filter((id) => id !== nodeId));
1064 | }
1065 | }
1066 | }
1067 | },
1068 |
1069 | collapseNode: (nodeId: string | number) => {
1070 | // nodeId is source ID, which matches TreeNode.key
1071 | const wasExpanded = expandedIds.includes(nodeId);
1072 | if (!wasExpanded) return; // Nothing to collapse
1073 |
1074 | // Find the node and collect all its descendants
1075 | const nodeToCollapse = Object.values(treeItemsById).find(
1076 | (n) => String(n.key) === String(nodeId),
1077 | );
1078 | if (nodeToCollapse) {
1079 | const idsToRemove = new Set<string>();
1080 |
1081 | // Recursively collect all descendant IDs
1082 | const collectDescendants = (treeNode: TreeNode) => {
1083 | idsToRemove.add(String(treeNode.key));
1084 | if (treeNode.children) {
1085 | treeNode.children.forEach((child) => collectDescendants(child));
1086 | }
1087 | };
1088 |
1089 | collectDescendants(nodeToCollapse);
1090 |
1091 | // Remove all descendant IDs from expanded list
1092 | setExpandedIds((prev) => prev.filter((id) => !idsToRemove.has(String(id))));
1093 |
1094 | // Fire nodeDidCollapse event
1095 | if (nodeToCollapse && onNodeCollapsed) {
1096 | // Convert to FlatTreeNode format for the event
1097 | const flatNode: FlatTreeNode = {
1098 | ...nodeToCollapse,
1099 | isExpanded: false,
1100 | depth: nodeToCollapse.parentIds.length,
1101 | hasChildren: !!(nodeToCollapse.children && nodeToCollapse.children.length > 0),
1102 | };
1103 | onNodeCollapsed(flatNode);
1104 | }
1105 | }
1106 | },
1107 |
1108 | // Selection methods
1109 | selectNode: (nodeId: string | number) => {
1110 | // Check if node exists before calling setSelectedNodeById
1111 | if (nodeExists(nodeId)) {
1112 | return setSelectedNodeById(nodeId);
1113 | } else {
1114 | return setSelectedNodeById(undefined);
1115 | }
1116 | },
1117 |
1118 | clearSelection,
1119 |
1120 | // Utility methods
1121 | getNodeById,
1122 |
1123 | getExpandedNodes,
1124 |
1125 | getSelectedNode,
1126 |
1127 | scrollIntoView: (nodeId: string | number, options?: ScrollIntoViewOptions) => {
1128 | // Find the target node
1129 | const targetNode = Object.values(treeItemsById).find(
1130 | (n) => String(n.key) === String(nodeId),
1131 | );
1132 | if (!targetNode) {
1133 | return; // Node not found
1134 | }
1135 |
1136 | // Collect all parent IDs that need to be expanded
1137 | const parentsToExpand: (string | number)[] = [];
1138 | const collectParents = (node: TreeNode) => {
1139 | if (node.parentIds && node.parentIds.length > 0) {
1140 | // Add all parent IDs to expansion list
1141 | parentsToExpand.push(...node.parentIds);
1142 | }
1143 | };
1144 |
1145 | collectParents(targetNode);
1146 |
1147 | // Calculate the new expanded IDs including parents
1148 | const newExpandedIds = [...new Set([...expandedIds, ...parentsToExpand])];
1149 |
1150 | // Expand all parent nodes if they aren't already expanded
1151 | if (parentsToExpand.length > 0) {
1152 | setExpandedIds(newExpandedIds);
1153 | }
1154 |
1155 | // Use setTimeout to ensure DOM is updated after expansion state change
1156 | setTimeout(() => {
1157 | // Generate the flat tree data with the new expanded state to find the correct index
1158 | const updatedFlatTreeData = toFlatTree(
1159 | treeData,
1160 | newExpandedIds,
1161 | fieldConfig.dynamicField,
1162 | nodeStates,
1163 | );
1164 | const nodeIndex = updatedFlatTreeData.findIndex(
1165 | (item) => String(item.key) === String(nodeId),
1166 | );
1167 |
1168 | if (nodeIndex >= 0 && listRef.current) {
1169 | // Scroll to the item using virtua's scrollToIndex method
1170 | listRef.current.scrollToIndex(nodeIndex, { align: "center" });
1171 | }
1172 | }, 0);
1173 | },
1174 |
1175 | scrollToItem: (nodeId: string | number) => {
1176 | // Simple scroll without expanding - just scroll to the item if it's visible
1177 | const nodeIndex = findNodeIndexById(nodeId);
1178 |
1179 | if (nodeIndex >= 0 && listRef.current) {
1180 | listRef.current.scrollToIndex(nodeIndex, { align: "center" });
1181 | }
1182 | },
1183 |
1184 | appendNode: (parentNodeId: string | number | undefined | null, nodeData: any) => {
1185 | // Generate a new ID if not provided
1186 | const nodeId = nodeData[fieldConfig.idField] || Date.now();
1187 |
1188 | // Create the new node with proper field mapping
1189 | const newNode = {
1190 | ...nodeData,
1191 | [fieldConfig.idField]: nodeId,
1192 | };
1193 |
1194 | // For flat data format, set the parent ID field
1195 | if (dataFormat === "flat") {
1196 | newNode[fieldConfig.parentField || "parentId"] = parentNodeId || null;
1197 | }
1198 |
1199 | // Update the internal data state
1200 | updateInternalData((prevData) => {
1201 | const currentData = prevData ?? data;
1202 |
1203 | if (dataFormat === "flat") {
1204 | // For flat format, just append to the array
1205 | return Array.isArray(currentData) ? [...currentData, newNode] : [newNode];
1206 | } else if (dataFormat === "hierarchy") {
1207 | // For hierarchy format, we need to find the parent and add to its children
1208 | const addToHierarchy = (nodes: any[]): any[] => {
1209 | if (!parentNodeId) {
1210 | // Add to root level
1211 | return [...nodes, { ...newNode, [fieldConfig.childrenField || "children"]: [] }];
1212 | }
1213 |
1214 | return nodes.map((node) => {
1215 | if (node[fieldConfig.idField] === parentNodeId) {
1216 | const children = node[fieldConfig.childrenField || "children"] || [];
1217 | return {
1218 | ...node,
1219 | [fieldConfig.childrenField || "children"]: [
1220 | ...children,
1221 | { ...newNode, [fieldConfig.childrenField || "children"]: [] },
1222 | ],
1223 | };
1224 | }
1225 |
1226 | // Recursively check children
1227 | const childrenField = fieldConfig.childrenField || "children";
1228 | if (node[childrenField] && Array.isArray(node[childrenField])) {
1229 | return {
1230 | ...node,
1231 | [childrenField]: addToHierarchy(node[childrenField]),
1232 | };
1233 | }
1234 |
1235 | return node;
1236 | });
1237 | };
1238 |
1239 | if (Array.isArray(currentData)) {
1240 | return addToHierarchy(currentData);
1241 | } else {
1242 | return currentData;
1243 | }
1244 | }
1245 |
1246 | return currentData;
1247 | });
1248 | },
1249 |
1250 | removeNode: (nodeId: string | number) => {
1251 | // Helper function to recursively find and remove a node and its descendants
1252 | const removeFromFlat = (data: any[]): any[] => {
1253 | const nodeIdToRemove = String(nodeId);
1254 | const fieldId = fieldConfig.idField || "id";
1255 | const fieldParent = fieldConfig.parentField || "parentId";
1256 |
1257 | // First, collect all descendant IDs recursively
1258 | const getDescendantIds = (parentId: string): string[] => {
1259 | const descendants: string[] = [];
1260 | for (const item of data) {
1261 | if (String(item[fieldParent]) === parentId) {
1262 | const itemId = String(item[fieldId]);
1263 | descendants.push(itemId);
1264 | descendants.push(...getDescendantIds(itemId));
1265 | }
1266 | }
1267 | return descendants;
1268 | };
1269 |
1270 | // Get all IDs to remove (node itself + all descendants)
1271 | const idsToRemove = new Set([nodeIdToRemove, ...getDescendantIds(nodeIdToRemove)]);
1272 |
1273 | // Filter out all nodes with IDs in the removal set
1274 | return data.filter((item) => !idsToRemove.has(String(item[fieldId])));
1275 | };
1276 |
1277 | const removeFromHierarchy = (nodes: any[]): any[] => {
1278 | const fieldId = fieldConfig.idField || "id";
1279 | const fieldChildren = fieldConfig.childrenField || "children";
1280 |
1281 | return nodes.reduce((acc: any[], node: any) => {
1282 | // If this is the node to remove, don't include it (and its descendants)
1283 | if (String(node[fieldId]) === String(nodeId)) {
1284 | return acc;
1285 | }
1286 |
1287 | // Otherwise, include the node but recursively process its children
1288 | const children = node[fieldChildren];
1289 | if (children && Array.isArray(children)) {
1290 | acc.push({
1291 | ...node,
1292 | [fieldChildren]: removeFromHierarchy(children),
1293 | });
1294 | } else {
1295 | acc.push(node);
1296 | }
1297 |
1298 | return acc;
1299 | }, []);
1300 | };
1301 |
1302 | // Update the internal data state
1303 | setInternalData((prevData) => {
1304 | const currentData = prevData ?? data;
1305 |
1306 | if (dataFormat === "flat" && Array.isArray(currentData)) {
1307 | return removeFromFlat(currentData);
1308 | } else if (dataFormat === "hierarchy" && Array.isArray(currentData)) {
1309 | return removeFromHierarchy(currentData);
1310 | }
1311 |
1312 | return currentData;
1313 | });
1314 | },
1315 |
1316 | removeChildren: (nodeId: string | number) => {
1317 | // Helper function to remove only the children of a node in flat format
1318 | const removeChildrenFromFlat = (data: any[]): any[] => {
1319 | const parentNodeId = String(nodeId);
1320 | const fieldId = fieldConfig.idField || "id";
1321 | const fieldParent = fieldConfig.parentField || "parentId";
1322 |
1323 | // First, collect all descendant IDs recursively (but not the parent node itself)
1324 | const getDescendantIds = (parentId: string): string[] => {
1325 | const descendants: string[] = [];
1326 | for (const item of data) {
1327 | if (String(item[fieldParent]) === parentId) {
1328 | const itemId = String(item[fieldId]);
1329 | descendants.push(itemId);
1330 | descendants.push(...getDescendantIds(itemId));
1331 | }
1332 | }
1333 | return descendants;
1334 | };
1335 |
1336 | // Get all descendant IDs to remove (children and their descendants)
1337 | const idsToRemove = new Set(getDescendantIds(parentNodeId));
1338 |
1339 | // Filter out all descendant nodes but keep the parent node
1340 | return data.filter((item) => !idsToRemove.has(String(item[fieldId])));
1341 | };
1342 |
1343 | const removeChildrenFromHierarchy = (nodes: any[]): any[] => {
1344 | const fieldId = fieldConfig.idField || "id";
1345 | const fieldChildren = fieldConfig.childrenField || "children";
1346 |
1347 | return nodes.map((node: any) => {
1348 | // If this is the target node, remove all its children
1349 | if (String(node[fieldId]) === String(nodeId)) {
1350 | return {
1351 | ...node,
1352 | [fieldChildren]: [],
1353 | };
1354 | }
1355 |
1356 | // Otherwise, recursively process children
1357 | const children = node[fieldChildren];
1358 | if (children && Array.isArray(children)) {
1359 | return {
1360 | ...node,
1361 | [fieldChildren]: removeChildrenFromHierarchy(children),
1362 | };
1363 | }
1364 |
1365 | return node;
1366 | });
1367 | };
1368 |
1369 | // Update the internal data state
1370 | setInternalData((prevData) => {
1371 | const currentData = prevData ?? data;
1372 |
1373 | if (dataFormat === "flat" && Array.isArray(currentData)) {
1374 | return removeChildrenFromFlat(currentData);
1375 | } else if (dataFormat === "hierarchy" && Array.isArray(currentData)) {
1376 | return removeChildrenFromHierarchy(currentData);
1377 | }
1378 |
1379 | return currentData;
1380 | });
1381 | },
1382 |
1383 | insertNodeBefore: (beforeNodeId: string | number, nodeData: any) => {
1384 | // Generate a new ID if not provided
1385 | const nodeId = nodeData[fieldConfig.idField] || Date.now();
1386 |
1387 | // Create the new node with proper field mapping
1388 | const newNode = {
1389 | ...nodeData,
1390 | [fieldConfig.idField]: nodeId,
1391 | };
1392 |
1393 | // Helper function to insert before a node in flat format
1394 | const insertBeforeInFlat = (data: any[]): any[] => {
1395 | const beforeNodeIdStr = String(beforeNodeId);
1396 | const fieldId = fieldConfig.idField || "id";
1397 | const fieldParent = fieldConfig.parentField || "parentId";
1398 |
1399 | // Find the target node to get its parent
1400 | const targetNode = data.find((item) => String(item[fieldId]) === beforeNodeIdStr);
1401 | if (!targetNode) {
1402 | // If target node not found, just append to root level
1403 | return [...data, { ...newNode, [fieldParent]: null }];
1404 | }
1405 |
1406 | // Set the same parent as the target node
1407 | const parentId = targetNode[fieldParent];
1408 | newNode[fieldParent] = parentId;
1409 |
1410 | // Find the index of the target node and insert before it
1411 | const targetIndex = data.findIndex((item) => String(item[fieldId]) === beforeNodeIdStr);
1412 | const result = [...data];
1413 | result.splice(targetIndex, 0, newNode);
1414 | return result;
1415 | };
1416 |
1417 | const insertBeforeInHierarchy = (nodes: any[]): any[] => {
1418 | const beforeNodeIdStr = String(beforeNodeId);
1419 | const fieldId = fieldConfig.idField || "id";
1420 | const fieldChildren = fieldConfig.childrenField || "children";
1421 |
1422 | // Check if the target node is at this level
1423 | const targetIndex = nodes.findIndex((node) => String(node[fieldId]) === beforeNodeIdStr);
1424 | if (targetIndex >= 0) {
1425 | // Insert before the target node at this level
1426 | const result = [...nodes];
1427 | const nodeWithChildren = { ...newNode, [fieldChildren]: [] };
1428 | result.splice(targetIndex, 0, nodeWithChildren);
1429 | return result;
1430 | }
1431 |
1432 | // Otherwise, recursively search in children
1433 | return nodes.map((node: any) => {
1434 | const children = node[fieldChildren];
1435 | if (children && Array.isArray(children)) {
1436 | const updatedChildren = insertBeforeInHierarchy(children);
1437 | if (updatedChildren !== children) {
1438 | return {
1439 | ...node,
1440 | [fieldChildren]: updatedChildren,
1441 | };
1442 | }
1443 | }
1444 | return node;
1445 | });
1446 | };
1447 |
1448 | // Update the internal data state
1449 | setInternalData((prevData) => {
1450 | const currentData = prevData ?? data;
1451 |
1452 | if (dataFormat === "flat" && Array.isArray(currentData)) {
1453 | return insertBeforeInFlat(currentData);
1454 | } else if (dataFormat === "hierarchy" && Array.isArray(currentData)) {
1455 | return insertBeforeInHierarchy(currentData);
1456 | }
1457 |
1458 | return currentData;
1459 | });
1460 | },
1461 |
1462 | insertNodeAfter: (afterNodeId: string | number, nodeData: any) => {
1463 | // Generate a new ID if not provided
1464 | const nodeId = nodeData[fieldConfig.idField] || Date.now();
1465 |
1466 | // Create the new node with proper field mapping
1467 | const newNode = {
1468 | ...nodeData,
1469 | [fieldConfig.idField]: nodeId,
1470 | };
1471 |
1472 | // Helper function to insert after a node in flat format
1473 | const insertAfterInFlat = (data: any[]): any[] => {
1474 | const afterNodeIdStr = String(afterNodeId);
1475 | const fieldId = fieldConfig.idField || "id";
1476 | const fieldParent = fieldConfig.parentField || "parentId";
1477 |
1478 | // Find the target node to get its parent
1479 | const targetNode = data.find((item) => String(item[fieldId]) === afterNodeIdStr);
1480 | if (!targetNode) {
1481 | // If target node not found, just append to root level
1482 | return [...data, { ...newNode, [fieldParent]: null }];
1483 | }
1484 |
1485 | // Set the same parent as the target node
1486 | const parentId = targetNode[fieldParent];
1487 | newNode[fieldParent] = parentId;
1488 |
1489 | // Find the index of the target node and insert after it
1490 | const targetIndex = data.findIndex((item) => String(item[fieldId]) === afterNodeIdStr);
1491 | const result = [...data];
1492 | result.splice(targetIndex + 1, 0, newNode);
1493 | return result;
1494 | };
1495 |
1496 | const insertAfterInHierarchy = (nodes: any[]): any[] => {
1497 | const afterNodeIdStr = String(afterNodeId);
1498 | const fieldId = fieldConfig.idField || "id";
1499 | const fieldChildren = fieldConfig.childrenField || "children";
1500 |
1501 | // Check if the target node is at this level
1502 | const targetIndex = nodes.findIndex((node) => String(node[fieldId]) === afterNodeIdStr);
1503 | if (targetIndex >= 0) {
1504 | // Insert after the target node at this level
1505 | const result = [...nodes];
1506 | const nodeWithChildren = { ...newNode, [fieldChildren]: [] };
1507 | result.splice(targetIndex + 1, 0, nodeWithChildren);
1508 | return result;
1509 | }
1510 |
1511 | // Otherwise, recursively search in children
1512 | return nodes.map((node: any) => {
1513 | const children = node[fieldChildren];
1514 | if (children && Array.isArray(children)) {
1515 | const updatedChildren = insertAfterInHierarchy(children);
1516 | if (updatedChildren !== children) {
1517 | return {
1518 | ...node,
1519 | [fieldChildren]: updatedChildren,
1520 | };
1521 | }
1522 | }
1523 | return node;
1524 | });
1525 | };
1526 |
1527 | // Update the internal data state
1528 | setInternalData((prevData) => {
1529 | const currentData = prevData ?? data;
1530 |
1531 | if (dataFormat === "flat" && Array.isArray(currentData)) {
1532 | return insertAfterInFlat(currentData);
1533 | } else if (dataFormat === "hierarchy" && Array.isArray(currentData)) {
1534 | return insertAfterInHierarchy(currentData);
1535 | }
1536 |
1537 | return currentData;
1538 | });
1539 | },
1540 |
1541 | // Node state management methods
1542 |
1543 | getNodeLoadingState: (nodeId: string | number) => {
1544 | return getNodeState(nodeId);
1545 | },
1546 |
1547 | markNodeLoaded: (nodeId: string | number) => {
1548 | setNodeState(nodeId, "loaded");
1549 | },
1550 |
1551 | markNodeUnloaded: (nodeId: string | number) => {
1552 | setNodeState(nodeId, "unloaded");
1553 | treeApiMethods.collapseNode(nodeId);
1554 | },
1555 | };
1556 | }, [
1557 | treeData,
1558 | treeItemsById,
1559 | expandedIds,
1560 | effectiveSelectedId,
1561 | flatTreeData,
1562 | onNodeExpanded,
1563 | onNodeCollapsed,
1564 | setSelectedNodeById,
1565 | nodeExists,
1566 | fieldConfig,
1567 | dataFormat,
1568 | data,
1569 | setInternalData,
1570 | ]);
1571 |
1572 | // Register component API methods for external access
1573 | useEffect(() => {
1574 | if (registerComponentApi) {
1575 | registerComponentApi(treeApiMethods);
1576 | }
1577 | }, [registerComponentApi, treeApiMethods]);
1578 |
1579 | // Simplified focus management for the tree container
1580 | const handleTreeFocus = useCallback(() => {
1581 | if (flatTreeData.length > 0 && focusedIndex === -1) {
1582 | // Initialize to selected item or first item on focus
1583 | const selectedIndex = findNodeIndexById(effectiveSelectedId);
1584 | const targetIndex = selectedIndex >= 0 ? selectedIndex : 0;
1585 | setFocusedIndex(targetIndex);
1586 | }
1587 | }, [focusedIndex, flatTreeData, effectiveSelectedId]);
1588 |
1589 | const handleTreeBlur = useCallback((e: React.FocusEvent) => {
1590 | // Check if focus is moving to another element within the tree
1591 | const isMovingWithinTree = e.relatedTarget && e.currentTarget.contains(e.relatedTarget as Node);
1592 |
1593 | if (!isMovingWithinTree) {
1594 | // Clear focus when tree loses focus completely
1595 | setFocusedIndex(-1);
1596 | }
1597 | }, []);
1598 |
1599 | return (
1600 | <div
1601 | ref={treeContainerRef}
1602 | className={classnames(styles.wrapper, className)}
1603 | role="tree"
1604 | aria-label="Tree navigation"
1605 | aria-multiselectable="false"
1606 | tabIndex={0}
1607 | onFocus={handleTreeFocus}
1608 | onBlur={handleTreeBlur}
1609 | onKeyDown={handleKeyDown}
1610 | style={{ height: "100%", overflow: "auto" }}
1611 | >
1612 | <Virtualizer ref={listRef}>
1613 | {flatTreeData.map((node, index) => (
1614 | <TreeRow key={node.key} index={index} data={itemData} />
1615 | ))}
1616 | </Virtualizer>
1617 | </div>
1618 | );
1619 | });
1620 |
```