@openreachtech/crm-kit-frontend
v1.1.0
Published
Frontend kit for building CRM applications.
Keywords
Readme
@openreachtech/crm-kit-frontend
Frontend kit for building CRM applications on Nuxt 3.
Concept
The kit ships the parts a CRM screen is made of: a list view over a records table, a detail view over one record, and the modules beside them for comments, memos, notifications, global search, email and file preview. Each part draws through furo-vue and reads its data through an adapter the consuming app writes.
Nothing in the kit knows your backend. A consuming project customizes it three ways and never forks it:
- An adapter wraps your data source behind one contract and returns the
uniform
{ isSuccessful, payload, error }result the kit reads. - A registry decides every multi-way choice: how a column renders, which editor a field uses, what a row action does. You pass strategies and the kit merges them over its own defaults, last one wins.
- The theme retunes CSS custom properties in your own cascade layer. No component edits, no specificity fights.
Installation
Requires Node.js 20.x (the version the CI builds against).
npm install @openreachtech/crm-kit-frontendThe package publishes to public npmjs.com and needs no registry setup. If your
project already points the whole @openreachtech scope at GitHub Packages, the
install fails on @openreachtech/furo-vue, which publishes to npmjs.com. npm
offers no per-package registry override, so point the scope at npmjs.com:
@openreachtech:registry=https://registry.npmjs.orgnuxt and @nuxt/icon are peer dependencies your app brings itself. The kit
imports Icon from #components, a virtual module only Nuxt resolves, so the
consuming app must be a Nuxt app.
It is an ES module ("type": "module"); import it with ESM import syntax.
Wiring
Three steps, and the package cannot perform any of them for you. Skip one and the kit fails in a way that looks like a bug in the kit.
1. Transpile the package
// nuxt.config.js
build: {
transpile: [
'@openreachtech/crm-kit-frontend',
],
},Twenty-four files under lib/ import Icon from #components. Without
transpilation the packaged .vue files do not resolve that specifier and the
build fails.
2. Register the furo icon renderer
// plugins/001.furo-icons.js
import {
defineNuxtPlugin,
} from 'nuxt/app'
import {
Icon,
} from '#components'
import {
furoIconRegistry,
} from '@openreachtech/furo-vue/icons'
export default defineNuxtPlugin(() => {
furoIconRegistry.useIconRenderer({
component: Icon,
})
})furo-vue's offline registry carries 49 icon names and none of the kit's. Skip this and icons render empty.
3. Load the five published stylesheets, in this order, ahead of your own
// nuxt.config.js
css: [
'@openreachtech/crm-kit-frontend/lib/assets/css/0000.crm-kit-layers.css',
'@openreachtech/furo-nuxt/lib/assets/css/0100.reset.css',
'@openreachtech/furo-vue/lib/assets/css/furo.css',
'@openreachtech/crm-kit-frontend/lib/assets/css/crm-kit-theme.css',
'@openreachtech/crm-kit-frontend/lib/assets/css/crm-kit-components.css',
'~/assets/css/your-app.css',
],The order carries meaning. 0000.crm-kit-layers.css declares the layer order
in one statement, and a layer's position is fixed where its name first appears,
so loading anything ahead of it pins a layer in the wrong place and your own
@layer app quietly stops winning. furo.css carries the design tokens every
kit component reads. crm-kit-theme.css is required, not optional: it defines
the semantic tokens furo-vue does not ship, and without it edited cells,
completed path-group stages and mentions render unstyled.
docs/theming.md documents the sizing and motion tokens, the class contract and how to override a rule. It ships inside this package rather than as a GitHub link, because the repository is private and such a link would 404; ADR 0017 records that departure from the README convention.
Usage
Import from the package root:
import {
ListView,
ListViewKit,
buildErrorResult,
buildSuccessResult,
} from '@openreachtech/crm-kit-frontend'Write an adapter over your data source:
export default class CustomerListViewAdapter {
constructor ({
graphqlClientHash,
}) {
this.graphqlClientHash = graphqlClientHash
}
static create ({
graphqlClientHash,
}) {
return new this({
graphqlClientHash,
})
}
async fetchRecords (query) {
const capsule = await this.graphqlClientHash
.customers
.invokeRequest({
variables: {
input: query,
},
})
if (capsule.hasError()) {
return buildErrorResult({
code: capsule.getErrorCode(),
message: capsule.getErrorMessage(),
})
}
return buildSuccessResult({
payload: {
records: capsule.records,
columns: capsule.columns,
rowKey: 'id',
pagination: {
page: query.pagination.page,
offset: query.pagination.offset,
limit: query.pagination.limit,
totalRecords: capsule.totalRecords,
},
},
})
}
// updateRecordsBatch completes the required half. fetchColumns (a header for an
// empty result set), fetchEditOptions, deleteRecordsBatch and bulkChangeRecords
// (the selection bar's Delete and Bulk change) are optional.
}Build the kit once, with your own strategies merged over the defaults:
const listViewKit = ListViewKit.create({
entityKey: 'customer',
listViewAdapter: CustomerListViewAdapter.create({
graphqlClientHash,
}),
customColumnRenderers: [
CurrencyColumnRenderer.create(),
],
})Then bind its props hash onto the part:
<template>
<ListView
v-bind="context.listViewKit.listViewProps"
@record:action="context.onRecordAction($event)"
@selection:change="context.onSelectionChange($event)"
/>
</template>API
index.js is the API. It re-exports every promised symbol, and the package
ships dist-types/, so an editor resolves each one's full signature from the
package itself. Read index.js for the list, grouped by area, and your editor
for the signatures.
Three kinds of symbol live there:
- Components and their contexts.
ListView,DetailViewSections,CommentsPaneland the rest. Twenty-nine of the thirty-one take a singleparcelprop whose type the root publishes as<Component>Parcel;ListViewandSidebarMenutake named props instead. Every component returns{ context }fromsetup(), so a template ref reaches its context. - Classes a consumer constructs.
ListViewKit.create()and the fourDefault*Registrarclasses seed the registries.StrategyRegistry.create()holds your strategies.ToastNotifier,KitMessageResolverand the DTOs (Comment,EmailDraft,MemoDraft,Notification) follow the same.create()factory shape. - Contracts a consumer implements, published as types only: the ten
adapters,
Notifier,MessageResolver,FetchFileandResolvePreviewHref. Name them off the package root, never off alib/path.
Stable surface
Four things are stable at 1.0.0, and nothing else is.
Every symbol re-exported from the package root. index.js is the list. Deep
lib/** subpaths keep resolving today and carry no promise, so a symbol absent
from the root can move or disappear in a minor release. 1.x will narrow the
package's exports map to the blessed paths, which are the root and the
stylesheets under lib/assets/css/. Reach for a deep subpath knowing that.
Eight theme tokens. Seven the kit owns. Six of those it defines outright,
because furo-vue ships nothing for them: --color-background-edited-cell,
--color-text-edited, --color-path-group-completed,
--color-path-group-completed-foreground, --color-background-mention and
--color-text-mention. The seventh, --size-focus-ring, is the kit's own name
for the width of every focus ring it draws, and it defaults to furo-vue's
--size-focus-ring-spread, a token furo-vue declares and no furo-vue component
reads. The eighth, --color-ring, is furo-vue's, and the kit promises it anyway
because the focus ring is the one furo token a consuming app must be able to
retune on its own. --color-primary, --color-primary-foreground,
--color-destructive and --color-destructive-foreground are furo-vue's too and
carry no promise here: the kit retunes them for its reference shell, and a
consuming app is expected to retune them again. It darkens --color-destructive
in light mode because furo-vue's value measures 3.67:1 there, below the 4.5:1
that error text needs. --color-destructive-surface is the kit's own name for
the fill a danger button takes, held apart so that button can follow the design
without dragging the error text with it.
The .unit-* class contract. A component's root element carries
.unit-<component-name>, and a child class is addressable only through that
root: write .unit-comment-entry > .body, never .body alone. The DOM shape
between root and child is free to change.
Message keys. A key the kit resolves through your MessageResolver keeps
its name and its parameters. EN_MESSAGES and JA_MESSAGES ship the kit's own
packs; override an entry by resolving the key yourself.
Contribution
The repository is private, so homepage, bugs and repository in
package.json resolve only for members of the openreachtech organization. Bug
reports, feature requests and code contributions are welcome from members
through GitHub Issues.
git clone https://github.com/openreachtech/crm-kit-frontend.git
cd crm-kit-frontend
npm install
npm run lint
npm testLicense
This project is released under the Apache License 2.0.
For more details, please see in the LICENSE file.
Developer
Copyright
© 2026 Open Reach Tech Inc.
