diff --git a/SECURITY.md b/SECURITY.md index e3067f13..32cc869a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,12 +4,19 @@ If you discover a security vulnerability, report it PRIVATELY with the maintaine ## Supported Versions -Compose is currently under active development.
We actively support these versions: +Compose is currently under active development. It is **NOT production ready.** and we may still make breaking changes to any part of the codebase. + +
**We actively support these versions:** | Version | Supported | | ------- | ------------------ | | latest (main) | :white_check_mark: | +for both packages: + +- [`@perfect-abstractions/compose`](https://www.npmjs.com/package/@perfect-abstractions/compose) +- [`@perfect-abstractions/compose-cli`](https://www.npmjs.com/package/@perfect-abstractions/compose-cli) + ## Reporting a Vulnerability Emails:
diff --git a/website/docs/contribution/documentation/all-components.mdx b/website/docs/contribution/documentation/all-components.mdx index e4ecf566..f4f21864 100644 --- a/website/docs/contribution/documentation/all-components.mdx +++ b/website/docs/contribution/documentation/all-components.mdx @@ -1766,13 +1766,14 @@ Smooth counting animation for numbers. ### WasThisHelpful -Feedback widget for collecting user feedback on documentation pages. +Feedback widget for documentation pages. The **`aside`** variant is injected automatically at the bottom of the doc TOC column on desktop (and inline above the page pager on mobile) via the swizzled doc layout. Use the **`card`** variant when you want an embedded demo in MDX. Feedback #### Live Example console.log(feedback)} /> @@ -1782,8 +1783,11 @@ Feedback widget for collecting user feedback on documentation pages. @@ -1791,9 +1795,10 @@ Feedback widget for collecting user feedback on documentation pages. ```jsx { - // Handle feedback + // Optional: PostHog events docs_helpful_vote / docs_helpful_submit still fire console.log(feedback); }} /> @@ -1801,17 +1806,14 @@ Feedback widget for collecting user feedback on documentation pages. #### Best Practices -- Place at bottom of pages -- Use unique pageId per page -- Integrate with analytics -- Respond to feedback +- Prefer the built-in global aside on real docs; use `variant="card"` only for examples +- Optional `pageId` helps PostHog filters; global aside uses permalink and title from the doc +- Events: `docs_helpful_vote`, `docs_helpful_submit` (when PostHog is enabled) #### When to Use -- End of documentation pages -- After tutorials -- Product documentation -- Help articles +- `variant="card"` in MDX examples or special pages without the global doc layout +- Most real docs rely on the built-in aside (no manual component needed) --- @@ -2209,6 +2211,4 @@ import Icon from '@site/src/components/ui/Icon'; --- - - \ No newline at end of file diff --git a/website/docs/design/index.mdx b/website/docs/design/index.mdx index 64c8af64..437a6904 100644 --- a/website/docs/design/index.mdx +++ b/website/docs/design/index.mdx @@ -18,35 +18,30 @@ This section contains the guidelines and rules for developing new facets and Sol title="Compose Is Written to Be Read" description="Emphasizes clarity first: keep facets and libraries self-contained, ordered top-to-bottom, and easy to read. Avoid clever abstractions that reduce readability." href="/docs/design/written-to-be-read" - icon={} size="medium" /> } size="medium" /> } size="medium" /> } size="medium" /> } size="medium" /> diff --git a/website/docs/facets/authentication.mdx b/website/docs/facets/authentication.mdx deleted file mode 100644 index 76739e98..00000000 --- a/website/docs/facets/authentication.mdx +++ /dev/null @@ -1,261 +0,0 @@ ---- -sidebar_position: 2 -title: Authentication -description: Access control and permission management for diamond contracts. Implement role-based access control (RBAC) and secure your Compose facets. -draft: true ---- - -import DocHero from '@site/src/components/docs/DocHero'; -import Callout from '@site/src/components/ui/Callout'; -import APIReference from '@site/src/components/api/APIReference'; -import FeatureGrid, { FeatureGridItem } from '@site/src/components/features/FeatureGrid'; - - - -## Overview - -Authentication in Compose is handled through composable facets that implement role-based access control (RBAC). The system is designed to be flexible, secure, and easy to integrate with your custom facets. - - -Authentication facets use the same shared storage pattern as all Compose components, allowing both standard and custom facets to check permissions consistently. - - -## Core Features - - - } - title="Role-Based Access" - description="Define custom roles with specific permissions for different user types." - /> - } - title="Flexible Permissions" - description="Grant and revoke permissions dynamically without redeployment." - /> - } - title="Secure by Default" - description="Built-in checks and modifiers to prevent unauthorized access." - /> - - -## Access Control Facet - -The `AccessControlFacet` provides standard role-based access control functionality: - -### Key Functions - - - - - - - -## Usage Example - -Here's how to integrate access control in your custom facet: - -```solidity -import {AccessControlMod} from "compose/AccessControlMod.sol"; - -contract AdminFacet { - // Define your custom role - bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); - - function adminOnlyFunction() external { - // Check if caller has admin role - require( - LibAccessControl.hasRole(ADMIN_ROLE, msg.sender), - "AccessControl: caller is not admin" - ); - - // Your admin logic here - performAdminAction(); - } - - function performAdminAction() internal { - // Implementation - } -} -``` - - -Always define role constants using `keccak256` hashes of descriptive names. This makes your code more readable and prevents typos. - - -## Role Hierarchy - -Compose supports hierarchical roles where certain roles can manage other roles: - -```solidity -// Setup role hierarchy -bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; -bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); -bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); - -// DEFAULT_ADMIN_ROLE can grant/revoke MINTER_ROLE and BURNER_ROLE -// MINTER_ROLE can only mint -// BURNER_ROLE can only burn -``` - -## Integration with Custom Facets - -Your custom facets can use `AccessControlMod` to check permissions: - -```solidity -import {AccessControlMod} from "compose/AccessControlMod.sol"; -import {ERC20Mod} from "compose/ERC20Mod.sol"; - -contract TokenMinterFacet { - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - function mintTokens(address to, uint256 amount) external { - // Check minter permission - require( - AccessControlMod.hasRole(MINTER_ROLE, msg.sender), - "TokenMinter: caller is not minter" - ); - - // Mint using Compose's ERC20 library - ERC20Mod.mint(to, amount); - } -} -``` - - -The `AccessControlMod` library accesses the same storage as `AccessControlFacet`, so permissions set through the facet are instantly available to your custom facets! - - -## Security Considerations - - -Always protect role management functions. The `DEFAULT_ADMIN_ROLE` has ultimate control over all roles. - - -### Best Practices - -1. **Use Role Modifiers**: Create reusable modifiers for role checks -2. **Minimize Admin Privileges**: Grant only necessary permissions -3. **Audit Role Changes**: Emit events for all role grants/revokes -4. **Test Thoroughly**: Verify access control in all scenarios -5. **Document Roles**: Clearly document what each role can do - -## Common Patterns - -### Multi-Signature Admin - -```solidity -// Use a multi-sig wallet as the admin role holder -address multiSigWallet = 0x...; -LibAccessControl.grantRole(DEFAULT_ADMIN_ROLE, multiSigWallet); -``` - -### Time-Locked Roles - -```solidity -contract TimeLockFacet { - bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); - - mapping(address => uint256) public roleExpiry; - - function grantTemporaryRole( - address account, - uint256 duration - ) external { - require( - AccessControlMod.hasRole(DEFAULT_ADMIN_ROLE, msg.sender), - "Not admin" - ); - - AccessControlMod.grantRole(OPERATOR_ROLE, account); - roleExpiry[account] = block.timestamp + duration; - } - - function revokeExpiredRoles(address account) external { - if (block.timestamp >= roleExpiry[account]) { - AccessControlMod.revokeRole(OPERATOR_ROLE, account); - } - } -} -``` - -## Next Steps - - - } - title="Facets & Modules" - description="Learn how authentication integrates with the facet architecture." - href="/docs/foundations/facets-and-modules" - /> - } - title="Security Best Practices" - description="Explore comprehensive security patterns for your contracts." - href="/docs/" - /> - - diff --git a/website/docs/facets/facets-and-modules.mdx b/website/docs/facets/facets-and-modules.mdx deleted file mode 100644 index e4856b10..00000000 --- a/website/docs/facets/facets-and-modules.mdx +++ /dev/null @@ -1,337 +0,0 @@ ---- -sidebar_position: 3 -title: Facets & Modules -description: How facets and modules work together through shared storage. When to use facets vs modules and how to build composable smart contract systems. -draft: true ---- - -import DocHero from '@site/src/components/docs/DocHero'; -import Callout from '@site/src/components/ui/Callout'; -import FeatureGrid, { FeatureGridItem } from '@site/src/components/features/FeatureGrid'; -import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; - - - -## Overview - -Compose uses a dual-component architecture where **facets** provide complete implementations and **modules** provide reusable helper functions. Both work with the same shared storage, enabling seamless integration. - - -Facets are complete implementations. Modules are helper functions. Both access the **same storage**. - - -## Architecture Comparison - -
- -| Component | Purpose | Use Case | -|-----------|---------|----------| -| **Facets** | Complete, self-contained implementations | Use as-is in your diamond for standard functionality | -| **Modules** | Helper functions for custom facets | Import into your custom facets to extend Compose | - -
- -## Facets: Complete Implementations - -Facets are fully functional smart contracts that can be added to your diamond: - - - } - title="Self-Contained" - description="Complete implementations ready to use out of the box." - /> - } - title="Plug & Play" - description="Add to your diamond via the diamond cut function." - /> - } - title="Verified" - description="(In the future) Audited, tested, and deployed on multiple chains." - /> - - -### Example: ERC721Facet - -```solidity -// ERC721Facet provides standard NFT functionality -contract ERC721Facet { - function balanceOf(address owner) external view returns (uint256) { - return ERC721Mod.balanceOf(owner); - } - - function ownerOf(uint256 tokenId) external view returns (address) { - return ERC721Mod.ownerOf(tokenId); - } - - function transferFrom( - address from, - address to, - uint256 tokenId - ) external { - ERC721Mod.transferFrom(from, to, tokenId); - } - - // ... more standard ERC721 functions -} -``` - - -You can add `ERC721Facet` to your diamond and immediately have full ERC721 functionality! - - -## Modules: Helper Functions - -Modules provide internal functions that your custom facets can use: - - - } - title="Building Blocks" - description="Reusable functions for your custom logic." - /> - } - title="Shared Storage" - description="Access the same data as standard facets." - /> - } - title="Flexible" - description="Combine with your custom logic however you need." - /> - - -### Example: ERC721Mod - -```solidity -// ERC721Mod provides helper functions for custom facets -library ERC721Mod { - function mint(address to, uint256 tokenId) internal { - // Modifies storage that ERC721Facet reads - ERC721Storage storage s = erc721Storage(); - s.owners[tokenId] = to; - s.balances[to]++; - // ... emit events, etc. - } - - function burn(uint256 tokenId) internal { - // Modifies same storage - ERC721Storage storage s = erc721Storage(); - address owner = s.owners[tokenId]; - delete s.owners[tokenId]; - s.balances[owner]--; - // ... emit events, etc. - } - - // ... more helper functions -} -``` - -## The Magic: Shared Storage - -Both facets and modules access the **same storage location** in your diamond: - -```solidity -// Your custom facet -import {ERC721Mod} from "compose/ERC721Mod.sol"; - -contract GameNFTFacet { - function mintWithGameLogic( - address player, - uint256 tokenId - ) external { - // Your custom game logic - require( - playerHasEnoughPoints(player), - "Not enough points" - ); - - // Use ERC721Mod to mint - // This modifies the SAME storage that - // ERC721Facet uses! - ERC721Mod.mint(player, tokenId); - - // Now ERC721Facet.ownerOf(tokenId) - // returns player! - updatePlayerStats(player); - } -} -``` - - -You don't need to inherit from anything. You don't need to override functions. Just use the module functions and everything works together! - - -## Visual Diagram - -``` -┌────────────────────────────────────────┐ -│ Diamond Contract │ -├────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌───────────────┐ │ -│ │ ERC721Facet │ │ GameNFTFacet │ │ -│ │ │ │ │ │ -│ │ - balanceOf()│ │ - mint with │ │ -│ │ - ownerOf() │ │ game logic │ │ -│ │ - transfer() │ │ │ │ -│ └──────┬───────┘ └───────┬───────┘ │ -│ │ │ │ -│ │ ┌─────────────────┘ │ -│ ▼ ▼ │ -│ ┌─────────────┐ │ -│ │ ERC721Mod │ │ -│ └──────┬──────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────┐ │ -│ │ Storage │ │ -│ │ (Shared) │ │ -│ └─────────────┘ │ -│ │ -└────────────────────────────────────────┘ -``` - -## When to Use Each - - - } - title="Use Facets When..." - description="You want standard, complete functionality with no customization needed." - > -
    -
  • Implementing standard token interfaces
  • -
  • Adding access control
  • -
  • Using governance systems
  • -
-
- } - title="Use Modules When..." - description="You're building custom facets that need to integrate with Compose." - > -
    -
  • Creating custom minting logic
  • -
  • Building game mechanics
  • -
  • Implementing custom rules
  • -
-
-
- -## Real-World Example - -Let's build a game where players earn NFTs by completing quests: - -```solidity -import {ERC721Mod} from "compose/ERC721Mod.sol"; -import {AccessControlMod} from "compose/AccessControlMod.sol"; - -contract QuestRewardsFacet { - bytes32 public constant GAME_MASTER = keccak256("GAME_MASTER"); - - mapping(uint256 => bool) public questCompleted; - - function completeQuest( - address player, - uint256 questId, - uint256 tokenId - ) external { - // Use AccessControlMod to check permissions - require( - AccessControlMod.hasRole(GAME_MASTER, msg.sender), - "Not authorized" - ); - - // Your game logic - require(!questCompleted[questId], "Quest already claimed"); - questCompleted[questId] = true; - - // Use ERC721Mod to mint reward NFT - ERC721Mod.mint(player, tokenId); - - // The standard ERC721Facet.ownerOf(tokenId) now works! - // The player can transfer using ERC721Facet.transferFrom()! - } -} -``` - - -Your `QuestRewardsFacet` mints NFTs that work with all standard ERC721 functions. No inheritance, no complexity! - - -## Best Practices - - - } - title="Do: Use Modules in Custom Facets" - description="Import and use module functions to integrate with Compose functionality." - /> - } - title="Do: Add Standard Facets As-Is" - description="Use complete facets when you don't need customization." - /> - } - title="Don't: Modify Standard Facets" - description="Create custom facets instead. Keep standard facets unchanged." - /> - } - title="Don't: Call Facets from Facets" - description="Use modules for facet-to-facet communication, not external calls." - /> - - -## Available Facets & Modules - - - } - title="ERC20" - description="Fungible token standard with mint, burn, and transfer functionality." - /> - } - title="ERC721" - description="Non-fungible token (NFT) standard with full metadata support." - /> - } - title="ERC1155" - description="Multi-token standard supporting both fungible and non-fungible tokens." - /> - } - title="Access Control" - description="Role-based permission system for managing contract access." - /> - - -## Next Steps - - - } - title="Storage Patterns" - description="Deep dive into how shared storage works under the hood." - href="/docs/" - /> - } - title="Quick Start" - description="Build your first diamond with facets and modules." - href="/docs/getting-started/quick-start" - /> - - diff --git a/website/docs/foundations/index.mdx b/website/docs/foundations/index.mdx index 4859a2e3..f4a41da1 100644 --- a/website/docs/foundations/index.mdx +++ b/website/docs/foundations/index.mdx @@ -20,42 +20,36 @@ import CalloutBox from '@site/src/components/ui/CalloutBox'; title="Diamond Smart Contracts" description="A diamond contract is made up of multiple parts. It exists at one address and holds all storage, but uses separate smart contracts called facets to provide its functionality." href="/docs/foundations/diamond-contracts" - icon={} size="medium" /> } size="medium" /> } size="medium" /> } size="medium" /> } size="medium" /> } size="medium" /> diff --git a/website/docs/getting-started/installation.md b/website/docs/getting-started/installation.md index 4dcbe6f2..c4f9dc3d 100644 --- a/website/docs/getting-started/installation.md +++ b/website/docs/getting-started/installation.md @@ -17,7 +17,7 @@ Compose is published as two npm packages: ## Prerequisites -- **Node.js >= 20**: required for the CLI and for npm-based Hardhat workflows +- **[Node.js](https://nodejs.org/) >= 20**: required for the CLI and for npm-based Hardhat workflows - **[Foundry](https://book.getfoundry.sh/getting-started/installation) (Optional)**: required if you use a Foundry scaffold or integrate Solidity with Forge - **Git**: used by Foundry dependency installs inside generated projects @@ -38,7 +38,7 @@ npm install -g @perfect-abstractions/compose-cli compose init ``` -## Add Compose to an existing project (Manual Installation) +## Add Compose to an existing project ### Foundry @@ -64,13 +64,12 @@ npm install @perfect-abstractions/compose import {DiamondMod} from "@perfect-abstractions/compose/diamond/DiamondMod.sol"; ``` - +- **[Core Concepts](/docs/foundations)** - Learn about facets, libraries, and shared storage +- **[Explore Available Contracts](https://github.com/Perfect-Abstractions/Compose/tree/main/src)** - See what else you can add ## Getting Help diff --git a/website/docs/intro.mdx b/website/docs/intro.mdx index 768a2a3a..8b9deec5 100644 --- a/website/docs/intro.mdx +++ b/website/docs/intro.mdx @@ -26,34 +26,28 @@ That's **Compose** — where your unique value and functionality connects seamle ## Why Choose Compose? - + } title="On-chain Standard Library" description="(Not yet, in the future) - Access verified, audited smart contracts deployed once and reused across multiple diamonds on multiple blockchains." /> } title="Composable Architecture" description="Mix and match on-chain components to build exactly what you need." /> } title="Readability First" description="Code designed to be understood. Compose prioritizes clarity over cleverness." /> } title="Upgradeable by Design" description="Full power of ERC-2535 Diamonds means your contracts can evolve without full redeployment." /> } title="Battle-Tested Patterns" description="Community-reviewed implementations following proven best practices and security standards." /> } title="Developer Experience" description="Intuitive APIs, comprehensive documentation, and helpful libraries make development a breeze." /> @@ -88,14 +82,12 @@ Our smart contract library is perfect for: } href="/docs/foundations" size="large" /> } href="/docs/design" size="large" /> diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 1d00f2c4..3224c704 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -35,6 +35,11 @@ const config = { // For GitHub pages deployment, it is often '//' baseUrl: '/', + customFields: { + reportIssueUrl: + process.env.DOC_REPORT_ISSUE_URL || + 'https://github.com/Perfect-Abstractions/Compose/issues/new/choose', + }, // Broken link handling onBrokenLinks: 'throw', @@ -193,13 +198,12 @@ const config = { // ], // }, { - href: 'https://discord.gg/DCBD2UKbxc', - label: 'Discord', + type: 'custom-githubStars', position: 'right', }, { - href: 'https://github.com/Perfect-Abstractions/Compose', - label: 'GitHub', + href: 'https://discord.gg/DCBD2UKbxc', + label: 'Discord', position: 'right', }, ], @@ -211,18 +215,34 @@ const config = { title: 'Docs', items: [ { - label: 'Getting Started', - to: '/docs', + label: 'Introduction', + to: '/docs/', }, { label: 'Installation', to: '/docs/getting-started/installation', }, + { + label: 'Foundations', + to: '/docs/foundations/', + }, + { + label: "Design Principles", + to: '/docs/design/', + }, + { + label: 'How to Contribute', + to: '/docs/contribution/how-to-contribute', + }, ], }, { title: 'Community', items: [ + { + label: 'Blog', + to: '/blog', + }, { label: 'Discord', href: 'https://discord.gg/DCBD2UKbxc', @@ -234,16 +254,16 @@ const config = { ], }, { - title: 'More', + title: 'Project', items: [ - { - label: 'Blog', - to: '/blog', - }, { label: 'GitHub', href: 'https://github.com/Perfect-Abstractions/Compose', }, + { + label: 'Security', + href: 'https://github.com/Perfect-Abstractions/Compose?tab=security-ov-file', + }, { label: 'MIT License', href: 'https://github.com/Perfect-Abstractions/Compose/blob/main/LICENSE.md', diff --git a/website/sidebars.js b/website/sidebars.js index f8f0990b..a6326118 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -20,7 +20,7 @@ const sidebars = { { type: 'category', label: 'Getting Started', - collapsed: true, + collapsed: false, items: [ { type: 'autogenerated', @@ -46,7 +46,7 @@ const sidebars = { { type: 'category', label: 'Design', - collapsed: false, + collapsed: true, link: { type: 'doc', id: 'design/index', @@ -58,19 +58,6 @@ const sidebars = { }, ], }, - /* - { - type: 'category', - label: 'Facets', - collapsed: true, - items: [ - { - type: 'autogenerated', - dirName: 'facets', - }, - ], - }, - */ { type: 'category', label: 'Contribution', diff --git a/website/src/components/docs/DocCard/index.js b/website/src/components/docs/DocCard/index.js index a8072213..3c52d4a0 100644 --- a/website/src/components/docs/DocCard/index.js +++ b/website/src/components/docs/DocCard/index.js @@ -5,7 +5,7 @@ import Icon from '../../ui/Icon'; import styles from './styles.module.css'; /** - * DocCard Component - MoneyKit-inspired documentation card + * DocCard * * @param {string} title - Card title * @param {string} description - Card description diff --git a/website/src/components/docs/DocCard/styles.module.css b/website/src/components/docs/DocCard/styles.module.css index 9b245175..355a40c8 100644 --- a/website/src/components/docs/DocCard/styles.module.css +++ b/website/src/components/docs/DocCard/styles.module.css @@ -1,20 +1,36 @@ -/* DocCard - MoneyKit-inspired card component */ +/* DocCard — matches homepage feature cards (featuresSection) */ + .docCard { display: flex; align-items: center; gap: 1.25rem; padding: 2rem; - background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 0.75rem; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); text-decoration: none; color: inherit; position: relative; overflow: hidden; + isolation: isolate; cursor: pointer; -} - + border-radius: 1.25rem; + background: linear-gradient(155deg, #ffffff 0%, #f1f5f9 55%, #f8fafc 100%); + border: 1px solid rgba(15, 23, 42, 0.08); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.7) inset, + 0 1px 0 rgba(255, 255, 255, 0.9) inset, + 0 22px 44px -18px rgba(15, 23, 42, 0.12), + 0 4px 12px rgba(15, 23, 42, 0.04); + transition: + border-color var(--motion-duration-normal) var(--motion-ease-standard), + box-shadow var(--motion-duration-normal) var(--motion-ease-standard), + transform var(--motion-duration-normal) var(--motion-ease-standard); +} + +.docCard:focus-visible { + outline: 3px solid var(--focus-ring-color); + outline-offset: 3px; +} + +/* Facet highlight sweep (top edge) */ .docCard::before { content: ''; position: absolute; @@ -22,34 +38,81 @@ left: 0; right: 0; height: 3px; - background: linear-gradient(90deg, var(--ifm-color-primary) 0%, var(--ifm-color-accent) 100%); + background: linear-gradient( + 90deg, + transparent 0%, + var(--compose-primary-500) 22%, + #60a5fa 55%, + rgba(96, 165, 250, 0.35) 100% + ); + transform: scaleX(0); + transform-origin: left; + transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1); + z-index: 2; + pointer-events: none; +} + +/* Radial bloom on hover */ +.docCard::after { + content: ''; + position: absolute; + inset: -40% -20% auto -20%; + height: 85%; + background: radial-gradient(closest-side, rgba(59, 130, 246, 0.14), transparent 72%); opacity: 0; - transition: opacity 0.3s ease; + transition: opacity var(--motion-duration-normal) var(--motion-ease-standard); + pointer-events: none; + z-index: 0; } .docCard:hover { - transform: translateY(-4px); - border-color: var(--ifm-color-primary); - box-shadow: 0 12px 24px -6px rgba(59, 130, 246, 0.25); + border-color: rgba(59, 130, 246, 0.28); + box-shadow: + 0 0 0 1px rgba(59, 130, 246, 0.12) inset, + 0 28px 56px -20px rgba(37, 99, 235, 0.18), + 0 12px 28px rgba(15, 23, 42, 0.08); + transform: translateY(-5px); } .docCard:hover::before { + transform: scaleX(1); +} + +.docCard:hover::after { opacity: 1; } -/* Dark theme */ [data-theme='dark'] .docCard { - background: #141414; - border-color: #2a2a2a; + background: linear-gradient( + 155deg, + rgba(30, 41, 59, 0.92) 0%, + rgba(15, 23, 42, 0.72) 48%, + rgba(10, 14, 26, 0.55) 100% + ); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border-color: rgba(148, 163, 184, 0.14); + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.45) inset, + 0 1px 0 rgba(255, 255, 255, 0.06) inset, + 0 28px 56px -24px rgba(0, 0, 0, 0.65), + 0 0 0 1px rgba(59, 130, 246, 0.06); +} + +[data-theme='dark'] .docCard::after { + background: radial-gradient(closest-side, rgba(96, 165, 250, 0.22), transparent 70%); } [data-theme='dark'] .docCard:hover { - background: #1a1a1a; - border-color: #60a5fa; - box-shadow: 0 12px 24px -6px rgba(96, 165, 250, 0.25); + border-color: rgba(96, 165, 250, 0.35); + box-shadow: + 0 0 0 1px rgba(59, 130, 246, 0.2) inset, + 0 1px 0 rgba(255, 255, 255, 0.09) inset, + 0 0 40px -8px rgba(59, 130, 246, 0.35), + 0 32px 64px -28px rgba(0, 0, 0, 0.75); } -/* Card Icon */ +/* Card Icon — aligned with code showcase feature icons */ .docCardIcon { flex-shrink: 0; display: flex; @@ -58,16 +121,21 @@ width: 3.5rem; height: 3.5rem; border-radius: 0.75rem; - background: linear-gradient(135deg, var(--ifm-color-primary-lightest) 0%, var(--ifm-color-primary-lighter) 100%); - transition: transform 0.3s ease; + position: relative; + z-index: 1; + color: var(--compose-primary-500); + background: linear-gradient(145deg, rgba(59, 130, 246, 0.14), rgba(37, 99, 235, 0.06)); + border: 1px solid rgba(59, 130, 246, 0.22); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.75) inset; + transition: + border-color var(--motion-duration-normal) var(--motion-ease-standard), + box-shadow var(--motion-duration-normal) var(--motion-ease-standard); } [data-theme='dark'] .docCardIcon { - background: linear-gradient(135deg, rgba(96, 165, 250, 0.15) 0%, rgba(59, 130, 246, 0.15) 100%); -} - -.docCard:hover .docCardIcon { - transform: scale(1.1); + background: linear-gradient(145deg, rgba(59, 130, 246, 0.2), rgba(15, 23, 42, 0.35)); + border-color: rgba(96, 165, 250, 0.26); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset; } .iconEmoji { @@ -79,51 +147,86 @@ .docCardContent { flex: 1; min-width: 0; + position: relative; + z-index: 1; } .docCardTitle { margin: 0 0 0.5rem 0; font-size: 1.125rem; - font-weight: 700; - color: var(--ifm-heading-color); - letter-spacing: -0.01em; + font-weight: 750; + letter-spacing: -0.025em; + line-height: 1.22; + color: #0f172a; } .docCardDescription { margin: 0; - font-size: 0.95rem; - line-height: 1.6; - color: var(--ifm-color-emphasis-700); + font-size: 0.96875rem; + line-height: 1.65; + color: #334155; +} + +[data-theme='dark'] .docCardTitle { + color: #ffffff; } [data-theme='dark'] .docCardDescription { - color: #a0a0a0; + color: rgba(226, 232, 240, 0.82); } -/* Card Arrow */ +/* Arrow — homepage “hint” color logic */ .docCardArrow { flex-shrink: 0; display: flex; align-items: center; justify-content: center; - color: var(--ifm-color-emphasis-500); - transition: all 0.3s ease; + position: relative; + z-index: 1; + color: #1e40af; + transition: + color var(--motion-duration-normal) var(--motion-ease-standard), + transform var(--motion-duration-normal) var(--motion-ease-standard); +} + +[data-theme='dark'] .docCardArrow { + color: #93c5fd; + opacity: 0.5; } .docCard:hover .docCardArrow { - color: var(--ifm-color-primary); - transform: translateX(4px); + color: #1d4ed8; + transform: translateX(5px); } [data-theme='dark'] .docCard:hover .docCardArrow { - color: #60a5fa; + color: #4189f7; + opacity: 0.95; } -/* Card Variants */ +/* Primary variant */ .docCard--primary { - background: linear-gradient(135deg, var(--ifm-color-primary) 0%, var(--ifm-color-primary-dark) 100%); - border-color: var(--ifm-color-primary); + background: linear-gradient(135deg, var(--compose-primary-500) 0%, var(--compose-primary-600) 100%); + border-color: var(--compose-primary-500); color: white; + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.12) inset, + 0 22px 44px -18px rgba(37, 99, 235, 0.45), + 0 4px 14px rgba(59, 130, 246, 0.35); +} + +.docCard--primary::before { + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.85) 35%, + rgba(255, 255, 255, 0.45) 70%, + transparent 100% + ); +} + +.docCard--primary::after { + background: radial-gradient(closest-side, rgba(255, 255, 255, 0.2), transparent 70%); } .docCard--primary .docCardTitle, @@ -132,29 +235,58 @@ color: white; } +.docCard--primary .docCardArrow { + opacity: 1; +} + .docCard--primary .docCardIcon { background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); + border-color: rgba(255, 255, 255, 0.35); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25) inset; + color: white; } .docCard--primary:hover { - transform: translateY(-4px); - box-shadow: 0 16px 32px -8px rgba(59, 130, 246, 0.4); + border-color: rgba(255, 255, 255, 0.45); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.18) inset, + 0 32px 64px -20px rgba(37, 99, 235, 0.55), + 0 12px 28px rgba(15, 23, 42, 0.2); } +.docCard--primary:hover .docCardArrow { + color: #ffffff; +} + +/* Secondary — slightly stronger edge, same family as default */ .docCard--secondary { - border: 2px solid var(--ifm-color-emphasis-300); + border-color: rgba(15, 23, 42, 0.14); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.65) inset, + 0 1px 0 rgba(255, 255, 255, 0.85) inset, + 0 18px 36px -16px rgba(15, 23, 42, 0.14), + 0 3px 10px rgba(15, 23, 42, 0.05); +} + +[data-theme='dark'] .docCard--secondary { + border-color: rgba(148, 163, 184, 0.22); +} + +.docCard--secondary:hover { + border-color: rgba(59, 130, 246, 0.32); } /* Card Sizes */ .docCard--small { padding: 1.25rem 1.5rem; gap: 1rem; + border-radius: 1.125rem; } .docCard--small .docCardIcon { width: 2.5rem; height: 2.5rem; + border-radius: 0.625rem; } .docCard--small .iconEmoji { @@ -163,10 +295,11 @@ .docCard--small .docCardTitle { font-size: 1rem; + font-weight: 700; } .docCard--small .docCardDescription { - font-size: 0.875rem; + font-size: 0.9375rem; } .docCard--large { @@ -184,38 +317,35 @@ } .docCard--large .docCardTitle { - font-size: 1.5rem; + font-size: 1.3125rem; } -/* DocCardGrid */ +/* DocCardGrid — match homepage features grid rhythm */ .docCardGrid { display: grid; grid-template-columns: repeat(var(--grid-columns, 2), 1fr); - gap: 1.5rem; + gap: 2rem; margin: 2rem 0; } -/* When sidebar is visible (typical desktop width), adjust for better fit */ @media (min-width: 997px) and (max-width: 1200px) { .docCardGrid { grid-template-columns: 1fr; - gap: 1.25rem; + gap: 1.5rem; } } -/* Wider screens with sidebar, allow 2 columns if space permits */ @media (min-width: 1201px) and (max-width: 1400px) { .docCardGrid { grid-template-columns: repeat(var(--grid-columns, 2), 1fr); - gap: 1.25rem; + gap: 1.5rem; } } -/* Very wide screens, allow configured columns */ @media (min-width: 1401px) { .docCardGrid { grid-template-columns: repeat(var(--grid-columns, 2), 1fr); - gap: 1.5rem; + gap: 2rem; } } @@ -226,13 +356,12 @@ } } -/* Responsive adjustments for cards when sidebar is visible */ @media (min-width: 997px) and (max-width: 1200px) { .docCard { padding: 1.75rem; gap: 1.125rem; } - + .docCardIcon { width: 3.25rem; height: 3.25rem; @@ -241,16 +370,50 @@ @media (max-width: 768px) { .docCard { - padding: 1.5rem; + padding: 1.75rem 1.5rem 2rem; gap: 1rem; } - + .docCardIcon { width: 3rem; height: 3rem; } - + .iconEmoji { font-size: 1.75rem; } -} \ No newline at end of file + + .docCardTitle { + font-size: 1.1875rem; + } + + .docCardDescription { + font-size: 0.9375rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .docCard { + transition: + border-color var(--motion-duration-fast) ease, + box-shadow var(--motion-duration-fast) ease; + } + + .docCard:hover { + transform: none; + } + + .docCard::before { + transition: transform 0.01ms; + } + + .docCardArrow { + transition: + color var(--motion-duration-fast) ease, + transform 0.01ms; + } + + .docCard:hover .docCardArrow { + transform: none; + } +} diff --git a/website/src/components/docs/DocPageAside/index.js b/website/src/components/docs/DocPageAside/index.js new file mode 100644 index 00000000..cf79c429 --- /dev/null +++ b/website/src/components/docs/DocPageAside/index.js @@ -0,0 +1,47 @@ +import React from 'react'; +import clsx from 'clsx'; +import { useDoc } from '@docusaurus/plugin-content-docs/client'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import EditThisPage from '@theme/EditThisPage'; +import Link from '@docusaurus/Link'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import Icon from '@site/src/components/ui/Icon'; +import styles from './styles.module.css'; + +/** + * @param {boolean} [soloInSidebar] — top of right column with no TOC above; drop extra top rule + */ +export default function DocPageAside({ soloInSidebar = false }) { + const { metadata } = useDoc(); + const { editUrl, permalink, title } = metadata; + const { siteConfig } = useDocusaurusContext(); + const reportIssueUrl = + siteConfig.customFields?.reportIssueUrl ?? + 'https://github.com/Perfect-Abstractions/Compose/issues/new/choose'; + + const reportIssueLink = ( + + + Report issue + + ); + + return ( + + ); +} diff --git a/website/src/components/docs/DocPageAside/styles.module.css b/website/src/components/docs/DocPageAside/styles.module.css new file mode 100644 index 00000000..74715c44 --- /dev/null +++ b/website/src/components/docs/DocPageAside/styles.module.css @@ -0,0 +1,71 @@ +.aside { + margin-top: 1.25rem; + padding-top: 1.25rem; + border-top: 1px solid var(--ifm-color-emphasis-200); +} + +/* Right column with no TOC above: avoid double separator at rail top */ +.asideSoloInRail { + margin-top: 0; + padding-top: 0; + border-top: none; +} + +[data-theme='dark'] .aside { + border-top-color: var(--ifm-color-emphasis-300); +} + +.links { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.625rem; + margin-top: 1rem; +} + +.editWrap :global(a) { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.8125rem; + font-weight: 500; + color: var(--ifm-color-emphasis-700); + text-decoration: none; +} + +.editWrap :global(a:hover) { + color: var(--ifm-color-primary); + text-decoration: none; +} + +[data-theme='dark'] .editWrap :global(a) { + color: var(--ifm-color-emphasis-600); +} + +.reportLink { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.8125rem; + font-weight: 500; + color: var(--ifm-color-emphasis-700); + text-decoration: none; +} + +.reportLink:hover { + color: var(--ifm-color-primary); + text-decoration: none; +} + +[data-theme='dark'] .reportLink { + color: var(--ifm-color-emphasis-600); +} + +@media (hover: hover) and (pointer: fine) { + .editWrap :global(a), + .reportLink { + transition: + color 160ms cubic-bezier(0.23, 1, 0.32, 1), + opacity 160ms cubic-bezier(0.23, 1, 0.32, 1); + } +} diff --git a/website/src/components/docs/RelatedDocs/styles.module.css b/website/src/components/docs/RelatedDocs/styles.module.css index 0860610b..6e8817d5 100644 --- a/website/src/components/docs/RelatedDocs/styles.module.css +++ b/website/src/components/docs/RelatedDocs/styles.module.css @@ -1,20 +1,26 @@ -/* RelatedDocs Component */ +/* RelatedDocs — section + cards match homepage / DocCard language */ + .relatedDocs { margin: 3rem 0; padding: 2rem; - background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 0.75rem; + border-radius: 1.25rem; + background: linear-gradient(155deg, #f8fafc 0%, #f1f5f9 100%); + border: 1px solid rgba(15, 23, 42, 0.08); } [data-theme='dark'] .relatedDocs { - background: #0f0f0f; - border-color: #2a2a2a; + background: linear-gradient( + 155deg, + rgba(30, 41, 59, 0.5) 0%, + rgba(15, 23, 42, 0.65) 100% + ); + border-color: rgba(148, 163, 184, 0.14); } .relatedDocsTitle { font-size: 1.5rem; - font-weight: 700; + font-weight: 750; + letter-spacing: -0.02em; margin-bottom: 1.5rem; color: var(--ifm-heading-color); } @@ -26,82 +32,193 @@ } .relatedDocCard { + position: relative; display: flex; align-items: flex-start; gap: 1rem; - padding: 1.25rem; - background: var(--ifm-background-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 0.5rem; + padding: 1.25rem 1.35rem; text-decoration: none; color: inherit; - transition: all 0.2s ease; + overflow: hidden; + isolation: isolate; + border-radius: 1.125rem; + background: linear-gradient(155deg, #ffffff 0%, #f1f5f9 55%, #f8fafc 100%); + border: 1px solid rgba(15, 23, 42, 0.08); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.7) inset, + 0 1px 0 rgba(255, 255, 255, 0.9) inset, + 0 14px 32px -16px rgba(15, 23, 42, 0.1); + transition: + border-color var(--motion-duration-normal) var(--motion-ease-standard), + box-shadow var(--motion-duration-normal) var(--motion-ease-standard), + transform var(--motion-duration-normal) var(--motion-ease-standard); } -[data-theme='dark'] .relatedDocCard { - background: #0a0a0a; - border-color: #2a2a2a; +.relatedDocCard:focus-visible { + outline: 3px solid var(--focus-ring-color); + outline-offset: 2px; +} + +.relatedDocCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient( + 90deg, + transparent 0%, + var(--compose-primary-500) 22%, + #60a5fa 55%, + rgba(96, 165, 250, 0.35) 100% + ); + transform: scaleX(0); + transform-origin: left; + transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1); + z-index: 2; + pointer-events: none; +} + +.relatedDocCard::after { + content: ''; + position: absolute; + inset: -50% -30% auto -30%; + height: 90%; + background: radial-gradient(closest-side, rgba(59, 130, 246, 0.12), transparent 72%); + opacity: 0; + transition: opacity var(--motion-duration-normal) var(--motion-ease-standard); + pointer-events: none; + z-index: 0; } .relatedDocCard:hover { - border-color: var(--ifm-color-primary); - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); - transform: translateY(-2px); + border-color: rgba(59, 130, 246, 0.28); + box-shadow: + 0 0 0 1px rgba(59, 130, 246, 0.1) inset, + 0 20px 40px -18px rgba(37, 99, 235, 0.16), + 0 8px 20px rgba(15, 23, 42, 0.06); + transform: translateY(-4px); text-decoration: none; color: inherit; } +.relatedDocCard:hover::before { + transform: scaleX(1); +} + +.relatedDocCard:hover::after { + opacity: 1; +} + +[data-theme='dark'] .relatedDocCard { + background: linear-gradient( + 155deg, + rgba(30, 41, 59, 0.92) 0%, + rgba(15, 23, 42, 0.72) 48%, + rgba(10, 14, 26, 0.55) 100% + ); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-color: rgba(148, 163, 184, 0.14); + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.45) inset, + 0 1px 0 rgba(255, 255, 255, 0.06) inset, + 0 20px 44px -22px rgba(0, 0, 0, 0.55); +} + +[data-theme='dark'] .relatedDocCard::after { + background: radial-gradient(closest-side, rgba(96, 165, 250, 0.18), transparent 70%); +} + [data-theme='dark'] .relatedDocCard:hover { - border-color: #60a5fa; - box-shadow: 0 4px 12px rgba(96, 165, 250, 0.2); + border-color: rgba(96, 165, 250, 0.35); + box-shadow: + 0 0 0 1px rgba(59, 130, 246, 0.18) inset, + 0 1px 0 rgba(255, 255, 255, 0.08) inset, + 0 0 32px -6px rgba(59, 130, 246, 0.28), + 0 24px 48px -24px rgba(0, 0, 0, 0.65); } .relatedDocIcon { - font-size: 1.5rem; + position: relative; + z-index: 1; flex-shrink: 0; + width: 2.75rem; + height: 2.75rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.35rem; line-height: 1; + border-radius: 0.625rem; + background: linear-gradient(145deg, rgba(59, 130, 246, 0.14), rgba(37, 99, 235, 0.06)); + border: 1px solid rgba(59, 130, 246, 0.22); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.75) inset; +} + +[data-theme='dark'] .relatedDocIcon { + background: linear-gradient(145deg, rgba(59, 130, 246, 0.2), rgba(15, 23, 42, 0.35)); + border-color: rgba(96, 165, 250, 0.26); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset; } .relatedDocContent { + position: relative; + z-index: 1; flex: 1; min-width: 0; } .relatedDocTitle { font-size: 1rem; - font-weight: 600; + font-weight: 700; + letter-spacing: -0.02em; margin: 0 0 0.5rem 0; - color: var(--ifm-heading-color); - line-height: 1.4; + color: #0f172a; + line-height: 1.35; } .relatedDocDescription { font-size: 0.875rem; - color: var(--ifm-color-emphasis-600); + line-height: 1.55; + color: #334155; margin: 0; - line-height: 1.5; +} + +[data-theme='dark'] .relatedDocTitle { + color: #ffffff; } [data-theme='dark'] .relatedDocDescription { - color: #909090; + color: rgba(226, 232, 240, 0.78); } .relatedDocArrow { + position: relative; + z-index: 1; flex-shrink: 0; - color: var(--ifm-color-emphasis-400); - transition: transform 0.2s ease, color 0.2s ease; + color: #1e40af; + transition: + color var(--motion-duration-normal) var(--motion-ease-standard), + transform var(--motion-duration-normal) var(--motion-ease-standard); +} + +[data-theme='dark'] .relatedDocArrow { + color: #93c5fd; + opacity: 0.5; } .relatedDocCard:hover .relatedDocArrow { - transform: translateX(4px); - color: var(--ifm-color-primary); + transform: translateX(5px); + color: #1d4ed8; } [data-theme='dark'] .relatedDocCard:hover .relatedDocArrow { - color: #60a5fa; + color: #4189f7; + opacity: 0.95; } -/* Responsive */ @media (max-width: 996px) { .relatedDocsGrid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); @@ -118,11 +235,26 @@ } .relatedDocCard { - padding: 1rem; + padding: 1.1rem 1.2rem; } } +@media (prefers-reduced-motion: reduce) { + .relatedDocCard { + transition: + border-color var(--motion-duration-fast) ease, + box-shadow var(--motion-duration-fast) ease; + } + .relatedDocCard:hover { + transform: none; + } + .relatedDocCard::before { + transition: transform 0.01ms; + } - + .relatedDocCard:hover .relatedDocArrow { + transform: none; + } +} diff --git a/website/src/components/docs/WasThisHelpful/index.js b/website/src/components/docs/WasThisHelpful/index.js index 35f8454b..c8ae4655 100644 --- a/website/src/components/docs/WasThisHelpful/index.js +++ b/website/src/components/docs/WasThisHelpful/index.js @@ -1,41 +1,211 @@ import React, { useState } from 'react'; import clsx from 'clsx'; +import { useColorMode } from '@docusaurus/theme-common'; import Icon from '../../ui/Icon'; +import { + captureDocsHelpfulSubmit, + captureDocsHelpfulVote, +} from '@site/src/utils/captureDocsFeedback'; import styles from './styles.module.css'; +/** Same path as static/icons/checkmark-stroke.svg; inline so currentColor matches success text (img ignores it). */ +function FeedbackCheckIcon({ size = 20, className }) { + return ( + + + + ); +} + /** - * WasThisHelpful Component - Feedback widget for documentation pages - * - * @param {string} pageId - Unique identifier for the page - * @param {Function} onSubmit - Callback function when feedback is submitted + * @param {string} [pageId] + * @param {string} [permalink] + * @param {string} [title] + * @param {'card'|'aside'} [variant] + * @param {Function} [onSubmit] + * @param {import('react').ReactNode} [asideEndSlot] — e.g. Report issue; beside Yes/No on mobile; desktop: below buttons, or below textarea+Submit when “No” */ -export default function WasThisHelpful({ +export default function WasThisHelpful({ pageId, - onSubmit + permalink, + title, + onSubmit, + variant = 'card', + asideEndSlot, }) { + const { colorMode } = useColorMode(); const [feedback, setFeedback] = useState(null); const [comment, setComment] = useState(''); const [submitted, setSubmitted] = useState(false); - const handleFeedback = (value) => { + const thumbUpName = colorMode === 'dark' ? 'thumbs-up-dark' : 'thumbs-up'; + const thumbDownName = colorMode === 'dark' ? 'thumbs-down-dark' : 'thumbs-down'; + + const analyticsContext = { pageId, permalink, title, variant }; + + const handleCardFeedback = (value) => { setFeedback(value); + captureDocsHelpfulVote({ helpful: value, ...analyticsContext }); }; - const handleSubmit = () => { + const handleCardSubmit = () => { if (onSubmit) { onSubmit({ pageId, feedback, comment }); - } else { - // Default behavior - could log to analytics - console.log('Feedback submitted:', { pageId, feedback, comment }); + } + if (feedback) { + captureDocsHelpfulSubmit({ + helpful: feedback, + comment: comment.trim(), + ...analyticsContext, + }); + } + setSubmitted(true); + }; + + const handleAsideYes = () => { + captureDocsHelpfulSubmit({ + helpful: 'yes', + comment: '', + ...analyticsContext, + }); + if (onSubmit) { + onSubmit({ pageId, feedback: 'yes', comment: '' }); + } + setSubmitted(true); + }; + + const handleAsideNo = () => { + setFeedback('no'); + captureDocsHelpfulVote({ helpful: 'no', ...analyticsContext }); + }; + + const handleAsideSubmit = () => { + captureDocsHelpfulSubmit({ + helpful: 'no', + comment: comment.trim(), + ...analyticsContext, + }); + if (onSubmit) { + onSubmit({ pageId, feedback: 'no', comment: comment.trim() }); } setSubmitted(true); }; if (submitted) { - return ( -
- + const submittedBody = ( + <> + Thank you for your feedback! + + ); + + if (variant === 'aside' && asideEndSlot) { + return ( +
+
+ {submittedBody} +
+
{asideEndSlot}
+
+ ); + } + + return ( +
+ {submittedBody} +
+ ); + } + + if (variant === 'aside') { + const buttonsRow = ( +
+ + +
+ ); + + return ( +
+

Was this helpful?

+ {buttonsRow} + {feedback === 'no' && ( +
+