This is page 31 of 141. Use http://codebase.md/xmlui-org/xmlui?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ └── config.json
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── layout-changes.md
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ └── lorem-ipsum.png
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ └── welcome-to-the-xmlui-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ └── PageNotFound.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── blog-theme.ts
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── HelloMd.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── github.svg
│ │ │ ├── images
│ │ │ │ ├── apiaction-tutorial
│ │ │ │ │ ├── add-success.png
│ │ │ │ │ ├── apiaction-param.png
│ │ │ │ │ ├── change-completed.png
│ │ │ │ │ ├── change-in-progress.png
│ │ │ │ │ ├── confirm-delete.png
│ │ │ │ │ ├── data-error.png
│ │ │ │ │ ├── data-progress.png
│ │ │ │ │ ├── data-success.png
│ │ │ │ │ ├── display-1.png
│ │ │ │ │ ├── item-deleted.png
│ │ │ │ │ ├── item-updated.png
│ │ │ │ │ ├── missing-api-key.png
│ │ │ │ │ ├── new-item-added.png
│ │ │ │ │ └── test-message.png
│ │ │ │ ├── chat-api
│ │ │ │ │ └── domain-model.svg
│ │ │ │ ├── components
│ │ │ │ │ ├── image
│ │ │ │ │ │ └── breakfast.jpg
│ │ │ │ │ ├── markdown
│ │ │ │ │ │ └── colors.png
│ │ │ │ │ └── modal
│ │ │ │ │ ├── deep_link_dialog_1.jpg
│ │ │ │ │ └── deep_link_dialog_2.jpg
│ │ │ │ ├── create-apps
│ │ │ │ │ ├── collapsed-vertical.png
│ │ │ │ │ ├── using-forms-warning-dialog.png
│ │ │ │ │ └── using-forms.png
│ │ │ │ ├── datasource-tutorial
│ │ │ │ │ ├── data-with-header.png
│ │ │ │ │ ├── filtered-data.png
│ │ │ │ │ ├── filtered-items.png
│ │ │ │ │ ├── initial-page-items.png
│ │ │ │ │ ├── list-items.png
│ │ │ │ │ ├── next-page-items.png
│ │ │ │ │ ├── no-data.png
│ │ │ │ │ ├── pagination-1.jpg
│ │ │ │ │ ├── pagination-1.png
│ │ │ │ │ ├── polling-1.png
│ │ │ │ │ ├── refetch-data.png
│ │ │ │ │ ├── slow-loading.png
│ │ │ │ │ ├── test-message.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── unconventional-data.png
│ │ │ │ │ └── unfiltered-items.png
│ │ │ │ ├── flower.jpg
│ │ │ │ ├── get-started
│ │ │ │ │ ├── add-new-contact.png
│ │ │ │ │ ├── app-modified.png
│ │ │ │ │ ├── app-start.png
│ │ │ │ │ ├── app-with-boxes.png
│ │ │ │ │ ├── app-with-toast.png
│ │ │ │ │ ├── boilerplate-structure.png
│ │ │ │ │ ├── cl-initial.png
│ │ │ │ │ ├── cl-start.png
│ │ │ │ │ ├── contact-counts.png
│ │ │ │ │ ├── contact-dialog-title.png
│ │ │ │ │ ├── contact-dialog.png
│ │ │ │ │ ├── contact-menus.png
│ │ │ │ │ ├── contact-predicates.png
│ │ │ │ │ ├── context-menu.png
│ │ │ │ │ ├── dashboard-numbers.png
│ │ │ │ │ ├── default-contact-list.png
│ │ │ │ │ ├── delete-contact.png
│ │ │ │ │ ├── delete-task.png
│ │ │ │ │ ├── detailed-template.png
│ │ │ │ │ ├── edit-contact-details.png
│ │ │ │ │ ├── edited-contact-saved.png
│ │ │ │ │ ├── empty-sections.png
│ │ │ │ │ ├── filter-completed.png
│ │ │ │ │ ├── fullwidth-desktop.png
│ │ │ │ │ ├── fullwidth-mobile.png
│ │ │ │ │ ├── initial-table.png
│ │ │ │ │ ├── items-and-badges.png
│ │ │ │ │ ├── loading-message.png
│ │ │ │ │ ├── new-contact-button.png
│ │ │ │ │ ├── new-contact-saved.png
│ │ │ │ │ ├── no-empty-sections.png
│ │ │ │ │ ├── personal-todo-initial.png
│ │ │ │ │ ├── piechart.png
│ │ │ │ │ ├── review-today.png
│ │ │ │ │ ├── rudimentary-dashboard.png
│ │ │ │ │ ├── section-collapsed.png
│ │ │ │ │ ├── sectioned-items.png
│ │ │ │ │ ├── sections-ordered.png
│ │ │ │ │ ├── spacex-list-with-links.png
│ │ │ │ │ ├── spacex-list.png
│ │ │ │ │ ├── start-personal-todo-1.png
│ │ │ │ │ ├── submit-new-contact.png
│ │ │ │ │ ├── submit-new-task.png
│ │ │ │ │ ├── syntax-highlighting.png
│ │ │ │ │ ├── table-with-badge.png
│ │ │ │ │ ├── template-with-card.png
│ │ │ │ │ ├── test-emulated-api.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── todo-logo.png
│ │ │ │ │ └── xmlui-tools.png
│ │ │ │ ├── HelloApp.png
│ │ │ │ ├── HelloApp2.png
│ │ │ │ ├── logos
│ │ │ │ │ ├── xmlui1.svg
│ │ │ │ │ ├── xmlui2.svg
│ │ │ │ │ ├── xmlui3.svg
│ │ │ │ │ ├── xmlui4.svg
│ │ │ │ │ ├── xmlui5.svg
│ │ │ │ │ ├── xmlui6.svg
│ │ │ │ │ └── xmlui7.svg
│ │ │ │ ├── pdf
│ │ │ │ │ └── dummy-pdf.jpg
│ │ │ │ ├── rendering-engine
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ ├── Component.svg
│ │ │ │ │ ├── CompoundComponent.svg
│ │ │ │ │ ├── RootComponent.svg
│ │ │ │ │ └── tree-with-containers.svg
│ │ │ │ ├── reviewers-guide
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ └── incbutton-in-action.png
│ │ │ │ ├── tools
│ │ │ │ │ └── boilerplate-structure.png
│ │ │ │ ├── try.svg
│ │ │ │ ├── tutorial
│ │ │ │ │ ├── app-chat-history.png
│ │ │ │ │ ├── app-content-placeholder.png
│ │ │ │ │ ├── app-header-and-content.png
│ │ │ │ │ ├── app-links-channel-selected.png
│ │ │ │ │ ├── app-links-click.png
│ │ │ │ │ ├── app-navigation.png
│ │ │ │ │ ├── finished-ex01.png
│ │ │ │ │ ├── finished-ex02.png
│ │ │ │ │ ├── hello.png
│ │ │ │ │ ├── splash-screen-advanced.png
│ │ │ │ │ ├── splash-screen-after-click.png
│ │ │ │ │ ├── splash-screen-centered.png
│ │ │ │ │ ├── splash-screen-events.png
│ │ │ │ │ ├── splash-screen-expression.png
│ │ │ │ │ ├── splash-screen-reuse-after.png
│ │ │ │ │ ├── splash-screen-reuse-before.png
│ │ │ │ │ └── splash-screen.png
│ │ │ │ └── tutorial-01.png
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── Boxes.xmlui
│ │ │ ├── Breadcrumb.xmlui
│ │ │ ├── ChangeLog.xmlui
│ │ │ ├── ColorPalette.xmlui
│ │ │ ├── DocumentLinks.xmlui
│ │ │ ├── DocumentPage.xmlui
│ │ │ ├── DocumentPageNoTOC.xmlui
│ │ │ ├── Icons.xmlui
│ │ │ ├── IncButton.xmlui
│ │ │ ├── IncButton2.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── docs-theme.ts
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── Animation.tsx
│ │ │ ├── AnimationNative.tsx
│ │ │ ├── FadeAnimation.tsx
│ │ │ ├── FadeInAnimation.tsx
│ │ │ ├── FadeOutAnimation.tsx
│ │ │ ├── index.tsx
│ │ │ ├── ScaleAnimation.tsx
│ │ │ └── SlideInAnimation.tsx
│ │ └── tsconfig.json
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ ├── tsconfig.json
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── HelloWorld.module.scss
│ │ │ ├── HelloWorld.tsx
│ │ │ ├── HelloWorldNative.tsx
│ │ │ └── index.tsx
│ │ └── tsconfig.json
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── index.tsx
│ │ │ ├── IPhoneFrame.module.scss
│ │ │ ├── IPhoneFrame.tsx
│ │ │ ├── MacOSAppFrame.module.scss
│ │ │ ├── MacOSAppFrame.tsx
│ │ │ ├── WindowsAppFrame.module.scss
│ │ │ └── WindowsAppFrame.tsx
│ │ └── tsconfig.json
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── index.tsx
│ │ │ ├── LazyPdfNative.tsx
│ │ │ ├── Pdf.module.scss
│ │ │ └── Pdf.tsx
│ │ └── tsconfig.json
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── hooks
│ │ │ │ ├── usePlayground.ts
│ │ │ │ └── useToast.ts
│ │ │ ├── index.tsx
│ │ │ ├── playground
│ │ │ │ ├── Box.module.scss
│ │ │ │ ├── Box.tsx
│ │ │ │ ├── CodeSelector.tsx
│ │ │ │ ├── ConfirmationDialog.module.scss
│ │ │ │ ├── ConfirmationDialog.tsx
│ │ │ │ ├── Editor.tsx
│ │ │ │ ├── Header.module.scss
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── Playground.tsx
│ │ │ │ ├── PlaygroundContent.module.scss
│ │ │ │ ├── PlaygroundContent.tsx
│ │ │ │ ├── PlaygroundNative.module.scss
│ │ │ │ ├── PlaygroundNative.tsx
│ │ │ │ ├── Preview.module.scss
│ │ │ │ ├── Preview.tsx
│ │ │ │ ├── Select.module.scss
│ │ │ │ ├── StandalonePlayground.tsx
│ │ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ │ ├── ThemeSwitcher.module.scss
│ │ │ │ ├── ThemeSwitcher.tsx
│ │ │ │ ├── ToneSwitcher.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── providers
│ │ │ │ ├── Toast.module.scss
│ │ │ │ └── ToastProvider.tsx
│ │ │ ├── state
│ │ │ │ └── store.ts
│ │ │ ├── themes
│ │ │ │ └── theme.ts
│ │ │ └── utils
│ │ │ └── helpers.ts
│ │ └── tsconfig.json
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── index.tsx
│ │ │ ├── Search.module.scss
│ │ │ └── Search.tsx
│ │ └── tsconfig.json
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── index.tsx
│ │ │ ├── Spreadsheet.tsx
│ │ │ └── SpreadsheetNative.tsx
│ │ └── tsconfig.json
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ ├── src
│ │ ├── Carousel
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── FancyButton
│ │ │ ├── FancyButton.module.scss
│ │ │ ├── FancyButton.tsx
│ │ │ └── FancyButton.xmlui
│ │ ├── Hello
│ │ │ ├── Hello.tsx
│ │ │ ├── Hello.xmlui
│ │ │ └── Hello.xmlui.xs
│ │ ├── HeroSection
│ │ │ ├── HeroSection.module.scss
│ │ │ ├── HeroSection.tsx
│ │ │ └── HeroSectionNative.tsx
│ │ ├── index.tsx
│ │ ├── ScrollToTop
│ │ │ ├── ScrollToTop.module.scss
│ │ │ ├── ScrollToTop.tsx
│ │ │ └── ScrollToTopNative.tsx
│ │ └── vite-env.d.ts
│ └── tsconfig.json
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── playwright.config.ts
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── get-langserver-metadata.mjs
│ ├── inline-links.mjs
│ └── README-e2e-summary.md
├── src
│ ├── abstractions
│ │ ├── _conventions.md
│ │ ├── ActionDefs.ts
│ │ ├── AppContextDefs.ts
│ │ ├── ComponentDefs.ts
│ │ ├── ContainerDefs.ts
│ │ ├── ExtensionDefs.ts
│ │ ├── FunctionDefs.ts
│ │ ├── RendererDefs.ts
│ │ ├── scripting
│ │ │ ├── BlockScope.ts
│ │ │ ├── Compilation.ts
│ │ │ ├── LogicalThread.ts
│ │ │ ├── LoopScope.ts
│ │ │ ├── modules.ts
│ │ │ ├── ScriptParserError.ts
│ │ │ ├── Token.ts
│ │ │ ├── TryScope.ts
│ │ │ └── TryScopeExp.ts
│ │ └── ThemingDefs.ts
│ ├── components
│ │ ├── _conventions.md
│ │ ├── abstractions.ts
│ │ ├── Accordion
│ │ │ ├── Accordion.md
│ │ │ ├── Accordion.module.scss
│ │ │ ├── Accordion.spec.ts
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── AccordionItem.tsx
│ │ │ ├── AccordionItemNative.tsx
│ │ │ └── AccordionNative.tsx
│ │ ├── Animation
│ │ │ └── AnimationNative.tsx
│ │ ├── APICall
│ │ │ ├── APICall.md
│ │ │ ├── APICall.spec.ts
│ │ │ ├── APICall.tsx
│ │ │ └── APICallNative.tsx
│ │ ├── App
│ │ │ ├── App.md
│ │ │ ├── App.module.scss
│ │ │ ├── App.spec.ts
│ │ │ ├── App.tsx
│ │ │ ├── AppLayoutContext.ts
│ │ │ ├── AppNative.tsx
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── AppHeader
│ │ │ ├── AppHeader.md
│ │ │ ├── AppHeader.module.scss
│ │ │ ├── AppHeader.spec.ts
│ │ │ ├── AppHeader.tsx
│ │ │ └── AppHeaderNative.tsx
│ │ ├── AppState
│ │ │ ├── AppState.md
│ │ │ ├── AppState.spec.ts
│ │ │ ├── AppState.tsx
│ │ │ └── AppStateNative.tsx
│ │ ├── AutoComplete
│ │ │ ├── AutoComplete.md
│ │ │ ├── AutoComplete.module.scss
│ │ │ ├── AutoComplete.spec.ts
│ │ │ ├── AutoComplete.tsx
│ │ │ ├── AutoCompleteContext.tsx
│ │ │ └── AutoCompleteNative.tsx
│ │ ├── Avatar
│ │ │ ├── Avatar.md
│ │ │ ├── Avatar.module.scss
│ │ │ ├── Avatar.spec.ts
│ │ │ ├── Avatar.tsx
│ │ │ └── AvatarNative.tsx
│ │ ├── Backdrop
│ │ │ ├── Backdrop.md
│ │ │ ├── Backdrop.module.scss
│ │ │ ├── Backdrop.spec.ts
│ │ │ ├── Backdrop.tsx
│ │ │ └── BackdropNative.tsx
│ │ ├── Badge
│ │ │ ├── Badge.md
│ │ │ ├── Badge.module.scss
│ │ │ ├── Badge.spec.ts
│ │ │ ├── Badge.tsx
│ │ │ └── BadgeNative.tsx
│ │ ├── Bookmark
│ │ │ ├── Bookmark.md
│ │ │ ├── Bookmark.module.scss
│ │ │ ├── Bookmark.spec.ts
│ │ │ ├── Bookmark.tsx
│ │ │ └── BookmarkNative.tsx
│ │ ├── Breakout
│ │ │ ├── Breakout.module.scss
│ │ │ ├── Breakout.spec.ts
│ │ │ ├── Breakout.tsx
│ │ │ └── BreakoutNative.tsx
│ │ ├── Button
│ │ │ ├── Button-style.spec.ts
│ │ │ ├── Button.md
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.spec.ts
│ │ │ ├── Button.tsx
│ │ │ └── ButtonNative.tsx
│ │ ├── Card
│ │ │ ├── Card.md
│ │ │ ├── Card.module.scss
│ │ │ ├── Card.spec.ts
│ │ │ ├── Card.tsx
│ │ │ └── CardNative.tsx
│ │ ├── Carousel
│ │ │ ├── Carousel.md
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.spec.ts
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ ├── CarouselItem.tsx
│ │ │ ├── CarouselItemNative.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── ChangeListener
│ │ │ ├── ChangeListener.md
│ │ │ ├── ChangeListener.spec.ts
│ │ │ ├── ChangeListener.tsx
│ │ │ └── ChangeListenerNative.tsx
│ │ ├── chart-color-schemes.ts
│ │ ├── Charts
│ │ │ ├── AreaChart
│ │ │ │ ├── AreaChart.md
│ │ │ │ ├── AreaChart.spec.ts
│ │ │ │ ├── AreaChart.tsx
│ │ │ │ └── AreaChartNative.tsx
│ │ │ ├── BarChart
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── BarChart.module.scss
│ │ │ │ ├── BarChart.spec.ts
│ │ │ │ ├── BarChart.tsx
│ │ │ │ └── BarChartNative.tsx
│ │ │ ├── DonutChart
│ │ │ │ ├── DonutChart.spec.ts
│ │ │ │ └── DonutChart.tsx
│ │ │ ├── LabelList
│ │ │ │ ├── LabelList.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ ├── LabelListNative.module.scss
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ └── ContentSeparatorNative.tsx
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.mjs
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── infrastructure
│ │ │ ├── index.html
│ │ │ ├── main.tsx
│ │ │ ├── public
│ │ │ │ ├── mockServiceWorker.js
│ │ │ │ ├── resources
│ │ │ │ │ ├── bell.svg
│ │ │ │ │ ├── box.svg
│ │ │ │ │ ├── doc.svg
│ │ │ │ │ ├── eye.svg
│ │ │ │ │ ├── flower-640x480.jpg
│ │ │ │ │ ├── sun.svg
│ │ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ │ └── txt.svg
│ │ │ │ └── serve.json
│ │ │ └── TestBed.tsx
│ │ └── themed-app-test-helpers.ts
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.bin.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/xmlui/src/components/Pagination/Pagination.tsx:
--------------------------------------------------------------------------------
```typescript
import { createComponentRenderer } from "../../components-core/renderers";
import { parseScssVar } from "../../components-core/theming/themeVars";
import { createMetadata, d, dEnabled } from "../metadata-helpers";
import {
PositionValues,
defaultProps,
type PageNumber,
PageNumberValues,
PaginationNative,
} from "./PaginationNative";
import styles from "./Pagination.module.scss";
import {
orientationOptionMd,
type OrientationOptions,
orientationOptionValues,
} from "../abstractions";
const COMP = "Pagination";
export const PaginationMd = createMetadata({
status: "experimental",
description:
"`Pagination` enables navigation through large datasets by dividing content into pages. " +
"It provides controls for page navigation and can display current page information.",
props: {
enabled: dEnabled(),
itemCount: d(
"Total number of items to paginate. " +
"If not provided, the component renders simplified pagination controls " +
"that are enabled/disabled using the `hasPrevPage` and `hasNextPage` props.",
undefined,
"number",
),
pageSize: d("Number of items per page", undefined, "number", defaultProps.pageSize),
pageIndex: d("Current page index (0-based)", undefined, "number", defaultProps.pageIndex),
maxVisiblePages: d(
"Maximum number of page buttons to display. " +
"If the value is not among the allowed values, it will fall back to the default.",
PageNumberValues,
"number",
defaultProps.maxVisiblePages,
),
showPageInfo: d(
"Whether to show page information",
undefined,
"boolean",
defaultProps.showPageInfo,
),
showPageSizeSelector: d(
"Whether to show the page size selector",
undefined,
"boolean",
defaultProps.showPageSizeSelector,
),
showCurrentPage: d(
"Whether to show the current page indicator",
undefined,
"boolean",
defaultProps.showCurrentPage,
),
pageSizeOptions: d(
"Array of page sizes the user can select from. If provided, shows a page size selector dropdown",
),
hasPrevPage: d(
"Whether to disable the previous page button. Only takes effect if itemCount is not provided.",
undefined,
"boolean",
),
hasNextPage: d(
"Whether to disable the next page button. Only takes effect if itemCount is not provided.",
undefined,
"boolean",
),
orientation: {
description: "Layout orientation of the pagination component",
options: orientationOptionValues,
type: "string",
availableValues: orientationOptionMd,
default: defaultProps.orientation,
},
pageSizeSelectorPosition: {
description: "Determines where to place the page size selector in the layout.",
options: PositionValues,
type: "string",
default: defaultProps.pageSizeSelectorPosition,
},
pageInfoPosition: {
description: "Determines where to place the page information in the layout.",
options: PositionValues,
type: "string",
default: defaultProps.pageInfoPosition,
},
buttonRowPosition: d(
"Determines where to place the pagination button row in the layout.",
PositionValues,
"string",
defaultProps.buttonRowPosition,
),
},
events: {
pageDidChange: d("Fired when the current page changes"),
pageSizeDidChange: d("Fired when the page size changes"),
},
apis: {
moveFirst: {
description: "Moves to the first page",
signature: "moveFirst(): void",
},
moveLast: {
description: "Moves to the last page",
signature: "moveLast(): void",
},
movePrev: {
description: "Moves to the previous page",
signature: "movePrev(): void",
},
moveNext: {
description: "Moves to the next page",
signature: "moveNext(): void",
},
currentPage: {
description: "Gets the current page number (1-based)",
},
currentPageSize: {
description: "Gets the current page size",
},
},
themeVars: parseScssVar(styles.themeVars),
defaultThemeVars: {
"padding-Pagination": "$space-4",
"backgroundColor-Pagination": "transparent",
"borderColor-Pagination": "$color-gray-300",
"textColor-Pagination": "$color-gray-600",
"backgroundColor-selector-Pagination": "transparent",
"textColor-selector-Pagination": "$color-gray-600",
"borderRadius-selector-Pagination": "$borderRadius",
"gap-buttonRow-Pagination": "$space-2",
},
});
export const paginationComponentRenderer = createComponentRenderer(
COMP,
PaginationMd,
({ node, extractValue, lookupEventHandler, registerComponentApi, updateState, className }) => {
let maxVisiblePages = extractValue.asOptionalNumber(
node.props.maxVisiblePages,
defaultProps.maxVisiblePages,
);
if (!PageNumberValues.includes(maxVisiblePages as any)) {
console.warn(
`Invalid maxVisiblePages value provided To Pagination: ${maxVisiblePages}. Falling back to default.`,
);
maxVisiblePages = defaultProps.maxVisiblePages;
}
let orientation = extractValue.asOptionalString(
node.props.orientation,
defaultProps.orientation,
);
if (!orientationOptionValues.includes(orientation as any)) {
console.warn(
`Invalid orientation value provided To Pagination: ${orientation}. Falling back to default.`,
);
orientation = defaultProps.orientation;
}
return (
<PaginationNative
enabled={extractValue.asOptionalBoolean(node.props.enabled, true)}
itemCount={extractValue.asOptionalNumber(node.props.itemCount)}
pageSize={extractValue.asOptionalNumber(node.props.pageSize, defaultProps.pageSize)}
pageIndex={extractValue.asOptionalNumber(node.props.pageIndex, defaultProps.pageIndex)}
showPageInfo={extractValue.asOptionalBoolean(
node.props.showPageInfo,
defaultProps.showPageInfo,
)}
showPageSizeSelector={extractValue.asOptionalBoolean(
node.props.showPageSizeSelector,
defaultProps.showPageSizeSelector,
)}
showCurrentPage={extractValue.asOptionalBoolean(
node.props.showCurrentPage,
defaultProps.showCurrentPage,
)}
hasPrevPage={extractValue.asOptionalBoolean(node.props.hasPrevPage)}
hasNextPage={extractValue.asOptionalBoolean(node.props.hasNextPage)}
maxVisiblePages={maxVisiblePages as PageNumber}
pageSizeOptions={extractValue(node.props.pageSizeOptions) as number[] | undefined}
orientation={orientation as OrientationOptions}
buttonRowPosition={extractValue.asOptionalString(
node.props.buttonRowPosition,
defaultProps.buttonRowPosition,
)}
pageSizeSelectorPosition={extractValue.asOptionalString(
node.props.pageSizeSelectorPosition,
defaultProps.pageSizeSelectorPosition,
)}
pageInfoPosition={extractValue.asOptionalString(
node.props.pageInfoPosition,
defaultProps.pageInfoPosition,
)}
onPageDidChange={lookupEventHandler("pageDidChange")}
onPageSizeDidChange={lookupEventHandler("pageSizeDidChange")}
registerComponentApi={registerComponentApi}
updateState={updateState}
className={className}
/>
);
},
);
```
--------------------------------------------------------------------------------
/xmlui/src/components/RadioGroup/RadioGroupNative.tsx:
--------------------------------------------------------------------------------
```typescript
import React, {
createContext,
type CSSProperties,
type ForwardedRef,
forwardRef,
useCallback,
useContext,
useEffect,
useId,
useMemo,
useRef,
} from "react";
import * as InnerRadioGroup from "@radix-ui/react-radio-group";
import classnames from "classnames";
import styles from "./RadioGroup.module.scss";
import type { RegisterComponentApiFn, UpdateStateFn } from "../../abstractions/RendererDefs";
import { noop } from "../../components-core/constants";
import { useEvent } from "../../components-core/utils/misc";
import type { Option, ValidationStatus } from "../abstractions";
import OptionTypeProvider from "../Option/OptionTypeProvider";
import { UnwrappedRadioItem } from "./RadioItemNative";
import { convertOptionValue } from "../Option/OptionNative";
type RadioGroupProps = {
id?: string;
initialValue?: string;
autofocus?: boolean;
value?: string;
enabled?: boolean;
style?: CSSProperties;
className?: string;
validationStatus?: ValidationStatus;
label?: string;
labelPosition?: string;
labelWidth?: string;
labelBreak?: boolean;
required?: boolean;
readOnly?: boolean;
updateState?: UpdateStateFn;
onDidChange?: (newValue: string) => void;
onFocus?: (ev: React.FocusEvent<HTMLDivElement>) => void;
onBlur?: (ev: React.FocusEvent<HTMLDivElement>) => void;
children?: React.ReactNode;
registerComponentApi?: RegisterComponentApiFn;
};
export const defaultProps: Pick<
RadioGroupProps,
"value" | "initialValue" | "enabled" | "validationStatus" | "required" | "readOnly"
> = {
value: "",
initialValue: "",
enabled: true,
validationStatus: "none" as ValidationStatus,
required: false,
readOnly: false,
};
const RadioGroupStatusContext = createContext<{
value?: string;
setValue?: (value: string) => void;
status: ValidationStatus;
enabled?: boolean;
}>({
status: "none",
enabled: defaultProps.enabled,
});
export const RadioGroup = forwardRef(function RadioGroup(
{
id,
value = defaultProps.value,
initialValue = defaultProps.initialValue,
autofocus,
enabled = defaultProps.enabled,
validationStatus = defaultProps.validationStatus,
label,
labelPosition,
labelWidth,
labelBreak,
required = defaultProps.required,
readOnly = defaultProps.readOnly,
updateState = noop,
onDidChange = noop,
onFocus = noop,
onBlur = noop,
children,
registerComponentApi,
style,
className,
...rest
}: RadioGroupProps,
forwardedRef: ForwardedRef<HTMLDivElement>,
) {
const [focused, setFocused] = React.useState(false);
const radioGroupRef = useRef<HTMLDivElement>(null);
// --- Initialize the related field with the input's initial value
useEffect(() => {
updateState({ value: convertOptionValue(initialValue) }, { initial: true });
}, [initialValue, updateState]);
// --- Handle autofocus by focusing the first radio option
useEffect(() => {
if (autofocus && radioGroupRef.current) {
// Find the first radio item element
const firstRadioItem = radioGroupRef.current.querySelector('[role="radio"]');
if (firstRadioItem) {
(firstRadioItem as HTMLElement).focus();
}
}
}, [autofocus]);
// --- Custom focus handler for label clicks
const focusActiveOption = useCallback(() => {
if (radioGroupRef.current) {
// First try to find the currently selected radio option
const selectedRadio = radioGroupRef.current.querySelector('[role="radio"][aria-checked="true"]');
if (selectedRadio) {
(selectedRadio as HTMLElement).focus();
return;
}
// If no option is selected, focus the first one
const firstRadio = radioGroupRef.current.querySelector('[role="radio"]');
if (firstRadio) {
(firstRadio as HTMLElement).focus();
}
}
}, []);
// --- Handle the value change events for this input
const updateValue = useCallback(
(value: string) => {
updateState({ value });
onDidChange(value);
},
[onDidChange, updateState],
);
const onInputChange = useCallback(
(value: string) => {
if (readOnly) return;
updateValue(value);
},
[updateValue, readOnly],
);
// --- Manage obtaining and losing the focus
const handleOnFocus = useCallback(
(ev: React.FocusEvent<HTMLDivElement, Element>) => {
setFocused(true);
onFocus?.(ev);
},
[onFocus],
);
const handleOnBlur = useCallback(
(ev: React.FocusEvent<HTMLDivElement, Element>) => {
setFocused(false);
onBlur?.(ev);
},
[onBlur],
);
const setValue = useEvent((val: string) => {
updateValue(val);
});
useEffect(() => {
registerComponentApi?.({
//focus,
setValue,
});
}, [/* focus, */ registerComponentApi, setValue]);
const contextValue = useMemo(() => {
return { value, setValue: updateValue, status: validationStatus, enabled };
}, [value, updateValue, validationStatus, enabled]);
return (
<OptionTypeProvider Component={RadioGroupOption}>
<RadioGroupStatusContext.Provider value={contextValue}>
<InnerRadioGroup.Root
{...rest}
style={style}
ref={radioGroupRef}
id={id}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
onValueChange={onInputChange}
value={value}
disabled={!enabled}
required={required}
aria-readonly={readOnly}
className={classnames(className, styles.radioGroupContainer, {
[styles.focused]: focused,
[styles.disabled]: !enabled,
})}
>
{children}
</InnerRadioGroup.Root>
</RadioGroupStatusContext.Provider>
</OptionTypeProvider>
);
});
export const RadioGroupOption = ({
value,
label,
enabled = true,
optionRenderer,
style,
className,
}: Option) => {
const id = useId();
const radioGroupContext = useContext(RadioGroupStatusContext);
const statusStyles = useMemo(
() => ({
[styles.disabled]: radioGroupContext.enabled === false ? true : !enabled,
[styles.error]: value === radioGroupContext.value && radioGroupContext.status === "error",
[styles.warning]: value === radioGroupContext.value && radioGroupContext.status === "warning",
[styles.valid]: value === radioGroupContext.value && radioGroupContext.status === "valid",
}),
[enabled, radioGroupContext, value],
);
const item = useMemo(
() => (
<>
<UnwrappedRadioItem
id={id}
value={value}
checked={value === radioGroupContext.value}
disabled={!enabled}
statusStyles={statusStyles}
/>
<label htmlFor={id} className={classnames(styles.label, statusStyles)}>
{label ?? value}
</label>
</>
),
[enabled, id, label, statusStyles, value, radioGroupContext],
);
return (
<div
key={id}
className={classnames(styles.radioOptionContainer, className)}
style={style}
data-radio-item
>
{!!optionRenderer ? (
<label className={styles.optionLabel}>
<div className={styles.itemContainer}>{item}</div>
{optionRenderer({
$checked: value === radioGroupContext.value,
$setChecked: radioGroupContext.setValue,
})}
</label>
) : (
item
)}
</div>
);
};
```
--------------------------------------------------------------------------------
/xmlui/src/components/ModalDialog/ModalDialog.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { expect, test } from "../../testing/fixtures";
// =============================================================================
// OPEN/CLOSE TESTS
// =============================================================================
test.describe("Open/Close", () => {
test("Imperative open - without params", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment>
<Button testId="button" onClick="modal.open()">open modal (imperative)</Button>
<ModalDialog id="modal">
<Text testId="textInModal">Hello</Text>
</ModalDialog>
</Fragment>
`);
await expect(page.getByTestId("textInModal")).not.toBeVisible();
await page.getByTestId("button").click();
await expect(page.getByTestId("textInModal")).toBeVisible();
});
test("Imperative open - with param inside", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment>
<Button testId="button" onClick="modal.open('PARAM_VALUE')">open modal (imperative)</Button>
<ModalDialog id="modal">
<Text testId="textInModal">{$param}</Text>
</ModalDialog>
</Fragment>
`);
await page.getByTestId("button").click();
await expect(page.getByTestId("textInModal")).toHaveText("PARAM_VALUE");
});
test("Imperative open - with multiple param inside", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment>
<Button testId="button" onClick="modal.open('PARAM_VALUE1', 'PARAM_VALUE2')">open modal (imperative)</Button>
<ModalDialog id="modal">
<Text testId="textInModal1">{$params[0]}</Text>
<Text testId="textInModal2">{$params[1]}</Text>
</ModalDialog>
</Fragment>
`);
await page.getByTestId("button").click();
await expect(page.getByTestId("textInModal1")).toHaveText("PARAM_VALUE1");
await expect(page.getByTestId("textInModal2")).toHaveText("PARAM_VALUE2");
});
test("Imperative open - with param in title", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment>
<Button testId="button" onClick="modal.open('PARAM_VALUE')">open modal (imperative)</Button>
<ModalDialog id="modal" title="{$param}"/>
</Fragment>
`);
await page.getByTestId("button").click();
await expect(page.getByTestId("modal").getByRole("heading")).toHaveText("PARAM_VALUE");
});
test("Preserves $item context variable from Table Column", async ({ page, initTestBed }) => {
await initTestBed(`
<Table data='{[
{id: 1, company: "Acme Corp", order: 1},
]}'>
<Column>
<ModalDialog id="dialog" testId="dialog" title="{$item.company}">
<Text testId="modal-text">{JSON.stringify($item)}</Text>
</ModalDialog>
<Button testId="btn-{$itemIndex}" onClick="dialog.open()">{$item.company}</Button>
</Column>
</Table>
`);
// Test first row
await page.getByTestId("btn-0").click();
const modal = page.getByTestId("dialog");
await expect(modal).toBeVisible();
await expect(modal).toContainText("Acme Corp");
});
test("Declarative open/close", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.isOpen="{false}">
<Button testId="button" onClick="isOpen = true">open modal</Button>
<ModalDialog when="{isOpen}" onClose="isOpen = false" testId="modal">
<Text testId="textInModal">Hello</Text>
</ModalDialog>
<Text testId="isOpen">{isOpen + ''}</Text>
</Fragment>
`);
await expect(page.getByTestId("textInModal")).not.toBeVisible();
await expect(page.getByTestId("isOpen")).toHaveText("false");
await page.getByTestId("button").click();
await expect(page.getByTestId("textInModal")).toBeVisible();
await expect(page.getByTestId("isOpen")).toHaveText("true");
//click modal close button
await page.getByTestId("modal").getByRole("button").click();
await expect(page.getByTestId("textInModal")).not.toBeVisible();
await expect(page.getByTestId("isOpen")).toHaveText("false");
});
test("maxWidth works", async ({ page, initTestBed, createModalDialogDriver }) => {
await initTestBed(`
<Fragment>
<Button testId="button" onClick="modal.open()">open modal (imperative)</Button>
<ModalDialog id="modal" maxWidth="250px">
<Text testId="textInModal">Hello</Text>
</ModalDialog>
</Fragment>
`);
const modal = await createModalDialogDriver("modal");
await expect(modal.component).not.toBeVisible();
await expect(page.getByTestId("textInModal")).not.toBeVisible();
await page.getByTestId("button").click();
await expect(modal.component).toBeVisible();
await expect(modal.component).toHaveCSS("max-width", "250px");
await expect(page.getByTestId("textInModal")).toBeVisible();
});
test("backgroundColor works", async ({ page, initTestBed, createModalDialogDriver }) => {
await initTestBed(`
<Fragment>
<Button testId="button" onClick="modal.open()">open modal (imperative)</Button>
<ModalDialog id="modal" backgroundColor="rgb(255, 255, 0)">
<Text testId="textInModal">Hello</Text>
</ModalDialog>
</Fragment>
`);
const modal = await createModalDialogDriver("modal");
await page.getByTestId("button").click();
await expect(modal.component).toBeVisible();
await expect(modal.component).toHaveCSS("background-color", "rgb(255, 255, 0)");
await expect(page.getByTestId("textInModal")).toBeVisible();
});
test("modal is scrollable in fullScreen mode and hides background content", async ({ page, initTestBed, createModalDialogDriver }) => {
await initTestBed(`
<Fragment>
<!-- Background page content that should be completely hidden by the modal -->
<VStack testId="pageContent" height="300vh" backgroundColor="lightblue">
<Button testId="button" onClick="modal.open()">
open modal
</Button>
<Stack height="*" verticalAlignment="end">
<Text testId="pageBottomText">
Should be hidden
</Text>
</Stack>
</VStack>
<ModalDialog id="modal" fullScreen="{true}">
<VStack>
<Text testId="textInModal">
Content that should be scrollable
</Text>
<Stack
testId="longContent"
height="300vh"
verticalAlignment="end">
<Text testId="bottomText">
Long content
</Text>
</Stack>
</VStack>
</ModalDialog>
</Fragment>
`);
const modal = await createModalDialogDriver("modal");
// Verify page content is initially visible before opening modal
await expect(page.getByTestId("pageContent")).toBeVisible();
await expect(page.getByTestId("pageEndText")).not.toBeInViewport();
// Open the modal
await page.getByTestId("button").click();
await expect(modal.component).toBeVisible();
// Verify modal completely covers page content - all page content should be hidden
await expect(page.getByTestId("textInModal")).toBeVisible();
await expect(page.getByTestId("bottomText")).not.toBeInViewport();
await page.getByTestId("bottomText").scrollIntoViewIfNeeded();
await expect(page.getByTestId("pageBottomText")).not.toBeInViewport();
});
});
```
--------------------------------------------------------------------------------
/xmlui/src/components/TreeDisplay/TreeDisplayNative.tsx:
--------------------------------------------------------------------------------
```typescript
import { type CSSProperties, type ForwardedRef, forwardRef, type ReactNode, useMemo } from "react";
import classnames from "classnames";
import styles from "./TreeDisplay.module.scss";
type TreeNode = {
label: string;
level: number;
children: TreeNode[];
isLast?: boolean;
};
type Props = {
style?: CSSProperties;
className?: string;
children?: ReactNode;
content?: string;
itemHeight?: number;
};
export const defaultProps: Pick<Props, "content" | "itemHeight"> = {
content: "",
itemHeight: 20
};
// Parse the indented text into a tree structure
const parseTreeContent = (content: string): TreeNode[] => {
if (!content) return [];
const lines = content.split("\n").filter((line) => line.trim());
if (lines.length === 0) return [];
const rootNodes: TreeNode[] = [];
const levelMap: { [level: number]: TreeNode[] } = {};
lines.forEach((line) => {
// Count leading spaces to determine level
const leadingSpaces = line.length - line.trimLeft().length;
const currentLevel = leadingSpaces;
const label = line.trim();
const node: TreeNode = { label, level: currentLevel, children: [] };
// If we're at level 0, this is a root node
if (currentLevel === 0) {
rootNodes.push(node);
levelMap[0] = rootNodes;
return;
}
// Find the nearest parent level
let parentLevel = currentLevel - 1;
while (parentLevel >= 0) {
if (levelMap[parentLevel] && levelMap[parentLevel].length > 0) {
// Get the last node at the parent level
const parent = levelMap[parentLevel][levelMap[parentLevel].length - 1];
parent.children.push(node);
// Initialize or update this level's map entry
if (!levelMap[currentLevel]) {
levelMap[currentLevel] = [];
}
levelMap[currentLevel].push(node);
return;
}
parentLevel--;
}
// If we get here, something is wrong with the tree structure
// Add to root as fallback
rootNodes.push(node);
});
// Mark last children at each level for correct connector rendering
const markLastChildren = (nodes: TreeNode[]) => {
if (nodes.length > 0) {
nodes[nodes.length - 1].isLast = true;
nodes.forEach((node) => {
if (node.children.length > 0) {
markLastChildren(node.children);
}
});
}
};
markLastChildren(rootNodes);
return rootNodes;
};
// Calculate the total height of a node and its descendants
const calculateNodeHeight = (node: TreeNode, itemHeight: number): number => {
if (node.children.length === 0) {
return itemHeight; // Single node height
}
return (
itemHeight +
node.children.reduce((acc, child) => acc + calculateNodeHeight(child, itemHeight), 0)
);
};
// Render a node and its children with SVG line connectors
const renderTreeNode = (
node: TreeNode,
index: number,
itemHeight: number,
level = 0,
ancestorLines: boolean[] = [],
): JSX.Element => {
const isLast = node.isLast;
const isRoot = level === 0;
const hasChildren = node.children.length > 0;
const nodeHeight = hasChildren ? calculateNodeHeight(node, itemHeight) : itemHeight;
const halfHeight = itemHeight / 2;
return (
<div key={`${node.label}-${index}`} className={styles.treeNode}>
<div className={styles.treeNodeRow} style={{ height: `${itemHeight}px` }}>
<div
className={styles.connectorArea}
style={{
marginLeft: `${level * itemHeight}px`,
width: `${itemHeight}px`,
height: `${itemHeight}px`,
}}
>
{/* Single SVG for all connector lines at this level */}
<svg
className={styles.connector}
width={itemHeight}
height={hasChildren ? nodeHeight : itemHeight}
viewBox={`0 0 ${itemHeight} ${hasChildren ? nodeHeight : itemHeight}`}
style={{ position: "absolute", top: 0, left: 0 }}
>
{isRoot && (
<>
{/* Horizontal line from the left edge to the text */}
<line
x1={halfHeight}
y1={halfHeight}
x2={itemHeight}
y2={halfHeight}
strokeWidth="1.5"
className={styles.connectorLine}
/>
</>
)}
{!isRoot && (
<>
{/* Vertical line from top */}
<line
x1={halfHeight}
y1={0}
x2={halfHeight}
y2={isLast ? halfHeight : itemHeight}
strokeWidth="1.5"
className={styles.connectorLine}
/>
{/* Horizontal line to content */}
<line
x1={halfHeight}
y1={halfHeight}
x2={itemHeight}
y2={halfHeight}
strokeWidth="1.5"
className={styles.connectorLine}
/>
</>
)}
{/* Vertical line down through children if not last child */}
{!isLast && hasChildren && (
<line
x1={halfHeight}
y1={halfHeight}
x2={halfHeight}
y2={nodeHeight}
strokeWidth="1.5"
className={styles.connectorLine}
/>
)}
{/* Render ancestor vertical lines */}
{ancestorLines.map(
(shouldDraw, i) =>
shouldDraw && (
<line
key={`ancestor-${i}`}
x1={halfHeight - (level - i) * itemHeight}
y1={0}
x2={halfHeight - (level - i) * itemHeight}
y2={nodeHeight}
strokeWidth="1.5"
className={styles.connectorLine}
/>
),
)}
</svg>
</div>
<div className={styles.treeNodeContent} style={{ lineHeight: `${itemHeight}px` }}>
{node.label}
</div>
</div>
{hasChildren && (
<div className={styles.childrenContainer}>
{node.children.map((child, i) => {
// Create new ancestor lines array for child nodes
const newAncestorLines = [...ancestorLines];
while (newAncestorLines.length <= level) {
newAncestorLines.push(false);
}
// Set current level's line status: draw if this child is not the last child
newAncestorLines[level] = i !== node.children.length - 1;
return renderTreeNode(child, i, itemHeight, level + 1, newAncestorLines);
})}
</div>
)}
</div>
);
};
export const TreeDisplay = forwardRef(function TreeDisplay(
{ style, className, children, content = defaultProps.content, itemHeight = defaultProps.itemHeight }: Props,
forwardedRef: ForwardedRef<HTMLDivElement>,
) {
const contentString = (content || children?.toString() || "").toString();
const treeNodes = useMemo(() => parseTreeContent(contentString), [contentString]);
return (
<div className={classnames(styles.treeDisplay, className)} style={style} ref={forwardedRef}>
<div className={styles.content}>
{treeNodes.map((node, index) => renderTreeNode(node, index, itemHeight, 0, []))}
</div>
</div>
);
});
```
--------------------------------------------------------------------------------
/xmlui/src/components/Animation/AnimationNative.tsx:
--------------------------------------------------------------------------------
```typescript
import { animated, useSpring, useInView } from "@react-spring/web";
import type { ForwardedRef } from "react";
import React, { Children, forwardRef, useEffect, useId, useMemo, useState } from "react";
import { useCallback } from "react";
import { composeRefs } from "@radix-ui/react-compose-refs";
import type { RegisterComponentApiFn } from "../..";
import { isPlainObject } from "lodash-es";
export type AnimationProps = {
children?: React.ReactNode;
animation: any;
registerComponentApi?: RegisterComponentApiFn;
onStop?: () => void;
onStart?: () => void;
animateWhenInView?: boolean;
duration?: number;
reverse?: boolean;
loop?: boolean;
once?: boolean;
delay?: number;
};
const AnimatedComponent = animated(
forwardRef(function InlineComponentDef(props: any, ref: ForwardedRef<any>) {
const { children, ...rest } = props;
if (!React.isValidElement(children)) {
return children;
}
return React.cloneElement(children, { ...rest, ref });
}),
);
export const defaultProps: Pick<
AnimationProps,
"delay" | "animateWhenInView" | "reverse" | "loop" | "once"
> = {
delay: 0,
animateWhenInView: false,
reverse: false,
loop: false,
once: false,
};
export const parseAnimation = (animation: string | object): object => {
if (typeof animation === "object") {
return animation;
}
const presetAnimations: Record<string, object> = {
fadein: {
from: { opacity: 0 },
to: { opacity: 1 },
},
fadeout: {
from: { opacity: 1 },
to: { opacity: 0 },
},
slidein: {
from: { transform: "translateX(-100%)" },
to: { transform: "translateX(0%)" },
},
slideout: {
from: { transform: "translateX(0%)" },
to: { transform: "translateX(100%)" },
},
popin: {
from: { transform: "scale(0.8)", opacity: 0 },
to: { transform: "scale(1)", opacity: 1 },
},
popout: {
from: { transform: "scale(1)", opacity: 1 },
to: { transform: "scale(0.8)", opacity: 0 },
},
flipin: {
from: { transform: "rotateY(90deg)", opacity: 0 },
to: { transform: "rotateY(0deg)", opacity: 1 },
},
flipout: {
from: { transform: "rotateY(0deg)", opacity: 1 },
to: { transform: "rotateY(90deg)", opacity: 0 },
},
rotatein: {
from: { transform: "rotate(-180deg)", opacity: 0 },
to: { transform: "rotate(0deg)", opacity: 1 },
},
rotateout: {
from: { transform: "rotate(0deg)", opacity: 1 },
to: { transform: "rotate(180deg)", opacity: 0 },
},
zoomin: {
from: { transform: "scale(0)", opacity: 0 },
to: { transform: "scale(1)", opacity: 1 },
},
zoomout: {
from: { transform: "scale(1)", opacity: 1 },
to: { transform: "scale(0)", opacity: 0 },
},
};
return presetAnimations[animation] || { from: {}, to: {} };
};
/**
* Helper function to parse a single animation-specific option value.
*/
function parseAnimationOptionValue(name: string, value: string): any {
switch (name) {
case "duration":
case "delay":
const num = parseInt(value, 10);
return isNaN(num) ? undefined : num;
case "animateWhenInView":
case "reverse":
case "loop":
case "once":
const lowerVal = value.toLowerCase();
if (lowerVal === "true" || lowerVal === "1" || lowerVal === "yes") return true;
if (lowerVal === "false" || lowerVal === "0" || lowerVal === "no") return false;
return undefined;
default:
return undefined;
}
}
/**
* Parses animation options from a string or object.
*/
export function parseAnimationOptions(input: any): Partial<AnimationProps> {
if (isPlainObject(input)) {
return input as Partial<AnimationProps>;
}
if (typeof input === "string") {
const options: Partial<AnimationProps> = {};
const values = input
.split(";")
.map((value) => value.trim())
.filter((value) => value.length > 0);
for (const value of values) {
if (value.includes(":")) {
const [name, val] = value.split(":").map((part) => part.trim());
if (name && val) {
const parsedValue = parseAnimationOptionValue(name, val);
if (parsedValue !== undefined) {
(options as any)[name] = parsedValue;
}
}
} else {
const booleanOptions = ["animateWhenInView", "reverse", "loop", "once"];
if (booleanOptions.includes(value)) {
(options as any)[value] = true;
} else if (value.startsWith("!") && value.length > 1) {
const optionName = value.substring(1);
if (booleanOptions.includes(optionName)) {
(options as any)[optionName] = false;
}
}
}
}
return options;
}
return {};
}
export const Animation = forwardRef(function Animation(
{
children,
registerComponentApi,
animation,
duration,
onStop,
onStart,
delay = defaultProps.delay,
animateWhenInView = defaultProps.animateWhenInView,
reverse = defaultProps.reverse,
loop = defaultProps.loop,
once = defaultProps.once,
...rest
}: AnimationProps,
forwardedRef: ForwardedRef<HTMLDivElement>,
) {
const [_animation] = useState(animation);
const [toggle, setToggle] = useState(false);
const [reset, setReset] = useState(false);
const [count, setCount] = useState(0);
const times = 1;
const animationId = useId();
const animationSettings = useMemo<any>(
() => ({
from: _animation.from,
to: _animation.to,
config: {
..._animation.config,
duration,
},
delay,
loop,
reset,
reverse: toggle,
onRest: () => {
onStop?.();
if (loop) {
if (reverse) {
setCount(count + 1);
setToggle(!toggle);
}
setReset(true);
} else {
if (reverse && count < times) {
setCount(count + 1);
setToggle(!toggle);
}
}
},
onStart: () => {
onStart?.();
},
}),
[
_animation.config,
_animation.from,
_animation.to,
count,
delay,
duration,
loop,
onStart,
onStop,
reset,
once,
reverse,
toggle,
],
);
const [springs, api] = useSpring(
() => ({
...animationSettings,
}),
[animationSettings],
);
const [ref, animationStyles] = useInView(() => animationSettings, {
once,
});
const composedRef = ref ? composeRefs(ref, forwardedRef) : forwardedRef;
const startAnimation = useCallback(() => {
void api.start(_animation);
return () => {
api.stop();
};
}, [_animation, api]);
const stopAnimation = useCallback(() => {
api.stop();
}, [api]);
useEffect(() => {
registerComponentApi?.({
start: startAnimation,
stop: stopAnimation,
});
}, [registerComponentApi, startAnimation, stopAnimation]);
const content = useMemo(() => {
return Children.map(children, (child, index) =>
animateWhenInView ? (
<AnimatedComponent key={index} {...rest} style={animationStyles} ref={composedRef}>
{child}
</AnimatedComponent>
) : (
<AnimatedComponent key={index} {...rest} style={springs} ref={forwardedRef}>
{child}
</AnimatedComponent>
),
);
}, [animateWhenInView, animationStyles, children, springs, rest, composedRef, forwardedRef]);
return content;
});
```
--------------------------------------------------------------------------------
/xmlui/src/components/ChangeListener/ChangeListener.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { test, expect } from "../../testing/fixtures";
// =============================================================================
// BASIC FUNCTIONALITY TESTS
// =============================================================================
test("component fires didChange event when listenTo value changes", async ({
page,
initTestBed,
}) => {
await initTestBed(
`<App var.counter="{0}" var.oneBigger="{1}">
<Button label="Increment" onClick="{counter++}" />
<ChangeListener
listenTo="{counter}"
onDidChange="oneBigger = counter + 1" />
<Text testId="counterText">{counter}</Text>
<Text testId="oneBiggerText">{oneBigger}</Text>
</App>`,
);
const counterText = page.getByTestId("counterText");
const oneBiggerText = page.getByTestId("oneBiggerText");
await expect(counterText).toHaveText("0");
await expect(oneBiggerText).toHaveText("1");
await page.getByRole("button", { name: "Increment" }).click();
await expect(counterText).toHaveText("1");
await expect(oneBiggerText).toHaveText("2");
});
test("component does not fire event when unrelated values change", async ({
page,
initTestBed,
}) => {
await initTestBed(
`<App var.a="{0}" var.counter="{0}" var.oneBigger="{1}">
<Button label="Increment" onClick="{counter++}" />
<ChangeListener
listenTo="{a}"
onDidChange="oneBigger = counter + 1" />
<Text testId="counterText">{counter}</Text>
<Text testId="oneBiggerText">{oneBigger}</Text>
</App>`,
);
const counterText = page.getByTestId("counterText");
const oneBiggerText = page.getByTestId("oneBiggerText");
await expect(counterText).toHaveText("0");
await expect(oneBiggerText).toHaveText("1");
await page.getByRole("button", { name: "Increment" }).click();
await expect(counterText).toHaveText("1");
await expect(oneBiggerText).toHaveText("1");
});
test("component passes both previous and new values to the handler", async ({
page,
initTestBed,
}) => {
await initTestBed(`
<VStack var.counter="{0}" var.clone="{0}">
<Button testId="button" onClick="counter++">Increment</Button>
<Text testId="text">{clone}</Text>
<ChangeListener
listenTo="{counter}"
onDidChange="chg => clone = chg.prevValue + '|' + chg.newValue" />
</VStack>
`);
// Click the button to change the counter value
await page.locator("button").click();
// Check that the event fired and testState was updated
await expect(page.getByTestId("text")).toHaveText("0|1");
});
// =============================================================================
// EDGE CASE TESTS
// =============================================================================
test("component handles complex data types", async ({ page, initTestBed }) => {
await initTestBed(`
<VStack var.data="{{ a: 1, b: [2, { counter: 10}] }}" var.clone="{0}">
<Button testId="incRelevant" onClick="data.b[1].counter++">Increment</Button>
<Button testId="incIrrelevant" onClick="data.a++">Increment irrelevant</Button>
<Text testId="text">{clone}</Text>
<ChangeListener
listenTo="{data.b[1].counter}"
onDidChange="chg => clone = chg.newValue" />
</VStack>
`);
await expect(page.getByTestId("text")).toHaveText("10");
// make sure the irrelevant parts of the data doesn't get listenedTo
await page.getByTestId("incIrrelevant").click();
await expect(page.getByTestId("text")).toHaveText("10");
await page.getByTestId("incRelevant").click();
// Check that the event fired and testState was updated
await expect(page.getByTestId("text")).toHaveText("11");
});
test("component handles undefined and null values", async ({ page, initTestBed }) => {
await initTestBed(`
<VStack var.counter="{0}" var.clone="{0}">
<Button testId="setNull" onClick="counter = null">Set Null</Button>
<Button testId="setUndefined" onClick="counter = undefined">Set Undefined</Button>
<Button testId="setValue" onClick="counter = 3">Set Value</Button>
<Text testId="text">
{clone === undefined ? 'undefined' : JSON.stringify(clone)}
</Text>
<ChangeListener listenTo="{counter}" onDidChange="chg => clone = chg.newValue" />
</VStack>`);
await page.getByTestId("setNull").click();
await expect(page.getByTestId("text")).toHaveText("null");
await page.getByTestId("setUndefined").click();
await expect(page.getByTestId("text")).toHaveText("undefined");
await page.getByTestId("setValue").click();
await expect(page.getByTestId("text")).toHaveText("3");
});
test("component doesn't fire on identical primitive values", async ({ page, initTestBed }) => {
await initTestBed(`
<VStack var.counter="{0}" var.clicked="{0}" var.data="{1}">
<Button testId="setToPrimitive" onClick="clicked++; data = data + 1 - 1">
Set The Same Primitive
</Button>
<Text testId="text">{clicked}|{counter}|{data}</Text>
<ChangeListener listenTo="{data}" onDidChange="counter++" />
</VStack>
`);
await expect(page.getByTestId("text")).toHaveText("0|1|1");
await page.locator("button").click();
await expect(page.getByTestId("text")).toHaveText("1|1|1");
await page.locator("button").click();
await expect(page.getByTestId("text")).toHaveText("2|1|1");
});
// =============================================================================
// INTEGRATION TESTS
// =============================================================================
test("component works with multiple listeners on the same value", async ({ page, initTestBed }) => {
await initTestBed(`
<VStack var.counter1="{0}" var.counter2="{0}" var.clicked="{0}">
<Button testId="setToPrimitive" onClick="clicked++;">
Set The Same Primitive
</Button>
<Text testId="text">{clicked}|{counter1}|{counter2}</Text>
<ChangeListener listenTo="{clicked}" onDidChange="counter1++" />
<ChangeListener listenTo="{clicked}" onDidChange="counter2++" />
</VStack>
`);
await expect(page.getByTestId("text")).toHaveText("0|1|1");
await page.locator("button").click();
await expect(page.getByTestId("text")).toHaveText("1|2|2");
await page.locator("button").click();
await expect(page.getByTestId("text")).toHaveText("2|3|3");
});
test("component works with conditional rendering", async ({ page, initTestBed }) => {
await initTestBed(`
<VStack var.counter="{0}" var.clone="{0}" var.show="{false}">
<Button testId="button" onClick="counter++">Increment</Button>
<Button testId="showButton" onClick="show = !show">Toggle Show</Button>
<Text testId="text">
{clone}
</Text>
<ChangeListener
when="{show}"
listenTo="{counter}"
onDidChange="clone = counter" />
</VStack>
`);
// Click the button to change the counter value
await page.getByTestId("button").click();
await expect(page.getByTestId("text")).toHaveText("0");
await page.getByTestId("button").click();
await expect(page.getByTestId("text")).toHaveText("0");
await page.getByTestId("button").click();
await expect(page.getByTestId("text")).toHaveText("0");
await page.getByTestId("showButton").click();
await expect(page.getByTestId("text")).toHaveText("3");
await page.getByTestId("button").click();
await expect(page.getByTestId("text")).toHaveText("4");
await page.getByTestId("showButton").click();
await page.getByTestId("button").click();
await expect(page.getByTestId("text")).toHaveText("4");
});
```
--------------------------------------------------------------------------------
/docs/content/components/ColorPicker.md:
--------------------------------------------------------------------------------
```markdown
# ColorPicker [#colorpicker]
`ColorPicker` enables users to choose colors by specifying RGB, HSL, or HEX values.
## Using `ColorPicker` [#using-colorpicker]
This component allows you to edit or select a color using RGB, HSL, or CSS HEX notation. It displays a popup over the UI and lets you use the mouse or keyboard to edit or select a color.
```xmlui-pg copy display name="Example: using ColorPicker"
<App>
<ColorPicker id="colorPicker" label="Select your favorite color" />
<Text>Selected color: {colorPicker.value}</Text>
</App>
```
## Properties [#properties]
### `autoFocus` (default: false) [#autofocus-default-false]
If this property is set to `true`, the component gets the focus automatically when displayed.
### `enabled` (default: true) [#enabled-default-true]
This boolean property value indicates whether the component responds to user events (`true`) or not (`false`).
### `initialValue` [#initialvalue]
This property sets the component's initial value.
```xmlui-pg copy display name="Example: using ColorPicker"
<App>
<ColorPicker
id="colorPicker"
label="Select your favorite color"
initialValue="#ff0080"
/>
<Text>Selected color: {colorPicker.value}</Text>
</App>
```
### `readOnly` (default: false) [#readonly-default-false]
Set this property to `true` to disallow changing the component value.
```xmlui-pg copy display name="Example: readOnly"
<App>
<ColorPicker initialValue="#ffff00" label="Cannot be edited" readOnly />
</App>
```
### `required` (default: false) [#required-default-false]
Set this property to `true` to indicate it must have a value before submitting the containing form.
### `validationStatus` (default: "none") [#validationstatus-default-none]
This property allows you to set the validation status of the input component.
Available values:
| Value | Description |
| --- | --- |
| `valid` | Visual indicator for an input that is accepted |
| `warning` | Visual indicator for an input that produced a warning |
| `error` | Visual indicator for an input that produced an error |
```xmlui-pg copy display name="Example: validationStatus"
<App>
<ColorPicker initialValue="#c0c0ff" label="Valid" validationStatus="valid" />
<ColorPicker initialValue="#c0c0ff" label="Warning" validationStatus="warning" />
<ColorPicker initialValue="#c0c0ff" label="Error" validationStatus="error" />
</App>
```
## Events [#events]
### `didChange` [#didchange]
This event is triggered when value of ColorPicker has changed.
### `gotFocus` [#gotfocus]
This event is triggered when the ColorPicker has received the focus.
### `lostFocus` [#lostfocus]
This event is triggered when the ColorPicker has lost the focus.
## Exposed Methods [#exposed-methods]
### `focus` [#focus]
Focus the ColorPicker component.
**Signature**: `focus(): void`
### `setValue` [#setvalue]
This method sets the current value of the ColorPicker.
**Signature**: `set value(value: string): void`
- `value`: The new value to set for the color picker.
```xmlui-pg copy display name="Example: setValue"
<App>
<App>
<ColorPicker
id="colorPicker"
label="Select your favorite color"
initialValue="#808080" />
<HStack>
<Button
label="Set to red"
onClick="colorPicker.setValue('#ff0000')" />
<Button
label="Set to green"
onClick="colorPicker.setValue('#00c000')" />
<Button
label="Set to blue"
onClick="colorPicker.setValue('#0000ff')" />
</HStack>
</App>
</App>
```
### `value` [#value]
This method returns the current value of the ColorPicker.
**Signature**: `get value(): string`
## Styling [#styling]
### Theme Variables [#theme-variables]
| Variable | Default Value (Light) | Default Value (Dark) |
| --- | --- | --- |
| [backgroundColor](../styles-and-themes/common-units/#color)-ColorPicker | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-default | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-default--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-default--hover | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-error | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-error--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-error--hover | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-success | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-success--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-success--hover | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-warning | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-warning--focus | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-ColorPicker-warning--hover | *none* | *none* |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-ColorPicker-default | *none* | *none* |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-ColorPicker-error | *none* | *none* |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-ColorPicker-success | *none* | *none* |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-ColorPicker-warning | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-ColorPicker-default | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-ColorPicker-error | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-ColorPicker-success | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-ColorPicker-warning | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-ColorPicker-default | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-ColorPicker-error | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-ColorPicker-success | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-ColorPicker-warning | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-default | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-default--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-default--hover | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-error | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-error--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-error--hover | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-success | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-success--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-success--hover | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-warning | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-warning--focus | *none* | *none* |
| [boxShadow](../styles-and-themes/common-units/#boxShadow)-ColorPicker-warning--hover | *none* | *none* |
```
--------------------------------------------------------------------------------
/xmlui/src/components/TextArea/TextAreaNative.tsx:
--------------------------------------------------------------------------------
```typescript
import React, {
type ChangeEventHandler,
type CSSProperties,
type ForwardedRef,
forwardRef,
type TextareaHTMLAttributes,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import classnames from "classnames";
import TextareaAutosize from "react-textarea-autosize";
import { isNil } from "lodash-es";
import styles from "./TextArea.module.scss";
import type { RegisterComponentApiFn, UpdateStateFn } from "../../abstractions/RendererDefs";
import { noop } from "../../components-core/constants";
import { useEvent } from "../../components-core/utils/misc";
import type { ValidationStatus } from "../abstractions";
import TextAreaResizable from "./TextAreaResizable";
import { PART_INPUT } from "../../components-core/parts";
import { composeRefs } from "@radix-ui/react-compose-refs";
export const resizeOptionKeys = ["horizontal", "vertical", "both"] as const;
export type ResizeOptions = (typeof resizeOptionKeys)[number];
type Props = {
id?: string;
value?: string;
placeholder?: string;
required?: boolean;
readOnly?: boolean;
allowCopy?: boolean;
updateState?: UpdateStateFn;
validationStatus?: ValidationStatus;
autoFocus?: boolean;
initialValue?: string;
resize?: ResizeOptions;
enterSubmits?: boolean;
escResets?: boolean;
onDidChange?: (e: any) => void;
onFocus?: () => void;
onBlur?: () => void;
controlled?: boolean;
style?: CSSProperties;
className?: string;
registerComponentApi?: RegisterComponentApiFn;
autoSize?: boolean;
maxRows?: number;
minRows?: number;
maxLength?: number;
rows?: number;
enabled?: boolean;
};
export const defaultProps = {
value: "",
placeholder: "",
required: false,
readOnly: false,
allowCopy: true,
updateState: noop,
autoFocus: false,
initialValue: "",
onDidChange: noop,
onFocus: noop,
onBlur: noop,
controlled: true,
enterSubmits: true,
rows: 2,
enabled: true,
};
export const TextArea = forwardRef(function TextArea(
{
id,
value = defaultProps.value,
placeholder = defaultProps.placeholder,
required = defaultProps.required,
readOnly = defaultProps.readOnly,
allowCopy = defaultProps.allowCopy,
updateState = defaultProps.updateState,
validationStatus,
autoFocus = defaultProps.autoFocus,
initialValue = defaultProps.initialValue,
resize,
onDidChange = defaultProps.onDidChange,
onFocus = defaultProps.onFocus,
onBlur = defaultProps.onBlur,
controlled = defaultProps.controlled,
enterSubmits = defaultProps.enterSubmits,
escResets,
style,
className,
registerComponentApi,
autoSize,
maxRows,
minRows,
maxLength,
rows = defaultProps.rows,
enabled = defaultProps.enabled,
...rest
}: Props,
forwardedRef: ForwardedRef<HTMLTextAreaElement>,
) {
// --- The component is initially unfocused
const inputRef = useRef<HTMLTextAreaElement>(null);
const ref = forwardRef ? composeRefs(forwardedRef, inputRef) : inputRef;
const [cursorPosition, setCursorPosition] = useState(null);
const [focused, setFocused] = React.useState(false);
const updateValue = useCallback(
(value: string) => {
updateState({ value: value });
onDidChange(value);
},
[onDidChange, updateState],
);
const onInputChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
(event) => {
updateValue(event.target.value);
},
[updateValue],
);
useEffect(() => {
updateState({ value: initialValue }, { initial: true });
}, [initialValue, updateState]);
useEffect(() => {
if (autoFocus) {
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}
}, [autoFocus, inputRef.current]);
// --- Execute this function when the user copies the value
const handleCopy = (event: React.SyntheticEvent) => {
if (allowCopy) {
return true;
} else {
event.preventDefault();
return false;
}
};
// --- Manage obtaining and losing the focus
const handleOnFocus = () => {
setFocused(true);
onFocus?.();
};
const handleOnBlur = () => {
setFocused(false);
onBlur?.();
};
const focus = useCallback(() => {
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}, []);
const insert = useCallback(
(text: any) => {
const input = inputRef?.current;
if (input && text) {
const start = input.selectionStart;
const value = input.value;
onInputChange({
// @ts-ignore
target: {
value: value.substring(0, start) + text + value.substring(start),
},
});
setCursorPosition(start + text.length);
}
},
[inputRef, onInputChange],
);
const setValue = useEvent((val: string) => {
updateValue(val);
});
useEffect(() => {
if (cursorPosition) {
const input = inputRef?.current;
if (input) {
input.setSelectionRange(cursorPosition, cursorPosition);
setCursorPosition(null);
}
}
}, [value, cursorPosition, inputRef]);
useEffect(() => {
registerComponentApi?.({
focus,
insert,
setValue,
});
}, [focus, insert, registerComponentApi, setValue]);
// --- Handle the Enter key press
const handleEnter = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.currentTarget.form && enterSubmits && e.key.toLowerCase() === "enter" && !e.shiftKey) {
// -- Do not generate a new line
e.preventDefault();
e.currentTarget.form?.requestSubmit();
}
if (e.currentTarget.form && escResets && e.key.toLowerCase() === "escape" && !e.shiftKey) {
e.preventDefault();
e.currentTarget.form?.reset();
}
},
[enterSubmits, escResets],
);
let classes = classnames(className, styles.textarea, {
[styles.resizeHorizontal]: resize === "horizontal",
[styles.resizeVertical]: resize === "vertical",
[styles.resizeBoth]: resize === "both",
[styles.focused]: focused,
[styles.disabled]: !enabled,
[styles.error]: validationStatus === "error",
[styles.warning]: validationStatus === "warning",
[styles.valid]: validationStatus === "valid",
});
const textareaProps: TextareaHTMLAttributes<HTMLTextAreaElement> &
React.RefAttributes<HTMLTextAreaElement> = {
...rest,
id,
className: classes,
ref,
style: style as any,
value: controlled ? value || "" : undefined,
disabled: !enabled,
name: id,
placeholder,
required,
maxLength,
"aria-multiline": true,
"aria-readonly": readOnly,
readOnly: readOnly,
onChange: onInputChange,
onCopy: handleCopy,
onFocus: handleOnFocus,
onBlur: handleOnBlur,
onKeyDown: handleEnter,
autoComplete: "off",
};
if (resize === "both" || resize === "horizontal" || resize === "vertical") {
return (
<TextAreaResizable
{...textareaProps}
data-part-id={PART_INPUT}
style={style as any}
className={classnames(classes)}
maxRows={maxRows}
minRows={minRows}
rows={rows}
/>
);
}
if (autoSize || !isNil(maxRows) || !isNil(minRows)) {
return (
<TextareaAutosize
{...textareaProps}
data-part-id={PART_INPUT}
style={style as any}
className={classnames(classes)}
maxRows={maxRows}
minRows={minRows}
rows={rows}
/>
);
}
return (
<textarea
{...textareaProps}
data-part-id={PART_INPUT}
rows={rows}
className={classnames(classes)}
/>
);
});
```
--------------------------------------------------------------------------------
/xmlui/src/components/Splitter/SplitterNative.tsx:
--------------------------------------------------------------------------------
```typescript
import React, { useEffect, useState, useMemo } from "react";
import classnames from "classnames";
import styles from "./Splitter.module.scss";
import { noop } from "../../components-core/constants";
import { parseSize, toPercentage } from "../Splitter/utils";
import type { OrientationOptions } from "../abstractions";
export const defaultProps = {
initialPrimarySize: "50%",
minPrimarySize: "0%",
maxPrimarySize: "100%",
orientation: "vertical" as OrientationOptions,
swapped: false,
floating: false,
};
type SplitterProps = {
children: React.ReactNode[] | React.ReactNode;
style?: React.CSSProperties;
className?: string;
splitterTemplate?: React.ReactNode;
orientation?: OrientationOptions;
floating?: boolean;
resize?: (sizes: [number, number]) => void;
swapped?: boolean;
initialPrimarySize?: string;
minPrimarySize?: string;
maxPrimarySize?: string;
};
export const Splitter = ({
initialPrimarySize = defaultProps.initialPrimarySize,
minPrimarySize = defaultProps.minPrimarySize,
maxPrimarySize = defaultProps.maxPrimarySize,
orientation = defaultProps.orientation,
children,
style,
className,
swapped = defaultProps.swapped,
floating = defaultProps.floating,
splitterTemplate,
resize = noop,
...rest
}: SplitterProps) => {
const [size, setSize] = useState(0);
const [splitter, setSplitter] = useState<HTMLDivElement | null>(null);
const [resizerVisible, setResizerVisible] = useState(false);
const [resizer, setResizer] = useState<HTMLDivElement | null>(null);
const [floatingResizer, setFloatingResizer] = useState<HTMLDivElement | null>(null);
const resizerElement = useMemo(
() => (floating ? floatingResizer : resizer),
[floating, resizer, floatingResizer],
);
useEffect(() => {
if (splitter) {
const containerSize =
orientation === "horizontal"
? splitter.getBoundingClientRect().width
: splitter.getBoundingClientRect().height;
const initialParsedSize = parseSize(initialPrimarySize, containerSize);
setSize(initialParsedSize);
if (resize) {
resize([
toPercentage(initialParsedSize, containerSize),
toPercentage(containerSize - initialParsedSize, containerSize),
]);
}
}
}, [initialPrimarySize, orientation, resize, splitter, swapped]);
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
if (splitter && resizerElement) {
const containerSize =
orientation === "horizontal"
? splitter.getBoundingClientRect().width
: splitter.getBoundingClientRect().height;
const newSize =
orientation === "horizontal"
? Math.min(
Math.max(
event.clientX - splitter.getBoundingClientRect().left,
parseSize(minPrimarySize, containerSize),
),
parseSize(maxPrimarySize, containerSize),
)
: Math.min(
Math.max(
event.clientY - splitter.getBoundingClientRect().top,
parseSize(minPrimarySize, containerSize),
),
parseSize(maxPrimarySize, containerSize),
);
setSize(newSize);
if (resize) {
resize([
toPercentage(newSize, containerSize),
toPercentage(containerSize - newSize, containerSize),
]);
}
}
};
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
const handleMouseDown = () => {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
if (resizerElement) {
resizerElement.addEventListener("mousedown", handleMouseDown);
}
return () => {
if (resizerElement) {
resizerElement.removeEventListener("mousedown", handleMouseDown);
}
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [minPrimarySize, maxPrimarySize, orientation, resize, floating, resizerElement, splitter]);
useEffect(() => {
const watchResizer = (event: MouseEvent) => {
const cursorPosition = orientation === "horizontal" ? event.clientX : event.clientY;
if (splitter) {
const paneStart =
orientation === "horizontal"
? splitter.getBoundingClientRect().left
: splitter.getBoundingClientRect().top;
const resizerPosition = paneStart + size;
// Check if the cursor is near the resizer (within 20 pixels)
if (cursorPosition > resizerPosition - 20 && cursorPosition < resizerPosition + 20) {
setResizerVisible(true);
} else {
setResizerVisible(false);
}
}
};
if (splitter) {
splitter.addEventListener("mousemove", watchResizer);
splitter.addEventListener("mouseleave", () => setResizerVisible(false));
}
return () => {
if (splitter) {
splitter.removeEventListener("mouseleave", () => setResizerVisible(false));
splitter.removeEventListener("mousemove", watchResizer);
}
};
}, [size, orientation, splitter]);
useEffect(() => {
if (floatingResizer) {
floatingResizer.style.opacity = resizerVisible ? "1" : "0";
}
}, [floatingResizer, resizerVisible]);
return (
<div
{...rest}
ref={(s) => setSplitter(s)}
className={classnames(
styles.splitter,
{
[styles.horizontal]: orientation === "horizontal",
[styles.vertical]: orientation === "vertical",
},
className,
)}
style={style}
>
{React.Children.count(children) > 1 ? (
<>
<div
style={!swapped ? { flexBasis: size } : {}}
className={classnames({
[styles.primaryPanel]: !swapped,
[styles.secondaryPanel]: swapped,
})}
>
{React.Children.toArray(children)[0]}
</div>
{!floating && (
<div
className={classnames(styles.resizer, {
[styles.horizontal]: orientation === "horizontal",
[styles.vertical]: orientation === "vertical",
})}
ref={(r) => setResizer(r)}
>
{splitterTemplate}
</div>
)}
<div
className={classnames({
[styles.primaryPanel]: swapped,
[styles.secondaryPanel]: !swapped,
})}
style={swapped ? { flexBasis: size } : {}}
>
{React.Children.toArray(children)[1]}
</div>
{floating && (
<div
ref={(fr) => setFloatingResizer(fr)}
className={classnames(styles.floatingResizer, {
[styles.horizontal]: orientation === "horizontal",
[styles.vertical]: orientation === "vertical",
})}
style={{
top: orientation === "horizontal" ? 0 : size,
left: orientation === "horizontal" ? size : 0,
}}
>
{splitterTemplate}
</div>
)}
</>
) : (
<>
{React.Children.toArray(children)?.[0] && (
<div className={styles.panel}>{React.Children.toArray(children)[0]}</div>
)}
</>
)}
</div>
);
};
```
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/resources/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="alternate" type="application/rss+xml" title="XMLUI Blog" href="/feed.rss" />
<script>window.__PUBLIC_PATH = '/'</script>
<base href="/">
<!-- Google tag (gtag.js) --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-S7NYYDMNPR"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-S7NYYDMNPR'); </script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.ts"></script>
<script type="text/javascript">
window.transformStops = function(stops) {
return stops.map(function(stop) {
// Helper to extract a value from additionalProperties by key
function getProp(key) {
if (!stop.additionalProperties) return '';
var propObj = stop.additionalProperties.find(function(p) { return p.key === key; });
return propObj ? propObj.value : '';
}
return {
name: stop.commonName,
zone: getProp('Zone'),
wifi: getProp('WiFi'),
toilets: getProp('Toilets'),
// A comma-separated list of line names that serve this stop
lines: stop.lines
? stop.lines.map(function(line) { return line.name; }).join(', ')
: ''
};
});
}
window.getComponentsList = function(items) {
const onlyComponents = items.filter(item => item.name === 'components');
return onlyComponents;
}
window.sliderValueToDate = function (value) {
// Convert a slider value (representing days from start date) to an actual date string
// value: number of days offset from startDate
// returns: date string in YYYY-MM-DD format
// Use fixed start date instead of window.startDate
const start = new Date('2022-06-01'); // <-- FIXED: hard-coded instead of window.startDate
// Create a new date by adding the slider value (days) to the start date
const resultDate = new Date(start);
resultDate.setDate(start.getDate() + value);
// Format the date as YYYY-MM-DD
const year = resultDate.getFullYear();
const month = String(resultDate.getMonth() + 1).padStart(2, '0'); // +1 because months are 0-indexed
const day = String(resultDate.getDate()).padStart(2, '0');
const result = `${year}-${month}-${day}`;
return result
}
window.formatMonth = function (month) {
// Input: '2022-06'
const [year, monthNum] = month.split('-');
const date = new Date(year, monthNum - 1);
return date.toLocaleString('default', { month: 'short', year: '2-digit' });
// Output: 'Jun 22'
}
window.formatToday = function (plusDays = 0) {
const date = new Date();
if (plusDays !== 0) {
date.setDate(date.getDate() + plusDays);
}
return date.toISOString().slice(0, 10).replace(/-/g, '/');
}
window.lineItemTotal = function (lineItems) {
if (!lineItems || !Array.isArray(lineItems)) {
return 0;
}
let total = 0;
for (const item of lineItems) {
if (item && typeof item.amount === 'number') {
total += item.amount;
}
}
return total;
}
window.sampleInvoice = {
id: 55,
invoice_number: "INV-1003",
client: 'Globex Corporation',
issue_date: "2025-03-13",
due_date: "2025-03-28",
status: "sent",
notes: "Monthly service invoice",
items: "[{\"id\": 14, \"name\": \"API Integration\", \"price\": 105, \"quantity\": 5, \"total\": 525}]",
total: 525,
created_at: "2025-04-19T23:45:47.937465",
paid_date: null
}
window.filterInvoicesAfter = function (data, dateAfter) {
if (!dateAfter) return data;
return data
.filter(invoice => new Date(invoice.issue_date) >= new Date(dateAfter))
.sort((a, b) => new Date(a.issue_date) - new Date(b.issue_date));
}
window.formatDate = function (date) {
return date.substring(0, 10)
}
window.filterSearchResults = function (clients, products, invoices, searchTerm) {
if (!searchTerm) return [];
const term = searchTerm.toLowerCase();
const results = [];
// Search clients - access the .value property
if (clients && clients.value) {
clients.value.forEach(client => {
const nameMatch = client.name && client.name.toLowerCase().includes(term);
const emailMatch = client.email && client.email.toLowerCase().includes(term);
const phoneMatch = client.phone && client.phone.toLowerCase().includes(term);
const addressMatch = client.address && client.address.toLowerCase().includes(term);
if (nameMatch || emailMatch || phoneMatch || addressMatch) {
let snippet = client.name;
if (nameMatch) snippet = `Name: ${client.name}`;
else if (emailMatch) snippet = `Email: ${client.email}`;
else if (phoneMatch) snippet = `Phone: ${client.phone}`;
else if (addressMatch) snippet = `Address: ${client.address}`;
results.push({
table_name: 'clients',
record_id: client.id,
title: client.name,
snippet: snippet
});
}
});
}
// Search products
if (products && products.value) {
products.value.forEach(product => {
const nameMatch = product.name && product.name.toLowerCase().includes(term);
const descMatch = product.description && product.description.toLowerCase().includes(term);
if (nameMatch || descMatch) {
let snippet = product.name;
if (nameMatch) snippet = `Product: ${product.name}`;
else if (descMatch) snippet = `Description: ${product.description}`;
results.push({
table_name: 'products',
record_id: product.id,
title: product.name,
snippet: snippet
});
}
});
}
// Search invoices
if (invoices && invoices.value) {
invoices.value.forEach(invoice => {
const numberMatch = invoice.invoice_number && invoice.invoice_number.toLowerCase().includes(term);
const notesMatch = invoice.notes && invoice.notes.toLowerCase().includes(term);
const itemsMatch = invoice.items && invoice.items.toLowerCase().includes(term);
if (numberMatch || notesMatch || itemsMatch) {
let snippet = `Invoice: ${invoice.invoice_number}`;
if (numberMatch) snippet = `Invoice: ${invoice.invoice_number}`;
else if (notesMatch) snippet = `Notes: ${invoice.notes.substring(0, 100)}...`;
else if (itemsMatch) snippet = `Items: ${invoice.items.substring(0, 100)}...`;
results.push({
table_name: 'invoices',
record_id: invoice.id,
title: invoice.invoice_number,
snippet: snippet
});
}
});
}
return results;
}
window.handleHelloClick = function(event) {
console.log('Hello World clicked!', event);
alert('Button clicked!');
};
window.handleHelloReset = function(event) {
console.log('Hello World reset!', event);
alert('Counter was reset!');
};
</script>
</body>
</html>
```
--------------------------------------------------------------------------------
/xmlui/src/components/ProgressBar/ProgressBar.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { test, expect } from "../../testing/fixtures";
// =============================================================================
// BASIC FUNCTIONALITY TESTS
// =============================================================================
test.describe("Basic Functionality", () => {
test("component renders with basic props", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{0.5}" />`);
const driver = await createProgressBarDriver();
await expect(driver.component).toBeVisible();
await expect(driver.bar).toBeVisible();
});
test("displays progress value correctly", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{0.7}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBeCloseTo(0.7, 2);
});
test("handles zero value", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{0}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
test("handles maximum value", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{1}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(1);
});
test("handles decimal values", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{0.335}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBeCloseTo(0.335, 3);
});
test("handles string value prop", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="0.6" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBeCloseTo(0.6, 2);
});
test("component renders without value prop", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar />`);
const driver = await createProgressBarDriver();
await expect(driver.component).toBeVisible();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
});
// =============================================================================
// ACCESSIBILITY TESTS
// =============================================================================
test.describe("Accessibility", () => {
test("has proper accessibility roles", async ({ initTestBed, page }) => {
await initTestBed(`<ProgressBar value="{0.6}" />`);
const bar = page.getByRole("progressbar");
await expect(bar).toHaveAttribute("aria-valuemin", "0");
await expect(bar).toHaveAttribute("aria-valuemax", "100");
await expect(bar).toHaveAttribute("aria-valuenow", "60");
});
test("maintains accessibility attributes with edge case values", async ({
initTestBed,
page,
}) => {
await initTestBed(`<ProgressBar value="{-5}" />`);
const bar = page.getByRole("progressbar");
await expect(bar).toHaveAttribute("aria-valuenow", "0");
await expect(bar).toHaveAttribute("aria-valuemin", "0");
await expect(bar).toHaveAttribute("aria-valuemax", "100");
});
});
// =============================================================================
// EDGE CASES TESTS
// =============================================================================
test.describe("Edge Cases", () => {
test("handles negative values by clamping to 0", async ({
initTestBed,
createProgressBarDriver,
}) => {
await initTestBed(`<ProgressBar value="{-0.5}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
test("handles values greater than 1 by clamping to 1", async ({
initTestBed,
createProgressBarDriver,
}) => {
await initTestBed(`<ProgressBar value="{1.5}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(1);
});
test("handles NaN values", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{NaN}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
test("handles undefined value", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{undefined}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
test("handles null value", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{null}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
test("handles very small decimal values", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{0.001}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBeCloseTo(0.001, 3);
});
test("handles string values that are not numbers", async ({
initTestBed,
createProgressBarDriver,
}) => {
await initTestBed(`<ProgressBar value="invalid" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
test("handles empty string value", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
test("handles 'true' value", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{true}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(1);
});
test("handles 'false' value", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{false}" />`);
const driver = await createProgressBarDriver();
const value = await driver.getProgressRatio();
expect(value).toBe(0);
});
});
// =============================================================================
// THEME VARIABLES TESTS
// =============================================================================
test.describe("Theme Variables", () => {
test("applies theme variables correctly", async ({ initTestBed, createProgressBarDriver }) => {
await initTestBed(`<ProgressBar value="{0.5}" />`, {
testThemeVars: {
"borderRadius-ProgressBar": "10px",
"borderRadius-indicator-ProgressBar": "5px",
"thickness-ProgressBar": "20px",
"backgroundColor-ProgressBar": "rgb(255, 0, 0)",
"color-indicator-ProgressBar": "rgb(0, 255, 0)",
},
});
const driver = await createProgressBarDriver();
await expect(driver.component).toHaveCSS("border-radius", "10px");
await expect(driver.bar).toHaveCSS("border-radius", "5px");
await expect(driver.component).toHaveCSS("height", "20px");
await expect(driver.component).toHaveCSS("background-color", "rgb(255, 0, 0)");
await expect(driver.bar).toHaveCSS("background-color", "rgb(0, 255, 0)");
});
});
```
--------------------------------------------------------------------------------
/xmlui/src/components/AppHeader/AppHeader.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { test, expect } from "../../testing/fixtures";
const CODE = `
<AppHeader>
Hello, World!
</AppHeader>
`;
// =============================================================================
// BASIC FUNCTIONALITY TESTS
// =============================================================================
test.describe("Basic Functionality", () => {
test("renders with basic props", async ({ initTestBed, createAppHeaderDriver }) => {
await initTestBed(`<App><AppHeader testId="header" /></App>`);
const driver = await createAppHeaderDriver();
await expect(driver.component).toBeVisible();
});
test("renders with title prop", async ({ initTestBed, createAppHeaderDriver }) => {
const TITLE = "Application Title";
await initTestBed(`<App><AppHeader testId="header" title="${TITLE}" /></App>`);
const driver = await createAppHeaderDriver();
await expect(driver.component).toBeVisible();
await expect(driver.component).toContainText(TITLE);
});
test("renders with custom logo content", async ({ initTestBed, createAppHeaderDriver, page }) => {
await initTestBed(`
<App>
<AppHeader testId="header">
<property name="logoTemplate">
<Icon name="star" testId="customLogo" />
</property>
</AppHeader>
</App>`);
const driver = await createAppHeaderDriver();
await expect(driver.component).toBeVisible();
await expect(page.getByTestId("customLogo")).toBeVisible();
});
});
// =============================================================================
// ACCESSIBILITY TESTS
// =============================================================================
test.describe("Accessibility", () => {
test("has correct accessibility structure", async ({
initTestBed,
createAppHeaderDriver,
}) => {
// TODO: review these Copilot-created tests
await initTestBed(`<App><AppHeader testId="header" title="Accessible Header" /></App>`);
const driver = await createAppHeaderDriver("header");
// AppHeader should have a role of "banner"
await expect(driver.component).toHaveAttribute("role", "banner");
});
test("properly handles focus management", async ({ initTestBed, page }) => {
await initTestBed(`
<App>
<AppHeader testId="header">
<NavLink testId="headerLink1" to="#">Link 1</NavLink>
<NavLink testId="headerLink2" to="#">Link 2</NavLink>
</AppHeader>
</App>`);
// Test that links within the header are keyboard focusable
await page.getByTestId("headerLink1").focus();
await expect(page.getByTestId("headerLink1")).toBeFocused();
// Test tab navigation works between links
await page.keyboard.press("Tab");
await expect(page.getByTestId("headerLink2")).toBeFocused();
});
});
// =============================================================================
// THEME VARIABLE TESTS
// =============================================================================
test.describe("Theme Variables", () => {
test("applies background color theme variable correctly", async ({
initTestBed,
createAppHeaderDriver,
}) => {
const EXPECTED_BG_COLOR = "rgb(0, 240, 0)";
await initTestBed(`<App><AppHeader testId="header" /></App>`, {
testThemeVars: {
"backgroundColor-AppHeader": EXPECTED_BG_COLOR,
},
});
const driver = await createAppHeaderDriver("header");
await expect(driver.component).toHaveCSS("background-color", EXPECTED_BG_COLOR);
});
test("applies height theme variable correctly", async ({
initTestBed,
createAppHeaderDriver,
}) => {
const EXPECTED_HEIGHT = "80px";
await initTestBed(`<App><AppHeader testId="header" /></App>`, {
testThemeVars: {
"height-AppHeader": EXPECTED_HEIGHT,
},
});
const driver = await createAppHeaderDriver("header");
await expect(driver.component).toHaveCSS("height", EXPECTED_HEIGHT);
});
test("border", async ({ initTestBed, createAppHeaderDriver }) => {
const EXPECTED_COLOR = "rgb(255, 0, 0)";
const EXPECTED_WIDTH = "5px";
const EXPECTED_STYLE = "dotted";
await initTestBed(CODE, {
testThemeVars: {
"border-AppHeader": `${EXPECTED_STYLE} ${EXPECTED_COLOR} ${EXPECTED_WIDTH}`,
},
});
const component = (await createAppHeaderDriver()).component;
await expect(component).toHaveCSS("border-top-color", EXPECTED_COLOR);
await expect(component).toHaveCSS("border-top-width", EXPECTED_WIDTH);
await expect(component).toHaveCSS("border-top-style", EXPECTED_STYLE);
await expect(component).toHaveCSS("border-right-color", EXPECTED_COLOR);
await expect(component).toHaveCSS("border-right-width", EXPECTED_WIDTH);
await expect(component).toHaveCSS("border-right-style", EXPECTED_STYLE);
await expect(component).toHaveCSS("border-bottom-color", EXPECTED_COLOR);
await expect(component).toHaveCSS("border-bottom-width", EXPECTED_WIDTH);
await expect(component).toHaveCSS("border-bottom-style", EXPECTED_STYLE);
await expect(component).toHaveCSS("border-left-color", EXPECTED_COLOR);
await expect(component).toHaveCSS("border-left-width", EXPECTED_WIDTH);
await expect(component).toHaveCSS("border-left-style", EXPECTED_STYLE);
});
test("borderLeft", async ({ initTestBed, createAppHeaderDriver }) => {
const EXPECTED_COLOR = "rgb(255, 0, 0)";
const EXPECTED_WIDTH = "5px";
const EXPECTED_STYLE = "dotted";
await initTestBed(CODE, {
testThemeVars: {
"borderLeft-AppHeader": `${EXPECTED_STYLE} ${EXPECTED_COLOR} ${EXPECTED_WIDTH}`,
},
});
const component = (await createAppHeaderDriver()).component;
await expect(component).not.toHaveCSS("border-top-color", EXPECTED_COLOR);
await expect(component).not.toHaveCSS("border-right-color", EXPECTED_COLOR);
await expect(component).not.toHaveCSS("border-bottom-color", EXPECTED_COLOR);
await expect(component).toHaveCSS("border-left-color", EXPECTED_COLOR);
await expect(component).toHaveCSS("border-left-width", EXPECTED_WIDTH);
await expect(component).toHaveCSS("border-left-style", EXPECTED_STYLE);
});
});
// =============================================================================
// EDGE CASE TESTS
// =============================================================================
test.describe("Edge Cases", () => {
test("handles undefined props gracefully", async ({ initTestBed, createAppHeaderDriver }) => {
await initTestBed(`<App><AppHeader testId="header" title="{undefined}" /></App>`);
const driver = await createAppHeaderDriver("header");
// Component should render without errors when props are undefined
await expect(driver.component).toBeVisible();
});
test("handles special characters in title prop", async ({
initTestBed,
createAppHeaderDriver,
}) => {
const SPECIAL_TITLE = "应用程序 & Test – Special Character's Test 🚀 ñáéíóú ≤≥∞ «»";
await initTestBed(`<App><AppHeader testId="header" title="${SPECIAL_TITLE}" /></App>`);
const driver = await createAppHeaderDriver("header");
// Special characters should display correctly
await expect(driver.component).toContainText(SPECIAL_TITLE);
});
test("handles empty child components", async ({ initTestBed, createAppHeaderDriver }) => {
await initTestBed(`
<App>
<AppHeader testId="header">
<property name="logoTemplate"></property>
</AppHeader>
</App>
`);
const driver = await createAppHeaderDriver("header");
// Component should handle empty template slots gracefully
await expect(driver.component).toBeVisible();
});
});
```
--------------------------------------------------------------------------------
/xmlui/src/components-core/script-runner/process-statement-common.ts:
--------------------------------------------------------------------------------
```typescript
import type { BlockScope } from "../../abstractions/scripting/BlockScope";
import type { LogicalThread } from "../../abstractions/scripting/LogicalThread";
import type { LoopScope } from "../../abstractions/scripting/LoopScope";
import {
T_ARROW_EXPRESSION,
T_EMPTY_STATEMENT,
T_FUNCTION_DECLARATION,
type FunctionDeclaration,
type LoopStatement,
type Statement,
type TryStatement,
} from "./ScriptingSourceTree";
import type { TryScope } from "../../abstractions/scripting/TryScopeExp";
import { obtainClosures } from "./eval-tree-common";
import type {
StatementQueueItem,
StatementWithInfo} from "./statement-queue";
import {
mapStatementsToQueueItems,
} from "./statement-queue";
import type { BindingTreeEvaluationContext } from "./BindingTreeEvaluationContext";
import { createXmlUiTreeNodeId } from "../../parsers/scripting/Parser";
export function innermostLoopScope(thread: LogicalThread): LoopScope {
if (!thread.loops || thread.loops.length === 0) {
throw new Error("Missing loop scope");
}
return thread.loops[thread.loops.length - 1];
}
export function innermostBlockScope(thread: LogicalThread): BlockScope | undefined {
if (!thread.blocks || thread.blocks.length === 0) return undefined;
return thread.blocks[thread.blocks.length - 1];
}
export function innermostTryScope(thread: LogicalThread): TryScope {
if (!thread.tryBlocks || thread.tryBlocks.length === 0) {
throw new Error("Missing try scope");
}
return thread.tryBlocks[thread.tryBlocks.length - 1];
}
export function createLoopScope(thread: LogicalThread, continueOffset = 0): LoopScope {
thread.loops ??= [];
const breakDepth = thread.blocks?.length ?? 0;
const tryDepth = thread.tryBlocks?.length ?? 0;
const loopScope: LoopScope = {
breakLabel: -1,
continueLabel: -1,
breakBlockDepth: breakDepth,
continueBlockDepth: breakDepth + continueOffset,
tryBlockDepth: tryDepth,
};
thread.loops.push(loopScope);
return loopScope;
}
export function releaseLoopScope(thread: LogicalThread, skipContinuation = true): void {
const loopScope = innermostLoopScope(thread);
if (skipContinuation) {
thread.loops?.pop();
}
if (thread.blocks) {
thread.blocks.length = skipContinuation
? loopScope.breakBlockDepth
: loopScope.continueBlockDepth;
}
}
// --- Converts statement to queable items
export function toStatementItems(statements: Statement[]): StatementWithInfo[] {
return statements.map((s) => ({ statement: s }));
}
// --- Create a guarded statement for the specified one
export function guard(statement: Statement): StatementWithInfo {
return { statement, execInfo: { guard: true } };
}
// --- Create a closing statement that removes the block scope
export function closing(): StatementWithInfo {
return {
statement: { type: T_EMPTY_STATEMENT, nodeId: createXmlUiTreeNodeId() },
execInfo: { removeBlockScope: true },
};
}
// --- Create a list of body statements according to the specified loop statement and scope
export function provideLoopBody(
loopScope: LoopScope,
loopStatement: LoopStatement,
breakLabelValue: number | undefined,
): StatementQueueItem[] {
// --- Stay in the loop, add the body and the guard condition
const guardStatement = guard(loopStatement);
const toUnshift = mapStatementsToQueueItems([{ statement: loopStatement.body }, guardStatement]);
// --- The next queue label is for "break"
loopScope.breakLabel = breakLabelValue ?? -1;
// --- The guard action's label is for "continue"
loopScope.continueLabel = toUnshift[1].label;
return toUnshift;
}
// --- Create a list of body statements according to the specified try statement scope
export function createTryScope(thread: LogicalThread, tryStatement: TryStatement): TryScope {
thread.tryBlocks ??= [];
const loopScope: TryScope = {
statement: tryStatement,
processingPhase: "try",
tryLabel: -1,
};
thread.tryBlocks.push(loopScope);
return loopScope;
}
// --- Provide a body for the try block
export function provideTryBody(thread: LogicalThread, tryScope: TryScope): StatementQueueItem[] {
// --- Stay in the error handling block, add the body and the guard condition
const guardStatement = guard(tryScope.statement);
// --- New block scope for try
thread.blocks!.push({
vars: {},
});
const toUnshift = mapStatementsToQueueItems([
...toStatementItems(tryScope.statement.tryB.stmts),
closing(),
guardStatement,
]);
tryScope.tryLabel = toUnshift[toUnshift.length - 1].label;
return toUnshift;
}
// --- Provide a body for the catch block
export function provideCatchBody(
thread: LogicalThread,
tryScope: TryScope,
): StatementQueueItem[] {
// --- Stay in the error handling block, add the body and the guard condition
const guardStatement = guard(tryScope.statement);
// --- New block scope for catch
thread.blocks!.push({
vars: {},
});
const toUnshift = mapStatementsToQueueItems([
...toStatementItems(tryScope.statement.catchB!.stmts),
closing(),
guardStatement,
]);
tryScope.tryLabel = toUnshift[toUnshift.length - 1].label;
return toUnshift;
}
// --- Provide a body for the finally block
export function provideFinallyBody(
thread: LogicalThread,
tryScope: TryScope,
): StatementQueueItem[] {
// --- Stay in the error handling block, add the body and the guard condition
const guardStatement = guard(tryScope.statement);
// --- New block scope for finally
thread.blocks!.push({
vars: {},
});
const finallyBlock = tryScope.statement.finallyB;
const toUnshift = mapStatementsToQueueItems([
...toStatementItems(finallyBlock ? finallyBlock.stmts : []),
closing(),
guardStatement,
]);
tryScope.tryLabel = toUnshift[toUnshift.length - 1].label;
return toUnshift;
}
// --- Provide a body for the error block in finally
export function provideFinallyErrorBody(tryScope: TryScope): StatementQueueItem[] {
// --- Stay in the error handling block, add the body and the guard condition
const guardStatement = guard(tryScope.statement);
const toUnshift = mapStatementsToQueueItems([guardStatement]);
tryScope.tryLabel = toUnshift[0].label;
return toUnshift;
}
// --- Ensure that the evaluation context has a main thread
export function ensureMainThread(evalContext: BindingTreeEvaluationContext): LogicalThread {
if (!evalContext.mainThread) {
evalContext.mainThread = {
childThreads: [],
blocks: [
{
vars: {},
},
],
loops: [],
breakLabelValue: -1,
};
}
return evalContext.mainThread;
}
// --- Hoist function definitions to the innermost block scope
export function hoistFunctionDeclarations(thread: LogicalThread, statements: Statement[]): void {
const block = innermostBlockScope(thread);
if (!block) {
throw new Error("Missing block scope");
}
statements
.filter((stmt) => stmt.type === T_FUNCTION_DECLARATION)
.forEach((stmt) => {
const funcDecl = stmt as FunctionDeclaration;
// --- Turn the function into an arrow expression
const arrowExpression = {
type: T_ARROW_EXPRESSION,
args: funcDecl.args,
statement: funcDecl.stmt,
closureContext: obtainClosures(thread),
_ARROW_EXPR_: true,
};
// --- Remove the functions from the closure list
// --- Check name uniqueness
const id = funcDecl.id.name;
if (block.vars[id]) {
throw new Error(`Variable ${id} is already declared in the current scope.`);
}
// --- Function is a constant
block.vars[id] = arrowExpression;
block.constVars ??= new Set<string>();
block.constVars.add(id);
});
}
```