diff --git a/apps/docs/scripts/validate-code-imports.ts b/apps/docs/scripts/validate-code-imports.ts
index 0e8855199a..6c77be587e 100644
--- a/apps/docs/scripts/validate-code-imports.ts
+++ b/apps/docs/scripts/validate-code-imports.ts
@@ -29,6 +29,7 @@ const EXACT_SUPERDOC_IMPORTS = new Set([
'@superdoc-dev/react/style.css',
'@superdoc-dev/template-builder',
'@superdoc-dev/template-builder/defaults',
+ '@superdoc-dev/template-builder/field-types.css',
'@superdoc-dev/superdoc-yjs-collaboration',
]);
diff --git a/apps/docs/solutions/template-builder/api-reference.mdx b/apps/docs/solutions/template-builder/api-reference.mdx
index 525a685489..98f887be44 100644
--- a/apps/docs/solutions/template-builder/api-reference.mdx
+++ b/apps/docs/solutions/template-builder/api-reference.mdx
@@ -37,7 +37,7 @@ None - the component works with zero configuration.
Pre-existing fields in the document (auto-discovered if not provided)
- Enable users to create new fields on the fly
+ Show a "Create New Field" option in the menu. The create form lets users pick inline/block mode and owner/signer field type.
@@ -46,7 +46,7 @@ None - the component works with zero configuration.
Field insertion menu configuration
-
+
Custom menu component
@@ -59,18 +59,36 @@ None - the component works with zero configuration.
Field list sidebar configuration
-
+
Custom list component
-
- Sidebar position
+
+ Sidebar position. Omit to hide the sidebar entirely.
-
- Document editing toolbar. Can be: - `boolean` - show/hide default toolbar -
- `string` - space-separated tool names - `object` - full toolbar configuration
+
+ Document editing toolbar.
+ - `true` — render a default toolbar container
+ - `string` — CSS selector of an existing element to mount the toolbar into
+ - `object` — full toolbar configuration (see ToolbarConfig)
+
+
+
+ Content Security Policy nonce for dynamically injected styles
+
+
+
+ CSS class name for the root container
+
+
+
+ Inline styles for the root container
+
+
+
+ Height of the document editor area
@@ -122,7 +140,7 @@ None - the component works with zero configuration.
path="onFieldCreate"
type="(field: FieldDefinition) => void | Promise"
>
- Called when user creates a new field (requires `fields.allowCreate = true`)
+ Called when user creates a new field (requires `fields.allowCreate = true`). Return a modified `FieldDefinition` to override the field before insertion, or `void` to use the field as-is.
@@ -149,12 +167,13 @@ Available fields that users can insert:
```typescript
interface FieldDefinition {
- id: string; // Unique identifier
- label: string; // Display name
- defaultValue?: string; // Default value for new instances
- metadata?: Record; // Custom metadata
- mode?: "inline" | "block"; // Field insertion mode (default: "inline")
- group?: string; // Category/group name
+ id: string; // Unique identifier
+ label: string; // Display name
+ defaultValue?: string; // Default value for new instances
+ metadata?: Record; // Custom metadata stored in the SDT tag
+ mode?: "inline" | "block"; // Insertion mode (default: "inline")
+ group?: string; // Group ID for linked fields
+ fieldType?: string; // Field type, e.g. "owner" or "signer" (default: "owner")
}
```
@@ -164,12 +183,13 @@ Fields that exist in the template document:
```typescript
interface TemplateField {
- id: string | number; // Unique instance ID
- alias: string; // Field name/label
- tag?: string; // JSON metadata string
- position?: number; // Position in document
+ id: string | number; // Unique instance ID
+ alias: string; // Field name/label
+ tag?: string; // JSON metadata string
+ position?: number; // Position in document
mode?: "inline" | "block"; // Rendering mode
- group?: string; // Group ID for related fields
+ group?: string; // Group ID for linked fields
+ fieldType?: string; // Field type, e.g. "owner" or "signer"
}
```
@@ -179,13 +199,9 @@ Information about trigger detection:
```typescript
interface TriggerEvent {
- query: string; // Text after trigger pattern
- position: {
- // Cursor position
- top: number;
- left: number;
- };
- mode: "inline" | "block"; // Context mode
+ position: { from: number; to: number }; // Document position of the trigger
+ bounds?: DOMRect; // Viewport coordinates for menu positioning
+ cleanup: () => void; // Removes the trigger text from the document
}
```
@@ -196,36 +212,42 @@ Data provided when a template is exported:
```typescript
interface ExportEvent {
fields: TemplateField[]; // All fields in the template
- blob?: Blob; // Document blob (when triggerDownload: false)
- fileName: string; // Export filename
+ blob?: Blob; // Document blob (when triggerDownload: false)
+ fileName: string; // Export filename
}
```
-### MenuProps
+### FieldMenuProps
Props passed to custom menu components:
```typescript
-interface MenuProps {
- fields: FieldDefinition[]; // Available fields
- onInsert: (field: FieldDefinition) => void; // Insert handler
- onClose: () => void; // Close menu handler
- position: { top: number; left: number }; // Menu position
- query: string; // Current search query
- mode: "inline" | "block"; // Insertion mode
+interface FieldMenuProps {
+ isVisible: boolean; // Whether the menu should be shown
+ position?: DOMRect; // Viewport coordinates for positioning
+ availableFields: FieldDefinition[]; // All available fields
+ filteredFields?: FieldDefinition[]; // Fields filtered by the typed query
+ filterQuery?: string; // Text typed after the trigger pattern
+ allowCreate?: boolean; // Whether "Create New Field" is enabled
+ existingFields?: TemplateField[]; // Fields already in the document
+ onSelect: (field: FieldDefinition) => void; // Insert a new field
+ onSelectExisting?: (field: TemplateField) => void; // Insert a linked copy
+ onClose: () => void; // Close the menu
+ onCreateField?: (field: FieldDefinition) => void | Promise;
}
```
-### ListProps
+### FieldListProps
Props passed to custom list components:
```typescript
-interface ListProps {
- fields: TemplateField[]; // Fields in template
- selectedField: TemplateField | null; // Currently selected field
- onFieldSelect: (field: TemplateField) => void; // Selection handler
- onFieldDelete: (fieldId: string | number) => void; // Delete handler
+interface FieldListProps {
+ fields: TemplateField[]; // Fields in the template
+ onSelect: (field: TemplateField) => void; // Select/navigate to a field
+ onDelete: (fieldId: string | number) => void; // Delete a field
+ onUpdate?: (field: TemplateField) => void; // Update a field
+ selectedFieldId?: string | number; // Currently selected field ID
}
```
@@ -235,8 +257,8 @@ Configuration for template export:
```typescript
interface ExportConfig {
- fileName?: string; // Download filename
- triggerDownload?: boolean; // Auto-download file
+ fileName?: string; // Download filename (default: "document")
+ triggerDownload?: boolean; // Auto-download file (default: true)
}
```
@@ -250,12 +272,12 @@ const builderRef = useRef(null);
### insertField()
-Insert a field at the current cursor position:
+Insert an inline field at the current cursor position:
```typescript
builderRef.current?.insertField({
alias: "customer_name",
- mode: "inline",
+ fieldType: "owner",
});
```
@@ -267,8 +289,8 @@ Insert a block-level field:
```typescript
builderRef.current?.insertBlockField({
- alias: "terms_section",
- mode: "block",
+ alias: "signature",
+ fieldType: "signer",
});
```
@@ -281,7 +303,6 @@ Update an existing field:
```typescript
builderRef.current?.updateField("field-id", {
alias: "new_name",
- tag: JSON.stringify({ groupId: "123" }),
});
```
@@ -295,7 +316,7 @@ Remove a field from the template:
builderRef.current?.deleteField("field-id");
```
-Returns `boolean` - true if deleted successfully.
+Returns `boolean` - true if deleted successfully. If the deleted field was the last in a group with two members, the remaining field's group tag is automatically removed.
### selectField()
@@ -337,7 +358,7 @@ Export the template as a .docx file:
```typescript
// Trigger download
await builderRef.current?.exportTemplate({
- fileName: "my-template.docx",
+ fileName: "my-template",
triggerDownload: true,
});
@@ -345,9 +366,6 @@ await builderRef.current?.exportTemplate({
const blob = await builderRef.current?.exportTemplate({
triggerDownload: false,
});
-
-// Use blob for API upload or database storage
-await uploadToServer(blob);
```
Returns `Promise` depending on `triggerDownload` setting.
@@ -359,33 +377,28 @@ Access the underlying SuperDoc editor instance:
```typescript
const superdoc = builderRef.current?.getSuperDoc();
-// Use SuperDoc API directly
-if (superdoc) {
- const editor = superdoc.getEditor();
+if (superdoc?.activeEditor) {
// Full access to SuperDoc/SuperEditor APIs
+ superdoc.activeEditor.commands.search("hello");
}
```
Returns `SuperDoc | null`.
-## Default components
+## Field type styling
-The package exports default UI components you can use as a starting point:
+Import the optional CSS to color-code fields by type in the editor:
-```typescript
-import {
- DefaultFieldMenu,
- DefaultFieldList,
-} from "@superdoc-dev/template-builder/defaults";
-
-// Use as-is or extend
-function MyCustomMenu(props: MenuProps) {
- return (
-
-
-
-
- );
+```jsx
+import "@superdoc-dev/template-builder/field-types.css";
+```
+
+Override colors with CSS variables:
+
+```css
+:root {
+ --superdoc-field-owner-color: #629be7;
+ --superdoc-field-signer-color: #d97706;
}
```
@@ -393,22 +406,19 @@ function MyCustomMenu(props: MenuProps) {
Target these classes for custom styling:
-| Class | Element |
-| ------------------------------- | ------------------------ |
-| `.superdoc-template-builder` | Root container |
-| `.superdoc-field-menu` | Field insertion popup |
-| `.superdoc-field-menu-item` | Individual menu item |
-| `.superdoc-field-list` | Sidebar container |
-| `.superdoc-field-list-item` | Individual field in list |
-| `.superdoc-field-list-group` | Grouped fields container |
-| `.superdoc-field-tag` | Field in document |
-| `.superdoc-field-tag--selected` | Selected field |
-| `.superdoc-field-tag--inline` | Inline field mode |
-| `.superdoc-field-tag--block` | Block field mode |
+| Class | Element |
+| ---------------------------------------- | ------------------------ |
+| `.superdoc-template-builder` | Root container |
+| `.superdoc-template-builder-sidebar` | Sidebar wrapper |
+| `.superdoc-template-builder-document` | Document area wrapper |
+| `.superdoc-template-builder-editor` | Editor container |
+| `.superdoc-template-builder-toolbar` | Default toolbar container|
+| `.superdoc-field-menu` | Field insertion popup |
+| `.superdoc-field-list` | Sidebar field list |
## Import types
-All TypeScript types are exported for use in your code:
+All TypeScript types are exported:
```typescript
import type {
@@ -418,23 +428,13 @@ import type {
TemplateField,
TriggerEvent,
ExportEvent,
- MenuProps,
- ListProps,
ExportConfig,
+ FieldMenuProps,
+ FieldListProps,
+ ToolbarConfig,
+ DocumentConfig,
+ FieldsConfig,
+ MenuConfig,
+ ListConfig,
} from "@superdoc-dev/template-builder";
```
-
-## Keyboard shortcuts
-
-Built-in keyboard navigation:
-
-| Shortcut | Action |
-| -------------- | ------------------------------- |
-| `Tab` | Jump to next field |
-| `Shift + Tab` | Jump to previous field |
-| `{{` (default) | Open field menu |
-| `Esc` | Close field menu |
-| `Enter` | Insert selected field from menu |
-| `↑` / `↓` | Navigate menu items |
-
-The trigger pattern is configurable via the `menu.trigger` prop.
diff --git a/apps/docs/solutions/template-builder/configuration.mdx b/apps/docs/solutions/template-builder/configuration.mdx
index 1ffd3cc7bf..26f065f11c 100644
--- a/apps/docs/solutions/template-builder/configuration.mdx
+++ b/apps/docs/solutions/template-builder/configuration.mdx
@@ -18,8 +18,8 @@ Control which document is loaded and how users interact with it:
/>
```
-**Editing mode** - Users can edit document content and insert fields
-**Viewing mode** - Read-only document display, fields can still be inserted
+**Editing mode** - Users can edit document content and insert fields.
+**Viewing mode** - Read-only document display, fields can still be inserted.
## Field system
@@ -34,21 +34,58 @@ Define which fields users can insert:
{
id: "1",
label: "Customer Name",
- defaultValue: "John Doe", // Optional
- metadata: { type: "text" }, // Optional
- group: "customer", // Optional category
+ defaultValue: "John Doe",
+ metadata: { type: "text" },
},
{
id: "2",
label: "Signature",
- mode: "block", // Force block-level insertion
+ mode: "block",
+ fieldType: "signer",
},
],
- allowCreate: true, // Let users create new fields on the fly
+ allowCreate: true,
+ }}
+/>
+```
+
+### Field types
+
+Tag fields with a `fieldType` to distinguish roles:
+
+```tsx
+
```
+Import the optional CSS to color-code fields in the editor:
+
+```jsx
+import "@superdoc-dev/template-builder/field-types.css";
+```
+
+Customize colors with CSS variables:
+
+```css
+:root {
+ --superdoc-field-owner-color: #629be7;
+ --superdoc-field-signer-color: #d97706;
+}
+```
+
+
+ Without `field-types.css`, structured content fields use the default Word-like appearance: borders are transparent and only visible on selection. Importing this stylesheet makes field borders always visible and color-coded by field type, which helps template authors quickly identify fields in the document.
+
+
+The `fieldType` value flows through all callbacks (`onFieldInsert`, `onFieldsChange`, `onExport`, etc.) and is stored in the SDT tag metadata.
+
### Field creation
Allow users to create new fields while building templates:
@@ -60,7 +97,9 @@ Allow users to create new fields while building templates:
allowCreate: true,
}}
onFieldCreate={async (field) => {
- // Validate or save to database
+ // field.id starts with "custom_"
+ // field.fieldType is "owner" or "signer" (user-selected)
+ // field.mode is "inline" or "block" (user-selected)
const savedField = await api.createField(field);
// Return updated field or void
@@ -69,7 +108,13 @@ Allow users to create new fields while building templates:
/>
```
-When enabled, the field menu shows a "Create new field" option at the bottom.
+When enabled, the field menu shows a "Create New Field" option with inputs for name, mode (inline/block), and field type (owner/signer).
+
+### Linked fields
+
+When a user selects an existing field from the "Existing Fields" section in the menu, a linked copy is inserted. Both instances share a group ID and stay in sync.
+
+The menu automatically groups existing fields and shows the count. When the last field in a group is deleted, the remaining field's group tag is removed.
## Menu customization
@@ -90,23 +135,42 @@ Change what opens the field insertion menu:
Replace the default field menu entirely:
```tsx
-import { DefaultFieldMenu } from "@superdoc-dev/template-builder/defaults";
+function CustomMenu({
+ isVisible,
+ position,
+ filteredFields,
+ filterQuery,
+ existingFields,
+ allowCreate,
+ onSelect,
+ onSelectExisting,
+ onClose,
+}) {
+ if (!isVisible) return null;
-function CustomMenu({ fields, onInsert, onClose, position, query }) {
return (
-