This is page 48 of 143. 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
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog-optimized.yml
│ ├── deploy-blog-swa.yml
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs-swa.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── an-advanced-codefence.gif
│ │ │ │ ├── an-advanced-codefence.mp4
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── codefence-runner.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ ├── lorem-ipsum.png
│ │ │ │ ├── playground-checkbox-source.png
│ │ │ │ ├── playground.png
│ │ │ │ ├── use-xmlui-mcp-to-find-a-howto.png
│ │ │ │ └── xmlui-demo-gallery.png
│ │ │ ├── introducing-xmlui.md
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ ├── xmlui-playground.md
│ │ │ └── xmlui-powered-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ ├── staticwebapp.config.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ └── PageNotFound.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── blog-theme.ts
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── FancyButton.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── control-cache-invalidation.md
│ │ │ │ ├── debounce-user-input-for-api-calls.md
│ │ │ │ ├── debounce-with-changelistener.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── github.svg
│ │ │ ├── images
│ │ │ │ ├── apiaction-tutorial
│ │ │ │ │ ├── add-success.png
│ │ │ │ │ ├── apiaction-param.png
│ │ │ │ │ ├── change-completed.png
│ │ │ │ │ ├── change-in-progress.png
│ │ │ │ │ ├── confirm-delete.png
│ │ │ │ │ ├── data-error.png
│ │ │ │ │ ├── data-progress.png
│ │ │ │ │ ├── data-success.png
│ │ │ │ │ ├── display-1.png
│ │ │ │ │ ├── item-deleted.png
│ │ │ │ │ ├── item-updated.png
│ │ │ │ │ ├── missing-api-key.png
│ │ │ │ │ ├── new-item-added.png
│ │ │ │ │ └── test-message.png
│ │ │ │ ├── chat-api
│ │ │ │ │ └── domain-model.svg
│ │ │ │ ├── components
│ │ │ │ │ ├── image
│ │ │ │ │ │ └── breakfast.jpg
│ │ │ │ │ ├── markdown
│ │ │ │ │ │ └── colors.png
│ │ │ │ │ └── modal
│ │ │ │ │ ├── deep_link_dialog_1.jpg
│ │ │ │ │ └── deep_link_dialog_2.jpg
│ │ │ │ ├── create-apps
│ │ │ │ │ ├── collapsed-vertical.png
│ │ │ │ │ ├── using-forms-warning-dialog.png
│ │ │ │ │ └── using-forms.png
│ │ │ │ ├── datasource-tutorial
│ │ │ │ │ ├── data-with-header.png
│ │ │ │ │ ├── filtered-data.png
│ │ │ │ │ ├── filtered-items.png
│ │ │ │ │ ├── initial-page-items.png
│ │ │ │ │ ├── list-items.png
│ │ │ │ │ ├── next-page-items.png
│ │ │ │ │ ├── no-data.png
│ │ │ │ │ ├── pagination-1.jpg
│ │ │ │ │ ├── pagination-1.png
│ │ │ │ │ ├── polling-1.png
│ │ │ │ │ ├── refetch-data.png
│ │ │ │ │ ├── slow-loading.png
│ │ │ │ │ ├── test-message.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── unconventional-data.png
│ │ │ │ │ └── unfiltered-items.png
│ │ │ │ ├── flower.jpg
│ │ │ │ ├── get-started
│ │ │ │ │ ├── add-new-contact.png
│ │ │ │ │ ├── app-modified.png
│ │ │ │ │ ├── app-start.png
│ │ │ │ │ ├── app-with-boxes.png
│ │ │ │ │ ├── app-with-toast.png
│ │ │ │ │ ├── boilerplate-structure.png
│ │ │ │ │ ├── cl-initial.png
│ │ │ │ │ ├── cl-start.png
│ │ │ │ │ ├── contact-counts.png
│ │ │ │ │ ├── contact-dialog-title.png
│ │ │ │ │ ├── contact-dialog.png
│ │ │ │ │ ├── contact-menus.png
│ │ │ │ │ ├── contact-predicates.png
│ │ │ │ │ ├── context-menu.png
│ │ │ │ │ ├── dashboard-numbers.png
│ │ │ │ │ ├── default-contact-list.png
│ │ │ │ │ ├── delete-contact.png
│ │ │ │ │ ├── delete-task.png
│ │ │ │ │ ├── detailed-template.png
│ │ │ │ │ ├── edit-contact-details.png
│ │ │ │ │ ├── edited-contact-saved.png
│ │ │ │ │ ├── empty-sections.png
│ │ │ │ │ ├── filter-completed.png
│ │ │ │ │ ├── fullwidth-desktop.png
│ │ │ │ │ ├── fullwidth-mobile.png
│ │ │ │ │ ├── initial-table.png
│ │ │ │ │ ├── items-and-badges.png
│ │ │ │ │ ├── loading-message.png
│ │ │ │ │ ├── new-contact-button.png
│ │ │ │ │ ├── new-contact-saved.png
│ │ │ │ │ ├── no-empty-sections.png
│ │ │ │ │ ├── personal-todo-initial.png
│ │ │ │ │ ├── piechart.png
│ │ │ │ │ ├── review-today.png
│ │ │ │ │ ├── rudimentary-dashboard.png
│ │ │ │ │ ├── section-collapsed.png
│ │ │ │ │ ├── sectioned-items.png
│ │ │ │ │ ├── sections-ordered.png
│ │ │ │ │ ├── spacex-list-with-links.png
│ │ │ │ │ ├── spacex-list.png
│ │ │ │ │ ├── start-personal-todo-1.png
│ │ │ │ │ ├── submit-new-contact.png
│ │ │ │ │ ├── submit-new-task.png
│ │ │ │ │ ├── syntax-highlighting.png
│ │ │ │ │ ├── table-with-badge.png
│ │ │ │ │ ├── template-with-card.png
│ │ │ │ │ ├── test-emulated-api.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── todo-logo.png
│ │ │ │ │ └── xmlui-tools.png
│ │ │ │ ├── HelloApp.png
│ │ │ │ ├── HelloApp2.png
│ │ │ │ ├── logos
│ │ │ │ │ ├── xmlui1.svg
│ │ │ │ │ ├── xmlui2.svg
│ │ │ │ │ ├── xmlui3.svg
│ │ │ │ │ ├── xmlui4.svg
│ │ │ │ │ ├── xmlui5.svg
│ │ │ │ │ ├── xmlui6.svg
│ │ │ │ │ └── xmlui7.svg
│ │ │ │ ├── pdf
│ │ │ │ │ └── dummy-pdf.jpg
│ │ │ │ ├── rendering-engine
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ ├── Component.svg
│ │ │ │ │ ├── CompoundComponent.svg
│ │ │ │ │ ├── RootComponent.svg
│ │ │ │ │ └── tree-with-containers.svg
│ │ │ │ ├── reviewers-guide
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ └── incbutton-in-action.png
│ │ │ │ ├── tools
│ │ │ │ │ └── boilerplate-structure.png
│ │ │ │ ├── try.svg
│ │ │ │ ├── tutorial
│ │ │ │ │ ├── app-chat-history.png
│ │ │ │ │ ├── app-content-placeholder.png
│ │ │ │ │ ├── app-header-and-content.png
│ │ │ │ │ ├── app-links-channel-selected.png
│ │ │ │ │ ├── app-links-click.png
│ │ │ │ │ ├── app-navigation.png
│ │ │ │ │ ├── finished-ex01.png
│ │ │ │ │ ├── finished-ex02.png
│ │ │ │ │ ├── hello.png
│ │ │ │ │ ├── splash-screen-advanced.png
│ │ │ │ │ ├── splash-screen-after-click.png
│ │ │ │ │ ├── splash-screen-centered.png
│ │ │ │ │ ├── splash-screen-events.png
│ │ │ │ │ ├── splash-screen-expression.png
│ │ │ │ │ ├── splash-screen-reuse-after.png
│ │ │ │ │ ├── splash-screen-reuse-before.png
│ │ │ │ │ └── splash-screen.png
│ │ │ │ └── tutorial-01.png
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── Boxes.xmlui
│ │ │ ├── Breadcrumb.xmlui
│ │ │ ├── ChangeLog.xmlui
│ │ │ ├── ColorPalette.xmlui
│ │ │ ├── DocumentLinks.xmlui
│ │ │ ├── DocumentPage.xmlui
│ │ │ ├── DocumentPageNoTOC.xmlui
│ │ │ ├── Icons.xmlui
│ │ │ ├── IncButton.xmlui
│ │ │ ├── IncButton2.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── docs-theme.ts
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── Animation.tsx
│ │ ├── AnimationNative.tsx
│ │ ├── FadeAnimation.tsx
│ │ ├── FadeInAnimation.tsx
│ │ ├── FadeOutAnimation.tsx
│ │ ├── index.tsx
│ │ ├── ScaleAnimation.tsx
│ │ └── SlideInAnimation.tsx
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── HelloWorld.module.scss
│ │ ├── HelloWorld.tsx
│ │ ├── HelloWorldNative.tsx
│ │ └── index.tsx
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── IPhoneFrame.module.scss
│ │ ├── IPhoneFrame.tsx
│ │ ├── MacOSAppFrame.module.scss
│ │ ├── MacOSAppFrame.tsx
│ │ ├── WindowsAppFrame.module.scss
│ │ └── WindowsAppFrame.tsx
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── LazyPdfNative.tsx
│ │ ├── Pdf.module.scss
│ │ └── Pdf.tsx
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── hooks
│ │ │ ├── usePlayground.ts
│ │ │ └── useToast.ts
│ │ ├── index.tsx
│ │ ├── playground
│ │ │ ├── Box.module.scss
│ │ │ ├── Box.tsx
│ │ │ ├── CodeSelector.module.scss
│ │ │ ├── CodeSelector.tsx
│ │ │ ├── ConfirmationDialog.module.scss
│ │ │ ├── ConfirmationDialog.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── Header.module.scss
│ │ │ ├── Header.tsx
│ │ │ ├── Playground.tsx
│ │ │ ├── PlaygroundContent.module.scss
│ │ │ ├── PlaygroundContent.tsx
│ │ │ ├── PlaygroundNative.module.scss
│ │ │ ├── PlaygroundNative.tsx
│ │ │ ├── Preview.tsx
│ │ │ ├── StandalonePlayground.tsx
│ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ ├── ThemeSwitcher.module.scss
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ └── utils.ts
│ │ ├── providers
│ │ │ ├── Toast.module.scss
│ │ │ └── ToastProvider.tsx
│ │ ├── state
│ │ │ └── store.ts
│ │ ├── themes
│ │ │ └── theme.ts
│ │ └── utils
│ │ └── helpers.ts
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Search.module.scss
│ │ └── Search.tsx
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Spreadsheet.tsx
│ │ └── SpreadsheetNative.tsx
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ └── src
│ ├── Carousel
│ │ ├── Carousel.module.scss
│ │ ├── Carousel.tsx
│ │ ├── CarouselContext.tsx
│ │ └── CarouselNative.tsx
│ ├── FancyButton
│ │ ├── FancyButton.module.scss
│ │ ├── FancyButton.tsx
│ │ └── FancyButton.xmlui
│ ├── Hello
│ │ ├── Hello.tsx
│ │ ├── Hello.xmlui
│ │ └── Hello.xmlui.xs
│ ├── HeroSection
│ │ ├── HeroSection.module.scss
│ │ ├── HeroSection.spec.ts
│ │ ├── HeroSection.tsx
│ │ └── HeroSectionNative.tsx
│ ├── index.tsx
│ ├── ScrollToTop
│ │ ├── ScrollToTop.module.scss
│ │ ├── ScrollToTop.tsx
│ │ └── ScrollToTopNative.tsx
│ └── vite-env.d.ts
├── playwright.config.ts
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.cjs
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── component-metadata.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── theme-variables-refactoring.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── extract-component-metadata.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── generate-metadata-markdown.js
│ ├── get-langserver-metadata.js
│ ├── inline-links.mjs
│ └── README-e2e-summary.md
├── src
│ ├── abstractions
│ │ ├── _conventions.md
│ │ ├── ActionDefs.ts
│ │ ├── AppContextDefs.ts
│ │ ├── ComponentDefs.ts
│ │ ├── ContainerDefs.ts
│ │ ├── ExtensionDefs.ts
│ │ ├── FunctionDefs.ts
│ │ ├── RendererDefs.ts
│ │ ├── scripting
│ │ │ ├── BlockScope.ts
│ │ │ ├── Compilation.ts
│ │ │ ├── LogicalThread.ts
│ │ │ ├── LoopScope.ts
│ │ │ ├── modules.ts
│ │ │ ├── ScriptParserError.ts
│ │ │ ├── Token.ts
│ │ │ ├── TryScope.ts
│ │ │ └── TryScopeExp.ts
│ │ └── ThemingDefs.ts
│ ├── components
│ │ ├── _conventions.md
│ │ ├── abstractions.ts
│ │ ├── Accordion
│ │ │ ├── Accordion.md
│ │ │ ├── Accordion.module.scss
│ │ │ ├── Accordion.spec.ts
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── AccordionItem.tsx
│ │ │ ├── AccordionItemNative.tsx
│ │ │ └── AccordionNative.tsx
│ │ ├── Animation
│ │ │ └── AnimationNative.tsx
│ │ ├── APICall
│ │ │ ├── APICall.md
│ │ │ ├── APICall.spec.ts
│ │ │ ├── APICall.tsx
│ │ │ └── APICallNative.tsx
│ │ ├── App
│ │ │ ├── App.md
│ │ │ ├── App.module.scss
│ │ │ ├── App.spec.ts
│ │ │ ├── App.tsx
│ │ │ ├── AppLayoutContext.ts
│ │ │ ├── AppNative.tsx
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── AppHeader
│ │ │ ├── AppHeader.md
│ │ │ ├── AppHeader.module.scss
│ │ │ ├── AppHeader.spec.ts
│ │ │ ├── AppHeader.tsx
│ │ │ └── AppHeaderNative.tsx
│ │ ├── AppState
│ │ │ ├── AppState.md
│ │ │ ├── AppState.spec.ts
│ │ │ ├── AppState.tsx
│ │ │ └── AppStateNative.tsx
│ │ ├── AutoComplete
│ │ │ ├── AutoComplete.md
│ │ │ ├── AutoComplete.module.scss
│ │ │ ├── AutoComplete.spec.ts
│ │ │ ├── AutoComplete.tsx
│ │ │ ├── AutoCompleteContext.tsx
│ │ │ └── AutoCompleteNative.tsx
│ │ ├── Avatar
│ │ │ ├── Avatar.md
│ │ │ ├── Avatar.module.scss
│ │ │ ├── Avatar.spec.ts
│ │ │ ├── Avatar.tsx
│ │ │ └── AvatarNative.tsx
│ │ ├── Backdrop
│ │ │ ├── Backdrop.md
│ │ │ ├── Backdrop.module.scss
│ │ │ ├── Backdrop.spec.ts
│ │ │ ├── Backdrop.tsx
│ │ │ └── BackdropNative.tsx
│ │ ├── Badge
│ │ │ ├── Badge.md
│ │ │ ├── Badge.module.scss
│ │ │ ├── Badge.spec.ts
│ │ │ ├── Badge.tsx
│ │ │ └── BadgeNative.tsx
│ │ ├── Bookmark
│ │ │ ├── Bookmark.md
│ │ │ ├── Bookmark.module.scss
│ │ │ ├── Bookmark.spec.ts
│ │ │ ├── Bookmark.tsx
│ │ │ └── BookmarkNative.tsx
│ │ ├── Breakout
│ │ │ ├── Breakout.module.scss
│ │ │ ├── Breakout.spec.ts
│ │ │ ├── Breakout.tsx
│ │ │ └── BreakoutNative.tsx
│ │ ├── Button
│ │ │ ├── Button-style.spec.ts
│ │ │ ├── Button.md
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.spec.ts
│ │ │ ├── Button.tsx
│ │ │ └── ButtonNative.tsx
│ │ ├── Card
│ │ │ ├── Card.md
│ │ │ ├── Card.module.scss
│ │ │ ├── Card.spec.ts
│ │ │ ├── Card.tsx
│ │ │ └── CardNative.tsx
│ │ ├── Carousel
│ │ │ ├── Carousel.md
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.spec.ts
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ ├── CarouselItem.tsx
│ │ │ ├── CarouselItemNative.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── ChangeListener
│ │ │ ├── ChangeListener.md
│ │ │ ├── ChangeListener.spec.ts
│ │ │ ├── ChangeListener.tsx
│ │ │ └── ChangeListenerNative.tsx
│ │ ├── chart-color-schemes.ts
│ │ ├── Charts
│ │ │ ├── AreaChart
│ │ │ │ ├── AreaChart.md
│ │ │ │ ├── AreaChart.spec.ts
│ │ │ │ ├── AreaChart.tsx
│ │ │ │ └── AreaChartNative.tsx
│ │ │ ├── BarChart
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── BarChart.module.scss
│ │ │ │ ├── BarChart.spec.ts
│ │ │ │ ├── BarChart.tsx
│ │ │ │ └── BarChartNative.tsx
│ │ │ ├── DonutChart
│ │ │ │ ├── DonutChart.spec.ts
│ │ │ │ └── DonutChart.tsx
│ │ │ ├── LabelList
│ │ │ │ ├── LabelList.module.scss
│ │ │ │ ├── LabelList.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ └── ContentSeparatorNative.tsx
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.js
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── index.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── index.ts
│ │ ├── infrastructure
│ │ │ ├── index.html
│ │ │ ├── main.tsx
│ │ │ ├── public
│ │ │ │ ├── mockServiceWorker.js
│ │ │ │ ├── resources
│ │ │ │ │ ├── bell.svg
│ │ │ │ │ ├── box.svg
│ │ │ │ │ ├── doc.svg
│ │ │ │ │ ├── eye.svg
│ │ │ │ │ ├── flower-640x480.jpg
│ │ │ │ │ ├── sun.svg
│ │ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ │ └── txt.svg
│ │ │ │ └── serve.json
│ │ │ └── TestBed.tsx
│ │ └── themed-app-test-helpers.ts
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.json
├── tsdown.config.ts
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/xmlui/tests/parsers/style-parser/tokens.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it } from "vitest";
import { StyleTokenType } from "../../../src/parsers/style-parser/tokens";
import { StyleLexer } from "../../../src/parsers/style-parser/StyleLexer";
import { StyleInputStream } from "../../../src/parsers/style-parser/StyleInputStream";
describe("Parser - Binary operations", () => {
const simpleTokens = [
{ src: " ", token: StyleTokenType.Ws },
{ src: "\t", token: StyleTokenType.Ws },
{ src: "\n", token: StyleTokenType.Ws },
{ src: "\r", token: StyleTokenType.Ws },
{ src: "*", token: StyleTokenType.Star },
{ src: ",", token: StyleTokenType.Comma },
{ src: "(", token: StyleTokenType.LParent },
{ src: ")", token: StyleTokenType.RParent },
{ src: "%", token: StyleTokenType.Percentage },
{ src: "/", token: StyleTokenType.Slash },
{ src: "none", token: StyleTokenType.None },
{ src: "reset", token: StyleTokenType.Reset },
];
simpleTokens.forEach((s) =>
it(`Token: ${s.src}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s.src));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s.src);
expect(token.type).equal(s.token);
})
);
const sizeUnitTokens = ["px", "cm", "mm", "in", "pt", "pc", "em", "rem", "vw", "vh", "ex", "ch", "vmin", "vmax"];
sizeUnitTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.SizeUnit);
})
);
const alignmentTokens = ["start", "center", "end"];
alignmentTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.Alignment);
})
);
const textAlignTokens = ["left", "right", "justify"];
textAlignTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.TextAlignment);
})
);
const orientationTokens = ["horizontal", "vertical"];
orientationTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.Orientation);
})
);
const borderStyleTokens = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset"];
borderStyleTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.BorderStyle);
})
);
const directionTokens = ["ltr", "rtl"];
directionTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.Direction);
})
);
const fontWeightTokens = ["lighter", "normal", "bold", "bolder", "100", "200"];
fontWeightTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).to.be.oneOf([StyleTokenType.FontWeight, StyleTokenType.Number]);
})
);
const zIndexTokens = ["-1", "0", "1", "99"];
zIndexTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.Number);
})
);
const scrollingTokens = ["visible", "hidden", "scroll"];
scrollingTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.Scrolling);
})
);
const fontFamilyTokens = ["$fontFamily", "serif", "sansSerif", "mono"];
fontFamilyTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).to.be.oneOf([StyleTokenType.FontFamily, StyleTokenType.Identifier]);
})
);
const angleTokens = ["deg", "rad", "grad", "turn"];
angleTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.Angle);
})
);
const numberTokens = [
{ src: "0", value: 0 },
{ src: "1", value: 1 },
{ src: "2", value: 2 },
{ src: "3", value: 3 },
{ src: "4", value: 4 },
{ src: "5", value: 5 },
{ src: "6", value: 6 },
{ src: "7", value: 7 },
{ src: "8", value: 8 },
{ src: "9", value: 9 },
{ src: "123", value: 123 },
{ src: ".25", value: 0.25 },
{ src: "1234.125", value: 1234.125 },
{ src: "-9", value: -9 },
{ src: "-123", value: -123 },
{ src: "-.25", value: -0.25 },
{ src: "-1234.125", value: -1234.125 },
];
numberTokens.forEach((s) =>
it(`Token: ${s.src}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s.src));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s.src);
expect(token.type).equal(StyleTokenType.Number);
expect(parseFloat(token.text)).equal(s.value);
})
);
const hexaColorTokens = [
{ src: "#", token: StyleTokenType.Unknown },
{ src: "#1", token: StyleTokenType.Unknown },
{ src: "#f2", token: StyleTokenType.Unknown },
{ src: "#abc", token: StyleTokenType.HexaColor },
{ src: "#abcf", token: StyleTokenType.HexaColor },
{ src: "#1abcf", token: StyleTokenType.Unknown },
{ src: "#12abcf", token: StyleTokenType.HexaColor },
{ src: "#012abcf", token: StyleTokenType.Unknown },
{ src: "#4312abcf", token: StyleTokenType.HexaColor },
{ src: "#14312abcf", token: StyleTokenType.Unknown },
{ src: "#d14312abcf", token: StyleTokenType.Unknown },
];
hexaColorTokens.forEach((s) =>
it(`Token: ${s.src}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s.src));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s.src);
expect(token.type).equal(s.token);
})
);
it("Token: none", () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream("none"));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal("none");
expect(token.type).equal(StyleTokenType.None);
});
const colorFunctionTokens = ["rgb", "rgba", "hsl", "hsla"];
colorFunctionTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.ColorFunc);
})
);
const colorNameTokens = [
"transparent",
"black",
"silver",
"gray",
"white",
"maroon",
"red",
"purple",
"fuchsia",
"green",
"lime",
"olive",
"yellow",
"navy",
"blue",
"teal",
"aqua",
"orange",
"aliceblue",
"antiquewhite",
"aquamarine",
"azure",
"beige",
"bisque",
"blanchedalmond",
"blueviolet",
"brown",
"burlywood",
"cadetblue",
"chartreuse",
"chocolate",
"coral",
"cornflowerblue",
"cornsilk",
"crimson",
"cyan",
"darkblue",
"darkcyan",
"darkgoldenrod",
"darkgray",
"darkgreen",
"darkgrey",
"darkkhaki",
"darkmagenta",
"darkolivegreen",
"darkorange",
"darkorchid",
"darkred",
"darksalmon",
"darkseagreen",
"darkslateblue",
"darkslategray",
"darkslategrey",
"darkturquoise",
"darkviolet",
"deeppink",
"deepskyblue",
"dimgray",
"dimgrey",
"dodgerblue",
"firebrick",
"floralwhite",
"forestgreen",
"gainsboro",
"ghostwhite",
"gold",
"goldenrod",
"greenyellow",
"grey",
"honeydew",
"hotpink",
"indianred",
"indigo",
"ivory",
"khaki",
"lavender",
"lavenderblush",
"lawngreen",
"lemonchiffon",
"lightblue",
"lightcoral",
"lightcyan",
"lightgoldenrodyellow",
"lightgray",
"lightgreen",
"lightgrey",
"lightpink",
"lightsalmon",
"lightseagreen",
"lightskyblue",
"lightslategray",
"lightslategrey",
"lightsteelblue",
"lightyellow",
"limegreen",
"linen",
"magenta",
"mediumaquamarine",
"mediumblue",
"mediumorchid",
"mediumpurple",
"mediumseagreen",
"mediumslateblue",
"mediumspringgreen",
"mediumturquoise",
"mediumvioletred",
"midnightblue",
"mintcream",
"mistyrose",
"moccasin",
"navajowhite",
"oldlace",
"olivedrab",
"orangered",
"orchid",
"palegoldenrod",
"palegreen",
"paleturquoise",
"palevioletred",
"papayawhip",
"peachpuff",
"peru",
"pink",
"plum",
"powderblue",
"rosybrown",
"royalblue",
"saddlebrown",
"salmon",
"sandybrown",
"seagreen",
"seashell",
"sienna",
"skyblue",
"slateblue",
"slategray",
"slategrey",
"snow",
"springgreen",
"steelblue",
"tan",
"thistle",
"tomato",
"turquoise",
"violet",
"wheat",
"whitesmoke",
"yellowgreen",
"rebeccapurple",
];
colorNameTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.ColorName);
})
);
const lineDecorationTokens = ["underline", "overline", "line-through"];
lineDecorationTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.DecorationLine);
})
);
const stringTokens = ["'start'", "'Courier New'", '"end"'];
stringTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.String);
})
);
const userSelectTokens = ["all", "text", "contain"];
userSelectTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.UserSelect);
})
);
const textTransformTokens = ["capitalize", "uppercase", "lowercase", "full-width", "full-size-kana"];
textTransformTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
expect(token.type).equal(StyleTokenType.TextTransform);
})
);
const cursorTokens = [
"auto",
"default",
"none",
"context-menu",
"help",
"pointer",
"progress",
"wait",
"cell",
"crosshair",
"text",
"vertical-text",
"alias",
"copy",
"move",
"no-drop",
"not-allowed",
"grab",
"grabbing",
"all-scroll",
"col-resize",
"row-resize",
"n-resize",
"e-resize",
"s-resize",
"w-resize",
"ne-resize",
"nw-resize",
"se-resize",
"sw-resize",
"ew-resize",
"ns-resize",
"nesw-resize",
"nwse-resize",
"zoom-in",
"zoom-out",
];
cursorTokens.forEach((s) =>
it(`Token: ${s}`, () => {
// --- Arrange
const lexer = new StyleLexer(new StyleInputStream(s));
// --- Act
const token = lexer.get(true);
// --- Assert
expect(token.text).equal(s);
})
);
});
```
--------------------------------------------------------------------------------
/xmlui/src/components/Markdown/utils.ts:
--------------------------------------------------------------------------------
```typescript
import { encodeToBase64 } from "../../components-core/utils/base64-utils";
export type SegmentProps = {
display?: boolean;
copy?: boolean;
noPopup?: boolean;
noFrame?: boolean;
noHeader?: boolean;
splitView?: boolean;
initiallyShowCode?: boolean;
highlights?: (number | [number, number])[];
filename?: string;
name?: string;
height?: string;
popOutUrl?: string;
content?: string;
order?: number;
patterns?: string[];
borderedPatterns?: string[];
withSplashScreen?: boolean;
immediate?: boolean;
};
export type PlaygroundPattern = {
default?: SegmentProps;
app?: SegmentProps;
components?: SegmentProps[];
descriptions?: SegmentProps[];
config?: SegmentProps;
api?: SegmentProps;
};
export function observePlaygroundPattern(content: string): [number, number, string] | null {
const startPattern = "```xmlui-pg";
const endPattern = "```";
const startIndex = content.indexOf(startPattern);
if (startIndex === -1) {
return null; // No match for the start pattern
}
// Find the end of the start pattern
const startContentIndex = content.indexOf("\n", startIndex);
if (startContentIndex === -1) {
return null; // Malformed pattern, no newline after start
}
// Search for the end pattern after the start content
let endIndex = startContentIndex;
while (endIndex !== -1) {
endIndex = content.indexOf(endPattern, endIndex + 1);
if (endIndex !== -1) {
// Check if the end pattern is not part of {pg-content} (escaped with backticks)
const precedingChar = content[endIndex - 1];
if (precedingChar !== "\\") {
return [
startIndex,
endIndex + endPattern.length,
content.substring(startIndex, endIndex + endPattern.length),
];
}
}
}
return null; // No valid end pattern found
}
export function parseSegmentProps(input: string): SegmentProps {
const segment: SegmentProps = {};
// --- Match the "display" flag
if (/\bdisplay\b/.test(input)) {
segment.display = true;
}
// --- Match the "copy" flag
if (/\bcopy\b/.test(input)) {
segment.copy = true;
}
// --- Match the "noPopup" flag
if (/\bnoPopup\b/.test(input)) {
segment.noPopup = true;
}
// --- Match the "noFrame" flag
if (/\bnoFrame\b/.test(input)) {
segment.noFrame = true;
}
// --- Match the "immediate" flag
if (/\bimmediate\b/.test(input)) {
segment.immediate = true;
}
// --- Match the "withSplashScreen" flag
if (/\bwithSplashScreen\b/.test(input)) {
segment.withSplashScreen = true;
}
// --- Match the "noHeader" flag
if (/\bnoHeader\b/.test(input)) {
segment.noHeader = true;
}
// --- Match the "splitView" flag
if (/\bsplitView\b/.test(input)) {
segment.splitView = true;
}
// --- Match the "initiallyShowCode" flag
if (/\binitiallyShowCode\b/.test(input)) {
segment.initiallyShowCode = true;
}
// Match the "highlights" pattern
const highlightsMatch = input.match(/\{([^\}]+)\}/);
if (highlightsMatch) {
const highlights = highlightsMatch[1].split(",").map((range) => {
if (range.includes("-")) {
const [start, end] = range.split("-").map(Number);
return [start, end]; // Represent ranges as [start, end]
}
return Number(range); // Represent single numbers as numbers
});
segment.highlights = highlights as (number | [number, number])[];
}
// Match the "filename" property
const filenameMatch = input.match(/\bfilename="([^"]+)"/);
if (filenameMatch) {
segment.filename = filenameMatch[1];
}
// Match the "name" property
const nameMatch = input.match(/\bname="([^"]+)"/);
if (nameMatch) {
segment.name = nameMatch[1];
}
// Match the "height" property
const heightMatch = input.match(/\bheight="([^"]+)"/);
if (heightMatch) {
segment.height = heightMatch[1];
}
// Match the "popOutUrl" property
const popOutUrlMatch = input.match(/\bpopOutUrl="([^"]+)"/);
if (popOutUrlMatch) {
segment.popOutUrl = popOutUrlMatch[1];
}
// Match patterns enclosed in /pattern/ format
const patternMatches = input.match(/\/([^\/]+)\//g);
if (patternMatches) {
segment.patterns = patternMatches.map((pattern) =>
// Remove the surrounding slashes
pattern.substring(1, pattern.length - 1),
);
}
// Match bordered patterns enclosed in !/pattern/ format
const borderedPatternMatches = input.match(/!\/(.[^\/]+)\//g);
if (borderedPatternMatches) {
segment.borderedPatterns = borderedPatternMatches.map((pattern) =>
// Remove the leading !/ and trailing /
pattern.substring(2, pattern.length - 1),
);
}
return segment;
}
export function parsePlaygroundPattern(content: string): PlaygroundPattern {
const pattern: PlaygroundPattern = {};
const match = observePlaygroundPattern(content);
if (!match) {
return pattern;
}
// --- Extract the pattern content
const [_startIndex, _endIndex, patternContent] = match;
const lines = patternContent.split("\n");
pattern.default = parseSegmentProps(lines[0].trim());
let segmentContent = "";
let currentMode = "";
let foundSegment = false;
let order = 0;
for (let i = 1; i < lines.length - 1; i++) {
const line = lines[i];
if (line.startsWith("---app")) {
const appSegment = parseSegmentProps(line);
pattern.app = { ...appSegment };
closeCurrentMode("app");
} else if (line.startsWith("---comp")) {
closeCurrentMode("comp");
const compSegment = parseSegmentProps(line);
pattern.components ??= [];
pattern.components.push(compSegment);
} else if (line.startsWith("---config")) {
const configSegment = parseSegmentProps(line);
pattern.config ??= { ...configSegment };
closeCurrentMode("config");
} else if (line.startsWith("---api")) {
const apiSegment = parseSegmentProps(line);
pattern.api ??= { ...apiSegment };
closeCurrentMode("api");
} else if (line.startsWith("---desc")) {
closeCurrentMode("desc");
const descSegment = parseSegmentProps(line);
pattern.descriptions ??= [];
pattern.descriptions.push(descSegment);
} else {
// Append the line to the current segment content
segmentContent += line + "\n";
}
}
// --- Handle the last segment
if (foundSegment) {
closeCurrentMode("");
} else {
pattern.app = {
...pattern.default,
content: segmentContent,
order,
};
}
return pattern;
function closeCurrentMode(newMode: string) {
foundSegment = true;
switch (currentMode) {
case "app":
pattern.app.content = segmentContent;
pattern.app.order = order++;
break;
case "comp":
pattern.components[pattern.components.length - 1].content = segmentContent;
pattern.components[pattern.components.length - 1].order = order++;
break;
case "config":
pattern.config.content = segmentContent;
pattern.config.order = order++;
break;
case "api":
pattern.api.content = segmentContent;
pattern.api.order = order++;
break;
case "desc":
pattern.descriptions[pattern.descriptions.length - 1].content = segmentContent;
pattern.descriptions[pattern.descriptions.length - 1].order = order++;
break;
}
segmentContent = "";
currentMode = newMode;
}
}
export function convertPlaygroundPatternToMarkdown(content: string): string {
const pattern = parsePlaygroundPattern(content);
// --- Determine max order for segments
let maxOrder = 0;
if (pattern.app?.order > maxOrder) {
maxOrder = pattern.app.order;
}
if (pattern.config?.order > maxOrder) {
maxOrder = pattern.config.order;
}
if (pattern.api?.order > maxOrder) {
maxOrder = pattern.api.order;
}
if (pattern.descriptions) {
pattern.descriptions.forEach((desc) => {
if (desc.order > maxOrder) {
maxOrder = desc.order;
}
});
}
if (pattern.components) {
pattern.components.forEach((comp) => {
if (comp.order > maxOrder) {
maxOrder = comp.order;
}
});
}
// --- Assemble the final markdown content
let markdownContent = "";
const pgContent: any = {
noPopup: pattern.default?.noPopup,
noFrame: pattern.default?.noFrame,
noHeader: pattern.default?.noHeader,
splitView: pattern.default?.splitView,
initiallyShowCode: pattern.default?.initiallyShowCode,
popOutUrl: pattern.default?.popOutUrl,
immediate: pattern.default?.immediate,
withSplashScreen: pattern.default?.withSplashScreen,
};
// --- Extract optional playground attributes
if (pattern.default.height) {
pgContent.height = pattern.default.height;
}
if (pattern.default.name) {
pgContent.name = pattern.default.name;
}
if (pattern.default.popOutUrl) {
pgContent.popOutUrl = pattern.default.popOutUrl;
}
// --- Iterate through segments
for (let i = 0; i <= maxOrder; i++) {
let segment: SegmentProps | undefined;
let type = "";
if (pattern.app?.order === i) {
segment = pattern.app;
type = "app";
} else if (pattern.config?.order === i) {
segment = pattern.config;
type = "config";
} else if (pattern.api?.order === i) {
segment = pattern.api;
type = "api";
}
if (!segment && pattern.descriptions) {
const descSegment = pattern.descriptions.find((desc) => desc.order === i);
if (descSegment) {
segment = descSegment;
type = "desc";
}
}
if (!segment && pattern.components) {
const compSegment = pattern.components.find((comp) => comp.order === i);
if (compSegment) {
segment = compSegment;
type = "comp";
}
}
if (segment === undefined) {
continue; // Skip if no segment found
}
// --- Assemble the app
// --- Assemble segment attributes
let segmentAttrs =
`${segment.copy ? "copy" : ""} ` +
`${segment.filename ? `filename="${segment.filename}"` : ""} ` +
`${segment.name ? `name="${segment.name}"` : ""} ` +
`${segment.popOutUrl ? `popOutUrl="${segment.popOutUrl}"` : ""}`;
if (segment.highlights && segment.highlights.length > 0) {
const highlights = segment.highlights
.map((highlight) =>
Array.isArray(highlight) ? `${highlight[0]}-${highlight[1]}` : highlight,
)
.join(",");
segmentAttrs += `{${highlights}}`;
}
if (segment.patterns && segment.patterns.length > 0) {
segmentAttrs += " " + segment.patterns.map((p) => `/${p}/`).join(" ");
}
if (segment.borderedPatterns && segment.borderedPatterns.length > 0) {
segmentAttrs += " " + segment.borderedPatterns.map((p) => `!/` + p + `/`).join(" ");
}
segmentAttrs = segmentAttrs.trim().replace(/\s+/g, " ");
switch (type) {
case "app":
if (segment.display) {
markdownContent += "```xmlui " + segmentAttrs + "\n" + segment.content + "```\n\n";
}
pgContent.app = segment.content;
break;
case "config":
if (segment.display) {
markdownContent += "```json " + segmentAttrs + "\n" + segment.content + "```\n\n";
}
pgContent.config = segment.content;
break;
case "api":
// --- Never display API segments
pgContent.api = segment.content;
break;
case "comp":
if (segment.display) {
markdownContent += "```xmlui " + segmentAttrs + "\n" + segment.content + "```\n\n";
}
pgContent.components ??= [];
pgContent.components.push(segment.content);
break;
case "desc":
markdownContent += segment.content + "\n";
break;
}
}
// --- Convert the JSON representation of pgContent to a base64 string
const jsonString = JSON.stringify(pgContent);
const base64ContentString = encodeToBase64(jsonString);
const base64MarkdownString = encodeToBase64(markdownContent);
return (
'<samp data-pg-content="' +
base64ContentString +
'" data-pg-markdown="' +
base64MarkdownString +
'"></samp>\n\n'
);
}
export function observeTreeDisplay(content: string): [number, number, string] | null {
const startPattern = "```xmlui-tree";
const endPattern = "```";
const startIndex = content.indexOf(startPattern);
if (startIndex === -1) {
return null; // No match for the start pattern
}
// Find the end of the start pattern
const startContentIndex = content.indexOf("\n", startIndex);
if (startContentIndex === -1) {
return null; // Malformed pattern, no newline after start
}
// Search for the end pattern after the start content
let endIndex = startContentIndex;
while (endIndex !== -1) {
endIndex = content.indexOf(endPattern, endIndex + 1);
if (endIndex !== -1) {
// Check if the end pattern is not escaped
const precedingChar = content[endIndex - 1];
if (precedingChar !== "\\") {
return [
startIndex,
endIndex + endPattern.length,
content.substring(startIndex, endIndex + endPattern.length),
];
}
}
}
return null; // No valid end pattern found
}
export function convertTreeDisplayToMarkdown(content: string): string {
if (content.startsWith("```xmlui-tree") && content.endsWith("```")) {
const treeContent = content
.slice("```xmlui-tree".length, content.indexOf("```", "```xmlui-tree".length))
.trim();
return `<section data-tree-content="${encodeToBase64(treeContent)}"></section>\n\n`;
}
}
```
--------------------------------------------------------------------------------
/xmlui/src/components/App/App.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { expect, test } from "../../testing/fixtures";
// =============================================================================
// BASIC FUNCTIONALITY TESTS
// =============================================================================
test.describe("Basic Functionality", () => {
test("renders with basic props", async ({ initTestBed, page }) => {
await initTestBed(`<App name="Test App" testId="app"/>`);
await expect(page.getByTestId("app")).toBeVisible();
});
test("renders with horizontal layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="horizontal">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});
test("renders with horizontal-sticky layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="horizontal-sticky">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});
test("renders with condensed layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="condensed">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});
test("renders with condensed-sticky layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="condensed-sticky">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});
test("renders with vertical layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="vertical">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});
test("renders with vertical-sticky layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="vertical-sticky">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});
test("renders with vertical-full-header layout", async ({ initTestBed, page }) => {
await initTestBed(`<App layout="vertical-full-header">test text</App>`);
await expect(page.getByText("test text")).toBeVisible();
});
test("handles layout prop changes correctly", async ({
page,
initTestBed,
createButtonDriver,
}) => {
await initTestBed(`
<App var.lo="vertical" layout="{lo}" testId="app">
<Button testId="toggleLayout" label="Toggle" onClick="lo = 'horizontal'" />
</App>
`);
const buttonDriver = await createButtonDriver("toggleLayout");
await expect(page.getByTestId("app")).toHaveClass(/vertical/);
await buttonDriver.click();
await expect(page.getByTestId("app")).toHaveClass(/horizontal/);
});
test("sets document title from name prop", async ({ initTestBed, page }) => {
const APP_NAME = "My Test Application";
await initTestBed(`<App name="${APP_NAME}" testId="app"/>`);
await expect(page.getByTestId("app")).toBeVisible();
expect(await page.title()).toBe(APP_NAME);
});
test("handles different visual states with scrolling options", async ({ initTestBed, page }) => {
// Test with scrollWholePage=true
await initTestBed(`<App scrollWholePage="true" testId="app"/>`);
await expect(page.getByTestId("app")).toHaveClass(/scrollWholePage/);
// Test with scrollWholePage=false
await initTestBed(`<App scrollWholePage="false" testId="app"/>`);
await expect(page.getByTestId("app")).not.toHaveClass(/scrollWholePage/);
});
});
// =============================================================================
// EDGE CASE TESTS
// =============================================================================
test.describe("Edge Cases", () => {
test("handles undefined props gracefully", async ({ initTestBed, page }) => {
await initTestBed(`<App testId="app" />`);
await expect(page.getByTestId("app")).toBeVisible();
// App should use a default layout
await expect(page.getByTestId("app")).toHaveClass(/horizontal/);
});
test("works correctly with basic content structure", async ({ initTestBed, page }) => {
await initTestBed(`
<App testId="app">
<Text testId="content">Content</Text>
</App>`);
await expect(page.getByTestId("app")).toBeVisible();
await expect(page.getByTestId("content")).toBeVisible();
await expect(page.getByText("Content")).toBeVisible();
});
test("works correctly with complex content structure", async ({ initTestBed, page }) => {
await initTestBed(`
<App testId="app">
<AppHeader testId="header">Header Content</AppHeader>
<NavPanel testId="nav">
<NavLink testId="link1" to="#">Link 1</NavLink>
<NavGroup testId="group" label="Group">
<NavLink testId="nestedLink" to="#">Nested Link</NavLink>
</NavGroup>
</NavPanel>
<Pages fallbackPath="/">
<Page url="/" testId="homePage">
<Text testId="homeContent">Home Content</Text>
</Page>
</Pages>
<Footer testId="footer">Footer Content</Footer>
</App>`);
await expect(page.getByTestId("app")).toBeVisible();
// Test component structure
await expect(page.getByTestId("header")).toBeVisible();
await expect(page.getByTestId("nav")).toBeVisible();
await expect(page.getByTestId("footer")).toBeVisible();
// Test content
await expect(page.getByText("Header Content")).toBeVisible();
await expect(page.getByText("Link 1")).toBeVisible();
await expect(page.getByText("Group")).toBeVisible();
await expect(page.getByText("Home Content")).toBeVisible();
await expect(page.getByText("Footer Content")).toBeVisible();
});
});
// =============================================================================
// EVENT HANDLING TESTS
// =============================================================================
test.describe("Event Handling", () => {
test("ready event is triggered when App component finishes rendering", async ({
initTestBed,
}) => {
const { testStateDriver } = await initTestBed(`
<App
onReady="() => testState = 'app-ready'"
testId="app"
/>
`);
// Verify the ready event was fired
await expect.poll(testStateDriver.testState).toEqual("app-ready");
});
test("ready event is triggered for App with complex content", async ({ initTestBed }) => {
const { testStateDriver } = await initTestBed(`
<App
onReady="() => testState = 'complex-app-ready'"
layout="horizontal"
testId="app"
>
<AppHeader>
<property name="logoTemplate">
<Text value="Test App" />
</property>
</AppHeader>
<Pages fallbackPath="/">
<Page url="/">
<Text value="Home Page" />
</Page>
</Pages>
<Footer>
<Text value="Footer Content" />
</Footer>
</App>
`);
// Verify the ready event was fired even with complex content
await expect.poll(testStateDriver.testState).toEqual("complex-app-ready");
});
test("ready event fires only once during component lifecycle", async ({ initTestBed, page }) => {
const { testStateDriver } = await initTestBed(`
<App
var.counter="{0}"
onReady="() => { counter = counter + 1; testState = counter; }"
testId="app"
>
<Button
testId="trigger-rerender"
label="Re-render"
onClick="counter = counter"
/>
</App>
`);
// Initial ready event should fire
await expect.poll(testStateDriver.testState).toEqual(1);
// Trigger a re-render by clicking the button
await page.getByTestId("trigger-rerender").click();
// Counter should still be 1 (ready event doesn't fire again)
await expect.poll(testStateDriver.testState).toEqual(1);
});
test("messageReceived event is triggered when window receives a message", async ({
initTestBed,
page,
}) => {
const { testStateDriver } = await initTestBed(`
<App
onMessageReceived="(msg, ev) => testState = msg"
testId="app"
/>
`);
// Send a message to the window
await page.evaluate(() => {
window.postMessage("test-message", "*");
});
// Verify the event was received and handled
await expect.poll(testStateDriver.testState).toEqual("test-message");
});
test("messageReceived event receives both message data and event object", async ({
initTestBed,
page,
}) => {
const { testStateDriver } = await initTestBed(`
<App
onMessageReceived="(msg, ev) => testState = { message: msg, eventType: ev.type, origin: ev.origin }"
testId="app"
/>
`);
// Send a message to the window
await page.evaluate(() => {
window.postMessage("event-test", "*");
});
// Verify both parameters are accessible
await expect.poll(testStateDriver.testState).toMatchObject({
message: "event-test",
eventType: "message",
origin: expect.any(String),
});
});
test("messageReceived event handles complex data objects", async ({ initTestBed, page }) => {
const { testStateDriver } = await initTestBed(`
<App
onMessageReceived="(msg, ev) => testState = msg"
testId="app"
/>
`);
// Send a complex object as message data
const testData = { action: "test", payload: { id: 123, name: "test-item" } };
await page.evaluate((data) => {
window.postMessage(data, "*");
}, testData);
// Verify the complex data is received correctly
await expect.poll(testStateDriver.testState).toEqual(testData);
});
});
// =============================================================================
// Drawer HANDLING TESTS
// =============================================================================
test.describe("Drawer Handling", () => {
test("Drawer displayed if NavPanel has no 'when'", async ({
initTestBed,
page,
}) => {
// Set small viewport to trigger drawer mode
await page.setViewportSize({ width: 400, height: 600 });
await initTestBed(`
<App layout="condensed">
<AppHeader testId="appHeader"/>
<NavPanel>
<NavGroup label="Pages">
<NavLink label="Page 1" to="/page1"/>
<NavLink label="Page 2" to="/page2"/>
</NavGroup>
</NavPanel>
</App>
`);
// Open drawer by clicking hamburger button
const appHeader = page.getByTestId("appHeader");
const hamburgerButton = appHeader.locator('[data-part-id="hamburger"]').first();
await expect (hamburgerButton).toBeVisible();
});
test("Drawer displayed if NavPanel has when='true'", async ({
initTestBed,
page,
}) => {
// Set small viewport to trigger drawer mode
await page.setViewportSize({ width: 400, height: 600 });
await initTestBed(`
<App layout="condensed">
<AppHeader testId="appHeader"/>
<NavPanel when="true">
<NavGroup label="Pages">
<NavLink label="Page 1" to="/page1"/>
<NavLink label="Page 2" to="/page2"/>
</NavGroup>
</NavPanel>
</App>
`);
// Open drawer by clicking hamburger button
const appHeader = page.getByTestId("appHeader");
const hamburgerButton = appHeader.locator('[data-part-id="hamburger"]').first();
await expect (hamburgerButton).toBeVisible();
});
test("Drawer displayed if NavPanel has when='{true}'", async ({
initTestBed,
page,
}) => {
// Set small viewport to trigger drawer mode
await page.setViewportSize({ width: 400, height: 600 });
await initTestBed(`
<App layout="condensed">
<AppHeader testId="appHeader"/>
<NavPanel when="{true}">
<NavGroup label="Pages">
<NavLink label="Page 1" to="/page1"/>
<NavLink label="Page 2" to="/page2"/>
</NavGroup>
</NavPanel>
</App>
`);
// Open drawer by clicking hamburger button
const appHeader = page.getByTestId("appHeader");
const hamburgerButton = appHeader.locator('[data-part-id="hamburger"]').first();
await expect (hamburgerButton).toBeVisible();
});
test("Drawer not displayed if NavPanel has when='false'", async ({
initTestBed,
page,
}) => {
// Set small viewport to trigger drawer mode
await page.setViewportSize({ width: 400, height: 600 });
await initTestBed(`
<App layout="condensed">
<AppHeader testId="appHeader"/>
<NavPanel when="false">
<NavGroup label="Pages">
<NavLink label="Page 1" to="/page1"/>
<NavLink label="Page 2" to="/page2"/>
</NavGroup>
</NavPanel>
</App>
`);
// Open drawer by clicking hamburger button
const appHeader = page.getByTestId("appHeader");
const hamburgerButton = appHeader.locator('[data-part-id="hamburger"]').first();
await expect (hamburgerButton).not.toBeVisible();
});
test("Drawer not displayed if NavPanel has when='{false}'", async ({
initTestBed,
page,
}) => {
// Set small viewport to trigger drawer mode
await page.setViewportSize({ width: 400, height: 600 });
await initTestBed(`
<App layout="condensed">
<AppHeader testId="appHeader"/>
<NavPanel when="{false}">
<NavGroup label="Pages">
<NavLink label="Page 1" to="/page1"/>
<NavLink label="Page 2" to="/page2"/>
</NavGroup>
</NavPanel>
</App>
`);
// Open drawer by clicking hamburger button
const appHeader = page.getByTestId("appHeader");
const hamburgerButton = appHeader.locator('[data-part-id="hamburger"]').first();
await expect (hamburgerButton).not.toBeVisible();
});
});
```
--------------------------------------------------------------------------------
/docs/content/components/NavLink.md:
--------------------------------------------------------------------------------
```markdown
# NavLink [#navlink]
`NavLink` creates interactive navigation items that connect users to different destinations within an app or external URLs. It automatically indicates active states, supports custom icons and labels, and can execute custom actions instead of navigation when needed.
**Key features:**
- **Custom actions**: Execute JavaScript code instead of navigation when using onClick handlers
- **Visual customization**: Support for icons, labels, and completely custom nested content
- **Accessibility support**: Proper focus management and keyboard navigation
## Using NavLink [#using-navlink]
### `NavLink` Appearance [#navlink-appearance]
You can use the `label` and `icon` properties of a `NavLink` to set its text and icon to display.
If you want a custom appearance, you can nest define custom visuals for the `NavLink` by nesting:
```xmlui-pg copy {6-14} display name="Example: NavLink appearance" height="250px"
<App layout="horizontal">
<AppHeader>
<H1>MyApp</H1>
</AppHeader>
<NavPanel>
<NavLink to="/">
<Stack width="16px" height="16px" backgroundColor="purple" />
Home
</NavLink>
<NavLink to="/about">
<Stack width="16px" height="16px" backgroundColor="green" />
About
</NavLink>
</NavPanel>
<Pages>
<Page url="/">Home</Page>
<Page url="/about">About</Page>
</Pages>
</App>
```
### Actions [#actions]
By default, activating (clicking) a link navigates to the target URL.
However, you can create a link that executes an explicit action responding to the `click` event instead of the default navigation:
```xmlui-pg copy {7} display name="Example: custom NavLink action" height="250px"
<App layout="horizontal">
<AppHeader>
<H1>MyApp</H1>
</AppHeader>
<NavPanel>
<NavLink to="/" label="Home" />
<NavLink label="Danger!" onClick="toast('Be careful with this action!')" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
</Pages>
</App>
```
## Properties [#properties]
### `active` (default: false) [#active-default-false]
This property indicates if the particular navigation is an active link. An active link has a particular visual appearance, provided its [`displayActive`](#displayactive) property is set to `true`.
### `displayActive` (default: true) [#displayactive-default-true]
This Boolean property indicates if the active state of a link should have a visual indication. Setting it to `false` removes the visual indication of an active link.
```xmlui-pg copy display name="Example: displayActive" height="250px"
<App layout="horizontal">
<NavPanel>
<NavLink to="/" label="Home" displayActive="false" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
</Pages>
</App>
```
### `enabled` (default: true) [#enabled-default-true]
This boolean property value indicates whether the component responds to user events (`true`) or not (`false`).
In the following app, the "Hotels" link is disabled:
```xmlui-pg copy {8} display name="Example: enabled" height="250px"
<App layout="horizontal">
<AppHeader>
<H1>MyTravel App</H1>
</AppHeader>
<NavPanel>
<NavLink label="Home" to="/" />
<NavLink label="Flights" to="/flights" />
<NavLink label="Hotels" to="/hotels" enabled="false" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
<Page url="/flights">Flights Page</Page>
<Page url="/hotels">Hotels Page</Page>
</Pages>
</App>
```
### `icon` [#icon]
This property allows you to add an optional icon (specify the icon's name) to the navigation link.
```xmlui-pg copy {6-7} display name="Example: icon" height="250px"
<App layout="horizontal">
<AppHeader>
<H1>MyApp</H1>
</AppHeader>
<NavPanel>
<NavLink label="Home" to="/" icon="home" />
<NavLink label="Drives" to="/drives" icon="drive" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
<Page url="/drives">Drives Page</Page>
</Pages>
</App>
```
### `label` [#label]
This property sets the label of the component. If not set, the component will not display a label.
```xmlui-pg copy display name="Example: label" height="250px"
<App layout="horizontal">
<NavPanel>
<NavLink to="/" label="Home" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
</Pages>
</App>
```
### `target` [#target]
This optionally property specifies how to open the clicked link.
Available values:
| Value | Description |
| --- | --- |
| `_self` | The link will open in the same frame as it was clicked. |
| `_blank` | The link will open in a new window or tab. |
| `_parent` | The link will open in the parent frame. If no parent, behaves as _self. |
| `_top` | The topmost browsing context. The link will open in the full body of the window. If no ancestors, behaves as _self. |
| `_unfencedTop` | Allows embedded fenced frames to navigate the top-level frame, i.e. traversing beyond the root of the fenced frame. |
The following example opens the "About XMLUI" link in a new tab:
```xmlui-pg copy {7} display name="Example: target" height="250px"
<App layout="horizontal">
<AppHeader>
<H1>MyApp</H1>
</AppHeader>
<NavPanel>
<NavLink label="Home" to="/" />
<NavLink label="About XMLUI" to="https://docs.xmlui.org/" target="_blank" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
<Page url="/drives">Drives Page</Page>
</Pages>
</App>
```
### `to` [#to]
This property defines the URL of the link.
### `vertical` [#vertical]
This property sets how the active status is displayed on the `NavLink` component. If set to true, the indicator is displayed on the side which lends itself to a vertically aligned navigation menu. By default, it displays a horizontal indicator.
Usually, you do not need to use this property.
However, if you create a custom navigation menu component that runs vertically,
you need to manually set this property for the active state to be displayed properly.
The default value for this property is `false`.
```xmlui-pg copy display name="Example: vertical" height="250px"
<App layout="horizontal">
<NavPanel>
<NavLink to="/" label="Home" vertical="true" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
</Pages>
</App>
```
## Events [#events]
### `click` [#click]
This event is triggered when the NavLink is clicked.
The following example shows a message and navigates to the "/status" link after closing the message window:
```xmlui-pg copy {7} display name="Example: click" height="250px"
<App layout="horizontal">
<AppHeader>
<H1>MyApp</H1>
</AppHeader>
<NavPanel>
<NavLink to="/" label="Home" />
<NavLink label="Check my status" onClick="
toast('You will be redirected');
Actions.navigate('/status');
" />
</NavPanel>
<Pages>
<Page url="/">Home</Page>
<Page url="/status">My Status</Page>
</Pages>
</App>
```
## Exposed Methods [#exposed-methods]
This component does not expose any methods.
## Styling [#styling]
### Theme Variables [#theme-variables]
| Variable | Default Value (Light) | Default Value (Dark) |
| --- | --- | --- |
| [backgroundColor](../styles-and-themes/common-units/#color)-NavLink | transparent | transparent |
| [backgroundColor](../styles-and-themes/common-units/#color)-NavLink--active | *none* | *none* |
| [backgroundColor](../styles-and-themes/common-units/#color)-NavLink--hover | *none* | *none* |
| [backgroundColor](../styles-and-themes/common-units/#color)-NavLink--hover--active | *none* | *none* |
| [backgroundColor](../styles-and-themes/common-units/#color)-NavLink--pressed | *none* | *none* |
| [backgroundColor](../styles-and-themes/common-units/#color)-NavLink--pressed--active | *none* | *none* |
| [border](../styles-and-themes/common-units/#border)-NavLink | 0px solid $borderColor | 0px solid $borderColor |
| [borderBottom](../styles-and-themes/common-units/#border)-NavLink | *none* | *none* |
| [borderBottomColor](../styles-and-themes/common-units/#color)-NavLink | *none* | *none* |
| [borderBottomStyle](../styles-and-themes/common-units/#border-style)-NavLink | *none* | *none* |
| [borderBottomWidth](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [borderColor](../styles-and-themes/common-units/#color)-NavLink | *none* | *none* |
| [borderEndEndRadius](../styles-and-themes/common-units/#border-rounding)-NavLink | *none* | *none* |
| [borderEndStartRadius](../styles-and-themes/common-units/#border-rounding)-NavLink | *none* | *none* |
| [borderHorizontal](../styles-and-themes/common-units/#border)-NavLink | *none* | *none* |
| [borderHorizontalColor](../styles-and-themes/common-units/#color)-NavLink | *none* | *none* |
| [borderHorizontalStyle](../styles-and-themes/common-units/#border-style)-NavLink | *none* | *none* |
| [borderHorizontalWidth](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [borderLeft](../styles-and-themes/common-units/#border)-NavLink | *none* | *none* |
| [color](../styles-and-themes/common-units/#color)-NavLink | *none* | *none* |
| [borderLeftStyle](../styles-and-themes/common-units/#border-style)-NavLink | *none* | *none* |
| [borderLeftWidth](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-indicator-NavLink | $borderRadius | $borderRadius |
| [borderRadius](../styles-and-themes/common-units/#border-rounding)-NavLink | $borderRadius | $borderRadius |
| [borderRight](../styles-and-themes/common-units/#border)-NavLink | *none* | *none* |
| [color](../styles-and-themes/common-units/#color)-NavLink | *none* | *none* |
| [borderRightStyle](../styles-and-themes/common-units/#border-style)-NavLink | *none* | *none* |
| [borderRightWidth](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [borderStartEndRadius](../styles-and-themes/common-units/#border-rounding)-NavLink | *none* | *none* |
| [borderStartStartRadius](../styles-and-themes/common-units/#border-rounding)-NavLink | *none* | *none* |
| [borderStyle](../styles-and-themes/common-units/#border-style)-NavLink | *none* | *none* |
| [borderTop](../styles-and-themes/common-units/#border)-NavLink | *none* | *none* |
| [borderTopColor](../styles-and-themes/common-units/#color)-NavLink | *none* | *none* |
| [borderTopStyle](../styles-and-themes/common-units/#border-style)-NavLink | *none* | *none* |
| [borderTopWidth](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [borderHorizontal](../styles-and-themes/common-units/#border)-NavLink | *none* | *none* |
| [borderVerticalColor](../styles-and-themes/common-units/#color)-NavLink | *none* | *none* |
| [borderVerticalStyle](../styles-and-themes/common-units/#border-style)-NavLink | *none* | *none* |
| [borderVerticalWidth](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [borderWidth](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [color](../styles-and-themes/common-units/#color)-icon-NavLink | $color-surface-500 | $color-surface-500 |
| [color](../styles-and-themes/common-units/#color)-indicator-NavLink | *none* | *none* |
| [color](../styles-and-themes/common-units/#color)-indicator-NavLink--active | $color-primary-500 | $color-primary-500 |
| [color](../styles-and-themes/common-units/#color)-indicator-NavLink--hover | $color-primary-600 | $color-primary-600 |
| [color](../styles-and-themes/common-units/#color)-indicator-NavLink--pressed | $color-primary-500 | $color-primary-500 |
| [fontFamily](../styles-and-themes/common-units/#fontFamily)-NavLink | $fontFamily | $fontFamily |
| [fontSize](../styles-and-themes/common-units/#size)-NavLink | $fontSize-sm | $fontSize-sm |
| [fontWeight](../styles-and-themes/common-units/#fontWeight)-NavLink | $fontWeight-normal | $fontWeight-normal |
| [fontWeight](../styles-and-themes/common-units/#fontWeight)-NavLink--active | *none* | *none* |
| [fontWeight](../styles-and-themes/common-units/#fontWeight)-NavLink--pressed | $fontWeight-normal | $fontWeight-normal |
| [outlineColor](../styles-and-themes/common-units/#color)-NavLink--focus | $outlineColor--focus | $outlineColor--focus |
| [outlineOffset](../styles-and-themes/common-units/#size)-NavLink--focus | -1px | -1px |
| [outlineStyle](../styles-and-themes/common-units/#border)-NavLink--focus | $outlineStyle--focus | $outlineStyle--focus |
| [outlineWidth](../styles-and-themes/common-units/#size)-NavLink--focus | $outlineWidth--focus | $outlineWidth--focus |
| [padding](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [paddingBottom](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [paddingHorizontal](../styles-and-themes/common-units/#size)-NavLink | $space-4 | $space-4 |
| [paddingLeft](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [paddingRight](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [paddingTop](../styles-and-themes/common-units/#size)-NavLink | *none* | *none* |
| [paddingVertical](../styles-and-themes/common-units/#size)-NavLink | $space-2 | $space-2 |
| [textColor](../styles-and-themes/common-units/#color)-NavLink | $textColor-primary | $textColor-primary |
| [textColor](../styles-and-themes/common-units/#color)-NavLink--active | *none* | *none* |
| [textColor](../styles-and-themes/common-units/#color)-NavLink--hover | *none* | *none* |
| [textColor](../styles-and-themes/common-units/#color)-NavLink--hover--active | *none* | *none* |
| [textColor](../styles-and-themes/common-units/#color)-NavLink--pressed | *none* | *none* |
| [textColor](../styles-and-themes/common-units/#color)-NavLink--pressed--active | *none* | *none* |
| [thickness](../styles-and-themes/common-units/#size)-indicator-NavLink | $space-0_5 | $space-0_5 |
| [wordWrap](../styles-and-themes/common-units/#word-wrap)-NavLink | *none* | *none* |
### Variable Explanations [#variable-explanations]
| Theme Variable | Description |
| --- | --- |
| **`color-indicator-NavLink`** | Provides the following states: `--hover`, `--active`, `--pressed` |
```
--------------------------------------------------------------------------------
/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("");
}
```