Documentation
FastAnn is a production-ready UI annotation library. Attach contextual notes, badges, and dialogs to any DOM element — with zero layout impact. Works with Angular, React, Vue, and plain HTML/jQuery.
Installation
Each framework has its own adapter package. Install only what you need:
# Angular 17+
npm install @fastann/angular
# React 18+
npm install @fastann/react
# Vue 3
npm install @fastann/vue
# jQuery 3+ / plain HTML
npm install @fastann/core @fastann/jquery
The Angular, React, and Vue packages already include @fastann/core as a dependency — you do not need to install core separately for those frameworks.
Cell Keys
A cell key (data-ck) is a stable string identifier for each annotatable element. All directives and components set this attribute automatically.
- Use a path-style format:
col:revenue,row-42:margin,kpi-leads - Keys must be stable across page loads — use your data's primary key, not a loop index
- Keys are scoped to
domain + routeby the backend, so the same key can exist on different pages
<!-- Angular -->
<td ann-cell-key="row-42:revenue">...</td>
<!-- Vue -->
<AnnotationCell :annotation="ann" cell-key="row-42:revenue">...</AnnotationCell>
<!-- React -->
<AnnotationCell annotation={ann} cellKey="row-42:revenue">...</AnnotationCell>
<!-- jQuery / plain HTML -->
<td data-ck="row-42:revenue">...</td>
Annotation Types
| Type | Badge | Use case |
|---|---|---|
info | ● Blue | Definitions, descriptions |
rule | ● Orange-red | Business rules, constraints |
comment | ● Green | General notes |
warning | ● Amber | Data quality alerts |
formula | ● Purple | Calculation logic |
action | ● Indigo | Action items |
task | ● Dark blue | Tasks with to_do / in_progress / done status |
Permissions
FastAnn uses a granular per-user, per-page permission model. Set permissions on the backend and pass them in the user object via AnnotationAuthService.setCurrentUser() (Angular) or equivalent.
| Key | Description |
|---|---|
add_new_dialog_custom | Create a new annotation |
edit_title / edit_body / edit_tag / edit_type | Edit annotation fields |
edit_status | Change task status (to_do / in_progress / done) |
edit_assignee / edit_due_date | Assign tasks |
set_position_size | Move / resize dialogs |
delete_dialog_custom | Delete an annotation |
view_comment / send_comment / delete_comment | Comment thread actions |
edit_permission | Manage page permissions |
view_logs | View activity history |
set_anchor | Pin dialog anchor position |
Angular Guide
Download ExampleAngular 17+ Uses Angular Signals, standalone components, and ng-packagr ESM bundles.
1. Import global CSS
Add to styles.scss (or angular.json styles[]):
@import '@fastann/angular/styles/annotation.css';
2. Register providers
In app.config.ts, provide the backend URL, license key, and (optionally) the domain used to scope annotations:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideHttpClient } from '@angular/common/http';
import { ANN_API_URL, ANN_LICENSE_KEY, ANN_DOMAIN } from '@fastann/angular';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimations(),
provideHttpClient(),
{ provide: ANN_API_URL, useValue: 'https://api.yourapp.com/api' },
{ provide: ANN_LICENSE_KEY, useValue: 'YOUR_PRO_LICENSE_KEY' },
// Optional — override the domain used to scope annotations.
// Defaults to window.location.hostname when omitted.
// { provide: ANN_DOMAIN, useValue: 'yourapp.com' },
],
};
| Token | Required | Description |
|---|---|---|
ANN_API_URL | Yes | Base URL of your FastAnn backend API (no trailing slash). |
ANN_LICENSE_KEY | Pro | Your Pro license key. Omit or leave blank to run in Free mode. |
ANN_DOMAIN | Optional | Pin the domain used to scope and load annotations. Useful for multi-tenant apps or when your app URL differs from the licensed domain. Falls back to window.location.hostname if not provided. |
3. Add the stage overlay
Place <app-annotation-stage /> once at the app root. It renders floating dialogs above all page content.
// app.component.ts
import { AnnotationStageComponent } from '@fastann/angular';
@Component({
standalone: true,
imports: [RouterOutlet, AnnotationStageComponent],
template: `
<main><router-outlet /></main>
<app-annotation-stage />
`,
})
export class AppComponent {}
4. Make cells annotatable
Use AnnotationCellDirective on any element. Declare the annotation inline with ann-* attributes, or pass an Annotation object from the service.
// my-page.component.ts
import {
AnnotationCellDirective,
AnnotationKeyDirective,
CustomAnnotationService,
DotVisiblePipe,
} from '@fastann/angular';
@Component({
standalone: true,
imports: [CommonModule, AnnotationCellDirective, AnnotationKeyDirective, DotVisiblePipe],
templateUrl: './my-page.component.html',
})
export class MyPageComponent {
constructor(public customService: CustomAnnotationService) {}
}
<!-- Option A: inline system annotation (library auto-generates id & syncs to DB) -->
<th
ann-cell-key="col:sales"
ann-type="info"
ann-source="Finance"
ann-title="Sales (USD)"
ann-body="Gross sales, net of refunds, FX-normalised to USD."
ann-tag="KPI"
[ann-default-size-w]="300"
[ann-default-size-h]="160">
Sales (USD)
</th>
<!-- Option B: pass API annotation object (dynamic rows) -->
<td
[annCell]="customService.forCell(row.id + ':sales')"
[ann-cell-key]="row.id + ':sales'">
{{ "{{" }} row.sales | number {{ "}}" }}
</td>
<!-- Option C: key-only (no pre-declared annotation — users add notes at runtime) -->
<th [annKey]="'th:product'">Product</th>
5. Load annotations from the API
Call loadFromApi() in your page component's ngOnInit:
import { Component, OnInit, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Router } from '@angular/router';
import { CustomAnnotationService } from '@fastann/angular';
@Component({ ... })
export class MyPageComponent implements OnInit {
constructor(
public customService: CustomAnnotationService,
private router: Router,
@Inject(DOCUMENT) private doc: Document,
) {}
ngOnInit() {
this.customService.loadFromApi(
this.doc.location.hostname, // ignored when ANN_DOMAIN is provided via DI
this.router.url, // e.g. '/sales/dashboard'
);
}
}
Tip: When ANN_DOMAIN is injected (see step 2), it takes precedence over the hostname passed to loadFromApi(). You can safely pass document.location.hostname as a fallback — the injected value wins.
6. Set the current user
After login, pass the user to AnnotationAuthService. This controls which permission-gated actions are shown:
import { AnnotationAuthService } from '@fastann/angular';
constructor(private annAuth: AnnotationAuthService) {}
onLoginSuccess(user: any) {
this.annAuth.setCurrentUser({
code: user.code,
name: user.name,
role: user.role,
permissions: user.ann_permissions, // AnnPermissionItem[] from your API
});
}
7. (Optional) Built-in panel
AnnPanelComponent provides a draggable panel with filter controls, dark mode, language switch, and permission management. Add it once per page:
import { AnnPanelComponent } from '@fastann/angular';
// in imports: [AnnPanelComponent]
<!-- my-page.component.html -->
<ann-panel />
<div class="page-content"> ... </div>
8. Multi-language
Switch UI language at any time. Supported: 'en', 'vi', 'zh', 'ja', 'ko', 'fr', 'de', 'es', 'pt', 'it', 'ru', 'tr', 'id', 'th', 'hi', 'ar', 'nl', 'pl'.
import { AnnotationLabelsService } from '@fastann/angular';
constructor(private labels: AnnotationLabelsService) {}
switchLang(lang: string) { this.labels.setLang(lang as any); }
Vite pre-bundling note
When using @angular-devkit/build-angular v17 (Vite dev server), exclude the library from Vite's dep pre-bundling to avoid esbuild conflicts. Add to angular.json under serve > options:
"prebundle": { "exclude": ["@fastann/angular"] }
React Guide
Download ExampleReact 18+ Uses useSyncExternalStore for reactive state. No context provider required — the core store is a singleton.
Note: The React adapter is currently in preview. Core store integration and all UI components work; backend sync helpers are planned.
1. Import global CSS
// main.tsx or App.tsx
import '@fastann/react/styles';
2. Add the stage overlay
Place <AnnotationStage /> once at the root. It renders the filter toolbar and floating dialogs:
// App.tsx
import { AnnotationStage } from '@fastann/react';
import '@fastann/react/styles';
export default function App() {
return (
<>
<AnnotationStage />
<YourPageContent />
</>
);
}
3. Make cells annotatable
Use AnnotationCell to wrap any element. Pass an annotation object and a stable cellKey:
// SalesTable.tsx
import { AnnotationCell, AnnotationKey } from '@fastann/react';
import type { Annotation } from '@fastann/react';
const revenueAnn: Annotation = {
id: 'ann-col-revenue',
type: 'info',
source: 'Finance',
title: 'Total Revenue',
body: 'Gross revenue net of refunds, FX-normalised to USD.',
tag: 'KPI',
};
export function SalesTable({ rows }) {
return (
<table>
<thead>
<tr>
<AnnotationCell as="th" annotation={revenueAnn} cellKey="col:revenue">
Revenue
</AnnotationCell>
{/* Key-only: no pre-declared annotation, users can add notes */}
<AnnotationKey as="th" annKey="col:product">Product</AnnotationKey>
</tr>
</thead>
<tbody>
{rows.map(row => (
<tr key={row.id}>
<td>{row.product}</td>
<AnnotationCell as="td" annotation={rowAnn(row)} cellKey={`${row.id}:revenue`}>
{row.revenue.toLocaleString()}
</AnnotationCell>
</tr>
))}
</tbody>
</table>
);
}
4. Access the store in custom components
import { useAnnotationStore, useCustomAnnotations } from '@fastann/react';
function MyToolbar() {
const { mode, setMode, toggleAddMode, addMode, closeAll, toggleDark } = useAnnotationStore();
return (
<div>
<button onClick={() => setMode('all')}>All</button>
<button onClick={toggleAddMode} className={addMode ? 'active' : ''}>
+ Add
</button>
<button onClick={closeAll}>Close all</button>
<button onClick={toggleDark}>🌙</button>
</div>
);
}
Vue Guide
Download ExampleVue 3 Uses composables backed by @fastann/core pub-sub store and Vue ref reactivity. No Pinia or Vuex required.
Note: The Vue adapter is currently in preview. Core store integration and all UI components work; backend sync helpers are planned.
1. Import global CSS
// main.ts
import '@fastann/vue/styles';
2. Add the stage overlay
Place <AnnotationStage /> once, typically in App.vue. It renders the toolbar and floating dialogs:
<!-- App.vue -->
<script setup lang="ts">
import { AnnotationStage } from '@fastann/vue';
</script>
<template>
<AnnotationStage />
<RouterView />
</template>
3. Make cells annotatable
Use AnnotationCell and AnnotationKey components. The as prop sets the rendered tag (default: div):
<!-- SalesTable.vue -->
<script setup lang="ts">
import { AnnotationCell, AnnotationKey } from '@fastann/vue';
import type { Annotation } from '@fastann/vue';
const revenueAnn: Annotation = {
id: 'ann-col-revenue',
type: 'info',
source: 'Finance',
title: 'Total Revenue',
body: 'Gross revenue net of refunds.',
tag: 'KPI',
};
</script>
<template>
<table>
<thead>
<tr>
<AnnotationCell as="th" :annotation="revenueAnn" cell-key="col:revenue">
Revenue
</AnnotationCell>
<!-- Key-only: users can add notes at runtime -->
<AnnotationKey as="th" ann-key="col:product">Product</AnnotationKey>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>{{ "{{" }} row.product {{ "}}" }}</td>
<AnnotationCell
as="td"
:annotation="rowAnn(row)"
:cell-key="`${row.id}:revenue`">
{{ "{{" }} row.revenue.toLocaleString() {{ "}}" }}
</AnnotationCell>
</tr>
</tbody>
</table>
</template>
4. Access the store via composables
<script setup lang="ts">
import { useAnnotation, useCustomAnnotations } from '@fastann/vue';
const store = useAnnotation();
const custom = useCustomAnnotations();
// Reactive refs
// store.mode.value, store.addMode.value, store.dialogs.value
function toggleAdd() { store.toggleAddMode(); }
function closeAll() { store.closeAll(); }
</script>
Svelte Guide
Download ExampleSvelte 4/5 Native Svelte components with reactive stores and context-based dependency injection.
1. Configuration
import { setAnnotationContext } from '@fastann/svelte';
// In your root +layout.svelte
setAnnotationContext({
apiUrl: 'https://api.yoursite.com/api',
licenseKey: 'YOUR_PRO_LICENSE_KEY'
});
2. Usage
<script>
import { AnnotationCell, AnnotationKey } from '@fastann/svelte';
let rows = [...];
</script>
<table>
<thead>
<tr>
<AnnotationCell as="th" cellKey="col:revenue">Revenue</AnnotationCell>
<AnnotationKey as="th" annKey="col:product">Product</AnnotationKey>
</tr>
</thead>
<tbody>
{#each rows as row}
<tr>
<td>{row.product}</td>
<AnnotationCell as="td" cellKey="{row.id}:revenue">
{row.revenue}
</AnnotationCell>
</tr>
{/each}
</tbody>
</table>
jQuery / Plain HTML Guide
Download ExamplejQuery 3+ UMD bundle — works in any page with a <script> tag, no build step required. Also available as an ESM import for bundler-based projects.
Option A — CDN / script tags (no build step)
<!-- 1. jQuery (skip if already loaded) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- 2. FastAnn core (exposes window.AnnCore) -->
<script src="node_modules/@fastann/core/dist/index.umd.cjs"></script>
<!-- 3. FastAnn jQuery adapter (exposes window.AnnJQuery) -->
<script src="node_modules/@fastann/jquery/dist/annotation.umd.js"></script>
<!-- 4. Styles -->
<link rel="stylesheet" href="node_modules/@fastann/jquery/dist/annotation.css">
Option B — ESM / npm
import { init } from '@fastann/jquery';
import '@fastann/jquery/styles';
Initialize
Call init($) once after the DOM is ready. It mounts the toolbar and dialog overlay automatically:
// ESM
import { init } from '@fastann/jquery';
const ann = init($);
// UMD / CDN
const ann = AnnJQuery.init(window.$);
Make elements annotatable
Use ann.cell() on any DOM element, or the jQuery plugin $.fn.annotationCell:
const salesAnn = {
id: 'ann-col-sales',
type: 'info',
source: 'Finance',
title: 'Sales (USD)',
body: 'Gross sales net of refunds.',
tag: 'KPI',
};
// Plain JS API
ann.cell(document.getElementById('th-sales'), salesAnn, 'col:sales');
// jQuery plugin
$('#th-sales').annotationCell(salesAnn, 'col:sales');
// Key-only — no annotation object yet, toolbar add-mode lets users create one
document.querySelectorAll('[data-ck]').forEach(el => {
ann.cell(el, { id: el.dataset.ck, type: 'comment', source: 'User',
title: el.dataset.ck, body: '', tag: '' });
});
Full HTML example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="node_modules/@fastann/jquery/dist/annotation.css">
</head>
<body>
<table>
<thead>
<tr>
<th id="th-product" data-ck="th:product">Product</th>
<th id="th-sales" data-ck="th:sales" >Sales</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget A</td>
<td id="r1-sales" data-ck="r1:sales">1,200</td>
</tr>
</tbody>
</table>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@fastann/core/dist/index.umd.cjs"></script>
<script src="node_modules/@fastann/jquery/dist/annotation.umd.js"></script>
<script>
const ann = AnnJQuery.init($);
$('#th-sales').annotationCell({
id: 'ann-th-sales', type: 'info', source: 'Finance',
title: 'Sales (USD)', body: 'Gross sales, net of refunds.', tag: 'KPI',
}, 'th:sales');
ann.cell(document.getElementById('r1-sales'), {
id: 'ann-r1-sales', type: 'comment', source: 'System',
title: 'Row 1 — Sales', body: '', tag: '',
}, 'r1:sales');
</script>
</body>
</html>
Add-mode (create annotations at runtime)
Click the + Add annotation button in the toolbar to enter add-mode. Then click any data-ck element — a prompt dialog collects the title, body, and tag.
To disable add-mode programmatically:
import { annotationStore } from '@fastann/core';
annotationStore.stopAddMode();
.NET Core 8+ Guide
Download ExampleComprehensive middleware for annotation sync, RSA license verification, and entity-framework persistence.
// Program.cs
builder.Services.AddFastAnn(options => {
options.UseSqlServer(connectionString);
options.PublicKeyPem = publicKey;
});
app.UseFastAnn();
NodeJS / Express Guide
Download ExampleFastAnn provider for Node.js environments. Supports MongoDB and PostgreSQL via Prisma.
import { FastAnnServer } from '@fastann/node-server';
const fastann = new FastAnnServer({
db: process.env.DATABASE_URL,
secret: process.env.ANN_SECRET
});
app.use('/api/ann', fastann.middleware());
License Key Format
A Pro license key is structured as:
Base64(UTF8(payloadJson)) . Base64(RSA_PKCS1_SHA256_signature)
Where the payload JSON is:
{ "dom": "yoursite.com", "plan": "Pro", "uid": "a1b2c3d4" }
Frontend Verification
Verify licenses entirely offline — no round-trip to your server:
export async function verifyLicense(licenseKey: string): Promise<boolean> {
const [payloadB64, sigB64] = licenseKey.split('.');
if (!payloadB64 || !sigB64) return false;
const payloadBytes = Uint8Array.from(atob(payloadB64), c => c.charCodeAt(0));
const sigBytes = Uint8Array.from(atob(sigB64), c => c.charCodeAt(0));
const cryptoKey = await crypto.subtle.importKey(
'spki', pemToArrayBuffer(PUBLIC_KEY_PEM),
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false, ['verify'],
);
const valid = await crypto.subtle.verify(
'RSASSA-PKCS1-v1_5', cryptoKey, sigBytes, payloadBytes,
);
if (!valid) return false;
const payload = JSON.parse(new TextDecoder().decode(payloadBytes));
return payload.dom === window.location.hostname.toLowerCase();
}
License Integration
Once you receive your Pro license key from the purchase confirmation email, register it in your project's entry point as shown below. Replace YOUR_PRO_LICENSE_KEY with the actual key string.
import { ApplicationConfig } from '@angular/core';
import { ANN_LICENSE_KEY, ANN_DOMAIN } from '@fastann/angular';
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
{ provide: ANN_LICENSE_KEY,
useValue: 'YOUR_PRO_LICENSE_KEY' },
// Must match the domain encoded in your license payload
{ provide: ANN_DOMAIN,
useValue: 'yourapp.com' },
],
};
import { AnnProvider } from '@fastann/react';
ReactDOM.createRoot(document.getElementById('root')!)
.render(
<AnnProvider
licenseKey="YOUR_PRO_LICENSE_KEY"
>
<App />
</AnnProvider>
);
import { createApp } from 'vue';
import { FastAnn } from '@fastann/vue';
import App from './App.vue';
const app = createApp(App);
app.use(FastAnn, {
licenseKey: 'YOUR_PRO_LICENSE_KEY',
});
app.mount('#app');
import { initFastAnn } from '@fastann/jquery';
// or via CDN: <script src="fastann.jquery.min.js"></script>
initFastAnn({
licenseKey: 'YOUR_PRO_LICENSE_KEY',
});
The license key is validated offline against the public RSA key embedded in the library — no network call is made at runtime. If the key is invalid or the domain does not match, FastAnn will run in Free mode automatically.