This is page 59 of 190. Use http://codebase.md/xmlui-org/xmlui/xmlui/mockApiDef.js?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ └── config.json
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog-optimized.yml
│ ├── deploy-blog-swa.yml
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs-swa.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── an-advanced-codefence.gif
│ │ │ │ ├── an-advanced-codefence.mp4
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── codefence-runner.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ ├── lorem-ipsum.png
│ │ │ │ ├── playground-checkbox-source.png
│ │ │ │ ├── playground.png
│ │ │ │ ├── use-xmlui-mcp-to-find-a-howto.png
│ │ │ │ └── xmlui-demo-gallery.png
│ │ │ ├── introducing-xmlui.md
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ ├── xmlui-playground.md
│ │ │ └── xmlui-powered-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── icons
│ │ │ │ ├── github.svg
│ │ │ │ └── rss.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ ├── staticwebapp.config.json
│ │ └── web.config
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── LinkButton.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ └── Separator.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── blog-theme.ts
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── FancyButton.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── control-cache-invalidation.md
│ │ │ │ ├── debounce-user-input-for-api-calls.md
│ │ │ │ ├── debounce-with-changelistener.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── icons
│ │ │ │ ├── github.svg
│ │ │ │ └── rss.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
│ │ ├── staticwebapp.config.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
│ │ │ ├── LinkButton.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── Separator.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── docs-theme.ts
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── Animation.tsx
│ │ ├── AnimationNative.tsx
│ │ ├── FadeAnimation.tsx
│ │ ├── FadeInAnimation.tsx
│ │ ├── FadeOutAnimation.tsx
│ │ ├── index.tsx
│ │ ├── ScaleAnimation.tsx
│ │ └── SlideInAnimation.tsx
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── HelloWorld.module.scss
│ │ ├── HelloWorld.tsx
│ │ ├── HelloWorldNative.tsx
│ │ └── index.tsx
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── IPhoneFrame.module.scss
│ │ ├── IPhoneFrame.tsx
│ │ ├── MacOSAppFrame.module.scss
│ │ ├── MacOSAppFrame.tsx
│ │ ├── WindowsAppFrame.module.scss
│ │ └── WindowsAppFrame.tsx
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── LazyPdfNative.tsx
│ │ ├── Pdf.module.scss
│ │ └── Pdf.tsx
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── hooks
│ │ │ ├── usePlayground.ts
│ │ │ └── useToast.ts
│ │ ├── index.tsx
│ │ ├── playground
│ │ │ ├── Box.module.scss
│ │ │ ├── Box.tsx
│ │ │ ├── CodeSelector.module.scss
│ │ │ ├── CodeSelector.tsx
│ │ │ ├── ConfirmationDialog.module.scss
│ │ │ ├── ConfirmationDialog.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── Header.module.scss
│ │ │ ├── Header.tsx
│ │ │ ├── Playground.tsx
│ │ │ ├── PlaygroundContent.module.scss
│ │ │ ├── PlaygroundContent.tsx
│ │ │ ├── PlaygroundNative.module.scss
│ │ │ ├── PlaygroundNative.tsx
│ │ │ ├── Preview.tsx
│ │ │ ├── StandalonePlayground.tsx
│ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ ├── ThemeSwitcher.module.scss
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ └── utils.ts
│ │ ├── providers
│ │ │ ├── Toast.module.scss
│ │ │ └── ToastProvider.tsx
│ │ ├── state
│ │ │ └── store.ts
│ │ ├── themes
│ │ │ └── theme.ts
│ │ └── utils
│ │ └── helpers.ts
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Search.module.scss
│ │ └── Search.tsx
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Spreadsheet.tsx
│ │ └── SpreadsheetNative.tsx
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ └── src
│ ├── Carousel
│ │ ├── Carousel.module.scss
│ │ ├── Carousel.tsx
│ │ ├── CarouselContext.tsx
│ │ └── CarouselNative.tsx
│ ├── FancyButton
│ │ ├── FancyButton.module.scss
│ │ ├── FancyButton.tsx
│ │ └── FancyButton.xmlui
│ ├── Hello
│ │ ├── Hello.tsx
│ │ ├── Hello.xmlui
│ │ └── Hello.xmlui.xs
│ ├── HeroSection
│ │ ├── HeroSection.module.scss
│ │ ├── HeroSection.spec.ts
│ │ ├── HeroSection.tsx
│ │ └── HeroSectionNative.tsx
│ ├── index.tsx
│ ├── ScrollToTop
│ │ ├── ScrollToTop.module.scss
│ │ ├── ScrollToTop.tsx
│ │ └── ScrollToTopNative.tsx
│ └── vite-env.d.ts
├── playwright.config.ts
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.cjs
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── component-metadata.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── theme-variables-refactoring.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── extract-component-metadata.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── generate-metadata-markdown.js
│ ├── get-langserver-metadata.js
│ ├── inline-links.mjs
│ └── README-e2e-summary.md
├── src
│ ├── abstractions
│ │ ├── _conventions.md
│ │ ├── ActionDefs.ts
│ │ ├── AppContextDefs.ts
│ │ ├── ComponentDefs.ts
│ │ ├── ContainerDefs.ts
│ │ ├── ExtensionDefs.ts
│ │ ├── FunctionDefs.ts
│ │ ├── RendererDefs.ts
│ │ ├── scripting
│ │ │ ├── BlockScope.ts
│ │ │ ├── Compilation.ts
│ │ │ ├── LogicalThread.ts
│ │ │ ├── LoopScope.ts
│ │ │ ├── modules.ts
│ │ │ ├── ScriptParserError.ts
│ │ │ ├── Token.ts
│ │ │ ├── TryScope.ts
│ │ │ └── TryScopeExp.ts
│ │ └── ThemingDefs.ts
│ ├── components
│ │ ├── _conventions.md
│ │ ├── abstractions.ts
│ │ ├── Accordion
│ │ │ ├── Accordion.md
│ │ │ ├── Accordion.module.scss
│ │ │ ├── Accordion.spec.ts
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── AccordionItem.tsx
│ │ │ ├── AccordionItemNative.tsx
│ │ │ └── AccordionNative.tsx
│ │ ├── Animation
│ │ │ └── AnimationNative.tsx
│ │ ├── APICall
│ │ │ ├── APICall.md
│ │ │ ├── APICall.spec.ts
│ │ │ ├── APICall.tsx
│ │ │ └── APICallNative.tsx
│ │ ├── App
│ │ │ ├── App.md
│ │ │ ├── App.module.scss
│ │ │ ├── App.spec.ts
│ │ │ ├── App.tsx
│ │ │ ├── AppLayoutContext.ts
│ │ │ ├── AppNative.tsx
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── AppHeader
│ │ │ ├── AppHeader.md
│ │ │ ├── AppHeader.module.scss
│ │ │ ├── AppHeader.spec.ts
│ │ │ ├── AppHeader.tsx
│ │ │ └── AppHeaderNative.tsx
│ │ ├── AppState
│ │ │ ├── AppState.md
│ │ │ ├── AppState.spec.ts
│ │ │ ├── AppState.tsx
│ │ │ └── AppStateNative.tsx
│ │ ├── AutoComplete
│ │ │ ├── AutoComplete.md
│ │ │ ├── AutoComplete.module.scss
│ │ │ ├── AutoComplete.spec.ts
│ │ │ ├── AutoComplete.tsx
│ │ │ ├── AutoCompleteContext.tsx
│ │ │ └── AutoCompleteNative.tsx
│ │ ├── Avatar
│ │ │ ├── Avatar.md
│ │ │ ├── Avatar.module.scss
│ │ │ ├── Avatar.spec.ts
│ │ │ ├── Avatar.tsx
│ │ │ └── AvatarNative.tsx
│ │ ├── Backdrop
│ │ │ ├── Backdrop.md
│ │ │ ├── Backdrop.module.scss
│ │ │ ├── Backdrop.spec.ts
│ │ │ ├── Backdrop.tsx
│ │ │ └── BackdropNative.tsx
│ │ ├── Badge
│ │ │ ├── Badge.md
│ │ │ ├── Badge.module.scss
│ │ │ ├── Badge.spec.ts
│ │ │ ├── Badge.tsx
│ │ │ └── BadgeNative.tsx
│ │ ├── Bookmark
│ │ │ ├── Bookmark.md
│ │ │ ├── Bookmark.module.scss
│ │ │ ├── Bookmark.spec.ts
│ │ │ ├── Bookmark.tsx
│ │ │ └── BookmarkNative.tsx
│ │ ├── Breakout
│ │ │ ├── Breakout.module.scss
│ │ │ ├── Breakout.spec.ts
│ │ │ ├── Breakout.tsx
│ │ │ └── BreakoutNative.tsx
│ │ ├── Button
│ │ │ ├── Button-style.spec.ts
│ │ │ ├── Button.md
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.spec.ts
│ │ │ ├── Button.tsx
│ │ │ └── ButtonNative.tsx
│ │ ├── Card
│ │ │ ├── Card.md
│ │ │ ├── Card.module.scss
│ │ │ ├── Card.spec.ts
│ │ │ ├── Card.tsx
│ │ │ └── CardNative.tsx
│ │ ├── Carousel
│ │ │ ├── Carousel.md
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.spec.ts
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ ├── CarouselItem.tsx
│ │ │ ├── CarouselItemNative.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── ChangeListener
│ │ │ ├── ChangeListener.md
│ │ │ ├── ChangeListener.spec.ts
│ │ │ ├── ChangeListener.tsx
│ │ │ └── ChangeListenerNative.tsx
│ │ ├── chart-color-schemes.ts
│ │ ├── Charts
│ │ │ ├── AreaChart
│ │ │ │ ├── AreaChart.md
│ │ │ │ ├── AreaChart.spec.ts
│ │ │ │ ├── AreaChart.tsx
│ │ │ │ └── AreaChartNative.tsx
│ │ │ ├── BarChart
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── BarChart.module.scss
│ │ │ │ ├── BarChart.spec.ts
│ │ │ │ ├── BarChart.tsx
│ │ │ │ └── BarChartNative.tsx
│ │ │ ├── DonutChart
│ │ │ │ ├── DonutChart.spec.ts
│ │ │ │ └── DonutChart.tsx
│ │ │ ├── LabelList
│ │ │ │ ├── LabelList.module.scss
│ │ │ │ ├── LabelList.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ ├── ContentSeparatorNative.tsx
│ │ │ └── test-padding.xmlui
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.js
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── index.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── index.ts
│ │ ├── infrastructure
│ │ │ ├── index.html
│ │ │ ├── main.tsx
│ │ │ ├── public
│ │ │ │ ├── mockServiceWorker.js
│ │ │ │ ├── resources
│ │ │ │ │ ├── bell.svg
│ │ │ │ │ ├── box.svg
│ │ │ │ │ ├── doc.svg
│ │ │ │ │ ├── eye.svg
│ │ │ │ │ ├── flower-640x480.jpg
│ │ │ │ │ ├── sun.svg
│ │ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ │ └── txt.svg
│ │ │ │ └── serve.json
│ │ │ └── TestBed.tsx
│ │ └── themed-app-test-helpers.ts
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.json
├── tsdown.config.ts
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/xmlui/src/components-core/action/APICall.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import toast from "react-hot-toast";
2 | import type { QueryClient, QueryKey } from "@tanstack/react-query";
3 | import { createDraft, finishDraft } from "immer";
4 |
5 | import type { AppContextObject } from "../../abstractions/AppContextDefs";
6 | import type { AsyncFunction } from "../../abstractions/FunctionDefs";
7 | import type { ActionExecutionContext, LookupAsyncFnInner } from "../../abstractions/ActionDefs";
8 | import { invalidateQueries } from "../utils/actionUtils";
9 | import { extractParam, shouldKeep } from "../utils/extractParam";
10 | import { randomUUID } from "../utils/misc";
11 | import type { ApiActionOptions, ApiOperationDef } from "../RestApiProxy";
12 | import RestApiProxy from "../RestApiProxy";
13 | import { createAction } from "./actions";
14 |
15 | function findQueryKeysToUpdate(updates: string | string[], queryClient: QueryClient) {
16 | const queryKeysToUpdate: Array<QueryKey> = [];
17 | if (updates) {
18 | let updatesArray: Array<string>;
19 | if (Array.isArray(updates)) {
20 | updatesArray = updates;
21 | } else {
22 | updatesArray = [updates];
23 | }
24 | updatesArray.forEach((queryUrl) => {
25 | queryClient
26 | .getQueryCache()
27 | .getAll()
28 | .forEach((query) => {
29 | if (query.queryKey[0] === queryUrl) {
30 | queryKeysToUpdate.push(query.queryKey);
31 | }
32 | });
33 | });
34 | }
35 | return queryKeysToUpdate;
36 | }
37 |
38 | function prepareOptimisticValue(value: any, clientTxId: string) {
39 | return {
40 | ...value,
41 | id: value.id || clientTxId,
42 | _optimisticValue: true,
43 | _initiatorClientTxId: clientTxId,
44 | };
45 | }
46 |
47 | async function prepareOptimisticValuesForQueries(
48 | queryKeys: Array<QueryKey>,
49 | queryClient: QueryClient,
50 | clientTxId: string,
51 | stateContext: any,
52 | resolvedOptimisticValue?: any,
53 | optimisticValueGetter?: AsyncFunction,
54 | ) {
55 | const ret: Map<QueryKey, any> = new Map();
56 |
57 | await Promise.all(
58 | queryKeys.map(async (queryKey) => {
59 | if (resolvedOptimisticValue) {
60 | ret.set(queryKey, prepareOptimisticValue(resolvedOptimisticValue, clientTxId));
61 | return;
62 | }
63 | if (!optimisticValueGetter) {
64 | return;
65 | }
66 | const currentData = queryClient.getQueryData(queryKey) as any;
67 | const flatData = currentData?.pages
68 | ? currentData.pages.flatMap((page: any) => page)
69 | : currentData;
70 | const optimisticValue = await optimisticValueGetter(flatData, stateContext["$param"]);
71 | if (optimisticValue) {
72 | ret.set(queryKey, prepareOptimisticValue(optimisticValue, clientTxId));
73 | }
74 | }),
75 | );
76 |
77 | return ret;
78 | }
79 |
80 | async function doOptimisticUpdate(
81 | optimisticValuesByQueryKeys: Map<QueryKey, any>,
82 | queryClient: QueryClient,
83 | ) {
84 | if (!optimisticValuesByQueryKeys.size) {
85 | return;
86 | }
87 | for (const entry of optimisticValuesByQueryKeys.entries()) {
88 | const [key, optimisticValue] = entry;
89 | await queryClient.cancelQueries({ queryKey: key });
90 | const oldData = queryClient.getQueryData(key) as any;
91 |
92 | const draft = createDraft(oldData as any);
93 | if (draft.pages) {
94 | let updated = false;
95 | draft.pages.forEach((page: any) => {
96 | page.forEach((item: any) => {
97 | if (item.id === optimisticValue.id) {
98 | Object.assign(item, optimisticValue);
99 | updated = true;
100 | }
101 | });
102 | });
103 | if (!updated) {
104 | draft.pages[draft.pages.length - 1].push(optimisticValue);
105 | }
106 | } else {
107 | let updated = false;
108 | draft.forEach((item: any) => {
109 | if (item.id === optimisticValue.id) {
110 | Object.assign(item, optimisticValue);
111 | updated = true;
112 | }
113 | });
114 | if (!updated) {
115 | draft.push(optimisticValue);
116 | }
117 | }
118 | const newData = finishDraft(draft);
119 | queryClient.setQueryData(key, newData);
120 | // console.log("optimistic added", { finalOptimisticValue, newData });
121 | }
122 | }
123 |
124 | function updateQueriesWithResult(
125 | queryKeysToUpdate: Array<QueryKey>,
126 | optimisticValuesByQueryKeys: Map<QueryKey, any>,
127 | clientTxId: string,
128 | queryClient: QueryClient,
129 | result: any,
130 | ) {
131 | if (!queryKeysToUpdate.length) {
132 | return;
133 | }
134 | queryKeysToUpdate.forEach((key) => {
135 | const oldData = queryClient.getQueryData(key) as any;
136 | const draft = createDraft(oldData as any);
137 | const optimisticValue = optimisticValuesByQueryKeys.get(key);
138 | if (draft.pages) {
139 | //pageable loader
140 | if (optimisticValue) {
141 | draft.pages[draft.pages.length - 1] = draft.pages[draft.pages.length - 1].map(
142 | (item: any) =>
143 | item.id === optimisticValue.id && item._initiatorClientTxId === clientTxId
144 | ? result || {
145 | ...item,
146 | _optimisticValue: undefined,
147 | _initiatorClientTxId: undefined,
148 | }
149 | : item,
150 | );
151 | } else {
152 | let updated = false;
153 | draft.pages.forEach((page: any) => {
154 | page?.forEach((item: any) => {
155 | if (item.id === result?.id) {
156 | Object.assign(item, result);
157 | updated = true;
158 | }
159 | });
160 | });
161 | if (!updated && result) {
162 | draft.pages[draft.pages.length - 1].push(result);
163 | }
164 | }
165 | } else {
166 | if (optimisticValue) {
167 | draft.forEach((item: any, index: number) => {
168 | if (item.id === optimisticValue.id && item._initiatorClientTxId === clientTxId) {
169 | draft[index] = result || {
170 | ...item,
171 | _optimisticValue: undefined,
172 | _initiatorClientTxId: undefined,
173 | };
174 | }
175 | });
176 | } else {
177 | let updated = false;
178 | draft.forEach((item: any, index: number) => {
179 | if (item.id === result.id) {
180 | draft[index] = result || {
181 | ...item,
182 | _optimisticValue: undefined,
183 | _initiatorClientTxId: undefined,
184 | };
185 | updated = true;
186 | }
187 | });
188 | if (!updated && result) {
189 | draft.push(result);
190 | }
191 | }
192 | }
193 | const newData = finishDraft(draft);
194 | queryClient.setQueryData(key, newData);
195 | });
196 | }
197 |
198 | async function updateQueriesWithOptimisticValue({
199 | stateContext,
200 | updates,
201 | appContext,
202 | queryClient,
203 | clientTxId,
204 | optimisticValue,
205 | lookupAction,
206 | getOptimisticValue,
207 | uid,
208 | }: {
209 | stateContext: any;
210 | updates: string | string[] | undefined;
211 | appContext: AppContextObject;
212 | queryClient: QueryClient;
213 | clientTxId: string;
214 | optimisticValue: any;
215 | lookupAction: LookupAsyncFnInner;
216 | getOptimisticValue?: string;
217 | uid: symbol;
218 | }) {
219 | const queryKeysToUpdate = findQueryKeysToUpdate(
220 | extractParam(stateContext, updates, appContext),
221 | queryClient,
222 | );
223 | const optimisticValuesByQueryKeys = await prepareOptimisticValuesForQueries(
224 | queryKeysToUpdate,
225 | queryClient,
226 | clientTxId,
227 | stateContext,
228 | extractParam(stateContext, optimisticValue, appContext),
229 | lookupAction(getOptimisticValue, uid),
230 | );
231 |
232 | await doOptimisticUpdate(optimisticValuesByQueryKeys, queryClient);
233 | return { queryKeysToUpdate, optimisticValuesByQueryKeys };
234 | }
235 |
236 | type APICall = {
237 | invalidates?: string | string[];
238 | updates?: string | string[];
239 | confirmTitle?: string;
240 | confirmMessage?: string;
241 | confirmButtonLabel?: string;
242 | params?: any;
243 | payloadType?: string;
244 | optimisticValue?: any;
245 | getOptimisticValue?: string;
246 | inProgressNotificationMessage?: string;
247 | completedNotificationMessage?: string;
248 | errorNotificationMessage?: string;
249 |
250 | uid?: string | symbol;
251 | when?: string;
252 |
253 | onBeforeRequest?: string;
254 | onSuccess?: string;
255 | onProgress?: string;
256 | onError?: string;
257 | } & ApiOperationDef;
258 |
259 | export async function callApi(
260 | { state, appContext, lookupAction, getCurrentState, apiInstance }: ActionExecutionContext,
261 | {
262 | confirmTitle,
263 | confirmMessage,
264 | confirmButtonLabel,
265 | params = {},
266 | onBeforeRequest,
267 | onSuccess,
268 | onError,
269 | invalidates,
270 | updates,
271 | optimisticValue,
272 | payloadType,
273 | when,
274 | getOptimisticValue,
275 | inProgressNotificationMessage,
276 | completedNotificationMessage,
277 | errorNotificationMessage,
278 | uid: actionUid,
279 | onProgress,
280 |
281 | //operation
282 | headers,
283 | url,
284 | queryParams,
285 | rawBody,
286 | method,
287 | body,
288 | }: APICall,
289 | { resolveBindingExpressions }: ApiActionOptions = {},
290 | ) {
291 | const uid = typeof actionUid === "symbol" ? actionUid : Symbol(actionUid);
292 | const stateContext = { ...state, ...params };
293 | if (!shouldKeep(when, stateContext, appContext)) {
294 | return;
295 | }
296 | if (confirmTitle || confirmMessage || confirmButtonLabel) {
297 | const title = extractParam(stateContext, confirmTitle, appContext);
298 | const message = extractParam(stateContext, confirmMessage, appContext);
299 | const buttonLabel = extractParam(stateContext, confirmButtonLabel, appContext);
300 | const dialogCheck = await appContext.confirm(
301 | title ?? "Confirm Operation",
302 | message ?? "Are you sure you want to perform this operation?",
303 | buttonLabel ?? "Yes",
304 | );
305 | if (!dialogCheck) return;
306 | }
307 | const resolvedInvalidates = extractParam(stateContext, invalidates, appContext);
308 |
309 | const clientTxId = randomUUID();
310 | const beforeRequestFn = lookupAction(onBeforeRequest, uid);
311 | const beforeRequestResult = await beforeRequestFn?.();
312 | if (typeof beforeRequestResult === "boolean" && beforeRequestResult === false) {
313 | return;
314 | }
315 |
316 | const { queryKeysToUpdate, optimisticValuesByQueryKeys } = await updateQueriesWithOptimisticValue(
317 | {
318 | stateContext,
319 | updates,
320 | appContext,
321 | queryClient: appContext.queryClient!,
322 | clientTxId,
323 | optimisticValue,
324 | lookupAction,
325 | getOptimisticValue,
326 | uid,
327 | },
328 | );
329 |
330 | const inProgressMessage = extractParam(stateContext, inProgressNotificationMessage, appContext);
331 |
332 | let loadingToastId;
333 | if (inProgressMessage) {
334 | loadingToastId = toast.loading(inProgressMessage);
335 | }
336 | try {
337 | const operation: ApiOperationDef = {
338 | headers,
339 | url,
340 | queryParams,
341 | rawBody,
342 | method,
343 | body,
344 | payloadType,
345 | };
346 | const _onProgress = lookupAction(onProgress, uid, {
347 | eventName: "progress",
348 | });
349 | const result = await new RestApiProxy(appContext, apiInstance).execute({
350 | operation,
351 | params: stateContext,
352 | transactionId: clientTxId,
353 | resolveBindingExpressions,
354 | onProgress: _onProgress,
355 | });
356 |
357 | const onSuccessFn = lookupAction(onSuccess, uid, {
358 | eventName: "success",
359 | context: getCurrentState()
360 | });
361 | await onSuccessFn?.(result, stateContext["$param"]);
362 |
363 | updateQueriesWithResult(
364 | queryKeysToUpdate,
365 | optimisticValuesByQueryKeys,
366 | clientTxId,
367 | appContext.queryClient!,
368 | result,
369 | );
370 |
371 | if (resolvedInvalidates || !updates) {
372 | await invalidateQueries(resolvedInvalidates, appContext, state);
373 | }
374 | const completedMessage = extractParam(
375 | { ...stateContext, $result: result },
376 | completedNotificationMessage,
377 | appContext,
378 | );
379 | if (completedMessage) {
380 | toast.success(completedMessage, {
381 | id: loadingToastId,
382 | });
383 | } else if (loadingToastId) {
384 | toast.dismiss(loadingToastId);
385 | }
386 | return result;
387 | } catch (e) {
388 | if (optimisticValuesByQueryKeys.size) {
389 | await appContext.queryClient!.invalidateQueries();
390 | }
391 | const onErrorFn = lookupAction(onError, uid, {
392 | eventName: "error",
393 | });
394 | const result = await onErrorFn?.(e, stateContext["$param"]);
395 | const errorMessage = extractParam(
396 | { ...stateContext, $error: e },
397 | errorNotificationMessage,
398 | appContext,
399 | );
400 | if (errorMessage) {
401 | toast.error(errorMessage, {
402 | id: loadingToastId,
403 | });
404 | } else {
405 | if (loadingToastId) {
406 | toast.dismiss(loadingToastId);
407 | }
408 | if (result !== false) {
409 | //stop the error propagation, if the error handler returns false
410 | throw e;
411 | }
412 | }
413 | }
414 | }
415 |
416 | export const apiAction = createAction("callApi", callApi);
417 |
```
--------------------------------------------------------------------------------
/xmlui/src/components/Slot/Slot.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 children content in default slot", async ({ initTestBed, page }) => {
9 | await initTestBed(`
10 | <Custom>
11 | <Button label="Create" />
12 | <Button label="Edit" />
13 | <Button label="Delete" />
14 | </Custom>
15 | `, {
16 | components: [
17 | `
18 | <Component name="Custom">
19 | <Card>
20 | <H3>Use these actions</H3>
21 | <HStack>
22 | <Slot />
23 | </HStack>
24 | </Card>
25 | </Component>
26 | `
27 | ]
28 | });
29 |
30 | // Verify all passed children are rendered in the slot
31 | await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
32 | await expect(page.getByRole("button", { name: "Edit" })).toBeVisible();
33 | await expect(page.getByRole("button", { name: "Delete" })).toBeVisible();
34 |
35 | // Verify they are within the card structure from the component
36 | await expect(page.getByText("Use these actions")).toBeVisible();
37 | });
38 |
39 | test("renders default content when no children provided", async ({ initTestBed, page }) => {
40 | await initTestBed(`<ActionBar />`, {
41 | components: [
42 | `
43 | <Component name="ActionBar">
44 | <Card>
45 | <H3>Use these actions</H3>
46 | <HStack>
47 | <Slot>
48 | <Button label="Default" />
49 | </Slot>
50 | </HStack>
51 | </Card>
52 | </Component>
53 | `
54 | ]
55 | });
56 |
57 | // Verify default content is rendered when no children provided
58 | await expect(page.getByRole("button", { name: "Default" })).toBeVisible();
59 | await expect(page.getByText("Use these actions")).toBeVisible();
60 | });
61 |
62 | test("overrides default content with provided children", async ({ initTestBed, page }) => {
63 | await initTestBed(`
64 | <ActionBar>
65 | <Button label="Custom Button" />
66 | </ActionBar>
67 | `, {
68 | components: [
69 | `
70 | <Component name="ActionBar">
71 | <Card>
72 | <H3>Use these actions</H3>
73 | <HStack>
74 | <Slot>
75 | <Button label="Default" />
76 | </Slot>
77 | </HStack>
78 | </Card>
79 | </Component>
80 | `
81 | ]
82 | });
83 |
84 | // Verify provided children override default content
85 | await expect(page.getByRole("button", { name: "Custom Button" })).toBeVisible();
86 | await expect(page.getByRole("button", { name: "Default" })).not.toBeVisible();
87 | });
88 |
89 | test("works with named slots using property template syntax", async ({ initTestBed, page }) => {
90 | await initTestBed(`
91 | <ActionBar>
92 | <property name="headerTemplate">
93 | <H2>Click one of these actions</H2>
94 | </property>
95 | <property name="footerTemplate">
96 | <Text>Footer content goes here</Text>
97 | </property>
98 | <Button label="Create" />
99 | <Button label="Edit" />
100 | <Button label="Delete" />
101 | </ActionBar>
102 | `, {
103 | components: [
104 | `
105 | <Component name="ActionBar">
106 | <Card>
107 | <Slot name="headerTemplate">
108 | <H3>Use these actions</H3>
109 | </Slot>
110 | <HStack>
111 | <Slot>
112 | <Button label="Default" />
113 | </Slot>
114 | </HStack>
115 | <Slot name="footerTemplate" />
116 | </Card>
117 | </Component>
118 | `
119 | ]
120 | });
121 |
122 | // Verify named slots render their provided content
123 | await expect(page.getByRole("heading", { name: "Click one of these actions" })).toBeVisible();
124 | await expect(page.getByText("Footer content goes here")).toBeVisible();
125 |
126 | // Verify default header is not shown (overridden by headerTemplate)
127 | await expect(page.getByRole("heading", { name: "Use these actions" })).not.toBeVisible();
128 |
129 | // Verify default slot content (buttons)
130 | await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
131 | await expect(page.getByRole("button", { name: "Edit" })).toBeVisible();
132 | await expect(page.getByRole("button", { name: "Delete" })).toBeVisible();
133 | });
134 |
135 | test("renders default content for named slots when no template provided", async ({ initTestBed, page }) => {
136 | await initTestBed(`
137 | <ActionBar>
138 | <Button label="Main Button" />
139 | </ActionBar>
140 | `, {
141 | components: [
142 | `
143 | <Component name="ActionBar">
144 | <Card>
145 | <Slot name="headerTemplate">
146 | <H3>Default header</H3>
147 | </Slot>
148 | <HStack>
149 | <Slot />
150 | </HStack>
151 | <Slot name="footerTemplate">
152 | <Text>Default footer</Text>
153 | </Slot>
154 | </Card>
155 | </Component>
156 | `
157 | ]
158 | });
159 |
160 | // Verify default content for named slots is rendered
161 | await expect(page.getByRole("heading", { name: "Default header" })).toBeVisible();
162 | await expect(page.getByText("Default footer")).toBeVisible();
163 |
164 | // Verify main slot content
165 | await expect(page.getByRole("button", { name: "Main Button" })).toBeVisible();
166 | });
167 |
168 | test("supports template properties with context variables", async ({ initTestBed, page }) => {
169 | await initTestBed(`
170 | <ActionBar header="Action Bar Example">
171 | <property name="headerTemplate">
172 | <Text variant="title">{$processedHeader}</Text>
173 | </property>
174 | <Button label="Create" />
175 | <Button label="Edit" />
176 | <Button label="Delete" />
177 | </ActionBar>
178 | `, {
179 | components: [
180 | `
181 | <Component name="ActionBar">
182 | <Card var.transformedHeader="*** {$props.header.toUpperCase()} ***">
183 | <Slot name="headerTemplate" processedHeader="{transformedHeader}">
184 | <H3>{transformedHeader}</H3>
185 | </Slot>
186 | <HStack>
187 | <Slot>
188 | <Button label="Default" />
189 | </Slot>
190 | </HStack>
191 | </Card>
192 | </Component>
193 | `
194 | ]
195 | });
196 |
197 | // Verify template property is processed and passed to slot template
198 | await expect(page.getByText("*** ACTION BAR EXAMPLE ***")).toBeVisible();
199 |
200 | // Verify main slot content
201 | await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
202 | await expect(page.getByRole("button", { name: "Edit" })).toBeVisible();
203 | await expect(page.getByRole("button", { name: "Delete" })).toBeVisible();
204 | });
205 |
206 | test("works with multiple context variables in template properties", async ({ initTestBed, page }) => {
207 | await initTestBed(`
208 | <DataDisplay>
209 | <property name="contentTemplate">
210 | <Text>{$title}: {$value}</Text>
211 | </property>
212 | </DataDisplay>
213 | `, {
214 | components: [
215 | `
216 | <Component name="DataDisplay">
217 | <Slot name="contentTemplate" title="Score" value="{42}">
218 | <Text>No data available</Text>
219 | </Slot>
220 | </Component>
221 | `
222 | ]
223 | });
224 |
225 | // Verify multiple context variables work
226 | await expect(page.getByText("Score: 42")).toBeVisible();
227 | await expect(page.getByText("No data available")).not.toBeVisible();
228 | });
229 |
230 | test("supports complex nested content in slots", async ({ initTestBed, page }) => {
231 | await initTestBed(`
232 | <CustomCard>
233 | <property name="headerTemplate">
234 | <HStack>
235 | <Icon name="star" />
236 | <Text fontWeight="bold">Custom Header</Text>
237 | </HStack>
238 | </property>
239 | <VStack>
240 | <Text>Line 1</Text>
241 | <Text>Line 2</Text>
242 | <Button label="Action" />
243 | </VStack>
244 | </CustomCard>
245 | `, {
246 | components: [
247 | `
248 | <Component name="CustomCard">
249 | <Card>
250 | <Slot name="headerTemplate">
251 | <H3>Default Header</H3>
252 | </Slot>
253 | <ContentSeparator />
254 | <Slot />
255 | </Card>
256 | </Component>
257 | `
258 | ]
259 | });
260 |
261 | // Verify complex nested content renders correctly
262 | await expect(page.getByText("Custom Header")).toBeVisible();
263 | await expect(page.getByText("Line 1")).toBeVisible();
264 | await expect(page.getByText("Line 2")).toBeVisible();
265 | await expect(page.getByRole("button", { name: "Action" })).toBeVisible();
266 |
267 | // Verify default header is not shown
268 | await expect(page.getByRole("heading", { name: "Default Header" })).not.toBeVisible();
269 | });
270 |
271 | test("maintains proper rendering order with multiple slots", async ({ initTestBed, page }) => {
272 | await initTestBed(`
273 | <MultiSlotComponent>
274 | <property name="topTemplate">
275 | <Text testId="top">Top Content</Text>
276 | </property>
277 | <property name="bottomTemplate">
278 | <Text testId="bottom">Bottom Content</Text>
279 | </property>
280 | <Text testId="middle">Middle Content</Text>
281 | </MultiSlotComponent>
282 | `, {
283 | components: [
284 | `
285 | <Component name="MultiSlotComponent">
286 | <VStack>
287 | <Slot name="topTemplate" />
288 | <Slot />
289 | <Slot name="bottomTemplate" />
290 | </VStack>
291 | </Component>
292 | `
293 | ]
294 | });
295 |
296 | // Verify all content is rendered
297 | await expect(page.getByTestId("top")).toBeVisible();
298 | await expect(page.getByTestId("middle")).toBeVisible();
299 | await expect(page.getByTestId("bottom")).toBeVisible();
300 |
301 | // Verify rendering order by checking positions
302 | const topElement = page.getByTestId("top");
303 | const middleElement = page.getByTestId("middle");
304 | const bottomElement = page.getByTestId("bottom");
305 |
306 | const topBox = await topElement.boundingBox();
307 | const middleBox = await middleElement.boundingBox();
308 | const bottomBox = await bottomElement.boundingBox();
309 |
310 | expect(topBox?.y).toBeLessThan(middleBox?.y || 0);
311 | expect(middleBox?.y).toBeLessThan(bottomBox?.y || 0);
312 | });
313 |
314 | test("works with dynamic context in template properties", async ({ initTestBed, page }) => {
315 | await initTestBed(`
316 | <ContextComponent>
317 | <property name="messageTemplate">
318 | <Text>{$message}</Text>
319 | </property>
320 | </ContextComponent>
321 | `, {
322 | components: [
323 | `
324 | <Component name="ContextComponent" var.currentMessage="Initial Message">
325 | <VStack>
326 | <Button label="Update Message" onClick="currentMessage = 'Updated Message'" />
327 | <Slot name="messageTemplate" message="{currentMessage}">
328 | <Text>No message</Text>
329 | </Slot>
330 | </VStack>
331 | </Component>
332 | `
333 | ]
334 | });
335 |
336 | // Verify initial context value
337 | await expect(page.getByText("Initial Message")).toBeVisible();
338 |
339 | // Update the context and verify template updates
340 | await page.getByRole("button", { name: "Update Message" }).click();
341 | await expect(page.getByText("Updated Message")).toBeVisible();
342 | await expect(page.getByText("Initial Message")).not.toBeVisible();
343 | });
344 |
345 | test("handles empty slots gracefully", async ({ initTestBed, page }) => {
346 | await initTestBed(`
347 | <EmptySlotComponent>
348 | </EmptySlotComponent>
349 | `, {
350 | components: [
351 | `
352 | <Component name="EmptySlotComponent">
353 | <Card>
354 | <Text>Before slot</Text>
355 | <Slot />
356 | <Text>After slot</Text>
357 | </Card>
358 | </Component>
359 | `
360 | ]
361 | });
362 |
363 | // Verify component renders without errors when slot is empty
364 | await expect(page.getByText("Before slot")).toBeVisible();
365 | await expect(page.getByText("After slot")).toBeVisible();
366 | });
367 |
368 | test("supports slot with no default content", async ({ initTestBed, page }) => {
369 | await initTestBed(`
370 | <MinimalComponent>
371 | <Text>Provided content</Text>
372 | </MinimalComponent>
373 | `, {
374 | components: [
375 | `
376 | <Component name="MinimalComponent">
377 | <VStack>
378 | <Text>Component content</Text>
379 | <Slot name="contentTemplate" />
380 | <Slot />
381 | </VStack>
382 | </Component>
383 | `
384 | ]
385 | });
386 |
387 | // Verify provided content renders in default slot
388 | await expect(page.getByText("Provided content")).toBeVisible();
389 |
390 | // Verify component structure renders without errors
391 | await expect(page.getByText("Component content")).toBeVisible();
392 | });
393 | });
394 |
```
--------------------------------------------------------------------------------
/xmlui/src/components-core/theming/ThemeProvider.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { useCallback, useEffect, useMemo, useState } from "react";
2 | import {
3 | generateBaseFontSizes,
4 | generateBaseSpacings,
5 | generateBaseTones,
6 | generateBorderSegments,
7 | generateButtonTones,
8 | generatePaddingSegments,
9 | generateTextFontSizes,
10 | resolveThemeVar,
11 | } from "./transformThemeVars";
12 | import { normalizePath } from "../utils/misc";
13 | import { matchThemeVar } from "../theming/hvar";
14 | import { ThemeContext, ThemesContext } from "../theming/ThemeContext";
15 | import themeVars, { getVarKey } from "../theming/themeVars";
16 | import { EMPTY_ARRAY, EMPTY_OBJECT } from "../constants";
17 | import { collectThemeChainByExtends } from "../theming/extendThemeUtils";
18 | import { useComponentRegistry } from "../../components/ComponentRegistryContext";
19 | import {
20 | XmlUiCyanThemeDefinition,
21 | XmlUiGrayThemeDefinition,
22 | XmlUiGreenThemeDefinition,
23 | XmlUiOrangeThemeDefinition,
24 | XmlUiPurpleThemeDefinition,
25 | XmlUiRedThemeDefinition,
26 | XmlUiThemeDefinition,
27 | } from "../theming/themes/xmlui";
28 | import { useIsomorphicLayoutEffect } from "../utils/hooks";
29 | import type {
30 | AppThemes,
31 | FontDef,
32 | ThemeDefinition,
33 | ThemeScope,
34 | ThemeTone,
35 | } from "../../abstractions/ThemingDefs";
36 | import { omit } from "lodash-es";
37 | import { ThemeToneKeys } from "./utils";
38 | import { useDomRoot } from "./StyleContext";
39 |
40 | export function useCompiledTheme(
41 | activeTheme: ThemeDefinition | undefined,
42 | activeTone: ThemeTone,
43 | themes: ThemeDefinition[] = EMPTY_ARRAY,
44 | resources: Record<string, string> = EMPTY_OBJECT,
45 | resourceMap: Record<string, string> = EMPTY_OBJECT,
46 | ) {
47 | const componentRegistry = useComponentRegistry();
48 | const { componentThemeVars, componentDefaultThemeVars } = componentRegistry;
49 |
50 | const themeDefChain = useMemo(() => {
51 | if (activeTheme) {
52 | return collectThemeChainByExtends(activeTheme, themes, componentDefaultThemeVars);
53 | }
54 | return undefined;
55 | }, [activeTheme, componentDefaultThemeVars, themes]);
56 |
57 | const allResources = useMemo(() => {
58 | let mergedResources: ThemeDefinition["resources"] = {};
59 | themeDefChain?.forEach((theme) => {
60 | mergedResources = {
61 | ...mergedResources,
62 | ...theme.resources,
63 | ...theme.tones?.[activeTone]?.resources,
64 | };
65 | });
66 | return {
67 | ...resources,
68 | ...mergedResources,
69 | };
70 | }, [themeDefChain, resources, activeTone]);
71 |
72 | const allFonts = useMemo(() => {
73 | const ret: Array<FontDef> = [];
74 | Object.entries(allResources).forEach(([key, value]) => {
75 | if (key.startsWith("font.")) {
76 | ret?.push(value as FontDef);
77 | }
78 | });
79 | return ret;
80 | }, [allResources]);
81 |
82 | const getResourceUrl = useCallback(
83 | (resourceString?: string) => {
84 | let resourceUrl = resourceString;
85 | if (resourceString?.startsWith("resource:")) {
86 | const resourceName = resourceString?.replace("resource:", "");
87 | resourceUrl = allResources[resourceName] as string;
88 | }
89 | if (!resourceUrl) {
90 | return resourceUrl;
91 | }
92 | if (resourceMap[resourceUrl]) {
93 | return resourceMap[resourceUrl];
94 | }
95 | if (resourceUrl.startsWith("/") && resourceMap[resourceUrl.substring(1)]) {
96 | return resourceMap[resourceUrl.substring(1)];
97 | }
98 | return normalizePath(resourceUrl);
99 | },
100 | [allResources, resourceMap],
101 | );
102 |
103 | const fontLinks: Array<string> = useMemo(() => {
104 | return (allFonts?.filter((theme) => typeof theme === "string") || []) as Array<string>;
105 | }, [allFonts]);
106 |
107 | const themeDefChainVars = useMemo(() => {
108 | if (!themeDefChain?.length) {
109 | return [];
110 | }
111 | let mergedThemeVars = {};
112 | themeDefChain?.forEach((theme) => {
113 | mergedThemeVars = {
114 | ...mergedThemeVars,
115 | ...omit(theme.themeVars, "light", "dark"),
116 | ...(theme.themeVars?.[activeTone] as unknown as Record<string, string>),
117 | ...theme.tones?.[activeTone]?.themeVars,
118 | };
119 | });
120 |
121 | //we put the generated theme vars before the last item in the chain
122 | const resultedTheme = [
123 | ...themeDefChain
124 | .map((themeDef) => ({
125 | ...omit(themeDef.themeVars, "light", "dark"),
126 | ...(themeDef.themeVars?.[activeTone] as unknown as Record<string, string>),
127 | ...themeDef.tones?.[activeTone]?.themeVars,
128 | }))
129 | .slice(0, themeDefChain.length - 1),
130 | {
131 | ...generateBaseSpacings(mergedThemeVars),
132 | ...generateBaseFontSizes(mergedThemeVars),
133 | ...generatePaddingSegments(mergedThemeVars),
134 | ...generateBorderSegments(mergedThemeVars),
135 | ...generateBaseTones(mergedThemeVars),
136 | ...generateButtonTones(mergedThemeVars),
137 | },
138 | {
139 | ...omit(themeDefChain[themeDefChain.length - 1].themeVars, "light", "dark"),
140 | ...generateTextFontSizes(mergedThemeVars),
141 | ...(themeDefChain[themeDefChain.length - 1].themeVars?.[activeTone] as unknown as Record<
142 | string,
143 | string
144 | >),
145 | ...themeDefChain[themeDefChain.length - 1].tones?.[activeTone]?.themeVars,
146 | },
147 | ];
148 | return resultedTheme;
149 | }, [activeTone, themeDefChain]);
150 |
151 | const allThemeVarsWithResolvedHierarchicalVars = useMemo(() => {
152 | let mergedThemeVars: Record<string, string> = {};
153 |
154 | themeDefChainVars?.forEach((theme) => {
155 | theme = generatePaddingSegments(theme);
156 | theme = generateBorderSegments(theme);
157 | mergedThemeVars = { ...mergedThemeVars, ...theme };
158 | });
159 |
160 | const resolvedThemeVarsFromChains: Record<string, string> = {};
161 |
162 | new Set([...Object.keys(themeVars.themeVars), ...componentThemeVars]).forEach((themeVar) => {
163 | const result = matchThemeVar(themeVar, themeDefChainVars);
164 | if (
165 | result &&
166 | result.forValue &&
167 | result.matchedValue &&
168 | result.forValue !== result.matchedValue
169 | ) {
170 | resolvedThemeVarsFromChains[result.forValue] = `$${result.matchedValue}`;
171 | }
172 | });
173 |
174 | return resolveThemeVarsWithCssVars({
175 | ...mergedThemeVars,
176 | ...resolvedThemeVarsFromChains,
177 | });
178 | }, [componentThemeVars, themeDefChainVars]);
179 |
180 | const themeCssVars = useMemo(() => {
181 | const ret: Record<string, string> = {};
182 | Object.entries(allThemeVarsWithResolvedHierarchicalVars).forEach(([key, value]) => {
183 | const themeKey = `--${themeVars.keyPrefix}-${key}`;
184 | if (value) {
185 | ret[themeKey] = value;
186 | }
187 | });
188 | return ret;
189 | }, [allThemeVarsWithResolvedHierarchicalVars]);
190 |
191 | const getThemeVar = useCallback(
192 | (varName: string) => {
193 | return resolveThemeVar(varName, allThemeVarsWithResolvedHierarchicalVars);
194 | },
195 | [allThemeVarsWithResolvedHierarchicalVars],
196 | );
197 |
198 | useEffect(() => {
199 | allFonts.forEach(async (font) => {
200 | if (typeof font !== "string") {
201 | const resolvedSrc = getResourceUrl(font.src);
202 | let src = `url(${resolvedSrc})`;
203 | if (font.format) {
204 | src = `${src} format('${font.format}')`;
205 | }
206 | const ff = new FontFace(font.fontFamily, src, {
207 | weight: font.fontWeight,
208 | style: font.fontStyle,
209 | display: font.fontDisplay as any,
210 | });
211 | try {
212 | const loadedFontFace = await ff.load();
213 | document.fonts.add(loadedFontFace);
214 | } catch (e) {
215 | console.error("loading fonts failed", e);
216 | }
217 | }
218 | });
219 | }, [themeDefChain, getResourceUrl, allFonts]);
220 |
221 | return {
222 | getResourceUrl,
223 | fontLinks,
224 | allThemeVarsWithResolvedHierarchicalVars,
225 | themeCssVars,
226 | getThemeVar,
227 | };
228 | }
229 |
230 | export const builtInThemes: Array<ThemeDefinition> = [
231 | XmlUiThemeDefinition,
232 | XmlUiGreenThemeDefinition,
233 | XmlUiGrayThemeDefinition,
234 | XmlUiOrangeThemeDefinition,
235 | XmlUiPurpleThemeDefinition,
236 | XmlUiCyanThemeDefinition,
237 | XmlUiRedThemeDefinition,
238 | /*SolidThemeDefinition,*/
239 | ];
240 |
241 | type ThemeProviderProps = {
242 | children?: React.ReactNode;
243 | themes?: Array<ThemeDefinition>;
244 | defaultTheme?: string;
245 | defaultTone?: ThemeTone;
246 | resources?: Record<string, string>;
247 | resourceMap?: Record<string, string>;
248 | localThemes?: Record<string, string>;
249 | };
250 |
251 | // theme-overriding properties change.
252 | function ThemeProvider({
253 | children,
254 | themes: custThemes = EMPTY_ARRAY,
255 | defaultTheme = "xmlui",
256 | defaultTone = "light",
257 | resources = EMPTY_OBJECT,
258 | resourceMap = EMPTY_OBJECT,
259 | localThemes = EMPTY_OBJECT,
260 | }: ThemeProviderProps) {
261 | const [activeThemeTone, setActiveThemeTone] = useState<ThemeTone>(() => {
262 | if (!defaultTone) {
263 | return ThemeToneKeys[0];
264 | }
265 | return defaultTone;
266 | });
267 |
268 | useEffect(() => {
269 | if (defaultTone) {
270 | setActiveThemeTone(defaultTone);
271 | }
272 | }, [defaultTone]);
273 |
274 | const themes: Array<ThemeDefinition> = useMemo(() => {
275 | return [...custThemes, ...builtInThemes];
276 | }, [custThemes]);
277 |
278 | const availableThemeIds = useMemo(() => {
279 | return [...new Set(themes.map((theme) => theme.id))];
280 | }, [themes]);
281 |
282 | const [activeThemeId, setActiveThemeId] = useState<string>(() => {
283 | if (!defaultTheme) {
284 | return availableThemeIds[0];
285 | }
286 | return defaultTheme;
287 | });
288 |
289 | useIsomorphicLayoutEffect(() => {
290 | //we sync the activeThemeId with the default theme (mostly for HMR)
291 | if (defaultTheme && availableThemeIds.includes(defaultTheme)) {
292 | setActiveThemeId(defaultTheme);
293 | } else {
294 | setActiveThemeId(availableThemeIds[0]);
295 | }
296 | }, [availableThemeIds, defaultTheme]);
297 |
298 | const activeTheme: ThemeDefinition = useMemo(() => {
299 | let foundTheme: ThemeDefinition;
300 | if (activeThemeId) {
301 | foundTheme = themes.find((theme) => theme.id === activeThemeId);
302 | }
303 | if (!foundTheme) {
304 | throw new Error(`Theme ${activeThemeId} not found, available themes: ${availableThemeIds}`);
305 | }
306 | return foundTheme;
307 | }, [activeThemeId, availableThemeIds, themes]);
308 |
309 | const { allThemeVarsWithResolvedHierarchicalVars, themeCssVars, getResourceUrl, getThemeVar } =
310 | useCompiledTheme(activeTheme, activeThemeTone, themes, resources, resourceMap);
311 |
312 | const domRoot = useDomRoot();
313 | const [root, setRoot] = useState(null);
314 |
315 | useIsomorphicLayoutEffect(() => {
316 | if (typeof document !== "undefined") {
317 | if(domRoot instanceof ShadowRoot){
318 | setRoot(domRoot.getElementById("nested-app-root"));
319 | } else {
320 | setRoot(document.body);
321 | }
322 | }
323 | }, [domRoot]);
324 |
325 | const themeValue = useMemo(() => {
326 | const themeVal: AppThemes = {
327 | themes,
328 | resources,
329 | resourceMap,
330 | activeThemeId,
331 | activeThemeTone,
332 | setActiveThemeId,
333 | setActiveThemeTone,
334 | availableThemeIds,
335 | activeTheme,
336 | toggleThemeTone: () => setActiveThemeTone(activeThemeTone === "light" ? "dark" : "light"),
337 | };
338 | return themeVal;
339 | }, [
340 | activeTheme,
341 | activeThemeId,
342 | activeThemeTone,
343 | availableThemeIds,
344 | resourceMap,
345 | resources,
346 | themes,
347 | ]);
348 |
349 | const currentThemeContextValue = useMemo(() => {
350 | const themeVal: ThemeScope = {
351 | root,
352 | setRoot,
353 | activeThemeId,
354 | activeThemeTone: activeThemeTone,
355 | activeTheme,
356 | themeStyles: themeCssVars,
357 | themeVars: allThemeVarsWithResolvedHierarchicalVars,
358 | getResourceUrl,
359 | getThemeVar,
360 | };
361 | return themeVal;
362 | }, [
363 | activeTheme,
364 | activeThemeId,
365 | activeThemeTone,
366 | allThemeVarsWithResolvedHierarchicalVars,
367 | getResourceUrl,
368 | getThemeVar,
369 | root,
370 | themeCssVars,
371 | ]);
372 |
373 | return (
374 | <ThemesContext.Provider value={themeValue}>
375 | <ThemeContext.Provider value={currentThemeContextValue}>{children}</ThemeContext.Provider>
376 | </ThemesContext.Provider>
377 | );
378 | }
379 |
380 | function resolveThemeVarsWithCssVars(theme?: Record<string, string>) {
381 | if (!theme) {
382 | return {};
383 | }
384 | const ret: Record<string, string> = {};
385 | Object.keys(theme).forEach((key) => {
386 | ret[key] = resolveThemeVarToCssVars(key, theme);
387 | });
388 | return ret;
389 |
390 | function resolveThemeVarToCssVars(varName: string, theme: Record<string, string>) {
391 | const value = theme[varName];
392 | if (typeof value === "string" && value.includes("$")) {
393 | return replaceThemeVar(value);
394 | }
395 | return value;
396 | }
397 |
398 | function replaceThemeVar(input: string) {
399 | const regex = /\$([a-zA-Z0-9_-]+)/gi;
400 | const matches = input.matchAll(regex);
401 |
402 | //we go from 1, because result[1] is the whole stuff
403 | if (matches) {
404 | let ret = input;
405 | for (const match of matches) {
406 | const varName = match[1];
407 | if (varName) {
408 | ret = ret.replace(match[0], `var(${getVarKey(varName)})`);
409 | }
410 | }
411 | return ret;
412 | }
413 |
414 | return input;
415 | }
416 | }
417 |
418 | export default ThemeProvider;
419 |
```
--------------------------------------------------------------------------------
/xmlui/src/components/HtmlTags/HtmlTags.module.scss:
--------------------------------------------------------------------------------
```scss
1 | @use "../../components-core/theming/themes" as t;
2 |
3 | // --- table
4 |
5 | $themeVarsTable: ();
6 |
7 | @function createThemeVarTable($componentVariable) {
8 | $themeVarsTable: t.appendThemeVar($themeVarsTable, $componentVariable) !global;
9 | @return t.getThemeVar($themeVarsTable, $componentVariable);
10 | }
11 |
12 | $textColor-HtmlTable: createThemeVarTable("textColor-HtmlTable");
13 | $backgroundColor-HtmlTable: createThemeVarTable("backgroundColor-HtmlTable");
14 | $fontFamily-HtmlTable: createThemeVarTable("fontFamily-HtmlTable");
15 | $fontSize-HtmlTable: createThemeVarTable("fontSize-HtmlTable");
16 | $fontWeight-HtmlTable: createThemeVarTable("fontWeight-HtmlTable");
17 | $textTransform-HtmlTable: createThemeVarTable("textTransform-HtmlTable");
18 | $marginTop-HtmlTable: createThemeVarTable("marginTop-HtmlTable");
19 | $marginBottom-HtmlTable: createThemeVarTable("marginBottom-HtmlTable");
20 | $width-HtmlTable: createThemeVarTable("width-HtmlTable");
21 | $themeVarsTable: t.composeBorderVars($themeVarsTable, "HtmlTable");
22 | $themeVarsTable: t.composePaddingVars($themeVarsTable, "HtmlTable");
23 |
24 | @layer components {
25 | .htmlTable {
26 | background-color: $backgroundColor-HtmlTable;
27 | color: $textColor-HtmlTable;
28 | font-family: $fontFamily-HtmlTable;
29 | font-size: $fontSize-HtmlTable;
30 | font-weight: $fontWeight-HtmlTable;
31 | text-transform: $textTransform-HtmlTable;
32 | margin-top: $marginTop-HtmlTable;
33 | margin-bottom: $marginBottom-HtmlTable;
34 | width: $width-HtmlTable;
35 | @include t.borderVars($themeVarsTable, "HtmlTable");
36 | @include t.paddingVars($themeVarsTable, "HtmlTable");
37 | }
38 | }
39 |
40 | // --- thead
41 |
42 | $themeVarsThead: ();
43 |
44 | @function createThemeVarThead($componentVariable) {
45 | $themeVarsThead: t.appendThemeVar($themeVarsThead, $componentVariable) !global;
46 | @return t.getThemeVar($themeVarsThead, $componentVariable);
47 | }
48 |
49 | $backgroundColor-HtmlThead: createThemeVarThead("backgroundColor-HtmlThead");
50 | $textColor-HtmlThead: createThemeVarThead("textColor-HtmlThead");
51 | $fontWeight-HtmlThead: createThemeVarThead("fontWeight-HtmlThead");
52 | $fontSize-HtmlThead: createThemeVarThead("fontSize-HtmlThead");
53 | $textTransform-HtmlThead: createThemeVarThead("textTransform-HtmlThead");
54 | $themeVarsThead: t.composeBorderVars($themeVarsThead, "HtmlThead");
55 | $themeVarsThead: t.composePaddingVars($themeVarsThead, "HtmlThead");
56 |
57 | @layer components {
58 | .htmlThead {
59 | background-color: $backgroundColor-HtmlThead;
60 | color: $textColor-HtmlThead;
61 | font-weight: $fontWeight-HtmlThead;
62 | font-size: $fontSize-HtmlThead;
63 | text-transform: $textTransform-HtmlThead;
64 | @include t.borderVars($themeVarsThead, "HtmlThead");
65 | @include t.paddingVars($themeVarsThead, "HtmlThead");
66 | }
67 | }
68 |
69 | // --- tbody
70 |
71 | $themeVarsTbody: ();
72 |
73 | @function createThemeVarTbody($componentVariable) {
74 | $themeVarsTbody: t.appendThemeVar($themeVarsTbody, $componentVariable) !global;
75 | @return t.getThemeVar($themeVarsTbody, $componentVariable);
76 | }
77 |
78 | $backgroundColor-HtmlTbody: createThemeVarTbody("backgroundColor-HtmlTbody");
79 | $textColor-HtmlTbody: createThemeVarTbody("textColor-HtmlTbody");
80 | $textAlign-HtmlTbody: createThemeVarTbody("textAlign-HtmlTbody");
81 | $verticalAlignment-HtmlTbody: createThemeVarTbody("verticalAlignment-HtmlTbody");
82 | $textTransform-HtmlTbody: createThemeVarTbody("textTransform-HtmlTbody");
83 |
84 | @layer components {
85 | .htmlTbody {
86 | background-color: $backgroundColor-HtmlTbody;
87 | color: $textColor-HtmlTbody;
88 | text-align: $textAlign-HtmlTbody;
89 | vertical-align: $verticalAlignment-HtmlTbody;
90 | text-transform: $textTransform-HtmlTbody;
91 | }
92 | }
93 |
94 | // --- tfoot
95 |
96 | $themeVarsTfoot: ();
97 |
98 | @function createThemeVarTfoot($componentVariable) {
99 | $themeVarsTfoot: t.appendThemeVar($themeVarsTfoot, $componentVariable) !global;
100 | @return t.getThemeVar($themeVarsTfoot, $componentVariable);
101 | }
102 |
103 | $backgroundColor-HtmlTfoot: createThemeVarTfoot("backgroundColor-HtmlTfoot");
104 | $textColor-HtmlTfoot: createThemeVarTfoot("textColor-HtmlTfoot");
105 |
106 | @layer components {
107 | .htmlTfoot {
108 | background-color: $backgroundColor-HtmlTfoot;
109 | color: $textColor-HtmlTfoot;
110 | }
111 | }
112 |
113 | // --- th
114 |
115 | $themeVarsTh: ();
116 |
117 | @function createThemeVarTh($componentVariable) {
118 | $themeVarsTh: t.appendThemeVar($themeVarsTh, $componentVariable) !global;
119 | @return t.getThemeVar($themeVarsTh, $componentVariable);
120 | }
121 |
122 | $backgroundColor-HtmlTh: createThemeVarTh("backgroundColor-HtmlTh");
123 | $textColor-HtmlTh: createThemeVarTh("textColor-HtmlTh");
124 | $fontWeight-HtmlTh: createThemeVarTh("fontWeight-HtmlTh");
125 | $fontSize-HtmlTh: createThemeVarTh("fontSize-HtmlTh");
126 | $backgroundColor-HtmlTh--hover: createThemeVarTh("backgroundColor-HtmlTh--hover");
127 | $themeVarsTh: t.composeBorderVars($themeVarsTh, "HtmlTh");
128 | $themeVarsTh: t.composePaddingVars($themeVarsTh, "HtmlTh");
129 |
130 | @layer components {
131 | .htmlTh {
132 | background-color: $backgroundColor-HtmlTh;
133 | color: $textColor-HtmlTh;
134 | font-weight: $fontWeight-HtmlTh;
135 | font-size: $fontSize-HtmlTh;
136 | @include t.borderVars($themeVarsTh, "HtmlTh");
137 | @include t.paddingVars($themeVarsTh, "HtmlTh");
138 |
139 | &:hover {
140 | background-color: $backgroundColor-HtmlTh--hover;
141 | }
142 | }
143 | }
144 |
145 | // --- tr
146 |
147 | $themeVarsTr: ();
148 |
149 | @function createThemeVarTr($componentVariable) {
150 | $themeVarsTr: t.appendThemeVar($themeVarsTr, $componentVariable) !global;
151 | @return t.getThemeVar($themeVarsTr, $componentVariable);
152 | }
153 |
154 | $backgroundColor-HtmlTr: createThemeVarTr("backgroundColor-HtmlTr");
155 | $backgroundColor-HtmlTr--hover: createThemeVarTr("backgroundColor-HtmlTr--hover");
156 | $backgroundColor-even-HtmlTr: createThemeVarTr("backgroundColor-even-HtmlTr");
157 | $textColor-HtmlTr: createThemeVarTr("textColor-HtmlTr");
158 | $textColor-HtmlTr--hover: createThemeVarTr("textColor-HtmlTr--hover");
159 | $fontSize-HtmlTr: createThemeVarTr("fontSize-HtmlTr");
160 | $fontWeight-HtmlTr: createThemeVarTr("fontWeight-HtmlTr");
161 | $themeVarsTr: t.composeBorderVars($themeVarsTr, "HtmlTr");
162 |
163 | @layer components {
164 | .htmlTr {
165 | background-color: $backgroundColor-HtmlTr;
166 | color: $textColor-HtmlTr;
167 | font-weight: $fontWeight-HtmlTr;
168 | font-size: $fontSize-HtmlTr;
169 | @include t.borderVars($themeVarsTr, "HtmlTr");
170 |
171 | &:nth-child(even) {
172 | background-color: $backgroundColor-even-HtmlTr;
173 | }
174 |
175 | // Apply hover effect for rows with mixed th, td elements
176 | &:hover {
177 | background-color: $backgroundColor-HtmlTr--hover;
178 | color: $textColor-HtmlTr--hover;
179 | }
180 |
181 | // Remove hover effect for rows with only th elements
182 | &:hover:not(:has(:not(th))) {
183 | background-color: initial;
184 | }
185 | }
186 | }
187 |
188 | // --- td
189 |
190 | $themeVarsTd: ();
191 |
192 | @function createThemeVarTd($componentVariable) {
193 | $themeVarsTd: t.appendThemeVar($themeVarsTd, $componentVariable) !global;
194 | @return t.getThemeVar($themeVarsTd, $componentVariable);
195 | }
196 |
197 | $backgroundColor-HtmlTd: createThemeVarTd("backgroundColor-HtmlTd");
198 | $text-align-HtmlTd: createThemeVarTd("text-align-HtmlTd");
199 | $verticalAlignment-HtmlTd: createThemeVarTd("verticalAlignment-HtmlTd");
200 | $fontSize-HtmlTd: createThemeVarTd("fontSize-HtmlTd");
201 | $fontWeight-HtmlTd: createThemeVarTd("fontWeight-HtmlTd");
202 | $themeVarsTd: t.composeBorderVars($themeVarsTd, "HtmlTd");
203 | $themeVarsTd: t.composePaddingVars($themeVarsTd, "HtmlTd");
204 |
205 | @layer components {
206 | .htmlTd {
207 | background-color: $backgroundColor-HtmlTd;
208 | text-align: $text-align-HtmlTd;
209 | vertical-align: $verticalAlignment-HtmlTd;
210 | font-size: $fontSize-HtmlTd;
211 | font-weight: $fontWeight-HtmlTd;
212 | @include t.borderVars($themeVarsTd, "HtmlTd");
213 | @include t.paddingVars($themeVarsTd, "HtmlTd");
214 | }
215 | }
216 |
217 | // --- List styling (for both ul and ol)
218 |
219 | $themeVarsList: ();
220 |
221 | @function createThemeVarList($componentVariable) {
222 | $themeVarsList: t.appendThemeVar($themeVarsList, $componentVariable) !global;
223 | @return t.getThemeVar($themeVarsList, $componentVariable);
224 | }
225 |
226 | $themeVarsList: t.composeBorderVars($themeVarsList, "HtmlOl");
227 | $themeVarsList: t.composePaddingVars($themeVarsList, "HtmlOl");
228 | $marginTop-HtmlOl: createThemeVarList("marginTop-HtmlOl");
229 | $marginBottom-HtmlOl: createThemeVarList("marginBottom-HtmlOl");
230 |
231 | $themeVarsList: t.composeBorderVars($themeVarsList, "HtmlUl");
232 | $themeVarsList: t.composePaddingVars($themeVarsList, "HtmlUl");
233 | $marginTop-HtmlUl: createThemeVarList("marginTop-HtmlUl");
234 | $marginBottom-HtmlUl: createThemeVarList("marginBottom-HtmlUl");
235 |
236 | $themeVarsList: t.composeBorderVars($themeVarsList, "HtmlLi");
237 | $themeVarsList: t.composePaddingVars($themeVarsList, "HtmlLi");
238 | $marginLeft-HtmlLi: createThemeVarList("marginLeft-HtmlLi");
239 | $marginTop-HtmlLi: createThemeVarList("marginTop-HtmlLi");
240 | $marginBottom-HtmlLi: createThemeVarList("marginBottom-HtmlLi");
241 | $listStyleType-HtmlLi: createThemeVarList("listStyleType-HtmlLi");
242 |
243 | @layer components {
244 | // Apply to both ul and ol elements
245 | .htmlUl {
246 | margin-top: $marginTop-HtmlUl;
247 | margin-bottom: $marginBottom-HtmlUl;
248 | @include t.borderVars($themeVarsList, "HtmlUl");
249 | @include t.paddingVars($themeVarsList, "HtmlUl");
250 | }
251 |
252 | .htmlOl {
253 | margin-top: $marginTop-HtmlOl;
254 | margin-bottom: $marginBottom-HtmlOl;
255 | @include t.borderVars($themeVarsList, "HtmlOl");
256 | @include t.paddingVars($themeVarsList, "HtmlOl");
257 | }
258 |
259 | .htmlLi {
260 | margin-top: $marginTop-HtmlLi;
261 | margin-bottom: $marginBottom-HtmlLi;
262 | list-style-type: $listStyleType-HtmlLi;
263 | margin-left: $marginLeft-HtmlLi;
264 | @include t.borderVars($themeVarsList, "HtmlLi");
265 | @include t.paddingVars($themeVarsList, "HtmlLi");
266 | }
267 |
268 | }
269 | /*
270 | // Compress when nested
271 | li .htmlUl,
272 | li .htmlOl {
273 | margin-top: 0;
274 | margin-bottom: 0;
275 | }
276 | */
277 |
278 | // --- Heading styling
279 | $themeVarsHeading: ();
280 |
281 | @function createThemeVarHeading($componentVariable) {
282 | $themeVarsHeading: t.appendThemeVar($themeVarsHeading, $componentVariable) !global;
283 | @return t.getThemeVar($themeVarsHeading, $componentVariable);
284 | }
285 |
286 | $marginTop-HtmlHeading: createThemeVarHeading("marginTop-HtmlHeading");
287 | $marginBottom-HtmlHeading: createThemeVarHeading("marginBottom-HtmlHeading");
288 |
289 | @layer components {
290 | .htmlH1,
291 | .htmlH2,
292 | .htmlH3,
293 | .htmlH4,
294 | .htmlH5,
295 | .htmlH6 {
296 | margin-top: $marginTop-HtmlHeading;
297 | margin-bottom: $marginBottom-HtmlHeading;
298 | }
299 | }
300 |
301 | // --- Video
302 |
303 | $themeVarsVideo: ();
304 |
305 | @function createThemeVarVideo($componentVariable) {
306 | $themeVarsVideo: t.appendThemeVar($themeVarsVideo, $componentVariable) !global;
307 | @return t.getThemeVar($themeVarsVideo, $componentVariable);
308 | }
309 |
310 | $marginTop-HtmlVideo: createThemeVarVideo("marginTop-HtmlVideo");
311 | $marginBottom-HtmlVideo: createThemeVarVideo("marginBottom-HtmlVideo");
312 |
313 | @layer components {
314 | .htmlVideo {
315 | margin-top: $marginTop-HtmlVideo;
316 | margin-bottom: $marginBottom-HtmlVideo;
317 | }
318 | }
319 |
320 | // --- Details
321 |
322 | $themeVarsDetails: ();
323 |
324 | @function createThemeVarDetails($componentVariable) {
325 | $themeVarsDetails: t.appendThemeVar($themeVarsDetails, $componentVariable) !global;
326 | @return t.getThemeVar($themeVarsDetails, $componentVariable);
327 | }
328 |
329 | $themeVarsDetails: t.composeBorderVars($themeVarsDetails, "HtmlDetails");
330 | $themeVarsDetails: t.composePaddingVars($themeVarsDetails, "HtmlDetails");
331 | $marginTop-HtmlDetails: createThemeVarDetails("marginTop-HtmlDetails");
332 | $marginBottom-HtmlDetails: createThemeVarDetails("marginBottom-HtmlDetails");
333 |
334 | @layer components {
335 | .htmlDetails {
336 | margin-top: $marginTop-HtmlDetails;
337 | margin-bottom: $marginBottom-HtmlDetails;
338 | @include t.borderVars($themeVarsDetails, "HtmlDetails");
339 | @include t.paddingVars($themeVarsDetails, "HtmlDetails");
340 | }
341 | }
342 |
343 | // --- Details
344 |
345 | $themeVarsDetails: ();
346 |
347 | @function createThemeVarDetails($componentVariable) {
348 | $themeVarsDetails: t.appendThemeVar($themeVarsDetails, $componentVariable) !global;
349 | @return t.getThemeVar($themeVarsDetails, $componentVariable);
350 | }
351 |
352 | $marginTop-HtmlDetails: createThemeVarDetails("marginTop-HtmlDetails");
353 | $marginBottom-HtmlDetails: createThemeVarDetails("marginBottom-HtmlDetails");
354 |
355 | @layer components {
356 | .htmlDetails {
357 | margin-top: $marginTop-HtmlDetails;
358 | margin-bottom: $marginBottom-HtmlDetails;
359 | }
360 | }
361 |
362 | // --- We export the theme variables to add them to the component renderer
363 | :export {
364 | themeVarsTable: t.json-stringify($themeVarsTable);
365 | themeVarsThead: t.json-stringify($themeVarsThead);
366 | themeVarsTbody: t.json-stringify($themeVarsTbody);
367 | themeVarsTfoot: t.json-stringify($themeVarsTfoot);
368 | themeVarsTh: t.json-stringify($themeVarsTh);
369 | themeVarsTr: t.json-stringify($themeVarsTr);
370 | themeVarsTd: t.json-stringify($themeVarsTd);
371 | themeVarsList: t.json-stringify($themeVarsList);
372 | themeVarsHeading: t.json-stringify($themeVarsHeading);
373 | themeVarsVideo: t.json-stringify($themeVarsVideo);
374 | themeVarsDetails: t.json-stringify($themeVarsDetails);
375 | }
376 |
```
--------------------------------------------------------------------------------
/xmlui/src/abstractions/ComponentDefs.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { RenderChildFn } from "./RendererDefs";
2 | import type { CollectedDeclarations } from "../components-core/script-runner/ScriptingSourceTree";
3 | import type { DefaultThemeVars } from "./ThemingDefs";
4 |
5 | /**
6 | * This interface represents the core properties of a component definition
7 | * (independent of component metadata).
8 | */
9 | export interface ComponentDefCore {
10 | /**
11 | * The type discriminator field of the component; it defines the unique ID of the component type.
12 | */
13 | type: string;
14 |
15 | /**
16 | * Unique identifier of a component-like object
17 | */
18 | uid?: string;
19 |
20 | /**
21 | * An optional identifier we use for e2e tests; it does not influence the rendering of a component.
22 | */
23 | testId?: string;
24 |
25 | /**
26 | * Though components manage their state internally, the app logic may require user
27 | * state management. Components may have user *variables*, which the UI logic uses to
28 | * manage the application state. This property holds the variables (name and value
29 | * pairs) associated with this component definition.
30 | */
31 | vars?: Record<string, any>;
32 |
33 | /**
34 | * Each component may have child components to constitute a hierarchy of components.
35 | * This property holds the definition of these nested children.
36 | */
37 | children?: ComponentDef[];
38 |
39 | /**
40 | * Components may have slots that can be filled with other components. This property
41 | * holds the contents of the slots.
42 | */
43 | slots?: Record<string, ComponentDef[]>;
44 |
45 | /**
46 | * This property is evaluated to a Boolean value during run time. When this value is
47 | * `true`, the component with its children chain is rendered; otherwise, the entire
48 | * component hierarchy is omitted from the rendered tree.
49 | */
50 | when?: string | boolean;
51 |
52 | /**
53 | * Some components work with data obtained asynchronously. Fetching this data requires
54 | * some state management handling the complexity (including error handling) of data
55 | * access. A *loader* is responsible for managing this logic. This property holds the
56 | * loaders associated with this component definition.
57 | */
58 | loaders?: ComponentDef[];
59 |
60 | /**
61 | * Components may have functions that are used to perform some logic. This property
62 | * holds the functions (name and function body) associated with this component
63 | * definition.
64 | */
65 | functions?: Record<string, any>;
66 |
67 | /**
68 | * Components managing state through variables or loaders are wrapped with containers
69 | * responsible for this job. Just as components, containers form a hierarchy. While
70 | * working with this hierarchy, parent components may flow state values (key and value
71 | * pairs) to their child containers. This property holds the name of state values to
72 | * flow down to the direct child containers.
73 | */
74 | uses?: string[];
75 |
76 | /**
77 | * Arbitrary debug information that can be attached to a component definition.
78 | * Current usage:
79 | * - `debug: { source: { start: number, end: number } }` The start and end
80 | * positions of the source belonging to the particular component definition.
81 | */
82 | debug?: Record<string, any>;
83 | }
84 | // This interface represents the properties of a component definition.
85 | export interface ComponentDef<TMd extends ComponentMetadata = ComponentMetadata>
86 | extends ComponentDefCore,
87 | Scriptable {
88 | // Component properties
89 | props?: Record<keyof TMd["props"], any>;
90 |
91 | // Component events
92 | events?: Record<keyof TMd["events"], any>;
93 |
94 | // Components may have an API that other components can use to interact with them.
95 | // This property holds the API methods associated with this component definition.
96 | api?: Record<keyof TMd["apis"], any>;
97 |
98 | // Components may provide context variables that can be used to in expressions and
99 | // event handlers within the component.
100 |
101 | contextVars?: Record<keyof TMd["contextVars"], string>;
102 | }
103 |
104 | // XMLUI allows the creation of reusable components assembled from other XMLUI
105 | // components (with markup). This type represents such components. The name
106 | // `CompoundComponent` refers to the assembled nature of reusable components.
107 | export interface CompoundComponentDef extends Scriptable {
108 | // Each compound component must have a unique name. The markup uses this name to refer
109 | // to the particular compound component.
110 | name: string;
111 |
112 | // Each compound component must have a single root component defining the component
113 | // contents.
114 | component: ComponentDef;
115 |
116 | // Compound components may provide an API that other components can use to interact
117 | // with them. This property holds the API methods associated with this compound
118 | // component definition.
119 | api?: Record<string, any>;
120 |
121 | // This property holds the variables (name and value pairs) associated with this
122 | // compound component definition.
123 |
124 | vars?: Record<string, any>;
125 |
126 | // A component can define namespaces on it, with the <ComponentName xmlns:KEY="VALUE" />
127 | // syntax. These are used later to resolve the `type` of the componentDef.
128 | // <KEY:Button/> will have type `VALUE.Button` (joined with a "." (dot)).
129 | namespaces?: Record<string, string>;
130 |
131 | // Arbitrary debug information that can be attached to a component definition.
132 | // Current usage:
133 | // - `debug: { source: { start: number, end: number } }` The start and end
134 | // positions of the source belonging to the particular component definition.
135 | debug?: Record<string, any>;
136 |
137 | codeBehind?: string;
138 | }
139 |
140 | // Sometimes, components and compound components can both be used
141 | export type ComponentLike = ComponentDef | CompoundComponentDef;
142 |
143 | // Some components render their nested child components dynamically using the current
144 | // context of their parents. For example, reusable components (`CompoundComponentDef`)
145 | // have a `Slot` placeholder that marks the location where the children should be
146 | // rendered. Other component types (e.g., `ApiBoundComponent` and `ContainerComponent`)
147 | // use this dynamic rendering, too.
148 | //
149 | // This interface represents this functionality.
150 | export interface DynamicChildComponentDef extends ComponentDef {
151 | // This property holds a function that can render a particular child or children into
152 | // a specific layout context.
153 | renderChild: RenderChildFn;
154 |
155 | // This property holds the child component that should be rendered.
156 | childToRender: ComponentDef;
157 | }
158 |
159 | // This interface holds the properties representing a scriptable component definition.
160 | interface Scriptable {
161 | // This property holds the text defined in all <script> sections attached to a
162 | // component.
163 | script?: string;
164 |
165 | // This property holds the parsed form of scripts stored in code-behind files.
166 | scriptCollected?: CollectedDeclarations;
167 |
168 | // This property holds errors coming from parsing the code-behind scripts.
169 | scriptError?: any;
170 | }
171 |
172 | export type PropertyValueType = "boolean" | "string" | "number" | "any" | "ComponentDef";
173 |
174 | // A generic validation function that retrieves either a hint (the validation argument
175 | // has issues) or undefined (the argument is valid).
176 | export type IsValidFunction<T> = (
177 | propKey: string,
178 | propValue: T,
179 | ) => string | string[] | undefined | null;
180 |
181 | // This type represents the description of a property value, which can be a string, a
182 | // number, or an object with a value and a description. This type is used in the
183 | // metadata of a component.
184 | export type PropertyValueDescription<T = string | number> =
185 | | T
186 | | {
187 | value: T;
188 | description: string;
189 | };
190 |
191 | // Components have properties, events, context values, and exposed API endpoints, each
192 | // holding metadata the rendering engine uses at run time. This type defines the
193 | // structure of such metadata.
194 | export type ComponentPropertyMetadata = {
195 | // This field defines the description explaining the property. You can use markdown,
196 | // as the UI may display this value.
197 | readonly description: string;
198 |
199 | // This field defines the type of the property. The rendering engine uses this
200 | // information to validate the property value.
201 | readonly valueType?: PropertyValueType;
202 |
203 | // This field defines the available values of the property. The rendering engine
204 | // uses this information to validate the property value.
205 | readonly availableValues?: readonly PropertyValueDescription[];
206 |
207 | // This field defines the default value of the property. The rendering engine uses
208 | // this information to set the default value of the property.
209 | defaultValue?: any;
210 |
211 | // This field defines a validation function that the rendering engine uses to
212 | // validate the property value. The function returns one or more hints if the
213 | // property value is invalid.
214 | isValid?: IsValidFunction<any>;
215 |
216 | // Indicates that a particular property is internal and should not be exposed in the
217 | // documentation
218 | isInternal?: boolean;
219 |
220 | // Indicates that a particular property is required for the component to essentially
221 | // function.
222 | isRequired?: boolean;
223 | };
224 |
225 | // This type defines the metadata of a component API. It is used to describe the
226 | // methods that the component exposes for interaction.
227 | export type ComponentApiMetadata = {
228 | // This field defines the description explaining the property. You can use markdown,
229 | // as the UI may display this value.
230 | readonly description: string;
231 |
232 | // This field defines the signature of the API method using TypeScript-like syntax.
233 | readonly signature?: string;
234 |
235 | // This field defines the parameters of the API method. It is an object where each key
236 | // is the parameter name, and the value is its description.
237 | readonly parameters?: Record<string, string>;
238 | };
239 |
240 | // This type defines the metadata of a component part. It is used to describe the
241 | // individual parts that make up the component.
242 | export type ComponentPartMetadata = {
243 | description: string;
244 | };
245 |
246 | // Components have metadata that the rendering engine uses to render the component.
247 | // This type defines the structure of such metadata.
248 | //
249 | // The type has generic parameters to ensure that type-checking works with the
250 | // metadata defined here in concert with the renderer object using the same metadata
251 | // type.
252 | export type ComponentMetadata<
253 | TProps extends Record<string, ComponentPropertyMetadata> = Record<string, any>,
254 | TEvents extends Record<string, ComponentPropertyMetadata> = Record<string, any>,
255 | TContextValues extends Record<string, ComponentPropertyMetadata> = Record<string, any>,
256 | TApis extends Record<string, ComponentApiMetadata> = Record<string, any>,
257 | > = {
258 | // The current status of the component. This field is now mandatory.
259 | status?: "stable" | "experimental" | "deprecated" | "in progress" | "internal";
260 |
261 | // Component description in markdown; it goes into the generated documentation
262 | description?: string;
263 |
264 | // Optional short description of the component to display in visual tools
265 | shortDescription?: string;
266 |
267 | // Description of component properties
268 | props?: TProps;
269 |
270 | // Description of component events
271 | events?: TEvents;
272 |
273 | // Description of component context variables
274 | contextVars?: TContextValues;
275 |
276 | // Description of component APIs
277 | apis?: TApis;
278 |
279 | // Indicates that a particular component does not render any visual element on its own (Default: false)
280 | nonVisual?: boolean;
281 |
282 | // childrenAsTemplate?: keyof TProps;
283 | childrenAsTemplate?: string;
284 |
285 | opaque?: boolean;
286 |
287 | // List of theme variables available for the component
288 | themeVars?: Record<string, string>;
289 |
290 | // Optional description of theme variables available for the component
291 | themeVarDescriptions?: Record<string, string>;
292 |
293 | // Theme variable default values for the "solid" theme
294 | defaultThemeVars?: DefaultThemeVars;
295 |
296 | // Theme variable defaults for a particular tone-specific theme
297 | toneSpecificThemeVars?: Record<string, Record<string, string>>;
298 |
299 | // Indicates that the documentation should include only the theme variables
300 | // including the component name
301 | limitThemeVarsToComponent?: boolean;
302 |
303 | // Indicates that the component allows arbitrary props (not just the named ones)
304 | allowArbitraryProps?: boolean;
305 |
306 | // If the component is specalized, this property holds the name of the parent component
307 | specializedFrom?: string;
308 |
309 | // Contains the folder name if it does not match the component name
310 | docFolder?: string;
311 |
312 | // Indicates that the component represent an HTML tag
313 | isHtmlTag?: boolean;
314 |
315 | // Describes the individual parts that make up the component
316 | parts?: Record<string, ComponentPartMetadata>;
317 |
318 | // The name of the default part. Layout properties will be applied to this part by default.
319 | defaultPart?: string;
320 | };
321 |
322 | export interface ParentRenderContext {
323 | renderChild: RenderChildFn;
324 | children?: ComponentDef[];
325 | props?: Record<string, any>;
326 | }
327 |
```
--------------------------------------------------------------------------------
/xmlui/src/components/FormItem/FormItem.md:
--------------------------------------------------------------------------------
```markdown
1 | %-DESC-START
2 |
3 | **Key features:**
4 | - **Data binding**: Automatically syncs control values with form data using the `bindTo` property
5 | - **Validation**: Displays validation states and error messages for the associated input
6 | - **Flexible labeling**: Supports labels, helper text, and various label positioning options
7 | - **Layout management**: Handles consistent spacing and alignment of form elements
8 |
9 | See [this guide](/forms) for details.
10 |
11 | %-DESC-END
12 |
13 | %-PROP-START bindTo
14 |
15 | Try to enter some kind of text in the input field labelled `Lastname` and submit the form. Note how the submitted data looks like compared to the one set in `data`.
16 |
17 | ```xmlui-pg copy display name="Example: bindTo"
18 | <App>
19 | <Form
20 | data="{{ firstname: 'James', lastname: 'Clewell' }}"
21 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
22 | <FormItem label="Firstname" bindTo="firstname" />
23 | <FormItem label="Lastname" />
24 | </Form>
25 | </App>
26 | ```
27 |
28 | %-PROP-END
29 |
30 | %-PROP-START enabled
31 |
32 | ```xmlui-pg copy display name="Example: enabled"
33 | <App>
34 | <Form>
35 | <FormItem label="Firstname" enabled="true" />
36 | <FormItem label="Lastname" enabled="false" />
37 | </Form>
38 | </App>
39 | ```
40 |
41 | %-PROP-END
42 |
43 | %-PROP-START initialValue
44 |
45 | ```xmlui-pg copy display name="Example: initialValue"
46 | <App>
47 | <Form data="{{ firstname: 'Michael', lastname: undefined }}">
48 | <FormItem label="Firstname" bindTo="firstname" initialValue="James" />
49 | <FormItem label="Lastname" bindTo="lastname" initialValue="Jordan" />
50 | </Form>
51 | </App>
52 | ```
53 |
54 | %-PROP-END
55 |
56 | %-PROP-START label
57 |
58 | ```xmlui-pg copy display name="Example: label"
59 | <App>
60 | <Form>
61 | <FormItem label="Firstname" />
62 | </Form>
63 | </App>
64 | ```
65 |
66 | %-PROP-END
67 |
68 | %-PROP-START labelPosition
69 |
70 | Different input components have different layout methods
71 | (i.e. `TextBox` labels are positioned at the top, `Checkbox` labels are on the right side).
72 |
73 | ```xmlui-pg copy display name="Example: labelPosition"
74 | <App>
75 | <Form>
76 | <FormItem label="Start Label" labelPosition="start" />
77 | <FormItem label="Top Label" labelPosition="top" />
78 | <FormItem label="End Label" labelPosition="end" />
79 | <FormItem label="Bottom Label" labelPosition="bottom" />
80 | </Form>
81 | </App>
82 | ```
83 |
84 | %-PROP-END
85 |
86 | %-PROP-START lengthInvalidMessage
87 |
88 | In the app, type a name longer than four characters in both fields, then leave the edited field. The two fields will display different error messages; the second uses the customized one.
89 |
90 | ```xmlui-pg copy display name="Example: lengthInvalidMessage"
91 | <App>
92 | <Form
93 | data="{{ firstname: 'James', lastname: 'Clewell' }}"
94 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
95 | <FormItem maxLength="4" bindTo="firstname" />
96 | <FormItem lengthInvalidMessage="Name is too long!" maxLength="4" bindTo="lastname" />
97 | </Form>
98 | </App>
99 | ```
100 |
101 | %-PROP-END
102 |
103 | %-PROP-START lengthInvalidSeverity
104 |
105 | In the app, type a name longer than four characters in both fields, then leave the edited field. The two fields will display different error messages; the second uses a warning instead of an error.
106 |
107 | ```xmlui-pg copy display name="Example: lengthInvalidSeverity"
108 | <App>
109 | <Form
110 | data="{{ firstname: 'James', lastname: 'Clewell' }}"
111 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
112 | <FormItem maxLength="4" bindTo="firstname" />
113 | <FormItem lengthInvalidSeverity="warning" maxLength="4" bindTo="lastname" />
114 | </Form>
115 | </App>
116 | ```
117 |
118 | %-PROP-END
119 |
120 | %-PROP-START maxLength
121 |
122 | Note that it is not possible for the user to enter a string larger than the value of the `maxLength`,
123 | but setting such a value programmatically still results in a validation check.
124 |
125 | In the demo below, try to enter an input longer than 4 characters or submit the form as is.
126 |
127 | ```xmlui-pg copy display name="Example: maxLength"
128 | <App>
129 | <Form
130 | data="{{ firstname: 'James' }}"
131 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
132 | <FormItem maxLength="4" bindTo="firstname" />
133 | </Form>
134 | </App>
135 | ```
136 |
137 | %-PROP-END
138 |
139 | %-PROP-START minLength
140 |
141 | In the demo below, enter an input shorter than 4 characters or just submit the form as is.
142 |
143 | ```xmlui-pg copy display name="Example: minLength"
144 | <App>
145 | <Form
146 | data="{{ firstname: '' }}"
147 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
148 | <FormItem minLength="4" bindTo="firstname" />
149 | </Form>
150 | </App>
151 | ```
152 |
153 | %-PROP-END
154 |
155 | %-PROP-START maxValue
156 |
157 | Note that it is not possible for the user to enter a number larger than the value of the `maxValue`,
158 | but setting such a value programmatically still results in a validation check.
159 |
160 | In the demo below, enter an input greater than 99 or just submit the form as is.
161 |
162 | ```xmlui-pg copy display name="Example: maxValue"
163 | <App>
164 | <Form
165 | data="{{ age: 100 }}"
166 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
167 | <FormItem maxValue="99" bindTo="age" type="integer" />
168 | </Form>
169 | </App>
170 | ```
171 |
172 | %-PROP-END
173 |
174 | %-PROP-START minValue
175 |
176 | Note that it is not possible for the user to enter a number smaller than the value of the `minValue`,
177 | but setting such a value programmatically still results in a validation check.
178 |
179 | In the demo below, enter an input smaller than 18 or just submit the form as is.
180 |
181 | ```xmlui-pg copy display name="Example: minValue"
182 | <App>
183 | <Form
184 | data="{{ age: 0 }}"
185 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
186 | <FormItem minValue="18" bindTo="age" type="integer" />
187 | </Form>
188 | </App>
189 | ```
190 |
191 | %-PROP-END
192 |
193 | %-PROP-START pattern
194 |
195 | | Value | Description |
196 | | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
197 | | `email` | Accepts the `[username]@[second level domain].[top level domain]` format |
198 | | `phone` | Accepts a wide range of characters: numbers, upper- and lowercase letters and the following symbols: `#`, `*`, `)`, `(`, `+`, `.`, `\`, `-`, `_`, `&`, `'` |
199 | | `url` | Accepts URLs and URIs starting with either `http` or `https` |
200 |
201 | > **Note:** To define custom patterns and regular expressions, see the [regex section](#regex).
202 |
203 | In the demo below, enter an input that is not solely one lowercase string or just submit the form as is.
204 |
205 | ```xmlui-pg copy display name="Example: pattern"
206 | <App>
207 | <Form
208 | data="{{ userEmail: 'mailto' }}"
209 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
210 | <FormItem pattern="email" bindTo="userEmail" />
211 | </Form>
212 | </App>
213 | ```
214 |
215 | %-PROP-END
216 |
217 | %-PROP-START patternInvalidMessage
218 |
219 | In the demo below, enter anything that does not look like an email and click outside to see the regular and custom message.
220 |
221 | ```xmlui-pg copy display name="Example: patternInvalidMessage"
222 | <App>
223 | <Form
224 | data="{{ oldEmail: 'mailto', newEmail: 'mailto' }}"
225 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
226 | <FormItem pattern="email" bindTo="oldEmail" />
227 | <FormItem
228 | patternInvalidMessage="This does not look like an email"
229 | pattern="email" bindTo="newEmail" />
230 | </Form>
231 | </App>
232 | ```
233 |
234 | %-PROP-END
235 |
236 | %-PROP-START patternInvalidSeverity
237 |
238 | In the demo below, enter a string of characters that does not look like an email to see the difference in feedback.
239 |
240 | ```xmlui-pg copy display name="Example: patternInvalidSeverity"
241 | <App>
242 | <Form
243 | data="{{ oldEmail: 'mailto', newEmail: 'mailto' }}"
244 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
245 | <FormItem pattern="email" bindTo="oldEmail" />
246 | <FormItem patternInvalidSeverity="warning" pattern="email" bindTo="newEmail" />
247 | </Form>
248 | </App>
249 | ```
250 |
251 | %-PROP-END
252 |
253 | %-PROP-START rangeInvalidMessage
254 |
255 | In the demo below, enter any value that is out of range in the input fields and click outside to see the regular and custom message.
256 | Just submitting the form as is also produces the same error.
257 |
258 | ```xmlui-pg copy display name="Example: rangeInvalidMessage"
259 | <App>
260 | <Form
261 | data="{{ age: 100, customAge: 100 }}"
262 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
263 | <FormItem minValue="0" maxValue="99" bindTo="age" type="integer" />
264 | <FormItem
265 | minValue="0"
266 | maxValue="99"
267 | rangeInvalidMessage="Out of range!"
268 | bindTo="customAge"
269 | type="integer" />
270 | </Form>
271 | </App>
272 | ```
273 |
274 | %-PROP-END
275 |
276 | %-PROP-START rangeInvalidSeverity
277 |
278 | In the demo below, enter any value that is out of range in the input fields and click outside to see the regular and custom message.
279 | Just submitting the form as is also produces the same error.
280 |
281 | ```xmlui-pg copy display name="Example: rangeInvalidSeverity"
282 | <App>
283 | <Form
284 | data="{{ age: 100, customAge: 100 }}"
285 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
286 | <FormItem minValue="0" maxValue="99" bindTo="age" type="integer" />
287 | <FormItem
288 | minValue="0" maxValue="99"
289 | rangeInvalidSeverity="warning"
290 | bindTo="customAge"
291 | type="integer" />
292 | </Form>
293 | </App>
294 | ```
295 |
296 | %-PROP-END
297 |
298 | %-PROP-START regex
299 |
300 | In the demo below, enter an input that is not solely one lowercase string or just submit the form as is.
301 |
302 | ```xmlui-pg copy display name="Example: regex"
303 | <App>
304 | <Form
305 | data="{{ password: 'PASSWORD123' }}"
306 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
307 | <FormItem regex="^[a-z]+$" bindTo="password" />
308 | </Form>
309 | </App>
310 | ```
311 |
312 | %-PROP-END
313 |
314 | %-PROP-START regexInvalidMessage
315 |
316 | In the demo below, enter a password that is not a lowercase string and click outside to see the regular and custom message.
317 |
318 | ```xmlui-pg copy display name="Example: regexInvalidMessage"
319 | <App>
320 | <Form
321 | data="{{ oldPassword: 'PASSWORD123', newPassword: 'PASSWORD123' }}"
322 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
323 | <FormItem regex="^[a-z]+$" bindTo="oldPassword" />
324 | <FormItem
325 | regexInvalidMessage="Password must be all lowercase"
326 | regex="^[a-z]+$" bindTo="newPassword" />
327 | </Form>
328 | </App>
329 | ```
330 |
331 | %-PROP-END
332 |
333 | %-PROP-START regexInvalidSeverity
334 |
335 | In the demo below, enter a password that is not a lowercase string and click outside to see the regular and custom message.
336 | Just submitting the form as is also produces the same error.
337 |
338 | ```xmlui-pg copy display name="Example: regexInvalidSeverity"
339 | <App>
340 | <Form
341 | data="{{ oldPassword: 'PASSWORD123', newPassword: 'PASSWORD123' }}"
342 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
343 | <FormItem regex="^[a-z]+$" bindTo="oldPassword" />
344 | <FormItem regexInvalidSeverity="warning" regex="^[a-z]+$" bindTo="newPassword" />
345 | </Form>
346 | </App>
347 | ```
348 |
349 | %-PROP-END
350 |
351 | %-PROP-START required
352 |
353 | ```xmlui-pg copy display name="Example: required"
354 | <App>
355 | <Form
356 | data="{{ name: undefined }}"
357 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
358 | <FormItem required="true" label="Name" bindTo="name" />
359 | </Form>
360 | </App>
361 | ```
362 |
363 | %-PROP-END
364 |
365 | %-PROP-START requiredInvalidMessage
366 |
367 | In the demo below, leave the field empty and click outside to see the regular and custom message.
368 |
369 | ```xmlui-pg copy display name="Example: requiredInvalidMessage"
370 | <App>
371 | <Form
372 | data="{{ firstname: undefined, lastname: undefined }}"
373 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
374 | <FormItem required="true" label="First Name" bindTo="firstname" />
375 | <FormItem
376 | requiredInvalidMessage="Last Name is required!"
377 | required="true"
378 | label="Last Name"
379 | bindTo="lastname" />
380 | </Form>
381 | </App>
382 | ```
383 |
384 | %-PROP-END
385 |
386 | %-PROP-START type
387 |
388 | >[!INFO]
389 | > For custom controls, there is no need to explicitly set the `type` to `custom`.
390 | > Omitting the type and providing child components implicitly sets it to custom.
391 |
392 | %-PROP-END
393 |
394 | %-PROP-START customValidationsDebounce
395 |
396 | Note how changing the input in the demo below will result in a slight delay of input checks noted by the appearance of a new "I" character.
397 |
398 | ```xmlui-pg copy display name="Example: customValidationsDebounce"
399 | <App>
400 | <Form
401 | var.validations="Validations: "
402 | data="{{ name: 'Joe' }}"
403 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
404 | <FormItem
405 | customValidationsDebounce="3000"
406 | onValidate="(value) => {
407 | validations += '| ';
408 | return value === value.toUpperCase();
409 | }"
410 | bindTo="name" />
411 | <Text value="{validations}" />
412 | </Form>
413 | </App>
414 | ```
415 |
416 | %-PROP-END
417 |
418 | %-EVENT-START validate
419 |
420 | In the demo below, leave the field as is and submit the form or enter an input that is not all capital letters.
421 |
422 | ```xmlui-pg copy {7} display name="Example: validate"
423 | <App>
424 | <Form
425 | data="{{ name: 'James' }}"
426 | onSubmit="(toSave) => toast(JSON.stringify(toSave))">
427 | <FormItem
428 | bindTo="name"
429 | onValidate="(value) => value === value.toUpperCase()" />
430 | </Form>
431 | </App>
432 | ```
433 |
434 | %-EVENT-END
435 |
```