diff --git a/apps/app/src/app/[locale]/(no-header)/decisions/edit/[slug]/page.tsx b/apps/app/src/app/[locale]/(no-header)/decisions/edit/[slug]/page.tsx index 1c2377055..dc62ec6e9 100644 --- a/apps/app/src/app/[locale]/(no-header)/decisions/edit/[slug]/page.tsx +++ b/apps/app/src/app/[locale]/(no-header)/decisions/edit/[slug]/page.tsx @@ -1,44 +1,51 @@ import { createClient } from '@op/api/serverClient'; -import { SidebarLayout } from '@op/ui/Sidebar'; import { notFound } from 'next/navigation'; +import { ProcessBuilderContent } from '@/components/decisions/ProcessBuilder/ProcessBuilderContent'; import { ProcessBuilderHeader } from '@/components/decisions/ProcessBuilder/ProcessBuilderHeader'; -import { ProcessBuilderProvider } from '@/components/decisions/ProcessBuilder/ProcessBuilderProvider'; -import { ProcessBuilderSidebar } from '@/components/decisions/ProcessBuilder/ProcessBuilderSidebar'; +import { ProcessBuilderSidebar } from '@/components/decisions/ProcessBuilder/ProcessBuilderSectionNav'; +import { + DEFAULT_NAVIGATION_CONFIG, + type NavigationConfig, +} from '@/components/decisions/ProcessBuilder/navigationConfig'; const EditDecisionPage = async ({ params, }: { params: Promise<{ slug: string }>; }) => { - const { slug } = await params; const client = await createClient(); + const { slug } = await params; - const decisionProfile = await client.decision.getDecisionBySlug({ slug }); + // Get the decision profile to find the instance ID + const decisionProfile = await client.decision.getDecisionBySlug({ + slug, + }); - if (!decisionProfile || !decisionProfile.processInstance) { + if (!decisionProfile?.processInstance) { notFound(); } + // TODO: Get navigation config from process instance or process type + const navigationConfig: NavigationConfig = DEFAULT_NAVIGATION_CONFIG; + return ( - +
- - -
- {/* Main content area - will show section content based on query param */} -

Editing: {decisionProfile.name}

- {/* TODO: Add section-specific content components */} -
-
- +
+ +
+ +
+
+
); }; diff --git a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderContent.tsx b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderContent.tsx new file mode 100644 index 000000000..d91b8570d --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderContent.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { type SectionProps, getContentComponent } from './contentRegistry'; +import { type NavigationConfig } from './navigationConfig'; +import { useProcessNavigation } from './useProcessNavigation'; + +export function ProcessBuilderContent({ + decisionId, + decisionName, + navigationConfig, +}: SectionProps & { navigationConfig?: NavigationConfig }) { + const { currentStep, currentSection } = + useProcessNavigation(navigationConfig); + + const ContentComponent = getContentComponent( + currentStep?.id, + currentSection?.id, + ); + + if (!ContentComponent) { + return
Section not found
; + } + + return ( + + ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx index fd1c77caf..f36927878 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx @@ -1,57 +1,118 @@ 'use client'; import { Button } from '@op/ui/Button'; -import { useQueryState } from 'nuqs'; +import { Key } from '@op/ui/RAC'; +import { + Sidebar, + SidebarProvider, + SidebarTrigger, + useSidebar, +} from '@op/ui/Sidebar'; +import { Tab, TabList, Tabs } from '@op/ui/Tabs'; import { LuChevronRight, LuCircleAlert, LuHouse, LuPlus } from 'react-icons/lu'; -import { Link } from '@/lib/i18n'; +import { Link, useTranslations } from '@/lib/i18n'; import { UserAvatarMenu } from '@/components/SiteHeader'; +import { type NavigationConfig } from './navigationConfig'; +import { useProcessNavigation } from './useProcessNavigation'; + export const ProcessBuilderHeader = ({ - steps, + processName, + navigationConfig, }: { - steps?: { id: string; label: string }[]; + processName?: string; + navigationConfig?: NavigationConfig; }) => { - const [currentStep, setCurrentStep] = useQueryState('step'); return ( -
+ + + + + + ); +}; + +const ProcessBuilderHeaderContent = ({ + processName, + navigationConfig, +}: { + processName?: string; + navigationConfig?: NavigationConfig; +}) => { + const t = useTranslations(); + const { visibleSteps, currentStep, setStep } = + useProcessNavigation(navigationConfig); + const hasSteps = visibleSteps.length > 0; + + const { setOpen } = useSidebar(); + + const handleSelectionChange = (key: Key) => { + setStep(String(key)); + setOpen(false); + }; + + return ( +
- + {hasSteps && } + + - Home + {t('Home')} - - New process + + + {processName || t('New process')}
- + {visibleSteps.map((step) => ( + + {t(step.labelKey)} + + ))} + + + + )}
- {steps && steps.length > 0 && ( + {hasSteps && (
)} @@ -60,3 +121,58 @@ export const ProcessBuilderHeader = ({
); }; + +const MobileSidebar = ({ + navigationConfig, +}: { + navigationConfig?: NavigationConfig; +}) => { + const t = useTranslations(); + const { visibleSteps, currentStep, setStep } = + useProcessNavigation(navigationConfig); + const hasSteps = visibleSteps.length > 0; + const { setOpen } = useSidebar(); + + const handleSelectionChange = (key: Key) => { + setStep(String(key)); + setOpen(false); + }; + + if (!hasSteps) { + return null; + } + return ( + + + + ); +}; diff --git a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderProvider.tsx b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderProvider.tsx deleted file mode 100644 index d930cab69..000000000 --- a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderProvider.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; - -import { SidebarProvider } from '@op/ui/Sidebar'; -import { useQueryState } from 'nuqs'; - -export const ProcessBuilderProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const [selectedProcess] = useQueryState('process'); - return ( - - {children} - - ); -}; diff --git a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderSectionNav.tsx b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderSectionNav.tsx new file mode 100644 index 000000000..0c79b893e --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderSectionNav.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { Key } from '@op/ui/RAC'; +import { Tab, TabList, Tabs } from '@op/ui/Tabs'; + +import { useTranslations } from '@/lib/i18n'; + +import { type NavigationConfig } from './navigationConfig'; +import { useProcessNavigation } from './useProcessNavigation'; + +export const ProcessBuilderSidebar = ({ + navigationConfig, +}: { + navigationConfig?: NavigationConfig; +}) => { + const t = useTranslations(); + const { visibleSections, currentSection, currentStep, setSection } = + useProcessNavigation(navigationConfig); + + const handleSelectionChange = (key: Key) => { + setSection(String(key)); + }; + + // Don't render sidebar for single-section steps + // These steps manage their own layout (e.g., template step with form builder) + if (visibleSections.length <= 1) { + return null; + } + + return ( + + ); +}; diff --git a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderSidebar.tsx b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderSidebar.tsx deleted file mode 100644 index b3060d84f..000000000 --- a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderSidebar.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { MenuItem } from '@op/ui/Menu'; -import { Sidebar } from '@op/ui/Sidebar'; -import { cn } from '@op/ui/utils'; -import { useQueryState } from 'nuqs'; -import { Menu } from 'react-aria-components'; - -import { useTranslations } from '@/lib/i18n'; - -export const ProcessBuilderSidebar = () => { - const [section, setSection] = useQueryState('section'); - const t = useTranslations(); - - const navSections = [ - { slug: 'overview', label: t('Overview') }, - { slug: 'phases', label: t('Phases') }, - { slug: 'categories', label: t('Proposal Categories') }, - { slug: 'voting', label: t('Voting') }, - ]; - - return ( - - - {navSections.map(({ slug, label }) => ( - setSection(slug)} - label={label} - /> - ))} - - - ); -}; - -const SidebarLink = ({ - label, - isCurrent, - onPress, -}: { - label: string; - isCurrent: boolean; - onPress: () => void; -}) => { - return ( - - {label} - - ); -}; diff --git a/apps/app/src/components/decisions/ProcessBuilder/contentRegistry.tsx b/apps/app/src/components/decisions/ProcessBuilder/contentRegistry.tsx new file mode 100644 index 000000000..1fceb64ec --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/contentRegistry.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { type ComponentType } from 'react'; + +import type { StepId } from './navigationConfig'; +import MembersSection from './stepContent/members/MembersSection'; +import RolesSection from './stepContent/members/RolesSection'; +import OverviewSection from './stepContent/overview/OverviewSection'; +import PhasesSection from './stepContent/overview/PhasesSection'; +import ProposalCategoriesSection from './stepContent/overview/ProposalCategoriesSection'; +import VotingSection from './stepContent/overview/VotingSection'; +import CriteriaSection from './stepContent/rubric/CriteriaSection'; +import SettingsSection from './stepContent/rubric/SettingsSection'; +import FormBuilderSection from './stepContent/template/FormBuilderSection'; + +// Props that all section components receive +export interface SectionProps { + decisionId: string; + decisionName: string; +} + +type SectionComponent = ComponentType; + +// Registry structure - allows partial coverage +type ContentRegistry = { + [S in StepId]?: Partial>; +}; + +const CONTENT_REGISTRY: ContentRegistry = { + general: { + overview: OverviewSection, + phases: PhasesSection, + proposalCategories: ProposalCategoriesSection, + voting: VotingSection, + }, + template: { + formBuilder: FormBuilderSection, + }, + rubric: { + criteria: CriteriaSection, + settings: SettingsSection, + }, + members: { + roles: RolesSection, + members: MembersSection, + }, +}; + +export function getContentComponent( + stepId: StepId | undefined, + sectionId: string | undefined, +): SectionComponent | null { + if (!stepId || !sectionId) { + return null; + } + return CONTENT_REGISTRY[stepId]?.[sectionId] ?? null; +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/navigationConfig.ts b/apps/app/src/components/decisions/ProcessBuilder/navigationConfig.ts new file mode 100644 index 000000000..ac7257f03 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/navigationConfig.ts @@ -0,0 +1,58 @@ +// Process Builder Navigation Configuration +// Steps and sections are defined here with `as const` - types are derived from this config + +export const STEPS = [ + { id: 'general', labelKey: 'General' }, + { id: 'template', labelKey: 'Proposal Template' }, + { id: 'rubric', labelKey: 'Review Rubric' }, + { id: 'members', labelKey: 'Members' }, +] as const; + +// Derive StepId first so we can use it in SECTIONS_BY_STEP +export type StepId = (typeof STEPS)[number]['id']; + +export const SECTIONS_BY_STEP = { + general: [ + { id: 'overview', labelKey: 'Overview' }, + { id: 'phases', labelKey: 'Phases' }, + { id: 'proposalCategories', labelKey: 'Proposal Categories' }, + { id: 'voting', labelKey: 'Voting' }, + ], + template: [{ id: 'formBuilder', labelKey: 'Form Builder' }], + rubric: [ + { id: 'criteria', labelKey: 'Criteria' }, + { id: 'settings', labelKey: 'Settings' }, + ], + members: [ + { id: 'roles', labelKey: 'Roles & permissions' }, + { id: 'members', labelKey: 'Members' }, + ], +} as const satisfies Record< + StepId, + readonly { id: string; labelKey: string }[] +>; + +// Derive SectionId from all sections across all steps +export type SectionId = (typeof SECTIONS_BY_STEP)[StepId][number]['id']; + +// Navigation configuration (from API) +export interface NavigationConfig { + steps?: Partial>; + sections?: Partial>; +} + +// Default navigation config (all steps and sections visible) +export const DEFAULT_NAVIGATION_CONFIG: NavigationConfig = { + steps: { + general: true, + template: true, + rubric: true, + members: true, + }, + sections: { + general: ['overview', 'phases', 'proposalCategories', 'voting'], + template: ['formBuilder'], + rubric: ['criteria', 'settings'], + members: ['roles', 'members'], + }, +}; diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/members/MembersSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/members/MembersSection.tsx new file mode 100644 index 000000000..6454513ab --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/members/MembersSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function MembersSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Members

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement members configuration */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/members/RolesSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/members/RolesSection.tsx new file mode 100644 index 000000000..277df25fb --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/members/RolesSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function RolesSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Roles & Permissions

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement roles and permissions configuration */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/OverviewSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/OverviewSection.tsx new file mode 100644 index 000000000..aaa722bd8 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/OverviewSection.tsx @@ -0,0 +1,87 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function OverviewSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Overview

+

Decision: {decisionName}

+

ID: {decisionId}

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+

+ Cupidatat pariatur irure et labore quis aliquip ullamco. Laboris + cupidatat amet duis deserunt reprehenderit. Voluptate duis qui Lorem + irure aliqua velit sint. Officia reprehenderit labore minim eiusmod + dolore deserunt labore. Enim sit officia tempor consequat sunt duis. Ea + eiusmod do qui nostrud cupidatat fugiat adipisicing cupidatat nisi + eiusmod mollit nulla sint. Duis velit officia aliquip nulla + reprehenderit elit. +

+ {/* TODO: Implement overview section */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/PhasesSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/PhasesSection.tsx new file mode 100644 index 000000000..e3a7f6d71 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/PhasesSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function PhasesSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Phases

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement phases configuration */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/ProposalCategoriesSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/ProposalCategoriesSection.tsx new file mode 100644 index 000000000..ad1b0748c --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/ProposalCategoriesSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function ProposalCategoriesSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Proposal Categories

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement proposal categories configuration */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/VotingSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/VotingSection.tsx new file mode 100644 index 000000000..045f05232 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/overview/VotingSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function VotingSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Voting

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement voting configuration */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/rubric/CriteriaSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/rubric/CriteriaSection.tsx new file mode 100644 index 000000000..67905eef1 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/rubric/CriteriaSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function CriteriaSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Criteria

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement rubric criteria configuration */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/rubric/SettingsSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/rubric/SettingsSection.tsx new file mode 100644 index 000000000..fa85daf84 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/rubric/SettingsSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function SettingsSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Settings

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement rubric settings configuration */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/template/FormBuilderSection.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/template/FormBuilderSection.tsx new file mode 100644 index 000000000..5a7c8e668 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/template/FormBuilderSection.tsx @@ -0,0 +1,15 @@ +import type { SectionProps } from '../../contentRegistry'; + +export default function FormBuilderSection({ + decisionId, + decisionName, +}: SectionProps) { + return ( +
+

Form Builder

+

Decision: {decisionName}

+

ID: {decisionId}

+ {/* TODO: Implement form builder with custom sidebar */} +
+ ); +} diff --git a/apps/app/src/components/decisions/ProcessBuilder/useProcessNavigation.ts b/apps/app/src/components/decisions/ProcessBuilder/useProcessNavigation.ts new file mode 100644 index 000000000..76c635432 --- /dev/null +++ b/apps/app/src/components/decisions/ProcessBuilder/useProcessNavigation.ts @@ -0,0 +1,131 @@ +'use client'; + +import { useQueryState } from 'nuqs'; +import { useCallback, useEffect, useMemo } from 'react'; + +import { + DEFAULT_NAVIGATION_CONFIG, + type NavigationConfig, + SECTIONS_BY_STEP, + STEPS, + type StepId, +} from './navigationConfig'; + +export function useProcessNavigation( + navigationConfig: NavigationConfig = DEFAULT_NAVIGATION_CONFIG, +) { + const [stepParam, setStepParam] = useQueryState('step', { history: 'push' }); + const [sectionParam, setSectionParam] = useQueryState('section', { + history: 'push', + }); + + // Filter to visible steps only + const visibleSteps = useMemo( + () => + STEPS.filter((s) => { + const visibility = navigationConfig.steps?.[s.id]; + return visibility === true; + }), + [navigationConfig.steps], + ); + + // Current step (fallback to first visible step) + const currentStep = useMemo(() => { + const found = visibleSteps.find((s) => s.id === stepParam); + return found ?? visibleSteps[0]; + }, [stepParam, visibleSteps]); + + // Get visible sections for current step + const visibleSections = useMemo(() => { + if (!currentStep) { + return []; + } + + const allSections = SECTIONS_BY_STEP[currentStep.id]; + const allowedSectionIds = navigationConfig.sections?.[currentStep.id]; + + // If no section config, show no sections + if (!allowedSectionIds) { + return []; + } + + // Filter to only allowed sections + return allSections.filter((s) => + allowedSectionIds.some((id) => id === s.id), + ); + }, [currentStep, navigationConfig.sections]); + + // Current section (fallback to first visible section) + const currentSection = useMemo(() => { + const found = visibleSections.find((s) => s.id === sectionParam); + return found ?? visibleSections[0]; + }, [sectionParam, visibleSections]); + + // Replace invalid params in URL + useEffect(() => { + if (stepParam && !visibleSteps.some((s) => s.id === stepParam)) { + setStepParam(null); + } + if (sectionParam && !visibleSections.some((s) => s.id === sectionParam)) { + setSectionParam(currentSection?.id ?? null); + } + }, [ + stepParam, + sectionParam, + visibleSteps, + visibleSections, + currentSection, + setStepParam, + setSectionParam, + ]); + + // Hide section param for single-section steps + useEffect(() => { + if (sectionParam && visibleSections.length <= 1) { + setSectionParam(null); + } + }, [sectionParam, visibleSections.length, setSectionParam]); + + // Handle step change - resets section to first of new step + const setStep = useCallback( + (newStepId: StepId | string) => { + const newStep = visibleSteps.find((s) => s.id === newStepId); + if (!newStep) { + return; + } + + // Get first section of the new step + const newStepSections = SECTIONS_BY_STEP[newStep.id]; + const allowedSectionIds = navigationConfig.sections?.[newStep.id]; + const firstVisibleSection = allowedSectionIds + ? newStepSections.find((s) => + allowedSectionIds.some((id) => id === s.id), + ) + : newStepSections[0]; + + setStepParam(newStepId); + // Only set section param if step has multiple sections + setSectionParam( + newStepSections.length > 1 ? (firstVisibleSection?.id ?? null) : null, + ); + }, + [visibleSteps, navigationConfig.sections, setStepParam, setSectionParam], + ); + + // Handle section change + const setSection = useCallback( + (newSectionId: string) => { + setSectionParam(newSectionId); + }, + [setSectionParam], + ); + + return { + currentStep, + currentSection, + visibleSteps, + visibleSections, + setStep, + setSection, + }; +} diff --git a/apps/app/src/lib/i18n/dictionaries/bn.json b/apps/app/src/lib/i18n/dictionaries/bn.json index 724983faf..85f469af7 100644 --- a/apps/app/src/lib/i18n/dictionaries/bn.json +++ b/apps/app/src/lib/i18n/dictionaries/bn.json @@ -436,5 +436,21 @@ "User avatar": "ব্যবহারকারীর অবতার", "Done": "সম্পন্ন", "How do you want to structure your decision-making process?": "আপনি কীভাবে আপনার সিদ্ধান্ত গ্রহণ প্রক্রিয়া গঠন করতে চান?", - "No templates found": "কোনো টেমপ্লেট পাওয়া যায়নি" + "No templates found": "কোনো টেমপ্লেট পাওয়া যায়নি", + "Overview": "সারসংক্ষেপ", + "Proposal Template": "প্রস্তাব টেমপ্লেট", + "Review Rubric": "পর্যালোচনা রুব্রিক", + "Phases": "পর্যায়সমূহ", + "Proposal Categories": "প্রস্তাব বিভাগসমূহ", + "Voting": "ভোটদান", + "Form Builder": "ফর্ম নির্মাতা", + "Criteria": "মানদণ্ড", + "Settings": "সেটিংস", + "Roles & permissions": "ভূমিকা ও অনুমতিসমূহ", + "{stepCount, plural, =1 {1 step} other {# steps}} remaining": "{stepCount}টি ধাপ বাকি", + "Process steps": "প্রক্রিয়ার ধাপসমূহ", + "New process": "নতুন প্রক্রিয়া", + "Launch": "চালু করুন", + "Process": "প্রক্রিয়া", + "Section navigation": "বিভাগ নেভিগেশন" } diff --git a/apps/app/src/lib/i18n/dictionaries/en.json b/apps/app/src/lib/i18n/dictionaries/en.json index 19a13bfa7..0d584569d 100644 --- a/apps/app/src/lib/i18n/dictionaries/en.json +++ b/apps/app/src/lib/i18n/dictionaries/en.json @@ -436,5 +436,21 @@ "User avatar": "User avatar", "Done": "Done", "How do you want to structure your decision-making process?": "How do you want to structure your decision-making process?", - "No templates found": "No templates found" + "No templates found": "No templates found", + "Overview": "Overview", + "Proposal Template": "Proposal Template", + "Review Rubric": "Review Rubric", + "Phases": "Phases", + "Proposal Categories": "Proposal Categories", + "Voting": "Voting", + "Form Builder": "Form Builder", + "Criteria": "Criteria", + "Settings": "Settings", + "Roles & permissions": "Roles & permissions", + "{stepCount, plural, =1 {1 step} other {# steps}} remaining": "{stepCount, plural, =1 {1 step} other {# steps}} remaining", + "Process steps": "Process steps", + "New process": "New process", + "Launch": "Launch", + "Process": "Process", + "Section navigation": "Section navigation" } diff --git a/apps/app/src/lib/i18n/dictionaries/es.json b/apps/app/src/lib/i18n/dictionaries/es.json index a7ad5cc75..1ce8d329c 100644 --- a/apps/app/src/lib/i18n/dictionaries/es.json +++ b/apps/app/src/lib/i18n/dictionaries/es.json @@ -435,5 +435,21 @@ "User avatar": "Avatar del usuario", "Done": "Listo", "How do you want to structure your decision-making process?": "¿Cómo quieres estructurar tu proceso de toma de decisiones?", - "No templates found": "No se encontraron plantillas" + "No templates found": "No se encontraron plantillas", + "Overview": "Resumen", + "Proposal Template": "Plantilla de propuesta", + "Review Rubric": "Rúbrica de revisión", + "Phases": "Fases", + "Proposal Categories": "Categorías de propuestas", + "Voting": "Votación", + "Form Builder": "Constructor de formularios", + "Criteria": "Criterios", + "Settings": "Configuración", + "Roles & permissions": "Roles y permisos", + "{stepCount, plural, =1 {1 step} other {# steps}} remaining": "{stepCount, plural, =1 {1 paso restante} other {# pasos restantes}}", + "Process steps": "Pasos del proceso", + "New process": "Nuevo proceso", + "Launch": "Lanzar", + "Process": "Proceso", + "Section navigation": "Navegación de secciones" } diff --git a/apps/app/src/lib/i18n/dictionaries/fr.json b/apps/app/src/lib/i18n/dictionaries/fr.json index 4e064c56c..ce9729de1 100644 --- a/apps/app/src/lib/i18n/dictionaries/fr.json +++ b/apps/app/src/lib/i18n/dictionaries/fr.json @@ -436,5 +436,21 @@ "User avatar": "Avatar de l'utilisateur", "Done": "Terminé", "How do you want to structure your decision-making process?": "Comment souhaitez-vous structurer votre processus de prise de décision ?", - "No templates found": "Aucun modèle trouvé" + "No templates found": "Aucun modèle trouvé", + "Overview": "Aperçu", + "Proposal Template": "Modèle de proposition", + "Review Rubric": "Grille d'évaluation", + "Phases": "Phases", + "Proposal Categories": "Catégories de propositions", + "Voting": "Vote", + "Form Builder": "Constructeur de formulaire", + "Criteria": "Critères", + "Settings": "Paramètres", + "Roles & permissions": "Rôles et permissions", + "{stepCount, plural, =1 {1 step} other {# steps}} remaining": "{stepCount, plural, =1 {1 étape restante} other {# étapes restantes}}", + "Process steps": "Étapes du processus", + "New process": "Nouveau processus", + "Launch": "Lancer", + "Process": "Processus", + "Section navigation": "Navigation des sections" } diff --git a/apps/app/src/lib/i18n/dictionaries/pt.json b/apps/app/src/lib/i18n/dictionaries/pt.json index fd1cd55a4..fb754f35f 100644 --- a/apps/app/src/lib/i18n/dictionaries/pt.json +++ b/apps/app/src/lib/i18n/dictionaries/pt.json @@ -436,5 +436,21 @@ "User avatar": "Avatar do usuário", "Done": "Concluído", "How do you want to structure your decision-making process?": "Como você deseja estruturar seu processo de tomada de decisão?", - "No templates found": "Nenhum modelo encontrado" + "No templates found": "Nenhum modelo encontrado", + "Overview": "Visão geral", + "Proposal Template": "Modelo de proposta", + "Review Rubric": "Rubrica de avaliação", + "Phases": "Fases", + "Proposal Categories": "Categorias de propostas", + "Voting": "Votação", + "Form Builder": "Construtor de formulário", + "Criteria": "Critérios", + "Settings": "Configurações", + "Roles & permissions": "Funções e permissões", + "{stepCount, plural, =1 {1 step} other {# steps}} remaining": "{stepCount, plural, =1 {1 passo restante} other {# passos restantes}}", + "Process steps": "Passos do processo", + "New process": "Novo processo", + "Launch": "Lançar", + "Process": "Processo", + "Section navigation": "Navegação de seções" } diff --git a/packages/ui/src/components/Sidebar/Sidebar.tsx b/packages/ui/src/components/Sidebar/Sidebar.tsx index 0756a54d3..07f23ce7d 100644 --- a/packages/ui/src/components/Sidebar/Sidebar.tsx +++ b/packages/ui/src/components/Sidebar/Sidebar.tsx @@ -35,10 +35,7 @@ const SidebarProvider = ({ defaultOpen = false, isOpen: openProp, onOpenChange: setOpenProp, - className, - style, children, - ...props }: React.ComponentProps<'div'> & { defaultOpen?: boolean; isOpen?: boolean; @@ -85,13 +82,7 @@ const SidebarProvider = ({ return ( -
- {children} -
+ {children}
); }; @@ -101,9 +92,11 @@ const Sidebar = ({ side = 'left', className, label, + mobileOnly = false, }: React.ComponentProps<'div'> & { side?: 'left' | 'right'; label?: string; + mobileOnly?: boolean; }) => { const { isMobile, state, open, setOpen } = useSidebar(); @@ -121,7 +114,7 @@ const Sidebar = ({
( - tabListStyles({ ...renderProps, variant: props.variant, className }), + tabListStyles({ + ...renderProps, + variant: props.variant, + orientation: props.orientation, + className, + }), )} /> ); @@ -72,11 +77,11 @@ const tabProps = tv({ base: 'flex h-8 cursor-default items-center px-2 py-3 text-base font-normal text-nowrap text-neutral-gray4 outline-hidden transition forced-color-adjust-none focus-visible:bg-neutral-offWhite sm:h-auto sm:bg-transparent', variants: { variant: { - default: '', - pill: 'border-b-none rounded-sm bg-neutral-offWhite p-3 sm:py-2', + default: 'border-b-none border-transparent', + pill: 'rounded-sm border-none border-transparent bg-neutral-offWhite p-3 focus-visible:outline-2 focus-visible:outline-solid sm:py-2', }, isSelected: { - false: '', + false: 'border-b border-transparent', true: 'border-b border-charcoal text-charcoal', }, isDisabled: { @@ -90,6 +95,11 @@ const tabProps = tv({ class: 'border-none bg-neutral-gray1 text-neutral-charcoal sm:bg-neutral-gray1', }, + { + variant: 'pill', + isSelected: false, + class: 'border-none', + }, ], defaultVariants: { variant: 'default',