From 3f4e71c5ac30634bd176e8925e3050e7dc36e7c4 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:06:52 -0500 Subject: [PATCH 01/17] fix(stories): extract inline hooks into named wrapper components Fixed 47 react-hooks/rules-of-hooks violations in 22 story files by extracting inline useState/useEffect from render functions into named wrapper components. Reduced total ESLint errors from 194 to 144. --- .../AddContactModal.stories.tsx | 141 +++++----- .../Address/AddressForm.stories.tsx | 86 +++--- .../BookingDialog/BookingDialog.stories.tsx | 45 +-- .../CSVColumnMapper.stories.tsx | 124 +++++---- .../CheckrIntegration.stories.tsx | 63 +++-- .../ConnectionStatus.stories.tsx | 8 +- .../CookieConsent/CookieConsent.stories.tsx | 12 +- .../DateRangePicker.stories.tsx | 4 +- .../EditUserRoleModal.stories.tsx | 70 ++--- .../ErrorPage/ErrorPage.stories.tsx | 2 +- .../HRISProviderSelector.stories.tsx | 28 +- .../InviteUserModal.stories.tsx | 99 ++++--- .../LanguageSelector.stories.tsx | 130 ++++----- .../LoadingPage/LoadingPage.stories.tsx | 90 +++--- .../OnboardingWizard.stories.tsx | 84 +++--- .../OrderList/OrderList.stories.tsx | 132 ++++----- .../PhoneInput/PhoneInputGroup.stories.tsx | 32 ++- .../ProductVersion/ProductVersion.stories.tsx | 2 +- .../ProviderCard/ProviderCard.stories.tsx | 4 +- .../ProviderDetailHeader.stories.tsx | 14 +- .../ProviderSearchBar.stories.tsx | 34 +-- .../ProviderSearchFilters.stories.tsx | 110 ++++---- .../ProviderSelector.stories.tsx | 242 ++++++++-------- .../RecurringServiceCard.stories.tsx | 260 +++++++++--------- .../RejectionModal/RejectionModal.stories.tsx | 34 +-- .../ServiceAccordion.stories.tsx | 24 +- .../ServiceBadge/ServiceBadge.stories.tsx | 60 ++-- .../ServicePicker/ServicePicker.stories.tsx | 78 +++--- .../SiteFooter/SiteFooter.stories.tsx | 6 +- .../SiteHeader/SiteHeader.stories.tsx | 8 +- .../WebChartReportViewer.stories.tsx | 106 +++---- 31 files changed, 1121 insertions(+), 1011 deletions(-) diff --git a/src/components/AddContactModal/AddContactModal.stories.tsx b/src/components/AddContactModal/AddContactModal.stories.tsx index 2c00a31..1ee941b 100644 --- a/src/components/AddContactModal/AddContactModal.stories.tsx +++ b/src/components/AddContactModal/AddContactModal.stories.tsx @@ -79,6 +79,78 @@ function AddContactModalWrapper(props: Partial { + console.log('Updated contact:', contact); + setOpen(false); + }} + contact={{ + id: '123', + firstName: 'Jane', + lastName: 'Smith', + sex: 'F', + positionTitle: 'Office Manager', + degree: 'MBA', + email: 'jane.smith@example.com', + phone: '(555) 123-4567', + address: { + street1: '123 Main St', + street2: 'Suite 100', + city: 'Fort Wayne', + state: 'IN', + postalCode: '46802', + }, + customFields: [ + { name: 'Extension', value: '1234' }, + { name: 'Department', value: 'Administration' }, + ], + }} + /> + ); +} + +// Wrapper for SavingState story +function SavingStateWrapper() { + const [open, setOpen] = useState(true); + + return ( + {}} + isSaving={true} + /> + ); +} + +// Wrapper for WithValidationErrors story +function WithValidationErrorsWrapper() { + const [open, setOpen] = useState(true); + + return ( + { + console.log('Saved contact:', contact); + setOpen(false); + }} + contact={{ + firstName: '', + lastName: '', + email: 'invalid-email', + }} + /> + ); +} + export const Default: Story = { render: (args) => , args: { @@ -90,41 +162,7 @@ export const Default: Story = { }; export const EditMode: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - { - console.log('Updated contact:', contact); - setOpen(false); - }} - contact={{ - id: '123', - firstName: 'Jane', - lastName: 'Smith', - sex: 'F', - positionTitle: 'Office Manager', - degree: 'MBA', - email: 'jane.smith@example.com', - phone: '(555) 123-4567', - address: { - street1: '123 Main St', - street2: 'Suite 100', - city: 'Fort Wayne', - state: 'IN', - postalCode: '46802', - }, - customFields: [ - { name: 'Extension', value: '1234' }, - { name: 'Department', value: 'Administration' }, - ], - }} - /> - ); - }, + render: () => , }; export const MinimalFields: Story = { @@ -148,40 +186,11 @@ export const WithPhoneOnly: Story = { }; export const SavingState: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - {}} - isSaving={true} - /> - ); - }, + render: () => , }; export const WithValidationErrors: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - { - console.log('Saved contact:', contact); - setOpen(false); - }} - contact={{ - firstName: '', - lastName: '', - email: 'invalid-email', - }} - /> - ); - }, + render: () => , parameters: { docs: { description: { diff --git a/src/components/Address/AddressForm.stories.tsx b/src/components/Address/AddressForm.stories.tsx index 1976c10..3f5391c 100644 --- a/src/components/Address/AddressForm.stories.tsx +++ b/src/components/Address/AddressForm.stories.tsx @@ -150,21 +150,23 @@ export const WithErrors: Story = { }; // Disabled state -export const Disabled: Story = { - render: () => { - const [address] = useState>({ - street1: '123 Healthcare Way', - street2: 'Suite 500', - city: 'Indianapolis', - state: 'IN', - postalCode: '46220', - country: 'US', - }); +function DisabledWrapper() { + const [address] = useState>({ + street1: '123 Healthcare Way', + street2: 'Suite 500', + city: 'Indianapolis', + state: 'IN', + postalCode: '46220', + country: 'US', + }); - return ( - {}} disabled showCountry /> - ); - }, + return ( + {}} disabled showCountry /> + ); +} + +export const Disabled: Story = { + render: () => , parameters: { docs: { description: { @@ -234,25 +236,24 @@ export const Mobile: Story = { }; // Google Places note -export const GooglePlacesInfo: Story = { - render: () => { - const [address, setAddress] = useState>({}); +function GooglePlacesInfoWrapper() { + const [address, setAddress] = useState>({}); - return ( -
-
-

- Google Places Autocomplete -

-

- To enable Google Places autocomplete, pass the{' '} - - googlePlaces - {' '} - prop: -

-
-            {`
+      
+

+ Google Places Autocomplete +

+

+ To enable Google Places autocomplete, pass the{' '} + + googlePlaces + {' '} + prop: +

+
+          {` console.log(place),
   }}
 />`}
-          
-

- Note: Requires Google Maps JavaScript API with Places library loaded - in your app. -

-
- +
+

+ Note: Requires Google Maps JavaScript API with Places library loaded + in your app. +

- ); - }, + +
+ ); +} + +export const GooglePlacesInfo: Story = { + render: () => , parameters: { docs: { description: { diff --git a/src/components/BookingDialog/BookingDialog.stories.tsx b/src/components/BookingDialog/BookingDialog.stories.tsx index 0efb7de..233ad44 100644 --- a/src/components/BookingDialog/BookingDialog.stories.tsx +++ b/src/components/BookingDialog/BookingDialog.stories.tsx @@ -59,10 +59,10 @@ function BookingDialogWrapper(args: Story['args']) { provider={args?.provider || mockProvider} services={args?.services || mockServices} onSubmit={(data) => { - alert(`Booking requested for ${data.firstName} ${data.lastName}`); + window.alert(`Booking requested for ${data.firstName} ${data.lastName}`); setIsOpen(false); }} - onCall={(phone) => alert(`Calling ${phone}...`)} + onCall={(phone) => window.alert(`Calling ${phone}...`)} /> ); @@ -106,23 +106,26 @@ export const FloatingInputDemo: StoryObj = { ), }; +// Wrapper for ServiceSelectDemo +function ServiceSelectDemoWrapper() { + const [selected, setSelected] = React.useState([]); + return ( +
+ +

+ Selected: {selected.join(', ') || 'None'} +

+
+ ); +} + export const ServiceSelectDemo: StoryObj = { - render: () => { - const [selected, setSelected] = React.useState([]); - return ( -
- -

- Selected: {selected.join(', ') || 'None'} -

-
- ); - }, + render: () => , }; export const InlineForm: StoryObj = { @@ -131,7 +134,7 @@ export const InlineForm: StoryObj = { alert(`Booking for ${data.firstName}`)} + onSubmit={(data) => window.alert(`Booking for ${data.firstName}`)} /> ), @@ -142,8 +145,8 @@ export const QuickBook: StoryObj = {
alert('Opening booking...')} - onCall={(phone) => alert(`Calling ${phone}`)} + onBook={() => window.alert('Opening booking...')} + onCall={(phone) => window.alert(`Calling ${phone}`)} />
), diff --git a/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx b/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx index 18bc121..506fa46 100644 --- a/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx +++ b/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx @@ -50,47 +50,71 @@ const childFieldOptions = { ], }; -export const Default: Story = { - render: () => { - const [columns, setColumns] = useState(sampleColumns); - - const handleColumnChange = (index: number, mappedTo: string, childField?: string) => { - setColumns((prev) => - prev.map((col, i) => - i === index ? { ...col, mappedTo, childField } : col - ) - ); - }; - - const handleIgnoreToggle = (index: number, ignored: boolean) => { - setColumns((prev) => - prev.map((col, i) => (i === index ? { ...col, ignored } : col)) - ); - }; - - const handleBulkAction = (action: 'ignoreAll' | 'includeAll' | 'ignoreUncompleted') => { - setColumns((prev) => - prev.map((col) => { - if (action === 'ignoreAll') return { ...col, ignored: true }; - if (action === 'includeAll') return { ...col, ignored: false }; - if (action === 'ignoreUncompleted' && !col.mappedTo) return { ...col, ignored: true }; - return col; - }) - ); - }; - - return ( - alert('Import triggered!')} - /> +// Wrapper for Default story with interactive state +function CSVColumnMapperWrapper() { + const [columns, setColumns] = useState(sampleColumns); + + const handleColumnChange = (index: number, mappedTo: string, childField?: string) => { + setColumns((prev) => + prev.map((col, i) => + i === index ? { ...col, mappedTo, childField } : col + ) ); - }, + }; + + const handleIgnoreToggle = (index: number, ignored: boolean) => { + setColumns((prev) => + prev.map((col, i) => (i === index ? { ...col, ignored } : col)) + ); + }; + + const handleBulkAction = (action: 'ignoreAll' | 'includeAll' | 'ignoreUncompleted') => { + setColumns((prev) => + prev.map((col) => { + if (action === 'ignoreAll') return { ...col, ignored: true }; + if (action === 'includeAll') return { ...col, ignored: false }; + if (action === 'ignoreUncompleted' && !col.mappedTo) return { ...col, ignored: true }; + return col; + }) + ); + }; + + return ( + window.alert('Import triggered!')} + /> + ); +} + +// Wrapper for FileUpload story +function FileUploadWrapper() { + const [file, setFile] = useState(null); + + return ( +
+ { + setFile(f); + window.alert(`Selected: ${f.name}`); + }} + /> + {file && ( +

+ Selected file: {file.name} +

+ )} +
+ ); +} + +export const Default: Story = { + render: () => , }; export const WithPhoneMapping: Story = { @@ -132,25 +156,7 @@ export const AllIgnored: Story = { }; export const FileUpload: StoryObj = { - render: () => { - const [file, setFile] = useState(null); - - return ( -
- { - setFile(f); - alert(`Selected: ${f.name}`); - }} - /> - {file && ( -

- Selected file: {file.name} -

- )} -
- ); - }, + render: () => , }; export const FileUploadProcessing: StoryObj = { diff --git a/src/components/CheckrIntegration/CheckrIntegration.stories.tsx b/src/components/CheckrIntegration/CheckrIntegration.stories.tsx index d084730..24ae843 100644 --- a/src/components/CheckrIntegration/CheckrIntegration.stories.tsx +++ b/src/components/CheckrIntegration/CheckrIntegration.stories.tsx @@ -58,37 +58,40 @@ const sampleReports: BackgroundCheckReport[] = [ }, ]; -export const Default: Story = { - render: () => { - const [connected, setConnected] = useState(true); - const [reports, setReports] = useState(sampleReports); +// Wrapper for Default story with interactive state +function CheckrIntegrationWrapper() { + const [connected, setConnected] = useState(true); + const [reports, setReports] = useState(sampleReports); - return ( - setConnected(true)} - onDisconnect={() => setConnected(false)} - onInviteCandidate={(candidate, packageId) => { - console.log('Invite:', candidate, packageId); - setReports([ - ...reports, - { - id: String(Date.now()), - candidate: { id: String(Date.now()), ...candidate }, - status: 'pending', - createdAt: new Date(), - packageName: samplePackages.find((p) => p.id === packageId)?.name, - }, - ]); - }} - onViewReport={(report) => window.open(report.reportUrl, '_blank')} - onRefresh={() => console.log('Refresh')} - /> - ); - }, + return ( + setConnected(true)} + onDisconnect={() => setConnected(false)} + onInviteCandidate={(candidate, packageId) => { + console.log('Invite:', candidate, packageId); + setReports([ + ...reports, + { + id: String(Date.now()), + candidate: { id: String(Date.now()), ...candidate }, + status: 'pending', + createdAt: new Date(), + packageName: samplePackages.find((p) => p.id === packageId)?.name, + }, + ]); + }} + onViewReport={(report) => window.open(report.reportUrl, '_blank')} + onRefresh={() => console.log('Refresh')} + /> + ); +} + +export const Default: Story = { + render: () => , }; export const NotConnected: Story = { diff --git a/src/components/ConnectionStatus/ConnectionStatus.stories.tsx b/src/components/ConnectionStatus/ConnectionStatus.stories.tsx index 464601f..e130e73 100644 --- a/src/components/ConnectionStatus/ConnectionStatus.stories.tsx +++ b/src/components/ConnectionStatus/ConnectionStatus.stories.tsx @@ -41,7 +41,7 @@ export const Default: Story = { args: { isVisible: true, connection: { status: 'disconnected' }, - onReload: () => alert('Reloading...'), + onReload: () => window.alert('Reloading...'), }, }; @@ -62,7 +62,7 @@ export const WaitingToRetry: Story = { retryCount: 3, retryTime: Date.now() + 15000, }, - onReload: () => alert('Reloading...'), + onReload: () => window.alert('Reloading...'), }, }; @@ -72,8 +72,8 @@ export const UpdateAvailable: StoryObj = { alert('Updating...')} - onLater={() => alert('Dismissed')} + onUpdateNow={() => window.alert('Updating...')} + onLater={() => window.alert('Dismissed')} appName="BlueHive" /> ), diff --git a/src/components/CookieConsent/CookieConsent.stories.tsx b/src/components/CookieConsent/CookieConsent.stories.tsx index 3f08966..7050d61 100644 --- a/src/components/CookieConsent/CookieConsent.stories.tsx +++ b/src/components/CookieConsent/CookieConsent.stories.tsx @@ -47,7 +47,7 @@ type Story = StoryObj; export const Default: Story = { args: { isVisible: true, - onAccept: () => alert('Cookies accepted!'), + onAccept: () => window.alert('Cookies accepted!'), termsLink: { label: 'Terms and Conditions', href: '/terms' }, privacyLink: { label: 'Privacy Policy', href: '/privacy' }, }, @@ -57,9 +57,9 @@ export const Default: Story = { export const WithAllOptions: Story = { args: { isVisible: true, - onAccept: () => alert('Accepted'), - onDecline: () => alert('Declined'), - onCustomize: () => alert('Customize'), + onAccept: () => window.alert('Accepted'), + onDecline: () => window.alert('Declined'), + onCustomize: () => window.alert('Customize'), showDecline: true, showCustomize: true, termsLink: { label: 'Terms', href: '/terms' }, @@ -73,7 +73,7 @@ export const CornerCard: Story = { args: { isVisible: true, position: 'bottom-right', - onAccept: () => alert('Accepted'), + onAccept: () => window.alert('Accepted'), termsLink: { label: 'Terms', href: '/terms' }, privacyLink: { label: 'Privacy', href: '/privacy' }, }, @@ -84,7 +84,7 @@ export const Compact: StoryObj = { render: () => ( alert('Accepted')} + onAccept={() => window.alert('Accepted')} privacyHref="/privacy" /> ), diff --git a/src/components/DateRangePicker/DateRangePicker.stories.tsx b/src/components/DateRangePicker/DateRangePicker.stories.tsx index bcff089..e88babe 100644 --- a/src/components/DateRangePicker/DateRangePicker.stories.tsx +++ b/src/components/DateRangePicker/DateRangePicker.stories.tsx @@ -51,8 +51,8 @@ export const WithPrintExport: Story = { activePreset={preset} showPrint showExport - onPrint={() => alert('Print clicked!')} - onExport={() => alert('Export clicked!')} + onPrint={() => window.alert('Print clicked!')} + onExport={() => window.alert('Export clicked!')} /> ); }, diff --git a/src/components/EditUserRoleModal/EditUserRoleModal.stories.tsx b/src/components/EditUserRoleModal/EditUserRoleModal.stories.tsx index 3070a08..f572593 100644 --- a/src/components/EditUserRoleModal/EditUserRoleModal.stories.tsx +++ b/src/components/EditUserRoleModal/EditUserRoleModal.stories.tsx @@ -108,42 +108,48 @@ export const NewUser: Story = { ), }; +// Wrapper for WithError story +function WithErrorWrapper() { + const [open, setOpen] = useState(true); + + return ( + <> + + + + ); +} + +// Wrapper for Submitting story +function SubmittingWrapper() { + const [open, setOpen] = useState(true); + + return ( + <> + + + + ); +} + export const WithError: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - <> - - - - ); - }, + render: () => , }; export const Submitting: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - <> - - - - ); - }, + render: () => , }; export const SimpleRoles: Story = { diff --git a/src/components/ErrorPage/ErrorPage.stories.tsx b/src/components/ErrorPage/ErrorPage.stories.tsx index f3e3d3b..68beef6 100644 --- a/src/components/ErrorPage/ErrorPage.stories.tsx +++ b/src/components/ErrorPage/ErrorPage.stories.tsx @@ -81,7 +81,7 @@ export const CustomError: Story = { type: 'generic', title: 'Unable to Process Request', description: 'Please try again or contact support.', - primaryAction: { label: 'Try Again', onClick: () => alert('Retry') }, + primaryAction: { label: 'Try Again', onClick: () => window.alert('Retry') }, secondaryAction: { label: 'Contact Support', href: '/support' }, showHomeButton: false, showBackButton: false, diff --git a/src/components/HRISProviderSelector/HRISProviderSelector.stories.tsx b/src/components/HRISProviderSelector/HRISProviderSelector.stories.tsx index 46f8e65..db33d88 100644 --- a/src/components/HRISProviderSelector/HRISProviderSelector.stories.tsx +++ b/src/components/HRISProviderSelector/HRISProviderSelector.stories.tsx @@ -29,20 +29,22 @@ const sampleProviders: HRISProvider[] = [ { id: 'zenefits', displayName: 'Zenefits', logoUrl: 'https://finchdata.io/integrations/zenefits.svg' }, ]; -export const Default: Story = { - render: () => { - const [search, setSearch] = useState(''); +function DefaultWrapper() { + const [search, setSearch] = useState(''); - return ( - alert(`Selected: ${p.displayName}`)} - onCSVImport={() => alert('CSV Import clicked')} - /> - ); - }, + return ( + window.alert(`Selected: ${p.displayName}`)} + onCSVImport={() => window.alert('CSV Import clicked')} + /> + ); +} + +export const Default: Story = { + render: () => , }; export const Connected: Story = { diff --git a/src/components/InviteUserModal/InviteUserModal.stories.tsx b/src/components/InviteUserModal/InviteUserModal.stories.tsx index 4cdd13f..f22f50f 100644 --- a/src/components/InviteUserModal/InviteUserModal.stories.tsx +++ b/src/components/InviteUserModal/InviteUserModal.stories.tsx @@ -131,58 +131,67 @@ export const WithDefaultRole: Story = { ), }; +// Wrapper for WithError story +function WithErrorWrapper() { + const [open, setOpen] = useState(true); + + return ( + <> + + + + ); +} + +// Wrapper for WithSuccess story +function WithSuccessWrapper() { + const [open, setOpen] = useState(true); + + return ( + <> + + + + ); +} + +// Wrapper for Submitting story +function SubmittingWrapper() { + const [open, setOpen] = useState(true); + + return ( + <> + + + + ); +} + export const WithError: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - <> - - - - ); - }, + render: () => , }; export const WithSuccess: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - <> - - - - ); - }, + render: () => , }; export const Submitting: Story = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - <> - - - - ); - }, + render: () => , }; export const MinimalRoles: Story = { diff --git a/src/components/LanguageSelector/LanguageSelector.stories.tsx b/src/components/LanguageSelector/LanguageSelector.stories.tsx index 1e3e7b4..e379994 100644 --- a/src/components/LanguageSelector/LanguageSelector.stories.tsx +++ b/src/components/LanguageSelector/LanguageSelector.stories.tsx @@ -58,47 +58,75 @@ export const Default: Story = { render: () => , }; -// All three selector types comparison -export const AllVariantsComparison: Story = { - render: () => { - const [value, setValue] = React.useState('en'); - const handleChange = (lang: Language) => setValue(lang.code); +// Wrapper for AllVariantsComparison story +function AllVariantsComparisonWrapper() { + const [value, setValue] = React.useState('en'); + const handleChange = (lang: Language) => setValue(lang.code); - return ( -
-
-

- Custom Dropdown -

- -
-
-

- Native Select -

- -
-
-

- Inline Buttons -

- -
+ return ( +
+
+

+ Custom Dropdown +

+
- ); - }, +
+

+ Native Select +

+ +
+
+

+ Inline Buttons +

+ +
+
+ ); +} + +// Wrapper for InHeader story +function InHeaderWrapper() { + const [value, setValue] = React.useState('en'); + + return ( +
+
+ BlueHive +
+
+ setValue(lang.code)} + variant="ghost" + size="sm" + languages={limitedLanguages} + /> + +
+
+ ); +} + +// All three selector types comparison +export const AllVariantsComparison: Story = { + render: () => , }; // Visual variants (default, ghost, minimal) @@ -133,27 +161,5 @@ export const Disabled: Story = { // In header context export const InHeader: Story = { - render: () => { - const [value, setValue] = React.useState('en'); - - return ( -
-
- BlueHive -
-
- setValue(lang.code)} - variant="ghost" - size="sm" - languages={limitedLanguages} - /> - -
-
- ); - }, + render: () => , }; diff --git a/src/components/LoadingPage/LoadingPage.stories.tsx b/src/components/LoadingPage/LoadingPage.stories.tsx index 72947b2..8d5b5f3 100644 --- a/src/components/LoadingPage/LoadingPage.stories.tsx +++ b/src/components/LoadingPage/LoadingPage.stories.tsx @@ -27,6 +27,52 @@ const meta: Meta = { export default meta; type Story = StoryObj; +// Wrapper for WithProgress story +function WithProgressWrapper() { + const [progress, setProgress] = React.useState(0); + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((p) => (p >= 100 ? 0 : p + 10)); + }, 500); + return () => clearInterval(timer); + }, []); + return ( + + ); +} + +// Wrapper for Overlay story +function OverlayWrapper() { + const [loading, setLoading] = React.useState(false); + return ( +
+ +
+

Edit Profile

+ + +
+
+
+ ); +} + /** Default full-page loading state */ export const Default: Story = { args: { @@ -37,52 +83,12 @@ export const Default: Story = { /** Loading with progress bar */ export const WithProgress: Story = { - render: () => { - const [progress, setProgress] = React.useState(0); - React.useEffect(() => { - const timer = setInterval(() => { - setProgress((p) => (p >= 100 ? 0 : p + 10)); - }, 500); - return () => clearInterval(timer); - }, []); - return ( - - ); - }, + render: () => , }; /** Overlay on content */ export const Overlay: Story = { - render: () => { - const [loading, setLoading] = React.useState(false); - return ( -
- -
-

Edit Profile

- - -
-
-
- ); - }, + render: () => , }; /** Skeleton placeholders */ diff --git a/src/components/OnboardingWizard/OnboardingWizard.stories.tsx b/src/components/OnboardingWizard/OnboardingWizard.stories.tsx index 870b5be..6c6d50f 100644 --- a/src/components/OnboardingWizard/OnboardingWizard.stories.tsx +++ b/src/components/OnboardingWizard/OnboardingWizard.stories.tsx @@ -112,27 +112,29 @@ const sampleSteps = [ content: ( alert('Start order!')} - onGoToDashboard={() => alert('Go to dashboard!')} - onGoToEmployees={() => alert('Go to employees!')} + onStartOrder={() => window.alert('Start order!')} + onGoToDashboard={() => window.alert('Go to dashboard!')} + onGoToEmployees={() => window.alert('Go to employees!')} /> ), }, ]; +function DefaultWrapper() { + const [currentStep, setCurrentStep] = useState(0); + return ( + window.alert('Onboarding complete!')} + onSkip={(step) => console.log('Skipped step:', step)} + /> + ); +} + export const Default: Story = { - render: () => { - const [currentStep, setCurrentStep] = useState(0); - return ( - alert('Onboarding complete!')} - onSkip={(step) => console.log('Skipped step:', step)} - /> - ); - }, + render: () => , }; export const Loading: Story = { @@ -184,32 +186,34 @@ export const NextDisabled: Story = { }, }; +function IncompleteStepsWrapper() { + const incompleteSteps = [ + ...sampleSteps.slice(0, 4), + { + id: 'step5', + title: 'Complete', + content: ( + window.alert(`Go to step ${step}`)} + /> + ), + }, + ]; + return ( + + ); +} + export const IncompleteSteps: Story = { - render: () => { - const incompleteSteps = [ - ...sampleSteps.slice(0, 4), - { - id: 'step5', - title: 'Complete', - content: ( - alert(`Go to step ${step}`)} - /> - ), - }, - ]; - return ( - - ); - }, + render: () => , }; export const NoHeader: Story = { diff --git a/src/components/OrderList/OrderList.stories.tsx b/src/components/OrderList/OrderList.stories.tsx index 3e21eee..fb1da29 100644 --- a/src/components/OrderList/OrderList.stories.tsx +++ b/src/components/OrderList/OrderList.stories.tsx @@ -215,74 +215,80 @@ export const SimpleTabs: Story = { ), }; -export const Loading: Story = { - render: () => { - const [activeTab, setActiveTab] = useState('all'); +function LoadingWrapper() { + const [activeTab, setActiveTab] = useState('all'); - return ( -
- - orders={[]} - activeTab={activeTab} - tabs={defaultOrderTabs} - onTabChange={setActiveTab} - renderOrder={() => null} - isLoading - /> -
- ); - }, + return ( +
+ + orders={[]} + activeTab={activeTab} + tabs={defaultOrderTabs} + onTabChange={setActiveTab} + renderOrder={() => null} + isLoading + /> +
+ ); +} + +export const Loading: Story = { + render: () => , }; -export const Empty: Story = { - render: () => { - const [activeTab, setActiveTab] = useState('all'); +function EmptyWrapper() { + const [activeTab, setActiveTab] = useState('all'); - return ( -
- - orders={[]} - activeTab={activeTab} - tabs={defaultOrderTabs} - onTabChange={setActiveTab} - renderOrder={() => null} - emptyMessage="No orders yet. Create your first order to get started." - /> -
- ); - }, + return ( +
+ + orders={[]} + activeTab={activeTab} + tabs={defaultOrderTabs} + onTabChange={setActiveTab} + renderOrder={() => null} + emptyMessage="No orders yet. Create your first order to get started." + /> +
+ ); +} + +export const Empty: Story = { + render: () => , }; -export const CustomEmptyIcon: Story = { - render: () => { - const [activeTab, setActiveTab] = useState('all'); +function CustomEmptyIconWrapper() { + const [activeTab, setActiveTab] = useState('all'); - return ( -
- - orders={[]} - activeTab={activeTab} - tabs={defaultOrderTabs} - onTabChange={setActiveTab} - renderOrder={() => null} - emptyMessage="All caught up! No pending orders." - emptyIcon={ - - - - } - /> -
- ); - }, + return ( +
+ + orders={[]} + activeTab={activeTab} + tabs={defaultOrderTabs} + onTabChange={setActiveTab} + renderOrder={() => null} + emptyMessage="All caught up! No pending orders." + emptyIcon={ + + + + } + /> +
+ ); +} + +export const CustomEmptyIcon: Story = { + render: () => , }; diff --git a/src/components/PhoneInput/PhoneInputGroup.stories.tsx b/src/components/PhoneInput/PhoneInputGroup.stories.tsx index b61a254..ab2cc22 100644 --- a/src/components/PhoneInput/PhoneInputGroup.stories.tsx +++ b/src/components/PhoneInput/PhoneInputGroup.stories.tsx @@ -105,22 +105,24 @@ export const WithValidation: Story = { }; // Disabled state +function DisabledWrapper() { + const [phones] = useState([ + { number: '(555) 123-4567', type: 'cell' }, + { number: '(555) 987-6543', type: 'work' }, + ]); + + return ( + {}} + disabled + label="Phone Numbers" + /> + ); +} + export const Disabled: Story = { - render: () => { - const [phones] = useState([ - { number: '(555) 123-4567', type: 'cell' }, - { number: '(555) 987-6543', type: 'work' }, - ]); - - return ( - {}} - disabled - label="Phone Numbers" - /> - ); - }, + render: () => , parameters: { docs: { description: { diff --git a/src/components/ProductVersion/ProductVersion.stories.tsx b/src/components/ProductVersion/ProductVersion.stories.tsx index 2f9b4b1..b3641c3 100644 --- a/src/components/ProductVersion/ProductVersion.stories.tsx +++ b/src/components/ProductVersion/ProductVersion.stories.tsx @@ -240,7 +240,7 @@ export const Clickable: Story = { name: 'BlueHive', version: '2.1.0', variant: 'minimal', - onClick: () => alert('Version clicked! (Could show debug info)'), + onClick: () => window.alert('Version clicked! (Could show debug info)'), }, parameters: { docs: { diff --git a/src/components/ProviderCard/ProviderCard.stories.tsx b/src/components/ProviderCard/ProviderCard.stories.tsx index 84c406c..bb66411 100644 --- a/src/components/ProviderCard/ProviderCard.stories.tsx +++ b/src/components/ProviderCard/ProviderCard.stories.tsx @@ -86,7 +86,7 @@ export const Default: Story = { args: { provider: sampleProvider, variant: 'compact', - onClick: (provider) => alert(`Clicked ${provider.name}`), + onClick: (provider) => window.alert(`Clicked ${provider.name}`), }, decorators: [ (Story) => ( @@ -151,7 +151,7 @@ export const Grid: StoryObj = { alert(`Clicked ${provider.name}`)} + onProviderClick={(provider) => window.alert(`Clicked ${provider.name}`)} />
), diff --git a/src/components/ProviderDetailHeader/ProviderDetailHeader.stories.tsx b/src/components/ProviderDetailHeader/ProviderDetailHeader.stories.tsx index 8171040..e660c9e 100644 --- a/src/components/ProviderDetailHeader/ProviderDetailHeader.stories.tsx +++ b/src/components/ProviderDetailHeader/ProviderDetailHeader.stories.tsx @@ -74,9 +74,9 @@ export const Default: Story = { args: { provider: mockProvider, breadcrumbs: mockBreadcrumbs, - onShare: () => alert('Share'), - onCall: () => alert('Call'), - onBook: () => alert('Book'), + onShare: () => window.alert('Share'), + onCall: () => window.alert('Call'), + onBook: () => window.alert('Book'), }, }; @@ -182,7 +182,7 @@ export const CompactHeader: StoryObj = { render: () => ( alert('Book clicked')} + onBook={() => window.alert('Book clicked')} /> ), }; @@ -198,9 +198,9 @@ export const FullPageDemo: Story = { alert('Share')} - onCall={() => alert('Call')} - onBook={() => alert('Book')} + onShare={() => window.alert('Share')} + onCall={() => window.alert('Call')} + onBook={() => window.alert('Book')} />
diff --git a/src/components/ProviderSearchBar/ProviderSearchBar.stories.tsx b/src/components/ProviderSearchBar/ProviderSearchBar.stories.tsx index 35ff758..9b5dfc7 100644 --- a/src/components/ProviderSearchBar/ProviderSearchBar.stories.tsx +++ b/src/components/ProviderSearchBar/ProviderSearchBar.stories.tsx @@ -56,24 +56,26 @@ export const AllVariants: Story = { }; // With geolocation button -export const WithGeolocation: Story = { - render: () => { - const [status, setStatus] = React.useState('idle'); +function WithGeolocationWrapper() { + const [status, setStatus] = React.useState('idle'); - const handleGeolocate = () => { - setStatus('loading'); - setTimeout(() => setStatus('success'), 1500); - }; + const handleGeolocate = () => { + setStatus('loading'); + setTimeout(() => setStatus('success'), 1500); + }; - return ( - console.log('Search:', zip)} - onGeolocate={handleGeolocate} - geoStatus={status} - providerCount={17500} - /> - ); - }, + return ( + console.log('Search:', zip)} + onGeolocate={handleGeolocate} + geoStatus={status} + providerCount={17500} + /> + ); +} + +export const WithGeolocation: Story = { + render: () => , }; // With search results diff --git a/src/components/ProviderSearchFilters/ProviderSearchFilters.stories.tsx b/src/components/ProviderSearchFilters/ProviderSearchFilters.stories.tsx index d88e227..8eadfee 100644 --- a/src/components/ProviderSearchFilters/ProviderSearchFilters.stories.tsx +++ b/src/components/ProviderSearchFilters/ProviderSearchFilters.stories.tsx @@ -90,65 +90,71 @@ export const Compact: Story = { }; // Sub-component: Service Multi-Select +function ServiceSelectDemoWrapper() { + const [selected, setSelected] = React.useState([]); + return ( +
+ +
+ ); +} + export const ServiceSelectDemo: StoryObj = { - render: () => { - const [selected, setSelected] = React.useState([]); - return ( -
- -
- ); - }, + render: () => , }; // Sub-component: Active Filters chips +function ActiveFiltersDemoWrapper() { + const [filters, setFilters] = React.useState({ + services: ['drug-testing', 'dot-physical'], + radius: 10, + zipCode: '46220', + }); + return ( + { + if (field === 'services' && value) { + setFilters({ + ...filters, + services: filters.services.filter((s) => s !== value), + }); + } else if (field === 'zipCode') { + setFilters({ ...filters, zipCode: undefined }); + } else if (field === 'radius') { + setFilters({ ...filters, radius: 25 }); + } + }} + onClearAll={() => setFilters({ services: [], radius: 25 })} + /> + ); +} + export const ActiveFiltersDemo: StoryObj = { - render: () => { - const [filters, setFilters] = React.useState({ - services: ['drug-testing', 'dot-physical'], - radius: 10, - zipCode: '46220', - }); - return ( - { - if (field === 'services' && value) { - setFilters({ - ...filters, - services: filters.services.filter((s) => s !== value), - }); - } else if (field === 'zipCode') { - setFilters({ ...filters, zipCode: undefined }); - } else if (field === 'radius') { - setFilters({ ...filters, radius: 25 }); - } - }} - onClearAll={() => setFilters({ services: [], radius: 25 })} - /> - ); - }, + render: () => , }; // Sub-component: Compact filter bar +function CompactFilterBarDemoWrapper() { + const [filters, setFilters] = React.useState({ + radius: 25, + services: [], + }); + return ( + + ); +} + export const CompactFilterBarDemo: StoryObj = { - render: () => { - const [filters, setFilters] = React.useState({ - radius: 25, - services: [], - }); - return ( - - ); - }, + render: () => , }; diff --git a/src/components/ProviderSelector/ProviderSelector.stories.tsx b/src/components/ProviderSelector/ProviderSelector.stories.tsx index d99307f..c073d87 100644 --- a/src/components/ProviderSelector/ProviderSelector.stories.tsx +++ b/src/components/ProviderSelector/ProviderSelector.stories.tsx @@ -92,22 +92,24 @@ export const WithSearch: Story = { render: () => , }; -export const NoSelection: Story = { - render: () => { - const [selected, setSelected] = useState(null); +function NoSelectionWrapper() { + const [selected, setSelected] = useState(null); - return ( -
- -
- ); - }, + return ( +
+ +
+ ); +} + +export const NoSelection: Story = { + render: () => , }; export const Loading: Story = { @@ -143,116 +145,122 @@ export const LargeSize: Story = { render: () => , }; -export const WithLogos: Story = { - render: () => { - const providersWithLogos: ProviderOption[] = [ - { - id: '1', - name: 'MedCare Health', - logoUrl: 'https://placehold.co/40x40/0066cc/white?text=MC', - location: 'Fort Wayne, IN', - }, - { - id: '2', - name: 'Wellness First', - logoUrl: 'https://placehold.co/40x40/00cc66/white?text=WF', - location: 'Indianapolis, IN', - }, - { - id: '3', - name: 'Health Partners', - logoUrl: 'https://placehold.co/40x40/cc6600/white?text=HP', - location: 'South Bend, IN', - }, - ]; +const providersWithLogos: ProviderOption[] = [ + { + id: '1', + name: 'MedCare Health', + logoUrl: 'https://placehold.co/40x40/0066cc/white?text=MC', + location: 'Fort Wayne, IN', + }, + { + id: '2', + name: 'Wellness First', + logoUrl: 'https://placehold.co/40x40/00cc66/white?text=WF', + location: 'Indianapolis, IN', + }, + { + id: '3', + name: 'Health Partners', + logoUrl: 'https://placehold.co/40x40/cc6600/white?text=HP', + location: 'South Bend, IN', + }, +]; - const [selected, setSelected] = useState( - providersWithLogos[0] - ); +function WithLogosWrapper() { + const [selected, setSelected] = useState( + providersWithLogos[0] + ); - return ( -
- -
- ); - }, + return ( +
+ +
+ ); +} + +export const WithLogos: Story = { + render: () => , }; -export const ManyProviders: Story = { - render: () => { - const manyProviders: ProviderOption[] = Array.from( - { length: 20 }, - (_, i) => ({ - id: String(i + 1), - name: `Provider ${i + 1}`, - code: `PRV${String(i + 1).padStart(3, '0')}`, - location: `City ${i + 1}, State`, - type: i % 3 === 0 ? 'Type A' : i % 3 === 1 ? 'Type B' : 'Type C', - }) - ); +const manyProviders: ProviderOption[] = Array.from( + { length: 20 }, + (_, i) => ({ + id: String(i + 1), + name: `Provider ${i + 1}`, + code: `PRV${String(i + 1).padStart(3, '0')}`, + location: `City ${i + 1}, State`, + type: i % 3 === 0 ? 'Type A' : i % 3 === 1 ? 'Type B' : 'Type C', + }) +); - const [selected, setSelected] = useState(manyProviders[0]); +function ManyProvidersWrapper() { + const [selected, setSelected] = useState(manyProviders[0]); - return ( -
- -
- ); - }, + return ( +
+ +
+ ); +} + +export const ManyProviders: Story = { + render: () => , }; -export const InactiveProviders: Story = { - render: () => { - const providers: ProviderOption[] = [ - { - id: '1', - name: 'Active Provider A', - location: 'Location A', - isActive: true, - }, - { - id: '2', - name: 'Active Provider B', - location: 'Location B', - isActive: true, - }, - { - id: '3', - name: 'Inactive Provider C', - location: 'Location C', - isActive: false, - }, - { - id: '4', - name: 'Inactive Provider D', - location: 'Location D', - isActive: false, - }, - ]; +const inactiveProvidersList: ProviderOption[] = [ + { + id: '1', + name: 'Active Provider A', + location: 'Location A', + isActive: true, + }, + { + id: '2', + name: 'Active Provider B', + location: 'Location B', + isActive: true, + }, + { + id: '3', + name: 'Inactive Provider C', + location: 'Location C', + isActive: false, + }, + { + id: '4', + name: 'Inactive Provider D', + location: 'Location D', + isActive: false, + }, +]; - const [selected, setSelected] = useState(providers[0]); +function InactiveProvidersWrapper() { + const [selected, setSelected] = useState(inactiveProvidersList[0]); - return ( -
- -
- ); - }, + return ( +
+ +
+ ); +} + +export const InactiveProviders: Story = { + render: () => , }; diff --git a/src/components/RecurringServiceCard/RecurringServiceCard.stories.tsx b/src/components/RecurringServiceCard/RecurringServiceCard.stories.tsx index 0913e99..d014993 100644 --- a/src/components/RecurringServiceCard/RecurringServiceCard.stories.tsx +++ b/src/components/RecurringServiceCard/RecurringServiceCard.stories.tsx @@ -88,142 +88,152 @@ export const PastDueOrder: Story = { export const AddCard: StoryObj = { render: () => (
- alert('Add clicked')} /> + window.alert('Add clicked')} />
), }; +const setupModalProviders = [ + { id: '1', name: 'MedExpress Urgent Care' }, + { id: '2', name: 'Quest Diagnostics' }, + { id: '3', name: 'AudioHealth Center' }, +]; + +const setupModalServices = [ + { id: '1', name: 'DOT Physical Examination' }, + { id: '2', name: 'Drug Screen - 5 Panel' }, + { id: '3', name: 'Hearing Test' }, + { id: '4', name: 'Vision Test' }, +]; + +function SetupModalWrapper() { + const [open, setOpen] = useState(true); + + return ( + <> + + setOpen(false)} + onSave={(data) => { + console.log('Save:', data); + setOpen(false); + }} + providers={setupModalProviders} + services={setupModalServices} + /> + + ); +} + export const SetupModal: StoryObj = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - <> - - setOpen(false)} - onSave={(data) => { - console.log('Save:', data); - setOpen(false); - }} - providers={[ - { id: '1', name: 'MedExpress Urgent Care' }, - { id: '2', name: 'Quest Diagnostics' }, - { id: '3', name: 'AudioHealth Center' }, - ]} - services={[ - { id: '1', name: 'DOT Physical Examination' }, - { id: '2', name: 'Drug Screen - 5 Panel' }, - { id: '3', name: 'Hearing Test' }, - { id: '4', name: 'Vision Test' }, - ]} - /> - - ); - }, + render: () => , }; +function SetupModalBrandedPortalWrapper() { + const [open, setOpen] = useState(true); + + return ( + <> + + setOpen(false)} + onSave={(data) => { + console.log('Save:', data); + setOpen(false); + }} + showProviderSelector={false} + services={setupModalServices.slice(0, 3)} + /> + + ); +} + export const SetupModalBrandedPortal: StoryObj = { - render: () => { - const [open, setOpen] = useState(true); - - return ( - <> - - setOpen(false)} - onSave={(data) => { - console.log('Save:', data); - setOpen(false); - }} - showProviderSelector={false} - services={[ - { id: '1', name: 'DOT Physical Examination' }, - { id: '2', name: 'Drug Screen - 5 Panel' }, - { id: '3', name: 'Hearing Test' }, - ]} - /> - - ); - }, + render: () => , }; +const gridProviders = [ + { id: 'prv-001', name: 'MedExpress Urgent Care' }, + { id: 'prv-002', name: 'Quest Diagnostics' }, + { id: 'prv-003', name: 'AudioHealth Center' }, +]; + +const gridServices = [ + { id: 'srv-001', name: 'DOT Physical Examination' }, + { id: 'srv-002', name: 'Drug Screen - 5 Panel' }, + { id: 'srv-003', name: 'Hearing Test' }, + { id: 'srv-004', name: 'Vision Test' }, +]; + +function GridWrapper() { + const [services, setServices] = useState(sampleServices); + const [modalOpen, setModalOpen] = useState(false); + const [editingService, setEditingService] = useState(null); + + const handleDelete = (service: RecurringService) => { + if (confirm(`Delete ${service.serviceName}?`)) { + setServices(services.filter((s) => s.id !== service.id)); + } + }; + + const handleEdit = (service: RecurringService) => { + setEditingService(service); + setModalOpen(true); + }; + + const handleAdd = () => { + setEditingService(null); + setModalOpen(true); + }; + + const handleSave = (data: RecurringServiceFormData) => { + console.log('Save:', data); + setModalOpen(false); + }; + + return ( + <> + + setModalOpen(false)} + onSave={handleSave} + initialData={ + editingService + ? { + providerId: editingService.providerId, + serviceId: editingService.serviceId, + occurrence: editingService.occurrence, + overrideConsent: editingService.overrideConsent || false, + } + : undefined + } + providers={gridProviders} + services={gridServices} + /> + + ); +} + export const Grid: StoryObj = { - render: () => { - const [services, setServices] = useState(sampleServices); - const [modalOpen, setModalOpen] = useState(false); - const [editingService, setEditingService] = useState(null); - - const handleDelete = (service: RecurringService) => { - if (confirm(`Delete ${service.serviceName}?`)) { - setServices(services.filter((s) => s.id !== service.id)); - } - }; - - const handleEdit = (service: RecurringService) => { - setEditingService(service); - setModalOpen(true); - }; - - const handleAdd = () => { - setEditingService(null); - setModalOpen(true); - }; - - const handleSave = (data: RecurringServiceFormData) => { - console.log('Save:', data); - setModalOpen(false); - }; - - return ( - <> - - setModalOpen(false)} - onSave={handleSave} - initialData={ - editingService - ? { - providerId: editingService.providerId, - serviceId: editingService.serviceId, - occurrence: editingService.occurrence, - overrideConsent: editingService.overrideConsent || false, - } - : undefined - } - providers={[ - { id: 'prv-001', name: 'MedExpress Urgent Care' }, - { id: 'prv-002', name: 'Quest Diagnostics' }, - { id: 'prv-003', name: 'AudioHealth Center' }, - ]} - services={[ - { id: 'srv-001', name: 'DOT Physical Examination' }, - { id: 'srv-002', name: 'Drug Screen - 5 Panel' }, - { id: 'srv-003', name: 'Hearing Test' }, - { id: 'srv-004', name: 'Vision Test' }, - ]} - /> - - ); - }, + render: () => , }; export const GridBrandedPortal: StoryObj = { diff --git a/src/components/RejectionModal/RejectionModal.stories.tsx b/src/components/RejectionModal/RejectionModal.stories.tsx index 8e6d1d4..bd2f063 100644 --- a/src/components/RejectionModal/RejectionModal.stories.tsx +++ b/src/components/RejectionModal/RejectionModal.stories.tsx @@ -168,21 +168,23 @@ export const NonDangerVariant: Story = { ), }; -export const Submitting: Story = { - render: () => { - const [open, setOpen] = useState(true); +function SubmittingWrapper() { + const [open, setOpen] = useState(true); - return ( - <> - - - - ); - }, + return ( + <> + + + + ); +} + +export const Submitting: Story = { + render: () => , }; diff --git a/src/components/ServiceAccordion/ServiceAccordion.stories.tsx b/src/components/ServiceAccordion/ServiceAccordion.stories.tsx index 8b9d7ee..633f07c 100644 --- a/src/components/ServiceAccordion/ServiceAccordion.stories.tsx +++ b/src/components/ServiceAccordion/ServiceAccordion.stories.tsx @@ -111,18 +111,20 @@ export const AllVariants: Story = { }; // With controlled expanded state +function ControlledExpandedWrapper() { + const [expanded, setExpanded] = React.useState(['Drug & Alcohol Testing']); + return ( + console.log('Clicked:', service.slug)} + /> + ); +} + export const ControlledExpanded: Story = { - render: () => { - const [expanded, setExpanded] = React.useState(['Drug & Alcohol Testing']); - return ( - console.log('Clicked:', service.slug)} - /> - ); - }, + render: () => , }; // Sub-component: Tag Cloud diff --git a/src/components/ServiceBadge/ServiceBadge.stories.tsx b/src/components/ServiceBadge/ServiceBadge.stories.tsx index f4a40a8..33905f6 100644 --- a/src/components/ServiceBadge/ServiceBadge.stories.tsx +++ b/src/components/ServiceBadge/ServiceBadge.stories.tsx @@ -83,25 +83,27 @@ export const AllSizes: Story = { }; // Removable badge -export const Removable: Story = { - render: () => { - const [visible, setVisible] = React.useState(true); - if (!visible) { - return ( - - ); - } +function RemovableWrapper() { + const [visible, setVisible] = React.useState(true); + if (!visible) { return ( - setVisible(false)}> - Remove Me - + ); - }, + } + return ( + setVisible(false)}> + Remove Me + + ); +} + +export const Removable: Story = { + render: () => , }; // Badge group for provider cards @@ -135,18 +137,20 @@ export const CategoryBadges: StoryObj = { }; // Selected services (filter chips) +function SelectedServicesWrapper() { + const [services, setServices] = React.useState(mockServices.slice(0, 2)); + return ( + + setServices(services.filter((s) => s.slug !== slug)) + } + /> + ); +} + export const SelectedServices: StoryObj = { - render: () => { - const [services, setServices] = React.useState(mockServices.slice(0, 2)); - return ( - - setServices(services.filter((s) => s.slug !== slug)) - } - /> - ); - }, + render: () => , }; // DOT compliance badge diff --git a/src/components/ServicePicker/ServicePicker.stories.tsx b/src/components/ServicePicker/ServicePicker.stories.tsx index 2b9843e..18349c9 100644 --- a/src/components/ServicePicker/ServicePicker.stories.tsx +++ b/src/components/ServicePicker/ServicePicker.stories.tsx @@ -207,19 +207,21 @@ export const NoSearch: Story = { }; // Loading state +function LoadingWrapper() { + const [selectedIds, setSelectedIds] = useState([]); + return ( + + ); +} + export const Loading: Story = { - render: () => { - const [selectedIds, setSelectedIds] = useState([]); - return ( - - ); - }, + render: () => , parameters: { docs: { description: { story: 'Service picker in loading state.' }, @@ -228,19 +230,21 @@ export const Loading: Story = { }; // Error state +function ErrorWrapper() { + const [selectedIds, setSelectedIds] = useState([]); + return ( + + ); +} + export const Error: Story = { - render: () => { - const [selectedIds, setSelectedIds] = useState([]); - return ( - - ); - }, + render: () => , parameters: { docs: { description: { story: 'Service picker showing an error state.' }, @@ -249,19 +253,21 @@ export const Error: Story = { }; // Empty state +function EmptyWrapper() { + const [selectedIds, setSelectedIds] = useState([]); + return ( + + ); +} + export const Empty: Story = { - render: () => { - const [selectedIds, setSelectedIds] = useState([]); - return ( - - ); - }, + render: () => , parameters: { docs: { description: { story: 'Service picker with no available services.' }, diff --git a/src/components/SiteFooter/SiteFooter.stories.tsx b/src/components/SiteFooter/SiteFooter.stories.tsx index a15d503..73e6c70 100644 --- a/src/components/SiteFooter/SiteFooter.stories.tsx +++ b/src/components/SiteFooter/SiteFooter.stories.tsx @@ -92,7 +92,7 @@ export const WithNewsletter: Story = { linkGroups: defaultLinkGroups, socialLinks: defaultSocialLinks, showNewsletter: true, - onNewsletterSubmit: (email) => alert(`Subscribed: ${email}`), + onNewsletterSubmit: (email) => window.alert(`Subscribed: ${email}`), }, }; @@ -133,13 +133,13 @@ export const NewsletterDemo: StoryObj = {
alert(`Subscribed: ${email}`)} + onSubmit={(email) => window.alert(`Subscribed: ${email}`)} />
alert(`Subscribed: ${email}`)} + onSubmit={(email) => window.alert(`Subscribed: ${email}`)} />
diff --git a/src/components/SiteHeader/SiteHeader.stories.tsx b/src/components/SiteHeader/SiteHeader.stories.tsx index 2ec28bc..4ad543d 100644 --- a/src/components/SiteHeader/SiteHeader.stories.tsx +++ b/src/components/SiteHeader/SiteHeader.stories.tsx @@ -51,8 +51,8 @@ export const Default: Story = { args: { logo: { name: 'BlueHive' }, links: defaultLinks, - onLogin: () => alert('Login clicked'), - onSignUp: () => alert('Sign up clicked'), + onLogin: () => window.alert('Login clicked'), + onSignUp: () => window.alert('Sign up clicked'), }, }; @@ -62,8 +62,8 @@ export const LoggedIn: Story = { logo: { name: 'BlueHive' }, links: defaultLinks, user: sampleUser, - onLogout: () => alert('Logout clicked'), - onProfile: () => alert('Profile clicked'), + onLogout: () => window.alert('Logout clicked'), + onProfile: () => window.alert('Profile clicked'), }, }; diff --git a/src/components/WebChartReportViewer/WebChartReportViewer.stories.tsx b/src/components/WebChartReportViewer/WebChartReportViewer.stories.tsx index df29311..80173fb 100644 --- a/src/components/WebChartReportViewer/WebChartReportViewer.stories.tsx +++ b/src/components/WebChartReportViewer/WebChartReportViewer.stories.tsx @@ -50,39 +50,41 @@ const sampleResult: ReportResult = { `, }; +function DefaultWrapper() { + const [currentReport, setCurrentReport] = useState(); + const [reportResult, setReportResult] = useState(); + const [loadingReport, setLoadingReport] = useState(false); + + const handleReportSelect = (report: SystemReport) => { + setCurrentReport(report); + setLoadingReport(true); + // Simulate loading + setTimeout(() => { + setReportResult(sampleResult); + setLoadingReport(false); + }, 1500); + }; + + return ( + console.log('Refresh reports')} + onRefreshReport={() => console.log('Refresh report')} + dateRange={{ + start: new Date(new Date().getFullYear(), new Date().getMonth(), 1), + end: new Date(), + }} + onDateRangeChange={(start, end) => console.log('Date range:', start, end)} + /> + ); +} + export const Default: Story = { - render: () => { - const [currentReport, setCurrentReport] = useState(); - const [reportResult, setReportResult] = useState(); - const [loadingReport, setLoadingReport] = useState(false); - - const handleReportSelect = (report: SystemReport) => { - setCurrentReport(report); - setLoadingReport(true); - // Simulate loading - setTimeout(() => { - setReportResult(sampleResult); - setLoadingReport(false); - }, 1500); - }; - - return ( - console.log('Refresh reports')} - onRefreshReport={() => console.log('Refresh report')} - dateRange={{ - start: new Date(new Date().getFullYear(), new Date().getMonth(), 1), - end: new Date(), - }} - onDateRangeChange={(start, end) => console.log('Date range:', start, end)} - /> - ); - }, + render: () => , }; export const Loading: Story = { @@ -117,26 +119,28 @@ export const ReportWithError: Story = { }, }; +function DatePickerWrapper() { + const [range, setRange] = useState({ + start: new Date(new Date().getFullYear(), new Date().getMonth(), 1), + end: new Date(), + }); + + return ( + { + setRange({ + start: typeof start === 'string' ? new Date(start) : start, + end: typeof end === 'string' ? new Date(end) : end, + }); + }} + /> + ); +} + export const DatePicker: StoryObj = { - render: () => { - const [range, setRange] = useState({ - start: new Date(new Date().getFullYear(), new Date().getMonth(), 1), - end: new Date(), - }); - - return ( - { - setRange({ - start: typeof start === 'string' ? new Date(start) : start, - end: typeof end === 'string' ? new Date(end) : end, - }); - }} - /> - ); - }, + render: () => , }; export const CustomBranding: Story = { From 739545342b4d368a9cd77a881088fb8a993e5903 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:16:48 -0500 Subject: [PATCH 02/17] fix(lint): remove unused variables and imports across 24 components --- src/components/AuthDialog/AuthDialog.tsx | 1 - .../BookingDialog/BookingDialog.tsx | 2 +- .../BusinessHoursEditor.tsx | 4 +-- .../CSVColumnMapper/CSVColumnMapper.tsx | 3 +- .../ConnectionStatus/ConnectionStatus.tsx | 2 +- .../CreateReferralModal.tsx | 2 -- .../DateRangePicker/DateRangePicker.tsx | 4 +-- .../DocumentScanner.stories.tsx | 1 - .../EmployeeProfile/EmployeeProfile.tsx | 1 - .../EmployerServiceModal.tsx | 1 - src/components/FileManager/FileManager.tsx | 1 - .../HRISProviderSelector.tsx | 1 - .../InviteUserModal/InviteUserModal.tsx | 2 +- src/components/InvoiceView/InvoiceView.tsx | 2 +- .../OnboardingWizard/OnboardingWizard.tsx | 2 +- src/components/Progress/Progress.stories.tsx | 34 +----------------- src/components/ProviderCard/ProviderCard.tsx | 1 - .../ProviderOverview/ProviderOverview.tsx | 2 +- .../ProviderSearchBar/ProviderSearchBar.tsx | 21 ----------- .../ProviderSearchFilters.tsx | 36 ------------------- .../ScheduleCalendar/ScheduleCalendar.tsx | 9 ----- .../ServiceAccordion/ServiceAccordion.tsx | 2 +- .../ServicePricingManager.tsx | 8 +---- .../WebChartReportViewer.tsx | 1 - 24 files changed, 14 insertions(+), 129 deletions(-) diff --git a/src/components/AuthDialog/AuthDialog.tsx b/src/components/AuthDialog/AuthDialog.tsx index a30fbb6..5ef6394 100644 --- a/src/components/AuthDialog/AuthDialog.tsx +++ b/src/components/AuthDialog/AuthDialog.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '../../utils/cn'; // ============================================================================= diff --git a/src/components/BookingDialog/BookingDialog.tsx b/src/components/BookingDialog/BookingDialog.tsx index 5e52659..ceeadbe 100644 --- a/src/components/BookingDialog/BookingDialog.tsx +++ b/src/components/BookingDialog/BookingDialog.tsx @@ -588,7 +588,7 @@ export interface InlineBookingFormProps { } export function InlineBookingForm({ - provider, + provider: _provider, services = [], onSubmit, defaultValues, diff --git a/src/components/BusinessHoursEditor/BusinessHoursEditor.tsx b/src/components/BusinessHoursEditor/BusinessHoursEditor.tsx index ce58810..3281cc2 100644 --- a/src/components/BusinessHoursEditor/BusinessHoursEditor.tsx +++ b/src/components/BusinessHoursEditor/BusinessHoursEditor.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { useState, useCallback } from 'react'; +import { useCallback } from 'react'; import { Button } from '../Button/Button'; import { Input } from '../Input/Input'; import { cn } from '../../utils/cn'; @@ -94,7 +94,7 @@ export function BusinessHoursEditor({ onChange, disabled = false, showDescription = true, - use24Hour = false, + use24Hour: _use24Hour = false, weekStartsOn = 0, className, addHoursLabel = 'Add Hours', diff --git a/src/components/CSVColumnMapper/CSVColumnMapper.tsx b/src/components/CSVColumnMapper/CSVColumnMapper.tsx index 85e4130..9ecde11 100644 --- a/src/components/CSVColumnMapper/CSVColumnMapper.tsx +++ b/src/components/CSVColumnMapper/CSVColumnMapper.tsx @@ -204,7 +204,7 @@ interface CSVColumnCardProps { function CSVColumnCard({ column, - index, + index: _index, fieldOptions, childFieldOptions, onMappingChange, @@ -354,7 +354,6 @@ export function CSVFileUpload({ }: CSVFileUploadProps) { const { selectFile = 'Select a file to upload or drag and drop', - dragAndDrop = 'Drag and drop your CSV file here', selectButton = 'Select File to Upload', } = labels; diff --git a/src/components/ConnectionStatus/ConnectionStatus.tsx b/src/components/ConnectionStatus/ConnectionStatus.tsx index af980f8..ec9221c 100644 --- a/src/components/ConnectionStatus/ConnectionStatus.tsx +++ b/src/components/ConnectionStatus/ConnectionStatus.tsx @@ -105,7 +105,7 @@ export function ConnectionStatusOverlay({ animate = true, onReload, message, - logoUrl, + logoUrl: _logoUrl, className, }: ConnectionStatusOverlayProps) { if (!isVisible || connection.status === 'connected') return null; diff --git a/src/components/CreateReferralModal/CreateReferralModal.tsx b/src/components/CreateReferralModal/CreateReferralModal.tsx index 1352b9e..ac8c24e 100644 --- a/src/components/CreateReferralModal/CreateReferralModal.tsx +++ b/src/components/CreateReferralModal/CreateReferralModal.tsx @@ -3,8 +3,6 @@ import * as React from 'react'; import { Modal, ModalHeader, ModalTitle, ModalFooter } from '../Modal/Modal'; import { Button } from '../Button/Button'; -import { Input } from '../Input/Input'; -import { Badge } from '../Badge/Badge'; export interface Employee { id: string; diff --git a/src/components/DateRangePicker/DateRangePicker.tsx b/src/components/DateRangePicker/DateRangePicker.tsx index cbc5d3c..301ac63 100644 --- a/src/components/DateRangePicker/DateRangePicker.tsx +++ b/src/components/DateRangePicker/DateRangePicker.tsx @@ -218,7 +218,7 @@ function calculateDateRange(presetKey: string): DateRange { } } -function formatDateRange(range: DateRange, format?: string): string { +function formatDateRange(range: DateRange, _format?: string): string { if (!range.start && !range.end) return ''; const formatDate = (d: Date | null) => { if (!d) return ''; @@ -396,7 +396,7 @@ export interface DateRangeFilterProps { * ``` */ export function DateRangeFilter({ - value, + value: _value, onChange, presets, activePreset, diff --git a/src/components/DocumentScanner/DocumentScanner.stories.tsx b/src/components/DocumentScanner/DocumentScanner.stories.tsx index db16132..7b0270c 100644 --- a/src/components/DocumentScanner/DocumentScanner.stories.tsx +++ b/src/components/DocumentScanner/DocumentScanner.stories.tsx @@ -1,7 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import * as React from 'react'; import { DocumentScanner } from './DocumentScanner'; -import { ThemeProvider } from '../ThemeProvider'; /** * The DocumentScanner component provides a comprehensive solution for scanning diff --git a/src/components/EmployeeProfile/EmployeeProfile.tsx b/src/components/EmployeeProfile/EmployeeProfile.tsx index 4af799e..76cb688 100644 --- a/src/components/EmployeeProfile/EmployeeProfile.tsx +++ b/src/components/EmployeeProfile/EmployeeProfile.tsx @@ -12,7 +12,6 @@ import { Check, X, Clock, - DollarSign, Edit2, } from 'lucide-react'; diff --git a/src/components/EmployerServiceModal/EmployerServiceModal.tsx b/src/components/EmployerServiceModal/EmployerServiceModal.tsx index ec3edcc..6dc74eb 100644 --- a/src/components/EmployerServiceModal/EmployerServiceModal.tsx +++ b/src/components/EmployerServiceModal/EmployerServiceModal.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { Modal, ModalHeader, ModalTitle, ModalFooter } from '../Modal/Modal'; import { Button } from '../Button/Button'; import { Input } from '../Input/Input'; -import { Select } from '../Select/Select'; import { Switch } from '../Switch/Switch'; export interface EmployerServiceModalProps { diff --git a/src/components/FileManager/FileManager.tsx b/src/components/FileManager/FileManager.tsx index 9816ffc..e0d0248 100644 --- a/src/components/FileManager/FileManager.tsx +++ b/src/components/FileManager/FileManager.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Card } from '../Card/Card'; -import { Button } from '../Button/Button'; import { Progress } from '../Progress/Progress'; export interface FileItem { diff --git a/src/components/HRISProviderSelector/HRISProviderSelector.tsx b/src/components/HRISProviderSelector/HRISProviderSelector.tsx index 00ce31c..59d6b44 100644 --- a/src/components/HRISProviderSelector/HRISProviderSelector.tsx +++ b/src/components/HRISProviderSelector/HRISProviderSelector.tsx @@ -72,7 +72,6 @@ export function HRISProviderSelector({ const { search = 'Search HR Providers...', importCSV = 'Import from CSV', - connected = 'Connected', lastSync = 'Last HRIS Sync', disconnect = 'Disconnect', refreshSync = 'Update Employees', diff --git a/src/components/InviteUserModal/InviteUserModal.tsx b/src/components/InviteUserModal/InviteUserModal.tsx index 6d3eb8f..1221152 100644 --- a/src/components/InviteUserModal/InviteUserModal.tsx +++ b/src/components/InviteUserModal/InviteUserModal.tsx @@ -51,7 +51,7 @@ export function InviteUserModal({ roles, defaultRoleId, isSubmitting = false, - entityName = 'provider', + entityName: _entityName = 'provider', entityDisplayName, errorMessage, successMessage, diff --git a/src/components/InvoiceView/InvoiceView.tsx b/src/components/InvoiceView/InvoiceView.tsx index 1fc4ed4..2b185f8 100644 --- a/src/components/InvoiceView/InvoiceView.tsx +++ b/src/components/InvoiceView/InvoiceView.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Badge } from '../Badge/Badge'; import { Button } from '../Button/Button'; -import { Card, CardHeader, CardTitle, CardContent } from '../Card/Card'; +import { Card, CardContent } from '../Card/Card'; export interface InvoiceLineItem { id: string; diff --git a/src/components/OnboardingWizard/OnboardingWizard.tsx b/src/components/OnboardingWizard/OnboardingWizard.tsx index 0000b77..8af9b13 100644 --- a/src/components/OnboardingWizard/OnboardingWizard.tsx +++ b/src/components/OnboardingWizard/OnboardingWizard.tsx @@ -270,7 +270,7 @@ export function OnboardingStepQuestion({ description, options = [], onSelect, - multiple = false, + multiple: _multiple = false, children, }: OnboardingStepQuestionProps) { return ( diff --git a/src/components/Progress/Progress.stories.tsx b/src/components/Progress/Progress.stories.tsx index ec3d0b9..830ec77 100644 --- a/src/components/Progress/Progress.stories.tsx +++ b/src/components/Progress/Progress.stories.tsx @@ -193,39 +193,7 @@ export const CustomMax: Story = { }; // Circular Progress stories -const circularMeta: Meta = { - component: CircularProgress, - argTypes: { - value: { - control: { type: 'range', min: 0, max: 100 }, - description: 'Current progress value (0-100)', - }, - size: { - control: 'select', - options: ['sm', 'md', 'lg', 'xl'], - description: 'Size of the circular progress', - }, - variant: { - control: 'select', - options: ['default', 'success', 'warning', 'danger'], - description: 'Visual style variant', - }, - showValue: { - control: 'boolean', - description: 'Show the percentage value in the center', - }, - indeterminate: { - control: 'boolean', - description: 'Show indeterminate spinning state', - }, - strokeWidth: { - control: { type: 'number', min: 1, max: 20 }, - description: 'Width of the progress stroke', - }, - }, -}; - -type CircularStory = StoryObj; +type CircularStory = StoryObj; export const CircularDefault: CircularStory = { args: { diff --git a/src/components/ProviderCard/ProviderCard.tsx b/src/components/ProviderCard/ProviderCard.tsx index d447c1e..9f03ece 100644 --- a/src/components/ProviderCard/ProviderCard.tsx +++ b/src/components/ProviderCard/ProviderCard.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '../../utils/cn'; import { Badge } from '../Badge'; -import { Button } from '../Button'; import { Tooltip } from '../Tooltip'; // ============================================================================ diff --git a/src/components/ProviderOverview/ProviderOverview.tsx b/src/components/ProviderOverview/ProviderOverview.tsx index 8a03769..03c6473 100644 --- a/src/components/ProviderOverview/ProviderOverview.tsx +++ b/src/components/ProviderOverview/ProviderOverview.tsx @@ -67,7 +67,7 @@ export function ProviderOverview({ onStatClick, onQuickActionClick, onActivityClick, - currency = '$', + currency: _currency = '$', isLoading = false, className = '', }: ProviderOverviewProps) { diff --git a/src/components/ProviderSearchBar/ProviderSearchBar.tsx b/src/components/ProviderSearchBar/ProviderSearchBar.tsx index b3a8db1..399c1bd 100644 --- a/src/components/ProviderSearchBar/ProviderSearchBar.tsx +++ b/src/components/ProviderSearchBar/ProviderSearchBar.tsx @@ -52,27 +52,6 @@ const searchBarVariants = cva('', { // Icons // ============================================================================ -const LocationIcon: React.FC<{ className?: string }> = ({ className }) => ( - - - - -); - const CrosshairsIcon: React.FC<{ className?: string }> = ({ className }) => ( - ); -} - function XMarkIcon({ className }: { className?: string }) { return ( - ); -} - // ============================================================================ // Sub-components // ============================================================================ diff --git a/src/components/ScheduleCalendar/ScheduleCalendar.tsx b/src/components/ScheduleCalendar/ScheduleCalendar.tsx index 2f543b5..c57e357 100644 --- a/src/components/ScheduleCalendar/ScheduleCalendar.tsx +++ b/src/components/ScheduleCalendar/ScheduleCalendar.tsx @@ -1,7 +1,6 @@ 'use client'; import * as React from 'react'; -import { Badge } from '../Badge/Badge'; import { Button } from '../Button/Button'; export interface CalendarAppointment { @@ -64,14 +63,6 @@ export function ScheduleCalendar({ }); }; - const formatDateHeader = (date: Date) => { - return date.toLocaleDateString('en-US', { - weekday: 'short', - month: 'short', - day: 'numeric', - }); - }; - const isSameDay = (date1: Date, date2: Date) => { return ( date1.getFullYear() === date2.getFullYear() && diff --git a/src/components/ServiceAccordion/ServiceAccordion.tsx b/src/components/ServiceAccordion/ServiceAccordion.tsx index fd8bb57..81c42b7 100644 --- a/src/components/ServiceAccordion/ServiceAccordion.tsx +++ b/src/components/ServiceAccordion/ServiceAccordion.tsx @@ -294,7 +294,7 @@ function CategoryAccordionItem({ basePath, onServiceClick, index, - allowMultiple = true, + allowMultiple: _allowMultiple = true, expandedCategories, onExpandChange, }: CategoryAccordionItemProps) { diff --git a/src/components/ServicePricingManager/ServicePricingManager.tsx b/src/components/ServicePricingManager/ServicePricingManager.tsx index cbbbdc9..859bdc5 100644 --- a/src/components/ServicePricingManager/ServicePricingManager.tsx +++ b/src/components/ServicePricingManager/ServicePricingManager.tsx @@ -47,7 +47,7 @@ export function ServicePricingManager({ onBulkUpdate, isSaving = false, isLoading = false, - categories = [], + categories: _categories = [], className = '', }: ServicePricingManagerProps) { const [searchTerm, setSearchTerm] = React.useState(''); @@ -66,12 +66,6 @@ export function ServicePricingManager({ }).format(amount); }; - const formatDate = (date?: Date | string) => { - if (!date) return 'Never'; - const d = typeof date === 'string' ? new Date(date) : date; - return d.toLocaleDateString(); - }; - // Filter services const filteredServices = services.filter((service) => { const matchesSearch = diff --git a/src/components/WebChartReportViewer/WebChartReportViewer.tsx b/src/components/WebChartReportViewer/WebChartReportViewer.tsx index a7fa8ee..464eb05 100644 --- a/src/components/WebChartReportViewer/WebChartReportViewer.tsx +++ b/src/components/WebChartReportViewer/WebChartReportViewer.tsx @@ -103,7 +103,6 @@ export function WebChartReportViewer({ refreshReport = 'Refresh', reconnect = 'Reconnect', noReports = 'No reports available', - loadingReports = 'Loading reports...', loadingData = 'Fetching latest data from Enterprise Health...', close = 'Close', dateFrom = 'From', From cd7a8eae4b6a35e7918e27ddd752d6d420b3874a Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:20:03 -0500 Subject: [PATCH 03/17] fix(lint): escape JSX entities and add missing DOM globals to eslint config - Escaped apostrophes and quotes in JSX text content: - AddContactModal: "Add Field" quotes - AuthDialog: We've, Don't, we'll apostrophes - InvoicePaymentPage: you're, doesn't apostrophes - OnboardingWizard: You're apostrophes - Added missing DOM globals to eslint.config.js: - HTMLFormElement, HTMLSelectElement, HTMLAnchorElement - SVGSVGElement, Node - confirm (browser API) - google (Google Maps API) Reduces total errors from 111 to 83 (-28 errors) --- eslint.config.js | 11 +++++++++++ src/components/AddContactModal/AddContactModal.tsx | 2 +- src/components/AuthDialog/AuthDialog.tsx | 6 +++--- .../InvoicePaymentPage/InvoicePaymentPage.tsx | 2 +- src/components/OnboardingWizard/OnboardingWizard.tsx | 4 ++-- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index d5a84e2..dcdfad5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,7 @@ export default [ clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', + // DOM Elements HTMLElement: 'readonly', HTMLInputElement: 'readonly', HTMLButtonElement: 'readonly', @@ -45,6 +46,12 @@ export default [ HTMLCanvasElement: 'readonly', HTMLImageElement: 'readonly', HTMLVideoElement: 'readonly', + HTMLFormElement: 'readonly', + HTMLSelectElement: 'readonly', + HTMLAnchorElement: 'readonly', + SVGSVGElement: 'readonly', + Node: 'readonly', + // Events KeyboardEvent: 'readonly', MouseEvent: 'readonly', FocusEvent: 'readonly', @@ -52,6 +59,10 @@ export default [ MediaQueryList: 'readonly', MediaQueryListEvent: 'readonly', NodeJS: 'readonly', + // Browser APIs + confirm: 'readonly', + // Google Maps API (loaded externally) + google: 'readonly', // Audio/Media APIs Blob: 'readonly', URL: 'readonly', diff --git a/src/components/AddContactModal/AddContactModal.tsx b/src/components/AddContactModal/AddContactModal.tsx index 5be579c..d8e3a96 100644 --- a/src/components/AddContactModal/AddContactModal.tsx +++ b/src/components/AddContactModal/AddContactModal.tsx @@ -449,7 +449,7 @@ export function AddContactModal({ {(!formData.customFields || formData.customFields.length === 0) && (

- No custom fields added. Click "Add Field" to add custom information. + No custom fields added. Click "Add Field" to add custom information.

)}
diff --git a/src/components/AuthDialog/AuthDialog.tsx b/src/components/AuthDialog/AuthDialog.tsx index 5ef6394..5a28183 100644 --- a/src/components/AuthDialog/AuthDialog.tsx +++ b/src/components/AuthDialog/AuthDialog.tsx @@ -314,7 +314,7 @@ export function AuthDialog({

- We've sent a verification email to your inbox. Please click the + We've sent a verification email to your inbox. Please click the link to verify your account.

From 3a1a6072e87ca275f354a1efe0cc5898d4f99f9f Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:22:38 -0500 Subject: [PATCH 04/17] fix(a11y): convert placeholder anchor links to buttons - HelpSupportPanel: Quick Links section now uses buttons - OnboardingWizard: Logo section uses span instead of anchor - OrderLookupForm: Contact Support uses button - ProductVersion.stories: Footer links use buttons - StripeBadge.stories: Privacy Policy uses button Reduces total errors from 83 to 73 (-10 errors) --- .../HelpSupportPanel/HelpSupportPanel.tsx | 32 +++++++++---------- .../OnboardingWizard/OnboardingWizard.tsx | 4 +-- .../OrderLookupForm/OrderLookupForm.tsx | 6 ++-- .../ProductVersion/ProductVersion.stories.tsx | 12 +++---- .../StripeBadge/StripeBadge.stories.tsx | 4 +-- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/components/HelpSupportPanel/HelpSupportPanel.tsx b/src/components/HelpSupportPanel/HelpSupportPanel.tsx index f486a0f..de32d46 100644 --- a/src/components/HelpSupportPanel/HelpSupportPanel.tsx +++ b/src/components/HelpSupportPanel/HelpSupportPanel.tsx @@ -357,42 +357,42 @@ export function HelpSupportPanel({ Quick Links - Getting Started Guide - - + diff --git a/src/components/OnboardingWizard/OnboardingWizard.tsx b/src/components/OnboardingWizard/OnboardingWizard.tsx index 738cebe..21e17bb 100644 --- a/src/components/OnboardingWizard/OnboardingWizard.tsx +++ b/src/components/OnboardingWizard/OnboardingWizard.tsx @@ -121,7 +121,7 @@ export function OnboardingWizard({ {showHeader && ( diff --git a/src/components/OrderLookupForm/OrderLookupForm.tsx b/src/components/OrderLookupForm/OrderLookupForm.tsx index c8d1235..2d30174 100644 --- a/src/components/OrderLookupForm/OrderLookupForm.tsx +++ b/src/components/OrderLookupForm/OrderLookupForm.tsx @@ -189,12 +189,12 @@ export function OrderLookupForm({

Need help?{' '} - Contact Support - +

diff --git a/src/components/ProductVersion/ProductVersion.stories.tsx b/src/components/ProductVersion/ProductVersion.stories.tsx index b3641c3..4541396 100644 --- a/src/components/ProductVersion/ProductVersion.stories.tsx +++ b/src/components/ProductVersion/ProductVersion.stories.tsx @@ -148,15 +148,15 @@ export const InFooter: Story = { size="sm" /> diff --git a/src/components/StripeBadge/StripeBadge.stories.tsx b/src/components/StripeBadge/StripeBadge.stories.tsx index 347ab20..169abbc 100644 --- a/src/components/StripeBadge/StripeBadge.stories.tsx +++ b/src/components/StripeBadge/StripeBadge.stories.tsx @@ -173,9 +173,9 @@ export const InCheckoutFooter: Story = {
256-bit SSL encryption - +
), From f02add188b8afa53dfe011a20e3462971d4d5aa4 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:23:49 -0500 Subject: [PATCH 05/17] fix(lint): fix type declarations and case block scope - Address.tsx: Convert AddressInlineProps and AddressCompactProps from empty interfaces to type aliases - ErrorPage.tsx: Convert NotFoundPageProps from empty interface to type alias - WebChartReportViewer.tsx: Wrap case block const declaration in braces Reduces total errors from 73 to 69 (-4 errors) --- src/components/Address/Address.tsx | 6 +++--- src/components/ErrorPage/ErrorPage.tsx | 2 +- .../WebChartReportViewer/WebChartReportViewer.tsx | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Address/Address.tsx b/src/components/Address/Address.tsx index f1ff1d3..a8dd6f4 100644 --- a/src/components/Address/Address.tsx +++ b/src/components/Address/Address.tsx @@ -368,7 +368,7 @@ export function AddressCard({ // AddressInline - Convenience component for inline addresses // ============================================================================= -export interface AddressInlineProps extends Omit {} +export type AddressInlineProps = Omit; /** * Convenience component for inline address display. @@ -382,10 +382,10 @@ export function AddressInline(props: AddressInlineProps) { // AddressCompact - Convenience component for compact addresses // ============================================================================= -export interface AddressCompactProps extends Omit< +export type AddressCompactProps = Omit< AddressProps, 'format' | 'hideStreet' -> {} +>; /** * Convenience component for compact city/state display. diff --git a/src/components/ErrorPage/ErrorPage.tsx b/src/components/ErrorPage/ErrorPage.tsx index 704b312..890ab9c 100644 --- a/src/components/ErrorPage/ErrorPage.tsx +++ b/src/components/ErrorPage/ErrorPage.tsx @@ -227,7 +227,7 @@ export function ErrorPage({ // NotFoundPage Component // ============================================================================= -export interface NotFoundPageProps extends Omit {} +export type NotFoundPageProps = Omit; /** * A pre-configured 404 Not Found page. diff --git a/src/components/WebChartReportViewer/WebChartReportViewer.tsx b/src/components/WebChartReportViewer/WebChartReportViewer.tsx index 464eb05..0df5e11 100644 --- a/src/components/WebChartReportViewer/WebChartReportViewer.tsx +++ b/src/components/WebChartReportViewer/WebChartReportViewer.tsx @@ -357,10 +357,11 @@ export function ReportDatePicker({ case 'this-month': start = new Date(now.getFullYear(), now.getMonth(), 1); break; - case 'this-quarter': + case 'this-quarter': { const quarter = Math.floor(now.getMonth() / 3); start = new Date(now.getFullYear(), quarter * 3, 1); break; + } case 'this-year': start = new Date(now.getFullYear(), 0, 1); break; From d673e07ae53d7c1ca02b500da5408d916e7698df Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:26:50 -0500 Subject: [PATCH 06/17] fix(a11y): add keyboard accessibility to clickable elements - Added role='button', tabIndex, and onKeyDown handlers to interactive divs - AuthDialog: backdrop supports Escape key - CheckrIntegration: modal backdrop is keyboard accessible - EmployerContactCard: contact items support Enter key - EmployerList: employer items support Enter key - LanguageSelector: options support Enter key - NotificationCenter: notification items support Enter key - ProviderOverview: activity items and stat cards support Enter key - ScheduleCalendar: hour slots and appointments support Enter key - WebChartReportViewer: offcanvas backdrop is keyboard accessible Reduces total errors from 69 to 49 (-20 errors) --- src/components/AuthDialog/AuthDialog.tsx | 1 + .../CheckrIntegration/CheckrIntegration.tsx | 4 ++++ .../EmployerContactCard/EmployerContactCard.tsx | 3 +++ src/components/EmployerList/EmployerList.tsx | 3 +++ src/components/LanguageSelector/LanguageSelector.tsx | 1 + .../NotificationCenter/NotificationCenter.tsx | 12 ++++++++++++ src/components/ProviderOverview/ProviderOverview.tsx | 6 ++++++ src/components/ScheduleCalendar/ScheduleCalendar.tsx | 12 ++++++++++++ .../WebChartReportViewer/WebChartReportViewer.tsx | 4 ++++ 9 files changed, 46 insertions(+) diff --git a/src/components/AuthDialog/AuthDialog.tsx b/src/components/AuthDialog/AuthDialog.tsx index 5a28183..7c438e7 100644 --- a/src/components/AuthDialog/AuthDialog.tsx +++ b/src/components/AuthDialog/AuthDialog.tsx @@ -160,6 +160,7 @@ export function AuthDialog({ aria-labelledby="auth-dialog-title" className="animate-in fade-in fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm duration-200" onClick={(e) => e.target === e.currentTarget && onClose()} + onKeyDown={(e) => e.key === 'Escape' && onClose()} >
setShowInviteModal(false)} + onKeyDown={(e) => e.key === 'Enter' && setShowInviteModal(false)} />

{inviteCandidate}

diff --git a/src/components/EmployerContactCard/EmployerContactCard.tsx b/src/components/EmployerContactCard/EmployerContactCard.tsx index 9d0c130..1d27974 100644 --- a/src/components/EmployerContactCard/EmployerContactCard.tsx +++ b/src/components/EmployerContactCard/EmployerContactCard.tsx @@ -130,11 +130,14 @@ export function EmployerContactCard({ {contacts.map((contact) => (
onContactClick?.(contact)} + onKeyDown={(e) => e.key === 'Enter' && onContactClick?.(contact)} >
diff --git a/src/components/EmployerList/EmployerList.tsx b/src/components/EmployerList/EmployerList.tsx index aed7678..316a735 100644 --- a/src/components/EmployerList/EmployerList.tsx +++ b/src/components/EmployerList/EmployerList.tsx @@ -178,7 +178,10 @@ export function EmployerList({ {filteredEmployers.map((employer) => (
onEmployerClick?.(employer)} + onKeyDown={(e) => e.key === 'Enter' && onEmployerClick?.(employer)} className={` p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg ${onEmployerClick ? 'cursor-pointer hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-sm transition-all' : ''} diff --git a/src/components/LanguageSelector/LanguageSelector.tsx b/src/components/LanguageSelector/LanguageSelector.tsx index c9fe955..8a71e57 100644 --- a/src/components/LanguageSelector/LanguageSelector.tsx +++ b/src/components/LanguageSelector/LanguageSelector.tsx @@ -227,6 +227,7 @@ export function LanguageSelector({ role="option" aria-selected={language.code === value} onClick={() => handleSelect(language)} + onKeyDown={(e) => e.key === 'Enter' && handleSelect(language)} className={cn( 'flex cursor-pointer items-center gap-2 px-3 py-2 text-sm transition-colors', language.code === value diff --git a/src/components/NotificationCenter/NotificationCenter.tsx b/src/components/NotificationCenter/NotificationCenter.tsx index 59f68eb..7f9fcc5 100644 --- a/src/components/NotificationCenter/NotificationCenter.tsx +++ b/src/components/NotificationCenter/NotificationCenter.tsx @@ -204,6 +204,8 @@ export function NotificationCenter({ {visibleNotifications.map((notification) => (
{ + if (e.key === 'Enter') { + if (onNotificationClick) { + onNotificationClick(notification); + } + if (onMarkRead && !notification.isRead) { + onMarkRead(notification.id); + } + } + }} >
{/* Icon or Avatar */} diff --git a/src/components/ProviderOverview/ProviderOverview.tsx b/src/components/ProviderOverview/ProviderOverview.tsx index 03c6473..2797c8c 100644 --- a/src/components/ProviderOverview/ProviderOverview.tsx +++ b/src/components/ProviderOverview/ProviderOverview.tsx @@ -259,7 +259,10 @@ export function ProviderOverview({ {recentActivity.map((activity) => (
onActivityClick?.(activity)} + onKeyDown={(e) => e.key === 'Enter' && onActivityClick?.(activity)} className={` flex items-start gap-3 p-2 rounded-lg ${onActivityClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800' : ''} @@ -324,7 +327,10 @@ function StatCard({ label, value, icon, color, onClick }: StatCardProps) { return (
e.key === 'Enter' && onClick?.()} className={` p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 ${onClick ? 'cursor-pointer hover:border-gray-300 dark:hover:border-gray-600' : ''} diff --git a/src/components/ScheduleCalendar/ScheduleCalendar.tsx b/src/components/ScheduleCalendar/ScheduleCalendar.tsx index c57e357..2bec825 100644 --- a/src/components/ScheduleCalendar/ScheduleCalendar.tsx +++ b/src/components/ScheduleCalendar/ScheduleCalendar.tsx @@ -267,6 +267,8 @@ export function ScheduleCalendar({ {hours.map((hour) => (
{ if (onAddAppointment) { @@ -275,6 +277,13 @@ export function ScheduleCalendar({ onAddAppointment(clickDate, `${hour}:00`); } }} + onKeyDown={(e) => { + if (e.key === 'Enter' && onAddAppointment) { + const clickDate = new Date(date); + clickDate.setHours(hour, 0, 0, 0); + onAddAppointment(clickDate, `${hour}:00`); + } + }} /> ))} @@ -284,6 +293,8 @@ export function ScheduleCalendar({ return (
onAppointmentClick?.(appointment)} + onKeyDown={(e) => e.key === 'Enter' && onAppointmentClick?.(appointment)} >

{appointment.patientName || appointment.title} diff --git a/src/components/WebChartReportViewer/WebChartReportViewer.tsx b/src/components/WebChartReportViewer/WebChartReportViewer.tsx index 0df5e11..e74ee8f 100644 --- a/src/components/WebChartReportViewer/WebChartReportViewer.tsx +++ b/src/components/WebChartReportViewer/WebChartReportViewer.tsx @@ -202,8 +202,12 @@ export function WebChartReportViewer({

{/* Backdrop */}
e.key === 'Enter' && handleClose()} /> {/* Offcanvas Panel */} From f997e0ee77ec1c506b5065267824b769fb8ab8d0 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:28:00 -0500 Subject: [PATCH 07/17] fix(a11y): add eslint-disable for intentional autofocus usage - Added eslint-disable comments for autoFocus in modals and dropdowns where focusing the input improves UX - Added eslint-disable for dialog backdrop click handler - InviteUserModal, ProviderSearchFilters, ProviderSelector, AuthDialog Reduces total errors from 49 to 45 (-4 errors) --- src/components/AuthDialog/AuthDialog.tsx | 1 + src/components/InviteUserModal/InviteUserModal.tsx | 1 + src/components/ProviderSearchFilters/ProviderSearchFilters.tsx | 1 + src/components/ProviderSelector/ProviderSelector.tsx | 1 + 4 files changed, 4 insertions(+) diff --git a/src/components/AuthDialog/AuthDialog.tsx b/src/components/AuthDialog/AuthDialog.tsx index 7c438e7..ed1e97f 100644 --- a/src/components/AuthDialog/AuthDialog.tsx +++ b/src/components/AuthDialog/AuthDialog.tsx @@ -154,6 +154,7 @@ export function AuthDialog({ }; return ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
setEmail(e.target.value)} placeholder="user@example.com" required + // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus /> diff --git a/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx b/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx index 9dfa798..8812bd6 100644 --- a/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx +++ b/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx @@ -445,6 +445,7 @@ export function ServiceMultiSelect({ onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search services..." className={cn(inputVariants({ hasIcon: true }), 'pl-8')} + // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus /> diff --git a/src/components/ProviderSelector/ProviderSelector.tsx b/src/components/ProviderSelector/ProviderSelector.tsx index 2c34776..e4e3a78 100644 --- a/src/components/ProviderSelector/ProviderSelector.tsx +++ b/src/components/ProviderSelector/ProviderSelector.tsx @@ -251,6 +251,7 @@ export function ProviderSelector({ value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500" + // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus />
From 65036eaf15f024d6f8cc07f869efdf5cccc2c0a3 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:30:00 -0500 Subject: [PATCH 08/17] fix: resolve rules-of-hooks and exhaustive-deps violations - Fix conditional useId() calls in AddressForm.tsx and ProviderSearchFilters.tsx - Always call useId() unconditionally first, then use value conditionally - Add eslint-disable for intentional exhaustive-deps in Google Places effect --- src/components/Address/AddressForm.tsx | 4 +++- .../ProviderSearchFilters/ProviderSearchFilters.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Address/AddressForm.tsx b/src/components/Address/AddressForm.tsx index 86ac56c..4b97f4f 100644 --- a/src/components/Address/AddressForm.tsx +++ b/src/components/Address/AddressForm.tsx @@ -204,7 +204,8 @@ export function AddressForm({ className, googlePlaces, }: AddressFormProps) { - const idPrefix = id || React.useId(); + const generatedId = React.useId(); + const idPrefix = id || generatedId; const autocompleteRef = React.useRef( null ); @@ -308,6 +309,7 @@ export function AddressForm({ google.maps.event.clearInstanceListeners(autocompleteRef.current); } }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- Only re-init when enabled flag changes; other deps are stable refs }, [googlePlaces?.enabled]); return ( diff --git a/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx b/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx index 8812bd6..d8f9fde 100644 --- a/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx +++ b/src/components/ProviderSearchFilters/ProviderSearchFilters.tsx @@ -190,7 +190,8 @@ interface InputFieldProps extends React.InputHTMLAttributes { } function InputField({ label, icon, className, id, ...props }: InputFieldProps) { - const inputId = id || React.useId(); + const generatedId = React.useId(); + const inputId = id || generatedId; return (
@@ -227,7 +228,8 @@ function SelectField({ id, ...props }: SelectFieldProps) { - const selectId = id || React.useId(); + const generatedId = React.useId(); + const selectId = id || generatedId; return (
From df8035dc9240f990b3c5ba2b7b8b03b5e7448e33 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 29 Jan 2026 16:35:36 -0500 Subject: [PATCH 09/17] fix: resolve all jsx-a11y/label-has-associated-control errors --- .../CreateInvoiceModal/CreateInvoiceModal.tsx | 3 +- .../CreateReferralModal.tsx | 3 +- .../EditUserRoleModal/EditUserRoleModal.tsx | 4 +- .../EmployerServiceModal.tsx | 6 ++- .../HelpSupportPanel/HelpSupportPanel.tsx | 9 ++-- .../InviteUserModal/InviteUserModal.tsx | 3 +- .../OrderConfirmationWizard.tsx | 8 ++-- src/components/PhoneInput/PhoneInput.tsx | 3 +- .../ProviderSettings/ProviderSettings.tsx | 48 ++++++++++++------- .../RejectionModal/RejectionModal.tsx | 4 +- .../ServiceGeneralSettings.tsx | 9 ++-- .../ServicePricingManager.tsx | 6 ++- .../ServiceShippingSettings.tsx | 3 +- .../SetupServiceModal/SetupServiceModal.tsx | 6 ++- .../StripeBadge/StripeBadge.stories.tsx | 12 ++--- 15 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/components/CreateInvoiceModal/CreateInvoiceModal.tsx b/src/components/CreateInvoiceModal/CreateInvoiceModal.tsx index 2cd1d63..ab506ce 100644 --- a/src/components/CreateInvoiceModal/CreateInvoiceModal.tsx +++ b/src/components/CreateInvoiceModal/CreateInvoiceModal.tsx @@ -365,10 +365,11 @@ export function CreateInvoiceModal({ />
-