Clarity Platform Architecture / Frontend Architecture

Frontend Architecture

The Phoenix frontend is built on Remix v2 with React, using standard React components, TypeScript, and Tailwind CSS. Server-side rendering provides performance and SEO benefits out of the box.

web

Frontend Overview

code

Remix v2 + React

Standard React components with Remix conventions for routing, loaders, and server rendering.

javascript

TypeScript

Full type safety across the frontend codebase with auto-generated API types from the backend.

palette

Tailwind CSS

Utility-first CSS with theme editor integration for dynamic, customizable styling.

route

Routing System

Phoenix uses a filename-based routing system. Any file that ends with .route.tsx will be accessible as a page, with the URL path derived from the file name.

Path Resolution Rules

  1. Remove the .route.tsx suffix
  2. Replace all . characters with /
  3. Remove any segment of the file name that begins with an _

Route Naming Examples

admin.settings.route.tsx
arrow_forward
/admin/settings
_site.dashboard.orders.route.tsx
arrow_forward
/dashboard/orders
_site.catalog.$slug.route.tsx
arrow_forward
/catalog/:slug

The _site segment groups routes that share a common layout or upstream data, but is removed from the resulting URL path.

dashboard_customize

Layouts & Outlets

Route files can function as layouts for nested routes by including an <Outlet /> component. Nested pages render inside their parent layout at the Outlet position.

info Route prefix conventions
  • _site — storefront / user-facing pages
  • admin — admin pages (no underscore prefix)
code Layout + Nested Page
// _sample.route.tsx (layout)
export default function MyLayout() {
  return (
    <div>
      <h3>This is my layout</h3>
      <Outlet />
    </div>
  );
}

// _sample.nested.route.tsx (nested page)
export default function Page() {
  return <p>Hello!</p>;
}

Accessing /nested renders the _sample layout first, then renders the nested page contents in place of the <Outlet />.

download

Loaders

Loaders are server-side data loading functions executed before a page renders. They return data used to server-render the page and deliver a pre-populated HTML response to the end user.

code Basic Loader
export async function loader(args: LoaderFunctionArgs) {
  const api = await getApi(args.request);
  const myData = await api.GetSomeData();
  return json(myData);
}
code Loader with Redirect
export async function loader(args: LoaderFunctionArgs) {
  const api = await getApi(args.request);
  const hasRole = await api.CheckRole("Store Admin");
  if (!hasRole) {
    return redirect("/login");
  }
  return json(myData);
}
lightbulb

Convention

Put loaders at the top of your route files, and define them as async function loader(...) instead of const loader = async (...).

api

Client-side API (useApi)

For API requests from React components (rather than loaders), use the useApi() hook. This returns an API object similar to the legacy cvApi from CEF.

code useApi Hook
export const MyComponent = () => {
  const api = useApi();

  useEffect(() => {
    api.GetSomeData().then(r => setSomeState(r));
  }, []);
};

Auto-generated Files

apiRoutes.ts

API route definitions, auto-generated from backend endpoints.

apidtos.ts

Input/output DTO types, auto-generated from backend models.

When to Use useApi vs. Loaders

Scenario Use
Static / public data Loader
SEO-critical content Loader
User-specific data (carts, profile) useApi
Data mutations (create, update, delete) useApi
warning

Actions are Deprecated

Remix Actions are painful to work with and often cause bloat or confusion. DO NOT create any new Actions. Use useApi for all data mutations instead.

table_chart

CV Grid

The CV Grid is a data grid component for paginated data, supporting both client-side and server-side data sources. It requires the data array, a total count, a unique row key, and column definitions.

Core Props

Prop Description
source Array of items to display in the grid
totalCount Total count of the full result set (for pagination)
rowKey Function returning a unique key per row (e.g. database ID)
columns Array of column definitions with title, content renderer, and optional className
code CVGrid Example
<CVGrid
  source={myArrayOfItems}
  totalCount={myArrayOfItems.length}
  rowKey={i => i.Id}
  columns={[
    { title: "Id", content: x => x.Id },
    { title: "Name", content: x =>
      <p>{x.FirstName} {x.LastName}</p> },
    { title: "Details", className: "w-0",
      content: x => <Button>Click Here</Button> }
  ]}
/>

Filters

Specify filters on a CV Grid by providing an array of filter definitions. Supported filter types:

number Exact match
text String matching
select Dropdown list
numberrange Min/max numeric
daterange Min/max date
code Filters Example
<CVGrid
  ...
  filters={[
    { title: "ID", type: "number", key: "Id" },
    { title: "Date", type: "daterange", key: "CreatedDate" }
  ]}
/>

Column Classes

Columns support three class properties, merged via the cn() utility:

Property Applies to Priority
className Both th and td Overrides grid defaults
thClassName Header (th) only Overrides className
tdClassName Cell (td) only Overrides className
add_circle

Extension Points

Extension points let submodules open themselves to customization from other submodules or the client project, without being directly edited or overridden.

Creating an Extension Point

code ExtensionPoint Component
import ExtensionPoint from "@core/components/PipelineComponents/ExtensionPoint";

export default function MyComponent() {
  return (
    <div>
      <h1>My Component</h1>
      <ExtensionPoint name="mycomponent.content" />
    </div>
  );
}

Registering an Extension

Create a .ext.tsx file and call registerExtension(). All .ext.tsx files are automatically loaded via glob pattern in root.tsx.

code MyWidget.ext.tsx
import { registerExtension } from
  "@core/components/PipelineComponents/ExtensionProvider";

const MyWidget = () => {
  return <div>Hello from my widget!</div>;
};

registerExtension("mycomponent.content", MyWidget, "MyWidget");

Sort Order Constants

Control rendering order by specifying a sort order as the fifth argument to registerExtension(). Lower values render first.

Constant Value Use
FIRST 0 Critical system-level extensions
VERY_HIGH 100 Important system features
HIGH 300 Important plugin features
NORMAL 1000 Default (most extensions)
LOW 1500 Less important content
VERY_LOW 2000 Supplemental content
LAST 9999 Debug / admin tools
code Sort Order Usage
import { registerExtension, EXTENSION_SORT_ORDER }
  from "@core/components/PipelineComponents/ExtensionProvider";

// Using constants (recommended)
registerExtension(
  "admin.dashboard", HighPriorityWidget,
  "HighPriority", false, EXTENSION_SORT_ORDER.HIGH
);

// Using direct numbers (fine-grained control)
registerExtension(
  "admin.dashboard", CustomWidget,
  "Custom", false, 250
);

// Default order (1000) — omit sort order
registerExtension(
  "admin.dashboard", NormalWidget, "Normal"
);

Passing Props to Extensions

Extension points can pass arbitrary props to their registered extensions:

code Props Forwarding
// In the host component
<ExtensionPoint
  name="dashboard.products.$id.Form"
  form={form}
/>

// In the extension
interface DashboardProductIdFormProps {
  form: UseFormReturn<any, any, undefined>;
}

export const ConditionSelector =
  ({ form }: DashboardProductIdFormProps) => {
    // ... use the form object ...
  };

registerExtension(
  "dashboard.products.$id.Form",
  ConditionSelector, "ConditionSelector"
);

Extension Point Rendering Flow

ExtensionPoint Rendered
Component mounts in the host tree
arrow_downward
Collects Registered Components
Queries registry for matching extension name
arrow_downward
Sorts by Order
Ascending sort by sort order value
arrow_downward
Renders in Sequence
Each extension receives forwarded props
file_copy

File Overrides

Phoenix supports overriding files from plugins. To override a plugin file, create the same file under /overrides/ instead of /plugins/. The compiler handles the replacement automatically.

Override Resolution

Original (plugin)
/RemixUI/app/plugins/core/components/login-form.tsx
arrow_forward
Override
/RemixUI/app/overrides/core/components/login-form.tsx
info

Restart Required

After creating an override file, restart your dev server for the change to take effect.

warning

DO NOT Import Override Files Directly

Always import the original file path. The compiler handles the override replacement. Importing override files directly causes hard-to-diagnose build issues.

lightbulb

Use Sparingly

Excessive overrides complicate upgrades, as they create diverged files that must be reconciled. If a plugin file is difficult to customize without overriding, that may indicate the plugin itself needs improvement.

palette

Tailwind CSS

Phoenix uses Tailwind CSS (instead of Bootstrap). Tailwind provides utility-first CSS with powerful features like exact values (p-[4px]) and responsive prefixes.

Media Breakpoints

Apply classes conditionally at specific breakpoints using prefixes. For example, md:px-4 is equivalent to Bootstrap's px-md-4.

Theme Editor Values

The theme editor sets colors as Tailwind variables, available as standard color utilities:

bg-danger-500 text-primary-800 border-success-300

Color Scale

Colors range from -50 (lightest) to -950 (darkest) in increments of 100, matching Tailwind's default color variable structure.

Borders & Radii

Class Purpose
rounded-base Component corner radius from theme editor
rounded-container Container corner radius from theme editor
border Border thickness from theme editor

The cn() Function

Use the cn() function to merge Tailwind classes cleanly. Classes are combined left to right, with later values overriding earlier ones.

code cn() Merging
const MyComponent = ({ className }: MyProps) => {
  return (
    <div className={cn("p-4 my-base-classes", className)}>
      Hello!
    </div>
  );
};
code Conditional Classes
<div className={cn("border", someCondition && "bg-danger-500")}>
warning

DO NOT Interpolate Class Names

Tailwind analyzes code statically to determine which CSS classes to include. Interpolated strings like p-${padding} will not be recognized. Instead, accept a className prop and merge with cn().

brush

Theme Editor

The theme editor provides a visual interface for customizing site appearance. Styles are defined as Tailwind variables and applied through a set of critical files.

Key Files

appSettings.theme.json

Default styles configuration

useElementTheme()

Hook that returns theme classes for an element

EssentialStyling.cs

CSS property definitions on the backend

tailwind.constants.ts

Safelisting for dynamic Tailwind classes

Adding Styles for a Themed Element

Follow these four steps to add theme editor support for a new element:

1

Edit appSettings.theme.json

Add default styles for your element in the appropriate section. Create the section if it does not exist.

2

Edit tailwind.constants.ts

Add your section and element name to the safelist array. This ensures the generated classes are included in the build.

3

Use useElementTheme() in your component

Call const themeClasses = useElementTheme("YourElementName") to get the theme classes.

4

Apply with cn()

Use cn("default-classes", themeClasses, parentClassNames) so theme values can be overridden when needed.

code useElementTheme Usage
const MiniMenuButton = ({ className }: Props) => {
  const themeClasses = useElementTheme("MiniMenuButton");

  return (
    <button
      className={cn(
        "px-3 py-2 rounded-base border",
        themeClasses,
        className
      )}
    >
      ...
    </button>
  );
};

Adding a New Style Property

Follow these five steps to add a new CSS property (e.g. maxHeight) to the theme editor:

1

Add property in EssentialStyling.cs

public EssentialStyling MaxHeight { get; set; } = new() { EditorType = "slider", Unit = "px" }

2

Extend tailwind.config.ts

Add to the extend object: maxHeight: extendFromComponents(getComponentCss, "MaxHeight")

3

Add prefix to tailwind.constants.ts

Add a new key to TailwindPrefix (e.g. "max-h") and to standardPrefixesToPascalCSSProperty.

4

Update makeSafeList in tailwind.helpers.ts

Add a new item to the result array matching your prefix.

5

Update useElementTheme.ts (if needed)

Some properties need special handling. For example, Tailwind cannot distinguish text- between font-size and color, requiring text-[length:var(--)] syntax.

lightbulb

Naming Convention

Be specific with element names. Use MiniMenuButton or MiniMenuIcon, not a generic MiniMenu. A "section" can be any container, not just a page.