This is page 16 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 │ └── cool-queens-look.md ├── .eslintrc.cjs ├── .github │ ├── build-checklist.png │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── deploy-blog.yml │ ├── deploy-docs-optimized.yml │ ├── deploy-docs.yml │ ├── prepare-versions.yml │ ├── release-packages.yml │ ├── run-all-tests.yml │ └── run-smoke-tests.yml ├── .gitignore ├── .prettierrc.js ├── .vscode │ ├── launch.json │ └── settings.json ├── blog │ ├── .gitignore │ ├── .gitkeep │ ├── CHANGELOG.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── 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 │ │ ├── netlify.toml │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ └── for-download │ │ │ │ └── xmlui │ │ │ │ └── xmlui-standalone.umd.js │ │ │ ├── github.svg │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ ├── Debug.xmlui │ │ │ └── PageNotFound.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ ├── Main.xmlui.xs │ │ └── 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 │ ├── component-behaviors.md │ ├── containers.md │ ├── glossary.md │ ├── index.md │ ├── next │ │ ├── component-dev-guide.md │ │ ├── configuration-management-enhancement-summary.md │ │ ├── documentation-scripts-refactoring-complete-summary.md │ │ ├── documentation-scripts-refactoring-plan.md │ │ ├── duplicate-pattern-extraction-summary.md │ │ ├── error-handling-standardization-summary.md │ │ ├── generating-component-reference.md │ │ ├── index.md │ │ ├── logging-consistency-implementation-summary.md │ │ ├── project-build.md │ │ ├── project-structure.md │ │ ├── theme-context.md │ │ ├── tiptap-design-considerations.md │ │ ├── working-with-code.md │ │ ├── xmlui-runtime-architecture │ │ └── xmlui-wcag-accessibility-report.md │ ├── react-fundamentals.md │ ├── release-method.md │ ├── standalone-app.md │ ├── state-management.md │ └── xmlui-repo.md ├── package.json ├── playwright.config.ts ├── scripts │ ├── coverage-only.js │ ├── e2e-test-summary.js │ ├── generate-docs │ │ ├── build-downloads-map.mjs │ │ ├── build-pages-map.mjs │ │ ├── components-config.json │ │ ├── configuration-management.mjs │ │ ├── constants.mjs │ │ ├── create-theme-files.mjs │ │ ├── DocsGenerator.mjs │ │ ├── error-handling.mjs │ │ ├── extensions-config.json │ │ ├── folders.mjs │ │ ├── generate-summary-files.mjs │ │ ├── get-docs.mjs │ │ ├── input-handler.mjs │ │ ├── logger.mjs │ │ ├── logging-standards.mjs │ │ ├── MetadataProcessor.mjs │ │ ├── pattern-utilities.mjs │ │ └── utils.mjs │ ├── get-langserver-metadata.mjs │ ├── inline-links.mjs │ └── README-e2e-summary.md ├── src │ ├── abstractions │ │ ├── _conventions.md │ │ ├── ActionDefs.ts │ │ ├── AppContextDefs.ts │ │ ├── ComponentDefs.ts │ │ ├── ContainerDefs.ts │ │ ├── ExtensionDefs.ts │ │ ├── FunctionDefs.ts │ │ ├── RendererDefs.ts │ │ ├── scripting │ │ │ ├── BlockScope.ts │ │ │ ├── Compilation.ts │ │ │ ├── LogicalThread.ts │ │ │ ├── LoopScope.ts │ │ │ ├── modules.ts │ │ │ ├── ScriptParserError.ts │ │ │ ├── Token.ts │ │ │ ├── TryScope.ts │ │ │ └── TryScopeExp.ts │ │ └── ThemingDefs.ts │ ├── components │ │ ├── _conventions.md │ │ ├── abstractions.ts │ │ ├── Accordion │ │ │ ├── Accordion.md │ │ │ ├── Accordion.module.scss │ │ │ ├── Accordion.spec.ts │ │ │ ├── Accordion.tsx │ │ │ ├── AccordionContext.tsx │ │ │ ├── AccordionItem.tsx │ │ │ ├── AccordionItemNative.tsx │ │ │ └── AccordionNative.tsx │ │ ├── Animation │ │ │ └── AnimationNative.tsx │ │ ├── APICall │ │ │ ├── APICall.md │ │ │ ├── APICall.spec.ts │ │ │ ├── APICall.tsx │ │ │ └── APICallNative.tsx │ │ ├── App │ │ │ ├── App.md │ │ │ ├── App.module.scss │ │ │ ├── App.spec.ts │ │ │ ├── App.tsx │ │ │ ├── AppLayoutContext.ts │ │ │ ├── AppNative.tsx │ │ │ ├── AppStateContext.ts │ │ │ ├── doc-resources │ │ │ │ ├── condensed-sticky.xmlui │ │ │ │ ├── condensed.xmlui │ │ │ │ ├── horizontal-sticky.xmlui │ │ │ │ ├── horizontal.xmlui │ │ │ │ ├── vertical-full-header.xmlui │ │ │ │ ├── vertical-sticky.xmlui │ │ │ │ └── vertical.xmlui │ │ │ ├── IndexerContext.ts │ │ │ ├── LinkInfoContext.ts │ │ │ ├── SearchContext.tsx │ │ │ ├── Sheet.module.scss │ │ │ └── Sheet.tsx │ │ ├── AppHeader │ │ │ ├── AppHeader.md │ │ │ ├── AppHeader.module.scss │ │ │ ├── AppHeader.spec.ts │ │ │ ├── AppHeader.tsx │ │ │ └── AppHeaderNative.tsx │ │ ├── AppState │ │ │ ├── AppState.md │ │ │ ├── AppState.spec.ts │ │ │ ├── AppState.tsx │ │ │ └── AppStateNative.tsx │ │ ├── AutoComplete │ │ │ ├── AutoComplete.md │ │ │ ├── AutoComplete.module.scss │ │ │ ├── AutoComplete.spec.ts │ │ │ ├── AutoComplete.tsx │ │ │ ├── AutoCompleteContext.tsx │ │ │ └── AutoCompleteNative.tsx │ │ ├── Avatar │ │ │ ├── Avatar.md │ │ │ ├── Avatar.module.scss │ │ │ ├── Avatar.spec.ts │ │ │ ├── Avatar.tsx │ │ │ └── AvatarNative.tsx │ │ ├── Backdrop │ │ │ ├── Backdrop.md │ │ │ ├── Backdrop.module.scss │ │ │ ├── Backdrop.spec.ts │ │ │ ├── Backdrop.tsx │ │ │ └── BackdropNative.tsx │ │ ├── Badge │ │ │ ├── Badge.md │ │ │ ├── Badge.module.scss │ │ │ ├── Badge.spec.ts │ │ │ ├── Badge.tsx │ │ │ └── BadgeNative.tsx │ │ ├── Bookmark │ │ │ ├── Bookmark.md │ │ │ ├── Bookmark.module.scss │ │ │ ├── Bookmark.spec.ts │ │ │ ├── Bookmark.tsx │ │ │ └── BookmarkNative.tsx │ │ ├── Breakout │ │ │ ├── Breakout.module.scss │ │ │ ├── Breakout.spec.ts │ │ │ ├── Breakout.tsx │ │ │ └── BreakoutNative.tsx │ │ ├── Button │ │ │ ├── Button-style.spec.ts │ │ │ ├── Button.md │ │ │ ├── Button.module.scss │ │ │ ├── Button.spec.ts │ │ │ ├── Button.tsx │ │ │ └── ButtonNative.tsx │ │ ├── Card │ │ │ ├── Card.md │ │ │ ├── Card.module.scss │ │ │ ├── Card.spec.ts │ │ │ ├── Card.tsx │ │ │ └── CardNative.tsx │ │ ├── Carousel │ │ │ ├── Carousel.md │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.spec.ts │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ ├── CarouselItem.tsx │ │ │ ├── CarouselItemNative.tsx │ │ │ └── CarouselNative.tsx │ │ ├── ChangeListener │ │ │ ├── ChangeListener.md │ │ │ ├── ChangeListener.spec.ts │ │ │ ├── ChangeListener.tsx │ │ │ └── ChangeListenerNative.tsx │ │ ├── chart-color-schemes.ts │ │ ├── Charts │ │ │ ├── AreaChart │ │ │ │ ├── AreaChart.md │ │ │ │ ├── AreaChart.spec.ts │ │ │ │ ├── AreaChart.tsx │ │ │ │ └── AreaChartNative.tsx │ │ │ ├── BarChart │ │ │ │ ├── BarChart.md │ │ │ │ ├── BarChart.module.scss │ │ │ │ ├── BarChart.spec.ts │ │ │ │ ├── BarChart.tsx │ │ │ │ └── BarChartNative.tsx │ │ │ ├── DonutChart │ │ │ │ ├── DonutChart.spec.ts │ │ │ │ └── DonutChart.tsx │ │ │ ├── LabelList │ │ │ │ ├── LabelList.spec.ts │ │ │ │ ├── LabelList.tsx │ │ │ │ ├── LabelListNative.module.scss │ │ │ │ └── LabelListNative.tsx │ │ │ ├── Legend │ │ │ │ ├── Legend.spec.ts │ │ │ │ ├── Legend.tsx │ │ │ │ └── LegendNative.tsx │ │ │ ├── LineChart │ │ │ │ ├── LineChart.md │ │ │ │ ├── LineChart.module.scss │ │ │ │ ├── LineChart.spec.ts │ │ │ │ ├── LineChart.tsx │ │ │ │ └── LineChartNative.tsx │ │ │ ├── PieChart │ │ │ │ ├── PieChart.md │ │ │ │ ├── PieChart.spec.ts │ │ │ │ ├── PieChart.tsx │ │ │ │ ├── PieChartNative.module.scss │ │ │ │ └── PieChartNative.tsx │ │ │ ├── RadarChart │ │ │ │ ├── RadarChart.md │ │ │ │ ├── RadarChart.spec.ts │ │ │ │ ├── RadarChart.tsx │ │ │ │ └── RadarChartNative.tsx │ │ │ ├── Tooltip │ │ │ │ ├── TooltipContent.module.scss │ │ │ │ ├── TooltipContent.spec.ts │ │ │ │ └── TooltipContent.tsx │ │ │ └── utils │ │ │ ├── abstractions.ts │ │ │ └── ChartProvider.tsx │ │ ├── Checkbox │ │ │ ├── Checkbox.md │ │ │ ├── Checkbox.spec.ts │ │ │ └── Checkbox.tsx │ │ ├── CodeBlock │ │ │ ├── CodeBlock.module.scss │ │ │ ├── CodeBlock.spec.ts │ │ │ ├── CodeBlock.tsx │ │ │ ├── CodeBlockNative.tsx │ │ │ └── highlight-code.ts │ │ ├── collectedComponentMetadata.ts │ │ ├── ColorPicker │ │ │ ├── ColorPicker.md │ │ │ ├── ColorPicker.module.scss │ │ │ ├── ColorPicker.spec.ts │ │ │ ├── ColorPicker.tsx │ │ │ └── ColorPickerNative.tsx │ │ ├── Column │ │ │ ├── Column.md │ │ │ ├── Column.tsx │ │ │ ├── ColumnNative.tsx │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ └── TableContext.tsx │ │ ├── component-utils.ts │ │ ├── ComponentProvider.tsx │ │ ├── ComponentRegistryContext.tsx │ │ ├── container-helpers.tsx │ │ ├── ContentSeparator │ │ │ ├── ContentSeparator.md │ │ │ ├── ContentSeparator.module.scss │ │ │ ├── ContentSeparator.spec.ts │ │ │ ├── ContentSeparator.tsx │ │ │ └── ContentSeparatorNative.tsx │ │ ├── DataSource │ │ │ ├── DataSource.md │ │ │ └── DataSource.tsx │ │ ├── DateInput │ │ │ ├── DateInput.md │ │ │ ├── DateInput.module.scss │ │ │ ├── DateInput.spec.ts │ │ │ ├── DateInput.tsx │ │ │ └── DateInputNative.tsx │ │ ├── DatePicker │ │ │ ├── DatePicker.md │ │ │ ├── DatePicker.module.scss │ │ │ ├── DatePicker.spec.ts │ │ │ ├── DatePicker.tsx │ │ │ └── DatePickerNative.tsx │ │ ├── DropdownMenu │ │ │ ├── DropdownMenu.md │ │ │ ├── DropdownMenu.module.scss │ │ │ ├── DropdownMenu.spec.ts │ │ │ ├── DropdownMenu.tsx │ │ │ ├── DropdownMenuNative.tsx │ │ │ ├── MenuItem.md │ │ │ └── SubMenuItem.md │ │ ├── EmojiSelector │ │ │ ├── EmojiSelector.md │ │ │ ├── EmojiSelector.spec.ts │ │ │ ├── EmojiSelector.tsx │ │ │ └── EmojiSelectorNative.tsx │ │ ├── ExpandableItem │ │ │ ├── ExpandableItem.module.scss │ │ │ ├── ExpandableItem.spec.ts │ │ │ ├── ExpandableItem.tsx │ │ │ └── ExpandableItemNative.tsx │ │ ├── FileInput │ │ │ ├── FileInput.md │ │ │ ├── FileInput.module.scss │ │ │ ├── FileInput.spec.ts │ │ │ ├── FileInput.tsx │ │ │ └── FileInputNative.tsx │ │ ├── FileUploadDropZone │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FileUploadDropZone.module.scss │ │ │ ├── FileUploadDropZone.spec.ts │ │ │ ├── FileUploadDropZone.tsx │ │ │ └── FileUploadDropZoneNative.tsx │ │ ├── FlowLayout │ │ │ ├── FlowLayout.md │ │ │ ├── FlowLayout.module.scss │ │ │ ├── FlowLayout.spec.ts │ │ │ ├── FlowLayout.spec.ts-snapshots │ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png │ │ │ ├── FlowLayout.tsx │ │ │ └── FlowLayoutNative.tsx │ │ ├── Footer │ │ │ ├── Footer.md │ │ │ ├── Footer.module.scss │ │ │ ├── Footer.spec.ts │ │ │ ├── Footer.tsx │ │ │ └── FooterNative.tsx │ │ ├── Form │ │ │ ├── Form.md │ │ │ ├── Form.module.scss │ │ │ ├── Form.spec.ts │ │ │ ├── Form.tsx │ │ │ ├── formActions.ts │ │ │ ├── FormContext.ts │ │ │ └── FormNative.tsx │ │ ├── FormItem │ │ │ ├── FormItem.md │ │ │ ├── FormItem.module.scss │ │ │ ├── FormItem.spec.ts │ │ │ ├── FormItem.tsx │ │ │ ├── FormItemNative.tsx │ │ │ ├── HelperText.module.scss │ │ │ ├── HelperText.tsx │ │ │ ├── ItemWithLabel.tsx │ │ │ └── Validations.ts │ │ ├── FormSection │ │ │ ├── FormSection.md │ │ │ ├── FormSection.ts │ │ │ └── FormSection.xmlui │ │ ├── Fragment │ │ │ ├── Fragment.spec.ts │ │ │ └── Fragment.tsx │ │ ├── Heading │ │ │ ├── abstractions.ts │ │ │ ├── H1.md │ │ │ ├── H1.spec.ts │ │ │ ├── H2.md │ │ │ ├── H2.spec.ts │ │ │ ├── H3.md │ │ │ ├── H3.spec.ts │ │ │ ├── H4.md │ │ │ ├── H4.spec.ts │ │ │ ├── H5.md │ │ │ ├── H5.spec.ts │ │ │ ├── H6.md │ │ │ ├── H6.spec.ts │ │ │ ├── Heading.md │ │ │ ├── Heading.module.scss │ │ │ ├── Heading.spec.ts │ │ │ ├── Heading.tsx │ │ │ └── HeadingNative.tsx │ │ ├── HoverCard │ │ │ ├── HoverCard.tsx │ │ │ └── HovercardNative.tsx │ │ ├── HtmlTags │ │ │ ├── HtmlTags.module.scss │ │ │ ├── HtmlTags.spec.ts │ │ │ └── HtmlTags.tsx │ │ ├── Icon │ │ │ ├── AdmonitionDanger.tsx │ │ │ ├── AdmonitionInfo.tsx │ │ │ ├── AdmonitionNote.tsx │ │ │ ├── AdmonitionTip.tsx │ │ │ ├── AdmonitionWarning.tsx │ │ │ ├── ApiIcon.tsx │ │ │ ├── ArrowDropDown.module.scss │ │ │ ├── ArrowDropDown.tsx │ │ │ ├── ArrowDropUp.module.scss │ │ │ ├── ArrowDropUp.tsx │ │ │ ├── ArrowLeft.module.scss │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowRight.module.scss │ │ │ ├── ArrowRight.tsx │ │ │ ├── Attach.tsx │ │ │ ├── Binding.module.scss │ │ │ ├── Binding.tsx │ │ │ ├── BoardIcon.tsx │ │ │ ├── BoxIcon.tsx │ │ │ ├── CheckIcon.tsx │ │ │ ├── ChevronDownIcon.tsx │ │ │ ├── ChevronLeft.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── ChevronUpIcon.tsx │ │ │ ├── CodeFileIcon.tsx │ │ │ ├── CodeSandbox.tsx │ │ │ ├── CompactListIcon.tsx │ │ │ ├── ContentCopyIcon.tsx │ │ │ ├── DarkToLightIcon.tsx │ │ │ ├── DatabaseIcon.module.scss │ │ │ ├── DatabaseIcon.tsx │ │ │ ├── DocFileIcon.tsx │ │ │ ├── DocIcon.tsx │ │ │ ├── DotMenuHorizontalIcon.tsx │ │ │ ├── DotMenuIcon.tsx │ │ │ ├── EmailIcon.tsx │ │ │ ├── EmptyFolderIcon.tsx │ │ │ ├── ErrorIcon.tsx │ │ │ ├── ExpressionIcon.tsx │ │ │ ├── FillPlusCricleIcon.tsx │ │ │ ├── FilterIcon.tsx │ │ │ ├── FolderIcon.tsx │ │ │ ├── GlobeIcon.tsx │ │ │ ├── HomeIcon.tsx │ │ │ ├── HyperLinkIcon.tsx │ │ │ ├── Icon.md │ │ │ ├── Icon.module.scss │ │ │ ├── Icon.spec.ts │ │ │ ├── Icon.tsx │ │ │ ├── IconNative.tsx │ │ │ ├── ImageFileIcon.tsx │ │ │ ├── Inspect.tsx │ │ │ ├── LightToDark.tsx │ │ │ ├── LinkIcon.tsx │ │ │ ├── ListIcon.tsx │ │ │ ├── LooseListIcon.tsx │ │ │ ├── MoonIcon.tsx │ │ │ ├── MoreOptionsIcon.tsx │ │ │ ├── NoSortIcon.tsx │ │ │ ├── PDFIcon.tsx │ │ │ ├── PenIcon.tsx │ │ │ ├── PhoneIcon.tsx │ │ │ ├── PhotoIcon.tsx │ │ │ ├── PlusIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── ShareIcon.tsx │ │ │ ├── SortAscendingIcon.tsx │ │ │ ├── SortDescendingIcon.tsx │ │ │ ├── StarsIcon.tsx │ │ │ ├── SunIcon.tsx │ │ │ ├── svg │ │ │ │ ├── admonition_danger.svg │ │ │ │ ├── admonition_info.svg │ │ │ │ ├── admonition_note.svg │ │ │ │ ├── admonition_tip.svg │ │ │ │ ├── admonition_warning.svg │ │ │ │ ├── api.svg │ │ │ │ ├── arrow-dropdown.svg │ │ │ │ ├── arrow-left.svg │ │ │ │ ├── arrow-right.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── attach.svg │ │ │ │ ├── binding.svg │ │ │ │ ├── box.svg │ │ │ │ ├── bulb.svg │ │ │ │ ├── code-file.svg │ │ │ │ ├── code-sandbox.svg │ │ │ │ ├── dark_to_light.svg │ │ │ │ ├── database.svg │ │ │ │ ├── doc.svg │ │ │ │ ├── empty-folder.svg │ │ │ │ ├── expression.svg │ │ │ │ ├── eye-closed.svg │ │ │ │ ├── eye-dark.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── file-text.svg │ │ │ │ ├── filter.svg │ │ │ │ ├── folder.svg │ │ │ │ ├── img.svg │ │ │ │ ├── inspect.svg │ │ │ │ ├── light_to_dark.svg │ │ │ │ ├── moon.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── photo.svg │ │ │ │ ├── share.svg │ │ │ │ ├── stars.svg │ │ │ │ ├── sun.svg │ │ │ │ ├── trending-down.svg │ │ │ │ ├── trending-level.svg │ │ │ │ ├── trending-up.svg │ │ │ │ ├── txt.svg │ │ │ │ ├── unknown-file.svg │ │ │ │ ├── unlink.svg │ │ │ │ └── xls.svg │ │ │ ├── TableDeleteColumnIcon.tsx │ │ │ ├── TableDeleteRowIcon.tsx │ │ │ ├── TableInsertColumnIcon.tsx │ │ │ ├── TableInsertRowIcon.tsx │ │ │ ├── TrashIcon.tsx │ │ │ ├── TrendingDownIcon.tsx │ │ │ ├── TrendingLevelIcon.tsx │ │ │ ├── TrendingUpIcon.tsx │ │ │ ├── TxtIcon.tsx │ │ │ ├── UnknownFileIcon.tsx │ │ │ ├── UnlinkIcon.tsx │ │ │ ├── UserIcon.tsx │ │ │ ├── WarningIcon.tsx │ │ │ └── XlsIcon.tsx │ │ ├── IconProvider.tsx │ │ ├── IconRegistryContext.tsx │ │ ├── IFrame │ │ │ ├── IFrame.md │ │ │ ├── IFrame.module.scss │ │ │ ├── IFrame.spec.ts │ │ │ ├── IFrame.tsx │ │ │ └── IFrameNative.tsx │ │ ├── Image │ │ │ ├── Image.md │ │ │ ├── Image.module.scss │ │ │ ├── Image.spec.ts │ │ │ ├── Image.tsx │ │ │ └── ImageNative.tsx │ │ ├── Input │ │ │ ├── index.ts │ │ │ ├── InputAdornment.module.scss │ │ │ ├── InputAdornment.tsx │ │ │ ├── InputDivider.module.scss │ │ │ ├── InputDivider.tsx │ │ │ ├── InputLabel.module.scss │ │ │ ├── InputLabel.tsx │ │ │ ├── PartialInput.module.scss │ │ │ └── PartialInput.tsx │ │ ├── InspectButton │ │ │ ├── InspectButton.module.scss │ │ │ └── InspectButton.tsx │ │ ├── Items │ │ │ ├── Items.md │ │ │ ├── Items.spec.ts │ │ │ ├── Items.tsx │ │ │ └── ItemsNative.tsx │ │ ├── Link │ │ │ ├── Link.md │ │ │ ├── Link.module.scss │ │ │ ├── Link.spec.ts │ │ │ ├── Link.tsx │ │ │ └── LinkNative.tsx │ │ ├── List │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── List.md │ │ │ ├── List.module.scss │ │ │ ├── List.spec.ts │ │ │ ├── List.tsx │ │ │ └── ListNative.tsx │ │ ├── Logo │ │ │ ├── doc-resources │ │ │ │ └── xmlui-logo.svg │ │ │ ├── Logo.md │ │ │ ├── Logo.tsx │ │ │ └── LogoNative.tsx │ │ ├── Markdown │ │ │ ├── CodeText.module.scss │ │ │ ├── CodeText.tsx │ │ │ ├── Markdown.md │ │ │ ├── Markdown.module.scss │ │ │ ├── Markdown.spec.ts │ │ │ ├── Markdown.tsx │ │ │ ├── MarkdownNative.tsx │ │ │ ├── parse-binding-expr.ts │ │ │ └── utils.ts │ │ ├── metadata-helpers.ts │ │ ├── ModalDialog │ │ │ ├── ConfirmationModalContextProvider.tsx │ │ │ ├── Dialog.module.scss │ │ │ ├── Dialog.tsx │ │ │ ├── ModalDialog.md │ │ │ ├── ModalDialog.module.scss │ │ │ ├── ModalDialog.spec.ts │ │ │ ├── ModalDialog.tsx │ │ │ ├── ModalDialogNative.tsx │ │ │ └── ModalVisibilityContext.tsx │ │ ├── NavGroup │ │ │ ├── NavGroup.md │ │ │ ├── NavGroup.module.scss │ │ │ ├── NavGroup.spec.ts │ │ │ ├── NavGroup.tsx │ │ │ ├── NavGroupContext.ts │ │ │ └── NavGroupNative.tsx │ │ ├── NavLink │ │ │ ├── NavLink.md │ │ │ ├── NavLink.module.scss │ │ │ ├── NavLink.spec.ts │ │ │ ├── NavLink.tsx │ │ │ └── NavLinkNative.tsx │ │ ├── NavPanel │ │ │ ├── NavPanel.md │ │ │ ├── NavPanel.module.scss │ │ │ ├── NavPanel.spec.ts │ │ │ ├── NavPanel.tsx │ │ │ └── NavPanelNative.tsx │ │ ├── NestedApp │ │ │ ├── AppWithCodeView.module.scss │ │ │ ├── AppWithCodeView.tsx │ │ │ ├── AppWithCodeViewNative.tsx │ │ │ ├── defaultProps.tsx │ │ │ ├── logo.svg │ │ │ ├── NestedApp.module.scss │ │ │ ├── NestedApp.tsx │ │ │ ├── NestedAppNative.tsx │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.tsx │ │ │ └── utils.ts │ │ ├── NoResult │ │ │ ├── NoResult.md │ │ │ ├── NoResult.module.scss │ │ │ ├── NoResult.spec.ts │ │ │ ├── NoResult.tsx │ │ │ └── NoResultNative.tsx │ │ ├── NumberBox │ │ │ ├── numberbox-abstractions.ts │ │ │ ├── NumberBox.md │ │ │ ├── NumberBox.module.scss │ │ │ ├── NumberBox.spec.ts │ │ │ ├── NumberBox.tsx │ │ │ └── NumberBoxNative.tsx │ │ ├── Option │ │ │ ├── Option.md │ │ │ ├── Option.spec.ts │ │ │ ├── Option.tsx │ │ │ ├── OptionNative.tsx │ │ │ └── OptionTypeProvider.tsx │ │ ├── PageMetaTitle │ │ │ ├── PageMetaTilteNative.tsx │ │ │ ├── PageMetaTitle.md │ │ │ ├── PageMetaTitle.spec.ts │ │ │ └── PageMetaTitle.tsx │ │ ├── Pages │ │ │ ├── Page.md │ │ │ ├── Pages.md │ │ │ ├── Pages.module.scss │ │ │ ├── Pages.tsx │ │ │ └── PagesNative.tsx │ │ ├── Pagination │ │ │ ├── Pagination.md │ │ │ ├── Pagination.module.scss │ │ │ ├── Pagination.spec.ts │ │ │ ├── Pagination.tsx │ │ │ └── PaginationNative.tsx │ │ ├── PositionedContainer │ │ │ ├── PositionedContainer.module.scss │ │ │ ├── PositionedContainer.tsx │ │ │ └── PositionedContainerNative.tsx │ │ ├── ProfileMenu │ │ │ ├── ProfileMenu.module.scss │ │ │ └── ProfileMenu.tsx │ │ ├── ProgressBar │ │ │ ├── ProgressBar.md │ │ │ ├── ProgressBar.module.scss │ │ │ ├── ProgressBar.spec.ts │ │ │ ├── ProgressBar.tsx │ │ │ └── ProgressBarNative.tsx │ │ ├── Queue │ │ │ ├── Queue.md │ │ │ ├── Queue.spec.ts │ │ │ ├── Queue.tsx │ │ │ ├── queueActions.ts │ │ │ └── QueueNative.tsx │ │ ├── RadioGroup │ │ │ ├── RadioGroup.md │ │ │ ├── RadioGroup.module.scss │ │ │ ├── RadioGroup.spec.ts │ │ │ ├── RadioGroup.tsx │ │ │ ├── RadioGroupNative.tsx │ │ │ ├── RadioItem.tsx │ │ │ └── RadioItemNative.tsx │ │ ├── RealTimeAdapter │ │ │ ├── RealTimeAdapter.tsx │ │ │ └── RealTimeAdapterNative.tsx │ │ ├── Redirect │ │ │ ├── Redirect.md │ │ │ ├── Redirect.spec.ts │ │ │ └── Redirect.tsx │ │ ├── ResponsiveBar │ │ │ ├── README.md │ │ │ ├── ResponsiveBar.md │ │ │ ├── ResponsiveBar.module.scss │ │ │ ├── ResponsiveBar.spec.ts │ │ │ ├── ResponsiveBar.tsx │ │ │ └── ResponsiveBarNative.tsx │ │ ├── Select │ │ │ ├── HiddenOption.tsx │ │ │ ├── MultiSelectOption.tsx │ │ │ ├── OptionContext.ts │ │ │ ├── Select.md │ │ │ ├── Select.module.scss │ │ │ ├── Select.spec.ts │ │ │ ├── Select.tsx │ │ │ ├── SelectContext.tsx │ │ │ ├── SelectNative.tsx │ │ │ ├── SelectOption.tsx │ │ │ └── SimpleSelect.tsx │ │ ├── SelectionStore │ │ │ ├── SelectionStore.md │ │ │ ├── SelectionStore.tsx │ │ │ └── SelectionStoreNative.tsx │ │ ├── Slider │ │ │ ├── Slider.md │ │ │ ├── Slider.module.scss │ │ │ ├── Slider.spec.ts │ │ │ ├── Slider.tsx │ │ │ └── SliderNative.tsx │ │ ├── Slot │ │ │ ├── Slot.md │ │ │ ├── Slot.spec.ts │ │ │ └── Slot.ts │ │ ├── SlotItem.tsx │ │ ├── SpaceFiller │ │ │ ├── SpaceFiller.md │ │ │ ├── SpaceFiller.module.scss │ │ │ ├── SpaceFiller.spec.ts │ │ │ ├── SpaceFiller.tsx │ │ │ └── SpaceFillerNative.tsx │ │ ├── Spinner │ │ │ ├── Spinner.md │ │ │ ├── Spinner.module.scss │ │ │ ├── Spinner.spec.ts │ │ │ ├── Spinner.tsx │ │ │ └── SpinnerNative.tsx │ │ ├── Splitter │ │ │ ├── HSplitter.md │ │ │ ├── HSplitter.spec.ts │ │ │ ├── Splitter.md │ │ │ ├── Splitter.module.scss │ │ │ ├── Splitter.spec.ts │ │ │ ├── Splitter.tsx │ │ │ ├── SplitterNative.tsx │ │ │ ├── utils.ts │ │ │ ├── VSplitter.md │ │ │ └── VSplitter.spec.ts │ │ ├── Stack │ │ │ ├── CHStack.md │ │ │ ├── CHStack.spec.ts │ │ │ ├── CVStack.md │ │ │ ├── CVStack.spec.ts │ │ │ ├── HStack.md │ │ │ ├── HStack.spec.ts │ │ │ ├── Stack.md │ │ │ ├── Stack.module.scss │ │ │ ├── Stack.spec.ts │ │ │ ├── Stack.tsx │ │ │ ├── StackNative.tsx │ │ │ ├── VStack.md │ │ │ └── VStack.spec.ts │ │ ├── StickyBox │ │ │ ├── StickyBox.md │ │ │ ├── StickyBox.module.scss │ │ │ ├── StickyBox.tsx │ │ │ └── StickyBoxNative.tsx │ │ ├── Switch │ │ │ ├── Switch.md │ │ │ ├── Switch.spec.ts │ │ │ └── Switch.tsx │ │ ├── Table │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── react-table-config.d.ts │ │ │ ├── Table.md │ │ │ ├── Table.module.scss │ │ │ ├── Table.spec.ts │ │ │ ├── Table.tsx │ │ │ ├── TableNative.tsx │ │ │ └── useRowSelection.tsx │ │ ├── TableOfContents │ │ │ ├── TableOfContents.module.scss │ │ │ ├── TableOfContents.spec.ts │ │ │ ├── TableOfContents.tsx │ │ │ └── TableOfContentsNative.tsx │ │ ├── Tabs │ │ │ ├── TabContext.tsx │ │ │ ├── TabItem.md │ │ │ ├── TabItem.tsx │ │ │ ├── TabItemNative.tsx │ │ │ ├── Tabs.md │ │ │ ├── Tabs.module.scss │ │ │ ├── Tabs.spec.ts │ │ │ ├── Tabs.tsx │ │ │ └── TabsNative.tsx │ │ ├── Text │ │ │ ├── Text.md │ │ │ ├── Text.module.scss │ │ │ ├── Text.spec.ts │ │ │ ├── Text.tsx │ │ │ └── TextNative.tsx │ │ ├── TextArea │ │ │ ├── TextArea.md │ │ │ ├── TextArea.module.scss │ │ │ ├── TextArea.spec.ts │ │ │ ├── TextArea.tsx │ │ │ ├── TextAreaNative.tsx │ │ │ ├── TextAreaResizable.tsx │ │ │ └── useComposedRef.ts │ │ ├── TextBox │ │ │ ├── TextBox.md │ │ │ ├── TextBox.module.scss │ │ │ ├── TextBox.spec.ts │ │ │ ├── TextBox.tsx │ │ │ └── TextBoxNative.tsx │ │ ├── Theme │ │ │ ├── NotificationToast.tsx │ │ │ ├── Theme.md │ │ │ ├── Theme.module.scss │ │ │ ├── Theme.spec.ts │ │ │ ├── Theme.tsx │ │ │ └── ThemeNative.tsx │ │ ├── TimeInput │ │ │ ├── TimeInput.md │ │ │ ├── TimeInput.module.scss │ │ │ ├── TimeInput.spec.ts │ │ │ ├── TimeInput.tsx │ │ │ ├── TimeInputNative.tsx │ │ │ └── utils.ts │ │ ├── Timer │ │ │ ├── Timer.md │ │ │ ├── Timer.spec.ts │ │ │ ├── Timer.tsx │ │ │ └── TimerNative.tsx │ │ ├── Toggle │ │ │ ├── Toggle.module.scss │ │ │ └── Toggle.tsx │ │ ├── ToneChangerButton │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneChangerButton.spec.ts │ │ │ └── ToneChangerButton.tsx │ │ ├── ToneSwitch │ │ │ ├── ToneSwitch.md │ │ │ ├── ToneSwitch.module.scss │ │ │ ├── ToneSwitch.spec.ts │ │ │ ├── ToneSwitch.tsx │ │ │ └── ToneSwitchNative.tsx │ │ ├── Tooltip │ │ │ ├── Tooltip.md │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.spec.ts │ │ │ ├── Tooltip.tsx │ │ │ └── TooltipNative.tsx │ │ ├── Tree │ │ │ ├── testData.ts │ │ │ ├── Tree-dynamic.spec.ts │ │ │ ├── Tree-icons.spec.ts │ │ │ ├── Tree.md │ │ │ ├── Tree.spec.ts │ │ │ ├── TreeComponent.module.scss │ │ │ ├── TreeComponent.tsx │ │ │ └── TreeNative.tsx │ │ ├── TreeDisplay │ │ │ ├── TreeDisplay.md │ │ │ ├── TreeDisplay.module.scss │ │ │ ├── TreeDisplay.tsx │ │ │ └── TreeDisplayNative.tsx │ │ ├── ValidationSummary │ │ │ ├── ValidationSummary.module.scss │ │ │ └── ValidationSummary.tsx │ │ └── VisuallyHidden.tsx │ ├── components-core │ │ ├── abstractions │ │ │ ├── ComponentRenderer.ts │ │ │ ├── LoaderRenderer.ts │ │ │ ├── standalone.ts │ │ │ └── treeAbstractions.ts │ │ ├── action │ │ │ ├── actions.ts │ │ │ ├── APICall.tsx │ │ │ ├── FileDownloadAction.tsx │ │ │ ├── FileUploadAction.tsx │ │ │ ├── NavigateAction.tsx │ │ │ └── TimedAction.tsx │ │ ├── ApiBoundComponent.tsx │ │ ├── appContext │ │ │ ├── date-functions.ts │ │ │ ├── math-function.ts │ │ │ └── misc-utils.ts │ │ ├── AppContext.tsx │ │ ├── behaviors │ │ │ ├── Behavior.tsx │ │ │ └── CoreBehaviors.tsx │ │ ├── component-hooks.ts │ │ ├── ComponentDecorator.tsx │ │ ├── ComponentViewer.tsx │ │ ├── CompoundComponent.tsx │ │ ├── constants.ts │ │ ├── DebugViewProvider.tsx │ │ ├── descriptorHelper.ts │ │ ├── devtools │ │ │ ├── InspectorDialog.module.scss │ │ │ ├── InspectorDialog.tsx │ │ │ └── InspectorDialogVisibilityContext.tsx │ │ ├── EngineError.ts │ │ ├── event-handlers.ts │ │ ├── InspectorButton.module.scss │ │ ├── InspectorContext.tsx │ │ ├── interception │ │ │ ├── abstractions.ts │ │ │ ├── ApiInterceptor.ts │ │ │ ├── ApiInterceptorProvider.tsx │ │ │ ├── apiInterceptorWorker.ts │ │ │ ├── Backend.ts │ │ │ ├── Errors.ts │ │ │ ├── IndexedDb.ts │ │ │ ├── initMock.ts │ │ │ ├── InMemoryDb.ts │ │ │ ├── ReadonlyCollection.ts │ │ │ └── useApiInterceptorContext.tsx │ │ ├── loader │ │ │ ├── ApiLoader.tsx │ │ │ ├── DataLoader.tsx │ │ │ ├── ExternalDataLoader.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MockLoaderRenderer.tsx │ │ │ └── PageableLoader.tsx │ │ ├── LoaderComponent.tsx │ │ ├── markup-check.ts │ │ ├── parts.ts │ │ ├── renderers.ts │ │ ├── rendering │ │ │ ├── AppContent.tsx │ │ │ ├── AppRoot.tsx │ │ │ ├── AppWrapper.tsx │ │ │ ├── buildProxy.ts │ │ │ ├── collectFnVarDeps.ts │ │ │ ├── ComponentAdapter.tsx │ │ │ ├── ComponentWrapper.tsx │ │ │ ├── Container.tsx │ │ │ ├── containers.ts │ │ │ ├── ContainerWrapper.tsx │ │ │ ├── ErrorBoundary.module.scss │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── InvalidComponent.module.scss │ │ │ ├── InvalidComponent.tsx │ │ │ ├── nodeUtils.ts │ │ │ ├── reducer.ts │ │ │ ├── renderChild.tsx │ │ │ ├── StandaloneComponent.tsx │ │ │ ├── StateContainer.tsx │ │ │ ├── UnknownComponent.module.scss │ │ │ ├── UnknownComponent.tsx │ │ │ └── valueExtractor.ts │ │ ├── reportEngineError.ts │ │ ├── RestApiProxy.ts │ │ ├── script-runner │ │ │ ├── asyncProxy.ts │ │ │ ├── AttributeValueParser.ts │ │ │ ├── bannedFunctions.ts │ │ │ ├── BindingTreeEvaluationContext.ts │ │ │ ├── eval-tree-async.ts │ │ │ ├── eval-tree-common.ts │ │ │ ├── eval-tree-sync.ts │ │ │ ├── ParameterParser.ts │ │ │ ├── process-statement-async.ts │ │ │ ├── process-statement-common.ts │ │ │ ├── process-statement-sync.ts │ │ │ ├── ScriptingSourceTree.ts │ │ │ ├── simplify-expression.ts │ │ │ ├── statement-queue.ts │ │ │ └── visitors.ts │ │ ├── StandaloneApp.tsx │ │ ├── StandaloneExtensionManager.ts │ │ ├── TableOfContentsContext.tsx │ │ ├── theming │ │ │ ├── _themes.scss │ │ │ ├── component-layout-resolver.ts │ │ │ ├── extendThemeUtils.ts │ │ │ ├── hvar.ts │ │ │ ├── layout-resolver.ts │ │ │ ├── parse-layout-props.ts │ │ │ ├── StyleContext.tsx │ │ │ ├── StyleRegistry.ts │ │ │ ├── ThemeContext.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ ├── themes │ │ │ │ ├── base-utils.ts │ │ │ │ ├── palette.ts │ │ │ │ ├── root.ts │ │ │ │ ├── solid.ts │ │ │ │ ├── theme-colors.ts │ │ │ │ └── xmlui.ts │ │ │ ├── themeVars.module.scss │ │ │ ├── themeVars.ts │ │ │ ├── transformThemeVars.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── actionUtils.ts │ │ │ ├── audio-utils.ts │ │ │ ├── compound-utils.ts │ │ │ ├── css-utils.ts │ │ │ ├── DataLoaderQueryKeyGenerator.ts │ │ │ ├── date-utils.ts │ │ │ ├── extractParam.ts │ │ │ ├── hooks.tsx │ │ │ ├── LruCache.ts │ │ │ ├── mergeProps.ts │ │ │ ├── misc.ts │ │ │ ├── request-params.ts │ │ │ ├── statementUtils.ts │ │ │ └── treeUtils.ts │ │ └── xmlui-parser.ts │ ├── index-standalone.ts │ ├── index.scss │ ├── index.ts │ ├── language-server │ │ ├── server-common.ts │ │ ├── server-web-worker.ts │ │ ├── server.ts │ │ ├── services │ │ │ ├── common │ │ │ │ ├── docs-generation.ts │ │ │ │ ├── lsp-utils.ts │ │ │ │ ├── metadata-utils.ts │ │ │ │ └── syntax-node-utilities.ts │ │ │ ├── completion.ts │ │ │ ├── diagnostic.ts │ │ │ ├── format.ts │ │ │ └── hover.ts │ │ └── xmlui-metadata-generated.mjs │ ├── logging │ │ ├── LoggerContext.tsx │ │ ├── LoggerInitializer.tsx │ │ ├── LoggerService.ts │ │ └── xmlui.ts │ ├── logo.svg │ ├── parsers │ │ ├── common │ │ │ ├── GenericToken.ts │ │ │ ├── InputStream.ts │ │ │ └── utils.ts │ │ ├── scripting │ │ │ ├── code-behind-collect.ts │ │ │ ├── Lexer.ts │ │ │ ├── modules.ts │ │ │ ├── Parser.ts │ │ │ ├── ParserError.ts │ │ │ ├── ScriptingNodeTypes.ts │ │ │ ├── TokenTrait.ts │ │ │ ├── TokenType.ts │ │ │ └── tree-visitor.ts │ │ ├── style-parser │ │ │ ├── errors.ts │ │ │ ├── source-tree.ts │ │ │ ├── StyleInputStream.ts │ │ │ ├── StyleLexer.ts │ │ │ ├── StyleParser.ts │ │ │ └── tokens.ts │ │ └── xmlui-parser │ │ ├── CharacterCodes.ts │ │ ├── diagnostics.ts │ │ ├── fileExtensions.ts │ │ ├── index.ts │ │ ├── lint.ts │ │ ├── parser.ts │ │ ├── ParserError.ts │ │ ├── scanner.ts │ │ ├── syntax-kind.ts │ │ ├── syntax-node.ts │ │ ├── transform.ts │ │ ├── utils.ts │ │ ├── xmlui-serializer.ts │ │ └── xmlui-tree.ts │ ├── react-app-env.d.ts │ ├── syntax │ │ ├── monaco │ │ │ ├── grammar.monacoLanguage.ts │ │ │ ├── index.ts │ │ │ ├── xmlui-dark.ts │ │ │ ├── xmlui-light.ts │ │ │ └── xmluiscript.monacoLanguage.ts │ │ └── textMate │ │ ├── index.ts │ │ ├── xmlui-dark.json │ │ ├── xmlui-light.json │ │ ├── xmlui.json │ │ └── xmlui.tmLanguage.json │ ├── testing │ │ ├── assertions.ts │ │ ├── component-test-helpers.ts │ │ ├── ComponentDrivers.ts │ │ ├── drivers │ │ │ ├── DateInputDriver.ts │ │ │ ├── ModalDialogDriver.ts │ │ │ ├── NumberBoxDriver.ts │ │ │ ├── TextBoxDriver.ts │ │ │ ├── TimeInputDriver.ts │ │ │ ├── TimerDriver.ts │ │ │ └── TreeDriver.ts │ │ ├── fixtures.ts │ │ ├── infrastructure │ │ │ ├── index.html │ │ │ ├── main.tsx │ │ │ ├── public │ │ │ │ ├── mockServiceWorker.js │ │ │ │ ├── resources │ │ │ │ │ ├── bell.svg │ │ │ │ │ ├── box.svg │ │ │ │ │ ├── doc.svg │ │ │ │ │ ├── eye.svg │ │ │ │ │ ├── flower-640x480.jpg │ │ │ │ │ ├── sun.svg │ │ │ │ │ ├── test-image-100x100.jpg │ │ │ │ │ └── txt.svg │ │ │ │ └── serve.json │ │ │ └── TestBed.tsx │ │ └── themed-app-test-helpers.ts │ └── vite-env.d.ts ├── tests │ ├── components │ │ ├── CodeBlock │ │ │ └── hightlight-code.test.ts │ │ ├── playground-pattern.test.ts │ │ └── Tree │ │ └── Tree-states.test.ts │ ├── components-core │ │ ├── abstractions │ │ │ └── treeAbstractions.test.ts │ │ ├── container │ │ │ └── buildProxy.test.ts │ │ ├── interception │ │ │ ├── orderBy.test.ts │ │ │ ├── ReadOnlyCollection.test.ts │ │ │ └── request-param-converter.test.ts │ │ ├── scripts-runner │ │ │ ├── AttributeValueParser.test.ts │ │ │ ├── eval-tree-arrow-async.test.ts │ │ │ ├── eval-tree-arrow.test.ts │ │ │ ├── eval-tree-func-decl-async.test.ts │ │ │ ├── eval-tree-func-decl.test.ts │ │ │ ├── eval-tree-pre-post.test.ts │ │ │ ├── eval-tree-regression.test.ts │ │ │ ├── eval-tree.test.ts │ │ │ ├── function-proxy.test.ts │ │ │ ├── parser-regression.test.ts │ │ │ ├── process-event.test.ts │ │ │ ├── process-function.test.ts │ │ │ ├── process-implicit-context.test.ts │ │ │ ├── process-statement-asgn.test.ts │ │ │ ├── process-statement-destruct.test.ts │ │ │ ├── process-statement-regs.test.ts │ │ │ ├── process-statement-sync.test.ts │ │ │ ├── process-statement.test.ts │ │ │ ├── process-switch-sync.test.ts │ │ │ ├── process-switch.test.ts │ │ │ ├── process-try-sync.test.ts │ │ │ ├── process-try.test.ts │ │ │ └── test-helpers.ts │ │ ├── test-metadata-handler.ts │ │ ├── theming │ │ │ ├── border-segments.test.ts │ │ │ ├── component-layout.resolver.test.ts │ │ │ ├── layout-property-parser.test.ts │ │ │ ├── layout-resolver.test.ts │ │ │ ├── layout-resolver2.test.ts │ │ │ ├── layout-vp-override.test.ts │ │ │ └── padding-segments.test.ts │ │ └── utils │ │ ├── date-utils.test.ts │ │ ├── format-human-elapsed-time.test.ts │ │ └── LruCache.test.ts │ ├── language-server │ │ ├── completion.test.ts │ │ ├── format.test.ts │ │ ├── hover.test.ts │ │ └── mockData.ts │ └── parsers │ ├── common │ │ └── input-stream.test.ts │ ├── markdown │ │ └── parse-binding-expression.test.ts │ ├── parameter-parser.test.ts │ ├── paremeter-parser.test.ts │ ├── scripting │ │ ├── eval-tree-arrow.test.ts │ │ ├── eval-tree-pre-post.test.ts │ │ ├── eval-tree.test.ts │ │ ├── function-proxy.test.ts │ │ ├── lexer-literals.test.ts │ │ ├── lexer-misc.test.ts │ │ ├── module-parse.test.ts │ │ ├── parser-arrow.test.ts │ │ ├── parser-assignments.test.ts │ │ ├── parser-binary.test.ts │ │ ├── parser-destructuring.test.ts │ │ ├── parser-errors.test.ts │ │ ├── parser-expressions.test.ts │ │ ├── parser-function.test.ts │ │ ├── parser-literals.test.ts │ │ ├── parser-primary.test.ts │ │ ├── parser-regex.test.ts │ │ ├── parser-statements.test.ts │ │ ├── parser-unary.test.ts │ │ ├── process-event.test.ts │ │ ├── process-implicit-context.test.ts │ │ ├── process-statement-asgn.test.ts │ │ ├── process-statement-destruct.test.ts │ │ ├── process-statement-regs.test.ts │ │ ├── process-statement-sync.test.ts │ │ ├── process-statement.test.ts │ │ ├── process-switch-sync.test.ts │ │ ├── process-switch.test.ts │ │ ├── process-try-sync.test.ts │ │ ├── process-try.test.ts │ │ ├── simplify-expression.test.ts │ │ ├── statement-hooks.test.ts │ │ └── test-helpers.ts │ ├── style-parser │ │ ├── generateHvarChain.test.ts │ │ ├── parseHVar.test.ts │ │ ├── parser.test.ts │ │ └── tokens.test.ts │ └── xmlui │ ├── lint.test.ts │ ├── parser.test.ts │ ├── scanner.test.ts │ ├── transform.attr.test.ts │ ├── transform.circular.test.ts │ ├── transform.element.test.ts │ ├── transform.errors.test.ts │ ├── transform.escape.test.ts │ ├── transform.regression.test.ts │ ├── transform.script.test.ts │ ├── transform.test.ts │ └── xmlui.ts ├── tests-e2e │ ├── api-bound-component-regression.spec.ts │ ├── api-call-as-extracted-component.spec.ts │ ├── assign-to-object-or-array-regression.spec.ts │ ├── binding-regression.spec.ts │ ├── children-as-template-context-vars.spec.ts │ ├── compound-component.spec.ts │ ├── context-vars-regression.spec.ts │ ├── data-bindings.spec.ts │ ├── datasource-and-api-usage-in-var.spec.ts │ ├── datasource-direct-binding.spec.ts │ ├── datasource-onLoaded-regression.spec.ts │ ├── modify-array-item-regression.spec.ts │ ├── namespaces.spec.ts │ ├── push-to-array-regression.spec.ts │ ├── screen-breakpoints.spec.ts │ ├── scripting.spec.ts │ ├── state-scope-in-pages.spec.ts │ └── state-var-scopes.spec.ts ├── tsconfig.bin.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /xmlui/src/components-core/devtools/InspectorDialog.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "xmlui/themes.scss" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $component: "ModalDialog"; 5 | $themeVars: (); 6 | @function createThemeVar($componentVariable) { 7 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 8 | @return t.getThemeVar($themeVars, $componentVariable); 9 | } 10 | 11 | // --- Theme vars for paddings 12 | $themeVars: t.composePaddingVars($themeVars, $component); 13 | $themeVars: t.composePaddingVars($themeVars, "overlay-#{$component}"); 14 | $padding-ModalDialog: createThemeVar("padding-#{$component}"); 15 | $backgroundColor-ModalDialog: createThemeVar("Dialog:backgroundColor-#{$component}"); 16 | $backgroundColor-overlay-ModalDialog: createThemeVar("Dialog:backgroundColor-overlay-#{$component}"); 17 | $borderRadius-ModalDialog: createThemeVar("Dialog:borderRadius-#{$component}"); 18 | $fontFamily-ModalDialog: createThemeVar("Dialog:fontFamily-#{$component}"); 19 | $textColor-ModalDialog: createThemeVar("Dialog:textColor-#{$component}"); 20 | $minWidth-ModalDialog: createThemeVar("Dialog:minWidth-#{$component}"); 21 | $maxWidth-ModalDialog: createThemeVar("Dialog:maxWidth-#{$component}"); 22 | $marginBottom-title-ModalDialog: createThemeVar("Dialog:marginBottom-title-#{$component}"); 23 | 24 | 25 | .overlay { 26 | position: fixed; 27 | overflow-y: auto; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | inset: 0; 32 | background-color: $backgroundColor-overlay-ModalDialog; 33 | padding: t.$space-4; 34 | } 35 | 36 | .overlayBg { 37 | background-color: $backgroundColor-overlay-ModalDialog; 38 | position: fixed; 39 | inset: 0; 40 | } 41 | 42 | .contentWrapper { 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | width: 100%; 47 | height: 100%; 48 | } 49 | 50 | .content { 51 | border-radius: $borderRadius-ModalDialog; 52 | font-family: $fontFamily-ModalDialog; 53 | color: $textColor-ModalDialog; 54 | width: 90vw; 55 | max-width: 960px; 56 | min-width: 240px; 57 | isolation: isolate; 58 | position: relative; 59 | display: flex; 60 | flex-direction: column; 61 | max-height: 80vh; 62 | overflow-y: auto; 63 | } 64 | 65 | .content:focus { 66 | outline: none; 67 | } 68 | 69 | .dialogTitle { 70 | flex: 1; 71 | margin-bottom: $marginBottom-title-ModalDialog; 72 | font-size: t.$fontSize-2xl; 73 | } 74 | 75 | .innerContent { 76 | display: flex; 77 | flex-direction: column; 78 | min-height: 0; 79 | gap: var(--stack-gap-default); 80 | flex: 1; 81 | } 82 | 83 | .closeButton { 84 | position: absolute; 85 | right: 0.5rem; 86 | top: 0.4rem; 87 | } 88 | 89 | .actions { 90 | display: inline-flex; 91 | align-items: center; 92 | justify-content: flex-end; 93 | } 94 | 95 | .header { 96 | padding: t.$space-2; 97 | justify-content: space-between; 98 | display: flex; 99 | flex-direction: row; 100 | border-bottom: 1px solid t.$borderColor; 101 | } 102 | 103 | @media (max-width: 70em) { 104 | .dialog, .content { 105 | max-width: 90%; 106 | } 107 | } 108 | 109 | @media (max-width: 50em) { 110 | .dialog, .content { 111 | width: 100%; 112 | max-width: calc(100% - #{t.$space-6}); 113 | min-width: 0 !important; 114 | } 115 | } 116 | 117 | // --- We export the theme variables to add them to the component renderer 118 | :export { 119 | themeVars: t.json-stringify($themeVars); 120 | } 121 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/action/FileDownloadAction.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type { ComponentDef } from "../../abstractions/ComponentDefs"; 2 | import type { ActionExecutionContext } from "../../abstractions/ActionDefs"; 3 | import type { ApiActionOptions, DownloadOperationDef } from "../RestApiProxy"; 4 | import RestApiProxy from "../RestApiProxy"; 5 | 6 | import { createAction } from "./actions"; 7 | 8 | export interface DownloadActionComponent extends ComponentDef { 9 | props: DownloadOperationDef; 10 | } 11 | 12 | async function download( 13 | { state, appContext }: ActionExecutionContext, 14 | { 15 | params, 16 | url, 17 | queryParams, 18 | method, 19 | rawBody, 20 | body, 21 | fileName, 22 | headers, 23 | }: { 24 | params: any; 25 | } & DownloadOperationDef, 26 | { resolveBindingExpressions }: ApiActionOptions = {} 27 | ) { 28 | const context = { ...params, ...state }; 29 | const operation: DownloadOperationDef = { 30 | url, 31 | queryParams, 32 | method, 33 | rawBody, 34 | body, 35 | fileName, 36 | headers, 37 | }; 38 | 39 | const api = new RestApiProxy(appContext); 40 | const _url = api.resolveUrl({ operation, params: context, resolveBindingExpressions }); 41 | 42 | if ( 43 | (operation.method && (operation.method as string).toLowerCase() !== "get") || 44 | Object.keys(appContext.appGlobals?.headers || {}).length !== 0 || //if we have any headers for the api, we can't use the iframe trick 45 | appContext.apiInterceptorContext.isMocked(_url) //if we mock this url, the mock can't work in an iframe, so we must fall back to download it with the restApiProxy 46 | ) { 47 | const file: File = await api.execute({ 48 | operation, 49 | params: context, 50 | parseOptions: { 51 | asFile: true, 52 | }, 53 | resolveBindingExpressions, 54 | }); 55 | downloadWithAnchor(file); 56 | } else { 57 | downloadInIframe(_url); 58 | } 59 | } 60 | 61 | //we use a hidden iframe trick here, 62 | // we set the iframe source as the download url, this way the browser will ask to download the file, and show a progress bar 63 | // (we could use an anchor tag with a download attribute, but in this case we can't show progress ) 64 | // we can use it if we don't have to add extra headers to the request in order to download a file (pre-signed urls, or public urls) 65 | function downloadInIframe(fileUrl: string) { 66 | const iframe = document.createElement("iframe"); 67 | iframe.style.display = "none"; 68 | iframe.hidden = true; 69 | iframe.name = fileUrl; 70 | iframe.id = `download-iframe_${fileUrl}`; 71 | iframe.src = fileUrl; 72 | document.body.appendChild(iframe); 73 | setTimeout(() => { 74 | iframe.remove(); 75 | }, 20000); 76 | } 77 | 78 | // we can use it if we do have to add extra headers to the request in order to download a file (urls require authentication) 79 | function downloadWithAnchor(file: File) { 80 | const url = window.URL.createObjectURL(file); 81 | const a = document.createElement("a"); 82 | a.style.display = "none"; 83 | a.href = url; 84 | // the filename you want 85 | a.download = file.name; 86 | document.body.appendChild(a); 87 | a.click(); 88 | window.URL.revokeObjectURL(url); 89 | } 90 | 91 | export const downloadAction = createAction("download", download); 92 | ``` -------------------------------------------------------------------------------- /xmlui/scripts/inline-links.mjs: -------------------------------------------------------------------------------- ``` 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | const includedFileExtensions = [".mdx", ".md"]; 5 | 6 | // Read the file contents 7 | const fileContent = fs.readFileSync("../docs/meta/pages.js", "utf-8"); 8 | 9 | // Match lines like: export const NAME = "value"; 10 | const regex = /export const (\w+)\s*=\s*["'`](.*?)["'`];/g; 11 | 12 | const constants = {}; 13 | let match; 14 | while ((match = regex.exec(fileContent)) !== null) { 15 | const [, key, value] = match; 16 | constants[key] = value; 17 | } 18 | 19 | inlineLinks("../docs/pages"); 20 | 21 | function inlineLinks(pagesFolder) { 22 | traverseDirectory({ name: "", path: pagesFolder }, (item, _) => { 23 | /** 24 | * name: the folder's/file's name (eg. "hello-app-engine") 25 | * path: the path to the root of the given folder from the project root (eg. "src/apps/1_basic/samples/hello-app-engine") 26 | * parent: parent node 27 | * children: children file/folder names 28 | */ 29 | if (fs.statSync(item.path).isDirectory()) { 30 | // Node is a folder 31 | } else { 32 | // Node is a file 33 | if (includedFileExtensions.includes(path.extname(item.name))) { 34 | console.log(item.name); 35 | const mdxContent = fs.readFileSync(item.path, "utf-8"); 36 | 37 | const regex = /<SmartLink\s+href=\{([^}]+)\}>/g; 38 | const newMdxContent = mdxContent.replace(regex, (_, hrefExpr) => { 39 | return `<SmartLink href="${constants[hrefExpr] || ''}">`; 40 | }); 41 | 42 | fs.writeFileSync(item.path, newMdxContent); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * Recursive function that traverses a given folder and applies an optional function on 50 | * each of the folders/files found inside. 51 | */ 52 | export function traverseDirectory(node, visitor, level = 0) { 53 | level++; 54 | const dirContents = fs.readdirSync(node.path); 55 | if (!node.children) node.children = dirContents; 56 | for (const itemName of dirContents) { 57 | const itemPath = [winPathToPosix(node.path), itemName].join(path.posix.sep); 58 | const itemIsDir = fs.statSync(itemPath).isDirectory(); 59 | const childNode = { 60 | name: itemName, 61 | path: itemPath, 62 | parent: node, 63 | }; 64 | visitor && visitor(childNode, level); 65 | if (itemIsDir) { 66 | traverseDirectory(childNode, visitor, level); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Multi-liner (commented and compatible with really old javascript versions) 73 | * Source: https://stackoverflow.com/a/62732509 74 | */ 75 | export function winPathToPosix(windowsPath) { 76 | // handle the edge-case of Window's long file names 77 | // See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#short-vs-long-names 78 | windowsPath = windowsPath.replace(/^\\\\\?\\/, ""); 79 | 80 | // convert the separators, valid since both \ and / can't be in a windows filename 81 | windowsPath = windowsPath.replace(/\\/g, "/"); 82 | 83 | // compress any // or /// to be just /, which is a safe oper under POSIX 84 | // and prevents accidental errors caused by manually doing path1+path2 85 | windowsPath = windowsPath.replace(/\/\/+/g, "/"); 86 | 87 | return windowsPath; 88 | } 89 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Stack/VStack.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | test("renders items vertically", async ({ initTestBed, page }) => { 9 | await initTestBed(` 10 | <VStack testId="vstack"> 11 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 12 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 13 | <Stack testId="item3" height="32px" width="32px" backgroundColor="green" /> 14 | </VStack> 15 | `); 16 | 17 | const vstack = page.getByTestId("vstack"); 18 | const item1 = page.getByTestId("item1"); 19 | const item2 = page.getByTestId("item2"); 20 | const item3 = page.getByTestId("item3"); 21 | 22 | await expect(vstack).toBeVisible(); 23 | await expect(item1).toBeVisible(); 24 | await expect(item2).toBeVisible(); 25 | await expect(item3).toBeVisible(); 26 | 27 | // Get bounding boxes to verify vertical layout 28 | const item1Box = await item1.boundingBox(); 29 | const item2Box = await item2.boundingBox(); 30 | const item3Box = await item3.boundingBox(); 31 | 32 | // Verify items are stacked vertically (item2 should be below item1, item3 below item2) 33 | expect(item2Box!.y).toBeGreaterThan(item1Box!.y + item1Box!.height - 1); // -1 for floating point tolerance 34 | expect(item3Box!.y).toBeGreaterThan(item2Box!.y + item2Box!.height - 1); // -1 for floating point tolerance 35 | 36 | // Verify items are horizontally aligned (should start at roughly the same x position) 37 | expect(Math.abs(item1Box!.x - item2Box!.x)).toBeLessThan(1); 38 | expect(Math.abs(item2Box!.x - item3Box!.x)).toBeLessThan(1); 39 | }); 40 | 41 | test("renders empty VStack", async ({ initTestBed, page }) => { 42 | await initTestBed(`<VStack testId="vstack"></VStack>`); 43 | 44 | const vstack = page.getByTestId("vstack"); 45 | await expect(vstack).toBeAttached(); 46 | await expect(vstack).toBeEmpty(); 47 | }); 48 | 49 | test("ignores orientation property", async ({ initTestBed, page }) => { 50 | await initTestBed(` 51 | <VStack testId="vstack" orientation="horizontal"> 52 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 53 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 54 | </VStack> 55 | `); 56 | 57 | const item1 = page.getByTestId("item1"); 58 | const item2 = page.getByTestId("item2"); 59 | 60 | await expect(item1).toBeVisible(); 61 | await expect(item2).toBeVisible(); 62 | 63 | // Get bounding boxes to verify still renders vertically despite orientation="horizontal" 64 | const item1Box = await item1.boundingBox(); 65 | const item2Box = await item2.boundingBox(); 66 | 67 | // Verify items are still stacked vertically (orientation prop should be ignored) 68 | expect(item2Box!.y).toBeGreaterThan(item1Box!.y + item1Box!.height - 1); 69 | }); 70 | }); 71 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Option/Option.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createComponentRenderer } from "../../components-core/renderers"; 2 | import { MemoizedItem } from "../container-helpers"; 3 | import { createMetadata, d } from "../metadata-helpers"; 4 | import { OptionNative, defaultProps } from "./OptionNative"; 5 | 6 | const COMP = "Option"; 7 | 8 | export const OptionMd = createMetadata({ 9 | status: "stable", 10 | description: 11 | "`Option` defines selectable items for choice-based components, providing both " + 12 | "the underlying value and display text for selection interfaces. It serves as " + 13 | "a non-visual data structure that describes individual choices within " + 14 | "[Select](/components/Select), [AutoComplete](/components/AutoComplete), " + 15 | "and other selection components.", 16 | props: { 17 | label: d( 18 | `This property defines the text to display for the option. If \`label\` is not defined, ` + 19 | `\`Option\` will use the \`value\` as the label.`, 20 | ), 21 | value: d( 22 | "This property defines the value of the option. If `value` is not defined, " + 23 | "`Option` will use the `label` as the value. If neither is defined, " + 24 | "the option is not displayed.", 25 | ), 26 | enabled: { 27 | description: "This boolean property indicates whether the option is enabled or disabled.", 28 | valueType: "boolean", 29 | defaultValue: defaultProps.enabled, 30 | }, 31 | keywords: d( 32 | "An array of keywords that can be used for searching and filtering the option. " + 33 | "These keywords are not displayed but help users find the option through search.", 34 | ), 35 | }, 36 | }); 37 | 38 | export const optionComponentRenderer = createComponentRenderer( 39 | COMP, 40 | OptionMd, 41 | ({ node, extractValue, className, renderChild, layoutContext }) => { 42 | const label = extractValue.asOptionalString(node.props.label); 43 | let value = extractValue(node.props.value); 44 | if (label === undefined && value === undefined) { 45 | return null; 46 | } 47 | 48 | const hasTextNodeChild = node.children?.length === 1 && (node.children[0].type === "TextNode" || node.children[0].type === "TextNodeCData"); 49 | const textNodeChild = hasTextNodeChild ? renderChild(node.children) as string : undefined; 50 | 51 | return ( 52 | <OptionNative 53 | label={label || textNodeChild} 54 | value={value !== undefined && value !== "" ? value : label} 55 | enabled={extractValue.asOptionalBoolean(node.props.enabled)} 56 | keywords={extractValue.asOptionalStringArray(node.props.keywords)} 57 | className={className} 58 | optionRenderer={ 59 | node.children?.length > 0 60 | ? !hasTextNodeChild ? (contextVars) => ( 61 | <MemoizedItem 62 | node={node.children} 63 | renderChild={renderChild} 64 | contextVars={contextVars} 65 | layoutContext={layoutContext} 66 | /> 67 | ) : undefined 68 | : undefined 69 | } 70 | > 71 | {!hasTextNodeChild && renderChild(node.children)} 72 | </OptionNative> 73 | ); 74 | }, 75 | ); 76 | ``` -------------------------------------------------------------------------------- /docs/public/pages/howto/paginate-a-list.md: -------------------------------------------------------------------------------- ```markdown 1 | # Paginate a List 2 | 3 | XMLUI provides a `Pagination` component that can be used to display visual controls for the pagination feature, no matter whether it is handled inside or outside of a layout component requiring that feature. 4 | 5 | The [`Table`](./table) component provides out-of-the-box support for pagination, 6 | so you can access pagination options via the following properties: `isPaginated`, `pageSize`, `pageSizeOptions`, `paginationControlsLocation`. 7 | 8 | ```xmlui noHeader copy 9 | <Table 10 | data="/api/endpoint" 11 | isPaginated 12 | pageSize="10" 13 | pageSizeOptions="{[5, 10, 20, 30]}" 14 | paginationControlsLocation="both" 15 | > 16 | ... 17 | </Table> 18 | ``` 19 | 20 | Other components, such as the `List`, can be hooked up with pagination using a `DataSource` combined with the `Pagination` component. This pattern works as a more generic solution where either the component does not have pagination implemented in the component itself, or you wish to use custom pagination logic. 21 | 22 | In this case the `DataSource` component does the heavy lifting by querying the page index, the previous and next page IDs. This can be done using variables and query parameters. 23 | 24 | ```xmlui-pg 25 | ---app display 26 | <App var.pageSize="{5}" var.currentPage="{0}" var.before="{0}" var.after="{pageSize-1}"> 27 | <DataSource id="pagination_ds" url="/api/pagination_items" queryParams="{{ from: before, to: after }}" /> 28 | <Pagination 29 | itemCount="20" 30 | pageSize="{pageSize}" 31 | pageIndex="{currentPage}" 32 | onPageDidChange="(page, size, total) => { 33 | currentPage = page; 34 | before = page * size; 35 | after = before + size - 1; 36 | }" 37 | /> 38 | <List data="{pagination_ds}" /> 39 | </App> 40 | ---api 41 | { 42 | "apiUrl": "/api", 43 | "initialize": "$state.pagination_items = [{ id: 1, name: 'Laptop Pro', price: 1299 },{ id: 2, name: 'Wireless Mouse', price: 29 },{ id: 3, name: 'Mechanical Keyboard', price: 149 },{ id: 4, name: '4K Monitor', price: 399 },{ id: 5, name: 'USB-C Hub', price: 79 },{ id: 6, name: 'Bluetooth Headphones', price: 199 },{ id: 7, name: 'Webcam HD', price: 89 },{ id: 8, name: 'Standing Desk', price: 299 },{ id: 9, name: 'Ergonomic Chair', price: 249 },{ id: 10, name: 'Desk Lamp', price: 45 },{ id: 11, name: 'Cable Organizer', price: 15 },{ id: 12, name: 'Mouse Pad', price: 12 },{ id: 13, name: 'Laptop Stand', price: 35 },{ id: 14, name: 'External SSD', price: 129 },{ id: 15, name: 'Wireless Charger', price: 59 },{ id: 16, name: 'Smart Speaker', price: 99 },{ id: 17, name: 'Fitness Tracker', price: 199 },{ id: 18, name: 'Tablet Pro', price: 799 },{ id: 19, name: 'Gaming Mouse', price: 89 },{ id: 20, name: 'Noise Cancelling Headphones', price: 349 }]", 44 | "operations": { 45 | "get-pagination-items": { 46 | "url": "/pagination_items", 47 | "method": "get", 48 | "queryParams": { 49 | "from": "integer", 50 | "to": "integer" 51 | }, 52 | "handler": "$state.pagination_items.slice(Number($queryParams.from), Number($queryParams.to) + 1);" 53 | } 54 | } 55 | } 56 | ``` 57 | ``` -------------------------------------------------------------------------------- /docs/content/components/FormSection.md: -------------------------------------------------------------------------------- ```markdown 1 | # FormSection [#formsection] 2 | 3 | `FormSection` groups elements within a `Form`. Child components are placed in a [FlowLayout](/components/FlowLayout). 4 | 5 | ## Properties [#properties] 6 | 7 | ### `columnGap` (default: "3rem") [#columngap-default-3rem] 8 | 9 | The gap between columns of items within the section. 10 | 11 | ```xmlui-pg copy display name="Example: columnGap" 12 | <Form padding="1rem"> 13 | <FormSection columnGap="1rem"> 14 | <FormItem width="50%" label="Name" bindTo="" /> 15 | <FormItem width="50%" label="Occupation" bindTo="" /> 16 | </FormSection> 17 | </Form> 18 | ``` 19 | 20 | ### `heading` [#heading] 21 | 22 | The heading text to be displayed at the top of the form section. 23 | 24 | ```xmlui-pg copy display name="Example: heading" 25 | <Form padding="1rem"> 26 | <FormSection heading="Basic Heading"> 27 | <FormItem label="Input Field" bindTo="" /> 28 | </FormSection> 29 | </Form> 30 | ``` 31 | 32 | ### `headingLevel` (default: "h3") [#headinglevel-default-h3] 33 | 34 | The semantic and visual level of the heading. 35 | 36 | Available values: `h1`, `h2`, `h3` **(default)**, `h4`, `h5`, `h6` 37 | 38 | ```xmlui-pg copy display name="Example: headingLevel" 39 | <Form padding="1rem"> 40 | <FormSection heading="Basic Heading" headingLevel="h1"> 41 | <FormItem label="Input Field" bindTo="" /> 42 | </FormSection> 43 | </Form> 44 | ``` 45 | 46 | ### `headingWeight` (default: "bold") [#headingweight-default-bold] 47 | 48 | The font weight of the heading. 49 | 50 | The default weight is `bold`. 51 | 52 | ```xmlui-pg copy display name="Example: headingWeight" 53 | <Form padding="1rem"> 54 | <FormSection heading="Basic Heading" headingWeight="normal"> 55 | <FormItem label="Input Field" bindTo="" /> 56 | </FormSection> 57 | </Form> 58 | ``` 59 | 60 | ### `info` [#info] 61 | 62 | Informational text displayed below the heading. 63 | 64 | ```xmlui-pg copy display name="Example: info" 65 | <Form padding="1rem"> 66 | <FormSection info="This is some information about a particular section."> 67 | <FormItem label="Input Field" bindTo="" /> 68 | </FormSection> 69 | </Form> 70 | ``` 71 | 72 | ### `infoFontSize` (default: "0.8rem") [#infofontsize-default-0-8rem] 73 | 74 | The font size of the informational text. 75 | 76 | ```xmlui-pg copy {4} display name="Example: infoFontSize" 77 | <Form padding="1rem"> 78 | <FormSection 79 | info="This is some information about a particular section." 80 | infoFontSize="18px" 81 | > 82 | <FormItem label="Input Field" bindTo="" /> 83 | </FormSection> 84 | </Form> 85 | ``` 86 | 87 | ### `paddingTop` (default: "$space-normal") [#paddingtop-default-space-normal] 88 | 89 | The top padding of the FlowLayout where the section's children are placed. 90 | 91 | ### `rowGap` (default: "$space-normal") [#rowgap-default-space-normal] 92 | 93 | The gap between rows of items within the section. 94 | 95 | ```xmlui-pg copy display name="Example: rowGap" 96 | <Form padding="1rem"> 97 | <FormSection rowGap="2rem"> 98 | <FormItem label="Name" bindTo="" /> 99 | <FormItem label="Occupation" bindTo="" /> 100 | </FormSection> 101 | </Form> 102 | ``` 103 | 104 | ## Events [#events] 105 | 106 | This component does not have any events. 107 | 108 | ## Exposed Methods [#exposed-methods] 109 | 110 | This component does not expose any methods. 111 | 112 | ## Styling [#styling] 113 | 114 | This component does not have any styles. 115 | ``` -------------------------------------------------------------------------------- /packages/xmlui-devtools/src/devtools/ModalDialog.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "xmlui/themes.scss" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $component: "ModalDialog"; 5 | $themeVars: (); 6 | @function createThemeVar($componentVariable) { 7 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 8 | @return t.getThemeVar($themeVars, $componentVariable); 9 | } 10 | 11 | // --- Theme vars for paddings 12 | $themeVars: t.composePaddingVars($themeVars, $component); 13 | $themeVars: t.composePaddingVars($themeVars, "overlay-#{$component}"); 14 | $padding-ModalDialog: createThemeVar("padding-#{$component}"); 15 | $backgroundColor-ModalDialog: createThemeVar("Dialog:backgroundColor-#{$component}"); 16 | $backgroundColor-overlay-ModalDialog: createThemeVar("Dialog:backgroundColor-overlay-#{$component}"); 17 | $borderRadius-ModalDialog: createThemeVar("Dialog:borderRadius-#{$component}"); 18 | $fontFamily-ModalDialog: createThemeVar("Dialog:fontFamily-#{$component}"); 19 | $textColor-ModalDialog: createThemeVar("Dialog:textColor-#{$component}"); 20 | $minWidth-ModalDialog: createThemeVar("Dialog:minWidth-#{$component}"); 21 | $maxWidth-ModalDialog: createThemeVar("Dialog:maxWidth-#{$component}"); 22 | $marginBottom-title-ModalDialog: createThemeVar("Dialog:marginBottom-title-#{$component}"); 23 | 24 | 25 | .overlay { 26 | position: fixed; 27 | overflow-y: auto; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | inset: 0; 32 | padding: t.$space-4; 33 | } 34 | 35 | .overlayBg { 36 | background-color: $backgroundColor-overlay-ModalDialog; 37 | position: fixed; 38 | inset: 0; 39 | } 40 | 41 | .contentWrapper { 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | width: 100%; 46 | height: 100%; 47 | } 48 | 49 | .content { 50 | background-color: $backgroundColor-ModalDialog; 51 | border-radius: $borderRadius-ModalDialog; 52 | font-family: $fontFamily-ModalDialog; 53 | color: $textColor-ModalDialog; 54 | box-shadow: t.$boxShadow-spread; 55 | width: 90vw; 56 | max-width: 960px; 57 | min-width: 240px; 58 | isolation: isolate; 59 | position: relative; 60 | display: flex; 61 | flex-direction: column; 62 | max-height: 80vh; 63 | overflow-y: auto; 64 | } 65 | 66 | .content:focus { 67 | outline: none; 68 | } 69 | 70 | .dialogTitle { 71 | flex: 1; 72 | margin-bottom: $marginBottom-title-ModalDialog; 73 | font-size: t.$fontSize-2xl; 74 | } 75 | 76 | .innerContent { 77 | display: flex; 78 | flex-direction: column; 79 | min-height: 0; 80 | gap: var(--stack-gap-default); 81 | flex: 1; 82 | } 83 | 84 | .closeButton { 85 | position: absolute; 86 | right: 0.5rem; 87 | top: 0.4rem; 88 | } 89 | 90 | .actions { 91 | display: inline-flex; 92 | align-items: center; 93 | justify-content: flex-end; 94 | } 95 | 96 | .header { 97 | padding: t.$space-2; 98 | justify-content: space-between; 99 | display: flex; 100 | flex-direction: row; 101 | border-bottom: 1px solid t.$borderColor; 102 | } 103 | 104 | @media (max-width: 70em) { 105 | .dialog, .content { 106 | max-width: 90%; 107 | } 108 | } 109 | 110 | @media (max-width: 50em) { 111 | .dialog, .content { 112 | width: 100%; 113 | max-width: calc(100% - #{t.$space-6}); 114 | min-width: 0 !important; 115 | } 116 | } 117 | 118 | // --- We export the theme variables to add them to the component renderer 119 | :export { 120 | themeVars: t.json-stringify($themeVars); 121 | } 122 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Stack/HStack.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | test("renders items horizontally", async ({ initTestBed, page }) => { 9 | await initTestBed(` 10 | <HStack testId="hstack"> 11 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 12 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 13 | <Stack testId="item3" height="32px" width="32px" backgroundColor="green" /> 14 | </HStack> 15 | `); 16 | 17 | const hstack = page.getByTestId("hstack"); 18 | const item1 = page.getByTestId("item1"); 19 | const item2 = page.getByTestId("item2"); 20 | const item3 = page.getByTestId("item3"); 21 | 22 | await expect(hstack).toBeVisible(); 23 | await expect(item1).toBeVisible(); 24 | await expect(item2).toBeVisible(); 25 | await expect(item3).toBeVisible(); 26 | 27 | // Get bounding boxes to verify horizontal layout 28 | const item1Box = await item1.boundingBox(); 29 | const item2Box = await item2.boundingBox(); 30 | const item3Box = await item3.boundingBox(); 31 | 32 | // Verify items are stacked horizontally (item2 should be to the right of item1, item3 to the right of item2) 33 | expect(item2Box!.x).toBeGreaterThan(item1Box!.x + item1Box!.width - 1); // -1 for floating point tolerance 34 | expect(item3Box!.x).toBeGreaterThan(item2Box!.x + item2Box!.width - 1); // -1 for floating point tolerance 35 | 36 | // Verify items are vertically aligned (should start at roughly the same y position) 37 | expect(Math.abs(item1Box!.y - item2Box!.y)).toBeLessThan(1); 38 | expect(Math.abs(item2Box!.y - item3Box!.y)).toBeLessThan(1); 39 | }); 40 | 41 | test("renders empty HStack", async ({ initTestBed, page }) => { 42 | await initTestBed(`<HStack testId="hstack"></HStack>`); 43 | 44 | const hstack = page.getByTestId("hstack"); 45 | await expect(hstack).toBeAttached(); 46 | await expect(hstack).toBeEmpty(); 47 | }); 48 | 49 | test("ignores orientation property", async ({ initTestBed, page }) => { 50 | await initTestBed(` 51 | <HStack testId="hstack" orientation="vertical"> 52 | <Stack testId="item1" height="32px" width="32px" backgroundColor="red" /> 53 | <Stack testId="item2" height="32px" width="32px" backgroundColor="blue" /> 54 | </HStack> 55 | `); 56 | 57 | const item1 = page.getByTestId("item1"); 58 | const item2 = page.getByTestId("item2"); 59 | 60 | await expect(item1).toBeVisible(); 61 | await expect(item2).toBeVisible(); 62 | 63 | // Get bounding boxes to verify still renders horizontally despite orientation="vertical" 64 | const item1Box = await item1.boundingBox(); 65 | const item2Box = await item2.boundingBox(); 66 | 67 | // Verify items are still stacked horizontally (orientation prop should be ignored) 68 | expect(item2Box!.x).toBeGreaterThan(item1Box!.x + item1Box!.width - 1); 69 | }); 70 | }); 71 | ``` -------------------------------------------------------------------------------- /xmlui/bin/vite-xmlui-plugin.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { dataToEsm } from "@rollup/pluginutils"; 2 | import type { Plugin } from "vite"; 3 | import { 4 | collectCodeBehindFromSource, 5 | removeCodeBehindTokensFromTree, 6 | } from "../src/parsers/scripting/code-behind-collect"; 7 | import { 8 | codeBehindFileExtension, 9 | componentFileExtension, 10 | moduleFileExtension, 11 | } from "../src/parsers/xmlui-parser/fileExtensions"; 12 | import { Parser } from "../src/parsers/scripting/Parser"; 13 | import * as fs from "fs"; 14 | import * as path from "path"; 15 | import { errReportComponent, xmlUiMarkupToComponent } from "../src/components-core/xmlui-parser"; 16 | 17 | export type PluginOptions = { 18 | // --- Add plugin options here. 19 | }; 20 | 21 | const xmluiExtension = new RegExp(`.${componentFileExtension}$`); 22 | const xmluiScriptExtension = new RegExp(`.${codeBehindFileExtension}$`); 23 | const moduleScriptExtension = new RegExp(`.${moduleFileExtension}$`); 24 | 25 | /** 26 | * Transform XMLUI files to JS objects. 27 | */ 28 | export default function viteXmluiPlugin(pluginOptions: PluginOptions = {}): Plugin { 29 | let itemIndex = 0; 30 | return { 31 | name: "vite:transform-xmlui", 32 | 33 | async transform(code: string, id: string, options) { 34 | const moduleNameResolver = (moduleName: string) => { 35 | return path.resolve(path.dirname(id), moduleName); 36 | }; 37 | 38 | if (xmluiExtension.test(id)) { 39 | const fileId = "" + itemIndex++; 40 | let { component, errors, erroneousCompoundComponentName } = xmlUiMarkupToComponent( 41 | code, 42 | fileId, 43 | ); 44 | if (errors.length > 0) { 45 | component = errReportComponent(errors, id, erroneousCompoundComponentName); 46 | } 47 | const file = { 48 | component, 49 | src: code, 50 | file: fileId, 51 | }; 52 | 53 | return { 54 | code: dataToEsm(file), 55 | map: { mappings: "" }, 56 | }; 57 | } 58 | 59 | const hasXmluiScriptExtension = xmluiScriptExtension.test(id); 60 | const hasModuleScriptExtension = moduleScriptExtension.test(id); 61 | if (hasXmluiScriptExtension || hasModuleScriptExtension) { 62 | // --- We parse the module file to catch parsing errors 63 | 64 | const parser = new Parser(code); 65 | parser.parseStatements(); 66 | const moduleName = hasXmluiScriptExtension 67 | ? id.substring(0, id.length - (codeBehindFileExtension.length + 1)) 68 | : id.substring(0, id.length - (moduleFileExtension.length + 1)); 69 | 70 | const codeBehind = collectCodeBehindFromSource(moduleNameResolver(moduleName), code); 71 | removeCodeBehindTokensFromTree(codeBehind); 72 | 73 | // TODO: Add error handling. 74 | // Check, if codeBehind.moduleErrors is not empty (Record<string, ModuleErrors[]>); each module 75 | // should be checked for errors and warnings. If there are errors, throw an error. 76 | 77 | return { 78 | code: dataToEsm({...codeBehind, src: code}), 79 | map: { mappings: "" }, 80 | }; 81 | } 82 | return null; 83 | }, 84 | // async generateBundle(opts, bundle, isWrite){ 85 | // console.log('generate bundle', opts); 86 | // } 87 | }; 88 | } 89 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/loader/ApiLoader.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { useCallback } from "react"; 2 | 3 | import type { 4 | LoaderErrorFn, 5 | LoaderInProgressChangedFn, 6 | LoaderLoadedFn, 7 | } from "../abstractions/LoaderRenderer"; 8 | import type { ComponentDef } from "../../abstractions/ComponentDefs"; 9 | import type { ContainerState } from "../rendering/ContainerWrapper"; 10 | import { removeNullProperties } from "../utils/misc"; 11 | import { extractParam } from "../utils/extractParam"; 12 | import { createLoaderRenderer } from "../renderers"; 13 | import { useAppContext } from "../AppContext"; 14 | import { Loader } from "./Loader"; 15 | import { createMetadata, d } from "../../components/metadata-helpers"; 16 | 17 | /** 18 | * Properties of the API loader component 19 | */ 20 | type ApiLoaderProps = { 21 | loader: ApiLoaderDef; 22 | loaderInProgressChanged: LoaderInProgressChangedFn; 23 | loaderIsRefetchingChanged: LoaderInProgressChangedFn; 24 | loaderLoaded: LoaderLoadedFn; 25 | loaderError: LoaderErrorFn; 26 | state: ContainerState; 27 | doNotRemoveNulls?: boolean; 28 | structuralSharing?: boolean; 29 | }; 30 | 31 | /** 32 | * Represents a non-displayed React component, which handles the specified API loader 33 | */ 34 | function ApiLoader({ 35 | loader, 36 | loaderInProgressChanged, 37 | loaderIsRefetchingChanged, 38 | loaderLoaded, 39 | loaderError, 40 | state, 41 | doNotRemoveNulls, 42 | structuralSharing = true, 43 | }: ApiLoaderProps) { 44 | const appContext = useAppContext(); 45 | 46 | const url = extractParam(state, loader.props.url, appContext); 47 | const loadable = !!url; 48 | 49 | const doLoad = useCallback(async () => { 50 | if (!loadable) { 51 | return; 52 | } 53 | const response = await fetch(url); 54 | if (loader.props.raw) { 55 | return await response.text(); 56 | } 57 | const responseObj = await response.json(); 58 | if (!doNotRemoveNulls) { 59 | removeNullProperties(responseObj); 60 | } 61 | return responseObj; 62 | }, [doNotRemoveNulls, loadable, loader.props.raw, url]); 63 | 64 | return ( 65 | <Loader 66 | state={state} 67 | loader={loader} 68 | loaderInProgressChanged={loaderInProgressChanged} 69 | loaderIsRefetchingChanged={loaderIsRefetchingChanged} 70 | loaderLoaded={loaderLoaded} 71 | loaderError={loaderError} 72 | loaderFn={doLoad} 73 | structuralSharing={structuralSharing} 74 | /> 75 | ); 76 | } 77 | 78 | const ApiLoaderMd = createMetadata({ 79 | status: "stable", 80 | description: `Represents a loader that calls an API through an HTTP/HTTPS GET request`, 81 | props: { 82 | url: d("URL segment to use in the GET request"), 83 | raw: d("If true, the loader returns the raw text response instead of parsing it as JSON"), 84 | }, 85 | }); 86 | 87 | type ApiLoaderDef = ComponentDef<typeof ApiLoaderMd>; 88 | 89 | export const apiLoaderRenderer = createLoaderRenderer( 90 | "ApiLoader", 91 | ({ loader, state, loaderInProgressChanged, loaderIsRefetchingChanged, loaderLoaded, loaderError }) => { 92 | return ( 93 | <ApiLoader 94 | loader={loader} 95 | state={state} 96 | loaderInProgressChanged={loaderInProgressChanged} 97 | loaderIsRefetchingChanged={loaderIsRefetchingChanged} 98 | loaderLoaded={loaderLoaded} 99 | loaderError={loaderError} 100 | /> 101 | ); 102 | }, 103 | ApiLoaderMd, 104 | ); 105 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/ToneChangerButton/ToneChangerButton.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { useThemes } from "../../components-core/theming/ThemeContext"; 2 | import { createComponentRenderer } from "../../components-core/renderers"; 3 | import { Button } from "../Button/ButtonNative"; 4 | import { Icon } from "../Icon/IconNative"; 5 | import { createMetadata, dClick } from "../metadata-helpers"; 6 | import { noop } from "lodash-es"; 7 | 8 | const COMP = "ToneChangerButton"; 9 | const LIGHT_TO_DARK_ICON = "lightToDark:ToneChangerButton"; 10 | const DARK_TO_LIGHT_ICON = "darkToLight:ToneChangerButton"; 11 | 12 | export const defaultProps = { 13 | lightToDarkIcon: LIGHT_TO_DARK_ICON, 14 | darkToLightIcon: DARK_TO_LIGHT_ICON, 15 | onClick: noop, 16 | }; 17 | 18 | export const ToneChangerButtonMd = createMetadata({ 19 | status: "stable", 20 | description: "`ToneChangerButton` enables the user to switch between light and dark modes.", 21 | props: { 22 | lightToDarkIcon: { 23 | description: 24 | `The icon displayed when the theme is in light mode and will switch to dark. You can change ` + 25 | `the default icon for all ${COMP} instances with the "icon.lightToDark:ToneChangerButton" ` + 26 | `declaration in the app configuration file.`, 27 | defaultValue: defaultProps.lightToDarkIcon, 28 | }, 29 | darkToLightIcon: { 30 | description: 31 | `The icon displayed when the theme is in dark mode and will switch to light. You can change ` + 32 | `the default icon for all ${COMP} instances with the "icon.darkToLight:ToneChangerButton" ` + 33 | `declaration in the app configuration file.`, 34 | defaultValue: defaultProps.darkToLightIcon, 35 | }, 36 | }, 37 | events: { 38 | click: dClick(COMP), 39 | }, 40 | }); 41 | 42 | export function ToneChangerButton({ 43 | lightToDarkIcon = defaultProps.lightToDarkIcon, 44 | darkToLightIcon = defaultProps.darkToLightIcon, 45 | onClick = defaultProps.onClick, 46 | }) { 47 | const { activeThemeTone, setActiveThemeTone } = useThemes(); 48 | 49 | // Use the direct icon name as both the main icon and the fallback 50 | // This ensures we always have a working icon 51 | const iconName = activeThemeTone === "light" ? lightToDarkIcon : darkToLightIcon; 52 | const fallbackIcon = activeThemeTone === "light" ? "lightToDark" : "darkToLight"; 53 | 54 | return ( 55 | <Button 56 | variant="ghost" 57 | style={{ flexShrink: 0 }} 58 | icon={<Icon name={iconName} fallback={fallbackIcon} />} 59 | onClick={() => { 60 | if (activeThemeTone === "light") { 61 | setActiveThemeTone("dark"); 62 | onClick?.("dark"); 63 | } else { 64 | setActiveThemeTone("light"); 65 | onClick?.("light"); 66 | } 67 | }} 68 | /> 69 | ); 70 | } 71 | 72 | /** 73 | * Define the renderer for the ToneChangerButton component 74 | */ 75 | export const toneChangerButtonComponentRenderer = createComponentRenderer( 76 | COMP, 77 | ToneChangerButtonMd, 78 | ({ node, extractValue, lookupEventHandler }) => { 79 | return ( 80 | <ToneChangerButton 81 | onClick={lookupEventHandler("click")} 82 | lightToDarkIcon={extractValue.asOptionalString(node.props.lightToDarkIcon)} 83 | darkToLightIcon={extractValue.asOptionalString(node.props.darkToLightIcon)} 84 | /> 85 | ); 86 | }, 87 | ); 88 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Pages/PagesNative.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type { CSSProperties, ReactNode } from "react"; 2 | import { useMemo } from "react"; 3 | import { Navigate, Route, Routes, useParams } from "@remix-run/react"; 4 | import classnames from "classnames"; 5 | 6 | import type { ComponentDef } from "../../abstractions/ComponentDefs"; 7 | import type { LayoutContext, RenderChildFn, ValueExtractor } from "../../abstractions/RendererDefs"; 8 | import { EMPTY_ARRAY, EMPTY_OBJECT } from "../../components-core/constants"; 9 | import type { PageMd } from "./Pages"; 10 | import styles from "./Pages.module.scss"; 11 | 12 | // Default props for Pages component 13 | export const defaultProps = { 14 | fallbackPath: "/", 15 | }; 16 | 17 | // --- We need this component to make sure all the child routes are wrapped in a 18 | // --- container and this way they can access the routeParams 19 | type RouteWrapperProps = { 20 | childRoute?: ComponentDef | Array<ComponentDef>; 21 | renderChild: RenderChildFn; 22 | layoutContext?: LayoutContext; 23 | style?: CSSProperties; 24 | className?: string; 25 | uid?: string; 26 | }; 27 | 28 | export function RouteWrapper({ 29 | childRoute = EMPTY_ARRAY, 30 | renderChild, 31 | layoutContext, 32 | style, 33 | className, 34 | uid, 35 | }: RouteWrapperProps) { 36 | const params = useParams(); 37 | 38 | //we need to wrap the child route in a container to make sure the route params are available. 39 | // we do this wrapping by providing an empty object to vars. 40 | // this way it becomes an 'implicit' container (vars/state inside this container is propagated to the parent) 41 | const wrappedWithContainer = useMemo(() => { 42 | if (Array.isArray(childRoute)) { 43 | return { 44 | type: "Fragment", 45 | uid, 46 | vars: EMPTY_OBJECT, 47 | children: childRoute, 48 | }; 49 | } 50 | return { 51 | type: "Fragment", 52 | uid, 53 | vars: EMPTY_OBJECT, 54 | children: [childRoute], 55 | }; 56 | }, [childRoute, uid]); 57 | 58 | return ( 59 | <div 60 | key={JSON.stringify(params)} 61 | className={classnames(className, styles.wrapper, "xmlui-page-root")} 62 | style={style} 63 | > 64 | {renderChild(wrappedWithContainer, layoutContext)} 65 | </div> 66 | ); 67 | } 68 | 69 | type PageComponentDef = ComponentDef<typeof PageMd>; 70 | 71 | type PagesProps = { 72 | fallbackPath?: string; 73 | node?: ComponentDef; 74 | renderChild: RenderChildFn; 75 | extractValue: ValueExtractor; 76 | children?: ReactNode; 77 | className?: ReactNode; 78 | }; 79 | 80 | export function Pages({ 81 | node, 82 | renderChild, 83 | extractValue, 84 | fallbackPath = defaultProps.fallbackPath, 85 | }: PagesProps) { 86 | const routes: Array<PageComponentDef> = []; 87 | const restChildren: Array<ComponentDef> = []; 88 | node.children?.forEach((child) => { 89 | if (child.type === "Page") { 90 | routes.push(child as PageComponentDef); 91 | } else { 92 | restChildren.push(child); 93 | } 94 | }); 95 | return ( 96 | <> 97 | <Routes> 98 | {routes.map((child, i) => { 99 | return ( 100 | <Route path={extractValue(child.props.url)} key={i} element={renderChild(child)} /> 101 | ); 102 | })} 103 | {fallbackPath && <Route path="*" element={<Navigate to={fallbackPath} replace />} />} 104 | </Routes> 105 | {renderChild(restChildren)} 106 | </> 107 | ); 108 | } 109 | ``` -------------------------------------------------------------------------------- /docs/public/pages/app-structure.md: -------------------------------------------------------------------------------- ```markdown 1 | # Structure of an XMLUI app 2 | 3 | The [XMLUI Invoice demo app](https://github.com/xmlui-org/xmlui-invoice/releases) exhibits the typical structure of an XMLUI app. 4 | 5 | ```xmlui-tree 6 | <root> 7 | index.html 8 | Main.xmlui 9 | config.json 10 | components 11 | ClientDetails.xmlui 12 | Clients.xmlui 13 | ... 14 | MonthlyRevenue.xmlui 15 | WeeklyRevenue.xmlui 16 | resources 17 | favicon.ico 18 | xmlui-logo-inverted.svg 19 | xmlui-logo.svg 20 | themes 21 | invoice.json 22 | xmlui 23 | 0.9.23.js 24 | charts-0.1.21.js 25 | start.bat 26 | start.sh 27 | api.json 28 | data.db 29 | xmlui-test-server 30 | ``` 31 | 32 | > [!INFO] The `xmlui` folder contains the xmlui engine with a version number, specifically `0.9.23.js`. We recommend this practice in order to know when/whether to upgrade. 33 | 34 | 35 | | file| description | 36 | |---|---| 37 | | **`index.html`** | The default webpage to display | 38 | | **`Main.xmlui`** | The XMLUI app's entry point | 39 | | **`config.json`** | The XMLUI app's configuration file | 40 | | **`components`** | The folder with your custom components | 41 | | **`resources`** | The folder with static app resources | 42 | | **`themes`** | The folder with your custom themes | 43 | | **`xmlui`** | The folder with the XMLUI core framework and extensions | 44 | | **`start.bat`** | The batch file to start the test server on Windows | 45 | | **`start.sh`** | The bash script file to start the test server on Mac, Linux, or WSL | 46 | | **`api.json`** | *Optional*: API description file for use with xmlui-test-server | 47 | | **`data.db`** | *Optional*: SQLite database for use with xmlui-test-server| 48 | | **`xmlui-test-server`** | *Optional*: server, you can use any static web server| 49 | 50 | 51 | You can deploy this tree structure (minus the optional `api.json`, `data.db`, and `xmlui-test-server`) to any static webserver that's configured to serve `index.html`. Consider this minimal app. 52 | 53 | ```xmlui-tree 54 | xmlui-minimal 55 | index.html 56 | Main.xmlui 57 | components 58 | Home.xmlui 59 | resources 60 | favicon.ico 61 | xmlui-logo-inverted.svg 62 | xmlui-logo.svg 63 | xmlui 64 | 0.9.23.js 65 | ``` 66 | 67 | ## index.html 68 | 69 | ```html 70 | <!DOCTYPE html> 71 | <html lang="en"> 72 | 73 | <head> 74 | <meta charset="UTF-8" /> 75 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 76 | <script src="xmlui/0.9.23.js"></script> 77 | </head> 78 | 79 | <body> 80 | </body> 81 | 82 | </html> 83 | ``` 84 | 85 | ## Main.xmlui 86 | 87 | ```xmlui 88 | <App name="XMLUI Minimal"> 89 | 90 | <NavPanel> 91 | <NavLink label="Home" to="/Home" /> 92 | </NavPanel> 93 | 94 | <Pages> 95 | <Page url="/Home"> 96 | <Home /> 97 | </Page> 98 | </Pages> 99 | 100 | </App> 101 | ``` 102 | 103 | ## Home.xmlui 104 | 105 | ```xmlui 106 | <Component name="Home" > 107 | 108 | A minimal XMLUI app 109 | 110 | </Component> 111 | ``` 112 | 113 | ## Local deployment 114 | 115 | If you are working locally, in a folder at the root of this tree, here are some ways you can serve the app. 116 | 117 | If you have node.js and npm: 118 | 119 | ``` 120 | npx -y http-server 121 | 122 | $ npx -y http-server 123 | Starting up http-server, serving ./ 124 | 125 | Available on: 126 | http://127.0.0.1:8080 127 | ``` 128 | 129 | If you have python: 130 | 131 | ``` 132 | $ python -m http.server 8080 133 | Serving HTTP on :: port 8080 (http://[::]:8080/) ... 134 | ``` 135 | 136 | In either case, visit http://localhost:8080 to view the app. 137 | 138 | See also [Hosted deployment](/hosted-deployment). 139 | 140 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Charts/DonutChart/DonutChart.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { createComponentRenderer } from "../../../components-core/renderers"; 2 | import styles from "../PieChart/PieChartNative.module.scss"; 3 | import { defaultProps, PieChart } from "../PieChart/PieChartNative"; 4 | import { parseScssVar } from "../../../components-core/theming/themeVars"; 5 | import { createMetadata } from "../../metadata-helpers"; 6 | 7 | const COMP = "DonutChart"; 8 | 9 | const defaultPropsDonut = { 10 | ...defaultProps, 11 | innerRadius: 60, 12 | }; 13 | 14 | export const DonutChartMd = createMetadata({ 15 | status: "experimental", 16 | description: "A derivative of [PieChart](/components/PieChart) with a hollow center. " + 17 | "Note that the height of the component or its parent needs to be set explicitly.", 18 | props: { 19 | data: { 20 | description: "The data to be displayed in the chart. Needs to be an array of objects.", 21 | }, 22 | nameKey: { 23 | description: 24 | "Specifies the key in the data objects that will be used to label the different data series.", 25 | valueType: "string", 26 | }, 27 | dataKey: { 28 | description: 29 | "This property specifies the key in the data objects that will be used to render the chart.", 30 | valueType: "string", 31 | }, 32 | showLabel: { 33 | description: "Toggles whether to show labels (\`true\`) or not (\`false\`).", 34 | valueType: "boolean", 35 | defaultValue: defaultPropsDonut.showLabel, 36 | }, 37 | innerRadius: { 38 | description: "Sets the inner radius of the donut chart.", 39 | valueType: "number", 40 | defaultValue: defaultPropsDonut.innerRadius, 41 | }, 42 | showLabelList: { 43 | description: "Whether to show labels in a list (\`true\`) or not (\`false\`).", 44 | valueType: "boolean", 45 | defaultValue: defaultPropsDonut.showLabelList, 46 | }, 47 | showLegend: { 48 | description: "Whether to show a legend (\`true\`) or not (\`false\`).", 49 | valueType: "boolean", 50 | defaultValue: defaultPropsDonut.showLegend, 51 | }, 52 | }, 53 | themeVars: parseScssVar(styles.themeVars), 54 | defaultThemeVars: { 55 | "textColor-labelList-PieChart": "$textColor-primary", 56 | }, 57 | }); 58 | 59 | export const donutChartComponentRenderer = createComponentRenderer( 60 | COMP, 61 | DonutChartMd, 62 | ({ extractValue, node, className, renderChild }) => { 63 | return ( 64 | <PieChart 65 | showLabelList={extractValue.asOptionalBoolean( 66 | node.props?.showLabelList, 67 | defaultPropsDonut.showLabelList, 68 | )} 69 | innerRadius={extractValue.asOptionalNumber( 70 | node.props?.innerRadius, 71 | defaultPropsDonut.innerRadius, 72 | )} 73 | data={extractValue(node.props?.data)} 74 | className={className} 75 | showLabel={extractValue.asOptionalBoolean( 76 | node.props?.showLabel, 77 | defaultPropsDonut.showLabel, 78 | )} 79 | dataKey={extractValue(node.props?.dataKey)} 80 | nameKey={extractValue(node.props?.nameKey)} 81 | showLegend={extractValue.asOptionalBoolean( 82 | node.props?.showLegend, 83 | defaultPropsDonut.showLegend, 84 | )} 85 | > 86 | {renderChild(node.children)} 87 | </PieChart> 88 | ); 89 | }, 90 | ); 91 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/List/List.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "../../components-core/theming/themes" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $themeVars: (); 5 | @function createThemeVar($componentVariable) { 6 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 7 | @return t.getThemeVar($themeVars, $componentVariable); 8 | } 9 | 10 | @layer components { 11 | .outerWrapper { 12 | overflow: auto; 13 | //max-height: 100%; 14 | overflow-anchor: none; 15 | 16 | &.hasOutsideScroll { 17 | overflow: initial; 18 | } 19 | } 20 | 21 | .innerWrapper { 22 | visibility: hidden; 23 | display: flex; 24 | flex-direction: column; 25 | min-height: 100%; 26 | 27 | &.reverse { 28 | justify-content: flex-end; 29 | } 30 | } 31 | 32 | .infoWrapper { 33 | width: 100%; 34 | margin: 0 auto; 35 | } 36 | 37 | .loadingWrapper { 38 | display: flex; 39 | flex-direction: row; 40 | justify-content: center; 41 | padding-top: t.$space-2; 42 | padding-bottom: t.$space-2; 43 | } 44 | 45 | .noRows { 46 | width: 100%; 47 | text-align: center; 48 | margin-top: t.$space-4; 49 | } 50 | 51 | // --- We export the theme variables to add them to the component renderer 52 | :export { 53 | themeVars: t.json-stringify($themeVars); 54 | } 55 | 56 | .borderCollapse { 57 | &:not(.sectioned) { 58 | .row { 59 | //first row in the list (but not the last) 60 | &:first-child:not(:last-child) { 61 | & > * { 62 | border-bottom-left-radius: 0; 63 | border-bottom-right-radius: 0; 64 | border-bottom: 0; 65 | } 66 | } 67 | 68 | //last row in the list (but not the first) 69 | &:last-child:not(:first-child) { 70 | & > * { 71 | border-top-left-radius: 0; 72 | border-top-right-radius: 0; 73 | } 74 | } 75 | 76 | //rows in the middle of the list 77 | &:not(:first-child):not(:last-child) { 78 | & > * { 79 | border-bottom-left-radius: 0; 80 | border-bottom-right-radius: 0; 81 | border-top-left-radius: 0; 82 | border-top-right-radius: 0; 83 | border-bottom: 0; 84 | } 85 | } 86 | } 87 | } 88 | 89 | &.sectioned { 90 | //.row after the section header (first row in a section) 91 | .section + .row:not(:has(+ .sectionFooter)) { 92 | & > * { 93 | border-bottom-left-radius: 0; 94 | border-bottom-right-radius: 0; 95 | border-bottom: 0; 96 | } 97 | } 98 | 99 | //.row before the sectionFooter (last row in a section) 100 | .row:has(+ .sectionFooter):not(.section + .row) { 101 | & > * { 102 | border-top-left-radius: 0; 103 | border-top-right-radius: 0; 104 | } 105 | } 106 | 107 | //we select the rows that has .row as direct sibling, but not the first row of a section (rows in the middle of the section) 108 | .row:has(+ .row):not(.section + .row) { 109 | & > * { 110 | border-bottom-left-radius: 0; 111 | border-bottom-right-radius: 0; 112 | border-top-left-radius: 0; 113 | border-top-right-radius: 0; 114 | border-bottom: 0; 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | // --- We export the theme variables to add them to the component renderer 122 | :export { 123 | themeVars: t.json-stringify($themeVars); 124 | } 125 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/ToneSwitch/ToneSwitch.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { expect, test } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | test("renders in light mode by default", async ({ initTestBed, page }) => { 9 | await initTestBed(` 10 | <App> 11 | <ToneSwitch /> 12 | <Text>{activeThemeTone}</Text> 13 | </App> 14 | `); 15 | const toggle = page.getByRole("switch"); 16 | await expect(toggle).toBeVisible(); 17 | await expect(page.getByText("light")).toBeVisible(); 18 | }); 19 | 20 | test("toggles to dark mode when clicked", async ({ initTestBed, page }) => { 21 | await initTestBed(` 22 | <App> 23 | <ToneSwitch /> 24 | <Text>{activeThemeTone}</Text> 25 | </App> 26 | `); 27 | const toggle = page.getByRole("switch"); 28 | await expect(toggle).toBeVisible(); 29 | await toggle.click({ force: true }); 30 | await expect(page.getByText("dark")).toBeVisible(); 31 | await expect(toggle).toBeChecked(); 32 | }); 33 | 34 | test("toggles back to light mode when clicked again", async ({ initTestBed, page }) => { 35 | await initTestBed(` 36 | <App> 37 | <ToneSwitch /> 38 | <Text>{activeThemeTone}</Text> 39 | </App> 40 | `); 41 | const toggle = page.getByRole("switch"); 42 | await expect(toggle).toBeVisible(); 43 | await toggle.click({ force: true }); 44 | await expect(page.getByText("dark")).toBeVisible(); 45 | await expect(toggle).toBeChecked(); 46 | await toggle.click({ force: true }); 47 | await expect(page.getByText("light")).toBeVisible(); 48 | await expect(toggle).not.toBeChecked(); 49 | }); 50 | }); 51 | 52 | // ============================================================================= 53 | // ACCESSIBILITY TESTS 54 | // ============================================================================= 55 | 56 | test.describe("Accessibility", () => { 57 | test("has switch role", async ({ initTestBed, page }) => { 58 | await initTestBed(`<ToneSwitch />`); 59 | const toggle = page.getByRole("switch"); 60 | await expect(toggle).toBeVisible(); 61 | }); 62 | 63 | test("is keyboard accessible with Space key", async ({ initTestBed, page }) => { 64 | await initTestBed(`<ToneSwitch />`); 65 | const toggle = page.getByRole("switch"); 66 | 67 | await toggle.focus(); 68 | await expect(toggle).toBeFocused(); 69 | 70 | await page.keyboard.press("Space"); 71 | await expect(toggle).toBeChecked(); 72 | }); 73 | 74 | test("maintains focus during interactions", async ({ initTestBed, page }) => { 75 | await initTestBed(`<ToneSwitch />`); 76 | const toggle = page.getByRole("switch"); 77 | 78 | await toggle.focus(); 79 | await toggle.click({ force: true }); 80 | await expect(toggle).toBeFocused(); 81 | }); 82 | 83 | test("has appropriate aria-checked state", async ({ initTestBed, page }) => { 84 | await initTestBed(`<ToneSwitch />`); 85 | const toggle = page.getByRole("switch"); 86 | 87 | await expect(toggle).toHaveAttribute("aria-checked", "false"); 88 | 89 | await toggle.click({ force: true }); 90 | await expect(toggle).toHaveAttribute("aria-checked", "true"); 91 | }); 92 | }); 93 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Charts/PieChart/PieChart.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { defaultProps, PieChart } from "./PieChartNative"; 2 | import styles from "./PieChartNative.module.scss"; 3 | import { LabelPositionValues } from "../utils/abstractions"; 4 | import { parseScssVar } from "../../../components-core/theming/themeVars"; 5 | import { createComponentRenderer } from "../../../components-core/renderers"; 6 | import type { LabelPosition } from "recharts/types/component/Label"; 7 | import { createMetadata, d } from "../../metadata-helpers"; 8 | 9 | const COMP = "PieChart"; 10 | 11 | export const PieChartMd = createMetadata({ 12 | status: "experimental", 13 | description: 14 | "`PieChart` visualizes proportional data as circular segments; each slice " + 15 | "represents a percentage of the whole. Note that the height of the component or " + 16 | "its parent needs to be set explicitly.", 17 | docFolder: "Charts/PieChart", 18 | props: { 19 | data: { 20 | description: "The data to be displayed in the chart. Needs to be an array of objects.", 21 | }, 22 | nameKey: { 23 | description: 24 | "Specifies the key in the data objects that will be used to label the different data series.", 25 | valueType: "string", 26 | }, 27 | dataKey: { 28 | description: 29 | "This property specifies the key in the data objects that will be used to render the chart.", 30 | valueType: "string", 31 | }, 32 | showLabel: { 33 | description: "Toggles whether to show labels (\`true\`) or not (\`false\`).", 34 | valueType: "boolean", 35 | defaultValue: defaultProps.showLabel, 36 | }, 37 | showLabelList: { 38 | description: "Whether to show labels in a list (\`true\`) or not (\`false\`).", 39 | valueType: "boolean", 40 | defaultValue: defaultProps.showLabelList, 41 | }, 42 | labelListPosition: { 43 | description: "The position of the label list.", 44 | valueType: "string", 45 | defaultValue: defaultProps.labelListPosition, 46 | availableValues: LabelPositionValues, 47 | }, 48 | outerRadius: d( 49 | "The outer radius of the pie chart, can be a number or a string (e.g., '100%').", 50 | ), 51 | showLegend: { 52 | description: "Toggles whether to show legend (\`true\`) or not (\`false\`).", 53 | valueType: "boolean", 54 | defaultValue: defaultProps.showLegend, 55 | } 56 | }, 57 | themeVars: parseScssVar(styles.themeVars), 58 | defaultThemeVars: { 59 | "textColor-labelList-PieChart": "$textColor-primary", 60 | }, 61 | }); 62 | 63 | export const pieChartComponentRenderer = createComponentRenderer( 64 | COMP, 65 | PieChartMd, 66 | ({ extractValue, node, className, renderChild }) => { 67 | return ( 68 | <PieChart 69 | showLabelList={extractValue.asOptionalBoolean(node.props?.showLabelList)} 70 | labelListPosition={extractValue.asOptionalString(node.props?.labelListPosition) as LabelPosition} 71 | data={extractValue(node.props?.data)} 72 | className={className} 73 | showLabel={extractValue.asOptionalBoolean(node.props?.showLabel)} 74 | showLegend={extractValue.asOptionalBoolean(node.props?.showLegend)} 75 | dataKey={extractValue(node.props?.dataKey)} 76 | nameKey={extractValue(node.props?.nameKey)} 77 | > 78 | {renderChild(node.children)} 79 | </PieChart> 80 | ); 81 | }, 82 | ); 83 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Tooltip/Tooltip.module.scss: -------------------------------------------------------------------------------- ```scss 1 | @use "../../components-core/theming/themes" as t; 2 | 3 | // --- This code snippet is required to collect the theme variables used in this module 4 | $themeVars: (); 5 | @function createThemeVar($componentVariable) { 6 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 7 | @return t.getThemeVar($themeVars, $componentVariable); 8 | } 9 | 10 | $component: "Tooltip"; 11 | $themeVars: t.composePaddingVars($themeVars, $component); 12 | $themeVars: t.composeBorderVars($themeVars, $component); 13 | 14 | 15 | // --- Theme variables for Tooltip component 16 | $backgroundColor-Tooltip: createThemeVar("backgroundColor-#{$component}"); 17 | $textColor-Tooltip: createThemeVar("textColor-#{$component}"); 18 | $fontSize-Tooltip: createThemeVar("fontSize-#{$component}"); 19 | $lineHeight-Tooltip: createThemeVar("lineHeight-#{$component}"); 20 | $boxShadow-Tooltip: createThemeVar("boxShadow-#{$component}"); 21 | $fill-arrow-Tooltip: createThemeVar("fill-arrow-#{$component}"); 22 | $stroke-arrow-Tooltip: createThemeVar("stroke-arrow-#{$component}"); 23 | $strokeWidth-arrow-Tooltip: createThemeVar("strokeWidth-arrow-#{$component}"); 24 | $animationDuration-Tooltip: createThemeVar("animationDuration-#{$component}"); 25 | $animation-Tooltip: createThemeVar("animation-#{$component}"); 26 | 27 | /* Tooltip styles */ 28 | 29 | .hiddenTrigger { 30 | position: fixed; 31 | left: 0; 32 | top: 0; 33 | width: 1px; 34 | height: 1px; 35 | pointer-events: none; 36 | z-index: -1; 37 | opacity: 0; 38 | } 39 | 40 | .content { 41 | @include t.paddingVars($themeVars, $component); 42 | @include t.borderVars($themeVars, $component); 43 | font-size: $fontSize-Tooltip; 44 | line-height: $lineHeight-Tooltip; 45 | color: $textColor-Tooltip; 46 | background-color: $backgroundColor-Tooltip; 47 | box-shadow: $boxShadow-Tooltip; 48 | user-select: none; 49 | animation-duration: $animationDuration-Tooltip; 50 | animation-timing-function: $animation-Tooltip; 51 | will-change: transform, opacity; 52 | z-index: 9999; 53 | } 54 | 55 | .content[data-state='delayed-open'][data-side='top'] { 56 | animation-name: slideDownAndFade; 57 | } 58 | 59 | .content[data-state='delayed-open'][data-side='right'] { 60 | animation-name: slideLeftAndFade; 61 | } 62 | 63 | .content[data-state='delayed-open'][data-side='bottom'] { 64 | animation-name: slideUpAndFade; 65 | } 66 | 67 | .content[data-state='delayed-open'][data-side='left'] { 68 | animation-name: slideRightAndFade; 69 | } 70 | 71 | .arrow { 72 | fill: $fill-arrow-Tooltip; 73 | stroke: $stroke-arrow-Tooltip; 74 | stroke-width: $strokeWidth-arrow-Tooltip; 75 | } 76 | 77 | @keyframes slideUpAndFade { 78 | from { 79 | opacity: 0; 80 | transform: translateY(2px); 81 | } 82 | to { 83 | opacity: 1; 84 | transform: translateY(0); 85 | } 86 | } 87 | 88 | @keyframes slideRightAndFade { 89 | from { 90 | opacity: 0; 91 | transform: translateX(-2px); 92 | } 93 | to { 94 | opacity: 1; 95 | transform: translateX(0); 96 | } 97 | } 98 | 99 | @keyframes slideDownAndFade { 100 | from { 101 | opacity: 0; 102 | transform: translateY(-2px); 103 | } 104 | to { 105 | opacity: 1; 106 | transform: translateY(0); 107 | } 108 | } 109 | 110 | @keyframes slideLeftAndFade { 111 | from { 112 | opacity: 0; 113 | transform: translateX(2px); 114 | } 115 | to { 116 | opacity: 1; 117 | transform: translateX(0); 118 | } 119 | } 120 | 121 | // --- We export the theme variables to add them to the component renderer 122 | :export { 123 | themeVars: t.json-stringify($themeVars); 124 | } 125 | ``` -------------------------------------------------------------------------------- /xmlui/tests/parsers/scripting/eval-tree-arrow.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { evalBindingExpression } from "../../../src/components-core/script-runner/eval-tree-sync"; 4 | import { createEvalContext } from "./test-helpers"; 5 | 6 | describe("Evaluate arrow expressions", () => { 7 | it("Arrow #1", () => { 8 | // --- Arrange 9 | const source = "(x => 2 * x)(4)"; 10 | const context = createEvalContext({}); 11 | 12 | // --- Act 13 | const value = evalBindingExpression(source, context); 14 | 15 | // --- Arrange 16 | expect(value).equal(8); 17 | }); 18 | 19 | it("Arrow #2", () => { 20 | // --- Arrange 21 | const source = "((x, y) => x + y)(1, 2)"; 22 | const context = createEvalContext({}); 23 | 24 | // --- Act 25 | const value = evalBindingExpression(source, context); 26 | 27 | // --- Arrange 28 | expect(value).equal(3); 29 | }); 30 | 31 | it("Arrow #3", () => { 32 | // --- Arrange 33 | const source = "((x, y) => { return x + y })(1, 2)"; 34 | const context = createEvalContext({}); 35 | 36 | // --- Act 37 | const value = evalBindingExpression(source, context); 38 | 39 | // --- Arrange 40 | expect(value).equal(3); 41 | }); 42 | 43 | it("Arrow #4", () => { 44 | // --- Arrange 45 | const source = "(x => (++x.h))(count)"; 46 | const context = createEvalContext({ 47 | localContext: { 48 | count: { h: 3 }, 49 | }, 50 | }); 51 | 52 | // --- Act 53 | const value = evalBindingExpression(source, context); 54 | 55 | // --- Arrange 56 | expect(value).equal(4); 57 | }); 58 | 59 | it("Arrow #5", () => { 60 | // --- Arrange 61 | const source = "(x => x += 2)(count)"; 62 | const context = createEvalContext({ 63 | localContext: { 64 | count: 3, 65 | }, 66 | }); 67 | 68 | // --- Act 69 | const value = evalBindingExpression(source, context); 70 | 71 | // --- Arrange 72 | expect(value).equal(5); 73 | }); 74 | 75 | it("Arrow #6", () => { 76 | // --- Arrange 77 | const source = "(x => x += 2)(count + 4)"; 78 | const context = createEvalContext({ 79 | localContext: { 80 | count: 3, 81 | }, 82 | }); 83 | 84 | // --- Act 85 | const value = evalBindingExpression(source, context); 86 | 87 | // --- Arrange 88 | expect(value).equal(9); 89 | }); 90 | 91 | it("Arrow #7", () => { 92 | // --- Arrange 93 | const source = "[1,2,3,4,5].filter(x => x % 2 === 0)[1]"; 94 | const context = createEvalContext({ 95 | localContext: { 96 | count: 3, 97 | }, 98 | }); 99 | 100 | // --- Act 101 | const value = evalBindingExpression(source, context); 102 | 103 | // --- Arrange 104 | expect(value).equal(4); 105 | }); 106 | 107 | it("Arrow #8", () => { 108 | // --- Arrange 109 | const source = "containsArray.array.filter(item => item % 2 === 0)[1]"; 110 | const context = createEvalContext({ 111 | localContext: { 112 | containsArray: { 113 | array: [5, 4, 3, 2, 1], 114 | }, 115 | }, 116 | }); 117 | 118 | // --- Act 119 | const value = evalBindingExpression(source, context); 120 | 121 | // --- Arrange 122 | expect(value).equal(2); 123 | }); 124 | 125 | it("Arrow #9", () => { 126 | // --- Arrange 127 | const source = "array.reduce((acc, item) => acc + item, 0)"; 128 | const context = createEvalContext({ 129 | localContext: { 130 | array: [5, 4, 3, 2, 1], 131 | }, 132 | }); 133 | 134 | // --- Act 135 | const value = evalBindingExpression(source, context); 136 | 137 | // --- Arrange 138 | expect(value).equal(15); 139 | }); 140 | }); 141 | ``` -------------------------------------------------------------------------------- /xmlui/src/components-core/script-runner/asyncProxy.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Gets a proxy function for one that does not support async operations 3 | * @param fn Function to replace with a proxy 4 | * @param origArgs Original function arguments 5 | * @param context Function context ("this" of the function invocation) 6 | * @return The proxy, if found; otherwise the original function 7 | */ 8 | export function getAsyncProxy(fn: Function, origArgs: any[], context: any): Function { 9 | const proxyFn = asyncProxies.get(fn); 10 | if (!proxyFn) return fn; 11 | 12 | origArgs.unshift(context); 13 | return proxyFn; 14 | } 15 | 16 | // Async implementations for JavaScript functions that do not support async arguments 17 | const asyncProxies = new Map<Function, Function>(); 18 | asyncProxies.set(Array.prototype.filter, asyncFilter); 19 | asyncProxies.set(Array.prototype.forEach, asyncForEach); 20 | asyncProxies.set(Array.prototype.map, asyncMap); 21 | asyncProxies.set(Array.prototype.every, asyncEvery); 22 | asyncProxies.set(Array.prototype.findIndex, asyncFindIndex); 23 | asyncProxies.set(Array.prototype.find, asyncFind); 24 | asyncProxies.set(Array.prototype.flatMap, asyncFlatMap); 25 | asyncProxies.set(Array.prototype.some, asyncSome); 26 | 27 | // The async implementation of Array.prototype.some 28 | async function asyncSome(arr: any[], predicate: (...args: any[]) => boolean) { 29 | const results = await Promise.all(arr.map(predicate)); 30 | return arr.some((_v, index) => results[index]); 31 | } 32 | 33 | // The async implementation of Array.prototype.filter 34 | async function asyncFilter(arr: any[], predicate: (...args: any[]) => boolean) { 35 | const results = await Promise.all(arr.map(predicate)); 36 | return arr.filter((_v, index) => results[index]); 37 | } 38 | 39 | // The async implementation of Array.prototype.forEach 40 | async function asyncForEach(arr: any[], predicate: (...args: any[]) => void) { 41 | for (let i = 0; i < arr.length; i++) { 42 | await predicate(arr[i], i, arr); 43 | } 44 | } 45 | 46 | // The async implementation of Array.prototype.map 47 | async function asyncMap(arr: any[], predicate: (...args: any[]) => Promise<any[]>) { 48 | const result = []; 49 | for (let i = 0; i < arr.length; i++) { 50 | result.push(await predicate(arr[i], i, arr)); 51 | } 52 | return result; 53 | } 54 | 55 | // The async implementation of Array.prototype.asyncEvery 56 | async function asyncEvery(arr: any[], callback: (...args: any[]) => any) { 57 | const results = await Promise.all(arr.map(callback)); 58 | return results.every((_v, index) => results[index]); 59 | } 60 | 61 | // The async implementation of Array.prototype.asyncFind 62 | async function asyncFind(arr: any[], predicate: (...args: any[]) => boolean) { 63 | const results = await Promise.all(arr.map(predicate)); 64 | return arr.find((_v, index) => results[index]); 65 | } 66 | 67 | // The async implementation of Array.prototype.asyncFindIndex 68 | async function asyncFindIndex(arr: any[], predicate: (...args: any[]) => boolean) { 69 | const results = await Promise.all(arr.map(predicate)); 70 | return arr.findIndex((_v, index) => results[index]); 71 | } 72 | 73 | // The async implementation of Array.prototype.asyncFlatMap 74 | async function asyncFlatMap(arr: any[], predicate: (...args: any[]) => boolean) { 75 | const results = await Promise.all(arr.map(predicate)); 76 | return arr.flatMap((_v, index) => results[index]); 77 | } 78 | ``` -------------------------------------------------------------------------------- /docs/public/pages/user-defined-components.md: -------------------------------------------------------------------------------- ```markdown 1 | # User-defined components 2 | 3 | You can define your own components, pass properties to them, and use them interchangeably with core components. When you find yourself writing a component with more than a few dozen lines of XMLUI markup, consider refactoring in order to name and package the key blocks of code. This strategy not only enables reuse but, just as importantly, ensures that the refactored component will be easy to read and maintain. 4 | 5 | Such refactoring requires you to create and name new `.xmlui` files, identify inline elements that need to be passed as properties from a refactored component, and use those properties in newly-created subcomponents. Historically that kind of overhead has been a disincentive to refactoring in any programming environment. Now you can often outsource that gruntwork to AI assistants. It works particularly well with XMLUI, we use this strategy extensively, and we highly recommend it. 6 | 7 | Here's a simple component to package a name/value pair. 8 | 9 | ```xmlui-pg display noHeader 10 | ---app display 11 | <App> 12 | <NameValue name="Mary" value="123" /> 13 | </App> 14 | ---comp display 15 | <Component name="NameValue"> 16 | <Card width="20%"> 17 | <Text>Name: { $props.name} </Text> 18 | <Text>Value: { $props.value} </Text> 19 | </Card> 20 | </Component> 21 | ``` 22 | 23 | The component's name must start with an uppercase letter followed by letters, digits, the underscore (`_`), or the dollar sign (`$`) character. Components must be placed into separate files in the `components` folder within the app's root folder. The component's name must match its filename. 24 | 25 | Here's how you can define default values for properties. 26 | 27 | ```xmlui 28 | <Component name="NameValue"> 29 | <Card width="20%"> 30 | <Text>Name: { $props.name ?? '[no name]' } </Text> 31 | <Text>Value: { $props.value ?? '[no value]' } </Text> 32 | </Card> 33 | </Component> 34 | ``` 35 | 36 | ## Events 37 | 38 | The `<IncButton>` component increments its value for every click, and notifies its environment by firing an event. The event's handler receives the current counter as an event parameter. 39 | 40 | ```xmlui-pg noHeader 41 | ---app display 42 | <App> 43 | <Card width="30%"> 44 | <variable name="text" value=""/> 45 | <IncButton onIncremented="(clickCount) => text += ' ' + clickCount" /> 46 | <Text value="{text}" /> 47 | </Card> 48 | </App> 49 | ---comp display 50 | <Component name="IncButton"> 51 | <variable name="count" value="{0}" /> 52 | <Button 53 | label="Click to increment: {count}" 54 | onClick="count++; emitEvent('incremented', count)" 55 | /> 56 | </Component> 57 | ``` 58 | 59 | ## Methods 60 | 61 | The `<UsingInternalModal>` component exports the `open` method of the `ModalDialog` that it defines. 62 | 63 | ```xmlui-pg noHeader 64 | ---app display 65 | <App height="300px" > 66 | <UsingInternalModal id="component"/> 67 | <Button label="Open the internal dialog" onClick="component.openDialog()" /> 68 | </App> 69 | ---comp display 70 | <Component name="UsingInternalModal"> 71 | <ModalDialog id="dialog" title="Example Dialog"> 72 | <Button label="Close Dialog" onClick="dialog.close()" /> 73 | </ModalDialog> 74 | 75 | <H1>Using an Internal Modal Dialog</H1> 76 | 77 | <method name="openDialog"> 78 | console.log('internal method called') 79 | dialog.open(); 80 | </method> 81 | </Component> 82 | ``` 83 | ```