---
name: frontend
description: Guide for building frontend features in Kantree including key architecture concepts and common patterns to use.
---
# Kantree Frontend

## Table of Contents

- [Code Style Guidelines](#code-style-guidelines)
- [Files organization](#files-organization)
- [Design System](#design-system)
- [Lifecycle](#lifecycle)
- [Stores](#stores)
- [Loaders](#loaders)
- [Global State & Services](#global-state--services)
- [Push (real-time)](#push-real-time)
- [Offline](#offline)
- [Actions](#actions)
- [Components](#components)
- [Permissions](#permissions)
- [Internationalization (i18n)](#internationalization-i18n)
- [API](#api)
- [Routes](#routes)
- [System architecture](#system-architecture)

---

Historically, Kantree was an AngularJS app. SolidJS was chosen as the technology to succeed AngularJS. Following refactoring work carried out in 2022, Kantree is now a SolidJS app that launches an AngularJS app. The architecture and lifecycle are completely extracted from AngularJS, but a majority of the components have not yet been migrated.

LESS is used for as a CSS preprocessor. LESS files are located in the `public/app` folder along their components.
Global styles and LESS utilities are located in `public/less`.

SolidJS documentation is available at <https://docs.solidjs.com/llms.txt>.

Kantree's frontend code is located in `public/app`. This folder is aliased as `@kantree` for imports.
The app entry points is located at `public/app/index.js`.

The app is built using esbuild. The backend will render the html page. The template for it is located at `src/kantree/web/templates/app_layout.html` and `src/kantree/web/templates/layout.html`.

Assets are managed using [webassets](https://webassets.readthedocs.io/en/latest/) (a python library) and [Flask-Assets-Pipeline](https://github.com/hyperflask/flask-assets-pipeline), a flask extension that manage assets build using esbuild.

All the frontend source files are located in `public/`.

## Code Style Guidelines

- **SolidJS** for new components, **AngularJS** for legacy
- Import paths use `@kantree/*` alias mapping to `./public/app/*`
- ES6+ modules, bundling via esbuild
- No strict TypeScript - config allows gradual adoption

## Files organization

Inside `public/app` you will find:

- `actions`: functions that performs actions with the API and update stores
- `apps`: the different "apps" used inside workspaces
- `attributes`: the various available attribute types and their visual components
- `config`: static configuration and constants
- `extensions`: external integrations (Dropbox, Google, OneDrive, SSO, emailing...)
- `loaders`: functions to load data into stores from the backend
- `modals`: modals that can be open from anywhere in the app
- `routes`: the various "pages"
- `services`: global services
- `stores`: data stores
- `ui`: UI components used across the app
    - `base`: base components to create interfaces using our design system
    - `system`: base components for the app layout
- `utils`

### Key configuration files

- `@kantree/config/icons`: `iconClass(name)` converts icon names to CSS classes (supports `bi-*`, `fa-*`, `kfi-*` prefixes)
- `@kantree/config/roles`: `availableOrgRoles`, `availableTeamRoles`, `rolePermissionMap` for permissions system
- `@kantree/config/project/views`: `projectViewsAvailableProps`, `projectViewsDefaultProps` for view configuration

## Design System

The design system is currently in transition, with the goal of standardizing around SolidJS components.

**Current state:**
- **SolidJS components**: `@kantree/ui/base` (buttons, forms, layout, modals, etc.) — preferred for new code
- **CSS classes**: `public/less/base` and `public/less/components` — legacy, still used by AngularJS components
- **Utility classes**: `public/less/utilities/ruleset.less` — Tailwind-like utility classes for spacing, flex, etc.

**Direction:**
- New UI should use SolidJS components from `@kantree/ui/base`
- CSS classes may still be used internally by these components
- Tailwind or similar utility-first approach is under consideration

### LESS structure

Entry point: `public/less/all.less`. Import order:
1. `variables.less` — design tokens (colors, spacing, breakpoints, typography)
2. `theme.less` — CSS custom properties for dynamic theming
3. `mixins.less` — LESS utilities
4. `base/` — reboot, layout, grid
5. `components/` — buttons, forms, modals, etc.
6. `utilities/` — utility classes
7. `public/app/**/*.less` — component-specific styles

### Dynamic theming

Organization colors adapt via CSS custom properties (`--theme-base-*`) defined in `theme.less`. The theme is set dynamically by `@kantree/ui/system/theme.js` based on HSL values, with hover/active/light variants computed automatically.

### Utility classes

Tailwind-like classes (`.d-flex`, `.m-2`, `.gap-3`, etc.) generated from `utilities/ruleset.less`. Responsive variants use `-sm`, `-md`, `-lg` suffixes. See the file for the complete list.

## Lifecycle

Kantree's operation follows a model popularized by Flux and then Redux: updates are unidirectional.
This pattern ensures that all parts of the application remain synchronized with a single data source.
It simplifies the application's architecture and lifecycle.

This responsiveness is based on Solid's reactivity mechanisms.
The different components of this lifecycle are:

- Stores: collections of observable documents
- Loaders: load data from the backend into the store
- Actions: call the API to execute an action and then update the stores
- Components: retrieve data from the store and react to changes, triggering actions

Our architecture is simpler than Redux; there's no concept of a reducer, etc.
It's quite similar to MobX but doesn't group actions and stores into the same class.

It's crucial to never break this lifecycle.

## Stores

Stores constitute the **State Layer** of our architecture. They implement the **Repository** pattern combined with the **Observable** pattern, serving both as a local cache and a reactive source of truth.

Stores are defined in `@kantree/stores` and extend from `@kantree/stores/core/store/Store`. They are built on top of SolidJS `createStore()`.

### Key concepts

- **Stable identity**: an object from a store always keeps the same reference. Components can hold a reference and automatically receive updates.
- **Reactive properties**: all properties are Solid signals, enabling fine-grained reactivity.
- **Transactions**: wrap multiple operations with `store.transaction()` to batch events and persist once.
- **Reactive queries**: use `store.react(filters)` to get a Signal that auto-updates when matching objects change.

### Persistence and cache strategy

Most stores are persisted in IndexedDB (`@kantree/services/storage`). This approach implements the **Stale-While-Revalidate** pattern: on load, stores are populated from IndexedDB (potentially stale but immediately available data), before being updated by the API (fresh data).

### Global vs scoped persistence

There are two persistence strategies depending on the store type:

**Global stores** (`projectStore`, `cardModelStore`, `viewStore`, etc.):
- Persisted with a unique key (`{typeName}-store`)
- Cleared and replaced on each project navigation via `sideLoadProject()`

**Project-scoped stores** (`cardStore`, `activityStore`, `cardSubscriptionStore`, `cardGroupSubscriptionStore`):
- Managed by `ProjectLocalStorageManager` (`@kantree/ui/project/local-storage.js`)
- Persisted with a **per-project key** (`{typeName}-store-project-{id}`)
- Enables **multi-project cache**: each visited project keeps its data in IndexedDB
- On project entry: load from IndexedDB cache, then update via API
- On project exit: memory is cleared, but IndexedDB cache remains for fast subsequent access

This system enables fast navigation between previously visited projects and per-project offline access.

### Historical compatibility

The current version of our store system is an evolution from the previous AngularJS-based version. The system is backward compatible and methods allowing AngularJS to observe the store are still present.

## Loaders

Loaders are simple async functions that fetch data from the API and populate stores. Utilities are available in `@kantree/loaders/utils` (`loadIntoStore`, `singletonLoader`...).

## Global State & Services

Beyond stores, several services manage global application state. They follow a common pattern: a **reactive store** (or signal) combined with a **singleton loader**.

### Authentication (`@kantree/services/auth`)

- `currentUser`: reactive store containing the authenticated user
- `isAuthenticated()`: signal returning `true` if logged in
- `loadCurrentUser()`: singleton that fetches and populates `currentUser`

### App Context (`@kantree/services/app-context`)

Aggregates user data after authentication: organizations, teams, projects.

- `appContext`: reactive store with `orgs`, `teams`, `projectsCount`
- `userProjectStore`, `favoriteProjectStore`, `personalProjectStore`, `sharedProjectStore`: pre-scoped project stores
- `loadAppContext()`: singleton that loads all context data
- `fetchAppContextData()`: refetch context (after org/team changes)

### Server Config (`@kantree/services/config`)

- `serverConfig`: reactive store with backend configuration (feature flags, integrations, limits)
- `loadServerConfig()`: singleton with offline fallback to localStorage

### Connectivity (`@kantree/services/connectivity`)

- `isOnline()`: browser online status
- `isConnected()`: WebSocket connection status
- `ensureConnectivity()`: helper that throws if offline

### Storage (`@kantree/services/storage`)

- `jsonLocalStorage`: localStorage wrapper with JSON serialization
- `idbLocalStorage`: IndexedDB wrapper (used by stores for persistence)

### Events (`@kantree/utils/events`)

Custom event system outside Solid's reactivity. Exports `createEvent()` and `onEvent()`.

## Push (real-time)

Push enables receiving real-time updates from the backend via WebSocket (Socket.IO).

This mechanism is essential for collaborative work: when a user modifies data, all other connected users receive the update without having to refresh.

### Connection management

The connection is managed by `@kantree/services/push`:
- `getPushConnection()` returns a singleton instance of the connection

### Event levels

There are two levels of events:
- **Global events** (`conn.on`): `logout`, `notified`, `join_org`, `leave_project`...
- **Project events** (`room.on`): after joining a room with `conn.join(project.push_id)`

### Project event handling

Project event handling is centralized in `ProjectPushListener` (`@kantree/ui/project/push-listener.js`).
The principle is the same as for actions: events trigger loaders that update the stores.

For some simple cases (deletions, archive/restore, reordering), the store can be updated directly without an API call. This is a form of **inverse Optimistic Update**: the update comes from the server but we trust the delta rather than re-fetching the complete entity.

### Origin filtering

If a client is the origin of an operation, it will not receive a push message. Push messages are broadcasted to all clients that were not at the origin of the operation.

## Offline

The app supports a limited offline mode, partially implementing the **Offline-First** pattern:

- **Reading**: stores persisted in IndexedDB allow browsing data without a connection (read-only)
- **Writing**: a queue system exists in `@kantree/services/offline-sync` but is only used for comments (`queueRemoteAction`). The queue is in-memory and not persisted.

> **Architectural note**: A true Offline-First mode would require a persisted queue with conflict reconciliation (CRDTs, Operational Transform, or last-write-wins strategies). Our current approach prioritizes simplicity for common use cases.

## Actions

Actions are functions in `@kantree/actions/*` that call the API and update stores accordingly. They are the **only place** where API calls and store mutations should happen together.

### Action patterns

1. **Lock mechanism**: always acquire a lock with `store.lock(obj)` before API mutation to prevent race conditions
2. **Standard pattern**: API call → store update
3. **Optimistic update**: store update → API call → rollback on error
4. **Offline-capable**: queue action when offline (limited support, see `@kantree/services/offline-sync`)

For long-running operations, use `waitForAsyncJobResult()` from `@kantree/utils/async-api-response`.

See files in `@kantree/actions/` for examples of each pattern.

### Why actions must update stores

Because push messages are not sent to clients at the origin of the operation, the store update must be performed by the action. This enables optimistic rendering and ensures the originating client sees immediate feedback.

## Components

New components should always use SolidJS. Use our base components from `@kantree/ui/base` to compose an interface.

Components constitute the **View Layer** and should remain **pure projections** of the store state.

Principles to follow:
- **No local state for business data**: state should live in stores
- **Declarative reactivity**: use Solid primitives (`createEffect`, `createMemo`) to react to changes
- **Separation of concerns**: components read from stores and trigger actions, they never modify stores directly

Components leverage Solid's fine-grained reactivity: only UI parts depending on modified data re-render.

### UI utilities

| Utility | Location |
|---------|----------|
| Base components (Button, Input...) | `@kantree/ui/base` |
| Dialogs (`confirmDialog`, `errorDialog`, `openModal`) | `@kantree/ui/base` |
| Toast notifications (`toastNotify`) | `@kantree/ui/system/toast` |
| Responsive helpers (`screensize`, `isScreensizeXS`...) | `@kantree/ui/base/screensize` |

## Permissions

Permission checks in the frontend use `@kantree/stores/utils/role`. See [docs/backend.md](backend.md#permissions-system) for the full permissions model.

**Data:** `project.user_permissions` contains computed permissions for current user.

**Checking:** Use `isAllowedInProject(project, objectType, actions, objectId?)` for generic checks, or named helpers like `canEditCard()`, `canArchiveCard()`, `canManageSettings()`.

See `@kantree/stores/utils/role` for the complete list of helpers.

### AngularJS

There are still a lot of angularjs components in the app which usually looks like this:

    import angular from 'angular';
    import { AngularClass } from '@kantree/utils/angular';

    angular.module('kantree').component({           // always use the kantree module
        templateUrl: '/path/to/template.html',      // templates should be located in a templates folder relative to the component file
        bindings: {                                 // similar to props in solidjs
            prop1: BINDING_TYPE,
            prop2: BINDING_TYPE,
            //...
        },
        controller: class extends AngularClass {    // our custom base class for components
            static $inject = ['$scope', '$element', 'anyService', /* ... */];     // Dependency injection

            $onInit() {                             // angularjs lifecycle function
                // called at init but after $onChanges

                // injected dependencies are available via this.$
                this.$scope.$on('event', () => {}); // if the dependency starts with a $, no need to double it
                this.$element;
                this.$anyService;
            }

            $onChanges(changes) {

            }

            $postLink() {

            }

            $onDestroy() {
                
            }
        }
    });

An angularjs component can be used inside a solidjs component using `<NgComponent>`:

    import { NgComponent } from '@kantree/ui/utils/ng-component';

    function SolidWrapper(props) {
        return <NgComponent directive="my-ng-component" prop={props.prop} />
    }

Similarly, a solidjs component can be used in an angularjs component template:

    <solidjs-component component="$ctrl.Component" prop="$ctrl.prop"></solidjs-component>

## Internationalization (i18n)

Gettext-like system at `@kantree/services/i18n`. All user-facing strings must be wrapped.

- `_(str, vars?)`: simple translation with optional interpolation (`{varName}` syntax)
- `_n(singular, plural, count)`: pluralization
- `_lazy(str)`: deferred evaluation (for configs, route definitions)

## API

The api can be used using `@kantree/api` which is generated by the backend:

    import api from '@kantree/api';

    // prototype: api.service.endpoint.method_http(params, options);

    const project = await api.projects.get_project.get({project_id: 1});

A global error handler is enabled: a message will be shown to the user if an API call results in an error. It can be disabled like this:

    try {
        const project = await api.projects.get_project.get({project_id: 1}, {ignoreErrorHandlers: true});
    } catch (e) {
        console.error(e);
    }

## Routes

The router being used is [solid-router](https://github.com/solidjs/solid-router) with additional custom tooling.

### Route definition

Define new routes using `@kantree/routes/routing/addRoute()`:

    import { addRoute } from '@kantree/routes/routing';

    addRoute({
        name: 'myRoute',
        path: '/path',
        component: MyComponent,
        children: [
            {
                name: 'subRoute',
                path: '/:id',
                component: MySubComponent
            }
        ]
    });

The main addition to Solid Router Config Based Routing is named routes.
Nested routes can be referenced by their fully qualified name (eg: `myRoute.subRoute`)

Some "page decorators" are provided in `@kantree/routes/page-decorators` to wrap route components with common behaviors.
Example:

    addRoute({
        component: () => <AuthenticatedPage><MyRoutComponent /></AuthenticatedPage>
    })

### Navigation

Use our custom `useNavigate()`:

    import { useNavigate } from '@kantree/routes/routing';

    const navigate = useNavigate();
    navigate('myRoute.subRoute', {id: 1});

A custom version of `<A>` is also available:

    import { A } from '@kantree/routes/routing';

    return <A route="myRoute.subRoute" routeParams={{id: 1}}>click me</A>

### Route data

In `@kantree/routes/route-data` there is some utilities to handle loading data when a route is accessed.

Wrap a route component with `<RouteData>` and provide a loader function that returns a Solid resource. A loading indicator will be displayed until the resource is loaded.
Use `useRouteData()` to access the data in your route function.

    function routeData() {
        const [data] = createResource(/** ... **/);
        return data;
    }

    addRoute({
        path: '/',
        component: () => <RouteData loader={routeData}><MyRouteComponent /></RouteData>
    })

## System architecture

For an overview of the domain model (workspaces, cards, groups, attributes, etc.), see [docs/domain-model.md](domain-model.md).

### Project page architecture

The project page is orchestrated by `ProjectScopeManager` (`@kantree/ui/project/scope-manager`).

This class coordinates:
- **Project/board loading**: fetches and side-loads project data into stores
- **`ProjectLocalStorageManager`**: manages per-project IndexedDB persistence (see Stores section)
- **`ProjectPushListener`**: handles real-time updates for the project room (see Push section)
- **`ProjectPresenceManager`**: tracks connected users in the project
- **`ProjectContext`**: provides project context to apps

Extended by `PortfolioScopeManager` and `MyWorkProjectScopeManager` for other page types.

### Apps

The main interface is the `project` route in `@kantree/routes/project`.

Users visualize their project data using "views". Each view is handled by an app defined in `@kantree/apps/project`.

[Views that are used to display cards](public/app/apps/project/cards) (kanban, table, list, ...) use the `<cards-app>` component which is the frontend to the various visualization modes of cards. The component and all visualization modes are defined in `@kantree/apps/cards`.

### Attributes

Attribute types available for cards are defined in `@kantree/attributes`. See `registry.js` for how types are registered, and individual type files for implementation examples.

# SolidJS Documentation

> Solid is a JavaScript library built around signals. It prioritizes a simple and predictable development experience, making it a great choice for developers of all skill levels. These are the documention for the main official projects.
> - SolidJS: The core library for building reactive user interfaces.
> - SolidStart: A full-stack framework for building web applications.
> - Solid Router: A routing library for building web applications.
> - Solid Meta: A library for managing the HTML head and meta tags.
