From ef75093fb220d2f532af7f8152cf2c4a75509596 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasevin Date: Thu, 27 Nov 2025 00:25:08 +0200 Subject: [PATCH 1/2] chore(common): remove old constitution bak --- .specify/memory/constitution_bak.md | 108 ---------------------------- 1 file changed, 108 deletions(-) delete mode 100644 .specify/memory/constitution_bak.md diff --git a/.specify/memory/constitution_bak.md b/.specify/memory/constitution_bak.md deleted file mode 100644 index 96185002..00000000 --- a/.specify/memory/constitution_bak.md +++ /dev/null @@ -1,108 +0,0 @@ - - -# UI Builder Constitution - -## Core Principles - -### I. Chain-Agnostic Core, Adapter-Led Architecture (NON-NEGOTIABLE) - -- The builder app (`packages/builder`), renderer (`packages/renderer`), UI components (`packages/ui`), utils (`packages/utils`), react-core (`packages/react-core`), storage (`packages/storage`), styles (`packages/styles`), and shared types (`packages/types`) MUST remain chain-agnostic. -- All chain-specific logic, dependencies, and polyfills live exclusively in adapter packages (`packages/adapter-*`). -- Adapters MUST implement `ContractAdapter` from `packages/types/src/adapters/base.ts` and be instantiated with a `NetworkConfig`. -- The builder MUST resolve adapters through dynamic ecosystem registration via the ecosystem manager (`packages/builder/src/core/...`). -- Chain-agnostic packages MUST NOT declare chain-specific dependencies. -- Validation rules originate in the adapter interface; `isValidAddress(address: string, addressType?: string)` supports chain-specific behavior. -- The EVM adapter is the reference implementation; all new adapters MUST mirror its structure, naming, and patterns. Stellar mirrors EVM wherever applicable. -- Rationale: Preserves ecosystem neutrality and predictable adapter boundaries to sustain long-term maintainability. - -### II. Type Safety, Linting, and Code Quality (NON-NEGOTIABLE) - -- TypeScript strictness, shared linting, and formatting rules apply across the monorepo. -- `console` usage in source code is prohibited; use `logger` from `@openzeppelin/ui-builder-utils` instead (exceptions only in tests, stories, or scripts). -- Logging is disabled by default outside development; enable explicitly via `logger.configure({ enabled: true, level })`. -- `any` types are disallowed without explicit justification; prefer precise generics. -- Public library APIs MUST include JSDoc annotations as configured. -- Rationale: Enforces consistent quality gates that prevent regressions and ensures actionable diagnostics across packages. - -### III. Tooling, Packaging, and Releases (NON-NEGOTIABLE) - -- `pnpm` is the sole package manager; use `pnpm -r` for workspace commands. -- Build outputs use `tsup` for bundling and `tsc --emitDeclarationOnly` for types, shipping both ESM and CJS where applicable. -- Versioning relies on Changesets; CI publishes releases on merge to `main` after tests, linting, and type checks pass. -- Shared configs (`tailwind.config.cjs`, `postcss.config.cjs`, `components.json`) are consumed via lightweight proxies. -- Rationale: Maintains reproducible builds, consistent release automation, and eliminates tooling drift. - -### IV. UI/Design System Consistency (NON-NEGOTIABLE) - -- A single design system governs the builder, renderer, and exported apps. -- Styling leverages Tailwind CSS v4 with shadcn/ui primitives via `@openzeppelin/ui-builder-styles` and root configs. -- Use the `cn` utility from `@openzeppelin/ui-builder-utils` for class composition. -- Prefer `lucide-react` icons; avoid emojis or inline raw SVG when reusable assets exist. -- Apply standard Tailwind sizing tokens; use arbitrary values only with documented justification. -- Follow form layout patterns such as `flex flex-col gap-2` or `space-y-*` for spacing consistency. -- Rationale: Guarantees cohesive UI/UX and reduces maintenance overhead across shared surfaces. - -### V. Testing, Documentation, and Exported Apps (NON-NEGOTIABLE) - -- Vitest is the standard for unit and integration tests; Storybook documents components and supports visual verification. -- Coverage metrics are tracked in CI; adapter docs and architectural references MUST remain current when interfaces change. -- Exported apps MUST build as standalone React + Vite bundles with runtime configuration supplied via `AppConfigService` (`public/app.config.json` for exports). -- Local exports reference `workspace:*`; production exports consume published `latest` versions. -- Feature flags gate functionality according to ecosystem readiness. -- Rationale: Ensures reliable distribution channels and prevents regressions in consumer-facing exports. - -### VI. Test-Driven Development for Business Logic (NON-NEGOTIABLE) - -- All business logic (services, adapters, validators, storage, networking, transforms) MUST follow TDD: write failing tests first, implement minimal code, then refactor. -- UI components in `.tsx` files are exempt but should leverage Storybook or interaction tests where feasible. -- Vitest remains the standard runner; use mocking only when essential to keep tests fast and focused. -- Rationale: Preserves confidence in critical logic and enforces disciplined implementation sequencing. - -### VII. Reuse-First Development (NON-NEGOTIABLE) - -- Existing utilities, types, services, and patterns MUST be preferred over new implementations unless reuse compromises integrity. -- Conduct thorough codebase analysis before planning or implementation to identify reusable modules (e.g., adapters, `AppConfigService`, transformers). -- Introduce new modules only when reuse is impossible; document rationale and alternatives. -- Extending `packages/types/src/adapters/base.ts` requires updating associated lint rules (e.g., `no-extra-adapter-methods`). -- Shared logic across adapters belongs in `packages/utils` or `packages/types`; avoid duplication of chain-agnostic behavior. -- Document reuse decisions in specs and plans; reviewers enforce compliance with this principle. -- Rationale: Minimizes redundancy, keeps abstractions aligned, and streamlines onboarding. - -## Additional Constraints - -- Do not add chain-specific code or polyfills to chain-agnostic packages; adapters own network concerns. -- Data schemas and shared types have a single source of truth in `packages/types`. -- Prefer shared utilities from `@openzeppelin/ui-builder-utils` (e.g., `logger`, `AppConfigService`, `cn`, ID generation); use lodash `debounce` when debouncing is required. -- Avoid noisy logging; rely on structured, level-based logs only when investigating issues. -- Contract comparisons MUST operate on raw contract definitions (ABI/IDL/etc.), not on internal `ContractSchema` representations. -- Security: do not hardcode secrets; use runtime configuration. - -## Development Workflow and Review Process - -- Use `pnpm` for all tasks (`pnpm dev`, `pnpm build`, `pnpm test`, `pnpm -r format:check`, `pnpm fix-all`). -- Commit messages follow Conventional Commits and pass commitlint; Commitizen (`pnpm commit`) is available. -- Before opening a PR, ensure tests, type checks, and linting pass workspace-wide; include a Changeset for public package changes. -- Adapter contributions MUST follow the Adapter Architecture Guide: network-aware constructor, exports, ecosystem registration, strict interface compliance. -- Code review enforces constitutional adherence, architecture boundaries, and design system consistency. -- Reuse-First Gate: reviewers verify reuse attempts before approving new modules. - -## Governance - -- This constitution supersedes other practices for architecture, quality, and workflow standards; non-negotiable rules MUST be enforced during development and review. -- Amendments require a documented proposal, updates to relevant guides/READMEs, migration notes if applicable, and approval via PR review. -- Breaking changes demand a Changeset with a major version bump and explicit upgrade notes; commit messages must flag breaking changes per convention. -- CI enforces compliance; PRs violating constitutional rules MUST be corrected before merge. - -**Version**: 1.2.1 | **Ratified**: 2025-09-17 | **Last Amended**: 2025-09-24 From d44ab4c185eacbd0ad040586d3946803dac89d7b Mon Sep 17 00:00:00 2001 From: Aleksandr Pasevin Date: Thu, 27 Nov 2025 00:28:20 +0200 Subject: [PATCH 2/2] refactor(ui): move sidebar components to ui package for reusability - Add SidebarButton, SidebarLayout, SidebarSection to @openzeppelin/ui-builder-ui - Refactor builder's AppSidebar to use generic components from ui package - Enable sidebar reuse across other projects like role-manager --- .changeset/move-sidebar-to-ui.md | 5 + .../Sidebar/AppSidebar/AppSidebar.tsx | 122 ++++-------------- .../Sidebar/AppSidebar/ContractUIsSection.tsx | 26 ++-- .../Sidebar/AppSidebar/MainActions.tsx | 2 +- .../Sidebar/AppSidebar/OtherToolsSection.tsx | 39 +++--- packages/ui/src/components/ui/index.ts | 1 + .../components/ui/sidebar}/SidebarButton.tsx | 54 ++++---- .../components/ui/sidebar/SidebarLayout.tsx | 109 ++++++++++++++++ .../components/ui/sidebar/SidebarSection.tsx | 39 ++++++ .../ui/src/components/ui/sidebar/index.ts | 3 + 10 files changed, 235 insertions(+), 165 deletions(-) create mode 100644 .changeset/move-sidebar-to-ui.md rename packages/{builder/src/components/Sidebar/AppSidebar => ui/src/components/ui/sidebar}/SidebarButton.tsx (70%) create mode 100644 packages/ui/src/components/ui/sidebar/SidebarLayout.tsx create mode 100644 packages/ui/src/components/ui/sidebar/SidebarSection.tsx create mode 100644 packages/ui/src/components/ui/sidebar/index.ts diff --git a/.changeset/move-sidebar-to-ui.md b/.changeset/move-sidebar-to-ui.md new file mode 100644 index 00000000..e68e4813 --- /dev/null +++ b/.changeset/move-sidebar-to-ui.md @@ -0,0 +1,5 @@ +--- +'@openzeppelin/ui-builder-ui': minor +--- + +Add reusable sidebar components (SidebarButton, SidebarLayout, SidebarSection) to enable sidebar reuse across projects diff --git a/packages/builder/src/components/Sidebar/AppSidebar/AppSidebar.tsx b/packages/builder/src/components/Sidebar/AppSidebar/AppSidebar.tsx index ab3d6252..40faec69 100644 --- a/packages/builder/src/components/Sidebar/AppSidebar/AppSidebar.tsx +++ b/packages/builder/src/components/Sidebar/AppSidebar/AppSidebar.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { cn } from '@openzeppelin/ui-builder-utils'; +import { SidebarLayout } from '@openzeppelin/ui-builder-ui'; import ContractUIImportDialog from '../ContractUIs/ContractUIImportDialog'; import SidebarContent from './SidebarContent'; @@ -21,7 +21,8 @@ interface AppSidebarProps { } /** - * Main application sidebar component with logo, actions, and saved Contract UIs + * Main application sidebar component with logo, actions, and saved Contract UIs. + * Uses the generic SidebarLayout from @openzeppelin/ui-builder-ui. */ export default function AppSidebar({ className, @@ -35,110 +36,35 @@ export default function AppSidebar({ }: AppSidebarProps) { const [showImportDialog, setShowImportDialog] = useState(false); - /** Shared sidebar scrollable content wrapper */ - const SidebarBody = ({ - paddingClass, - gapClass = 'gap-12', - onLoadContractUiHandler, - }: { - paddingClass: string; - gapClass?: string; - onLoadContractUiHandler: (id: string) => void; - }) => ( -
- setShowImportDialog(true)} - isInNewUIMode={isInNewUIMode} - onLoadContractUI={onLoadContractUiHandler} - onResetAfterDelete={onResetAfterDelete} - currentLoadedConfigurationId={currentLoadedConfigurationId} - gapClass={gapClass} - /> -
- ); + const handleLoadContractUI = (id: string) => { + // Close mobile sidebar when loading a contract UI + if (onOpenChange) { + onOpenChange(false); + } + onLoadContractUI?.(id); + }; return ( <> - {/* Sidebar */} - + {/* Import Dialog */} - - {/* Spacer to push content (desktop only) */} -
- - {/* Mobile slide-over */} - {typeof open === 'boolean' && onOpenChange && ( -
- {/* Backdrop */} -
onOpenChange(false)} - /> - {/* Panel */} -
-
-
- -
- { - onOpenChange(false); - onLoadContractUI?.(id); - }} - /> - {/* Mobile fixed footer icons */} -
- -
-
-
-
- )} ); } diff --git a/packages/builder/src/components/Sidebar/AppSidebar/ContractUIsSection.tsx b/packages/builder/src/components/Sidebar/AppSidebar/ContractUIsSection.tsx index 8fd37468..9beab6cf 100644 --- a/packages/builder/src/components/Sidebar/AppSidebar/ContractUIsSection.tsx +++ b/packages/builder/src/components/Sidebar/AppSidebar/ContractUIsSection.tsx @@ -1,3 +1,5 @@ +import { SidebarSection } from '@openzeppelin/ui-builder-ui'; + import { useContractUIStorage } from '../../../contexts/useContractUIStorage'; import ContractUIsList from '../ContractUIs/ContractUIsList'; @@ -26,22 +28,12 @@ export default function ContractUIsSection({ } return ( -
- {/* Section Header */} -
- {/* TODO: Replace hard-coded text color with OpenZeppelin theme */} - {/* Should use semantic token like 'text-sidebar-section-header' */} -
Contract UIs
-
- - {/* List Container */} -
- -
-
+ + + ); } diff --git a/packages/builder/src/components/Sidebar/AppSidebar/MainActions.tsx b/packages/builder/src/components/Sidebar/AppSidebar/MainActions.tsx index d5145de7..f87b13e5 100644 --- a/packages/builder/src/components/Sidebar/AppSidebar/MainActions.tsx +++ b/packages/builder/src/components/Sidebar/AppSidebar/MainActions.tsx @@ -6,12 +6,12 @@ import { SquarePen, } from 'lucide-react'; +import { SidebarButton } from '@openzeppelin/ui-builder-ui'; import { cn } from '@openzeppelin/ui-builder-utils'; import { useContractUIStorage } from '../../../contexts/useContractUIStorage'; import { useAnalytics } from '../../../hooks/useAnalytics'; import { recordHasMeaningfulContent } from '../../UIBuilder/utils/meaningfulContent'; -import SidebarButton from './SidebarButton'; interface MainActionsProps { onCreateNew?: () => void; diff --git a/packages/builder/src/components/Sidebar/AppSidebar/OtherToolsSection.tsx b/packages/builder/src/components/Sidebar/AppSidebar/OtherToolsSection.tsx index db72be13..ed9a4192 100644 --- a/packages/builder/src/components/Sidebar/AppSidebar/OtherToolsSection.tsx +++ b/packages/builder/src/components/Sidebar/AppSidebar/OtherToolsSection.tsx @@ -1,8 +1,8 @@ +import { SidebarButton, SidebarSection } from '@openzeppelin/ui-builder-ui'; import { appConfigService } from '@openzeppelin/ui-builder-utils'; import ContractsWizardIconSvg from '../../../assets/icons/contracts-wizard-icon.svg'; import { DevToolsDropdown } from '../../Common/DevToolsDropdown'; -import SidebarButton from './SidebarButton'; /** * Other Tools section component for the sidebar @@ -12,27 +12,22 @@ export default function OtherToolsSection() { const showDevTools = appConfigService.isFeatureEnabled('show_dev_tools'); return ( -
- {/* TODO: Replace hard-coded text color with OpenZeppelin theme */} - {/* Should use semantic token like 'text-sidebar-section-header' */} -
Other Tools
-
- } - href="https://wizard.openzeppelin.com/" - target="_blank" - rel="noopener noreferrer" - > - Contracts Wizard - + + } + href="https://wizard.openzeppelin.com/" + target="_blank" + rel="noopener noreferrer" + > + Contracts Wizard + - {/* Dev Tools - Only shown when feature flag is enabled */} - {showDevTools && ( -
- -
- )} -
-
+ {/* Dev Tools - Only shown when feature flag is enabled */} + {showDevTools && ( +
+ +
+ )} + ); } diff --git a/packages/ui/src/components/ui/index.ts b/packages/ui/src/components/ui/index.ts index eb600790..9f350439 100644 --- a/packages/ui/src/components/ui/index.ts +++ b/packages/ui/src/components/ui/index.ts @@ -18,6 +18,7 @@ export * from './network-status-badge'; export * from './progress'; export * from './radio-group'; export * from './select'; +export * from './sidebar'; export * from './tabs'; export * from './textarea'; export * from './tooltip'; diff --git a/packages/builder/src/components/Sidebar/AppSidebar/SidebarButton.tsx b/packages/ui/src/components/ui/sidebar/SidebarButton.tsx similarity index 70% rename from packages/builder/src/components/Sidebar/AppSidebar/SidebarButton.tsx rename to packages/ui/src/components/ui/sidebar/SidebarButton.tsx index f14ad88d..f5d9bb5b 100644 --- a/packages/builder/src/components/Sidebar/AppSidebar/SidebarButton.tsx +++ b/packages/ui/src/components/ui/sidebar/SidebarButton.tsx @@ -1,9 +1,9 @@ -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { cn } from '@openzeppelin/ui-builder-utils'; -interface SidebarButtonProps { - icon: ReactNode; +export interface SidebarButtonProps { + icon?: ReactNode; children: ReactNode; onClick?: () => void; size?: 'default' | 'small'; @@ -14,12 +14,14 @@ interface SidebarButtonProps { href?: string; target?: React.HTMLAttributeAnchorTarget; rel?: string; + className?: string; } /** - * Shared button component for sidebar actions with consistent styling + * A styled button component for sidebar actions with consistent styling. + * Can render as a button or anchor element depending on whether href is provided. */ -export default function SidebarButton({ +export function SidebarButton({ icon, children, onClick, @@ -30,12 +32,11 @@ export default function SidebarButton({ href, target, rel, -}: SidebarButtonProps) { + className, +}: SidebarButtonProps): React.ReactElement { const height = size === 'small' ? 'h-10' : 'h-11'; const commonClass = cn( - // TODO: Replace hard-coded text colors with OpenZeppelin theme - // Should use semantic tokens like 'text-sidebar-button hover:text-sidebar-button-hover' 'group relative flex items-center gap-2 px-3 py-2.5 rounded-lg font-semibold text-sm transition-colors', badge ? 'justify-between' : 'justify-start', disabled @@ -43,27 +44,12 @@ export default function SidebarButton({ : isSelected ? 'text-[#111928] bg-neutral-100' : 'text-gray-600 hover:text-gray-700 cursor-pointer hover:before:content-[""] hover:before:absolute hover:before:inset-x-0 hover:before:top-1 hover:before:bottom-1 hover:before:bg-muted/80 hover:before:rounded-lg hover:before:-z-10', - height + height, + className ); - if (href) { - return ( - -
- {icon} - {children} -
- {badge && ( - - {badge} - - )} -
- ); - } - - return ( - ); } diff --git a/packages/ui/src/components/ui/sidebar/SidebarLayout.tsx b/packages/ui/src/components/ui/sidebar/SidebarLayout.tsx new file mode 100644 index 00000000..e7ddb1ff --- /dev/null +++ b/packages/ui/src/components/ui/sidebar/SidebarLayout.tsx @@ -0,0 +1,109 @@ +import React, { ReactNode } from 'react'; + +import { cn } from '@openzeppelin/ui-builder-utils'; + +export interface SidebarLayoutProps { + /** Content for the sidebar header (e.g., logo) */ + header?: ReactNode; + /** Main scrollable content area */ + children: ReactNode; + /** Content for the fixed footer (e.g., nav icons) */ + footer?: ReactNode; + /** Additional CSS classes for the sidebar container */ + className?: string; + /** Width of the sidebar (default: 289px) */ + width?: number | string; + /** Background color/class for the sidebar */ + background?: string; + /** Controls visibility in mobile slide-over */ + mobileOpen?: boolean; + /** Close handler for mobile slide-over */ + onMobileOpenChange?: (open: boolean) => void; + /** Aria label for mobile dialog */ + mobileAriaLabel?: string; +} + +/** + * A flexible sidebar layout component with desktop sidebar and mobile slide-over. + * Provides slots for header, scrollable content, and footer. + */ +export function SidebarLayout({ + header, + children, + footer, + className, + width = 289, + background = 'bg-[rgba(245,245,245,0.31)]', + mobileOpen, + onMobileOpenChange, + mobileAriaLabel = 'Menu', +}: SidebarLayoutProps): React.ReactElement { + const widthStyle = typeof width === 'number' ? `${width}px` : width; + + return ( + <> + {/* Desktop Sidebar */} + + + {/* Spacer to push content (desktop only) */} +
+ + {/* Mobile slide-over */} + {typeof mobileOpen === 'boolean' && onMobileOpenChange && ( +
+ {/* Backdrop */} +
onMobileOpenChange(false)} + /> + {/* Panel */} +
+
+ {/* Mobile Header */} + {header &&
{header}
} + + {/* Mobile Scrollable Content */} +
{children}
+ + {/* Mobile Footer */} + {footer &&
{footer}
} +
+
+
+ )} + + ); +} diff --git a/packages/ui/src/components/ui/sidebar/SidebarSection.tsx b/packages/ui/src/components/ui/sidebar/SidebarSection.tsx new file mode 100644 index 00000000..ec1ba531 --- /dev/null +++ b/packages/ui/src/components/ui/sidebar/SidebarSection.tsx @@ -0,0 +1,39 @@ +import React, { ReactNode } from 'react'; + +import { cn } from '@openzeppelin/ui-builder-utils'; + +export interface SidebarSectionProps { + /** Optional section title displayed above the content */ + title?: string; + /** Content to render within the section */ + children: ReactNode; + /** Additional CSS classes */ + className?: string; + /** CSS classes for the title element */ + titleClassName?: string; + /** Whether this section should grow to fill available space */ + grow?: boolean; +} + +/** + * A generic sidebar section wrapper with optional title. + * Used to group related sidebar items together. + */ +export function SidebarSection({ + title, + children, + className, + titleClassName, + grow = false, +}: SidebarSectionProps): React.ReactElement { + return ( +
+ {title && ( +
+ {title} +
+ )} +
{children}
+
+ ); +} diff --git a/packages/ui/src/components/ui/sidebar/index.ts b/packages/ui/src/components/ui/sidebar/index.ts new file mode 100644 index 00000000..ef30280b --- /dev/null +++ b/packages/ui/src/components/ui/sidebar/index.ts @@ -0,0 +1,3 @@ +export { SidebarButton, type SidebarButtonProps } from './SidebarButton'; +export { SidebarLayout, type SidebarLayoutProps } from './SidebarLayout'; +export { SidebarSection, type SidebarSectionProps } from './SidebarSection';