diff --git a/apps/apollo-vertex/.oxlintrc.json b/apps/apollo-vertex/.oxlintrc.json index a2a35b09f..8c0bbc328 100644 --- a/apps/apollo-vertex/.oxlintrc.json +++ b/apps/apollo-vertex/.oxlintrc.json @@ -114,6 +114,12 @@ "rules": { "react/only-export-components": "off" } + }, + { + "files": ["templates/**/*.tsx"], + "rules": { + "eslint/max-lines": ["error", { "max": 500 }] + } } ] } diff --git a/apps/apollo-vertex/app/templates/_meta.ts b/apps/apollo-vertex/app/templates/_meta.ts index 84fe95d1b..d41075dcb 100644 --- a/apps/apollo-vertex/app/templates/_meta.ts +++ b/apps/apollo-vertex/app/templates/_meta.ts @@ -1,3 +1,4 @@ export default { "list-page": "List page", + settings: "Settings", }; diff --git a/apps/apollo-vertex/app/templates/settings/page.mdx b/apps/apollo-vertex/app/templates/settings/page.mdx new file mode 100644 index 000000000..241a1ac87 --- /dev/null +++ b/apps/apollo-vertex/app/templates/settings/page.mdx @@ -0,0 +1,115 @@ +import { SettingsTemplate } from '@/templates/settings/SettingsTemplate'; +import { PreviewFullScreen } from '@/app/_components/preview-full-screen'; + +# Settings + +A traditional settings page composition: a single, vertically-stacked form +constrained to a comfortable reading width, broken into titled sections. +Built from `PageHeader`, `Field` (and friends), `Input`, `Textarea`, +`Select`, `RadioGroup`, `Switch`, and `Button`. Intended to render inside +an [`ApolloShell`](/patterns/shell). + + + + + +## Composition + +From top to bottom the template stacks: + +- **[`Shell`](/patterns/shell)** (outer) — provides the chrome (sidebar, + header, auth, theme). Wrap the rest of the page inside `ApolloShell`. +- **`PageHeader`** — page title. +- **Sections** — four titled `
`s (Profile, Notifications, Regional, + Privacy & security) wrapped by a small in-file `Section` helper. Each + section starts with an `h2` + description header, then a `FieldGroup` of + `Field`s. Sections use `mb-8` for vertical rhythm. +- **Field stack** — within each `Field`, content stacks `FieldLabel` → + `FieldDescription` → control (Input, Textarea, Select, RadioGroup). Toggle + rows use `orientation="horizontal"` so the label sits left and the + `Switch` sits right. +- **Action footer** — right-aligned `Reset to defaults` (outline) and + `Save changes` (primary) `Button`s. `Save` is disabled until the draft + diverges from the saved state. + +## Grid + +The page uses the [foundation grid](/foundation/grid): + +- Outer container has `px-4 sm:px-6 lg:px-8` margins matching the default + `PageHeader`, and a `grid grid-cols-4 sm:grid-cols-8 lg:grid-cols-12 + gap-4` row. +- The form column spans `col-span-4 sm:col-span-8 lg:col-span-7` — full + width on mobile/tablet, ~58% width on desktop. This keeps line lengths + readable (~600–760px depending on viewport) instead of letting the form + stretch edge-to-edge on wide monitors. +- Within Regional, Language and Timezone share a nested + `grid grid-cols-1 sm:grid-cols-2 gap-4` so they pair on one row from + tablet up. + +| Breakpoint | Form column | +|---|---| +| Mobile (`grid-cols-4`) | `col-span-4` (full width) | +| Tablet (`sm:grid-cols-8`) | `sm:col-span-8` (full width) | +| Desktop (`lg:grid-cols-12`) | `lg:col-span-7` (~58%) | + +Sections inside the form column are separated vertically with `mb-8` (32px) +on each section. + +Source: [`templates/settings/WorkspaceSettings.tsx`](https://github.com/UiPath/apollo-ui/blob/main/apps/apollo-vertex/templates/settings/WorkspaceSettings.tsx) + +## Installation + +The composition itself is a pattern — copy the source from +`templates/settings/WorkspaceSettings.tsx` into your app, wrap it in +`ApolloShell`, and swap in your own state/persistence. The underlying +components install from the registry: + +```bash +npx shadcn@latest add @uipath/shell +npx shadcn@latest add @uipath/page-header +npx shadcn@latest add @uipath/field +npx shadcn@latest add @uipath/input +npx shadcn@latest add @uipath/textarea +npx shadcn@latest add @uipath/select +npx shadcn@latest add @uipath/radio-group +npx shadcn@latest add @uipath/switch +npx shadcn@latest add @uipath/button +``` + +## Customizing + +- **Form width** — `lg:col-span-7` matches the comfortable form line length + used across the Vertex apps. Bump to `lg:col-span-8` for slightly wider + controls or `lg:col-span-6` for tighter forms. Stay within the 12-column + grid so margins line up with the `PageHeader`. +- **Persistence** — the template uses two `useState` slots (`draft` and + `saved`) and compares them field-by-field to power the `Save` button. + Replace `handleSave` and `handleReset` with calls to your own store, + server action, or settings API. Toast feedback can be added with + `Sonner` from the registry. +- **Categorical fields** — `emailFrequency` and `visibility` are typed as + unions derived from their option arrays via `as const` plus a small type + guard at the `RadioGroup`'s `onValueChange`. Follow the same pattern when + adding more categorical settings so values stay type-safe end-to-end. +- **Date format** — the `dateFormat` option values (`short`, `medium`, + `long`) map directly to `Intl.DateTimeFormat({ dateStyle })` so they + render without a date library. Swap in `date-fns` format strings or your + own enum if you need finer control. +- **Sub-page navigation** — settings UIs often have multiple sub-sections + routed independently (e.g. `/settings/profile`, + `/settings/notifications`). Move each `
` to its own route and + add a left-rail nav with vertical `Tabs` or a sidebar. +- **Field orientation** — `Field` accepts `orientation="horizontal"` + (label left, control right) or `"vertical"` (label above control). The + template uses horizontal for `Switch` rows and vertical for everything + else. +- **Pairing fields** — wrap a couple of `Field`s in a nested + `grid grid-cols-1 sm:grid-cols-2 gap-4` to lay them side-by-side from + tablet up (as Language + Timezone do here). +- **Validation** — pair each `Field` with `FieldError` and pass an + `errors` array to surface validation messages, or wrap the page with + `react-hook-form` + `zod` for full form management. +- **i18n** — wrap your app in `ApolloShell` (which initializes i18n via + `LocaleProvider`) or provide your own `I18nextProvider` so any + registry-component strings render correctly. diff --git a/apps/apollo-vertex/templates/settings/SettingsTemplate.tsx b/apps/apollo-vertex/templates/settings/SettingsTemplate.tsx new file mode 100644 index 000000000..ee4c39276 --- /dev/null +++ b/apps/apollo-vertex/templates/settings/SettingsTemplate.tsx @@ -0,0 +1,17 @@ +"use client"; + +import dynamic from "next/dynamic"; +import { WorkspaceSettings } from "./WorkspaceSettings"; + +function SettingsTemplateContent() { + return ( +
+ +
+ ); +} + +export const SettingsTemplate = dynamic( + () => Promise.resolve(SettingsTemplateContent), + { ssr: false }, +); diff --git a/apps/apollo-vertex/templates/settings/WorkspaceSettings.tsx b/apps/apollo-vertex/templates/settings/WorkspaceSettings.tsx new file mode 100644 index 000000000..06037bfe6 --- /dev/null +++ b/apps/apollo-vertex/templates/settings/WorkspaceSettings.tsx @@ -0,0 +1,464 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import type { ReactNode } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { FieldGroup } from "@/components/ui/field"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + PageHeader, + PageHeaderNav, + PageHeaderTitle, +} from "@/components/ui/page-header"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; + +const EMAIL_FREQUENCIES = [ + { value: "realtime", label: "Realtime — every event" }, + { value: "daily", label: "Daily digest — once each morning" }, + { value: "weekly", label: "Weekly summary — Mondays" }, +] as const; + +const VISIBILITIES = [ + { value: "public", label: "Public — anyone with the link" }, + { value: "internal", label: "Internal — your organization" }, + { value: "private", label: "Private — invitation only" }, +] as const; + +const INDUSTRIES = [ + { value: "healthcare", label: "Healthcare" }, + { value: "financial-services", label: "Financial services" }, + { value: "manufacturing", label: "Manufacturing" }, + { value: "public-sector", label: "Public sector" }, +]; + +const LANGUAGES = [ + { value: "en-US", label: "English (United States)" }, + { value: "de-DE", label: "Deutsch" }, + { value: "fr-FR", label: "Français" }, + { value: "ja-JP", label: "日本語" }, +]; + +const TIMEZONES = [ + { value: "America/Los_Angeles", label: "(GMT−08:00) Pacific Time" }, + { value: "America/New_York", label: "(GMT−05:00) Eastern Time" }, + { value: "Europe/London", label: "(GMT+00:00) London" }, + { value: "Asia/Tokyo", label: "(GMT+09:00) Tokyo" }, +]; + +const DATE_FORMATS = [ + { value: "short", label: "4/24/26" }, + { value: "medium", label: "Apr 24, 2026" }, + { value: "long", label: "April 24, 2026" }, +]; + +const settingsSchema = z.object({ + workspaceName: z.string().min(1, "Workspace name is required"), + description: z.string(), + industry: z.string(), + emailFrequency: z.enum(["realtime", "daily", "weekly"]), + emailDigest: z.boolean(), + browserPush: z.boolean(), + language: z.string(), + timezone: z.string(), + dateFormat: z.string(), + visibility: z.enum(["public", "internal", "private"]), + require2fa: z.boolean(), +}); + +type SettingsValues = z.infer; + +const DEFAULT_VALUES: SettingsValues = { + workspaceName: "Acme Health", + description: "Clinical operations workspace for the Acme Health network.", + industry: "healthcare", + emailFrequency: "daily", + emailDigest: true, + browserPush: false, + language: "en-US", + timezone: "America/New_York", + dateFormat: "medium", + visibility: "internal", + require2fa: true, +}; + +interface SectionProps { + title: string; + description: string; + children: ReactNode; +} + +function Section({ title, description, children }: SectionProps) { + return ( +
+
+

{title}

+

{description}

+
+ {children} +
+ ); +} + +export function WorkspaceSettings() { + const form = useForm({ + resolver: zodResolver(settingsSchema), + defaultValues: DEFAULT_VALUES, + }); + + return ( +
+ + + Workspace settings + + + +
+ + void form.handleSubmit((data) => form.reset(data))(e) + } + className="px-4 sm:px-6 lg:px-8 pb-8 grid grid-cols-4 sm:grid-cols-8 lg:grid-cols-12 gap-4" + > +
+
+ + ( + + Workspace name + + + + + + )} + /> + ( + + Description + + Shown to invited members on the workspace landing page. + + +