This is page 44 of 143. Use http://codebase.md/xmlui-org/xmlui/xmlui/mockApiDef.js?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── cyan-tools-design.md
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog-optimized.yml
│ ├── deploy-blog-swa.yml
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs-swa.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── an-advanced-codefence.gif
│ │ │ │ ├── an-advanced-codefence.mp4
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── codefence-runner.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ ├── lorem-ipsum.png
│ │ │ │ ├── playground-checkbox-source.png
│ │ │ │ ├── playground.png
│ │ │ │ ├── use-xmlui-mcp-to-find-a-howto.png
│ │ │ │ └── xmlui-demo-gallery.png
│ │ │ ├── introducing-xmlui.md
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ ├── xmlui-playground.md
│ │ │ └── xmlui-powered-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ ├── staticwebapp.config.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ └── PageNotFound.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── blog-theme.ts
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── FancyButton.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── control-cache-invalidation.md
│ │ │ │ ├── debounce-user-input-for-api-calls.md
│ │ │ │ ├── debounce-with-changelistener.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── github.svg
│ │ │ ├── images
│ │ │ │ ├── apiaction-tutorial
│ │ │ │ │ ├── add-success.png
│ │ │ │ │ ├── apiaction-param.png
│ │ │ │ │ ├── change-completed.png
│ │ │ │ │ ├── change-in-progress.png
│ │ │ │ │ ├── confirm-delete.png
│ │ │ │ │ ├── data-error.png
│ │ │ │ │ ├── data-progress.png
│ │ │ │ │ ├── data-success.png
│ │ │ │ │ ├── display-1.png
│ │ │ │ │ ├── item-deleted.png
│ │ │ │ │ ├── item-updated.png
│ │ │ │ │ ├── missing-api-key.png
│ │ │ │ │ ├── new-item-added.png
│ │ │ │ │ └── test-message.png
│ │ │ │ ├── chat-api
│ │ │ │ │ └── domain-model.svg
│ │ │ │ ├── components
│ │ │ │ │ ├── image
│ │ │ │ │ │ └── breakfast.jpg
│ │ │ │ │ ├── markdown
│ │ │ │ │ │ └── colors.png
│ │ │ │ │ └── modal
│ │ │ │ │ ├── deep_link_dialog_1.jpg
│ │ │ │ │ └── deep_link_dialog_2.jpg
│ │ │ │ ├── create-apps
│ │ │ │ │ ├── collapsed-vertical.png
│ │ │ │ │ ├── using-forms-warning-dialog.png
│ │ │ │ │ └── using-forms.png
│ │ │ │ ├── datasource-tutorial
│ │ │ │ │ ├── data-with-header.png
│ │ │ │ │ ├── filtered-data.png
│ │ │ │ │ ├── filtered-items.png
│ │ │ │ │ ├── initial-page-items.png
│ │ │ │ │ ├── list-items.png
│ │ │ │ │ ├── next-page-items.png
│ │ │ │ │ ├── no-data.png
│ │ │ │ │ ├── pagination-1.jpg
│ │ │ │ │ ├── pagination-1.png
│ │ │ │ │ ├── polling-1.png
│ │ │ │ │ ├── refetch-data.png
│ │ │ │ │ ├── slow-loading.png
│ │ │ │ │ ├── test-message.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── unconventional-data.png
│ │ │ │ │ └── unfiltered-items.png
│ │ │ │ ├── flower.jpg
│ │ │ │ ├── get-started
│ │ │ │ │ ├── add-new-contact.png
│ │ │ │ │ ├── app-modified.png
│ │ │ │ │ ├── app-start.png
│ │ │ │ │ ├── app-with-boxes.png
│ │ │ │ │ ├── app-with-toast.png
│ │ │ │ │ ├── boilerplate-structure.png
│ │ │ │ │ ├── cl-initial.png
│ │ │ │ │ ├── cl-start.png
│ │ │ │ │ ├── contact-counts.png
│ │ │ │ │ ├── contact-dialog-title.png
│ │ │ │ │ ├── contact-dialog.png
│ │ │ │ │ ├── contact-menus.png
│ │ │ │ │ ├── contact-predicates.png
│ │ │ │ │ ├── context-menu.png
│ │ │ │ │ ├── dashboard-numbers.png
│ │ │ │ │ ├── default-contact-list.png
│ │ │ │ │ ├── delete-contact.png
│ │ │ │ │ ├── delete-task.png
│ │ │ │ │ ├── detailed-template.png
│ │ │ │ │ ├── edit-contact-details.png
│ │ │ │ │ ├── edited-contact-saved.png
│ │ │ │ │ ├── empty-sections.png
│ │ │ │ │ ├── filter-completed.png
│ │ │ │ │ ├── fullwidth-desktop.png
│ │ │ │ │ ├── fullwidth-mobile.png
│ │ │ │ │ ├── initial-table.png
│ │ │ │ │ ├── items-and-badges.png
│ │ │ │ │ ├── loading-message.png
│ │ │ │ │ ├── new-contact-button.png
│ │ │ │ │ ├── new-contact-saved.png
│ │ │ │ │ ├── no-empty-sections.png
│ │ │ │ │ ├── personal-todo-initial.png
│ │ │ │ │ ├── piechart.png
│ │ │ │ │ ├── review-today.png
│ │ │ │ │ ├── rudimentary-dashboard.png
│ │ │ │ │ ├── section-collapsed.png
│ │ │ │ │ ├── sectioned-items.png
│ │ │ │ │ ├── sections-ordered.png
│ │ │ │ │ ├── spacex-list-with-links.png
│ │ │ │ │ ├── spacex-list.png
│ │ │ │ │ ├── start-personal-todo-1.png
│ │ │ │ │ ├── submit-new-contact.png
│ │ │ │ │ ├── submit-new-task.png
│ │ │ │ │ ├── syntax-highlighting.png
│ │ │ │ │ ├── table-with-badge.png
│ │ │ │ │ ├── template-with-card.png
│ │ │ │ │ ├── test-emulated-api.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── todo-logo.png
│ │ │ │ │ └── xmlui-tools.png
│ │ │ │ ├── HelloApp.png
│ │ │ │ ├── HelloApp2.png
│ │ │ │ ├── logos
│ │ │ │ │ ├── xmlui1.svg
│ │ │ │ │ ├── xmlui2.svg
│ │ │ │ │ ├── xmlui3.svg
│ │ │ │ │ ├── xmlui4.svg
│ │ │ │ │ ├── xmlui5.svg
│ │ │ │ │ ├── xmlui6.svg
│ │ │ │ │ └── xmlui7.svg
│ │ │ │ ├── pdf
│ │ │ │ │ └── dummy-pdf.jpg
│ │ │ │ ├── rendering-engine
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ ├── Component.svg
│ │ │ │ │ ├── CompoundComponent.svg
│ │ │ │ │ ├── RootComponent.svg
│ │ │ │ │ └── tree-with-containers.svg
│ │ │ │ ├── reviewers-guide
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ └── incbutton-in-action.png
│ │ │ │ ├── tools
│ │ │ │ │ └── boilerplate-structure.png
│ │ │ │ ├── try.svg
│ │ │ │ ├── tutorial
│ │ │ │ │ ├── app-chat-history.png
│ │ │ │ │ ├── app-content-placeholder.png
│ │ │ │ │ ├── app-header-and-content.png
│ │ │ │ │ ├── app-links-channel-selected.png
│ │ │ │ │ ├── app-links-click.png
│ │ │ │ │ ├── app-navigation.png
│ │ │ │ │ ├── finished-ex01.png
│ │ │ │ │ ├── finished-ex02.png
│ │ │ │ │ ├── hello.png
│ │ │ │ │ ├── splash-screen-advanced.png
│ │ │ │ │ ├── splash-screen-after-click.png
│ │ │ │ │ ├── splash-screen-centered.png
│ │ │ │ │ ├── splash-screen-events.png
│ │ │ │ │ ├── splash-screen-expression.png
│ │ │ │ │ ├── splash-screen-reuse-after.png
│ │ │ │ │ ├── splash-screen-reuse-before.png
│ │ │ │ │ └── splash-screen.png
│ │ │ │ └── tutorial-01.png
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── Boxes.xmlui
│ │ │ ├── Breadcrumb.xmlui
│ │ │ ├── ChangeLog.xmlui
│ │ │ ├── ColorPalette.xmlui
│ │ │ ├── DocumentLinks.xmlui
│ │ │ ├── DocumentPage.xmlui
│ │ │ ├── DocumentPageNoTOC.xmlui
│ │ │ ├── Icons.xmlui
│ │ │ ├── IncButton.xmlui
│ │ │ ├── IncButton2.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── docs-theme.ts
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── Animation.tsx
│ │ ├── AnimationNative.tsx
│ │ ├── FadeAnimation.tsx
│ │ ├── FadeInAnimation.tsx
│ │ ├── FadeOutAnimation.tsx
│ │ ├── index.tsx
│ │ ├── ScaleAnimation.tsx
│ │ └── SlideInAnimation.tsx
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── HelloWorld.module.scss
│ │ ├── HelloWorld.tsx
│ │ ├── HelloWorldNative.tsx
│ │ └── index.tsx
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── IPhoneFrame.module.scss
│ │ ├── IPhoneFrame.tsx
│ │ ├── MacOSAppFrame.module.scss
│ │ ├── MacOSAppFrame.tsx
│ │ ├── WindowsAppFrame.module.scss
│ │ └── WindowsAppFrame.tsx
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── LazyPdfNative.tsx
│ │ ├── Pdf.module.scss
│ │ └── Pdf.tsx
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── hooks
│ │ │ ├── usePlayground.ts
│ │ │ └── useToast.ts
│ │ ├── index.tsx
│ │ ├── playground
│ │ │ ├── Box.module.scss
│ │ │ ├── Box.tsx
│ │ │ ├── CodeSelector.module.scss
│ │ │ ├── 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.tsx
│ │ │ ├── StandalonePlayground.tsx
│ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ ├── ThemeSwitcher.module.scss
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ └── utils.ts
│ │ ├── providers
│ │ │ ├── Toast.module.scss
│ │ │ └── ToastProvider.tsx
│ │ ├── state
│ │ │ └── store.ts
│ │ ├── themes
│ │ │ └── theme.ts
│ │ └── utils
│ │ └── helpers.ts
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Search.module.scss
│ │ └── Search.tsx
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Spreadsheet.tsx
│ │ └── SpreadsheetNative.tsx
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ └── src
│ ├── Carousel
│ │ ├── Carousel.module.scss
│ │ ├── Carousel.tsx
│ │ ├── CarouselContext.tsx
│ │ └── CarouselNative.tsx
│ ├── FancyButton
│ │ ├── FancyButton.module.scss
│ │ ├── FancyButton.tsx
│ │ └── FancyButton.xmlui
│ ├── Hello
│ │ ├── Hello.tsx
│ │ ├── Hello.xmlui
│ │ └── Hello.xmlui.xs
│ ├── HeroSection
│ │ ├── HeroSection.module.scss
│ │ ├── HeroSection.spec.ts
│ │ ├── HeroSection.tsx
│ │ └── HeroSectionNative.tsx
│ ├── index.tsx
│ ├── ScrollToTop
│ │ ├── ScrollToTop.module.scss
│ │ ├── ScrollToTop.tsx
│ │ └── ScrollToTopNative.tsx
│ └── vite-env.d.ts
├── playwright.config.ts
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.cjs
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── component-metadata.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── theme-variables-refactoring.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── extract-component-metadata.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── generate-metadata-markdown.js
│ ├── get-langserver-metadata.js
│ ├── inline-links.mjs
│ └── README-e2e-summary.md
├── src
│ ├── abstractions
│ │ ├── _conventions.md
│ │ ├── ActionDefs.ts
│ │ ├── AppContextDefs.ts
│ │ ├── ComponentDefs.ts
│ │ ├── ContainerDefs.ts
│ │ ├── ExtensionDefs.ts
│ │ ├── FunctionDefs.ts
│ │ ├── RendererDefs.ts
│ │ ├── scripting
│ │ │ ├── BlockScope.ts
│ │ │ ├── Compilation.ts
│ │ │ ├── LogicalThread.ts
│ │ │ ├── LoopScope.ts
│ │ │ ├── modules.ts
│ │ │ ├── ScriptParserError.ts
│ │ │ ├── Token.ts
│ │ │ ├── TryScope.ts
│ │ │ └── TryScopeExp.ts
│ │ └── ThemingDefs.ts
│ ├── components
│ │ ├── _conventions.md
│ │ ├── abstractions.ts
│ │ ├── Accordion
│ │ │ ├── Accordion.md
│ │ │ ├── Accordion.module.scss
│ │ │ ├── Accordion.spec.ts
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── AccordionItem.tsx
│ │ │ ├── AccordionItemNative.tsx
│ │ │ └── AccordionNative.tsx
│ │ ├── Animation
│ │ │ └── AnimationNative.tsx
│ │ ├── APICall
│ │ │ ├── APICall.md
│ │ │ ├── APICall.spec.ts
│ │ │ ├── APICall.tsx
│ │ │ └── APICallNative.tsx
│ │ ├── App
│ │ │ ├── App.md
│ │ │ ├── App.module.scss
│ │ │ ├── App.spec.ts
│ │ │ ├── App.tsx
│ │ │ ├── AppLayoutContext.ts
│ │ │ ├── AppNative.tsx
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── AppHeader
│ │ │ ├── AppHeader.md
│ │ │ ├── AppHeader.module.scss
│ │ │ ├── AppHeader.spec.ts
│ │ │ ├── AppHeader.tsx
│ │ │ └── AppHeaderNative.tsx
│ │ ├── AppState
│ │ │ ├── AppState.md
│ │ │ ├── AppState.spec.ts
│ │ │ ├── AppState.tsx
│ │ │ └── AppStateNative.tsx
│ │ ├── AutoComplete
│ │ │ ├── AutoComplete.md
│ │ │ ├── AutoComplete.module.scss
│ │ │ ├── AutoComplete.spec.ts
│ │ │ ├── AutoComplete.tsx
│ │ │ ├── AutoCompleteContext.tsx
│ │ │ └── AutoCompleteNative.tsx
│ │ ├── Avatar
│ │ │ ├── Avatar.md
│ │ │ ├── Avatar.module.scss
│ │ │ ├── Avatar.spec.ts
│ │ │ ├── Avatar.tsx
│ │ │ └── AvatarNative.tsx
│ │ ├── Backdrop
│ │ │ ├── Backdrop.md
│ │ │ ├── Backdrop.module.scss
│ │ │ ├── Backdrop.spec.ts
│ │ │ ├── Backdrop.tsx
│ │ │ └── BackdropNative.tsx
│ │ ├── Badge
│ │ │ ├── Badge.md
│ │ │ ├── Badge.module.scss
│ │ │ ├── Badge.spec.ts
│ │ │ ├── Badge.tsx
│ │ │ └── BadgeNative.tsx
│ │ ├── Bookmark
│ │ │ ├── Bookmark.md
│ │ │ ├── Bookmark.module.scss
│ │ │ ├── Bookmark.spec.ts
│ │ │ ├── Bookmark.tsx
│ │ │ └── BookmarkNative.tsx
│ │ ├── Breakout
│ │ │ ├── Breakout.module.scss
│ │ │ ├── Breakout.spec.ts
│ │ │ ├── Breakout.tsx
│ │ │ └── BreakoutNative.tsx
│ │ ├── Button
│ │ │ ├── Button-style.spec.ts
│ │ │ ├── Button.md
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.spec.ts
│ │ │ ├── Button.tsx
│ │ │ └── ButtonNative.tsx
│ │ ├── Card
│ │ │ ├── Card.md
│ │ │ ├── Card.module.scss
│ │ │ ├── Card.spec.ts
│ │ │ ├── Card.tsx
│ │ │ └── CardNative.tsx
│ │ ├── Carousel
│ │ │ ├── Carousel.md
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.spec.ts
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ ├── CarouselItem.tsx
│ │ │ ├── CarouselItemNative.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── ChangeListener
│ │ │ ├── ChangeListener.md
│ │ │ ├── ChangeListener.spec.ts
│ │ │ ├── ChangeListener.tsx
│ │ │ └── ChangeListenerNative.tsx
│ │ ├── chart-color-schemes.ts
│ │ ├── Charts
│ │ │ ├── AreaChart
│ │ │ │ ├── AreaChart.md
│ │ │ │ ├── AreaChart.spec.ts
│ │ │ │ ├── AreaChart.tsx
│ │ │ │ └── AreaChartNative.tsx
│ │ │ ├── BarChart
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── BarChart.module.scss
│ │ │ │ ├── BarChart.spec.ts
│ │ │ │ ├── BarChart.tsx
│ │ │ │ └── BarChartNative.tsx
│ │ │ ├── DonutChart
│ │ │ │ ├── DonutChart.spec.ts
│ │ │ │ └── DonutChart.tsx
│ │ │ ├── LabelList
│ │ │ │ ├── LabelList.module.scss
│ │ │ │ ├── LabelList.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ └── ContentSeparatorNative.tsx
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.js
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── index.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── index.ts
│ │ ├── infrastructure
│ │ │ ├── index.html
│ │ │ ├── main.tsx
│ │ │ ├── public
│ │ │ │ ├── mockServiceWorker.js
│ │ │ │ ├── resources
│ │ │ │ │ ├── bell.svg
│ │ │ │ │ ├── box.svg
│ │ │ │ │ ├── doc.svg
│ │ │ │ │ ├── eye.svg
│ │ │ │ │ ├── flower-640x480.jpg
│ │ │ │ │ ├── sun.svg
│ │ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ │ └── txt.svg
│ │ │ │ └── serve.json
│ │ │ └── TestBed.tsx
│ │ └── themed-app-test-helpers.ts
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.json
├── tsdown.config.ts
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/docs/content/components/Slider.md:
--------------------------------------------------------------------------------
```markdown
# Slider [#slider]
`Slider` provides an interactive control for selecting numeric values within a defined range, supporting both single value selection and range selection with multiple thumbs. It offers precise control through customizable steps and visual feedback with formatted value display.
Hover over the component to see the tooltip with the current value. On mobile, tap the thumb to see the tooltip.
**Key features:**
- **Range selection**: Single value or dual-thumb range selection with configurable minimum separation
- **Step control**: Precise incremental selection with customizable step values
- **Value formatting**: Custom display formatting for current values and visual feedback
## Properties [#properties]
### `autoFocus` (default: false) [#autofocus-default-false]
If this property is set to `true`, the component gets the focus automatically when displayed.
### `enabled` (default: true) [#enabled-default-true]
This boolean property value indicates whether the component responds to user events (`true`) or not (`false`).
### `initialValue` [#initialvalue]
This property sets the component's initial value.
```xmlui-pg
<Slider initialValue="5" />
```
### `maxValue` (default: 10) [#maxvalue-default-10]
This property specifies the maximum value of the allowed input range.
```xmlui-pg
<Slider maxValue="30" />
```
### `minStepsBetweenThumbs` (default: 1) [#minstepsbetweenthumbs-default-1]
This property sets the minimum number of steps required between multiple thumbs on the slider, ensuring they maintain a specified distance.
### `minValue` (default: 0) [#minvalue-default-0]
This property specifies the minimum value of the allowed input range.
```xmlui-pg
<Slider minValue="10" />
```
### `rangeStyle` [#rangestyle]
This optional property allows you to apply custom styles to the range element of the slider.
### `readOnly` (default: false) [#readonly-default-false]
Set this property to `true` to disallow changing the component value.
### `required` (default: false) [#required-default-false]
Set this property to `true` to indicate it must have a value before submitting the containing form.
### `showValues` (default: true) [#showvalues-default-true]
This property controls whether the slider shows the current values of the thumbs.
### `step` (default: 1) [#step-default-1]
This property defines the increment value for the slider, determining the allowed intervals between selectable values.
### `thumbStyle` [#thumbstyle]
This optional property allows you to apply custom styles to the thumb elements of the slider.
### `validationStatus` (default: "none") [#validationstatus-default-none]
This property allows you to set the validation status of the input component.
Available values:
| Value | Description |
| --- | --- |
| `valid` | Visual indicator for an input that is accepted |
| `warning` | Visual indicator for an input that produced a warning |
| `error` | Visual indicator for an input that produced an error |
### `valueFormat` (default: "(value) => value.toString()") [#valueformat-default-value-value-tostring]
This property allows you to customize how the values are displayed.
## Events [#events]
### `didChange` [#didchange]
This event is triggered when value of Slider has changed.
### `gotFocus` [#gotfocus]
This event is triggered when the Slider has received the focus.
### `lostFocus` [#lostfocus]
This event is triggered when the Slider has lost the focus.
## Exposed Methods [#exposed-methods]
### `focus` [#focus]
This method sets the focus on the slider component.
**Signature**: `focus(): void`
### `setValue` [#setvalue]
This API sets the value of the `Slider`. You can use it to programmatically change the value.
**Signature**: `setValue(value: number | [number, number] | undefined): void`
- `value`: The new value to set. Can be a single value or an array of values for range sliders.
### `value` [#value]
This API retrieves the current value of the `Slider`. You can use it to get the value programmatically.
**Signature**: `get value(): number | [number, number] | undefined`
## Styling [#styling]
### Theme Variables [#theme-variables]
| Variable | Default Value (Light) | Default Value (Dark) |
| --- | --- | --- |
| [backgroundColor](../styles-and-themes/common-units/#color)-range-Slider | $color-primary | $color-primary |
| [backgroundColor](../styles-and-themes/common-units/#color)-range-Slider | $color-primary | $color-primary |
| [backgroundColor](../styles-and-themes/common-units/#color)-range-Slider--disabled | $color-surface-400 | $color-surface-800 |
| [backgroundColor](../styles-and-themes/common-units/#color)-range-Slider--disabled | $color-surface-400 | $color-surface-800 |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider | $color-primary-500 | $color-primary-400 |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider | $color-primary-500 | $color-primary-400 |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider--active | $color-primary-400 | $color-primary-400 |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider--active | $color-primary-400 | $color-primary-400 |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider--focus | $color-primary | $color-primary |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider--focus | $color-primary | $color-primary |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider--hover | $color-primary | $color-primary |
| [backgroundColor](../styles-and-themes/common-units/#color)-thumb-Slider--hover | $color-primary | $color-primary |
| [backgroundColor](../styles-and-themes/common-units/#color)-track-Slider | $color-surface-200 | $color-surface-200 |
| [backgroundColor](../styles-and-themes/common-units/#color)-track-Slider | $color-surface-200 | $color-surface-200 |
| [backgroundColor](../styles-and-themes/common-units/#color)-track-Slider--disabled | $color-surface-300 | $color-surface-600 |
| [backgroundColor](../styles-and-themes/common-units/#color)-track-Slider--disabled | $color-surface-300 | $color-surface-600 |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--default | transparent | transparent |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--default | transparent | transparent |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--default--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--default--hover | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--error | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--error--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--error--hover | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--success | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--success--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--success--hover | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--warning | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--warning--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-Slider--warning--hover | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-thumb-Slider | $color-surface-50 | $color-surface-950 |
| [borderColor](../styles-and-themes/common-units/#color)-thumb-Slider | $color-surface-50 | $color-surface-950 |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-Slider--default | $borderRadius | $borderRadius |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-Slider--default | $borderRadius | $borderRadius |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-Slider--error | *none* | *none* |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-Slider--success | *none* | *none* |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-Slider--warning | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-Slider--default | solid | solid |
| [borderStyle](../styles-and-themes/common-units/#border-style)-Slider--default | solid | solid |
| [borderStyle](../styles-and-themes/common-units/#border-style)-Slider--error | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-Slider--success | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-Slider--warning | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-thumb-Slider | solid | solid |
| [borderStyle](../styles-and-themes/common-units/#border-style)-thumb-Slider | solid | solid |
| [borderWidth](../styles-and-themes/common-units/#size)-Slider--default | 0 | 0 |
| [borderWidth](../styles-and-themes/common-units/#size)-Slider--default | 0 | 0 |
| [borderWidth](../styles-and-themes/common-units/#size)-Slider--error | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-Slider--success | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-Slider--warning | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-thumb-Slider | 2px | 2px |
| [borderWidth](../styles-and-themes/common-units/#size)-thumb-Slider | 2px | 2px |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--default | none | none |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--default | none | none |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--default--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--default--hover | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--error | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--error--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--error--hover | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--success | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--success--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--success--hover | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--warning | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--warning--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-Slider--warning--hover | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-thumb-Slider | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-thumb-Slider--active | 0 0 0 6px rgb(from $color-primary r g b / 0.4) | 0 0 0 6px rgb(from $color-primary r g b / 0.4) |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-thumb-Slider--active | 0 0 0 6px rgb(from $color-primary r g b / 0.4) | 0 0 0 6px rgb(from $color-primary r g b / 0.4) |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-thumb-Slider--focus | 0 0 0 6px rgb(from $color-primary r g b / 0.4) | 0 0 0 6px rgb(from $color-primary r g b / 0.4) |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-thumb-Slider--focus | 0 0 0 6px rgb(from $color-primary r g b / 0.4) | 0 0 0 6px rgb(from $color-primary r g b / 0.4) |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-thumb-Slider--hover | 0 0 0 6px rgb(from $color-primary r g b / 0.4) | 0 0 0 6px rgb(from $color-primary r g b / 0.4) |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-thumb-Slider--hover | 0 0 0 6px rgb(from $color-primary r g b / 0.4) | 0 0 0 6px rgb(from $color-primary r g b / 0.4) |
```
--------------------------------------------------------------------------------
/xmlui/bin/build.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { cp, mkdir, rm, writeFile } from "fs/promises";
import * as dotenv from "dotenv";
import { existsSync } from "fs";
import * as glob from "glob";
import type { InlineConfig } from "vite";
import { build as viteBuild } from "vite";
import { getViteConfig } from "./viteConfig";
import * as fs from "node:fs";
type ApiInterceptorDefinition = any;
type CompoundComponentDef = any;
type ThemeDefinition = any;
// --- Set up the configuration
dotenv.config({ path: `${process.cwd()}/.env` });
dotenv.config({ path: `${process.cwd()}/.env.local`, override: true });
type StandaloneJsonConfig = {
name: string;
appGlobals?: Record<string, any>;
entryPoint?: string;
components?: string[];
themes?: string[];
defaultTheme?: string;
resources?: Record<string, string>;
apiInterceptor?: ApiInterceptorDefinition;
resourceMap?: Record<string, string>;
};
async function getExportedJsObjects<T>(path: string): Promise<Array<T>> {
return await new Promise((resolve, reject) => {
glob.glob(`${process.cwd()}/${path}`, (err: any, matches = []) => {
const modules = Promise.all(
matches.map(async (file: string) => {
return (await import(file)).default;
})
);
resolve(modules);
});
});
}
async function convertResourcesDir(distRoot: string, flatDist: boolean, filePrefix: string) {
if (!flatDist) {
return undefined;
}
distRoot = distRoot.replaceAll("\\", "/");
const resourcesDir = `${distRoot}/resources`;
if (!existsSync(resourcesDir)) {
return undefined;
}
const files = await new Promise<string[]>((resolve, reject) => {
glob.glob(`${resourcesDir}/**/*`, (err: any, matches = []) => {
resolve(matches);
});
});
const ret: Record<string, string> = {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (fs.statSync(file).isDirectory()) {
continue;
}
const relativePath = file.replace(`${distRoot}/`, "");
const convertedResource = `${filePrefix}${relativePath.replaceAll("/", "_")}`;
await cp(file, `${distRoot}/${convertedResource}`);
ret[relativePath] = convertedResource;
}
return ret;
}
export function removeLeadingSlashForPath(path: string): string {
if (path.startsWith("/")) {
return path.substring(1);
}
return path;
}
type UEBuildOptions = {
buildMode?: "CONFIG_ONLY" | "INLINE_ALL" | "ALL";
flatDist?: boolean;
withMock?: boolean;
withHostingMetaFiles?: boolean;
withRelativeRoot?: boolean;
};
export const build = async ({
buildMode = "CONFIG_ONLY",
flatDist = false,
withMock = false,
withHostingMetaFiles = false,
withRelativeRoot = false,
}: UEBuildOptions) => {
const flatDistUiPrefix = "ui_";
console.log("Building with options:", {
buildMode,
flatDist,
withMock,
withHostingMetaFiles,
withRelativeRoot,
});
await viteBuild({
...(await getViteConfig({
flatDist,
withRelativeRoot,
flatDistUiPrefix,
})),
define: {
"process.env.VITE_BUILD_MODE": JSON.stringify(buildMode),
"process.env.VITE_DEV_MODE": false,
"process.env.VITE_MOCK_ENABLED": withMock,
"process.env.VITE_APP_VERSION": JSON.stringify(process.env.VITE_APP_VERSION),
"process.env.VITE_USED_COMPONENTS_App": JSON.stringify(process.env.VITE_USED_COMPONENTS_App),
"process.env.VITE_USED_COMPONENTS_Chart": JSON.stringify(process.env.VITE_USED_COMPONENTS_Chart),
"process.env.VITE_USED_COMPONENTS_AppHeader": JSON.stringify(process.env.VITE_USED_COMPONENTS_AppHeader),
"process.env.VITE_USED_COMPONENTS_Stack": JSON.stringify(process.env.VITE_USED_COMPONENTS_Stack),
"process.env.VITE_USED_COMPONENTS_Link": JSON.stringify(process.env.VITE_USED_COMPONENTS_Link),
"process.env.VITE_USED_COMPONENTS_Text": JSON.stringify(process.env.VITE_USED_COMPONENTS_Text),
"process.env.VITE_USED_COMPONENTS_Button": JSON.stringify(process.env.VITE_USED_COMPONENTS_Button),
"process.env.VITE_USED_COMPONENTS_Card": JSON.stringify(process.env.VITE_USED_COMPONENTS_Card),
"process.env.VITE_USED_COMPONENTS_Image": JSON.stringify(process.env.VITE_USED_COMPONENTS_Image),
"process.env.VITE_USED_COMPONENTS_Footer": JSON.stringify(process.env.VITE_USED_COMPONENTS_Footer),
"process.env.VITE_USED_COMPONENTS_SpaceFiller": JSON.stringify(process.env.VITE_USED_COMPONENTS_SpaceFiller),
"process.env.VITE_USED_COMPONENTS_Pdf": JSON.stringify(process.env.VITE_USED_COMPONENTS_Pdf),
"process.env.VITE_USED_COMPONENTS_Textarea": JSON.stringify(process.env.VITE_USED_COMPONENTS_Textarea),
"process.env.VITE_USED_COMPONENTS_Logo": JSON.stringify(process.env.VITE_USED_COMPONENTS_Logo),
"process.env.VITE_USED_COMPONENTS_NavLink": JSON.stringify(process.env.VITE_USED_COMPONENTS_NavLink),
"process.env.VITE_USED_COMPONENTS_NavGroup": JSON.stringify(process.env.VITE_USED_COMPONENTS_NavGroup),
"process.env.VITE_USED_COMPONENTS_Form": JSON.stringify(process.env.VITE_USED_COMPONENTS_Form),
"process.env.VITE_USED_COMPONENTS_Tree": JSON.stringify(process.env.VITE_USED_COMPONENTS_Tree),
"process.env.VITE_USED_COMPONENTS_Checkbox": JSON.stringify(process.env.VITE_USED_COMPONENTS_Checkbox),
"process.env.VITE_USED_COMPONENTS_Switch": JSON.stringify(process.env.VITE_USED_COMPONENTS_Switch),
"process.env.VITE_USED_COMPONENTS_DotMenu": JSON.stringify(process.env.VITE_USED_COMPONENTS_DotMenu),
"process.env.VITE_USED_COMPONENTS_Heading": JSON.stringify(process.env.VITE_USED_COMPONENTS_Heading),
"process.env.VITE_USED_COMPONENTS_Fragment": JSON.stringify(process.env.VITE_USED_COMPONENTS_Fragment),
"process.env.VITE_USED_COMPONENTS_Table": JSON.stringify(process.env.VITE_USED_COMPONENTS_Table),
"process.env.VITE_USED_COMPONENTS_List": JSON.stringify(process.env.VITE_USED_COMPONENTS_List),
"process.env.VITE_USED_COMPONENTS_XmluiCodeHightlighter": JSON.stringify(process.env.VITE_USED_COMPONENTS_XmluiCodeHightlighter),
"process.env.VITE_USED_COMPONENTS_Charts": JSON.stringify(process.env.VITE_USED_COMPONENTS_Charts),
"process.env.VITE_INCLUDE_HTML_COMPONENTS": JSON.stringify(process.env.VITE_INCLUDE_HTML_COMPONENTS),
"process.env.VITE_USED_COMPONENTS_DatePicker": JSON.stringify(process.env.VITE_USED_COMPONENTS_DatePicker),
"process.env.VITE_USED_COMPONENTS_NavPanel": JSON.stringify(process.env.VITE_USED_COMPONENTS_NavPanel),
"process.env.VITE_USED_COMPONENTS_Pages": JSON.stringify(process.env.VITE_USED_COMPONENTS_Pages),
"process.env.VITE_USED_COMPONENTS_StickyBox": JSON.stringify(process.env.VITE_USED_COMPONENTS_StickyBox),
"process.env.VITE_USED_COMPONENTS_Badge": JSON.stringify(process.env.VITE_USED_COMPONENTS_Badge),
"process.env.VITE_USED_COMPONENTS_Avatar": JSON.stringify(process.env.VITE_USED_COMPONENTS_Avatar),
"process.env.VITE_USED_COMPONENTS_ContentSeparator": JSON.stringify(
process.env.VITE_USED_COMPONENTS_ContentSeparator
),
"process.env.VITE_USED_COMPONENTS_FlowLayout": JSON.stringify(process.env.VITE_USED_COMPONENTS_FlowLayout),
"process.env.VITE_USED_COMPONENTS_ModalDialog": JSON.stringify(process.env.VITE_USED_COMPONENTS_ModalDialog),
"process.env.VITE_USED_COMPONENTS_NoResult": JSON.stringify(process.env.VITE_USED_COMPONENTS_NoResult),
"process.env.VITE_USED_COMPONENTS_Option": JSON.stringify(process.env.VITE_USED_COMPONENTS_Option),
"process.env.VITE_USED_COMPONENTS_FileUploadDropZone": JSON.stringify(
process.env.VITE_USED_COMPONENTS_FileUploadDropZone
),
"process.env.VITE_USED_COMPONENTS_Icon": JSON.stringify(process.env.VITE_USED_COMPONENTS_Icon),
"process.env.VITE_USED_COMPONENTS_Items": JSON.stringify(process.env.VITE_USED_COMPONENTS_Items),
"process.env.VITE_USED_COMPONENTS_SelectionStore": JSON.stringify(
process.env.VITE_USED_COMPONENTS_SelectionStore
),
"process.env.VITE_INCLUDE_REST_COMPONENTS": JSON.stringify(process.env.VITE_INCLUDE_REST_COMPONENTS),
"process.env.VITE_USED_COMPONENTS_EmojiSelector": JSON.stringify(process.env.VITE_USED_COMPONENTS_EmojiSelector),
},
} as InlineConfig);
if (buildMode === "INLINE_ALL") {
return;
}
const distPath = "/dist";
const themesFolder = flatDist ? "" : "themes";
const componentsFolder = flatDist ? "" : "components";
const themesFolderPath = flatDist ? distPath : `${distPath}/${themesFolder}`;
const componentsFolderPath = flatDist ? distPath : `${distPath}/${componentsFolder}`;
function getThemeFileName(theme: ThemeDefinition) {
return flatDist ? `${flatDistUiPrefix}theme_${theme.id}` : theme.id;
}
function getComponentFileName(component: CompoundComponentDef) {
return flatDist ? `${flatDistUiPrefix}component_${component.name}` : component.name;
}
function getEntrypointFileName() {
return flatDist ? `${flatDistUiPrefix}entrypoint` : `app`;
}
if (buildMode === "CONFIG_ONLY") {
let appDef;
try {
appDef = (await import(process.cwd() + "/src/config")).default;
} catch (e) {
console.log("couldn't find config");
}
const themePaths: string[] = [];
const themes = await getExportedJsObjects<ThemeDefinition>("/src/themes/**.*");
const fullDistPath = `${process.cwd()}${distPath}`;
if (themes) {
for (const theme of themes) {
if (!existsSync(process.cwd() + themesFolderPath)) {
await mkdir(process.cwd() + themesFolderPath, { recursive: true });
}
await writeFile(
`${process.cwd()}${themesFolderPath}/${getThemeFileName(theme)}.json`,
JSON.stringify(theme, null, 4)
);
themePaths.push(removeLeadingSlashForPath(`${themesFolder}/${getThemeFileName(theme)}.json`));
}
}
const configJson: StandaloneJsonConfig = {
...appDef,
resourceMap: await convertResourcesDir(fullDistPath, flatDist, flatDistUiPrefix),
themes: themePaths,
};
delete configJson["components"];
delete configJson["entryPoint"];
if (!withMock) {
delete configJson.apiInterceptor;
}
await writeFile(`${process.cwd()}${distPath}/config.json`, JSON.stringify(configJson, null, 4));
} else if (buildMode === "ALL") {
// const componentPaths: string[] = [];
// const components = await getExportedJsObjects<CompoundComponentDef>("/src/components/**.*");
// if (components) {
// for (const component of components) {
// if (!existsSync(`${process.cwd()}${componentsFolderPath}`)) {
// await mkdir(`${process.cwd()}${componentsFolderPath}`, { recursive: true });
// }
// await writeFile(
// `${process.cwd()}${componentsFolderPath}/${getComponentFileName(component)}.json`,
// JSON.stringify(component, null, 4)
// );
// componentPaths.push(removeLeadingSlashForPath(`${componentsFolder}/${getComponentFileName(component)}.json`));
// }
// }
//
// await writeFile(
// `${process.cwd()}${distPath}/${getEntrypointFileName()}.json`,
// JSON.stringify(appDef.entryPoint, null, 4)
// );
//
// configJson = {
// ...configJson,
// entryPoint: `${getEntrypointFileName()}.json`,
// components: componentPaths,
// };
}
//TODO temp, we should enforce user(who writes the metadata)-added-resources to be put in the resources folder
if (flatDist) {
await rm(`${process.cwd()}${distPath}/resources`, { recursive: true, force: true });
}
if (!withMock) {
try {
await rm(`${process.cwd()}${distPath}/mockServiceWorker.js`);
} catch (ignored) {}
}
if (!withHostingMetaFiles) {
try {
await rm(`${process.cwd()}${distPath}/serve.json`);
} catch (ignored) {}
try {
await rm(`${process.cwd()}${distPath}/web.config`);
} catch (ignored) {}
try {
await rm(`${process.cwd()}${distPath}/_redirects`);
} catch (ignored) {}
}
};
```
--------------------------------------------------------------------------------
/xmlui/src/components/Text/TextNative.tsx:
--------------------------------------------------------------------------------
```typescript
import type { CSSProperties } from "react";
import type React from "react";
import { forwardRef, useMemo, useRef, useCallback, useEffect, memo } from "react";
import { composeRefs } from "@radix-ui/react-compose-refs";
import classnames from "classnames";
import styles from "./Text.module.scss";
import { getMaxLinesStyle } from "../../components-core/utils/css-utils";
import {
type BreakMode,
type OverflowMode,
type TextVariant,
TextVariantElement,
} from "../abstractions";
import type { RegisterComponentApiFn } from "../..";
import { useComponentStyle } from "../../components-core/theming/StyleContext";
import { EMPTY_OBJECT } from "../../components-core/constants";
import { toCssVar } from "../../components-core/theming/layout-resolver";
// =============================================================================
// Custom Variant CSS Cache Infrastructure
// =============================================================================
/**
* Cached CSS information for a custom variant.
*/
interface CustomVariantCacheEntry {
/** The generated CSS class name for this variant */
className: string;
/** The CSS text content that defines the styles for this variant */
cssText: string;
/** Timestamp when this entry was created (for debugging/cleanup) */
createdAt: number;
}
/**
* Global cache that stores custom variant CSS styles.
* Key: variant value (string)
*
* This cache ensures the same variant value always generates the same CSS.
*/
const customVariantCache = new Map<string, CustomVariantCacheEntry>();
/**
* Retrieves a cached custom variant entry if it exists.
*/
export function getCustomVariantCache(
variant: string,
): CustomVariantCacheEntry | undefined {
return customVariantCache.get(variant);
}
/**
* Stores a custom variant entry in the cache.
*/
export function setCustomVariantCache(
variant: string,
entry: Omit<CustomVariantCacheEntry, "createdAt">,
): void {
customVariantCache.set(variant, {
...entry,
createdAt: Date.now(),
});
}
/**
* Checks if a custom variant is already cached.
*/
export function hasCustomVariantCache(variant: string): boolean {
return customVariantCache.has(variant);
}
/**
* Clears the entire custom variant cache.
* Useful for testing or full app resets.
*/
export function clearCustomVariantCache(): void {
customVariantCache.clear();
}
/**
* Gets cache statistics for debugging.
*/
export function getCustomVariantCacheStats() {
return {
totalEntries: customVariantCache.size,
entries: Array.from(customVariantCache.entries()).map(([key, entry]) => ({
key,
className: entry.className,
createdAt: new Date(entry.createdAt).toISOString(),
})),
};
}
// =============================================================================
// Component Definition
// =============================================================================
type TextProps = {
uid?: string;
children?: React.ReactNode;
variant?: TextVariant;
maxLines?: number;
preserveLinebreaks?: boolean;
ellipses?: boolean;
overflowMode?: OverflowMode;
breakMode?: BreakMode;
style?: CSSProperties;
className?: string;
registerComponentApi?: RegisterComponentApiFn;
[variantSpecificProps: string]: any;
};
export const defaultProps = {
maxLines: 0,
preserveLinebreaks: false,
ellipses: true,
overflowMode: undefined as OverflowMode | undefined,
breakMode: "normal" as BreakMode | undefined,
};
export const Text = forwardRef(function Text(
{
uid,
variant,
maxLines = defaultProps.maxLines,
style,
className,
children,
preserveLinebreaks = defaultProps.preserveLinebreaks,
ellipses = defaultProps.ellipses,
overflowMode = defaultProps.overflowMode,
breakMode = defaultProps.breakMode,
registerComponentApi,
...variantSpecificProps
}: TextProps,
forwardedRef,
) {
const innerRef = useRef<HTMLElement>(null);
const ref = forwardedRef ? composeRefs(innerRef, forwardedRef) : innerRef;
// Implement hasOverflow function
const hasOverflow = useCallback((): boolean => {
const element = innerRef.current;
if (!element) return false;
// Check both horizontal and vertical overflow
const hasHorizontalOverflow = element.scrollWidth > element.clientWidth;
const hasVerticalOverflow = element.scrollHeight > element.clientHeight;
return hasHorizontalOverflow || hasVerticalOverflow;
}, []);
// Register API with XMLUI if provided
useEffect(() => {
if (registerComponentApi) {
registerComponentApi({ hasOverflow });
}
}, [registerComponentApi, hasOverflow]);
// NOTE: This is to accept syntax highlight classes coming from shiki
// classes need not to be added to the rendered html element, so we remove them from props
const { syntaxHighlightClasses, ...restVariantSpecificProps } = variantSpecificProps;
const Element = useMemo(() => {
if (!variant || !TextVariantElement[variant]) return "div";
return TextVariantElement[variant];
}, [variant]);
// Custom variant CSS generation
// Following React hook rules: hooks must be called unconditionally
// We always call useComponentStyle, passing empty object for known variants
const isCustomVariant = useMemo(() => {
return variant && !TextVariantElement[variant];
}, [variant]);
// Always call useComponentStyle (React hook rule: no conditional hooks)
// For now, pass empty object; later this will contain assembled CSS properties
const variantSpec = useMemo(
() => {
if (!isCustomVariant) return EMPTY_OBJECT;
const subject = `-Text-${variant}`;
const cssInput = {
color: toCssVar(`$textColor${subject}`),
"font-family": toCssVar(`$fontFamily${subject}`),
"font-size": toCssVar(`$fontSize${subject}`),
"font-style": toCssVar(`$fontStyle${subject}`),
"font-weight": toCssVar(`$fontWeight${subject}`),
"font-stretch": toCssVar(`$fontStretch${subject}`),
"text-decoration-line": toCssVar(`$textDecorationLine${subject}`),
"text-decoration-color": toCssVar(`$textDecorationColor${subject}`),
"text-decoration-style": toCssVar(`$textDecorationStyle${subject}`),
"text-decoration-thickness": toCssVar(`$textDecorationThickness${subject}`),
"text-underline-offset": toCssVar(`$textUnderlineOffset${subject}`),
"line-height": toCssVar(`$lineHeight${subject}`),
"background-color": toCssVar(`$backgroundColor${subject}`),
"text-transform": toCssVar(`$textTransform${subject}`),
"letter-spacing": toCssVar(`$letterSpacing${subject}`),
"word-spacing": toCssVar(`$wordSpacing${subject}`),
"text-shadow": toCssVar(`$textShadow${subject}`),
"text-indent": toCssVar(`$textIndent${subject}`),
"text-align": toCssVar(`$textAlign${subject}`),
"text-align-last": toCssVar(`$textAlignLast${subject}`),
"word-break": toCssVar(`$wordBreak${subject}`),
"word-wrap": toCssVar(`$wordWrap${subject}`),
direction: toCssVar(`$direction${subject}`),
"writing-mode": toCssVar(`$writingMode${subject}`),
"line-break": toCssVar(`$lineBreak${subject}`),
};
return cssInput;
},
[isCustomVariant, variant],
);
const customVariantClassName = useComponentStyle(variantSpec);
// Store custom variant in cache if it's a new custom variant
useEffect(() => {
if (isCustomVariant && variant && customVariantClassName) {
// Check if this variant is already cached
if (!hasCustomVariantCache(variant)) {
// TODO: When CSS generation is implemented, extract the actual CSS text
// For now, store placeholder information
setCustomVariantCache(variant, {
className: customVariantClassName,
cssText: "", // Will be populated when CSS generation is implemented
});
}
}
}, [isCustomVariant, variant, customVariantClassName]);
// Determine overflow mode classes based on overflowMode and existing props
const overflowClasses = useMemo(() => {
const classes: Record<string, boolean> = {};
// If overflowMode is not explicitly set, use original behavior
if (!overflowMode) {
classes[styles.truncateOverflow] = maxLines > 0;
classes[styles.noEllipsis] = !ellipses;
return classes;
}
switch (overflowMode) {
case "none":
// CSS: overflow: hidden + text-overflow: clip + normal wrapping
// Effect: Text wraps normally but clips cleanly at container boundaries without ellipsis
classes[styles.overflowNone] = true;
break;
case "scroll":
// CSS: white-space: nowrap + overflow-x: auto + overflow-y: hidden
// Effect: Forces single line, enables horizontal scrollbar when content overflows
classes[styles.overflowScroll] = true;
break;
case "ellipsis":
// CSS: Uses -webkit-line-clamp for multi-line or white-space: nowrap + text-overflow: ellipsis for single line
// Effect: Shows "..." when text is truncated, respects maxLines for multi-line truncation
classes[styles.truncateOverflow] = true;
classes[styles.noEllipsis] = !ellipses;
break;
case "flow":
// CSS: white-space: normal + overflow-y: auto + overflow-x: hidden
// Effect: Text wraps to multiple lines with vertical scrolling when needed, no horizontal scrollbar
// Note: Flow mode ignores maxLines to allow unlimited text wrapping
classes[styles.overflowFlow] = true;
break;
}
return classes;
}, [overflowMode, maxLines, ellipses]);
// Determine break mode classes
const breakClasses = useMemo(() => {
const classes: Record<string, boolean> = {};
// Only apply break mode classes if explicitly set (preserves theme variable support)
if (breakMode) {
switch (breakMode) {
case "normal":
// CSS: word-break: normal + overflow-wrap: normal
// Effect: Standard word breaking at natural boundaries (spaces, hyphens)
classes[styles.breakNormal] = true;
break;
case "word":
// CSS: overflow-wrap: break-word
// Effect: Breaks long words only when necessary to prevent overflow, preserves word boundaries when possible
classes[styles.breakWord] = true;
break;
case "anywhere":
// CSS: word-break: break-all + overflow-wrap: anywhere
// Effect: Most aggressive breaking - allows breaking between any characters to fit container
classes[styles.breakAnywhere] = true;
break;
case "keep":
// CSS: word-break: keep-all
// Effect: Prevents breaking within words entirely (useful for CJK text or technical terms)
classes[styles.breakKeep] = true;
break;
case "hyphenate":
// CSS: hyphens: auto + overflow-wrap: break-word
// Effect: Uses browser's hyphenation dictionary to break words with proper hyphens
classes[styles.breakHyphenate] = true;
break;
}
}
return classes;
}, [breakMode]);
return (
<Element
{...restVariantSpecificProps}
ref={ref as any}
className={classnames(
syntaxHighlightClasses,
styles.text,
// Use custom variant className if it's a custom variant, otherwise use predefined variant style
isCustomVariant ? customVariantClassName : styles[variant || "default"],
{
[styles.preserveLinebreaks]: preserveLinebreaks,
...overflowClasses,
...breakClasses,
},
className,
)}
style={{
...style,
// Apply maxLines style for "ellipsis" mode and default behavior
// "none", "scroll", and "flow" modes ignore maxLines for predictable, reliable behavior
...(overflowMode === "ellipsis" || (!overflowMode && maxLines)
? getMaxLinesStyle(maxLines)
: {}),
}}
>
{children}
</Element>
);
});
```
--------------------------------------------------------------------------------
/xmlui/src/testing/component-test-helpers.ts:
--------------------------------------------------------------------------------
```typescript
import type { Locator } from "@playwright/test";
import { type ParserResult, xmlUiMarkupToComponent } from "../components-core/xmlui-parser";
import type { ComponentDef, CompoundComponentDef } from "../abstractions/ComponentDefs";
import chroma, { type Color } from "chroma-js";
import { ComponentDriver } from "./ComponentDrivers";
export type ThemeTestDesc = {
themeVar: string;
themeVarAsCSS: string;
expected: string;
dependsOnVars?: Record<string, string>;
};
export function mapObject<K extends (val: any) => any, V extends (val: string) => string | number>(
obj: Record<string, any>,
valFn: K = ((val) => val) as K,
keyFn: V = ((val) => val) as V,
) {
const newObject = {} as Record<ReturnType<V>, ReturnType<K>>;
Object.entries(obj).forEach(([key, value]) => {
newObject[keyFn(key)] = valFn(value);
});
return newObject;
}
export function getComponentTagName(locator: Locator) {
return locator.evaluate((el) => el.tagName.toLowerCase());
}
export function parseComponentIfNecessary(
rawComponent: ComponentDef<any> | CompoundComponentDef | string,
): ParserResult {
if (typeof rawComponent === "string") {
return xmlUiMarkupToComponent(rawComponent);
}
return {
component: rawComponent,
errors: [],
erroneousCompoundComponentName: undefined,
};
}
/**
* Scales a value by a percentage, using NumericCSS for parsing and clarity.
* @param scalarOf100Percent The value representing 100%.
* @param percentage A percentage string in the format "NN%", e.g., "40%". Must end with '%'.
* @returns The scaled value.
*/
export function scaleByPercent(scalarOf100Percent: number, percentage: string) {
const parsed = parseAsNumericCss(percentage);
if (parsed.unit !== "%") {
throw new Error(`Expected percentage unit (%), got: ${parsed.unit}`);
}
return (scalarOf100Percent / 100) * parsed.value;
}
export function getBoundingRect(locator: Locator) {
return locator.evaluate((element) => element.getBoundingClientRect());
}
export function getElementStyle(specifier: Locator, style: string) {
return specifier.evaluate(
(element, style) => window.getComputedStyle(element).getPropertyValue(style),
style,
);
}
/**
* Retreives all the provided style properties from the locator
* @returns an object with the keys being the elements of the styles argument
*/
export function getElementStyles(locator: Locator, styles: string[] = []) {
return locator.evaluate(
(element, styles) =>
Object.fromEntries(
styles.map((styleName) => [
styleName,
window.getComputedStyle(element).getPropertyValue(styleName),
]),
),
styles,
);
}
export function isIndeterminate(specifier: ComponentDriver | Locator) {
if (specifier instanceof ComponentDriver) specifier = specifier.component;
return specifier.evaluate((el: HTMLInputElement) => el.indeterminate);
}
export async function overflows(locator: Locator, direction: "x" | "y" | "both" = "both") {
const [width, height, scrollWidth, scrollHeight] = await locator.evaluate((element) => [
element.clientWidth,
element.clientHeight,
element.scrollWidth,
element.scrollHeight,
]);
if (direction === "x") return scrollWidth > width;
if (direction === "y") return scrollHeight > height;
return scrollWidth > width && scrollHeight > height;
}
// ----------------------------------
// ComponentDriver style helpers
// ----------------------------------
export function getStyles(specifier: ComponentDriver | Locator, style: string | string[]) {
if (specifier instanceof ComponentDriver) specifier = specifier.component;
style = Array.isArray(style) ? style : [style];
return specifier.evaluate(
(element, styles) =>
Object.fromEntries(
styles.map((styleName) => [
styleName
.trim()
.split("-")
.map((n, idx) => (idx === 0 ? n : n[0].toUpperCase() + n.slice(1)))
.join(""),
window.getComputedStyle(element).getPropertyValue(styleName),
]),
),
style,
);
}
export function getPseudoStyles(
specifier: ComponentDriver | Locator,
pseudoElement: string,
style: string | string[],
) {
if (specifier instanceof ComponentDriver) specifier = specifier.component;
style = Array.isArray(style) ? style : [style];
return specifier.evaluate(
(element, obj) =>
Object.fromEntries(
obj.style.map((styleName) => [
styleName
.trim()
.split("-")
.map((n, idx) => (idx === 0 ? n : n[0].toUpperCase() + n.slice(1)))
.join(""),
window.getComputedStyle(element, obj.pseudoElement).getPropertyValue(styleName),
]),
),
{ style, pseudoElement },
);
}
export async function getHtmlAttributes(
specifier: ComponentDriver | Locator,
attributes: string | string[],
): Promise<{ [k: string]: string }> {
if (specifier instanceof ComponentDriver) specifier = specifier.component;
attributes = Array.isArray(attributes) ? attributes : [attributes];
const mapped = await Promise.all(
attributes.map(async (attr) => {
return [attr, await specifier.getAttribute(attr)];
}),
);
return Object.fromEntries(mapped);
}
export async function getPaddings(specifier: ComponentDriver | Locator) {
const paddings = mapObject(
await getStyles(specifier, ["padding-left", "padding-right", "padding-top", "padding-bottom"]),
parseAsNumericCss,
);
return {
left: paddings.paddingLeft,
right: paddings.paddingRight,
top: paddings.paddingTop,
bottom: paddings.paddingBottom,
};
}
export async function getBorders(specifier: ComponentDriver | Locator) {
const borders = mapObject(
await getStyles(specifier, ["border-left", "border-right", "border-top", "border-bottom"]),
parseAsCssBorder,
);
return {
left: borders.borderLeft,
right: borders.borderRight,
top: borders.borderTop,
bottom: borders.borderBottom,
};
}
export function getMargins(specifier: ComponentDriver | Locator) {
return getStyles(specifier, ["margin-left", "margin-right", "margin-top", "margin-bottom"]);
}
/**
* Retrieves the bounding rectangle of the component including **margins** and **padding**
* added to the dimensions.
*/
export async function getBounds(specifier: ComponentDriver | Locator) {
if (specifier instanceof ComponentDriver) specifier = specifier.component;
const boundingRect = await specifier.evaluate((element) => element.getBoundingClientRect());
const m = mapObject(await getMargins(specifier), parseFloat);
const width = boundingRect.width + m.marginLeft + m.marginRight;
const height = boundingRect.height + m.marginTop + m.marginBottom;
const left = boundingRect.left - m.marginLeft;
const right = boundingRect.right + m.marginRight;
const top = boundingRect.top - m.marginTop;
const bottom = boundingRect.bottom + m.marginBottom;
return {
width,
height,
left,
right,
top,
bottom,
};
}
/**
* Provides annotations for skipped tests and the ability to specify a reason.
* Use it via the SKIP_REASON exported variable.
*/
class TestSkipReason {
private addAnnotation(type: string, description?: string) {
return {
annotation: {
type,
description: description ?? "",
},
};
}
NOT_IMPLEMENTED_XMLUI(description?: string) {
return this.addAnnotation("not implemented in xmlui", description);
}
TO_BE_IMPLEMENTED(description?: string) {
return this.addAnnotation("to be implemented", description);
}
XMLUI_BUG(description?: string) {
return this.addAnnotation("xmlui bug", description);
}
TEST_INFRA_BUG(description?: string) {
return this.addAnnotation("test infra bug", description);
}
TEST_NOT_WORKING(description?: string) {
return this.addAnnotation("test not working", description);
}
TEST_INFRA_NOT_IMPLEMENTED(description?: string) {
return this.addAnnotation("test infra not implemented", description);
}
REFACTOR(description?: string) {
return this.addAnnotation("refactor", description);
}
// Need to specify a reason here!
UNSURE(description: string) {
return this.addAnnotation("unsure", description);
}
}
/**
* Provides annotations for skipped tests and the ability to specify a reason.
*
* Usage:
*
* ```ts
* import { SKIP_REASON } from "./tests/component-test-helpers";
*
* test.skip(
* "test name",
* SKIP_REASON.NOT_IMPLEMENTED_XMLUI("This test is not implemented in xmlui")
* async ({}) => {},
* );
* ```
*/
export const SKIP_REASON = new TestSkipReason();
// --- CSS types and parsers
export function pixelStrToNum(pixelStr: string) {
return Number(pixelStr.replace("px", ""));
}
export function parseAsCssBorder(str: string) {
const parts = str.split(/\s+(?=[a-z]+|\()/i);
if (parts.length > 3) {
throw new Error(`Provided value ${str} cannot be parsed as a CSS border`);
}
const style = parts.filter(isCSSBorderStyle);
if (style.length > 1) {
throw new Error(`Too many border styles provided in ${str}`);
}
const width = parts.filter(isNumericCSS);
if (width.length > 1) {
throw new Error(`Too many border widths provided in ${str}`);
}
const color = parts.filter((p) => chroma.valid(p));
if (color.length > 1) {
throw new Error(`Too many border colors provided in ${str}`);
}
const result: CSSBorder = {
width: !!width[0] ? parseAsNumericCss(width[0]) : undefined,
style: style[0],
color: chroma(color[0]),
};
return result;
}
export interface NumericCSS {
value: number;
unit: CSSUnit;
}
const numericCSSRegex = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/;
export function isNumericCSS(str: any): str is NumericCSS {
const parts = str.match(numericCSSRegex);
if (!parts) return false;
if (parts.length < 3) return false;
if (isNaN(parseFloat(parts[1]))) return false;
if (!isCSSUnit(parts[2])) return false;
return true;
}
export function parseAsNumericCss(str: string) {
const parts = str.match(numericCSSRegex);
if (!parts) {
throw new Error(`Provided value ${str} cannot be parsed as a numeric CSS value`);
}
if (parts.length < 3) {
throw new Error(`${parts[0]} is not a correct numeric CSS value`);
}
const value = parseFloat(parts[1]);
if (isNaN(value)) {
throw new Error(`${value} is not a valid number in ${str}`);
}
const unit = parts[2];
if (!isCSSUnit(unit)) {
throw new Error(`${unit} is not have a valid CSS unit in ${str}`);
}
const result: NumericCSS = { value, unit };
return result;
}
export function numericCSSToString(cssValue: NumericCSS) {
return `${cssValue.value}${cssValue.unit}`;
}
export type CSSColor = chroma.Color;
export function isCSSColor(str: any): str is CSSColor {
return chroma.valid(str);
}
export function parseAsCSSColor(str: string): CSSColor {
return chroma(str);
}
const CSSUnitValues = [
"px",
"em",
"rem",
"vh",
"vw",
"%",
"cm",
"mm",
"in",
"pt",
"pc",
"ex",
"ch",
"vmin",
"vmax",
] as const;
type CSSUnit = (typeof CSSUnitValues)[number];
function isCSSUnit(str: string): str is CSSUnit {
return CSSUnitValues.includes(str as any);
}
export type CSSBorder = {
width?: NumericCSS;
style?: CSSBorderStyle;
color?: Color;
};
const CSSBorderStyleValues = [
"solid",
"dotted",
"dashed",
"double",
"none",
"hidden",
"groove",
"ridge",
"inset",
"outset",
] as const;
export type CSSBorderStyle = (typeof CSSBorderStyleValues)[number];
export function isCSSBorderStyle(str: string): str is CSSBorderStyle {
return CSSBorderStyleValues.includes(str as any);
}
export function parseAsCSSBorderStyle(str: string) {
if (!isCSSBorderStyle(str)) {
throw new Error(`Provided value ${str} cannot be parsed as a CSS border style`);
}
return str;
}
export const BorderSideValues = ["top", "bottom", "left", "right"] as const;
export type BorderSide = (typeof BorderSideValues)[number];
export function isBorderSide(str: string): str is BorderSide {
return BorderSideValues.includes(str as any);
}
```
--------------------------------------------------------------------------------
/xmlui/src/components/Tree/Tree.md:
--------------------------------------------------------------------------------
```markdown
%-DESC-START
**Key features:**
- **Flat and hierarchical data structures**: You can select the most convenient data format to represent the tree. A set of properties enables you to map your data structure to the visual representation of the tree.
- **Flexible expand/collapse**: You have several properties to represent the expanded and collapsed state of tree nodes. You can also specify several options that determine which tree items are collapsed initially.
- **Tree API**: Several exposed methods allow you to manage the tree's view state imperatively.
## Specifying Data
With the `dataFormat` property, you can select between "flat" or "hierarchy" formats. The component transforms the data according to the value of this property into a visual representation.
The "flat" and "hierarchy" data structures both use these fields for a particular tree node:
- `id`: Unique ID of tree node
- `name`: The field to be used as the display label
- `icon`: An optional icon identifier. If specified, this icon is displayed with the tree item.
- `iconExpanded`: An optional icon identifier. This icon is displayed when the field is expanded.
- `iconCollapsed`: An optional icon identifier. This icon is displayed when the field is collapsed.
- `selectable`: Indicates if the node can be selected.
The "flat" structure refers to its direct parent node via the `parentId` property, which contains the ID of the node it is referring to.
The "hierarchy" structure uses a `children` property, which is an array of nested child nodes (using the common node property set above).
This example demonstrates the use of the "flat" data mode:
```xmlui-pg display copy height="220px" /"flat"/ /parentId/ name="Example: flat data format"
<App>
<Tree
testId="tree"
dataFormat="flat"
defaultExpanded="all"
data='{[
{ id: 1, icon:"folder", name: "Root Item 1", parentId: null },
{ id: 2, icon:"folder", name: "Child Item 1.1", parentId: 1 },
{ id: 3, icon: "code", name: "Child Item 1.2", parentId: 1 },
{ id: 4, icon: "code", name: "Grandchild Item 1.1.1", parentId: 2 },
]}'>
</Tree>
</App>
```
This example demonstrates the use of the "hiearchy" data mode:
```xmlui-pg display copy height="220px" /"flat"/ /children/ name="Example: hierarchical data format"
<App>
<Tree
testId="tree"
dataFormat="hierarchy"
defaultExpanded="all"
data='{[
{
id: 1, icon: "folder", name: "Root Item 1",
children: [
{ id: 2, icon: "code", name: "Child Item 1.1" },
{ id: 3, icon: "folder", name: "Child Item 1.2",
children: [
{ id: 4, icon: "code", name: "Grandchild Item 1.2.1"}
],
}
],
},
]}'>
</Tree>
</App>
```
When you use data (for example, retrieved from a backend), those structures may use different property names. The `Tree` component allows mapping data field names through these properties:
- `idField` (default: `id`)
- `nameField` (default: `name`)
- `iconField` (default: `icon`)
- `iconExpandedField` (default: `iconExpanded`)
- `iconCollapsedField` (default: `iconCollapsed`)
- `parentIdField` (default: `parentId`)
- `childrenField` (default: `children`)
- `selectableField` (default: `selectable`)
The following example uses the `idField`, `nameField`, and `parentIdField` mapping properties:
```xmlui-pg display copy height="220px" /idField/ /nameField/ /parentIdField/ /uid/ /label/ /parent/ name="Example: mapping data fields"
<App>
<Tree
testId="tree"
dataFormat="flat"
defaultExpanded="all"
idField="uid"
nameField="label"
parentIdField="parent"
data='{[
{ uid: 1, icon:"folder", label: "Root Item 1", parent: null },
{ uid: 2, icon:"folder", label: "Child Item 1.1", parent: 1 },
{ uid: 3, icon: "code", label: "Child Item 1.2", parent: 1 },
{ uid: 4, icon: "code", label: "Grandchild Item 1.1.1", parent: 2 },
]}'>
</Tree>
</App>
```
## Expanding and collapsing tree nodes
By default, when you click a tree node outside of its expand/collapse icon, the specified item is selected. With the `expandOnItemClick` property (using the `true` value), you can change this behavior to expand or collapse the item when clicking its surface anywhere.
You can use the `defaultExpanded` property to specify what nodes you want to see expanded initially. You can set this property to a list of node IDs or a string. When you specify IDs, the component expands the hierarchy to reveal the specified nodes. When the value is a string, you can use these options:
- `none`: all nodes are collapsed (default)
- `first-level`: all first-level nodes are expanded
- `all`: all nodes are expanded
The following example demonstrates the use of `defaultExpanded` with tree node IDs:
```xmlui-pg display copy height="300px" /doc-root/ /proj-web/ /media-profile-pic/ name="Example: defaultExpanded with node IDs"
<App>
<Tree
testId="tree"
dataFormat="flat"
defaultExpanded="{['doc-root', 'proj-web', 'media-profile-pic']}"
data='{[
// Branch A: Documents
{ id: "doc-root", name: "[Documents]", parentId: null },
{ id: "doc-reports", name: "Reports", parentId: "doc-root" },
{ id: "doc-invoices", name: "Invoices", parentId: "doc-root" },
{ id: "doc-q1-report", name: "Q1 Report.pdf", parentId: "doc-reports" },
{ id: "doc-q2-report", name: "Q2 Report.pdf", parentId: "doc-reports" },
{ id: "doc-inv-001", name: "Invoice-001.pdf", parentId: "doc-invoices" },
// Branch B: Projects
{ id: "proj-root", name: "Projects", parentId: null },
{ id: "proj-web", name: "[Web Apps]", parentId: "proj-root" },
{ id: "proj-mobile", name: "Mobile Apps", parentId: "proj-root" },
{ id: "proj-ecommerce", name: "E-commerce Site", parentId: "proj-web" },
{ id: "proj-dashboard", name: "Admin Dashboard", parentId: "proj-web" },
{ id: "proj-ios-app", name: "iOS Shopping App", parentId: "proj-mobile" },
// Branch C: Media
{ id: "media-root", name: "Media", parentId: null },
{ id: "media-images", name: "Images", parentId: "media-root" },
{ id: "media-videos", name: "Videos", parentId: "media-root" },
{ id: "media-profile-pic", name: "[profile.jpg]", parentId: "media-images" },
{ id: "media-banner", name: "banner.png", parentId: "media-images" },
]}'>
</Tree>
</App>
```
You have several options to style the icons representing the expanded or collapsed state:
- The icons used for the expanded and collapsed states can be changed with the `iconExpanded` and `iconCollapsed` properties, respectively.
- You can specify a different size with the `iconSize` property (using only numeric values considered as pixels)
- Using a rotate animation when changing the state with the `animateExpand` flag.
The following option demonstrates the last two options:
```xmlui-pg display copy {4-5} height="220px" name="Example: expand/collapse options"
<App>
<Tree
testId="tree"
iconSize="24"
animateExpand
dataFormat="flat"
defaultExpanded="all"
data='{[
{ id: 1, name: "Root Item 1", parentId: null },
{ id: 2, name: "Child Item 1.1", parentId: 1 },
{ id: 3, name: "Child Item 1.2", parentId: 1 },
{ id: 4, name: "Grandchild Item 1.1.1", parentId: 2 },
]}'>
</Tree>
</App>
```
## Selection
Each tree node is selectable by default, unless the node item's data does not have a `selectable` property (or the one specified in `selectedField`).
A selectable item can be selected by clicking the mouse or pressing the Enter or Space keys when it has focus.
You can set the `selectedValue` property to define the selected tree item, ot use the `selectNode` exposed method for imperative selection.
## Item templates
You can override the default template used to display a tree item with the `itemTemplate` property. The template definition can use the `$item` context variable to access the item's attributes for display. `$item` provides these properties:
- `id`: The unique node ID
- `name`: The name of the node
- `depth`: The depth level in the tree
- `isExpanded`: Indicates if the tree node is expanded
- `hasChildren`: Indicates if the tree node has children
- `children`: The children of the tree node
- `selectable`: Indicates if the node can be selected
- `parentId`: The ID of the node's parent
- `parentIds`: A list of parent IDs from the root node to the direct parent of the node
- `path`: An array with the node names following the path from the root node to the displayed node.
- `loadingState`: The current state of a dynamic node ("unloaded", "loading", or "loaded")
This example demonstrates these concepts:
```xmlui-pg display copy {20-30} height="400px" /$item.id/ /$item.name/ /$item.hasChildren/ name="Example: itemTemplate"
<App>
<Tree
testId="tree"
id="tree"
defaultExpanded="all"
data='{[
{ id: "root", name: "My Files", parentId: null },
{ id: "doc-root", name: "Documents", parentId: "root" },
{ id: "doc-reports", name: "Reports", parentId: "doc-root" },
{ id: "doc-q1-report", name: "Q1 Report.pdf", parentId: "doc-reports" },
{ id: "doc-q2-report", name: "Q2 Report.pdf", parentId: "doc-reports" },
{ id: "proj-root", name: "Projects", parentId: "root" },
{ id: "proj-web", name: "Web Apps", parentId: "proj-root" },
{ id: "proj-ecommerce", name: "E-commerce Site", parentId: "proj-web" },
{ id: "proj-dashboard", name: "Admin Dashboard", parentId: "proj-web" },
{ id: "media-root", name: "Media", parentId: "root" },
{ id: "media-images", name: "Images", parentId: "media-root" },
{ id: "media-videos", name: "Videos", parentId: "media-root" },
]}'>
<property name="itemTemplate">
<HStack testId="{$item.id}" verticalAlignment="center" gap="$space-1">
<Icon name="{$item.hasChildren ? 'folder' : 'code'}" />
<Text>
({$item.id}):
</Text>
<Text variant="strong">
{$item.name}
</Text>
</HStack>
</property>
</Tree>
</App>
```
## Dynamic tree nodes
When initializing the tree with its `data` property, you can set the `dynamic` property of the node to `true` (you can use a field name alias with the `dynamicField` property). When you extend a dynamic node, the tree fires the `loadChildren` event, and the nodes returned by the event handler will be the actual nodes.
By default, nodes are not dynamic.
While the child nodes are being queried, the tree node displays a spinner to indicate the loading state.
You can use the `markNodeUnloaded` exposed method to reset the state of an already loaded dynamic tree node. The next time the user expands the node, its content will be loaded again.
The following sample demonstrates this feature. Click the "Child Item 1.2" node to check how it loads its children. Click the Unload button to reload the items when the node is expanded the next time.
```xmlui-pg display copy {16-19} height="340px" /dynamic: true/ /onLoadChildren/ name="Example: dynamic nodes"
<App var.loadCount="{0}">
<Tree
testId="tree"
defaultExpanded="all"
id="tree"
itemClickExpands
data='{[
{ id: 1, name: "Root Item 1", parentId: null },
{ id: 2, name: "Child Item 1.1", parentId: 1 },
{ id: 3, name: "Child Item 1.2", parentId: 1, dynamic: true },
{ id: 4, name: "Child Item 1.3", parentId: 1 },
]}'
onLoadChildren="(node) => {
loadCount++;
delay(1000);
return ([
{ id: 5, name: `Dynamic Item 1.2.1 (${loadCount})` },
{ id: 6, name: `Dynamic Item 2.2.2 (${loadCount})` },
])
}"
>
<property name="itemTemplate">
<HStack testId="{$item.id}" verticalAlignment="center" gap="$space-1">
<Icon name="{$item.hasChildren
? ($item.loadingState === 'loaded' ? 'folder' : 'folder-outline' )
: 'code'}"
/>
<Text>{$item.name}</Text>
</HStack>
</property>
</Tree>
<Button onClick="tree.markNodeUnloaded(3)">Unload</Button>
</App>
```
%-DESC-END
```
--------------------------------------------------------------------------------
/xmlui/tests/parsers/scripting/statement-hooks.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it } from "vitest";
import { processStatementQueueAsync } from "../../../src/components-core/script-runner/process-statement-async";
import { processStatementQueue } from "../../../src/components-core/script-runner/process-statement-sync";
import { createEvalContext, parseStatements } from "./test-helpers";
describe("Statement hooks", () => {
const stmtCases = [
{ src: "let x = 0;", exp: 1 },
{ src: "let x = 0; x++;", exp: 2 },
{ src: "let x = 0; while (x < 1) { x++ };", exp: 6 },
];
stmtCases.forEach((c) => {
it(`onStatementStarted (async) works ${c.src}`, async () => {
// --- Arrange
let counter = 0;
const evalContext = createEvalContext({
localContext: {},
onStatementStarted: () => {
counter++;
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(counter).equal(c.exp);
});
});
stmtCases.forEach((c) => {
it(`onStatementStarted (sync) works ${c.src}`, () => {
// --- Arrange
let counter = 0;
const evalContext = createEvalContext({
localContext: {},
onStatementStarted: () => {
counter++;
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(counter).equal(c.exp);
});
});
stmtCases.forEach((c) => {
it(`onStatementCompleted (async) works ${c.src}`, async () => {
// --- Arrange
let counter = 0;
const evalContext = createEvalContext({
localContext: {},
onStatementCompleted: () => {
counter++;
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(counter).equal(c.exp);
});
});
stmtCases.forEach((c) => {
it(`onStatementCompleted (sync) works ${c.src}`, () => {
// --- Arrange
let counter = 0;
const evalContext = createEvalContext({
localContext: {},
onStatementCompleted: () => {
counter++;
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(counter).equal(c.exp);
});
});
stmtCases.forEach((c) => {
it(`onStatementStarted&Completed (async) works ${c.src}`, async () => {
// --- Arrange
let counter = 0;
const evalContext = createEvalContext({
localContext: {},
onStatementStarted: () => {
counter++;
},
onStatementCompleted: () => {
counter++;
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(counter).equal(c.exp + c.exp);
});
});
stmtCases.forEach((c) => {
it(`onStatementStarted&Completed (sync) works ${c.src}`, () => {
// --- Arrange
let counter = 0;
const evalContext = createEvalContext({
localContext: {},
onStatementStarted: () => {
counter++;
},
onStatementCompleted: () => {
counter++;
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(counter).equal(c.exp + c.exp);
});
});
const updateAsgnCases = [
{ src: "let x = 0; x = 3", exp: [] },
{ src: "x = 0;", exp: ["x"] },
{ src: "x.a = 0;", exp: ["x"] },
{ src: "x.a = []; x.a[3] = 12", exp: ["x", "x"] },
{ src: "x.a = []; x.a[3] = {}; x.a[3].f = 24", exp: ["x", "x", "x"] },
{ src: "x = 0; y = 0;", exp: ["x", "y"] },
];
updateAsgnCases.forEach((c) => {
it(`onWillUpdate (async) works ${c.src}`, async () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onWillUpdate: (_scope, index, type) => {
if (type === "assignment") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
updateAsgnCases.forEach((c) => {
it(`onWillUpdate (sync) works ${c.src}`, () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onWillUpdate: (_scope, index, type) => {
if (type === "assignment") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
updateAsgnCases.forEach((c) => {
it(`onDidUpdate (async) works ${c.src}`, async () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onDidUpdate: (_scope, index, type) => {
if (type === "assignment") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
updateAsgnCases.forEach((c) => {
it(`onDidUpdate (sync) works ${c.src}`, () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onDidUpdate: (_scope, index, type) => {
if (type === "assignment") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
const prePostCases = [
{ src: "let x = 0; x++", exp: [] },
{ src: "x = 0; x++", exp: ["x"] },
{ src: "x.a = 0; ++x.a", exp: ["x"] },
{ src: "x.a = []; x.a[3] = 12; --x.a[3]", exp: ["x"] },
{ src: "x.a = []; x.a[3] = {}; x.a[3].f = 24; x.a[3].f--", exp: ["x"] },
{ src: "x = 0; x++; y = 0; --y;", exp: ["x", "y"] },
];
prePostCases.forEach((c) => {
it(`onWillUpdate (async) works ${c.src}`, async () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onWillUpdate: (_scope, index, type) => {
if (type === "pre-post") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
prePostCases.forEach((c) => {
it(`onWillUpdate (sync) works ${c.src}`, () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onWillUpdate: (_scope, index, type) => {
if (type === "pre-post") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
prePostCases.forEach((c) => {
it(`onDidUpdate (async) works ${c.src}`, async () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onDidUpdate: (_scope, index, type) => {
if (type === "pre-post") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
prePostCases.forEach((c) => {
it(`onDidUpdate (sync) works ${c.src}`, () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onDidUpdate: (_scope, index, type) => {
if (type === "pre-post") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
const funcCallCases = [
{ src: "let x = () => {}; x()", exp: [] },
{ src: "x = () => {}; x()", exp: ["x"] },
{ src: "x.a = () => {}; x.a()", exp: ["x"] },
{ src: "x.a = []; x.a[3] = () => {}; x.a[3]()", exp: ["x"] },
{ src: "x.a = []; x.a[3] = {}; x.a[3].f = () => {}; x.a[3].f()", exp: ["x"] },
{ src: "x = () => {}; x(); y = () => {}; y();", exp: ["x", "y"] },
];
funcCallCases.forEach((c) => {
it(`onWillUpdate (async) works ${c.src}`, async () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onWillUpdate: (_scope, index, type) => {
if (type === "function-call") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
funcCallCases.forEach((c) => {
it(`onWillUpdate (sync) works ${c.src}`, () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onWillUpdate: (_scope, index, type) => {
if (type === "function-call") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
funcCallCases.forEach((c) => {
it(`onDidUpdate (async) works ${c.src}`, async () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onDidUpdate: (_scope, index, type) => {
if (type === "function-call") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
await processStatementQueueAsync(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
funcCallCases.forEach((c) => {
it(`onDidUpdate (sync) works ${c.src}`, () => {
// --- Arrange
let updated: string[] = [];
const evalContext = createEvalContext({
localContext: {
x: {},
y: {},
},
onDidUpdate: (_scope, index, type) => {
if (type === "function-call") {
updated.push(index as string);
}
},
});
const statements = parseStatements(c.src);
// --- Act
processStatementQueue(statements, evalContext);
// --- Assert
expect(updated).toStrictEqual(c.exp);
});
});
});
```
--------------------------------------------------------------------------------
/xmlui/src/components-core/action/APICall.tsx:
--------------------------------------------------------------------------------
```typescript
import toast from "react-hot-toast";
import type { QueryClient, QueryKey } from "@tanstack/react-query";
import { createDraft, finishDraft } from "immer";
import type { AppContextObject } from "../../abstractions/AppContextDefs";
import type { AsyncFunction } from "../../abstractions/FunctionDefs";
import type { ActionExecutionContext, LookupAsyncFnInner } from "../../abstractions/ActionDefs";
import { invalidateQueries } from "../utils/actionUtils";
import { extractParam, shouldKeep } from "../utils/extractParam";
import { randomUUID } from "../utils/misc";
import type { ApiActionOptions, ApiOperationDef } from "../RestApiProxy";
import RestApiProxy from "../RestApiProxy";
import { createAction } from "./actions";
function findQueryKeysToUpdate(updates: string | string[], queryClient: QueryClient) {
const queryKeysToUpdate: Array<QueryKey> = [];
if (updates) {
let updatesArray: Array<string>;
if (Array.isArray(updates)) {
updatesArray = updates;
} else {
updatesArray = [updates];
}
updatesArray.forEach((queryUrl) => {
queryClient
.getQueryCache()
.getAll()
.forEach((query) => {
if (query.queryKey[0] === queryUrl) {
queryKeysToUpdate.push(query.queryKey);
}
});
});
}
return queryKeysToUpdate;
}
function prepareOptimisticValue(value: any, clientTxId: string) {
return {
...value,
id: value.id || clientTxId,
_optimisticValue: true,
_initiatorClientTxId: clientTxId,
};
}
async function prepareOptimisticValuesForQueries(
queryKeys: Array<QueryKey>,
queryClient: QueryClient,
clientTxId: string,
stateContext: any,
resolvedOptimisticValue?: any,
optimisticValueGetter?: AsyncFunction,
) {
const ret: Map<QueryKey, any> = new Map();
await Promise.all(
queryKeys.map(async (queryKey) => {
if (resolvedOptimisticValue) {
ret.set(queryKey, prepareOptimisticValue(resolvedOptimisticValue, clientTxId));
return;
}
if (!optimisticValueGetter) {
return;
}
const currentData = queryClient.getQueryData(queryKey) as any;
const flatData = currentData?.pages
? currentData.pages.flatMap((page: any) => page)
: currentData;
const optimisticValue = await optimisticValueGetter(flatData, stateContext["$param"]);
if (optimisticValue) {
ret.set(queryKey, prepareOptimisticValue(optimisticValue, clientTxId));
}
}),
);
return ret;
}
async function doOptimisticUpdate(
optimisticValuesByQueryKeys: Map<QueryKey, any>,
queryClient: QueryClient,
) {
if (!optimisticValuesByQueryKeys.size) {
return;
}
for (const entry of optimisticValuesByQueryKeys.entries()) {
const [key, optimisticValue] = entry;
await queryClient.cancelQueries({ queryKey: key });
const oldData = queryClient.getQueryData(key) as any;
const draft = createDraft(oldData as any);
if (draft.pages) {
let updated = false;
draft.pages.forEach((page: any) => {
page.forEach((item: any) => {
if (item.id === optimisticValue.id) {
Object.assign(item, optimisticValue);
updated = true;
}
});
});
if (!updated) {
draft.pages[draft.pages.length - 1].push(optimisticValue);
}
} else {
let updated = false;
draft.forEach((item: any) => {
if (item.id === optimisticValue.id) {
Object.assign(item, optimisticValue);
updated = true;
}
});
if (!updated) {
draft.push(optimisticValue);
}
}
const newData = finishDraft(draft);
queryClient.setQueryData(key, newData);
// console.log("optimistic added", { finalOptimisticValue, newData });
}
}
function updateQueriesWithResult(
queryKeysToUpdate: Array<QueryKey>,
optimisticValuesByQueryKeys: Map<QueryKey, any>,
clientTxId: string,
queryClient: QueryClient,
result: any,
) {
if (!queryKeysToUpdate.length) {
return;
}
queryKeysToUpdate.forEach((key) => {
const oldData = queryClient.getQueryData(key) as any;
const draft = createDraft(oldData as any);
const optimisticValue = optimisticValuesByQueryKeys.get(key);
if (draft.pages) {
//pageable loader
if (optimisticValue) {
draft.pages[draft.pages.length - 1] = draft.pages[draft.pages.length - 1].map(
(item: any) =>
item.id === optimisticValue.id && item._initiatorClientTxId === clientTxId
? result || {
...item,
_optimisticValue: undefined,
_initiatorClientTxId: undefined,
}
: item,
);
} else {
let updated = false;
draft.pages.forEach((page: any) => {
page?.forEach((item: any) => {
if (item.id === result?.id) {
Object.assign(item, result);
updated = true;
}
});
});
if (!updated && result) {
draft.pages[draft.pages.length - 1].push(result);
}
}
} else {
if (optimisticValue) {
draft.forEach((item: any, index: number) => {
if (item.id === optimisticValue.id && item._initiatorClientTxId === clientTxId) {
draft[index] = result || {
...item,
_optimisticValue: undefined,
_initiatorClientTxId: undefined,
};
}
});
} else {
let updated = false;
draft.forEach((item: any, index: number) => {
if (item.id === result.id) {
draft[index] = result || {
...item,
_optimisticValue: undefined,
_initiatorClientTxId: undefined,
};
updated = true;
}
});
if (!updated && result) {
draft.push(result);
}
}
}
const newData = finishDraft(draft);
queryClient.setQueryData(key, newData);
});
}
async function updateQueriesWithOptimisticValue({
stateContext,
updates,
appContext,
queryClient,
clientTxId,
optimisticValue,
lookupAction,
getOptimisticValue,
uid,
}: {
stateContext: any;
updates: string | string[] | undefined;
appContext: AppContextObject;
queryClient: QueryClient;
clientTxId: string;
optimisticValue: any;
lookupAction: LookupAsyncFnInner;
getOptimisticValue?: string;
uid: symbol;
}) {
const queryKeysToUpdate = findQueryKeysToUpdate(
extractParam(stateContext, updates, appContext),
queryClient,
);
const optimisticValuesByQueryKeys = await prepareOptimisticValuesForQueries(
queryKeysToUpdate,
queryClient,
clientTxId,
stateContext,
extractParam(stateContext, optimisticValue, appContext),
lookupAction(getOptimisticValue, uid),
);
await doOptimisticUpdate(optimisticValuesByQueryKeys, queryClient);
return { queryKeysToUpdate, optimisticValuesByQueryKeys };
}
type APICall = {
invalidates?: string | string[];
updates?: string | string[];
confirmTitle?: string;
confirmMessage?: string;
confirmButtonLabel?: string;
params?: any;
payloadType?: string;
optimisticValue?: any;
getOptimisticValue?: string;
inProgressNotificationMessage?: string;
completedNotificationMessage?: string;
errorNotificationMessage?: string;
uid?: string | symbol;
when?: string;
onBeforeRequest?: string;
onSuccess?: string;
onProgress?: string;
onError?: string;
} & ApiOperationDef;
export async function callApi(
{ state, appContext, lookupAction, getCurrentState, apiInstance }: ActionExecutionContext,
{
confirmTitle,
confirmMessage,
confirmButtonLabel,
params = {},
onBeforeRequest,
onSuccess,
onError,
invalidates,
updates,
optimisticValue,
payloadType,
when,
getOptimisticValue,
inProgressNotificationMessage,
completedNotificationMessage,
errorNotificationMessage,
uid: actionUid,
onProgress,
//operation
headers,
url,
queryParams,
rawBody,
method,
body,
}: APICall,
{ resolveBindingExpressions }: ApiActionOptions = {},
) {
const uid = typeof actionUid === "symbol" ? actionUid : Symbol(actionUid);
const stateContext = { ...state, ...params };
if (!shouldKeep(when, stateContext, appContext)) {
return;
}
if (confirmTitle || confirmMessage || confirmButtonLabel) {
const title = extractParam(stateContext, confirmTitle, appContext);
const message = extractParam(stateContext, confirmMessage, appContext);
const buttonLabel = extractParam(stateContext, confirmButtonLabel, appContext);
const dialogCheck = await appContext.confirm(
title ?? "Confirm Operation",
message ?? "Are you sure you want to perform this operation?",
buttonLabel ?? "Yes",
);
if (!dialogCheck) return;
}
const resolvedInvalidates = extractParam(stateContext, invalidates, appContext);
const clientTxId = randomUUID();
const beforeRequestFn = lookupAction(onBeforeRequest, uid);
const beforeRequestResult = await beforeRequestFn?.();
if (typeof beforeRequestResult === "boolean" && beforeRequestResult === false) {
return;
}
const { queryKeysToUpdate, optimisticValuesByQueryKeys } = await updateQueriesWithOptimisticValue(
{
stateContext,
updates,
appContext,
queryClient: appContext.queryClient!,
clientTxId,
optimisticValue,
lookupAction,
getOptimisticValue,
uid,
},
);
const inProgressMessage = extractParam(stateContext, inProgressNotificationMessage, appContext);
let loadingToastId;
if (inProgressMessage) {
loadingToastId = toast.loading(inProgressMessage);
}
try {
const operation: ApiOperationDef = {
headers,
url,
queryParams,
rawBody,
method,
body,
payloadType,
};
const _onProgress = lookupAction(onProgress, uid, {
eventName: "progress",
});
const result = await new RestApiProxy(appContext, apiInstance).execute({
operation,
params: stateContext,
transactionId: clientTxId,
resolveBindingExpressions,
onProgress: _onProgress,
});
const onSuccessFn = lookupAction(onSuccess, uid, {
eventName: "success",
context: getCurrentState()
});
await onSuccessFn?.(result, stateContext["$param"]);
updateQueriesWithResult(
queryKeysToUpdate,
optimisticValuesByQueryKeys,
clientTxId,
appContext.queryClient!,
result,
);
if (resolvedInvalidates || !updates) {
await invalidateQueries(resolvedInvalidates, appContext, state);
}
const completedMessage = extractParam(
{ ...stateContext, $result: result },
completedNotificationMessage,
appContext,
);
if (completedMessage) {
toast.success(completedMessage, {
id: loadingToastId,
});
} else if (loadingToastId) {
toast.dismiss(loadingToastId);
}
return result;
} catch (e) {
if (optimisticValuesByQueryKeys.size) {
await appContext.queryClient!.invalidateQueries();
}
const onErrorFn = lookupAction(onError, uid, {
eventName: "error",
});
const result = await onErrorFn?.(e, stateContext["$param"]);
const errorMessage = extractParam(
{ ...stateContext, $error: e },
errorNotificationMessage,
appContext,
);
if (errorMessage) {
toast.error(errorMessage, {
id: loadingToastId,
});
} else {
if (loadingToastId) {
toast.dismiss(loadingToastId);
}
if (result !== false) {
//stop the error propagation, if the error handler returns false
throw e;
}
}
}
}
export const apiAction = createAction("callApi", callApi);
```
--------------------------------------------------------------------------------
/xmlui/src/components/Slot/Slot.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { test, expect } from "../../testing/fixtures";
// =============================================================================
// BASIC FUNCTIONALITY TESTS
// =============================================================================
test.describe("Basic Functionality", () => {
test("renders children content in default slot", async ({ initTestBed, page }) => {
await initTestBed(`
<Custom>
<Button label="Create" />
<Button label="Edit" />
<Button label="Delete" />
</Custom>
`, {
components: [
`
<Component name="Custom">
<Card>
<H3>Use these actions</H3>
<HStack>
<Slot />
</HStack>
</Card>
</Component>
`
]
});
// Verify all passed children are rendered in the slot
await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
await expect(page.getByRole("button", { name: "Edit" })).toBeVisible();
await expect(page.getByRole("button", { name: "Delete" })).toBeVisible();
// Verify they are within the card structure from the component
await expect(page.getByText("Use these actions")).toBeVisible();
});
test("renders default content when no children provided", async ({ initTestBed, page }) => {
await initTestBed(`<ActionBar />`, {
components: [
`
<Component name="ActionBar">
<Card>
<H3>Use these actions</H3>
<HStack>
<Slot>
<Button label="Default" />
</Slot>
</HStack>
</Card>
</Component>
`
]
});
// Verify default content is rendered when no children provided
await expect(page.getByRole("button", { name: "Default" })).toBeVisible();
await expect(page.getByText("Use these actions")).toBeVisible();
});
test("overrides default content with provided children", async ({ initTestBed, page }) => {
await initTestBed(`
<ActionBar>
<Button label="Custom Button" />
</ActionBar>
`, {
components: [
`
<Component name="ActionBar">
<Card>
<H3>Use these actions</H3>
<HStack>
<Slot>
<Button label="Default" />
</Slot>
</HStack>
</Card>
</Component>
`
]
});
// Verify provided children override default content
await expect(page.getByRole("button", { name: "Custom Button" })).toBeVisible();
await expect(page.getByRole("button", { name: "Default" })).not.toBeVisible();
});
test("works with named slots using property template syntax", async ({ initTestBed, page }) => {
await initTestBed(`
<ActionBar>
<property name="headerTemplate">
<H2>Click one of these actions</H2>
</property>
<property name="footerTemplate">
<Text>Footer content goes here</Text>
</property>
<Button label="Create" />
<Button label="Edit" />
<Button label="Delete" />
</ActionBar>
`, {
components: [
`
<Component name="ActionBar">
<Card>
<Slot name="headerTemplate">
<H3>Use these actions</H3>
</Slot>
<HStack>
<Slot>
<Button label="Default" />
</Slot>
</HStack>
<Slot name="footerTemplate" />
</Card>
</Component>
`
]
});
// Verify named slots render their provided content
await expect(page.getByRole("heading", { name: "Click one of these actions" })).toBeVisible();
await expect(page.getByText("Footer content goes here")).toBeVisible();
// Verify default header is not shown (overridden by headerTemplate)
await expect(page.getByRole("heading", { name: "Use these actions" })).not.toBeVisible();
// Verify default slot content (buttons)
await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
await expect(page.getByRole("button", { name: "Edit" })).toBeVisible();
await expect(page.getByRole("button", { name: "Delete" })).toBeVisible();
});
test("renders default content for named slots when no template provided", async ({ initTestBed, page }) => {
await initTestBed(`
<ActionBar>
<Button label="Main Button" />
</ActionBar>
`, {
components: [
`
<Component name="ActionBar">
<Card>
<Slot name="headerTemplate">
<H3>Default header</H3>
</Slot>
<HStack>
<Slot />
</HStack>
<Slot name="footerTemplate">
<Text>Default footer</Text>
</Slot>
</Card>
</Component>
`
]
});
// Verify default content for named slots is rendered
await expect(page.getByRole("heading", { name: "Default header" })).toBeVisible();
await expect(page.getByText("Default footer")).toBeVisible();
// Verify main slot content
await expect(page.getByRole("button", { name: "Main Button" })).toBeVisible();
});
test("supports template properties with context variables", async ({ initTestBed, page }) => {
await initTestBed(`
<ActionBar header="Action Bar Example">
<property name="headerTemplate">
<Text variant="title">{$processedHeader}</Text>
</property>
<Button label="Create" />
<Button label="Edit" />
<Button label="Delete" />
</ActionBar>
`, {
components: [
`
<Component name="ActionBar">
<Card var.transformedHeader="*** {$props.header.toUpperCase()} ***">
<Slot name="headerTemplate" processedHeader="{transformedHeader}">
<H3>{transformedHeader}</H3>
</Slot>
<HStack>
<Slot>
<Button label="Default" />
</Slot>
</HStack>
</Card>
</Component>
`
]
});
// Verify template property is processed and passed to slot template
await expect(page.getByText("*** ACTION BAR EXAMPLE ***")).toBeVisible();
// Verify main slot content
await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
await expect(page.getByRole("button", { name: "Edit" })).toBeVisible();
await expect(page.getByRole("button", { name: "Delete" })).toBeVisible();
});
test("works with multiple context variables in template properties", async ({ initTestBed, page }) => {
await initTestBed(`
<DataDisplay>
<property name="contentTemplate">
<Text>{$title}: {$value}</Text>
</property>
</DataDisplay>
`, {
components: [
`
<Component name="DataDisplay">
<Slot name="contentTemplate" title="Score" value="{42}">
<Text>No data available</Text>
</Slot>
</Component>
`
]
});
// Verify multiple context variables work
await expect(page.getByText("Score: 42")).toBeVisible();
await expect(page.getByText("No data available")).not.toBeVisible();
});
test("supports complex nested content in slots", async ({ initTestBed, page }) => {
await initTestBed(`
<CustomCard>
<property name="headerTemplate">
<HStack>
<Icon name="star" />
<Text fontWeight="bold">Custom Header</Text>
</HStack>
</property>
<VStack>
<Text>Line 1</Text>
<Text>Line 2</Text>
<Button label="Action" />
</VStack>
</CustomCard>
`, {
components: [
`
<Component name="CustomCard">
<Card>
<Slot name="headerTemplate">
<H3>Default Header</H3>
</Slot>
<ContentSeparator />
<Slot />
</Card>
</Component>
`
]
});
// Verify complex nested content renders correctly
await expect(page.getByText("Custom Header")).toBeVisible();
await expect(page.getByText("Line 1")).toBeVisible();
await expect(page.getByText("Line 2")).toBeVisible();
await expect(page.getByRole("button", { name: "Action" })).toBeVisible();
// Verify default header is not shown
await expect(page.getByRole("heading", { name: "Default Header" })).not.toBeVisible();
});
test("maintains proper rendering order with multiple slots", async ({ initTestBed, page }) => {
await initTestBed(`
<MultiSlotComponent>
<property name="topTemplate">
<Text testId="top">Top Content</Text>
</property>
<property name="bottomTemplate">
<Text testId="bottom">Bottom Content</Text>
</property>
<Text testId="middle">Middle Content</Text>
</MultiSlotComponent>
`, {
components: [
`
<Component name="MultiSlotComponent">
<VStack>
<Slot name="topTemplate" />
<Slot />
<Slot name="bottomTemplate" />
</VStack>
</Component>
`
]
});
// Verify all content is rendered
await expect(page.getByTestId("top")).toBeVisible();
await expect(page.getByTestId("middle")).toBeVisible();
await expect(page.getByTestId("bottom")).toBeVisible();
// Verify rendering order by checking positions
const topElement = page.getByTestId("top");
const middleElement = page.getByTestId("middle");
const bottomElement = page.getByTestId("bottom");
const topBox = await topElement.boundingBox();
const middleBox = await middleElement.boundingBox();
const bottomBox = await bottomElement.boundingBox();
expect(topBox?.y).toBeLessThan(middleBox?.y || 0);
expect(middleBox?.y).toBeLessThan(bottomBox?.y || 0);
});
test("works with dynamic context in template properties", async ({ initTestBed, page }) => {
await initTestBed(`
<ContextComponent>
<property name="messageTemplate">
<Text>{$message}</Text>
</property>
</ContextComponent>
`, {
components: [
`
<Component name="ContextComponent" var.currentMessage="Initial Message">
<VStack>
<Button label="Update Message" onClick="currentMessage = 'Updated Message'" />
<Slot name="messageTemplate" message="{currentMessage}">
<Text>No message</Text>
</Slot>
</VStack>
</Component>
`
]
});
// Verify initial context value
await expect(page.getByText("Initial Message")).toBeVisible();
// Update the context and verify template updates
await page.getByRole("button", { name: "Update Message" }).click();
await expect(page.getByText("Updated Message")).toBeVisible();
await expect(page.getByText("Initial Message")).not.toBeVisible();
});
test("handles empty slots gracefully", async ({ initTestBed, page }) => {
await initTestBed(`
<EmptySlotComponent>
</EmptySlotComponent>
`, {
components: [
`
<Component name="EmptySlotComponent">
<Card>
<Text>Before slot</Text>
<Slot />
<Text>After slot</Text>
</Card>
</Component>
`
]
});
// Verify component renders without errors when slot is empty
await expect(page.getByText("Before slot")).toBeVisible();
await expect(page.getByText("After slot")).toBeVisible();
});
test("supports slot with no default content", async ({ initTestBed, page }) => {
await initTestBed(`
<MinimalComponent>
<Text>Provided content</Text>
</MinimalComponent>
`, {
components: [
`
<Component name="MinimalComponent">
<VStack>
<Text>Component content</Text>
<Slot name="contentTemplate" />
<Slot />
</VStack>
</Component>
`
]
});
// Verify provided content renders in default slot
await expect(page.getByText("Provided content")).toBeVisible();
// Verify component structure renders without errors
await expect(page.getByText("Component content")).toBeVisible();
});
});
```