This is page 101 of 181. Use http://codebase.md/xmlui-org/xmlui/mockApiDef.js?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ └── config.json
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog-optimized.yml
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── an-advanced-codefence.gif
│ │ │ │ ├── an-advanced-codefence.mp4
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── codefence-runner.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ ├── lorem-ipsum.png
│ │ │ │ ├── playground-checkbox-source.png
│ │ │ │ ├── playground.png
│ │ │ │ ├── use-xmlui-mcp-to-find-a-howto.png
│ │ │ │ └── xmlui-demo-gallery.png
│ │ │ ├── introducing-xmlui.md
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ ├── xmlui-playground.md
│ │ │ └── xmlui-powered-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ └── PageNotFound.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── blog-theme.ts
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── HelloMd.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── control-cache-invalidation.md
│ │ │ │ ├── debounce-user-input-for-api-calls.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── github.svg
│ │ │ ├── images
│ │ │ │ ├── apiaction-tutorial
│ │ │ │ │ ├── add-success.png
│ │ │ │ │ ├── apiaction-param.png
│ │ │ │ │ ├── change-completed.png
│ │ │ │ │ ├── change-in-progress.png
│ │ │ │ │ ├── confirm-delete.png
│ │ │ │ │ ├── data-error.png
│ │ │ │ │ ├── data-progress.png
│ │ │ │ │ ├── data-success.png
│ │ │ │ │ ├── display-1.png
│ │ │ │ │ ├── item-deleted.png
│ │ │ │ │ ├── item-updated.png
│ │ │ │ │ ├── missing-api-key.png
│ │ │ │ │ ├── new-item-added.png
│ │ │ │ │ └── test-message.png
│ │ │ │ ├── chat-api
│ │ │ │ │ └── domain-model.svg
│ │ │ │ ├── components
│ │ │ │ │ ├── image
│ │ │ │ │ │ └── breakfast.jpg
│ │ │ │ │ ├── markdown
│ │ │ │ │ │ └── colors.png
│ │ │ │ │ └── modal
│ │ │ │ │ ├── deep_link_dialog_1.jpg
│ │ │ │ │ └── deep_link_dialog_2.jpg
│ │ │ │ ├── create-apps
│ │ │ │ │ ├── collapsed-vertical.png
│ │ │ │ │ ├── using-forms-warning-dialog.png
│ │ │ │ │ └── using-forms.png
│ │ │ │ ├── datasource-tutorial
│ │ │ │ │ ├── data-with-header.png
│ │ │ │ │ ├── filtered-data.png
│ │ │ │ │ ├── filtered-items.png
│ │ │ │ │ ├── initial-page-items.png
│ │ │ │ │ ├── list-items.png
│ │ │ │ │ ├── next-page-items.png
│ │ │ │ │ ├── no-data.png
│ │ │ │ │ ├── pagination-1.jpg
│ │ │ │ │ ├── pagination-1.png
│ │ │ │ │ ├── polling-1.png
│ │ │ │ │ ├── refetch-data.png
│ │ │ │ │ ├── slow-loading.png
│ │ │ │ │ ├── test-message.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── unconventional-data.png
│ │ │ │ │ └── unfiltered-items.png
│ │ │ │ ├── flower.jpg
│ │ │ │ ├── get-started
│ │ │ │ │ ├── add-new-contact.png
│ │ │ │ │ ├── app-modified.png
│ │ │ │ │ ├── app-start.png
│ │ │ │ │ ├── app-with-boxes.png
│ │ │ │ │ ├── app-with-toast.png
│ │ │ │ │ ├── boilerplate-structure.png
│ │ │ │ │ ├── cl-initial.png
│ │ │ │ │ ├── cl-start.png
│ │ │ │ │ ├── contact-counts.png
│ │ │ │ │ ├── contact-dialog-title.png
│ │ │ │ │ ├── contact-dialog.png
│ │ │ │ │ ├── contact-menus.png
│ │ │ │ │ ├── contact-predicates.png
│ │ │ │ │ ├── context-menu.png
│ │ │ │ │ ├── dashboard-numbers.png
│ │ │ │ │ ├── default-contact-list.png
│ │ │ │ │ ├── delete-contact.png
│ │ │ │ │ ├── delete-task.png
│ │ │ │ │ ├── detailed-template.png
│ │ │ │ │ ├── edit-contact-details.png
│ │ │ │ │ ├── edited-contact-saved.png
│ │ │ │ │ ├── empty-sections.png
│ │ │ │ │ ├── filter-completed.png
│ │ │ │ │ ├── fullwidth-desktop.png
│ │ │ │ │ ├── fullwidth-mobile.png
│ │ │ │ │ ├── initial-table.png
│ │ │ │ │ ├── items-and-badges.png
│ │ │ │ │ ├── loading-message.png
│ │ │ │ │ ├── new-contact-button.png
│ │ │ │ │ ├── new-contact-saved.png
│ │ │ │ │ ├── no-empty-sections.png
│ │ │ │ │ ├── personal-todo-initial.png
│ │ │ │ │ ├── piechart.png
│ │ │ │ │ ├── review-today.png
│ │ │ │ │ ├── rudimentary-dashboard.png
│ │ │ │ │ ├── section-collapsed.png
│ │ │ │ │ ├── sectioned-items.png
│ │ │ │ │ ├── sections-ordered.png
│ │ │ │ │ ├── spacex-list-with-links.png
│ │ │ │ │ ├── spacex-list.png
│ │ │ │ │ ├── start-personal-todo-1.png
│ │ │ │ │ ├── submit-new-contact.png
│ │ │ │ │ ├── submit-new-task.png
│ │ │ │ │ ├── syntax-highlighting.png
│ │ │ │ │ ├── table-with-badge.png
│ │ │ │ │ ├── template-with-card.png
│ │ │ │ │ ├── test-emulated-api.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── todo-logo.png
│ │ │ │ │ └── xmlui-tools.png
│ │ │ │ ├── HelloApp.png
│ │ │ │ ├── HelloApp2.png
│ │ │ │ ├── logos
│ │ │ │ │ ├── xmlui1.svg
│ │ │ │ │ ├── xmlui2.svg
│ │ │ │ │ ├── xmlui3.svg
│ │ │ │ │ ├── xmlui4.svg
│ │ │ │ │ ├── xmlui5.svg
│ │ │ │ │ ├── xmlui6.svg
│ │ │ │ │ └── xmlui7.svg
│ │ │ │ ├── pdf
│ │ │ │ │ └── dummy-pdf.jpg
│ │ │ │ ├── rendering-engine
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ ├── Component.svg
│ │ │ │ │ ├── CompoundComponent.svg
│ │ │ │ │ ├── RootComponent.svg
│ │ │ │ │ └── tree-with-containers.svg
│ │ │ │ ├── reviewers-guide
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ └── incbutton-in-action.png
│ │ │ │ ├── tools
│ │ │ │ │ └── boilerplate-structure.png
│ │ │ │ ├── try.svg
│ │ │ │ ├── tutorial
│ │ │ │ │ ├── app-chat-history.png
│ │ │ │ │ ├── app-content-placeholder.png
│ │ │ │ │ ├── app-header-and-content.png
│ │ │ │ │ ├── app-links-channel-selected.png
│ │ │ │ │ ├── app-links-click.png
│ │ │ │ │ ├── app-navigation.png
│ │ │ │ │ ├── finished-ex01.png
│ │ │ │ │ ├── finished-ex02.png
│ │ │ │ │ ├── hello.png
│ │ │ │ │ ├── splash-screen-advanced.png
│ │ │ │ │ ├── splash-screen-after-click.png
│ │ │ │ │ ├── splash-screen-centered.png
│ │ │ │ │ ├── splash-screen-events.png
│ │ │ │ │ ├── splash-screen-expression.png
│ │ │ │ │ ├── splash-screen-reuse-after.png
│ │ │ │ │ ├── splash-screen-reuse-before.png
│ │ │ │ │ └── splash-screen.png
│ │ │ │ └── tutorial-01.png
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── Boxes.xmlui
│ │ │ ├── Breadcrumb.xmlui
│ │ │ ├── ChangeLog.xmlui
│ │ │ ├── ColorPalette.xmlui
│ │ │ ├── DocumentLinks.xmlui
│ │ │ ├── DocumentPage.xmlui
│ │ │ ├── DocumentPageNoTOC.xmlui
│ │ │ ├── Icons.xmlui
│ │ │ ├── IncButton.xmlui
│ │ │ ├── IncButton2.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── docs-theme.ts
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── Animation.tsx
│ │ ├── AnimationNative.tsx
│ │ ├── FadeAnimation.tsx
│ │ ├── FadeInAnimation.tsx
│ │ ├── FadeOutAnimation.tsx
│ │ ├── index.tsx
│ │ ├── ScaleAnimation.tsx
│ │ └── SlideInAnimation.tsx
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── HelloWorld.module.scss
│ │ ├── HelloWorld.tsx
│ │ ├── HelloWorldNative.tsx
│ │ └── index.tsx
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── IPhoneFrame.module.scss
│ │ ├── IPhoneFrame.tsx
│ │ ├── MacOSAppFrame.module.scss
│ │ ├── MacOSAppFrame.tsx
│ │ ├── WindowsAppFrame.module.scss
│ │ └── WindowsAppFrame.tsx
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── LazyPdfNative.tsx
│ │ ├── Pdf.module.scss
│ │ └── Pdf.tsx
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── hooks
│ │ │ ├── usePlayground.ts
│ │ │ └── useToast.ts
│ │ ├── index.tsx
│ │ ├── playground
│ │ │ ├── Box.module.scss
│ │ │ ├── Box.tsx
│ │ │ ├── CodeSelector.tsx
│ │ │ ├── ConfirmationDialog.module.scss
│ │ │ ├── ConfirmationDialog.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── Header.module.scss
│ │ │ ├── Header.tsx
│ │ │ ├── Playground.tsx
│ │ │ ├── PlaygroundContent.module.scss
│ │ │ ├── PlaygroundContent.tsx
│ │ │ ├── PlaygroundNative.module.scss
│ │ │ ├── PlaygroundNative.tsx
│ │ │ ├── Preview.module.scss
│ │ │ ├── Preview.tsx
│ │ │ ├── Select.module.scss
│ │ │ ├── StandalonePlayground.tsx
│ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ ├── ThemeSwitcher.module.scss
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ ├── ToneSwitcher.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── providers
│ │ │ ├── Toast.module.scss
│ │ │ └── ToastProvider.tsx
│ │ ├── state
│ │ │ └── store.ts
│ │ ├── themes
│ │ │ └── theme.ts
│ │ └── utils
│ │ └── helpers.ts
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Search.module.scss
│ │ └── Search.tsx
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Spreadsheet.tsx
│ │ └── SpreadsheetNative.tsx
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ └── src
│ ├── Carousel
│ │ ├── Carousel.module.scss
│ │ ├── Carousel.tsx
│ │ ├── CarouselContext.tsx
│ │ └── CarouselNative.tsx
│ ├── FancyButton
│ │ ├── FancyButton.module.scss
│ │ ├── FancyButton.tsx
│ │ └── FancyButton.xmlui
│ ├── Hello
│ │ ├── Hello.tsx
│ │ ├── Hello.xmlui
│ │ └── Hello.xmlui.xs
│ ├── HeroSection
│ │ ├── HeroSection.module.scss
│ │ ├── HeroSection.spec.ts
│ │ ├── HeroSection.tsx
│ │ └── HeroSectionNative.tsx
│ ├── index.tsx
│ ├── ScrollToTop
│ │ ├── ScrollToTop.module.scss
│ │ ├── ScrollToTop.tsx
│ │ └── ScrollToTopNative.tsx
│ └── vite-env.d.ts
├── playwright.config.ts
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.cjs
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── get-langserver-metadata.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.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ ├── LabelListNative.module.scss
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ └── ContentSeparatorNative.tsx
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.js
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── index.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── index.ts
│ │ ├── infrastructure
│ │ │ ├── index.html
│ │ │ ├── main.tsx
│ │ │ ├── public
│ │ │ │ ├── mockServiceWorker.js
│ │ │ │ ├── resources
│ │ │ │ │ ├── bell.svg
│ │ │ │ │ ├── box.svg
│ │ │ │ │ ├── doc.svg
│ │ │ │ │ ├── eye.svg
│ │ │ │ │ ├── flower-640x480.jpg
│ │ │ │ │ ├── sun.svg
│ │ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ │ └── txt.svg
│ │ │ │ └── serve.json
│ │ │ └── TestBed.tsx
│ │ └── themed-app-test-helpers.ts
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.bin.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/xmlui/src/components/Icon/Icon.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { create } from "domain";
2 | import { expect, test } from "../../testing/fixtures";
3 |
4 | // =============================================================================
5 | // BASIC FUNCTIONALITY TESTS
6 | // =============================================================================
7 |
8 | test.describe("Basic Functionality", () => {
9 | test("component renders with valid icon name", async ({ initTestBed, page }) => {
10 | await initTestBed(`<Icon name="home"/>`);
11 | const icon = page.getByTestId("test-id-component");
12 | await expect(icon).toBeVisible();
13 | });
14 |
15 | test("component does not render without name prop", async ({ initTestBed, page }) => {
16 | await initTestBed(`<Icon/>`);
17 | const icon = page.getByTestId("test-id-component");
18 | const exists = await icon.count();
19 | expect(exists).toBe(0); // Icon doesn't render without a valid name
20 | });
21 | });
22 |
23 | // =============================================================================
24 | // NAME PROPERTY TESTS
25 | // =============================================================================
26 |
27 | test.describe("name Property", () => {
28 | test("displays icon when valid name is provided", async ({ initTestBed, page }) => {
29 | await initTestBed(`<Icon name="home"/>`);
30 | const icon = page.getByTestId("test-id-component");
31 | await expect(icon).toBeVisible();
32 | });
33 |
34 | test("handles non-existent icon name gracefully", async ({ initTestBed, page }) => {
35 | await initTestBed(`<Icon name="non-existent-icon"/>`);
36 | const icon = page.getByTestId("test-id-component");
37 | // Component doesn't render when icon name doesn't exist
38 | const exists = await icon.count();
39 | expect(exists).toBe(0);
40 | });
41 |
42 | test("handles empty string name", async ({ initTestBed, page }) => {
43 | await initTestBed(`<Icon name=""/>`);
44 | const icon = page.getByTestId("test-id-component");
45 | // Component doesn't render with empty string name
46 | const exists = await icon.count();
47 | expect(exists).toBe(0);
48 | });
49 |
50 | test("handles null name value", async ({ initTestBed, page }) => {
51 | await initTestBed(`<Icon name="{null}"/>`);
52 | const icon = page.getByTestId("test-id-component");
53 | // Component doesn't render with null name
54 | const exists = await icon.count();
55 | expect(exists).toBe(0);
56 | });
57 |
58 | test("handles undefined name value", async ({ initTestBed, page }) => {
59 | await initTestBed(`<Icon name="{undefined}"/>`);
60 | const icon = page.getByTestId("test-id-component");
61 | // Component doesn't render with undefined name
62 | const exists = await icon.count();
63 | expect(exists).toBe(0);
64 | });
65 |
66 | test("handles special characters in name", async ({ initTestBed, page }) => {
67 | await initTestBed(`<Icon name="test-icon_with$pecial@chars"/>`);
68 | const icon = page.getByTestId("test-id-component");
69 | // Component doesn't render with non-existent special character name
70 | const exists = await icon.count();
71 | expect(exists).toBe(0);
72 | });
73 |
74 | test("handles unicode characters in name", async ({ initTestBed, page }) => {
75 | await initTestBed(`<Icon name="测试图标"/>`);
76 | const icon = page.getByTestId("test-id-component");
77 | // Component doesn't render with non-existent unicode name
78 | const exists = await icon.count();
79 | expect(exists).toBe(0);
80 | });
81 |
82 | test("handles emoji in name", async ({ initTestBed, page }) => {
83 | await initTestBed(`<Icon name="🏠"/>`);
84 | const icon = page.getByTestId("test-id-component");
85 | // Component doesn't render with non-existent emoji name
86 | const exists = await icon.count();
87 | expect(exists).toBe(0);
88 | });
89 |
90 | test("handles component-specific icon name syntax", async ({ initTestBed, page }) => {
91 | await initTestBed(`<Icon name="component:specific-icon"/>`);
92 | const icon = page.getByTestId("test-id-component");
93 | // Component doesn't render with non-existent component-specific name
94 | const exists = await icon.count();
95 | expect(exists).toBe(0);
96 | });
97 |
98 | test("handles very long icon name", async ({ initTestBed, page }) => {
99 | const longName = "a".repeat(1000);
100 | await initTestBed(`<Icon name="${longName}"/>`);
101 | const icon = page.getByTestId("test-id-component");
102 | // Component doesn't render with non-existent long name
103 | const exists = await icon.count();
104 | expect(exists).toBe(0);
105 | });
106 | });
107 |
108 | // =============================================================================
109 | // SIZE PROPERTY TESTS
110 | // =============================================================================
111 |
112 | test.describe("size Property", () => {
113 | test("renders with predefined size 'xs'", async ({ initTestBed, createIconDriver }) => {
114 | await initTestBed(`<Icon testId="icon" name="home" size="xs"/>`);
115 | const iconDrv = await createIconDriver("icon");
116 | const icon = iconDrv.svgIcon;
117 | await expect(icon).toBeVisible();
118 | // Check computed width/height using CSS
119 | const computedStyle = await icon.evaluate((el) => {
120 | const style = window.getComputedStyle(el);
121 | return { width: style.width, height: style.height };
122 | });
123 | expect(computedStyle.width).toBe("12px"); // 0.75em calculated
124 | });
125 |
126 | test("renders with predefined size 'sm'", async ({ initTestBed, createIconDriver }) => {
127 | await initTestBed(`<Icon testId="icon" name="home" size="sm"/>`);
128 | const iconDrv = await createIconDriver("icon");
129 | const icon = iconDrv.svgIcon;
130 | await expect(icon).toBeVisible();
131 | const computedStyle = await icon.evaluate((el) => {
132 | const style = window.getComputedStyle(el);
133 | return { width: style.width, height: style.height };
134 | });
135 | expect(computedStyle.width).toBe("16px"); // 1em calculated
136 | });
137 |
138 | test("renders with predefined size 'md'", async ({ initTestBed, createIconDriver }) => {
139 | await initTestBed(`<Icon testId="icon" name="home" size="md"/>`);
140 | const iconDrv = await createIconDriver("icon");
141 | const icon = iconDrv.svgIcon;
142 | await expect(icon).toBeVisible();
143 | const computedStyle = await icon.evaluate((el) => {
144 | const style = window.getComputedStyle(el);
145 | return { width: style.width, height: style.height };
146 | });
147 | expect(computedStyle.width).toBe("24px"); // 1.5rem calculated
148 | });
149 |
150 | test("renders with predefined size 'lg'", async ({ initTestBed, createIconDriver }) => {
151 | await initTestBed(`<Icon testId="icon" name="home" size="lg"/>`);
152 | const iconDrv = await createIconDriver("icon");
153 | const icon = iconDrv.svgIcon;
154 | await expect(icon).toBeVisible();
155 | const computedStyle = await icon.evaluate((el) => {
156 | const style = window.getComputedStyle(el);
157 | return { width: style.width, height: style.height };
158 | });
159 | expect(computedStyle.width).toBe("32px"); // 2em calculated
160 | });
161 |
162 | test("renders with custom pixel size", async ({ initTestBed, createIconDriver }) => {
163 | await initTestBed(`<Icon testId="icon" name="home" size="48px"/>`);
164 | const iconDrv = await createIconDriver("icon");
165 | const icon = iconDrv.svgIcon;
166 | await expect(icon).toBeVisible();
167 | const computedStyle = await icon.evaluate((el) => {
168 | const style = window.getComputedStyle(el);
169 | return { width: style.width, height: style.height };
170 | });
171 | expect(computedStyle.width).toBe("48px");
172 | });
173 |
174 | test("renders with custom em size", async ({ initTestBed, createIconDriver }) => {
175 | await initTestBed(`<Icon testId="icon" name="home" size="3em"/>`);
176 | const iconDrv = await createIconDriver("icon");
177 | const icon = iconDrv.svgIcon;
178 | await expect(icon).toBeVisible();
179 | const computedStyle = await icon.evaluate((el) => {
180 | const style = window.getComputedStyle(el);
181 | return { width: style.width, height: style.height };
182 | });
183 | expect(computedStyle.width).toBe("48px"); // 3em calculated at 16px base
184 | });
185 |
186 | test("renders with custom rem size", async ({ initTestBed, createIconDriver }) => {
187 | await initTestBed(`<Icon testId="icon" name="home" size="2rem"/>`);
188 | const iconDrv = await createIconDriver("icon");
189 | const icon = iconDrv.svgIcon;
190 | await expect(icon).toBeVisible();
191 | const computedStyle = await icon.evaluate((el) => {
192 | const style = window.getComputedStyle(el);
193 | return { width: style.width, height: style.height };
194 | });
195 | expect(computedStyle.width).toBe("32px"); // 2rem calculated
196 | });
197 |
198 | test("handles invalid size gracefully", async ({ initTestBed, createIconDriver }) => {
199 | await initTestBed(`<Icon testId="icon" name="home" size="invalid-size"/>`);
200 | const iconDrv = await createIconDriver("icon");
201 | const icon = iconDrv.svgIcon;
202 | await expect(icon).not.toBeVisible();
203 | });
204 |
205 | test("handles negative size value", async ({ initTestBed, createIconDriver }) => {
206 | await initTestBed(`<Icon testId="icon" name="home" size="-20px"/>`);
207 | const iconDrv = await createIconDriver("icon");
208 | const icon = iconDrv.svgIcon;
209 |
210 | // With negative size, element exists but browser normalizes to 0px
211 | const exists = await icon.count();
212 | expect(exists).toBe(1);
213 |
214 | const computedStyle = await icon.evaluate((el) => {
215 | const style = window.getComputedStyle(el);
216 | return { width: style.width, height: style.height };
217 | });
218 | // Browser normalizes negative values to 0px
219 | expect(computedStyle.width).toBe("0px");
220 | });
221 |
222 | test("handles zero size value", async ({ initTestBed, createIconDriver }) => {
223 | await initTestBed(`<Icon testId="icon" name="home" size="0px"/>`);
224 | const iconDrv = await createIconDriver("icon");
225 | const icon = iconDrv.svgIcon;
226 | // Zero-size elements might not be "visible" but should exist
227 | const exists = await icon.count();
228 | expect(exists).toBe(1);
229 |
230 | const computedStyle = await icon.evaluate((el) => {
231 | const style = window.getComputedStyle(el);
232 | return { width: style.width, height: style.height };
233 | });
234 | expect(computedStyle.width).toBe("0px");
235 | });
236 |
237 | test("handles null size value", async ({ initTestBed, createIconDriver }) => {
238 | await initTestBed(`<Icon testId="icon" name="home" size="{null}"/>`);
239 | const iconDrv = await createIconDriver("icon");
240 | const icon = iconDrv.svgIcon;
241 | await expect(icon).toBeVisible();
242 | });
243 |
244 | test("handles undefined size value", async ({ initTestBed, createIconDriver }) => {
245 | await initTestBed(`<Icon testId="icon" name="home" size="{undefined}"/>`);
246 | const iconDrv = await createIconDriver("icon");
247 | const icon = iconDrv.svgIcon;
248 | await expect(icon).toBeVisible();
249 | });
250 | });
251 |
252 | // =============================================================================
253 | // FALLBACK PROPERTY TESTS
254 | // =============================================================================
255 |
256 | test.describe("fallback Property", () => {
257 | test("displays fallback icon when primary icon doesn't exist", async ({ initTestBed, page }) => {
258 | await initTestBed(`<Icon name="non-existent-icon" fallback="trash"/>`);
259 | const icon = page.getByTestId("test-id-component");
260 | await expect(icon).toBeVisible();
261 | });
262 |
263 | test("ignores fallback when primary icon exists", async ({ initTestBed, page }) => {
264 | await initTestBed(`<Icon name="home" fallback="trash"/>`);
265 | const icon = page.getByTestId("test-id-component");
266 | await expect(icon).toBeVisible();
267 | });
268 |
269 | test("handles empty string fallback", async ({ initTestBed, page }) => {
270 | await initTestBed(`<Icon name="non-existent-icon" fallback=""/>`);
271 | const icon = page.getByTestId("test-id-component");
272 | // Empty fallback means no icon renders
273 | const exists = await icon.count();
274 | expect(exists).toBe(0);
275 | });
276 |
277 | test("handles null fallback value", async ({ initTestBed, page }) => {
278 | await initTestBed(`<Icon name="non-existent-icon" fallback="{null}"/>`);
279 | const icon = page.getByTestId("test-id-component");
280 | // Null fallback means no icon renders
281 | const exists = await icon.count();
282 | expect(exists).toBe(0);
283 | });
284 |
285 | test("handles undefined fallback value", async ({ initTestBed, page }) => {
286 | await initTestBed(`<Icon name="non-existent-icon" fallback="{undefined}"/>`);
287 | const icon = page.getByTestId("test-id-component");
288 | // Undefined fallback means no icon renders
289 | const exists = await icon.count();
290 | expect(exists).toBe(0);
291 | });
292 |
293 | test("handles special characters in fallback", async ({ initTestBed, page }) => {
294 | await initTestBed(`<Icon name="non-existent-icon" fallback="test-icon_with$pecial@chars"/>`);
295 | const icon = page.getByTestId("test-id-component");
296 | // Non-existent fallback means no icon renders
297 | const exists = await icon.count();
298 | expect(exists).toBe(0);
299 | });
300 |
301 | test("handles non-existent fallback icon", async ({ initTestBed, page }) => {
302 | await initTestBed(`<Icon name="non-existent-icon" fallback="also-non-existent"/>`);
303 | const icon = page.getByTestId("test-id-component");
304 | // Non-existent fallback means no icon renders
305 | const exists = await icon.count();
306 | expect(exists).toBe(0);
307 | });
308 | });
309 |
310 | // =============================================================================
311 | // CLICK EVENT TESTS
312 | // =============================================================================
313 |
314 | test.describe("click Event", () => {
315 | test("fires click event when icon is clicked", async ({ initTestBed, createIconDriver }) => {
316 | const { testStateDriver } = await initTestBed(`
317 | <Icon testId="icon" name="home" onClick="testState = 'clicked'"/>
318 | `);
319 |
320 | const iconDrv = await createIconDriver("icon");
321 | const icon = iconDrv.svgIcon;
322 | await icon.click();
323 |
324 | await expect.poll(testStateDriver.testState).toEqual("clicked");
325 | });
326 |
327 | test("applies clickable cursor when click handler is present", async ({
328 | initTestBed,
329 | createIconDriver,
330 | }) => {
331 | await initTestBed(`<Icon testId="icon" name="home" onClick="testState = 'clicked'"/>`);
332 |
333 | const iconDrv = await createIconDriver("icon");
334 | const icon = iconDrv.svgIcon;
335 | await expect(icon).toHaveCSS("cursor", "pointer");
336 | });
337 |
338 | test("does not apply clickable cursor when no click handler", async ({ initTestBed, createIconDriver }) => {
339 | await initTestBed(`<Icon testId="icon" name="home"/>`);
340 | const iconDrv = await createIconDriver("icon");
341 | const icon = iconDrv.svgIcon;
342 | const cursor = await icon.evaluate((el) => window.getComputedStyle(el).cursor);
343 | expect(cursor).not.toBe("pointer");
344 | });
345 |
346 | test("click event provides event object", async ({ initTestBed, createIconDriver }) => {
347 | const { testStateDriver } = await initTestBed(`
348 | <Icon testId="icon" name="home" onClick="event => testState = event.type"/>
349 | `);
350 |
351 | const iconDrv = await createIconDriver("icon");
352 | const icon = iconDrv.svgIcon;
353 | await icon.click();
354 |
355 | await expect.poll(testStateDriver.testState).toEqual("click");
356 | });
357 |
358 | test("multiple clicks increment counter", async ({ initTestBed, createIconDriver }) => {
359 | const { testStateDriver } = await initTestBed(`
360 | <Icon testId="icon" name="home" onClick="testState = (testState || 0) + 1"/>
361 | `);
362 |
363 | const iconDrv = await createIconDriver("icon");
364 | const icon = iconDrv.svgIcon;
365 |
366 | await icon.click();
367 | await expect.poll(testStateDriver.testState).toEqual(1);
368 |
369 | await icon.click();
370 | await expect.poll(testStateDriver.testState).toEqual(2);
371 |
372 | await icon.click();
373 | await expect.poll(testStateDriver.testState).toEqual(3);
374 | });
375 | });
376 |
377 | // =============================================================================
378 | // ACCESSIBILITY TESTS
379 | // =============================================================================
380 |
381 | test.describe("Accessibility", () => {
382 | test("icon renders as inline element by default", async ({ initTestBed, createIconDriver }) => {
383 | await initTestBed(`<Icon testId="icon" name="home"/>`);
384 | const icon = await createIconDriver("icon");
385 | const display = await icon.svgIcon.evaluate((el) => window.getComputedStyle(el).display);
386 | expect(display).toBe("inline-block");
387 | });
388 |
389 | test("icon has correct vertical alignment", async ({ initTestBed, createIconDriver }) => {
390 | await initTestBed(`<Icon testId="icon" name="home"/>`);
391 | const icon = await createIconDriver("icon");
392 |
393 | await expect(icon.svgIcon as any).toHaveCSS("vertical-align", "text-bottom");
394 | });
395 |
396 | test("icon inherits color from parent", async ({ initTestBed, createIconDriver }) => {
397 | await initTestBed(`
398 | <Text color="red">
399 | <Icon testId="icon" name="home"/>
400 | </Text>
401 | `);
402 | const icon = await createIconDriver("icon");
403 |
404 | const color = await icon.svgIcon.evaluate((el) => window.getComputedStyle(el).color);
405 | // Should inherit red color or have some computed red value
406 | expect(color).not.toBe(""); // Should have some color value
407 | });
408 |
409 | test("icon is clickable when click handler is present", async ({ initTestBed, createIconDriver }) => {
410 | const { testStateDriver } = await initTestBed(`
411 | <Icon testId="icon" name="home" onClick="testState = 'activated'"/>
412 | `);
413 |
414 | const icon = await createIconDriver("icon");
415 | await icon.click();
416 |
417 | await expect.poll(testStateDriver.testState).toEqual("activated");
418 | });
419 |
420 | test(
421 | "icon can receive focus via tabIndex when clickable",
422 | async ({ initTestBed, createIconDriver }) => {
423 | await initTestBed(`<Icon testId="icon" name="home" onClick="testState = 'clicked'" />`);
424 |
425 | const icon = (await createIconDriver("icon")).svgIcon;
426 |
427 | // For SVG elements, we test focus capability by checking if it can be found after focus
428 | await expect(icon).toBeVisible();
429 | await icon.focus();
430 | await expect(icon).toBeFocused();
431 | },
432 | );
433 |
434 | test(
435 | "icon supports keyboard activation with Enter",
436 | async ({ initTestBed, createIconDriver }) => {
437 | const testBed = await initTestBed(`<Icon testId="icon" name="home" onClick="testState = 'clicked';" />`);
438 | const { testStateDriver } = testBed;
439 |
440 | const driver = await createIconDriver("icon");
441 | const icon = driver.svgIcon;
442 |
443 | // For SVG elements, we test focus capability by checking if it can be found after focus
444 | await expect(icon).toBeVisible();
445 | await icon.press("Enter");
446 | expect(await testStateDriver.testState()).toBe("clicked");
447 | },
448 | );
449 |
450 | test(
451 | "icon supports keyboard activation with Space",
452 | async ({ initTestBed, createIconDriver }) => {
453 | const testBed = await initTestBed(`<Icon testId="icon" name="home" onClick="testState = 'clicked';" />`);
454 | const { testStateDriver } = testBed;
455 |
456 | const driver = await createIconDriver("icon");
457 | const icon = driver.svgIcon;
458 |
459 | // For SVG elements, we test focus capability by checking if it can be found after focus
460 | await expect(icon).toBeVisible();
461 | await icon.press("Space");
462 | expect(await testStateDriver.testState()).toBe("clicked");
463 | },
464 | );
465 |
466 | });
467 |
468 | // =============================================================================
469 | // THEME VARIABLE TESTS
470 | // =============================================================================
471 |
472 | test.describe("Theme Variables", () => {
473 | test("applies theme variable for size", async ({ initTestBed, createIconDriver }) => {
474 | await initTestBed(`<Icon testId="icon" name="home"/>`, {
475 | testThemeVars: {
476 | "size-Icon": "3rem",
477 | },
478 | });
479 |
480 | const iconDrv = await createIconDriver("icon");
481 | const icon = iconDrv.svgIcon;
482 | const computedStyle = await icon.evaluate((el) => {
483 | const style = window.getComputedStyle(el);
484 | return { width: style.width, height: style.height };
485 | });
486 |
487 | expect(computedStyle.width).toBe("48px"); // 3rem calculated
488 | expect(computedStyle.height).toBe("48px");
489 | });
490 |
491 | test("size prop overrides theme variable", async ({ initTestBed, createIconDriver }) => {
492 | await initTestBed(`<Icon testId="icon" name="home" size="2rem"/>`, {
493 | testThemeVars: {
494 | "size-Icon": "4rem",
495 | },
496 | });
497 |
498 | const icon = await createIconDriver("icon");
499 | const computedStyle = await icon.svgIcon.evaluate((el) => {
500 | const style = window.getComputedStyle(el);
501 | return { width: style.width, height: style.height };
502 | });
503 |
504 | expect(computedStyle.width).toBe("32px"); // 2rem calculated, not 4rem from theme
505 | expect(computedStyle.height).toBe("32px");
506 | });
507 |
508 | test("applies theme variable with custom CSS variable", async ({ initTestBed, createIconDriver }) => {
509 | await initTestBed(`<Icon testId="icon" name="home"/>`, {
510 | testThemeVars: {
511 | "size-Icon": "var(--custom-icon-size, 40px)",
512 | },
513 | });
514 |
515 | const icon = await createIconDriver("icon");
516 | const computedStyle = await icon.svgIcon.evaluate((el) => {
517 | const style = window.getComputedStyle(el);
518 | return { width: style.width, height: style.height };
519 | });
520 |
521 | expect(computedStyle.width).toBe("40px"); // Fallback value used
522 | expect(computedStyle.height).toBe("40px");
523 | });
524 | });
525 |
526 | // =============================================================================
527 | // OTHER EDGE CASE TESTS
528 | // =============================================================================
529 |
530 | test.describe("Other Edge Cases", () => {
531 | test("handles null and undefined props gracefully", async ({ initTestBed, createIconDriver }) => {
532 | await initTestBed(`<Icon testId="icon" name="home"/>`);
533 | const icon = await createIconDriver("icon");
534 | await expect(icon.svgIcon).toBeVisible();
535 | });
536 |
537 | test("handles component with all invalid props gracefully", async ({ initTestBed, createIconDriver }) => {
538 | await initTestBed(`<Icon testId="icon" invalidProp="invalid" anotherInvalid="{123}"/>`);
539 |
540 | // Component with invalid props may not be visible, but should not crash
541 | const icon = await createIconDriver("icon");
542 | const isVisible = await icon.svgIcon.isVisible();
543 |
544 | // Either it's visible with fallback behavior or gracefully hidden
545 | if (isVisible) {
546 | // If visible, it should have fallback behavior
547 | await expect(icon.svgIcon).toBeVisible();
548 | } else {
549 | // It's acceptable for component to be hidden with invalid props
550 | expect(isVisible).toBe(false);
551 | }
552 | });
553 |
554 | test("handles mixed valid and invalid props", async ({ initTestBed, createIconDriver }) => {
555 | await initTestBed(`<Icon testId="icon" name="home" invalidProp="invalid" size="lg"/>`);
556 | const iconDrv = await createIconDriver("icon");
557 | const icon = iconDrv.svgIcon;
558 | await expect(icon).toBeVisible();
559 |
560 | // Valid props should still work
561 | const computedStyle = await icon.evaluate((el) => {
562 | const style = window.getComputedStyle(el);
563 | return { width: style.width, height: style.height };
564 | });
565 | expect(computedStyle.width).toBe("32px"); // lg size should apply
566 | });
567 |
568 | test("handles object value for name prop", async ({ initTestBed, createIconDriver }) => {
569 | await initTestBed(`<Icon testId="icon" name="{{}}" />`);
570 |
571 | const icon = await createIconDriver("icon");
572 | const isVisible = await icon.svgIcon.isVisible();
573 |
574 | if (isVisible) {
575 | await expect(icon.svgIcon).toBeVisible();
576 | } else {
577 | expect(isVisible).toBe(false);
578 | }
579 | });
580 |
581 | test("handles array value for size prop", async ({ initTestBed, createIconDriver }) => {
582 | await initTestBed(`<Icon testId="icon" name="home" size="{[]}" />`);
583 |
584 | const icon = await createIconDriver("icon");
585 | const isVisible = await icon.svgIcon.isVisible();
586 |
587 | if (isVisible) {
588 | await expect(icon.svgIcon).toBeVisible();
589 | } else {
590 | expect(isVisible).toBe(false);
591 | }
592 | });
593 |
594 | test("handles very long Unicode string in fallback", async ({ initTestBed, createIconDriver }) => {
595 | const longUnicodeString = "👨👩👧👦".repeat(100);
596 | await initTestBed(`<Icon testId="icon" name="home" fallback="${longUnicodeString}"/>`);
597 | const icon = await createIconDriver("icon");
598 | await expect(icon.svgIcon).toBeVisible(); // Icon renders with valid primary name, fallback ignored
599 | });
600 |
601 | test("handles concurrent property changes", async ({ initTestBed, createIconDriver }) => {
602 | await initTestBed(`<Icon testId="icon" name="home" size="sm"/>`);
603 | const icon = await createIconDriver("icon");
604 | await expect(icon.svgIcon).toBeVisible();
605 |
606 | // Test that initial state is correct
607 | let computedStyle = await icon.svgIcon.evaluate((el) => {
608 | const style = window.getComputedStyle(el);
609 | return { width: style.width, height: style.height };
610 | });
611 | expect(computedStyle.width).toBe("16px"); // sm size
612 | });
613 |
614 | test("handles rapid sequential clicks", async ({ initTestBed, createIconDriver }) => {
615 | const { testStateDriver } = await initTestBed(`
616 | <Icon testId="icon" name="home" onClick="testState = (testState || 0) + 1"/>
617 | `);
618 |
619 | const iconDrv = await createIconDriver("icon");
620 | const icon = iconDrv.svgIcon;
621 |
622 | // Rapid sequential clicks
623 | await Promise.all([icon.click(), icon.click(), icon.click(), icon.click(), icon.click()]);
624 |
625 | // Should eventually reach 5 (all clicks processed)
626 | await expect.poll(testStateDriver.testState).toEqual(5);
627 | });
628 | });
629 |
```
--------------------------------------------------------------------------------
/xmlui/src/components-core/script-runner/eval-tree-sync.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { LogicalThread } from "../../abstractions/scripting/LogicalThread";
2 | import type { Identifier, TemplateLiteralExpression } from "./ScriptingSourceTree";
3 | import {
4 | T_ARRAY_LITERAL,
5 | T_ARROW_EXPRESSION,
6 | T_ASSIGNMENT_EXPRESSION,
7 | T_BINARY_EXPRESSION,
8 | T_BLOCK_STATEMENT,
9 | T_CALCULATED_MEMBER_ACCESS_EXPRESSION,
10 | T_CONDITIONAL_EXPRESSION,
11 | T_DESTRUCTURE,
12 | T_EMPTY_STATEMENT,
13 | T_EXPRESSION_STATEMENT,
14 | T_FUNCTION_INVOCATION_EXPRESSION,
15 | T_IDENTIFIER,
16 | T_LITERAL,
17 | T_MEMBER_ACCESS_EXPRESSION,
18 | T_OBJECT_LITERAL,
19 | T_POSTFIX_OP_EXPRESSION,
20 | T_PREFIX_OP_EXPRESSION,
21 | T_RETURN_STATEMENT,
22 | T_SEQUENCE_EXPRESSION,
23 | T_SPREAD_EXPRESSION,
24 | T_TEMPLATE_LITERAL_EXPRESSION,
25 | T_UNARY_EXPRESSION,
26 | T_VAR_DECLARATION,
27 | type ArrayLiteral,
28 | type ArrowExpression,
29 | type AssignmentExpression,
30 | type BinaryExpression,
31 | type CalculatedMemberAccessExpression,
32 | type ConditionalExpression,
33 | type Expression,
34 | type FunctionInvocationExpression,
35 | type MemberAccessExpression,
36 | type ObjectLiteral,
37 | type PostfixOpExpression,
38 | type PrefixOpExpression,
39 | type SequenceExpression,
40 | type Statement,
41 | type UnaryExpression,
42 | type VarDeclaration,
43 | } from "./ScriptingSourceTree";
44 | import type { BlockScope } from "../../abstractions/scripting/BlockScope";
45 | import { createXmlUiTreeNodeId, Parser } from "../../parsers/scripting/Parser";
46 | import type { BindingTreeEvaluationContext } from "./BindingTreeEvaluationContext";
47 | import { isBannedFunction } from "./bannedFunctions";
48 | import {
49 | evalArrow,
50 | evalAssignmentCore,
51 | evalBinaryCore,
52 | evalCalculatedMemberAccessCore,
53 | evalIdentifier,
54 | evalLiteral,
55 | evalMemberAccessCore,
56 | evalPreOrPostCore,
57 | evalTemplateLiteralCore,
58 | evalUnaryCore,
59 | getExprValue,
60 | getRootIdScope,
61 | isPromise,
62 | setExprValue,
63 | } from "./eval-tree-common";
64 | import { ensureMainThread } from "./process-statement-common";
65 | import { processDeclarations, processStatementQueue } from "./process-statement-sync";
66 |
67 | // --- The type of function we use to evaluate a (partial) expression tree
68 | type EvaluatorFunction = (
69 | thisStack: any[],
70 | expr: Expression,
71 | evalContext: BindingTreeEvaluationContext,
72 | thread: LogicalThread,
73 | ) => any;
74 |
75 | /**
76 | * Evaluates the specified binding expression tree and retrieves the evaluated value
77 | * @param source Binding tree expression
78 | * @param evalContext Evaluation context
79 | * @param thread The logical thread to use for evaluation
80 | * This code uses the JavaScript semantics and errors when evaluating the code.
81 | */
82 | export function evalBindingExpression(
83 | source: string,
84 | evalContext: BindingTreeEvaluationContext,
85 | thread?: LogicalThread,
86 | ): any {
87 | // --- Use the main thread by default
88 | thread ??= evalContext.mainThread;
89 |
90 | // --- Parse the source code
91 | const wParser = new Parser(source);
92 | const tree = wParser.parseExpr();
93 | if (tree === null) {
94 | // --- This should happen only when an expression is empty
95 | return undefined;
96 | }
97 |
98 | // --- Check for expression termination
99 | if (!wParser.isEof) {
100 | throw new Error("Expression is not terminated properly");
101 | }
102 |
103 | // --- Ok, valid source, evaluate
104 | return evalBinding(tree, evalContext, thread);
105 | }
106 |
107 | /**
108 | * Evaluates a binding represented by the specified expression
109 | * @param expr Expression to evaluate
110 | * @param evalContext Evaluation context to use
111 | * @param thread The logical thread to use for evaluation
112 | */
113 | export function evalBinding(
114 | expr: Expression,
115 | evalContext: BindingTreeEvaluationContext,
116 | thread?: LogicalThread,
117 | ): any {
118 | const thisStack: any[] = [];
119 | ensureMainThread(evalContext);
120 | thread ??= evalContext.mainThread;
121 | return evalBindingExpressionTree(thisStack, expr, evalContext, thread ?? evalContext.mainThread!);
122 | }
123 |
124 | /**
125 | * Executes the specified arrow function
126 | * @param expr Arrow function expression to run
127 | * @param evalContext Evaluation context to use
128 | * @param thread The logical thread to use for evaluation
129 | * @param args Arguments of the arrow function to execute
130 | */
131 | export function executeArrowExpressionSync(
132 | expr: ArrowExpression,
133 | evalContext: BindingTreeEvaluationContext,
134 | thread?: LogicalThread,
135 | ...args: any[]
136 | ): Promise<any> {
137 | // --- Just an extra safety check
138 | if (expr.type !== T_ARROW_EXPRESSION) {
139 | throw new Error("executeArrowExpression expects an 'ArrowExpression' object.");
140 | }
141 |
142 | // --- This is the evaluator that an arrow expression uses internally
143 | const evaluator: EvaluatorFunction = evalBindingExpressionTree;
144 |
145 | // --- Compiles the Arrow function to a JavaScript function
146 | const nativeFunction = createArrowFunction(evaluator, expr, evalContext);
147 |
148 | // --- Run the compiled arrow function. Note, we have two prefix arguments:
149 | // --- #1: The names of arrow function arguments
150 | // --- #2: The evaluation context the arrow function runs in
151 | // --- #others: The real arguments of the arrow function
152 | return nativeFunction(expr.args, evalContext, thread ?? evalContext.mainThread, ...args);
153 | }
154 |
155 | /**
156 | * Evaluates the specified binding expression tree and retrieves the evaluated value
157 | * @param expr Binding tree expression
158 | * @param thisStack Stack of "this" object to use with function calls
159 | * @param evalContext Evaluation context
160 | * @param thread The logical thread to use for evaluation
161 | * This code uses the JavaScript semantics and errors when evaluating the code.
162 | * We use `thisStack` to keep track of the partial results of the evaluation tree so that we can set
163 | * the real `this` context when invoking a function.
164 | */
165 | function evalBindingExpressionTree(
166 | thisStack: any[],
167 | expr: Expression,
168 | evalContext: BindingTreeEvaluationContext,
169 | thread: LogicalThread,
170 | ): any {
171 | if (!evalContext.options) {
172 | evalContext.options = { defaultToOptionalMemberAccess: true };
173 | }
174 |
175 | // --- Prepare evaluation
176 | const evaluator: EvaluatorFunction = evalBindingExpressionTree;
177 |
178 | // --- Process the expression according to its type
179 | switch (expr.type) {
180 | case T_TEMPLATE_LITERAL_EXPRESSION:
181 | return evalTemplateLiteral(evaluator, thisStack, expr, evalContext, thread);
182 |
183 | case T_LITERAL:
184 | return evalLiteral(thisStack, expr, thread);
185 |
186 | case T_IDENTIFIER:
187 | return evalIdentifier(thisStack, expr, evalContext, thread);
188 |
189 | case T_MEMBER_ACCESS_EXPRESSION:
190 | return evalMemberAccess(evaluator, thisStack, expr, evalContext, thread);
191 |
192 | case T_CALCULATED_MEMBER_ACCESS_EXPRESSION:
193 | return evalCalculatedMemberAccess(evaluator, thisStack, expr, evalContext, thread);
194 |
195 | case T_SEQUENCE_EXPRESSION:
196 | return evalSequence(evaluator, thisStack, expr, evalContext, thread);
197 |
198 | case T_ARRAY_LITERAL:
199 | return evalArrayLiteral(evaluator, thisStack, expr, evalContext, thread);
200 |
201 | case T_OBJECT_LITERAL:
202 | return evalObjectLiteral(evaluator, thisStack, expr, evalContext, thread);
203 |
204 | case T_UNARY_EXPRESSION:
205 | return evalUnary(evaluator, thisStack, expr, evalContext, thread);
206 |
207 | case T_BINARY_EXPRESSION:
208 | return evalBinary(evaluator, thisStack, expr, evalContext, thread);
209 |
210 | case T_CONDITIONAL_EXPRESSION:
211 | return evalConditional(evaluator, thisStack, expr, evalContext, thread);
212 |
213 | case T_ASSIGNMENT_EXPRESSION:
214 | return evalAssignment(evaluator, thisStack, expr, evalContext, thread);
215 |
216 | case T_PREFIX_OP_EXPRESSION:
217 | case T_POSTFIX_OP_EXPRESSION:
218 | return evalPreOrPost(evaluator, thisStack, expr, evalContext, thread);
219 |
220 | case T_FUNCTION_INVOCATION_EXPRESSION:
221 | // --- Special sync handling
222 | const funcResult = evalFunctionInvocation(evaluator, thisStack, expr, evalContext, thread);
223 | if (isPromise(funcResult)) {
224 | throw new Error("Promises (async function calls) are not allowed in binding expressions.");
225 | }
226 | return funcResult;
227 |
228 | case T_ARROW_EXPRESSION:
229 | // --- Special sync handling
230 | return evalArrow(thisStack, expr, thread);
231 |
232 | case T_SPREAD_EXPRESSION:
233 | throw new Error("Cannot use spread expression (...) with the current intermediate value.");
234 |
235 | default:
236 | throw new Error(`Unknown expression tree node: ${(expr as any).type}`);
237 | }
238 | }
239 |
240 | function evalTemplateLiteral(
241 | evaluator: EvaluatorFunction,
242 | thisStack: any[],
243 | expr: TemplateLiteralExpression,
244 | evalContext: BindingTreeEvaluationContext,
245 | thread: LogicalThread,
246 | ): any {
247 | const segmentValues = expr.segments.map((s) => {
248 | const evaledValue = evaluator(thisStack, s, evalContext, thread);
249 | thisStack.pop();
250 | return evaledValue;
251 | });
252 | const value = evalTemplateLiteralCore(segmentValues);
253 | setExprValue(expr, { value }, thread);
254 | thisStack.push(value);
255 | return value;
256 | }
257 |
258 | function evalMemberAccess(
259 | evaluator: EvaluatorFunction,
260 | thisStack: any[],
261 | expr: MemberAccessExpression,
262 | evalContext: BindingTreeEvaluationContext,
263 | thread: LogicalThread,
264 | ): any {
265 | evaluator(thisStack, expr.obj, evalContext, thread);
266 | // --- At this point we definitely keep the parent object on `thisStack`, as it will be the context object
267 | // --- of a FunctionInvocationExpression, if that follows the MemberAccess. Other operations would call
268 | // --- `thisStack.pop()` to remove the result from the previous `evalBindingExpressionTree` call.
269 | return evalMemberAccessCore(thisStack, expr, evalContext, thread);
270 | }
271 |
272 | function evalCalculatedMemberAccess(
273 | evaluator: EvaluatorFunction,
274 | thisStack: any[],
275 | expr: CalculatedMemberAccessExpression,
276 | evalContext: BindingTreeEvaluationContext,
277 | thread: LogicalThread,
278 | ): any {
279 | evaluator(thisStack, expr.obj, evalContext, thread);
280 | evaluator(thisStack, expr.member, evalContext, thread);
281 |
282 | thisStack.pop();
283 | return evalCalculatedMemberAccessCore(thisStack, expr, evalContext, thread);
284 | }
285 |
286 | function evalSequence(
287 | evaluator: EvaluatorFunction,
288 | thisStack: any[],
289 | expr: SequenceExpression,
290 | evalContext: BindingTreeEvaluationContext,
291 | thread: LogicalThread,
292 | ): any {
293 | if (!expr.exprs || expr.exprs.length === 0) {
294 | throw new Error(`Missing expression sequence`);
295 | }
296 | const result = expr.exprs.map((e) => {
297 | const value = evaluator(thisStack, e, evalContext, thread);
298 | setExprValue(e, { value }, thread);
299 | thisStack.pop();
300 | return value;
301 | });
302 | const lastObj = result[result.length - 1];
303 | thisStack.push(lastObj);
304 | return lastObj;
305 | }
306 |
307 | function evalArrayLiteral(
308 | evaluator: EvaluatorFunction,
309 | thisStack: any[],
310 | expr: ArrayLiteral,
311 | evalContext: BindingTreeEvaluationContext,
312 | thread: LogicalThread,
313 | ): any {
314 | const value: any[] = [];
315 | for (const item of expr.items) {
316 | if (item.type === T_SPREAD_EXPRESSION) {
317 | const spreadArray = evaluator(thisStack, item.expr, evalContext, thread);
318 | thisStack.pop();
319 | if (!Array.isArray(spreadArray)) {
320 | throw new Error("Spread operator within an array literal expects an array operand.");
321 | }
322 | value.push(...spreadArray);
323 | } else {
324 | value.push(evaluator(thisStack, item, evalContext, thread));
325 | thisStack.pop();
326 | thisStack.push(value);
327 | }
328 | }
329 |
330 | // --- Done.
331 | setExprValue(expr, { value }, thread);
332 | thisStack.push(value);
333 | return value;
334 | }
335 |
336 | function evalObjectLiteral(
337 | evaluator: EvaluatorFunction,
338 | thisStack: any[],
339 | expr: ObjectLiteral,
340 | evalContext: BindingTreeEvaluationContext,
341 | thread: LogicalThread,
342 | ): any {
343 | const objectHash: any = {};
344 | for (const prop of expr.props) {
345 | if (!Array.isArray(prop)) {
346 | // --- We're using a spread expression
347 | const spreadItems = evaluator(thisStack, prop.expr, evalContext, thread);
348 | thisStack.pop();
349 | if (Array.isArray(spreadItems)) {
350 | // --- Spread of an array
351 | for (let i = 0; i < spreadItems.length; i++) {
352 | objectHash[i] = spreadItems[i];
353 | }
354 | } else if (typeof spreadItems === "object") {
355 | // --- Spread of a hash object
356 | for (const [key, value] of Object.entries(spreadItems)) {
357 | objectHash[key] = value;
358 | }
359 | }
360 | continue;
361 | }
362 |
363 | // --- We're using key/[value] pairs
364 | let key: any;
365 | switch (prop[0].type) {
366 | case T_LITERAL:
367 | key = prop[0].value;
368 | break;
369 | case T_IDENTIFIER:
370 | key = prop[0].name;
371 | break;
372 | default:
373 | key = evaluator(thisStack, prop[0], evalContext, thread);
374 | thisStack.pop();
375 | break;
376 | }
377 | objectHash[key] = evaluator(thisStack, prop[1], evalContext, thread);
378 | thisStack.pop();
379 | }
380 |
381 | // --- Done.
382 | setExprValue(expr, { value: objectHash }, thread);
383 | thisStack.push(objectHash);
384 | return objectHash;
385 | }
386 |
387 | function evalUnary(
388 | evaluator: EvaluatorFunction,
389 | thisStack: any[],
390 | expr: UnaryExpression,
391 | evalContext: BindingTreeEvaluationContext,
392 | thread: LogicalThread,
393 | ): any {
394 | evaluator(thisStack, expr.expr, evalContext, thread);
395 | thisStack.pop();
396 | return evalUnaryCore(expr, thisStack, evalContext, thread);
397 | }
398 |
399 | function evalBinary(
400 | evaluator: EvaluatorFunction,
401 | thisStack: any[],
402 | expr: BinaryExpression,
403 | evalContext: BindingTreeEvaluationContext,
404 | thread: LogicalThread,
405 | ): any {
406 | evaluator(thisStack, expr.left, evalContext, thread);
407 | thisStack.pop();
408 | const l = getExprValue(expr.left, thread)?.value;
409 | if (expr.op === "&&" && !l) {
410 | setExprValue(expr, { value: l }, thread);
411 | thisStack.push(l);
412 | return l;
413 | }
414 | if (expr.op === "||" && l) {
415 | setExprValue(expr, { value: l }, thread);
416 | thisStack.push(l);
417 | return l;
418 | }
419 | if (expr.op === "??" && l !== null && l !== undefined) {
420 | setExprValue(expr, { value: l }, thread);
421 | thisStack.push(l);
422 | return l;
423 | }
424 | evaluator(thisStack, expr.right, evalContext, thread);
425 | thisStack.pop();
426 | return evalBinaryCore(expr, thisStack, evalContext, thread);
427 | }
428 |
429 | function evalConditional(
430 | evaluator: EvaluatorFunction,
431 | thisStack: any[],
432 | expr: ConditionalExpression,
433 | evalContext: BindingTreeEvaluationContext,
434 | thread: LogicalThread,
435 | ): any {
436 | const condition = evaluator(thisStack, expr.cond, evalContext, thread);
437 | thisStack.pop();
438 | const value = evaluator(thisStack, condition ? expr.thenE : expr.elseE, evalContext, thread);
439 | setExprValue(expr, { value }, thread);
440 | return value;
441 | }
442 |
443 | function evalAssignment(
444 | evaluator: EvaluatorFunction,
445 | thisStack: any[],
446 | expr: AssignmentExpression,
447 | evalContext: BindingTreeEvaluationContext,
448 | thread: LogicalThread,
449 | ): any {
450 | const leftValue = expr.leftValue;
451 | const rootScope = getRootIdScope(leftValue, evalContext, thread);
452 | const updatesState = rootScope && rootScope.type !== "block";
453 | if (updatesState && evalContext.onWillUpdate) {
454 | void evalContext.onWillUpdate(rootScope, rootScope.name, "assignment");
455 | }
456 | evaluator(thisStack, leftValue, evalContext, thread);
457 | thisStack.pop();
458 | evaluator(thisStack, expr.expr, evalContext, thread);
459 | thisStack.pop();
460 | const value = evalAssignmentCore(thisStack, expr, evalContext, thread);
461 | if (updatesState && evalContext.onDidUpdate) {
462 | void evalContext.onDidUpdate(rootScope, rootScope.name, "assignment");
463 | }
464 | return value;
465 | }
466 |
467 | function evalPreOrPost(
468 | evaluator: EvaluatorFunction,
469 | thisStack: any[],
470 | expr: PrefixOpExpression | PostfixOpExpression,
471 | evalContext: BindingTreeEvaluationContext,
472 | thread: LogicalThread,
473 | ): any {
474 | const rootScope = getRootIdScope(expr.expr, evalContext, thread);
475 | const updatesState = rootScope && rootScope.type !== "block";
476 | if (updatesState && evalContext.onWillUpdate) {
477 | void evalContext.onWillUpdate(rootScope, rootScope.name, "pre-post");
478 | }
479 | evaluator(thisStack, expr.expr, evalContext, thread);
480 | thisStack.pop();
481 | const value = evalPreOrPostCore(thisStack, expr, evalContext, thread);
482 | if (updatesState && evalContext.onDidUpdate) {
483 | void evalContext.onDidUpdate(rootScope, rootScope.name, "pre-post");
484 | }
485 | return value;
486 | }
487 |
488 | function evalFunctionInvocation(
489 | evaluator: EvaluatorFunction,
490 | thisStack: any[],
491 | expr: FunctionInvocationExpression,
492 | evalContext: BindingTreeEvaluationContext,
493 | thread: LogicalThread,
494 | ): any {
495 | let functionObj: any;
496 | let implicitContextObject: any = null;
497 |
498 | // --- Check for contexted object
499 | if (expr.obj.type === T_MEMBER_ACCESS_EXPRESSION) {
500 | const hostObject = evaluator(thisStack, expr.obj.obj, evalContext, thread);
501 | functionObj = evalMemberAccessCore(thisStack, expr.obj, evalContext, thread);
502 | if (expr.obj.obj.type === T_IDENTIFIER && hostObject?._SUPPORT_IMPLICIT_CONTEXT) {
503 | implicitContextObject = hostObject;
504 | }
505 | } else {
506 | // --- Get the object on which to invoke the function
507 | functionObj = evaluator(thisStack, expr.obj, evalContext, thread);
508 | }
509 | thisStack.pop();
510 |
511 | // --- Keep function arguments here, we pass it to the function later
512 | const functionArgs: any[] = [];
513 |
514 | // --- The functionObj may be an ArrowExpression. In this care we need to create the invokable arrow function
515 | if (functionObj?._ARROW_EXPR_) {
516 | functionArgs.push(
517 | functionObj.args,
518 | evalContext,
519 | thread,
520 | ...expr.arguments.map((a) => ({ ...a, _EXPRESSION_: true })),
521 | );
522 | functionObj = createArrowFunction(evaluator, functionObj as ArrowExpression, evalContext);
523 | } else if (expr.obj.type === T_ARROW_EXPRESSION) {
524 | // --- We delay evaluating expression values. We pass the argument names as the first parameter, and then
525 | // --- all parameter expressions
526 | functionArgs.push(
527 | expr.obj.args,
528 | evalContext,
529 | thread,
530 | ...expr.arguments.map((a) => ({ ...a, _EXPRESSION_: true })),
531 | );
532 | } else {
533 | // --- We evaluate the argument values to pass to a JavaScript function
534 | for (let i = 0; i < expr.arguments.length; i++) {
535 | const arg = expr.arguments[i];
536 | if (arg.type === T_SPREAD_EXPRESSION) {
537 | const funcArg = evaluator([], arg.expr, evalContext, thread);
538 | if (!Array.isArray(funcArg)) {
539 | throw new Error("Spread operator within a function invocation expects an array operand.");
540 | }
541 | functionArgs.push(...funcArg);
542 | } else {
543 | if (arg.type === T_ARROW_EXPRESSION) {
544 | const funcArg = createArrowFunction(evaluator, arg, evalContext);
545 | const wrappedFunc = (...args: any[]) => funcArg(arg.args, evalContext, thread, ...args);
546 | functionArgs.push(wrappedFunc);
547 | } else {
548 | const funcArg = evaluator([], arg, evalContext, thread);
549 | if (funcArg?._ARROW_EXPR_) {
550 | const wrappedFuncArg = createArrowFunction(evaluator, funcArg, evalContext);
551 | const wrappedFunc = (...args: any[]) =>
552 | wrappedFuncArg(funcArg.args, evalContext, thread, ...args);
553 | functionArgs.push(wrappedFunc);
554 | } else {
555 | functionArgs.push(funcArg);
556 | }
557 | }
558 | }
559 | }
560 |
561 | // --- We can pass implicit arguments to JavaScript function
562 | if (implicitContextObject) {
563 | // --- Let's obtain the implicit context
564 | if (evalContext.implicitContextGetter) {
565 | const implicitContext = evalContext.implicitContextGetter(implicitContextObject);
566 | functionArgs.unshift(implicitContext);
567 | } else {
568 | throw new Error("Cannot use implicitContextGetter, it is undefined");
569 | }
570 | }
571 | }
572 |
573 | // --- Check if the function is banned from running
574 | const bannedInfo = isBannedFunction(functionObj);
575 | if (bannedInfo.banned) {
576 | throw new Error(
577 | `Function ${bannedInfo.func?.name ?? "unknown"} is not allowed to call. ${bannedInfo?.help ?? ""}`,
578 | );
579 | }
580 |
581 | // --- We use context for "this"
582 | const currentContext = thisStack.length > 0 ? thisStack.pop() : evalContext.localContext;
583 |
584 | // --- Now, invoke the function
585 | const rootScope = getRootIdScope(expr.obj, evalContext, thread);
586 | const updatesState = rootScope && rootScope.type !== "block";
587 | if (updatesState && evalContext.onWillUpdate) {
588 | void evalContext.onWillUpdate(rootScope, rootScope.name, "function-call");
589 | }
590 |
591 | const value = evalContext.options?.defaultToOptionalMemberAccess
592 | ? (functionObj as Function)?.call(currentContext, ...functionArgs)
593 | : (functionObj as Function).call(currentContext, ...functionArgs);
594 |
595 | if (updatesState && evalContext.onDidUpdate) {
596 | void evalContext.onDidUpdate(rootScope, rootScope.name, "function-call");
597 | }
598 |
599 | setExprValue(expr, { value }, thread);
600 | thisStack.push(value);
601 | return value;
602 | }
603 |
604 | function createArrowFunction(
605 | evaluator: EvaluatorFunction,
606 | expr: ArrowExpression,
607 | evalContext: BindingTreeEvaluationContext,
608 | ): Function {
609 | // --- Use this function, it evaluates the arrow function
610 | return (...args: any[]) => {
611 | // --- Prepare the variables to pass
612 | const runTimeEvalContext = args[1] as BindingTreeEvaluationContext;
613 | const runtimeThread = args[2] as LogicalThread;
614 |
615 | // --- Create the thread that runs the arrow function
616 | const workingThread: LogicalThread = {
617 | parent: runtimeThread,
618 | childThreads: [],
619 | blocks: [{ vars: {} }],
620 | loops: [],
621 | breakLabelValue: -1,
622 | closures: (expr as any).closureContext,
623 | };
624 | runtimeThread.childThreads.push(workingThread);
625 |
626 | // --- Create a block for a named arrow function
627 | if (expr.name) {
628 | const functionBlock: BlockScope = { vars: {} };
629 | workingThread.blocks ??= [];
630 | workingThread.blocks.push(functionBlock);
631 | functionBlock.vars[expr.name] = expr;
632 | functionBlock.constVars = new Set([expr.name]);
633 | }
634 |
635 | // --- Assign argument values to names
636 | const arrowBlock: BlockScope = { vars: {} };
637 | workingThread.blocks ??= [];
638 | workingThread.blocks.push(arrowBlock);
639 | const argSpecs = args[0] as Expression[];
640 | let restFound = false;
641 | for (let i = 0; i < argSpecs.length; i++) {
642 | // --- Turn argument specification into processable variable declarations
643 | const argSpec = argSpecs[i];
644 | let decl: VarDeclaration | undefined;
645 | switch (argSpec.type) {
646 | case T_IDENTIFIER: {
647 | decl = {
648 | type: T_VAR_DECLARATION,
649 | id: argSpec.name,
650 | } as VarDeclaration;
651 | break;
652 | }
653 | case T_DESTRUCTURE: {
654 | decl = {
655 | type: T_VAR_DECLARATION,
656 | id: argSpec.id,
657 | aDestr: argSpec.aDestr,
658 | oDestr: argSpec.oDestr,
659 | } as VarDeclaration;
660 | break;
661 | }
662 | case T_SPREAD_EXPRESSION: {
663 | restFound = true;
664 | decl = {
665 | type: T_VAR_DECLARATION,
666 | id: (argSpec.expr as unknown as Identifier).name,
667 | } as VarDeclaration;
668 | break;
669 | }
670 |
671 | default:
672 | throw new Error("Unexpected arrow argument specification");
673 | }
674 | if (decl) {
675 | if (restFound) {
676 | // --- Get the rest of the arguments
677 | const restArgs = args.slice(i + 3);
678 | let argVals: any[] = [];
679 | for (const arg of restArgs) {
680 | if (arg?._EXPRESSION_) {
681 | argVals.push(evaluator([], arg, runTimeEvalContext, runtimeThread));
682 | } else {
683 | argVals.push(arg);
684 | }
685 | }
686 | processDeclarations(
687 | arrowBlock,
688 | runTimeEvalContext,
689 | runtimeThread,
690 | [decl],
691 | false,
692 | true,
693 | argVals,
694 | );
695 | } else {
696 | // --- Get the actual value to work with
697 | let argVal = args[i + 3];
698 | if (argVal?._EXPRESSION_) {
699 | argVal = evaluator([], argVal, runTimeEvalContext, runtimeThread);
700 | }
701 | processDeclarations(
702 | arrowBlock,
703 | runTimeEvalContext,
704 | runtimeThread,
705 | [decl],
706 | false,
707 | true,
708 | argVal,
709 | );
710 | }
711 | }
712 | }
713 |
714 | // --- Evaluate the arrow expression body
715 | let returnValue: any;
716 | let statements: Statement[];
717 |
718 | switch (expr.statement.type) {
719 | case T_EMPTY_STATEMENT:
720 | statements = [];
721 | break;
722 | case T_EXPRESSION_STATEMENT:
723 | // --- Create a new thread for the call
724 | statements = [
725 | {
726 | type: T_RETURN_STATEMENT,
727 | nodeId: createXmlUiTreeNodeId(),
728 | expr: expr.statement.expr,
729 | },
730 | ];
731 | break;
732 | case T_BLOCK_STATEMENT:
733 | // --- Create a new thread for the call
734 | statements = expr.statement.stmts;
735 | break;
736 | default:
737 | throw new Error(
738 | `Arrow expression with a body of '${expr.statement.type}' is not supported yet.`,
739 | );
740 | }
741 |
742 | // --- Process the statement with a new processor
743 | processStatementQueue(statements, runTimeEvalContext, workingThread);
744 |
745 | // --- Return value is in a return value slot
746 | returnValue = workingThread.returnValue;
747 |
748 | // --- Remove the current working thread
749 | const workingIndex = runtimeThread.childThreads.indexOf(workingThread);
750 | if (workingIndex < 0) {
751 | throw new Error("Cannot find thread to remove.");
752 | }
753 | runtimeThread.childThreads.splice(workingIndex, 1);
754 |
755 | // --- Remove the function level block
756 | workingThread.blocks.pop();
757 |
758 | // --- Return the function value
759 | return returnValue;
760 | };
761 | }
762 |
```
--------------------------------------------------------------------------------
/xmlui/dev-docs/component-behaviors.md:
--------------------------------------------------------------------------------
```markdown
1 | # Component Behaviors
2 |
3 | This document explains XMLUI's component behavior system - a cross-cutting concern mechanism that enables attaching reusable functionality to components declaratively. It covers the behavior architecture, the three core behaviors (tooltip, animation, label), the attachment mechanism, and implementation details for framework developers working on the XMLUI core.
4 |
5 | ## What Are Component Behaviors?
6 |
7 | **Component behaviors** are decorator-like objects that wrap rendered React components with additional functionality based on component properties. They implement cross-cutting concerns that apply to multiple components without requiring changes to component implementations.
8 |
9 | A behavior examines a component's definition and metadata to determine eligibility, then wraps the rendered React node with enhanced functionality. This approach enables features like tooltips, animations, and form labels to work consistently across all visual components without coupling the feature implementation to specific component renderers.
10 |
11 | **Key Characteristics:**
12 |
13 | - **Declarative Attachment** - Behaviors attach automatically when specific properties are present
14 | - **Zero Component Coupling** - Components need no knowledge of behaviors; they simply render
15 | - **Composable** - Multiple behaviors can wrap the same component in sequence
16 | - **Renderer Context Access** - Behaviors access the full renderer context for value extraction and state management
17 | - **Metadata-Aware** - Behaviors inspect component metadata to determine applicability
18 |
19 | ## Architectural Overview
20 |
21 | ### The Behavior Interface
22 |
23 | All behaviors implement a simple contract with three members:
24 |
25 | ```typescript
26 | export interface Behavior {
27 | name: string;
28 | canAttach: (node: ComponentDef, metadata: ComponentMetadata) => boolean;
29 | // The attach method now receives an optional metadata argument so
30 | // behaviors can make use of component descriptor information when
31 | // transforming the rendered node.
32 | attach: (
33 | context: RendererContext<any>,
34 | node: ReactNode,
35 | metadata?: ComponentMetadata
36 | ) => ReactNode;
37 | }
38 | ```
39 |
40 | - **name** - Unique identifier for the behavior (e.g., "tooltip", "animation", "label")
41 | - **canAttach** - Predicate determining if the behavior applies to a specific component instance based on its definition and metadata
42 | - **attach** - Transformation function that wraps the rendered React node with enhanced functionality
43 |
44 | **Important Note on Transformation Flexibility:**
45 |
46 | The `attach` function can transform the rendered component in any valid way - wrapping it in other React components is just one approach. Behaviors can also clone the rendered node using `React.cloneElement()` to add or modify properties (such as CSS classes, event handlers, or attributes), compose multiple transformations, or apply any other valid React node manipulation. The only requirement is that `attach` must return a valid React node.
47 |
48 | ### Application in ComponentAdapter
49 |
50 | Behaviors apply in `ComponentAdapter` immediately after the component renderer produces the initial React node:
51 |
52 | ```typescript
53 | // 1. Component renderer executes
54 | renderedNode = renderer(rendererContext);
55 |
56 | // 2. Retrieve registered behaviors from component registry
57 | const behaviors = componentRegistry.getBehaviors();
58 |
59 | // 3. Apply behaviors sequentially (skip compound components)
60 | if (!isCompoundComponent) {
61 | for (const behavior of behaviors) {
62 | if (behavior.canAttach(rendererContext.node, descriptor)) {
63 | // Pass the component metadata into attach so behaviors that need
64 | // descriptor-level information (for example, to avoid wrapping
65 | // components marked `opaque` or `nonVisual`) can use it.
66 | renderedNode = behavior.attach(rendererContext, renderedNode, descriptor);
67 | }
68 | }
69 | }
70 |
71 | // 4. Continue with decoration, API binding, layout wrapping...
72 | ```
73 |
74 | **Application Logic:**
75 |
76 | 1. **Renderer Execution** - Component's renderer function produces the initial React node from the component definition
77 | 2. **Behavior Retrieval** - `componentRegistry.getBehaviors()` returns all registered behaviors from the central registry (framework behaviors plus any contributed by external packages)
78 | 3. **Compound Check** - If the component is a compound (XMLUI-defined) component, skip all behaviors to avoid wrapping internal structure
79 | 4. **Sequential Evaluation** - For each behavior in order:
80 | - Call `canAttach(node, metadata)` with the component definition and its metadata descriptor
81 | - If true, call `attach(context, renderedNode, metadata)` to wrap the node; the metadata is passed so behaviors can consult descriptor-level information during attachment
82 | - The wrapped node becomes the input for the next behavior
83 | 5. **Result** - Multiple behaviors create nested wrappers in application order (tooltip innermost, label outermost)
84 |
85 | This placement ensures behaviors wrap the core component but remain inside decorations (test IDs), API bindings, and layout wrappers.
86 |
87 | ### Behavior Registration Architecture
88 |
89 | Behaviors are registered centrally in the `ComponentRegistry` class within `ComponentProvider`, following the same pattern used for components, actions, and loaders. This centralized registry enables both framework behaviors and external package behaviors to coexist.
90 |
91 | **Registration in ComponentRegistry:**
92 |
93 | ```typescript
94 | class ComponentRegistry {
95 | private behaviors: Behavior[] = [];
96 |
97 | constructor(contributes: ContributesDefinition = {}, ...) {
98 | // ... component and action registration ...
99 |
100 | // Register framework-implemented behaviors
101 | this.registerBehavior(tooltipBehavior);
102 | this.registerBehavior(animationBehavior);
103 | this.registerBehavior(labelBehavior);
104 |
105 | // Register external behaviors from contributes
106 | contributes.behaviors?.forEach((behavior) => {
107 | this.registerBehavior(behavior);
108 | });
109 | }
110 |
111 | private registerBehavior(
112 | behavior: Behavior,
113 | location: "before" | "after" = "after",
114 | position?: string
115 | ) {
116 | if (position) {
117 | const targetIndex = this.behaviors.findIndex(b => b.name === position);
118 | if (targetIndex !== -1) {
119 | const insertIndex = location === "before" ? targetIndex : targetIndex + 1;
120 | this.behaviors.splice(insertIndex, 0, behavior);
121 | return;
122 | }
123 | }
124 | this.behaviors.push(behavior);
125 | }
126 |
127 | getBehaviors(): Behavior[] {
128 | return this.behaviors;
129 | }
130 | }
131 | ```
132 |
133 | **ComponentAdapter Retrieval:**
134 |
135 | The `ComponentAdapter` retrieves registered behaviors from the component registry:
136 |
137 | ```typescript
138 | // In ComponentAdapter.tsx
139 | const componentRegistry = useComponentRegistry();
140 | const behaviors = componentRegistry.getBehaviors();
141 |
142 | // Apply behaviors to rendered node
143 | for (const behavior of behaviors) {
144 | if (behavior.canAttach(rendererContext.node, descriptor)) {
145 | renderedNode = behavior.attach(rendererContext, renderedNode);
146 | }
147 | }
148 | ```
149 |
150 | **External Package Registration:**
151 |
152 | External component packages can contribute custom behaviors through the `ContributesDefinition`:
153 |
154 | ```typescript
155 | // In an external package (e.g., packages/my-package/src/index.tsx)
156 | import { myCustomBehavior } from "./behaviors/MyCustomBehavior";
157 |
158 | export default {
159 | namespace: "MyPackage",
160 | components: [myComponentRenderer],
161 | behaviors: [myCustomBehavior], // Custom behaviors register here
162 | };
163 | ```
164 |
165 | **Positioned Registration:**
166 |
167 | The `registerBehavior` method supports precise control over behavior execution order through optional `location` and `position` parameters:
168 |
169 | - **location**: `"before" | "after"` - Specifies placement relative to the target behavior
170 | - **position**: `string` - The name of the target behavior for positioning
171 |
172 | This allows external packages to insert behaviors at specific points in the execution sequence:
173 |
174 | ```typescript
175 | // Insert a custom behavior before the animation behavior
176 | registerBehavior(myBehavior, "before", "animation");
177 |
178 | // Insert a custom behavior after the tooltip behavior
179 | registerBehavior(myBehavior, "after", "tooltip");
180 | ```
181 |
182 | **Benefits of Registry Architecture:**
183 |
184 | - **Extensibility** - Third-party packages can register custom behaviors without modifying framework code
185 | - **Consistency** - Follows the same pattern as components, actions, and loaders
186 | - **Order Control** - Positioned registration enables fine-grained control over behavior application sequence
187 | - **Centralized Management** - All behaviors accessible through single `getBehaviors()` method
188 | - **Type Safety** - Full TypeScript type checking for behavior implementations
189 |
190 | ## Framework-Implemented Behaviors
191 |
192 | XMLUI currently implements three behaviors that handle common cross-cutting concerns. These serve as examples of the behavior pattern and provide essential functionality across all visual components:
193 |
194 | ### Tooltip Behavior
195 |
196 | Displays informational text or markdown content when hovering over visual components. The tooltip behavior activates when a component has either the `tooltip` or `tooltipMarkdown` property defined.
197 |
198 | **Attachment Criteria:**
199 | ```typescript
200 | canAttach: (node) => {
201 | const tooltipText = node.props?.tooltip;
202 | const tooltipMarkdown = node.props?.tooltipMarkdown;
203 | return !!tooltipText || !!tooltipMarkdown;
204 | }
205 | ```
206 |
207 | **Usage Example:**
208 | ```xmlui
209 | <Button
210 | label="Click me"
211 | tooltip="This button saves your changes"
212 | tooltipOptions="right; delayDuration: 800" />
213 | ```
214 |
215 | **Wrapping Process:**
216 | 1. Extracts `tooltip`, `tooltipMarkdown`, and `tooltipOptions` properties using the renderer context's value extractor
217 | 2. Parses tooltip options (side, alignment, delay, etc.) via `parseTooltipOptions()`
218 | 3. Wraps the rendered component in a `<Tooltip>` component with extracted configuration
219 | 4. The wrapped component becomes the tooltip trigger; the Tooltip component handles display logic
220 |
221 | ### Animation Behavior
222 |
223 | Applies entry/exit animations to components using CSS animations or transitions. The animation behavior activates when a component has the `animation` property defined.
224 |
225 | **Attachment Criteria:**
226 | ```typescript
227 | canAttach: (node) => {
228 | return !!node.props?.animation;
229 | }
230 | ```
231 |
232 | **Usage Example:**
233 | ```xmlui
234 | <Card
235 | title="Welcome"
236 | animation="fadeIn"
237 | animationOptions="duration: 500; delay: 200" />
238 | ```
239 |
240 | **Wrapping Process:**
241 | 1. Extracts `animation` and `animationOptions` properties from the component definition
242 | 2. Parses animation configuration via `parseAnimation()` and `parseAnimationOptions()`
243 | 3. Wraps the component in an `<Animation>` component that manages the animation lifecycle
244 | 4. Special handling for ModalDialog components - passes `externalAnimation={true}` to prevent animation conflicts
245 |
246 | **ModalDialog Special Case:**
247 | ```typescript
248 | return (
249 | <Animation animation={parseAnimation(animation)} {...parsedOptions}>
250 | {context.node.type === "ModalDialog"
251 | ? cloneElement(node as ReactElement, {
252 | externalAnimation: true,
253 | })
254 | : node}
255 | </Animation>
256 | );
257 | ```
258 |
259 | ModalDialog components have internal animation support. When wrapped by the animation behavior, the `externalAnimation` prop signals the dialog to defer to external animation control.
260 |
261 | ### Label Behavior
262 |
263 | Wraps form input components with labels, positioning, and visual indicators (required asterisk, validation states). The label behavior activates when a component has the `label` property and does not explicitly handle its own labeling.
264 |
265 | **Attachment Criteria:**
266 | ```typescript
267 | canAttach: (node, metadata) => {
268 | // Don't attach if component declares its own label prop handling
269 | if (metadata?.props?.label) {
270 | return false;
271 | }
272 | // Only attach if component has a label prop
273 | if (!node.props?.label) {
274 | return false;
275 | }
276 | return true;
277 | }
278 | ```
279 |
280 | **Usage Example:**
281 | ```xmlui
282 | <TextBox
283 | label="Email Address"
284 | labelPosition="top"
285 | required={true}
286 | placeholder="[email protected]" />
287 | ```
288 |
289 | **Wrapping Process:**
290 | 1. Extracts label-related properties: `label`, `labelPosition`, `labelWidth`, `labelBreak`, `required`, `enabled`, `style`, `readOnly`, `shrinkToLabel`
291 | 2. Uses the renderer context's `className` to maintain styling consistency
292 | 3. Wraps the component in `<ItemWithLabel>` which handles label rendering, positioning, and required indicators
293 | 4. Special handling for `inputTemplate` - the `isInputTemplateUsed` flag adjusts label layout when custom input templates are present
294 |
295 | **Metadata Check Explanation:**
296 |
297 | The label behavior checks `metadata?.props?.label` to determine if a component has explicit label property metadata defined. Components like Checkbox or Radio that include label rendering as part of their core functionality define `label` in their metadata. For these components, the label behavior does not attach - they handle their own labels.
298 |
299 | Components like TextBox or Select do not define `label` in their metadata because they expect the label behavior to handle labeling. The presence of a `label` prop in the component instance (but absence in metadata) signals that the behavior should attach.
300 |
301 | ### Behavior Execution Order
302 |
303 | Behaviors are registered in the `ComponentRegistry` during construction. The framework registers its three core behaviors first, followed by any behaviors contributed by external packages:
304 |
305 | ```typescript
306 | // Framework behaviors registered in ComponentRegistry constructor
307 | this.registerBehavior(tooltipBehavior);
308 | this.registerBehavior(animationBehavior);
309 | this.registerBehavior(labelBehavior);
310 |
311 | // External behaviors registered after
312 | contributes.behaviors?.forEach((behavior) => {
313 | this.registerBehavior(behavior);
314 | });
315 | ```
316 |
317 | **Order Significance:**
318 |
319 | The registration order matters because behaviors wrap sequentially. The current order (tooltip, animation, label) produces this nesting:
320 |
321 | ```
322 | <ItemWithLabel> ← Label behavior (outermost)
323 | <Animation> ← Animation behavior (middle)
324 | <Tooltip> ← Tooltip behavior (innermost)
325 | <Button /> ← Original component
326 | </Tooltip>
327 | </Animation>
328 | </ItemWithLabel>
329 | ```
330 |
331 | This order ensures:
332 | - Tooltips appear on the actual interactive component
333 | - Animations affect the component and its tooltip together
334 | - Labels encompass the entire animated, tooltip-enabled component
335 |
336 | Changing the order would alter this nesting and could break visual consistency or interaction behavior.
337 |
338 | ## Renderer Context in Behaviors
339 |
340 | Behaviors receive the full `RendererContext` when attaching, providing access to all rendering capabilities:
341 |
342 | ```typescript
343 | export interface RendererContext<TMd extends ComponentMetadata = ComponentMetadata>
344 | extends ComponentRendererContextBase<TMd> {
345 | node: ComponentDef<TMd>;
346 | state: any;
347 | updateState: (state: any) => void;
348 | appContext: AppContextType;
349 | extractValue: ValueExtractor;
350 | lookupEventHandler: LookupEventHandlerFn<TMd>;
351 | lookupAction: LookupAsyncFn;
352 | lookupSyncCallback: LookupSyncFn;
353 | extractResourceUrl: (url?: unknown) => string | undefined;
354 | renderChild: RenderChildFn;
355 | registerComponentApi: RegisterComponentApiFn;
356 | className: string | undefined;
357 | layoutContext: LayoutContext | undefined;
358 | uid: symbol;
359 | }
360 | ```
361 |
362 | **Key Context Properties Used by Behaviors:**
363 |
364 | - **extractValue** - Extracts property values from expressions, handles data binding, evaluates context variables
365 | - **node** - The component definition containing props, events, children, and type information
366 | - **className** - Computed CSS classes from layout properties, passed to wrapped components for style consistency
367 | - **renderChild** - Function to render child components, used when behaviors need to render templates (e.g., tooltip templates)
368 | - **appContext** - Global application context with navigation, state buckets, media queries, and theme information
369 |
370 | **Example - Tooltip Behavior Using Context:**
371 |
372 | ```typescript
373 | attach: (context, node, metadata) => {
374 | const { extractValue } = context;
375 | const tooltipText = extractValue(context.node.props?.tooltip, true);
376 | const tooltipMarkdown = extractValue(context.node.props?.tooltipMarkdown, true);
377 | const tooltipOptions = extractValue(context.node.props?.tooltipOptions, true);
378 | const parsedOptions = parseTooltipOptions(tooltipOptions);
379 |
380 | // metadata may be used to alter behavior for components marked nonVisual
381 | // or opaque; it's passed in for richer decision-making.
382 | return (
383 | <Tooltip text={tooltipText} markdown={tooltipMarkdown} {...parsedOptions}>
384 | {node}
385 | </Tooltip>
386 | );
387 | }
388 | ```
389 |
390 | The `extractValue` function handles:
391 | - Static string values: `tooltip="Click me"` → `"Click me"`
392 | - Expression evaluation: `tooltip={message}` → evaluates `message` variable
393 | - Context variable resolution: `tooltip={$user.name}` → retrieves from app context
394 | - Data binding: `tooltip={item.description}` → extracts from current data context
395 |
396 | ## Implementation Details
397 |
398 | ### ComponentAdapter Integration Point
399 |
400 | The behavior application occurs in `ComponentAdapter` after the renderer produces the initial node but before decoration and event handler attachment:
401 |
402 | ```typescript
403 | // From ComponentAdapter.tsx (simplified flow)
404 | let renderedNode: ReactNode = null;
405 |
406 | try {
407 | if (safeNode.type === "Slot") {
408 | renderedNode = slotRenderer(rendererContext, parentRenderContext);
409 | } else {
410 | if (!renderer) {
411 | return <UnknownComponent message={`${safeNode.type}`} />;
412 | }
413 | renderedNode = renderer(rendererContext);
414 | }
415 |
416 | /**
417 | * Apply behaviors to the component.
418 | */
419 | const behaviors = componentRegistry.getBehaviors();
420 | if (!isCompoundComponent) {
421 | for (const behavior of behaviors) {
422 | if (behavior.canAttach(rendererContext.node, descriptor)) {
423 | renderedNode = behavior.attach(rendererContext, renderedNode, descriptor);
424 | }
425 | }
426 | }
427 |
428 | // --- Components may have a `testId` property for E2E testing purposes
429 | if ((appContext?.decorateComponentsWithTestId && ...) || inspectId !== undefined) {
430 | renderedNode = (
431 | <ComponentDecorator attr={{ "data-testid": resolvedUid, ... }}>
432 | {cloneElement(renderedNode as ReactElement, { ... })}
433 | </ComponentDecorator>
434 | );
435 | }
436 |
437 | // --- API-bound components provide helpful behavior
438 | if (isApiBound) {
439 | return <ApiBoundComponent ... />;
440 | }
441 |
442 | // --- Layout context wrapping
443 | if (layoutContextRef.current?.wrapChild) {
444 | renderedNode = layoutContextRef.current.wrapChild(...);
445 | }
446 | } catch (e) {
447 | renderingError = (e as Error)?.message || "Internal error";
448 | }
449 | ```
450 |
451 | **Pipeline Position:**
452 |
453 | 1. **Renderer Execution** - Component-specific rendering logic produces initial node
454 | 2. **Behavior Application** ← **Behaviors apply here**
455 | 3. **Test Decoration** - Test IDs and inspection attributes added via ComponentDecorator
456 | 4. **API Binding** - API-bound components wrapped in ApiBoundComponent
457 | 5. **Layout Wrapping** - Layout context applies layout-specific wrappers
458 |
459 | This position ensures behaviors wrap the core component but remain inside decorations and API bindings. The layering ensures:
460 | - Behaviors affect only the visual component, not decoration infrastructure
461 | - Test IDs and inspection attributes apply to behavior-wrapped components
462 | - API binding affects the entire behavior-enhanced component
463 | - Layout wrapping encompasses all previous layers
464 |
465 | ### Behavior Transformation Examples
466 |
467 | The following examples demonstrate how the framework-implemented behaviors transform components. These behaviors all use the wrapping approach, where the rendered component is wrapped in additional React components to provide enhanced functionality.
468 |
469 | **Single Behavior (Tooltip):**
470 |
471 | ```xmlui
472 | <Button label="Save" tooltip="Save your changes" />
473 | ```
474 |
475 | Produces:
476 | ```jsx
477 | <Tooltip text="Save your changes">
478 | <button className="xmlui-button">Save</button>
479 | </Tooltip>
480 | ```
481 |
482 | **Multiple Behaviors (Tooltip + Animation):**
483 |
484 | ```xmlui
485 | <Card
486 | title="Welcome"
487 | tooltip="Dashboard card"
488 | animation="fadeIn" />
489 | ```
490 |
491 | Produces:
492 | ```jsx
493 | <Animation animation={fadeInAnimation}>
494 | <Tooltip text="Dashboard card">
495 | <div className="xmlui-card">
496 | <div className="card-title">Welcome</div>
497 | </div>
498 | </Tooltip>
499 | </Animation>
500 | ```
501 |
502 | **All Three Behaviors:**
503 |
504 | ```xmlui
505 | <TextBox
506 | label="Email"
507 | tooltip="Enter your email address"
508 | animation="slideIn" />
509 | ```
510 |
511 | Produces:
512 | ```jsx
513 | <ItemWithLabel label="Email">
514 | <Animation animation={slideInAnimation}>
515 | <Tooltip text="Enter your email address">
516 | <input type="text" className="xmlui-textbox" />
517 | </Tooltip>
518 | </Animation>
519 | </ItemWithLabel>
520 | ```
521 |
522 | ### Metadata-Driven Attachment
523 |
524 | Behaviors inspect component metadata passed to `canAttach()` to make attachment decisions. The metadata contains component descriptor information from the component registry:
525 |
526 | ```typescript
527 | export type ComponentMetadata<
528 | TProps extends Record<string, ComponentPropertyMetadata> = Record<string, any>,
529 | TEvents extends Record<string, ComponentPropertyMetadata> = Record<string, any>,
530 | TContextValues extends Record<string, ComponentPropertyMetadata> = Record<string, any>,
531 | TApis extends Record<string, ComponentApiMetadata> = Record<string, any>,
532 | > = {
533 | status?: "stable" | "experimental" | "deprecated" | "in progress" | "internal";
534 | description?: string;
535 | props?: TProps;
536 | events?: TEvents;
537 | contextVars?: TContextValues;
538 | apis?: TApis;
539 | nonVisual?: boolean;
540 | childrenAsTemplate?: string;
541 | opaque?: boolean;
542 | // ... theme-related fields
543 | };
544 | ```
545 |
546 | **Label Behavior Metadata Check:**
547 |
548 | ```typescript
549 | canAttach: (node, metadata) => {
550 | // Check if component defines its own label handling
551 | if (metadata?.props?.label) {
552 | return false; // Component handles labels itself
553 | }
554 | // Check if instance has label prop
555 | if (!node.props?.label) {
556 | return false; // No label to attach
557 | }
558 | return true; // Attach label behavior
559 | }
560 | ```
561 |
562 | **Metadata Usage Patterns:**
563 |
564 | - **nonVisual** - Behaviors can check this flag to avoid attaching to non-visual components like DataSource or Container
565 | - **props** - Behaviors check for property metadata to determine if a component explicitly handles a feature
566 | - **opaque** - Indicates internal component structure should not be affected by behaviors
567 |
568 | ## Behavior Execution Flow
569 |
570 | ### Complete Rendering Flow with Behaviors
571 |
572 | ```mermaid
573 | graph TD
574 | A[ComponentWrapper receives component definition] --> B[ComponentWrapper routes to ComponentAdapter]
575 | B --> C[ComponentAdapter prepares renderer context]
576 | C --> D[ComponentAdapter calls component renderer]
577 | D --> E[Initial React node created]
578 | E --> F[ComponentAdapter retrieves behaviors from registry via componentRegistry.getBehaviors]
579 | F --> G[FOR EACH behavior: Evaluate canAttach]
580 | G --> H{canAttach returns true?}
581 | H -->|Yes| I[behavior.attach wraps node]
582 | H -->|No| J[Skip this behavior]
583 | I --> K[Node transformed with behavior functionality]
584 | J --> K
585 | K --> L{More behaviors?}
586 | L -->|Yes| G
587 | L -->|No| M[ComponentAdapter applies test decoration]
588 | M --> N[ComponentAdapter checks for API-bound properties]
589 | N --> O{Is API-bound?}
590 | O -->|Yes| P[Wrap in ApiBoundComponent]
591 | O -->|No| Q[ComponentAdapter applies layout context wrapping]
592 | P --> Q
593 | Q --> R[ComponentAdapter returns final transformed node]
594 | ```
595 |
596 | ### Behavior Attachment Decision Tree
597 |
598 | ```mermaid
599 | flowchart TD
600 | A[Component rendered] --> B{Is compound component?}
601 | B -->|Yes| Z[Skip all behaviors]
602 | B -->|No| C[Tooltip Behavior Check]
603 | C --> D{Has tooltip or tooltipMarkdown prop?}
604 | D -->|Yes| E[Wrap in Tooltip component]
605 | D -->|No| F[Animation Behavior Check]
606 | E --> F
607 | F --> G{Has animation prop?}
608 | G -->|Yes| H[Wrap in Animation component]
609 | G -->|No| I[Label Behavior Check]
610 | H --> I
611 | I --> J{Metadata has label prop?}
612 | J -->|Yes| K[Skip - component handles own label]
613 | J -->|No| L{Instance has label prop?}
614 | K --> M[Return transformed node]
615 | L -->|Yes| N[Wrap in ItemWithLabel component]
616 | L -->|No| M
617 | N --> M
618 | Z --> M
619 | ```
620 |
621 | ## Summary
622 |
623 | Component behaviors provide a powerful mechanism for applying cross-cutting concerns to XMLUI components declaratively. Behaviors integrate seamlessly into the rendering pipeline, transforming components after initial rendering but before decoration. They leverage the full renderer context for value extraction, event handling, and state management.
624 |
625 | The behavior system reduces code duplication, ensures consistency, and enables framework-level enhancements without component-level changes. Behaviors are registered in the central `ComponentRegistry` and retrieved via `componentRegistry.getBehaviors()`, then applied sequentially in `ComponentAdapter`.
626 |
627 | The registry-based architecture enables extensibility - both framework behaviors and external package behaviors coexist through the `ContributesDefinition` mechanism. External packages can register custom behaviors that execute alongside framework behaviors, with optional positioning control for precise execution order.
628 |
629 | For framework developers working on XMLUI core, behaviors represent a critical architectural pattern for implementing features that span multiple components uniformly and maintainably.
630 |
```