This is page 127 of 181. Use http://codebase.md/xmlui-org/xmlui/xmlui-hello-world.js?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ └── config.json ├── .eslintrc.cjs ├── .github │ ├── build-checklist.png │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── deploy-blog.yml │ ├── deploy-docs-optimized.yml │ ├── deploy-docs.yml │ ├── prepare-versions.yml │ ├── release-packages.yml │ ├── run-all-tests.yml │ └── run-smoke-tests.yml ├── .gitignore ├── .prettierrc.js ├── .vscode │ ├── launch.json │ └── settings.json ├── blog │ ├── .gitignore │ ├── .gitkeep │ ├── CHANGELOG.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── layout-changes.md │ ├── package.json │ ├── public │ │ ├── blog │ │ │ ├── images │ │ │ │ ├── blog-page-component.png │ │ │ │ ├── blog-scrabble.png │ │ │ │ ├── integrated-blog-search.png │ │ │ │ └── lorem-ipsum.png │ │ │ ├── lorem-ipsum.md │ │ │ ├── newest-post.md │ │ │ ├── older-post.md │ │ │ └── welcome-to-the-xmlui-blog.md │ │ ├── mockServiceWorker.js │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ └── for-download │ │ │ │ └── xmlui │ │ │ │ └── xmlui-standalone.umd.js │ │ │ ├── github.svg │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ ├── rss.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ └── PageNotFound.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ └── blog-theme.ts │ └── tsconfig.json ├── CONTRIBUTING.md ├── docs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── ComponentRefLinks.txt │ ├── content │ │ ├── _meta.json │ │ ├── components │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── APICall.md │ │ │ ├── App.md │ │ │ ├── AppHeader.md │ │ │ ├── AppState.md │ │ │ ├── AutoComplete.md │ │ │ ├── Avatar.md │ │ │ ├── Backdrop.md │ │ │ ├── Badge.md │ │ │ ├── BarChart.md │ │ │ ├── Bookmark.md │ │ │ ├── Breakout.md │ │ │ ├── Button.md │ │ │ ├── Card.md │ │ │ ├── Carousel.md │ │ │ ├── ChangeListener.md │ │ │ ├── Checkbox.md │ │ │ ├── CHStack.md │ │ │ ├── ColorPicker.md │ │ │ ├── Column.md │ │ │ ├── ContentSeparator.md │ │ │ ├── CVStack.md │ │ │ ├── DataSource.md │ │ │ ├── DateInput.md │ │ │ ├── DatePicker.md │ │ │ ├── DonutChart.md │ │ │ ├── DropdownMenu.md │ │ │ ├── EmojiSelector.md │ │ │ ├── ExpandableItem.md │ │ │ ├── FileInput.md │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FlowLayout.md │ │ │ ├── Footer.md │ │ │ ├── Form.md │ │ │ ├── FormItem.md │ │ │ ├── FormSection.md │ │ │ ├── Fragment.md │ │ │ ├── H1.md │ │ │ ├── H2.md │ │ │ ├── H3.md │ │ │ ├── H4.md │ │ │ ├── H5.md │ │ │ ├── H6.md │ │ │ ├── Heading.md │ │ │ ├── HSplitter.md │ │ │ ├── HStack.md │ │ │ ├── Icon.md │ │ │ ├── IFrame.md │ │ │ ├── Image.md │ │ │ ├── Items.md │ │ │ ├── LabelList.md │ │ │ ├── Legend.md │ │ │ ├── LineChart.md │ │ │ ├── Link.md │ │ │ ├── List.md │ │ │ ├── Logo.md │ │ │ ├── Markdown.md │ │ │ ├── MenuItem.md │ │ │ ├── MenuSeparator.md │ │ │ ├── ModalDialog.md │ │ │ ├── NavGroup.md │ │ │ ├── NavLink.md │ │ │ ├── NavPanel.md │ │ │ ├── NoResult.md │ │ │ ├── NumberBox.md │ │ │ ├── Option.md │ │ │ ├── Page.md │ │ │ ├── PageMetaTitle.md │ │ │ ├── Pages.md │ │ │ ├── Pagination.md │ │ │ ├── PasswordInput.md │ │ │ ├── PieChart.md │ │ │ ├── ProgressBar.md │ │ │ ├── Queue.md │ │ │ ├── RadioGroup.md │ │ │ ├── RealTimeAdapter.md │ │ │ ├── Redirect.md │ │ │ ├── Select.md │ │ │ ├── Slider.md │ │ │ ├── Slot.md │ │ │ ├── SpaceFiller.md │ │ │ ├── Spinner.md │ │ │ ├── Splitter.md │ │ │ ├── Stack.md │ │ │ ├── StickyBox.md │ │ │ ├── SubMenuItem.md │ │ │ ├── Switch.md │ │ │ ├── TabItem.md │ │ │ ├── Table.md │ │ │ ├── TableOfContents.md │ │ │ ├── Tabs.md │ │ │ ├── Text.md │ │ │ ├── TextArea.md │ │ │ ├── TextBox.md │ │ │ ├── Theme.md │ │ │ ├── TimeInput.md │ │ │ ├── Timer.md │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneSwitch.md │ │ │ ├── Tooltip.md │ │ │ ├── Tree.md │ │ │ ├── VSplitter.md │ │ │ ├── VStack.md │ │ │ ├── xmlui-animations │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── Animation.md │ │ │ │ ├── FadeAnimation.md │ │ │ │ ├── FadeInAnimation.md │ │ │ │ ├── FadeOutAnimation.md │ │ │ │ ├── ScaleAnimation.md │ │ │ │ └── SlideInAnimation.md │ │ │ ├── xmlui-pdf │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Pdf.md │ │ │ ├── xmlui-spreadsheet │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Spreadsheet.md │ │ │ └── xmlui-website-blocks │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── Carousel.md │ │ │ ├── HelloMd.md │ │ │ ├── HeroSection.md │ │ │ └── ScrollToTop.md │ │ └── extensions │ │ ├── _meta.json │ │ ├── xmlui-animations │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── Animation.md │ │ │ ├── FadeAnimation.md │ │ │ ├── FadeInAnimation.md │ │ │ ├── FadeOutAnimation.md │ │ │ ├── ScaleAnimation.md │ │ │ └── SlideInAnimation.md │ │ └── xmlui-website-blocks │ │ ├── _meta.json │ │ ├── _overview.md │ │ ├── Carousel.md │ │ ├── HelloMd.md │ │ ├── HeroSection.md │ │ └── ScrollToTop.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── public │ │ ├── feed.rss │ │ ├── mockServiceWorker.js │ │ ├── pages │ │ │ ├── _meta.json │ │ │ ├── app-structure.md │ │ │ ├── build-editor-component.md │ │ │ ├── build-hello-world-component.md │ │ │ ├── components-intro.md │ │ │ ├── context-variables.md │ │ │ ├── forms.md │ │ │ ├── globals.md │ │ │ ├── glossary.md │ │ │ ├── helper-tags.md │ │ │ ├── hosted-deployment.md │ │ │ ├── howto │ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md │ │ │ │ ├── chain-a-refetch.md │ │ │ │ ├── debug-a-component.md │ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md │ │ │ │ ├── delegate-a-method.md │ │ │ │ ├── do-custom-form-validation.md │ │ │ │ ├── expose-a-method-from-a-component.md │ │ │ │ ├── filter-and-transform-data-from-an-api.md │ │ │ │ ├── group-items-in-list-by-a-property.md │ │ │ │ ├── handle-background-operations.md │ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md │ │ │ │ ├── make-a-set-of-equal-width-cards.md │ │ │ │ ├── make-a-table-responsive.md │ │ │ │ ├── make-navpanel-width-responsive.md │ │ │ │ ├── modify-a-value-reported-in-a-column.md │ │ │ │ ├── paginate-a-list.md │ │ │ │ ├── pass-data-to-a-modal-dialog.md │ │ │ │ ├── react-to-button-click-not-keystrokes.md │ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md │ │ │ │ ├── share-a-modaldialog-across-components.md │ │ │ │ ├── sync-selections-between-table-and-list-views.md │ │ │ │ ├── update-ui-optimistically.md │ │ │ │ ├── use-built-in-form-validation.md │ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md │ │ │ ├── howto.md │ │ │ ├── intro.md │ │ │ ├── layout.md │ │ │ ├── markup.md │ │ │ ├── mcp.md │ │ │ ├── modal-dialogs.md │ │ │ ├── news-and-reviews.md │ │ │ ├── reactive-intro.md │ │ │ ├── refactoring.md │ │ │ ├── routing-and-links.md │ │ │ ├── samples │ │ │ │ ├── color-palette.xmlui │ │ │ │ ├── color-values.xmlui │ │ │ │ ├── shadow-sizes.xmlui │ │ │ │ ├── spacing-sizes.xmlui │ │ │ │ ├── swatch.xmlui │ │ │ │ ├── theme-gallery-brief.xmlui │ │ │ │ └── theme-gallery.xmlui │ │ │ ├── scoping.md │ │ │ ├── scripting.md │ │ │ ├── styles-and-themes │ │ │ │ ├── common-units.md │ │ │ │ ├── layout-props.md │ │ │ │ ├── theme-variable-defaults.md │ │ │ │ ├── theme-variables.md │ │ │ │ └── themes.md │ │ │ ├── template-properties.md │ │ │ ├── test.md │ │ │ ├── tutorial-01.md │ │ │ ├── tutorial-02.md │ │ │ ├── tutorial-03.md │ │ │ ├── tutorial-04.md │ │ │ ├── tutorial-05.md │ │ │ ├── tutorial-06.md │ │ │ ├── tutorial-07.md │ │ │ ├── tutorial-08.md │ │ │ ├── tutorial-09.md │ │ │ ├── tutorial-10.md │ │ │ ├── tutorial-11.md │ │ │ ├── tutorial-12.md │ │ │ ├── universal-properties.md │ │ │ ├── user-defined-components.md │ │ │ ├── vscode.md │ │ │ ├── working-with-markdown.md │ │ │ ├── working-with-text.md │ │ │ ├── xmlui-animations │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── Animation.md │ │ │ │ ├── FadeAnimation.md │ │ │ │ ├── FadeInAnimation.md │ │ │ │ ├── FadeOutAnimation.md │ │ │ │ ├── ScaleAnimation.md │ │ │ │ └── SlideInAnimation.md │ │ │ ├── xmlui-charts │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── BarChart.md │ │ │ │ ├── DonutChart.md │ │ │ │ ├── LabelList.md │ │ │ │ ├── Legend.md │ │ │ │ ├── LineChart.md │ │ │ │ └── PieChart.md │ │ │ ├── xmlui-pdf │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Pdf.md │ │ │ └── xmlui-spreadsheet │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ └── Spreadsheet.md │ │ ├── resources │ │ │ ├── devdocs │ │ │ │ ├── debug-proxy-object-2.png │ │ │ │ ├── debug-proxy-object.png │ │ │ │ ├── table_editor_01.png │ │ │ │ ├── table_editor_02.png │ │ │ │ ├── table_editor_03.png │ │ │ │ ├── table_editor_04.png │ │ │ │ ├── table_editor_05.png │ │ │ │ ├── table_editor_06.png │ │ │ │ ├── table_editor_07.png │ │ │ │ ├── table_editor_08.png │ │ │ │ ├── table_editor_09.png │ │ │ │ ├── table_editor_10.png │ │ │ │ ├── table_editor_11.png │ │ │ │ ├── table-editor-01.png │ │ │ │ ├── table-editor-02.png │ │ │ │ ├── table-editor-03.png │ │ │ │ ├── table-editor-04.png │ │ │ │ ├── table-editor-06.png │ │ │ │ ├── table-editor-07.png │ │ │ │ ├── table-editor-08.png │ │ │ │ ├── table-editor-09.png │ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ ├── clients.json │ │ │ │ ├── daily-revenue.json │ │ │ │ ├── dashboard-stats.json │ │ │ │ ├── demo.xmlui │ │ │ │ ├── demo.xmlui.xs │ │ │ │ ├── downloads │ │ │ │ │ └── downloads.json │ │ │ │ ├── for-download │ │ │ │ │ ├── index-with-api.html │ │ │ │ │ ├── index.html │ │ │ │ │ ├── mockApi.js │ │ │ │ │ ├── start-darwin.sh │ │ │ │ │ ├── start-linux.sh │ │ │ │ │ ├── start.bat │ │ │ │ │ └── xmlui │ │ │ │ │ └── xmlui-standalone.umd.js │ │ │ │ ├── getting-started │ │ │ │ │ ├── cl-tutorial-final.zip │ │ │ │ │ ├── cl-tutorial.zip │ │ │ │ │ ├── cl-tutorial2.zip │ │ │ │ │ ├── cl-tutorial3.zip │ │ │ │ │ ├── cl-tutorial4.zip │ │ │ │ │ ├── cl-tutorial5.zip │ │ │ │ │ ├── cl-tutorial6.zip │ │ │ │ │ ├── getting-started.zip │ │ │ │ │ ├── hello-xmlui.zip │ │ │ │ │ ├── xmlui-empty.zip │ │ │ │ │ └── xmlui-starter.zip │ │ │ │ ├── howto │ │ │ │ │ └── component-icons │ │ │ │ │ └── up-arrow.svg │ │ │ │ ├── invoices.json │ │ │ │ ├── monthly-status.json │ │ │ │ ├── news-and-reviews.json │ │ │ │ ├── products.json │ │ │ │ ├── releases.json │ │ │ │ ├── tutorials │ │ │ │ │ ├── datasource │ │ │ │ │ │ └── api.ts │ │ │ │ │ └── p2do │ │ │ │ │ ├── api.ts │ │ │ │ │ └── todo-logo.svg │ │ │ │ └── xmlui.json │ │ │ ├── github.svg │ │ │ ├── images │ │ │ │ ├── apiaction-tutorial │ │ │ │ │ ├── add-success.png │ │ │ │ │ ├── apiaction-param.png │ │ │ │ │ ├── change-completed.png │ │ │ │ │ ├── change-in-progress.png │ │ │ │ │ ├── confirm-delete.png │ │ │ │ │ ├── data-error.png │ │ │ │ │ ├── data-progress.png │ │ │ │ │ ├── data-success.png │ │ │ │ │ ├── display-1.png │ │ │ │ │ ├── item-deleted.png │ │ │ │ │ ├── item-updated.png │ │ │ │ │ ├── missing-api-key.png │ │ │ │ │ ├── new-item-added.png │ │ │ │ │ └── test-message.png │ │ │ │ ├── chat-api │ │ │ │ │ └── domain-model.svg │ │ │ │ ├── components │ │ │ │ │ ├── image │ │ │ │ │ │ └── breakfast.jpg │ │ │ │ │ ├── markdown │ │ │ │ │ │ └── colors.png │ │ │ │ │ └── modal │ │ │ │ │ ├── deep_link_dialog_1.jpg │ │ │ │ │ └── deep_link_dialog_2.jpg │ │ │ │ ├── create-apps │ │ │ │ │ ├── collapsed-vertical.png │ │ │ │ │ ├── using-forms-warning-dialog.png │ │ │ │ │ └── using-forms.png │ │ │ │ ├── datasource-tutorial │ │ │ │ │ ├── data-with-header.png │ │ │ │ │ ├── filtered-data.png │ │ │ │ │ ├── filtered-items.png │ │ │ │ │ ├── initial-page-items.png │ │ │ │ │ ├── list-items.png │ │ │ │ │ ├── next-page-items.png │ │ │ │ │ ├── no-data.png │ │ │ │ │ ├── pagination-1.jpg │ │ │ │ │ ├── pagination-1.png │ │ │ │ │ ├── polling-1.png │ │ │ │ │ ├── refetch-data.png │ │ │ │ │ ├── slow-loading.png │ │ │ │ │ ├── test-message.png │ │ │ │ │ ├── Thumbs.db │ │ │ │ │ ├── unconventional-data.png │ │ │ │ │ └── unfiltered-items.png │ │ │ │ ├── flower.jpg │ │ │ │ ├── get-started │ │ │ │ │ ├── add-new-contact.png │ │ │ │ │ ├── app-modified.png │ │ │ │ │ ├── app-start.png │ │ │ │ │ ├── app-with-boxes.png │ │ │ │ │ ├── app-with-toast.png │ │ │ │ │ ├── boilerplate-structure.png │ │ │ │ │ ├── cl-initial.png │ │ │ │ │ ├── cl-start.png │ │ │ │ │ ├── contact-counts.png │ │ │ │ │ ├── contact-dialog-title.png │ │ │ │ │ ├── contact-dialog.png │ │ │ │ │ ├── contact-menus.png │ │ │ │ │ ├── contact-predicates.png │ │ │ │ │ ├── context-menu.png │ │ │ │ │ ├── dashboard-numbers.png │ │ │ │ │ ├── default-contact-list.png │ │ │ │ │ ├── delete-contact.png │ │ │ │ │ ├── delete-task.png │ │ │ │ │ ├── detailed-template.png │ │ │ │ │ ├── edit-contact-details.png │ │ │ │ │ ├── edited-contact-saved.png │ │ │ │ │ ├── empty-sections.png │ │ │ │ │ ├── filter-completed.png │ │ │ │ │ ├── fullwidth-desktop.png │ │ │ │ │ ├── fullwidth-mobile.png │ │ │ │ │ ├── initial-table.png │ │ │ │ │ ├── items-and-badges.png │ │ │ │ │ ├── loading-message.png │ │ │ │ │ ├── new-contact-button.png │ │ │ │ │ ├── new-contact-saved.png │ │ │ │ │ ├── no-empty-sections.png │ │ │ │ │ ├── personal-todo-initial.png │ │ │ │ │ ├── piechart.png │ │ │ │ │ ├── review-today.png │ │ │ │ │ ├── rudimentary-dashboard.png │ │ │ │ │ ├── section-collapsed.png │ │ │ │ │ ├── sectioned-items.png │ │ │ │ │ ├── sections-ordered.png │ │ │ │ │ ├── spacex-list-with-links.png │ │ │ │ │ ├── spacex-list.png │ │ │ │ │ ├── start-personal-todo-1.png │ │ │ │ │ ├── submit-new-contact.png │ │ │ │ │ ├── submit-new-task.png │ │ │ │ │ ├── syntax-highlighting.png │ │ │ │ │ ├── table-with-badge.png │ │ │ │ │ ├── template-with-card.png │ │ │ │ │ ├── test-emulated-api.png │ │ │ │ │ ├── Thumbs.db │ │ │ │ │ ├── todo-logo.png │ │ │ │ │ └── xmlui-tools.png │ │ │ │ ├── HelloApp.png │ │ │ │ ├── HelloApp2.png │ │ │ │ ├── logos │ │ │ │ │ ├── xmlui1.svg │ │ │ │ │ ├── xmlui2.svg │ │ │ │ │ ├── xmlui3.svg │ │ │ │ │ ├── xmlui4.svg │ │ │ │ │ ├── xmlui5.svg │ │ │ │ │ ├── xmlui6.svg │ │ │ │ │ └── xmlui7.svg │ │ │ │ ├── pdf │ │ │ │ │ └── dummy-pdf.jpg │ │ │ │ ├── rendering-engine │ │ │ │ │ ├── AppEngine-flow.svg │ │ │ │ │ ├── Component.svg │ │ │ │ │ ├── CompoundComponent.svg │ │ │ │ │ ├── RootComponent.svg │ │ │ │ │ └── tree-with-containers.svg │ │ │ │ ├── reviewers-guide │ │ │ │ │ ├── AppEngine-flow.svg │ │ │ │ │ └── incbutton-in-action.png │ │ │ │ ├── tools │ │ │ │ │ └── boilerplate-structure.png │ │ │ │ ├── try.svg │ │ │ │ ├── tutorial │ │ │ │ │ ├── app-chat-history.png │ │ │ │ │ ├── app-content-placeholder.png │ │ │ │ │ ├── app-header-and-content.png │ │ │ │ │ ├── app-links-channel-selected.png │ │ │ │ │ ├── app-links-click.png │ │ │ │ │ ├── app-navigation.png │ │ │ │ │ ├── finished-ex01.png │ │ │ │ │ ├── finished-ex02.png │ │ │ │ │ ├── hello.png │ │ │ │ │ ├── splash-screen-advanced.png │ │ │ │ │ ├── splash-screen-after-click.png │ │ │ │ │ ├── splash-screen-centered.png │ │ │ │ │ ├── splash-screen-events.png │ │ │ │ │ ├── splash-screen-expression.png │ │ │ │ │ ├── splash-screen-reuse-after.png │ │ │ │ │ ├── splash-screen-reuse-before.png │ │ │ │ │ └── splash-screen.png │ │ │ │ └── tutorial-01.png │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ ├── Boxes.xmlui │ │ │ ├── Breadcrumb.xmlui │ │ │ ├── ChangeLog.xmlui │ │ │ ├── ColorPalette.xmlui │ │ │ ├── DocumentLinks.xmlui │ │ │ ├── DocumentPage.xmlui │ │ │ ├── DocumentPageNoTOC.xmlui │ │ │ ├── Icons.xmlui │ │ │ ├── IncButton.xmlui │ │ │ ├── IncButton2.xmlui │ │ │ ├── NameValue.xmlui │ │ │ ├── PageNotFound.xmlui │ │ │ ├── PaletteItem.xmlui │ │ │ ├── Palettes.xmlui │ │ │ ├── SectionHeader.xmlui │ │ │ ├── TBD.xmlui │ │ │ ├── Test.xmlui │ │ │ ├── ThemesIntro.xmlui │ │ │ ├── ThousandThemes.xmlui │ │ │ ├── TubeStops.xmlui │ │ │ ├── TubeStops.xmlui.xs │ │ │ └── TwoColumnCode.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ ├── docs-theme.ts │ │ ├── earthtone.ts │ │ ├── xmlui-gray-on-default.ts │ │ ├── xmlui-green-on-default.ts │ │ └── xmlui-orange-on-default.ts │ └── tsconfig.json ├── LICENSE ├── package-lock.json ├── package.json ├── packages │ ├── xmlui-animations │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── Animation.tsx │ │ │ ├── AnimationNative.tsx │ │ │ ├── FadeAnimation.tsx │ │ │ ├── FadeInAnimation.tsx │ │ │ ├── FadeOutAnimation.tsx │ │ │ ├── index.tsx │ │ │ ├── ScaleAnimation.tsx │ │ │ └── SlideInAnimation.tsx │ │ └── tsconfig.json │ ├── xmlui-devtools │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── devtools │ │ │ │ ├── DevTools.tsx │ │ │ │ ├── DevToolsNative.module.scss │ │ │ │ ├── DevToolsNative.tsx │ │ │ │ ├── ModalDialog.module.scss │ │ │ │ ├── ModalDialog.tsx │ │ │ │ ├── ModalVisibilityContext.tsx │ │ │ │ ├── Tooltip.module.scss │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── utils.ts │ │ │ ├── editor │ │ │ │ └── Editor.tsx │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── vite.config-overrides.ts │ ├── xmlui-hello-world │ │ ├── .gitignore │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── HelloWorld.module.scss │ │ │ ├── HelloWorld.tsx │ │ │ ├── HelloWorldNative.tsx │ │ │ └── index.tsx │ │ └── tsconfig.json │ ├── xmlui-os-frames │ │ ├── .gitignore │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── IPhoneFrame.module.scss │ │ │ ├── IPhoneFrame.tsx │ │ │ ├── MacOSAppFrame.module.scss │ │ │ ├── MacOSAppFrame.tsx │ │ │ ├── WindowsAppFrame.module.scss │ │ │ └── WindowsAppFrame.tsx │ │ └── tsconfig.json │ ├── xmlui-pdf │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ ├── components │ │ │ │ └── Pdf.xmlui │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── LazyPdfNative.tsx │ │ │ ├── Pdf.module.scss │ │ │ └── Pdf.tsx │ │ └── tsconfig.json │ ├── xmlui-playground │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── usePlayground.ts │ │ │ │ └── useToast.ts │ │ │ ├── index.tsx │ │ │ ├── playground │ │ │ │ ├── Box.module.scss │ │ │ │ ├── Box.tsx │ │ │ │ ├── CodeSelector.tsx │ │ │ │ ├── ConfirmationDialog.module.scss │ │ │ │ ├── ConfirmationDialog.tsx │ │ │ │ ├── Editor.tsx │ │ │ │ ├── Header.module.scss │ │ │ │ ├── Header.tsx │ │ │ │ ├── Playground.tsx │ │ │ │ ├── PlaygroundContent.module.scss │ │ │ │ ├── PlaygroundContent.tsx │ │ │ │ ├── PlaygroundNative.module.scss │ │ │ │ ├── PlaygroundNative.tsx │ │ │ │ ├── Preview.module.scss │ │ │ │ ├── Preview.tsx │ │ │ │ ├── Select.module.scss │ │ │ │ ├── StandalonePlayground.tsx │ │ │ │ ├── StandalonePlaygroundNative.module.scss │ │ │ │ ├── StandalonePlaygroundNative.tsx │ │ │ │ ├── ThemeSwitcher.module.scss │ │ │ │ ├── ThemeSwitcher.tsx │ │ │ │ ├── ToneSwitcher.tsx │ │ │ │ ├── Tooltip.module.scss │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── utils.ts │ │ │ ├── providers │ │ │ │ ├── Toast.module.scss │ │ │ │ └── ToastProvider.tsx │ │ │ ├── state │ │ │ │ └── store.ts │ │ │ ├── themes │ │ │ │ └── theme.ts │ │ │ └── utils │ │ │ └── helpers.ts │ │ └── tsconfig.json │ ├── xmlui-search │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── Search.module.scss │ │ │ └── Search.tsx │ │ └── tsconfig.json │ ├── xmlui-spreadsheet │ │ ├── .gitignore │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── Spreadsheet.tsx │ │ │ └── SpreadsheetNative.tsx │ │ └── tsconfig.json │ └── xmlui-website-blocks │ ├── .gitignore │ ├── CHANGELOG.md │ ├── demo │ │ ├── components │ │ │ ├── HeroBackgroundBreakoutPage.xmlui │ │ │ ├── HeroBackgroundsPage.xmlui │ │ │ ├── HeroContentsPage.xmlui │ │ │ ├── HeroTextAlignPage.xmlui │ │ │ ├── HeroTextPage.xmlui │ │ │ └── HeroTonesPage.xmlui │ │ ├── Main.xmlui │ │ └── themes │ │ └── default.ts │ ├── index.html │ ├── index.ts │ ├── meta │ │ └── componentsMetadata.ts │ ├── package.json │ ├── public │ │ └── resources │ │ ├── building.jpg │ │ └── xmlui-logo.svg │ ├── src │ │ ├── Carousel │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ └── CarouselNative.tsx │ │ ├── FancyButton │ │ │ ├── FancyButton.module.scss │ │ │ ├── FancyButton.tsx │ │ │ └── FancyButton.xmlui │ │ ├── Hello │ │ │ ├── Hello.tsx │ │ │ ├── Hello.xmlui │ │ │ └── Hello.xmlui.xs │ │ ├── HeroSection │ │ │ ├── HeroSection.module.scss │ │ │ ├── HeroSection.tsx │ │ │ └── HeroSectionNative.tsx │ │ ├── index.tsx │ │ ├── ScrollToTop │ │ │ ├── ScrollToTop.module.scss │ │ │ ├── ScrollToTop.tsx │ │ │ └── ScrollToTopNative.tsx │ │ └── vite-env.d.ts │ └── tsconfig.json ├── README.md ├── tools │ ├── codefence │ │ └── xmlui-code-fence-docs.md │ ├── create-app │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── create-app.ts │ │ ├── helpers │ │ │ ├── copy.ts │ │ │ ├── get-pkg-manager.ts │ │ │ ├── git.ts │ │ │ ├── install.ts │ │ │ ├── is-folder-empty.ts │ │ │ ├── is-writeable.ts │ │ │ ├── make-dir.ts │ │ │ └── validate-pkg.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── templates │ │ │ ├── default │ │ │ │ └── ts │ │ │ │ ├── gitignore │ │ │ │ ├── index.html │ │ │ │ ├── index.ts │ │ │ │ ├── public │ │ │ │ │ ├── mockServiceWorker.js │ │ │ │ │ ├── resources │ │ │ │ │ │ ├── favicon.ico │ │ │ │ │ │ └── xmlui-logo.svg │ │ │ │ │ └── serve.json │ │ │ │ └── src │ │ │ │ ├── components │ │ │ │ │ ├── ApiAware.xmlui │ │ │ │ │ ├── Home.xmlui │ │ │ │ │ ├── IncButton.xmlui │ │ │ │ │ └── PagePanel.xmlui │ │ │ │ ├── config.ts │ │ │ │ └── Main.xmlui │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── create-xmlui-hello-world │ │ ├── index.js │ │ └── package.json │ └── vscode │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── .vscodeignore │ ├── build.sh │ ├── CHANGELOG.md │ ├── esbuild.js │ ├── eslint.config.mjs │ ├── formatter-docs.md │ ├── generate-test-sample.sh │ ├── LICENSE.md │ ├── package-lock.json │ ├── package.json │ ├── README.md │ ├── resources │ │ ├── xmlui-logo.png │ │ └── xmlui-markup-syntax-highlighting.png │ ├── src │ │ ├── extension.ts │ │ └── server.ts │ ├── syntaxes │ │ └── xmlui.tmLanguage.json │ ├── test-samples │ │ └── sample.xmlui │ ├── tsconfig.json │ └── tsconfig.tsbuildinfo ├── turbo.json └── xmlui ├── .gitignore ├── bin │ ├── bootstrap.js │ ├── build-lib.ts │ ├── build.ts │ ├── index.ts │ ├── preview.ts │ ├── start.ts │ ├── vite-xmlui-plugin.ts │ └── viteConfig.ts ├── CHANGELOG.md ├── conventions │ ├── component-qa-checklist.md │ ├── copilot-conventions.md │ ├── create-xmlui-components.md │ ├── mermaid.md │ ├── testing-conventions.md │ └── xmlui-in-a-nutshell.md ├── dev-docs │ ├── accessibility.md │ ├── build-system.md │ ├── build-xmlui.md │ ├── component-behaviors.md │ ├── components-with-options.md │ ├── containers.md │ ├── data-operations.md │ ├── glossary.md │ ├── index.md │ ├── next │ │ ├── component-dev-guide.md │ │ ├── configuration-management-enhancement-summary.md │ │ ├── documentation-scripts-refactoring-complete-summary.md │ │ ├── documentation-scripts-refactoring-plan.md │ │ ├── duplicate-pattern-extraction-summary.md │ │ ├── error-handling-standardization-summary.md │ │ ├── generating-component-reference.md │ │ ├── index.md │ │ ├── logging-consistency-implementation-summary.md │ │ ├── project-build.md │ │ ├── project-structure.md │ │ ├── theme-context.md │ │ ├── tiptap-design-considerations.md │ │ ├── working-with-code.md │ │ ├── xmlui-runtime-architecture │ │ └── xmlui-wcag-accessibility-report.md │ ├── react-fundamentals.md │ ├── release-method.md │ ├── standalone-app.md │ ├── ud-components.md │ └── xmlui-repo.md ├── package.json ├── playwright.config.ts ├── scripts │ ├── coverage-only.js │ ├── e2e-test-summary.js │ ├── generate-docs │ │ ├── build-downloads-map.mjs │ │ ├── build-pages-map.mjs │ │ ├── components-config.json │ │ ├── configuration-management.mjs │ │ ├── constants.mjs │ │ ├── create-theme-files.mjs │ │ ├── DocsGenerator.mjs │ │ ├── error-handling.mjs │ │ ├── extensions-config.json │ │ ├── folders.mjs │ │ ├── generate-summary-files.mjs │ │ ├── get-docs.mjs │ │ ├── input-handler.mjs │ │ ├── logger.mjs │ │ ├── logging-standards.mjs │ │ ├── MetadataProcessor.mjs │ │ ├── pattern-utilities.mjs │ │ └── utils.mjs │ ├── get-langserver-metadata.mjs │ ├── inline-links.mjs │ └── README-e2e-summary.md ├── src │ ├── abstractions │ │ ├── _conventions.md │ │ ├── ActionDefs.ts │ │ ├── AppContextDefs.ts │ │ ├── ComponentDefs.ts │ │ ├── ContainerDefs.ts │ │ ├── ExtensionDefs.ts │ │ ├── FunctionDefs.ts │ │ ├── RendererDefs.ts │ │ ├── scripting │ │ │ ├── BlockScope.ts │ │ │ ├── Compilation.ts │ │ │ ├── LogicalThread.ts │ │ │ ├── LoopScope.ts │ │ │ ├── modules.ts │ │ │ ├── ScriptParserError.ts │ │ │ ├── Token.ts │ │ │ ├── TryScope.ts │ │ │ └── TryScopeExp.ts │ │ └── ThemingDefs.ts │ ├── components │ │ ├── _conventions.md │ │ ├── abstractions.ts │ │ ├── Accordion │ │ │ ├── Accordion.md │ │ │ ├── Accordion.module.scss │ │ │ ├── Accordion.spec.ts │ │ │ ├── Accordion.tsx │ │ │ ├── AccordionContext.tsx │ │ │ ├── AccordionItem.tsx │ │ │ ├── AccordionItemNative.tsx │ │ │ └── AccordionNative.tsx │ │ ├── Animation │ │ │ └── AnimationNative.tsx │ │ ├── APICall │ │ │ ├── APICall.md │ │ │ ├── APICall.spec.ts │ │ │ ├── APICall.tsx │ │ │ └── APICallNative.tsx │ │ ├── App │ │ │ ├── App.md │ │ │ ├── App.module.scss │ │ │ ├── App.spec.ts │ │ │ ├── App.tsx │ │ │ ├── AppLayoutContext.ts │ │ │ ├── AppNative.tsx │ │ │ ├── AppStateContext.ts │ │ │ ├── doc-resources │ │ │ │ ├── condensed-sticky.xmlui │ │ │ │ ├── condensed.xmlui │ │ │ │ ├── horizontal-sticky.xmlui │ │ │ │ ├── horizontal.xmlui │ │ │ │ ├── vertical-full-header.xmlui │ │ │ │ ├── vertical-sticky.xmlui │ │ │ │ └── vertical.xmlui │ │ │ ├── IndexerContext.ts │ │ │ ├── LinkInfoContext.ts │ │ │ ├── SearchContext.tsx │ │ │ ├── Sheet.module.scss │ │ │ └── Sheet.tsx │ │ ├── AppHeader │ │ │ ├── AppHeader.md │ │ │ ├── AppHeader.module.scss │ │ │ ├── AppHeader.spec.ts │ │ │ ├── AppHeader.tsx │ │ │ └── AppHeaderNative.tsx │ │ ├── AppState │ │ │ ├── AppState.md │ │ │ ├── AppState.spec.ts │ │ │ ├── AppState.tsx │ │ │ └── AppStateNative.tsx │ │ ├── AutoComplete │ │ │ ├── AutoComplete.md │ │ │ ├── AutoComplete.module.scss │ │ │ ├── AutoComplete.spec.ts │ │ │ ├── AutoComplete.tsx │ │ │ ├── AutoCompleteContext.tsx │ │ │ └── AutoCompleteNative.tsx │ │ ├── Avatar │ │ │ ├── Avatar.md │ │ │ ├── Avatar.module.scss │ │ │ ├── Avatar.spec.ts │ │ │ ├── Avatar.tsx │ │ │ └── AvatarNative.tsx │ │ ├── Backdrop │ │ │ ├── Backdrop.md │ │ │ ├── Backdrop.module.scss │ │ │ ├── Backdrop.spec.ts │ │ │ ├── Backdrop.tsx │ │ │ └── BackdropNative.tsx │ │ ├── Badge │ │ │ ├── Badge.md │ │ │ ├── Badge.module.scss │ │ │ ├── Badge.spec.ts │ │ │ ├── Badge.tsx │ │ │ └── BadgeNative.tsx │ │ ├── Bookmark │ │ │ ├── Bookmark.md │ │ │ ├── Bookmark.module.scss │ │ │ ├── Bookmark.spec.ts │ │ │ ├── Bookmark.tsx │ │ │ └── BookmarkNative.tsx │ │ ├── Breakout │ │ │ ├── Breakout.module.scss │ │ │ ├── Breakout.spec.ts │ │ │ ├── Breakout.tsx │ │ │ └── BreakoutNative.tsx │ │ ├── Button │ │ │ ├── Button-style.spec.ts │ │ │ ├── Button.md │ │ │ ├── Button.module.scss │ │ │ ├── Button.spec.ts │ │ │ ├── Button.tsx │ │ │ └── ButtonNative.tsx │ │ ├── Card │ │ │ ├── Card.md │ │ │ ├── Card.module.scss │ │ │ ├── Card.spec.ts │ │ │ ├── Card.tsx │ │ │ └── CardNative.tsx │ │ ├── Carousel │ │ │ ├── Carousel.md │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.spec.ts │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ ├── CarouselItem.tsx │ │ │ ├── CarouselItemNative.tsx │ │ │ └── CarouselNative.tsx │ │ ├── ChangeListener │ │ │ ├── ChangeListener.md │ │ │ ├── ChangeListener.spec.ts │ │ │ ├── ChangeListener.tsx │ │ │ └── ChangeListenerNative.tsx │ │ ├── chart-color-schemes.ts │ │ ├── Charts │ │ │ ├── AreaChart │ │ │ │ ├── AreaChart.md │ │ │ │ ├── AreaChart.spec.ts │ │ │ │ ├── AreaChart.tsx │ │ │ │ └── AreaChartNative.tsx │ │ │ ├── BarChart │ │ │ │ ├── BarChart.md │ │ │ │ ├── BarChart.module.scss │ │ │ │ ├── BarChart.spec.ts │ │ │ │ ├── BarChart.tsx │ │ │ │ └── BarChartNative.tsx │ │ │ ├── DonutChart │ │ │ │ ├── DonutChart.spec.ts │ │ │ │ └── DonutChart.tsx │ │ │ ├── LabelList │ │ │ │ ├── LabelList.spec.ts │ │ │ │ ├── LabelList.tsx │ │ │ │ ├── LabelListNative.module.scss │ │ │ │ └── LabelListNative.tsx │ │ │ ├── Legend │ │ │ │ ├── Legend.spec.ts │ │ │ │ ├── Legend.tsx │ │ │ │ └── LegendNative.tsx │ │ │ ├── LineChart │ │ │ │ ├── LineChart.md │ │ │ │ ├── LineChart.module.scss │ │ │ │ ├── LineChart.spec.ts │ │ │ │ ├── LineChart.tsx │ │ │ │ └── LineChartNative.tsx │ │ │ ├── PieChart │ │ │ │ ├── PieChart.md │ │ │ │ ├── PieChart.spec.ts │ │ │ │ ├── PieChart.tsx │ │ │ │ ├── PieChartNative.module.scss │ │ │ │ └── PieChartNative.tsx │ │ │ ├── RadarChart │ │ │ │ ├── RadarChart.md │ │ │ │ ├── RadarChart.spec.ts │ │ │ │ ├── RadarChart.tsx │ │ │ │ └── RadarChartNative.tsx │ │ │ ├── Tooltip │ │ │ │ ├── TooltipContent.module.scss │ │ │ │ ├── TooltipContent.spec.ts │ │ │ │ └── TooltipContent.tsx │ │ │ └── utils │ │ │ ├── abstractions.ts │ │ │ └── ChartProvider.tsx │ │ ├── Checkbox │ │ │ ├── Checkbox.md │ │ │ ├── Checkbox.spec.ts │ │ │ └── Checkbox.tsx │ │ ├── CodeBlock │ │ │ ├── CodeBlock.module.scss │ │ │ ├── CodeBlock.spec.ts │ │ │ ├── CodeBlock.tsx │ │ │ ├── CodeBlockNative.tsx │ │ │ └── highlight-code.ts │ │ ├── collectedComponentMetadata.ts │ │ ├── ColorPicker │ │ │ ├── ColorPicker.md │ │ │ ├── ColorPicker.module.scss │ │ │ ├── ColorPicker.spec.ts │ │ │ ├── ColorPicker.tsx │ │ │ └── ColorPickerNative.tsx │ │ ├── Column │ │ │ ├── Column.md │ │ │ ├── Column.tsx │ │ │ ├── ColumnNative.tsx │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ └── TableContext.tsx │ │ ├── component-utils.ts │ │ ├── ComponentProvider.tsx │ │ ├── ComponentRegistryContext.tsx │ │ ├── container-helpers.tsx │ │ ├── ContentSeparator │ │ │ ├── ContentSeparator.md │ │ │ ├── ContentSeparator.module.scss │ │ │ ├── ContentSeparator.spec.ts │ │ │ ├── ContentSeparator.tsx │ │ │ └── ContentSeparatorNative.tsx │ │ ├── DataSource │ │ │ ├── DataSource.md │ │ │ └── DataSource.tsx │ │ ├── DateInput │ │ │ ├── DateInput.md │ │ │ ├── DateInput.module.scss │ │ │ ├── DateInput.spec.ts │ │ │ ├── DateInput.tsx │ │ │ └── DateInputNative.tsx │ │ ├── DatePicker │ │ │ ├── DatePicker.md │ │ │ ├── DatePicker.module.scss │ │ │ ├── DatePicker.spec.ts │ │ │ ├── DatePicker.tsx │ │ │ └── DatePickerNative.tsx │ │ ├── DropdownMenu │ │ │ ├── DropdownMenu.md │ │ │ ├── DropdownMenu.module.scss │ │ │ ├── DropdownMenu.spec.ts │ │ │ ├── DropdownMenu.tsx │ │ │ ├── DropdownMenuNative.tsx │ │ │ ├── MenuItem.md │ │ │ └── SubMenuItem.md │ │ ├── EmojiSelector │ │ │ ├── EmojiSelector.md │ │ │ ├── EmojiSelector.spec.ts │ │ │ ├── EmojiSelector.tsx │ │ │ └── EmojiSelectorNative.tsx │ │ ├── ExpandableItem │ │ │ ├── ExpandableItem.module.scss │ │ │ ├── ExpandableItem.spec.ts │ │ │ ├── ExpandableItem.tsx │ │ │ └── ExpandableItemNative.tsx │ │ ├── FileInput │ │ │ ├── FileInput.md │ │ │ ├── FileInput.module.scss │ │ │ ├── FileInput.spec.ts │ │ │ ├── FileInput.tsx │ │ │ └── FileInputNative.tsx │ │ ├── FileUploadDropZone │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FileUploadDropZone.module.scss │ │ │ ├── FileUploadDropZone.spec.ts │ │ │ ├── FileUploadDropZone.tsx │ │ │ └── FileUploadDropZoneNative.tsx │ │ ├── FlowLayout │ │ │ ├── FlowLayout.md │ │ │ ├── FlowLayout.module.scss │ │ │ ├── FlowLayout.spec.ts │ │ │ ├── FlowLayout.spec.ts-snapshots │ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png │ │ │ ├── FlowLayout.tsx │ │ │ └── FlowLayoutNative.tsx │ │ ├── Footer │ │ │ ├── Footer.md │ │ │ ├── Footer.module.scss │ │ │ ├── Footer.spec.ts │ │ │ ├── Footer.tsx │ │ │ └── FooterNative.tsx │ │ ├── Form │ │ │ ├── Form.md │ │ │ ├── Form.module.scss │ │ │ ├── Form.spec.ts │ │ │ ├── Form.tsx │ │ │ ├── formActions.ts │ │ │ ├── FormContext.ts │ │ │ └── FormNative.tsx │ │ ├── FormItem │ │ │ ├── FormItem.md │ │ │ ├── FormItem.module.scss │ │ │ ├── FormItem.spec.ts │ │ │ ├── FormItem.tsx │ │ │ ├── FormItemNative.tsx │ │ │ ├── HelperText.module.scss │ │ │ ├── HelperText.tsx │ │ │ ├── ItemWithLabel.tsx │ │ │ └── Validations.ts │ │ ├── FormSection │ │ │ ├── FormSection.md │ │ │ ├── FormSection.ts │ │ │ └── FormSection.xmlui │ │ ├── Fragment │ │ │ ├── Fragment.spec.ts │ │ │ └── Fragment.tsx │ │ ├── Heading │ │ │ ├── abstractions.ts │ │ │ ├── H1.md │ │ │ ├── H1.spec.ts │ │ │ ├── H2.md │ │ │ ├── H2.spec.ts │ │ │ ├── H3.md │ │ │ ├── H3.spec.ts │ │ │ ├── H4.md │ │ │ ├── H4.spec.ts │ │ │ ├── H5.md │ │ │ ├── H5.spec.ts │ │ │ ├── H6.md │ │ │ ├── H6.spec.ts │ │ │ ├── Heading.md │ │ │ ├── Heading.module.scss │ │ │ ├── Heading.spec.ts │ │ │ ├── Heading.tsx │ │ │ └── HeadingNative.tsx │ │ ├── HoverCard │ │ │ ├── HoverCard.tsx │ │ │ └── HovercardNative.tsx │ │ ├── HtmlTags │ │ │ ├── HtmlTags.module.scss │ │ │ ├── HtmlTags.spec.ts │ │ │ └── HtmlTags.tsx │ │ ├── Icon │ │ │ ├── AdmonitionDanger.tsx │ │ │ ├── AdmonitionInfo.tsx │ │ │ ├── AdmonitionNote.tsx │ │ │ ├── AdmonitionTip.tsx │ │ │ ├── AdmonitionWarning.tsx │ │ │ ├── ApiIcon.tsx │ │ │ ├── ArrowDropDown.module.scss │ │ │ ├── ArrowDropDown.tsx │ │ │ ├── ArrowDropUp.module.scss │ │ │ ├── ArrowDropUp.tsx │ │ │ ├── ArrowLeft.module.scss │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowRight.module.scss │ │ │ ├── ArrowRight.tsx │ │ │ ├── Attach.tsx │ │ │ ├── Binding.module.scss │ │ │ ├── Binding.tsx │ │ │ ├── BoardIcon.tsx │ │ │ ├── BoxIcon.tsx │ │ │ ├── CheckIcon.tsx │ │ │ ├── ChevronDownIcon.tsx │ │ │ ├── ChevronLeft.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── ChevronUpIcon.tsx │ │ │ ├── CodeFileIcon.tsx │ │ │ ├── CodeSandbox.tsx │ │ │ ├── CompactListIcon.tsx │ │ │ ├── ContentCopyIcon.tsx │ │ │ ├── DarkToLightIcon.tsx │ │ │ ├── DatabaseIcon.module.scss │ │ │ ├── DatabaseIcon.tsx │ │ │ ├── DocFileIcon.tsx │ │ │ ├── DocIcon.tsx │ │ │ ├── DotMenuHorizontalIcon.tsx │ │ │ ├── DotMenuIcon.tsx │ │ │ ├── EmailIcon.tsx │ │ │ ├── EmptyFolderIcon.tsx │ │ │ ├── ErrorIcon.tsx │ │ │ ├── ExpressionIcon.tsx │ │ │ ├── FillPlusCricleIcon.tsx │ │ │ ├── FilterIcon.tsx │ │ │ ├── FolderIcon.tsx │ │ │ ├── GlobeIcon.tsx │ │ │ ├── HomeIcon.tsx │ │ │ ├── HyperLinkIcon.tsx │ │ │ ├── Icon.md │ │ │ ├── Icon.module.scss │ │ │ ├── Icon.spec.ts │ │ │ ├── Icon.tsx │ │ │ ├── IconNative.tsx │ │ │ ├── ImageFileIcon.tsx │ │ │ ├── Inspect.tsx │ │ │ ├── LightToDark.tsx │ │ │ ├── LinkIcon.tsx │ │ │ ├── ListIcon.tsx │ │ │ ├── LooseListIcon.tsx │ │ │ ├── MoonIcon.tsx │ │ │ ├── MoreOptionsIcon.tsx │ │ │ ├── NoSortIcon.tsx │ │ │ ├── PDFIcon.tsx │ │ │ ├── PenIcon.tsx │ │ │ ├── PhoneIcon.tsx │ │ │ ├── PhotoIcon.tsx │ │ │ ├── PlusIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── ShareIcon.tsx │ │ │ ├── SortAscendingIcon.tsx │ │ │ ├── SortDescendingIcon.tsx │ │ │ ├── StarsIcon.tsx │ │ │ ├── SunIcon.tsx │ │ │ ├── svg │ │ │ │ ├── admonition_danger.svg │ │ │ │ ├── admonition_info.svg │ │ │ │ ├── admonition_note.svg │ │ │ │ ├── admonition_tip.svg │ │ │ │ ├── admonition_warning.svg │ │ │ │ ├── api.svg │ │ │ │ ├── arrow-dropdown.svg │ │ │ │ ├── arrow-left.svg │ │ │ │ ├── arrow-right.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── attach.svg │ │ │ │ ├── binding.svg │ │ │ │ ├── box.svg │ │ │ │ ├── bulb.svg │ │ │ │ ├── code-file.svg │ │ │ │ ├── code-sandbox.svg │ │ │ │ ├── dark_to_light.svg │ │ │ │ ├── database.svg │ │ │ │ ├── doc.svg │ │ │ │ ├── empty-folder.svg │ │ │ │ ├── expression.svg │ │ │ │ ├── eye-closed.svg │ │ │ │ ├── eye-dark.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── file-text.svg │ │ │ │ ├── filter.svg │ │ │ │ ├── folder.svg │ │ │ │ ├── img.svg │ │ │ │ ├── inspect.svg │ │ │ │ ├── light_to_dark.svg │ │ │ │ ├── moon.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── photo.svg │ │ │ │ ├── share.svg │ │ │ │ ├── stars.svg │ │ │ │ ├── sun.svg │ │ │ │ ├── trending-down.svg │ │ │ │ ├── trending-level.svg │ │ │ │ ├── trending-up.svg │ │ │ │ ├── txt.svg │ │ │ │ ├── unknown-file.svg │ │ │ │ ├── unlink.svg │ │ │ │ └── xls.svg │ │ │ ├── TableDeleteColumnIcon.tsx │ │ │ ├── TableDeleteRowIcon.tsx │ │ │ ├── TableInsertColumnIcon.tsx │ │ │ ├── TableInsertRowIcon.tsx │ │ │ ├── TrashIcon.tsx │ │ │ ├── TrendingDownIcon.tsx │ │ │ ├── TrendingLevelIcon.tsx │ │ │ ├── TrendingUpIcon.tsx │ │ │ ├── TxtIcon.tsx │ │ │ ├── UnknownFileIcon.tsx │ │ │ ├── UnlinkIcon.tsx │ │ │ ├── UserIcon.tsx │ │ │ ├── WarningIcon.tsx │ │ │ └── XlsIcon.tsx │ │ ├── IconProvider.tsx │ │ ├── IconRegistryContext.tsx │ │ ├── IFrame │ │ │ ├── IFrame.md │ │ │ ├── IFrame.module.scss │ │ │ ├── IFrame.spec.ts │ │ │ ├── IFrame.tsx │ │ │ └── IFrameNative.tsx │ │ ├── Image │ │ │ ├── Image.md │ │ │ ├── Image.module.scss │ │ │ ├── Image.spec.ts │ │ │ ├── Image.tsx │ │ │ └── ImageNative.tsx │ │ ├── Input │ │ │ ├── index.ts │ │ │ ├── InputAdornment.module.scss │ │ │ ├── InputAdornment.tsx │ │ │ ├── InputDivider.module.scss │ │ │ ├── InputDivider.tsx │ │ │ ├── InputLabel.module.scss │ │ │ ├── InputLabel.tsx │ │ │ ├── PartialInput.module.scss │ │ │ └── PartialInput.tsx │ │ ├── InspectButton │ │ │ ├── InspectButton.module.scss │ │ │ └── InspectButton.tsx │ │ ├── Items │ │ │ ├── Items.md │ │ │ ├── Items.spec.ts │ │ │ ├── Items.tsx │ │ │ └── ItemsNative.tsx │ │ ├── Link │ │ │ ├── Link.md │ │ │ ├── Link.module.scss │ │ │ ├── Link.spec.ts │ │ │ ├── Link.tsx │ │ │ └── LinkNative.tsx │ │ ├── List │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── List.md │ │ │ ├── List.module.scss │ │ │ ├── List.spec.ts │ │ │ ├── List.tsx │ │ │ └── ListNative.tsx │ │ ├── Logo │ │ │ ├── doc-resources │ │ │ │ └── xmlui-logo.svg │ │ │ ├── Logo.md │ │ │ ├── Logo.tsx │ │ │ └── LogoNative.tsx │ │ ├── Markdown │ │ │ ├── CodeText.module.scss │ │ │ ├── CodeText.tsx │ │ │ ├── Markdown.md │ │ │ ├── Markdown.module.scss │ │ │ ├── Markdown.spec.ts │ │ │ ├── Markdown.tsx │ │ │ ├── MarkdownNative.tsx │ │ │ ├── parse-binding-expr.ts │ │ │ └── utils.ts │ │ ├── metadata-helpers.ts │ │ ├── ModalDialog │ │ │ ├── ConfirmationModalContextProvider.tsx │ │ │ ├── Dialog.module.scss │ │ │ ├── Dialog.tsx │ │ │ ├── ModalDialog.md │ │ │ ├── ModalDialog.module.scss │ │ │ ├── ModalDialog.spec.ts │ │ │ ├── ModalDialog.tsx │ │ │ ├── ModalDialogNative.tsx │ │ │ └── ModalVisibilityContext.tsx │ │ ├── NavGroup │ │ │ ├── NavGroup.md │ │ │ ├── NavGroup.module.scss │ │ │ ├── NavGroup.spec.ts │ │ │ ├── NavGroup.tsx │ │ │ ├── NavGroupContext.ts │ │ │ └── NavGroupNative.tsx │ │ ├── NavLink │ │ │ ├── NavLink.md │ │ │ ├── NavLink.module.scss │ │ │ ├── NavLink.spec.ts │ │ │ ├── NavLink.tsx │ │ │ └── NavLinkNative.tsx │ │ ├── NavPanel │ │ │ ├── NavPanel.md │ │ │ ├── NavPanel.module.scss │ │ │ ├── NavPanel.spec.ts │ │ │ ├── NavPanel.tsx │ │ │ └── NavPanelNative.tsx │ │ ├── NestedApp │ │ │ ├── AppWithCodeView.module.scss │ │ │ ├── AppWithCodeView.tsx │ │ │ ├── AppWithCodeViewNative.tsx │ │ │ ├── defaultProps.tsx │ │ │ ├── logo.svg │ │ │ ├── NestedApp.module.scss │ │ │ ├── NestedApp.tsx │ │ │ ├── NestedAppNative.tsx │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.tsx │ │ │ └── utils.ts │ │ ├── NoResult │ │ │ ├── NoResult.md │ │ │ ├── NoResult.module.scss │ │ │ ├── NoResult.spec.ts │ │ │ ├── NoResult.tsx │ │ │ └── NoResultNative.tsx │ │ ├── NumberBox │ │ │ ├── numberbox-abstractions.ts │ │ │ ├── NumberBox.md │ │ │ ├── NumberBox.module.scss │ │ │ ├── NumberBox.spec.ts │ │ │ ├── NumberBox.tsx │ │ │ └── NumberBoxNative.tsx │ │ ├── Option │ │ │ ├── Option.md │ │ │ ├── Option.spec.ts │ │ │ ├── Option.tsx │ │ │ ├── OptionNative.tsx │ │ │ └── OptionTypeProvider.tsx │ │ ├── PageMetaTitle │ │ │ ├── PageMetaTilteNative.tsx │ │ │ ├── PageMetaTitle.md │ │ │ ├── PageMetaTitle.spec.ts │ │ │ └── PageMetaTitle.tsx │ │ ├── Pages │ │ │ ├── Page.md │ │ │ ├── Pages.md │ │ │ ├── Pages.module.scss │ │ │ ├── Pages.tsx │ │ │ └── PagesNative.tsx │ │ ├── Pagination │ │ │ ├── Pagination.md │ │ │ ├── Pagination.module.scss │ │ │ ├── Pagination.spec.ts │ │ │ ├── Pagination.tsx │ │ │ └── PaginationNative.tsx │ │ ├── PositionedContainer │ │ │ ├── PositionedContainer.module.scss │ │ │ ├── PositionedContainer.tsx │ │ │ └── PositionedContainerNative.tsx │ │ ├── ProfileMenu │ │ │ ├── ProfileMenu.module.scss │ │ │ └── ProfileMenu.tsx │ │ ├── ProgressBar │ │ │ ├── ProgressBar.md │ │ │ ├── ProgressBar.module.scss │ │ │ ├── ProgressBar.spec.ts │ │ │ ├── ProgressBar.tsx │ │ │ └── ProgressBarNative.tsx │ │ ├── Queue │ │ │ ├── Queue.md │ │ │ ├── Queue.spec.ts │ │ │ ├── Queue.tsx │ │ │ ├── queueActions.ts │ │ │ └── QueueNative.tsx │ │ ├── RadioGroup │ │ │ ├── RadioGroup.md │ │ │ ├── RadioGroup.module.scss │ │ │ ├── RadioGroup.spec.ts │ │ │ ├── RadioGroup.tsx │ │ │ ├── RadioGroupNative.tsx │ │ │ ├── RadioItem.tsx │ │ │ └── RadioItemNative.tsx │ │ ├── RealTimeAdapter │ │ │ ├── RealTimeAdapter.tsx │ │ │ └── RealTimeAdapterNative.tsx │ │ ├── Redirect │ │ │ ├── Redirect.md │ │ │ ├── Redirect.spec.ts │ │ │ └── Redirect.tsx │ │ ├── ResponsiveBar │ │ │ ├── README.md │ │ │ ├── ResponsiveBar.md │ │ │ ├── ResponsiveBar.module.scss │ │ │ ├── ResponsiveBar.spec.ts │ │ │ ├── ResponsiveBar.tsx │ │ │ └── ResponsiveBarNative.tsx │ │ ├── Select │ │ │ ├── HiddenOption.tsx │ │ │ ├── OptionContext.ts │ │ │ ├── Select.md │ │ │ ├── Select.module.scss │ │ │ ├── Select.spec.ts │ │ │ ├── Select.tsx │ │ │ ├── SelectContext.tsx │ │ │ └── SelectNative.tsx │ │ ├── SelectionStore │ │ │ ├── SelectionStore.md │ │ │ ├── SelectionStore.tsx │ │ │ └── SelectionStoreNative.tsx │ │ ├── Slider │ │ │ ├── Slider.md │ │ │ ├── Slider.module.scss │ │ │ ├── Slider.spec.ts │ │ │ ├── Slider.tsx │ │ │ └── SliderNative.tsx │ │ ├── Slot │ │ │ ├── Slot.md │ │ │ ├── Slot.spec.ts │ │ │ └── Slot.ts │ │ ├── SlotItem.tsx │ │ ├── SpaceFiller │ │ │ ├── SpaceFiller.md │ │ │ ├── SpaceFiller.module.scss │ │ │ ├── SpaceFiller.spec.ts │ │ │ ├── SpaceFiller.tsx │ │ │ └── SpaceFillerNative.tsx │ │ ├── Spinner │ │ │ ├── Spinner.md │ │ │ ├── Spinner.module.scss │ │ │ ├── Spinner.spec.ts │ │ │ ├── Spinner.tsx │ │ │ └── SpinnerNative.tsx │ │ ├── Splitter │ │ │ ├── HSplitter.md │ │ │ ├── HSplitter.spec.ts │ │ │ ├── Splitter.md │ │ │ ├── Splitter.module.scss │ │ │ ├── Splitter.spec.ts │ │ │ ├── Splitter.tsx │ │ │ ├── SplitterNative.tsx │ │ │ ├── utils.ts │ │ │ ├── VSplitter.md │ │ │ └── VSplitter.spec.ts │ │ ├── Stack │ │ │ ├── CHStack.md │ │ │ ├── CHStack.spec.ts │ │ │ ├── CVStack.md │ │ │ ├── CVStack.spec.ts │ │ │ ├── HStack.md │ │ │ ├── HStack.spec.ts │ │ │ ├── Stack.md │ │ │ ├── Stack.module.scss │ │ │ ├── Stack.spec.ts │ │ │ ├── Stack.tsx │ │ │ ├── StackNative.tsx │ │ │ ├── VStack.md │ │ │ └── VStack.spec.ts │ │ ├── StickyBox │ │ │ ├── StickyBox.md │ │ │ ├── StickyBox.module.scss │ │ │ ├── StickyBox.tsx │ │ │ └── StickyBoxNative.tsx │ │ ├── Switch │ │ │ ├── Switch.md │ │ │ ├── Switch.spec.ts │ │ │ └── Switch.tsx │ │ ├── Table │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── react-table-config.d.ts │ │ │ ├── Table.md │ │ │ ├── Table.module.scss │ │ │ ├── Table.spec.ts │ │ │ ├── Table.tsx │ │ │ ├── TableNative.tsx │ │ │ └── useRowSelection.tsx │ │ ├── TableOfContents │ │ │ ├── TableOfContents.module.scss │ │ │ ├── TableOfContents.spec.ts │ │ │ ├── TableOfContents.tsx │ │ │ └── TableOfContentsNative.tsx │ │ ├── Tabs │ │ │ ├── TabContext.tsx │ │ │ ├── TabItem.md │ │ │ ├── TabItem.tsx │ │ │ ├── TabItemNative.tsx │ │ │ ├── Tabs.md │ │ │ ├── Tabs.module.scss │ │ │ ├── Tabs.spec.ts │ │ │ ├── Tabs.tsx │ │ │ └── TabsNative.tsx │ │ ├── Text │ │ │ ├── Text.md │ │ │ ├── Text.module.scss │ │ │ ├── Text.spec.ts │ │ │ ├── Text.tsx │ │ │ └── TextNative.tsx │ │ ├── TextArea │ │ │ ├── TextArea.md │ │ │ ├── TextArea.module.scss │ │ │ ├── TextArea.spec.ts │ │ │ ├── TextArea.tsx │ │ │ ├── TextAreaNative.tsx │ │ │ ├── TextAreaResizable.tsx │ │ │ └── useComposedRef.ts │ │ ├── TextBox │ │ │ ├── TextBox.md │ │ │ ├── TextBox.module.scss │ │ │ ├── TextBox.spec.ts │ │ │ ├── TextBox.tsx │ │ │ └── TextBoxNative.tsx │ │ ├── Theme │ │ │ ├── NotificationToast.tsx │ │ │ ├── Theme.md │ │ │ ├── Theme.module.scss │ │ │ ├── Theme.spec.ts │ │ │ ├── Theme.tsx │ │ │ └── ThemeNative.tsx │ │ ├── TimeInput │ │ │ ├── TimeInput.md │ │ │ ├── TimeInput.module.scss │ │ │ ├── TimeInput.spec.ts │ │ │ ├── TimeInput.tsx │ │ │ ├── TimeInputNative.tsx │ │ │ └── utils.ts │ │ ├── Timer │ │ │ ├── Timer.md │ │ │ ├── Timer.spec.ts │ │ │ ├── Timer.tsx │ │ │ └── TimerNative.tsx │ │ ├── Toggle │ │ │ ├── Toggle.module.scss │ │ │ └── Toggle.tsx │ │ ├── ToneChangerButton │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneChangerButton.spec.ts │ │ │ └── ToneChangerButton.tsx │ │ ├── ToneSwitch │ │ │ ├── ToneSwitch.md │ │ │ ├── ToneSwitch.module.scss │ │ │ ├── ToneSwitch.spec.ts │ │ │ ├── ToneSwitch.tsx │ │ │ └── ToneSwitchNative.tsx │ │ ├── Tooltip │ │ │ ├── Tooltip.md │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.spec.ts │ │ │ ├── Tooltip.tsx │ │ │ └── TooltipNative.tsx │ │ ├── Tree │ │ │ ├── testData.ts │ │ │ ├── Tree-dynamic.spec.ts │ │ │ ├── Tree-icons.spec.ts │ │ │ ├── Tree.md │ │ │ ├── Tree.spec.ts │ │ │ ├── TreeComponent.module.scss │ │ │ ├── TreeComponent.tsx │ │ │ └── TreeNative.tsx │ │ ├── TreeDisplay │ │ │ ├── TreeDisplay.md │ │ │ ├── TreeDisplay.module.scss │ │ │ ├── TreeDisplay.tsx │ │ │ └── TreeDisplayNative.tsx │ │ ├── ValidationSummary │ │ │ ├── ValidationSummary.module.scss │ │ │ └── ValidationSummary.tsx │ │ └── VisuallyHidden.tsx │ ├── components-core │ │ ├── abstractions │ │ │ ├── ComponentRenderer.ts │ │ │ ├── LoaderRenderer.ts │ │ │ ├── standalone.ts │ │ │ └── treeAbstractions.ts │ │ ├── action │ │ │ ├── actions.ts │ │ │ ├── APICall.tsx │ │ │ ├── FileDownloadAction.tsx │ │ │ ├── FileUploadAction.tsx │ │ │ ├── NavigateAction.tsx │ │ │ └── TimedAction.tsx │ │ ├── ApiBoundComponent.tsx │ │ ├── appContext │ │ │ ├── date-functions.ts │ │ │ ├── math-function.ts │ │ │ └── misc-utils.ts │ │ ├── AppContext.tsx │ │ ├── behaviors │ │ │ ├── Behavior.tsx │ │ │ └── CoreBehaviors.tsx │ │ ├── component-hooks.ts │ │ ├── ComponentDecorator.tsx │ │ ├── ComponentViewer.tsx │ │ ├── CompoundComponent.tsx │ │ ├── constants.ts │ │ ├── DebugViewProvider.tsx │ │ ├── descriptorHelper.ts │ │ ├── devtools │ │ │ ├── InspectorDialog.module.scss │ │ │ ├── InspectorDialog.tsx │ │ │ └── InspectorDialogVisibilityContext.tsx │ │ ├── EngineError.ts │ │ ├── event-handlers.ts │ │ ├── InspectorButton.module.scss │ │ ├── InspectorContext.tsx │ │ ├── interception │ │ │ ├── abstractions.ts │ │ │ ├── ApiInterceptor.ts │ │ │ ├── ApiInterceptorProvider.tsx │ │ │ ├── apiInterceptorWorker.ts │ │ │ ├── Backend.ts │ │ │ ├── Errors.ts │ │ │ ├── IndexedDb.ts │ │ │ ├── initMock.ts │ │ │ ├── InMemoryDb.ts │ │ │ ├── ReadonlyCollection.ts │ │ │ └── useApiInterceptorContext.tsx │ │ ├── loader │ │ │ ├── ApiLoader.tsx │ │ │ ├── DataLoader.tsx │ │ │ ├── ExternalDataLoader.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MockLoaderRenderer.tsx │ │ │ └── PageableLoader.tsx │ │ ├── LoaderComponent.tsx │ │ ├── markup-check.ts │ │ ├── parts.ts │ │ ├── renderers.ts │ │ ├── rendering │ │ │ ├── AppContent.tsx │ │ │ ├── AppRoot.tsx │ │ │ ├── AppWrapper.tsx │ │ │ ├── buildProxy.ts │ │ │ ├── collectFnVarDeps.ts │ │ │ ├── ComponentAdapter.tsx │ │ │ ├── ComponentWrapper.tsx │ │ │ ├── Container.tsx │ │ │ ├── containers.ts │ │ │ ├── ContainerWrapper.tsx │ │ │ ├── ErrorBoundary.module.scss │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── InvalidComponent.module.scss │ │ │ ├── InvalidComponent.tsx │ │ │ ├── nodeUtils.ts │ │ │ ├── reducer.ts │ │ │ ├── renderChild.tsx │ │ │ ├── StandaloneComponent.tsx │ │ │ ├── StateContainer.tsx │ │ │ ├── UnknownComponent.module.scss │ │ │ ├── UnknownComponent.tsx │ │ │ └── valueExtractor.ts │ │ ├── reportEngineError.ts │ │ ├── RestApiProxy.ts │ │ ├── script-runner │ │ │ ├── asyncProxy.ts │ │ │ ├── AttributeValueParser.ts │ │ │ ├── bannedFunctions.ts │ │ │ ├── BindingTreeEvaluationContext.ts │ │ │ ├── eval-tree-async.ts │ │ │ ├── eval-tree-common.ts │ │ │ ├── eval-tree-sync.ts │ │ │ ├── ParameterParser.ts │ │ │ ├── process-statement-async.ts │ │ │ ├── process-statement-common.ts │ │ │ ├── process-statement-sync.ts │ │ │ ├── ScriptingSourceTree.ts │ │ │ ├── simplify-expression.ts │ │ │ ├── statement-queue.ts │ │ │ └── visitors.ts │ │ ├── StandaloneApp.tsx │ │ ├── StandaloneExtensionManager.ts │ │ ├── TableOfContentsContext.tsx │ │ ├── theming │ │ │ ├── _themes.scss │ │ │ ├── component-layout-resolver.ts │ │ │ ├── extendThemeUtils.ts │ │ │ ├── hvar.ts │ │ │ ├── layout-resolver.ts │ │ │ ├── parse-layout-props.ts │ │ │ ├── StyleContext.tsx │ │ │ ├── StyleRegistry.ts │ │ │ ├── ThemeContext.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ ├── themes │ │ │ │ ├── base-utils.ts │ │ │ │ ├── palette.ts │ │ │ │ ├── root.ts │ │ │ │ ├── solid.ts │ │ │ │ ├── theme-colors.ts │ │ │ │ └── xmlui.ts │ │ │ ├── themeVars.module.scss │ │ │ ├── themeVars.ts │ │ │ ├── transformThemeVars.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── actionUtils.ts │ │ │ ├── audio-utils.ts │ │ │ ├── base64-utils.ts │ │ │ ├── compound-utils.ts │ │ │ ├── css-utils.ts │ │ │ ├── DataLoaderQueryKeyGenerator.ts │ │ │ ├── date-utils.ts │ │ │ ├── extractParam.ts │ │ │ ├── hooks.tsx │ │ │ ├── LruCache.ts │ │ │ ├── mergeProps.ts │ │ │ ├── misc.ts │ │ │ ├── request-params.ts │ │ │ ├── statementUtils.ts │ │ │ └── treeUtils.ts │ │ └── xmlui-parser.ts │ ├── index-standalone.ts │ ├── index.scss │ ├── index.ts │ ├── language-server │ │ ├── server-common.ts │ │ ├── server-web-worker.ts │ │ ├── server.ts │ │ ├── services │ │ │ ├── common │ │ │ │ ├── docs-generation.ts │ │ │ │ ├── lsp-utils.ts │ │ │ │ ├── metadata-utils.ts │ │ │ │ └── syntax-node-utilities.ts │ │ │ ├── completion.ts │ │ │ ├── diagnostic.ts │ │ │ ├── format.ts │ │ │ └── hover.ts │ │ └── xmlui-metadata-generated.mjs │ ├── logging │ │ ├── LoggerContext.tsx │ │ ├── LoggerInitializer.tsx │ │ ├── LoggerService.ts │ │ └── xmlui.ts │ ├── logo.svg │ ├── parsers │ │ ├── common │ │ │ ├── GenericToken.ts │ │ │ ├── InputStream.ts │ │ │ └── utils.ts │ │ ├── scripting │ │ │ ├── code-behind-collect.ts │ │ │ ├── Lexer.ts │ │ │ ├── modules.ts │ │ │ ├── Parser.ts │ │ │ ├── ParserError.ts │ │ │ ├── ScriptingNodeTypes.ts │ │ │ ├── TokenTrait.ts │ │ │ ├── TokenType.ts │ │ │ └── tree-visitor.ts │ │ ├── style-parser │ │ │ ├── errors.ts │ │ │ ├── source-tree.ts │ │ │ ├── StyleInputStream.ts │ │ │ ├── StyleLexer.ts │ │ │ ├── StyleParser.ts │ │ │ └── tokens.ts │ │ └── xmlui-parser │ │ ├── CharacterCodes.ts │ │ ├── diagnostics.ts │ │ ├── fileExtensions.ts │ │ ├── index.ts │ │ ├── lint.ts │ │ ├── parser.ts │ │ ├── ParserError.ts │ │ ├── scanner.ts │ │ ├── syntax-kind.ts │ │ ├── syntax-node.ts │ │ ├── transform.ts │ │ ├── utils.ts │ │ ├── xmlui-serializer.ts │ │ └── xmlui-tree.ts │ ├── react-app-env.d.ts │ ├── syntax │ │ ├── monaco │ │ │ ├── grammar.monacoLanguage.ts │ │ │ ├── index.ts │ │ │ ├── xmlui-dark.ts │ │ │ ├── xmlui-light.ts │ │ │ └── xmluiscript.monacoLanguage.ts │ │ └── textMate │ │ ├── index.ts │ │ ├── xmlui-dark.json │ │ ├── xmlui-light.json │ │ ├── xmlui.json │ │ └── xmlui.tmLanguage.json │ ├── testing │ │ ├── assertions.ts │ │ ├── component-test-helpers.ts │ │ ├── ComponentDrivers.ts │ │ ├── drivers │ │ │ ├── DateInputDriver.ts │ │ │ ├── ModalDialogDriver.ts │ │ │ ├── NumberBoxDriver.ts │ │ │ ├── TextBoxDriver.ts │ │ │ ├── TimeInputDriver.ts │ │ │ ├── TimerDriver.ts │ │ │ └── TreeDriver.ts │ │ ├── fixtures.ts │ │ ├── infrastructure │ │ │ ├── index.html │ │ │ ├── main.tsx │ │ │ ├── public │ │ │ │ ├── mockServiceWorker.js │ │ │ │ ├── resources │ │ │ │ │ ├── bell.svg │ │ │ │ │ ├── box.svg │ │ │ │ │ ├── doc.svg │ │ │ │ │ ├── eye.svg │ │ │ │ │ ├── flower-640x480.jpg │ │ │ │ │ ├── sun.svg │ │ │ │ │ ├── test-image-100x100.jpg │ │ │ │ │ └── txt.svg │ │ │ │ └── serve.json │ │ │ └── TestBed.tsx │ │ └── themed-app-test-helpers.ts │ └── vite-env.d.ts ├── tests │ ├── components │ │ ├── CodeBlock │ │ │ └── hightlight-code.test.ts │ │ ├── playground-pattern.test.ts │ │ └── Tree │ │ └── Tree-states.test.ts │ ├── components-core │ │ ├── abstractions │ │ │ └── treeAbstractions.test.ts │ │ ├── container │ │ │ └── buildProxy.test.ts │ │ ├── interception │ │ │ ├── orderBy.test.ts │ │ │ ├── ReadOnlyCollection.test.ts │ │ │ └── request-param-converter.test.ts │ │ ├── scripts-runner │ │ │ ├── AttributeValueParser.test.ts │ │ │ ├── eval-tree-arrow-async.test.ts │ │ │ ├── eval-tree-arrow.test.ts │ │ │ ├── eval-tree-func-decl-async.test.ts │ │ │ ├── eval-tree-func-decl.test.ts │ │ │ ├── eval-tree-pre-post.test.ts │ │ │ ├── eval-tree-regression.test.ts │ │ │ ├── eval-tree.test.ts │ │ │ ├── function-proxy.test.ts │ │ │ ├── parser-regression.test.ts │ │ │ ├── process-event.test.ts │ │ │ ├── process-function.test.ts │ │ │ ├── process-implicit-context.test.ts │ │ │ ├── process-statement-asgn.test.ts │ │ │ ├── process-statement-destruct.test.ts │ │ │ ├── process-statement-regs.test.ts │ │ │ ├── process-statement-sync.test.ts │ │ │ ├── process-statement.test.ts │ │ │ ├── process-switch-sync.test.ts │ │ │ ├── process-switch.test.ts │ │ │ ├── process-try-sync.test.ts │ │ │ ├── process-try.test.ts │ │ │ └── test-helpers.ts │ │ ├── test-metadata-handler.ts │ │ ├── theming │ │ │ ├── border-segments.test.ts │ │ │ ├── component-layout.resolver.test.ts │ │ │ ├── layout-property-parser.test.ts │ │ │ ├── layout-resolver.test.ts │ │ │ ├── layout-resolver2.test.ts │ │ │ ├── layout-vp-override.test.ts │ │ │ └── padding-segments.test.ts │ │ └── utils │ │ ├── date-utils.test.ts │ │ ├── format-human-elapsed-time.test.ts │ │ └── LruCache.test.ts │ ├── language-server │ │ ├── completion.test.ts │ │ ├── format.test.ts │ │ ├── hover.test.ts │ │ └── mockData.ts │ └── parsers │ ├── common │ │ └── input-stream.test.ts │ ├── markdown │ │ └── parse-binding-expression.test.ts │ ├── parameter-parser.test.ts │ ├── paremeter-parser.test.ts │ ├── scripting │ │ ├── eval-tree-arrow.test.ts │ │ ├── eval-tree-pre-post.test.ts │ │ ├── eval-tree.test.ts │ │ ├── function-proxy.test.ts │ │ ├── lexer-literals.test.ts │ │ ├── lexer-misc.test.ts │ │ ├── module-parse.test.ts │ │ ├── parser-arrow.test.ts │ │ ├── parser-assignments.test.ts │ │ ├── parser-binary.test.ts │ │ ├── parser-destructuring.test.ts │ │ ├── parser-errors.test.ts │ │ ├── parser-expressions.test.ts │ │ ├── parser-function.test.ts │ │ ├── parser-literals.test.ts │ │ ├── parser-primary.test.ts │ │ ├── parser-regex.test.ts │ │ ├── parser-statements.test.ts │ │ ├── parser-unary.test.ts │ │ ├── process-event.test.ts │ │ ├── process-implicit-context.test.ts │ │ ├── process-statement-asgn.test.ts │ │ ├── process-statement-destruct.test.ts │ │ ├── process-statement-regs.test.ts │ │ ├── process-statement-sync.test.ts │ │ ├── process-statement.test.ts │ │ ├── process-switch-sync.test.ts │ │ ├── process-switch.test.ts │ │ ├── process-try-sync.test.ts │ │ ├── process-try.test.ts │ │ ├── simplify-expression.test.ts │ │ ├── statement-hooks.test.ts │ │ └── test-helpers.ts │ ├── style-parser │ │ ├── generateHvarChain.test.ts │ │ ├── parseHVar.test.ts │ │ ├── parser.test.ts │ │ └── tokens.test.ts │ └── xmlui │ ├── lint.test.ts │ ├── parser.test.ts │ ├── scanner.test.ts │ ├── transform.attr.test.ts │ ├── transform.circular.test.ts │ ├── transform.element.test.ts │ ├── transform.errors.test.ts │ ├── transform.escape.test.ts │ ├── transform.regression.test.ts │ ├── transform.script.test.ts │ ├── transform.test.ts │ └── xmlui.ts ├── tests-e2e │ ├── api-bound-component-regression.spec.ts │ ├── api-call-as-extracted-component.spec.ts │ ├── assign-to-object-or-array-regression.spec.ts │ ├── binding-regression.spec.ts │ ├── children-as-template-context-vars.spec.ts │ ├── compound-component.spec.ts │ ├── context-vars-regression.spec.ts │ ├── data-bindings.spec.ts │ ├── datasource-and-api-usage-in-var.spec.ts │ ├── datasource-direct-binding.spec.ts │ ├── datasource-onLoaded-regression.spec.ts │ ├── modify-array-item-regression.spec.ts │ ├── namespaces.spec.ts │ ├── push-to-array-regression.spec.ts │ ├── screen-breakpoints.spec.ts │ ├── scripting.spec.ts │ ├── state-scope-in-pages.spec.ts │ └── state-var-scopes.spec.ts ├── tsconfig.bin.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /xmlui/src/components/Stack/Stack.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "../../testing/fixtures"; 2 | import { overflows, getBounds } from "../../testing/component-test-helpers"; 3 | 4 | const PAGE_WIDTH = 1280; 5 | const PAGE_HEIGHT = 720; 6 | test.use({ viewport: { width: PAGE_WIDTH, height: PAGE_HEIGHT } }); 7 | 8 | // ============================================================================= 9 | // BASIC FUNCTIONALITY TESTS 10 | // ============================================================================= 11 | 12 | test.describe("Basic Functionality", () => { 13 | test("can render empty", async ({ page, initTestBed }) => { 14 | await initTestBed(`<Stack testId="stack"></Stack>`); 15 | await expect(page.getByTestId("stack")).toBeAttached(); 16 | await expect(page.getByTestId("stack")).toBeEmpty(); 17 | }); 18 | }); 19 | 20 | // ============================================================================= 21 | // LAYOUT TESTS 22 | // ============================================================================= 23 | 24 | test.describe("Layout", () => { 25 | // "(horizontal) children with unspecified dimensions" -> width is content size, height is content size 26 | test("(horizontal) children with unspecified dimensions", async ({ page, initTestBed }) => { 27 | await initTestBed(` 28 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" gap="0"> 29 | <Text testId="item_0" backgroundColor="cyan">Text #1</Text> 30 | <Text testId="item_1" backgroundColor="yellow">Text #2</Text> 31 | <Text testId="item_2" backgroundColor="lightgreen">Text #3</Text> 32 | </Stack> 33 | `); 34 | 35 | const { width: stackWidth, height: stackHeight } = await getBounds(page.getByTestId("stack")); 36 | const { width: itemWidth0, height: itemHeight0 } = await getBounds(page.getByTestId("item_0")); 37 | const { width: itemWidth1 } = await getBounds(page.getByTestId("item_1")); 38 | const { width: itemWidth2, right: itemRight2 } = await getBounds(page.getByTestId("item_2")); 39 | const itemWidthSum = itemWidth0 + itemWidth1 + itemWidth2; 40 | 41 | // ensure the elements are not overflowing and that the stack is not as wide as the width-sum of the elements 42 | // this can be stated, since we set the viewport size at the start, 43 | // which is bigger than the width-sum of the elements 44 | expect(itemWidthSum).toBeLessThan(stackWidth); 45 | 46 | //no gaps between elements 47 | // with tolerance, since we are comparing floating point number. The pixel values could be non-whole numbers 48 | // in which case adding up fractions could have a very small difference that would make the test fail 49 | expect(itemWidthSum).toEqualWithTolerance(itemRight2); 50 | 51 | // enusre that the stack is as tall as the tallest element (they all have the same height in this case) 52 | expect(stackHeight).toEqual(itemHeight0); 53 | }); 54 | 55 | // "(vertical) children with unspecified dimensions" -> width fills available space, height is content size 56 | test("(vertical) children with unspecified dimensions, orientation is implicit", async ({ 57 | page, 58 | initTestBed, 59 | }) => { 60 | await initTestBed(` 61 | <Stack testId="stack" backgroundColor="lightgray" gap="0"> 62 | <Text testId="item_0" backgroundColor="cyan">Text #1</Text> 63 | <Text testId="item_1" backgroundColor="yellow">Text #2</Text> 64 | <Text testId="item_2" backgroundColor="lightgreen">Text #3</Text> 65 | </Stack> 66 | `); 67 | 68 | const { height: stackHeight, width: stackWidth } = await getBounds(page.getByTestId("stack")); 69 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 70 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 71 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 72 | 73 | const itemHeightSum = itemDims0.height + itemDims1.height + itemDims2.height; 74 | 75 | expect(itemHeightSum).toEqualWithTolerance(stackHeight); 76 | expect(itemDims0.width).toEqual(stackWidth); 77 | expect(itemDims1.width).toEqual(stackWidth); 78 | expect(itemDims2.width).toEqual(stackWidth); 79 | }); 80 | 81 | // "(horizontal) block children with unspecified dimensions" -> width is content size, 82 | // height is content size, block children are treated as inline elements 83 | test("(horizontal) block children with unspecified dimensions", async ({ page, initTestBed }) => { 84 | await initTestBed(` 85 | <Fragment> 86 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" gap="0"> 87 | <Text testId="item_0" backgroundColor="cyan">Heading 1</Text> 88 | <Text testId="item_1" backgroundColor="yellow">Heading 2</Text> 89 | <Text testId="item_2" backgroundColor="lightgreen">Heading 3</Text> 90 | </Stack> 91 | <Stack testId="stack2" orientation="horizontal" backgroundColor="lightgray"> 92 | <Text testId="item_3" backgroundColor="coral">Heading 1Heading 2Heading 3</Text> 93 | </Stack> 94 | </Fragment> 95 | `); 96 | 97 | const { width: stackWidth, height: stackHeight } = await getBounds(page.getByTestId("stack")); 98 | const { width: itemWidth0, height: itemHeight0 } = await getBounds(page.getByTestId("item_0")); 99 | const { width: itemWidth1, height: itemHeight1 } = await getBounds(page.getByTestId("item_1")); 100 | const { width: itemWidth2, height: itemHeight2 } = await getBounds(page.getByTestId("item_2")); 101 | const { width: itemWidth3 } = await getBounds(page.getByTestId("item_3")); 102 | 103 | const itemWidthSum = itemWidth0 + itemWidth1 + itemWidth2; 104 | const tallestItemHeight = Math.max(itemHeight0, itemHeight1, itemHeight2); 105 | 106 | expect(itemWidthSum).toBeLessThan(stackWidth); 107 | expect(itemWidthSum).toEqualWithTolerance(itemWidth3); 108 | expect(stackHeight).toEqual(tallestItemHeight); 109 | }); 110 | 111 | // "(horizontal) children with fixed dimensions" -> Stack does not alter dimensions 112 | test("(horizontal) children with fixed dimensions", async ({ page, initTestBed }) => { 113 | await initTestBed(` 114 | <Stack testId="stack" orientation="horizontal" gap="0"> 115 | <Text testId="item_0" backgroundColor="cyan" width="72px" height="36px">72 x 36</Text> 116 | <Text testId="item_1" backgroundColor="yellow" width="144px" height="72px">144 x 72</Text> 117 | <Text testId="item_2" backgroundColor="lightgreen" width="64px" height="48px">64 x 48</Text> 118 | </Stack> 119 | `); 120 | 121 | const { height: stackHeight } = await getBounds(page.getByTestId("stack")); 122 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 123 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 124 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 125 | 126 | const tallestItemHeight = 72; 127 | const itemWidthSum = 72 + 144 + 64; 128 | 129 | //no gaps between items 130 | expect(itemWidthSum).toEqualWithTolerance(itemDims2.right); 131 | expect(stackHeight).toEqual(tallestItemHeight); 132 | expect(itemDims0).toMatchObject({ width: 72, height: 36 }); 133 | expect(itemDims1).toMatchObject({ width: 144, height: 72 }); 134 | expect(itemDims2).toMatchObject({ width: 64, height: 48 }); 135 | }); 136 | 137 | // "(vertical) children with fixed dimensions" -> Stack does not alter dimensions 138 | test("(vertical) children with fixed dimensions", async ({ page, initTestBed }) => { 139 | await initTestBed(` 140 | <Stack testId="stack" orientation="vertical" gap="0"> 141 | <Text testId="item_0" backgroundColor="cyan" width="72px" height="36px">72 x 36</Text> 142 | <Text testId="item_1" backgroundColor="yellow" width="144px" height="72px">144 x 72</Text> 143 | <Text testId="item_2" backgroundColor="lightgreen" width="64px" height="48px">64 x 48</Text> 144 | </Stack> 145 | `); 146 | 147 | const { width: stackWidth, height: stackHeight } = await getBounds(page.getByTestId("stack")); 148 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 149 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 150 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 151 | 152 | const widestItemWidth = 144; 153 | const itemHeightSum = 36 + 72 + 48; 154 | 155 | expect(widestItemWidth).toBeLessThan(stackWidth); 156 | expect(itemHeightSum).toEqualWithTolerance(itemDims2.bottom); 157 | expect(itemHeightSum).toEqualWithTolerance(stackHeight); 158 | expect(itemDims0).toMatchObject({ width: 72, height: 36 }); 159 | expect(itemDims1).toMatchObject({ width: 144, height: 72 }); 160 | expect(itemDims2).toMatchObject({ width: 64, height: 48 }); 161 | }); 162 | 163 | // (horizontal) children with fixed width and unspecified height -> item height is the same as stack height 164 | test("(horizontal) children with fixed width and unspecified height", async ({ 165 | page, 166 | initTestBed, 167 | }) => { 168 | await initTestBed(` 169 | <Stack testId="stack" orientation="horizontal" gap="0"> 170 | <Text testId="item_0" backgroundColor="cyan" width="72px">W: 72</Text> 171 | <Text testId="item_1" backgroundColor="yellow" width="144px">W: 144</Text> 172 | <Text testId="item_2" backgroundColor="lightgreen" width="48px">W: 48 + long, long, long text</Text> 173 | </Stack> 174 | `); 175 | 176 | const { height: stackHeight, width: stackWidth } = await getBounds(page.getByTestId("stack")); 177 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 178 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 179 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 180 | 181 | const itemWidthSum = itemDims0.width + itemDims1.width + itemDims2.width; 182 | 183 | expect(itemWidthSum).toEqualWithTolerance(itemDims2.right); 184 | expect(itemWidthSum).toBeLessThan(stackWidth); 185 | 186 | expect(itemDims0.height).toEqual(stackHeight); 187 | expect(itemDims1.height).toEqual(stackHeight); 188 | expect(itemDims2.height).toEqual(stackHeight); 189 | 190 | expect(itemDims0.width).toEqual(72); 191 | expect(itemDims1.width).toEqual(144); 192 | expect(itemDims2.width).toEqual(48); 193 | }); 194 | 195 | // (vertical) children with fixed height and unspecified width -> width fills available space 196 | test("(vertical) children with fixed height and unspecified width ", async ({ 197 | page, 198 | initTestBed, 199 | }) => { 200 | await initTestBed(` 201 | <Stack testId="stack" orientation="vertical" gap="0"> 202 | <Text testId="item_0" backgroundColor="cyan" height="36px">H: 36</Text> 203 | <Text testId="item_1" backgroundColor="yellow" height="72px">H: 72</Text> 204 | <Text testId="item_2" backgroundColor="lightgreen" height="48px">H: 48 + long, long, long text</Text> 205 | </Stack> 206 | `); 207 | 208 | const { height: stackHeight, width: stackWidth } = await getBounds(page.getByTestId("stack")); 209 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 210 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 211 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 212 | 213 | const itemHeightSum = 36 + 72 + 48; 214 | 215 | expect(itemHeightSum).toEqualWithTolerance(itemDims2.bottom); 216 | expect(itemHeightSum).toEqualWithTolerance(stackHeight); 217 | 218 | expect(itemDims0.width).toEqual(stackWidth); 219 | expect(itemDims1.width).toEqual(stackWidth); 220 | expect(itemDims2.width).toEqual(stackWidth); 221 | 222 | expect(itemDims0.height).toEqual(36); 223 | expect(itemDims1.height).toEqual(72); 224 | expect(itemDims2.height).toEqual(48); 225 | }); 226 | 227 | // (horizontal) children with fixed height and unspecified width -> item widths are conent size, item heights are not altered 228 | test("(horizontal) children with fixed height and unspecified width", async ({ 229 | page, 230 | initTestBed, 231 | }) => { 232 | await initTestBed(` 233 | <Stack testId="stack" orientation="horizontal" gap="0"> 234 | <Text testId="item_0" backgroundColor="cyan" height="36px">H: 36</Text> 235 | <Text testId="item_1" backgroundColor="yellow" height="72px">H: 72</Text> 236 | <Text testId="item_2" backgroundColor="lightgreen" height="48px">H: 48</Text> 237 | </Stack> 238 | `); 239 | 240 | const { height: stackHeight, width: stackWidth } = await getBounds(page.getByTestId("stack")); 241 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 242 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 243 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 244 | 245 | const itemWidthSum = itemDims0.width + itemDims1.width + itemDims2.width; 246 | const tallestItemHeight = 72; 247 | 248 | expect(itemWidthSum).toBeLessThan(stackWidth); 249 | expect(stackHeight).toEqual(tallestItemHeight); 250 | 251 | expect(itemDims0.height).toEqual(36); 252 | expect(itemDims1.height).toEqual(72); 253 | expect(itemDims2.height).toEqual(48); 254 | }); 255 | 256 | // (vertical) children with fixed width and unspecified height -> item heights are content size, widths are not altered 257 | test("(vertical) children with fixed width and unspecified height", async ({ 258 | page, 259 | initTestBed, 260 | }) => { 261 | await initTestBed(` 262 | <Stack testId="stack" orientation="vertical" gap="0"> 263 | <Text testId="item_0" backgroundColor="cyan" width="72px">W: 72</Text> 264 | <Text testId="item_1" backgroundColor="yellow" width="144px">W: 144</Text> 265 | <Text testId="item_2" backgroundColor="lightgreen" width="48px">W: 48 + long, long, long text</Text> 266 | </Stack> 267 | `); 268 | 269 | const { height: stackHeight } = await getBounds(page.getByTestId("stack")); 270 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 271 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 272 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 273 | 274 | const itemHeightSum = itemDims0.height + itemDims1.height + itemDims2.height; 275 | 276 | expect(itemHeightSum).toEqualWithTolerance(itemDims2.bottom); 277 | expect(itemHeightSum).toEqualWithTolerance(stackHeight); 278 | 279 | expect(itemDims0.width).toEqual(72); 280 | expect(itemDims1.width).toEqual(144); 281 | expect(itemDims2.width).toEqual(48); 282 | }); 283 | 284 | test("(horizontal) percentage sizing", async ({ page, initTestBed }) => { 285 | await initTestBed(` 286 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" gap="0"> 287 | <Text testId="item_0" backgroundColor="cyan" width="20%">W: 20%</Text> 288 | <H2 testId="item_1" backgroundColor="yellow" width="50%">W: 50%</H2> 289 | <H5 testId="item_2" backgroundColor="lightgreen" width="20%">W: 20% + long, long, long text</H5> 290 | </Stack> 291 | `); 292 | 293 | const { width: stackWidth } = await getBounds(page.getByTestId("stack")); 294 | const { width: itemWidth0 } = await getBounds(page.getByTestId("item_0")); 295 | const { width: itemWidth1 } = await getBounds(page.getByTestId("item_1")); 296 | const { width: itemWidth2, right: lastItemRight } = await getBounds(page.getByTestId("item_2")); 297 | const itemWidthSum = itemWidth0 + itemWidth1 + itemWidth2; 298 | 299 | expect(stackWidth).toEqual(PAGE_WIDTH); 300 | expect(itemWidthSum).toEqualWithTolerance(lastItemRight); 301 | expect(itemWidth0).toEqualWithTolerance(0.2 * stackWidth); 302 | expect(itemWidth1).toEqualWithTolerance(0.5 * stackWidth); 303 | expect(itemWidth2).toEqualWithTolerance(0.2 * stackWidth); 304 | }); 305 | 306 | test("(vertical) percentage sizing", async ({ page, initTestBed }) => { 307 | await initTestBed(` 308 | <Stack testId="stack" height="180px" orientation="vertical" backgroundColor="lightgray" gap="0"> 309 | <Text testId="item_0" backgroundColor="cyan" height="20%">W: 20%</Text> 310 | <Text testId="item_1" backgroundColor="yellow" height="50%">W: 50%</Text> 311 | <Text testId="item_2" backgroundColor="lightgreen" height="20%">W: 20% + long, long, long text</Text> 312 | </Stack> 313 | `); 314 | 315 | const { height: stackHeight, width: stackWidth } = await getBounds(page.getByTestId("stack")); 316 | const { height: itemHeight0, width: itemWidth0 } = await getBounds(page.getByTestId("item_0")); 317 | const { height: itemHeight1 } = await getBounds(page.getByTestId("item_1")); 318 | const { height: itemHeight2, bottom: lastItemBottom } = await getBounds( 319 | page.getByTestId("item_2"), 320 | ); 321 | const itemHeightSum = itemHeight0 + itemHeight1 + itemHeight2; 322 | 323 | expect(itemWidth0).toEqual(stackWidth); 324 | expect(itemHeightSum).toEqualWithTolerance(lastItemBottom); 325 | expect(itemHeight0).toEqualWithTolerance(0.2 * stackHeight); 326 | expect(itemHeight1).toEqualWithTolerance(0.5 * stackHeight); 327 | expect(itemHeight2).toEqualWithTolerance(0.2 * stackHeight); 328 | }); 329 | 330 | test("(horizontal) percentage sizing fully filled", async ({ page, initTestBed }) => { 331 | await initTestBed(` 332 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" gap="0"> 333 | <Text testId="item_0" backgroundColor="cyan" width="20%">W: 20%</Text> 334 | <H2 testId="item_1" backgroundColor="yellow" width="50%">W: 50%</H2> 335 | <H5 testId="item_2" backgroundColor="lightgreen" width="30%">W: 30% + long, long, long text</H5> 336 | </Stack> 337 | `); 338 | const { width: stackWidth } = await getBounds(page.getByTestId("stack")); 339 | const { right: lastItemRight } = await getBounds(page.getByTestId("item_2")); 340 | expect(stackWidth).toEqualWithTolerance(lastItemRight); 341 | }); 342 | 343 | test("(vertical) percentage sizing fully filled", async ({ page, initTestBed }) => { 344 | await initTestBed(` 345 | <Stack testId="stack" height="180px" orientation="vertical" backgroundColor="lightgray" gap="0"> 346 | <Text testId="item_0" backgroundColor="cyan" height="20%">W: 20%</Text> 347 | <Text testId="item_1" backgroundColor="yellow" height="50%">W: 50%</Text> 348 | <Text testId="item_2" backgroundColor="lightgreen" height="30%">W: 30% + long, long, long text</Text> 349 | </Stack> 350 | `); 351 | const { height: stackHeight } = await getBounds(page.getByTestId("stack")); 352 | const { bottom: lastItemBottom } = await getBounds(page.getByTestId("item_2")); 353 | expect(stackHeight).toEqualWithTolerance(lastItemBottom); 354 | }); 355 | 356 | // (horizontal) percentage overflow X 357 | test("(horizontal) percentage overflow X", async ({ page, initTestBed }) => { 358 | await initTestBed(` 359 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" gap="0"> 360 | <Text testId="item_0" backgroundColor="cyan" width="30%">W: 30%</Text> 361 | <H2 testId="item_1" backgroundColor="yellow" width="50%">W: 50%</H2> 362 | <H5 testId="item_2" backgroundColor="lightgreen" width="40%">W: 40% + long, long, long text</H5> 363 | </Stack> 364 | `); 365 | const isOverflown = await overflows(page.getByTestId("stack"), "x"); 366 | expect(isOverflown).toEqual(true); 367 | }); 368 | 369 | // (vertical) percentage overflow Y 370 | test("(vertical) percentage overflow Y", async ({ page, initTestBed }) => { 371 | await initTestBed(` 372 | <Stack testId="stack" orientation="vertical" height="180px" backgroundColor="lightgray" gap="0"> 373 | <Text testId="item_0" backgroundColor="cyan" height="30%">H: 30%</Text> 374 | <H2 testId="item_1" backgroundColor="yellow" height="60%">H: 60%</H2> 375 | <H5 testId="item_2" backgroundColor="lightgreen" height="20%">H: 20% + long, long, long text</H5> 376 | </Stack> 377 | `); 378 | const isOverflown = await overflows(page.getByTestId("stack"), "y"); 379 | expect(isOverflown).toEqual(true); 380 | }); 381 | 382 | test("(horizontal) star sizing", async ({ page, initTestBed }) => { 383 | await initTestBed(` 384 | <Stack testId="stack" width="100%" orientation="horizontal" backgroundColor="lightgray" gap="0"> 385 | <Text testId="item_0" backgroundColor="cyan" width="100px">W: 100</Text> 386 | <Text testId="item_1" backgroundColor="yellow" width="3*">W: 3*</Text> 387 | <Text testId="item_2" backgroundColor="lightgreen" width="*">W: *</Text> 388 | </Stack> 389 | `); 390 | const { width: stackWidth } = await getBounds(page.getByTestId("stack")); 391 | const { width: itemWidth0 } = await getBounds(page.getByTestId("item_0")); 392 | const { width: itemWidth1 } = await getBounds(page.getByTestId("item_1")); 393 | const { width: itemWidth2, right: lastItemRight } = await getBounds(page.getByTestId("item_2")); 394 | const itemWidthSum = itemWidth0 + itemWidth1 + itemWidth2; 395 | 396 | const expectedItemWidth0 = 100; 397 | const expectedItemWidth1 = (stackWidth - 100) * (3 / 4); 398 | const expectedItemWidth2 = (stackWidth - 100) * (1 / 4); 399 | 400 | expect(itemWidthSum).toEqualWithTolerance(lastItemRight); 401 | expect(itemWidth0).toEqual(expectedItemWidth0); 402 | expect(itemWidth1).toEqualWithTolerance(expectedItemWidth1); 403 | expect(itemWidth2).toEqualWithTolerance(expectedItemWidth2); 404 | }); 405 | 406 | // (horizontal) star sizing comparison -> Larger Stack have larger star sized children, px sizes are same 407 | test("(horizontal) star sizing comparison", async ({ page, initTestBed }) => { 408 | await initTestBed(` 409 | <Fragment> 410 | <Stack testId="stack_0" orientation="horizontal" width="600px" gap="0"> 411 | <Text testId="ref_item_0" backgroundColor="cyan" width="100px">W: 100</Text> 412 | <Text testId="ref_item_1" backgroundColor="yellow" width="3*">W: 3*</Text> 413 | <Text testId="ref_item_2" backgroundColor="lightgreen" width="*">W: *</Text> 414 | </Stack> 415 | <Stack testId="stack_1" orientation="horizontal" width="300px" gap="0"> 416 | <Text testId="item_0" backgroundColor="cyan" width="100px">W: 100</Text> 417 | <Text testId="item_1" backgroundColor="yellow" width="3*">W: 3*</Text> 418 | <Text testId="item_2" backgroundColor="lightgreen" width="*">W: *</Text> 419 | </Stack> 420 | </Fragment> 421 | `); 422 | const { width: refItemWidth0 } = await getBounds(page.getByTestId("ref_item_0")); 423 | const { width: refItemWidth1 } = await getBounds(page.getByTestId("ref_item_1")); 424 | const { width: refItemWidth2 } = await getBounds(page.getByTestId("ref_item_2")); 425 | const { width: itemWidth0 } = await getBounds(page.getByTestId("item_0")); 426 | const { width: itemWidth1 } = await getBounds(page.getByTestId("item_1")); 427 | const { width: itemWidth2 } = await getBounds(page.getByTestId("item_2")); 428 | 429 | expect(refItemWidth0).toEqual(itemWidth0); 430 | expect(refItemWidth1).toBeGreaterThan(itemWidth1); 431 | expect(refItemWidth2).toBeGreaterThan(itemWidth2); 432 | }); 433 | 434 | // (vertical) star sizing comparison -> Larger Stack have larger star sized children, px sizes are same 435 | test("(vertical) star sizing comparison", async ({ page, initTestBed }) => { 436 | await initTestBed(` 437 | <Fragment> 438 | <Stack testId="stack_0" orientation="vertical" height="600px" gap="0"> 439 | <Text testId="ref_item_0" backgroundColor="cyan" height="100px">W: 100</Text> 440 | <Text testId="ref_item_1" backgroundColor="yellow" height="3*">W: 3*</Text> 441 | <Text testId="ref_item_2" backgroundColor="lightgreen" height="*">W: *</Text> 442 | </Stack> 443 | <Stack testId="stack_1" orientation="vertical" height="300px" gap="0"> 444 | <Text testId="item_0" backgroundColor="cyan" height="100px">W: 100</Text> 445 | <Text testId="item_1" backgroundColor="yellow" height="3*">W: 3*</Text> 446 | <Text testId="item_2" backgroundColor="lightgreen" height="*">W: *</Text> 447 | </Stack> 448 | </Fragment> 449 | `); 450 | const { height: refItemHeight0 } = await getBounds(page.getByTestId("ref_item_0")); 451 | const { height: refItemHeight1 } = await getBounds(page.getByTestId("ref_item_1")); 452 | const { height: refItemHeight2 } = await getBounds(page.getByTestId("ref_item_2")); 453 | const { height: itemHeight0 } = await getBounds(page.getByTestId("item_0")); 454 | const { height: itemHeight1 } = await getBounds(page.getByTestId("item_1")); 455 | const { height: itemHeight2 } = await getBounds(page.getByTestId("item_2")); 456 | 457 | expect(refItemHeight0).toEqual(itemHeight0); 458 | expect(refItemHeight1).toBeGreaterThan(itemHeight1); 459 | expect(refItemHeight2).toBeGreaterThan(itemHeight2); 460 | }); 461 | 462 | // (horizontal) gaps + percentage -> children use available space 463 | test("(horizontal) paddings + percentage", async ({ page, initTestBed }) => { 464 | await initTestBed(` 465 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" gap="0" 466 | paddingHorizontal="50px" paddingVertical="50px"> 467 | <Text testId="item_0" backgroundColor="cyan" width="25%">W: 25%</Text> 468 | <Text testId="item_1" backgroundColor="yellow" width="50%">W: 50%</Text> 469 | <Text testId="item_2" backgroundColor="lightgreen" width="25%">W: 25%</Text> 470 | </Stack> 471 | `); 472 | const { width: stackWidth } = await getBounds(page.getByTestId("stack")); 473 | const { width: itemWidth0, left: firstItemLeft } = await getBounds(page.getByTestId("item_0")); 474 | const { width: itemWidth1 } = await getBounds(page.getByTestId("item_1")); 475 | const { width: itemWidth2, right: lastItemRight } = await getBounds(page.getByTestId("item_2")); 476 | 477 | const itemWidthSum = itemWidth0 + itemWidth1 + itemWidth2; 478 | expect(firstItemLeft).toEqual(50); 479 | expect(lastItemRight).toEqual(stackWidth - 50); 480 | expect(itemWidthSum).toEqualWithTolerance(stackWidth - 2 * 50); 481 | }); 482 | 483 | // (vertical) gaps + percentage -> children use available space 484 | test("(vertical) paddings + percentage", async ({ page, initTestBed }) => { 485 | await initTestBed(` 486 | <Stack testId="stack" orientation="vertical" backgroundColor="lightgray" gap="0" 487 | height="240px" paddingHorizontal="50px" paddingVertical="50px"> 488 | <Text testId="item_0" backgroundColor="cyan" height="25%">H: 25%</Text> 489 | <Text testId="item_1" backgroundColor="yellow" height="50%">H: 50%</Text> 490 | <Text testId="item_2" backgroundColor="lightgreen" height="25%">H: 25%</Text> 491 | </Stack> 492 | `); 493 | const { height: stackHeight } = await getBounds(page.getByTestId("stack")); 494 | const { top: firstItemTop } = await getBounds(page.getByTestId("item_0")); 495 | const { bottom: lastItemBottom } = await getBounds(page.getByTestId("item_2")); 496 | 497 | expect(firstItemTop).toEqual(50); 498 | expect(lastItemBottom).toEqual(stackHeight - 50); 499 | }); 500 | 501 | test("(horizontal) gaps", async ({ page, initTestBed }) => { 502 | await initTestBed(` 503 | <Stack orientation="horizontal" backgroundColor="lightgray" gap="50px"> 504 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 505 | <Stack testId="item_1" backgroundColor="green" height="36px" width="36px" /> 506 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="36px" /> 507 | </Stack> 508 | `); 509 | 510 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 511 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 512 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 513 | 514 | expect(itemDims0.left).toEqual(0); 515 | expect(itemDims1.left).toEqual(itemDims0.right + 50); 516 | expect(itemDims2.left).toEqual(2 * (itemDims0.right + 50)); 517 | }); 518 | 519 | test("(vertical) gaps", async ({ page, initTestBed }) => { 520 | await initTestBed(` 521 | <Stack orientation="vertical" backgroundColor="lightgray" gap="50px"> 522 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 523 | <Stack testId="item_1" backgroundColor="green" height="36px" width="36px" /> 524 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="36px" /> 525 | </Stack> 526 | `); 527 | 528 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 529 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 530 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 531 | 532 | expect(itemDims0.top).toEqual(0); 533 | expect(itemDims1.top).toEqual(itemDims0.bottom + 50); 534 | expect(itemDims2.top).toEqual(2 * (itemDims0.bottom + 50)); 535 | }); 536 | 537 | // (horizontal) gaps + percentage -> gaps push the content out of the container 538 | test("(horizontal) gaps + percentage", async ({ page, initTestBed }) => { 539 | await initTestBed(` 540 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" padding="1rem" gap="2rem"> 541 | <Stack backgroundColor="red" height="36px" width="25%" /> 542 | <Stack backgroundColor="green" height="36px" width="50%" /> 543 | <Stack backgroundColor="blue" height="36px" width="25%" /> 544 | </Stack> 545 | `); 546 | const isOverflown = await overflows(page.getByTestId("stack"), "x"); 547 | expect(isOverflown).toEqual(true); 548 | }); 549 | 550 | // (horizontal) gaps + star sizing -> gaps don't push the content out of the container 551 | test("(horizontal) gaps + star sizing", async ({ page, initTestBed }) => { 552 | await initTestBed(` 553 | <Stack testId="stack" orientation="horizontal" backgroundColor="lightgray" padding="1rem" gap="2rem"> 554 | <Stack backgroundColor="red" height="36px" width="*" /> 555 | <Stack backgroundColor="green" height="36px" width="*" /> 556 | <Stack backgroundColor="blue" height="36px" width="*" /> 557 | <Stack backgroundColor="purple" height="36px" width="*" /> 558 | </Stack> 559 | `); 560 | const isOverflown = await overflows(page.getByTestId("stack"), "x"); 561 | expect(isOverflown).toEqual(false); 562 | }); 563 | 564 | test("(horizontal) rtl rendering direction", async ({ page, initTestBed }) => { 565 | await initTestBed(` 566 | <Stack testId="stack" orientation="horizontal" gap="1rem" direction="rtl"> 567 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 568 | <Stack testId="item_1" backgroundColor="green" height="36px" width="36px" /> 569 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="36px" /> 570 | </Stack> 571 | `); 572 | 573 | const { right: stackRight } = await getBounds(page.getByTestId("stack")); 574 | const { right: itemRight0, left: itemLeft0 } = await getBounds(page.getByTestId("item_0")); 575 | const { left: itemLeft1 } = await getBounds(page.getByTestId("item_1")); 576 | const { left: itemLeft2 } = await getBounds(page.getByTestId("item_2")); 577 | 578 | expect(itemLeft2).toBeLessThan(itemLeft1); 579 | expect(itemLeft1).toBeLessThan(itemLeft0); 580 | expect(itemRight0).toEqual(stackRight); 581 | }); 582 | 583 | test("(horizontal) reverse rendering direction", async ({ page, initTestBed }) => { 584 | await initTestBed(` 585 | <Stack testId="stack" orientation="horizontal" gap="1rem" reverse="true"> 586 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 587 | <Stack testId="item_1" backgroundColor="green" height="36px" width="36px" /> 588 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="36px" /> 589 | </Stack> 590 | `); 591 | const { right: stackRight } = await getBounds(page.getByTestId("stack")); 592 | const { right: itemRight0, left: itemLeft0 } = await getBounds(page.getByTestId("item_0")); 593 | const { left: itemLeft1 } = await getBounds(page.getByTestId("item_1")); 594 | const { left: itemLeft2 } = await getBounds(page.getByTestId("item_2")); 595 | 596 | expect(itemLeft2).toBeLessThan(itemLeft1); 597 | expect(itemLeft1).toBeLessThan(itemLeft0); 598 | expect(itemRight0).toEqual(stackRight); 599 | }); 600 | 601 | test("(vertical) reverse rendering direction", async ({ page, initTestBed }) => { 602 | await initTestBed(` 603 | <Stack testId="stack" orientation="vertical" gap="1rem" reverse="true"> 604 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 605 | <Stack testId="item_1" backgroundColor="green" height="36px" width="36px" /> 606 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="36px" /> 607 | </Stack> 608 | `); 609 | const { bottom: stackBottom } = await getBounds(page.getByTestId("stack")); 610 | const { bottom: itemBottom0, top: itemTop0 } = await getBounds(page.getByTestId("item_0")); 611 | const { top: itemTop1 } = await getBounds(page.getByTestId("item_1")); 612 | const { top: itemTop2 } = await getBounds(page.getByTestId("item_2")); 613 | 614 | expect(itemTop2).toBeLessThan(itemTop1); 615 | expect(itemTop1).toBeLessThan(itemTop0); 616 | expect(itemBottom0).toEqual(stackBottom); 617 | }); 618 | 619 | test("(horizontal) content wrapping", async ({ page, initTestBed }) => { 620 | await initTestBed(` 621 | <Stack testId="stack" orientation="horizontal" gap="1rem" wrapContent="true"> 622 | <Stack testId="item_0" backgroundColor="red" height="36px" width="25%" /> 623 | <Stack testId="item_1" backgroundColor="green" height="36px" width="40%" /> 624 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="20%" /> 625 | <Stack testId="item_3" backgroundColor="purple" height="36px" width="30%" /> 626 | </Stack> 627 | `); 628 | const { right: stackRight, bottom: stackBottom } = await getBounds(page.getByTestId("stack")); 629 | const { left: itemLeft0, bottom: itemBottom0 } = await getBounds(page.getByTestId("item_0")); 630 | const { left: itemLeft1 } = await getBounds(page.getByTestId("item_1")); 631 | const { left: itemLeft2, right: itemRight2 } = await getBounds(page.getByTestId("item_2")); 632 | const { 633 | left: itemLeft3, 634 | bottom: itemBottom3, 635 | top: itemTop3, 636 | } = await getBounds(page.getByTestId("item_3")); 637 | 638 | expect(itemLeft0).toBeLessThan(itemLeft1); 639 | expect(itemLeft1).toBeLessThan(itemLeft2); 640 | expect(itemRight2).toBeLessThan(stackRight); 641 | 642 | expect(itemBottom0).toBeLessThan(itemTop3); 643 | expect(itemLeft3).toEqual(itemLeft0); 644 | expect(itemBottom3).toEqual(stackBottom); 645 | }); 646 | 647 | test("(horizontal) horizontalAlignment center", async ({ page, initTestBed }) => { 648 | await initTestBed(` 649 | <Stack testId="stack" orientation="horizontal" horizontalAlignment="center" gap="0"> 650 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 651 | <Stack testId="item_1" backgroundColor="green" height="36px" width="36px" /> 652 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="36px" /> 653 | </Stack> 654 | `); 655 | const { right: stackRight } = await getBounds(page.getByTestId("stack")); 656 | const { left: itemLeft0 } = await getBounds(page.getByTestId("item_0")); 657 | const { right: itemRight2 } = await getBounds(page.getByTestId("item_2")); 658 | const itemWidthSum = 3 * 36; 659 | 660 | expect(itemLeft0).toEqual(stackRight / 2 - itemWidthSum / 2); 661 | expect(itemRight2).toEqual(stackRight / 2 + itemWidthSum / 2); 662 | }); 663 | 664 | test("(vertical) horizontal alignment end", async ({ page, initTestBed }) => { 665 | await initTestBed(` 666 | <Stack testId="stack" orientation="vertical" horizontalAlignment="end" gap="0"> 667 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 668 | <Stack testId="item_1" backgroundColor="green" height="36px" width="36px" /> 669 | <Stack testId="item_2" backgroundColor="blue" height="36px" width="36px" /> 670 | </Stack> 671 | `); 672 | const { right: stackRight } = await getBounds(page.getByTestId("stack")); 673 | const { right: itemRight0 } = await getBounds(page.getByTestId("item_0")); 674 | const { right: itemRight1 } = await getBounds(page.getByTestId("item_1")); 675 | const { right: itemRight2 } = await getBounds(page.getByTestId("item_2")); 676 | 677 | expect(itemRight0).toEqual(stackRight); 678 | expect(itemRight1).toEqual(stackRight); 679 | expect(itemRight2).toEqual(stackRight); 680 | }); 681 | 682 | test("(horizontal) verticalAlignment center", async ({ page, initTestBed }) => { 683 | await initTestBed(` 684 | <Stack testId="stack" orientation="horizontal" gap="1rem" verticalAlignment="center"> 685 | <Stack testId="item_0" backgroundColor="red" height="36px" width="36px" /> 686 | <Stack testId="item_1" backgroundColor="green" height="72px" width="36px" /> 687 | <Stack testId="item_2" backgroundColor="blue" height="48px" width="36px" /> 688 | </Stack> 689 | `); 690 | const stackDims = await getBounds(page.getByTestId("stack")); 691 | const itemDims0 = await getBounds(page.getByTestId("item_0")); 692 | const itemDims1 = await getBounds(page.getByTestId("item_1")); 693 | const itemDims2 = await getBounds(page.getByTestId("item_2")); 694 | 695 | expect(itemDims0.top).toEqual(stackDims.height / 2 - itemDims0.height / 2); 696 | expect(itemDims0.bottom).toEqual(stackDims.height / 2 + itemDims0.height / 2); 697 | 698 | expect(itemDims1.top).toEqual(stackDims.top); 699 | expect(itemDims1.bottom).toEqual(stackDims.bottom); 700 | 701 | expect(itemDims2.top).toEqual(stackDims.height / 2 - itemDims2.height / 2); 702 | expect(itemDims2.bottom).toEqual(stackDims.height / 2 + itemDims2.height / 2); 703 | }); 704 | 705 | test(`overflowY="scroll"`, async ({ page, initTestBed }) => { 706 | await initTestBed(` 707 | <VStack testId="stack" width="100px" height="60px" backgroundColor="cyan" 708 | overflowY="scroll"> 709 | <Text testId="item0"> 710 | As its container width and height are fixed, this long text does not 711 | fit into it; it will overflow. 712 | </Text> 713 | </VStack> 714 | `); 715 | const { scrollHeight, clientHeight } = await page 716 | .getByTestId("stack") 717 | .evaluate((elem) => ({ scrollHeight: elem.scrollHeight, clientHeight: elem.clientHeight })); 718 | 719 | expect(scrollHeight).toBeGreaterThan(clientHeight); 720 | }); 721 | 722 | test(`overflowX="scroll"`, async ({ page, initTestBed }) => { 723 | await initTestBed(` 724 | <HStack testId="stack" width="100px" height="60px" backgroundColor="cyan" 725 | overflowX="scroll"> 726 | <Text testId="item0"> 727 | As its container width and height are fixed, this long text does not 728 | fit into it; it will overflow. 729 | </Text> 730 | </HStack> 731 | `); 732 | const { scrollWidth, clientWidth } = await page 733 | .getByTestId("stack") 734 | .evaluate((elem) => ({ scrollWidth: elem.scrollWidth, clientWidth: elem.clientWidth })); 735 | 736 | expect(scrollWidth).toBeGreaterThan(clientWidth); 737 | }); 738 | 739 | // When you set overflowX to scroll, it will automatically set overflowY to scroll if the text exceeds the viewport vertically 740 | test(`overflowX sets overflowY`, async ({ page, initTestBed }) => { 741 | await initTestBed(` 742 | <VStack testId="stack" height="50px" width="300px" backgroundColor="cyan" 743 | overflowX="scroll"> 744 | <Text width="400px" backgroundColor="silver" opacity="0.8"> 745 | This text sets its size explicitly bigger than its container. 746 | As it does not fit into the container's viewport, it overflows. 747 | However, its container supports horizontal scrolling so you can 748 | see its content. 749 | </Text> 750 | </VStack> 751 | `); 752 | const { scrollHeight, clientHeight, scrollWidth, clientWidth } = await page 753 | .getByTestId("stack") 754 | .evaluate((elem) => ({ 755 | scrollHeight: elem.scrollHeight, 756 | clientHeight: elem.clientHeight, 757 | scrollWidth: elem.scrollWidth, 758 | clientWidth: elem.clientWidth, 759 | })); 760 | 761 | expect(scrollWidth).toBeGreaterThan(clientWidth); 762 | expect(scrollHeight).toBeGreaterThan(clientHeight); 763 | }); 764 | }); 765 | ``` -------------------------------------------------------------------------------- /xmlui/dev-docs/containers.md: -------------------------------------------------------------------------------- ```markdown 1 | # XMLUI Container-Based State Management 2 | 3 | A deep dive into XMLUI's hierarchical container system for managing component state, variables, and reactive data binding. 4 | 5 | ## Fundamentals of XMLUI Reactivity 6 | 7 | XMLUI implements **automatic reactivity** - a programming model where UI updates happen automatically when underlying data changes, without requiring manual DOM manipulation or explicit update calls. 8 | 9 | ### The Reactive Loop 10 | 11 | XMLUI follows a simple reactive cycle: 12 | 1. **Initial Render**: Framework renders the complete UI from declarative markup 13 | 2. **Event Occurrence**: User interactions or system events trigger changes 14 | 3. **Automatic State Detection**: Framework detects state mutations transparently 15 | 4. **Selective UI Refresh**: Only components affected by the state change re-render 16 | 5. **Loop Continuation**: Process repeats for subsequent events 17 | 18 | From a developer perspective, reactivity means: 19 | - **Write declarative expressions**: Use `{count}` to display values, `count++` to modify them 20 | - **No manual updates**: Framework automatically handles UI synchronization 21 | - **Transparent mutations**: Change variables directly without special APIs 22 | - **Predictable behavior**: UI always reflects current state consistently 23 | 24 | ### The Role of Containers in Reactivity Implementation 25 | 26 | Containers are the **core mechanism** that enables XMLUI's automatic reactivity. They provide the infrastructure necessary to detect state changes, coordinate updates, and maintain UI consistency. 27 | 28 | Reactivity requires several technical capabilities: 29 | - **State Storage**: A place to hold component variables and data 30 | - **Change Detection**: Mechanism to detect when state mutations occur 31 | - **Update Coordination**: System to propagate changes to affected UI components 32 | - **Scope Management**: Control over which components can access which state 33 | - **Performance Optimization**: Efficient updates that don't block the UI 34 | 35 | Containers provide all these capabilities through a hierarchical architecture that mirrors the component structure. 36 | 37 | ## Automatic Container Wrapping 38 | 39 | XMLUI **automatically wraps components with containers** when they need reactive capabilities. You declare variables or data sources, and the framework creates the container infrastructure. 40 | 41 | The framework creates containers for these cases: 42 | - **App Root Container** - Wraps the entire application entry point 43 | - **User-Defined Component Containers** - Each component instance gets isolated state 44 | - **Variables** - Reactive state holders that trigger UI updates 45 | - **Loaders** - Handle asynchronous data operations 46 | - **Uses Declarations** - Control state inheritance from parents 47 | - **Context Variables** - Framework-injected variables (routing, forms) 48 | - **Scripts** - JavaScript blocks with variables and functions 49 | - **Code-Behind Files** - External script files for application root or component roots 50 | 51 | Every component receives an identifier (user-defined or framework-assigned). **User-defined IDs enable programmatic component interaction** - accessing methods, properties, and APIs. Framework IDs handle internal state. Component IDs scope to their declaring file (.xmlui). These IDs are stored in the App Root and User-Defined Component Containers. 52 | 53 | ### App Root Container 54 | For every XMLUI application, a root container is automatically created to wrap the entire application entry point (typically Main.xmlui). This ensures there's always a top-level container to manage application-wide state. 55 | 56 | **Example - Main.xmlui:** 57 | ```xml 58 | <!-- File: Main.xmlui --> 59 | <App> 60 | <VStack> 61 | <Text id="title">Welcome to My App</Text> 62 | <Button id="actionBtn" label="Click Me" /> 63 | <Button label="Cancel" /> 64 | </VStack> 65 | </App> 66 | ``` 67 | 68 | ```mermaid 69 | %%{init: {"block": {"padding": 8}}}%% 70 | block-beta 71 | columns 1 72 | 73 | AR["<div style='text-align: left; width: 100%; padding: 0 15px;'>🏠 App Root Container<br/>💡 Container State:<br/> ├─ title → Text reference<br/> ├─ actionBtn → Button reference<br/> └─ [[cancel]] → Button reference<br/>🌳 Component Tree:<br/> VStack:<br/> ├─ Text (id: title)<br/> ├─ Button (id: actionBtn)<br/> └─ Button (id: [[cancel]])</div>"] 74 | 75 | classDef rootContainer fill:#f0f0f0,stroke:#888,stroke-width:2px,color:#333,padding-left:15px,padding-right:15px 76 | class AR rootContainer 77 | ``` 78 | 79 | > **Note**: `[[cancel]]` is an framework-assigned ID for the Cancel button since it doesn't have an explicit `id` attribute. 80 | 81 | In this example, XMLUI automatically creates a root container that stores the IDs for `title`, `actionBtn`, and `[[cancel]]`, making them accessible throughout the application. 82 | 83 | ### User-Defined Component Instance Containers 84 | Each time you use a user-defined component (created with `<Component name="...">`) in your markup, XMLUI automatically creates a container for that specific instance. This ensures **Instance Isolation** - each component usage gets its own state container with isolated internal state. 85 | 86 | **Example Files:** 87 | 88 | **File: Main.xmlui** 89 | ```xml 90 | <!-- File: Main.xmlui --> 91 | <App> 92 | <VStack> 93 | <!-- Each MyButton usage creates its own container --> 94 | <MyButton label="Save" /> <!-- Creates Container A --> 95 | <MyButton label="Cancel" /> <!-- Creates Container B --> 96 | <MyButton label="Delete" /> <!-- Creates Container C --> 97 | </VStack> 98 | </App> 99 | ``` 100 | 101 | **File: components/MyButton.xmlui** 102 | ```xml 103 | <!-- File: components/MyButton.xmlui --> 104 | <Component name="MyButton" var.count="{0}"> 105 | <Button 106 | label="{$props.label} ({count})" 107 | onClick="count++" 108 | /> 109 | </Component> 110 | ``` 111 | 112 | ```mermaid 113 | %%{init: {"block": {"padding": 8}}}%% 114 | block-beta 115 | columns 3 116 | 117 | block:AppRoot:3 118 | AR["<div style='text-align: left; width: 100%'>🏠 App Root Container<br/>💡 Container State: MyButton instances<br/>🌳 Component Tree:<br/> VStack:<br/> ├─ MyButton (Container A)<br/> ├─ MyButton (Container B)<br/> └─ MyButton (Container C)</div>"] 119 | end 120 | 121 | CA["<div style='text-align: left; width: 100%'>🏠 Container A<br/>💡 State:<br/> ├─ count: 0<br/> └─ $props.label: 'Save'<br/>🌳 Component: Button (Save 0)</div>"] 122 | 123 | CB["<div style='text-align: left; width: 100%'>🏠 Container B<br/>💡 State:<br/> ├─ count: 0<br/> └─ $props.label: 'Cancel'<br/>🌳 Component: Button (Cancel 0)</div>"] 124 | 125 | CC["<div style='text-align: left; width: 100%'>🏠 Container C<br/>💡 State:<br/> ├─ count: 0<br/> └─ $props.label: 'Delete'<br/>🌳 Component: Button (Delete 0)</div>"] 126 | 127 | classDef rootContainer fill:#f0f0f0,stroke:#888,stroke-width:2px,color:#333 128 | classDef innerContainer fill:#f8f8f8,stroke:#aaa,stroke-width:1px,color:#333 129 | 130 | class AR,CC,CB,CA rootContainer 131 | class ARS,ART,CHC,CHB,CHA,CSC,CSB,CSA innerContainer 132 | ``` 133 | 134 | Each `MyButton` instance gets its own container with isolated `count` variable - clicking one button doesn't affect the others. 135 | 136 | ### Variables 137 | Variables in XMLUI are reactive state holders that automatically trigger UI updates when changed. They can contain any JavaScript value, including functions. Variables can declare functions, and those functions can be invoked. 138 | 139 | **Simple Variable Example:** 140 | ```xml 141 | <Stack var.count="{0}"> 142 | <Button onClick="count++" label="Count: {count}" /> 143 | </Stack> 144 | ``` 145 | 146 | **Function Variable Example:** 147 | ```xml 148 | <Stack var.count="{0}" var.increment="{() => count++}"> 149 | <Button onClick="increment()" label="Count: {count}" /> 150 | </Stack> 151 | ``` 152 | 153 | Variables and functions can also be defined in `<script>` tags: 154 | 155 | ```xml 156 | <Stack> 157 | <script> 158 | let count = 0; 159 | const increment = () => count++; 160 | function reset() { count = 0; } 161 | </script> 162 | <Button onClick="increment()" label="Count: {count}" /> 163 | <Button onClick="reset()" label="Reset" /> 164 | </Stack> 165 | ``` 166 | 167 | **Role in Reactivity**: Variables provide the state storage layer for reactive data binding. When a variable changes, the container detects this through proxy-based change detection and triggers selective UI updates. Functions participate in dependency tracking - when they reference other variables, the system tracks these dependencies and re-evaluates when dependencies change. 168 | 169 | ### Loaders (`loaders`) 170 | Loaders are XMLUI's mechanism for handling asynchronous data operations. The framework automatically creates loaders when it recognizes that components contain data-handling requirements. This happens through the **ApiBoundComponent** system. 171 | 172 | **Automatic Loader Creation Cases:** 173 | 174 | 1. **Properties with DataSource/DataSourceRef types**: When a component property contains a `DataSource` or `DataSourceRef` object 175 | 2. **Events with API action types**: When a component event contains `APICall`, `FileDownload`, or `FileUpload` objects 176 | 3. **Components with data properties**: Any component with URL-based `data` properties 177 | 178 | ```xml 179 | <!-- Property-based loader creation --> 180 | <Table data="{users}" /> 181 | <!-- Framework creates internal DataLoader when 'users' references a DataSource --> 182 | 183 | <!-- Direct URL pattern (frequently used) --> 184 | <Table data="/api/users" /> 185 | <!-- Framework automatically creates DataLoader for URL-based data properties --> 186 | 187 | <!-- Event-based loader creation --> 188 | <Button> 189 | Save 190 | <event name="click"> 191 | <APICall url="/api/save" method="POST" /> 192 | </event> 193 | </Button> 194 | <!-- Framework creates APICall handler --> 195 | 196 | <!-- Explicit DataSource (also creates loaders) --> 197 | <DataSource id="users" url="/api/users" /> 198 | <Table data="{users}" /> 199 | <!-- Creates DataLoader managing: loading states, error states, caching, polling --> 200 | ``` 201 | 202 | **Framework Detection Process**: The `ComponentAdapter` scans component properties and events for specific object types that require API operations. It looks for: 203 | - **Properties**: Objects with `type: "DataSource"` or `type: "DataSourceRef"` 204 | - **Events**: Objects with `type: "APICall"`, `type: "FileDownload"`, or `type: "FileUpload"` 205 | 206 | When any of these object types are found, the framework wraps the component in an `ApiBoundComponent`, which automatically generates the necessary loaders and wires them into the container system. 207 | 208 | **Role in Reactivity**: Loaders manage asynchronous state transitions (loading → loaded/error) and integrate with the container's reducer system via actions like `LOADER_LOADED`, `LOADER_IN_PROGRESS_CHANGED`, etc. They provide reactive properties like `users.value`, `users.inProgress`, `users.loaded`. 209 | 210 | ### Uses Declarations (`uses`) 211 | The `uses` property provides explicit control over state inheritance from parent containers: 212 | 213 | ```xml 214 | <!-- Inherit all parent state (default) --> 215 | <Stack> 216 | <!-- children --> 217 | </Stack> 218 | 219 | <!-- Inherit no parent state --> 220 | <Stack uses="[]"> 221 | <!-- children --> 222 | </Stack> 223 | 224 | <!-- Inherit only specific parent state --> 225 | <Stack uses="['userInfo', 'theme']"> 226 | <!-- children --> 227 | </Stack> 228 | ``` 229 | 230 | **Note**: XMLUI is moving toward automating the use of `uses` declarations, with plans to eliminate this property in favor of automatic dependency detection. 231 | 232 | **Role in Reactivity**: Controls the scope of reactive data flow between parent and child containers. This prevents unnecessary re-renders when unrelated parent state changes and provides explicit data dependencies. 233 | 234 | ### Context Variables 235 | Context variables are automatically injected by the framework and component implementations to provide contextual information to child components. These cannot be declared through attributes but are provided by the React implementation of XMLUI components. 236 | 237 | **Naming Convention**: Context variables start with `$` by convention to distinguish them from user-defined variables. 238 | 239 | **Examples of Context Variables** (not exhaustive): 240 | - **Routing**: `$pathname`, `$routeParams`, `$queryParams`, `$linkInfo` 241 | - **Iterators**: `$item`, `$itemIndex` (in Lists, Tables, etc.) 242 | - **Forms**: `$data`, `$validationResult`, `$value`, `$setValue` (in FormItems) 243 | - **Events**: `$param` (in event handlers) 244 | 245 | Different components may inject additional context variables specific to their functionality. 246 | 247 | ```xml 248 | <!-- Context variables are automatically available --> 249 | <Table data="/api/users"> 250 | <Column bindTo="name"> 251 | <Text>{$item.name}</Text> <!-- $item provided by Table --> 252 | </Column> 253 | </Table> 254 | 255 | <Page url="/users/:id"> 256 | <Text>User ID: {$routeParams.id}</Text> <!-- $routeParams provided by Page --> 257 | </Page> 258 | ``` 259 | 260 | **Role in Reactivity**: Context variables are automatically injected into the container state, making them available for reactive data binding. When these variables change (e.g., route changes, current item changes), all dependent UI elements automatically update. 261 | 262 | ### Scripts 263 | JavaScript code blocks that declare variables and functions: 264 | 265 | ```xml 266 | <Stack> 267 | <script> 268 | var counter = 0; 269 | function increment() { counter++; } 270 | var multiplier = (x) => x * 2; 271 | </script> 272 | <Button onClick="increment()" label="Count: {counter}" /> 273 | </Stack> 274 | ``` 275 | 276 | **Role in Reactivity**: Scripts are parsed and their declarations (variables, functions) become part of the container's reactive state. The scripting system integrates with the dependency tracking and memoization systems to ensure efficient updates. 277 | 278 | ### Code-Behind Files 279 | Code-behind files provide a way to define scripts in separate files rather than inline within components. The framework automatically treats these external script files as if they were `<script>` declarations within the application root or user-defined component root. 280 | 281 | **File Naming Convention:** 282 | - **Application Root**: `Main.xmlui.xs` (for `Main.xmlui`) 283 | - **User-Defined Component**: `ComponentName.xmlui.xs` (for `ComponentName.xmlui`) 284 | 285 | **Role in Reactivity**: Code-behind files are processed identically to inline `<script>` blocks. Their variable and function declarations become part of the container's reactive state, with the same dependency tracking and memoization behavior. The framework automatically imports and executes code-behind files during component initialization, making their exports available in the component's reactive scope. 286 | 287 | ### Examples 288 | 289 | **Important**: Simply having an `id` attribute does NOT automatically create a container. Component IDs are handled differently and are stored in the nearest parent container. 290 | 291 | Components are wrapped in containers when they have any of these characteristics (determined by the `isContainerLike` function): 292 | 293 | ```tsx 294 | // From ContainerWrapper.tsx - Container creation logic 295 | export function isContainerLike(node: ComponentDef) { 296 | if (node.type === "Container") { 297 | return true; 298 | } 299 | 300 | // If any of the following properties have a value, we need a container 301 | return !!( 302 | node.loaders || // Data loading operations 303 | node.vars || // Variable declarations 304 | node.uses || // Parent state scoping 305 | node.contextVars || // Context variable provision 306 | node.functions || // Function declarations 307 | node.scriptCollected // Script blocks 308 | ); 309 | } 310 | ``` 311 | 312 | **Container Creation Examples:** 313 | ```xml 314 | <!-- Creates container - has variable --> 315 | <Stack var.count="{0}" /> 316 | 317 | <!-- Creates container - has script --> 318 | <Stack> 319 | <script>let x = 1;</script> 320 | </Stack> 321 | 322 | <!-- Creates container - has data loading --> 323 | <Table data="/api/users" /> 324 | 325 | <!-- Does NOT create container - only has ID --> 326 | <Stack id="myStack" /> 327 | 328 | <!-- Does NOT create container - only layout properties --> 329 | <Stack direction="horizontal" /> 330 | ``` 331 | 332 | ## Container Structure, Hierarchy, and Features 333 | 334 | This section explores the architectural design of containers, their hierarchical organization, and how they support XMLUI's special concepts like loaders, user-defined components, and reactive variables. 335 | 336 | ### Container Hierarchy 337 | 338 | The container system consists of three main React components working together: 339 | 340 | ``` 341 | ContainerWrapper 342 | └── ErrorBoundary 343 | └── StateContainer 344 | └── ErrorBoundary 345 | └── Container 346 | ``` 347 | 348 | ### Core Container Components 349 | 350 | #### ContainerWrapper 351 | - **Purpose**: Outer wrapper that converts components into containerized form 352 | - **Location**: `/xmlui/src/components-core/rendering/ContainerWrapper.tsx` 353 | - **Key Responsibility**: Determines if a component needs containerization and wraps it accordingly 354 | 355 | Components are wrapped in containers if they have any of these characteristics: 356 | - Loaders (data management operations) 357 | - Variables declared (`vars`) 358 | - Context variables (`contextVars`) 359 | - Scripts (`script` or `scriptCollected`) 360 | - Explicit state usage declarations (`uses` property) 361 | 362 | #### StateContainer 363 | - **Purpose**: Manages the actual state storage and state composition 364 | - **Location**: `/xmlui/src/components-core/rendering/StateContainer.tsx` 365 | - **Key Responsibility**: Assembles state from multiple sources and manages state lifecycle 366 | 367 | #### Container 368 | - **Purpose**: Executes event handlers and manages state changes while maintaining UI responsiveness 369 | - **Location**: `/xmlui/src/components-core/rendering/Container.tsx` 370 | - **Key Responsibility**: Asynchronous event handler execution and state synchronization 371 | 372 | ## State Composition and Management 373 | 374 | ### ContainerState Type 375 | 376 | ```typescript 377 | // From /xmlui/src/abstractions/ContainerDefs.ts 378 | export type ContainerState = Record<string | symbol, any>; 379 | ``` 380 | 381 | ### State Assembly Process 382 | 383 | The `StateContainer` assembles the complete state from multiple layers: 384 | 385 | 1. **State from Parent**: Inherited state from parent containers (with optional scoping via `uses`) 386 | 2. **Component APIs**: Exposed component methods and properties 387 | 3. **Local Variables**: Variables declared in the current component 388 | 4. **Context Variables**: Variables provided to child components 389 | 5. **Routing Parameters**: URL parameters, query strings, and navigation context 390 | 391 | ### State Flow Implementation 392 | 393 | ```typescript 394 | // Simplified flow from StateContainer.tsx 395 | const stateFromOutside = extractScopedState(parentState, node.uses); 396 | const componentStateWithApis = mergeComponentApis(componentState, componentApis); 397 | const localVarsStateContext = useCombinedState(stateFromOutside, componentStateWithApis, node.contextVars); 398 | const resolvedLocalVars = useVars(varDefinitions, functionDeps, localVarsStateContext, memoedVars); 399 | const mergedWithVars = useMergedState(resolvedLocalVars, componentStateWithApis); 400 | 401 | // State priority order (later overrides earlier): 402 | // 1. stateFromOutside (parent state - lowest priority) 403 | // 2. node.contextVars (context variables like $item) 404 | // 3. mergedWithVars (local vars + component APIs - highest priority) 405 | const combinedState = useCombinedState( 406 | stateFromOutside, // Parent state (lowest priority) - allows shadowing 407 | node.contextVars, // Context vars 408 | mergedWithVars, // Local vars and component APIs (highest priority) - enables shadowing 409 | routingParams // Routing parameters 410 | ); 411 | ``` 412 | 413 | **State Shadowing**: The priority order allows local variables to shadow (override) parent state variables with the same name. This enables child containers to define their own versions of variables without being forced to use the parent's state, supporting better component encapsulation. 414 | 415 | ### Variable Resolution and Memoization 416 | 417 | Variables are resolved through a sophisticated memoization system: 418 | 419 | ```typescript 420 | // From StateContainer.tsx - useVars function 421 | type MemoedVars = Map<any, { 422 | getDependencies: (value: string | CodeDeclaration, referenceTrackedApi: Record<string, ComponentApi>) => Array<string>; 423 | obtainValue: (expression: any, state: ContainerState, appContext: AppContextObject | undefined, strict: boolean | undefined, stateDeps: Record<string, any>, appContextDeps: Record<string, any>) => any; 424 | }>; 425 | ``` 426 | 427 | #### Variable Dependency Tracking 428 | 429 | 1. **Function Dependencies**: Arrow expressions and their variable dependencies are collected first 430 | 2. **Variable Dependencies**: Dependencies between variables are resolved in multiple passes 431 | 3. **Memoization**: Results are memoized based on dependency changes to avoid unnecessary recalculation 432 | 433 | ## Reducer-Based State Updates 434 | 435 | ### Container Actions 436 | 437 | State changes are managed through Redux-style actions via the `ContainerActionKind` enum: 438 | 439 | ```typescript 440 | // From /xmlui/src/components-core/rendering/containers.ts 441 | export const enum ContainerActionKind { 442 | LOADER_LOADED = "ContainerActionKind:LOADER_LOADED", 443 | LOADER_IN_PROGRESS_CHANGED = "ContainerActionKind:LOADER_IN_PROGRESS_CHANGED", 444 | LOADER_IS_REFETCHING_CHANGED = "ContainerActionKind:LOADER_IS_REFETCHING_CHANGED", 445 | LOADER_ERROR = "ContainerActionKind:LOADER_ERROR", 446 | EVENT_HANDLER_STARTED = "ContainerActionKind:EVENT_HANDLER_STARTED", 447 | EVENT_HANDLER_COMPLETED = "ContainerActionKind:EVENT_HANDLER_COMPLETED", 448 | EVENT_HANDLER_ERROR = "ContainerActionKind:EVENT_HANDLER_ERROR", 449 | COMPONENT_STATE_CHANGED = "ContainerActionKind:COMPONENT_STATE_CHANGED", 450 | STATE_PART_CHANGED = "ContainerActionKind:STATE_PART_CHANGED", 451 | } 452 | ``` 453 | 454 | #### Event Handler State Preservation 455 | 456 | The event handler lifecycle actions (`EVENT_HANDLER_STARTED`, `EVENT_HANDLER_COMPLETED`, `EVENT_HANDLER_ERROR`) implement a critical state preservation pattern. When setting lifecycle flags like `inProgress` or `error`, the reducer checks if state already exists for the component's `uid`: 457 | 458 | ```typescript 459 | // From reducer.ts - state preservation pattern 460 | case ContainerActionKind.EVENT_HANDLER_STARTED: 461 | state[uid] = state[uid] 462 | ? { ...state[uid], [inProgressFlagName]: true } 463 | : { [inProgressFlagName]: true }; 464 | break; 465 | ``` 466 | 467 | **Why This Matters**: Components with reducer state (like `DataSource` or `APICall`) maintain their `data`, `error`, and other properties throughout the event handler lifecycle. Without this preservation, setting the `inProgress` flag would replace the entire state object, losing critical data like loaded results or previous errors. This fix resolved issues where APIs would become non-functional after their first error because the state was being wiped out instead of updated. 468 | 469 | ### Partial State Changes 470 | 471 | The most sophisticated action is `STATE_PART_CHANGED`, which handles deep object/array mutations: 472 | 473 | ```typescript 474 | // Example: Updating nested property some.count++ 475 | { 476 | "type": "ContainerActionKind:STATE_PART_CHANGED", 477 | "payload": { 478 | "actionType": "set", 479 | "path": ["some", "count"], 480 | "target": { "count": 0 }, 481 | "value": 1, 482 | "localVars": resolvedLocalVars 483 | } 484 | } 485 | ``` 486 | 487 | The reducer uses Lodash's `setWith` to update nested paths while preserving object vs array types based on target structure. 488 | 489 | ## Proxy-Based Change Detection 490 | 491 | ### BuildProxy Function 492 | 493 | State changes are detected using JavaScript Proxies that intercept get/set operations: 494 | 495 | ```typescript 496 | // From /xmlui/src/components-core/rendering/buildProxy.ts 497 | export function buildProxy( 498 | proxyTarget: any, 499 | callback: (changeInfo: ProxyCallbackArgs) => void, 500 | tree: Array<string | symbol> = [], 501 | ): any { 502 | return new Proxy(proxyTarget, { 503 | get: function (target, prop, receiver) { 504 | const value = Reflect.get(target, prop, receiver); 505 | // Create nested proxies for objects and arrays 506 | if (value && typeof value === "object" && ["Array", "Object"].includes(value.constructor.name)) { 507 | if (!proxiedValues.has(value)) { 508 | proxiedValues.set(value, buildProxy(value, callback, tree.concat(prop))); 509 | } 510 | return proxiedValues.get(value); 511 | } 512 | return value; 513 | }, 514 | set: function (target, prop, value, receiver) { 515 | // Notify of state changes 516 | callback({ 517 | action: "set", 518 | path: getPath(prop), 519 | pathArray: tree.concat(prop), 520 | target, 521 | newValue: value, 522 | previousValue: Reflect.get(target, prop, receiver), 523 | }); 524 | return Reflect.set(target, prop, value, receiver); 525 | } 526 | }); 527 | } 528 | ``` 529 | 530 | ### Change Propagation 531 | 532 | When a proxied state change occurs: 533 | 534 | 1. **Detection**: Proxy intercepts the change and calls the callback 535 | 2. **Path Analysis**: Determines if the change belongs to current container or parent 536 | 3. **Dispatch**: Sends `STATE_PART_CHANGED` action to appropriate container 537 | 4. **Reduction**: Immer-based reducer applies the change immutably 538 | 5. **Re-render**: React re-renders affected components 539 | 540 | ## Component Identification and APIs 541 | 542 | ### Symbol-Based Component IDs 543 | 544 | Components within containers are identified using JavaScript Symbols: 545 | 546 | ```typescript 547 | // Component ID resolution 548 | const componentUid = componentId ?? Symbol(id); 549 | if (id) { 550 | componentUid.description = id; // For debugging and API access 551 | } 552 | ``` 553 | 554 | ### Component ID Storage vs Accessibility 555 | 556 | A key insight about XMLUI's component management is understanding where component IDs are stored, how they flow through the container hierarchy, and what happens when a component has an ID but doesn't create its own container. 557 | 558 | **Component ID Creation and Storage Flow** 559 | 560 | 1. **Universal ID Assignment**: ALL components receive a unique Symbol-based ID, regardless of whether they create containers: 561 | 562 | ```tsx 563 | // From ComponentAdapter.tsx - every component gets a UID 564 | const uid = useMemo(() => Symbol(safeNode.uid), [safeNode.uid]); 565 | ``` 566 | 567 | 2. **Container vs Non-Container Components**: 568 | - **Components that create containers**: Store their child component IDs in their own `componentApis` state 569 | - **Components that DON'T create containers**: Their IDs are stored in the nearest parent container 570 | 571 | 3. **Registration Mechanism**: Components register their APIs (including their ID) with the nearest container: 572 | 573 | ```tsx 574 | // From ComponentAdapter.tsx - all components register with nearest container 575 | const memoedRegisterComponentApi: RegisterComponentApiFn = useCallback( 576 | (api) => { 577 | registerComponentApi(uid, api); // Registers with nearest parent container 578 | }, 579 | [registerComponentApi, uid], 580 | ); 581 | ``` 582 | 583 | 4. **Storage in Parent Container**: The `StateContainer` maintains a `componentApis` map: 584 | 585 | ```tsx 586 | // From StateContainer.tsx - parent container stores all child component IDs 587 | const registerComponentApi: RegisterComponentApiFnInner = useCallback((uid, api) => { 588 | setComponentApis(produce((draft) => { 589 | if (!draft[uid]) { 590 | draft[uid] = {}; 591 | } 592 | Object.entries(api).forEach(([key, value]) => { 593 | draft[uid][key] = value; 594 | }); 595 | })); 596 | }, []); 597 | ``` 598 | 599 | **Global Access Through State Composition** 600 | Even though component IDs are stored hierarchically, they become accessible file-wide through state inheritance: 601 | 602 | ```tsx 603 | // From StateContainer.tsx - state flows down through hierarchy 604 | const combinedState = useCombinedState( 605 | stateFromOutside, // Parent state (contains parent component IDs) 606 | node.contextVars, // Framework context variables 607 | mergedWithVars, // Local variables and component state 608 | routingParams, // Routing parameters 609 | ); 610 | ``` 611 | 612 | **Example Scenarios**: 613 | 614 | 1. **Component with ID but no container**: 615 | ```xml 616 | <Stack var.count="{0}"> <!-- Creates container A --> 617 | <Button id="myBtn" /> <!-- ID stored in container A --> 618 | <Text id="myText" /> <!-- ID also stored in container A --> 619 | </Stack> 620 | ``` 621 | 622 | 2. **Nested containers with inheritance**: 623 | ```xml 624 | <Stack var.count="{0}"> <!-- Creates container A --> 625 | <Button id="myBtn" /> <!-- ID stored in container A --> 626 | <Stack var.data="{null}"> <!-- Creates container B --> 627 | <Text>{myBtn.visible}</Text> <!-- Can access myBtn via state inheritance --> 628 | <Button id="innerBtn" /> <!-- ID stored in container B --> 629 | </Stack> 630 | </Stack> 631 | ``` 632 | 633 | **Storage and Access Pattern**: 634 | - `myBtn` and `myText` IDs are stored in container A's `componentApis` (they don't create their own containers) 635 | - `innerBtn` ID is stored in container B's `componentApis` 636 | - Container B inherits parent state through `stateFromOutside`, making `myBtn` accessible 637 | - All components can reference each other within the same .xmlui file through the state inheritance chain 638 | 639 | **Root Component Storage**: 640 | If `<Stack id="myStack" />` is a root component that doesn't create its own container, its ID is stored in the automatically-created root container. XMLUI ensures there's always at least one container in the hierarchy to store component IDs. 641 | 642 | This design ensures that: 643 | - **Performance**: Each container only manages its direct responsibility 644 | - **Usability**: All components within a .xmlui file can reference each other 645 | - **Consistency**: Component IDs flow predictably through the container hierarchy 646 | - **Reliability**: There's always a root container to store component IDs, even for simple root components 647 | 648 | ### Component API Registration 649 | 650 | Components can expose APIs that other components can invoke: 651 | 652 | ```typescript 653 | // From StateContainer.tsx 654 | const registerComponentApi: RegisterComponentApiFnInner = useCallback((uid, api) => { 655 | setComponentApis(produce((draft) => { 656 | if (!draft[uid]) { 657 | draft[uid] = {}; 658 | } 659 | Object.entries(api).forEach(([key, value]) => { 660 | if (draft[uid][key] !== value) { 661 | draft[uid][key] = value; 662 | } 663 | }); 664 | })); 665 | }, []); 666 | ``` 667 | 668 | APIs are merged into the component state so they're accessible via the component's ID: 669 | 670 | ```typescript 671 | // API access pattern 672 | componentStateWithApis[componentId] = { ...componentState, ...exposedApi }; 673 | ``` 674 | 675 | #### API State Scoping 676 | 677 | Component API state (particularly reducer state from loaders like `DataSource` or `APICall`) is carefully scoped to prevent state leakage between containers. When component APIs are exposed as string keys in the container state (using the component's `id` as the key), the StateContainer only includes reducer state for APIs that are actually registered within that specific container. 678 | 679 | **Implementation**: The StateContainer maintains a Set of registered API keys and filters which reducer state properties are copied to string keys: 680 | 681 | ```typescript 682 | // From StateContainer.tsx - API state scoping 683 | const registeredApiKeys = new Set<string>(); 684 | for (const sym of Object.getOwnPropertySymbols(componentApis)) { 685 | if (sym.description) { 686 | registeredApiKeys.add(sym.description); 687 | } 688 | } 689 | 690 | // Only copy reducer state if the API is registered in this container 691 | for (const key of Object.keys(componentState)) { 692 | if (registeredApiKeys.has(key)) { 693 | ret[key] = { ...(ret[key] || {}), ...componentState[key] }; 694 | } 695 | } 696 | ``` 697 | 698 | **Benefit**: This prevents child containers from inheriting parent API reducer state. For example, if a parent has a `DataSource` with `id="users"`, child containers won't see `users.inProgress`, `users.error`, or other reducer properties unless they register their own API with that ID. This ensures clean separation and prevents confusing state interactions. 699 | 700 | ## Asynchronous Event Handler Execution 701 | 702 | ### UI Responsiveness Strategy 703 | 704 | The Container component maintains UI responsiveness during long-running operations through: 705 | 706 | 1. **Async Instruction Execution**: Each instruction in an event handler runs asynchronously 707 | 2. **State Synchronization Points**: Execution pauses after each instruction for state updates 708 | 3. **Promise-Based Coordination**: Uses promises to coordinate when state changes are committed 709 | 4. **Transition-Based Updates**: Uses React's `startTransition` for non-urgent updates 710 | 711 | ### Execution Flow 712 | 713 | ```typescript 714 | // Simplified execution pattern from Container.tsx 715 | const executeHandler = async (statements: Statement[], options: ExecutionOptions) => { 716 | for (const statement of statements) { 717 | // Execute statement 718 | await processStatement(statement, evalContext); 719 | 720 | // Collect state changes 721 | if (changes.length > 0) { 722 | // Dispatch state changes 723 | changes.forEach(change => { 724 | statePartChanged(change.pathArray, change.newValue, change.target, change.action); 725 | }); 726 | 727 | // Create promise for this statement 728 | const statementPromise = new Promise(resolve => { 729 | statementPromiseResolvers.current.set(statementId, resolve); 730 | }); 731 | 732 | // Increment version to trigger re-render 733 | startTransition(() => { 734 | setVersion(prev => prev + 1); 735 | }); 736 | 737 | // Wait for state to be committed 738 | await statementPromise; 739 | } 740 | } 741 | }; 742 | ``` 743 | 744 | ### Version-Based Synchronization 745 | 746 | The Container uses a version counter to coordinate state updates: 747 | 748 | ```typescript 749 | // When version changes, resolve pending promises 750 | useEffect(() => { 751 | // Resolve all pending statement promises 752 | statementPromiseResolvers.current.forEach(resolve => resolve()); 753 | statementPromiseResolvers.current.clear(); 754 | }, [version]); 755 | ``` 756 | 757 | ## State Scoping and Inheritance 758 | 759 | ### Parent State Extraction 760 | 761 | Child containers can limit which parent state they receive: 762 | 763 | ```typescript 764 | // From StateContainer.tsx 765 | function extractScopedState(parentState: ContainerState, uses?: string[]): ContainerState | undefined { 766 | if (!uses) { 767 | return parentState; // Inherit all parent state 768 | } 769 | if (uses.length === 0) { 770 | return EMPTY_OBJECT; // Inherit no parent state 771 | } 772 | return pick(parentState, uses); // Inherit only specified keys 773 | } 774 | ``` 775 | 776 | ### Context Variables 777 | 778 | Containers can provide context variables to their children: 779 | 780 | ```typescript 781 | // contextVars flow down to child containers automatically 782 | const localVarsStateContext = useCombinedState( 783 | stateFromOutside, 784 | componentStateWithApis, 785 | node.contextVars // These become available to children 786 | ); 787 | ``` 788 | 789 | ### State Merging Strategies 790 | 791 | Different merging strategies are used for different state layers: 792 | 793 | 1. **Override**: Higher-priority state overrides lower-priority (`useCombinedState`) 794 | 2. **Merge**: Object and array values are deep-merged (`useMergedState`) 795 | 3. **Replace**: Simple values replace entirely 796 | 797 | ## Routing Integration 798 | 799 | ### Routing Parameters as State 800 | 801 | Routing information is automatically injected into container state: 802 | 803 | ```typescript 804 | // From StateContainer.tsx - useRoutingParams 805 | const routingParams = useMemo(() => { 806 | return { 807 | $pathname: location.pathname, 808 | $routeParams: routeParams, // URL parameters like :id 809 | $queryParams: queryParamsMap, // Query string parameters 810 | $linkInfo: linkInfo, // Navigation context 811 | }; 812 | }, [linkInfo, location.pathname, queryParamsMap, routeParams]); 813 | ``` 814 | 815 | These are automatically available in all containers as context variables. 816 | 817 | ## Error Boundaries and Resilience 818 | 819 | ### Layered Error Protection 820 | 821 | The container system has multiple error boundaries: 822 | 823 | 1. **Container-Level**: Protects StateContainer from Container errors 824 | 2. **StateContainer-Level**: Protects parent containers from child container errors 825 | 3. **Component-Level**: Individual component errors don't crash containers 826 | 827 | ### State Preservation 828 | 829 | If a Container crashes during event handler execution, the StateContainer preserves its state, allowing recovery or graceful degradation. 830 | 831 | ## Performance Optimizations 832 | 833 | ### Memoization Strategies 834 | 835 | 1. **Component Memoization**: Container components are memoized to prevent unnecessary re-renders 836 | 2. **Variable Memoization**: Variable resolution results are cached based on dependencies 837 | 3. **Shallow Comparison**: State objects use shallow comparison for change detection 838 | 4. **Reference Tracking**: Proxy objects maintain stable references for nested access 839 | 840 | ### Change Detection Optimizations 841 | 842 | 1. **Path-Based Updates**: Only affected parts of state tree are updated 843 | 2. **Dependency Tracking**: Variables are only re-evaluated when their dependencies change 844 | 3. **Batch Updates**: Multiple changes in single instruction are batched together 845 | 4. **Transition Updates**: Non-urgent updates use React transitions for better performance 846 | 847 | ## Debugging Support 848 | 849 | ### State Transition Logging 850 | 851 | The container reducer can log state transitions for debugging: 852 | 853 | ```typescript 854 | // From reducer.ts 855 | export function createContainerReducer(debugView: IDebugViewContext) { 856 | const allowLogging = debugView.collectStateTransitions; 857 | // ... logging implementation 858 | } 859 | ``` 860 | 861 | ### Component Keys and Resolution 862 | 863 | Components receive resolved keys for debugging and tracking: 864 | 865 | ```typescript 866 | // From renderChild.tsx 867 | const key = extractParam(state, node.uid, appContext, true); 868 | return ( 869 | <ComponentWrapper 870 | key={key} 871 | resolvedKey={key} // For debugging container parent chains 872 | // ... 873 | /> 874 | ); 875 | ``` 876 | 877 | ## Integration Points 878 | 879 | ### DataSource Integration 880 | 881 | DataSource components integrate with the container system through loader actions: 882 | 883 | ```typescript 884 | // Loader state management 885 | dispatch({ 886 | type: ContainerActionKind.LOADER_LOADED, 887 | payload: { 888 | uid: dataSourceUid, 889 | data: fetchedData, 890 | pageInfo: paginationInfo 891 | } 892 | }); 893 | ``` 894 | 895 | ### Form Integration 896 | 897 | Form components manage their state through the container system while maintaining their own internal state for performance. 898 | 899 | ### API Integration 900 | 901 | Component APIs are registered with containers and become part of the state, making them accessible to other components via component IDs. 902 | 903 | ## Key Design Principles 904 | 905 | 1. **Hierarchical Encapsulation**: State is scoped to container boundaries while allowing controlled inheritance 906 | 2. **Reactive Consistency**: All state changes automatically propagate to dependent UI elements 907 | 3. **Async Coordination**: Long-running operations don't block the UI thread 908 | 4. **Immutable Updates**: All state changes produce new immutable state objects 909 | 5. **Proxy Transparency**: State mutations are detected transparently without special syntax 910 | 6. **Error Isolation**: Component errors are contained and don't propagate to affect other parts of the application 911 | 912 | This container-based architecture provides XMLUI with a robust, scalable state management solution that maintains the declarative programming model while handling complex state interactions and ensuring optimal performance characteristics. ```