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 ( -
- { - /* filter logic */ - }} - /> - {fields.map((field) => ( - + ))} +
+ )} + +

Available fields

+ {filteredFields.map((field) => ( + ))} + + ); } @@ -114,7 +178,7 @@ function CustomMenu({ fields, onInsert, onClose, position, query }) { ; ``` -The component handles trigger detection and positioning, you just render the UI. +The component handles trigger detection, filtering, and positioning. You render the UI. ## List sidebar @@ -123,7 +187,7 @@ The component handles trigger detection and positioning, you just render the UI. ```tsx ``` @@ -135,29 +199,25 @@ Omit `list` prop entirely to hide the sidebar. Replace the default sidebar: ```tsx -function CustomFieldList({ - fields, - selectedField, - onFieldSelect, - onFieldDelete, -}) { +function CustomFieldList({ fields, onSelect, onDelete, selectedFieldId }) { return (