From c01e15f5c9524aa74cc90dfd6c3e7432380a68a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 07:12:55 +0000 Subject: [PATCH 01/12] feat: add custom ZendeskTriggerButton example Add example implementation showing how to customize the Zendesk trigger button component with custom styling. Co-authored-by: gabrielremote --- example/src/Components.tsx | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/example/src/Components.tsx b/example/src/Components.tsx index 511ad33fd..486ec66cc 100644 --- a/example/src/Components.tsx +++ b/example/src/Components.tsx @@ -8,6 +8,7 @@ import type { PDFPreviewComponentProps, TelFieldComponentProps, TimeFieldComponentProps, + ZendeskTriggerButtonComponentProps, } from '@remoteoss/remote-flows'; import { FileUploader } from '@remoteoss/remote-flows/internals'; import { splitAccordionDescription } from './utils/transformHtml'; @@ -491,6 +492,35 @@ const TimeField = ({ ); }; +const ZendeskTriggerButton = ({ + zendeskId, + onClick, + children, + className, +}: ZendeskTriggerButtonComponentProps) => { + const handleClick = () => { + onClick?.(zendeskId); + }; + + return ( + + ); +}; + export const components: Components = { button: Button, text: Input, @@ -506,5 +536,6 @@ export const components: Components = { pdfViewer: PDFPreview, tel: TelField, time: TimeField, + zendeskTriggerButton: ZendeskTriggerButton, //zendeskDrawer: ZendeskDialog, }; From 9edd18da8231b90753c85bd5bbdc7e9ae6f4154f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 07:13:04 +0000 Subject: [PATCH 02/12] feat: add customization support for ZendeskTriggerButton component - Add ZendeskTriggerButtonComponentProps type - Add zendeskTriggerButton to Components type - Update ZendeskTriggerButton to support custom component override - Add comprehensive tests for custom component usage - Update documentation with examples and usage guide This allows consumers to override the trigger button with their own components, avoiding issues with Tailwind utility classes and enabling better integration with custom design systems. Co-authored-by: gabrielremote --- docs/COMPONENT_CUSTOMIZATION.md | 72 ++++++++++++++ .../zendesk-drawer/ZendeskTriggerButton.tsx | 32 ++++++ .../tests/ZendeskTriggerButton.test.tsx | 98 +++++++++++++++++++ src/index.tsx | 1 + src/types/remoteFlows.ts | 27 +++++ 5 files changed, 230 insertions(+) diff --git a/docs/COMPONENT_CUSTOMIZATION.md b/docs/COMPONENT_CUSTOMIZATION.md index c70d04d1a..c36882e19 100644 --- a/docs/COMPONENT_CUSTOMIZATION.md +++ b/docs/COMPONENT_CUSTOMIZATION.md @@ -107,6 +107,7 @@ Available component types include: - `table` - Table components - `drawer` - Drawer components - `zendeskDrawer` - Zendesk drawer components +- `zendeskTriggerButton` - Zendesk trigger button components - `pdfViewer` - PDF viewer component - `tel` - Tel field component - `time` - Time field component @@ -117,6 +118,7 @@ and their typescript definitions - `ButtonComponentProps`: For custom button components - `StatementComponentProps`: For custom statement components - `ZendeskDrawerComponentProps`: For custom zendesk drawer +- `ZendeskTriggerButtonComponentProps`: For custom zendesk trigger button - `FileComponentProps`: for custom file field components - `CountryComponentProps`: for country field components - `TextFieldComponentProps`: for textfield components @@ -393,6 +395,76 @@ type JSFCustomComponentProps = { }; ``` +### ZendeskTriggerButtonComponentProps + +For custom Zendesk trigger button components: + +```tsx +import { ZendeskTriggerButtonComponentProps } from '@remoteoss/remote-flows'; + +type ZendeskTriggerButtonComponentProps = { + zendeskId: number; + className?: string; + onClick?: (zendeskId: number) => void; + children?: React.ReactNode; + external?: boolean; +} & Record; +``` + +**Example: Custom Zendesk Trigger Button** + +```tsx +import { + RemoteFlows, + ZendeskTriggerButtonComponentProps, +} from '@remoteoss/remote-flows'; + +const CustomZendeskTriggerButton = ({ + zendeskId, + onClick, + children, + className, + external, +}: ZendeskTriggerButtonComponentProps) => { + const handleClick = () => { + onClick?.(zendeskId); + }; + + if (external) { + return ( + + {children} + + ); + } + + return ( + + ); +}; + +function App() { + return ( + + {/* All flows will use this custom Zendesk trigger button */} + + ); +} +``` + ## When to Use Each Method ### Use Global Override When: diff --git a/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx b/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx index c4594588b..328dd015e 100644 --- a/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx +++ b/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx @@ -2,6 +2,7 @@ import { cn } from '@/src/lib/utils'; import { ZendeskDrawer } from './ZendeskDrawer'; import { buildZendeskURL } from './utils'; import { useState } from 'react'; +import { useFormFields } from '@/src/context'; interface ZendeskTriggerButtonProps { /** @@ -36,6 +37,7 @@ export function ZendeskTriggerButton({ children, external = false, }: ZendeskTriggerButtonProps) { + const { components } = useFormFields(); const [isOpen, setIsOpen] = useState(false); const handleClick = () => { @@ -45,6 +47,36 @@ export function ZendeskTriggerButton({ onClick?.(zendeskId); }; + const CustomZendeskTriggerButton = components?.zendeskTriggerButton; + + // If a custom trigger button is provided, use it + if (CustomZendeskTriggerButton) { + const customTriggerElement = ( + + {children} + + ); + + if (external) { + return customTriggerElement; + } + + return ( + setIsOpen(false)} + Trigger={customTriggerElement} + /> + ); + } + + // Default implementation if (external) { return ( { const mockArticle = { @@ -176,4 +177,101 @@ describe('ZendeskTriggerButton', () => { expect(links).toHaveLength(1); }); }); + + describe('with custom trigger button component', () => { + const CustomTriggerButton = ({ + zendeskId, + onClick, + children, + className, + }: ZendeskTriggerButtonComponentProps) => { + return ( + + ); + }; + + const WrapperWithCustomButton = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + it('renders custom trigger button when provided', () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + const customButton = screen.getByTestId('custom-trigger'); + expect(customButton).toBeInTheDocument(); + expect(customButton).toHaveTextContent('Custom: Open Article'); + }); + + it('calls onClick when custom trigger button is clicked', async () => { + const onClick = vi.fn(); + + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + await userEvent.click(screen.getByTestId('custom-trigger')); + expect(onClick).toHaveBeenCalledWith(123456); + }); + + it('opens drawer when custom trigger button is clicked and external is false', async () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + await userEvent.click(screen.getByTestId('custom-trigger')); + + // Wait for drawer to load and display content + const title = await screen.findByText('Test Article'); + expect(title).toBeInTheDocument(); + }); + + it('does not open drawer when custom trigger button is clicked and external is true', async () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + await userEvent.click(screen.getByTestId('custom-trigger')); + + // Drawer should not appear + const title = screen.queryByText('Test Article'); + expect(title).not.toBeInTheDocument(); + }); + + it('passes className to custom trigger button', () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + const customButton = screen.getByTestId('custom-trigger'); + expect(customButton).toHaveClass('custom-class-from-parent'); + }); + }); }); diff --git a/src/index.tsx b/src/index.tsx index 95163a77d..90f5f148b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -152,6 +152,7 @@ export type { FieldSetToggleComponentProps, ButtonComponentProps, ZendeskDrawerComponentProps, + ZendeskTriggerButtonComponentProps, DrawerComponentProps, PDFPreviewComponentProps, Meta, diff --git a/src/types/remoteFlows.ts b/src/types/remoteFlows.ts index 509144ac3..33f5c6626 100644 --- a/src/types/remoteFlows.ts +++ b/src/types/remoteFlows.ts @@ -104,6 +104,32 @@ export type ZendeskDrawerComponentProps = { Trigger: React.ReactElement; }; +/** + * Props for custom Zendesk trigger button components. + */ +export type ZendeskTriggerButtonComponentProps = { + /** + * The Zendesk ID for the help article + */ + zendeskId: number; + /** + * The class name for the button + */ + className?: string; + /** + * The callback function to be called when the button is clicked + */ + onClick?: (zendeskId: number) => void; + /** + * The children to be rendered inside the button + */ + children?: React.ReactNode; + /** + * Whether to open the help article in a new tab + */ + external?: boolean; +} & Record; + export type DrawerComponentProps = { open: boolean; onOpenChange: (open: boolean) => void; @@ -156,6 +182,7 @@ export type Components = { button?: React.ComponentType; fieldsetToggle?: React.ComponentType; zendeskDrawer?: React.ComponentType; + zendeskTriggerButton?: React.ComponentType; drawer?: React.ComponentType; table?: React.ComponentType; 'work-schedule'?: React.ComponentType; From bed2cb802e6f5f2d41afd1a6cd9bea1539a7e349 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 10:22:05 +0200 Subject: [PATCH 03/12] docs: add cursor rules for component customization patterns Add two new rules to prevent common issues when adding customizable components: 1. component-documentation.mdc - Ensures docs/COMPONENT_CUSTOMIZATION.md stays in sync when new components are added 2. component-pattern.mdc - Enforces the Main + Default component pattern with lazy loading These rules address issues found in PR #1262 where documentation updates and architectural patterns were missed. Related: PBYR-4544 --- .cursor/rules/component-documentation.mdc | 75 +++++++++ .cursor/rules/component-pattern.mdc | 191 ++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 .cursor/rules/component-documentation.mdc create mode 100644 .cursor/rules/component-pattern.mdc diff --git a/.cursor/rules/component-documentation.mdc b/.cursor/rules/component-documentation.mdc new file mode 100644 index 000000000..11917e8f4 --- /dev/null +++ b/.cursor/rules/component-documentation.mdc @@ -0,0 +1,75 @@ +# Component Documentation Rule + +## Philosophy: Keep Documentation in Sync + +When adding a new customizable component to the `Components` type, documentation must be updated immediately. This ensures library consumers can discover and use new customization options. + +## Required Updates + +When adding a new customizable component: + +### 1. Update docs/COMPONENT_CUSTOMIZATION.md + +Add the component type to the **"Available component types"** list (around line 92-113): + +```markdown +- `componentName` - Component description +``` + +Add the TypeScript type to the **"and their typescript definitions"** list (around line 115-130): + +```markdown +- `ComponentNameComponentProps`: For custom componentName components +``` + +If the component has unique props or usage patterns, add an example section similar to the existing `ZendeskTriggerButtonComponentProps` example (lines 398-467). + +### 2. Verify Exports + +Ensure the component props type is exported from: +- `src/types/remoteFlows.ts` - Type definition +- `src/index.tsx` - Public API export + +### 3. When to Update + +**Always check and update this documentation when:** +- Adding a new field to `Components` type in `src/types/remoteFlows.ts` +- Creating a new customizable component +- Exporting a new component props type from `src/index.tsx` +- Adding a new field type that consumers can override + +## Example Pattern + +For a new `CustomWidget` component: + +1. **docs/COMPONENT_CUSTOMIZATION.md** (line ~113): +```markdown +- `customWidget` - Custom widget component +``` + +2. **docs/COMPONENT_CUSTOMIZATION.md** (line ~130): +```markdown +- `CustomWidgetComponentProps`: For custom widget components +``` + +3. **Verify exports**: +```typescript +// src/types/remoteFlows.ts +export type CustomWidgetComponentProps = { /* ... */ }; + +// src/index.tsx +export type { CustomWidgetComponentProps } from '@/src/types/remoteFlows'; +``` + +## Red Flags + +If you find yourself: +- Adding a type to `Components` without updating docs +- Exporting a new `*ComponentProps` type without documenting it +- Implementing a custom component without checking the docs + +**STOP** and update the documentation first. + +## Remember + +The documentation is the contract with library consumers. Missing documentation means features that are undiscoverable and unused. diff --git a/.cursor/rules/component-pattern.mdc b/.cursor/rules/component-pattern.mdc new file mode 100644 index 000000000..57cfe3a7f --- /dev/null +++ b/.cursor/rules/component-pattern.mdc @@ -0,0 +1,191 @@ +# Component Pattern Rule + +## Philosophy: Separation of Concerns + +All customizable components in Remote Flows follow a consistent pattern: **main component** (logic) + **default component** (presentation). This separation enables lazy loading, reduces bundle size, and maintains consistency. + +## The Pattern + +### Structure + +``` +src/ +├── components/ +│ ├── form/fields/ +│ │ ├── ComponentName.tsx # Main component with logic +│ │ └── default/ +│ │ └── ComponentNameDefault.tsx # Default implementation +│ └── shared/ +│ └── feature-name/ +│ ├── ComponentName.tsx +│ └── default/ +│ └── ComponentNameDefault.tsx +``` + +### Main Component (Logic) + +**Example**: `ForcedValueField.tsx` + +```typescript +import { useFormFields } from '@/src/context'; + +export function ComponentName(props) { + const { components } = useFormFields(); + + // Business logic here + const processedData = /* ... */; + + const Component = components?.componentName; + + if (!Component) { + throw new Error(`Component not found for field ${name}`); + } + + return ; +} +``` + +**Responsibilities:** +- Business logic and data processing +- Context consumption +- Component resolution via `useFormFields()` +- Error handling (throw if component not found) +- **NO inline rendering** of default UI + +### Default Component (Presentation) + +**Example**: `ForcedValueFieldDefault.tsx` + +```typescript +import { ComponentNameComponentProps } from '@/src/types/remoteFlows'; + +export function ComponentNameDefault({ fieldData }: ComponentNameComponentProps) { + return ( +
+ {/* Pure presentation - no business logic */} +
+ ); +} +``` + +**Responsibilities:** +- Pure presentation component +- Receives props from main component +- No context consumption +- No business logic +- Lives in `default/` subdirectory + +### Lazy Loading Registration + +**Always add to** `src/lazy-default-components.ts`: + +```typescript +export const lazyDefaultComponents: Components = { + componentName: lazy(() => + import('./components/path/to/default/ComponentNameDefault').then((m) => ({ + default: m.ComponentNameDefault, + })), + ), + // ... other components +}; +``` + +## Complete Implementation Checklist + +When adding a new customizable component: + +### 1. Component Files +- [ ] Create `ComponentName.tsx` with business logic +- [ ] Create `default/ComponentNameDefault.tsx` with presentation +- [ ] Main component uses `useFormFields()` to get custom component +- [ ] Main component throws error if component not found +- [ ] No inline default rendering in main component + +### 2. Type Definitions +- [ ] Export `ComponentNameComponentProps` in `src/types/remoteFlows.ts` +- [ ] Add `componentName?: React.ComponentType` to `Components` type +- [ ] Export props type from `src/index.tsx` + +### 3. Lazy Loading +- [ ] Add to `src/lazy-default-components.ts` using `React.lazy()` +- [ ] Import path points to Default component file +- [ ] Test that import path is correct + +### 4. Documentation +- [ ] Update `docs/COMPONENT_CUSTOMIZATION.md` (see component-documentation.mdc) + +### 5. Tests +- [ ] Create test file in `tests/` subdirectory +- [ ] Test default rendering +- [ ] Test custom component override +- [ ] Test props are passed correctly +- [ ] Use `TestProviders` with custom components prop + +### 6. Validation +- [ ] Run `npm run format` +- [ ] Run `npm run lint` +- [ ] Run `npm run type-check` +- [ ] Run `npm test` + +## Examples from Codebase + +### Good Examples + +**ForcedValueField** (lines 1-67): +- Main component: `src/components/form/fields/ForcedValueField.tsx` +- Default: `src/components/form/fields/default/ForcedValueFieldDefault.tsx` +- Lazy: `src/lazy-default-components.ts` (lines 63-69) + +**ZendeskDrawer** (lines 1-47): +- Main component: `src/components/shared/zendesk-drawer/ZendeskDrawer.tsx` +- Default: `src/components/shared/zendesk-drawer/ZendeskDrawerDefault.tsx` +- Follows pattern correctly + +### Anti-Pattern (What NOT to Do) + +```typescript +// ❌ BAD: Inline default rendering in main component +export function ComponentName(props) { + const { components } = useFormFields(); + const CustomComponent = components?.componentName; + + if (CustomComponent) { + return ; + } + + // ❌ Don't do this - create a Default component instead + return ( +
+ Default rendering here +
+ ); +} +``` + +**Why this is bad:** +- No lazy loading (default always included in bundle) +- Violates separation of concerns +- Inconsistent with codebase patterns +- Makes testing harder + +## Why This Matters + +1. **Bundle size**: Defaults are lazy-loaded only when needed +2. **Consistency**: All components follow the same pattern +3. **Maintenance**: Changes to default styling happen in one place +4. **Testing**: Easier to test logic separately from presentation +5. **Type safety**: Props are explicitly typed and exported + +## Red Flags + +Stop and refactor if you find yourself: +- Writing inline JSX in the main component for default case +- Not creating a Default component file +- Skipping the lazy-default-components.ts registration +- Creating a component that doesn't follow this structure + +## Remember + +**Every customizable component = Main (logic) + Default (presentation) + Lazy loading** + +No exceptions. This pattern is enforced by the codebase architecture and bundle size limits. From 200c1c1f951a5e0232892c3d547769f027cbd5ba Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 10:24:01 +0200 Subject: [PATCH 04/12] refactor: extract ZendeskTriggerButton default implementation Follow the established component pattern by separating business logic from presentation: - Create ZendeskTriggerButtonDefault.tsx with presentation logic - Refactor ZendeskTriggerButton.tsx to use component from context - Add to lazy-default-components.ts for lazy loading - Add to default-components.ts for tests This follows the pattern used by all other customizable components (ForcedValueField, ZendeskDrawer, etc.) where: - Main component contains business logic and requires component from context - Default component is pure presentation - Default is lazy-loaded to reduce bundle size - Tests use non-lazy default components Benefits: - Reduces bundle size for users who customize the component - Maintains consistency with codebase patterns - Separates concerns (logic vs presentation) - Enables proper lazy loading Related: PBYR-4544 --- .../zendesk-drawer/ZendeskTriggerButton.tsx | 62 +++++-------------- .../default/ZendeskTriggerButtonDefault.tsx | 38 ++++++++++++ src/default-components.ts | 2 + src/lazy-default-components.ts | 7 +++ 4 files changed, 62 insertions(+), 47 deletions(-) create mode 100644 src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault.tsx diff --git a/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx b/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx index 328dd015e..2be94a2de 100644 --- a/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx +++ b/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx @@ -1,6 +1,4 @@ -import { cn } from '@/src/lib/utils'; import { ZendeskDrawer } from './ZendeskDrawer'; -import { buildZendeskURL } from './utils'; import { useState } from 'react'; import { useFormFields } from '@/src/context'; @@ -27,9 +25,6 @@ interface ZendeskTriggerButtonProps { external?: boolean; } -const baseClassName = - 'RemoteFlows__ZendeskTriggerButton text-blue-500 hover:underline inline-block text-xs bg-transparent border-none cursor-pointer p-0'; - export function ZendeskTriggerButton({ zendeskId, className, @@ -49,46 +44,23 @@ export function ZendeskTriggerButton({ const CustomZendeskTriggerButton = components?.zendeskTriggerButton; - // If a custom trigger button is provided, use it - if (CustomZendeskTriggerButton) { - const customTriggerElement = ( - - {children} - - ); - - if (external) { - return customTriggerElement; - } - - return ( - setIsOpen(false)} - Trigger={customTriggerElement} - /> - ); + if (!CustomZendeskTriggerButton) { + throw new Error(`Zendesk trigger button component not found`); } - // Default implementation + const triggerElement = ( + + {children} + + ); + if (external) { - return ( -
- {children} - - ); + return triggerElement; } return ( @@ -96,11 +68,7 @@ export function ZendeskTriggerButton({ zendeskId={zendeskId} open={isOpen} onClose={() => setIsOpen(false)} - Trigger={ - - } + Trigger={triggerElement} /> ); } diff --git a/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault.tsx b/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault.tsx new file mode 100644 index 000000000..26543e8d3 --- /dev/null +++ b/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault.tsx @@ -0,0 +1,38 @@ +import { ZendeskTriggerButtonComponentProps } from '@/src/types/remoteFlows'; +import { cn } from '@/src/lib/utils'; +import { buildZendeskURL } from '../utils'; + +const baseClassName = + 'RemoteFlows__ZendeskTriggerButton text-blue-500 hover:underline inline-block text-xs bg-transparent border-none cursor-pointer p-0'; + +export function ZendeskTriggerButtonDefault({ + zendeskId, + onClick, + children, + className, + external, +}: ZendeskTriggerButtonComponentProps) { + const handleClick = () => { + onClick?.(zendeskId); + }; + + if (external) { + return ( + + {children} + + ); + } + + return ( + + ); +} diff --git a/src/default-components.ts b/src/default-components.ts index 7ef41d213..a8d96b881 100644 --- a/src/default-components.ts +++ b/src/default-components.ts @@ -14,6 +14,7 @@ import { TextFieldDefault } from '@/src/components/form/fields/default/TextField import { FieldsetToggleButtonDefault } from '@/src/components/form/fields/default/FieldsetToggleButtonDefault'; import { DrawerDefault } from '@/src/components/shared/drawer/DrawerDefault'; import { ZendeskDrawerDefault } from '@/src/components/shared/zendesk-drawer/ZendeskDrawerDefault'; +import { ZendeskTriggerButtonDefault } from '@/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault'; import { TableFieldDefault } from '@/src/components/shared/table/TableFieldDefault'; import { CheckboxFieldDefault } from '@/src/components/form/fields/default/CheckboxFieldDefault'; import { WorkScheduleFieldDefault } from '@/src/components/form/fields/default/WorkScheduleFieldDefault'; @@ -47,6 +48,7 @@ export const defaultComponents: Components = { textarea: TextAreaFieldDefault, text: TextFieldDefault, zendeskDrawer: ZendeskDrawerDefault, + zendeskTriggerButton: ZendeskTriggerButtonDefault, table: TableFieldDefault, 'work-schedule': WorkScheduleFieldDefault, pdfViewer: PDFPreviewDefault, diff --git a/src/lazy-default-components.ts b/src/lazy-default-components.ts index b66fe20b1..5550151f4 100644 --- a/src/lazy-default-components.ts +++ b/src/lazy-default-components.ts @@ -142,4 +142,11 @@ export const lazyDefaultComponents: Components = { }), ), ), + zendeskTriggerButton: lazy(() => + import('./components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault').then( + (m) => ({ + default: m.ZendeskTriggerButtonDefault, + }), + ), + ), }; From 3cc61e32d17d3ba32dfa688101f0051080eb3e11 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 10:25:11 +0200 Subject: [PATCH 05/12] fix --- .cursor/rules/component-documentation.mdc | 6 ++++++ .cursor/rules/component-pattern.mdc | 12 ++++++++++++ .../tests/ZendeskTriggerButton.test.tsx | 6 +++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.cursor/rules/component-documentation.mdc b/.cursor/rules/component-documentation.mdc index 11917e8f4..65700fe52 100644 --- a/.cursor/rules/component-documentation.mdc +++ b/.cursor/rules/component-documentation.mdc @@ -27,12 +27,14 @@ If the component has unique props or usage patterns, add an example section simi ### 2. Verify Exports Ensure the component props type is exported from: + - `src/types/remoteFlows.ts` - Type definition - `src/index.tsx` - Public API export ### 3. When to Update **Always check and update this documentation when:** + - Adding a new field to `Components` type in `src/types/remoteFlows.ts` - Creating a new customizable component - Exporting a new component props type from `src/index.tsx` @@ -43,16 +45,19 @@ Ensure the component props type is exported from: For a new `CustomWidget` component: 1. **docs/COMPONENT_CUSTOMIZATION.md** (line ~113): + ```markdown - `customWidget` - Custom widget component ``` 2. **docs/COMPONENT_CUSTOMIZATION.md** (line ~130): + ```markdown - `CustomWidgetComponentProps`: For custom widget components ``` 3. **Verify exports**: + ```typescript // src/types/remoteFlows.ts export type CustomWidgetComponentProps = { /* ... */ }; @@ -64,6 +69,7 @@ export type { CustomWidgetComponentProps } from '@/src/types/remoteFlows'; ## Red Flags If you find yourself: + - Adding a type to `Components` without updating docs - Exporting a new `*ComponentProps` type without documenting it - Implementing a custom component without checking the docs diff --git a/.cursor/rules/component-pattern.mdc b/.cursor/rules/component-pattern.mdc index 57cfe3a7f..ee28503f2 100644 --- a/.cursor/rules/component-pattern.mdc +++ b/.cursor/rules/component-pattern.mdc @@ -46,6 +46,7 @@ export function ComponentName(props) { ``` **Responsibilities:** + - Business logic and data processing - Context consumption - Component resolution via `useFormFields()` @@ -69,6 +70,7 @@ export function ComponentNameDefault({ fieldData }: ComponentNameComponentProps) ``` **Responsibilities:** + - Pure presentation component - Receives props from main component - No context consumption @@ -95,6 +97,7 @@ export const lazyDefaultComponents: Components = { When adding a new customizable component: ### 1. Component Files + - [ ] Create `ComponentName.tsx` with business logic - [ ] Create `default/ComponentNameDefault.tsx` with presentation - [ ] Main component uses `useFormFields()` to get custom component @@ -102,19 +105,23 @@ When adding a new customizable component: - [ ] No inline default rendering in main component ### 2. Type Definitions + - [ ] Export `ComponentNameComponentProps` in `src/types/remoteFlows.ts` - [ ] Add `componentName?: React.ComponentType` to `Components` type - [ ] Export props type from `src/index.tsx` ### 3. Lazy Loading + - [ ] Add to `src/lazy-default-components.ts` using `React.lazy()` - [ ] Import path points to Default component file - [ ] Test that import path is correct ### 4. Documentation + - [ ] Update `docs/COMPONENT_CUSTOMIZATION.md` (see component-documentation.mdc) ### 5. Tests + - [ ] Create test file in `tests/` subdirectory - [ ] Test default rendering - [ ] Test custom component override @@ -122,6 +129,7 @@ When adding a new customizable component: - [ ] Use `TestProviders` with custom components prop ### 6. Validation + - [ ] Run `npm run format` - [ ] Run `npm run lint` - [ ] Run `npm run type-check` @@ -132,11 +140,13 @@ When adding a new customizable component: ### Good Examples **ForcedValueField** (lines 1-67): + - Main component: `src/components/form/fields/ForcedValueField.tsx` - Default: `src/components/form/fields/default/ForcedValueFieldDefault.tsx` - Lazy: `src/lazy-default-components.ts` (lines 63-69) **ZendeskDrawer** (lines 1-47): + - Main component: `src/components/shared/zendesk-drawer/ZendeskDrawer.tsx` - Default: `src/components/shared/zendesk-drawer/ZendeskDrawerDefault.tsx` - Follows pattern correctly @@ -163,6 +173,7 @@ export function ComponentName(props) { ``` **Why this is bad:** + - No lazy loading (default always included in bundle) - Violates separation of concerns - Inconsistent with codebase patterns @@ -179,6 +190,7 @@ export function ComponentName(props) { ## Red Flags Stop and refactor if you find yourself: + - Writing inline JSX in the main component for default case - Not creating a Default component file - Skipping the lazy-default-components.ts registration diff --git a/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx b/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx index 5854e0b2b..bb799ade2 100644 --- a/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx +++ b/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx @@ -196,7 +196,11 @@ describe('ZendeskTriggerButton', () => { ); }; - const WrapperWithCustomButton = ({ children }: { children: React.ReactNode }) => ( + const WrapperWithCustomButton = ({ + children, + }: { + children: React.ReactNode; + }) => ( {children} From 3dbfe76242f24b7c0c84b9da0896b1aa502e1edc Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 10:28:07 +0200 Subject: [PATCH 06/12] run format on agent --- .cursor/hooks.json | 18 ++++++++++++++ .cursor/hooks/auto-format.sh | 25 +++++++++++++++++++ .cursor/hooks/validate-all.sh | 46 +++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 .cursor/hooks.json create mode 100644 .cursor/hooks/auto-format.sh create mode 100644 .cursor/hooks/validate-all.sh diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 000000000..a9a32fdf9 --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "hooks": { + "afterFileEdit": [ + { + "command": ".cursor/hooks/auto-format.sh", + "matcher": ".*\\.(ts|tsx|js|jsx|json|md|mdc)$", + "timeout": 60 + } + ], + "stop": [ + { + "command": ".cursor/hooks/validate-all.sh", + "timeout": 120 + } + ] + } +} diff --git a/.cursor/hooks/auto-format.sh b/.cursor/hooks/auto-format.sh new file mode 100644 index 000000000..49b64c2a4 --- /dev/null +++ b/.cursor/hooks/auto-format.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Auto-format hook: runs after file edits to ensure consistent formatting + +set -euo pipefail + +# Read hook input from stdin +input=$(cat) + +# Extract the file path from the hook input +file_path=$(echo "$input" | jq -r '.path // empty') + +# Only run format if we're in a git repository and npm is available +if [ -d ".git" ] && command -v npm &> /dev/null; then + # Run oxfmt to format the edited file and any others that need it + # Suppress output to avoid noise in the agent's context + npm run format > /dev/null 2>&1 || true + + # Return success to allow the edit to continue + echo '{ "additional_context": "Auto-formatted code with oxfmt" }' +else + # If not in git repo or npm not available, just pass through + echo '{ "additional_context": "" }' +fi + +exit 0 diff --git a/.cursor/hooks/validate-all.sh b/.cursor/hooks/validate-all.sh new file mode 100644 index 000000000..80687f30c --- /dev/null +++ b/.cursor/hooks/validate-all.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Validate-all hook: runs when agent stops to validate code quality + +set -euo pipefail + +# Read hook input (though we don't need it for this hook) +input=$(cat) + +# Only run validation if we're in a git repository and npm is available +if [ -d ".git" ] && command -v npm &> /dev/null; then + echo "Running validation checks..." >&2 + + # Run format check + if npm run check-format > /tmp/format-check.log 2>&1; then + format_status="✅ Format check passed" + else + format_status="❌ Format check failed (run 'npm run format')" + fi + + # Run lint + if npm run lint > /tmp/lint-check.log 2>&1; then + lint_status="✅ Lint check passed" + else + lint_status="⚠️ Lint warnings (see 'npm run lint')" + fi + + # Run type check + if npm run type-check > /tmp/type-check.log 2>&1; then + type_status="✅ Type check passed" + else + type_status="❌ Type check failed (see 'npm run type-check')" + fi + + # Build summary message + summary="Validation Results:\n$format_status\n$lint_status\n$type_status" + + # Return the validation summary as a followup message + echo "{ + \"followup_message\": \"$summary\" + }" +else + # If not in git repo or npm not available, skip validation + echo '{ "followup_message": "Skipped validation (not in git repo or npm not available)" }' +fi + +exit 0 From 6d3e0652858f616e42989d2f50a14e1407c5cd0c Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 10:31:45 +0200 Subject: [PATCH 07/12] format --- docs/COMPONENT_CUSTOMIZATION.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/COMPONENT_CUSTOMIZATION.md b/docs/COMPONENT_CUSTOMIZATION.md index c36882e19..88ad4ec88 100644 --- a/docs/COMPONENT_CUSTOMIZATION.md +++ b/docs/COMPONENT_CUSTOMIZATION.md @@ -106,6 +106,7 @@ Available component types include: - `statement` - Statement/information display - `table` - Table components - `drawer` - Drawer components +- `forcedValue` - Forced value components - `zendeskDrawer` - Zendesk drawer components - `zendeskTriggerButton` - Zendesk trigger button components - `pdfViewer` - PDF viewer component @@ -127,6 +128,7 @@ and their typescript definitions - `PDFPreviewComponentProps`: for the pdf viewer component - `TelFieldComponentProps`: for the tel component - `TimeFieldComponentProps`: for the timefield component +- `ForcedValueComponentProps`: for the forced value component > **Tip:** Check [src/default-components.ts](../src/default-components.ts) to see the default implementations. You can use these as a starting point or reference when building your own custom components. From 27b86f34796fe082050701618b33a7218e5258cb Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 11:09:28 +0200 Subject: [PATCH 08/12] chore: remove .cursor changes that belong in separate PRs --- .cursor/hooks.json | 18 --------- .cursor/hooks/auto-format.sh | 25 ------------ .cursor/hooks/validate-all.sh | 46 ----------------------- .cursor/rules/component-documentation.mdc | 6 --- .cursor/rules/component-pattern.mdc | 12 ------ 5 files changed, 107 deletions(-) delete mode 100644 .cursor/hooks.json delete mode 100644 .cursor/hooks/auto-format.sh delete mode 100644 .cursor/hooks/validate-all.sh diff --git a/.cursor/hooks.json b/.cursor/hooks.json deleted file mode 100644 index a9a32fdf9..000000000 --- a/.cursor/hooks.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": 1, - "hooks": { - "afterFileEdit": [ - { - "command": ".cursor/hooks/auto-format.sh", - "matcher": ".*\\.(ts|tsx|js|jsx|json|md|mdc)$", - "timeout": 60 - } - ], - "stop": [ - { - "command": ".cursor/hooks/validate-all.sh", - "timeout": 120 - } - ] - } -} diff --git a/.cursor/hooks/auto-format.sh b/.cursor/hooks/auto-format.sh deleted file mode 100644 index 49b64c2a4..000000000 --- a/.cursor/hooks/auto-format.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Auto-format hook: runs after file edits to ensure consistent formatting - -set -euo pipefail - -# Read hook input from stdin -input=$(cat) - -# Extract the file path from the hook input -file_path=$(echo "$input" | jq -r '.path // empty') - -# Only run format if we're in a git repository and npm is available -if [ -d ".git" ] && command -v npm &> /dev/null; then - # Run oxfmt to format the edited file and any others that need it - # Suppress output to avoid noise in the agent's context - npm run format > /dev/null 2>&1 || true - - # Return success to allow the edit to continue - echo '{ "additional_context": "Auto-formatted code with oxfmt" }' -else - # If not in git repo or npm not available, just pass through - echo '{ "additional_context": "" }' -fi - -exit 0 diff --git a/.cursor/hooks/validate-all.sh b/.cursor/hooks/validate-all.sh deleted file mode 100644 index 80687f30c..000000000 --- a/.cursor/hooks/validate-all.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# Validate-all hook: runs when agent stops to validate code quality - -set -euo pipefail - -# Read hook input (though we don't need it for this hook) -input=$(cat) - -# Only run validation if we're in a git repository and npm is available -if [ -d ".git" ] && command -v npm &> /dev/null; then - echo "Running validation checks..." >&2 - - # Run format check - if npm run check-format > /tmp/format-check.log 2>&1; then - format_status="✅ Format check passed" - else - format_status="❌ Format check failed (run 'npm run format')" - fi - - # Run lint - if npm run lint > /tmp/lint-check.log 2>&1; then - lint_status="✅ Lint check passed" - else - lint_status="⚠️ Lint warnings (see 'npm run lint')" - fi - - # Run type check - if npm run type-check > /tmp/type-check.log 2>&1; then - type_status="✅ Type check passed" - else - type_status="❌ Type check failed (see 'npm run type-check')" - fi - - # Build summary message - summary="Validation Results:\n$format_status\n$lint_status\n$type_status" - - # Return the validation summary as a followup message - echo "{ - \"followup_message\": \"$summary\" - }" -else - # If not in git repo or npm not available, skip validation - echo '{ "followup_message": "Skipped validation (not in git repo or npm not available)" }' -fi - -exit 0 diff --git a/.cursor/rules/component-documentation.mdc b/.cursor/rules/component-documentation.mdc index 65700fe52..11917e8f4 100644 --- a/.cursor/rules/component-documentation.mdc +++ b/.cursor/rules/component-documentation.mdc @@ -27,14 +27,12 @@ If the component has unique props or usage patterns, add an example section simi ### 2. Verify Exports Ensure the component props type is exported from: - - `src/types/remoteFlows.ts` - Type definition - `src/index.tsx` - Public API export ### 3. When to Update **Always check and update this documentation when:** - - Adding a new field to `Components` type in `src/types/remoteFlows.ts` - Creating a new customizable component - Exporting a new component props type from `src/index.tsx` @@ -45,19 +43,16 @@ Ensure the component props type is exported from: For a new `CustomWidget` component: 1. **docs/COMPONENT_CUSTOMIZATION.md** (line ~113): - ```markdown - `customWidget` - Custom widget component ``` 2. **docs/COMPONENT_CUSTOMIZATION.md** (line ~130): - ```markdown - `CustomWidgetComponentProps`: For custom widget components ``` 3. **Verify exports**: - ```typescript // src/types/remoteFlows.ts export type CustomWidgetComponentProps = { /* ... */ }; @@ -69,7 +64,6 @@ export type { CustomWidgetComponentProps } from '@/src/types/remoteFlows'; ## Red Flags If you find yourself: - - Adding a type to `Components` without updating docs - Exporting a new `*ComponentProps` type without documenting it - Implementing a custom component without checking the docs diff --git a/.cursor/rules/component-pattern.mdc b/.cursor/rules/component-pattern.mdc index ee28503f2..57cfe3a7f 100644 --- a/.cursor/rules/component-pattern.mdc +++ b/.cursor/rules/component-pattern.mdc @@ -46,7 +46,6 @@ export function ComponentName(props) { ``` **Responsibilities:** - - Business logic and data processing - Context consumption - Component resolution via `useFormFields()` @@ -70,7 +69,6 @@ export function ComponentNameDefault({ fieldData }: ComponentNameComponentProps) ``` **Responsibilities:** - - Pure presentation component - Receives props from main component - No context consumption @@ -97,7 +95,6 @@ export const lazyDefaultComponents: Components = { When adding a new customizable component: ### 1. Component Files - - [ ] Create `ComponentName.tsx` with business logic - [ ] Create `default/ComponentNameDefault.tsx` with presentation - [ ] Main component uses `useFormFields()` to get custom component @@ -105,23 +102,19 @@ When adding a new customizable component: - [ ] No inline default rendering in main component ### 2. Type Definitions - - [ ] Export `ComponentNameComponentProps` in `src/types/remoteFlows.ts` - [ ] Add `componentName?: React.ComponentType` to `Components` type - [ ] Export props type from `src/index.tsx` ### 3. Lazy Loading - - [ ] Add to `src/lazy-default-components.ts` using `React.lazy()` - [ ] Import path points to Default component file - [ ] Test that import path is correct ### 4. Documentation - - [ ] Update `docs/COMPONENT_CUSTOMIZATION.md` (see component-documentation.mdc) ### 5. Tests - - [ ] Create test file in `tests/` subdirectory - [ ] Test default rendering - [ ] Test custom component override @@ -129,7 +122,6 @@ When adding a new customizable component: - [ ] Use `TestProviders` with custom components prop ### 6. Validation - - [ ] Run `npm run format` - [ ] Run `npm run lint` - [ ] Run `npm run type-check` @@ -140,13 +132,11 @@ When adding a new customizable component: ### Good Examples **ForcedValueField** (lines 1-67): - - Main component: `src/components/form/fields/ForcedValueField.tsx` - Default: `src/components/form/fields/default/ForcedValueFieldDefault.tsx` - Lazy: `src/lazy-default-components.ts` (lines 63-69) **ZendeskDrawer** (lines 1-47): - - Main component: `src/components/shared/zendesk-drawer/ZendeskDrawer.tsx` - Default: `src/components/shared/zendesk-drawer/ZendeskDrawerDefault.tsx` - Follows pattern correctly @@ -173,7 +163,6 @@ export function ComponentName(props) { ``` **Why this is bad:** - - No lazy loading (default always included in bundle) - Violates separation of concerns - Inconsistent with codebase patterns @@ -190,7 +179,6 @@ export function ComponentName(props) { ## Red Flags Stop and refactor if you find yourself: - - Writing inline JSX in the main component for default case - Not creating a Default component file - Skipping the lazy-default-components.ts registration From b8169b0b0f705c34aed919d4735488e3dd47e0d2 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 11:10:04 +0200 Subject: [PATCH 09/12] remove file --- .cursor/rules/component-documentation.mdc | 75 --------- .cursor/rules/component-pattern.mdc | 191 ---------------------- 2 files changed, 266 deletions(-) delete mode 100644 .cursor/rules/component-documentation.mdc delete mode 100644 .cursor/rules/component-pattern.mdc diff --git a/.cursor/rules/component-documentation.mdc b/.cursor/rules/component-documentation.mdc deleted file mode 100644 index 11917e8f4..000000000 --- a/.cursor/rules/component-documentation.mdc +++ /dev/null @@ -1,75 +0,0 @@ -# Component Documentation Rule - -## Philosophy: Keep Documentation in Sync - -When adding a new customizable component to the `Components` type, documentation must be updated immediately. This ensures library consumers can discover and use new customization options. - -## Required Updates - -When adding a new customizable component: - -### 1. Update docs/COMPONENT_CUSTOMIZATION.md - -Add the component type to the **"Available component types"** list (around line 92-113): - -```markdown -- `componentName` - Component description -``` - -Add the TypeScript type to the **"and their typescript definitions"** list (around line 115-130): - -```markdown -- `ComponentNameComponentProps`: For custom componentName components -``` - -If the component has unique props or usage patterns, add an example section similar to the existing `ZendeskTriggerButtonComponentProps` example (lines 398-467). - -### 2. Verify Exports - -Ensure the component props type is exported from: -- `src/types/remoteFlows.ts` - Type definition -- `src/index.tsx` - Public API export - -### 3. When to Update - -**Always check and update this documentation when:** -- Adding a new field to `Components` type in `src/types/remoteFlows.ts` -- Creating a new customizable component -- Exporting a new component props type from `src/index.tsx` -- Adding a new field type that consumers can override - -## Example Pattern - -For a new `CustomWidget` component: - -1. **docs/COMPONENT_CUSTOMIZATION.md** (line ~113): -```markdown -- `customWidget` - Custom widget component -``` - -2. **docs/COMPONENT_CUSTOMIZATION.md** (line ~130): -```markdown -- `CustomWidgetComponentProps`: For custom widget components -``` - -3. **Verify exports**: -```typescript -// src/types/remoteFlows.ts -export type CustomWidgetComponentProps = { /* ... */ }; - -// src/index.tsx -export type { CustomWidgetComponentProps } from '@/src/types/remoteFlows'; -``` - -## Red Flags - -If you find yourself: -- Adding a type to `Components` without updating docs -- Exporting a new `*ComponentProps` type without documenting it -- Implementing a custom component without checking the docs - -**STOP** and update the documentation first. - -## Remember - -The documentation is the contract with library consumers. Missing documentation means features that are undiscoverable and unused. diff --git a/.cursor/rules/component-pattern.mdc b/.cursor/rules/component-pattern.mdc deleted file mode 100644 index 57cfe3a7f..000000000 --- a/.cursor/rules/component-pattern.mdc +++ /dev/null @@ -1,191 +0,0 @@ -# Component Pattern Rule - -## Philosophy: Separation of Concerns - -All customizable components in Remote Flows follow a consistent pattern: **main component** (logic) + **default component** (presentation). This separation enables lazy loading, reduces bundle size, and maintains consistency. - -## The Pattern - -### Structure - -``` -src/ -├── components/ -│ ├── form/fields/ -│ │ ├── ComponentName.tsx # Main component with logic -│ │ └── default/ -│ │ └── ComponentNameDefault.tsx # Default implementation -│ └── shared/ -│ └── feature-name/ -│ ├── ComponentName.tsx -│ └── default/ -│ └── ComponentNameDefault.tsx -``` - -### Main Component (Logic) - -**Example**: `ForcedValueField.tsx` - -```typescript -import { useFormFields } from '@/src/context'; - -export function ComponentName(props) { - const { components } = useFormFields(); - - // Business logic here - const processedData = /* ... */; - - const Component = components?.componentName; - - if (!Component) { - throw new Error(`Component not found for field ${name}`); - } - - return ; -} -``` - -**Responsibilities:** -- Business logic and data processing -- Context consumption -- Component resolution via `useFormFields()` -- Error handling (throw if component not found) -- **NO inline rendering** of default UI - -### Default Component (Presentation) - -**Example**: `ForcedValueFieldDefault.tsx` - -```typescript -import { ComponentNameComponentProps } from '@/src/types/remoteFlows'; - -export function ComponentNameDefault({ fieldData }: ComponentNameComponentProps) { - return ( -
- {/* Pure presentation - no business logic */} -
- ); -} -``` - -**Responsibilities:** -- Pure presentation component -- Receives props from main component -- No context consumption -- No business logic -- Lives in `default/` subdirectory - -### Lazy Loading Registration - -**Always add to** `src/lazy-default-components.ts`: - -```typescript -export const lazyDefaultComponents: Components = { - componentName: lazy(() => - import('./components/path/to/default/ComponentNameDefault').then((m) => ({ - default: m.ComponentNameDefault, - })), - ), - // ... other components -}; -``` - -## Complete Implementation Checklist - -When adding a new customizable component: - -### 1. Component Files -- [ ] Create `ComponentName.tsx` with business logic -- [ ] Create `default/ComponentNameDefault.tsx` with presentation -- [ ] Main component uses `useFormFields()` to get custom component -- [ ] Main component throws error if component not found -- [ ] No inline default rendering in main component - -### 2. Type Definitions -- [ ] Export `ComponentNameComponentProps` in `src/types/remoteFlows.ts` -- [ ] Add `componentName?: React.ComponentType` to `Components` type -- [ ] Export props type from `src/index.tsx` - -### 3. Lazy Loading -- [ ] Add to `src/lazy-default-components.ts` using `React.lazy()` -- [ ] Import path points to Default component file -- [ ] Test that import path is correct - -### 4. Documentation -- [ ] Update `docs/COMPONENT_CUSTOMIZATION.md` (see component-documentation.mdc) - -### 5. Tests -- [ ] Create test file in `tests/` subdirectory -- [ ] Test default rendering -- [ ] Test custom component override -- [ ] Test props are passed correctly -- [ ] Use `TestProviders` with custom components prop - -### 6. Validation -- [ ] Run `npm run format` -- [ ] Run `npm run lint` -- [ ] Run `npm run type-check` -- [ ] Run `npm test` - -## Examples from Codebase - -### Good Examples - -**ForcedValueField** (lines 1-67): -- Main component: `src/components/form/fields/ForcedValueField.tsx` -- Default: `src/components/form/fields/default/ForcedValueFieldDefault.tsx` -- Lazy: `src/lazy-default-components.ts` (lines 63-69) - -**ZendeskDrawer** (lines 1-47): -- Main component: `src/components/shared/zendesk-drawer/ZendeskDrawer.tsx` -- Default: `src/components/shared/zendesk-drawer/ZendeskDrawerDefault.tsx` -- Follows pattern correctly - -### Anti-Pattern (What NOT to Do) - -```typescript -// ❌ BAD: Inline default rendering in main component -export function ComponentName(props) { - const { components } = useFormFields(); - const CustomComponent = components?.componentName; - - if (CustomComponent) { - return ; - } - - // ❌ Don't do this - create a Default component instead - return ( -
- Default rendering here -
- ); -} -``` - -**Why this is bad:** -- No lazy loading (default always included in bundle) -- Violates separation of concerns -- Inconsistent with codebase patterns -- Makes testing harder - -## Why This Matters - -1. **Bundle size**: Defaults are lazy-loaded only when needed -2. **Consistency**: All components follow the same pattern -3. **Maintenance**: Changes to default styling happen in one place -4. **Testing**: Easier to test logic separately from presentation -5. **Type safety**: Props are explicitly typed and exported - -## Red Flags - -Stop and refactor if you find yourself: -- Writing inline JSX in the main component for default case -- Not creating a Default component file -- Skipping the lazy-default-components.ts registration -- Creating a component that doesn't follow this structure - -## Remember - -**Every customizable component = Main (logic) + Default (presentation) + Lazy loading** - -No exceptions. This pattern is enforced by the codebase architecture and bundle size limits. From 0d7e8ee963bafaf0a4e46d5c8823379a8fa77edb Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 21 Aug 2026 11:19:07 +0200 Subject: [PATCH 10/12] export utility --- example/src/Components.tsx | 36 +++++++++++++++++++++++++----------- src/index.tsx | 5 ++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/example/src/Components.tsx b/example/src/Components.tsx index 486ec66cc..8c59828b0 100644 --- a/example/src/Components.tsx +++ b/example/src/Components.tsx @@ -1,14 +1,15 @@ -import type { - ButtonComponentProps, - Components, - FieldComponentProps, - FieldSetToggleComponentProps, - FileComponentProps, - ForcedValueComponentProps, - PDFPreviewComponentProps, - TelFieldComponentProps, - TimeFieldComponentProps, - ZendeskTriggerButtonComponentProps, +import { + buildZendeskURL, + type ButtonComponentProps, + type Components, + type FieldComponentProps, + type FieldSetToggleComponentProps, + type FileComponentProps, + type ForcedValueComponentProps, + type PDFPreviewComponentProps, + type TelFieldComponentProps, + type TimeFieldComponentProps, + type ZendeskTriggerButtonComponentProps, } from '@remoteoss/remote-flows'; import { FileUploader } from '@remoteoss/remote-flows/internals'; import { splitAccordionDescription } from './utils/transformHtml'; @@ -497,11 +498,24 @@ const ZendeskTriggerButton = ({ onClick, children, className, + external, }: ZendeskTriggerButtonComponentProps) => { const handleClick = () => { onClick?.(zendeskId); }; + if (external) { + return ( + + {children} + + ); + } + return ( diff --git a/example/src/flows/Onboarding/Onboarding.tsx b/example/src/flows/Onboarding/Onboarding.tsx index 9aeb14656..03b0427d4 100644 --- a/example/src/flows/Onboarding/Onboarding.tsx +++ b/example/src/flows/Onboarding/Onboarding.tsx @@ -26,6 +26,7 @@ import { sanitizeHtml } from '@remoteoss/remote-flows/internals'; import { ONBOARDING_OPTIONS } from './constants'; import { StepsNavigation } from './StepsNavigation'; import { PreviewEmploymentAgreementStep } from './PreviewEmploymentAgreementStep'; +import { components } from '../../Components'; import '../../css/main.css'; const BenefitsAboutSection = ({ @@ -399,6 +400,7 @@ const OnboardingWithProps = ({ Date: Fri, 21 Aug 2026 11:41:43 +0200 Subject: [PATCH 12/12] update component --- example/src/flows/Onboarding/Onboarding.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/src/flows/Onboarding/Onboarding.tsx b/example/src/flows/Onboarding/Onboarding.tsx index 03b0427d4..9aeb14656 100644 --- a/example/src/flows/Onboarding/Onboarding.tsx +++ b/example/src/flows/Onboarding/Onboarding.tsx @@ -26,7 +26,6 @@ import { sanitizeHtml } from '@remoteoss/remote-flows/internals'; import { ONBOARDING_OPTIONS } from './constants'; import { StepsNavigation } from './StepsNavigation'; import { PreviewEmploymentAgreementStep } from './PreviewEmploymentAgreementStep'; -import { components } from '../../Components'; import '../../css/main.css'; const BenefitsAboutSection = ({ @@ -400,7 +399,6 @@ const OnboardingWithProps = ({