This is page 16 of 182. Use http://codebase.md/xmlui-org/xmlui/xmlui-standalone.umd.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 │ │ │ └── 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 │ │ │ ├── Debug.xmlui │ │ │ ├── Headlines.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 │ ├── containers.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 │ ├── state-management.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 │ │ │ ├── MultiSelectOption.tsx │ │ │ ├── OptionContext.ts │ │ │ ├── Select.md │ │ │ ├── Select.module.scss │ │ │ ├── Select.spec.ts │ │ │ ├── Select.tsx │ │ │ ├── SelectContext.tsx │ │ │ ├── SelectNative.tsx │ │ │ ├── SelectOption.tsx │ │ │ └── SimpleSelect.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/Heading/H6.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | test("renders as h6 level heading", async ({ initTestBed, createHeadingDriver }) => { 9 | await initTestBed(`<H6 testId="h6">Test Heading</H6>`); 10 | 11 | const driver = await createHeadingDriver("h6"); 12 | 13 | await expect(driver.component).toBeVisible(); 14 | await expect(driver.component).toContainText("Test Heading"); 15 | await expect(driver.component).toHaveRole("heading"); 16 | 17 | // Verify it renders as h6 HTML element 18 | const tagName = await driver.getComponentTagName(); 19 | expect(tagName.toLowerCase()).toBe("h6"); 20 | }); 21 | 22 | test("renders with value property", async ({ initTestBed, createHeadingDriver }) => { 23 | await initTestBed(`<H6 testId="h6" value="Value Property Text" />`); 24 | 25 | const driver = await createHeadingDriver("h6"); 26 | 27 | await expect(driver.component).toBeVisible(); 28 | await expect(driver.component).toContainText("Value Property Text"); 29 | await expect(driver.component).toHaveRole("heading"); 30 | 31 | // Verify it renders as h6 HTML element 32 | const tagName = await driver.getComponentTagName(); 33 | expect(tagName.toLowerCase()).toBe("h6"); 34 | }); 35 | 36 | test("is equivalent to Heading with level='h6'", async ({ initTestBed, createHeadingDriver }) => { 37 | await initTestBed(` 38 | <Fragment> 39 | <Heading testId="heading" level="h6">Heading Content</Heading> 40 | <H6 testId="h6">H6 Content</H6> 41 | </Fragment> 42 | `); 43 | 44 | const headingDriver = await createHeadingDriver("heading"); 45 | const h6Driver = await createHeadingDriver("h6"); 46 | 47 | // Both should render as h6 elements 48 | const headingTagName = await headingDriver.getComponentTagName(); 49 | const h6TagName = await h6Driver.getComponentTagName(); 50 | 51 | expect(headingTagName.toLowerCase()).toBe("h6"); 52 | expect(h6TagName.toLowerCase()).toBe("h6"); 53 | expect(headingTagName).toEqual(h6TagName); 54 | 55 | // Both should have heading role 56 | await expect(headingDriver.component).toHaveRole("heading"); 57 | await expect(h6Driver.component).toHaveRole("heading"); 58 | }); 59 | 60 | test("ignores level property when provided", async ({ initTestBed, createHeadingDriver }) => { 61 | await initTestBed(`<H6 testId="h6" level="h1">Should be H6</H6>`); 62 | 63 | const driver = await createHeadingDriver("h6"); 64 | 65 | await expect(driver.component).toBeVisible(); 66 | await expect(driver.component).toContainText("Should be H6"); 67 | await expect(driver.component).toHaveRole("heading"); 68 | 69 | // Should always be h6 despite level="h1" prop (this is the expected behavior) 70 | const tagName = await driver.getComponentTagName(); 71 | expect(tagName.toLowerCase()).toBe("h6"); 72 | }); 73 | }); 74 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/ComponentViewer.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { InspectorDialog } from "./devtools/InspectorDialog"; 2 | import AppWithCodeViewNative from "../components/NestedApp/AppWithCodeViewNative"; 3 | import React, { useMemo } from "react"; 4 | import { useDevTools } from "./InspectorContext"; 5 | import { Tooltip } from "../components/NestedApp/Tooltip"; 6 | import styles from "../components/NestedApp/NestedApp.module.scss"; 7 | import { AiOutlineClose } from "react-icons/ai"; 8 | import { useAppContext } from "./AppContext"; 9 | 10 | export const ComponentViewer = () => { 11 | const { mockApi, setIsOpen, isOpen, inspectedNode, clickPosition, projectCompilation, sources } = 12 | useDevTools(); 13 | 14 | const { appGlobals } = useAppContext(); 15 | 16 | const components = useMemo<string[]>(() => { 17 | if (!projectCompilation) { 18 | return []; 19 | } 20 | return projectCompilation.components.map((component) => { 21 | return component.markupSource; 22 | }); 23 | }, [projectCompilation]); 24 | 25 | const value = useMemo(() => { 26 | const compSrc = inspectedNode?.debug?.source; 27 | 28 | if (!compSrc) { 29 | return ""; 30 | } 31 | if (!sources) { 32 | return ""; 33 | } 34 | const { start, end, fileId } = compSrc; 35 | const slicedSrc = sources[fileId].slice(start, end); 36 | 37 | let dropEmptyLines = true; 38 | const prunedLines: Array<string> = []; 39 | let trimBeginCount: number | undefined = undefined; 40 | slicedSrc.split("\n").forEach((line) => { 41 | if (line.trim() === "" && dropEmptyLines) { 42 | //drop empty lines from the beginning 43 | return; 44 | } else { 45 | dropEmptyLines = false; 46 | prunedLines.push(line); 47 | const startingWhiteSpaces = line.search(/\S|$/); 48 | if ( 49 | line.trim() !== "" && 50 | (trimBeginCount === undefined || startingWhiteSpaces < trimBeginCount) 51 | ) { 52 | trimBeginCount = startingWhiteSpaces; 53 | } 54 | } 55 | }); 56 | return prunedLines 57 | .map((line) => line.slice(trimBeginCount).replace(/inspect="true"/g, "")) 58 | .join("\n"); 59 | }, [inspectedNode, sources]); 60 | 61 | return process.env.VITE_USER_COMPONENTS_Inspect !== "false" && 62 | isOpen && 63 | inspectedNode !== null ? ( 64 | <InspectorDialog isOpen={isOpen} setIsOpen={setIsOpen} clickPosition={clickPosition}> 65 | <AppWithCodeViewNative 66 | height={"500px"} 67 | allowPlaygroundPopup 68 | initiallyShowCode={appGlobals?.initiallyShowCode ?? true} 69 | splitView={true} 70 | controlsWidth={"120px"} 71 | closeButton={ 72 | <Tooltip 73 | trigger={ 74 | <button 75 | className={styles.headerButton} 76 | onClick={() => { 77 | setIsOpen(false); 78 | }} 79 | > 80 | <AiOutlineClose /> 81 | </button> 82 | } 83 | label="Close" 84 | /> 85 | } 86 | markdown={`\`\`\`xmlui 87 | ${value} 88 | \`\`\``} 89 | api={mockApi} 90 | app={value} 91 | components={components} 92 | /> 93 | </InspectorDialog> 94 | ) : null; 95 | }; 96 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/devtools/InspectorDialog.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "xmlui/themes.scss" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $component: "ModalDialog"; 5 | $themeVars: (); 6 | @function createThemeVar($componentVariable) { 7 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 8 | @return t.getThemeVar($themeVars, $componentVariable); 9 | } 10 | 11 | // --- Theme vars for paddings 12 | $themeVars: t.composePaddingVars($themeVars, $component); 13 | $themeVars: t.composePaddingVars($themeVars, "overlay-#{$component}"); 14 | $padding-ModalDialog: createThemeVar("padding-#{$component}"); 15 | $backgroundColor-ModalDialog: createThemeVar("Dialog:backgroundColor-#{$component}"); 16 | $backgroundColor-overlay-ModalDialog: createThemeVar("Dialog:backgroundColor-overlay-#{$component}"); 17 | $borderRadius-ModalDialog: createThemeVar("Dialog:borderRadius-#{$component}"); 18 | $fontFamily-ModalDialog: createThemeVar("Dialog:fontFamily-#{$component}"); 19 | $textColor-ModalDialog: createThemeVar("Dialog:textColor-#{$component}"); 20 | $minWidth-ModalDialog: createThemeVar("Dialog:minWidth-#{$component}"); 21 | $maxWidth-ModalDialog: createThemeVar("Dialog:maxWidth-#{$component}"); 22 | $marginBottom-title-ModalDialog: createThemeVar("Dialog:marginBottom-title-#{$component}"); 23 | 24 | 25 | .overlay { 26 | position: fixed; 27 | overflow-y: auto; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | inset: 0; 32 | background-color: $backgroundColor-overlay-ModalDialog; 33 | padding: t.$space-4; 34 | } 35 | 36 | .overlayBg { 37 | background-color: $backgroundColor-overlay-ModalDialog; 38 | position: fixed; 39 | inset: 0; 40 | } 41 | 42 | .contentWrapper { 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | width: 100%; 47 | height: 100%; 48 | } 49 | 50 | .content { 51 | border-radius: $borderRadius-ModalDialog; 52 | font-family: $fontFamily-ModalDialog; 53 | color: $textColor-ModalDialog; 54 | width: 90vw; 55 | max-width: 960px; 56 | min-width: 240px; 57 | isolation: isolate; 58 | position: relative; 59 | display: flex; 60 | flex-direction: column; 61 | max-height: 80vh; 62 | overflow-y: auto; 63 | } 64 | 65 | .content:focus { 66 | outline: none; 67 | } 68 | 69 | .dialogTitle { 70 | flex: 1; 71 | margin-bottom: $marginBottom-title-ModalDialog; 72 | font-size: t.$fontSize-2xl; 73 | } 74 | 75 | .innerContent { 76 | display: flex; 77 | flex-direction: column; 78 | min-height: 0; 79 | gap: var(--stack-gap-default); 80 | flex: 1; 81 | } 82 | 83 | .closeButton { 84 | position: absolute; 85 | right: 0.5rem; 86 | top: 0.4rem; 87 | } 88 | 89 | .actions { 90 | display: inline-flex; 91 | align-items: center; 92 | justify-content: flex-end; 93 | } 94 | 95 | .header { 96 | padding: t.$space-2; 97 | justify-content: space-between; 98 | display: flex; 99 | flex-direction: row; 100 | border-bottom: 1px solid t.$borderColor; 101 | } 102 | 103 | @media (max-width: 70em) { 104 | .dialog, .content { 105 | max-width: 90%; 106 | } 107 | } 108 | 109 | @media (max-width: 50em) { 110 | .dialog, .content { 111 | width: 100%; 112 | max-width: calc(100% - #{t.$space-6}); 113 | min-width: 0 !important; 114 | } 115 | } 116 | 117 | // --- We export the theme variables to add them to the component renderer 118 | :export { 119 | themeVars: t.json-stringify($themeVars); 120 | } 121 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/action/FileDownloadAction.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type { ComponentDef } from "../../abstractions/ComponentDefs"; 2 | import type { ActionExecutionContext } from "../../abstractions/ActionDefs"; 3 | import type { ApiActionOptions, DownloadOperationDef } from "../RestApiProxy"; 4 | import RestApiProxy from "../RestApiProxy"; 5 | 6 | import { createAction } from "./actions"; 7 | 8 | export interface DownloadActionComponent extends ComponentDef { 9 | props: DownloadOperationDef; 10 | } 11 | 12 | async function download( 13 | { state, appContext }: ActionExecutionContext, 14 | { 15 | params, 16 | url, 17 | queryParams, 18 | method, 19 | rawBody, 20 | body, 21 | fileName, 22 | headers, 23 | }: { 24 | params: any; 25 | } & DownloadOperationDef, 26 | { resolveBindingExpressions }: ApiActionOptions = {} 27 | ) { 28 | const context = { ...params, ...state }; 29 | const operation: DownloadOperationDef = { 30 | url, 31 | queryParams, 32 | method, 33 | rawBody, 34 | body, 35 | fileName, 36 | headers, 37 | }; 38 | 39 | const api = new RestApiProxy(appContext); 40 | const _url = api.resolveUrl({ operation, params: context, resolveBindingExpressions }); 41 | 42 | if ( 43 | (operation.method && (operation.method as string).toLowerCase() !== "get") || 44 | Object.keys(appContext.appGlobals?.headers || {}).length !== 0 || //if we have any headers for the api, we can't use the iframe trick 45 | appContext.apiInterceptorContext.isMocked(_url) //if we mock this url, the mock can't work in an iframe, so we must fall back to download it with the restApiProxy 46 | ) { 47 | const file: File = await api.execute({ 48 | operation, 49 | params: context, 50 | parseOptions: { 51 | asFile: true, 52 | }, 53 | resolveBindingExpressions, 54 | }); 55 | downloadWithAnchor(file); 56 | } else { 57 | downloadInIframe(_url); 58 | } 59 | } 60 | 61 | //we use a hidden iframe trick here, 62 | // we set the iframe source as the download url, this way the browser will ask to download the file, and show a progress bar 63 | // (we could use an anchor tag with a download attribute, but in this case we can't show progress ) 64 | // we can use it if we don't have to add extra headers to the request in order to download a file (pre-signed urls, or public urls) 65 | function downloadInIframe(fileUrl: string) { 66 | const iframe = document.createElement("iframe"); 67 | iframe.style.display = "none"; 68 | iframe.hidden = true; 69 | iframe.name = fileUrl; 70 | iframe.id = `download-iframe_${fileUrl}`; 71 | iframe.src = fileUrl; 72 | document.body.appendChild(iframe); 73 | setTimeout(() => { 74 | iframe.remove(); 75 | }, 20000); 76 | } 77 | 78 | // we can use it if we do have to add extra headers to the request in order to download a file (urls require authentication) 79 | function downloadWithAnchor(file: File) { 80 | const url = window.URL.createObjectURL(file); 81 | const a = document.createElement("a"); 82 | a.style.display = "none"; 83 | a.href = url; 84 | // the filename you want 85 | a.download = file.name; 86 | document.body.appendChild(a); 87 | a.click(); 88 | window.URL.revokeObjectURL(url); 89 | } 90 | 91 | export const downloadAction = createAction("download", download); 92 | ``` -------------------------------------------------------------------------------- /xmlui/scripts/inline-links.mjs: -------------------------------------------------------------------------------- ``` 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | const includedFileExtensions = [".mdx", ".md"]; 5 | 6 | // Read the file contents 7 | const fileContent = fs.readFileSync("../docs/meta/pages.js", "utf-8"); 8 | 9 | // Match lines like: export const NAME = "value"; 10 | const regex = /export const (\w+)\s*=\s*["'`](.*?)["'`];/g; 11 | 12 | const constants = {}; 13 | let match; 14 | while ((match = regex.exec(fileContent)) !== null) { 15 | const [, key, value] = match; 16 | constants[key] = value; 17 | } 18 | 19 | inlineLinks("../docs/pages"); 20 | 21 | function inlineLinks(pagesFolder) { 22 | traverseDirectory({ name: "", path: pagesFolder }, (item, _) => { 23 | /** 24 | * name: the folder's/file's name (eg. "hello-app-engine") 25 | * path: the path to the root of the given folder from the project root (eg. "src/apps/1_basic/samples/hello-app-engine") 26 | * parent: parent node 27 | * children: children file/folder names 28 | */ 29 | if (fs.statSync(item.path).isDirectory()) { 30 | // Node is a folder 31 | } else { 32 | // Node is a file 33 | if (includedFileExtensions.includes(path.extname(item.name))) { 34 | console.log(item.name); 35 | const mdxContent = fs.readFileSync(item.path, "utf-8"); 36 | 37 | const regex = /<SmartLink\s+href=\{([^}]+)\}>/g; 38 | const newMdxContent = mdxContent.replace(regex, (_, hrefExpr) => { 39 | return `<SmartLink href="${constants[hrefExpr] || ''}">`; 40 | }); 41 | 42 | fs.writeFileSync(item.path, newMdxContent); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * Recursive function that traverses a given folder and applies an optional function on 50 | * each of the folders/files found inside. 51 | */ 52 | export function traverseDirectory(node, visitor, level = 0) { 53 | level++; 54 | const dirContents = fs.readdirSync(node.path); 55 | if (!node.children) node.children = dirContents; 56 | for (const itemName of dirContents) { 57 | const itemPath = [winPathToPosix(node.path), itemName].join(path.posix.sep); 58 | const itemIsDir = fs.statSync(itemPath).isDirectory(); 59 | const childNode = { 60 | name: itemName, 61 | path: itemPath, 62 | parent: node, 63 | }; 64 | visitor && visitor(childNode, level); 65 | if (itemIsDir) { 66 | traverseDirectory(childNode, visitor, level); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Multi-liner (commented and compatible with really old javascript versions) 73 | * Source: https://stackoverflow.com/a/62732509 74 | */ 75 | export function winPathToPosix(windowsPath) { 76 | // handle the edge-case of Window's long file names 77 | // See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#short-vs-long-names 78 | windowsPath = windowsPath.replace(/^\\\\\?\\/, ""); 79 | 80 | // convert the separators, valid since both \ and / can't be in a windows filename 81 | windowsPath = windowsPath.replace(/\\/g, "/"); 82 | 83 | // compress any // or /// to be just /, which is a safe oper under POSIX 84 | // and prevents accidental errors caused by manually doing path1+path2 85 | windowsPath = windowsPath.replace(/\/\/+/g, "/"); 86 | 87 | return windowsPath; 88 | } 89 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Stack/VStack.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | test("renders items vertically", async ({ initTestBed, page }) => { 9 | await initTestBed(` 10 | <VStack testId="vstack"> 11 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 12 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 13 | <Stack testId="item3" height="32px" width="32px" backgroundColor="green" /> 14 | </VStack> 15 | `); 16 | 17 | const vstack = page.getByTestId("vstack"); 18 | const item1 = page.getByTestId("item1"); 19 | const item2 = page.getByTestId("item2"); 20 | const item3 = page.getByTestId("item3"); 21 | 22 | await expect(vstack).toBeVisible(); 23 | await expect(item1).toBeVisible(); 24 | await expect(item2).toBeVisible(); 25 | await expect(item3).toBeVisible(); 26 | 27 | // Get bounding boxes to verify vertical layout 28 | const item1Box = await item1.boundingBox(); 29 | const item2Box = await item2.boundingBox(); 30 | const item3Box = await item3.boundingBox(); 31 | 32 | // Verify items are stacked vertically (item2 should be below item1, item3 below item2) 33 | expect(item2Box!.y).toBeGreaterThan(item1Box!.y + item1Box!.height - 1); // -1 for floating point tolerance 34 | expect(item3Box!.y).toBeGreaterThan(item2Box!.y + item2Box!.height - 1); // -1 for floating point tolerance 35 | 36 | // Verify items are horizontally aligned (should start at roughly the same x position) 37 | expect(Math.abs(item1Box!.x - item2Box!.x)).toBeLessThan(1); 38 | expect(Math.abs(item2Box!.x - item3Box!.x)).toBeLessThan(1); 39 | }); 40 | 41 | test("renders empty VStack", async ({ initTestBed, page }) => { 42 | await initTestBed(`<VStack testId="vstack"></VStack>`); 43 | 44 | const vstack = page.getByTestId("vstack"); 45 | await expect(vstack).toBeAttached(); 46 | await expect(vstack).toBeEmpty(); 47 | }); 48 | 49 | test("ignores orientation property", async ({ initTestBed, page }) => { 50 | await initTestBed(` 51 | <VStack testId="vstack" orientation="horizontal"> 52 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 53 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 54 | </VStack> 55 | `); 56 | 57 | const item1 = page.getByTestId("item1"); 58 | const item2 = page.getByTestId("item2"); 59 | 60 | await expect(item1).toBeVisible(); 61 | await expect(item2).toBeVisible(); 62 | 63 | // Get bounding boxes to verify still renders vertically despite orientation="horizontal" 64 | const item1Box = await item1.boundingBox(); 65 | const item2Box = await item2.boundingBox(); 66 | 67 | // Verify items are still stacked vertically (orientation prop should be ignored) 68 | expect(item2Box!.y).toBeGreaterThan(item1Box!.y + item1Box!.height - 1); 69 | }); 70 | }); 71 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Option/Option.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createComponentRenderer } from "../../components-core/renderers"; 2 | import { MemoizedItem } from "../container-helpers"; 3 | import { createMetadata, d } from "../metadata-helpers"; 4 | import { OptionNative, defaultProps } from "./OptionNative"; 5 | 6 | const COMP = "Option"; 7 | 8 | export const OptionMd = createMetadata({ 9 | status: "stable", 10 | description: 11 | "`Option` defines selectable items for choice-based components, providing both " + 12 | "the underlying value and display text for selection interfaces. It serves as " + 13 | "a non-visual data structure that describes individual choices within " + 14 | "[Select](/components/Select), [AutoComplete](/components/AutoComplete), " + 15 | "and other selection components.", 16 | props: { 17 | label: d( 18 | `This property defines the text to display for the option. If \`label\` is not defined, ` + 19 | `\`Option\` will use the \`value\` as the label.`, 20 | ), 21 | value: d( 22 | "This property defines the value of the option. If `value` is not defined, " + 23 | "`Option` will use the `label` as the value. If neither is defined, " + 24 | "the option is not displayed.", 25 | ), 26 | enabled: { 27 | description: "This boolean property indicates whether the option is enabled or disabled.", 28 | valueType: "boolean", 29 | defaultValue: defaultProps.enabled, 30 | }, 31 | keywords: d( 32 | "An array of keywords that can be used for searching and filtering the option. " + 33 | "These keywords are not displayed but help users find the option through search.", 34 | ), 35 | }, 36 | }); 37 | 38 | export const optionComponentRenderer = createComponentRenderer( 39 | COMP, 40 | OptionMd, 41 | ({ node, extractValue, className, renderChild, layoutContext }) => { 42 | const label = extractValue.asOptionalString(node.props.label); 43 | let value = extractValue(node.props.value); 44 | if (label === undefined && value === undefined) { 45 | return null; 46 | } 47 | 48 | const hasTextNodeChild = node.children?.length === 1 && (node.children[0].type === "TextNode" || node.children[0].type === "TextNodeCData"); 49 | const textNodeChild = hasTextNodeChild ? renderChild(node.children) as string : undefined; 50 | 51 | return ( 52 | <OptionNative 53 | label={label || textNodeChild} 54 | value={value !== undefined && value !== "" ? value : label} 55 | enabled={extractValue.asOptionalBoolean(node.props.enabled)} 56 | keywords={extractValue.asOptionalStringArray(node.props.keywords)} 57 | className={className} 58 | optionRenderer={ 59 | node.children?.length > 0 60 | ? !hasTextNodeChild ? (contextVars) => ( 61 | <MemoizedItem 62 | node={node.children} 63 | renderChild={renderChild} 64 | contextVars={contextVars} 65 | layoutContext={layoutContext} 66 | /> 67 | ) : undefined 68 | : undefined 69 | } 70 | > 71 | {!hasTextNodeChild && renderChild(node.children)} 72 | </OptionNative> 73 | ); 74 | }, 75 | ); 76 | ``` -------------------------------------------------------------------------------- /docs/public/pages/howto/paginate-a-list.md: -------------------------------------------------------------------------------- ```markdown 1 | # Paginate a List 2 | 3 | XMLUI provides a `Pagination` component that can be used to display visual controls for the pagination feature, no matter whether it is handled inside or outside of a layout component requiring that feature. 4 | 5 | The [`Table`](./table) component provides out-of-the-box support for pagination, 6 | so you can access pagination options via the following properties: `isPaginated`, `pageSize`, `pageSizeOptions`, `paginationControlsLocation`. 7 | 8 | ```xmlui noHeader copy 9 | <Table 10 | data="/api/endpoint" 11 | isPaginated 12 | pageSize="10" 13 | pageSizeOptions="{[5, 10, 20, 30]}" 14 | paginationControlsLocation="both" 15 | > 16 | ... 17 | </Table> 18 | ``` 19 | 20 | Other components, such as the `List`, can be hooked up with pagination using a `DataSource` combined with the `Pagination` component. This pattern works as a more generic solution where either the component does not have pagination implemented in the component itself, or you wish to use custom pagination logic. 21 | 22 | In this case the `DataSource` component does the heavy lifting by querying the page index, the previous and next page IDs. This can be done using variables and query parameters. 23 | 24 | ```xmlui-pg 25 | ---app display 26 | <App var.pageSize="{5}" var.currentPage="{0}" var.before="{0}" var.after="{pageSize-1}"> 27 | <DataSource id="pagination_ds" url="/api/pagination_items" queryParams="{{ from: before, to: after }}" /> 28 | <Pagination 29 | itemCount="20" 30 | pageSize="{pageSize}" 31 | pageIndex="{currentPage}" 32 | onPageDidChange="(page, size, total) => { 33 | currentPage = page; 34 | before = page * size; 35 | after = before + size - 1; 36 | }" 37 | /> 38 | <List data="{pagination_ds}" /> 39 | </App> 40 | ---api 41 | { 42 | "apiUrl": "/api", 43 | "initialize": "$state.pagination_items = [{ id: 1, name: 'Laptop Pro', price: 1299 },{ id: 2, name: 'Wireless Mouse', price: 29 },{ id: 3, name: 'Mechanical Keyboard', price: 149 },{ id: 4, name: '4K Monitor', price: 399 },{ id: 5, name: 'USB-C Hub', price: 79 },{ id: 6, name: 'Bluetooth Headphones', price: 199 },{ id: 7, name: 'Webcam HD', price: 89 },{ id: 8, name: 'Standing Desk', price: 299 },{ id: 9, name: 'Ergonomic Chair', price: 249 },{ id: 10, name: 'Desk Lamp', price: 45 },{ id: 11, name: 'Cable Organizer', price: 15 },{ id: 12, name: 'Mouse Pad', price: 12 },{ id: 13, name: 'Laptop Stand', price: 35 },{ id: 14, name: 'External SSD', price: 129 },{ id: 15, name: 'Wireless Charger', price: 59 },{ id: 16, name: 'Smart Speaker', price: 99 },{ id: 17, name: 'Fitness Tracker', price: 199 },{ id: 18, name: 'Tablet Pro', price: 799 },{ id: 19, name: 'Gaming Mouse', price: 89 },{ id: 20, name: 'Noise Cancelling Headphones', price: 349 }]", 44 | "operations": { 45 | "get-pagination-items": { 46 | "url": "/pagination_items", 47 | "method": "get", 48 | "queryParams": { 49 | "from": "integer", 50 | "to": "integer" 51 | }, 52 | "handler": "$state.pagination_items.slice(Number($queryParams.from), Number($queryParams.to) + 1);" 53 | } 54 | } 55 | } 56 | ``` 57 | ``` -------------------------------------------------------------------------------- /docs/content/components/FormSection.md: -------------------------------------------------------------------------------- ```markdown 1 | # FormSection [#formsection] 2 | 3 | `FormSection` groups elements within a `Form`. Child components are placed in a [FlowLayout](/components/FlowLayout). 4 | 5 | ## Properties [#properties] 6 | 7 | ### `columnGap` (default: "3rem") [#columngap-default-3rem] 8 | 9 | The gap between columns of items within the section. 10 | 11 | ```xmlui-pg copy display name="Example: columnGap" 12 | <Form padding="1rem"> 13 | <FormSection columnGap="1rem"> 14 | <FormItem width="50%" label="Name" bindTo="" /> 15 | <FormItem width="50%" label="Occupation" bindTo="" /> 16 | </FormSection> 17 | </Form> 18 | ``` 19 | 20 | ### `heading` [#heading] 21 | 22 | The heading text to be displayed at the top of the form section. 23 | 24 | ```xmlui-pg copy display name="Example: heading" 25 | <Form padding="1rem"> 26 | <FormSection heading="Basic Heading"> 27 | <FormItem label="Input Field" bindTo="" /> 28 | </FormSection> 29 | </Form> 30 | ``` 31 | 32 | ### `headingLevel` (default: "h3") [#headinglevel-default-h3] 33 | 34 | The semantic and visual level of the heading. 35 | 36 | Available values: `h1`, `h2`, `h3` **(default)**, `h4`, `h5`, `h6` 37 | 38 | ```xmlui-pg copy display name="Example: headingLevel" 39 | <Form padding="1rem"> 40 | <FormSection heading="Basic Heading" headingLevel="h1"> 41 | <FormItem label="Input Field" bindTo="" /> 42 | </FormSection> 43 | </Form> 44 | ``` 45 | 46 | ### `headingWeight` (default: "bold") [#headingweight-default-bold] 47 | 48 | The font weight of the heading. 49 | 50 | The default weight is `bold`. 51 | 52 | ```xmlui-pg copy display name="Example: headingWeight" 53 | <Form padding="1rem"> 54 | <FormSection heading="Basic Heading" headingWeight="normal"> 55 | <FormItem label="Input Field" bindTo="" /> 56 | </FormSection> 57 | </Form> 58 | ``` 59 | 60 | ### `info` [#info] 61 | 62 | Informational text displayed below the heading. 63 | 64 | ```xmlui-pg copy display name="Example: info" 65 | <Form padding="1rem"> 66 | <FormSection info="This is some information about a particular section."> 67 | <FormItem label="Input Field" bindTo="" /> 68 | </FormSection> 69 | </Form> 70 | ``` 71 | 72 | ### `infoFontSize` (default: "0.8rem") [#infofontsize-default-0-8rem] 73 | 74 | The font size of the informational text. 75 | 76 | ```xmlui-pg copy {4} display name="Example: infoFontSize" 77 | <Form padding="1rem"> 78 | <FormSection 79 | info="This is some information about a particular section." 80 | infoFontSize="18px" 81 | > 82 | <FormItem label="Input Field" bindTo="" /> 83 | </FormSection> 84 | </Form> 85 | ``` 86 | 87 | ### `paddingTop` (default: "$space-normal") [#paddingtop-default-space-normal] 88 | 89 | The top padding of the FlowLayout where the section's children are placed. 90 | 91 | ### `rowGap` (default: "$space-normal") [#rowgap-default-space-normal] 92 | 93 | The gap between rows of items within the section. 94 | 95 | ```xmlui-pg copy display name="Example: rowGap" 96 | <Form padding="1rem"> 97 | <FormSection rowGap="2rem"> 98 | <FormItem label="Name" bindTo="" /> 99 | <FormItem label="Occupation" bindTo="" /> 100 | </FormSection> 101 | </Form> 102 | ``` 103 | 104 | ## Events [#events] 105 | 106 | This component does not have any events. 107 | 108 | ## Exposed Methods [#exposed-methods] 109 | 110 | This component does not expose any methods. 111 | 112 | ## Styling [#styling] 113 | 114 | This component does not have any styles. 115 | ``` -------------------------------------------------------------------------------- /packages/xmlui-devtools/src/devtools/ModalDialog.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "xmlui/themes.scss" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $component: "ModalDialog"; 5 | $themeVars: (); 6 | @function createThemeVar($componentVariable) { 7 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 8 | @return t.getThemeVar($themeVars, $componentVariable); 9 | } 10 | 11 | // --- Theme vars for paddings 12 | $themeVars: t.composePaddingVars($themeVars, $component); 13 | $themeVars: t.composePaddingVars($themeVars, "overlay-#{$component}"); 14 | $padding-ModalDialog: createThemeVar("padding-#{$component}"); 15 | $backgroundColor-ModalDialog: createThemeVar("Dialog:backgroundColor-#{$component}"); 16 | $backgroundColor-overlay-ModalDialog: createThemeVar("Dialog:backgroundColor-overlay-#{$component}"); 17 | $borderRadius-ModalDialog: createThemeVar("Dialog:borderRadius-#{$component}"); 18 | $fontFamily-ModalDialog: createThemeVar("Dialog:fontFamily-#{$component}"); 19 | $textColor-ModalDialog: createThemeVar("Dialog:textColor-#{$component}"); 20 | $minWidth-ModalDialog: createThemeVar("Dialog:minWidth-#{$component}"); 21 | $maxWidth-ModalDialog: createThemeVar("Dialog:maxWidth-#{$component}"); 22 | $marginBottom-title-ModalDialog: createThemeVar("Dialog:marginBottom-title-#{$component}"); 23 | 24 | 25 | .overlay { 26 | position: fixed; 27 | overflow-y: auto; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | inset: 0; 32 | padding: t.$space-4; 33 | } 34 | 35 | .overlayBg { 36 | background-color: $backgroundColor-overlay-ModalDialog; 37 | position: fixed; 38 | inset: 0; 39 | } 40 | 41 | .contentWrapper { 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | width: 100%; 46 | height: 100%; 47 | } 48 | 49 | .content { 50 | background-color: $backgroundColor-ModalDialog; 51 | border-radius: $borderRadius-ModalDialog; 52 | font-family: $fontFamily-ModalDialog; 53 | color: $textColor-ModalDialog; 54 | box-shadow: t.$boxShadow-spread; 55 | width: 90vw; 56 | max-width: 960px; 57 | min-width: 240px; 58 | isolation: isolate; 59 | position: relative; 60 | display: flex; 61 | flex-direction: column; 62 | max-height: 80vh; 63 | overflow-y: auto; 64 | } 65 | 66 | .content:focus { 67 | outline: none; 68 | } 69 | 70 | .dialogTitle { 71 | flex: 1; 72 | margin-bottom: $marginBottom-title-ModalDialog; 73 | font-size: t.$fontSize-2xl; 74 | } 75 | 76 | .innerContent { 77 | display: flex; 78 | flex-direction: column; 79 | min-height: 0; 80 | gap: var(--stack-gap-default); 81 | flex: 1; 82 | } 83 | 84 | .closeButton { 85 | position: absolute; 86 | right: 0.5rem; 87 | top: 0.4rem; 88 | } 89 | 90 | .actions { 91 | display: inline-flex; 92 | align-items: center; 93 | justify-content: flex-end; 94 | } 95 | 96 | .header { 97 | padding: t.$space-2; 98 | justify-content: space-between; 99 | display: flex; 100 | flex-direction: row; 101 | border-bottom: 1px solid t.$borderColor; 102 | } 103 | 104 | @media (max-width: 70em) { 105 | .dialog, .content { 106 | max-width: 90%; 107 | } 108 | } 109 | 110 | @media (max-width: 50em) { 111 | .dialog, .content { 112 | width: 100%; 113 | max-width: calc(100% - #{t.$space-6}); 114 | min-width: 0 !important; 115 | } 116 | } 117 | 118 | // --- We export the theme variables to add them to the component renderer 119 | :export { 120 | themeVars: t.json-stringify($themeVars); 121 | } 122 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Stack/HStack.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | test("renders items horizontally", async ({ initTestBed, page }) => { 9 | await initTestBed(` 10 | <HStack testId="hstack"> 11 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 12 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 13 | <Stack testId="item3" height="32px" width="32px" backgroundColor="green" /> 14 | </HStack> 15 | `); 16 | 17 | const hstack = page.getByTestId("hstack"); 18 | const item1 = page.getByTestId("item1"); 19 | const item2 = page.getByTestId("item2"); 20 | const item3 = page.getByTestId("item3"); 21 | 22 | await expect(hstack).toBeVisible(); 23 | await expect(item1).toBeVisible(); 24 | await expect(item2).toBeVisible(); 25 | await expect(item3).toBeVisible(); 26 | 27 | // Get bounding boxes to verify horizontal layout 28 | const item1Box = await item1.boundingBox(); 29 | const item2Box = await item2.boundingBox(); 30 | const item3Box = await item3.boundingBox(); 31 | 32 | // Verify items are stacked horizontally (item2 should be to the right of item1, item3 to the right of item2) 33 | expect(item2Box!.x).toBeGreaterThan(item1Box!.x + item1Box!.width - 1); // -1 for floating point tolerance 34 | expect(item3Box!.x).toBeGreaterThan(item2Box!.x + item2Box!.width - 1); // -1 for floating point tolerance 35 | 36 | // Verify items are vertically aligned (should start at roughly the same y position) 37 | expect(Math.abs(item1Box!.y - item2Box!.y)).toBeLessThan(1); 38 | expect(Math.abs(item2Box!.y - item3Box!.y)).toBeLessThan(1); 39 | }); 40 | 41 | test("renders empty HStack", async ({ initTestBed, page }) => { 42 | await initTestBed(`<HStack testId="hstack"></HStack>`); 43 | 44 | const hstack = page.getByTestId("hstack"); 45 | await expect(hstack).toBeAttached(); 46 | await expect(hstack).toBeEmpty(); 47 | }); 48 | 49 | test("ignores orientation property", async ({ initTestBed, page }) => { 50 | await initTestBed(` 51 | <HStack testId="hstack" orientation="vertical"> 52 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 53 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 54 | </HStack> 55 | `); 56 | 57 | const item1 = page.getByTestId("item1"); 58 | const item2 = page.getByTestId("item2"); 59 | 60 | await expect(item1).toBeVisible(); 61 | await expect(item2).toBeVisible(); 62 | 63 | // Get bounding boxes to verify still renders horizontally despite orientation="vertical" 64 | const item1Box = await item1.boundingBox(); 65 | const item2Box = await item2.boundingBox(); 66 | 67 | // Verify items are still stacked horizontally (orientation prop should be ignored) 68 | expect(item2Box!.x).toBeGreaterThan(item1Box!.x + item1Box!.width - 1); 69 | }); 70 | }); 71 | ``` -------------------------------------------------------------------------------- /xmlui/bin/vite-xmlui-plugin.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { dataToEsm } from "@rollup/pluginutils"; 2 | import type { Plugin } from "vite"; 3 | import { 4 | collectCodeBehindFromSource, 5 | removeCodeBehindTokensFromTree, 6 | } from "../src/parsers/scripting/code-behind-collect"; 7 | import { 8 | codeBehindFileExtension, 9 | componentFileExtension, 10 | moduleFileExtension, 11 | } from "../src/parsers/xmlui-parser/fileExtensions"; 12 | import { Parser } from "../src/parsers/scripting/Parser"; 13 | import * as fs from "fs"; 14 | import * as path from "path"; 15 | import { errReportComponent, xmlUiMarkupToComponent } from "../src/components-core/xmlui-parser"; 16 | 17 | export type PluginOptions = { 18 | // --- Add plugin options here. 19 | }; 20 | 21 | const xmluiExtension = new RegExp(`.${componentFileExtension}$`); 22 | const xmluiScriptExtension = new RegExp(`.${codeBehindFileExtension}$`); 23 | const moduleScriptExtension = new RegExp(`.${moduleFileExtension}$`); 24 | 25 | /** 26 | * Transform XMLUI files to JS objects. 27 | */ 28 | export default function viteXmluiPlugin(pluginOptions: PluginOptions = {}): Plugin { 29 | let itemIndex = 0; 30 | return { 31 | name: "vite:transform-xmlui", 32 | 33 | async transform(code: string, id: string, options) { 34 | const moduleNameResolver = (moduleName: string) => { 35 | return path.resolve(path.dirname(id), moduleName); 36 | }; 37 | 38 | if (xmluiExtension.test(id)) { 39 | const fileId = "" + itemIndex++; 40 | let { component, errors, erroneousCompoundComponentName } = xmlUiMarkupToComponent( 41 | code, 42 | fileId, 43 | ); 44 | if (errors.length > 0) { 45 | component = errReportComponent(errors, id, erroneousCompoundComponentName); 46 | } 47 | const file = { 48 | component, 49 | src: code, 50 | file: fileId, 51 | }; 52 | 53 | return { 54 | code: dataToEsm(file), 55 | map: { mappings: "" }, 56 | }; 57 | } 58 | 59 | const hasXmluiScriptExtension = xmluiScriptExtension.test(id); 60 | const hasModuleScriptExtension = moduleScriptExtension.test(id); 61 | if (hasXmluiScriptExtension || hasModuleScriptExtension) { 62 | // --- We parse the module file to catch parsing errors 63 | 64 | const parser = new Parser(code); 65 | parser.parseStatements(); 66 | const moduleName = hasXmluiScriptExtension 67 | ? id.substring(0, id.length - (codeBehindFileExtension.length + 1)) 68 | : id.substring(0, id.length - (moduleFileExtension.length + 1)); 69 | 70 | const codeBehind = collectCodeBehindFromSource(moduleNameResolver(moduleName), code); 71 | removeCodeBehindTokensFromTree(codeBehind); 72 | 73 | // TODO: Add error handling. 74 | // Check, if codeBehind.moduleErrors is not empty (Record<string, ModuleErrors[]>); each module 75 | // should be checked for errors and warnings. If there are errors, throw an error. 76 | 77 | return { 78 | code: dataToEsm({...codeBehind, src: code}), 79 | map: { mappings: "" }, 80 | }; 81 | } 82 | return null; 83 | }, 84 | // async generateBundle(opts, bundle, isWrite){ 85 | // console.log('generate bundle', opts); 86 | // } 87 | }; 88 | } 89 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/loader/ApiLoader.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { useCallback } from "react"; 2 | 3 | import type { 4 | LoaderErrorFn, 5 | LoaderInProgressChangedFn, 6 | LoaderLoadedFn, 7 | } from "../abstractions/LoaderRenderer"; 8 | import type { ComponentDef } from "../../abstractions/ComponentDefs"; 9 | import type { ContainerState } from "../rendering/ContainerWrapper"; 10 | import { removeNullProperties } from "../utils/misc"; 11 | import { extractParam } from "../utils/extractParam"; 12 | import { createLoaderRenderer } from "../renderers"; 13 | import { useAppContext } from "../AppContext"; 14 | import { Loader } from "./Loader"; 15 | import { createMetadata, d } from "../../components/metadata-helpers"; 16 | 17 | /** 18 | * Properties of the API loader component 19 | */ 20 | type ApiLoaderProps = { 21 | loader: ApiLoaderDef; 22 | loaderInProgressChanged: LoaderInProgressChangedFn; 23 | loaderIsRefetchingChanged: LoaderInProgressChangedFn; 24 | loaderLoaded: LoaderLoadedFn; 25 | loaderError: LoaderErrorFn; 26 | state: ContainerState; 27 | doNotRemoveNulls?: boolean; 28 | structuralSharing?: boolean; 29 | }; 30 | 31 | /** 32 | * Represents a non-displayed React component, which handles the specified API loader 33 | */ 34 | function ApiLoader({ 35 | loader, 36 | loaderInProgressChanged, 37 | loaderIsRefetchingChanged, 38 | loaderLoaded, 39 | loaderError, 40 | state, 41 | doNotRemoveNulls, 42 | structuralSharing = true, 43 | }: ApiLoaderProps) { 44 | const appContext = useAppContext(); 45 | 46 | const url = extractParam(state, loader.props.url, appContext); 47 | const loadable = !!url; 48 | 49 | const doLoad = useCallback(async () => { 50 | if (!loadable) { 51 | return; 52 | } 53 | const response = await fetch(url); 54 | if (loader.props.raw) { 55 | return await response.text(); 56 | } 57 | const responseObj = await response.json(); 58 | if (!doNotRemoveNulls) { 59 | removeNullProperties(responseObj); 60 | } 61 | return responseObj; 62 | }, [doNotRemoveNulls, loadable, loader.props.raw, url]); 63 | 64 | return ( 65 | <Loader 66 | state={state} 67 | loader={loader} 68 | loaderInProgressChanged={loaderInProgressChanged} 69 | loaderIsRefetchingChanged={loaderIsRefetchingChanged} 70 | loaderLoaded={loaderLoaded} 71 | loaderError={loaderError} 72 | loaderFn={doLoad} 73 | structuralSharing={structuralSharing} 74 | /> 75 | ); 76 | } 77 | 78 | const ApiLoaderMd = createMetadata({ 79 | status: "stable", 80 | description: `Represents a loader that calls an API through an HTTP/HTTPS GET request`, 81 | props: { 82 | url: d("URL segment to use in the GET request"), 83 | raw: d("If true, the loader returns the raw text response instead of parsing it as JSON"), 84 | }, 85 | }); 86 | 87 | type ApiLoaderDef = ComponentDef<typeof ApiLoaderMd>; 88 | 89 | export const apiLoaderRenderer = createLoaderRenderer( 90 | "ApiLoader", 91 | ({ loader, state, loaderInProgressChanged, loaderIsRefetchingChanged, loaderLoaded, loaderError }) => { 92 | return ( 93 | <ApiLoader 94 | loader={loader} 95 | state={state} 96 | loaderInProgressChanged={loaderInProgressChanged} 97 | loaderIsRefetchingChanged={loaderIsRefetchingChanged} 98 | loaderLoaded={loaderLoaded} 99 | loaderError={loaderError} 100 | /> 101 | ); 102 | }, 103 | ApiLoaderMd, 104 | ); 105 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/ToneChangerButton/ToneChangerButton.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { useThemes } from "../../components-core/theming/ThemeContext"; 2 | import { createComponentRenderer } from "../../components-core/renderers"; 3 | import { Button } from "../Button/ButtonNative"; 4 | import { Icon } from "../Icon/IconNative"; 5 | import { createMetadata, dClick } from "../metadata-helpers"; 6 | import { noop } from "lodash-es"; 7 | 8 | const COMP = "ToneChangerButton"; 9 | const LIGHT_TO_DARK_ICON = "lightToDark:ToneChangerButton"; 10 | const DARK_TO_LIGHT_ICON = "darkToLight:ToneChangerButton"; 11 | 12 | export const defaultProps = { 13 | lightToDarkIcon: LIGHT_TO_DARK_ICON, 14 | darkToLightIcon: DARK_TO_LIGHT_ICON, 15 | onClick: noop, 16 | }; 17 | 18 | export const ToneChangerButtonMd = createMetadata({ 19 | status: "stable", 20 | description: "`ToneChangerButton` enables the user to switch between light and dark modes.", 21 | props: { 22 | lightToDarkIcon: { 23 | description: 24 | `The icon displayed when the theme is in light mode and will switch to dark. You can change ` + 25 | `the default icon for all ${COMP} instances with the "icon.lightToDark:ToneChangerButton" ` + 26 | `declaration in the app configuration file.`, 27 | defaultValue: defaultProps.lightToDarkIcon, 28 | }, 29 | darkToLightIcon: { 30 | description: 31 | `The icon displayed when the theme is in dark mode and will switch to light. You can change ` + 32 | `the default icon for all ${COMP} instances with the "icon.darkToLight:ToneChangerButton" ` + 33 | `declaration in the app configuration file.`, 34 | defaultValue: defaultProps.darkToLightIcon, 35 | }, 36 | }, 37 | events: { 38 | click: dClick(COMP), 39 | }, 40 | }); 41 | 42 | export function ToneChangerButton({ 43 | lightToDarkIcon = defaultProps.lightToDarkIcon, 44 | darkToLightIcon = defaultProps.darkToLightIcon, 45 | onClick = defaultProps.onClick, 46 | }) { 47 | const { activeThemeTone, setActiveThemeTone } = useThemes(); 48 | 49 | // Use the direct icon name as both the main icon and the fallback 50 | // This ensures we always have a working icon 51 | const iconName = activeThemeTone === "light" ? lightToDarkIcon : darkToLightIcon; 52 | const fallbackIcon = activeThemeTone === "light" ? "lightToDark" : "darkToLight"; 53 | 54 | return ( 55 | <Button 56 | variant="ghost" 57 | style={{ flexShrink: 0 }} 58 | icon={<Icon name={iconName} fallback={fallbackIcon} />} 59 | onClick={() => { 60 | if (activeThemeTone === "light") { 61 | setActiveThemeTone("dark"); 62 | onClick?.("dark"); 63 | } else { 64 | setActiveThemeTone("light"); 65 | onClick?.("light"); 66 | } 67 | }} 68 | /> 69 | ); 70 | } 71 | 72 | /** 73 | * Define the renderer for the ToneChangerButton component 74 | */ 75 | export const toneChangerButtonComponentRenderer = createComponentRenderer( 76 | COMP, 77 | ToneChangerButtonMd, 78 | ({ node, extractValue, lookupEventHandler }) => { 79 | return ( 80 | <ToneChangerButton 81 | onClick={lookupEventHandler("click")} 82 | lightToDarkIcon={extractValue.asOptionalString(node.props.lightToDarkIcon)} 83 | darkToLightIcon={extractValue.asOptionalString(node.props.darkToLightIcon)} 84 | /> 85 | ); 86 | }, 87 | ); 88 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Pages/PagesNative.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type { CSSProperties, ReactNode } from "react"; 2 | import { useMemo } from "react"; 3 | import { Navigate, Route, Routes, useParams } from "@remix-run/react"; 4 | import classnames from "classnames"; 5 | 6 | import type { ComponentDef } from "../../abstractions/ComponentDefs"; 7 | import type { LayoutContext, RenderChildFn, ValueExtractor } from "../../abstractions/RendererDefs"; 8 | import { EMPTY_ARRAY, EMPTY_OBJECT } from "../../components-core/constants"; 9 | import type { PageMd } from "./Pages"; 10 | import styles from "./Pages.module.scss"; 11 | 12 | // Default props for Pages component 13 | export const defaultProps = { 14 | fallbackPath: "/", 15 | }; 16 | 17 | // --- We need this component to make sure all the child routes are wrapped in a 18 | // --- container and this way they can access the routeParams 19 | type RouteWrapperProps = { 20 | childRoute?: ComponentDef | Array<ComponentDef>; 21 | renderChild: RenderChildFn; 22 | layoutContext?: LayoutContext; 23 | style?: CSSProperties; 24 | className?: string; 25 | uid?: string; 26 | }; 27 | 28 | export function RouteWrapper({ 29 | childRoute = EMPTY_ARRAY, 30 | renderChild, 31 | layoutContext, 32 | style, 33 | className, 34 | uid, 35 | }: RouteWrapperProps) { 36 | const params = useParams(); 37 | 38 | //we need to wrap the child route in a container to make sure the route params are available. 39 | // we do this wrapping by providing an empty object to vars. 40 | // this way it becomes an 'implicit' container (vars/state inside this container is propagated to the parent) 41 | const wrappedWithContainer = useMemo(() => { 42 | if (Array.isArray(childRoute)) { 43 | return { 44 | type: "Fragment", 45 | uid, 46 | vars: EMPTY_OBJECT, 47 | children: childRoute, 48 | }; 49 | } 50 | return { 51 | type: "Fragment", 52 | uid, 53 | vars: EMPTY_OBJECT, 54 | children: [childRoute], 55 | }; 56 | }, [childRoute, uid]); 57 | 58 | return ( 59 | <div 60 | key={JSON.stringify(params)} 61 | className={classnames(className, styles.wrapper, "xmlui-page-root")} 62 | style={style} 63 | > 64 | {renderChild(wrappedWithContainer, layoutContext)} 65 | </div> 66 | ); 67 | } 68 | 69 | type PageComponentDef = ComponentDef<typeof PageMd>; 70 | 71 | type PagesProps = { 72 | fallbackPath?: string; 73 | node?: ComponentDef; 74 | renderChild: RenderChildFn; 75 | extractValue: ValueExtractor; 76 | children?: ReactNode; 77 | className?: ReactNode; 78 | }; 79 | 80 | export function Pages({ 81 | node, 82 | renderChild, 83 | extractValue, 84 | fallbackPath = defaultProps.fallbackPath, 85 | }: PagesProps) { 86 | const routes: Array<PageComponentDef> = []; 87 | const restChildren: Array<ComponentDef> = []; 88 | node.children?.forEach((child) => { 89 | if (child.type === "Page") { 90 | routes.push(child as PageComponentDef); 91 | } else { 92 | restChildren.push(child); 93 | } 94 | }); 95 | return ( 96 | <> 97 | <Routes> 98 | {routes.map((child, i) => { 99 | return ( 100 | <Route path={extractValue(child.props.url)} key={i} element={renderChild(child)} /> 101 | ); 102 | })} 103 | {fallbackPath && <Route path="*" element={<Navigate to={fallbackPath} replace />} />} 104 | </Routes> 105 | {renderChild(restChildren)} 106 | </> 107 | ); 108 | } 109 | ``` -------------------------------------------------------------------------------- /docs/public/pages/app-structure.md: -------------------------------------------------------------------------------- ```markdown 1 | # Structure of an XMLUI app 2 | 3 | The [XMLUI Invoice demo app](https://github.com/xmlui-org/xmlui-invoice/releases) exhibits the typical structure of an XMLUI app. 4 | 5 | ```xmlui-tree 6 | <root> 7 | index.html 8 | Main.xmlui 9 | config.json 10 | components 11 | ClientDetails.xmlui 12 | Clients.xmlui 13 | ... 14 | MonthlyRevenue.xmlui 15 | WeeklyRevenue.xmlui 16 | resources 17 | favicon.ico 18 | xmlui-logo-inverted.svg 19 | xmlui-logo.svg 20 | themes 21 | invoice.json 22 | xmlui 23 | 0.9.23.js 24 | charts-0.1.21.js 25 | start.bat 26 | start.sh 27 | api.json 28 | data.db 29 | xmlui-test-server 30 | ``` 31 | 32 | > [!INFO] The `xmlui` folder contains the xmlui engine with a version number, specifically `0.9.23.js`. We recommend this practice in order to know when/whether to upgrade. 33 | 34 | 35 | | file| description | 36 | |---|---| 37 | | **`index.html`** | The default webpage to display | 38 | | **`Main.xmlui`** | The XMLUI app's entry point | 39 | | **`config.json`** | The XMLUI app's configuration file | 40 | | **`components`** | The folder with your custom components | 41 | | **`resources`** | The folder with static app resources | 42 | | **`themes`** | The folder with your custom themes | 43 | | **`xmlui`** | The folder with the XMLUI core framework and extensions | 44 | | **`start.bat`** | The batch file to start the test server on Windows | 45 | | **`start.sh`** | The bash script file to start the test server on Mac, Linux, or WSL | 46 | | **`api.json`** | *Optional*: API description file for use with xmlui-test-server | 47 | | **`data.db`** | *Optional*: SQLite database for use with xmlui-test-server| 48 | | **`xmlui-test-server`** | *Optional*: server, you can use any static web server| 49 | 50 | 51 | You can deploy this tree structure (minus the optional `api.json`, `data.db`, and `xmlui-test-server`) to any static webserver that's configured to serve `index.html`. Consider this minimal app. 52 | 53 | ```xmlui-tree 54 | xmlui-minimal 55 | index.html 56 | Main.xmlui 57 | components 58 | Home.xmlui 59 | resources 60 | favicon.ico 61 | xmlui-logo-inverted.svg 62 | xmlui-logo.svg 63 | xmlui 64 | 0.9.23.js 65 | ``` 66 | 67 | ## index.html 68 | 69 | ```html 70 | <!DOCTYPE html> 71 | <html lang="en"> 72 | 73 | <head> 74 | <meta charset="UTF-8" /> 75 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 76 | <script src="xmlui/0.9.23.js"></script> 77 | </head> 78 | 79 | <body> 80 | </body> 81 | 82 | </html> 83 | ``` 84 | 85 | ## Main.xmlui 86 | 87 | ```xmlui 88 | <App name="XMLUI Minimal"> 89 | 90 | <NavPanel> 91 | <NavLink label="Home" to="/Home" /> 92 | </NavPanel> 93 | 94 | <Pages> 95 | <Page url="/Home"> 96 | <Home /> 97 | </Page> 98 | </Pages> 99 | 100 | </App> 101 | ``` 102 | 103 | ## Home.xmlui 104 | 105 | ```xmlui 106 | <Component name="Home" > 107 | 108 | A minimal XMLUI app 109 | 110 | </Component> 111 | ``` 112 | 113 | ## Local deployment 114 | 115 | If you are working locally, in a folder at the root of this tree, here are some ways you can serve the app. 116 | 117 | If you have node.js and npm: 118 | 119 | ``` 120 | npx -y http-server 121 | 122 | $ npx -y http-server 123 | Starting up http-server, serving ./ 124 | 125 | Available on: 126 | http://127.0.0.1:8080 127 | ``` 128 | 129 | If you have python: 130 | 131 | ``` 132 | $ python -m http.server 8080 133 | Serving HTTP on :: port 8080 (http://[::]:8080/) ... 134 | ``` 135 | 136 | In either case, visit http://localhost:8080 to view the app. 137 | 138 | See also [Hosted deployment](/hosted-deployment). 139 | 140 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Charts/DonutChart/DonutChart.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createComponentRenderer } from "../../../components-core/renderers"; 2 | import styles from "../PieChart/PieChartNative.module.scss"; 3 | import { defaultProps, PieChart } from "../PieChart/PieChartNative"; 4 | import { parseScssVar } from "../../../components-core/theming/themeVars"; 5 | import { createMetadata } from "../../metadata-helpers"; 6 | 7 | const COMP = "DonutChart"; 8 | 9 | const defaultPropsDonut = { 10 | ...defaultProps, 11 | innerRadius: 60, 12 | }; 13 | 14 | export const DonutChartMd = createMetadata({ 15 | status: "experimental", 16 | description: "A derivative of [PieChart](/components/PieChart) with a hollow center. " + 17 | "Note that the height of the component or its parent needs to be set explicitly.", 18 | props: { 19 | data: { 20 | description: "The data to be displayed in the chart. Needs to be an array of objects.", 21 | }, 22 | nameKey: { 23 | description: 24 | "Specifies the key in the data objects that will be used to label the different data series.", 25 | valueType: "string", 26 | }, 27 | dataKey: { 28 | description: 29 | "This property specifies the key in the data objects that will be used to render the chart.", 30 | valueType: "string", 31 | }, 32 | showLabel: { 33 | description: "Toggles whether to show labels (\`true\`) or not (\`false\`).", 34 | valueType: "boolean", 35 | defaultValue: defaultPropsDonut.showLabel, 36 | }, 37 | innerRadius: { 38 | description: "Sets the inner radius of the donut chart.", 39 | valueType: "number", 40 | defaultValue: defaultPropsDonut.innerRadius, 41 | }, 42 | showLabelList: { 43 | description: "Whether to show labels in a list (\`true\`) or not (\`false\`).", 44 | valueType: "boolean", 45 | defaultValue: defaultPropsDonut.showLabelList, 46 | }, 47 | showLegend: { 48 | description: "Whether to show a legend (\`true\`) or not (\`false\`).", 49 | valueType: "boolean", 50 | defaultValue: defaultPropsDonut.showLegend, 51 | }, 52 | }, 53 | themeVars: parseScssVar(styles.themeVars), 54 | defaultThemeVars: { 55 | "textColor-labelList-PieChart": "$textColor-primary", 56 | }, 57 | }); 58 | 59 | export const donutChartComponentRenderer = createComponentRenderer( 60 | COMP, 61 | DonutChartMd, 62 | ({ extractValue, node, className, renderChild }) => { 63 | return ( 64 | <PieChart 65 | showLabelList={extractValue.asOptionalBoolean( 66 | node.props?.showLabelList, 67 | defaultPropsDonut.showLabelList, 68 | )} 69 | innerRadius={extractValue.asOptionalNumber( 70 | node.props?.innerRadius, 71 | defaultPropsDonut.innerRadius, 72 | )} 73 | data={extractValue(node.props?.data)} 74 | className={className} 75 | showLabel={extractValue.asOptionalBoolean( 76 | node.props?.showLabel, 77 | defaultPropsDonut.showLabel, 78 | )} 79 | dataKey={extractValue(node.props?.dataKey)} 80 | nameKey={extractValue(node.props?.nameKey)} 81 | showLegend={extractValue.asOptionalBoolean( 82 | node.props?.showLegend, 83 | defaultPropsDonut.showLegend, 84 | )} 85 | > 86 | {renderChild(node.children)} 87 | </PieChart> 88 | ); 89 | }, 90 | ); 91 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/List/List.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "../../components-core/theming/themes" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $themeVars: (); 5 | @function createThemeVar($componentVariable) { 6 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 7 | @return t.getThemeVar($themeVars, $componentVariable); 8 | } 9 | 10 | @layer components { 11 | .outerWrapper { 12 | overflow: auto; 13 | //max-height: 100%; 14 | overflow-anchor: none; 15 | 16 | &.hasOutsideScroll { 17 | overflow: initial; 18 | } 19 | } 20 | 21 | .innerWrapper { 22 | visibility: hidden; 23 | display: flex; 24 | flex-direction: column; 25 | min-height: 100%; 26 | 27 | &.reverse { 28 | justify-content: flex-end; 29 | } 30 | } 31 | 32 | .infoWrapper { 33 | width: 100%; 34 | margin: 0 auto; 35 | } 36 | 37 | .loadingWrapper { 38 | display: flex; 39 | flex-direction: row; 40 | justify-content: center; 41 | padding-top: t.$space-2; 42 | padding-bottom: t.$space-2; 43 | } 44 | 45 | .noRows { 46 | width: 100%; 47 | text-align: center; 48 | margin-top: t.$space-4; 49 | } 50 | 51 | // --- We export the theme variables to add them to the component renderer 52 | :export { 53 | themeVars: t.json-stringify($themeVars); 54 | } 55 | 56 | .borderCollapse { 57 | &:not(.sectioned) { 58 | .row { 59 | //first row in the list (but not the last) 60 | &:first-child:not(:last-child) { 61 | & > * { 62 | border-bottom-left-radius: 0; 63 | border-bottom-right-radius: 0; 64 | border-bottom: 0; 65 | } 66 | } 67 | 68 | //last row in the list (but not the first) 69 | &:last-child:not(:first-child) { 70 | & > * { 71 | border-top-left-radius: 0; 72 | border-top-right-radius: 0; 73 | } 74 | } 75 | 76 | //rows in the middle of the list 77 | &:not(:first-child):not(:last-child) { 78 | & > * { 79 | border-bottom-left-radius: 0; 80 | border-bottom-right-radius: 0; 81 | border-top-left-radius: 0; 82 | border-top-right-radius: 0; 83 | border-bottom: 0; 84 | } 85 | } 86 | } 87 | } 88 | 89 | &.sectioned { 90 | //.row after the section header (first row in a section) 91 | .section + .row:not(:has(+ .sectionFooter)) { 92 | & > * { 93 | border-bottom-left-radius: 0; 94 | border-bottom-right-radius: 0; 95 | border-bottom: 0; 96 | } 97 | } 98 | 99 | //.row before the sectionFooter (last row in a section) 100 | .row:has(+ .sectionFooter):not(.section + .row) { 101 | & > * { 102 | border-top-left-radius: 0; 103 | border-top-right-radius: 0; 104 | } 105 | } 106 | 107 | //we select the rows that has .row as direct sibling, but not the first row of a section (rows in the middle of the section) 108 | .row:has(+ .row):not(.section + .row) { 109 | & > * { 110 | border-bottom-left-radius: 0; 111 | border-bottom-right-radius: 0; 112 | border-top-left-radius: 0; 113 | border-top-right-radius: 0; 114 | border-bottom: 0; 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | // --- We export the theme variables to add them to the component renderer 122 | :export { 123 | themeVars: t.json-stringify($themeVars); 124 | } 125 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/ToneSwitch/ToneSwitch.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { expect, test } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | test("renders in light mode by default", async ({ initTestBed, page }) => { 9 | await initTestBed(` 10 | <App> 11 | <ToneSwitch /> 12 | <Text>{activeThemeTone}</Text> 13 | </App> 14 | `); 15 | const toggle = page.getByRole("switch"); 16 | await expect(toggle).toBeVisible(); 17 | await expect(page.getByText("light")).toBeVisible(); 18 | }); 19 | 20 | test("toggles to dark mode when clicked", async ({ initTestBed, page }) => { 21 | await initTestBed(` 22 | <App> 23 | <ToneSwitch /> 24 | <Text>{activeThemeTone}</Text> 25 | </App> 26 | `); 27 | const toggle = page.getByRole("switch"); 28 | await expect(toggle).toBeVisible(); 29 | await toggle.click({ force: true }); 30 | await expect(page.getByText("dark")).toBeVisible(); 31 | await expect(toggle).toBeChecked(); 32 | }); 33 | 34 | test("toggles back to light mode when clicked again", async ({ initTestBed, page }) => { 35 | await initTestBed(` 36 | <App> 37 | <ToneSwitch /> 38 | <Text>{activeThemeTone}</Text> 39 | </App> 40 | `); 41 | const toggle = page.getByRole("switch"); 42 | await expect(toggle).toBeVisible(); 43 | await toggle.click({ force: true }); 44 | await expect(page.getByText("dark")).toBeVisible(); 45 | await expect(toggle).toBeChecked(); 46 | await toggle.click({ force: true }); 47 | await expect(page.getByText("light")).toBeVisible(); 48 | await expect(toggle).not.toBeChecked(); 49 | }); 50 | }); 51 | 52 | // ============================================================================= 53 | // ACCESSIBILITY TESTS 54 | // ============================================================================= 55 | 56 | test.describe("Accessibility", () => { 57 | test("has switch role", async ({ initTestBed, page }) => { 58 | await initTestBed(`<ToneSwitch />`); 59 | const toggle = page.getByRole("switch"); 60 | await expect(toggle).toBeVisible(); 61 | }); 62 | 63 | test("is keyboard accessible with Space key", async ({ initTestBed, page }) => { 64 | await initTestBed(`<ToneSwitch />`); 65 | const toggle = page.getByRole("switch"); 66 | 67 | await toggle.focus(); 68 | await expect(toggle).toBeFocused(); 69 | 70 | await page.keyboard.press("Space"); 71 | await expect(toggle).toBeChecked(); 72 | }); 73 | 74 | test("maintains focus during interactions", async ({ initTestBed, page }) => { 75 | await initTestBed(`<ToneSwitch />`); 76 | const toggle = page.getByRole("switch"); 77 | 78 | await toggle.focus(); 79 | await toggle.click({ force: true }); 80 | await expect(toggle).toBeFocused(); 81 | }); 82 | 83 | test("has appropriate aria-checked state", async ({ initTestBed, page }) => { 84 | await initTestBed(`<ToneSwitch />`); 85 | const toggle = page.getByRole("switch"); 86 | 87 | await expect(toggle).toHaveAttribute("aria-checked", "false"); 88 | 89 | await toggle.click({ force: true }); 90 | await expect(toggle).toHaveAttribute("aria-checked", "true"); 91 | }); 92 | }); 93 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Charts/PieChart/PieChart.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { defaultProps, PieChart } from "./PieChartNative"; 2 | import styles from "./PieChartNative.module.scss"; 3 | import { LabelPositionValues } from "../utils/abstractions"; 4 | import { parseScssVar } from "../../../components-core/theming/themeVars"; 5 | import { createComponentRenderer } from "../../../components-core/renderers"; 6 | import type { LabelPosition } from "recharts/types/component/Label"; 7 | import { createMetadata, d } from "../../metadata-helpers"; 8 | 9 | const COMP = "PieChart"; 10 | 11 | export const PieChartMd = createMetadata({ 12 | status: "experimental", 13 | description: 14 | "`PieChart` visualizes proportional data as circular segments; each slice " + 15 | "represents a percentage of the whole. Note that the height of the component or " + 16 | "its parent needs to be set explicitly.", 17 | docFolder: "Charts/PieChart", 18 | props: { 19 | data: { 20 | description: "The data to be displayed in the chart. Needs to be an array of objects.", 21 | }, 22 | nameKey: { 23 | description: 24 | "Specifies the key in the data objects that will be used to label the different data series.", 25 | valueType: "string", 26 | }, 27 | dataKey: { 28 | description: 29 | "This property specifies the key in the data objects that will be used to render the chart.", 30 | valueType: "string", 31 | }, 32 | showLabel: { 33 | description: "Toggles whether to show labels (\`true\`) or not (\`false\`).", 34 | valueType: "boolean", 35 | defaultValue: defaultProps.showLabel, 36 | }, 37 | showLabelList: { 38 | description: "Whether to show labels in a list (\`true\`) or not (\`false\`).", 39 | valueType: "boolean", 40 | defaultValue: defaultProps.showLabelList, 41 | }, 42 | labelListPosition: { 43 | description: "The position of the label list.", 44 | valueType: "string", 45 | defaultValue: defaultProps.labelListPosition, 46 | availableValues: LabelPositionValues, 47 | }, 48 | outerRadius: d( 49 | "The outer radius of the pie chart, can be a number or a string (e.g., '100%').", 50 | ), 51 | showLegend: { 52 | description: "Toggles whether to show legend (\`true\`) or not (\`false\`).", 53 | valueType: "boolean", 54 | defaultValue: defaultProps.showLegend, 55 | } 56 | }, 57 | themeVars: parseScssVar(styles.themeVars), 58 | defaultThemeVars: { 59 | "textColor-labelList-PieChart": "$textColor-primary", 60 | }, 61 | }); 62 | 63 | export const pieChartComponentRenderer = createComponentRenderer( 64 | COMP, 65 | PieChartMd, 66 | ({ extractValue, node, className, renderChild }) => { 67 | return ( 68 | <PieChart 69 | showLabelList={extractValue.asOptionalBoolean(node.props?.showLabelList)} 70 | labelListPosition={extractValue.asOptionalString(node.props?.labelListPosition) as LabelPosition} 71 | data={extractValue(node.props?.data)} 72 | className={className} 73 | showLabel={extractValue.asOptionalBoolean(node.props?.showLabel)} 74 | showLegend={extractValue.asOptionalBoolean(node.props?.showLegend)} 75 | dataKey={extractValue(node.props?.dataKey)} 76 | nameKey={extractValue(node.props?.nameKey)} 77 | > 78 | {renderChild(node.children)} 79 | </PieChart> 80 | ); 81 | }, 82 | ); 83 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Tooltip/Tooltip.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "../../components-core/theming/themes" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $themeVars: (); 5 | @function createThemeVar($componentVariable) { 6 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 7 | @return t.getThemeVar($themeVars, $componentVariable); 8 | } 9 | 10 | $component: "Tooltip"; 11 | $themeVars: t.composePaddingVars($themeVars, $component); 12 | $themeVars: t.composeBorderVars($themeVars, $component); 13 | 14 | 15 | // --- Theme variables for Tooltip component 16 | $backgroundColor-Tooltip: createThemeVar("backgroundColor-#{$component}"); 17 | $textColor-Tooltip: createThemeVar("textColor-#{$component}"); 18 | $fontSize-Tooltip: createThemeVar("fontSize-#{$component}"); 19 | $lineHeight-Tooltip: createThemeVar("lineHeight-#{$component}"); 20 | $boxShadow-Tooltip: createThemeVar("boxShadow-#{$component}"); 21 | $fill-arrow-Tooltip: createThemeVar("fill-arrow-#{$component}"); 22 | $stroke-arrow-Tooltip: createThemeVar("stroke-arrow-#{$component}"); 23 | $strokeWidth-arrow-Tooltip: createThemeVar("strokeWidth-arrow-#{$component}"); 24 | $animationDuration-Tooltip: createThemeVar("animationDuration-#{$component}"); 25 | $animation-Tooltip: createThemeVar("animation-#{$component}"); 26 | 27 | /* Tooltip styles */ 28 | 29 | .hiddenTrigger { 30 | position: fixed; 31 | left: 0; 32 | top: 0; 33 | width: 1px; 34 | height: 1px; 35 | pointer-events: none; 36 | z-index: -1; 37 | opacity: 0; 38 | } 39 | 40 | .content { 41 | @include t.paddingVars($themeVars, $component); 42 | @include t.borderVars($themeVars, $component); 43 | font-size: $fontSize-Tooltip; 44 | line-height: $lineHeight-Tooltip; 45 | color: $textColor-Tooltip; 46 | background-color: $backgroundColor-Tooltip; 47 | box-shadow: $boxShadow-Tooltip; 48 | user-select: none; 49 | animation-duration: $animationDuration-Tooltip; 50 | animation-timing-function: $animation-Tooltip; 51 | will-change: transform, opacity; 52 | z-index: 9999; 53 | } 54 | 55 | .content[data-state='delayed-open'][data-side='top'] { 56 | animation-name: slideDownAndFade; 57 | } 58 | 59 | .content[data-state='delayed-open'][data-side='right'] { 60 | animation-name: slideLeftAndFade; 61 | } 62 | 63 | .content[data-state='delayed-open'][data-side='bottom'] { 64 | animation-name: slideUpAndFade; 65 | } 66 | 67 | .content[data-state='delayed-open'][data-side='left'] { 68 | animation-name: slideRightAndFade; 69 | } 70 | 71 | .arrow { 72 | fill: $fill-arrow-Tooltip; 73 | stroke: $stroke-arrow-Tooltip; 74 | stroke-width: $strokeWidth-arrow-Tooltip; 75 | } 76 | 77 | @keyframes slideUpAndFade { 78 | from { 79 | opacity: 0; 80 | transform: translateY(2px); 81 | } 82 | to { 83 | opacity: 1; 84 | transform: translateY(0); 85 | } 86 | } 87 | 88 | @keyframes slideRightAndFade { 89 | from { 90 | opacity: 0; 91 | transform: translateX(-2px); 92 | } 93 | to { 94 | opacity: 1; 95 | transform: translateX(0); 96 | } 97 | } 98 | 99 | @keyframes slideDownAndFade { 100 | from { 101 | opacity: 0; 102 | transform: translateY(-2px); 103 | } 104 | to { 105 | opacity: 1; 106 | transform: translateY(0); 107 | } 108 | } 109 | 110 | @keyframes slideLeftAndFade { 111 | from { 112 | opacity: 0; 113 | transform: translateX(2px); 114 | } 115 | to { 116 | opacity: 1; 117 | transform: translateX(0); 118 | } 119 | } 120 | 121 | // --- We export the theme variables to add them to the component renderer 122 | :export { 123 | themeVars: t.json-stringify($themeVars); 124 | } 125 | ``` -------------------------------------------------------------------------------- /xmlui/tests/parsers/scripting/eval-tree-arrow.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { evalBindingExpression } from "../../../src/components-core/script-runner/eval-tree-sync"; 4 | import { createEvalContext } from "./test-helpers"; 5 | 6 | describe("Evaluate arrow expressions", () => { 7 | it("Arrow #1", () => { 8 | // --- Arrange 9 | const source = "(x => 2 * x)(4)"; 10 | const context = createEvalContext({}); 11 | 12 | // --- Act 13 | const value = evalBindingExpression(source, context); 14 | 15 | // --- Arrange 16 | expect(value).equal(8); 17 | }); 18 | 19 | it("Arrow #2", () => { 20 | // --- Arrange 21 | const source = "((x, y) => x + y)(1, 2)"; 22 | const context = createEvalContext({}); 23 | 24 | // --- Act 25 | const value = evalBindingExpression(source, context); 26 | 27 | // --- Arrange 28 | expect(value).equal(3); 29 | }); 30 | 31 | it("Arrow #3", () => { 32 | // --- Arrange 33 | const source = "((x, y) => { return x + y })(1, 2)"; 34 | const context = createEvalContext({}); 35 | 36 | // --- Act 37 | const value = evalBindingExpression(source, context); 38 | 39 | // --- Arrange 40 | expect(value).equal(3); 41 | }); 42 | 43 | it("Arrow #4", () => { 44 | // --- Arrange 45 | const source = "(x => (++x.h))(count)"; 46 | const context = createEvalContext({ 47 | localContext: { 48 | count: { h: 3 }, 49 | }, 50 | }); 51 | 52 | // --- Act 53 | const value = evalBindingExpression(source, context); 54 | 55 | // --- Arrange 56 | expect(value).equal(4); 57 | }); 58 | 59 | it("Arrow #5", () => { 60 | // --- Arrange 61 | const source = "(x => x += 2)(count)"; 62 | const context = createEvalContext({ 63 | localContext: { 64 | count: 3, 65 | }, 66 | }); 67 | 68 | // --- Act 69 | const value = evalBindingExpression(source, context); 70 | 71 | // --- Arrange 72 | expect(value).equal(5); 73 | }); 74 | 75 | it("Arrow #6", () => { 76 | // --- Arrange 77 | const source = "(x => x += 2)(count + 4)"; 78 | const context = createEvalContext({ 79 | localContext: { 80 | count: 3, 81 | }, 82 | }); 83 | 84 | // --- Act 85 | const value = evalBindingExpression(source, context); 86 | 87 | // --- Arrange 88 | expect(value).equal(9); 89 | }); 90 | 91 | it("Arrow #7", () => { 92 | // --- Arrange 93 | const source = "[1,2,3,4,5].filter(x => x % 2 === 0)[1]"; 94 | const context = createEvalContext({ 95 | localContext: { 96 | count: 3, 97 | }, 98 | }); 99 | 100 | // --- Act 101 | const value = evalBindingExpression(source, context); 102 | 103 | // --- Arrange 104 | expect(value).equal(4); 105 | }); 106 | 107 | it("Arrow #8", () => { 108 | // --- Arrange 109 | const source = "containsArray.array.filter(item => item % 2 === 0)[1]"; 110 | const context = createEvalContext({ 111 | localContext: { 112 | containsArray: { 113 | array: [5, 4, 3, 2, 1], 114 | }, 115 | }, 116 | }); 117 | 118 | // --- Act 119 | const value = evalBindingExpression(source, context); 120 | 121 | // --- Arrange 122 | expect(value).equal(2); 123 | }); 124 | 125 | it("Arrow #9", () => { 126 | // --- Arrange 127 | const source = "array.reduce((acc, item) => acc + item, 0)"; 128 | const context = createEvalContext({ 129 | localContext: { 130 | array: [5, 4, 3, 2, 1], 131 | }, 132 | }); 133 | 134 | // --- Act 135 | const value = evalBindingExpression(source, context); 136 | 137 | // --- Arrange 138 | expect(value).equal(15); 139 | }); 140 | }); 141 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/script-runner/asyncProxy.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Gets a proxy function for one that does not support async operations 3 | * @param fn Function to replace with a proxy 4 | * @param origArgs Original function arguments 5 | * @param context Function context ("this" of the function invocation) 6 | * @return The proxy, if found; otherwise the original function 7 | */ 8 | export function getAsyncProxy(fn: Function, origArgs: any[], context: any): Function { 9 | const proxyFn = asyncProxies.get(fn); 10 | if (!proxyFn) return fn; 11 | 12 | origArgs.unshift(context); 13 | return proxyFn; 14 | } 15 | 16 | // Async implementations for JavaScript functions that do not support async arguments 17 | const asyncProxies = new Map<Function, Function>(); 18 | asyncProxies.set(Array.prototype.filter, asyncFilter); 19 | asyncProxies.set(Array.prototype.forEach, asyncForEach); 20 | asyncProxies.set(Array.prototype.map, asyncMap); 21 | asyncProxies.set(Array.prototype.every, asyncEvery); 22 | asyncProxies.set(Array.prototype.findIndex, asyncFindIndex); 23 | asyncProxies.set(Array.prototype.find, asyncFind); 24 | asyncProxies.set(Array.prototype.flatMap, asyncFlatMap); 25 | asyncProxies.set(Array.prototype.some, asyncSome); 26 | 27 | // The async implementation of Array.prototype.some 28 | async function asyncSome(arr: any[], predicate: (...args: any[]) => boolean) { 29 | const results = await Promise.all(arr.map(predicate)); 30 | return arr.some((_v, index) => results[index]); 31 | } 32 | 33 | // The async implementation of Array.prototype.filter 34 | async function asyncFilter(arr: any[], predicate: (...args: any[]) => boolean) { 35 | const results = await Promise.all(arr.map(predicate)); 36 | return arr.filter((_v, index) => results[index]); 37 | } 38 | 39 | // The async implementation of Array.prototype.forEach 40 | async function asyncForEach(arr: any[], predicate: (...args: any[]) => void) { 41 | for (let i = 0; i < arr.length; i++) { 42 | await predicate(arr[i], i, arr); 43 | } 44 | } 45 | 46 | // The async implementation of Array.prototype.map 47 | async function asyncMap(arr: any[], predicate: (...args: any[]) => Promise<any[]>) { 48 | const result = []; 49 | for (let i = 0; i < arr.length; i++) { 50 | result.push(await predicate(arr[i], i, arr)); 51 | } 52 | return result; 53 | } 54 | 55 | // The async implementation of Array.prototype.asyncEvery 56 | async function asyncEvery(arr: any[], callback: (...args: any[]) => any) { 57 | const results = await Promise.all(arr.map(callback)); 58 | return results.every((_v, index) => results[index]); 59 | } 60 | 61 | // The async implementation of Array.prototype.asyncFind 62 | async function asyncFind(arr: any[], predicate: (...args: any[]) => boolean) { 63 | const results = await Promise.all(arr.map(predicate)); 64 | return arr.find((_v, index) => results[index]); 65 | } 66 | 67 | // The async implementation of Array.prototype.asyncFindIndex 68 | async function asyncFindIndex(arr: any[], predicate: (...args: any[]) => boolean) { 69 | const results = await Promise.all(arr.map(predicate)); 70 | return arr.findIndex((_v, index) => results[index]); 71 | } 72 | 73 | // The async implementation of Array.prototype.asyncFlatMap 74 | async function asyncFlatMap(arr: any[], predicate: (...args: any[]) => boolean) { 75 | const results = await Promise.all(arr.map(predicate)); 76 | return arr.flatMap((_v, index) => results[index]); 77 | } 78 | ```