This is page 11 of 181. Use http://codebase.md/xmlui-org/xmlui/mockApiDef.js?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── stale-days-care.md ├── .eslintrc.cjs ├── .github │ ├── build-checklist.png │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── deploy-blog-optimized.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 │ ├── 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 │ │ │ └── PageNotFound.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 ├── 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 │ │ │ │ ├── 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 │ ├── 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 │ └── 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 │ │ │ ├── BehaviorContext.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 │ │ │ ├── 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/tests/parsers/scripting/process-event.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, assert } from "vitest"; 2 | 3 | import { 4 | ArrowExpressionStatement, 5 | T_ARROW_EXPRESSION, 6 | T_ARROW_EXPRESSION_STATEMENT, 7 | T_EXPRESSION_STATEMENT, 8 | } from "../../../src/components-core/script-runner/ScriptingSourceTree"; 9 | import { processStatementQueueAsync } from "../../../src/components-core/script-runner/process-statement-async"; 10 | import { createEvalContext, parseStatements } from "./test-helpers"; 11 | 12 | describe("Process statements", () => { 13 | it("Event with arrow function", async () => { 14 | // --- Arrange 15 | const source = "(x, y) => 2 * x + y"; 16 | const evalContext = createEvalContext({ 17 | localContext: {}, 18 | eventArgs: [123, 1], 19 | }); 20 | const statements = parseStatements(source); 21 | if ( 22 | statements?.length !== 1 || 23 | statements[0].type !== T_EXPRESSION_STATEMENT || 24 | statements[0].expr.type !== T_ARROW_EXPRESSION 25 | ) { 26 | assert.fail("Arrow expression expected"); 27 | } 28 | 29 | // --- Act 30 | const arrowStmt = { 31 | type: T_ARROW_EXPRESSION_STATEMENT, 32 | expr: statements[0].expr, 33 | } as ArrowExpressionStatement; 34 | const diag = await processStatementQueueAsync([arrowStmt], evalContext); 35 | 36 | // --- Assert 37 | const thread = evalContext.mainThread; 38 | expect(thread!.blocks!.length).equal(1); 39 | expect(thread!.blocks![0].returnValue).equal(247); 40 | 41 | expect(diag.processedStatements).equal(1); 42 | expect(diag.maxLoops).equal(0); 43 | expect(diag.maxBlocks).equal(1); 44 | expect(diag.maxQueueLength).equal(1); 45 | expect(diag.clearToLabels).equal(0); 46 | expect(diag.unshiftedItems).equal(0); 47 | }); 48 | 49 | it("Event issue", async () => { 50 | // --- Arrange 51 | const source = "(() => {let z = 0; while(z < 3) {console.log(z); z++}})()"; 52 | const evalContext = createEvalContext({ 53 | localContext: {}, 54 | eventArgs: [123, 1], 55 | }); 56 | const statements = parseStatements(source); 57 | await processStatementQueueAsync(statements, evalContext); 58 | 59 | // --- Assert 60 | expect(evalContext.mainThread!.blocks![0].returnValue).equal(undefined); 61 | }); 62 | }); 63 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/abstractions/treeAbstractions.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface TreeNode { 2 | id: string | number; 3 | key: string | number; 4 | path: any[]; 5 | displayName?: string; 6 | children?: TreeNode[]; 7 | parentIds: (string | number)[]; 8 | selectable: boolean; 9 | [x: string]: any; 10 | } 11 | 12 | export type TreeItem = { 13 | id: string | number; 14 | children?: Array<TreeItem>; 15 | [x: string]: any; 16 | }; 17 | 18 | export type UnPackedTreeData = { 19 | treeData: Array<TreeNode>; 20 | treeItemsById: Record<string | number, TreeNode>; 21 | }; 22 | 23 | export interface FlatTreeNode extends TreeNode { 24 | isExpanded: boolean; 25 | depth: number; 26 | hasChildren: boolean; 27 | } 28 | 29 | // New interfaces for Tree component refactoring 30 | export interface TreeFieldConfig { 31 | idField: string; 32 | labelField: string; 33 | iconField?: string; 34 | iconExpandedField?: string; 35 | iconCollapsedField?: string; 36 | parentField?: string; 37 | childrenField?: string; 38 | selectableField?: string; 39 | dynamicField?: string; 40 | } 41 | 42 | export interface TreeSelectionEvent { 43 | previousNode: FlatTreeNode | null; 44 | newNode: FlatTreeNode | null; 45 | } 46 | 47 | export type TreeDataFormat = 'flat' | 'hierarchy'; 48 | 49 | export type DefaultExpansion = 'none' | 'all' | 'first-level' | (string | number)[]; 50 | 51 | // Node loading states for dynamic node handling 52 | export type NodeLoadingState = 'unloaded' | 'loading' | 'loaded'; 53 | 54 | // Extended FlatTreeNode with loading state information 55 | export interface FlatTreeNodeWithState extends FlatTreeNode { 56 | loadingState: NodeLoadingState; 57 | } 58 | 59 | export interface TreeNodeInfo { 60 | id: string | number; // Source data ID 61 | item: any; // Original source item 62 | depth: number; // Nesting depth (0-based) 63 | isExpanded: boolean; // Current expansion state 64 | hasChildren: boolean;// Whether node has children 65 | isSelected: boolean; // Whether node is selected 66 | path: (string | number)[]; // Path from root (source IDs) 67 | parentId?: string | number; // Parent node ID (if any) 68 | childrenIds: (string | number)[]; // Direct children IDs 69 | } 70 | 71 | export interface TreeStats { 72 | totalNodes: number; 73 | maxDepth: number; 74 | expandedNodes: number; 75 | visibleNodes: number; 76 | rootNodes: number; 77 | } 78 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/event-handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { ComponentMetadata } from "../abstractions/ComponentDefs"; 2 | import type { LookupEventHandlerFn } from "../abstractions/RendererDefs"; 3 | import type { EventHandler} from "react"; 4 | import { useCallback } from "react"; 5 | import { EMPTY_OBJECT } from "./constants"; 6 | 7 | /** 8 | * This hook sets up the mouse event handlers for the component 9 | * @param lookupEvent Function to lookup the event handler 10 | * @param shouldSkip Indicates if the event handlers should be skipped 11 | * @returns 12 | */ 13 | export function useMouseEventHandlers(lookupEvent: LookupEventHandlerFn, shouldSkip: boolean) { 14 | // *** Because we use nested React hooks, we cannot use an early return 15 | // *** when shouldSkip is true. 16 | const onClick = useEventHandler("click", lookupEvent, shouldSkip); 17 | const onMouseLeave = useEventHandler("mouseLeave", lookupEvent, shouldSkip); 18 | const onMouseEnter = useEventHandler("mouseEnter", lookupEvent, shouldSkip); 19 | const onDoubleClick = useEventHandler("doubleClick", lookupEvent, shouldSkip); 20 | 21 | if (shouldSkip) { 22 | return EMPTY_OBJECT; 23 | } 24 | 25 | return Object.fromEntries( 26 | Object.entries({ 27 | onClick, 28 | onMouseLeave, 29 | onMouseEnter, 30 | onDoubleClick, 31 | }).filter(([, value]) => value !== undefined) 32 | ); 33 | 34 | // --- Creates a particular event handler 35 | function useEventHandler<TMd extends ComponentMetadata>( 36 | eventName: string, 37 | lookupEvent: LookupEventHandlerFn<TMd>, 38 | shouldSkip: boolean, 39 | ) { 40 | // *** Because we use nested React hooks, we cannot use an early return 41 | // *** when shouldSkip is true. 42 | const onEvent = shouldSkip 43 | ? undefined 44 | : lookupEvent(eventName as keyof NonNullable<TMd["events"]>); 45 | const eventHandler: EventHandler<any> = useCallback( 46 | (event) => { 47 | // If the event handler is not defined, we do nothing 48 | if (onEvent) { 49 | if (typeof event.stopPropagation === "function") { 50 | event?.stopPropagation(); 51 | } 52 | onEvent(event); 53 | } 54 | }, 55 | [onEvent], 56 | ); 57 | return !onEvent ? undefined : eventHandler; 58 | } 59 | } 60 | ``` -------------------------------------------------------------------------------- /xmlui/tests-e2e/state-var-scopes.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "../src/testing/fixtures"; 2 | 3 | test("vars shadowing works", async ({ initTestBed, page }) => { 4 | await initTestBed(` 5 | <Stack var.x="x in outer stack" var.y="y in outer stack"> 6 | <Stack var.y="y in inner stack"> 7 | <Button testId="button" onClick="y = 123">change y in inner stack</Button> 8 | <Text testId="y_in_inner_stack">{y}</Text> 9 | </Stack> 10 | <Text testId="y_in_outer_stack">{y}</Text> 11 | </Stack> 12 | `); 13 | await page.getByTestId("button").click(); 14 | await expect(page.getByTestId("y_in_inner_stack")).toHaveText("123"); 15 | await expect(page.getByTestId("y_in_outer_stack")).toHaveText("y in outer stack"); 16 | }); 17 | 18 | test("inner input is available in the file (implicit containers because of vars)", async ({ 19 | initTestBed, 20 | page, 21 | }) => { 22 | await initTestBed(` 23 | <Fragment> 24 | <Stack var.x="x in outer stack" var.y="y in outer stack"> 25 | <Stack var.y="y in inner stack"> 26 | <TextBox id="textbox"/> 27 | </Stack> 28 | </Stack> 29 | <Text testId="textbox_value_outside">{textbox.value}</Text> 30 | </Fragment> 31 | `); 32 | await page.getByTestId("textbox").getByRole("textbox").fill("textbox-value"); 33 | await expect(page.getByTestId("textbox_value_outside")).toHaveText("textbox-value"); 34 | }); 35 | 36 | test("inner datasource is available in the file (implicit containers because of vars)", async ({ 37 | initTestBed, 38 | page, 39 | }) => { 40 | await initTestBed( 41 | ` 42 | <Fragment> 43 | <Stack var.x="x in outer stack" var.y="y in outer stack"> 44 | <Stack var.y="y in inner stack"> 45 | <DataSource url="/data1" id="explicitDataSource"/> 46 | </Stack> 47 | </Stack> 48 | <Text testId="datasource_value_outside">{explicitDataSource.value}</Text> 49 | </Fragment> 50 | `, 51 | { 52 | apiInterceptor: { 53 | operations: { 54 | "load-api-data1": { 55 | url: "/data1", 56 | method: "get", 57 | handler: `()=>{ 58 | return 'data1'; 59 | }`, 60 | }, 61 | }, 62 | }, 63 | }, 64 | ); 65 | await expect(page.getByTestId("datasource_value_outside")).toHaveText("data1"); 66 | }); 67 | ``` -------------------------------------------------------------------------------- /docs/public/pages/intro.md: -------------------------------------------------------------------------------- ```markdown 1 | # Introduction 2 | 3 | XMLUI is a framework for building user interfaces declaratively, with XML markup and flexible theming. XMLUI apps are: 4 | 5 | - **Easy to create**. Build on the web platform with little or no knowledge of React or CSS. 6 | - **Clean and modern**. Enjoy themes that look great out of the box and are easy to modify. Create experiences that meet expectations for modern web apps. 7 | - **Connected**. Read and write APIs with little or no scripting. 8 | - **Modular**. Use a comprehensive suite of [components](/components/_overview) that you can extend with — again! — little or no scripting. 9 | - **Easy to deploy**. Just drop a handful of files onto a static webserver. 10 | 11 | This paragraph is static text displayed by XMLUI's [Markdown](/components/Markdown) component. 12 | 13 | This list is a live report on the status of London's tube stations. 14 | 15 | ```xmlui-pg name="London Tube Status" 16 | <App> 17 | <List data="https://api.tfl.gov.uk/line/mode/tube/status"> 18 | <Text>{$item.name}: {$item.lineStatuses[0].statusSeverityDescription}</Text> 19 | </List> 20 | </App> 21 | ``` 22 | 23 | When you reload the page you'll see fresh data. 24 | 25 | > [!INFO] 26 | > You can use the  icon to open live elements, like the London Tube Status report, in a playground where you read and edit the XMLUI markup. 27 | 28 | This is the XMLUI markup you'll see in the playground. 29 | 30 | ```xmlui 31 | <List data="https://api.tfl.gov.uk/line/mode/tube/status"> 32 | <Text>{$item.name}: {$item.lineStatuses[0].statusSeverityDescription}</Text> 33 | </List> 34 | ``` 35 | 36 | The [List](/components/List) component fetches JSON from a <a href="https://api.tfl.gov.uk/line/mode/tube/status" target="_blank">REST endpoint</a>, iterates through the array of objects returned from the API, and updates the [context variable](context-variables) called `$item` for each object. The [Text](/components/Text) component uses JavaScript dot notation and array indexing to extract station names and statuses from each `$item`. 37 | 38 | In this case the URL is static. In the next chapter you'll see how a data URL can vary to deliver changing data in response to UI interaction. 39 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Stack/StackNative.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { type CSSProperties, forwardRef, type ReactNode, type Ref } from "react"; 2 | import classnames from "classnames"; 3 | 4 | import styles from "./Stack.module.scss"; 5 | 6 | import { useContentAlignment } from "../../components-core/component-hooks"; 7 | import { useOnMount } from "../../components-core/utils/hooks"; 8 | 9 | export const DEFAULT_ORIENTATION = "vertical"; 10 | 11 | export const defaultProps = { 12 | orientation: DEFAULT_ORIENTATION, 13 | reverse: false, 14 | hoverContainer: false, 15 | visibleOnHover: false, 16 | }; 17 | 18 | type Props = { 19 | children: ReactNode; 20 | orientation?: string; 21 | uid?: string; 22 | horizontalAlignment?: string; 23 | verticalAlignment?: string; 24 | style?: CSSProperties; 25 | className?: string; 26 | reverse?: boolean; 27 | hoverContainer?: boolean; 28 | visibleOnHover?: boolean; 29 | onClick?: any; 30 | onMount?: any; 31 | }; 32 | 33 | // ===================================================================================================================== 34 | // Stack React component 35 | 36 | export const Stack = forwardRef(function Stack( 37 | { 38 | uid, 39 | children, 40 | orientation = defaultProps.orientation, 41 | horizontalAlignment, 42 | verticalAlignment, 43 | style, 44 | reverse = defaultProps.reverse, 45 | hoverContainer = defaultProps.hoverContainer, 46 | visibleOnHover = defaultProps.visibleOnHover, 47 | onClick, 48 | onMount, 49 | className, 50 | ...rest 51 | }: Props, 52 | ref: Ref<any>, 53 | ) { 54 | useOnMount(onMount); 55 | const { horizontal, vertical } = useContentAlignment( 56 | orientation, 57 | horizontalAlignment, 58 | verticalAlignment, 59 | ); 60 | return ( 61 | <div 62 | {...rest} 63 | onClick={onClick} 64 | ref={ref} 65 | style={style} 66 | className={classnames( 67 | className, 68 | styles.base, 69 | { 70 | [styles.vertical]: orientation === "vertical", 71 | [styles.horizontal]: orientation === "horizontal", 72 | [styles.reverse]: reverse, 73 | [styles.hoverContainer]: hoverContainer, 74 | "display-on-hover": visibleOnHover, 75 | [styles.handlesClick]: !!onClick, 76 | }, 77 | horizontal ?? "", 78 | vertical ?? "", 79 | )} 80 | > 81 | {children} 82 | </div> 83 | ); 84 | }); 85 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/theming/themeVars.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * The sole purpose of this module is to provide an object with the available keys of theme SCSS variables and the 3 | * prefix of the theme. 4 | */ 5 | import themeVars from "./themeVars.module.scss"; 6 | 7 | /** 8 | * This function extracts CSS variables from the specified SCSS input. It uses a hack to convert the CSS input to JSON 9 | * and then calls a JSON parser to create the desired object. 10 | * @param scssStr The scss input 11 | */ 12 | export function parseScssVar(scssStr: any) { 13 | if (!scssStr || typeof scssStr !== typeof "") { 14 | return scssStr; 15 | } 16 | 17 | // Lists and maps are surrounded by single quotes, e.g. "'[ \"string in list\", 5, \"5px\" ]'" 18 | // Remove them if they exist so they can be parsed correctly. 19 | let jsValue = scssStr.replace(/(^['"])|(['"]$)/g, ""); 20 | 21 | try { 22 | // JSON-formatted string from within SCSS file 23 | return JSON.parse(jsValue); 24 | } catch (errorParsingJsonGeneratedInUtilScssFile) { 25 | try { 26 | // Value was likely an SCSS literal string; attempt parsing it manually. 27 | // Example: inspect($my-map) => '(num: 10, numWithUnits: 5px, str: hello, color: #fff, "keyAsStr": false, other: null)' 28 | return JSON.parse( 29 | scssStr 30 | .replace("(", "{") 31 | .replace(")", "}") 32 | // JSON values: convert any collection of word characters followed by a comma or bracket to a string 33 | .replace(/: ?([^,}]+)([,}])/g, ': "$1"$2') 34 | // JSON keys: space/bracket/comma as first character, not already a string, anything not colon or 35 | // space (rules out JSON values), ended by colon 36 | .replace(/([\s{,])(?!")([^:\s]+)+:/g, '$1"$2":'), 37 | ); 38 | } catch (errorParsingScssStringLiteral) { 39 | return jsValue; 40 | } 41 | } 42 | } 43 | 44 | let keyPrefix = parseScssVar(themeVars.keyPrefix) || ""; 45 | let vars = parseScssVar(themeVars.themeVars); 46 | 47 | /** 48 | * Export the desired SCSS variables and prefix 49 | */ 50 | const theme = { 51 | keyPrefix: keyPrefix, 52 | themeVars: vars, 53 | }; 54 | 55 | export function getVarKey(varName: string) { 56 | if (keyPrefix) { 57 | return `--${keyPrefix}-${varName}`; 58 | } 59 | return `--${varName}`; 60 | } 61 | 62 | export default theme; 63 | ``` -------------------------------------------------------------------------------- /packages/xmlui-playground/src/providers/ToastProvider.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createContext, useState, type ReactNode } from "react"; 2 | import { noop } from "lodash-es"; 3 | import * as RadixToast from "@radix-ui/react-toast"; 4 | import styles from "./Toast.module.scss"; 5 | import { MdOutlineClose } from "react-icons/md"; 6 | import classnames from "classnames"; 7 | 8 | type ToastMessage = { 9 | type: "success" | "warning" | "error" | "info"; 10 | title?: string; 11 | description: string; 12 | }; 13 | 14 | type ToastContextDefinition = { 15 | toastMessage: ToastMessage | null; 16 | showToast: (toastMessage: ToastMessage) => void; 17 | }; 18 | 19 | export const ToastContext = createContext<ToastContextDefinition>({ 20 | toastMessage: null, 21 | showToast: noop, 22 | }); 23 | 24 | type ToastProviderProps = { 25 | children: ReactNode; 26 | }; 27 | 28 | export const ToastProvider = ({ children }: ToastProviderProps) => { 29 | const [toastMessage, setToastMessage] = useState<ToastMessage | null>(null); 30 | const [open, setOpen] = useState(false); 31 | 32 | const showToast = (toastMessage: ToastMessage) => { 33 | setToastMessage(toastMessage); 34 | setOpen(true); 35 | }; 36 | 37 | return ( 38 | <ToastContext.Provider value={{ toastMessage, showToast }}> 39 | <RadixToast.Provider swipeDirection="right"> 40 | {children} 41 | <RadixToast.Root 42 | open={open} 43 | onOpenChange={setOpen} 44 | className={classnames(styles.ToastRoot, { 45 | [styles.success]: toastMessage?.type === "success", 46 | [styles.warning]: toastMessage?.type === "warning", 47 | [styles.error]: toastMessage?.type === "error", 48 | })} 49 | > 50 | <RadixToast.Close className={styles.ToastClose}> 51 | <MdOutlineClose /> 52 | </RadixToast.Close> 53 | {toastMessage?.title && ( 54 | <RadixToast.Title className={styles.ToastTitle}>{toastMessage?.title}</RadixToast.Title> 55 | )} 56 | <RadixToast.Description className={styles.ToastDescription} asChild> 57 | <div>{toastMessage?.description}</div> 58 | </RadixToast.Description> 59 | </RadixToast.Root> 60 | <RadixToast.Viewport className={styles.ToastViewport} /> 61 | </RadixToast.Provider> 62 | </ToastContext.Provider> 63 | ); 64 | }; 65 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/ToneSwitch/ToneSwitch.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 | .iconSwitch { 12 | display: flex; 13 | align-items: center; 14 | padding: 4px; 15 | border-radius: 1rem; 16 | border: 1px solid createThemeVar("borderColor-ToneSwitch"); 17 | transition: background-color 0.3s ease, border-color 0.3s ease; 18 | cursor: pointer; 19 | min-height: 1rem; 20 | width: calc(1rem * 3); 21 | position: relative; 22 | pointer-events: auto; 23 | 24 | &:hover { 25 | border-color: createThemeVar("borderColor-ToneSwitch--hover"); 26 | } 27 | } 28 | 29 | .iconThumb { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | width: 1rem; 34 | height: 1rem; 35 | border-radius: 50%; 36 | background-color: white; 37 | transition: transform 0.3s ease; 38 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 39 | pointer-events: none; 40 | } 41 | 42 | .iconSwitch.light .iconThumb { 43 | transform: translateX(0); 44 | } 45 | 46 | .iconSwitch.dark .iconThumb { 47 | transform: translateX(calc(1rem + 4px)); 48 | } 49 | 50 | 51 | // Alternative global override for the label class 52 | :global(.toneSwitchContainer) { 53 | width: fit-content !important; 54 | 55 | :global(.label) { 56 | width: fit-content !important; 57 | } 58 | } 59 | 60 | .iconSwitch.light { 61 | background-color: createThemeVar("backgroundColor-ToneSwitch-light"); 62 | color: createThemeVar("color-ToneSwitch-light"); 63 | } 64 | 65 | .iconSwitch.dark { 66 | background-color: createThemeVar("backgroundColor-ToneSwitch-dark"); 67 | color: createThemeVar("color-ToneSwitch-dark"); 68 | } 69 | 70 | .icon { 71 | font-size: 12px; 72 | width: 12px; 73 | height: 12px; 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | color: var(--icon-color, #666); 78 | pointer-events: none; 79 | } 80 | } 81 | 82 | // --- We export the theme variables to add them to the component renderer 83 | :export { 84 | themeVars: t.json-stringify($themeVars); 85 | } 86 | ``` -------------------------------------------------------------------------------- /.github/workflows/deploy-blog-optimized.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Deploy blog (optimized) 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | env: 14 | NODE_OPTIONS: "--max-old-space-size=8192" 15 | TURBO_UI: "false" 16 | 17 | steps: 18 | - name: 1. Generate a token from the GitHub App 🤖 19 | id: generate_token 20 | uses: tibdex/github-app-token@v2 21 | with: 22 | app_id: ${{ secrets.APP_ID }} 23 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 22 28 | cache: "npm" 29 | registry-url: https://registry.npmjs.org/ 30 | 31 | - name: Cache for Turbo 32 | uses: rharkor/[email protected] 33 | 34 | - run: npm ci --prefer-offline 35 | 36 | - name: 4. Configure Git to use the App's token 🔑 37 | # The token generated in the first step is used here for authentication. 38 | run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/" 39 | - name: 5. DEBUG - Attempt to clone the private repo 40 | # This step will give a clear error if authentication is the problem 41 | run: git clone https://github.com/xmlui-org/xmlui-optimizer.git 42 | - name: 5. Install optimizer's dependencies 43 | run: npm install 44 | working-directory: ./xmlui-optimizer 45 | - name: DEBUG - install xmlui-optimizer 46 | # This step will give a clear error if authentication is the problem 47 | run: npm install ./xmlui-optimizer 48 | 49 | - name: Build extensions 50 | run: npm run build-extensions 51 | 52 | - run: cd blog && npm run build-optimized 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Deploy to Netlify 57 | uses: nwtgck/[email protected] 58 | with: 59 | publish-dir: ./blog/xmlui-optimized-output 60 | production-deploy: true 61 | env: 62 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_ACCESS_TOKEN }} 63 | NETLIFY_SITE_ID: ${{ secrets.XMLUI_BLOG_NETLIFY_SITE_ID }} 64 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Icon/Icon.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createComponentRenderer } from "../../components-core/renderers"; 2 | import styles from "./Icon.module.scss"; 3 | import { parseScssVar } from "../../components-core/theming/themeVars"; 4 | import Icon from "./IconNative"; 5 | import { createMetadata, d } from "../metadata-helpers"; 6 | 7 | const COMP = "Icon"; 8 | 9 | export const IconMd = createMetadata({ 10 | status: "stable", 11 | description: 12 | "`Icon` displays scalable vector icons from XMLUI's built-in icon registry " + 13 | "using simple name references. Icons are commonly used in buttons, navigation " + 14 | "elements, and status indicators.", 15 | props: { 16 | name: d( 17 | "This string property specifies the name of the icon to display. All icons have " + 18 | "unique, case-sensitive names identifying them. If the icon name is not set, the " + 19 | "`fallback` value is used.", 20 | ), 21 | size: { 22 | description: 23 | `This property defines the size of the \`${COMP}\`. Note that setting the \`height\` and/or ` + 24 | `the \`width\` of the component will override this property. You can use az explicit size ` + 25 | "value (e.g., 32px) or one of these predefined values: `xs`, `sm`, `md`, `lg`.", 26 | availableValues: ["xs", "sm", "md", "lg"], 27 | }, 28 | fallback: d( 29 | "This optional property provides a way to handle situations when the icon with the provided " + 30 | "[icon name](#name) name does not exist. If the icon cannot be found, no icon is displayed.", 31 | ), 32 | }, 33 | events: { 34 | click: d("This event is triggered when the icon is clicked."), 35 | }, 36 | 37 | themeVars: parseScssVar(styles.themeVars), 38 | defaultThemeVars: { 39 | [`size-${COMP}`]: "1.2em", 40 | }, 41 | }); 42 | 43 | export const iconComponentRenderer = createComponentRenderer( 44 | COMP, 45 | IconMd, 46 | ({ node, extractValue, className, lookupEventHandler }) => { 47 | return ( 48 | <Icon 49 | name={extractValue.asOptionalString(node.props.name)} 50 | size={extractValue(node.props.size)} 51 | className={className} 52 | fallback={extractValue.asOptionalString(node.props.fallback)} 53 | onClick={lookupEventHandler("click")} 54 | /> 55 | ); 56 | }, 57 | ); 58 | ``` -------------------------------------------------------------------------------- /docs/content/components/Spinner.md: -------------------------------------------------------------------------------- ```markdown 1 | # Spinner [#spinner] 2 | 3 | `Spinner` is an animated indicator that represents an action in progress with no deterministic progress value. 4 | 5 | While it is visible, the action is yet to be completed; on completion, the UI logic may opt to remove the component. 6 | 7 | ## Using the `Spinner` [#using-the-spinner] 8 | 9 | ```xmlui-pg copy display name="Example: using Spinner" 10 | <App> 11 | <Spinner /> 12 | </App> 13 | ``` 14 | 15 | >[!INFO] 16 | > `Spinner` ignores the `width`, `minWidth`, `maxWidth`, `height`, `minHeight`, and `maxHeight` properties. If you want to change its size, use the `size-Spinner` theme variable (see details is the [Styling](#styling) section). 17 | 18 | ## Properties [#properties] 19 | 20 | ### `delay` (default: 400) [#delay-default-400] 21 | 22 | The delay in milliseconds before the spinner is displayed. 23 | 24 | Use the buttons to toggle between the two `Spinners`. 25 | 26 | ```xmlui-pg copy {8-9} display name="Example: delay" 27 | <App> 28 | <variable name="noDelay" value="{true}" /> 29 | <variable name="yesDelay" value="{false}" /> 30 | <HStack gap="$space-0_5"> 31 | <Button label="No delay" onClick="noDelay = true; yesDelay = false;" /> 32 | <Button label="1000 ms delay" onClick="noDelay = false; yesDelay = true;" /> 33 | </HStack> 34 | <Spinner when="{noDelay}" delay="0" /> 35 | <Spinner when="{yesDelay}" delay="1000" /> 36 | </App> 37 | ``` 38 | 39 | ### `fullScreen` (default: false) [#fullscreen-default-false] 40 | 41 | If set to `true`, the component will be rendered in a full screen container. 42 | 43 | ```xmlui-pg copy display name="Example: fullScreen" height="200px" 44 | <App> 45 | <Spinner fullScreen="true" /> 46 | </App> 47 | ``` 48 | 49 | ## Events [#events] 50 | 51 | This component does not have any events. 52 | 53 | ## Exposed Methods [#exposed-methods] 54 | 55 | This component does not expose any methods. 56 | 57 | ## Styling [#styling] 58 | 59 | ### Theme Variables [#theme-variables] 60 | 61 | | Variable | Default Value (Light) | Default Value (Dark) | 62 | | --- | --- | --- | 63 | | [borderColor](../styles-and-themes/common-units/#color)-Spinner | $color-surface-400 | $color-surface-400 | 64 | | [size](../styles-and-themes/common-units/#size)-Spinner | $space-10 | $space-10 | 65 | | [thickness](../styles-and-themes/common-units/#size)-Spinner | $space-0_5 | $space-0_5 | 66 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Theme/Theme.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type { ThemeTone } from "../../abstractions/ThemingDefs"; 2 | import { createComponentRenderer } from "../../components-core/renderers"; 3 | import { createMetadata, d } from "../metadata-helpers"; 4 | import { Theme, defaultProps } from "./ThemeNative"; 5 | 6 | const COMP = "Theme"; 7 | 8 | export const ThemeMd = createMetadata({ 9 | status: "stable", 10 | description: 11 | "`Theme` creates styling contexts to customize the appearance of nested " + 12 | "components without using CSS.", 13 | allowArbitraryProps: true, 14 | props: { 15 | themeId: d(`This property specifies which theme to use by setting the theme's id.`), 16 | tone: { 17 | description: "This property allows the setting of the current theme's tone.", 18 | availableValues: ["light", "dark"], 19 | valueType: "string", 20 | defaultValue: "light", 21 | }, 22 | root: d( 23 | `This property indicates whether the component is at the root of the application.`, 24 | undefined, 25 | "boolean", 26 | defaultProps.root, 27 | ), 28 | applyIf: d( 29 | `This property controls whether the theme wrapper is applied. When true (default), the theme wraps the children. When false, children are rendered unwrapped.`, 30 | undefined, 31 | "boolean", 32 | true, 33 | ), 34 | }, 35 | opaque: true, 36 | }); 37 | 38 | export const themeComponentRenderer = createComponentRenderer( 39 | COMP, 40 | ThemeMd, 41 | ({ node, extractValue, renderChild, layoutContext, appContext }) => { 42 | const { tone, ...restProps } = node.props; 43 | const toastDuration = appContext?.appGlobals?.notifications?.duration; 44 | let themeTone = extractValue.asOptionalString(tone); 45 | if (themeTone && themeTone !== "dark") { 46 | themeTone = "light"; 47 | } 48 | return ( 49 | <Theme 50 | id={extractValue.asOptionalString(node.props.themeId)} 51 | isRoot={extractValue.asOptionalBoolean(node.props.root)} 52 | applyIf={extractValue.asOptionalBoolean(node.props.applyIf)} 53 | layoutContext={layoutContext} 54 | renderChild={renderChild} 55 | tone={themeTone as ThemeTone} 56 | toastDuration={toastDuration} 57 | themeVars={extractValue(restProps)} 58 | node={node} 59 | /> 60 | ); 61 | }, 62 | ); 63 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Charts/Tooltip/TooltipContent.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type React from "react"; 2 | import { forwardRef } from "react"; 3 | import styles from "./TooltipContent.module.scss"; 4 | import classnames from "classnames"; 5 | import type { Tooltip as RTooltip } from "recharts"; 6 | 7 | export const TooltipContent = forwardRef< 8 | HTMLDivElement, 9 | React.ComponentProps<typeof RTooltip> & 10 | React.ComponentProps<"div"> & { 11 | hideLabel?: boolean; 12 | hideIndicator?: boolean; 13 | indicator?: "line" | "dot" | "dashed"; 14 | nameKey?: string; 15 | labelKey?: string; 16 | } 17 | >(function ({ active, payload, indicator = "dot", hideLabel = false, label, color }, ref) { 18 | if (active && payload && payload.length) { 19 | const nestLabel = payload.length === 1 && indicator !== "dot"; 20 | return ( 21 | <div className={styles.tooltipContainer} ref={ref}> 22 | {!hideLabel && <p className="label">{label}</p>} 23 | 24 | <div className={styles.gridGap}> 25 | {payload?.map((item: any, index: any) => { 26 | const indicatorColor = color || item.payload?.fill || item.color; 27 | return ( 28 | <div key={index} className={styles.itemContainer}> 29 | <div 30 | className={classnames(styles.indicator, { 31 | [styles.dot]: indicator === "dot", 32 | [styles.line]: indicator === "line", 33 | [styles.dashed]: indicator === "dashed", 34 | [styles.nestDashed]: nestLabel && indicator === "dashed", 35 | })} 36 | style={{ backgroundColor: indicatorColor, borderColor: indicatorColor }} 37 | /> 38 | <div className={styles.valueContainer}> 39 | <div className={styles.labelGrid}> 40 | <span className={styles.mutedText}>{item.name}</span> 41 | </div> 42 | {item.value && ( 43 | <span className={styles.valueText}>{item.value.toLocaleString()}</span> 44 | )} 45 | </div> 46 | </div> 47 | ); 48 | })} 49 | </div> 50 | </div> 51 | ); 52 | } 53 | 54 | return null; 55 | }); 56 | 57 | TooltipContent.displayName = "TooltipContent"; 58 | ``` -------------------------------------------------------------------------------- /blog/public/web.config: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="utf-8" ?> 2 | <configuration> 3 | <system.webServer> 4 | <staticContent> 5 | <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" /> 6 | 7 | <mimeMap fileExtension=".json" mimeType="application/json" /> 8 | <mimeMap fileExtension=".rss" mimeType="application/rss+xml" /> 9 | <mimeMap fileExtension=".ts" mimeType="application/x-typescript" /> 10 | <mimeMap fileExtension=".xmlui" mimeType="application/xmlui" /> 11 | <mimeMap fileExtension=".xmlui.xs" mimeType="application/xmlui-xs" /> 12 | <mimeMap fileExtension="woff" mimeType="application/font-woff" /> 13 | <mimeMap fileExtension="woff2" mimeType="application/font-woff2" /> 14 | <mimeMap fileExtension="md" mimeType="text/markdown" /> 15 | </staticContent> 16 | <rewrite> 17 | <rules> 18 | <rule name="RewriteHTML" stopProcessing="true"> 19 | <match url="^([^.]+)$" /> 20 | <conditions> 21 | <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> 22 | <add input="{REQUEST_FILENAME}.html" matchType="IsFile" /> 23 | </conditions> 24 | <action type="Rewrite" url="{R:1}.html" /> 25 | </rule> 26 | <remove name="pushState" /> 27 | <rule name="pushState" stopProcessing="true"> 28 | <match url=".*" /> 29 | <conditions logicalGrouping="MatchAll"> 30 | <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> 31 | <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> 32 | </conditions> 33 | <action type="Rewrite" url="/" /> 34 | </rule> 35 | <rule name="Playground" stopProcessing="true"> 36 | <match url="^playground$" /> 37 | <action type="Rewrite" url="/playground" /> 38 | </rule> 39 | </rules> 40 | </rewrite> 41 | </system.webServer> 42 | </configuration> 43 | ``` -------------------------------------------------------------------------------- /docs/public/web.config: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="utf-8" ?> 2 | <configuration> 3 | <system.webServer> 4 | <staticContent> 5 | <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" /> 6 | 7 | <mimeMap fileExtension=".json" mimeType="application/json" /> 8 | <mimeMap fileExtension=".rss" mimeType="application/rss+xml" /> 9 | <mimeMap fileExtension=".ts" mimeType="application/x-typescript" /> 10 | <mimeMap fileExtension=".xmlui" mimeType="application/xmlui" /> 11 | <mimeMap fileExtension=".xmlui.xs" mimeType="application/xmlui-xs" /> 12 | <mimeMap fileExtension="woff" mimeType="application/font-woff" /> 13 | <mimeMap fileExtension="woff2" mimeType="application/font-woff2" /> 14 | <mimeMap fileExtension="md" mimeType="text/markdown" /> 15 | </staticContent> 16 | <rewrite> 17 | <rules> 18 | <rule name="RewriteHTML" stopProcessing="true"> 19 | <match url="^([^.]+)$" /> 20 | <conditions> 21 | <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> 22 | <add input="{REQUEST_FILENAME}.html" matchType="IsFile" /> 23 | </conditions> 24 | <action type="Rewrite" url="{R:1}.html" /> 25 | </rule> 26 | <remove name="pushState" /> 27 | <rule name="pushState" stopProcessing="true"> 28 | <match url=".*" /> 29 | <conditions logicalGrouping="MatchAll"> 30 | <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> 31 | <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> 32 | </conditions> 33 | <action type="Rewrite" url="/" /> 34 | </rule> 35 | <rule name="Playground" stopProcessing="true"> 36 | <match url="^playground$" /> 37 | <action type="Rewrite" url="/playground" /> 38 | </rule> 39 | </rules> 40 | </rewrite> 41 | </system.webServer> 42 | </configuration> 43 | ``` -------------------------------------------------------------------------------- /docs/public/pages/howto/share-a-modaldialog-across-components.md: -------------------------------------------------------------------------------- ```markdown 1 | # Share a ModalDialog across components 2 | 3 | ```xmlui-pg noHeader 4 | ---app 5 | <App> 6 | <Test /> 7 | </App> 8 | ---api 9 | { 10 | "apiUrl": "/api", 11 | "initialize": "$state.items = [ 12 | { id: 1, title: 'Mountain View' }, 13 | { id: 2, title: 'City Lights' }, 14 | { id: 3, title: 'Ocean Sunset' } 15 | ]", 16 | "operations": { 17 | "get-items": { 18 | "url": "/items", 19 | "method": "get", 20 | "handler": "return $state.items" 21 | } 22 | } 23 | } 24 | ---comp display 25 | <Component name="Test"> 26 | 27 | <AppState id="settings" bucket="appSettings" initialValue="{{ 28 | itemSize: 'medium', 29 | showDetails: true 30 | }}" /> 31 | 32 | <!-- Settings modal defined at App level - accessible to all components --> 33 | <ModalDialog id="settingsDialog" title="Settings"> 34 | <SettingsPanel /> 35 | </ModalDialog> 36 | 37 | <DataSource id="items" url="/api/items" /> 38 | 39 | <AppHeader title="Demo App"> 40 | <property name="profileMenuTemplate"> 41 | <Icon name="cog" onClick="settingsDialog.open()" /> 42 | </property> 43 | </AppHeader> 44 | 45 | <VStack gap="1rem"> 46 | <HStack gap="1rem"> 47 | <Text>Items ({settings.value.itemSize} size)</Text> 48 | <Button 49 | label="Settings" 50 | size="sm" 51 | onClick="settingsDialog.open()" 52 | /> 53 | </HStack> 54 | 55 | <List data="{items}"> 56 | <Card> 57 | <VStack> 58 | <Text>{$item.title}</Text> 59 | <Fragment when="{settings.value.showDetails}"> 60 | <Text variant="caption">ID: {$item.id}</Text> 61 | </Fragment> 62 | </VStack> 63 | </Card> 64 | </List> 65 | </VStack> 66 | 67 | </Component> 68 | ---comp display 69 | <Component name="SettingsPanel"> 70 | <AppState id="settings" bucket="appSettings" /> 71 | 72 | <VStack gap="1rem"> 73 | 74 | <Select 75 | label="Item Size" 76 | initialValue="{settings.value.itemSize}" 77 | onDidChange="(value) => settings.update({ itemSize: value })" 78 | > 79 | <Option value="small" label="Small" /> 80 | <Option value="medium" label="Medium" /> 81 | <Option value="large" label="Large" /> 82 | </Select> 83 | 84 | <Switch 85 | label="Show details" 86 | initialValue="{settings.value.showDetails}" 87 | onDidChange="(value) => settings.update({ showDetails: value })" 88 | /> 89 | 90 | </VStack> 91 | </Component> 92 | ``` 93 | ``` -------------------------------------------------------------------------------- /packages/xmlui-playground/src/playground/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Convert a string to its UTF-8 bytes and compress it. 3 | * 4 | * @param {string} str 5 | * @returns {Promise<Uint8Array>} 6 | */ 7 | async function compress(str: string): Promise<Uint8Array> { 8 | // Convert the string to a byte stream. 9 | const stream = new Blob([str]).stream(); 10 | 11 | // Create a compressed stream. 12 | const compressedStream = stream.pipeThrough(new CompressionStream("gzip")); 13 | 14 | // Convert the string to a byte stream. 15 | const reader = compressedStream.getReader(); 16 | const chunks = []; 17 | while (true) { 18 | const { done, value } = await reader.read(); 19 | if (done) break; 20 | chunks.push(value); 21 | } 22 | 23 | return await concatUint8Arrays(chunks); 24 | } 25 | 26 | /** 27 | * Decompress bytes into a UTF-8 string. 28 | * 29 | * @param {Uint8Array} compressedBytes 30 | * @returns {Promise<string>} 31 | */ 32 | async function decompress(compressedBytes: Uint8Array): Promise<string> { 33 | // Convert the bytes to a stream. 34 | const stream = new Blob([compressedBytes]).stream(); 35 | 36 | // Create a decompressed stream. 37 | const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip")); 38 | 39 | // Convert the string to a byte stream. 40 | const reader = decompressedStream.getReader(); 41 | const chunks = []; 42 | while (true) { 43 | const { done, value } = await reader.read(); 44 | if (done) break; 45 | chunks.push(value); 46 | } 47 | 48 | const stringBytes = await concatUint8Arrays(chunks); 49 | 50 | // Convert the bytes to a string. 51 | return new TextDecoder().decode(stringBytes); 52 | } 53 | 54 | /** 55 | * Combine multiple Uint8Arrays into one. 56 | * 57 | * @param {ReadonlyArray<Uint8Array>} uint8arrays 58 | * @returns {Promise<Uint8Array>} 59 | */ 60 | async function concatUint8Arrays(uint8arrays: Uint8Array[]): Promise<Uint8Array> { 61 | const blob = new Blob(uint8arrays); 62 | const buffer = await blob.arrayBuffer(); 63 | return new Uint8Array(buffer); 64 | } 65 | 66 | async function createQueryString(target: any): Promise<string> { 67 | // Convert the Uint8Array to a Base64 string. 68 | 69 | const compressed = await compress(target); 70 | const base64 = btoa(String.fromCharCode(...compressed)); 71 | 72 | // Create a query string. 73 | return encodeURIComponent(base64); 74 | } 75 | 76 | export { compress, decompress, createQueryString }; 77 | ``` -------------------------------------------------------------------------------- /xmlui/tests/language-server/hover.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { handleHoverCore } from "../../src/language-server/services/hover"; 3 | import { createXmlUiParser } from "../../src/parsers/xmlui-parser"; 4 | import { mockMetadataProvider } from "./mockData"; 5 | import { MarkupContent } from "vscode-languageserver"; 6 | 7 | describe("Hover", () => { 8 | it("documents component description", () => { 9 | const docs = hoverAtPoundSign("<Butt#on />").value; 10 | const expected = mockMetadataProvider.getComponent("Button").getMetadata().description; 11 | 12 | expect(docs).toContain(expected); 13 | }); 14 | 15 | it("documents prop description", () => { 16 | const docs = hoverAtPoundSign("<Button lab#el='Hello' />").value; 17 | const expected = mockMetadataProvider.getComponent("Button").getAttr("label").description; 18 | 19 | expect(docs).toContain(expected); 20 | }); 21 | 22 | it("documents event description", () => { 23 | const docs = hoverAtPoundSign("<Button onCl#ick='Hello' />").value; 24 | const expected = mockMetadataProvider.getComponent("Button").getAttr("onClick").description; 25 | 26 | expect(docs).toContain(expected); 27 | }); 28 | 29 | it("documents implicit prop description", () => { 30 | const docs = hoverAtPoundSign("<Button dat#a='Hello' />").value; 31 | const expected = mockMetadataProvider.getComponent("Button").getAttr("data").description; 32 | 33 | expect(docs).toContain(expected); 34 | }); 35 | 36 | it("documents layout prop description", () => { 37 | const docs = hoverAtPoundSign("<Button wid#th='Hello' />").value; 38 | const expected = mockMetadataProvider.getComponent("Button").getAttr("width").description; 39 | 40 | expect(docs).toContain(expected); 41 | }); 42 | }); 43 | 44 | function hoverAtPoundSign(source: string) { 45 | const cursorIndicator = "#"; 46 | const position = source.indexOf(cursorIndicator); 47 | if (position === -1) { 48 | throw new Error(`No '${cursorIndicator}' found in the tested source to denote the position of the cursor.`); 49 | } 50 | source = source.replace(cursorIndicator, ""); 51 | const parser = createXmlUiParser(source) 52 | 53 | const { node } = parser.parse() 54 | 55 | return handleHoverCore({ getText: parser.getText, node, metaByComp: mockMetadataProvider }, position) 56 | } 57 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/ToneSwitch/ToneSwitchNative.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { forwardRef } from "react"; 2 | import { useThemes } from "../../components-core/theming/ThemeContext"; 3 | import Icon from "../Icon/IconNative"; 4 | import { Toggle } from "../Toggle/Toggle"; 5 | import styles from "./ToneSwitch.module.scss"; 6 | import classnames from "classnames"; 7 | 8 | // Default icons for light and dark modes 9 | const DEFAULT_LIGHT_ICON = "sun"; 10 | const DEFAULT_DARK_ICON = "moon"; 11 | 12 | export type ToneSwitchProps = { 13 | /** 14 | * Icon to display for light mode 15 | * @default "sun" 16 | */ 17 | iconLight?: string; 18 | 19 | /** 20 | * Icon to display for dark mode 21 | * @default "moon" 22 | */ 23 | iconDark?: string; 24 | className?: string; 25 | }; 26 | 27 | export const ToneSwitch = forwardRef<HTMLDivElement, ToneSwitchProps>(function ToneSwitch({ 28 | iconLight = DEFAULT_LIGHT_ICON, 29 | iconDark = DEFAULT_DARK_ICON, 30 | className, 31 | ...rest 32 | }: ToneSwitchProps, ref) { 33 | const { activeThemeTone, setActiveThemeTone } = useThemes(); 34 | //console.log('ToneSwitch render - activeThemeTone:', activeThemeTone); // Debug log 35 | 36 | const handleChange = (isDark: boolean) => { 37 | setActiveThemeTone(isDark ? "dark" : "light"); 38 | }; 39 | 40 | return ( 41 | <div {...rest} ref={ref} style={{ width: "fit-content", display: "inline-block" }} className={className}> 42 | <Toggle 43 | value={activeThemeTone === "dark"} 44 | onDidChange={handleChange} 45 | variant="switch" 46 | style={{ width: "fit-content" }} 47 | inputRenderer={(contextVars) => { 48 | //console.log('ToneSwitch contextVars:', contextVars); // Debug log 49 | return ( 50 | <div 51 | className={classnames(styles.iconSwitch, { 52 | [styles.light]: !contextVars.$checked, 53 | [styles.dark]: contextVars.$checked, 54 | })} 55 | > 56 | <div className={styles.iconThumb}> 57 | {!contextVars.$checked ? ( 58 | <Icon name={iconLight} fallback="sun" className={styles.icon} /> 59 | ) : ( 60 | <Icon name={iconDark} fallback="moon" className={styles.icon} /> 61 | )} 62 | </div> 63 | </div> 64 | ); 65 | }} 66 | /> 67 | </div> 68 | ); 69 | }); 70 | ``` -------------------------------------------------------------------------------- /docs/public/resources/files/monthly-status.json: -------------------------------------------------------------------------------- ```json 1 | [{"month":"2022-06","paid_revenue":2243.97,"sent_revenue":0},{"month":"2022-07","paid_revenue":815,"sent_revenue":0},{"month":"2022-08","paid_revenue":2285,"sent_revenue":0},{"month":"2022-09","paid_revenue":1050,"sent_revenue":0},{"month":"2022-10","paid_revenue":1585,"sent_revenue":0},{"month":"2022-11","paid_revenue":473.81,"sent_revenue":660},{"month":"2022-12","paid_revenue":180,"sent_revenue":852.86},{"month":"2023-01","paid_revenue":1587.75,"sent_revenue":0},{"month":"2023-02","paid_revenue":845.05,"sent_revenue":0},{"month":"2023-03","paid_revenue":1419.54,"sent_revenue":0},{"month":"2023-04","paid_revenue":1000,"sent_revenue":0},{"month":"2023-05","paid_revenue":2301.41,"sent_revenue":0},{"month":"2023-06","paid_revenue":360,"sent_revenue":0},{"month":"2023-07","paid_revenue":1912.34,"sent_revenue":0},{"month":"2023-08","paid_revenue":1556.49,"sent_revenue":0},{"month":"2023-09","paid_revenue":1296.89,"sent_revenue":0},{"month":"2023-10","paid_revenue":1272.01,"sent_revenue":0},{"month":"2023-11","paid_revenue":1445,"sent_revenue":0},{"month":"2023-12","paid_revenue":108.3,"sent_revenue":0},{"month":"2024-01","paid_revenue":70,"sent_revenue":0},{"month":"2024-02","paid_revenue":4039.41,"sent_revenue":0},{"month":"2024-03","paid_revenue":1754.33,"sent_revenue":0},{"month":"2024-04","paid_revenue":1243.81,"sent_revenue":0},{"month":"2024-05","paid_revenue":568.57,"sent_revenue":0},{"month":"2024-06","paid_revenue":2818.28,"sent_revenue":0},{"month":"2024-07","paid_revenue":2818.69,"sent_revenue":0},{"month":"2024-08","paid_revenue":1226.97,"sent_revenue":0},{"month":"2024-09","paid_revenue":680,"sent_revenue":0},{"month":"2024-10","paid_revenue":940,"sent_revenue":0},{"month":"2024-11","paid_revenue":1208.35,"sent_revenue":0},{"month":"2024-12","paid_revenue":2475,"sent_revenue":0},{"month":"2025-01","paid_revenue":162.45,"sent_revenue":0},{"month":"2025-02","paid_revenue":882.73,"sent_revenue":555.04},{"month":"2025-03","paid_revenue":0,"sent_revenue":810},{"month":"2025-04","paid_revenue":700,"sent_revenue":0},{"month":"2025-05","paid_revenue":0,"sent_revenue":625}] 2 | ``` -------------------------------------------------------------------------------- /xmlui/tests-e2e/namespaces.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SKIP_REASON } from "../src/testing/component-test-helpers"; 2 | import { expect, test } from "../src/testing/fixtures"; 3 | 4 | test("core button renders without namespace", async ({ page, initTestBed }) => { 5 | await initTestBed(`<App><Button testId="button">CORE</Button></App>`, { 6 | components: [ 7 | ` 8 | <Component name="Button"> 9 | <Text>COMPOUND COMPONENT</Text> 10 | </Component> 11 | `, 12 | ], 13 | }); 14 | await expect(page.getByTestId("button")).toHaveText("CORE"); 15 | }); 16 | 17 | test("core button renders with XMLUI namespace", async ({ page, initTestBed }) => { 18 | await initTestBed( 19 | ` 20 | <App xmlns:XMLUI="core-ns"> 21 | <XMLUI:Button testId="button">CORE</XMLUI:Button> 22 | </App>`, 23 | { 24 | components: [ 25 | ` 26 | <Component name="Button"> 27 | <Text>COMPOUND COMPONENT</Text> 28 | </Component> 29 | `, 30 | ], 31 | }, 32 | ); 33 | await expect(page.getByTestId("button")).toHaveText("CORE"); 34 | }); 35 | 36 | test("compound component button renders with app-ns namespace", async ({ page, initTestBed }) => { 37 | await initTestBed( 38 | ` 39 | <App xmlns:My="app-ns"> 40 | <My:Button testId="button">CORE</My:Button> 41 | </App>`, 42 | { 43 | components: [ 44 | ` 45 | <Component name="Button"> 46 | <Text>COMPOUND COMPONENT</Text> 47 | </Component> 48 | `, 49 | ], 50 | }, 51 | ); 52 | await expect(page.getByTestId("button")).toHaveText("COMPOUND COMPONENT"); 53 | }); 54 | 55 | test("compound component renders without namespace (no name-conflict with core component)", async ({ 56 | page, 57 | initTestBed, 58 | }) => { 59 | await initTestBed( 60 | ` 61 | <App> 62 | <MyButton testId="button">CORE</MyButton> 63 | </App>`, 64 | { 65 | components: [ 66 | ` 67 | <Component name="MyButton"> 68 | <Text>COMPOUND COMPONENT</Text> 69 | </Component> 70 | `, 71 | ], 72 | }, 73 | ); 74 | await expect(page.getByTestId("button")).toHaveText("COMPOUND COMPONENT"); 75 | }); 76 | 77 | test("extension doesn't render without namespace", async ({ page, initTestBed }) => { 78 | await initTestBed(` 79 | <App> 80 | <TestComponent testId="testComp">EXTENSION CONTENT</TestComponent> 81 | </App>`); 82 | await expect(page.getByTestId("testComp")).not.toBeVisible(); 83 | }); 84 | ``` -------------------------------------------------------------------------------- /packages/xmlui-playground/src/playground/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { usePlayground } from "../hooks/usePlayground"; 2 | import { MdOutlinePalette } from "react-icons/md"; 3 | import styles from "./ThemeSwitcher.module.scss"; 4 | import classnames from "classnames"; 5 | import * as RadixMenu from "@radix-ui/react-dropdown-menu"; 6 | import { FiCheck } from "react-icons/fi"; 7 | import { activeThemeChanged } from "../state/store"; 8 | import { forwardRef } from "react"; 9 | import { Button, useTheme } from "xmlui"; 10 | 11 | export const ThemeSwitcher = forwardRef<HTMLButtonElement>((props, ref) => { 12 | const { appDescription, options, dispatch } = usePlayground(); 13 | const { root } = useTheme(); 14 | 15 | return ( 16 | <div> 17 | <RadixMenu.Root modal={false}> 18 | <RadixMenu.Trigger className={styles.button} {...props} asChild ref={ref}> 19 | <Button variant="ghost"> 20 | <MdOutlinePalette size={18} /> 21 | </Button> 22 | </RadixMenu.Trigger> 23 | <RadixMenu.Portal container={root}> 24 | <RadixMenu.Content className={classnames(styles.RadixMenuContent)}> 25 | <RadixMenu.Label className={styles.RadixMenuLabel}>Theme</RadixMenu.Label> 26 | <RadixMenu.RadioGroup 27 | className={styles.RadixMenuRadioGroup} 28 | value={options.activeTheme} 29 | onValueChange={(value: string) => dispatch(activeThemeChanged(value))} 30 | > 31 | {appDescription.availableThemes && 32 | appDescription.availableThemes.length > 0 && 33 | appDescription.availableThemes.map((theme, index) => ( 34 | <RadixMenu.RadioItem 35 | className={styles.RadixMenuRadioItem} 36 | value={theme.id} 37 | key={index} 38 | > 39 | {theme.id} 40 | <RadixMenu.ItemIndicator className={styles.RadixMenuItemIndicator}> 41 | <FiCheck /> 42 | </RadixMenu.ItemIndicator> 43 | </RadixMenu.RadioItem> 44 | ))} 45 | </RadixMenu.RadioGroup> 46 | </RadixMenu.Content> 47 | </RadixMenu.Portal> 48 | </RadixMenu.Root> 49 | </div> 50 | ); 51 | }); 52 | 53 | ThemeSwitcher.displayName = "ThemeSwitcher"; 54 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Icon/svg/admonition_note.svg: -------------------------------------------------------------------------------- ``` 1 | <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <path d="M7 5.15039H28C28.4694 5.15039 28.8496 5.53056 28.8496 6V26.6445C28.8496 26.8769 28.755 27.0994 28.5869 27.2598L24.0205 31.6152C23.8625 31.766 23.652 31.8496 23.4336 31.8496H7C6.53056 31.8496 6.15039 31.4694 6.15039 31V6C6.15039 5.53056 6.53056 5.15039 7 5.15039Z" fill="#FEFEFE" stroke="#666666" stroke-width="0.3"/> 3 | <path d="M26.0982 12.5596H8.58191V13.083H26.0982V12.5596Z" fill="#666666"/> 4 | <path d="M26.0982 15.3857H8.58191V15.9092H26.0982V15.3857Z" fill="#666666"/> 5 | <path d="M26.0982 18.2109H8.58191V18.7344H26.0982V18.2109Z" fill="#666666"/> 6 | <path d="M26.0982 21.0039H8.58191V21.5273H26.0982V21.0039Z" fill="#666666"/> 7 | <path d="M26.0982 23.8291H8.58191V24.3525H26.0982V23.8291Z" fill="#666666"/> 8 | <path d="M16.8516 27.0947H8.58191V27.6181H16.8516V27.0947Z" fill="#666666"/> 9 | <path d="M23.9056 32.3591L29 27.2646H23.9056V32.3591Z" fill="#F4F2F2"/> 10 | <path d="M21 9H14V10H21V9Z" fill="#535353"/> 11 | <path d="M23.9339 14.783L22.4576 13.9355L21.6053 15.4034L23.0816 16.2508L23.9339 14.783Z" fill="#E0E1E2"/> 12 | <path d="M25.6385 11.8475L24.8948 11.4206C24.7937 11.3625 24.6837 11.3307 24.5715 11.3222C24.5154 11.318 24.4589 11.3196 24.4026 11.3276C24.1208 11.3654 23.8457 11.5452 23.6789 11.8325L22.4576 13.9357L23.9339 14.7831L25.6385 11.8475Z" fill="#E85738"/> 13 | <path d="M24.9268 17.3103L23.0815 16.251L18.1808 24.6911L20.0261 25.7504L24.9268 17.3103Z" fill="#F2C91D"/> 14 | <path d="M25.7791 15.8425L23.9338 14.7832L23.0815 16.251L24.9268 17.3104L25.7791 15.8425Z" fill="#C4C4C4"/> 15 | <path d="M25.7791 15.8427L27.0003 13.7395C27.1671 13.4522 27.1541 13.107 26.9975 12.8174C26.9664 12.7598 26.9289 12.7036 26.8866 12.6514C26.8012 12.5465 26.6944 12.4539 26.568 12.3813L25.6384 11.8477L23.9338 14.7833L25.7791 15.8427Z" fill="#B13218"/> 16 | <path d="M20.0261 25.7505L16.7046 23.8438L16.6607 27.7328L20.0261 25.7505Z" fill="#FDEDD9"/> 17 | <path d="M17.8536 26.95L16.7465 26.3145L16.6608 27.7331L17.8536 26.95Z" fill="#4C4C4C"/> 18 | <path d="M23.0816 16.2508L21.6053 15.4033L16.7046 23.8434L18.1808 24.6909L23.0816 16.2508Z" fill="#FFD93C"/> 19 | </svg> 20 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Items/Items.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createComponentRenderer } from "../../components-core/renderers"; 2 | import { MemoizedItem } from "../container-helpers"; 3 | import { createMetadata, d, dComponent, dInternal } from "../metadata-helpers"; 4 | import { Items, defaultProps } from "./ItemsNative"; 5 | 6 | const COMP = "Items"; 7 | 8 | export const ItemsMd = createMetadata({ 9 | status: "stable", 10 | description: 11 | "`Items` renders data arrays without built-in layout or styling, providing " + 12 | "a lightweight alternative to `List`. Unlike `List`, it provides no " + 13 | "virtualization, grouping, or visual formatting — just pure data iteration.", 14 | props: { 15 | items: dInternal(`This property contains the list of data items this component renders.`), 16 | data: d( 17 | `This property contains the list of data items (obtained from a data source) this component renders.`, 18 | ), 19 | reverse: { 20 | description: 21 | "This property reverses the order in which data is mapped to template components.", 22 | type: "boolean", 23 | defaultValue: defaultProps.reverse, 24 | }, 25 | itemTemplate: dComponent("The component template to display a single item"), 26 | }, 27 | childrenAsTemplate: "itemTemplate", 28 | contextVars: { 29 | $item: dComponent("Current data item being rendered"), 30 | $itemIndex: dComponent( 31 | "Zero-based index of current item", 32 | ), 33 | $isFirst: dComponent("Boolean indicating if this is the first item"), 34 | $isLast: dComponent("Boolean indicating if this is the last item"), 35 | }, 36 | opaque: true, 37 | }); 38 | 39 | export const itemsComponentRenderer = createComponentRenderer(COMP, ItemsMd, (rendererContext) => { 40 | const { node, renderChild, extractValue, layoutContext } = rendererContext; 41 | return ( 42 | <Items 43 | items={extractValue(node.props.items) || extractValue(node.props.data)} 44 | reverse={extractValue(node.props.reverse)} 45 | renderItem={(contextVars, key) => { 46 | return ( 47 | <MemoizedItem 48 | key={key} 49 | contextVars={contextVars} 50 | node={node.props.itemTemplate} 51 | renderChild={renderChild} 52 | layoutContext={layoutContext} 53 | /> 54 | ); 55 | }} 56 | /> 57 | ); 58 | }); 59 | ``` -------------------------------------------------------------------------------- /xmlui/tests/parsers/xmlui/transform.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, assert } from "vitest"; 2 | import type { ComponentDef, CompoundComponentDef } from "../../../src/abstractions/ComponentDefs"; 3 | import { transformSource } from "./xmlui"; 4 | 5 | describe("Xmlui transform", () => { 6 | it("Empty code results in error", () => { 7 | try { 8 | transformSource(""); 9 | assert.fail("Exception expected"); 10 | } catch (err) { 11 | expect(err.toString().includes("T001")).equal(true); 12 | } 13 | }); 14 | 15 | it("Empty component #1", () => { 16 | const cd = transformSource("<Stack />") as ComponentDef; 17 | expect(cd.type).equal("Stack"); 18 | }); 19 | 20 | it("Empty component #2", () => { 21 | const cd = transformSource("<!-- This is a stack --><Stack />") as ComponentDef; 22 | expect(cd.type).equal("Stack"); 23 | }); 24 | 25 | it("Compound component needs a name #1", () => { 26 | try { 27 | transformSource("<Component><Stack/></Component>"); 28 | assert.fail("Exception expected"); 29 | } catch (err) { 30 | expect(err.toString().includes("T003")).equal(true); 31 | } 32 | }); 33 | 34 | it("Compound component needs a name #2", () => { 35 | try { 36 | transformSource("<Component name='haho'><Stack/></Component>"); 37 | assert.fail("Exception expected"); 38 | } catch (err) { 39 | expect(err.toString().includes("T004")).equal(true); 40 | } 41 | }); 42 | 43 | it("Compound component needs a component child #1", () => { 44 | const cd = transformSource("<Component name='MyComp'><!-- comment--></Component>") as CompoundComponentDef; 45 | expect((cd.component).type).equal("TextNode"); 46 | expect(((cd.component).props as any).value).equal(""); 47 | }); 48 | 49 | it("Compound component needs a component child #2", () => { 50 | const cd = transformSource("<Component name='MyComp'></Component>") as CompoundComponentDef; 51 | expect((cd.component).type).equal("TextNode"); 52 | expect(((cd.component).props as any).value).equal(""); 53 | }); 54 | 55 | it("Compound component cannot nest another one", () => { 56 | try { 57 | transformSource("<Component name='MyComp'><Component name='Other'/></Component>"); 58 | assert.fail("Exception expected"); 59 | } catch (err) { 60 | expect(err.toString().includes("T006")).equal(true); 61 | } 62 | }); 63 | }); 64 | ``` -------------------------------------------------------------------------------- /blog/scripts/download-latest-xmlui.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs").promises; 4 | const path = require("path"); 5 | const axios = require("axios"); 6 | const { sortByVersion, XMLUI_STANDALONE_PATTERN } = require("./utils.js"); 7 | 8 | const getLatestAssetUrl = async () => { 9 | try { 10 | const per_page = 100; 11 | const max_releases = 100; 12 | 13 | const url = `https://api.github.com/repos/xmlui-org/xmlui/releases?per_page=${per_page}`; 14 | 15 | const res = await fetch(url); 16 | 17 | if (!res.ok) { 18 | throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); 19 | } 20 | 21 | /** @type {Array<any>} */ 22 | const releases = await res.json(); 23 | 24 | const xmluiReleases = releases 25 | .filter((r) => r?.tag_name?.startsWith("xmlui@")) 26 | .sort(sortByVersion); 27 | 28 | const releasesToProcess = xmluiReleases.slice(0, max_releases); 29 | 30 | for (const release of releasesToProcess) { 31 | const xmluiStandaloneAsset = (release.assets || []).find((asset) => 32 | XMLUI_STANDALONE_PATTERN.test(asset.name), 33 | ); 34 | return xmluiStandaloneAsset.browser_download_url; 35 | } 36 | throw new Error("No matching standalone asset found in the latest releases"); 37 | } catch (e) { 38 | console.error("Error fetching latest release", e); 39 | throw e; 40 | } 41 | }; 42 | 43 | async function downloadFile(url) { 44 | try { 45 | const response = await axios.get(url, { 46 | responseType: "arraybuffer", 47 | maxRedirects: 5, 48 | }); 49 | return response.data; 50 | } catch (error) { 51 | console.log(error.message); 52 | } 53 | } 54 | 55 | (async () => { 56 | try { 57 | const rootDir = path.resolve(__dirname, ".."); 58 | const buildPublicDir = path.join(rootDir, "/public/resources/files/for-download/xmlui"); 59 | const browser_download_url = await getLatestAssetUrl(); 60 | 61 | if (!browser_download_url) { 62 | throw new Error("Missing browser_download_url"); 63 | } 64 | 65 | const filename = "xmlui-standalone.umd.js"; 66 | const outputPath = path.join(buildPublicDir, filename); 67 | 68 | const fileBuffer = await downloadFile(browser_download_url); 69 | 70 | await fs.mkdir(buildPublicDir, { recursive: true }); 71 | await fs.writeFile(outputPath, fileBuffer); 72 | } catch (err) { 73 | console.error(err.message); 74 | process.exit(1); 75 | } 76 | })(); 77 | ``` -------------------------------------------------------------------------------- /docs/scripts/download-latest-xmlui.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs").promises; 4 | const path = require("path"); 5 | const axios = require("axios"); 6 | const { sortByVersion, XMLUI_STANDALONE_PATTERN } = require("./utils.js"); 7 | 8 | const getLatestAssetUrl = async () => { 9 | try { 10 | const per_page = 100; 11 | const max_releases = 100; 12 | 13 | const url = `https://api.github.com/repos/xmlui-org/xmlui/releases?per_page=${per_page}`; 14 | 15 | const res = await fetch(url); 16 | 17 | if (!res.ok) { 18 | throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); 19 | } 20 | 21 | /** @type {Array<any>} */ 22 | const releases = await res.json(); 23 | 24 | const xmluiReleases = releases 25 | .filter((r) => r?.tag_name?.startsWith("xmlui@")) 26 | .sort(sortByVersion); 27 | 28 | const releasesToProcess = xmluiReleases.slice(0, max_releases); 29 | 30 | for (const release of releasesToProcess) { 31 | const xmluiStandaloneAsset = (release.assets || []).find((asset) => 32 | XMLUI_STANDALONE_PATTERN.test(asset.name), 33 | ); 34 | return xmluiStandaloneAsset.browser_download_url; 35 | } 36 | throw new Error("No matching standalone asset found in the latest releases"); 37 | } catch (e) { 38 | console.error("Error fetching latest release", e); 39 | throw e; 40 | } 41 | }; 42 | 43 | async function downloadFile(url) { 44 | try { 45 | const response = await axios.get(url, { 46 | responseType: "arraybuffer", 47 | maxRedirects: 5, 48 | }); 49 | return response.data; 50 | } catch (error) { 51 | console.log(error.message); 52 | } 53 | } 54 | 55 | (async () => { 56 | try { 57 | const rootDir = path.resolve(__dirname, ".."); 58 | const buildPublicDir = path.join(rootDir, "/public/resources/files/for-download/xmlui"); 59 | const browser_download_url = await getLatestAssetUrl(); 60 | 61 | if (!browser_download_url) { 62 | throw new Error("Missing browser_download_url"); 63 | } 64 | 65 | const filename = "xmlui-standalone.umd.js"; 66 | const outputPath = path.join(buildPublicDir, filename); 67 | 68 | const fileBuffer = await downloadFile(browser_download_url); 69 | 70 | await fs.mkdir(buildPublicDir, { recursive: true }); 71 | await fs.writeFile(outputPath, fileBuffer); 72 | } catch (err) { 73 | console.error(err.message); 74 | process.exit(1); 75 | } 76 | })(); 77 | ``` -------------------------------------------------------------------------------- /docs/content/components/Pages.md: -------------------------------------------------------------------------------- ```markdown 1 | # Pages [#pages] 2 | 3 | `Pages` serves as the routing coordinator within an [App](/components/App), managing which [Page](/components/Page) displays based on the current URL. 4 | 5 | **Key features:** 6 | 7 | - **Route coordination**: Automatically displays the correct Page based on current URL and navigation 8 | - **Default route handling**: Sets the initial page shown when the application loads 9 | - **Client-side routing**: Manages navigation without page refreshes or server requests 10 | 11 | ### Using the Pages and Page components [#using-the-pages-and-page-components] 12 | 13 | The `Page` component has a property called `url`. This is the route associated with the `Page's` contents. 14 | You can provide a link to this route to display a particular `Page`. 15 | Currently, all navigation is done on the clientside. 16 | No page is fetched from the server, thus the application operates as a [Single Page Application](https://developer.mozilla.org/en-US/docs/Glossary/SPA). 17 | 18 | ```xmlui-pg copy {3-4, 7, 10} display name="Example: using Pages and Page" height="170px" 19 | <App> 20 | <NavPanel> 21 | <NavLink label="Home" to="/" icon="home"/> 22 | <NavLink label="Account" to="/account" icon="user"/> 23 | </NavPanel> 24 | <Pages> 25 | <Page url="/"> 26 | <Text>Hello App!</Text> 27 | </Page> 28 | <Page url="/account"> 29 | <Text>This is the account page.</Text> 30 | </Page> 31 | </Pages> 32 | </App> 33 | ``` 34 | 35 | ## Properties [#properties] 36 | 37 | ### `fallbackPath` (default: "/") [#fallbackpath-default-] 38 | 39 | The fallback path when the current URL does not match any of the paths of the pages. 40 | 41 | ```xmlui-pg copy {6-13} display name="Example: fallbackPath" height="170px" 42 | <App> 43 | <NavPanel> 44 | <NavLink label="Not Home" to="/not-home" icon="trash"/> 45 | <NavLink label="Home" to="/home" icon="home"/> 46 | </NavPanel> 47 | <Pages fallbackPath="/home"> 48 | <Page url="/not-home"> 49 | <Text>This is not home...</Text> 50 | </Page> 51 | <Page url="/home"> 52 | <Text>Hello App!</Text> 53 | </Page> 54 | </Pages> 55 | </App> 56 | ``` 57 | 58 | ## Events [#events] 59 | 60 | This component does not have any events. 61 | 62 | ## Exposed Methods [#exposed-methods] 63 | 64 | This component does not expose any methods. 65 | 66 | ## Styling [#styling] 67 | 68 | This component does not have any styles. 69 | ``` -------------------------------------------------------------------------------- /docs/content/components/Redirect.md: -------------------------------------------------------------------------------- ```markdown 1 | # Redirect [#redirect] 2 | 3 | `Redirect` immediately redirects the browser to the URL in its `to` property when it gets visible (its `when` property gets `true`). It works only within [App](/components/App), not externally. 4 | 5 | ## Using `Redirect` [#using-redirect] 6 | 7 | The following app demonstrates two different patterns for using `Redirect`. 8 | 9 | 1. When you navigate to the "Redirect #1" page, it immediately redirects the app to the "Accounts" page. By default, the `when` property of `Redirect` (and any other component) is "true", so redirection immediately happens. 10 | 2. The "Redirect #2" page expects you to click the button before redirecting. The button click sets the `when` property of `Redirect` to true, and redirection happens at that moment. 11 | 12 | ```xmlui-pg copy {14, 20} display name="Example: providing children" height="170px" 13 | <App> 14 | <NavPanel> 15 | <NavLink to="/">Home</NavLink> 16 | <NavLink to="/accounts">Accounts</NavLink> 17 | <NavLink to="/products">Products</NavLink> 18 | <NavLink to="/redirect1">Redirect #1</NavLink> 19 | <NavLink to="/redirect2">Redirect #2</NavLink> 20 | </NavPanel> 21 | <Pages> 22 | <Page url="/">Home</Page> 23 | <Page url="/accounts">Accounts</Page> 24 | <Page url="/products">Products</Page> 25 | <Page url="/redirect1"> 26 | <Redirect to="/accounts" /> 27 | Redirecting to Accounts... 28 | </Page> 29 | <Page url="/redirect2"> 30 | <Fragment var.clicked="{false}"> 31 | <Button label="Click to redirect" onClick="clicked = true"/> 32 | <Redirect when="{clicked}" to="/accounts" /> 33 | Redirecting to Accounts... 34 | </Fragment> 35 | </Page> 36 | </Pages> 37 | </App> 38 | ``` 39 | 40 | ## Properties [#properties] 41 | 42 | ### `replace` (default: false) [#replace-default-false] 43 | 44 | This boolean property indicates whether the redirect should replace the current history entry or create a new one. 45 | 46 | ### `to` (default: "") [#to-default-] 47 | 48 | This property defines the URL to which this component is about to redirect requests. 49 | 50 | ## Events [#events] 51 | 52 | This component does not have any events. 53 | 54 | ## Exposed Methods [#exposed-methods] 55 | 56 | This component does not expose any methods. 57 | 58 | ## Styling [#styling] 59 | 60 | This component does not have any styles. 61 | ``` -------------------------------------------------------------------------------- /xmlui/scripts/generate-docs/build-downloads-map.mjs: -------------------------------------------------------------------------------- ``` 1 | import { writeFileSync, statSync } from "fs"; 2 | import { basename, extname } from "path"; 3 | import { gatherAndRemoveDuplicates, toNormalizedUpperCase, traverseDirectory } from "./utils.mjs"; 4 | import { createScopedLogger } from "./logging-standards.mjs"; 5 | import { DOWNLOADS_MAP_CONFIG } from "./constants.mjs"; 6 | import { 7 | generateExportStatements, 8 | processDuplicatesWithLogging 9 | } from "./pattern-utilities.mjs"; 10 | 11 | const baseUrlCutoff = DOWNLOADS_MAP_CONFIG.BASE_URL_CUTOFF; 12 | const includedFileExtensions = DOWNLOADS_MAP_CONFIG.INCLUDED_FILE_EXTENSIONS; 13 | 14 | /** 15 | * Creates a file containing download link constants for downloadable files. 16 | * @param {string} downloadsFolder The path to the downloads folder (use UNIX delimiters) 17 | * @param {string} outFilePathAndName The path and name of the output file (use UNIX delimiters) 18 | */ 19 | export function buildDownloadsMap(downloadsFolder, outFilePathAndName) { 20 | const logger = createScopedLogger("DownloadsMapBuilder"); 21 | logger.operationStart("building downloads map"); 22 | const downloads = []; 23 | traverseDirectory({ name: "", path: downloadsFolder }, (item, _) => { 24 | /** 25 | * name: the folder's/file's name (eg. "hello-app-engine") 26 | * path: the path to the root of the given folder from the project root (eg. "src/apps/1_basic/samples/hello-app-engine") 27 | * parent: parent node 28 | * children: children file/folder names 29 | */ 30 | if (statSync(item.path).isDirectory()) { 31 | // Node is a folder 32 | } else { 33 | // Node is a file 34 | if (includedFileExtensions.includes(extname(item.name))) { 35 | const relativePath = item.path.split(baseUrlCutoff)[1]; 36 | downloads.push({ 37 | id: toNormalizedUpperCase(basename(relativePath, extname(relativePath))), 38 | path: relativePath, 39 | }); 40 | } 41 | } 42 | }); 43 | 44 | const { filtered, duplicates } = gatherAndRemoveDuplicates(downloads); 45 | 46 | // Process duplicates with standardized logging 47 | processDuplicatesWithLogging(duplicates, logger, "download IDs and paths"); 48 | 49 | // Generate export statements using utility 50 | const outStr = generateExportStatements(filtered); 51 | 52 | writeFileSync(outFilePathAndName, outStr); 53 | } 54 | ``` -------------------------------------------------------------------------------- /packages/xmlui-devtools/src/devtools/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Convert a string to its UTF-8 bytes and compress it. 3 | * 4 | * @param {string} str 5 | * @returns {Promise<Uint8Array>} 6 | */ 7 | async function compress(str: string) { 8 | // Convert the string to a byte stream. 9 | const stream = new Blob([str]).stream(); 10 | 11 | // Create a compressed stream. 12 | const compressedStream = stream.pipeThrough( 13 | new CompressionStream("gzip") 14 | ); 15 | 16 | // Convert the string to a byte stream. 17 | const reader = compressedStream.getReader(); 18 | const chunks = []; 19 | while (true) { 20 | const { done, value } = await reader.read(); 21 | if (done) break; 22 | chunks.push(value); 23 | } 24 | 25 | return await concatUint8Arrays(chunks); 26 | } 27 | 28 | /** 29 | * Decompress bytes into a UTF-8 string. 30 | * 31 | * @param {Uint8Array} compressedBytes 32 | * @returns {Promise<string>} 33 | */ 34 | async function decompress(compressedBytes: Uint8Array) { 35 | // Convert the bytes to a stream. 36 | const stream = new Blob([compressedBytes]).stream(); 37 | 38 | // Create a decompressed stream. 39 | const decompressedStream = stream.pipeThrough( 40 | new DecompressionStream("gzip") 41 | ); 42 | 43 | // Convert the string to a byte stream. 44 | const reader = decompressedStream.getReader(); 45 | const chunks = []; 46 | while (true) { 47 | const { done, value } = await reader.read(); 48 | if (done) break; 49 | chunks.push(value); 50 | } 51 | 52 | const stringBytes = await concatUint8Arrays(chunks); 53 | 54 | // Convert the bytes to a string. 55 | return new TextDecoder().decode(stringBytes); 56 | } 57 | 58 | /** 59 | * Combine multiple Uint8Arrays into one. 60 | * 61 | * @param {ReadonlyArray<Uint8Array>} uint8arrays 62 | * @returns {Promise<Uint8Array>} 63 | */ 64 | async function concatUint8Arrays(uint8arrays: Uint8Array[]) { 65 | const blob = new Blob(uint8arrays); 66 | const buffer = await blob.arrayBuffer(); 67 | return new Uint8Array(buffer); 68 | } 69 | 70 | async function createQueryString(target: any) { 71 | // Convert the Uint8Array to a Base64 string. 72 | 73 | const compressed = await compress(target); 74 | const base64 = btoa(String.fromCharCode(...compressed)); 75 | 76 | // Create a query string. 77 | return encodeURIComponent(base64); 78 | } 79 | 80 | export { compress, decompress, createQueryString }; 81 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Bookmark/Bookmark.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createComponentRenderer } from "../../components-core/renderers"; 2 | import { createMetadata } from "../metadata-helpers"; 3 | import { Bookmark, defaultProps } from "./BookmarkNative"; 4 | 5 | const COMP = "Bookmark"; 6 | 7 | export const BookmarkMd = createMetadata({ 8 | status: "stable", 9 | description: 10 | "As its name suggests, this component places a bookmark into its parent component's view. The " + 11 | "component has an \`id\` that you can use in links to navigate (scroll to) the bookmark's location.", 12 | opaque: true, 13 | props: { 14 | id: { 15 | description: 16 | "The unique identifier of the bookmark. You can use this identifier in links " + 17 | "to navigate to this component's location. If this identifier is not set, you cannot " + 18 | "programmatically visit this bookmark.", 19 | valueType: "string", 20 | }, 21 | level: { 22 | description: 23 | "The level of the bookmark. The level is used to determine the bookmark's " + 24 | "position in the table of contents.", 25 | valueType: "number", 26 | defaultValue: defaultProps.level, 27 | }, 28 | title: { 29 | description: 30 | "Defines the text to display the bookmark in the table of contents. If this property is " + 31 | "empty, the text falls back to the value of \`id\`.", 32 | valueType: "string", 33 | }, 34 | omitFromToc: { 35 | description: "If true, this bookmark will be excluded from the table of contents.", 36 | valueType: "boolean", 37 | defaultValue: defaultProps.omitFromToc, 38 | }, 39 | }, 40 | apis: { 41 | scrollIntoView: { 42 | signature: "scrollIntoView()", 43 | description: "Scrolls the bookmark into view.", 44 | }, 45 | }, 46 | }); 47 | 48 | export const bookmarkComponentRenderer = createComponentRenderer( 49 | COMP, 50 | BookmarkMd, 51 | (rendererContext) => { 52 | const { node, renderChild, extractValue, layoutContext } = rendererContext; 53 | 54 | return ( 55 | <Bookmark 56 | uid={extractValue(node.uid)} 57 | level={extractValue(node.props.level)} 58 | title={extractValue(node.props.title)} 59 | omitFromToc={extractValue.asOptionalBoolean(node.props.omitFromToc)} 60 | > 61 | {renderChild(node.children, layoutContext)} 62 | </Bookmark> 63 | ); 64 | }, 65 | ); 66 | ``` -------------------------------------------------------------------------------- /xmlui/tests-e2e/api-call-as-extracted-component.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { expect, test } from "../src/testing/fixtures"; 2 | import { initApp } from "../src/testing/themed-app-test-helpers"; 3 | 4 | test("api call as an extracted component get called", async ({ page }) => { 5 | await initApp(page, { 6 | entryPoint: ` 7 | <Fragment> 8 | <APICall 9 | id="apiCall" 10 | url="/postUrl" 11 | method="post" 12 | body="{$param}" 13 | /> 14 | <Form onSubmit="(body)=>apiCall.execute(body)"> 15 | <FormItem bindTo="name" testId="nameInput"/> 16 | </Form> 17 | </Fragment> 18 | `, 19 | apiInterceptor: { 20 | operations: { 21 | "postUrl": { 22 | url: "/postUrl", 23 | method: "post", 24 | handler: `()=>{ 25 | return $requestBody; 26 | }`, 27 | } 28 | }, 29 | }, 30 | }); 31 | 32 | 33 | const responsePromise = page.waitForResponse((response) => response.url().includes("/postUrl")); 34 | 35 | await page.getByTestId("nameInput").getByRole("textbox").fill("John"); 36 | await page.locator("button[type='submit']").click(); 37 | 38 | 39 | const response = await responsePromise; 40 | const responseBody = await response.json(); 41 | expect(responseBody).toEqual({ 42 | name: "John" 43 | }); 44 | }); 45 | 46 | 47 | test("api call as an extracted component get called (function reference + default body)", async ({ page }) => { 48 | await initApp(page, { 49 | entryPoint: ` 50 | <Fragment> 51 | <APICall 52 | id="apiCall" 53 | url="/postUrl" 54 | method="post" 55 | /> 56 | <Form onSubmit="apiCall.execute"> 57 | <FormItem bindTo="name" testId="nameInput"/> 58 | </Form> 59 | </Fragment> 60 | `, 61 | apiInterceptor: { 62 | operations: { 63 | "postUrl": { 64 | url: "/postUrl", 65 | method: "post", 66 | handler: `()=>{ 67 | return $requestBody; 68 | }`, 69 | } 70 | }, 71 | }, 72 | }); 73 | 74 | 75 | const responsePromise = page.waitForResponse((response) => response.url().includes("/postUrl")); 76 | 77 | await page.getByTestId("nameInput").getByRole("textbox").fill("John"); 78 | await page.locator("button[type='submit']").click(); 79 | 80 | const response = await responsePromise; 81 | const responseBody = await response.json(); 82 | expect(responseBody).toEqual({ 83 | name: "John" 84 | }); 85 | }); 86 | ```