@ably/ui 17.4.2 → 17.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +573 -0
- package/README.md +120 -68
- package/core/Accordion/types.js.map +1 -1
- package/core/Accordion/utils.js +1 -1
- package/core/Accordion/utils.js.map +1 -1
- package/core/Accordion.js +1 -1
- package/core/Accordion.js.map +1 -1
- package/core/Badge.js +1 -1
- package/core/Badge.js.map +1 -1
- package/core/Code/Code.test.js +2 -0
- package/core/Code/Code.test.js.map +1 -0
- package/core/Code.js +1 -1
- package/core/Code.js.map +1 -1
- package/core/CodeSnippet/ApiKeySelector.js +1 -1
- package/core/CodeSnippet/ApiKeySelector.js.map +1 -1
- package/core/CodeSnippet/CodeSnippet.test.js +2 -0
- package/core/CodeSnippet/CodeSnippet.test.js.map +1 -0
- package/core/CodeSnippet/CopyButton.js +1 -1
- package/core/CodeSnippet/CopyButton.js.map +1 -1
- package/core/CodeSnippet/LanguageSelector.js +1 -1
- package/core/CodeSnippet/LanguageSelector.js.map +1 -1
- package/core/CodeSnippet/PlainCodeView.js +1 -1
- package/core/CodeSnippet/PlainCodeView.js.map +1 -1
- package/core/CodeSnippet/TooltipButton.js +1 -1
- package/core/CodeSnippet/TooltipButton.js.map +1 -1
- package/core/CodeSnippet/languages.js +1 -1
- package/core/CodeSnippet/languages.js.map +1 -1
- package/core/CodeSnippet/languages.test.js +2 -0
- package/core/CodeSnippet/languages.test.js.map +1 -0
- package/core/CodeSnippet.js +1 -1
- package/core/CodeSnippet.js.map +1 -1
- package/core/ConnectStateWrapper.js.map +1 -1
- package/core/ContentTile.js +2 -0
- package/core/ContentTile.js.map +1 -0
- package/core/DropdownMenu.js +1 -1
- package/core/DropdownMenu.js.map +1 -1
- package/core/Expander.js +1 -1
- package/core/Expander.js.map +1 -1
- package/core/FeaturedLink.js +1 -1
- package/core/FeaturedLink.js.map +1 -1
- package/core/Flash.js +1 -1
- package/core/Flash.js.map +1 -1
- package/core/Flyout.js +1 -1
- package/core/Flyout.js.map +1 -1
- package/core/Footer/data.js +1 -1
- package/core/Footer/data.js.map +1 -1
- package/core/Footer.js +1 -1
- package/core/Footer.js.map +1 -1
- package/core/Header/HeaderLinks.js +1 -1
- package/core/Header/HeaderLinks.js.map +1 -1
- package/core/Header/types.js +2 -0
- package/core/Header/types.js.map +1 -0
- package/core/Header.js +1 -1
- package/core/Header.js.map +1 -1
- package/core/Icon/components/icon-display-cloud-servers-mono.js +2 -0
- package/core/Icon/components/icon-display-cloud-servers-mono.js.map +1 -0
- package/core/Icon/components/icon-display-data-integrity.js +2 -0
- package/core/Icon/components/icon-display-data-integrity.js.map +1 -0
- package/core/Icon/components/icon-display-database-connector.js +2 -0
- package/core/Icon/components/icon-display-database-connector.js.map +1 -0
- package/core/Icon/components/icon-display-ephemeral-messages-dark-col.js +2 -0
- package/core/Icon/components/icon-display-ephemeral-messages-dark-col.js.map +1 -0
- package/core/Icon/components/icon-display-ephemeral-messages.js +2 -0
- package/core/Icon/components/icon-display-ephemeral-messages.js.map +1 -0
- package/core/Icon/components/icon-display-live-updates.js +2 -0
- package/core/Icon/components/icon-display-live-updates.js.map +1 -0
- package/core/Icon/components/icon-display-message-annotations-dark-col.js +2 -0
- package/core/Icon/components/icon-display-message-annotations-dark-col.js.map +1 -0
- package/core/Icon/components/icon-display-message-annotations.js +2 -0
- package/core/Icon/components/icon-display-message-annotations.js.map +1 -0
- package/core/Icon/components/icon-display-multi-user-spaces.js +2 -0
- package/core/Icon/components/icon-display-multi-user-spaces.js.map +1 -0
- package/core/Icon/components/icon-display-sdks.js +2 -0
- package/core/Icon/components/icon-display-sdks.js.map +1 -0
- package/core/Icon/components/icon-display-something-else-mono.js +2 -0
- package/core/Icon/components/icon-display-something-else-mono.js.map +1 -0
- package/core/Icon/components/icon-display-something-else.js +2 -0
- package/core/Icon/components/icon-display-something-else.js.map +1 -0
- package/core/Icon/components/icon-display-ui-mono.js +2 -0
- package/core/Icon/components/icon-display-ui-mono.js.map +1 -0
- package/core/Icon/components/icon-display-ui.js +2 -0
- package/core/Icon/components/icon-display-ui.js.map +1 -0
- package/core/Icon/components/icon-gui-checklist-checked.js +1 -1
- package/core/Icon/components/icon-gui-checklist-checked.js.map +1 -1
- package/core/Icon/components/icon-gui-code-doc.js +1 -1
- package/core/Icon/components/icon-gui-code-doc.js.map +1 -1
- package/core/Icon/components/icon-gui-cursor.js +1 -1
- package/core/Icon/components/icon-gui-cursor.js.map +1 -1
- package/core/Icon/components/icon-gui-expand.js +1 -1
- package/core/Icon/components/icon-gui-expand.js.map +1 -1
- package/core/Icon/components/icon-gui-filter-flow-step-0.js +1 -1
- package/core/Icon/components/icon-gui-filter-flow-step-0.js.map +1 -1
- package/core/Icon/components/icon-gui-flower-growth.js +1 -1
- package/core/Icon/components/icon-gui-flower-growth.js.map +1 -1
- package/core/Icon/components/icon-gui-glasses.js +1 -1
- package/core/Icon/components/icon-gui-glasses.js.map +1 -1
- package/core/Icon/components/icon-gui-heartbeat-outline.js +2 -0
- package/core/Icon/components/icon-gui-heartbeat-outline.js.map +1 -0
- package/core/Icon/components/icon-gui-heartbeat-solid.js +2 -0
- package/core/Icon/components/icon-gui-heartbeat-solid.js.map +1 -0
- package/core/Icon/components/icon-gui-mouse.js +1 -1
- package/core/Icon/components/icon-gui-mouse.js.map +1 -1
- package/core/Icon/components/icon-gui-pitfall.js +1 -1
- package/core/Icon/components/icon-gui-pitfall.js.map +1 -1
- package/core/Icon/components/icon-gui-prod-ai-transport-outline.js +2 -0
- package/core/Icon/components/icon-gui-prod-ai-transport-outline.js.map +1 -0
- package/core/Icon/components/icon-gui-prod-ai-transport-solid.js +2 -0
- package/core/Icon/components/icon-gui-prod-ai-transport-solid.js.map +1 -0
- package/core/Icon/components/icon-gui-quote-marks-fill.js +1 -1
- package/core/Icon/components/icon-gui-quote-marks-fill.js.map +1 -1
- package/core/Icon/components/icon-product-ai-transport-mono.js +2 -0
- package/core/Icon/components/icon-product-ai-transport-mono.js.map +1 -0
- package/core/Icon/components/icon-product-ai-transport.js +2 -0
- package/core/Icon/components/icon-product-ai-transport.js.map +1 -0
- package/core/Icon/components/icon-product-chat-mono.js +1 -1
- package/core/Icon/components/icon-product-chat-mono.js.map +1 -1
- package/core/Icon/components/icon-product-liveobjects-mono.js +1 -1
- package/core/Icon/components/icon-product-liveobjects-mono.js.map +1 -1
- package/core/Icon/components/icon-product-livesync-mono.js +1 -1
- package/core/Icon/components/icon-product-livesync-mono.js.map +1 -1
- package/core/Icon/components/icon-product-pubsub-mono.js +1 -1
- package/core/Icon/components/icon-product-pubsub-mono.js.map +1 -1
- package/core/Icon/components/icon-product-spaces-mono.js +1 -1
- package/core/Icon/components/icon-product-spaces-mono.js.map +1 -1
- package/core/Icon/components/icon-tech-claude-mono.js +2 -0
- package/core/Icon/components/icon-tech-claude-mono.js.map +1 -0
- package/core/Icon/components/icon-tech-claude.js +2 -0
- package/core/Icon/components/icon-tech-claude.js.map +1 -0
- package/core/Icon/components/icon-tech-jetpack.js +2 -0
- package/core/Icon/components/icon-tech-jetpack.js.map +1 -0
- package/core/Icon/components/icon-tech-terraform-outline.js +2 -0
- package/core/Icon/components/icon-tech-terraform-outline.js.map +1 -0
- package/core/Icon/components/index.js +1 -1
- package/core/Icon/components/index.js.map +1 -1
- package/core/Icon/computed-icons/display-icons.js +1 -1
- package/core/Icon/computed-icons/display-icons.js.map +1 -1
- package/core/Icon/computed-icons/gui-icons.js +1 -1
- package/core/Icon/computed-icons/gui-icons.js.map +1 -1
- package/core/Icon/computed-icons/product-icons.js +1 -1
- package/core/Icon/computed-icons/product-icons.js.map +1 -1
- package/core/Icon/computed-icons/tech-icons.js +1 -1
- package/core/Icon/computed-icons/tech-icons.js.map +1 -1
- package/core/Icon.js +1 -1
- package/core/Icon.js.map +1 -1
- package/core/LinkButton.js +1 -1
- package/core/LinkButton.js.map +1 -1
- package/core/Logo.js +1 -1
- package/core/Logo.js.map +1 -1
- package/core/Meganav/MeganavBlog.js +2 -0
- package/core/Meganav/MeganavBlog.js.map +1 -0
- package/core/Meganav/MeganavCustomerStories.js +2 -0
- package/core/Meganav/MeganavCustomerStories.js.map +1 -0
- package/core/Meganav/MeganavMobile.js +1 -1
- package/core/Meganav/MeganavMobile.js.map +1 -1
- package/core/Meganav/MeganavPanel.js +1 -1
- package/core/Meganav/MeganavPanel.js.map +1 -1
- package/core/Meganav/MeganavPanelItemLinks.js +2 -0
- package/core/Meganav/MeganavPanelItemLinks.js.map +1 -0
- package/core/Meganav/MeganavTile.js +2 -0
- package/core/Meganav/MeganavTile.js.map +1 -0
- package/core/Meganav/PanelTitle.js +2 -0
- package/core/Meganav/PanelTitle.js.map +1 -0
- package/core/Meganav/data.js +1 -1
- package/core/Meganav/data.js.map +1 -1
- package/core/Meganav/images/cust-logo-doxy-dark.png +0 -0
- package/core/Meganav/images/cust-logo-doxy-light.png +0 -0
- package/core/Meganav/utils/getMenuItemsForHeader.js +2 -0
- package/core/Meganav/utils/getMenuItemsForHeader.js.map +1 -0
- package/core/Meganav.js +1 -1
- package/core/Meganav.js.map +1 -1
- package/core/Notice/component.css +9 -3
- package/core/Notice/component.js +1 -1
- package/core/Notice/component.js.map +1 -1
- package/core/Notice.js +1 -1
- package/core/Notice.js.map +1 -1
- package/core/Pricing/PricingCards.js +1 -1
- package/core/Pricing/PricingCards.js.map +1 -1
- package/core/Pricing/data.js +1 -1
- package/core/Pricing/data.js.map +1 -1
- package/core/Pricing/types.js.map +1 -1
- package/core/ProductTile/ProductDescription.js +1 -1
- package/core/ProductTile/ProductDescription.js.map +1 -1
- package/core/ProductTile/ProductIcon.js +1 -1
- package/core/ProductTile/ProductIcon.js.map +1 -1
- package/core/ProductTile/ProductLabel.js +1 -1
- package/core/ProductTile/ProductLabel.js.map +1 -1
- package/core/ProductTile/data.js +1 -1
- package/core/ProductTile/data.js.map +1 -1
- package/core/ProductTile.js +1 -1
- package/core/ProductTile.js.map +1 -1
- package/core/SegmentedControl.js +1 -1
- package/core/SegmentedControl.js.map +1 -1
- package/core/Slider/component.js +1 -1
- package/core/Slider/component.js.map +1 -1
- package/core/Slider.js +1 -1
- package/core/Slider.js.map +1 -1
- package/core/TabMenu.js +1 -1
- package/core/TabMenu.js.map +1 -1
- package/core/Table/data.js +1 -1
- package/core/Table/data.js.map +1 -1
- package/core/Toggle.js +1 -1
- package/core/Toggle.js.map +1 -1
- package/core/Tooltip.js +1 -1
- package/core/Tooltip.js.map +1 -1
- package/core/fonts/NEXT-Book-Light-Italic.eot +0 -0
- package/core/fonts/NEXT-Book-Light-Italic.otf +0 -0
- package/core/fonts/NEXT-Book-Light-Italic.woff +0 -0
- package/core/fonts/NEXT-Book-Light-Italic.woff2 +0 -0
- package/core/fonts/NEXT-Book-Light.eot +0 -0
- package/core/fonts/NEXT-Book-Light.otf +0 -0
- package/core/fonts/NEXT-Book-Light.woff +0 -0
- package/core/fonts/NEXT-Book-Light.woff2 +0 -0
- package/core/fonts/NEXT-Book-Medium-Italic.eot +0 -0
- package/core/fonts/NEXT-Book-Medium-Italic.otf +0 -0
- package/core/fonts/NEXT-Book-Medium-Italic.woff +0 -0
- package/core/fonts/NEXT-Book-Medium-Italic.woff2 +0 -0
- package/core/fonts/NEXT-Book-Medium.eot +0 -0
- package/core/fonts/NEXT-Book-Medium.otf +0 -0
- package/core/fonts/NEXT-Book-Medium.woff +0 -0
- package/core/fonts/NEXT-Book-Medium.woff2 +0 -0
- package/core/hooks/use-content-height.js +2 -0
- package/core/hooks/use-content-height.js.map +1 -0
- package/core/hooks/use-themed-scrollpoints.js +2 -0
- package/core/hooks/use-themed-scrollpoints.js.map +1 -0
- package/core/hooks/use-themed-scrollpoints.test.js +2 -0
- package/core/hooks/use-themed-scrollpoints.test.js.map +1 -0
- package/core/icons/display/icon-display-cloud-servers-mono.svg +3 -0
- package/core/icons/display/icon-display-data-integrity.svg +9 -0
- package/core/icons/display/icon-display-database-connector.svg +13 -0
- package/core/icons/display/icon-display-ephemeral-messages-dark-col.svg +6 -0
- package/core/icons/display/icon-display-ephemeral-messages.svg +6 -0
- package/core/icons/display/icon-display-live-updates.svg +8 -0
- package/core/icons/display/icon-display-message-annotations-dark-col.svg +11 -0
- package/core/icons/display/icon-display-message-annotations.svg +11 -0
- package/core/icons/display/icon-display-multi-user-spaces.svg +13 -0
- package/core/icons/display/icon-display-sdks.svg +11 -0
- package/core/icons/display/icon-display-something-else-mono.svg +4 -0
- package/core/icons/display/icon-display-something-else.svg +4 -0
- package/core/icons/display/icon-display-ui-mono.svg +22 -0
- package/core/icons/display/icon-display-ui.svg +22 -0
- package/core/icons/gui/icon-gui-checklist-checked.svg +1 -1
- package/core/icons/gui/icon-gui-code-doc.svg +1 -1
- package/core/icons/gui/icon-gui-cursor.svg +1 -1
- package/core/icons/gui/icon-gui-expand.svg +1 -1
- package/core/icons/gui/icon-gui-filter-flow-step-0.svg +3 -3
- package/core/icons/gui/icon-gui-flower-growth.svg +1 -1
- package/core/icons/gui/icon-gui-glasses.svg +1 -1
- package/core/icons/gui/icon-gui-heartbeat-outline.svg +4 -0
- package/core/icons/gui/icon-gui-heartbeat-solid.svg +4 -0
- package/core/icons/gui/icon-gui-mouse.svg +1 -1
- package/core/icons/gui/icon-gui-pitfall.svg +1 -1
- package/core/icons/gui/icon-gui-prod-ai-transport-outline.svg +5 -0
- package/core/icons/gui/icon-gui-prod-ai-transport-solid.svg +5 -0
- package/core/icons/gui/icon-gui-quote-marks-fill.svg +2 -2
- package/core/icons/product/icon-product-ai-transport-mono.svg +5 -0
- package/core/icons/product/icon-product-ai-transport.svg +12 -0
- package/core/icons/product/icon-product-chat-mono.svg +1 -1
- package/core/icons/product/icon-product-liveobjects-mono.svg +1 -4
- package/core/icons/product/icon-product-livesync-mono.svg +4 -4
- package/core/icons/product/icon-product-pubsub-mono.svg +1 -1
- package/core/icons/product/icon-product-spaces-mono.svg +1 -1
- package/core/icons/tech/icon-tech-claude-mono.svg +5 -0
- package/core/icons/tech/icon-tech-claude.svg +3 -0
- package/core/icons/tech/icon-tech-jetpack.svg +1 -0
- package/core/icons/tech/icon-tech-terraform-outline.svg +5 -0
- package/core/images/badges/g2-best-meets-requirements-spring-2025.svg +26 -0
- package/core/images/badges/g2-best-results-spring-2025.svg +26 -0
- package/core/images/badges/g2-best-support-spring-2025.svg +26 -0
- package/core/images/badges/g2-easiest-to-use-spring-2025.svg +26 -0
- package/core/images/badges/g2-users-most-likely-to-recommend-spring-2025.svg +26 -0
- package/core/images/cust-logo-mentimeter-mono-pos.svg +0 -0
- package/core/insights/command-queue.js +1 -1
- package/core/insights/command-queue.js.map +1 -1
- package/core/insights/datalayer.js +1 -1
- package/core/insights/datalayer.js.map +1 -1
- package/core/insights/index.js +1 -1
- package/core/insights/index.js.map +1 -1
- package/core/insights/index.test.js +1 -1
- package/core/insights/index.test.js.map +1 -1
- package/core/insights/mixpanel.js +1 -1
- package/core/insights/mixpanel.js.map +1 -1
- package/core/insights/mixpanel.test.js +2 -0
- package/core/insights/mixpanel.test.js.map +1 -0
- package/core/insights/posthog.js +1 -1
- package/core/insights/posthog.js.map +1 -1
- package/core/insights/posthog.test.js +2 -0
- package/core/insights/posthog.test.js.map +1 -0
- package/core/insights/service.js +1 -1
- package/core/insights/service.js.map +1 -1
- package/core/insights/types.js.map +1 -1
- package/core/react-renderer.js.map +1 -1
- package/core/sprites-display.svg +1 -1
- package/core/sprites-gui.svg +1 -1
- package/core/sprites-product.svg +1 -1
- package/core/sprites-tech.svg +1 -1
- package/core/styles/buttons.css +6 -6
- package/core/styles/colors/types.js +1 -1
- package/core/styles/colors/types.js.map +1 -1
- package/core/styles/forms.css +5 -5
- package/core/styles/legacy-buttons.css +8 -8
- package/core/styles/properties.css +4 -4
- package/core/styles/text.css +2 -2
- package/core/styles.components.css +4 -4
- package/core/utils/syntax-highlighter.css +31 -0
- package/core/utils/syntax-highlighter.js +1 -1
- package/core/utils/syntax-highlighter.js.map +1 -1
- package/core/utils/syntax-highlighter.test.js +2 -0
- package/core/utils/syntax-highlighter.test.js.map +1 -0
- package/index.d.ts +1201 -118
- package/package.json +66 -59
- package/tailwind.config.js +2 -2
- package/core/CookieMessage/component.css +0 -15
- package/core/CookieMessage.js +0 -2
- package/core/CookieMessage.js.map +0 -1
- package/core/Icon/components/icon-display-asset-tracking-col.js +0 -2
- package/core/Icon/components/icon-display-asset-tracking-col.js.map +0 -1
- package/core/Icon/components/icon-gui-prod-asset-tracking-outline.js +0 -2
- package/core/Icon/components/icon-gui-prod-asset-tracking-outline.js.map +0 -1
- package/core/Icon/components/icon-gui-prod-asset-tracking-solid.js +0 -2
- package/core/Icon/components/icon-gui-prod-asset-tracking-solid.js.map +0 -1
- package/core/Icon/components/icon-product-asset-tracking-mono.js +0 -2
- package/core/Icon/components/icon-product-asset-tracking-mono.js.map +0 -1
- package/core/Icon/components/icon-product-asset-tracking.js +0 -2
- package/core/Icon/components/icon-product-asset-tracking.js.map +0 -1
- package/core/Meganav/MeganavProductTile.js +0 -2
- package/core/Meganav/MeganavProductTile.js.map +0 -1
- package/core/Meganav/images/fan-engagement-nav-image.png +0 -0
- package/core/Meganav/images/founders-nav-image.png +0 -0
- package/core/icons/display/icon-display-asset-tracking-col.svg +0 -18
- package/core/icons/gui/icon-gui-prod-asset-tracking-outline.svg +0 -3
- package/core/icons/gui/icon-gui-prod-asset-tracking-solid.svg +0 -3
- package/core/icons/product/icon-product-asset-tracking-mono.svg +0 -3
- package/core/icons/product/icon-product-asset-tracking.svg +0 -12
- package/core/images/g2-best-meets-requirements-2025.svg +0 -10
- package/core/images/g2-best-support-2025.svg +0 -10
- package/core/images/g2-high-performer-2025.svg +0 -9
- package/core/images/g2-users-most-likely-to-recommend-2025.svg +0 -10
- package/core/utils/useCopyToClipboard.js +0 -2
- package/core/utils/useCopyToClipboard.js.map +0 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# Agent Development Guide
|
|
2
|
+
|
|
3
|
+
This file is not necessarily for people, the intended audience is automated agents.
|
|
4
|
+
|
|
5
|
+
> This file contains @ably/ui package-specific guidance. For universal code style and git workflow, see the [root AGENTS.md](../../AGENTS.md).
|
|
6
|
+
|
|
7
|
+
## Other references
|
|
8
|
+
|
|
9
|
+
Consider the content of `README.md` as well, it contains technical information
|
|
10
|
+
used by contributors to this project, as well as consumers of this project
|
|
11
|
+
|
|
12
|
+
## Consumers
|
|
13
|
+
|
|
14
|
+
This project is intended to primarily be consumed by the Ably website, voltaire
|
|
15
|
+
& docs projects. It is distributed via NPM as the `@ably/ui` package.
|
|
16
|
+
|
|
17
|
+
## Build & Test Commands
|
|
18
|
+
|
|
19
|
+
- `pnpm build` - Build the library (prebuild, icons, swc, tsc, cleanup)
|
|
20
|
+
- `pnpm test` - Run all tests with Vitest
|
|
21
|
+
- `pnpm test -- src/core/insights/index.test.ts` - Run a single test file
|
|
22
|
+
- `pnpm lint` - Run ESLint on all files
|
|
23
|
+
- `pnpm format:check` - Check formatting with Prettier
|
|
24
|
+
- `pnpm format:write` - Auto-format all files with Prettier
|
|
25
|
+
- `pnpm storybook` - Start Storybook dev server on port 6006
|
|
26
|
+
- `pnpm start` - Start Vite dev server on port 5000
|
|
27
|
+
|
|
28
|
+
## Code Style
|
|
29
|
+
|
|
30
|
+
- **React**: Use functional components with hooks; React 18.x
|
|
31
|
+
- **Imports**: Default export for main component, named exports for types/utils
|
|
32
|
+
- **Naming**: PascalCase for components/types, camelCase for functions/variables,
|
|
33
|
+
kebab-case for files
|
|
34
|
+
- **Types**: Define prop types as `ComponentNameProps`, use `PropsWithChildren<T>`
|
|
35
|
+
when needed
|
|
36
|
+
- **Utility**: Use `cn()` from `./src/core/utils/cn` for className merging (clsx
|
|
37
|
+
& tailwind-merge)
|
|
38
|
+
- **Formatting**: Prettier defaults (no config = defaults), 2-space indent
|
|
39
|
+
- **Error Handling**: Wrap external service calls in try-catch, log with logger module
|
|
40
|
+
- **Comments**: JSDoc for props, inline comments for complex logic
|
|
41
|
+
|
|
42
|
+
### Custom Hooks
|
|
43
|
+
|
|
44
|
+
- **Naming**: `use` prefix with descriptive name (e.g., `useContentHeight`, `useThemedScrollpoints`)
|
|
45
|
+
- **JSDoc**: Always include for custom hooks, especially performance-related ones
|
|
46
|
+
- **Parameters**: Document with `@param` including types and defaults
|
|
47
|
+
- **Returns**: Document with `@returns` including type and semantic meaning
|
|
48
|
+
- **Performance rationale**: Include "why" in JSDoc when optimizing (e.g., "eliminates forced reflows")
|
|
49
|
+
- **Cleanup**: Always return cleanup function to prevent memory leaks
|
|
50
|
+
- **Shared constants**: Import from `src/core/utils/heights.ts` instead of duplicating
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
/**
|
|
56
|
+
* Tracks element height using ResizeObserver to avoid forced reflows.
|
|
57
|
+
*
|
|
58
|
+
* @param ref - React ref to the element to observe
|
|
59
|
+
* @param initialHeight - Initial height value (default: 0)
|
|
60
|
+
* @returns Current height in pixels
|
|
61
|
+
*/
|
|
62
|
+
export function useContentHeight(
|
|
63
|
+
ref: RefObject<HTMLElement>,
|
|
64
|
+
initialHeight = 0,
|
|
65
|
+
): number {
|
|
66
|
+
// Implementation...
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
- Run `pnpm lint` & `pnpm format:write` on files after making changes, we lint
|
|
73
|
+
files in CI and don't want preventable failures. `pnpm lint:fix` should also
|
|
74
|
+
apply our formatting rules while trying to fix most things for you
|
|
75
|
+
- Run tests with `pnpm test` after making file changes
|
|
76
|
+
- When testing with Storybook, use Chrome DevTools Performance tab to verify no forced reflows
|
|
77
|
+
- For performance-related changes, compare before/after metrics and include in commit/PR
|
|
78
|
+
|
|
79
|
+
### Chrome DevTools MCP Tools (Optional)
|
|
80
|
+
|
|
81
|
+
If you have MCP support (Claude Code detects `.mcp.json` automatically), use these tools for automated performance profiling and visual verification during component development:
|
|
82
|
+
|
|
83
|
+
**Primary use case:** Performance profiling during component development to detect forced reflows and layout thrashing.
|
|
84
|
+
|
|
85
|
+
**Development server URLs:**
|
|
86
|
+
- `http://localhost:6006` - Storybook (primary development environment for @ably/ui)
|
|
87
|
+
- `http://localhost:4000` - Voltaire docs site (for testing components in production-like context)
|
|
88
|
+
|
|
89
|
+
**Common workflows:**
|
|
90
|
+
|
|
91
|
+
1. **Automated forced reflow detection:**
|
|
92
|
+
- Start Storybook: `pnpm storybook`
|
|
93
|
+
- Use `mcp__chrome-devtools__performance_start_trace` with `reload: true, autoStop: true`
|
|
94
|
+
- Navigate to component story at `http://localhost:6006`
|
|
95
|
+
- Tool automatically stops trace after page load
|
|
96
|
+
- Review trace for forced reflow warnings (see Performance Optimization Guidelines above)
|
|
97
|
+
- If forced reflows found, refactor to use ResizeObserver/IntersectionObserver patterns
|
|
98
|
+
|
|
99
|
+
2. **Visual verification across light/dark themes:**
|
|
100
|
+
- Navigate to story: `mcp__chrome-devtools__navigate_page` with `url: "http://localhost:6006/..."`
|
|
101
|
+
- Take snapshot: `mcp__chrome-devtools__take_snapshot`
|
|
102
|
+
- Verify interactive states render correctly in both themes
|
|
103
|
+
- Check hover/active/focus states match styling guide patterns
|
|
104
|
+
|
|
105
|
+
3. **React warnings and console errors:**
|
|
106
|
+
- After making component changes, use `mcp__chrome-devtools__list_console_messages`
|
|
107
|
+
- Filter for errors/warnings to catch React lifecycle issues early
|
|
108
|
+
- Particularly useful for detecting missing cleanup in custom hooks
|
|
109
|
+
|
|
110
|
+
4. **Performance regression detection:**
|
|
111
|
+
- Record baseline trace before refactoring
|
|
112
|
+
- Make changes, record new trace
|
|
113
|
+
- Compare metrics (layout time, scripting time) to ensure improvements
|
|
114
|
+
- Include before/after metrics in commit/PR
|
|
115
|
+
|
|
116
|
+
**When to use:**
|
|
117
|
+
- After implementing custom hooks with ResizeObserver/IntersectionObserver
|
|
118
|
+
- When optimizing scroll/resize event handlers
|
|
119
|
+
- Before committing performance-related changes
|
|
120
|
+
- When testing components that depend on layout measurements
|
|
121
|
+
|
|
122
|
+
**Note:** Chrome DevTools MCP tools are optional. Manual Chrome DevTools Performance tab profiling works fine without them. These tools primarily automate workflows you'd otherwise do manually.
|
|
123
|
+
|
|
124
|
+
## Styling Guide
|
|
125
|
+
|
|
126
|
+
### Color Palettes
|
|
127
|
+
|
|
128
|
+
The design system uses semantic color palettes defined in `src/core/styles/properties.css`
|
|
129
|
+
and configured for Tailwind in `tailwind.config.js`. Each palette has a different
|
|
130
|
+
number of color values:
|
|
131
|
+
|
|
132
|
+
- **Neutral**: 000, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300 (14 values)
|
|
133
|
+
- **Orange**: 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100 (11 values)
|
|
134
|
+
- **Yellow**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
135
|
+
- **Green**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
136
|
+
- **Blue**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
137
|
+
- **Violet**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
138
|
+
- **Pink**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
139
|
+
|
|
140
|
+
### Interactive Element Styling Patterns
|
|
141
|
+
|
|
142
|
+
When developing components with @ably/ui, **always** use Tailwind classes following
|
|
143
|
+
these established patterns to ensure consistent interactive behavior across light
|
|
144
|
+
and dark modes:
|
|
145
|
+
|
|
146
|
+
#### Dark Mode Mirroring
|
|
147
|
+
|
|
148
|
+
For any given color, add a dark mode class that mirrors it across the palette.
|
|
149
|
+
Lower values (lighter colors) in light mode should map to higher values (darker
|
|
150
|
+
colors) in dark mode, and vice versa.
|
|
151
|
+
|
|
152
|
+
**Examples:**
|
|
153
|
+
|
|
154
|
+
- `bg-neutral-100` pairs with `dark:bg-neutral-1200`
|
|
155
|
+
- `bg-neutral-200` pairs with `dark:bg-neutral-1100`
|
|
156
|
+
- `bg-neutral-1200` pairs with `dark:bg-neutral-100`
|
|
157
|
+
- `text-neutral-1300` pairs with `dark:text-neutral-000`
|
|
158
|
+
- `bg-orange-200` pairs with `dark:bg-orange-900` (orange has 11 values: 200 + 900 = 1100)
|
|
159
|
+
- `bg-blue-300` pairs with `dark:bg-blue-700` (blue has 9 values: 300 + 700 = 1000)
|
|
160
|
+
|
|
161
|
+
The sum of mirrored color numbers should equal the total palette range. Different
|
|
162
|
+
palettes have different ranges, so calculate mirrors accordingly:
|
|
163
|
+
|
|
164
|
+
- Neutral (000-1300): `light + dark = 1300`
|
|
165
|
+
- Orange (100-1100): `light + dark = 1200`
|
|
166
|
+
- Secondary colors (100-900): `light + dark = 1000`
|
|
167
|
+
|
|
168
|
+
#### Hover States
|
|
169
|
+
|
|
170
|
+
Use the **next color value** along the palette for hover states:
|
|
171
|
+
|
|
172
|
+
- `bg-neutral-100` → `hover:bg-neutral-200`
|
|
173
|
+
- `bg-neutral-200` → `hover:bg-neutral-300`
|
|
174
|
+
- `bg-orange-600` → `hover:bg-orange-700`
|
|
175
|
+
|
|
176
|
+
Apply this pattern to both light and dark mode classes:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
bg-neutral-200 hover:bg-neutral-300
|
|
180
|
+
dark:bg-neutral-1100 dark:hover:bg-neutral-1000
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Active States
|
|
184
|
+
|
|
185
|
+
Use **two color values** along the palette for active/pressed states:
|
|
186
|
+
|
|
187
|
+
- `bg-neutral-100` → `active:bg-neutral-300`
|
|
188
|
+
- `bg-neutral-200` → `active:bg-neutral-400`
|
|
189
|
+
- `bg-orange-600` → `active:bg-orange-800`
|
|
190
|
+
|
|
191
|
+
Apply to both modes:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400
|
|
195
|
+
dark:bg-neutral-1100 dark:hover:bg-neutral-1000 dark:active:bg-neutral-900
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Focus Styles
|
|
199
|
+
|
|
200
|
+
Add the `focus-base` class to all interactive elements (buttons, links, inputs,
|
|
201
|
+
selects, etc.). This class is defined in `src/core/styles/utils.css` and provides
|
|
202
|
+
consistent focus styling with an accessible outline:
|
|
203
|
+
|
|
204
|
+
```css
|
|
205
|
+
.focus-base {
|
|
206
|
+
@apply focus:outline-none focus-visible:outline-4 focus-visible:outline-offset-0 focus-visible:outline-gui-focus;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Transitions
|
|
211
|
+
|
|
212
|
+
Add `transition-colors` to interactive elements unless a higher-specificity
|
|
213
|
+
`transition` class is already present (e.g., `transition-all`, `transition-transform`).
|
|
214
|
+
This ensures smooth visual feedback for state changes.
|
|
215
|
+
|
|
216
|
+
### Complete Example
|
|
217
|
+
|
|
218
|
+
Here's a complete button component demonstrating all patterns:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
<button
|
|
222
|
+
className={cn(
|
|
223
|
+
"px-4 py-2 rounded",
|
|
224
|
+
"bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400",
|
|
225
|
+
"dark:bg-neutral-1100 dark:hover:bg-neutral-1000 dark:active:bg-neutral-900",
|
|
226
|
+
"text-neutral-1300 dark:text-neutral-000",
|
|
227
|
+
"focus-base transition-colors",
|
|
228
|
+
)}
|
|
229
|
+
>
|
|
230
|
+
Click me
|
|
231
|
+
</button>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Additional Examples
|
|
235
|
+
|
|
236
|
+
**Select dropdown:**
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
<Select.Trigger
|
|
240
|
+
className="bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400 dark:bg-neutral-1100 dark:hover:bg-neutral-1000 dark:active:bg-neutral-900 focus-base transition-colors border border-neutral-300 dark:border-neutral-1000"
|
|
241
|
+
>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Badge with orange:**
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<span
|
|
248
|
+
className="bg-orange-200 hover:bg-orange-300 active:bg-orange-400 dark:bg-orange-900 dark:hover:bg-orange-800 dark:active:bg-orange-700 focus-base transition-colors"
|
|
249
|
+
>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Toggle/Switch:**
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
<Switch
|
|
256
|
+
className="bg-neutral-600 hover:bg-neutral-700 active:bg-neutral-800 data-[state=checked]:bg-orange-600 data-[state=checked]:hover:bg-orange-700 data-[state=checked]:active:bg-orange-800 focus-base transition-colors"
|
|
257
|
+
>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Performance Optimization Guidelines
|
|
261
|
+
|
|
262
|
+
### When to Optimize
|
|
263
|
+
|
|
264
|
+
Optimize when Chrome DevTools Performance profiling shows:
|
|
265
|
+
|
|
266
|
+
- Forced reflows/layouts in event handlers (scroll, resize, input)
|
|
267
|
+
- Long tasks blocking the main thread (>50ms)
|
|
268
|
+
- CPU throttling causing device overheating (especially iOS)
|
|
269
|
+
|
|
270
|
+
Common anti-patterns causing forced reflows:
|
|
271
|
+
|
|
272
|
+
- `getBoundingClientRect()` in scroll/resize handlers
|
|
273
|
+
- `clientHeight/scrollHeight/offsetHeight` reads during interactions
|
|
274
|
+
- Synchronous layout queries followed by style changes
|
|
275
|
+
- DOM queries inside throttled/debounced callbacks
|
|
276
|
+
|
|
277
|
+
### Observer API Patterns
|
|
278
|
+
|
|
279
|
+
#### IntersectionObserver (Scroll Position Detection)
|
|
280
|
+
|
|
281
|
+
Use for detecting when elements enter/exit viewport or cross specific boundaries.
|
|
282
|
+
|
|
283
|
+
**Example:** Header theme changes based on which section is visible
|
|
284
|
+
|
|
285
|
+
**Key patterns:**
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
289
|
+
const intersectingElementsRef = useRef<Map<string, IntersectionObserverEntry>>(new Map());
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
const intersectingElements = intersectingElementsRef.current;
|
|
293
|
+
|
|
294
|
+
observerRef.current = new IntersectionObserver(
|
|
295
|
+
(entries) => {
|
|
296
|
+
requestAnimationFrame(() => {
|
|
297
|
+
// Update tracking map
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
if (entry.isIntersecting) {
|
|
300
|
+
intersectingElements.set(entry.target.id, entry);
|
|
301
|
+
} else {
|
|
302
|
+
intersectingElements.delete(entry.target.id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Find best match from ALL intersecting elements
|
|
307
|
+
// (observer only reports changes, not all intersecting)
|
|
308
|
+
let bestMatch = null;
|
|
309
|
+
for (const [id, entry] of intersectingElements) {
|
|
310
|
+
const rect = entry.boundingClientRect ?? entry.target.getBoundingClientRect();
|
|
311
|
+
// Calculate match quality...
|
|
312
|
+
if (isBetterMatch) bestMatch = {...};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Only update state if changed
|
|
316
|
+
if (bestMatch && bestMatch.value !== previousValueRef.current) {
|
|
317
|
+
previousValueRef.current = bestMatch.value;
|
|
318
|
+
setState(bestMatch.value);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
rootMargin: "-64px 0px 0px 0px", // Adjust for fixed header
|
|
324
|
+
threshold: 0,
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Observe elements
|
|
329
|
+
elements.forEach(el => observerRef.current?.observe(el));
|
|
330
|
+
|
|
331
|
+
// CRITICAL: Manual initial state check
|
|
332
|
+
// IntersectionObserver callbacks only fire on CHANGES, not initial observation
|
|
333
|
+
const timeoutId = setTimeout(() => {
|
|
334
|
+
// Check which elements currently intersect
|
|
335
|
+
// Set initial state
|
|
336
|
+
}, 0);
|
|
337
|
+
|
|
338
|
+
return () => {
|
|
339
|
+
clearTimeout(timeoutId);
|
|
340
|
+
observerRef.current?.disconnect();
|
|
341
|
+
observerRef.current = null;
|
|
342
|
+
intersectingElements.clear();
|
|
343
|
+
};
|
|
344
|
+
}, [deps]);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Critical points:**
|
|
348
|
+
|
|
349
|
+
- Observer only reports state CHANGES, not all intersecting elements
|
|
350
|
+
- Use Map to track currently intersecting elements
|
|
351
|
+
- Manual initial check with `setTimeout(..., 0)` required
|
|
352
|
+
- Batch updates with `requestAnimationFrame()`
|
|
353
|
+
- Track previous value to skip redundant setState
|
|
354
|
+
- **Tiebreaker logic**: When multiple elements have equal distances, use array order (earlier in array wins)
|
|
355
|
+
- Clean up timeout, observer, and Map
|
|
356
|
+
|
|
357
|
+
**Tiebreaker pattern:**
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// When distances are equal, use scrollpoints array order
|
|
361
|
+
if (
|
|
362
|
+
!bestMatch ||
|
|
363
|
+
distance < bestMatch.distance ||
|
|
364
|
+
(distance === bestMatch.distance && scrollpointIndex < bestMatch.index)
|
|
365
|
+
) {
|
|
366
|
+
bestMatch = { scrollpoint, distance, index: scrollpointIndex };
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Why this matters:** In Voltaire, both `meganav` (transparent) and `main-theme-dark` (with border) start at position 0, giving identical distances. Without a tiebreaker, the header unpredictably showed the border. Array order ensures `meganav` (listed first) always wins.
|
|
371
|
+
|
|
372
|
+
#### ResizeObserver (Height/Size Tracking)
|
|
373
|
+
|
|
374
|
+
Use for tracking element dimensions without synchronous layout reads.
|
|
375
|
+
|
|
376
|
+
**Example:** Expander content height for expand/collapse animations
|
|
377
|
+
|
|
378
|
+
**Key patterns:**
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const rafIdRef = useRef<number | null>(null);
|
|
382
|
+
const observerRef = useRef<ResizeObserver | null>(null);
|
|
383
|
+
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
let isMounted = true;
|
|
386
|
+
|
|
387
|
+
observerRef.current = new ResizeObserver((entries) => {
|
|
388
|
+
// Cancel any pending RAF to avoid stale updates
|
|
389
|
+
if (rafIdRef.current !== null) {
|
|
390
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
rafIdRef.current = requestAnimationFrame(() => {
|
|
394
|
+
rafIdRef.current = null;
|
|
395
|
+
|
|
396
|
+
// Guard against updates after unmount
|
|
397
|
+
if (!isMounted) return;
|
|
398
|
+
|
|
399
|
+
const entry = entries[0];
|
|
400
|
+
if (entry && entry.contentRect) {
|
|
401
|
+
const newHeight = Math.round(entry.contentRect.height);
|
|
402
|
+
setState(newHeight);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
observerRef.current.observe(element);
|
|
408
|
+
|
|
409
|
+
return () => {
|
|
410
|
+
isMounted = false;
|
|
411
|
+
// Cancel pending RAF to prevent setState after unmount
|
|
412
|
+
if (rafIdRef.current !== null) {
|
|
413
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
414
|
+
rafIdRef.current = null;
|
|
415
|
+
}
|
|
416
|
+
observerRef.current?.disconnect();
|
|
417
|
+
observerRef.current = null;
|
|
418
|
+
};
|
|
419
|
+
}, [ref]);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Critical points:**
|
|
423
|
+
|
|
424
|
+
- Always capture RAF ID and cancel on cleanup
|
|
425
|
+
- Use `isMounted` flag to guard setState calls
|
|
426
|
+
- Cancel pending RAF before scheduling new one
|
|
427
|
+
- ResizeObserver doesn't need initial check (fires immediately on observe)
|
|
428
|
+
- Round numeric values for consistency
|
|
429
|
+
|
|
430
|
+
### Testing Async Hooks
|
|
431
|
+
|
|
432
|
+
#### Setup Pattern
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
describe("useMyHook", () => {
|
|
436
|
+
let originalIntersectionObserver: typeof IntersectionObserver;
|
|
437
|
+
let originalRequestAnimationFrame: typeof requestAnimationFrame;
|
|
438
|
+
|
|
439
|
+
beforeEach(() => {
|
|
440
|
+
vi.useFakeTimers();
|
|
441
|
+
|
|
442
|
+
// CRITICAL: Save originals BEFORE mocking
|
|
443
|
+
originalIntersectionObserver = global.IntersectionObserver;
|
|
444
|
+
originalRequestAnimationFrame = global.requestAnimationFrame;
|
|
445
|
+
|
|
446
|
+
// Mock global APIs
|
|
447
|
+
global.IntersectionObserver = vi.fn((callback) => ({
|
|
448
|
+
observe: vi.fn(),
|
|
449
|
+
disconnect: vi.fn(),
|
|
450
|
+
})) as unknown as typeof IntersectionObserver;
|
|
451
|
+
|
|
452
|
+
global.requestAnimationFrame = vi.fn((cb) => {
|
|
453
|
+
cb(0);
|
|
454
|
+
return 0;
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
afterEach(() => {
|
|
459
|
+
vi.clearAllMocks();
|
|
460
|
+
vi.useRealTimers();
|
|
461
|
+
document.body.innerHTML = "";
|
|
462
|
+
|
|
463
|
+
// CRITICAL: Restore originals to prevent test pollution
|
|
464
|
+
global.IntersectionObserver = originalIntersectionObserver;
|
|
465
|
+
global.requestAnimationFrame = originalRequestAnimationFrame;
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
#### Testing Observer Callbacks
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
it("updates state when observer fires", () => {
|
|
474
|
+
const elem = document.createElement("div");
|
|
475
|
+
elem.id = "test";
|
|
476
|
+
elem.getBoundingClientRect = vi.fn().mockReturnValue({ top: 0, bottom: 200 });
|
|
477
|
+
document.body.appendChild(elem);
|
|
478
|
+
|
|
479
|
+
const { result } = renderHook(() => useMyHook());
|
|
480
|
+
|
|
481
|
+
// Advance timers for initial check
|
|
482
|
+
act(() => {
|
|
483
|
+
vi.runAllTimers();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Simulate observer callback
|
|
487
|
+
act(() => {
|
|
488
|
+
observerCallback(
|
|
489
|
+
[
|
|
490
|
+
{
|
|
491
|
+
target: elem,
|
|
492
|
+
isIntersecting: true,
|
|
493
|
+
boundingClientRect: {
|
|
494
|
+
top: 0,
|
|
495
|
+
bottom: 200,
|
|
496
|
+
left: 0,
|
|
497
|
+
right: 0,
|
|
498
|
+
x: 0,
|
|
499
|
+
y: 0,
|
|
500
|
+
width: 0,
|
|
501
|
+
height: 200,
|
|
502
|
+
},
|
|
503
|
+
} as unknown as IntersectionObserverEntry,
|
|
504
|
+
],
|
|
505
|
+
{} as IntersectionObserver,
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(result.current).toBe("expected-value");
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Key points:**
|
|
514
|
+
|
|
515
|
+
- Mock `getBoundingClientRect` on test elements
|
|
516
|
+
- Provide `boundingClientRect` in IntersectionObserverEntry mocks
|
|
517
|
+
- Wrap timer advances and callback calls in `act()`
|
|
518
|
+
- Test both initial state and subsequent updates
|
|
519
|
+
|
|
520
|
+
### Common Pitfalls Checklist
|
|
521
|
+
|
|
522
|
+
When writing performance-optimized hooks:
|
|
523
|
+
|
|
524
|
+
- [ ] RAF cleanup: Store ID, cancel in cleanup
|
|
525
|
+
- [ ] isMounted guard: Prevent setState after unmount
|
|
526
|
+
- [ ] Initial state check: Manual check for IntersectionObserver
|
|
527
|
+
- [ ] Previous value tracking: Skip redundant setState
|
|
528
|
+
- [ ] Map/Set cleanup: Clear in cleanup function
|
|
529
|
+
- [ ] Test mock restoration: Save originals, restore in afterEach
|
|
530
|
+
- [ ] Console warnings: For missing DOM elements (not errors)
|
|
531
|
+
- [ ] Tiebreaker logic: When multiple candidates have equal scores
|
|
532
|
+
|
|
533
|
+
## Storybook Development
|
|
534
|
+
|
|
535
|
+
### Testing Interactive Components
|
|
536
|
+
|
|
537
|
+
- Use `http://localhost:6006` when developing/testing components
|
|
538
|
+
- Create stories that simulate production patterns (e.g., overlapping scrollpoints like Voltaire)
|
|
539
|
+
- Test edge cases in stories (empty arrays, missing DOM elements, rapid state changes)
|
|
540
|
+
|
|
541
|
+
### Performance Testing in Storybook
|
|
542
|
+
|
|
543
|
+
1. Open Chrome DevTools → Performance tab
|
|
544
|
+
2. Start recording while interacting with component
|
|
545
|
+
3. Search for forced reflow indicators:
|
|
546
|
+
- `getBoundingClientRect`
|
|
547
|
+
- `clientHeight`/`scrollHeight`/`offsetHeight`
|
|
548
|
+
- "Forced reflow" warnings
|
|
549
|
+
4. Measure total time in layout/reflow (should be <5ms for interactions)
|
|
550
|
+
|
|
551
|
+
### Simulating Production Patterns
|
|
552
|
+
|
|
553
|
+
When creating stories for layout-dependent components, replicate real-world scenarios:
|
|
554
|
+
|
|
555
|
+
Example - Overlapping scrollpoints (like Voltaire):
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
<div className="relative">
|
|
559
|
+
<div id="hero" className="absolute top-0 h-32" />
|
|
560
|
+
<div id="main" className="relative pt-32 h-screen" />
|
|
561
|
+
</div>
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
This allows testing tiebreaker logic and initial state detection. Storybook stories should replicate production layout patterns to catch bugs like the tiebreaker issue. The original simple sequential zones didn't expose the Voltaire bug where elements start at the same position.
|
|
565
|
+
|
|
566
|
+
## Git workflow
|
|
567
|
+
|
|
568
|
+
See the [root AGENTS.md](../../AGENTS.md) for universal git workflow guidance.
|
|
569
|
+
|
|
570
|
+
**UI package-specific notes:**
|
|
571
|
+
|
|
572
|
+
- Run `pnpm lint` and `pnpm test` before pushing
|
|
573
|
+
- For performance-related changes, compare before/after metrics and include in commit/PR
|