This is page 172 of 177. Use http://codebase.md/xmlui-org/xmlui/mockApiDef.js?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── cold-items-taste.md │ ├── config.json │ ├── empty-spiders-dress.md │ ├── shy-windows-allow.md │ ├── sour-coins-read.md │ ├── tame-zebras-invite.md │ ├── twenty-jeans-watch.md │ └── warm-spies-melt.md ├── .eslintrc.cjs ├── .github │ ├── build-checklist.png │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── deploy-docs-optimized.yml │ ├── deploy-docs.yml │ ├── prepare-versions.yml │ ├── release-packages.yml │ ├── run-all-tests.yml │ └── run-smoke-tests.yml ├── .gitignore ├── .prettierrc.js ├── .vscode │ ├── launch.json │ └── settings.json ├── blog │ ├── .gitignore │ ├── .gitkeep │ ├── CHANGELOG.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── public │ │ ├── blog │ │ │ ├── images │ │ │ │ ├── blog-page-component.png │ │ │ │ ├── blog-scrabble.png │ │ │ │ ├── integrated-blog-search.png │ │ │ │ └── lorem-ipsum.png │ │ │ ├── lorem-ipsum.md │ │ │ ├── newest-post.md │ │ │ ├── older-post.md │ │ │ └── welcome-to-the-xmlui-blog.md │ │ ├── mockServiceWorker.js │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ └── for-download │ │ │ │ └── xmlui │ │ │ │ └── xmlui-standalone.umd.js │ │ │ ├── github.svg │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ └── PageNotFound.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ ├── docs-theme.ts │ │ ├── earthtone.ts │ │ ├── xmlui-gray-on-default.ts │ │ ├── xmlui-green-on-default.ts │ │ └── xmlui-orange-on-default.ts │ └── tsconfig.json ├── CONTRIBUTING.md ├── docs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── ComponentRefLinks.txt │ ├── content │ │ ├── _meta.json │ │ ├── components │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── APICall.md │ │ │ ├── App.md │ │ │ ├── AppHeader.md │ │ │ ├── AppState.md │ │ │ ├── AutoComplete.md │ │ │ ├── Avatar.md │ │ │ ├── Backdrop.md │ │ │ ├── Badge.md │ │ │ ├── BarChart.md │ │ │ ├── Bookmark.md │ │ │ ├── Breakout.md │ │ │ ├── Button.md │ │ │ ├── Card.md │ │ │ ├── Carousel.md │ │ │ ├── ChangeListener.md │ │ │ ├── Checkbox.md │ │ │ ├── CHStack.md │ │ │ ├── ColorPicker.md │ │ │ ├── Column.md │ │ │ ├── ContentSeparator.md │ │ │ ├── CVStack.md │ │ │ ├── DataSource.md │ │ │ ├── DateInput.md │ │ │ ├── DatePicker.md │ │ │ ├── DonutChart.md │ │ │ ├── DropdownMenu.md │ │ │ ├── EmojiSelector.md │ │ │ ├── ExpandableItem.md │ │ │ ├── FileInput.md │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FlowLayout.md │ │ │ ├── Footer.md │ │ │ ├── Form.md │ │ │ ├── FormItem.md │ │ │ ├── FormSection.md │ │ │ ├── Fragment.md │ │ │ ├── H1.md │ │ │ ├── H2.md │ │ │ ├── H3.md │ │ │ ├── H4.md │ │ │ ├── H5.md │ │ │ ├── H6.md │ │ │ ├── Heading.md │ │ │ ├── HSplitter.md │ │ │ ├── HStack.md │ │ │ ├── Icon.md │ │ │ ├── IFrame.md │ │ │ ├── Image.md │ │ │ ├── Items.md │ │ │ ├── LabelList.md │ │ │ ├── Legend.md │ │ │ ├── LineChart.md │ │ │ ├── Link.md │ │ │ ├── List.md │ │ │ ├── Logo.md │ │ │ ├── Markdown.md │ │ │ ├── MenuItem.md │ │ │ ├── MenuSeparator.md │ │ │ ├── ModalDialog.md │ │ │ ├── NavGroup.md │ │ │ ├── NavLink.md │ │ │ ├── NavPanel.md │ │ │ ├── NoResult.md │ │ │ ├── NumberBox.md │ │ │ ├── Option.md │ │ │ ├── Page.md │ │ │ ├── PageMetaTitle.md │ │ │ ├── Pages.md │ │ │ ├── Pagination.md │ │ │ ├── PasswordInput.md │ │ │ ├── PieChart.md │ │ │ ├── ProgressBar.md │ │ │ ├── Queue.md │ │ │ ├── RadioGroup.md │ │ │ ├── RealTimeAdapter.md │ │ │ ├── Redirect.md │ │ │ ├── Select.md │ │ │ ├── Slider.md │ │ │ ├── Slot.md │ │ │ ├── SpaceFiller.md │ │ │ ├── Spinner.md │ │ │ ├── Splitter.md │ │ │ ├── Stack.md │ │ │ ├── StickyBox.md │ │ │ ├── SubMenuItem.md │ │ │ ├── Switch.md │ │ │ ├── TabItem.md │ │ │ ├── Table.md │ │ │ ├── TableOfContents.md │ │ │ ├── Tabs.md │ │ │ ├── Text.md │ │ │ ├── TextArea.md │ │ │ ├── TextBox.md │ │ │ ├── Theme.md │ │ │ ├── TimeInput.md │ │ │ ├── Timer.md │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneSwitch.md │ │ │ ├── Tooltip.md │ │ │ ├── Tree.md │ │ │ ├── VSplitter.md │ │ │ ├── VStack.md │ │ │ ├── xmlui-animations │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── Animation.md │ │ │ │ ├── FadeAnimation.md │ │ │ │ ├── FadeInAnimation.md │ │ │ │ ├── FadeOutAnimation.md │ │ │ │ ├── ScaleAnimation.md │ │ │ │ └── SlideInAnimation.md │ │ │ ├── xmlui-pdf │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Pdf.md │ │ │ ├── xmlui-spreadsheet │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Spreadsheet.md │ │ │ └── xmlui-website-blocks │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── Carousel.md │ │ │ ├── HelloMd.md │ │ │ ├── HeroSection.md │ │ │ └── ScrollToTop.md │ │ └── extensions │ │ ├── _meta.json │ │ ├── xmlui-animations │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── Animation.md │ │ │ ├── FadeAnimation.md │ │ │ ├── FadeInAnimation.md │ │ │ ├── FadeOutAnimation.md │ │ │ ├── ScaleAnimation.md │ │ │ └── SlideInAnimation.md │ │ └── xmlui-website-blocks │ │ ├── _meta.json │ │ ├── _overview.md │ │ ├── Carousel.md │ │ ├── HelloMd.md │ │ ├── HeroSection.md │ │ └── ScrollToTop.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── public │ │ ├── feed.rss │ │ ├── mockServiceWorker.js │ │ ├── pages │ │ │ ├── _meta.json │ │ │ ├── app-structure.md │ │ │ ├── build-editor-component.md │ │ │ ├── build-hello-world-component.md │ │ │ ├── components-intro.md │ │ │ ├── context-variables.md │ │ │ ├── forms.md │ │ │ ├── globals.md │ │ │ ├── glossary.md │ │ │ ├── helper-tags.md │ │ │ ├── hosted-deployment.md │ │ │ ├── howto │ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md │ │ │ │ ├── chain-a-refetch.md │ │ │ │ ├── debug-a-component.md │ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md │ │ │ │ ├── delegate-a-method.md │ │ │ │ ├── do-custom-form-validation.md │ │ │ │ ├── expose-a-method-from-a-component.md │ │ │ │ ├── filter-and-transform-data-from-an-api.md │ │ │ │ ├── group-items-in-list-by-a-property.md │ │ │ │ ├── handle-background-operations.md │ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md │ │ │ │ ├── make-a-set-of-equal-width-cards.md │ │ │ │ ├── make-a-table-responsive.md │ │ │ │ ├── modify-a-value-reported-in-a-column.md │ │ │ │ ├── paginate-a-list.md │ │ │ │ ├── pass-data-to-a-modal-dialog.md │ │ │ │ ├── react-to-button-click-not-keystrokes.md │ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md │ │ │ │ ├── share-a-modaldialog-across-components.md │ │ │ │ ├── sync-selections-between-table-and-list-views.md │ │ │ │ ├── update-ui-optimistically.md │ │ │ │ ├── use-built-in-form-validation.md │ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md │ │ │ ├── howto.md │ │ │ ├── intro.md │ │ │ ├── layout.md │ │ │ ├── markup.md │ │ │ ├── mcp.md │ │ │ ├── modal-dialogs.md │ │ │ ├── news-and-reviews.md │ │ │ ├── reactive-intro.md │ │ │ ├── refactoring.md │ │ │ ├── routing-and-links.md │ │ │ ├── samples │ │ │ │ ├── color-palette.xmlui │ │ │ │ ├── color-values.xmlui │ │ │ │ ├── shadow-sizes.xmlui │ │ │ │ ├── spacing-sizes.xmlui │ │ │ │ ├── swatch.xmlui │ │ │ │ ├── theme-gallery-brief.xmlui │ │ │ │ └── theme-gallery.xmlui │ │ │ ├── scoping.md │ │ │ ├── scripting.md │ │ │ ├── styles-and-themes │ │ │ │ ├── common-units.md │ │ │ │ ├── layout-props.md │ │ │ │ ├── theme-variable-defaults.md │ │ │ │ ├── theme-variables.md │ │ │ │ └── themes.md │ │ │ ├── template-properties.md │ │ │ ├── test.md │ │ │ ├── tutorial-01.md │ │ │ ├── tutorial-02.md │ │ │ ├── tutorial-03.md │ │ │ ├── tutorial-04.md │ │ │ ├── tutorial-05.md │ │ │ ├── tutorial-06.md │ │ │ ├── tutorial-07.md │ │ │ ├── tutorial-08.md │ │ │ ├── tutorial-09.md │ │ │ ├── tutorial-10.md │ │ │ ├── tutorial-11.md │ │ │ ├── tutorial-12.md │ │ │ ├── universal-properties.md │ │ │ ├── user-defined-components.md │ │ │ ├── vscode.md │ │ │ ├── working-with-markdown.md │ │ │ ├── working-with-text.md │ │ │ ├── xmlui-animations │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── Animation.md │ │ │ │ ├── FadeAnimation.md │ │ │ │ ├── FadeInAnimation.md │ │ │ │ ├── FadeOutAnimation.md │ │ │ │ ├── ScaleAnimation.md │ │ │ │ └── SlideInAnimation.md │ │ │ ├── xmlui-charts │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── BarChart.md │ │ │ │ ├── DonutChart.md │ │ │ │ ├── LabelList.md │ │ │ │ ├── Legend.md │ │ │ │ ├── LineChart.md │ │ │ │ └── PieChart.md │ │ │ ├── xmlui-pdf │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Pdf.md │ │ │ └── xmlui-spreadsheet │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ └── Spreadsheet.md │ │ ├── resources │ │ │ ├── devdocs │ │ │ │ ├── debug-proxy-object-2.png │ │ │ │ ├── debug-proxy-object.png │ │ │ │ ├── table_editor_01.png │ │ │ │ ├── table_editor_02.png │ │ │ │ ├── table_editor_03.png │ │ │ │ ├── table_editor_04.png │ │ │ │ ├── table_editor_05.png │ │ │ │ ├── table_editor_06.png │ │ │ │ ├── table_editor_07.png │ │ │ │ ├── table_editor_08.png │ │ │ │ ├── table_editor_09.png │ │ │ │ ├── table_editor_10.png │ │ │ │ ├── table_editor_11.png │ │ │ │ ├── table-editor-01.png │ │ │ │ ├── table-editor-02.png │ │ │ │ ├── table-editor-03.png │ │ │ │ ├── table-editor-04.png │ │ │ │ ├── table-editor-06.png │ │ │ │ ├── table-editor-07.png │ │ │ │ ├── table-editor-08.png │ │ │ │ ├── table-editor-09.png │ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ ├── clients.json │ │ │ │ ├── daily-revenue.json │ │ │ │ ├── dashboard-stats.json │ │ │ │ ├── demo.xmlui │ │ │ │ ├── demo.xmlui.xs │ │ │ │ ├── downloads │ │ │ │ │ └── downloads.json │ │ │ │ ├── for-download │ │ │ │ │ ├── index-with-api.html │ │ │ │ │ ├── index.html │ │ │ │ │ ├── mockApi.js │ │ │ │ │ ├── start-darwin.sh │ │ │ │ │ ├── start-linux.sh │ │ │ │ │ ├── start.bat │ │ │ │ │ └── xmlui │ │ │ │ │ └── xmlui-standalone.umd.js │ │ │ │ ├── getting-started │ │ │ │ │ ├── cl-tutorial-final.zip │ │ │ │ │ ├── cl-tutorial.zip │ │ │ │ │ ├── cl-tutorial2.zip │ │ │ │ │ ├── cl-tutorial3.zip │ │ │ │ │ ├── cl-tutorial4.zip │ │ │ │ │ ├── cl-tutorial5.zip │ │ │ │ │ ├── cl-tutorial6.zip │ │ │ │ │ ├── getting-started.zip │ │ │ │ │ ├── hello-xmlui.zip │ │ │ │ │ ├── xmlui-empty.zip │ │ │ │ │ └── xmlui-starter.zip │ │ │ │ ├── howto │ │ │ │ │ └── component-icons │ │ │ │ │ └── up-arrow.svg │ │ │ │ ├── invoices.json │ │ │ │ ├── monthly-status.json │ │ │ │ ├── news-and-reviews.json │ │ │ │ ├── products.json │ │ │ │ ├── releases.json │ │ │ │ ├── tutorials │ │ │ │ │ ├── datasource │ │ │ │ │ │ └── api.ts │ │ │ │ │ └── p2do │ │ │ │ │ ├── api.ts │ │ │ │ │ └── todo-logo.svg │ │ │ │ └── xmlui.json │ │ │ ├── github.svg │ │ │ ├── images │ │ │ │ ├── apiaction-tutorial │ │ │ │ │ ├── add-success.png │ │ │ │ │ ├── apiaction-param.png │ │ │ │ │ ├── change-completed.png │ │ │ │ │ ├── change-in-progress.png │ │ │ │ │ ├── confirm-delete.png │ │ │ │ │ ├── data-error.png │ │ │ │ │ ├── data-progress.png │ │ │ │ │ ├── data-success.png │ │ │ │ │ ├── display-1.png │ │ │ │ │ ├── item-deleted.png │ │ │ │ │ ├── item-updated.png │ │ │ │ │ ├── missing-api-key.png │ │ │ │ │ ├── new-item-added.png │ │ │ │ │ └── test-message.png │ │ │ │ ├── chat-api │ │ │ │ │ └── domain-model.svg │ │ │ │ ├── components │ │ │ │ │ ├── image │ │ │ │ │ │ └── breakfast.jpg │ │ │ │ │ ├── markdown │ │ │ │ │ │ └── colors.png │ │ │ │ │ └── modal │ │ │ │ │ ├── deep_link_dialog_1.jpg │ │ │ │ │ └── deep_link_dialog_2.jpg │ │ │ │ ├── create-apps │ │ │ │ │ ├── collapsed-vertical.png │ │ │ │ │ ├── using-forms-warning-dialog.png │ │ │ │ │ └── using-forms.png │ │ │ │ ├── datasource-tutorial │ │ │ │ │ ├── data-with-header.png │ │ │ │ │ ├── filtered-data.png │ │ │ │ │ ├── filtered-items.png │ │ │ │ │ ├── initial-page-items.png │ │ │ │ │ ├── list-items.png │ │ │ │ │ ├── next-page-items.png │ │ │ │ │ ├── no-data.png │ │ │ │ │ ├── pagination-1.jpg │ │ │ │ │ ├── pagination-1.png │ │ │ │ │ ├── polling-1.png │ │ │ │ │ ├── refetch-data.png │ │ │ │ │ ├── slow-loading.png │ │ │ │ │ ├── test-message.png │ │ │ │ │ ├── Thumbs.db │ │ │ │ │ ├── unconventional-data.png │ │ │ │ │ └── unfiltered-items.png │ │ │ │ ├── flower.jpg │ │ │ │ ├── get-started │ │ │ │ │ ├── add-new-contact.png │ │ │ │ │ ├── app-modified.png │ │ │ │ │ ├── app-start.png │ │ │ │ │ ├── app-with-boxes.png │ │ │ │ │ ├── app-with-toast.png │ │ │ │ │ ├── boilerplate-structure.png │ │ │ │ │ ├── cl-initial.png │ │ │ │ │ ├── cl-start.png │ │ │ │ │ ├── contact-counts.png │ │ │ │ │ ├── contact-dialog-title.png │ │ │ │ │ ├── contact-dialog.png │ │ │ │ │ ├── contact-menus.png │ │ │ │ │ ├── contact-predicates.png │ │ │ │ │ ├── context-menu.png │ │ │ │ │ ├── dashboard-numbers.png │ │ │ │ │ ├── default-contact-list.png │ │ │ │ │ ├── delete-contact.png │ │ │ │ │ ├── delete-task.png │ │ │ │ │ ├── detailed-template.png │ │ │ │ │ ├── edit-contact-details.png │ │ │ │ │ ├── edited-contact-saved.png │ │ │ │ │ ├── empty-sections.png │ │ │ │ │ ├── filter-completed.png │ │ │ │ │ ├── fullwidth-desktop.png │ │ │ │ │ ├── fullwidth-mobile.png │ │ │ │ │ ├── initial-table.png │ │ │ │ │ ├── items-and-badges.png │ │ │ │ │ ├── loading-message.png │ │ │ │ │ ├── new-contact-button.png │ │ │ │ │ ├── new-contact-saved.png │ │ │ │ │ ├── no-empty-sections.png │ │ │ │ │ ├── personal-todo-initial.png │ │ │ │ │ ├── piechart.png │ │ │ │ │ ├── review-today.png │ │ │ │ │ ├── rudimentary-dashboard.png │ │ │ │ │ ├── section-collapsed.png │ │ │ │ │ ├── sectioned-items.png │ │ │ │ │ ├── sections-ordered.png │ │ │ │ │ ├── spacex-list-with-links.png │ │ │ │ │ ├── spacex-list.png │ │ │ │ │ ├── start-personal-todo-1.png │ │ │ │ │ ├── submit-new-contact.png │ │ │ │ │ ├── submit-new-task.png │ │ │ │ │ ├── syntax-highlighting.png │ │ │ │ │ ├── table-with-badge.png │ │ │ │ │ ├── template-with-card.png │ │ │ │ │ ├── test-emulated-api.png │ │ │ │ │ ├── Thumbs.db │ │ │ │ │ ├── todo-logo.png │ │ │ │ │ └── xmlui-tools.png │ │ │ │ ├── HelloApp.png │ │ │ │ ├── HelloApp2.png │ │ │ │ ├── logos │ │ │ │ │ ├── xmlui1.svg │ │ │ │ │ ├── xmlui2.svg │ │ │ │ │ ├── xmlui3.svg │ │ │ │ │ ├── xmlui4.svg │ │ │ │ │ ├── xmlui5.svg │ │ │ │ │ ├── xmlui6.svg │ │ │ │ │ └── xmlui7.svg │ │ │ │ ├── pdf │ │ │ │ │ └── dummy-pdf.jpg │ │ │ │ ├── rendering-engine │ │ │ │ │ ├── AppEngine-flow.svg │ │ │ │ │ ├── Component.svg │ │ │ │ │ ├── CompoundComponent.svg │ │ │ │ │ ├── RootComponent.svg │ │ │ │ │ └── tree-with-containers.svg │ │ │ │ ├── reviewers-guide │ │ │ │ │ ├── AppEngine-flow.svg │ │ │ │ │ └── incbutton-in-action.png │ │ │ │ ├── tools │ │ │ │ │ └── boilerplate-structure.png │ │ │ │ ├── try.svg │ │ │ │ ├── tutorial │ │ │ │ │ ├── app-chat-history.png │ │ │ │ │ ├── app-content-placeholder.png │ │ │ │ │ ├── app-header-and-content.png │ │ │ │ │ ├── app-links-channel-selected.png │ │ │ │ │ ├── app-links-click.png │ │ │ │ │ ├── app-navigation.png │ │ │ │ │ ├── finished-ex01.png │ │ │ │ │ ├── finished-ex02.png │ │ │ │ │ ├── hello.png │ │ │ │ │ ├── splash-screen-advanced.png │ │ │ │ │ ├── splash-screen-after-click.png │ │ │ │ │ ├── splash-screen-centered.png │ │ │ │ │ ├── splash-screen-events.png │ │ │ │ │ ├── splash-screen-expression.png │ │ │ │ │ ├── splash-screen-reuse-after.png │ │ │ │ │ ├── splash-screen-reuse-before.png │ │ │ │ │ └── splash-screen.png │ │ │ │ └── tutorial-01.png │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ ├── Boxes.xmlui │ │ │ ├── Breadcrumb.xmlui │ │ │ ├── ChangeLog.xmlui │ │ │ ├── ColorPalette.xmlui │ │ │ ├── DocumentLinks.xmlui │ │ │ ├── DocumentPage.xmlui │ │ │ ├── DocumentPageNoTOC.xmlui │ │ │ ├── Icons.xmlui │ │ │ ├── IncButton.xmlui │ │ │ ├── IncButton2.xmlui │ │ │ ├── NameValue.xmlui │ │ │ ├── PageNotFound.xmlui │ │ │ ├── PaletteItem.xmlui │ │ │ ├── Palettes.xmlui │ │ │ ├── SectionHeader.xmlui │ │ │ ├── TBD.xmlui │ │ │ ├── Test.xmlui │ │ │ ├── ThemesIntro.xmlui │ │ │ ├── ThousandThemes.xmlui │ │ │ ├── TubeStops.xmlui │ │ │ ├── TubeStops.xmlui.xs │ │ │ └── TwoColumnCode.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ ├── docs-theme.ts │ │ ├── earthtone.ts │ │ ├── xmlui-gray-on-default.ts │ │ ├── xmlui-green-on-default.ts │ │ └── xmlui-orange-on-default.ts │ └── tsconfig.json ├── LICENSE ├── package-lock.json ├── package.json ├── packages │ ├── xmlui-animations │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── Animation.tsx │ │ │ ├── AnimationNative.tsx │ │ │ ├── FadeAnimation.tsx │ │ │ ├── FadeInAnimation.tsx │ │ │ ├── FadeOutAnimation.tsx │ │ │ ├── index.tsx │ │ │ ├── ScaleAnimation.tsx │ │ │ └── SlideInAnimation.tsx │ │ └── tsconfig.json │ ├── xmlui-devtools │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── devtools │ │ │ │ ├── DevTools.tsx │ │ │ │ ├── DevToolsNative.module.scss │ │ │ │ ├── DevToolsNative.tsx │ │ │ │ ├── ModalDialog.module.scss │ │ │ │ ├── ModalDialog.tsx │ │ │ │ ├── ModalVisibilityContext.tsx │ │ │ │ ├── Tooltip.module.scss │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── utils.ts │ │ │ ├── editor │ │ │ │ └── Editor.tsx │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── vite.config-overrides.ts │ ├── xmlui-hello-world │ │ ├── .gitignore │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── HelloWorld.module.scss │ │ │ ├── HelloWorld.tsx │ │ │ ├── HelloWorldNative.tsx │ │ │ └── index.tsx │ │ └── tsconfig.json │ ├── xmlui-os-frames │ │ ├── .gitignore │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── IPhoneFrame.module.scss │ │ │ ├── IPhoneFrame.tsx │ │ │ ├── MacOSAppFrame.module.scss │ │ │ ├── MacOSAppFrame.tsx │ │ │ ├── WindowsAppFrame.module.scss │ │ │ └── WindowsAppFrame.tsx │ │ └── tsconfig.json │ ├── xmlui-pdf │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ ├── components │ │ │ │ └── Pdf.xmlui │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── LazyPdfNative.tsx │ │ │ ├── Pdf.module.scss │ │ │ └── Pdf.tsx │ │ └── tsconfig.json │ ├── xmlui-playground │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── usePlayground.ts │ │ │ │ └── useToast.ts │ │ │ ├── index.tsx │ │ │ ├── playground │ │ │ │ ├── Box.module.scss │ │ │ │ ├── Box.tsx │ │ │ │ ├── CodeSelector.tsx │ │ │ │ ├── ConfirmationDialog.module.scss │ │ │ │ ├── ConfirmationDialog.tsx │ │ │ │ ├── Editor.tsx │ │ │ │ ├── Header.module.scss │ │ │ │ ├── Header.tsx │ │ │ │ ├── Playground.tsx │ │ │ │ ├── PlaygroundContent.module.scss │ │ │ │ ├── PlaygroundContent.tsx │ │ │ │ ├── PlaygroundNative.module.scss │ │ │ │ ├── PlaygroundNative.tsx │ │ │ │ ├── Preview.module.scss │ │ │ │ ├── Preview.tsx │ │ │ │ ├── Select.module.scss │ │ │ │ ├── StandalonePlayground.tsx │ │ │ │ ├── StandalonePlaygroundNative.module.scss │ │ │ │ ├── StandalonePlaygroundNative.tsx │ │ │ │ ├── ThemeSwitcher.module.scss │ │ │ │ ├── ThemeSwitcher.tsx │ │ │ │ ├── ToneSwitcher.tsx │ │ │ │ ├── Tooltip.module.scss │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── utils.ts │ │ │ ├── providers │ │ │ │ ├── Toast.module.scss │ │ │ │ └── ToastProvider.tsx │ │ │ ├── state │ │ │ │ └── store.ts │ │ │ ├── themes │ │ │ │ └── theme.ts │ │ │ └── utils │ │ │ └── helpers.ts │ │ └── tsconfig.json │ ├── xmlui-search │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── Search.module.scss │ │ │ └── Search.tsx │ │ └── tsconfig.json │ ├── xmlui-spreadsheet │ │ ├── .gitignore │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── Spreadsheet.tsx │ │ │ └── SpreadsheetNative.tsx │ │ └── tsconfig.json │ └── xmlui-website-blocks │ ├── .gitignore │ ├── CHANGELOG.md │ ├── demo │ │ ├── components │ │ │ ├── HeroBackgroundBreakoutPage.xmlui │ │ │ ├── HeroBackgroundsPage.xmlui │ │ │ ├── HeroContentsPage.xmlui │ │ │ ├── HeroTextAlignPage.xmlui │ │ │ ├── HeroTextPage.xmlui │ │ │ └── HeroTonesPage.xmlui │ │ ├── Main.xmlui │ │ └── themes │ │ └── default.ts │ ├── index.html │ ├── index.ts │ ├── meta │ │ └── componentsMetadata.ts │ ├── package.json │ ├── public │ │ └── resources │ │ ├── building.jpg │ │ └── xmlui-logo.svg │ ├── src │ │ ├── Carousel │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ └── CarouselNative.tsx │ │ ├── FancyButton │ │ │ ├── FancyButton.module.scss │ │ │ ├── FancyButton.tsx │ │ │ └── FancyButton.xmlui │ │ ├── Hello │ │ │ ├── Hello.tsx │ │ │ ├── Hello.xmlui │ │ │ └── Hello.xmlui.xs │ │ ├── HeroSection │ │ │ ├── HeroSection.module.scss │ │ │ ├── HeroSection.tsx │ │ │ └── HeroSectionNative.tsx │ │ ├── index.tsx │ │ ├── ScrollToTop │ │ │ ├── ScrollToTop.module.scss │ │ │ ├── ScrollToTop.tsx │ │ │ └── ScrollToTopNative.tsx │ │ └── vite-env.d.ts │ └── tsconfig.json ├── README.md ├── tools │ ├── codefence │ │ └── xmlui-code-fence-docs.md │ ├── create-app │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── create-app.ts │ │ ├── helpers │ │ │ ├── copy.ts │ │ │ ├── get-pkg-manager.ts │ │ │ ├── git.ts │ │ │ ├── install.ts │ │ │ ├── is-folder-empty.ts │ │ │ ├── is-writeable.ts │ │ │ ├── make-dir.ts │ │ │ └── validate-pkg.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── templates │ │ │ ├── default │ │ │ │ └── ts │ │ │ │ ├── gitignore │ │ │ │ ├── index.html │ │ │ │ ├── index.ts │ │ │ │ ├── public │ │ │ │ │ ├── mockServiceWorker.js │ │ │ │ │ ├── resources │ │ │ │ │ │ ├── favicon.ico │ │ │ │ │ │ └── xmlui-logo.svg │ │ │ │ │ └── serve.json │ │ │ │ └── src │ │ │ │ ├── components │ │ │ │ │ ├── ApiAware.xmlui │ │ │ │ │ ├── Home.xmlui │ │ │ │ │ ├── IncButton.xmlui │ │ │ │ │ └── PagePanel.xmlui │ │ │ │ ├── config.ts │ │ │ │ └── Main.xmlui │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── create-xmlui-hello-world │ │ ├── index.js │ │ └── package.json │ └── vscode │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── .vscodeignore │ ├── build.sh │ ├── CHANGELOG.md │ ├── esbuild.js │ ├── eslint.config.mjs │ ├── formatter-docs.md │ ├── generate-test-sample.sh │ ├── LICENSE.md │ ├── package-lock.json │ ├── package.json │ ├── README.md │ ├── resources │ │ ├── xmlui-logo.png │ │ └── xmlui-markup-syntax-highlighting.png │ ├── src │ │ ├── extension.ts │ │ └── server.ts │ ├── syntaxes │ │ └── xmlui.tmLanguage.json │ ├── test-samples │ │ └── sample.xmlui │ ├── tsconfig.json │ └── tsconfig.tsbuildinfo ├── turbo.json └── xmlui ├── .gitignore ├── bin │ ├── bootstrap.js │ ├── build-lib.ts │ ├── build.ts │ ├── index.ts │ ├── preview.ts │ ├── start.ts │ ├── vite-xmlui-plugin.ts │ └── viteConfig.ts ├── CHANGELOG.md ├── conventions │ ├── component-qa-checklist.md │ ├── copilot-conventions.md │ ├── create-xmlui-components.md │ ├── mermaid.md │ ├── testing-conventions.md │ └── xmlui-in-a-nutshell.md ├── dev-docs │ ├── accessibility.md │ ├── actions.md │ ├── AppRoot.md │ ├── component-apis.md │ ├── component-rendering.md │ ├── component-review-checklist.md │ ├── containers.md │ ├── data-sources.md │ ├── e2e-summary.md │ ├── expression-evaluation.md │ ├── glossary.md │ ├── helper-components.md │ ├── index.md │ ├── loaders.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 │ ├── rendering-fundamentals.md │ ├── reusable-components.md │ ├── standalone-apps.md │ ├── state-management.md │ └── xmlui-extensibility.xlsx ├── package.json ├── playwright.config.ts ├── scripts │ ├── coverage-only.js │ ├── e2e-test-summary.js │ ├── generate-docs │ │ ├── build-downloads-map.mjs │ │ ├── build-pages-map.mjs │ │ ├── components-config.json │ │ ├── configuration-management.mjs │ │ ├── constants.mjs │ │ ├── create-theme-files.mjs │ │ ├── DocsGenerator.mjs │ │ ├── error-handling.mjs │ │ ├── extensions-config.json │ │ ├── folders.mjs │ │ ├── generate-summary-files.mjs │ │ ├── get-docs.mjs │ │ ├── input-handler.mjs │ │ ├── logger.mjs │ │ ├── logging-standards.mjs │ │ ├── MetadataProcessor.mjs │ │ ├── pattern-utilities.mjs │ │ └── utils.mjs │ ├── get-langserver-metadata.mjs │ ├── inline-links.mjs │ └── README-e2e-summary.md ├── src │ ├── abstractions │ │ ├── _conventions.md │ │ ├── ActionDefs.ts │ │ ├── AppContextDefs.ts │ │ ├── ComponentDefs.ts │ │ ├── ContainerDefs.ts │ │ ├── ExtensionDefs.ts │ │ ├── FunctionDefs.ts │ │ ├── RendererDefs.ts │ │ ├── scripting │ │ │ ├── BlockScope.ts │ │ │ ├── Compilation.ts │ │ │ ├── LogicalThread.ts │ │ │ ├── LoopScope.ts │ │ │ ├── modules.ts │ │ │ ├── ScriptParserError.ts │ │ │ ├── Token.ts │ │ │ ├── TryScope.ts │ │ │ └── TryScopeExp.ts │ │ └── ThemingDefs.ts │ ├── components │ │ ├── _conventions.md │ │ ├── abstractions.ts │ │ ├── Accordion │ │ │ ├── Accordion.md │ │ │ ├── Accordion.module.scss │ │ │ ├── Accordion.spec.ts │ │ │ ├── Accordion.tsx │ │ │ ├── AccordionContext.tsx │ │ │ ├── AccordionItem.tsx │ │ │ ├── AccordionItemNative.tsx │ │ │ └── AccordionNative.tsx │ │ ├── Animation │ │ │ └── AnimationNative.tsx │ │ ├── APICall │ │ │ ├── APICall.md │ │ │ ├── APICall.spec.ts │ │ │ ├── APICall.tsx │ │ │ └── APICallNative.tsx │ │ ├── App │ │ │ ├── App.md │ │ │ ├── App.module.scss │ │ │ ├── App.spec.ts │ │ │ ├── App.tsx │ │ │ ├── AppLayoutContext.ts │ │ │ ├── AppNative.tsx │ │ │ ├── AppStateContext.ts │ │ │ ├── doc-resources │ │ │ │ ├── condensed-sticky.xmlui │ │ │ │ ├── condensed.xmlui │ │ │ │ ├── horizontal-sticky.xmlui │ │ │ │ ├── horizontal.xmlui │ │ │ │ ├── vertical-full-header.xmlui │ │ │ │ ├── vertical-sticky.xmlui │ │ │ │ └── vertical.xmlui │ │ │ ├── IndexerContext.ts │ │ │ ├── LinkInfoContext.ts │ │ │ ├── SearchContext.tsx │ │ │ ├── Sheet.module.scss │ │ │ └── Sheet.tsx │ │ ├── AppHeader │ │ │ ├── AppHeader.md │ │ │ ├── AppHeader.module.scss │ │ │ ├── AppHeader.spec.ts │ │ │ ├── AppHeader.tsx │ │ │ └── AppHeaderNative.tsx │ │ ├── AppState │ │ │ ├── AppState.md │ │ │ ├── AppState.spec.ts │ │ │ ├── AppState.tsx │ │ │ └── AppStateNative.tsx │ │ ├── AutoComplete │ │ │ ├── AutoComplete.md │ │ │ ├── AutoComplete.module.scss │ │ │ ├── AutoComplete.spec.ts │ │ │ ├── AutoComplete.tsx │ │ │ ├── AutoCompleteContext.tsx │ │ │ └── AutoCompleteNative.tsx │ │ ├── Avatar │ │ │ ├── Avatar.md │ │ │ ├── Avatar.module.scss │ │ │ ├── Avatar.spec.ts │ │ │ ├── Avatar.tsx │ │ │ └── AvatarNative.tsx │ │ ├── Backdrop │ │ │ ├── Backdrop.md │ │ │ ├── Backdrop.module.scss │ │ │ ├── Backdrop.spec.ts │ │ │ ├── Backdrop.tsx │ │ │ └── BackdropNative.tsx │ │ ├── Badge │ │ │ ├── Badge.md │ │ │ ├── Badge.module.scss │ │ │ ├── Badge.spec.ts │ │ │ ├── Badge.tsx │ │ │ └── BadgeNative.tsx │ │ ├── Bookmark │ │ │ ├── Bookmark.md │ │ │ ├── Bookmark.module.scss │ │ │ ├── Bookmark.spec.ts │ │ │ ├── Bookmark.tsx │ │ │ └── BookmarkNative.tsx │ │ ├── Breakout │ │ │ ├── Breakout.module.scss │ │ │ ├── Breakout.spec.ts │ │ │ ├── Breakout.tsx │ │ │ └── BreakoutNative.tsx │ │ ├── Button │ │ │ ├── Button-style.spec.ts │ │ │ ├── Button.md │ │ │ ├── Button.module.scss │ │ │ ├── Button.spec.ts │ │ │ ├── Button.tsx │ │ │ └── ButtonNative.tsx │ │ ├── Card │ │ │ ├── Card.md │ │ │ ├── Card.module.scss │ │ │ ├── Card.spec.ts │ │ │ ├── Card.tsx │ │ │ └── CardNative.tsx │ │ ├── Carousel │ │ │ ├── Carousel.md │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.spec.ts │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ ├── CarouselItem.tsx │ │ │ ├── CarouselItemNative.tsx │ │ │ └── CarouselNative.tsx │ │ ├── ChangeListener │ │ │ ├── ChangeListener.md │ │ │ ├── ChangeListener.spec.ts │ │ │ ├── ChangeListener.tsx │ │ │ └── ChangeListenerNative.tsx │ │ ├── chart-color-schemes.ts │ │ ├── Charts │ │ │ ├── AreaChart │ │ │ │ ├── AreaChart.md │ │ │ │ ├── AreaChart.spec.ts │ │ │ │ ├── AreaChart.tsx │ │ │ │ └── AreaChartNative.tsx │ │ │ ├── BarChart │ │ │ │ ├── BarChart.md │ │ │ │ ├── BarChart.module.scss │ │ │ │ ├── BarChart.spec.ts │ │ │ │ ├── BarChart.tsx │ │ │ │ └── BarChartNative.tsx │ │ │ ├── DonutChart │ │ │ │ ├── DonutChart.spec.ts │ │ │ │ └── DonutChart.tsx │ │ │ ├── LabelList │ │ │ │ ├── LabelList.spec.ts │ │ │ │ ├── LabelList.tsx │ │ │ │ ├── LabelListNative.module.scss │ │ │ │ └── LabelListNative.tsx │ │ │ ├── Legend │ │ │ │ ├── Legend.spec.ts │ │ │ │ ├── Legend.tsx │ │ │ │ └── LegendNative.tsx │ │ │ ├── LineChart │ │ │ │ ├── LineChart.md │ │ │ │ ├── LineChart.module.scss │ │ │ │ ├── LineChart.spec.ts │ │ │ │ ├── LineChart.tsx │ │ │ │ └── LineChartNative.tsx │ │ │ ├── PieChart │ │ │ │ ├── PieChart.md │ │ │ │ ├── PieChart.spec.ts │ │ │ │ ├── PieChart.tsx │ │ │ │ ├── PieChartNative.module.scss │ │ │ │ └── PieChartNative.tsx │ │ │ ├── RadarChart │ │ │ │ ├── RadarChart.md │ │ │ │ ├── RadarChart.spec.ts │ │ │ │ ├── RadarChart.tsx │ │ │ │ └── RadarChartNative.tsx │ │ │ ├── Tooltip │ │ │ │ ├── TooltipContent.module.scss │ │ │ │ ├── TooltipContent.spec.ts │ │ │ │ └── TooltipContent.tsx │ │ │ └── utils │ │ │ ├── abstractions.ts │ │ │ └── ChartProvider.tsx │ │ ├── Checkbox │ │ │ ├── Checkbox.md │ │ │ ├── Checkbox.spec.ts │ │ │ └── Checkbox.tsx │ │ ├── CodeBlock │ │ │ ├── CodeBlock.module.scss │ │ │ ├── CodeBlock.spec.ts │ │ │ ├── CodeBlock.tsx │ │ │ ├── CodeBlockNative.tsx │ │ │ └── highlight-code.ts │ │ ├── collectedComponentMetadata.ts │ │ ├── ColorPicker │ │ │ ├── ColorPicker.md │ │ │ ├── ColorPicker.module.scss │ │ │ ├── ColorPicker.spec.ts │ │ │ ├── ColorPicker.tsx │ │ │ └── ColorPickerNative.tsx │ │ ├── Column │ │ │ ├── Column.md │ │ │ ├── Column.tsx │ │ │ ├── ColumnNative.tsx │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ └── TableContext.tsx │ │ ├── component-utils.ts │ │ ├── ComponentProvider.tsx │ │ ├── ComponentRegistryContext.tsx │ │ ├── container-helpers.tsx │ │ ├── ContentSeparator │ │ │ ├── ContentSeparator.md │ │ │ ├── ContentSeparator.module.scss │ │ │ ├── ContentSeparator.spec.ts │ │ │ ├── ContentSeparator.tsx │ │ │ └── ContentSeparatorNative.tsx │ │ ├── DataSource │ │ │ ├── DataSource.md │ │ │ └── DataSource.tsx │ │ ├── DateInput │ │ │ ├── DateInput.md │ │ │ ├── DateInput.module.scss │ │ │ ├── DateInput.spec.ts │ │ │ ├── DateInput.tsx │ │ │ └── DateInputNative.tsx │ │ ├── DatePicker │ │ │ ├── DatePicker.md │ │ │ ├── DatePicker.module.scss │ │ │ ├── DatePicker.spec.ts │ │ │ ├── DatePicker.tsx │ │ │ └── DatePickerNative.tsx │ │ ├── DropdownMenu │ │ │ ├── DropdownMenu.md │ │ │ ├── DropdownMenu.module.scss │ │ │ ├── DropdownMenu.spec.ts │ │ │ ├── DropdownMenu.tsx │ │ │ ├── DropdownMenuNative.tsx │ │ │ ├── MenuItem.md │ │ │ └── SubMenuItem.md │ │ ├── EmojiSelector │ │ │ ├── EmojiSelector.md │ │ │ ├── EmojiSelector.spec.ts │ │ │ ├── EmojiSelector.tsx │ │ │ └── EmojiSelectorNative.tsx │ │ ├── ExpandableItem │ │ │ ├── ExpandableItem.module.scss │ │ │ ├── ExpandableItem.spec.ts │ │ │ ├── ExpandableItem.tsx │ │ │ └── ExpandableItemNative.tsx │ │ ├── FileInput │ │ │ ├── FileInput.md │ │ │ ├── FileInput.module.scss │ │ │ ├── FileInput.spec.ts │ │ │ ├── FileInput.tsx │ │ │ └── FileInputNative.tsx │ │ ├── FileUploadDropZone │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FileUploadDropZone.module.scss │ │ │ ├── FileUploadDropZone.spec.ts │ │ │ ├── FileUploadDropZone.tsx │ │ │ └── FileUploadDropZoneNative.tsx │ │ ├── FlowLayout │ │ │ ├── FlowLayout.md │ │ │ ├── FlowLayout.module.scss │ │ │ ├── FlowLayout.spec.ts │ │ │ ├── FlowLayout.spec.ts-snapshots │ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png │ │ │ ├── FlowLayout.tsx │ │ │ └── FlowLayoutNative.tsx │ │ ├── Footer │ │ │ ├── Footer.md │ │ │ ├── Footer.module.scss │ │ │ ├── Footer.spec.ts │ │ │ ├── Footer.tsx │ │ │ └── FooterNative.tsx │ │ ├── Form │ │ │ ├── Form.md │ │ │ ├── Form.module.scss │ │ │ ├── Form.spec.ts │ │ │ ├── Form.tsx │ │ │ ├── formActions.ts │ │ │ ├── FormContext.ts │ │ │ └── FormNative.tsx │ │ ├── FormItem │ │ │ ├── FormItem.md │ │ │ ├── FormItem.module.scss │ │ │ ├── FormItem.spec.ts │ │ │ ├── FormItem.tsx │ │ │ ├── FormItemNative.tsx │ │ │ ├── HelperText.module.scss │ │ │ ├── HelperText.tsx │ │ │ ├── ItemWithLabel.tsx │ │ │ └── Validations.ts │ │ ├── FormSection │ │ │ ├── FormSection.md │ │ │ ├── FormSection.ts │ │ │ └── FormSection.xmlui │ │ ├── Fragment │ │ │ ├── Fragment.spec.ts │ │ │ └── Fragment.tsx │ │ ├── Heading │ │ │ ├── abstractions.ts │ │ │ ├── H1.md │ │ │ ├── H1.spec.ts │ │ │ ├── H2.md │ │ │ ├── H2.spec.ts │ │ │ ├── H3.md │ │ │ ├── H3.spec.ts │ │ │ ├── H4.md │ │ │ ├── H4.spec.ts │ │ │ ├── H5.md │ │ │ ├── H5.spec.ts │ │ │ ├── H6.md │ │ │ ├── H6.spec.ts │ │ │ ├── Heading.md │ │ │ ├── Heading.module.scss │ │ │ ├── Heading.spec.ts │ │ │ ├── Heading.tsx │ │ │ └── HeadingNative.tsx │ │ ├── HoverCard │ │ │ ├── HoverCard.tsx │ │ │ └── HovercardNative.tsx │ │ ├── HtmlTags │ │ │ ├── HtmlTags.module.scss │ │ │ ├── HtmlTags.spec.ts │ │ │ └── HtmlTags.tsx │ │ ├── Icon │ │ │ ├── AdmonitionDanger.tsx │ │ │ ├── AdmonitionInfo.tsx │ │ │ ├── AdmonitionNote.tsx │ │ │ ├── AdmonitionTip.tsx │ │ │ ├── AdmonitionWarning.tsx │ │ │ ├── ApiIcon.tsx │ │ │ ├── ArrowDropDown.module.scss │ │ │ ├── ArrowDropDown.tsx │ │ │ ├── ArrowDropUp.module.scss │ │ │ ├── ArrowDropUp.tsx │ │ │ ├── ArrowLeft.module.scss │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowRight.module.scss │ │ │ ├── ArrowRight.tsx │ │ │ ├── Attach.tsx │ │ │ ├── Binding.module.scss │ │ │ ├── Binding.tsx │ │ │ ├── BoardIcon.tsx │ │ │ ├── BoxIcon.tsx │ │ │ ├── CheckIcon.tsx │ │ │ ├── ChevronDownIcon.tsx │ │ │ ├── ChevronLeft.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── ChevronUpIcon.tsx │ │ │ ├── CodeFileIcon.tsx │ │ │ ├── CodeSandbox.tsx │ │ │ ├── CompactListIcon.tsx │ │ │ ├── ContentCopyIcon.tsx │ │ │ ├── DarkToLightIcon.tsx │ │ │ ├── DatabaseIcon.module.scss │ │ │ ├── DatabaseIcon.tsx │ │ │ ├── DocFileIcon.tsx │ │ │ ├── DocIcon.tsx │ │ │ ├── DotMenuHorizontalIcon.tsx │ │ │ ├── DotMenuIcon.tsx │ │ │ ├── EmailIcon.tsx │ │ │ ├── EmptyFolderIcon.tsx │ │ │ ├── ErrorIcon.tsx │ │ │ ├── ExpressionIcon.tsx │ │ │ ├── FillPlusCricleIcon.tsx │ │ │ ├── FilterIcon.tsx │ │ │ ├── FolderIcon.tsx │ │ │ ├── GlobeIcon.tsx │ │ │ ├── HomeIcon.tsx │ │ │ ├── HyperLinkIcon.tsx │ │ │ ├── Icon.md │ │ │ ├── Icon.module.scss │ │ │ ├── Icon.spec.ts │ │ │ ├── Icon.tsx │ │ │ ├── IconNative.tsx │ │ │ ├── ImageFileIcon.tsx │ │ │ ├── Inspect.tsx │ │ │ ├── LightToDark.tsx │ │ │ ├── LinkIcon.tsx │ │ │ ├── ListIcon.tsx │ │ │ ├── LooseListIcon.tsx │ │ │ ├── MoonIcon.tsx │ │ │ ├── MoreOptionsIcon.tsx │ │ │ ├── NoSortIcon.tsx │ │ │ ├── PDFIcon.tsx │ │ │ ├── PenIcon.tsx │ │ │ ├── PhoneIcon.tsx │ │ │ ├── PhotoIcon.tsx │ │ │ ├── PlusIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── ShareIcon.tsx │ │ │ ├── SortAscendingIcon.tsx │ │ │ ├── SortDescendingIcon.tsx │ │ │ ├── StarsIcon.tsx │ │ │ ├── SunIcon.tsx │ │ │ ├── svg │ │ │ │ ├── admonition_danger.svg │ │ │ │ ├── admonition_info.svg │ │ │ │ ├── admonition_note.svg │ │ │ │ ├── admonition_tip.svg │ │ │ │ ├── admonition_warning.svg │ │ │ │ ├── api.svg │ │ │ │ ├── arrow-dropdown.svg │ │ │ │ ├── arrow-left.svg │ │ │ │ ├── arrow-right.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── attach.svg │ │ │ │ ├── binding.svg │ │ │ │ ├── box.svg │ │ │ │ ├── bulb.svg │ │ │ │ ├── code-file.svg │ │ │ │ ├── code-sandbox.svg │ │ │ │ ├── dark_to_light.svg │ │ │ │ ├── database.svg │ │ │ │ ├── doc.svg │ │ │ │ ├── empty-folder.svg │ │ │ │ ├── expression.svg │ │ │ │ ├── eye-closed.svg │ │ │ │ ├── eye-dark.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── file-text.svg │ │ │ │ ├── filter.svg │ │ │ │ ├── folder.svg │ │ │ │ ├── img.svg │ │ │ │ ├── inspect.svg │ │ │ │ ├── light_to_dark.svg │ │ │ │ ├── moon.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── photo.svg │ │ │ │ ├── share.svg │ │ │ │ ├── stars.svg │ │ │ │ ├── sun.svg │ │ │ │ ├── trending-down.svg │ │ │ │ ├── trending-level.svg │ │ │ │ ├── trending-up.svg │ │ │ │ ├── txt.svg │ │ │ │ ├── unknown-file.svg │ │ │ │ ├── unlink.svg │ │ │ │ └── xls.svg │ │ │ ├── TableDeleteColumnIcon.tsx │ │ │ ├── TableDeleteRowIcon.tsx │ │ │ ├── TableInsertColumnIcon.tsx │ │ │ ├── TableInsertRowIcon.tsx │ │ │ ├── TrashIcon.tsx │ │ │ ├── TrendingDownIcon.tsx │ │ │ ├── TrendingLevelIcon.tsx │ │ │ ├── TrendingUpIcon.tsx │ │ │ ├── TxtIcon.tsx │ │ │ ├── UnknownFileIcon.tsx │ │ │ ├── UnlinkIcon.tsx │ │ │ ├── UserIcon.tsx │ │ │ ├── WarningIcon.tsx │ │ │ └── XlsIcon.tsx │ │ ├── IconProvider.tsx │ │ ├── IconRegistryContext.tsx │ │ ├── IFrame │ │ │ ├── IFrame.md │ │ │ ├── IFrame.module.scss │ │ │ ├── IFrame.spec.ts │ │ │ ├── IFrame.tsx │ │ │ └── IFrameNative.tsx │ │ ├── Image │ │ │ ├── Image.md │ │ │ ├── Image.module.scss │ │ │ ├── Image.spec.ts │ │ │ ├── Image.tsx │ │ │ └── ImageNative.tsx │ │ ├── Input │ │ │ ├── index.ts │ │ │ ├── InputAdornment.module.scss │ │ │ ├── InputAdornment.tsx │ │ │ ├── InputDivider.module.scss │ │ │ ├── InputDivider.tsx │ │ │ ├── InputLabel.module.scss │ │ │ ├── InputLabel.tsx │ │ │ ├── PartialInput.module.scss │ │ │ └── PartialInput.tsx │ │ ├── InspectButton │ │ │ ├── InspectButton.module.scss │ │ │ └── InspectButton.tsx │ │ ├── Items │ │ │ ├── Items.md │ │ │ ├── Items.spec.ts │ │ │ ├── Items.tsx │ │ │ └── ItemsNative.tsx │ │ ├── Link │ │ │ ├── Link.md │ │ │ ├── Link.module.scss │ │ │ ├── Link.spec.ts │ │ │ ├── Link.tsx │ │ │ └── LinkNative.tsx │ │ ├── List │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── List.md │ │ │ ├── List.module.scss │ │ │ ├── List.spec.ts │ │ │ ├── List.tsx │ │ │ └── ListNative.tsx │ │ ├── Logo │ │ │ ├── doc-resources │ │ │ │ └── xmlui-logo.svg │ │ │ ├── Logo.md │ │ │ ├── Logo.tsx │ │ │ └── LogoNative.tsx │ │ ├── Markdown │ │ │ ├── CodeText.module.scss │ │ │ ├── CodeText.tsx │ │ │ ├── Markdown.md │ │ │ ├── Markdown.module.scss │ │ │ ├── Markdown.spec.ts │ │ │ ├── Markdown.tsx │ │ │ ├── MarkdownNative.tsx │ │ │ ├── parse-binding-expr.ts │ │ │ └── utils.ts │ │ ├── metadata-helpers.ts │ │ ├── ModalDialog │ │ │ ├── ConfirmationModalContextProvider.tsx │ │ │ ├── Dialog.module.scss │ │ │ ├── Dialog.tsx │ │ │ ├── ModalDialog.md │ │ │ ├── ModalDialog.module.scss │ │ │ ├── ModalDialog.spec.ts │ │ │ ├── ModalDialog.tsx │ │ │ ├── ModalDialogNative.tsx │ │ │ └── ModalVisibilityContext.tsx │ │ ├── NavGroup │ │ │ ├── NavGroup.md │ │ │ ├── NavGroup.module.scss │ │ │ ├── NavGroup.spec.ts │ │ │ ├── NavGroup.tsx │ │ │ ├── NavGroupContext.ts │ │ │ └── NavGroupNative.tsx │ │ ├── NavLink │ │ │ ├── NavLink.md │ │ │ ├── NavLink.module.scss │ │ │ ├── NavLink.spec.ts │ │ │ ├── NavLink.tsx │ │ │ └── NavLinkNative.tsx │ │ ├── NavPanel │ │ │ ├── NavPanel.md │ │ │ ├── NavPanel.module.scss │ │ │ ├── NavPanel.spec.ts │ │ │ ├── NavPanel.tsx │ │ │ └── NavPanelNative.tsx │ │ ├── NestedApp │ │ │ ├── AppWithCodeView.module.scss │ │ │ ├── AppWithCodeView.tsx │ │ │ ├── AppWithCodeViewNative.tsx │ │ │ ├── defaultProps.tsx │ │ │ ├── logo.svg │ │ │ ├── NestedApp.module.scss │ │ │ ├── NestedApp.tsx │ │ │ ├── NestedAppNative.tsx │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.tsx │ │ │ └── utils.ts │ │ ├── NoResult │ │ │ ├── NoResult.md │ │ │ ├── NoResult.module.scss │ │ │ ├── NoResult.spec.ts │ │ │ ├── NoResult.tsx │ │ │ └── NoResultNative.tsx │ │ ├── NumberBox │ │ │ ├── numberbox-abstractions.ts │ │ │ ├── NumberBox.md │ │ │ ├── NumberBox.module.scss │ │ │ ├── NumberBox.spec.ts │ │ │ ├── NumberBox.tsx │ │ │ └── NumberBoxNative.tsx │ │ ├── Option │ │ │ ├── Option.md │ │ │ ├── Option.spec.ts │ │ │ ├── Option.tsx │ │ │ ├── OptionNative.tsx │ │ │ └── OptionTypeProvider.tsx │ │ ├── PageMetaTitle │ │ │ ├── PageMetaTilteNative.tsx │ │ │ ├── PageMetaTitle.md │ │ │ ├── PageMetaTitle.spec.ts │ │ │ └── PageMetaTitle.tsx │ │ ├── Pages │ │ │ ├── Page.md │ │ │ ├── Pages.md │ │ │ ├── Pages.module.scss │ │ │ ├── Pages.tsx │ │ │ └── PagesNative.tsx │ │ ├── Pagination │ │ │ ├── Pagination.md │ │ │ ├── Pagination.module.scss │ │ │ ├── Pagination.spec.ts │ │ │ ├── Pagination.tsx │ │ │ └── PaginationNative.tsx │ │ ├── PositionedContainer │ │ │ ├── PositionedContainer.module.scss │ │ │ ├── PositionedContainer.tsx │ │ │ └── PositionedContainerNative.tsx │ │ ├── ProfileMenu │ │ │ ├── ProfileMenu.module.scss │ │ │ └── ProfileMenu.tsx │ │ ├── ProgressBar │ │ │ ├── ProgressBar.md │ │ │ ├── ProgressBar.module.scss │ │ │ ├── ProgressBar.spec.ts │ │ │ ├── ProgressBar.tsx │ │ │ └── ProgressBarNative.tsx │ │ ├── Queue │ │ │ ├── Queue.md │ │ │ ├── Queue.spec.ts │ │ │ ├── Queue.tsx │ │ │ ├── queueActions.ts │ │ │ └── QueueNative.tsx │ │ ├── RadioGroup │ │ │ ├── RadioGroup.md │ │ │ ├── RadioGroup.module.scss │ │ │ ├── RadioGroup.spec.ts │ │ │ ├── RadioGroup.tsx │ │ │ ├── RadioGroupNative.tsx │ │ │ ├── RadioItem.tsx │ │ │ └── RadioItemNative.tsx │ │ ├── RealTimeAdapter │ │ │ ├── RealTimeAdapter.tsx │ │ │ └── RealTimeAdapterNative.tsx │ │ ├── Redirect │ │ │ ├── Redirect.md │ │ │ ├── Redirect.spec.ts │ │ │ └── Redirect.tsx │ │ ├── ResponsiveBar │ │ │ ├── README.md │ │ │ ├── ResponsiveBar.md │ │ │ ├── ResponsiveBar.module.scss │ │ │ ├── ResponsiveBar.spec.ts │ │ │ ├── ResponsiveBar.tsx │ │ │ └── ResponsiveBarNative.tsx │ │ ├── Select │ │ │ ├── HiddenOption.tsx │ │ │ ├── OptionContext.ts │ │ │ ├── Select.md │ │ │ ├── Select.module.scss │ │ │ ├── Select.spec.ts │ │ │ ├── Select.tsx │ │ │ ├── SelectContext.tsx │ │ │ └── SelectNative.tsx │ │ ├── SelectionStore │ │ │ ├── SelectionStore.md │ │ │ ├── SelectionStore.tsx │ │ │ └── SelectionStoreNative.tsx │ │ ├── Slider │ │ │ ├── Slider.md │ │ │ ├── Slider.module.scss │ │ │ ├── Slider.spec.ts │ │ │ ├── Slider.tsx │ │ │ └── SliderNative.tsx │ │ ├── Slot │ │ │ ├── Slot.md │ │ │ ├── Slot.spec.ts │ │ │ └── Slot.ts │ │ ├── SlotItem.tsx │ │ ├── SpaceFiller │ │ │ ├── SpaceFiller.md │ │ │ ├── SpaceFiller.module.scss │ │ │ ├── SpaceFiller.spec.ts │ │ │ ├── SpaceFiller.tsx │ │ │ └── SpaceFillerNative.tsx │ │ ├── Spinner │ │ │ ├── Spinner.md │ │ │ ├── Spinner.module.scss │ │ │ ├── Spinner.spec.ts │ │ │ ├── Spinner.tsx │ │ │ └── SpinnerNative.tsx │ │ ├── Splitter │ │ │ ├── HSplitter.md │ │ │ ├── HSplitter.spec.ts │ │ │ ├── Splitter.md │ │ │ ├── Splitter.module.scss │ │ │ ├── Splitter.spec.ts │ │ │ ├── Splitter.tsx │ │ │ ├── SplitterNative.tsx │ │ │ ├── utils.ts │ │ │ ├── VSplitter.md │ │ │ └── VSplitter.spec.ts │ │ ├── Stack │ │ │ ├── CHStack.md │ │ │ ├── CHStack.spec.ts │ │ │ ├── CVStack.md │ │ │ ├── CVStack.spec.ts │ │ │ ├── HStack.md │ │ │ ├── HStack.spec.ts │ │ │ ├── Stack.md │ │ │ ├── Stack.module.scss │ │ │ ├── Stack.spec.ts │ │ │ ├── Stack.tsx │ │ │ ├── StackNative.tsx │ │ │ ├── VStack.md │ │ │ └── VStack.spec.ts │ │ ├── StickyBox │ │ │ ├── StickyBox.md │ │ │ ├── StickyBox.module.scss │ │ │ ├── StickyBox.tsx │ │ │ └── StickyBoxNative.tsx │ │ ├── Switch │ │ │ ├── Switch.md │ │ │ ├── Switch.spec.ts │ │ │ └── Switch.tsx │ │ ├── Table │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── react-table-config.d.ts │ │ │ ├── Table.md │ │ │ ├── Table.module.scss │ │ │ ├── Table.spec.ts │ │ │ ├── Table.tsx │ │ │ ├── TableNative.tsx │ │ │ └── useRowSelection.tsx │ │ ├── TableOfContents │ │ │ ├── TableOfContents.module.scss │ │ │ ├── TableOfContents.spec.ts │ │ │ ├── TableOfContents.tsx │ │ │ └── TableOfContentsNative.tsx │ │ ├── Tabs │ │ │ ├── TabContext.tsx │ │ │ ├── TabItem.md │ │ │ ├── TabItem.tsx │ │ │ ├── TabItemNative.tsx │ │ │ ├── Tabs.md │ │ │ ├── Tabs.module.scss │ │ │ ├── Tabs.spec.ts │ │ │ ├── Tabs.tsx │ │ │ └── TabsNative.tsx │ │ ├── Text │ │ │ ├── Text.md │ │ │ ├── Text.module.scss │ │ │ ├── Text.spec.ts │ │ │ ├── Text.tsx │ │ │ └── TextNative.tsx │ │ ├── TextArea │ │ │ ├── TextArea.md │ │ │ ├── TextArea.module.scss │ │ │ ├── TextArea.spec.ts │ │ │ ├── TextArea.tsx │ │ │ ├── TextAreaNative.tsx │ │ │ ├── TextAreaResizable.tsx │ │ │ └── useComposedRef.ts │ │ ├── TextBox │ │ │ ├── TextBox.md │ │ │ ├── TextBox.module.scss │ │ │ ├── TextBox.spec.ts │ │ │ ├── TextBox.tsx │ │ │ └── TextBoxNative.tsx │ │ ├── Theme │ │ │ ├── NotificationToast.tsx │ │ │ ├── Theme.md │ │ │ ├── Theme.module.scss │ │ │ ├── Theme.spec.ts │ │ │ ├── Theme.tsx │ │ │ └── ThemeNative.tsx │ │ ├── TimeInput │ │ │ ├── TimeInput.md │ │ │ ├── TimeInput.module.scss │ │ │ ├── TimeInput.spec.ts │ │ │ ├── TimeInput.tsx │ │ │ ├── TimeInputNative.tsx │ │ │ └── utils.ts │ │ ├── Timer │ │ │ ├── Timer.md │ │ │ ├── Timer.spec.ts │ │ │ ├── Timer.tsx │ │ │ └── TimerNative.tsx │ │ ├── Toggle │ │ │ ├── Toggle.module.scss │ │ │ └── Toggle.tsx │ │ ├── ToneChangerButton │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneChangerButton.spec.ts │ │ │ └── ToneChangerButton.tsx │ │ ├── ToneSwitch │ │ │ ├── ToneSwitch.md │ │ │ ├── ToneSwitch.module.scss │ │ │ ├── ToneSwitch.spec.ts │ │ │ ├── ToneSwitch.tsx │ │ │ └── ToneSwitchNative.tsx │ │ ├── Tooltip │ │ │ ├── Tooltip.md │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.spec.ts │ │ │ ├── Tooltip.tsx │ │ │ └── TooltipNative.tsx │ │ ├── Tree │ │ │ ├── testData.ts │ │ │ ├── Tree-dynamic.spec.ts │ │ │ ├── Tree-icons.spec.ts │ │ │ ├── Tree.md │ │ │ ├── Tree.spec.ts │ │ │ ├── TreeComponent.module.scss │ │ │ ├── TreeComponent.tsx │ │ │ └── TreeNative.tsx │ │ ├── TreeDisplay │ │ │ ├── TreeDisplay.md │ │ │ ├── TreeDisplay.module.scss │ │ │ ├── TreeDisplay.tsx │ │ │ └── TreeDisplayNative.tsx │ │ ├── ValidationSummary │ │ │ ├── ValidationSummary.module.scss │ │ │ └── ValidationSummary.tsx │ │ └── VisuallyHidden.tsx │ ├── components-core │ │ ├── abstractions │ │ │ ├── ComponentRenderer.ts │ │ │ ├── LoaderRenderer.ts │ │ │ ├── standalone.ts │ │ │ └── treeAbstractions.ts │ │ ├── action │ │ │ ├── actions.ts │ │ │ ├── APICall.tsx │ │ │ ├── FileDownloadAction.tsx │ │ │ ├── FileUploadAction.tsx │ │ │ ├── NavigateAction.tsx │ │ │ └── TimedAction.tsx │ │ ├── ApiBoundComponent.tsx │ │ ├── appContext │ │ │ ├── date-functions.ts │ │ │ ├── math-function.ts │ │ │ └── misc-utils.ts │ │ ├── AppContext.tsx │ │ ├── behaviors │ │ │ ├── Behavior.tsx │ │ │ ├── BehaviorContext.tsx │ │ │ └── CoreBehaviors.tsx │ │ ├── component-hooks.ts │ │ ├── ComponentDecorator.tsx │ │ ├── ComponentViewer.tsx │ │ ├── CompoundComponent.tsx │ │ ├── constants.ts │ │ ├── DebugViewProvider.tsx │ │ ├── descriptorHelper.ts │ │ ├── devtools │ │ │ ├── InspectorDialog.module.scss │ │ │ ├── InspectorDialog.tsx │ │ │ └── InspectorDialogVisibilityContext.tsx │ │ ├── EngineError.ts │ │ ├── event-handlers.ts │ │ ├── InspectorButton.module.scss │ │ ├── InspectorContext.tsx │ │ ├── interception │ │ │ ├── abstractions.ts │ │ │ ├── ApiInterceptor.ts │ │ │ ├── ApiInterceptorProvider.tsx │ │ │ ├── apiInterceptorWorker.ts │ │ │ ├── Backend.ts │ │ │ ├── Errors.ts │ │ │ ├── IndexedDb.ts │ │ │ ├── initMock.ts │ │ │ ├── InMemoryDb.ts │ │ │ ├── ReadonlyCollection.ts │ │ │ └── useApiInterceptorContext.tsx │ │ ├── loader │ │ │ ├── ApiLoader.tsx │ │ │ ├── DataLoader.tsx │ │ │ ├── ExternalDataLoader.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MockLoaderRenderer.tsx │ │ │ └── PageableLoader.tsx │ │ ├── LoaderComponent.tsx │ │ ├── markup-check.ts │ │ ├── parts.ts │ │ ├── renderers.ts │ │ ├── rendering │ │ │ ├── AppContent.tsx │ │ │ ├── AppRoot.tsx │ │ │ ├── AppWrapper.tsx │ │ │ ├── buildProxy.ts │ │ │ ├── collectFnVarDeps.ts │ │ │ ├── ComponentAdapter.tsx │ │ │ ├── ComponentWrapper.tsx │ │ │ ├── Container.tsx │ │ │ ├── containers.ts │ │ │ ├── ContainerWrapper.tsx │ │ │ ├── ErrorBoundary.module.scss │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── InvalidComponent.module.scss │ │ │ ├── InvalidComponent.tsx │ │ │ ├── nodeUtils.ts │ │ │ ├── reducer.ts │ │ │ ├── renderChild.tsx │ │ │ ├── StandaloneComponent.tsx │ │ │ ├── StateContainer.tsx │ │ │ ├── UnknownComponent.module.scss │ │ │ ├── UnknownComponent.tsx │ │ │ └── valueExtractor.ts │ │ ├── reportEngineError.ts │ │ ├── RestApiProxy.ts │ │ ├── script-runner │ │ │ ├── asyncProxy.ts │ │ │ ├── AttributeValueParser.ts │ │ │ ├── bannedFunctions.ts │ │ │ ├── BindingTreeEvaluationContext.ts │ │ │ ├── eval-tree-async.ts │ │ │ ├── eval-tree-common.ts │ │ │ ├── eval-tree-sync.ts │ │ │ ├── ParameterParser.ts │ │ │ ├── process-statement-async.ts │ │ │ ├── process-statement-common.ts │ │ │ ├── process-statement-sync.ts │ │ │ ├── ScriptingSourceTree.ts │ │ │ ├── simplify-expression.ts │ │ │ ├── statement-queue.ts │ │ │ └── visitors.ts │ │ ├── StandaloneApp.tsx │ │ ├── StandaloneExtensionManager.ts │ │ ├── TableOfContentsContext.tsx │ │ ├── theming │ │ │ ├── _themes.scss │ │ │ ├── component-layout-resolver.ts │ │ │ ├── extendThemeUtils.ts │ │ │ ├── hvar.ts │ │ │ ├── layout-resolver.ts │ │ │ ├── parse-layout-props.ts │ │ │ ├── StyleContext.tsx │ │ │ ├── StyleRegistry.ts │ │ │ ├── ThemeContext.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ ├── themes │ │ │ │ ├── base-utils.ts │ │ │ │ ├── palette.ts │ │ │ │ ├── root.ts │ │ │ │ ├── solid.ts │ │ │ │ ├── theme-colors.ts │ │ │ │ └── xmlui.ts │ │ │ ├── themeVars.module.scss │ │ │ ├── themeVars.ts │ │ │ ├── transformThemeVars.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── actionUtils.ts │ │ │ ├── audio-utils.ts │ │ │ ├── compound-utils.ts │ │ │ ├── css-utils.ts │ │ │ ├── DataLoaderQueryKeyGenerator.ts │ │ │ ├── date-utils.ts │ │ │ ├── extractParam.ts │ │ │ ├── hooks.tsx │ │ │ ├── LruCache.ts │ │ │ ├── mergeProps.ts │ │ │ ├── misc.ts │ │ │ ├── request-params.ts │ │ │ ├── statementUtils.ts │ │ │ └── treeUtils.ts │ │ └── xmlui-parser.ts │ ├── index-standalone.ts │ ├── index.scss │ ├── index.ts │ ├── language-server │ │ ├── server-common.ts │ │ ├── server-web-worker.ts │ │ ├── server.ts │ │ ├── services │ │ │ ├── common │ │ │ │ ├── docs-generation.ts │ │ │ │ ├── lsp-utils.ts │ │ │ │ ├── metadata-utils.ts │ │ │ │ └── syntax-node-utilities.ts │ │ │ ├── completion.ts │ │ │ ├── diagnostic.ts │ │ │ ├── format.ts │ │ │ └── hover.ts │ │ └── xmlui-metadata-generated.mjs │ ├── logging │ │ ├── LoggerContext.tsx │ │ ├── LoggerInitializer.tsx │ │ ├── LoggerService.ts │ │ └── xmlui.ts │ ├── logo.svg │ ├── parsers │ │ ├── common │ │ │ ├── GenericToken.ts │ │ │ ├── InputStream.ts │ │ │ └── utils.ts │ │ ├── scripting │ │ │ ├── code-behind-collect.ts │ │ │ ├── Lexer.ts │ │ │ ├── modules.ts │ │ │ ├── Parser.ts │ │ │ ├── ParserError.ts │ │ │ ├── ScriptingNodeTypes.ts │ │ │ ├── TokenTrait.ts │ │ │ ├── TokenType.ts │ │ │ └── tree-visitor.ts │ │ ├── style-parser │ │ │ ├── errors.ts │ │ │ ├── source-tree.ts │ │ │ ├── StyleInputStream.ts │ │ │ ├── StyleLexer.ts │ │ │ ├── StyleParser.ts │ │ │ └── tokens.ts │ │ └── xmlui-parser │ │ ├── CharacterCodes.ts │ │ ├── diagnostics.ts │ │ ├── fileExtensions.ts │ │ ├── index.ts │ │ ├── lint.ts │ │ ├── parser.ts │ │ ├── ParserError.ts │ │ ├── scanner.ts │ │ ├── syntax-kind.ts │ │ ├── syntax-node.ts │ │ ├── transform.ts │ │ ├── utils.ts │ │ ├── xmlui-serializer.ts │ │ └── xmlui-tree.ts │ ├── react-app-env.d.ts │ ├── syntax │ │ ├── monaco │ │ │ ├── grammar.monacoLanguage.ts │ │ │ ├── index.ts │ │ │ ├── xmlui-dark.ts │ │ │ ├── xmlui-light.ts │ │ │ └── xmluiscript.monacoLanguage.ts │ │ └── textMate │ │ ├── index.ts │ │ ├── xmlui-dark.json │ │ ├── xmlui-light.json │ │ ├── xmlui.json │ │ └── xmlui.tmLanguage.json │ ├── testing │ │ ├── assertions.ts │ │ ├── component-test-helpers.ts │ │ ├── ComponentDrivers.ts │ │ ├── drivers │ │ │ ├── DateInputDriver.ts │ │ │ ├── ModalDialogDriver.ts │ │ │ ├── NumberBoxDriver.ts │ │ │ ├── TextBoxDriver.ts │ │ │ ├── TimeInputDriver.ts │ │ │ ├── TimerDriver.ts │ │ │ └── TreeDriver.ts │ │ ├── fixtures.ts │ │ ├── infrastructure │ │ │ ├── index.html │ │ │ ├── main.tsx │ │ │ ├── public │ │ │ │ ├── mockServiceWorker.js │ │ │ │ ├── resources │ │ │ │ │ ├── bell.svg │ │ │ │ │ ├── box.svg │ │ │ │ │ ├── doc.svg │ │ │ │ │ ├── eye.svg │ │ │ │ │ ├── flower-640x480.jpg │ │ │ │ │ ├── sun.svg │ │ │ │ │ ├── test-image-100x100.jpg │ │ │ │ │ └── txt.svg │ │ │ │ └── serve.json │ │ │ └── TestBed.tsx │ │ └── themed-app-test-helpers.ts │ └── vite-env.d.ts ├── tests │ ├── components │ │ ├── CodeBlock │ │ │ └── hightlight-code.test.ts │ │ ├── playground-pattern.test.ts │ │ └── Tree │ │ └── Tree-states.test.ts │ ├── components-core │ │ ├── abstractions │ │ │ └── treeAbstractions.test.ts │ │ ├── container │ │ │ └── buildProxy.test.ts │ │ ├── interception │ │ │ ├── orderBy.test.ts │ │ │ ├── ReadOnlyCollection.test.ts │ │ │ └── request-param-converter.test.ts │ │ ├── scripts-runner │ │ │ ├── AttributeValueParser.test.ts │ │ │ ├── eval-tree-arrow-async.test.ts │ │ │ ├── eval-tree-arrow.test.ts │ │ │ ├── eval-tree-func-decl-async.test.ts │ │ │ ├── eval-tree-func-decl.test.ts │ │ │ ├── eval-tree-pre-post.test.ts │ │ │ ├── eval-tree-regression.test.ts │ │ │ ├── eval-tree.test.ts │ │ │ ├── function-proxy.test.ts │ │ │ ├── parser-regression.test.ts │ │ │ ├── process-event.test.ts │ │ │ ├── process-function.test.ts │ │ │ ├── process-implicit-context.test.ts │ │ │ ├── process-statement-asgn.test.ts │ │ │ ├── process-statement-destruct.test.ts │ │ │ ├── process-statement-regs.test.ts │ │ │ ├── process-statement-sync.test.ts │ │ │ ├── process-statement.test.ts │ │ │ ├── process-switch-sync.test.ts │ │ │ ├── process-switch.test.ts │ │ │ ├── process-try-sync.test.ts │ │ │ ├── process-try.test.ts │ │ │ └── test-helpers.ts │ │ ├── test-metadata-handler.ts │ │ ├── theming │ │ │ ├── border-segments.test.ts │ │ │ ├── component-layout.resolver.test.ts │ │ │ ├── layout-property-parser.test.ts │ │ │ ├── layout-resolver.test.ts │ │ │ ├── layout-resolver2.test.ts │ │ │ ├── layout-vp-override.test.ts │ │ │ └── padding-segments.test.ts │ │ └── utils │ │ ├── date-utils.test.ts │ │ ├── format-human-elapsed-time.test.ts │ │ └── LruCache.test.ts │ ├── language-server │ │ ├── completion.test.ts │ │ ├── format.test.ts │ │ ├── hover.test.ts │ │ └── mockData.ts │ └── parsers │ ├── common │ │ └── input-stream.test.ts │ ├── markdown │ │ └── parse-binding-expression.test.ts │ ├── parameter-parser.test.ts │ ├── paremeter-parser.test.ts │ ├── scripting │ │ ├── eval-tree-arrow.test.ts │ │ ├── eval-tree-pre-post.test.ts │ │ ├── eval-tree.test.ts │ │ ├── function-proxy.test.ts │ │ ├── lexer-literals.test.ts │ │ ├── lexer-misc.test.ts │ │ ├── module-parse.test.ts │ │ ├── parser-arrow.test.ts │ │ ├── parser-assignments.test.ts │ │ ├── parser-binary.test.ts │ │ ├── parser-destructuring.test.ts │ │ ├── parser-errors.test.ts │ │ ├── parser-expressions.test.ts │ │ ├── parser-function.test.ts │ │ ├── parser-literals.test.ts │ │ ├── parser-primary.test.ts │ │ ├── parser-regex.test.ts │ │ ├── parser-statements.test.ts │ │ ├── parser-unary.test.ts │ │ ├── process-event.test.ts │ │ ├── process-implicit-context.test.ts │ │ ├── process-statement-asgn.test.ts │ │ ├── process-statement-destruct.test.ts │ │ ├── process-statement-regs.test.ts │ │ ├── process-statement-sync.test.ts │ │ ├── process-statement.test.ts │ │ ├── process-switch-sync.test.ts │ │ ├── process-switch.test.ts │ │ ├── process-try-sync.test.ts │ │ ├── process-try.test.ts │ │ ├── simplify-expression.test.ts │ │ ├── statement-hooks.test.ts │ │ └── test-helpers.ts │ ├── style-parser │ │ ├── generateHvarChain.test.ts │ │ ├── parseHVar.test.ts │ │ ├── parser.test.ts │ │ └── tokens.test.ts │ └── xmlui │ ├── lint.test.ts │ ├── parser.test.ts │ ├── scanner.test.ts │ ├── transform.attr.test.ts │ ├── transform.circular.test.ts │ ├── transform.element.test.ts │ ├── transform.errors.test.ts │ ├── transform.escape.test.ts │ ├── transform.regression.test.ts │ ├── transform.script.test.ts │ ├── transform.test.ts │ └── xmlui.ts ├── tests-e2e │ ├── api-bound-component-regression.spec.ts │ ├── api-call-as-extracted-component.spec.ts │ ├── assign-to-object-or-array-regression.spec.ts │ ├── binding-regression.spec.ts │ ├── children-as-template-context-vars.spec.ts │ ├── compound-component.spec.ts │ ├── context-vars-regression.spec.ts │ ├── data-bindings.spec.ts │ ├── datasource-and-api-usage-in-var.spec.ts │ ├── datasource-direct-binding.spec.ts │ ├── datasource-onLoaded-regression.spec.ts │ ├── modify-array-item-regression.spec.ts │ ├── namespaces.spec.ts │ ├── push-to-array-regression.spec.ts │ ├── screen-breakpoints.spec.ts │ ├── scripting.spec.ts │ ├── state-scope-in-pages.spec.ts │ └── state-var-scopes.spec.ts ├── tsconfig.bin.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /xmlui/conventions/create-xmlui-components.md: -------------------------------------------------------------------------------- ```markdown 1 | # Creating XMLUI Components 2 | 3 | This document outlines the conventions, patterns, and best practices for creating new XMLUI components. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Component Structure](#component-structure) 8 | 2. [Component Metadata](#component-metadata) 9 | 3. [Component Parts Pattern](#component-parts-pattern) 10 | 4. [Component Renderers](#component-renderers) 11 | 5. [Theme and Styling](#theme-and-styling) 12 | 6. [Component Implementation](#component-implementation) 13 | 7. [Testing](#testing) 14 | 8. [Component Implementation Patterns](#component-implementation-patterns) 15 | 9. [Default Values Pattern](#default-values-pattern) 16 | 10. [ForwardRef Pattern](#forwardref-pattern) 17 | 11. [State Management Patterns](#state-management-patterns) 18 | 12. [Event Handling Patterns](#event-handling-patterns) 19 | 13. [API Registration and Programmatic Control Patterns](#api-registration-and-programmatic-control-patterns) 20 | 14. [XMLUI Renderer Patterns](#xmlui-renderer-patterns) 21 | 15. [Performance Patterns](#performance-patterns) 22 | 23 | ## Component Structure 24 | 25 | XMLUI components are built from four crucial concepts: 26 | 27 | 1. **Native React Component**: The actual UI implementation using standard React patterns 28 | 2. **Metadata**: Complete API description including props, events, APIs, and theme variables 29 | 3. **Renderer Function**: Maps XMLUI markup to React component calls 30 | 4. **Component Registration**: Makes the component available in XMLUI markup 31 | 32 | ### Core Component Concepts 33 | 34 | XMLUI components expose several key concepts that enable rich interactivity: 35 | 36 | - **Properties**: Configuration values passed to components (e.g., `size`, `variant`, `disabled`) 37 | - **Events**: User interactions that components can emit (e.g., `click`, `change`, `focus`) 38 | - **Event Handlers**: Functions that respond to events, often updating application state 39 | - **Exposed Methods**: Programmatic APIs that allow parent components to control child behavior (e.g., `setValue()`, `focus()`) 40 | - **Context Variables**: Data that components expose to their children, accessible via `$variableName` syntax 41 | 42 | ### Component Creation Conventions 43 | 44 | When creating new XMLUI components, follow these strict conventions: 45 | 46 | **File Structure:** 47 | - **Never create `index.ts` files** when creating components 48 | - **Never create example files** to demonstrate the component 49 | - **Only create end-to-end tests and documentation when explicitly requested** 50 | - **Do not add the React component to the xmlui folder's package.json file** 51 | 52 | **Focus on Core Functionality:** 53 | - Prioritize the component's core functionality and API design 54 | - Ensure proper XMLUI integration and registration 55 | - Examples and comprehensive documentation are secondary concerns unless specifically requested 56 | 57 | ### File Organization 58 | 59 | Each component should have its own directory under `src/components/` with the following structure: 60 | 61 | ``` 62 | ComponentName/ 63 | ├── ComponentName.tsx # Component definition (required) 64 | ├── ComponentNameNative.tsx # Native implementation (dual-file pattern) 65 | └── ComponentName.module.scss # Component styles (optional) 66 | ``` 67 | 68 | **Key files:** 69 | - **Component definition**: Always named exactly like the component (e.g., `Avatar.tsx`) 70 | - **Native file**: Appended with "Native" suffix (e.g., `AvatarNative.tsx`) 71 | - **SCSS module**: Always follows `.module.scss` pattern for scoped styles 72 | 73 | **Important conventions:** 74 | - **Never create `index.ts` files** when creating components - components should be imported directly from their main files 75 | - **Never create example files** to demonstrate the component - examples should be in documentation or playground only 76 | - **Create end-to-end tests and documentation only when explicitly requested** - focus on core functionality first 77 | 78 | ### Standard Dual-File Pattern 79 | 80 | Most XMLUI components use a dual-file pattern that separates concerns: 81 | 82 | - **Component Definition** (`ComponentName.tsx`) 83 | - Contains component metadata using `createMetadata` 84 | - Defines the renderer function with `createComponentRenderer` 85 | - Specifies theme variables and their defaults 86 | - Maps XMLUI props to native component props 87 | 88 | - **Native Component** (`ComponentNameNative.tsx`) 89 | - Pure React implementation using `forwardRef` 90 | - Contains actual rendering logic and component behavior 91 | - Defines TypeScript interfaces for props 92 | - Exports `defaultProps` object 93 | 94 | > **Note**: For very simple components, the native implementation can be included directly in the component definition file instead of creating a separate `*Native.tsx` file. 95 | 96 | ### Component Registration 97 | 98 | Components must be registered in `ComponentProvider.tsx` to be available in XMLUI markup: 99 | 100 | ```typescript 101 | // Import the component renderer 102 | import { avatarComponentRenderer } from "./Avatar/Avatar"; 103 | 104 | // Register in ComponentProvider class 105 | this.registerCoreComponent(avatarComponentRenderer); 106 | ``` 107 | 108 | ## Component Metadata 109 | 110 | Component metadata is a **fundamental and critical concept** in XMLUI. It serves as the single source of truth that describes a component's complete API surface, including properties, events, exposed methods, context variables, and theme variables. This metadata is not just documentation—it's actively used by: 111 | 112 | - **XMLUI Documentation System**: Auto-generates component documentation 113 | - **VS Code Extension**: Provides IntelliSense, auto-completion, and validation 114 | - **Type Checking**: Validates component usage at build time 115 | - **Developer Tools**: Powers debugging and inspection features 116 | - **Code Generation**: Enables automated tooling and scaffolding 117 | 118 | ### Metadata Structure 119 | 120 | Component metadata is defined using the `createMetadata` helper. Some components are non-visual and do not render any UI - these use the `nonVisual` metadata property set to `true`. 121 | 122 | Component metadata is defined using the `createMetadata` helper: 123 | 124 | ```typescript 125 | import { createMetadata, d, dClick } from "../metadata-helpers"; 126 | 127 | const COMP = "ComponentName"; 128 | 129 | export const ComponentNameMd = createMetadata({ 130 | status: "stable" | "experimental" | "deprecated", 131 | description: "Brief description of the component and its purpose", 132 | 133 | props: { 134 | propName: { 135 | description: "What this prop does", 136 | type: "string" | "number" | "boolean", 137 | availableValues: optionsArray, // For enum-like props 138 | defaultValue: defaultProps.propName, 139 | isRequired: false, 140 | }, 141 | }, 142 | 143 | events: { 144 | onClick: dClick(COMP), 145 | onCustomEvent: d("Description of custom event"), 146 | }, 147 | 148 | apis: { 149 | setValue: { 150 | description: "API method description", 151 | signature: "setValue(value: string): void", 152 | }, 153 | }, 154 | 155 | contextVars: { 156 | // Variables exposed to child components 157 | }, 158 | 159 | themeVars: parseScssVar(styles.themeVars), 160 | defaultThemeVars: { 161 | [`property-${COMP}`]: "defaultValue", 162 | }, 163 | }); 164 | ``` 165 | 166 | ### Metadata Helper Functions 167 | 168 | - `d(description, availableValues?, valueType?, defaultValue?, isValid?, isRequired?)` - General property descriptor 169 | - `dClick(componentName)` - Standard click event descriptor 170 | - `dGotFocus(componentName)` - Focus event descriptor 171 | - `dLostFocus(componentName)` - Blur event descriptor 172 | - `dInternal(description?)` - Internal-only property descriptor 173 | 174 | ## Component Parts Pattern 175 | 176 | The **parts pattern** is a metadata-driven approach that allows referencing and styling nested sub-components within complex XMLUI components. This pattern adds metadata to components that enables targeting specific parts for testing, styling, and layout applications. 177 | 178 | ### Parts Metadata Structure 179 | 180 | Components that use the parts pattern define a `parts` object in their metadata, where each part has a descriptive name and description: 181 | 182 | ```typescript 183 | export const ComponentNameMd = createMetadata({ 184 | // ... other metadata 185 | parts: { 186 | label: { 187 | description: "The label displayed for the component.", 188 | }, 189 | input: { 190 | description: "The main input area.", 191 | }, 192 | startAdornment: { 193 | description: "The adornment displayed at the start of the component.", 194 | }, 195 | endAdornment: { 196 | description: "The adornment displayed at the end of the component.", 197 | }, 198 | }, 199 | defaultPart: "input", // Optional: specifies which part receives layout properties by default 200 | // ... rest of metadata 201 | }); 202 | ``` 203 | 204 | ### Part Implementation in Native Components 205 | 206 | Parts are implemented in native components by applying CSS classes that mark specific DOM elements as parts. This is done using the `partClassName` function from the parts infrastructure: 207 | 208 | ```typescript 209 | import { partClassName, PART_INPUT, PART_START_ADORNMENT, PART_END_ADORNMENT } from "../../components-core/parts"; 210 | 211 | export const ComponentNative = forwardRef(function ComponentNative(props, ref) { 212 | return ( 213 | <div className={styles.container}> 214 | {/* Start adornment part */} 215 | {startAdornment && ( 216 | <div className={classnames(partClassName(PART_START_ADORNMENT), styles.adornment)}> 217 | {startAdornment} 218 | </div> 219 | )} 220 | 221 | {/* Main input part */} 222 | <input 223 | className={classnames(partClassName(PART_INPUT), styles.input)} 224 | {...inputProps} 225 | /> 226 | 227 | {/* End adornment part */} 228 | {endAdornment && ( 229 | <div className={classnames(partClassName(PART_END_ADORNMENT), styles.adornment)}> 230 | {endAdornment} 231 | </div> 232 | )} 233 | </div> 234 | ); 235 | }); 236 | ``` 237 | 238 | ### Standard Part Names 239 | 240 | XMLUI defines common part constants for consistency across components: 241 | 242 | - `PART_LABEL` - For component labels 243 | - `PART_INPUT` - For main input areas 244 | - `PART_START_ADORNMENT` - For decorative elements at the start 245 | - `PART_END_ADORNMENT` - For decorative elements at the end 246 | 247 | ### Component Examples Using Parts 248 | 249 | #### TextBox Component Parts 250 | ```typescript 251 | parts: { 252 | label: { description: "The label displayed for the text box." }, 253 | startAdornment: { description: "The adornment displayed at the start of the text box." }, 254 | endAdornment: { description: "The adornment displayed at the end of the text box." }, 255 | input: { description: "The text box input area." } 256 | }, 257 | defaultPart: "input" 258 | ``` 259 | 260 | #### TimeInput Component Parts 261 | ```typescript 262 | parts: { 263 | hour: { description: "The hour input field." }, 264 | minute: { description: "The minute input field." }, 265 | second: { description: "The second input field." }, 266 | ampm: { description: "The AM/PM indicator." }, 267 | clearButton: { description: "The button to clear the time input." } 268 | } 269 | ``` 270 | 271 | #### Checkbox Component Parts 272 | ```typescript 273 | parts: { 274 | label: { description: "The label displayed for the checkbox." }, 275 | input: { description: "The checkbox input area." } 276 | } 277 | ``` 278 | 279 | ### Benefits of the Parts Pattern 280 | 281 | 1. **Testing**: Parts provide stable selectors for automated testing by generating predictable CSS classes like `_PART_input_` 282 | 2. **Styling**: Theme variables and CSS can target specific parts of complex components 283 | 3. **Layout**: Layout properties can be applied to specific parts rather than the entire component 284 | 4. **Documentation**: Auto-generated documentation includes part descriptions for better developer understanding 285 | 5. **Consistency**: Standardized part names create consistent patterns across the component library 286 | 287 | ### When to Use Parts 288 | 289 | Use the parts pattern for components that: 290 | - Have multiple distinct visual elements that users might want to style separately 291 | - Contain input elements alongside labels, adornments, or other decorative elements 292 | - Have complex internal structure that benefits from targeted styling or testing 293 | - Need fine-grained control over layout application to sub-elements 294 | 295 | Simple components with a single visual element typically don't need the parts pattern. 296 | 297 | ## Component Renderers 298 | 299 | Component renderers are functions that bridge XMLUI markup and React components. They receive a `RendererContext` object containing all necessary information to render the component and return a React element. 300 | 301 | ### Renderer Context 302 | 303 | The `RendererContext` provides these key properties for accessing component data and functionality: 304 | 305 | - **`node`**: The component definition containing props, children, and metadata 306 | - **`state`**: The current state of the container in which the component is rendered 307 | - **`appContext`**: The application context for binding expressions and component usage 308 | - **`renderChild`**: Renders child components with optional layout context 309 | - **`layoutContext`**: Information about the layout context in which the component is rendered 310 | - **`uid`**: Unique identifier for the component instance 311 | - **`updateState`**: Updates component's internal state using reducer pattern 312 | - **`extractValue`**: Extracts and evaluates property values (handles binding expressions) 313 | - **`extractResourceUrl`**: Converts logical resource URLs to physical URLs 314 | - **`lookupEventHandler`**: Creates event handler functions from XMLUI event definitions 315 | - **`lookupAction`**: Obtains async action handlers by name with specified options 316 | - **`lookupSyncCallback`**: Retrieves synchronous callback functions 317 | - **`layoutCss`**: Pre-computed CSS properties for layout (position, size, etc.) 318 | - **`registerComponentApi`**: Registers component methods for programmatic access 319 | 320 | ### Value Extraction Patterns 321 | 322 | The `extractValue` function handles different data types with specialized methods: 323 | 324 | ```typescript 325 | // Basic extraction (any type) 326 | const value = extractValue(node.props.someProperty); 327 | 328 | // Typed extraction with defaults 329 | const size = extractValue.asOptionalString(node.props.size, "medium"); 330 | const enabled = extractValue.asOptionalBoolean(node.props.enabled, true); 331 | const count = extractValue.asOptionalNumber(node.props.count, 0); 332 | 333 | // Display text (handles spacing properly) 334 | const label = extractValue.asDisplayText(node.props.label); 335 | 336 | // CSS size values (with units) 337 | const width = extractValue.asSize(node.props.width); 338 | ``` 339 | 340 | ### Event Handler Patterns 341 | 342 | Event handlers are created through `lookupEventHandler` and connected to React component events: 343 | 344 | ```typescript 345 | // Simple event handlers 346 | onClick={lookupEventHandler("click")} 347 | onFocus={lookupEventHandler("gotFocus")} 348 | onBlur={lookupEventHandler("lostFocus")} 349 | 350 | // Custom events with specific payloads 351 | onDidChange={lookupEventHandler("didChange")} 352 | onSelectionChanged={lookupEventHandler("selectionDidChange")} 353 | ``` 354 | 355 | ### Renderer Examples 356 | 357 | #### Complex Component Renderer with Children 358 | ```typescript 359 | export const buttonComponentRenderer = createComponentRenderer( 360 | "Button", 361 | ButtonMd, 362 | ({ node, extractValue, renderChild, lookupEventHandler, layoutCss }) => { 363 | const iconName = extractValue.asString(node.props.icon); 364 | const label = extractValue.asDisplayText(node.props.label); 365 | 366 | return ( 367 | <Button 368 | variant={extractValue.asOptionalString(node.props.variant)} 369 | disabled={!extractValue.asOptionalBoolean(node.props.enabled, true)} 370 | icon={iconName && <Icon name={iconName} aria-hidden />} 371 | onClick={lookupEventHandler("click")} 372 | onFocus={lookupEventHandler("gotFocus")} 373 | style={layoutCss} 374 | > 375 | {renderChild(node.children, { type: "Stack", orientation: "horizontal" }) || label} 376 | </Button> 377 | ); 378 | }, 379 | ); 380 | ``` 381 | 382 | #### Component with State and API Registration 383 | ```typescript 384 | export const colorPickerComponentRenderer = createComponentRenderer( 385 | "ColorPicker", 386 | ColorPickerMd, 387 | ({ node, extractValue, state, updateState, registerComponentApi, lookupEventHandler, layoutCss }) => { 388 | return ( 389 | <ColorPicker 390 | value={state.value} 391 | initialValue={extractValue(node.props.initialValue)} 392 | updateState={updateState} 393 | registerComponentApi={registerComponentApi} 394 | onDidChange={lookupEventHandler("didChange")} 395 | style={layoutCss} 396 | enabled={extractValue.asOptionalBoolean(node.props.enabled, true)} 397 | /> 398 | ); 399 | }, 400 | ); 401 | ``` 402 | 403 | ## Theme and Styling 404 | 405 | Non-visual components do not use styling or theme variables. 406 | 407 | Each visual component requires a SCSS module file with this structure: 408 | 409 | ```scss 410 | // ComponentName.module.scss 411 | @use "../../components-core/theming/themes" as t; 412 | 413 | // --- This code snippet is required to collect the theme variables used in this module 414 | $themeVars: (); 415 | @function createThemeVar($componentVariable) { 416 | $themeVars: t.appendThemeVar($themeVars, $componentVariable) !global; 417 | @return t.getThemeVar($themeVars, $componentVariable); 418 | } 419 | 420 | // Define theme variables 421 | $backgroundColor-ComponentName: createThemeVar("backgroundColor-ComponentName"); 422 | $borderColor-ComponentName: createThemeVar("borderColor-ComponentName"); 423 | $textColor-ComponentName: createThemeVar("textColor-ComponentName"); 424 | 425 | // --- This part defines the CSS styles 426 | .componentName { 427 | background-color: $backgroundColor-ComponentName; 428 | border-color: $borderColor-ComponentName; 429 | color: $textColor-ComponentName; 430 | 431 | // Component-specific styles 432 | 433 | &.variantClass { 434 | // Variant styles 435 | } 436 | } 437 | 438 | // --- We export the theme variables to add them to the component renderer 439 | :export{ 440 | themeVars: t.json-stringify($themeVars) 441 | } 442 | ``` 443 | 444 | This structure is important because it helps collect all theme variables a particular component supports for documentation purposes. The pattern uses the `createThemeVar()` function to define theme variables that can be customized through the design system, then uses those variables in CSS styles, and finally exports them for the component renderer. 445 | 446 | ## Component Implementation 447 | 448 | Follow this implementation flow for creating new XMLUI components: 449 | 450 | 1. **Create the component metadata** - This information helps understand the component design and facilitates discussion 451 | 2. **Create the renderer function and export it** - Use the native component and pass XMLUI component properties and events to it (the code won't build yet as no native component exists) 452 | 3. **Create a rudimentary version of the native component** - Make the code compile with basic functionality 453 | 4. **Add component registration** - At this point you can test the rudimentary component in XMLUI markup 454 | 5. **Implement the native component in full** - Add complete functionality, styling, and behavior 455 | 456 | **Note**: End-to-end tests and comprehensive documentation should only be created when explicitly requested. Focus on core functionality first. 457 | 458 | ### Native Component Structure 459 | 460 | Native components must follow these patterns: 461 | 462 | ```typescript 463 | import React, { forwardRef, useRef, useImperativeHandle, useEffect } from "react"; 464 | import classnames from "classnames"; 465 | import styles from "./ComponentName.module.scss"; 466 | 467 | // Define props interface 468 | type Props = { 469 | id?: string; 470 | // Component-specific props 471 | children?: React.ReactNode; 472 | style?: CSSProperties; 473 | // Event handlers 474 | onClick?: (event: React.MouseEvent) => void; 475 | // Accessibility props 476 | } & React.HTMLAttributes<HTMLElement>; 477 | 478 | // Define default props 479 | export const defaultProps: Required<Pick<Props, "prop1" | "prop2">> = { 480 | prop1: "defaultValue", 481 | prop2: "anotherDefault", 482 | }; 483 | 484 | // Component implementation with forwardRef 485 | export const ComponentName = forwardRef(function ComponentName( 486 | { 487 | prop1 = defaultProps.prop1, 488 | prop2 = defaultProps.prop2, 489 | children, 490 | style, 491 | onClick, 492 | ...rest 493 | }: Props, 494 | ref: React.ForwardedRef<HTMLElement>, 495 | ) { 496 | const innerRef = useRef<HTMLElement>(null); 497 | 498 | // Compose refs if needed 499 | const composedRef = ref ? composeRefs(ref, innerRef) : innerRef; 500 | 501 | // Component logic here 502 | 503 | return ( 504 | <div 505 | ref={composedRef} 506 | className={classnames(styles.componentName, { 507 | [styles.variantClass]: condition, 508 | })} 509 | style={style} 510 | onClick={onClick} 511 | {...rest} 512 | > 513 | {children} 514 | </div> 515 | ); 516 | }); 517 | 518 | // Note: We do NOT use displayName in XMLUI components 519 | // React.displayName is not used in our component convention 520 | ``` 521 | 522 | **Key patterns**: Always use `forwardRef`, define clear TypeScript interfaces, provide sensible defaults via `defaultProps`, use scoped CSS modules, support standard HTML attributes, handle accessibility through proper ARIA attributes, and do **not** set `displayName` on components. 523 | 524 | ## Testing 525 | 526 | Component testing follows established patterns and conventions detailed in [testing-conventions.md](./testing-conventions.md). This includes component driver patterns, test structure, and best practices for ensuring component reliability and functionality. 527 | 528 | --- 529 | 530 | ## Component Implementation Patterns 531 | 532 | *Note: This is a temporary list for detailed expansion later* 533 | 534 | ### XMLUI Component Patterns 535 | 536 | **Specialized Component Patterns:** 537 | - Form components: Integration with FormContext, validation handling 538 | - Data-driven components: List virtualization, table column management 539 | - Interactive components: Complex state management, event propagation 540 | - Container components: Layout management, child component orchestration 541 | 542 | ### React Native Component Patterns 543 | 544 | **Accessibility Patterns:** 545 | - ARIA attribute management 546 | - Focus management and trap patterns 547 | - Screen reader optimization 548 | - High contrast and reduced motion support 549 | 550 | --- 551 | 552 | ## Default Values Pattern 553 | 554 | **Purpose**: Components need consistent, predictable default behavior while allowing customization. This pattern solves the problem of ensuring components work correctly when properties are omitted, reducing the need for consumers to specify every property explicitly. 555 | 556 | **Implementation Pattern**: 557 | 558 | 1. **Define defaults object in Native component**: 559 | ```typescript 560 | // In ComponentNative.tsx 561 | export const defaultProps = { 562 | enabled: true, 563 | variant: "primary" as const, 564 | size: "md" as const, 565 | showIcon: false, 566 | // ... other defaults 567 | }; 568 | ``` 569 | 570 | 2. **Apply defaults in Native component implementation**: 571 | ```typescript 572 | interface Props { 573 | enabled?: boolean; 574 | variant?: "primary" | "secondary" | "danger"; 575 | size?: "sm" | "md" | "lg"; 576 | showIcon?: boolean; 577 | } 578 | 579 | export const ComponentNative = ({ 580 | enabled = defaultProps.enabled, 581 | variant = defaultProps.variant, 582 | size = defaultProps.size, 583 | showIcon = defaultProps.showIcon, 584 | ...rest 585 | }: Props) => { 586 | // Component implementation uses the resolved defaults 587 | return ( 588 | <div 589 | className={classnames(styles.component, { 590 | [styles.disabled]: !enabled, 591 | [styles[variant]]: variant, 592 | [styles[size]]: size, 593 | })} 594 | {...rest} 595 | /> 596 | ); 597 | }; 598 | ``` 599 | 600 | 3. **Reference defaults in XMLUI metadata**: 601 | ```typescript 602 | export const ComponentMd = createMetadata({ 603 | props: { 604 | enabled: dEnabled(defaultProps.enabled), 605 | variant: { 606 | description: "Visual style variant", 607 | availableValues: ["primary", "secondary", "danger"], 608 | defaultValue: defaultProps.variant, 609 | valueType: "string", 610 | }, 611 | // ... other props with defaults 612 | }, 613 | }); 614 | ``` 615 | 616 | 4. **Pass values directly in renderer (no fallbacks needed)**: 617 | ```typescript 618 | export const componentRenderer = createComponentRenderer( 619 | COMP, 620 | ComponentMd, 621 | ({ node, extractValue }) => { 622 | return ( 623 | <ComponentNative 624 | enabled={extractValue.asOptionalBoolean(node.props.enabled)} 625 | variant={extractValue(node.props.variant)} 626 | size={extractValue(node.props.size)} 627 | // Native component handles undefined values with its own defaults 628 | /> 629 | ); 630 | }, 631 | ); 632 | ``` 633 | 634 | **Key Benefits**: 635 | - Consistent behavior across all components 636 | - Native components work correctly when used directly by other XMLUI components 637 | - Single source of truth for default values 638 | - Eliminates duplication between renderer and native component 639 | - Supports both XMLUI and direct React usage patterns seamlessly 640 | 641 | ## ForwardRef Pattern 642 | 643 | **Purpose**: React components need to expose DOM element references to parent components for imperative operations like focusing, scrolling, or measuring. The forwardRef pattern solves the problem of ref forwarding through component boundaries, enabling parent components to directly interact with child DOM elements and supporting imperative APIs. 644 | 645 | **React API Overview**: 646 | - **`forwardRef`**: A React function that enables a component to receive a `ref` from its parent and forward it to a child element or expose custom APIs 647 | - **`useImperativeHandle`**: A React hook that customizes the instance value exposed when using `ref`, allowing components to expose specific methods instead of raw DOM elements 648 | 649 | **Implementation Pattern**: 650 | 651 | 1. **Basic forwardRef structure with typed ref**: 652 | ```typescript 653 | import React, { forwardRef } from "react"; 654 | 655 | interface Props { 656 | children?: React.ReactNode; 657 | className?: string; 658 | // ... other props 659 | } 660 | 661 | export const ComponentNative = forwardRef<HTMLDivElement, Props>( 662 | function ComponentNative({ children, className, ...rest }, ref) { 663 | return ( 664 | <div ref={ref} className={className} {...rest}> 665 | {children} 666 | </div> 667 | ); 668 | } 669 | ); 670 | ``` 671 | 672 | 2. **ForwardRef with internal ref composition**: 673 | ```typescript 674 | import React, { forwardRef, useRef } from "react"; 675 | import { composeRefs } from "../../utils/ref-utils"; 676 | 677 | export const ComponentNative = forwardRef<HTMLDivElement, Props>( 678 | function ComponentNative({ children, ...rest }, ref) { 679 | const internalRef = useRef<HTMLDivElement>(null); 680 | const composedRef = composeRefs(ref, internalRef); 681 | 682 | // Use internalRef for component logic 683 | const handleClick = () => { 684 | internalRef.current?.focus(); 685 | }; 686 | 687 | return ( 688 | <div ref={composedRef} onClick={handleClick} {...rest}> 689 | {children} 690 | </div> 691 | ); 692 | } 693 | ); 694 | ``` 695 | 696 | 3. **ForwardRef with imperative API using useImperativeHandle**: 697 | ```typescript 698 | import React, { forwardRef, useImperativeHandle, useRef } from "react"; 699 | 700 | interface ComponentAPI { 701 | focus: () => void; 702 | blur: () => void; 703 | scrollIntoView: () => void; 704 | getValue: () => string; 705 | } 706 | 707 | export const ComponentNative = forwardRef<ComponentAPI, Props>( 708 | function ComponentNative({ initialValue, ...rest }, ref) { 709 | const elementRef = useRef<HTMLInputElement>(null); 710 | const [value, setValue] = useState(initialValue || ""); 711 | 712 | useImperativeHandle(ref, () => ({ 713 | focus: () => elementRef.current?.focus(), 714 | blur: () => elementRef.current?.blur(), 715 | scrollIntoView: () => elementRef.current?.scrollIntoView(), 716 | getValue: () => value, 717 | }), [value]); 718 | 719 | return ( 720 | <input 721 | ref={elementRef} 722 | value={value} 723 | onChange={(e) => setValue(e.target.value)} 724 | {...rest} 725 | /> 726 | ); 727 | } 728 | ); 729 | ``` 730 | 731 | 4. **Registration in XMLUI renderer with API exposure**: 732 | ```typescript 733 | export const componentRenderer = createComponentRenderer( 734 | COMP, 735 | ComponentMd, 736 | ({ node, extractValue, registerComponentApi }) => { 737 | return ( 738 | <ComponentNative 739 | ref={(instance) => { 740 | if (instance) { 741 | registerComponentApi({ 742 | focus: () => instance.focus(), 743 | blur: () => instance.blur(), 744 | getValue: () => instance.getValue(), 745 | }); 746 | } 747 | }} 748 | initialValue={extractValue(node.props.initialValue)} 749 | /> 750 | ); 751 | }, 752 | ); 753 | ``` 754 | 755 | **Key Benefits**: 756 | - Enables parent components to access child DOM elements directly 757 | - Supports imperative APIs for programmatic component control 758 | - Maintains clean separation between declarative props and imperative methods 759 | - Allows XMLUI components to expose methods callable from event handlers 760 | - Facilitates complex component interactions (focus management, animations, measurements) 761 | - Essential for form components that need validation and value access 762 | 763 | ## State Management Patterns 764 | 765 | **Purpose**: React components need different approaches to manage state depending on their complexity, interaction patterns, and integration requirements. These patterns solve various state-related challenges from simple local state to complex cross-component communication and XMLUI framework integration. 766 | 767 | **React Hooks Overview**: 768 | - **`useState`**: Manages component-local state with getter/setter pattern 769 | - **`useRef`**: Creates mutable references that persist across renders without causing re-renders 770 | - **`useMemo`**: Memoizes expensive calculations to prevent unnecessary recomputation 771 | - **`useCallback`**: Memoizes functions to prevent unnecessary re-creation and child re-renders 772 | - **`useEffect`**: Handles side effects, subscriptions, and cleanup operations 773 | 774 | ### Controlled vs Uncontrolled Component Pattern 775 | 776 | **Purpose**: Components need to handle user input and data flow in predictable ways, either allowing parent components to control the state (controlled) or managing it internally (uncontrolled). 777 | 778 | **Implementation Pattern**: 779 | 780 | ```typescript 781 | // Uncontrolled component - manages its own state 782 | export const UncontrolledComponent = ({ defaultValue, onChange }: Props) => { 783 | const [value, setValue] = useState(defaultValue || ""); 784 | 785 | const handleChange = (newValue: string) => { 786 | setValue(newValue); 787 | onChange?.(newValue); // Notify parent but don't depend on it 788 | }; 789 | 790 | return <input value={value} onChange={(e) => handleChange(e.target.value)} />; 791 | }; 792 | 793 | // Controlled component - parent controls the state 794 | export const ControlledComponent = ({ value, onChange }: Props) => { 795 | // No internal state - everything comes from props 796 | const handleChange = (newValue: string) => { 797 | onChange?.(newValue); // Parent must handle this 798 | }; 799 | 800 | return <input value={value} onChange={(e) => handleChange(e.target.value)} />; 801 | }; 802 | 803 | // Hybrid approach - supports both patterns 804 | export const FlexibleComponent = ({ value, defaultValue, onChange }: Props) => { 805 | const [internalValue, setInternalValue] = useState(defaultValue || ""); 806 | const isControlled = value !== undefined; 807 | const effectiveValue = isControlled ? value : internalValue; 808 | 809 | const handleChange = (newValue: string) => { 810 | if (!isControlled) { 811 | setInternalValue(newValue); 812 | } 813 | onChange?.(newValue); 814 | }; 815 | 816 | return <input value={effectiveValue} onChange={(e) => handleChange(e.target.value)} />; 817 | }; 818 | ``` 819 | 820 | ### Internal State with External Synchronization Pattern 821 | 822 | **Purpose**: Components need to maintain internal state while staying synchronized with external data sources or parent component changes. 823 | 824 | **Implementation Pattern**: 825 | 826 | ```typescript 827 | export const SynchronizedComponent = ({ externalValue, onValueChange }: Props) => { 828 | const [internalState, setInternalState] = useState({ 829 | value: externalValue || "", 830 | isDirty: false, 831 | lastSyncedValue: externalValue || "", 832 | }); 833 | 834 | // Sync with external changes 835 | useEffect(() => { 836 | if (externalValue !== internalState.lastSyncedValue) { 837 | setInternalState(prev => ({ 838 | ...prev, 839 | value: externalValue || "", 840 | lastSyncedValue: externalValue || "", 841 | isDirty: false, 842 | })); 843 | } 844 | }, [externalValue, internalState.lastSyncedValue]); 845 | 846 | const handleInternalChange = (newValue: string) => { 847 | setInternalState(prev => ({ 848 | ...prev, 849 | value: newValue, 850 | isDirty: newValue !== prev.lastSyncedValue, 851 | })); 852 | 853 | // Debounced external notification 854 | onValueChange?.(newValue); 855 | }; 856 | 857 | return ( 858 | <div> 859 | <input 860 | value={internalState.value} 861 | onChange={(e) => handleInternalChange(e.target.value)} 862 | /> 863 | {internalState.isDirty && <span>*</span>} 864 | </div> 865 | ); 866 | }; 867 | ``` 868 | 869 | ### Context Consumption for Shared State Pattern 870 | 871 | **Purpose**: Components need access to shared state across component trees without prop drilling, such as themes, user authentication, or application-wide settings. 872 | 873 | **Implementation Pattern**: 874 | 875 | ```typescript 876 | // Theme context example 877 | const ThemeContext = createContext<{ 878 | theme: 'light' | 'dark'; 879 | toggleTheme: () => void; 880 | }>({ 881 | theme: 'light', 882 | toggleTheme: () => {}, 883 | }); 884 | 885 | export const ThemeProvider = ({ children }: { children: ReactNode }) => { 886 | const [theme, setTheme] = useState<'light' | 'dark'>('light'); 887 | 888 | const toggleTheme = useCallback(() => { 889 | setTheme(prev => prev === 'light' ? 'dark' : 'light'); 890 | }, []); 891 | 892 | const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]); 893 | 894 | return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; 895 | }; 896 | 897 | // Component consuming theme context 898 | export const ThemedComponent = ({ children }: Props) => { 899 | const { theme, toggleTheme } = useContext(ThemeContext); 900 | 901 | return ( 902 | <div className={`theme-${theme}`}> 903 | {children} 904 | <button onClick={toggleTheme}>Toggle Theme</button> 905 | </div> 906 | ); 907 | }; 908 | 909 | // Form context example for shared form state 910 | const FormContext = createContext<{ 911 | formData: Record<string, any>; 912 | updateField: (field: string, value: any) => void; 913 | errors: Record<string, string>; 914 | }>({ 915 | formData: {}, 916 | updateField: () => {}, 917 | errors: {}, 918 | }); 919 | 920 | export const FormFieldComponent = ({ fieldName }: { fieldName: string }) => { 921 | const { formData, updateField, errors } = useContext(FormContext); 922 | 923 | return ( 924 | <div> 925 | <input 926 | value={formData[fieldName] || ''} 927 | onChange={(e) => updateField(fieldName, e.target.value)} 928 | /> 929 | {errors[fieldName] && <span className="error">{errors[fieldName]}</span>} 930 | </div> 931 | ); 932 | }; 933 | ``` 934 | 935 | ### Effect Hooks for Side Effects and Cleanup Pattern 936 | 937 | **Purpose**: Components need to handle side effects like data fetching, subscriptions, timers, and external API interactions while ensuring proper cleanup to prevent memory leaks. 938 | 939 | **Implementation Pattern**: 940 | 941 | ```typescript 942 | export const EffectfulComponent = ({ url, pollingInterval }: Props) => { 943 | const [data, setData] = useState(null); 944 | const [loading, setLoading] = useState(false); 945 | const [error, setError] = useState(null); 946 | 947 | // Data fetching effect 948 | useEffect(() => { 949 | let isCancelled = false; 950 | 951 | const fetchData = async () => { 952 | setLoading(true); 953 | setError(null); 954 | 955 | try { 956 | const response = await fetch(url); 957 | const result = await response.json(); 958 | 959 | if (!isCancelled) { 960 | setData(result); 961 | } 962 | } catch (err) { 963 | if (!isCancelled) { 964 | setError(err.message); 965 | } 966 | } finally { 967 | if (!isCancelled) { 968 | setLoading(false); 969 | } 970 | } 971 | }; 972 | 973 | fetchData(); 974 | 975 | // Cleanup function 976 | return () => { 977 | isCancelled = true; 978 | }; 979 | }, [url]); 980 | 981 | // Polling effect with cleanup 982 | useEffect(() => { 983 | if (!pollingInterval) return; 984 | 985 | const interval = setInterval(() => { 986 | // Refetch data 987 | fetch(url) 988 | .then(res => res.json()) 989 | .then(setData) 990 | .catch(setError); 991 | }, pollingInterval); 992 | 993 | return () => clearInterval(interval); 994 | }, [url, pollingInterval]); 995 | 996 | // Event listener effect 997 | useEffect(() => { 998 | const handleVisibilityChange = () => { 999 | if (document.visibilityState === 'visible') { 1000 | // Refetch when tab becomes visible 1001 | setData(null); 1002 | } 1003 | }; 1004 | 1005 | document.addEventListener('visibilitychange', handleVisibilityChange); 1006 | 1007 | return () => { 1008 | document.removeEventListener('visibilitychange', handleVisibilityChange); 1009 | }; 1010 | }, []); 1011 | 1012 | if (loading) return <div>Loading...</div>; 1013 | if (error) return <div>Error: {error}</div>; 1014 | return <div>{JSON.stringify(data)}</div>; 1015 | }; 1016 | ``` 1017 | 1018 | ### XMLUI State Management using updateState/state Reducer Pattern 1019 | 1020 | **Purpose**: XMLUI components need to integrate with the framework's container-based state management system, enabling complex state sharing across component hierarchies and integration with XMLUI's binding expressions and event system. 1021 | 1022 | **Implementation Pattern**: 1023 | 1024 | ```typescript 1025 | // XMLUI component renderer using updateState/state pattern 1026 | export const xmluiInputComponentRenderer = createComponentRenderer( 1027 | COMP, 1028 | ComponentMd, 1029 | ({ node, state, updateState, extractValue, lookupEventHandler, registerComponentApi }) => { 1030 | // State is managed by XMLUI's container system 1031 | const currentValue = state.value || extractValue(node.props.initialValue); 1032 | 1033 | return ( 1034 | <InputNative 1035 | value={currentValue} 1036 | updateState={updateState} // Pass XMLUI's state updater 1037 | initialValue={extractValue(node.props.initialValue)} 1038 | onDidChange={lookupEventHandler("didChange")} 1039 | registerComponentApi={registerComponentApi} 1040 | placeholder={extractValue(node.props.placeholder)} 1041 | /> 1042 | ); 1043 | }, 1044 | ); 1045 | 1046 | // Native component using XMLUI state management 1047 | export const InputNative = ({ 1048 | value, 1049 | updateState, 1050 | initialValue, 1051 | onDidChange, 1052 | registerComponentApi 1053 | }: Props) => { 1054 | const [localValue, setLocalValue] = useState(value || initialValue || ""); 1055 | 1056 | // Sync with XMLUI state changes 1057 | useEffect(() => { 1058 | if (value !== undefined && value !== localValue) { 1059 | setLocalValue(value); 1060 | } 1061 | }, [value, localValue]); 1062 | 1063 | const handleChange = useCallback((newValue: string) => { 1064 | setLocalValue(newValue); 1065 | 1066 | // Update XMLUI container state through reducer pattern 1067 | updateState({ value: newValue }); 1068 | 1069 | // Trigger XMLUI event handlers 1070 | onDidChange?.(); 1071 | }, [updateState, onDidChange]); 1072 | 1073 | // Register imperative API with XMLUI 1074 | useEffect(() => { 1075 | registerComponentApi({ 1076 | setValue: (newValue: string) => { 1077 | setLocalValue(newValue); 1078 | updateState({ value: newValue }); 1079 | }, 1080 | getValue: () => localValue, 1081 | focus: () => { 1082 | // Focus implementation 1083 | }, 1084 | }); 1085 | }, [registerComponentApi, localValue, updateState]); 1086 | 1087 | return ( 1088 | <input 1089 | value={localValue} 1090 | onChange={(e) => handleChange(e.target.value)} 1091 | /> 1092 | ); 1093 | }; 1094 | 1095 | // Complex state update patterns 1096 | export const ComplexStateComponent = ({ state, updateState }: Props) => { 1097 | const handleComplexUpdate = () => { 1098 | // Update multiple state properties atomically 1099 | updateState({ 1100 | currentPage: 1, 1101 | filters: { category: 'electronics', minPrice: 100 }, 1102 | sortBy: 'price', 1103 | lastUpdated: Date.now(), 1104 | }); 1105 | }; 1106 | 1107 | const handleNestedUpdate = () => { 1108 | // Update nested state properties 1109 | updateState({ 1110 | 'user.preferences.theme': 'dark', 1111 | 'user.preferences.notifications': true, 1112 | }); 1113 | }; 1114 | 1115 | return ( 1116 | <div> 1117 | <button onClick={handleComplexUpdate}>Update Multiple Fields</button> 1118 | <button onClick={handleNestedUpdate}>Update Nested Fields</button> 1119 | </div> 1120 | ); 1121 | }; 1122 | ``` 1123 | 1124 | **Key Benefits of Each Pattern**: 1125 | - **Controlled/Uncontrolled**: Clear data flow, predictable behavior, flexible usage patterns 1126 | - **External Synchronization**: Maintains UI responsiveness while staying in sync with external data 1127 | - **Context Consumption**: Eliminates prop drilling, centralizes shared state, improves maintainability 1128 | - **Effect Hooks**: Proper lifecycle management, memory leak prevention, external system integration 1129 | - **XMLUI State Management**: Framework integration, binding expression support, container hierarchy benefits, imperative API registration 1130 | 1131 | ## Event Handling Patterns 1132 | 1133 | **Purpose**: Components need robust, accessible, and performant event handling to respond to user interactions, manage complex input scenarios, and integrate with both React and XMLUI event systems. These patterns solve challenges around event propagation, accessibility, keyboard navigation, and framework integration. 1134 | 1135 | ### Event Callback Prop Pattern 1136 | 1137 | **Purpose**: Components need flexible event handling that supports optional callbacks while maintaining predictable behavior when handlers are not provided. 1138 | 1139 | **Implementation Pattern**: 1140 | 1141 | ```typescript 1142 | interface Props { 1143 | onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; 1144 | onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void; 1145 | onKeyDown?: (event: React.KeyboardEvent<HTMLButtonElement>) => void; 1146 | onValueChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void; 1147 | } 1148 | 1149 | export const EventHandlerComponent = ({ 1150 | onClick, 1151 | onFocus, 1152 | onKeyDown, 1153 | onValueChange, 1154 | children 1155 | }: Props) => { 1156 | const [value, setValue] = useState(""); 1157 | 1158 | const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { 1159 | // Internal logic 1160 | console.log("Button clicked"); 1161 | 1162 | // Call optional external handler 1163 | onClick?.(event); 1164 | }; 1165 | 1166 | const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => { 1167 | // Handle specific keys internally 1168 | if (event.key === 'Enter' || event.key === ' ') { 1169 | event.preventDefault(); 1170 | handleClick(event as any); 1171 | } 1172 | 1173 | // Call optional external handler 1174 | onKeyDown?.(event); 1175 | }; 1176 | 1177 | const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => { 1178 | const newValue = event.target.value; 1179 | setValue(newValue); 1180 | 1181 | // Pass both value and event to external handler 1182 | onValueChange?.(newValue, event); 1183 | }; 1184 | 1185 | return ( 1186 | <div> 1187 | <input 1188 | value={value} 1189 | onChange={handleValueChange} 1190 | onFocus={onFocus} 1191 | /> 1192 | <button 1193 | onClick={handleClick} 1194 | onKeyDown={handleKeyDown} 1195 | onFocus={onFocus} 1196 | > 1197 | {children} 1198 | </button> 1199 | </div> 1200 | ); 1201 | }; 1202 | ``` 1203 | 1204 | ### Event Object Creation and Propagation Pattern 1205 | 1206 | **Purpose**: Components need to create custom event objects and control event propagation for complex interactions and framework integration. 1207 | 1208 | **Implementation Pattern**: 1209 | 1210 | ```typescript 1211 | // Custom event types 1212 | interface CustomChangeEvent { 1213 | type: 'change'; 1214 | value: string; 1215 | previousValue: string; 1216 | isValid: boolean; 1217 | timestamp: number; 1218 | } 1219 | 1220 | interface CustomSelectionEvent { 1221 | type: 'selection'; 1222 | selectedItems: string[]; 1223 | action: 'add' | 'remove' | 'clear'; 1224 | item?: string; 1225 | } 1226 | 1227 | export const CustomEventComponent = ({ 1228 | onSelectionChange, 1229 | onValidatedChange 1230 | }: { 1231 | onSelectionChange?: (event: CustomSelectionEvent) => void; 1232 | onValidatedChange?: (event: CustomChangeEvent) => void; 1233 | }) => { 1234 | const [value, setValue] = useState(""); 1235 | const [selectedItems, setSelectedItems] = useState<string[]>([]); 1236 | 1237 | const createChangeEvent = (newValue: string, previousValue: string): CustomChangeEvent => ({ 1238 | type: 'change', 1239 | value: newValue, 1240 | previousValue, 1241 | isValid: newValue.length >= 3, 1242 | timestamp: Date.now(), 1243 | }); 1244 | 1245 | const handleValueChange = (newValue: string) => { 1246 | const changeEvent = createChangeEvent(newValue, value); 1247 | setValue(newValue); 1248 | 1249 | // Only trigger external handler if validation passes 1250 | if (changeEvent.isValid) { 1251 | onValidatedChange?.(changeEvent); 1252 | } 1253 | }; 1254 | 1255 | const handleItemSelection = (item: string, action: 'add' | 'remove') => { 1256 | let newSelection: string[]; 1257 | 1258 | if (action === 'add') { 1259 | newSelection = [...selectedItems, item]; 1260 | } else { 1261 | newSelection = selectedItems.filter(i => i !== item); 1262 | } 1263 | 1264 | setSelectedItems(newSelection); 1265 | 1266 | const selectionEvent: CustomSelectionEvent = { 1267 | type: 'selection', 1268 | selectedItems: newSelection, 1269 | action, 1270 | item, 1271 | }; 1272 | 1273 | onSelectionChange?.(selectionEvent); 1274 | }; 1275 | 1276 | return ( 1277 | <div> 1278 | <input 1279 | value={value} 1280 | onChange={(e) => handleValueChange(e.target.value)} 1281 | placeholder="Type at least 3 characters" 1282 | /> 1283 | <div> 1284 | {['apple', 'banana', 'cherry'].map(item => ( 1285 | <button 1286 | key={item} 1287 | onClick={() => handleItemSelection( 1288 | item, 1289 | selectedItems.includes(item) ? 'remove' : 'add' 1290 | )} 1291 | className={selectedItems.includes(item) ? 'selected' : ''} 1292 | > 1293 | {item} 1294 | </button> 1295 | ))} 1296 | </div> 1297 | </div> 1298 | ); 1299 | }; 1300 | ``` 1301 | 1302 | ### Keyboard Event Handling with Accessibility Pattern 1303 | 1304 | **Purpose**: Components need comprehensive keyboard support for accessibility compliance and enhanced user experience, following ARIA patterns and keyboard navigation standards. 1305 | 1306 | **Implementation Pattern**: 1307 | 1308 | ```typescript 1309 | export const AccessibleComponent = ({ 1310 | items, 1311 | onItemSelect, 1312 | onEscape 1313 | }: { 1314 | items: string[]; 1315 | onItemSelect?: (item: string) => void; 1316 | onEscape?: () => void; 1317 | }) => { 1318 | const [focusedIndex, setFocusedIndex] = useState(0); 1319 | const [isOpen, setIsOpen] = useState(false); 1320 | const listRef = useRef<HTMLUListElement>(null); 1321 | const itemRefs = useRef<(HTMLLIElement | null)[]>([]); 1322 | 1323 | const handleKeyDown = (event: React.KeyboardEvent) => { 1324 | switch (event.key) { 1325 | case 'ArrowDown': 1326 | event.preventDefault(); 1327 | setFocusedIndex(prev => 1328 | prev < items.length - 1 ? prev + 1 : 0 1329 | ); 1330 | break; 1331 | 1332 | case 'ArrowUp': 1333 | event.preventDefault(); 1334 | setFocusedIndex(prev => 1335 | prev > 0 ? prev - 1 : items.length - 1 1336 | ); 1337 | break; 1338 | 1339 | case 'Enter': 1340 | case ' ': 1341 | event.preventDefault(); 1342 | if (isOpen && items[focusedIndex]) { 1343 | onItemSelect?.(items[focusedIndex]); 1344 | setIsOpen(false); 1345 | } else { 1346 | setIsOpen(true); 1347 | } 1348 | break; 1349 | 1350 | case 'Escape': 1351 | event.preventDefault(); 1352 | setIsOpen(false); 1353 | onEscape?.(); 1354 | break; 1355 | 1356 | case 'Home': 1357 | event.preventDefault(); 1358 | setFocusedIndex(0); 1359 | break; 1360 | 1361 | case 'End': 1362 | event.preventDefault(); 1363 | setFocusedIndex(items.length - 1); 1364 | break; 1365 | 1366 | case 'Tab': 1367 | // Allow normal tab behavior 1368 | setIsOpen(false); 1369 | break; 1370 | 1371 | default: 1372 | // Handle alphanumeric navigation 1373 | if (event.key.length === 1) { 1374 | const char = event.key.toLowerCase(); 1375 | const nextIndex = items.findIndex((item, index) => 1376 | index > focusedIndex && 1377 | item.toLowerCase().startsWith(char) 1378 | ); 1379 | if (nextIndex !== -1) { 1380 | setFocusedIndex(nextIndex); 1381 | } 1382 | } 1383 | break; 1384 | } 1385 | }; 1386 | 1387 | // Focus management 1388 | useEffect(() => { 1389 | if (isOpen && itemRefs.current[focusedIndex]) { 1390 | itemRefs.current[focusedIndex]?.focus(); 1391 | } 1392 | }, [focusedIndex, isOpen]); 1393 | 1394 | return ( 1395 | <div 1396 | role="combobox" 1397 | aria-expanded={isOpen} 1398 | aria-haspopup="listbox" 1399 | onKeyDown={handleKeyDown} 1400 | tabIndex={0} 1401 | > 1402 | <button 1403 | onClick={() => setIsOpen(!isOpen)} 1404 | aria-label="Toggle dropdown" 1405 | > 1406 | Select Item 1407 | </button> 1408 | 1409 | {isOpen && ( 1410 | <ul 1411 | ref={listRef} 1412 | role="listbox" 1413 | aria-label="Available options" 1414 | > 1415 | {items.map((item, index) => ( 1416 | <li 1417 | key={item} 1418 | ref={el => itemRefs.current[index] = el} 1419 | role="option" 1420 | aria-selected={index === focusedIndex} 1421 | tabIndex={-1} 1422 | onClick={() => { 1423 | onItemSelect?.(item); 1424 | setIsOpen(false); 1425 | }} 1426 | onMouseEnter={() => setFocusedIndex(index)} 1427 | className={index === focusedIndex ? 'focused' : ''} 1428 | > 1429 | {item} 1430 | </li> 1431 | ))} 1432 | </ul> 1433 | )} 1434 | </div> 1435 | ); 1436 | }; 1437 | ``` 1438 | 1439 | ### Mouse/Touch Interaction Pattern 1440 | 1441 | **Purpose**: Components need to handle complex mouse and touch interactions including drag and drop, gestures, and multi-touch scenarios while maintaining performance. 1442 | 1443 | **Implementation Pattern**: 1444 | 1445 | ```typescript 1446 | export const InteractiveComponent = ({ 1447 | onDragComplete, 1448 | onTap, 1449 | onLongPress 1450 | }: { 1451 | onDragComplete?: (startPos: { x: number; y: number }, endPos: { x: number; y: number }) => void; 1452 | onTap?: () => void; 1453 | onLongPress?: () => void; 1454 | }) => { 1455 | const [dragState, setDragState] = useState<{ 1456 | isDragging: boolean; 1457 | startPos: { x: number; y: number } | null; 1458 | currentPos: { x: number; y: number } | null; 1459 | }>({ 1460 | isDragging: false, 1461 | startPos: null, 1462 | currentPos: null, 1463 | }); 1464 | 1465 | const longPressTimerRef = useRef<NodeJS.Timeout>(); 1466 | const tapStartTimeRef = useRef<number>(0); 1467 | 1468 | const handleMouseDown = (event: React.MouseEvent) => { 1469 | const pos = { x: event.clientX, y: event.clientY }; 1470 | setDragState({ 1471 | isDragging: false, 1472 | startPos: pos, 1473 | currentPos: pos, 1474 | }); 1475 | 1476 | tapStartTimeRef.current = Date.now(); 1477 | 1478 | // Start long press timer 1479 | longPressTimerRef.current = setTimeout(() => { 1480 | onLongPress?.(); 1481 | }, 500); 1482 | }; 1483 | 1484 | const handleMouseMove = (event: React.MouseEvent) => { 1485 | if (dragState.startPos) { 1486 | const currentPos = { x: event.clientX, y: event.clientY }; 1487 | const distance = Math.sqrt( 1488 | Math.pow(currentPos.x - dragState.startPos.x, 2) + 1489 | Math.pow(currentPos.y - dragState.startPos.y, 2) 1490 | ); 1491 | 1492 | // Start dragging if moved more than 5 pixels 1493 | if (distance > 5) { 1494 | setDragState(prev => ({ 1495 | ...prev, 1496 | isDragging: true, 1497 | currentPos, 1498 | })); 1499 | 1500 | // Cancel long press if dragging 1501 | if (longPressTimerRef.current) { 1502 | clearTimeout(longPressTimerRef.current); 1503 | } 1504 | } 1505 | } 1506 | }; 1507 | 1508 | const handleMouseUp = (event: React.MouseEvent) => { 1509 | const endPos = { x: event.clientX, y: event.clientY }; 1510 | 1511 | // Clear long press timer 1512 | if (longPressTimerRef.current) { 1513 | clearTimeout(longPressTimerRef.current); 1514 | } 1515 | 1516 | if (dragState.isDragging && dragState.startPos) { 1517 | onDragComplete?.(dragState.startPos, endPos); 1518 | } else if (dragState.startPos) { 1519 | // Check if it's a tap (quick and didn't move much) 1520 | const tapDuration = Date.now() - tapStartTimeRef.current; 1521 | const distance = Math.sqrt( 1522 | Math.pow(endPos.x - dragState.startPos.x, 2) + 1523 | Math.pow(endPos.y - dragState.startPos.y, 2) 1524 | ); 1525 | 1526 | if (tapDuration < 500 && distance < 5) { 1527 | onTap?.(); 1528 | } 1529 | } 1530 | 1531 | setDragState({ 1532 | isDragging: false, 1533 | startPos: null, 1534 | currentPos: null, 1535 | }); 1536 | }; 1537 | 1538 | // Touch events for mobile support 1539 | const handleTouchStart = (event: React.TouchEvent) => { 1540 | const touch = event.touches[0]; 1541 | handleMouseDown({ 1542 | clientX: touch.clientX, 1543 | clientY: touch.clientY, 1544 | } as React.MouseEvent); 1545 | }; 1546 | 1547 | const handleTouchMove = (event: React.TouchEvent) => { 1548 | event.preventDefault(); // Prevent scrolling 1549 | const touch = event.touches[0]; 1550 | handleMouseMove({ 1551 | clientX: touch.clientX, 1552 | clientY: touch.clientY, 1553 | } as React.MouseEvent); 1554 | }; 1555 | 1556 | const handleTouchEnd = (event: React.TouchEvent) => { 1557 | const touch = event.changedTouches[0]; 1558 | handleMouseUp({ 1559 | clientX: touch.clientX, 1560 | clientY: touch.clientY, 1561 | } as React.MouseEvent); 1562 | }; 1563 | 1564 | return ( 1565 | <div 1566 | style={{ 1567 | width: 200, 1568 | height: 200, 1569 | backgroundColor: dragState.isDragging ? '#e0e0e0' : '#f0f0f0', 1570 | border: '2px solid #ccc', 1571 | cursor: dragState.isDragging ? 'grabbing' : 'grab', 1572 | userSelect: 'none', 1573 | position: 'relative', 1574 | }} 1575 | onMouseDown={handleMouseDown} 1576 | onMouseMove={handleMouseMove} 1577 | onMouseUp={handleMouseUp} 1578 | onMouseLeave={handleMouseUp} // Handle mouse leaving component 1579 | onTouchStart={handleTouchStart} 1580 | onTouchMove={handleTouchMove} 1581 | onTouchEnd={handleTouchEnd} 1582 | > 1583 | <div>Interactive Area</div> 1584 | {dragState.isDragging && dragState.startPos && dragState.currentPos && ( 1585 | <div 1586 | style={{ 1587 | position: 'absolute', 1588 | left: Math.min(dragState.startPos.x, dragState.currentPos.x) - 200, 1589 | top: Math.min(dragState.startPos.y, dragState.currentPos.y) - 200, 1590 | width: Math.abs(dragState.currentPos.x - dragState.startPos.x), 1591 | height: Math.abs(dragState.currentPos.y - dragState.startPos.y), 1592 | border: '2px dashed #007acc', 1593 | backgroundColor: 'rgba(0, 122, 204, 0.1)', 1594 | pointerEvents: 'none', 1595 | }} 1596 | /> 1597 | )} 1598 | </div> 1599 | ); 1600 | }; 1601 | ``` 1602 | 1603 | **Key Benefits of Each Pattern**: 1604 | - **Event Callback Props**: Flexible, optional event handling with predictable defaults 1605 | - **Custom Event Objects**: Rich event data, controlled propagation, framework integration 1606 | - **Keyboard Accessibility**: WCAG compliance, enhanced UX, comprehensive navigation support 1607 | - **Mouse/Touch Interactions**: Cross-platform compatibility, gesture recognition, performance optimization 1608 | 1609 | ## API Registration and Programmatic Control Patterns 1610 | 1611 | **Purpose**: XMLUI components need to expose programmatic APIs that allow parent components and external systems to control behavior imperatively. This enables complex interactions like focus management, value manipulation, data refresh, and animation control beyond what declarative props can provide. 1612 | 1613 | ### Basic API Registration Pattern 1614 | 1615 | **Purpose**: Components need to expose simple methods for common operations like focus, blur, and value access that can be called from XMLUI event handlers or external JavaScript. 1616 | 1617 | **Implementation Pattern**: 1618 | 1619 | ```typescript 1620 | // Native component with imperative API 1621 | export const InputNative = forwardRef<HTMLInputElement, Props>( 1622 | function InputNative({ registerComponentApi, updateState, onDidChange }, ref) { 1623 | const inputRef = useRef<HTMLInputElement>(null); 1624 | const [value, setValue] = useState(""); 1625 | 1626 | // Register API methods with XMLUI framework 1627 | useEffect(() => { 1628 | if (registerComponentApi) { 1629 | registerComponentApi({ 1630 | // Basic DOM operations 1631 | focus: () => inputRef.current?.focus(), 1632 | blur: () => inputRef.current?.blur(), 1633 | 1634 | // Value operations 1635 | getValue: () => value, 1636 | setValue: (newValue: string) => { 1637 | setValue(newValue); 1638 | updateState?.({ value: newValue }); 1639 | onDidChange?.(); 1640 | }, 1641 | 1642 | // Validation operations 1643 | isValid: () => value.length >= 3, 1644 | validate: () => { 1645 | const valid = value.length >= 3; 1646 | updateState?.({ isValid: valid }); 1647 | return valid; 1648 | }, 1649 | }); 1650 | } 1651 | }, [registerComponentApi, value, updateState, onDidChange]); 1652 | 1653 | return ( 1654 | <input 1655 | ref={inputRef} 1656 | value={value} 1657 | onChange={(e) => setValue(e.target.value)} 1658 | /> 1659 | ); 1660 | } 1661 | ); 1662 | 1663 | // XMLUI renderer with API registration 1664 | export const inputComponentRenderer = createComponentRenderer( 1665 | "Input", 1666 | InputMd, 1667 | ({ node, registerComponentApi, extractValue, updateState, lookupEventHandler }) => { 1668 | return ( 1669 | <InputNative 1670 | ref={(instance) => { 1671 | // Forward imperative API to XMLUI 1672 | if (instance && registerComponentApi) { 1673 | registerComponentApi(instance); 1674 | } 1675 | }} 1676 | registerComponentApi={registerComponentApi} 1677 | updateState={updateState} 1678 | onDidChange={lookupEventHandler("didChange")} 1679 | placeholder={extractValue(node.props.placeholder)} 1680 | /> 1681 | ); 1682 | }, 1683 | ); 1684 | ``` 1685 | 1686 | ### Async API Operations Pattern 1687 | 1688 | **Purpose**: Components need to expose asynchronous operations like data fetching, file operations, or animations that return promises and can be awaited by calling code. 1689 | 1690 | **Implementation Pattern**: 1691 | 1692 | ```typescript 1693 | export const DataTableNative = forwardRef<DataTableAPI, Props>( 1694 | function DataTableNative({ registerComponentApi, dataSource, updateState }) { 1695 | const [data, setData] = useState([]); 1696 | const [loading, setLoading] = useState(false); 1697 | const [error, setError] = useState<string | null>(null); 1698 | 1699 | const refreshData = async (): Promise<void> => { 1700 | setLoading(true); 1701 | setError(null); 1702 | 1703 | try { 1704 | const response = await fetch(dataSource); 1705 | const newData = await response.json(); 1706 | setData(newData); 1707 | updateState?.({ data: newData, lastRefresh: Date.now() }); 1708 | } catch (err) { 1709 | const errorMessage = err instanceof Error ? err.message : 'Unknown error'; 1710 | setError(errorMessage); 1711 | updateState?.({ error: errorMessage }); 1712 | throw err; // Re-throw for caller handling 1713 | } finally { 1714 | setLoading(false); 1715 | } 1716 | }; 1717 | 1718 | const exportData = async (format: 'csv' | 'json' | 'xlsx'): Promise<Blob> => { 1719 | switch (format) { 1720 | case 'csv': 1721 | const csvContent = data.map(row => 1722 | Object.values(row).join(',') 1723 | ).join('\n'); 1724 | return new Blob([csvContent], { type: 'text/csv' }); 1725 | 1726 | case 'json': 1727 | return new Blob([JSON.stringify(data, null, 2)], { 1728 | type: 'application/json' 1729 | }); 1730 | 1731 | case 'xlsx': 1732 | // Simulate async Excel generation 1733 | await new Promise(resolve => setTimeout(resolve, 1000)); 1734 | return new Blob(['Excel data'], { 1735 | type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 1736 | }); 1737 | 1738 | default: 1739 | throw new Error(`Unsupported format: ${format}`); 1740 | } 1741 | }; 1742 | 1743 | const searchData = async (query: string): Promise<any[]> => { 1744 | // Simulate async search 1745 | await new Promise(resolve => setTimeout(resolve, 300)); 1746 | 1747 | const results = data.filter(item => 1748 | Object.values(item).some(value => 1749 | String(value).toLowerCase().includes(query.toLowerCase()) 1750 | ) 1751 | ); 1752 | 1753 | updateState?.({ searchResults: results, searchQuery: query }); 1754 | return results; 1755 | }; 1756 | 1757 | // Register async API methods 1758 | useEffect(() => { 1759 | if (registerComponentApi) { 1760 | registerComponentApi({ 1761 | // Async data operations 1762 | refreshData, 1763 | exportData, 1764 | searchData, 1765 | 1766 | // Sync getters 1767 | getData: () => data, 1768 | getRowCount: () => data.length, 1769 | isLoading: () => loading, 1770 | hasError: () => error !== null, 1771 | getError: () => error, 1772 | 1773 | // Selection operations 1774 | selectRow: (index: number) => { 1775 | updateState?.({ selectedRowIndex: index }); 1776 | }, 1777 | selectAll: () => { 1778 | updateState?.({ selectedRows: data.map((_, i) => i) }); 1779 | }, 1780 | clearSelection: () => { 1781 | updateState?.({ selectedRows: [], selectedRowIndex: -1 }); 1782 | }, 1783 | }); 1784 | } 1785 | }, [registerComponentApi, data, loading, error, updateState]); 1786 | 1787 | return ( 1788 | <div className="data-table"> 1789 | {loading && <div>Loading...</div>} 1790 | {error && <div className="error">Error: {error}</div>} 1791 | <table> 1792 | <tbody> 1793 | {data.map((row, index) => ( 1794 | <tr key={index}> 1795 | {Object.values(row).map((value, colIndex) => ( 1796 | <td key={colIndex}>{String(value)}</td> 1797 | ))} 1798 | </tr> 1799 | ))} 1800 | </tbody> 1801 | </table> 1802 | </div> 1803 | ); 1804 | } 1805 | ); 1806 | ``` 1807 | 1808 | ### Complex State Management API Pattern 1809 | 1810 | **Purpose**: Components with complex internal state need APIs that allow external control over multiple state aspects while maintaining internal consistency and validation. 1811 | 1812 | **Implementation Pattern**: 1813 | 1814 | ```typescript 1815 | interface FormState { 1816 | values: Record<string, any>; 1817 | errors: Record<string, string>; 1818 | touched: Record<string, boolean>; 1819 | isSubmitting: boolean; 1820 | isDirty: boolean; 1821 | } 1822 | 1823 | export const FormNative = forwardRef<FormAPI, Props>( 1824 | function FormNative({ registerComponentApi, updateState, onValidationChange }) { 1825 | const [formState, setFormState] = useState<FormState>({ 1826 | values: {}, 1827 | errors: {}, 1828 | touched: {}, 1829 | isSubmitting: false, 1830 | isDirty: false, 1831 | }); 1832 | 1833 | const validators = useRef<Record<string, (value: any) => string | null>>({}); 1834 | 1835 | const validateField = (fieldName: string, value: any): string | null => { 1836 | const validator = validators.current[fieldName]; 1837 | return validator ? validator(value) : null; 1838 | }; 1839 | 1840 | const validateForm = (): Record<string, string> => { 1841 | const errors: Record<string, string> = {}; 1842 | 1843 | Object.keys(formState.values).forEach(fieldName => { 1844 | const error = validateField(fieldName, formState.values[fieldName]); 1845 | if (error) { 1846 | errors[fieldName] = error; 1847 | } 1848 | }); 1849 | 1850 | return errors; 1851 | }; 1852 | 1853 | const setFieldValue = (fieldName: string, value: any, shouldValidate = true) => { 1854 | setFormState(prev => { 1855 | const newValues = { ...prev.values, [fieldName]: value }; 1856 | const newErrors = { ...prev.errors }; 1857 | 1858 | if (shouldValidate) { 1859 | const error = validateField(fieldName, value); 1860 | if (error) { 1861 | newErrors[fieldName] = error; 1862 | } else { 1863 | delete newErrors[fieldName]; 1864 | } 1865 | } 1866 | 1867 | const newState = { 1868 | ...prev, 1869 | values: newValues, 1870 | errors: newErrors, 1871 | isDirty: true, 1872 | }; 1873 | 1874 | // Update XMLUI state 1875 | updateState?.({ 1876 | formValues: newValues, 1877 | formErrors: newErrors, 1878 | formIsDirty: true, 1879 | }); 1880 | 1881 | return newState; 1882 | }); 1883 | }; 1884 | 1885 | const resetForm = (newValues?: Record<string, any>) => { 1886 | const resetState: FormState = { 1887 | values: newValues || {}, 1888 | errors: {}, 1889 | touched: {}, 1890 | isSubmitting: false, 1891 | isDirty: false, 1892 | }; 1893 | 1894 | setFormState(resetState); 1895 | updateState?.({ 1896 | formValues: resetState.values, 1897 | formErrors: resetState.errors, 1898 | formIsDirty: false, 1899 | }); 1900 | }; 1901 | 1902 | const submitForm = async (): Promise<boolean> => { 1903 | setFormState(prev => ({ ...prev, isSubmitting: true })); 1904 | 1905 | try { 1906 | const errors = validateForm(); 1907 | 1908 | if (Object.keys(errors).length > 0) { 1909 | setFormState(prev => ({ 1910 | ...prev, 1911 | errors, 1912 | isSubmitting: false, 1913 | touched: Object.keys(prev.values).reduce((acc, key) => ({ 1914 | ...acc, 1915 | [key]: true, 1916 | }), {}), 1917 | })); 1918 | 1919 | onValidationChange?.(false, errors); 1920 | return false; 1921 | } 1922 | 1923 | // Form is valid, proceed with submission 1924 | onValidationChange?.(true, {}); 1925 | return true; 1926 | 1927 | } catch (error) { 1928 | setFormState(prev => ({ ...prev, isSubmitting: false })); 1929 | throw error; 1930 | } 1931 | }; 1932 | 1933 | // Register comprehensive form API 1934 | useEffect(() => { 1935 | if (registerComponentApi) { 1936 | registerComponentApi({ 1937 | // Field operations 1938 | setFieldValue, 1939 | getFieldValue: (fieldName: string) => formState.values[fieldName], 1940 | setFieldError: (fieldName: string, error: string) => { 1941 | setFormState(prev => ({ 1942 | ...prev, 1943 | errors: { ...prev.errors, [fieldName]: error }, 1944 | })); 1945 | }, 1946 | clearFieldError: (fieldName: string) => { 1947 | setFormState(prev => { 1948 | const newErrors = { ...prev.errors }; 1949 | delete newErrors[fieldName]; 1950 | return { ...prev, errors: newErrors }; 1951 | }); 1952 | }, 1953 | 1954 | // Validation operations 1955 | validateField: (fieldName: string) => { 1956 | const error = validateField(fieldName, formState.values[fieldName]); 1957 | if (error) { 1958 | setFormState(prev => ({ 1959 | ...prev, 1960 | errors: { ...prev.errors, [fieldName]: error }, 1961 | })); 1962 | } 1963 | return !error; 1964 | }, 1965 | validateForm: () => { 1966 | const errors = validateForm(); 1967 | setFormState(prev => ({ ...prev, errors })); 1968 | return Object.keys(errors).length === 0; 1969 | }, 1970 | registerValidator: (fieldName: string, validator: (value: any) => string | null) => { 1971 | validators.current[fieldName] = validator; 1972 | }, 1973 | 1974 | // Form operations 1975 | submitForm, 1976 | resetForm, 1977 | setValues: (values: Record<string, any>) => { 1978 | setFormState(prev => ({ 1979 | ...prev, 1980 | values: { ...prev.values, ...values }, 1981 | isDirty: true, 1982 | })); 1983 | }, 1984 | 1985 | // State getters 1986 | getValues: () => formState.values, 1987 | getErrors: () => formState.errors, 1988 | isDirty: () => formState.isDirty, 1989 | isSubmitting: () => formState.isSubmitting, 1990 | isValid: () => Object.keys(formState.errors).length === 0, 1991 | }); 1992 | } 1993 | }, [registerComponentApi, formState, updateState, onValidationChange]); 1994 | 1995 | return ( 1996 | <form onSubmit={(e) => { e.preventDefault(); submitForm(); }}> 1997 | {/* Form content will be rendered by child components */} 1998 | <div className="form-content"> 1999 | {/* Child components access form state through context */} 2000 | </div> 2001 | </form> 2002 | ); 2003 | } 2004 | ); 2005 | ``` 2006 | 2007 | ### Animation and Media Control API Pattern 2008 | 2009 | **Purpose**: Components that handle animations, media playback, or complex visual effects need APIs for controlling timing, playback state, and visual transitions. 2010 | 2011 | **Implementation Pattern**: 2012 | 2013 | ```typescript 2014 | export const VideoPlayerNative = forwardRef<VideoPlayerAPI, Props>( 2015 | function VideoPlayerNative({ registerComponentApi, updateState, onPlaybackChange }) { 2016 | const videoRef = useRef<HTMLVideoElement>(null); 2017 | const [playerState, setPlayerState] = useState({ 2018 | isPlaying: false, 2019 | currentTime: 0, 2020 | duration: 0, 2021 | volume: 1, 2022 | playbackRate: 1, 2023 | isFullscreen: false, 2024 | }); 2025 | 2026 | const play = async (): Promise<void> => { 2027 | if (videoRef.current) { 2028 | await videoRef.current.play(); 2029 | setPlayerState(prev => ({ ...prev, isPlaying: true })); 2030 | updateState?.({ isPlaying: true }); 2031 | onPlaybackChange?.('play'); 2032 | } 2033 | }; 2034 | 2035 | const pause = (): void => { 2036 | if (videoRef.current) { 2037 | videoRef.current.pause(); 2038 | setPlayerState(prev => ({ ...prev, isPlaying: false })); 2039 | updateState?.({ isPlaying: false }); 2040 | onPlaybackChange?.('pause'); 2041 | } 2042 | }; 2043 | 2044 | const seekTo = (timeInSeconds: number): void => { 2045 | if (videoRef.current) { 2046 | videoRef.current.currentTime = timeInSeconds; 2047 | setPlayerState(prev => ({ ...prev, currentTime: timeInSeconds })); 2048 | updateState?.({ currentTime: timeInSeconds }); 2049 | } 2050 | }; 2051 | 2052 | const setVolume = (volume: number): void => { 2053 | const clampedVolume = Math.max(0, Math.min(1, volume)); 2054 | if (videoRef.current) { 2055 | videoRef.current.volume = clampedVolume; 2056 | setPlayerState(prev => ({ ...prev, volume: clampedVolume })); 2057 | updateState?.({ volume: clampedVolume }); 2058 | } 2059 | }; 2060 | 2061 | const setPlaybackRate = (rate: number): void => { 2062 | if (videoRef.current) { 2063 | videoRef.current.playbackRate = rate; 2064 | setPlayerState(prev => ({ ...prev, playbackRate: rate })); 2065 | updateState?.({ playbackRate: rate }); 2066 | } 2067 | }; 2068 | 2069 | const toggleFullscreen = async (): Promise<void> => { 2070 | if (!document.fullscreenElement) { 2071 | await videoRef.current?.requestFullscreen(); 2072 | setPlayerState(prev => ({ ...prev, isFullscreen: true })); 2073 | } else { 2074 | await document.exitFullscreen(); 2075 | setPlayerState(prev => ({ ...prev, isFullscreen: false })); 2076 | } 2077 | }; 2078 | 2079 | const captureFrame = (): string | null => { 2080 | if (videoRef.current) { 2081 | const canvas = document.createElement('canvas'); 2082 | const ctx = canvas.getContext('2d'); 2083 | if (ctx) { 2084 | canvas.width = videoRef.current.videoWidth; 2085 | canvas.height = videoRef.current.videoHeight; 2086 | ctx.drawImage(videoRef.current, 0, 0); 2087 | return canvas.toDataURL('image/png'); 2088 | } 2089 | } 2090 | return null; 2091 | }; 2092 | 2093 | // Register media control API 2094 | useEffect(() => { 2095 | if (registerComponentApi) { 2096 | registerComponentApi({ 2097 | // Playback controls 2098 | play, 2099 | pause, 2100 | stop: () => { 2101 | pause(); 2102 | seekTo(0); 2103 | }, 2104 | toggle: () => playerState.isPlaying ? pause() : play(), 2105 | 2106 | // Navigation controls 2107 | seekTo, 2108 | seekBy: (deltaSeconds: number) => { 2109 | seekTo(playerState.currentTime + deltaSeconds); 2110 | }, 2111 | seekToPercentage: (percentage: number) => { 2112 | seekTo((percentage / 100) * playerState.duration); 2113 | }, 2114 | 2115 | // Audio controls 2116 | setVolume, 2117 | mute: () => setVolume(0), 2118 | unmute: () => setVolume(1), 2119 | adjustVolume: (delta: number) => { 2120 | setVolume(playerState.volume + delta); 2121 | }, 2122 | 2123 | // Playback rate controls 2124 | setPlaybackRate, 2125 | setSpeed: setPlaybackRate, // Alias for common usage 2126 | normalSpeed: () => setPlaybackRate(1), 2127 | 2128 | // Display controls 2129 | toggleFullscreen, 2130 | enterFullscreen: () => { 2131 | if (!document.fullscreenElement) { 2132 | toggleFullscreen(); 2133 | } 2134 | }, 2135 | exitFullscreen: () => { 2136 | if (document.fullscreenElement) { 2137 | toggleFullscreen(); 2138 | } 2139 | }, 2140 | 2141 | // Utility functions 2142 | captureFrame, 2143 | getCurrentTime: () => playerState.currentTime, 2144 | getDuration: () => playerState.duration, 2145 | getVolume: () => playerState.volume, 2146 | getPlaybackRate: () => playerState.playbackRate, 2147 | isPlaying: () => playerState.isPlaying, 2148 | isFullscreen: () => playerState.isFullscreen, 2149 | 2150 | // Advanced controls 2151 | setCurrentTime: seekTo, // Alias for clarity 2152 | getCurrentTimePercentage: () => 2153 | playerState.duration > 0 ? (playerState.currentTime / playerState.duration) * 100 : 0, 2154 | }); 2155 | } 2156 | }, [registerComponentApi, playerState, updateState, onPlaybackChange]); 2157 | 2158 | return ( 2159 | <video 2160 | ref={videoRef} 2161 | onTimeUpdate={(e) => { 2162 | const currentTime = e.currentTarget.currentTime; 2163 | setPlayerState(prev => ({ ...prev, currentTime })); 2164 | updateState?.({ currentTime }); 2165 | }} 2166 | onLoadedMetadata={(e) => { 2167 | const duration = e.currentTarget.duration; 2168 | setPlayerState(prev => ({ ...prev, duration })); 2169 | updateState?.({ duration }); 2170 | }} 2171 | controls 2172 | /> 2173 | ); 2174 | } 2175 | ); 2176 | ``` 2177 | 2178 | **Key Benefits of API Registration Patterns**: 2179 | - **Basic API Registration**: Simple imperative operations, consistent interface, framework integration 2180 | - **Async Operations**: Promise-based APIs, error handling, progress tracking, external system integration 2181 | - **Complex State Management**: Multi-faceted control, validation integration, state consistency, external synchronization 2182 | - **Animation/Media Control**: Real-time control, frame-accurate operations, hardware integration, performance optimization 2183 | 2184 | ## XMLUI Renderer Patterns 2185 | 2186 | **Purpose**: XMLUI renderer functions need consistent, efficient patterns for translating component markup into React elements while handling complex scenarios like child rendering, state management, and dynamic property passing. These patterns solve architectural challenges and ensure optimal performance across the component ecosystem. 2187 | 2188 | ### Standard Component Renderer Structure Pattern 2189 | 2190 | **Purpose**: All XMLUI components need consistent renderer structure for maintainability, debugging, and framework integration. This pattern provides a standardized template that handles common concerns and reduces boilerplate. 2191 | 2192 | **Implementation Pattern**: 2193 | 2194 | ```typescript 2195 | // Import dependencies 2196 | import { createComponentRenderer } from "../renderer-helpers"; 2197 | import { ComponentNameMd } from "./ComponentNameMetadata"; 2198 | import { ComponentNameNative } from "./ComponentNameNative"; 2199 | 2200 | // Component constant for consistency 2201 | const COMP = "ComponentName"; 2202 | 2203 | // Standard renderer structure 2204 | export const componentNameComponentRenderer = createComponentRenderer( 2205 | COMP, // Component name (must match metadata) 2206 | ComponentNameMd, // Component metadata object 2207 | ({ // Destructured renderer context 2208 | // Core context properties 2209 | node, // Component definition with props/children 2210 | extractValue, // Value extraction with binding support 2211 | renderChild, // Child rendering function 2212 | layoutCss, // Pre-computed layout styles 2213 | 2214 | // State management 2215 | state, // Current component state 2216 | updateState, // State update function 2217 | 2218 | // Event system 2219 | lookupEventHandler, // Event handler factory 2220 | lookupAction, // Async action lookup 2221 | 2222 | // API registration 2223 | registerComponentApi, // Component API registration 2224 | 2225 | // Context and utilities 2226 | appContext, // Application context 2227 | uid, // Unique component identifier 2228 | layoutContext, // Layout context information 2229 | }) => { 2230 | 2231 | // Extract and process props with proper defaults and typing 2232 | const variant = extractValue.asOptionalString(node.props.variant, "primary"); 2233 | const enabled = extractValue.asOptionalBoolean(node.props.enabled, true); 2234 | const label = extractValue.asDisplayText(node.props.label); 2235 | const icon = extractValue.asString(node.props.icon); 2236 | 2237 | // Handle complex prop processing 2238 | const processedProps = { 2239 | variant, 2240 | enabled, 2241 | label, 2242 | // Transform XMLUI props to native component props 2243 | disabled: !enabled, 2244 | showIcon: Boolean(icon), 2245 | iconName: icon || undefined, 2246 | }; 2247 | 2248 | // Event handler creation with error boundaries 2249 | const eventHandlers = { 2250 | onClick: lookupEventHandler("click"), 2251 | onFocus: lookupEventHandler("gotFocus"), 2252 | onBlur: lookupEventHandler("lostFocus"), 2253 | onValueChange: lookupEventHandler("didChange"), 2254 | }; 2255 | 2256 | // Conditional rendering logic 2257 | if (!enabled && node.props.hideWhenDisabled) { 2258 | return null; 2259 | } 2260 | 2261 | // Return rendered component with all integrations 2262 | return ( 2263 | <ComponentNameNative 2264 | {...processedProps} 2265 | {...eventHandlers} 2266 | 2267 | // Framework integration 2268 | updateState={updateState} 2269 | registerComponentApi={registerComponentApi} 2270 | 2271 | // Layout and styling 2272 | style={layoutCss} 2273 | className={extractValue.asString(node.props.className)} 2274 | 2275 | // Child content handling 2276 | children={renderChild(node.children)} 2277 | 2278 | // Advanced props 2279 | ref={(instance) => { 2280 | if (instance && registerComponentApi) { 2281 | registerComponentApi(instance); 2282 | } 2283 | }} 2284 | /> 2285 | ); 2286 | }, 2287 | ); 2288 | 2289 | // Export with consistent naming 2290 | export { componentNameComponentRenderer as componentNameRenderer }; 2291 | ``` 2292 | 2293 | ### Child Rendering Patterns 2294 | 2295 | **Purpose**: Components need different strategies for rendering child content depending on their role as containers, the type of children they accept, and the layout context they provide. This pattern optimizes performance and provides proper component composition. 2296 | 2297 | **Implementation Pattern**: 2298 | 2299 | ```typescript 2300 | // Pattern 1: Simple pass-through rendering 2301 | export const simpleContainerRenderer = createComponentRenderer( 2302 | "SimpleContainer", 2303 | SimpleContainerMd, 2304 | ({ node, renderChild, layoutCss }) => { 2305 | return ( 2306 | <div style={layoutCss} className="simple-container"> 2307 | {/* Direct child rendering - preserves all children as-is */} 2308 | {renderChild(node.children)} 2309 | </div> 2310 | ); 2311 | }, 2312 | ); 2313 | 2314 | // Pattern 2: Layout context rendering with specific arrangements 2315 | export const stackContainerRenderer = createComponentRenderer( 2316 | "Stack", 2317 | StackMd, 2318 | ({ node, extractValue, renderChild, layoutCss }) => { 2319 | const orientation = extractValue.asOptionalString(node.props.orientation, "vertical"); 2320 | const spacing = extractValue.asOptionalString(node.props.spacing, "medium"); 2321 | const alignment = extractValue.asOptionalString(node.props.alignment, "start"); 2322 | 2323 | return ( 2324 | <div 2325 | style={layoutCss} 2326 | className={`stack stack-${orientation} spacing-${spacing} align-${alignment}`} 2327 | > 2328 | {/* Render children with layout context for optimal arrangement */} 2329 | {renderChild(node.children, { 2330 | type: "Stack", 2331 | orientation, 2332 | spacing, 2333 | alignment, 2334 | // Layout hints for child components 2335 | itemAlignment: alignment, 2336 | crossAxisAlignment: extractValue.asOptionalString(node.props.crossAxisAlignment), 2337 | })} 2338 | </div> 2339 | ); 2340 | }, 2341 | ); 2342 | 2343 | // Pattern 3: Conditional child rendering based on component state 2344 | export const expandableContainerRenderer = createComponentRenderer( 2345 | "ExpandableContainer", 2346 | ExpandableContainerMd, 2347 | ({ node, extractValue, renderChild, state, layoutCss }) => { 2348 | const isExpanded = state.expanded || extractValue.asOptionalBoolean(node.props.defaultExpanded, false); 2349 | const renderMode = extractValue.asOptionalString(node.props.renderMode, "lazy"); 2350 | 2351 | return ( 2352 | <div style={layoutCss} className="expandable-container"> 2353 | <div className="header"> 2354 | {extractValue.asDisplayText(node.props.title)} 2355 | </div> 2356 | 2357 | {/* Conditional rendering with performance optimization */} 2358 | {isExpanded && ( 2359 | <div className="content"> 2360 | {renderMode === "lazy" 2361 | ? renderChild(node.children) // Render only when expanded 2362 | : null // Children rendered separately with visibility control 2363 | } 2364 | </div> 2365 | )} 2366 | 2367 | {/* Always render but control visibility for "eager" mode */} 2368 | {renderMode === "eager" && ( 2369 | <div className={`content ${!isExpanded ? 'hidden' : ''}`}> 2370 | {renderChild(node.children)} 2371 | </div> 2372 | )} 2373 | </div> 2374 | ); 2375 | }, 2376 | ); 2377 | 2378 | // Pattern 4: Selective child rendering with filtering 2379 | export const tabContainerRenderer = createComponentRenderer( 2380 | "TabContainer", 2381 | TabContainerMd, 2382 | ({ node, extractValue, renderChild, state, layoutCss }) => { 2383 | const activeTabIndex = state.activeTabIndex || extractValue.asOptionalNumber(node.props.defaultActiveTab, 0); 2384 | 2385 | // Filter children to only render tab panels 2386 | const tabPanels = node.children?.filter(child => child.component === "TabPanel") || []; 2387 | const activePanel = tabPanels[activeTabIndex]; 2388 | 2389 | return ( 2390 | <div style={layoutCss} className="tab-container"> 2391 | <div className="tab-headers"> 2392 | {tabPanels.map((panel, index) => ( 2393 | <button 2394 | key={panel.uid || index} 2395 | className={`tab-header ${index === activeTabIndex ? 'active' : ''}`} 2396 | onClick={() => updateState({ activeTabIndex: index })} 2397 | > 2398 | {extractValue.asDisplayText(panel.props.title)} 2399 | </button> 2400 | ))} 2401 | </div> 2402 | 2403 | <div className="tab-content"> 2404 | {/* Render only the active tab panel for performance */} 2405 | {activePanel && renderChild([activePanel])} 2406 | </div> 2407 | </div> 2408 | ); 2409 | }, 2410 | ); 2411 | 2412 | // Pattern 5: Hybrid rendering - mix of children and direct props 2413 | export const buttonWithContentRenderer = createComponentRenderer( 2414 | "ButtonWithContent", 2415 | ButtonWithContentMd, 2416 | ({ node, extractValue, renderChild, lookupEventHandler, layoutCss }) => { 2417 | const label = extractValue.asDisplayText(node.props.label); 2418 | const hasChildren = node.children && node.children.length > 0; 2419 | 2420 | return ( 2421 | <button 2422 | style={layoutCss} 2423 | className="button-with-content" 2424 | onClick={lookupEventHandler("click")} 2425 | > 2426 | {/* Prioritize children over label prop */} 2427 | {hasChildren 2428 | ? renderChild(node.children, { 2429 | type: "ButtonContent", 2430 | inline: true, 2431 | // Pass button context to children 2432 | buttonVariant: extractValue.asOptionalString(node.props.variant), 2433 | buttonSize: extractValue.asOptionalString(node.props.size), 2434 | }) 2435 | : label || "Button" 2436 | } 2437 | </button> 2438 | ); 2439 | }, 2440 | ); 2441 | 2442 | // Pattern 6: Performance-optimized rendering with memoization 2443 | export const dataListRenderer = createComponentRenderer( 2444 | "DataList", 2445 | DataListMd, 2446 | ({ node, extractValue, renderChild, state, layoutCss }) => { 2447 | const data = state.data || extractValue(node.props.data) || []; 2448 | const itemTemplate = node.children?.find(child => child.component === "ItemTemplate"); 2449 | const emptyTemplate = node.children?.find(child => child.component === "EmptyTemplate"); 2450 | 2451 | // Memoize expensive rendering operations 2452 | const renderedItems = useMemo(() => { 2453 | if (!itemTemplate || data.length === 0) return null; 2454 | 2455 | return data.map((item, index) => { 2456 | // Clone template with item data context 2457 | const itemNode = { 2458 | ...itemTemplate, 2459 | uid: `${itemTemplate.uid}-${index}`, 2460 | // Inject item data into template context 2461 | props: { 2462 | ...itemTemplate.props, 2463 | $item: item, 2464 | $index: index, 2465 | }, 2466 | }; 2467 | 2468 | return renderChild([itemNode]); 2469 | }); 2470 | }, [data, itemTemplate, renderChild]); 2471 | 2472 | return ( 2473 | <div style={layoutCss} className="data-list"> 2474 | {data.length > 0 2475 | ? renderedItems 2476 | : emptyTemplate && renderChild([emptyTemplate]) 2477 | } 2478 | </div> 2479 | ); 2480 | }, 2481 | ); 2482 | ``` 2483 | 2484 | ### Conditional Property Passing Pattern 2485 | 2486 | **Purpose**: Components need dynamic behavior based on state, props, or context conditions. This pattern enables responsive component behavior while maintaining performance and preventing unnecessary re-renders. 2487 | 2488 | **Implementation Pattern**: 2489 | 2490 | ```typescript 2491 | // Pattern 1: State-based conditional props 2492 | export const dynamicInputRenderer = createComponentRenderer( 2493 | "DynamicInput", 2494 | DynamicInputMd, 2495 | ({ node, extractValue, state, updateState, lookupEventHandler, layoutCss }) => { 2496 | const inputType = extractValue.asOptionalString(node.props.type, "text"); 2497 | const isPassword = inputType === "password"; 2498 | const isReadOnly = state.readOnly || extractValue.asOptionalBoolean(node.props.readOnly, false); 2499 | const hasError = Boolean(state.error); 2500 | 2501 | // Build dynamic props object 2502 | const dynamicProps = { 2503 | // Base props always present 2504 | type: inputType, 2505 | value: state.value || extractValue(node.props.value) || "", 2506 | placeholder: extractValue.asString(node.props.placeholder), 2507 | 2508 | // Conditional props based on state and type 2509 | ...(isPassword && { 2510 | autoComplete: "current-password", 2511 | spellCheck: false, 2512 | }), 2513 | 2514 | ...(isReadOnly && { 2515 | readOnly: true, 2516 | tabIndex: -1, 2517 | }), 2518 | 2519 | ...(hasError && { 2520 | "aria-invalid": true, 2521 | "aria-describedby": `${node.uid}-error`, 2522 | }), 2523 | 2524 | // Performance optimization - only add expensive props when needed 2525 | ...(extractValue.asOptionalBoolean(node.props.enableSpellCheck) && { 2526 | spellCheck: true, 2527 | }), 2528 | 2529 | // Conditional event handlers 2530 | onChange: !isReadOnly ? lookupEventHandler("didChange") : undefined, 2531 | onFocus: !isReadOnly ? lookupEventHandler("gotFocus") : undefined, 2532 | onBlur: !isReadOnly ? lookupEventHandler("lostFocus") : undefined, 2533 | }; 2534 | 2535 | return ( 2536 | <div style={layoutCss} className="dynamic-input-container"> 2537 | <input 2538 | {...dynamicProps} 2539 | className={`input ${hasError ? 'error' : ''} ${isReadOnly ? 'readonly' : ''}`} 2540 | /> 2541 | 2542 | {/* Conditionally render error message */} 2543 | {hasError && ( 2544 | <div id={`${node.uid}-error`} className="error-message"> 2545 | {state.error} 2546 | </div> 2547 | )} 2548 | 2549 | {/* Conditionally render password toggle */} 2550 | {isPassword && !isReadOnly && ( 2551 | <button 2552 | type="button" 2553 | className="password-toggle" 2554 | onClick={() => updateState({ showPassword: !state.showPassword })} 2555 | > 2556 | {state.showPassword ? "Hide" : "Show"} 2557 | </button> 2558 | )} 2559 | </div> 2560 | ); 2561 | }, 2562 | ); 2563 | 2564 | // Pattern 2: Context-aware conditional rendering 2565 | export const responsiveCardRenderer = createComponentRenderer( 2566 | "ResponsiveCard", 2567 | ResponsiveCardMd, 2568 | ({ node, extractValue, renderChild, layoutContext, appContext, layoutCss }) => { 2569 | const variant = extractValue.asOptionalString(node.props.variant, "default"); 2570 | const size = extractValue.asOptionalString(node.props.size, "medium"); 2571 | 2572 | // Determine rendering strategy based on context 2573 | const isMobile = layoutContext?.breakpoint === "mobile" || appContext?.screenWidth < 768; 2574 | const isInGrid = layoutContext?.type === "Grid"; 2575 | const isInStack = layoutContext?.type === "Stack"; 2576 | 2577 | // Build conditional style and behavior props 2578 | const conditionalProps = { 2579 | // Base styling 2580 | className: `card card-${variant} card-${size}`, 2581 | 2582 | // Context-aware modifications 2583 | ...(isMobile && { 2584 | className: `card card-${variant} card-${size} card-mobile`, 2585 | // Simplified props for mobile 2586 | showSecondaryActions: false, 2587 | compactMode: true, 2588 | }), 2589 | 2590 | ...(isInGrid && { 2591 | // Grid-specific optimizations 2592 | className: `card card-${variant} card-${size} card-in-grid`, 2593 | aspectRatio: extractValue.asOptionalString(node.props.aspectRatio, "auto"), 2594 | }), 2595 | 2596 | ...(isInStack && layoutContext.orientation === "horizontal" && { 2597 | // Horizontal stack optimizations 2598 | className: `card card-${variant} card-${size} card-horizontal`, 2599 | layout: "horizontal", 2600 | }), 2601 | }; 2602 | 2603 | // Conditional feature rendering based on capabilities 2604 | const showAdvancedFeatures = !isMobile && appContext?.features?.advancedCardFeatures !== false; 2605 | const showAnimations = appContext?.settings?.enableAnimations !== false; 2606 | 2607 | return ( 2608 | <div 2609 | style={layoutCss} 2610 | {...conditionalProps} 2611 | {...(showAnimations && { 2612 | className: `${conditionalProps.className} animated`, 2613 | })} 2614 | > 2615 | {/* Always render main content */} 2616 | <div className="card-content"> 2617 | {renderChild(node.children?.filter(child => 2618 | child.component !== "CardActions" && child.component !== "CardMenu" 2619 | ))} 2620 | </div> 2621 | 2622 | {/* Conditionally render advanced features */} 2623 | {showAdvancedFeatures && ( 2624 | <> 2625 | {node.children?.find(child => child.component === "CardActions") && ( 2626 | <div className="card-actions"> 2627 | {renderChild(node.children.filter(child => child.component === "CardActions"))} 2628 | </div> 2629 | )} 2630 | 2631 | {node.children?.find(child => child.component === "CardMenu") && ( 2632 | <div className="card-menu"> 2633 | {renderChild(node.children.filter(child => child.component === "CardMenu"))} 2634 | </div> 2635 | )} 2636 | </> 2637 | )} 2638 | </div> 2639 | ); 2640 | }, 2641 | ); 2642 | 2643 | // Pattern 3: Performance-optimized conditional props with memoization 2644 | export const optimizedDataTableRenderer = createComponentRenderer( 2645 | "OptimizedDataTable", 2646 | OptimizedDataTableMd, 2647 | ({ node, extractValue, state, renderChild, layoutCss }) => { 2648 | const data = state.data || []; 2649 | const columns = extractValue(node.props.columns) || []; 2650 | const enableVirtualization = extractValue.asOptionalBoolean(node.props.enableVirtualization, data.length > 100); 2651 | const enableSorting = extractValue.asOptionalBoolean(node.props.enableSorting, true); 2652 | const enableFiltering = extractValue.asOptionalBoolean(node.props.enableFiltering, false); 2653 | 2654 | // Memoize expensive conditional props 2655 | const tableProps = useMemo(() => ({ 2656 | // Base props 2657 | data, 2658 | columns, 2659 | 2660 | // Conditional feature props - only compute when needed 2661 | ...(enableVirtualization && { 2662 | virtualizer: { 2663 | itemHeight: extractValue.asOptionalNumber(node.props.rowHeight, 40), 2664 | overscan: extractValue.asOptionalNumber(node.props.overscan, 5), 2665 | }, 2666 | }), 2667 | 2668 | ...(enableSorting && { 2669 | sortConfig: { 2670 | defaultSort: extractValue(node.props.defaultSort), 2671 | multiSort: extractValue.asOptionalBoolean(node.props.multiSort, false), 2672 | }, 2673 | }), 2674 | 2675 | ...(enableFiltering && { 2676 | filterConfig: { 2677 | globalFilter: state.globalFilter, 2678 | columnFilters: state.columnFilters || {}, 2679 | }, 2680 | }), 2681 | 2682 | // Performance props based on data size 2683 | ...(data.length > 1000 && { 2684 | deferredRendering: true, 2685 | batchSize: 50, 2686 | }), 2687 | 2688 | }), [data, columns, enableVirtualization, enableSorting, enableFiltering, state]); 2689 | 2690 | // Conditional className based on features and state 2691 | const tableClassName = [ 2692 | "data-table", 2693 | enableVirtualization && "virtualized", 2694 | enableSorting && "sortable", 2695 | enableFiltering && "filterable", 2696 | data.length > 1000 && "large-dataset", 2697 | state.isLoading && "loading", 2698 | ].filter(Boolean).join(" "); 2699 | 2700 | return ( 2701 | <div style={layoutCss} className="data-table-container"> 2702 | {/* Conditionally render filter UI */} 2703 | {enableFiltering && ( 2704 | <div className="table-filters"> 2705 | {renderChild(node.children?.filter(child => child.component === "TableFilter"))} 2706 | </div> 2707 | )} 2708 | 2709 | <div className={tableClassName}> 2710 | {/* Render table with conditional props */} 2711 | <DataTableNative 2712 | {...tableProps} 2713 | updateState={updateState} 2714 | registerComponentApi={registerComponentApi} 2715 | /> 2716 | </div> 2717 | 2718 | {/* Conditionally render pagination for large datasets */} 2719 | {data.length > 25 && ( 2720 | <div className="table-pagination"> 2721 | {renderChild(node.children?.filter(child => child.component === "TablePagination"))} 2722 | </div> 2723 | )} 2724 | </div> 2725 | ); 2726 | }, 2727 | ); 2728 | ``` 2729 | 2730 | **Key Benefits of Renderer Patterns**: 2731 | - **Standard Structure**: Consistent architecture, reduced boilerplate, easier maintenance, debugging support 2732 | - **Child Rendering**: Flexible composition, performance optimization, layout context awareness, selective rendering 2733 | - **Conditional Properties**: Dynamic behavior, performance optimization, context awareness, responsive design support 2734 | 2735 | ## Performance Patterns 2736 | 2737 | **Purpose**: React components need optimization strategies to handle large datasets, complex computations, and frequent updates without degrading user experience. These patterns solve performance bottlenecks through memoization, virtualization, lazy loading, and efficient update strategies. 2738 | 2739 | ### Memoization with useMemo and useCallback Pattern 2740 | 2741 | **Purpose**: Components need to prevent expensive recalculations and avoid unnecessary re-renders of child components by memoizing computed values and stable function references. 2742 | 2743 | **Implementation Pattern**: 2744 | 2745 | ```typescript 2746 | export const OptimizedDataProcessor = ({ 2747 | data, 2748 | filters, 2749 | sortConfig, 2750 | onDataChange, 2751 | onSelectionChange 2752 | }: Props) => { 2753 | const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set()); 2754 | const [searchQuery, setSearchQuery] = useState(""); 2755 | 2756 | // Memoize expensive data transformations 2757 | const filteredData = useMemo(() => { 2758 | console.log("Computing filtered data..."); // Only logs when dependencies change 2759 | 2760 | return data.filter(item => { 2761 | // Apply multiple filters 2762 | const matchesSearch = searchQuery === "" || 2763 | item.name.toLowerCase().includes(searchQuery.toLowerCase()) || 2764 | item.description.toLowerCase().includes(searchQuery.toLowerCase()); 2765 | 2766 | const matchesCategory = !filters.category || 2767 | item.category === filters.category; 2768 | 2769 | const matchesDateRange = !filters.dateRange || 2770 | (item.date >= filters.dateRange.start && item.date <= filters.dateRange.end); 2771 | 2772 | return matchesSearch && matchesCategory && matchesDateRange; 2773 | }); 2774 | }, [data, searchQuery, filters.category, filters.dateRange]); 2775 | 2776 | // Memoize expensive sorting operations 2777 | const sortedData = useMemo(() => { 2778 | console.log("Computing sorted data..."); // Only logs when dependencies change 2779 | 2780 | if (!sortConfig.field) return filteredData; 2781 | 2782 | return [...filteredData].sort((a, b) => { 2783 | const aVal = a[sortConfig.field]; 2784 | const bVal = b[sortConfig.field]; 2785 | 2786 | // Handle different data types 2787 | if (typeof aVal === 'number' && typeof bVal === 'number') { 2788 | return sortConfig.direction === 'asc' ? aVal - bVal : bVal - aVal; 2789 | } 2790 | 2791 | if (aVal instanceof Date && bVal instanceof Date) { 2792 | return sortConfig.direction === 'asc' 2793 | ? aVal.getTime() - bVal.getTime() 2794 | : bVal.getTime() - aVal.getTime(); 2795 | } 2796 | 2797 | // String comparison 2798 | const result = String(aVal).localeCompare(String(bVal)); 2799 | return sortConfig.direction === 'asc' ? result : -result; 2800 | }); 2801 | }, [filteredData, sortConfig.field, sortConfig.direction]); 2802 | 2803 | // Memoize computed statistics 2804 | const statistics = useMemo(() => { 2805 | return { 2806 | total: data.length, 2807 | filtered: filteredData.length, 2808 | selected: selectedItems.size, 2809 | categories: [...new Set(data.map(item => item.category))].length, 2810 | averageValue: data.reduce((sum, item) => sum + (item.value || 0), 0) / data.length, 2811 | }; 2812 | }, [data, filteredData.length, selectedItems.size]); 2813 | 2814 | // Memoize stable callback functions to prevent child re-renders 2815 | const handleItemClick = useCallback((itemId: string) => { 2816 | setSelectedItems(prev => { 2817 | const newSelection = new Set(prev); 2818 | if (newSelection.has(itemId)) { 2819 | newSelection.delete(itemId); 2820 | } else { 2821 | newSelection.add(itemId); 2822 | } 2823 | 2824 | // Notify parent with stable reference 2825 | onSelectionChange?.(Array.from(newSelection)); 2826 | return newSelection; 2827 | }); 2828 | }, [onSelectionChange]); 2829 | 2830 | const handleSelectAll = useCallback(() => { 2831 | const allIds = sortedData.map(item => item.id); 2832 | setSelectedItems(new Set(allIds)); 2833 | onSelectionChange?.(allIds); 2834 | }, [sortedData, onSelectionChange]); 2835 | 2836 | const handleClearSelection = useCallback(() => { 2837 | setSelectedItems(new Set()); 2838 | onSelectionChange?.([]); 2839 | }, [onSelectionChange]); 2840 | 2841 | // Memoize search handler with debouncing 2842 | const debouncedSearch = useCallback( 2843 | debounce((query: string) => { 2844 | setSearchQuery(query); 2845 | }, 300), 2846 | [] 2847 | ); 2848 | 2849 | const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { 2850 | debouncedSearch(event.target.value); 2851 | }, [debouncedSearch]); 2852 | 2853 | // Memoize expensive render functions 2854 | const renderStatistics = useCallback(() => ( 2855 | <div className="statistics"> 2856 | <span>Total: {statistics.total}</span> 2857 | <span>Filtered: {statistics.filtered}</span> 2858 | <span>Selected: {statistics.selected}</span> 2859 | <span>Categories: {statistics.categories}</span> 2860 | <span>Average: {statistics.averageValue.toFixed(2)}</span> 2861 | </div> 2862 | ), [statistics]); 2863 | 2864 | return ( 2865 | <div className="optimized-data-processor"> 2866 | {/* Search input - uses memoized handler */} 2867 | <input 2868 | type="text" 2869 | placeholder="Search..." 2870 | onChange={handleSearchChange} 2871 | /> 2872 | 2873 | {/* Statistics - memoized component */} 2874 | {renderStatistics()} 2875 | 2876 | {/* Action buttons - use memoized handlers */} 2877 | <div className="actions"> 2878 | <button onClick={handleSelectAll}>Select All</button> 2879 | <button onClick={handleClearSelection}>Clear Selection</button> 2880 | </div> 2881 | 2882 | {/* Data list - only re-renders when sortedData changes */} 2883 | <div className="data-list"> 2884 | {sortedData.map(item => ( 2885 | <MemoizedDataItem 2886 | key={item.id} 2887 | item={item} 2888 | isSelected={selectedItems.has(item.id)} 2889 | onClick={handleItemClick} 2890 | /> 2891 | ))} 2892 | </div> 2893 | </div> 2894 | ); 2895 | }; 2896 | 2897 | // Memoized child component to prevent unnecessary re-renders 2898 | const MemoizedDataItem = React.memo(({ 2899 | item, 2900 | isSelected, 2901 | onClick 2902 | }: { 2903 | item: DataItem; 2904 | isSelected: boolean; 2905 | onClick: (id: string) => void; 2906 | }) => { 2907 | const handleClick = useCallback(() => { 2908 | onClick(item.id); 2909 | }, [item.id, onClick]); 2910 | 2911 | return ( 2912 | <div 2913 | className={`data-item ${isSelected ? 'selected' : ''}`} 2914 | onClick={handleClick} 2915 | > 2916 | <h3>{item.name}</h3> 2917 | <p>{item.description}</p> 2918 | <span>{item.category}</span> 2919 | </div> 2920 | ); 2921 | }); 2922 | ``` 2923 | 2924 | ### Lazy Loading and Code Splitting Pattern 2925 | 2926 | **Purpose**: Components need to load content and code on-demand to reduce initial bundle size and improve perceived performance by deferring non-critical resources. 2927 | 2928 | **Implementation Pattern**: 2929 | 2930 | ```typescript 2931 | // Dynamic component import with lazy loading 2932 | const LazyChart = React.lazy(() => 2933 | import('./components/Chart').then(module => ({ 2934 | default: module.Chart 2935 | })) 2936 | ); 2937 | 2938 | const LazyDataTable = React.lazy(() => 2939 | import('./components/DataTable').then(module => ({ 2940 | default: module.DataTable 2941 | })) 2942 | ); 2943 | 2944 | const LazyImageEditor = React.lazy(() => 2945 | import('./components/ImageEditor').then(module => ({ 2946 | default: module.ImageEditor 2947 | })) 2948 | ); 2949 | 2950 | export const LazyLoadingContainer = ({ 2951 | activeTab, 2952 | data, 2953 | onTabChange 2954 | }: Props) => { 2955 | const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set(['overview'])); 2956 | const [imageCache, setImageCache] = useState<Map<string, string>>(new Map()); 2957 | 2958 | // Track which tabs have been viewed for preloading 2959 | const handleTabChange = useCallback((tabId: string) => { 2960 | setLoadedTabs(prev => new Set([...prev, tabId])); 2961 | onTabChange(tabId); 2962 | }, [onTabChange]); 2963 | 2964 | // Preload next likely tab based on user behavior 2965 | useEffect(() => { 2966 | const preloadMap = { 2967 | 'overview': 'analytics', 2968 | 'analytics': 'reports', 2969 | 'reports': 'settings', 2970 | }; 2971 | 2972 | const nextTab = preloadMap[activeTab as keyof typeof preloadMap]; 2973 | if (nextTab && !loadedTabs.has(nextTab)) { 2974 | // Preload after a delay 2975 | const timer = setTimeout(() => { 2976 | setLoadedTabs(prev => new Set([...prev, nextTab])); 2977 | }, 2000); 2978 | 2979 | return () => clearTimeout(timer); 2980 | } 2981 | }, [activeTab, loadedTabs]); 2982 | 2983 | // Lazy image loading with intersection observer 2984 | const LazyImage = useCallback(({ src, alt, ...props }: ImageProps) => { 2985 | const [isLoaded, setIsLoaded] = useState(false); 2986 | const [isInView, setIsInView] = useState(false); 2987 | const imgRef = useRef<HTMLImageElement>(null); 2988 | 2989 | useEffect(() => { 2990 | const observer = new IntersectionObserver( 2991 | ([entry]) => { 2992 | if (entry.isIntersecting) { 2993 | setIsInView(true); 2994 | observer.disconnect(); 2995 | } 2996 | }, 2997 | { threshold: 0.1 } 2998 | ); 2999 | 3000 | if (imgRef.current) { 3001 | observer.observe(imgRef.current); 3002 | } 3003 | 3004 | return () => observer.disconnect(); 3005 | }, []); 3006 | 3007 | useEffect(() => { 3008 | if (isInView && !isLoaded) { 3009 | // Check cache first 3010 | if (imageCache.has(src)) { 3011 | setIsLoaded(true); 3012 | return; 3013 | } 3014 | 3015 | // Preload image 3016 | const img = new Image(); 3017 | img.onload = () => { 3018 | setImageCache(prev => new Map([...prev, [src, src]])); 3019 | setIsLoaded(true); 3020 | }; 3021 | img.src = src; 3022 | } 3023 | }, [isInView, isLoaded, src, imageCache]); 3024 | 3025 | return ( 3026 | <div ref={imgRef} className="lazy-image-container" {...props}> 3027 | {isLoaded ? ( 3028 | <img src={src} alt={alt} className="lazy-image loaded" /> 3029 | ) : ( 3030 | <div className="lazy-image-placeholder"> 3031 | <div className="loading-spinner" /> 3032 | </div> 3033 | )} 3034 | </div> 3035 | ); 3036 | }, [imageCache]); 3037 | 3038 | // Lazy content loading based on visibility 3039 | const LazyContent = useCallback(({ 3040 | children, 3041 | fallback = <div>Loading...</div>, 3042 | threshold = 0.1 3043 | }: LazyContentProps) => { 3044 | const [shouldLoad, setShouldLoad] = useState(false); 3045 | const containerRef = useRef<HTMLDivElement>(null); 3046 | 3047 | useEffect(() => { 3048 | const observer = new IntersectionObserver( 3049 | ([entry]) => { 3050 | if (entry.isIntersecting) { 3051 | setShouldLoad(true); 3052 | observer.disconnect(); 3053 | } 3054 | }, 3055 | { threshold } 3056 | ); 3057 | 3058 | if (containerRef.current) { 3059 | observer.observe(containerRef.current); 3060 | } 3061 | 3062 | return () => observer.disconnect(); 3063 | }, [threshold]); 3064 | 3065 | return ( 3066 | <div ref={containerRef}> 3067 | {shouldLoad ? children : fallback} 3068 | </div> 3069 | ); 3070 | }, []); 3071 | 3072 | return ( 3073 | <div className="lazy-loading-container"> 3074 | <nav className="tabs"> 3075 | {['overview', 'analytics', 'reports', 'settings'].map(tab => ( 3076 | <button 3077 | key={tab} 3078 | className={`tab ${activeTab === tab ? 'active' : ''}`} 3079 | onClick={() => handleTabChange(tab)} 3080 | > 3081 | {tab.charAt(0).toUpperCase() + tab.slice(1)} 3082 | {!loadedTabs.has(tab) && <span className="loading-indicator" />} 3083 | </button> 3084 | ))} 3085 | </nav> 3086 | 3087 | <div className="tab-content"> 3088 | {/* Always render overview immediately */} 3089 | {activeTab === 'overview' && ( 3090 | <div className="overview-tab"> 3091 | <h2>Overview</h2> 3092 | <p>Key metrics and summary information</p> 3093 | 3094 | {/* Lazy load images in overview */} 3095 | <div className="image-gallery"> 3096 | {data.images?.map((img, index) => ( 3097 | <LazyImage 3098 | key={index} 3099 | src={img.src} 3100 | alt={img.alt} 3101 | className="gallery-image" 3102 | /> 3103 | ))} 3104 | </div> 3105 | </div> 3106 | )} 3107 | 3108 | {/* Lazy load analytics tab */} 3109 | {activeTab === 'analytics' && ( 3110 | <Suspense fallback={<div className="loading">Loading Analytics...</div>}> 3111 | <LazyContent> 3112 | {loadedTabs.has('analytics') && ( 3113 | <LazyChart 3114 | data={data.analytics} 3115 | type="line" 3116 | animated={true} 3117 | /> 3118 | )} 3119 | </LazyContent> 3120 | </Suspense> 3121 | )} 3122 | 3123 | {/* Lazy load reports tab */} 3124 | {activeTab === 'reports' && ( 3125 | <Suspense fallback={<div className="loading">Loading Reports...</div>}> 3126 | <LazyContent> 3127 | {loadedTabs.has('reports') && ( 3128 | <LazyDataTable 3129 | data={data.reports} 3130 | pagination={true} 3131 | exportable={true} 3132 | /> 3133 | )} 3134 | </LazyContent> 3135 | </Suspense> 3136 | )} 3137 | 3138 | {/* Lazy load settings tab */} 3139 | {activeTab === 'settings' && ( 3140 | <Suspense fallback={<div className="loading">Loading Settings...</div>}> 3141 | <LazyContent> 3142 | {loadedTabs.has('settings') && ( 3143 | <LazyImageEditor 3144 | features={['crop', 'filter', 'adjust']} 3145 | onSave={handleImageSave} 3146 | /> 3147 | )} 3148 | </LazyContent> 3149 | </Suspense> 3150 | )} 3151 | </div> 3152 | </div> 3153 | ); 3154 | }; 3155 | ``` 3156 | 3157 | ### Debouncing for Expensive Operations Pattern 3158 | 3159 | **Purpose**: Components need to optimize user input handling and expensive operations by reducing the frequency of execution through debouncing and throttling techniques. 3160 | 3161 | **Implementation Pattern**: 3162 | 3163 | ```typescript 3164 | export const OptimizedSearchComponent = ({ 3165 | onSearch, 3166 | onFilter, 3167 | onSort, 3168 | searchDelay = 300, 3169 | filterDelay = 500 3170 | }: Props) => { 3171 | const [searchQuery, setSearchQuery] = useState(""); 3172 | const [filters, setFilters] = useState<FilterState>({}); 3173 | const [sortConfig, setSortConfig] = useState<SortConfig>({}); 3174 | const [isSearching, setIsSearching] = useState(false); 3175 | 3176 | // Debounced search function 3177 | const debouncedSearch = useMemo( 3178 | () => debounce(async (query: string) => { 3179 | setIsSearching(true); 3180 | try { 3181 | await onSearch(query); 3182 | } finally { 3183 | setIsSearching(false); 3184 | } 3185 | }, searchDelay), 3186 | [onSearch, searchDelay] 3187 | ); 3188 | 3189 | // Debounced filter function with batching 3190 | const debouncedFilter = useMemo( 3191 | () => debounce((filterState: FilterState) => { 3192 | onFilter(filterState); 3193 | }, filterDelay), 3194 | [onFilter, filterDelay] 3195 | ); 3196 | 3197 | // Throttled sort function to prevent rapid sorting 3198 | const throttledSort = useMemo( 3199 | () => throttle((config: SortConfig) => { 3200 | onSort(config); 3201 | }, 200), 3202 | [onSort] 3203 | ); 3204 | 3205 | // Search input handler 3206 | const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { 3207 | const query = event.target.value; 3208 | setSearchQuery(query); 3209 | 3210 | // Cancel previous search and start new one 3211 | debouncedSearch.cancel(); 3212 | debouncedSearch(query); 3213 | }, [debouncedSearch]); 3214 | 3215 | // Filter change handler with batching 3216 | const handleFilterChange = useCallback((filterKey: string, value: any) => { 3217 | setFilters(prev => { 3218 | const newFilters = { ...prev, [filterKey]: value }; 3219 | 3220 | // Cancel previous filter operation 3221 | debouncedFilter.cancel(); 3222 | debouncedFilter(newFilters); 3223 | 3224 | return newFilters; 3225 | }); 3226 | }, [debouncedFilter]); 3227 | 3228 | // Sort change handler 3229 | const handleSortChange = useCallback((field: string, direction: 'asc' | 'desc') => { 3230 | const newConfig = { field, direction }; 3231 | setSortConfig(newConfig); 3232 | throttledSort(newConfig); 3233 | }, [throttledSort]); 3234 | 3235 | // Advanced debouncing with request cancellation 3236 | const advancedDebouncedSearch = useMemo(() => { 3237 | let currentController: AbortController | null = null; 3238 | 3239 | return debounce(async (query: string) => { 3240 | // Cancel previous request 3241 | if (currentController) { 3242 | currentController.abort(); 3243 | } 3244 | 3245 | // Create new abort controller 3246 | currentController = new AbortController(); 3247 | setIsSearching(true); 3248 | 3249 | try { 3250 | const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { 3251 | signal: currentController.signal 3252 | }); 3253 | 3254 | if (!results.ok) throw new Error('Search failed'); 3255 | 3256 | const data = await results.json(); 3257 | onSearch(data); 3258 | } catch (error) { 3259 | if (error.name !== 'AbortError') { 3260 | console.error('Search error:', error); 3261 | } 3262 | } finally { 3263 | if (currentController && !currentController.signal.aborted) { 3264 | setIsSearching(false); 3265 | } 3266 | currentController = null; 3267 | } 3268 | }, searchDelay); 3269 | }, [onSearch, searchDelay]); 3270 | 3271 | // Cleanup on unmount 3272 | useEffect(() => { 3273 | return () => { 3274 | debouncedSearch.cancel(); 3275 | debouncedFilter.cancel(); 3276 | throttledSort.cancel(); 3277 | advancedDebouncedSearch.cancel(); 3278 | }; 3279 | }, [debouncedSearch, debouncedFilter, throttledSort, advancedDebouncedSearch]); 3280 | 3281 | // Auto-save with debouncing 3282 | const [formData, setFormData] = useState({}); 3283 | const [lastSaved, setLastSaved] = useState<Date | null>(null); 3284 | 3285 | const debouncedAutoSave = useMemo( 3286 | () => debounce(async (data: any) => { 3287 | try { 3288 | await fetch('/api/autosave', { 3289 | method: 'POST', 3290 | headers: { 'Content-Type': 'application/json' }, 3291 | body: JSON.stringify(data) 3292 | }); 3293 | setLastSaved(new Date()); 3294 | } catch (error) { 3295 | console.error('Auto-save failed:', error); 3296 | } 3297 | }, 2000), 3298 | [] 3299 | ); 3300 | 3301 | const handleFormDataChange = useCallback((field: string, value: any) => { 3302 | setFormData(prev => { 3303 | const newData = { ...prev, [field]: value }; 3304 | debouncedAutoSave(newData); 3305 | return newData; 3306 | }); 3307 | }, [debouncedAutoSave]); 3308 | 3309 | // Resize handler with throttling 3310 | const [windowSize, setWindowSize] = useState({ 3311 | width: window.innerWidth, 3312 | height: window.innerHeight 3313 | }); 3314 | 3315 | const throttledResize = useMemo( 3316 | () => throttle(() => { 3317 | setWindowSize({ 3318 | width: window.innerWidth, 3319 | height: window.innerHeight 3320 | }); 3321 | }, 100), 3322 | [] 3323 | ); 3324 | 3325 | useEffect(() => { 3326 | window.addEventListener('resize', throttledResize); 3327 | return () => { 3328 | window.removeEventListener('resize', throttledResize); 3329 | throttledResize.cancel(); 3330 | }; 3331 | }, [throttledResize]); 3332 | 3333 | return ( 3334 | <div className="optimized-search-component"> 3335 | {/* Search input with debouncing */} 3336 | <div className="search-section"> 3337 | <input 3338 | type="text" 3339 | value={searchQuery} 3340 | onChange={handleSearchChange} 3341 | placeholder="Search..." 3342 | className={isSearching ? 'searching' : ''} 3343 | /> 3344 | {isSearching && <div className="search-spinner" />} 3345 | </div> 3346 | 3347 | {/* Filter controls with debounced updates */} 3348 | <div className="filter-section"> 3349 | <select 3350 | onChange={(e) => handleFilterChange('category', e.target.value)} 3351 | value={filters.category || ''} 3352 | > 3353 | <option value="">All Categories</option> 3354 | <option value="electronics">Electronics</option> 3355 | <option value="books">Books</option> 3356 | <option value="clothing">Clothing</option> 3357 | </select> 3358 | 3359 | <input 3360 | type="range" 3361 | min="0" 3362 | max="1000" 3363 | value={filters.maxPrice || 1000} 3364 | onChange={(e) => handleFilterChange('maxPrice', parseInt(e.target.value))} 3365 | /> 3366 | </div> 3367 | 3368 | {/* Sort controls with throttling */} 3369 | <div className="sort-section"> 3370 | <button onClick={() => handleSortChange('name', 'asc')}> 3371 | Sort by Name ↑ 3372 | </button> 3373 | <button onClick={() => handleSortChange('name', 'desc')}> 3374 | Sort by Name ↓ 3375 | </button> 3376 | <button onClick={() => handleSortChange('price', 'asc')}> 3377 | Sort by Price ↑ 3378 | </button> 3379 | <button onClick={() => handleSortChange('price', 'desc')}> 3380 | Sort by Price ↓ 3381 | </button> 3382 | </div> 3383 | 3384 | {/* Auto-save indicator */} 3385 | <div className="auto-save-section"> 3386 | {lastSaved && ( 3387 | <span>Last saved: {lastSaved.toLocaleTimeString()}</span> 3388 | )} 3389 | </div> 3390 | 3391 | {/* Window size display (responsive testing) */} 3392 | <div className="window-info"> 3393 | {windowSize.width} × {windowSize.height} 3394 | </div> 3395 | </div> 3396 | ); 3397 | }; 3398 | 3399 | // Utility functions 3400 | function debounce<T extends (...args: any[]) => any>( 3401 | func: T, 3402 | delay: number 3403 | ): T & { cancel: () => void } { 3404 | let timeoutId: NodeJS.Timeout; 3405 | 3406 | const debounced = (...args: Parameters<T>) => { 3407 | clearTimeout(timeoutId); 3408 | timeoutId = setTimeout(() => func(...args), delay); 3409 | }; 3410 | 3411 | debounced.cancel = () => { 3412 | clearTimeout(timeoutId); 3413 | }; 3414 | 3415 | return debounced as T & { cancel: () => void }; 3416 | } 3417 | 3418 | function throttle<T extends (...args: any[]) => any>( 3419 | func: T, 3420 | delay: number 3421 | ): T & { cancel: () => void } { 3422 | let inThrottle: boolean; 3423 | let timeoutId: NodeJS.Timeout; 3424 | 3425 | const throttled = (...args: Parameters<T>) => { 3426 | if (!inThrottle) { 3427 | func(...args); 3428 | inThrottle = true; 3429 | timeoutId = setTimeout(() => { 3430 | inThrottle = false; 3431 | }, delay); 3432 | } 3433 | }; 3434 | 3435 | throttled.cancel = () => { 3436 | clearTimeout(timeoutId); 3437 | inThrottle = false; 3438 | }; 3439 | 3440 | return throttled as T & { cancel: () => void }; 3441 | } 3442 | ``` 3443 | 3444 | **Key Benefits of Performance Patterns**: 3445 | - **Memoization**: Prevents expensive recalculations, reduces child re-renders, optimizes function stability, improves large dataset handling 3446 | - **Lazy Loading**: Reduces initial bundle size, improves perceived performance, loads content on-demand, supports code splitting 3447 | - **Debouncing**: Optimizes user input handling, reduces API calls, prevents excessive operations, includes request cancellation and auto-save features ```