This is page 49 of 144. Use http://codebase.md/xmlui-org/xmlui/tools/vscode/resources/assets/img/bg-iphone-14-pro.jpg?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ ├── cyan-tools-design.md
│ ├── every-moments-teach.md
│ ├── full-symbols-accept.md
│ └── tricky-zoos-crash.md
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog-optimized.yml
│ ├── deploy-blog-swa.yml
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs-swa.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── an-advanced-codefence.gif
│ │ │ │ ├── an-advanced-codefence.mp4
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── codefence-runner.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ ├── lorem-ipsum.png
│ │ │ │ ├── playground-checkbox-source.png
│ │ │ │ ├── playground.png
│ │ │ │ ├── use-xmlui-mcp-to-find-a-howto.png
│ │ │ │ └── xmlui-demo-gallery.png
│ │ │ ├── introducing-xmlui.md
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ ├── xmlui-playground.md
│ │ │ └── xmlui-powered-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ ├── staticwebapp.config.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ └── PageNotFound.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── blog-theme.ts
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── HelloMd.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── control-cache-invalidation.md
│ │ │ │ ├── debounce-user-input-for-api-calls.md
│ │ │ │ ├── debounce-with-changelistener.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── github.svg
│ │ │ ├── images
│ │ │ │ ├── apiaction-tutorial
│ │ │ │ │ ├── add-success.png
│ │ │ │ │ ├── apiaction-param.png
│ │ │ │ │ ├── change-completed.png
│ │ │ │ │ ├── change-in-progress.png
│ │ │ │ │ ├── confirm-delete.png
│ │ │ │ │ ├── data-error.png
│ │ │ │ │ ├── data-progress.png
│ │ │ │ │ ├── data-success.png
│ │ │ │ │ ├── display-1.png
│ │ │ │ │ ├── item-deleted.png
│ │ │ │ │ ├── item-updated.png
│ │ │ │ │ ├── missing-api-key.png
│ │ │ │ │ ├── new-item-added.png
│ │ │ │ │ └── test-message.png
│ │ │ │ ├── chat-api
│ │ │ │ │ └── domain-model.svg
│ │ │ │ ├── components
│ │ │ │ │ ├── image
│ │ │ │ │ │ └── breakfast.jpg
│ │ │ │ │ ├── markdown
│ │ │ │ │ │ └── colors.png
│ │ │ │ │ └── modal
│ │ │ │ │ ├── deep_link_dialog_1.jpg
│ │ │ │ │ └── deep_link_dialog_2.jpg
│ │ │ │ ├── create-apps
│ │ │ │ │ ├── collapsed-vertical.png
│ │ │ │ │ ├── using-forms-warning-dialog.png
│ │ │ │ │ └── using-forms.png
│ │ │ │ ├── datasource-tutorial
│ │ │ │ │ ├── data-with-header.png
│ │ │ │ │ ├── filtered-data.png
│ │ │ │ │ ├── filtered-items.png
│ │ │ │ │ ├── initial-page-items.png
│ │ │ │ │ ├── list-items.png
│ │ │ │ │ ├── next-page-items.png
│ │ │ │ │ ├── no-data.png
│ │ │ │ │ ├── pagination-1.jpg
│ │ │ │ │ ├── pagination-1.png
│ │ │ │ │ ├── polling-1.png
│ │ │ │ │ ├── refetch-data.png
│ │ │ │ │ ├── slow-loading.png
│ │ │ │ │ ├── test-message.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── unconventional-data.png
│ │ │ │ │ └── unfiltered-items.png
│ │ │ │ ├── flower.jpg
│ │ │ │ ├── get-started
│ │ │ │ │ ├── add-new-contact.png
│ │ │ │ │ ├── app-modified.png
│ │ │ │ │ ├── app-start.png
│ │ │ │ │ ├── app-with-boxes.png
│ │ │ │ │ ├── app-with-toast.png
│ │ │ │ │ ├── boilerplate-structure.png
│ │ │ │ │ ├── cl-initial.png
│ │ │ │ │ ├── cl-start.png
│ │ │ │ │ ├── contact-counts.png
│ │ │ │ │ ├── contact-dialog-title.png
│ │ │ │ │ ├── contact-dialog.png
│ │ │ │ │ ├── contact-menus.png
│ │ │ │ │ ├── contact-predicates.png
│ │ │ │ │ ├── context-menu.png
│ │ │ │ │ ├── dashboard-numbers.png
│ │ │ │ │ ├── default-contact-list.png
│ │ │ │ │ ├── delete-contact.png
│ │ │ │ │ ├── delete-task.png
│ │ │ │ │ ├── detailed-template.png
│ │ │ │ │ ├── edit-contact-details.png
│ │ │ │ │ ├── edited-contact-saved.png
│ │ │ │ │ ├── empty-sections.png
│ │ │ │ │ ├── filter-completed.png
│ │ │ │ │ ├── fullwidth-desktop.png
│ │ │ │ │ ├── fullwidth-mobile.png
│ │ │ │ │ ├── initial-table.png
│ │ │ │ │ ├── items-and-badges.png
│ │ │ │ │ ├── loading-message.png
│ │ │ │ │ ├── new-contact-button.png
│ │ │ │ │ ├── new-contact-saved.png
│ │ │ │ │ ├── no-empty-sections.png
│ │ │ │ │ ├── personal-todo-initial.png
│ │ │ │ │ ├── piechart.png
│ │ │ │ │ ├── review-today.png
│ │ │ │ │ ├── rudimentary-dashboard.png
│ │ │ │ │ ├── section-collapsed.png
│ │ │ │ │ ├── sectioned-items.png
│ │ │ │ │ ├── sections-ordered.png
│ │ │ │ │ ├── spacex-list-with-links.png
│ │ │ │ │ ├── spacex-list.png
│ │ │ │ │ ├── start-personal-todo-1.png
│ │ │ │ │ ├── submit-new-contact.png
│ │ │ │ │ ├── submit-new-task.png
│ │ │ │ │ ├── syntax-highlighting.png
│ │ │ │ │ ├── table-with-badge.png
│ │ │ │ │ ├── template-with-card.png
│ │ │ │ │ ├── test-emulated-api.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── todo-logo.png
│ │ │ │ │ └── xmlui-tools.png
│ │ │ │ ├── HelloApp.png
│ │ │ │ ├── HelloApp2.png
│ │ │ │ ├── logos
│ │ │ │ │ ├── xmlui1.svg
│ │ │ │ │ ├── xmlui2.svg
│ │ │ │ │ ├── xmlui3.svg
│ │ │ │ │ ├── xmlui4.svg
│ │ │ │ │ ├── xmlui5.svg
│ │ │ │ │ ├── xmlui6.svg
│ │ │ │ │ └── xmlui7.svg
│ │ │ │ ├── pdf
│ │ │ │ │ └── dummy-pdf.jpg
│ │ │ │ ├── rendering-engine
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ ├── Component.svg
│ │ │ │ │ ├── CompoundComponent.svg
│ │ │ │ │ ├── RootComponent.svg
│ │ │ │ │ └── tree-with-containers.svg
│ │ │ │ ├── reviewers-guide
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ └── incbutton-in-action.png
│ │ │ │ ├── tools
│ │ │ │ │ └── boilerplate-structure.png
│ │ │ │ ├── try.svg
│ │ │ │ ├── tutorial
│ │ │ │ │ ├── app-chat-history.png
│ │ │ │ │ ├── app-content-placeholder.png
│ │ │ │ │ ├── app-header-and-content.png
│ │ │ │ │ ├── app-links-channel-selected.png
│ │ │ │ │ ├── app-links-click.png
│ │ │ │ │ ├── app-navigation.png
│ │ │ │ │ ├── finished-ex01.png
│ │ │ │ │ ├── finished-ex02.png
│ │ │ │ │ ├── hello.png
│ │ │ │ │ ├── splash-screen-advanced.png
│ │ │ │ │ ├── splash-screen-after-click.png
│ │ │ │ │ ├── splash-screen-centered.png
│ │ │ │ │ ├── splash-screen-events.png
│ │ │ │ │ ├── splash-screen-expression.png
│ │ │ │ │ ├── splash-screen-reuse-after.png
│ │ │ │ │ ├── splash-screen-reuse-before.png
│ │ │ │ │ └── splash-screen.png
│ │ │ │ └── tutorial-01.png
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── Boxes.xmlui
│ │ │ ├── Breadcrumb.xmlui
│ │ │ ├── ChangeLog.xmlui
│ │ │ ├── ColorPalette.xmlui
│ │ │ ├── DocumentLinks.xmlui
│ │ │ ├── DocumentPage.xmlui
│ │ │ ├── DocumentPageNoTOC.xmlui
│ │ │ ├── Icons.xmlui
│ │ │ ├── IncButton.xmlui
│ │ │ ├── IncButton2.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── docs-theme.ts
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── Animation.tsx
│ │ ├── AnimationNative.tsx
│ │ ├── FadeAnimation.tsx
│ │ ├── FadeInAnimation.tsx
│ │ ├── FadeOutAnimation.tsx
│ │ ├── index.tsx
│ │ ├── ScaleAnimation.tsx
│ │ └── SlideInAnimation.tsx
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── HelloWorld.module.scss
│ │ ├── HelloWorld.tsx
│ │ ├── HelloWorldNative.tsx
│ │ └── index.tsx
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── IPhoneFrame.module.scss
│ │ ├── IPhoneFrame.tsx
│ │ ├── MacOSAppFrame.module.scss
│ │ ├── MacOSAppFrame.tsx
│ │ ├── WindowsAppFrame.module.scss
│ │ └── WindowsAppFrame.tsx
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── LazyPdfNative.tsx
│ │ ├── Pdf.module.scss
│ │ └── Pdf.tsx
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── hooks
│ │ │ ├── usePlayground.ts
│ │ │ └── useToast.ts
│ │ ├── index.tsx
│ │ ├── playground
│ │ │ ├── Box.module.scss
│ │ │ ├── Box.tsx
│ │ │ ├── CodeSelector.module.scss
│ │ │ ├── CodeSelector.tsx
│ │ │ ├── ConfirmationDialog.module.scss
│ │ │ ├── ConfirmationDialog.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── Header.module.scss
│ │ │ ├── Header.tsx
│ │ │ ├── Playground.tsx
│ │ │ ├── PlaygroundContent.module.scss
│ │ │ ├── PlaygroundContent.tsx
│ │ │ ├── PlaygroundNative.module.scss
│ │ │ ├── PlaygroundNative.tsx
│ │ │ ├── Preview.tsx
│ │ │ ├── StandalonePlayground.tsx
│ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ ├── ThemeSwitcher.module.scss
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ └── utils.ts
│ │ ├── providers
│ │ │ ├── Toast.module.scss
│ │ │ └── ToastProvider.tsx
│ │ ├── state
│ │ │ └── store.ts
│ │ ├── themes
│ │ │ └── theme.ts
│ │ └── utils
│ │ └── helpers.ts
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Search.module.scss
│ │ └── Search.tsx
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Spreadsheet.tsx
│ │ └── SpreadsheetNative.tsx
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ └── src
│ ├── Carousel
│ │ ├── Carousel.module.scss
│ │ ├── Carousel.tsx
│ │ ├── CarouselContext.tsx
│ │ └── CarouselNative.tsx
│ ├── FancyButton
│ │ ├── FancyButton.module.scss
│ │ ├── FancyButton.tsx
│ │ └── FancyButton.xmlui
│ ├── Hello
│ │ ├── Hello.tsx
│ │ ├── Hello.xmlui
│ │ └── Hello.xmlui.xs
│ ├── HeroSection
│ │ ├── HeroSection.module.scss
│ │ ├── HeroSection.spec.ts
│ │ ├── HeroSection.tsx
│ │ └── HeroSectionNative.tsx
│ ├── index.tsx
│ ├── ScrollToTop
│ │ ├── ScrollToTop.module.scss
│ │ ├── ScrollToTop.tsx
│ │ └── ScrollToTopNative.tsx
│ └── vite-env.d.ts
├── playwright.config.ts
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.cjs
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── component-metadata.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── theme-variables-refactoring.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── extract-component-metadata.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── generate-metadata-markdown.js
│ ├── get-langserver-metadata.js
│ ├── inline-links.mjs
│ └── README-e2e-summary.md
├── src
│ ├── abstractions
│ │ ├── _conventions.md
│ │ ├── ActionDefs.ts
│ │ ├── AppContextDefs.ts
│ │ ├── ComponentDefs.ts
│ │ ├── ContainerDefs.ts
│ │ ├── ExtensionDefs.ts
│ │ ├── FunctionDefs.ts
│ │ ├── RendererDefs.ts
│ │ ├── scripting
│ │ │ ├── BlockScope.ts
│ │ │ ├── Compilation.ts
│ │ │ ├── LogicalThread.ts
│ │ │ ├── LoopScope.ts
│ │ │ ├── modules.ts
│ │ │ ├── ScriptParserError.ts
│ │ │ ├── Token.ts
│ │ │ ├── TryScope.ts
│ │ │ └── TryScopeExp.ts
│ │ └── ThemingDefs.ts
│ ├── components
│ │ ├── _conventions.md
│ │ ├── abstractions.ts
│ │ ├── Accordion
│ │ │ ├── Accordion.md
│ │ │ ├── Accordion.module.scss
│ │ │ ├── Accordion.spec.ts
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── AccordionItem.tsx
│ │ │ ├── AccordionItemNative.tsx
│ │ │ └── AccordionNative.tsx
│ │ ├── Animation
│ │ │ └── AnimationNative.tsx
│ │ ├── APICall
│ │ │ ├── APICall.md
│ │ │ ├── APICall.spec.ts
│ │ │ ├── APICall.tsx
│ │ │ └── APICallNative.tsx
│ │ ├── App
│ │ │ ├── App.md
│ │ │ ├── App.module.scss
│ │ │ ├── App.spec.ts
│ │ │ ├── App.tsx
│ │ │ ├── AppLayoutContext.ts
│ │ │ ├── AppNative.tsx
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── AppHeader
│ │ │ ├── AppHeader.md
│ │ │ ├── AppHeader.module.scss
│ │ │ ├── AppHeader.spec.ts
│ │ │ ├── AppHeader.tsx
│ │ │ └── AppHeaderNative.tsx
│ │ ├── AppState
│ │ │ ├── AppState.md
│ │ │ ├── AppState.spec.ts
│ │ │ ├── AppState.tsx
│ │ │ └── AppStateNative.tsx
│ │ ├── AutoComplete
│ │ │ ├── AutoComplete.md
│ │ │ ├── AutoComplete.module.scss
│ │ │ ├── AutoComplete.spec.ts
│ │ │ ├── AutoComplete.tsx
│ │ │ ├── AutoCompleteContext.tsx
│ │ │ └── AutoCompleteNative.tsx
│ │ ├── Avatar
│ │ │ ├── Avatar.md
│ │ │ ├── Avatar.module.scss
│ │ │ ├── Avatar.spec.ts
│ │ │ ├── Avatar.tsx
│ │ │ └── AvatarNative.tsx
│ │ ├── Backdrop
│ │ │ ├── Backdrop.md
│ │ │ ├── Backdrop.module.scss
│ │ │ ├── Backdrop.spec.ts
│ │ │ ├── Backdrop.tsx
│ │ │ └── BackdropNative.tsx
│ │ ├── Badge
│ │ │ ├── Badge.md
│ │ │ ├── Badge.module.scss
│ │ │ ├── Badge.spec.ts
│ │ │ ├── Badge.tsx
│ │ │ └── BadgeNative.tsx
│ │ ├── Bookmark
│ │ │ ├── Bookmark.md
│ │ │ ├── Bookmark.module.scss
│ │ │ ├── Bookmark.spec.ts
│ │ │ ├── Bookmark.tsx
│ │ │ └── BookmarkNative.tsx
│ │ ├── Breakout
│ │ │ ├── Breakout.module.scss
│ │ │ ├── Breakout.spec.ts
│ │ │ ├── Breakout.tsx
│ │ │ └── BreakoutNative.tsx
│ │ ├── Button
│ │ │ ├── Button-style.spec.ts
│ │ │ ├── Button.md
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.spec.ts
│ │ │ ├── Button.tsx
│ │ │ └── ButtonNative.tsx
│ │ ├── Card
│ │ │ ├── Card.md
│ │ │ ├── Card.module.scss
│ │ │ ├── Card.spec.ts
│ │ │ ├── Card.tsx
│ │ │ └── CardNative.tsx
│ │ ├── Carousel
│ │ │ ├── Carousel.md
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.spec.ts
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ ├── CarouselItem.tsx
│ │ │ ├── CarouselItemNative.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── ChangeListener
│ │ │ ├── ChangeListener.md
│ │ │ ├── ChangeListener.spec.ts
│ │ │ ├── ChangeListener.tsx
│ │ │ └── ChangeListenerNative.tsx
│ │ ├── chart-color-schemes.ts
│ │ ├── Charts
│ │ │ ├── AreaChart
│ │ │ │ ├── AreaChart.md
│ │ │ │ ├── AreaChart.spec.ts
│ │ │ │ ├── AreaChart.tsx
│ │ │ │ └── AreaChartNative.tsx
│ │ │ ├── BarChart
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── BarChart.module.scss
│ │ │ │ ├── BarChart.spec.ts
│ │ │ │ ├── BarChart.tsx
│ │ │ │ └── BarChartNative.tsx
│ │ │ ├── DonutChart
│ │ │ │ ├── DonutChart.spec.ts
│ │ │ │ └── DonutChart.tsx
│ │ │ ├── LabelList
│ │ │ │ ├── LabelList.module.scss
│ │ │ │ ├── LabelList.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ ├── ContentSeparatorNative.tsx
│ │ │ └── test-padding.xmlui
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.js
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── index.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── index.ts
│ │ ├── infrastructure
│ │ │ ├── index.html
│ │ │ ├── main.tsx
│ │ │ ├── public
│ │ │ │ ├── mockServiceWorker.js
│ │ │ │ ├── resources
│ │ │ │ │ ├── bell.svg
│ │ │ │ │ ├── box.svg
│ │ │ │ │ ├── doc.svg
│ │ │ │ │ ├── eye.svg
│ │ │ │ │ ├── flower-640x480.jpg
│ │ │ │ │ ├── sun.svg
│ │ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ │ └── txt.svg
│ │ │ │ └── serve.json
│ │ │ └── TestBed.tsx
│ │ └── themed-app-test-helpers.ts
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.json
├── tsdown.config.ts
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/xmlui/src/components/Timer/Timer.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { SKIP_REASON } from "../../testing/component-test-helpers";
import { expect, test } from "../../testing/fixtures";
test.describe("Basic Functionality", () => {
test("component renders with default props", async ({ page, initTestBed }) => {
await initTestBed(`<Timer testId="timer" />`);
const timer = page.getByTestId("timer");
await expect(timer).toHaveAttribute("data-timer-enabled", "true");
await expect(timer).toHaveAttribute("data-timer-interval", "1000");
});
test("component renders with enabled=false", async ({ page, initTestBed }) => {
await initTestBed(`<Timer testId="timer" enabled="false" />`);
const timer = page.getByTestId("timer");
await expect(timer).toHaveAttribute("data-timer-enabled", "false");
await expect(timer).toHaveAttribute("data-timer-running", "false");
});
test("component renders with custom interval", async ({ page, initTestBed }) => {
await initTestBed(`<Timer testId="timer" interval="500" />`);
const timer = page.getByTestId("timer");
await expect(timer).toHaveAttribute("data-timer-interval", "500");
});
test("component renders with once=true", async ({ page, initTestBed }) => {
await initTestBed(`<Timer testId="timer" once="true" />`);
const timer = page.getByTestId("timer");
await expect(timer).toHaveAttribute("data-timer-once", "true");
});
test("component renders with initial delay", async ({ page, initTestBed }) => {
await initTestBed(`<Timer testId="timer" initialDelay="500" />`);
const timer = page.getByTestId("timer");
await expect(timer).toHaveAttribute("data-timer-initial-delay", "500");
});
test("component is not visible (non-visual component)", async ({ page, initTestBed }) => {
await initTestBed(`<Timer testId="timer" />`);
const timer = page.getByTestId("timer");
await expect(timer).not.toBeVisible();
});
});
test.describe.fixme("Event Handling", () => {
test("timer tick event is called", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickCount="{0}">
<Timer interval="800" enabled="true" onTick="tickCount++" />
<Text testId="counter">{tickCount}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
// Wait for a few ticks
await expect(counter).toHaveText("0");
// Wait for the first tick (1000ms + some buffer)
await page.waitForTimeout(1200);
await expect(counter).toHaveText("1");
// Wait for the second tick
await page.waitForTimeout(1000);
await expect(counter).toHaveText("2");
});
test("timer stops when enabled is false", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickCount="{0}" var.timerEnabled="{true}">
<Timer interval="800" enabled="{timerEnabled}" onTick="tickCount++" />
<Button testId="stopButton" onClick="timerEnabled = false">Stop Timer</Button>
<Text testId="counter">{tickCount}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
const stopButton = page.getByTestId("stopButton");
// Wait for the first tick
await page.waitForTimeout(1200);
await expect(counter).toHaveText("1");
// Stop the timer
await stopButton.click();
// Wait and verify it doesn't increment
await page.waitForTimeout(1500);
await expect(counter).toHaveText("1");
});
test.fixme("timer prevents overlapping events", async ({ page, initTestBed }) => {
// This test verifies that a new tick event doesn't fire if the previous one hasn't completed
await initTestBed(`
<Fragment var.tickStr="" var.processingTime="{2000}">
<Timer
interval="800"
enabled="true"
onTick="
tickStr += '+';
delay(processingTime);
"
/>
<Text testId="counter">{tickStr}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
// First tick should start and take 2000ms to complete
// but interval is 1000ms, so second tick should not start until first completes
await page.waitForTimeout(1500); // Wait for first tick to complete
await expect(counter).toHaveText("+");
// Wait a bit more to see if overlapping was prevented
await page.waitForTimeout(1500); // Should be enough for second tick to start
await expect(counter).toContainText("++");
});
});
test.describe.fixme("Initial Delay Functionality", () => {
test("timer waits for initial delay before first tick", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickStr="">
<Timer interval="800" initialDelay="1600" onTick="tickStr += '+'"/>
<Text testId="counter">{tickStr}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
// Should not tick immediately
await expect(counter).toHaveText("");
// Should not tick before initial delay
await page.waitForTimeout(1200);
await expect(counter).toHaveText("");
// Should tick after initial delay
await page.waitForTimeout(800); // Total 2500ms, past the 2000ms initial delay + 1000ms interval
await expect(counter).toHaveText("+");
// Should continue ticking at interval
await page.waitForTimeout(800);
await expect(counter).toHaveText("++");
});
test("initial delay only applies to first execution, not restarts", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickStr="" var.timerEnabled="{true}">
<Timer
interval="400"
initialDelay="800"
enabled="{timerEnabled}"
onTick="tickStr += '+'"
/>
<Button testId="restartButton" onClick="timerEnabled = false; timerEnabled = true">Restart</Button>
<Text testId="counter">{tickStr}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
const restartButton = page.getByTestId("restartButton");
// Wait for initial delay + first tick
await page.waitForTimeout(1300);
await expect(counter).toHaveText("+");
// Restart the timer
await restartButton.click();
// After restart, should tick immediately without initial delay
await page.waitForTimeout(500);
await expect(counter).toContainText("++");
});
test("initial delay works with once timer", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickStr="">
<Timer interval="800" initialDelay="1600" once="true" onTick="tickStr += '+'"/>
<Text testId="counter">{tickStr}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
// Should not tick before initial delay
await page.waitForTimeout(1200);
await expect(counter).toHaveText("");
// Should tick once after initial delay
await page.waitForTimeout(1000);
await expect(counter).toHaveText("+");
// Should not tick again
await page.waitForTimeout(1000);
await expect(counter).toHaveText("+");
});
});
test.describe.fixme("Pause and Resume API", () => {
test("timer can be paused and resumed via API", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickStr="">
<Timer
id="myTimer"
interval="800"
onTick="tickStr += '+'"
/>
<Button testId="pauseButton" onClick="myTimer.pause()">Pause</Button>
<Button testId="resumeButton" onClick="myTimer.resume()">Resume</Button>
<Text testId="counter">{tickStr}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
const pauseButton = page.getByTestId("pauseButton");
const resumeButton = page.getByTestId("resumeButton");
// Wait for first tick
await page.waitForTimeout(1000);
await expect(counter).toHaveText("+");
// Pause the timer
await pauseButton.click();
// Should not tick while paused
await page.waitForTimeout(1000);
await expect(counter).toHaveText("+");
// Resume the timer
await resumeButton.click();
// Should continue ticking after resume
await page.waitForTimeout(1000);
await expect(counter).toContainText("++");
});
test("pause state is reset when timer is disabled", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickStr="" var.timerEnabled="{true}">
<Timer
id="myTimer"
interval="1000"
enabled="{timerEnabled}"
onTick="tickStr += '+'"
/>
<Button testId="pauseButton" onClick="myTimer.pause()">Pause</Button>
<Button testId="disableButton" onClick="timerEnabled = false">Disable</Button>
<Button testId="enableButton" onClick="timerEnabled = true">Enable</Button>
<Text testId="counter">{tickStr}</Text>
<Text testId="paused">{myTimer.isPaused() ? 'Paused' : 'Running'}</Text>
</Fragment>
`);
const pauseButton = page.getByTestId("pauseButton");
const disableButton = page.getByTestId("disableButton");
const enableButton = page.getByTestId("enableButton");
const pausedStatus = page.getByTestId("paused");
// Pause the timer
await pauseButton.click();
await expect(pausedStatus).toHaveText("Paused");
// Disable the timer
await disableButton.click();
// Re-enable the timer - pause state should be reset
await enableButton.click();
await expect(pausedStatus).toHaveText("Running");
});
});
test.describe("Once Functionality", () => {
test.fixme("timer with once=true fires only once",
SKIP_REASON.TEST_NOT_WORKING("Waiting for timeouts are bad practice because they are flaky."),
async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickCount="{0}">
<Timer interval="1000" once="true" onTick="tickCount++" />
<Text testId="counter">{tickCount}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
// Wait for the first tick
await page.waitForTimeout(1200);
await expect(counter).toHaveText("1");
// Wait longer to ensure it doesn't tick again
await page.waitForTimeout(2000);
await expect(counter).toHaveText("1");
});
test.fixme("timer with once=true can be restarted by re-enabling",
SKIP_REASON.TEST_NOT_WORKING("Waiting for timeouts are bad practice because they are flaky."),
async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickCount="{0}" var.timerEnabled="{true}">
<Timer interval="1000" once="true" enabled="{timerEnabled}" onTick="tickCount++" />
<Button testId="restartButton" onClick="timerEnabled = false; timerEnabled = true">Restart</Button>
<Text testId="counter">{tickCount}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
const restartButton = page.getByTestId("restartButton");
// Wait for the first tick
await page.waitForTimeout(1200);
await expect(counter).toHaveText("1");
// Restart the timer
await restartButton.click();
// Wait for another tick
await page.waitForTimeout(1200);
await expect(counter).toHaveText("2");
});
test.fixme("once timer doesn't interfere with regular timer functionality",
SKIP_REASON.TEST_NOT_WORKING("Waiting for timeouts are bad practice because they are flaky."),
async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickStr="">
<Timer interval="1000" once="false" onTick="tickStr += '+'" />
<Text testId="counter">{tickStr}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
// Wait for multiple ticks
await page.waitForTimeout(2000);
await expect(counter).toContainText("+");
});
});
test.describe.fixme("State Management", () => {
test("timer can be dynamically enabled and disabled", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickCount="{0}" var.timerEnabled="{false}">
<Timer interval="800" enabled="{timerEnabled}" onTick="tickCount++" />
<Button testId="startButton" onClick="timerEnabled = true">Start Timer</Button>
<Button testId="stopButton" onClick="timerEnabled = false">Stop Timer</Button>
<Text testId="counter">{tickCount}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
const startButton = page.getByTestId("startButton");
const stopButton = page.getByTestId("stopButton");
// Initially disabled - should not increment
await page.waitForTimeout(1000);
await expect(counter).toHaveText("0");
// Start the timer
await startButton.click();
await page.waitForTimeout(1000);
await expect(counter).toHaveText("1");
// Stop the timer again
await stopButton.click();
const currentCount = await counter.textContent();
await page.waitForTimeout(1000);
await expect(counter).toHaveText(currentCount || "1");
});
test("timer interval can be changed dynamically", async ({ page, initTestBed }) => {
await initTestBed(`
<Fragment var.tickCount="{0}" var.timerInterval="{2000}">
<Timer interval="{timerInterval}" enabled="true" onTick="tickCount++" />
<Button testId="fasterButton" onClick="timerInterval = 500">Faster</Button>
<Text testId="counter">{tickCount}</Text>
</Fragment>
`);
const counter = page.getByTestId("counter");
const fasterButton = page.getByTestId("fasterButton");
// Start with 2000ms interval
await page.waitForTimeout(2500);
await expect(counter).toHaveText("1");
// Change to 500ms interval
await fasterButton.click();
await page.waitForTimeout(1500); // Should get multiple ticks quickly
const finalCount = parseInt(await counter.textContent() || "0");
expect(finalCount).toBeGreaterThan(1);
});
});
```
--------------------------------------------------------------------------------
/xmlui/src/components-core/script-runner/eval-tree-common.ts:
--------------------------------------------------------------------------------
```typescript
import type { LogicalThread, ValueResult } from "../../abstractions/scripting/LogicalThread";
import {
T_CALCULATED_MEMBER_ACCESS_EXPRESSION,
T_IDENTIFIER,
T_MEMBER_ACCESS_EXPRESSION,
T_PREFIX_OP_EXPRESSION,
type ArrowExpression,
type AssignmentExpression,
type BinaryExpression,
type CalculatedMemberAccessExpression,
type Expression,
type Identifier,
type Literal,
type MemberAccessExpression,
type PostfixOpExpression,
type PrefixOpExpression,
type UnaryExpression,
} from "./ScriptingSourceTree";
import type { BlockScope } from "../../abstractions/scripting/BlockScope";
import type { BindingTreeEvaluationContext } from "./BindingTreeEvaluationContext";
// --- Get the cached expression value
export function getExprValue(expr: Expression, thread: LogicalThread): any {
return thread?.valueCache?.get(expr)?.value;
}
// --- Set the cached expression value
export function setExprValue(expr: Expression, value: any, thread: LogicalThread): void {
thread.valueCache ??= new Map();
thread.valueCache.set(expr, { value });
}
// --- Type guard to check for a Promise
export function isPromise(obj: any): obj is Promise<any> {
return obj && typeof obj.then === "function";
}
// --- Evaluates a literal value (sync & async context)
export function evalLiteral(thisStack: any[], expr: Literal, thread: LogicalThread): any {
setExprValue(expr, { value: expr.value }, thread);
thisStack.push(expr.value);
return expr.value;
}
type IdentifierScope = "global" | "app" | "localContext" | "block";
// --- Gets the scope of an identifier
export function getIdentifierScope(
expr: Identifier,
evalContext: BindingTreeEvaluationContext,
thread?: LogicalThread,
): { type?: IdentifierScope; scope: any } {
let type: IdentifierScope | undefined;
let scope: any;
// --- Search for primary value scope
if (expr.isGlobal) {
// --- Use the global scope only
scope = globalThis;
type = "global";
} else {
// --- Iterate trough threads from the current to the parent
let currentThread: LogicalThread | undefined = thread ?? evalContext.mainThread;
while (currentThread && !scope) {
if (currentThread.blocks) {
// --- Search the block-scopes
for (let idx = currentThread.blocks.length - 1; idx >= 0; idx--) {
const blockContext = currentThread.blocks[idx]?.vars;
if (blockContext && expr.name in blockContext) {
scope = blockContext;
type = "block";
break;
}
}
}
// --- We may have already found the ID
if (scope) break;
if (currentThread.closures) {
// --- Search block-scopes of the closure list
for (let idx = currentThread.closures.length - 1; idx >= 0; idx--) {
const blockContext = currentThread.closures[idx]?.vars;
if (blockContext && expr.name in blockContext) {
scope = blockContext;
type = "block";
break;
}
}
}
// --- We may have already found the ID
if (scope) break;
// --- Check the parent thread
currentThread = currentThread.parent;
}
}
// --- If no identifier found so far, check the local context, the app context, and finally, the global context
if (!scope) {
if (evalContext.localContext && expr.name in evalContext.localContext) {
// --- Object in localContext
scope = evalContext.localContext;
type = "localContext";
} else if (evalContext.appContext?.[expr.name] !== undefined) {
// --- Object in appContext
scope = evalContext.appContext;
type = "app";
} else {
// --- Finally, check the global context
scope = globalThis;
type = "global";
}
}
// --- Done
return { type, scope: scope };
}
// --- Evaluates an identifier (sync & async context)
export function evalIdentifier(
thisStack: any[],
expr: Identifier,
evalContext: BindingTreeEvaluationContext,
thread: LogicalThread,
): any {
// --- Check the ID in the cache
let value: any;
// --- The cache does not contain the value.
// --- We need to find the value and store it in the cache.
const idScope = getIdentifierScope(expr, evalContext, thread);
const valueScope = idScope.scope;
let valueIndex: string | number = expr.name;
value = valueScope[valueIndex];
const newValue: ValueResult = {
valueScope,
valueIndex,
value,
};
setExprValue(expr, newValue, thread);
// --- Done.
thisStack.push(value);
return value;
}
// --- Gets the scope of the root ID
export function getRootIdScope(
expr: Expression,
evalContext: BindingTreeEvaluationContext,
thread?: LogicalThread,
): { type: IdentifierScope | undefined; name: string } | null {
switch (expr.type) {
case T_IDENTIFIER:
const idScope = getIdentifierScope(expr, evalContext, thread);
return { type: idScope.type, name: expr.name };
case T_MEMBER_ACCESS_EXPRESSION:
return getRootIdScope(expr.obj, evalContext, thread);
case T_CALCULATED_MEMBER_ACCESS_EXPRESSION:
return getRootIdScope(expr.obj, evalContext, thread);
}
return null;
}
// --- Evaluates a member access expression (sync & async context)
export function evalMemberAccessCore(
thisStack: any[],
expr: MemberAccessExpression,
evalContext: BindingTreeEvaluationContext,
thread: LogicalThread,
): any {
const parentObj = getExprValue(expr.obj, thread)?.value;
const value =
expr.opt || evalContext.options?.defaultToOptionalMemberAccess
? parentObj?.[expr.member]
: parentObj[expr.member];
setExprValue(
expr,
{
valueScope: parentObj,
valueIndex: expr.member,
value,
},
thread,
);
thisStack.push(value);
// --- Done.
return value;
}
// --- Evaluates a calculated member access expression (sync & async context)
export function evalCalculatedMemberAccessCore(
thisStack: any[],
expr: CalculatedMemberAccessExpression,
evalContext: BindingTreeEvaluationContext,
thread: LogicalThread,
): any {
const parentObj = getExprValue(expr.obj, thread)?.value;
const memberObj = getExprValue(expr.member, thread)?.value;
const value = evalContext.options?.defaultToOptionalMemberAccess
? parentObj?.[memberObj]
: parentObj[memberObj];
setExprValue(
expr,
{
valueScope: parentObj,
valueIndex: memberObj,
value,
},
thread,
);
thisStack.push(value);
// --- Done.
return value;
}
// --- Evaluates a unary expression (sync & async context)
export function evalUnaryCore(
expr: UnaryExpression,
thisStack: any[],
evalContext: BindingTreeEvaluationContext,
thread: LogicalThread,
): any {
let value: any;
const operand = getExprValue(expr.expr, thread);
const operValue = operand?.value;
switch (expr.op) {
case "typeof":
value = typeof operValue;
break;
case "delete":
if (operand.valueScope && operand.valueIndex) {
value = delete operand.valueScope[operand.valueIndex];
} else {
value = false;
}
break;
case "+":
value = operValue;
break;
case "-":
value = -operValue;
break;
case "!":
value = !operValue;
break;
case "~":
value = ~operValue;
break;
default:
throw new Error(`Unknown unary operator: ${expr.op}`);
}
setExprValue(expr, { value }, thread);
// --- Done.
thisStack.push(value);
return value;
}
// --- Evaluates a binary operation (sync & async context)
export function evalBinaryCore(
expr: BinaryExpression,
thisStack: any[],
evalContext: BindingTreeEvaluationContext,
thread: LogicalThread,
): any {
let value: any;
const l = getExprValue(expr.left, thread)?.value;
const r = getExprValue(expr.right, thread)?.value;
switch (expr.op) {
case "**":
value = l ** r;
break;
case "*":
value = l * r;
break;
case "/":
value = l / r;
break;
case "%":
value = l % r;
break;
case "+":
value = l + r;
break;
case "-":
value = l - r;
break;
case ">>":
value = l >> r;
break;
case ">>>":
value = l >>> r;
break;
case "<<":
value = l << r;
break;
case "<":
value = l < r;
break;
case "<=":
value = l <= r;
break;
case ">":
value = l > r;
break;
case ">=":
value = l >= r;
break;
case "in":
value = l in r;
break;
case "==":
// eslint-disable-next-line eqeqeq
value = l == r;
break;
case "!=":
// eslint-disable-next-line eqeqeq
value = l != r;
break;
case "===":
value = l === r;
break;
case "!==":
value = l !== r;
break;
case "&":
value = l & r;
break;
case "^":
value = l ^ r;
break;
case "|":
value = l | r;
break;
case "&&":
value = l && r;
break;
case "||":
value = l || r;
break;
case "??":
value = l ?? r;
break;
default:
throw new Error(`Unknown binary operator: ${expr.op}`);
}
setExprValue(expr, { value }, thread);
// --- Done.
thisStack.push(value);
return value;
}
// --- Evaluates an assignment operation (sync & async context)
export function evalAssignmentCore(
thisStack: any[],
expr: AssignmentExpression,
evalContext: BindingTreeEvaluationContext,
thread: LogicalThread,
): any {
const leftValue = getExprValue(expr.leftValue, thread);
const newValue = getExprValue(expr.expr, thread)?.value;
if (
!leftValue.valueScope ||
leftValue.valueIndex === undefined ||
leftValue.valueIndex === null
) {
throw new Error(`Evaluation of ${expr.op} requires a left-hand value.`);
}
const leftScope = leftValue.valueScope;
const leftIndex = leftValue.valueIndex;
if (typeof leftScope !== "object" || leftScope === null) {
// TODO: Specify the error location in the error message
throw new Error(`Unknown left-hand value`);
}
// --- Check for const value
if (expr.leftValue.type === T_IDENTIFIER) {
if (isConstVar(expr.leftValue.name, thread)) {
throw new Error("A const variable cannot be modified");
}
}
if (leftScope === globalThis && !(leftIndex in leftScope)) {
throw new Error(`Left value variable (${leftIndex}) not found in the scope.`);
}
thisStack.pop();
switch (expr.op) {
case "=":
leftScope[leftIndex] = newValue;
break;
case "+=":
leftScope[leftIndex] += newValue;
break;
case "-=":
leftScope[leftIndex] -= newValue;
break;
case "**=":
leftScope[leftIndex] **= newValue;
break;
case "*=":
leftScope[leftIndex] *= newValue;
break;
case "/=":
leftScope[leftIndex] /= newValue;
break;
case "%=":
leftScope[leftIndex] %= newValue;
break;
case "<<=":
leftScope[leftIndex] <<= newValue;
break;
case ">>=":
leftScope[leftIndex] >>= newValue;
break;
case ">>>=":
leftScope[leftIndex] >>>= newValue;
break;
case "&=":
leftScope[leftIndex] &= newValue;
break;
case "^=":
leftScope[leftIndex] ^= newValue;
break;
case "|=":
leftScope[leftIndex] |= newValue;
break;
case "&&=":
leftScope[leftIndex] &&= newValue;
break;
case "||=":
leftScope[leftIndex] ||= newValue;
break;
case "??=":
leftScope[leftIndex] += newValue;
break;
}
const value = leftScope[leftIndex];
setExprValue(expr, { value }, thread);
thisStack.push(value);
return value;
}
// --- Evaluates a prefix/postfix operator (sync & async context)
export function evalPreOrPostCore(
thisStack: any[],
expr: PrefixOpExpression | PostfixOpExpression,
evalContext: BindingTreeEvaluationContext,
thread: LogicalThread,
): any {
const operand = getExprValue(expr.expr, thread);
if (!operand.valueScope || operand.valueIndex === undefined) {
throw new Error(`Evaluation of ${expr.op} requires a left-hand value.`);
}
// --- Check for const value
if (expr.expr.type === T_IDENTIFIER) {
if (isConstVar(expr.expr.name, thread)) {
// --- We cannot modify a const value
throw new Error("A const variable cannot be modified");
}
}
const value =
expr.op === "++"
? expr.type === T_PREFIX_OP_EXPRESSION
? ++operand.valueScope[operand.valueIndex]
: operand.valueScope[operand.valueIndex]++
: expr.type === T_PREFIX_OP_EXPRESSION
? --operand.valueScope[operand.valueIndex]
: operand.valueScope[operand.valueIndex]--;
setExprValue(expr, { value }, thread);
thisStack.push(value);
return value;
}
// --- Evaluates an arrow expression (lazy, sync & async context)
export function evalArrow(
thisStack: any[],
expr: ArrowExpression,
thread: LogicalThread,
): ArrowExpression {
const lazyArrow = {
...expr,
_ARROW_EXPR_: true,
closureContext: obtainClosures(thread),
} as ArrowExpression;
setExprValue(expr, { value: lazyArrow }, thread);
thisStack.push(lazyArrow);
return lazyArrow;
}
export function obtainClosures(thread: LogicalThread): BlockScope[] {
const closures = thread.blocks?.slice(0) ?? [];
return thread.parent ? [...obtainClosures(thread.parent), ...closures] : closures;
}
/**
* Gets the context of the variable
* @param id Identifier to test
* @param thread Thread to use for evaluation
*/
function isConstVar(id: string, thread: LogicalThread): boolean {
// --- Start search the block context
if (thread.blocks) {
for (let idx = thread.blocks.length; idx >= 0; idx--) {
const constContext = thread.blocks[idx]?.constVars;
if (constContext && constContext.has(id)) return true;
}
}
// --- Not in block context
return false;
}
export function evalTemplateLiteralCore(segmentValues: any[]): string {
return segmentValues.map((value) => (typeof value === "string" ? value : `${value}`)).join("");
}
```
--------------------------------------------------------------------------------
/xmlui/src/components/Text/Text.md:
--------------------------------------------------------------------------------
```markdown
%-DESC-START
You can learn more about this component in the [Working with Text](/working-with-text) article.
Also note that variants of the `Text` component are also mapped to HtmlTag components.
See the [variant](#variant) section to check which variant maps to which HtmlTag.
## Custom Variants
In addition to the predefined variants, the `Text` component supports **custom variant names** that can be styled using theme variables. This allows you to create application-specific text styles without modifying the component itself.
When you use a custom variant name (one not in the predefined list), the component automatically applies theme variables following the naming pattern: `{cssProperty}-Text-{variantName}`.
```xmlui-pg display name="Example: custom variants"
<App>
<Theme
textColor-Text-brandTitle="rgb(41, 128, 185)"
fontSize-Text-brandTitle="28px"
fontWeight-Text-brandTitle="bold"
letterSpacing-Text-brandTitle="2px"
>
<Text variant="brandTitle">
Welcome to Our Application
</Text>
</Theme>
</App>
```
In this example, the custom variant `brandTitle` is styled using theme variables. Any CSS text property can be configured, including `textColor`, `fontSize`, `fontWeight`, `fontFamily`, `textDecoration*`, `lineHeight`, `backgroundColor`, `textTransform`, `letterSpacing`, `wordSpacing`, `textShadow`, and more.
%-DESC-END
%-PROP-START maxLines
```xmlui-pg copy display name="Example: maxLines"
<App>
<Text
maxWidth="120px"
backgroundColor="cyan"
color="black"
value="A long text that will likely overflow"
maxLines="2" />
</App>
```
%-PROP-END
%-PROP-START ellipses
```xmlui-pg copy display name="Example: ellipses"
<App>
<VStack width="120px">
<Text
backgroundColor="cyan"
color="black"
maxLines="1"
ellipses="false">
Though this long text does is about to crop!
</Text>
<Text
backgroundColor="cyan"
color="black"
maxLines="1">
Though this long text does is about to crop!
</Text>
</VStack>
</App>
```
%-PROP-END
%-PROP-START preserveLinebreaks
```xmlui-pg copy {7} display name="Example: preserveLinebreaks"
<App>
<HStack>
<Text
width="250px"
backgroundColor="cyan"
color="black"
preserveLinebreaks="true"
value="(preserve) This long text
with several line breaks
does not fit into a viewport with a 200-pixel width." />
<Text
width="250px"
backgroundColor="cyan"
color="black"
preserveLinebreaks="false"
value="(don't preserve) This long text
with several line breaks
does not fit into a viewport with a 200-pixel width." />
</HStack>
</App>
```
> **Note**: Remember to use the `value` property of the `Text`.
> Linebreaks are converted to spaces when nesting the text inside the `Text` component.
%-PROP-END
%-PROP-START value
```xmlui-pg copy display name="Example: value"
<App>
<Text value="An example text" />
<Text>An example text</Text>
</App>
```
%-PROP-END
%-PROP-START variant
```xmlui-pg name="Example: variant"
<App>
<HStack>
<Text width="150px">default:</Text>
<Text>This is an example text</Text>
</HStack>
<HStack>
<Text width="150px">paragraph:</Text>
<Text variant="paragraph">This is an example paragraph</Text>
</HStack>
<HStack>
<Text width="150px">abbr:</Text>
<Text variant="abbr">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">cite:</Text>
<Text variant="cite">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">code:</Text>
<Text variant="code">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">deleted:</Text>
<Text variant="deleted">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">inserted:</Text>
<Text variant="inserted">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">keyboard:</Text>
<Text variant="keyboard">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">marked:</Text>
<Text variant="marked">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">sample:</Text>
<Text variant="sample">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">sup:</Text>
<Text>
This is an example text
<Text variant="sup">(with some additional text)</Text>
</Text>
</HStack>
<HStack>
<Text width="150px">sub:</Text>
<Text>
This is an example text
<Text variant="sub">(with some additional text)</Text>
</Text>
</HStack>
<HStack>
<Text width="150px">var:</Text>
<Text variant="var">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">mono:</Text>
<Text variant="mono">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">strong:</Text>
<Text variant="strong">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">em:</Text>
<Text variant="em">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">title:</Text>
<Text variant="title">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">subtitle:</Text>
<Text variant="subtitle">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">small:</Text>
<Text variant="small">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">caption:</Text>
<Text variant="caption">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">placeholder:</Text>
<Text variant="placeholder">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">subheading:</Text>
<Text variant="subheading">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">tableheading:</Text>
<Text variant="tableheading">
This is an example text
</Text>
</HStack>
<HStack>
<Text width="150px">secondary:</Text>
<Text variant="secondary">
This is an example text
</Text>
</HStack>
</App>
```
**HtmlTag Mappings**
The table below indicates which Text `variant` maps to which HtmlTag component.
| Variant | Component |
| ----------- | --------- |
| `abbr` | abbr |
| `cite` | cite |
| `code` | code |
| `deleted` | del |
| `inserted` | ins |
| `keyboard` | kbd |
| `marked` | mark |
| `sample` | samp |
| `sub` | sub |
| `sup` | sup |
| `var` | var |
| `strong` | strong |
| `em` | em |
| `paragraph` | p |
%-PROP-END
%-PROP-START overflowMode
```xmlui-pg copy display name="Example: overflowMode"
<App>
<VStack gap="16px">
<VStack gap="8px">
<Text variant="strong">overflowMode="none"</Text>
<Text
width="200px"
backgroundColor="lightcoral"
padding="8px"
overflowMode="none"
maxLines="2">
This is a very long text that will be clipped cleanly without
any overflow indicator when it exceeds the specified lines.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">overflowMode="ellipsis" (default)</Text>
<Text
width="200px"
backgroundColor="lightblue"
padding="8px"
overflowMode="ellipsis"
maxLines="1">
This is a very long text that will show ellipsis when it
overflows the container width.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">overflowMode="scroll"</Text>
<Text
width="200px"
backgroundColor="lightgreen"
padding="8px"
overflowMode="scroll">
This is a very long text that will enable horizontal scrolling
when it overflows the container width.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">overflowMode="flow"</Text>
<Text
width="200px"
height="100px"
backgroundColor="lightyellow"
padding="8px"
overflowMode="flow">
This is a very long text that will wrap to multiple lines and show
a vertical scrollbar when the content exceeds the container height.
This mode ignores maxLines and allows unlimited text wrapping with
vertical scrolling when needed.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">overflowMode="flow" (no height constraint)</Text>
<Text
width="200px"
backgroundColor="lightpink"
padding="8px"
overflowMode="flow">
This is a very long text that demonstrates flow mode without a
height constraint. The text will wrap to multiple lines naturally
and the container will grow to accommodate all the content. No
scrollbar will appear since there's no height limitation - the text
flows freely across as many lines as needed.
</Text>
</VStack>
</VStack>
</App>
```
%-PROP-END
%-PROP-START breakMode
```xmlui-pg copy display name="Example: breakMode"
<App>
<VStack gap="16px">
<VStack gap="8px">
<Text variant="strong">breakMode="normal" (default)</Text>
<Text
width="150px"
backgroundColor="lightblue"
padding="8px"
breakMode="normal">
This text uses standardwordbreaking at natural boundaries
like spaces and hyphens.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">breakMode="word"</Text>
<Text
width="150px"
backgroundColor="lightgreen"
padding="8px"
breakMode="word">
This text will breakverylongwordswhenneeded to prevent
overflow while preserving readability.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">breakMode="anywhere"</Text>
<Text
width="150px"
backgroundColor="lightyellow"
padding="8px"
breakMode="anywhere">
Thistext willbreakanywhereif neededtofit thecontainer
eveninthe middleofwords.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">breakMode="keep"</Text>
<Text
width="150px"
backgroundColor="lightcoral"
padding="8px"
breakMode="keep">
This text will keep verylongwords intact and prevent
breaking within words entirely.
</Text>
</VStack>
<VStack gap="8px">
<Text variant="strong">breakMode="hyphenate"</Text>
<Text
width="150px"
backgroundColor="lavender"
padding="8px"
breakMode="hyphenate"
lang="en">
This text uses automatic hyphenation for
supercalifragilisticexpialidocious words.
</Text>
</VStack>
</VStack>
</App>
```
%-PROP-END
%-STYLE-START
### Custom Variant Theme Variables
When using custom variants, you can style them using theme variables with the naming pattern `{propertyName}-Text-{variantName}`. The following CSS properties are supported:
| Theme Variable Name | Description | Example Value |
|---------------------|-------------|---------------|
| `textColor-Text-{variant}` | Text color | `rgb(255, 0, 0)`, `#ff0000`, `red` |
| `fontFamily-Text-{variant}` | Font family | `"Arial, sans-serif"`, `monospace` |
| `fontSize-Text-{variant}` | Font size | `16px`, `1.5rem`, `large` |
| `fontStyle-Text-{variant}` | Font style | `normal`, `italic`, `oblique` |
| `fontWeight-Text-{variant}` | Font weight | `normal`, `bold`, `700` |
| `fontStretch-Text-{variant}` | Font stretch | `normal`, `expanded`, `condensed` |
| `textDecorationLine-Text-{variant}` | Decoration line type | `none`, `underline`, `overline`, `line-through` |
| `textDecorationColor-Text-{variant}` | Decoration color | `rgb(255, 0, 0)`, `currentColor` |
| `textDecorationStyle-Text-{variant}` | Decoration style | `solid`, `dashed`, `dotted`, `wavy` |
| `textDecorationThickness-Text-{variant}` | Decoration thickness | `2px`, `from-font`, `auto` |
| `textUnderlineOffset-Text-{variant}` | Underline offset | `5px`, `0.2em`, `auto` |
| `lineHeight-Text-{variant}` | Line height | `1.5`, `24px`, `normal` |
| `backgroundColor-Text-{variant}` | Background color | `rgb(255, 255, 0)`, `transparent` |
| `textTransform-Text-{variant}` | Text transformation | `none`, `uppercase`, `lowercase`, `capitalize` |
| `letterSpacing-Text-{variant}` | Space between letters | `1px`, `0.1em`, `normal` |
| `wordSpacing-Text-{variant}` | Space between words | `5px`, `0.2em`, `normal` |
| `textShadow-Text-{variant}` | Text shadow | `2px 2px 4px rgba(0,0,0,0.5)` |
| `textIndent-Text-{variant}` | First line indentation | `20px`, `2em`, `0` |
| `textAlign-Text-{variant}` | Horizontal alignment | `left`, `center`, `right`, `justify` |
| `textAlignLast-Text-{variant}` | Last line alignment | `left`, `center`, `right`, `justify` |
| `wordBreak-Text-{variant}` | Word breaking behavior | `normal`, `break-all`, `keep-all` |
| `wordWrap-Text-{variant}` | Word wrapping | `normal`, `break-word` |
| `direction-Text-{variant}` | Text direction | `ltr`, `rtl` |
| `writingMode-Text-{variant}` | Writing mode | `horizontal-tb`, `vertical-rl`, `vertical-lr` |
| `lineBreak-Text-{variant}` | Line breaking rules | `auto`, `normal`, `strict`, `loose` |
```xmlui-pg display name="Example: custom variant styles" /highlight/
<App>
<Theme
textColor-Text-highlight="rgb(255, 193, 7)"
fontWeight-Text-highlight="bold"
backgroundColor-Text-highlight="rgba(0, 0, 0, 0.8)"
padding-Text-highlight="4px 8px"
textShadow-Text-highlight="0 2px 4px rgba(0,0,0,0.5)"
>
<Text variant="highlight">Important Notice</Text>
<Text variant="highlight">This is Important Too</Text>
</Theme>
</App>
```
%-STYLE-END
```
--------------------------------------------------------------------------------
/xmlui/src/components/FileUploadDropZone/FileUploadDropZone.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { test, expect } from "../../testing/fixtures";
// =============================================================================
// BASIC FUNCTIONALITY TESTS
// =============================================================================
test("component renders with basic props", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
await expect(driver.component).toContainText("Upload Area");
});
test("component displays children content", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone>
<Text>Drag files here</Text>
<Button>Or click to browse</Button>
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toContainText("Drag files here");
await expect(driver.component).toContainText("Or click to browse");
});
test("component handles disabled state", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
// TODO: Disabled state handling needs investigation
await initTestBed(`<FileUploadDropZone enabled="false">Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
// Check that the component is visible but the input reflects disabled state
await expect(driver.component).toBeVisible();
await expect(driver.component).toHaveAttribute('data-drop-enabled', 'false');
});
test("component supports custom drop text", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone text="Custom drop message">Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
});
test("component supports allowPaste property", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone allowPaste="false">Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
});
test("component has hidden file input", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.getHiddenInput()).toBeAttached();
await expect(driver.getHiddenInput()).toHaveAttribute('type', 'file');
});
test("component initially hides drop placeholder", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
expect(await driver.isDropPlaceholderVisible()).toBe(false);
});
// =============================================================================
// ACCESSIBILITY TESTS (REQUIRED)
// =============================================================================
test("component has correct accessibility structure", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload files here</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.getHiddenInput()).toHaveAttribute('type', 'file');
await expect(driver.component).toBeVisible();
});
test("component supports keyboard interaction through children", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone>
<Button testId="customButton">Browse Files</Button>
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
const button = driver.component.getByTestId('customButton');
await expect(button).toBeVisible();
await button.focus();
await expect(button).toBeFocused();
});
test("component maintains semantic structure", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone>
<H3>Upload Documents</H3>
<Text>Supported formats: PDF, DOC, JPG</Text>
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
// Check that content is visible within the drop zone
await expect(driver.component).toContainText("Upload Documents");
await expect(driver.component).toContainText("Supported formats");
});
test("component supports screen reader announcements", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone>
<Text role="status" aria-live="polite">Ready for file upload</Text>
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toContainText("Ready for file upload");
});
// =============================================================================
// VISUAL STATE TESTS
// =============================================================================
test("component applies theme variables correctly", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload Area</FileUploadDropZone>`, {
testThemeVars: {
"backgroundColor-FileUploadDropZone": "rgb(255, 0, 0)",
},
});
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toHaveCSS("background-color", "rgb(255, 0, 0)");
});
test("component applies text color theme variables", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload Area</FileUploadDropZone>`, {
testThemeVars: {
"textColor-FileUploadDropZone": "rgb(0, 255, 0)",
},
});
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toHaveCSS("color", "rgb(0, 255, 0)");
});
test("component shows drop placeholder during drag operations", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone text="Drop files here">Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
// Initially hidden
expect(await driver.isDropPlaceholderVisible()).toBe(false);
// Would be visible during drag (tested functionally)
await expect(driver.component).toBeVisible();
});
test("component maintains layout with custom styles", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone style="min-height: 200px; border: 2px dashed #ccc;">
Upload Area
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
// Check that the component is visible and contains content
await expect(driver.component).toBeVisible();
await expect(driver.component).toContainText("Upload Area");
});
// =============================================================================
// EDGE CASE TESTS (CRITICAL)
// =============================================================================
test("component handles null and undefined props gracefully", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
await expect(driver.getHiddenInput()).toBeAttached();
});
test("component handles empty children", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone> </FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
// Component should be attached even with minimal content
await expect(driver.component).toBeAttached();
await expect(driver.getHiddenInput()).toBeAttached();
});
test("component handles special characters in text prop", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone text="Drop files here 📁 & upload 🚀">Upload</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
});
test("component handles boolean string props correctly", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone enabled="true" allowPaste="false">Upload</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
expect(await driver.isEnabled()).toBe(true);
});
test("component handles complex nested children", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone>
<VStack>
<Icon name="upload"/>
<Heading>Upload Files</Heading>
<Text>Drag and drop or click to browse</Text>
<HStack>
<Button>Browse</Button>
<Button variant="outline">Cancel</Button>
</HStack>
</VStack>
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
expect(await driver.hasChildren()).toBe(true);
});
// =============================================================================
// PERFORMANCE TESTS
// =============================================================================
test("component handles multiple rapid drag events efficiently", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone>Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
// Multiple rapid drag events should not cause issues
await driver.triggerDragEnter();
await driver.triggerDragLeave();
await driver.triggerDragEnter();
await driver.triggerDragLeave();
await expect(driver.component).toBeVisible();
});
test("component maintains performance with large child content", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
const largeContent = Array.from({ length: 10 }, (_, i) => `<Text>Content item ${i + 1}</Text>`).join('');
await initTestBed(`
<FileUploadDropZone>
${largeContent}
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
});
// =============================================================================
// INTEGRATION TESTS
// =============================================================================
test("component works correctly in different layout contexts", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<VStack>
<FileUploadDropZone>
<Text>Layout Test</Text>
</FileUploadDropZone>
</VStack>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
});
test("component works in form context", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<Form>
<FileUploadDropZone>
<FormItem label="Document Upload">
<Button>Choose Files</Button>
</FormItem>
</FileUploadDropZone>
</Form>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
});
test("component upload event handlers work correctly", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
const { testStateDriver } = await initTestBed(`
<FileUploadDropZone onUpload="files => testState = files.length">
Upload Area
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
await driver.triggerDrop(['file1.txt', 'file2.txt']);
await expect.poll(testStateDriver.testState).toEqual(2);
});
test("component supports programmatic file dialog opening", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<VStack>
<FileUploadDropZone id="dropzone">Upload Area</FileUploadDropZone>
<Button onClick="dropzone.open()">Open Dialog</Button>
</VStack>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
});
test("component maintains state with dynamic content", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone text="Dynamic drop text">
<Text>Dynamic content area</Text>
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component).toBeVisible();
await expect(driver.component).toContainText("Dynamic content area");
});
test("component works with nested interactive elements", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`
<FileUploadDropZone>
<VStack>
<Button testId="button">Upload from Computer</Button>
<Text>Upload Help</Text>
<TextBox testId="textbox" placeholder="File description"/>
</VStack>
</FileUploadDropZone>
`);
const driver = await createFileUploadDropZoneDriver();
await expect(driver.component.getByTestId('button')).toBeVisible();
await expect(driver.component).toContainText("Upload Help");
await expect(driver.component.getByTestId('textbox')).toBeVisible();
});
test("component handles paste operations when enabled", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone allowPaste="true">Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
// Component should be ready for paste operations
await expect(driver.component).toBeVisible();
await expect(driver.getHiddenInput()).toBeAttached();
});
test("component ignores paste operations when disabled", async ({ initTestBed, createFileUploadDropZoneDriver }) => {
await initTestBed(`<FileUploadDropZone allowPaste="false">Upload Area</FileUploadDropZone>`);
const driver = await createFileUploadDropZoneDriver();
// Component should be visible but paste functionality controlled by prop
await expect(driver.component).toBeVisible();
});
```
--------------------------------------------------------------------------------
/xmlui/src/components/DropdownMenu/DropdownMenu.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { expect, test } from "../../testing/fixtures";
// =============================================================================
// BASIC FUNCTIONALITY TESTS
// =============================================================================
test("renders with basic props", async ({ initTestBed, createDropdownMenuDriver }) => {
await initTestBed(`<DropdownMenu label="Menu" />`);
const driver = await createDropdownMenuDriver();
await expect(driver.component).toBeVisible();
});
test("renders with menu items", async ({ initTestBed, createDropdownMenuDriver, page }) => {
await initTestBed(`
<DropdownMenu label="Menu">
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
await expect(driver.component).toBeVisible();
await driver.open();
await expect(page.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Item 2" })).toBeVisible();
});
test("opens and closes menu correctly", async ({ initTestBed, createDropdownMenuDriver, page }) => {
await initTestBed(`
<DropdownMenu label="Menu">
<MenuItem>Item 1</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
await expect(page.getByRole("menuitem")).not.toBeVisible();
await driver.open();
await expect(page.getByRole("menuitem")).toBeVisible();
await driver.close();
await expect(page.getByRole("menuitem")).not.toBeVisible();
});
test("handles menu item clicks", async ({ initTestBed, createDropdownMenuDriver, page }) => {
const { testStateDriver } = await initTestBed(`
<DropdownMenu label="Menu">
<MenuItem onClick="testState = 'item1-clicked'">Item 1</MenuItem>
<MenuItem onClick="testState = 'item2-clicked'">Item 2</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
await driver.open();
await driver.clickMenuItem("Item 1");
await expect.poll(testStateDriver.testState).toEqual("item1-clicked");
// Menu should close after click
await expect(page.getByRole("menuitem", { name: "Item 1" })).not.toBeVisible();
await expect(page.getByRole("menuitem", { name: "Item 2" })).not.toBeVisible();
});
test("alignment='start' aligns popup menu left", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
await initTestBed(`
<HStack width="50%">
<DropdownMenu alignment="start" label="Menu">
<MenuItem >Item 1</MenuItem>
<MenuItem >Item 2</MenuItem>
</DropdownMenu>
</HStack>
`);
const driver = await createDropdownMenuDriver();
await driver.open();
const { x: triggerX } = await driver.component.boundingBox();
const { x: menuX } = await page.getByRole("menu").boundingBox();
expect(menuX).toBeCloseTo(triggerX);
});
test("alignment='end' aligns popup menu right", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
await initTestBed(`
<HStack width="50%" reverse="true">
<DropdownMenu alignment="end" label="Menu">
<MenuItem >Item 1</MenuItem>
<MenuItem >Item 2</MenuItem>
</DropdownMenu>
</HStack>
`);
const driver = await createDropdownMenuDriver();
await driver.open();
const { width: triggerWidth, x: triggerX } = await driver.component.boundingBox();
const { width: menuWidth, x: menuX } = await page.getByRole("menu").boundingBox();
const menuEndX = menuX + menuWidth;
const triggerEndX = triggerX + triggerWidth;
expect(menuEndX).toBeCloseTo(triggerEndX);
});
test("supports submenu functionality", async ({ initTestBed, createDropdownMenuDriver, page }) => {
await initTestBed(`
<DropdownMenu label="Menu">
<MenuItem>Regular Item</MenuItem>
<SubMenuItem label="Submenu">
<MenuItem>Submenu Item 1</MenuItem>
<MenuItem>Submenu Item 2</MenuItem>
</SubMenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
// Open main menu
await driver.open();
await expect(page.getByText("Regular Item")).toBeVisible();
await expect(page.getByText("Submenu")).toBeVisible();
// Open submenu using driver method
await driver.openSubMenu("Submenu");
await expect(page.getByRole("menuitem", { name: "Submenu Item 1" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Submenu Item 2" })).toBeVisible();
});
test("supports menu separators", async ({ initTestBed, createDropdownMenuDriver, page }) => {
await initTestBed(`
<DropdownMenu label="Menu">
<MenuItem>Item 1</MenuItem>
<MenuSeparator />
<MenuItem>Item 2</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
await driver.open();
await expect(page.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Item 2" })).toBeVisible();
// Check that separator is rendered using driver method
const separators = driver.getMenuSeparators();
await expect(separators).toHaveCount(1);
});
// =============================================================================
// ACCESSIBILITY TESTS
// =============================================================================
test("has correct accessibility attributes", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
await initTestBed(`
<DropdownMenu label="Accessible Menu">
<MenuItem>Item 1</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
let btn = page.getByRole("button", { name: "Accessible Menu" });
await expect(btn).toHaveAttribute("aria-haspopup", "menu");
await expect(btn).toHaveAttribute("aria-expanded", "false");
await driver.open();
await page.getByRole("button", { includeHidden: true, expanded: true }).waitFor();
await page.getByRole("menu").waitFor();
});
test("is keyboard accessible", async ({ initTestBed, createDropdownMenuDriver, page }) => {
const { testStateDriver } = await initTestBed(`
<DropdownMenu label="Keyboard Menu">
<MenuItem onClick="testState = 'keyboard-activated'">Item 1</MenuItem>
</DropdownMenu>
`);
const btn = page.getByRole("button");
await page.keyboard.press("Tab");
await expect(btn).toBeFocused();
// Open with Enter
await page.keyboard.press("Enter");
await expect(page.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
// Navigate and select with keyboard
await page.keyboard.press("ArrowDown");
await page.keyboard.press("Enter");
await expect.poll(testStateDriver.testState).toEqual("keyboard-activated");
});
test("disabled DropdownMenu can't be focused", async ({ initTestBed, page }) => {
await initTestBed(`<DropdownMenu label="Disabled Menu" enabled="false" />`);
const btn = page.getByRole("button");
await expect(btn).toBeDisabled();
// Should not be focusable when disabled
await btn.focus();
await expect(btn).not.toBeFocused();
});
// =============================================================================
// VISUAL STATE TESTS
// =============================================================================
test("applies theme variables correctly", async ({ initTestBed, createDropdownMenuDriver }) => {
await initTestBed(
`<DropdownMenu label="Themed Menu">
<MenuItem>Item 1</MenuItem>
</DropdownMenu>`,
{
testThemeVars: {
"backgroundColor-DropdownMenu": "rgb(255, 0, 0)",
"minWidth-DropdownMenu": "200px",
},
},
);
const driver = await createDropdownMenuDriver();
// Open menu to see styled content
await driver.open();
// Check theme variables are applied to menu content
const menuContent = driver.getMenuContent();
await expect(menuContent).toBeVisible();
await expect(menuContent).toHaveCSS("background-color", "rgb(255, 0, 0)");
await expect(menuContent).toHaveCSS("min-width", "200px");
});
// =============================================================================
// EDGE CASE TESTS
// =============================================================================
test("handles null label gracefully", async ({ initTestBed, page }) => {
await initTestBed(`<DropdownMenu label="{null}" />`);
await expect(page.getByRole("button")).toBeVisible();
});
test("handles special characters in labels", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
await initTestBed(`
<DropdownMenu label="Menu with émojis 🚀">
<MenuItem>Item with ñ and ü</MenuItem>
<MenuItem>Item with & < > quotes</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
await expect(driver.component).toBeVisible();
await expect(driver.component).toContainText("Menu with émojis 🚀");
await driver.open();
await expect(page.getByText("Item with ñ and ü")).toBeVisible();
await expect(page.getByText("Item with & < > quotes")).toBeVisible();
});
test("doesn't show empty menu with no MenuItem", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
await initTestBed(`<DropdownMenu label="Empty Menu" />`);
const driver = await createDropdownMenuDriver();
await driver.open();
await expect(page.getByRole("menu")).not.toBeVisible();
});
test("handles disabled menu items", async ({ initTestBed, createDropdownMenuDriver, page }) => {
const { testStateDriver } = await initTestBed(`
<DropdownMenu label="Menu">
<MenuItem enabled="true" onClick="testState = 'enabled-clicked'">Enabled Item</MenuItem>
<MenuItem enabled="false" onClick="testState = 'disabled-clicked'">Disabled Item</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
await driver.open();
// Enabled item should work
await driver.clickMenuItem("Enabled Item");
await expect.poll(testStateDriver.testState).toEqual("enabled-clicked");
// Reopen menu
await driver.open();
await page.getByText("Disabled Item").click();
await expect.poll(testStateDriver.testState).not.toEqual("disabled-clicked");
});
// =============================================================================
// INTEGRATION TESTS
// =============================================================================
test("supports custom trigger template", async ({ initTestBed, page }) => {
await initTestBed(`
<DropdownMenu>
<property name="triggerTemplate">
<Button variant="solid" themeColor="secondary">Custom Trigger</Button>
</property>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</DropdownMenu>
`);
const btn = page.getByRole("button", { name: "Custom Trigger" });
await expect(btn).toBeVisible();
await btn.click();
await expect(page.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Item 2" })).toBeVisible();
});
test("handles onWillOpen event correctly", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
const { testStateDriver } = await initTestBed(`
<DropdownMenu label="Event Menu" onWillOpen="testState = 'willOpen-fired'">
<MenuItem>Item 1</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
// Click to trigger onWillOpen event
await driver.open();
// Event should have fired
await expect.poll(testStateDriver.testState).toEqual("willOpen-fired");
// Menu should be open
await expect(page.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
});
test("prevents opening when onWillOpen returns false", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
await initTestBed(`
<DropdownMenu label="Prevented Menu" onWillOpen="return false">
<MenuItem>Item 1</MenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
// Click should not open menu due to onWillOpen returning false
await driver.open();
await expect(page.getByText("Item 1")).not.toBeVisible();
});
test("API methods work correctly", async ({ initTestBed, createDropdownMenuDriver, page }) => {
await initTestBed(`
<HStack>
<DropdownMenu id="apiMenu" label="API Menu">
<MenuItem>Item 1</MenuItem>
<MenuItem><Button testId="closeBtn" onGotFocus="apiMenu.close()" label="Close Menu" /></MenuItem>
</DropdownMenu>
<Button testId="openBtn" onClick="apiMenu.open()">Open Menu</Button>
</HStack>
`);
const menuItem = page.getByRole("menuitem", { name: "Item 1" });
await expect(menuItem).not.toBeVisible();
await page.getByTestId("openBtn").click();
await expect(menuItem).toBeVisible();
await page.getByTestId("closeBtn").focus();
await expect(menuItem).not.toBeVisible();
});
test("works with nested menu structures", async ({
initTestBed,
createDropdownMenuDriver,
page,
}) => {
await initTestBed(`
<DropdownMenu label="Main Menu">
<MenuItem>Top Level Item</MenuItem>
<SubMenuItem label="Category 1">
<MenuItem>Category 1 Item 1</MenuItem>
<SubMenuItem label="Subcategory">
<MenuItem>Deep Item 1</MenuItem>
<MenuItem>Deep Item 2</MenuItem>
</SubMenuItem>
</SubMenuItem>
<MenuSeparator />
<SubMenuItem label="Category 2">
<MenuItem>Category 2 Item 1</MenuItem>
<MenuItem>Category 2 Item 2</MenuItem>
</SubMenuItem>
</DropdownMenu>
`);
const driver = await createDropdownMenuDriver();
// Open main menu
await driver.open();
await expect(page.getByText("Top Level Item")).toBeVisible();
await expect(page.getByText("Category 1")).toBeVisible();
// Navigate to submenu using driver method
await expect(page.getByText("Category 1 Item 1")).not.toBeVisible();
await driver.openSubMenu("Category 1");
await expect(page.getByText("Category 1 Item 1")).toBeVisible();
await expect(page.getByText("Subcategory")).toBeVisible();
// Navigate to nested submenu
await expect(page.getByText("Deep Item 1")).not.toBeVisible();
await driver.openSubMenu("Subcategory");
await expect(page.getByText("Deep Item 1")).toBeVisible();
await expect(page.getByText("Deep Item 2")).toBeVisible();
});
```
--------------------------------------------------------------------------------
/docs/public/pages/forms.md:
--------------------------------------------------------------------------------
```markdown
# Forms
XMLUI enables to create forms without the hassle of managing, editing, validating, and saving the information you provide in the UI.
This example demonstrates the core elements: [Form](/components/Form) and [FormItem](/components/FormItem).
```xmlui-pg display
<App>
<Form data="{{ name: 'Joe', age: 43 }}">
<FlowLayout>
<H3>Customer information</H3>
<FormItem bindTo="name" label="Customer name" />
<FormItem bindTo="age" label="Age" type="integer" zeroOrPositive="true" />
</FlowLayout>
</Form>
</App>
```
- `Form` encapsulates the management of form UI elements and data handling
- The `data` property holds the form's data
- `FormItem` manages a piece of the data
- `bindTo` specifies the property name within the `data` to bind the corresponding field
- `type` determines the kind of input field needed for a given piece of data (number field, text area field, radio buttons, etc.)
- other properties support styling or validation
## Form Layouts
You can use any of XMLUI's layout mechanisms with a `Form`. Here is a single-column format using `FlowLayout`.
```xmlui-pg display
<App>
<Form data="{
{
firstname: 'Jake',
lastname: 'Hard',
jobTitle: 'janitor',
experience: 'broom'
}
}">
<FlowLayout>
<FormItem label="Firstname" bindTo="firstname" />
<FormItem label="Lastname" bindTo="lastname" />
<FormItem label="Job Title" bindTo="jobTitle" />
<FormItem label="Experience" bindTo="experience" />
</FlowLayout>
</Form>
</App>
```
Set each item's width to `50%` to create a two-column layout.
```xmlui-pg display
<App>
<Form
data="{{
firstname: 'Jake',
lastname: 'Hard',
jobTitle: 'janitor',
experience: 'broom'
}}">
<FlowLayout>
<FormItem label="Firstname" bindTo="firstname" width="50%" />
<FormItem label="Lastname" bindTo="lastname" width="50%" />
<FormItem label="Job Title" bindTo="jobTitle" width="50%" />
<FormItem label="Experience" bindTo="experience" width="50%" />
</FlowLayout>
</Form>
</App>
```
Use star sizing to allocate widths flexibly. Here `Firstname` and `Lastname` equally share the space remaining after the 100-px-wide `Title`.
```xmlui-pg display
<App>
<Form
data="{{
title: 'Mr.',
firstname: 'Jake',
lastname: 'Hard',
jobTitle: 'janitor',
experience: 'broom'
}}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FlowLayout>
<HStack>
<FormItem label="Title" bindTo="title" width="100px" />
<FormItem label="Firstname" bindTo="firstname" width="*" />
<FormItem label="Lastname" bindTo="lastname" width="*" />
</HStack>
<FormItem label="Job Title" bindTo="jobTitle" width="50%" />
<FormItem label="Experience" bindTo="experience" width="50%" />
</FlowLayout>
</Form>
</App>
```
## FormItem
`FormItem` is an intermediary layer between `Form` and an input component; it manages the data represented by that component. The [`type`](/components/FormItem#type-default-text) property of a `FormItem` specifies what input component to render.
### Checkbox
```xmlui-pg display
<App>
<Form data="{{ option1: true, option2: false, option3: true }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="checkbox" bindTo="option1" label="Option #1" labelPosition="end" />
<FormItem type="checkbox" bindTo="option2" label="Option #2" labelPosition="end" />
<FormItem type="checkbox" bindTo="option3" label="Option #3" labelPosition="end" />
</Form>
</App>
```
[Checkbox component doc](/components/Checkbox)
### DatePicker
```xmlui-pg display
<App>
<Form
data="{{ birthDate: '2021-04-08' }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="datePicker" bindTo="birthDate" label="Birthdate" />
</Form>
</App>
```
[DatePicker component doc](/components/DatePicker)
### File
Use `file` to select one or multiple files.
```xmlui-pg display
<App>
<Form
data="{{ articles: null }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="file" bindTo="articles" label="Articles file" />
</Form>
</App>
```
[File component doc](/components/FileInput)
### Integer
```xmlui-pg display
<App>
<Form
data="{{ age: 30 }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="integer" bindTo="age" label="Age" />
</Form>
</App>
```
[NumberBox component doc](/components/NumberBox)
### Number
```xmlui-pg display
<App>
<Form
data="{{ distance: 192.5 }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="number" bindTo="distance" label="Distance in miles" />
</Form>
</App>
```
[NumberBox component doc](/components/NumberBox)
### RadioGroup
```xmlui-pg display
<App>
<Form
data="{{ title: 'Mr.' }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="radioGroup" bindTo="title" label="Title">
<Option label="Mr." value="Mr." />
<Option label="Mrs." value="Mrs." />
<Option label="Ms." value="Ms." />
<Option label="Dr." value="Dr." />
</FormItem>
</Form>
</App>
```
[RadioGroup component doc](/components/RadioGroup)
### Select
```xmlui-pg display
<App>
<Form
data="{{ size: 'xs' }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="select" bindTo="size" label="Box size">
<Option label="Extra small" value="xs" />
<Option label="Small" value="sm" />
<Option label="Medium" value="md" />
<Option label="Large" value="lg" />
</FormItem>
</Form>
</App>
```
[Select component doc](/components/Select)
### Switch
```xmlui-pg display
<App>
<Form
data="{{ showBorder: true, showText: false, hideShadow: true }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="switch" bindTo="showBorder" label="Show border" labelPosition="right" />
<FormItem type="switch" bindTo="showText" label="Show text" labelPosition="right" />
<FormItem type="switch" bindTo="hideShadow" label="Hide shadow" labelPosition="right" />
</Form>
</App>
```
[Switch component doc](/components/Switch)
### TextBox
```xmlui-pg display
<App>
<Form
data="{{ name: 'Joe' }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="text" bindTo="name" label="Name" />
</Form>
</App>
```
[TextBox component doc](/components/TextBox)
### TextArea
```xmlui-pg display
<App>
<Form
data="{{ description: 'This is a description' }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem type="textarea" bindTo="description" label="Description" />
</Form>
</App>
```
[TextArea component doc](/components/TextArea)
### Custom
You can create a custom input component that uses the XMLUI forms infrastructure.
```xmlui-pg display
<App>
<Form
data="{{ userAvailable: false }}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem bindTo="userAvailable">
<HStack>
<Button
label="Toggle"
backgroundColor="{$value === false ? 'red' : 'green'}"
onClick="$setValue(!$value)"
/>
</HStack>
</FormItem>
</Form>
</App>
```
`$value` represents the current value of the component. `$setValue` changes the value.
## Provide data
You can define a `Form`s data structure and initial values directly.
```xmlui copy
<Form data="{{ name: 'Joe', age: 43 }}" />
```
Or via an API endpoint.
```xmlui
<Form data="/path/to/resource" />
```
Use the `bindTo` property to access fields in the structure.
```xmlui
<Form data="{{ name: 'Joe' }}">
<FormItem bindTo="name" />
</Form>
```
## Refer to data
The `$data` variable holds all the form's data. You can use values in `$data` to control `FormItem` properties. Here the `Switch`s value sets the `enabled` property of a `FormItem`.
```xmlui-pg display
<App>
<Form data="{{ isEnabled: true, name: 'Joe' }}">
<FormItem label="Enable name" bindTo="isEnabled" type="switch" />
<FormItem enabled="{$data.isEnabled}" label="Name" bindTo="name" />
</Form>
</App>
```
Other components in the form can reference the form's data too. Here the `Text` updates reactively when input values change.
```xmlui-pg display
<App>
<Form data="{{ firstname: 'John', lastname: 'Doe' }}">
<FormItem label="Firstname" bindTo="firstname" />
<FormItem label="Lastname" bindTo="lastname" />
<Text>Full name: {$data.firstname} {$data.lastname}</Text>
</Form>
</App>
```
You can drill into `$data` to reference nested fields.
```xmlui-pg display {9}
<App>
<Form
data="{{
name: 'John smith',
address: { street: '96th Ave N', city: 'Seattle', zip: '98005' }
}}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem bindTo="name" label="Name" />
<FormItem bindTo="address.street" label="Street" />
</Form>
</App>
```
## Validate data
The `Form` handles client-side validation, reporting issues interactively. Server-side validation happens when the form data is sent to the server. The `Form` handles the server's response and displays it in a summary or below input fields.
These are the `FormItem` validation properties.
### `minLength`
```xmlui-pg display
<App>
<Form data="{{ name: 'Billy Bob' }}">
<FormItem bindTo="name" minLength="10" label="minLength" />
</Form>
</App>
```
Try submitting with fewer than 10 characters.
### `maxLength`
```xmlui-pg display
<App>
<Form data="{{ name: 'Billy Bob' }}">
<FormItem bindTo="name" maxLength="11" label="maxLength" />
</Form>
</App>
```
Try entering more than 11 characters.
### `minValue`
```xmlui-pg display
<App>
<Form data="{{ age: 30 }}">
<FormItem bindTo="age" type="number" minValue="32" label="minValue" />
</Form>
</App>
```
Try entering a number smaller than 32.
### `maxValue`
```xmlui-pg display
<App>
<Form data="{{ age: 30 }}" >
<FormItem bindTo="age" type="number" maxValue="29" label="maxValue" />
</Form>
</App>
```
Try entering a number larger than 32.
### `pattern`
Evaluate predefined regex patterns: "email", "url", or "phone".
```xmlui-pg
<App>
<Form data="{{
mobile: '+13456123456',
website: 'http://www.blogsite.com',
email: '[email protected]'
}}">
<FormItem bindTo="mobile" pattern="phone" label="mobilePattern" />
<FormItem bindTo="website" pattern="url" label="websitePattern" />
<FormItem bindTo="email" pattern="email" label="emailPattern" />
</Form>
</App>
```
See the [pattern property](/components/FormItem#pattern) of `FormItem`.
### `regex`
Evaluate a custom regex pattern.
```xmlui-pg display
<App>
<Form data="{{ password: 'hello' }}">
<!-- Only all uppercase letters are accepted -->
<FormItem bindTo="password" regex="/^[A-Z]+$/" label="regex" />
</Form>
</App>
```
### Compound validation
You can use multiple validations.
```xmlui-pg display
<App>
<Form data="{{ site: 'http://www.example.com' }}">
<FormItem bindTo="site" minLength="10" maxLength="30"
pattern="url" label="Multiple Validations" />
</Form>
</App>
```
### Validation-specific severity
By default, all validations have a severity level of **"error"**. You can set whether a validation should have a level of **"warning"** or **"error"**.
```xmlui-pg display
<App>
<Form data="{{ mobile: '+13456123456', website: 'http://www.blogsite.com' }}" >
<FormItem
bindTo="mobile"
pattern="phone"
patternInvalidSeverity="warning"
label="mobilePattern" />
<FormItem
bindTo="website"
pattern="url"
patternInvalidSeverity="error"
label="websitePattern" />
</Form>
</App>
```
### Validation-specific messages
Predefined validations have built-in messages that you can change.
```xmlui-pg display
<App>
<Form data="{{ age: 20 }}" >
<FormItem
bindTo="age"
type="number"
minValue="21"
rangeInvalidMessage="The given age is too low!"
label="Invalid Message" />
</Form>
</App>
```
## Server-side validation
The `Form` component can receive and display a server-side validation response. Field related issues are shown just like client-side validation errors, removed when a field is edited. Non-field related issues are displayed in a validation summary view.
<br/>
<Image alt="Server-side Validation" src="/resources/images/create-apps/using-forms.png" />
## Submit data
By default the `Form` component provides a submit button to save the modified data.
```xmlui-pg display
<App>
<Form onSubmit="toast('Saved!')" />
</App>
```
The `onSubmit` accepts either a block of code or function. When you use a function it receives data in a parameter; in this example it's called `toSave` but you can use any name. The function can be defined inline, in a code-behind file, or in `index.html` attached to the global `window` variable. See the [Code](/code) chapter for details.
```xmlui-pg display
<App>
<Form
data="{{ name: 'Joe', age: 43 }}"
onSubmit="(d) => toast(JSON.stringify(d))"
>
<FormItem label="name" bindTo="name" />
<FormItem label="age" bindTo="age" />
</Form>
</App>
```
To submit via an `APICall`, use the `event` helper tag to bridge between the form and the API. The `Form`s `data` attribute maps to the `APICall`'s `$param` [context variable](/context-variables). A `Toast` popup reports success or error.
```xmlui
<App>
<Form data="{{ name: 'Joe', age: 43 }}">
<event name="submit">
<APICall
url="/api/contacts"
method="POST"
body="{$param}" />
</event>
<FormItem bindTo="name" label="name" />
<FormItem bindTo="age" label="age" />
</Form>
</App>
```
## Form in ModalDialog
[ModalDialog](/components/ModalDialog) supports `Form` as a first-class citizen component. When a `Form` nests directly in a `ModalDialog`, the dialog's button row is replaced with the form's own button row. When form submission is successful, the dialog closes.
```