Skip to content

Commit d2a012a

Browse files
authored
feat(template-builder): Cake Equity fixes — lockMode, fieldColors, reactive mode (SD-1866, SD-883, SD-2406) (#2727)
* feat(template-builder): wire lockMode through field insertion paths SuperDoc core already enforces SDT lock modes (SD-1616), but Template Builder never passed lockMode to the editor commands. This adds: - LockMode type and lockMode prop on FieldDefinition / TemplateField - defaultLockMode prop on SuperDocTemplateBuilderProps - lockMode threading through insertFieldInternal, handleSelectExisting, and handleMenuSelect - lockMode read-back in getTemplateFieldsFromEditor Closes SD-1866 * fix(template-builder): include lockMode in field equality check areTemplateFieldsEqual omitted lockMode, so lock-only changes were silently dropped by discoverFields. Also tightens the lockMode spread guard from truthiness to nullish check for consistency with the ?? resolution above. * docs(template-builder): add lockMode and defaultLockMode Documents field locking in configuration guide and API reference. * feat(template-builder): add lock mode selector to field creation form Adds a dropdown to the default FieldMenu create form so users can set a lock mode when creating new fields. Options: No lock, Unlocked, Container locked, Content locked, Fully locked. * fix(template-builder): respect lock mode in delete and show lock badge - deleteField no longer force-removes locked fields from state when the editor command is rejected by the lock plugin - FieldList sidebar shows a lock icon for fields with active lock modes * refactor(template-builder): simplify lock UI to a checkbox - Revert lock guard in deleteField — template authors can always delete fields regardless of lock mode - Replace 5-option lock dropdown with a simple "Locked" checkbox that maps to contentLocked - Full LockMode type still available for programmatic use * docs(template-builder): simplify lock mode documentation Focus on contentLocked as the default, remove the 4-mode table, clarify that locking only affects end users not template authors. * feat(template-builder): add fieldColors prop for field type styling Consumers can now pass a `fieldColors` prop to color-code fields by type in both the document and sidebar — no CSS import needed. - Generates scoped CSS from fieldColors and injects into <head> - Dynamic sidebar/menu badge colors via updated getFieldTypeStyle - Removes need for manual field-types.css import (still available) - Updates demo to use fieldColors instead of CSS variable overrides - Documents fieldColors in configuration guide and API reference Closes SD-883 * fix(template-builder): address review findings for fieldColors - Fix block label selector: use .superdoc-structured-content__label (not __block__label) to match actual DOM - Fix non-hex color support: use color-mix() instead of appending '1a' - Fix partial fieldColors: only generate default rule when owner key exists, preventing unrelated fields from being recolored - Simplify style injection: compute CSS in useMemo directly, remove JSON stringify/parse round-trip - Extract buildColorRules helper to deduplicate CSS template - Add 11 unit tests for getFieldTypeStyle and generateFieldColorCSS * fix(template-builder): pass fieldColors to right-side sidebar * feat(template-builder): reactive mode changes and refresh method - Remove document.mode from init effect deps so mode changes don't destroy and recreate the editor (no more scroll jump / content flash) - Add separate useEffect that calls setDocumentMode() imperatively, following the same pattern as the React wrapper - Add refresh() method to the imperative handle for re-discovering fields after async data delivery - Document refresh() in API reference Closes SD-2406 * fix(template-builder): queue mode changes during init If document.mode changes while SuperDoc is still loading, the change is queued and applied once handleReady fires. Follows the same pendingModeRef pattern as the React wrapper. * fix(template-builder): address review findings for mode change logic - Remove redundant prevModeRef — useEffect dep array handles this - Clear pendingModeRef in init cleanup to prevent stale mode on re-init - Clear pendingModeRef when applying mode directly - Extract applyDocumentMode helper to centralize the as-any cast * docs(template-builder): document reactive mode switching * fix(template-builder): emit unlocked when checkbox is unchecked When the "Locked" checkbox is unchecked, emit lockMode: 'unlocked' instead of omitting it, so it correctly overrides defaultLockMode.
1 parent f98650f commit d2a012a

10 files changed

Lines changed: 378 additions & 66 deletions

File tree

apps/docs/solutions/template-builder/api-reference.mdx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ None - the component works with zero configuration.
2121
Document to load. Can be a URL, File object, or Blob
2222
</ParamField>
2323
<ParamField path="mode" type="'editing' | 'viewing'" default="'editing'">
24-
Document interaction mode
24+
Document interaction mode. Reactive — changing this at runtime switches modes without recreating the editor.
2525
</ParamField>
2626
</Expandable>
2727
</ParamField>
@@ -75,6 +75,14 @@ None - the component works with zero configuration.
7575
- `object` — full toolbar configuration (see ToolbarConfig)
7676
</ParamField>
7777

78+
<ParamField path="defaultLockMode" type="'unlocked' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked'">
79+
Lock mode for all inserted fields. Per-field `lockMode` overrides this. See [lock modes](/extensions/structured-content#lock-modes).
80+
</ParamField>
81+
82+
<ParamField path="fieldColors" type="Record<string, string>">
83+
Colors for field types in the document and sidebar. Keys are `fieldType` values, values are CSS colors (e.g. `{ owner: '#629be7', signer: '#d97706' }`). Generates scoped CSS automatically — no stylesheet import needed.
84+
</ParamField>
85+
7886
<ParamField path="cspNonce" type="string">
7987
Content Security Policy nonce for dynamically injected styles
8088
</ParamField>
@@ -174,6 +182,7 @@ interface FieldDefinition {
174182
mode?: "inline" | "block"; // Insertion mode (default: "inline")
175183
group?: string; // Group ID for linked fields
176184
fieldType?: string; // Field type, e.g. "owner" or "signer" (default: "owner")
185+
lockMode?: LockMode; // Lock mode for this field (overrides defaultLockMode)
177186
}
178187
```
179188

@@ -190,6 +199,7 @@ interface TemplateField {
190199
mode?: "inline" | "block"; // Rendering mode
191200
group?: string; // Group ID for linked fields
192201
fieldType?: string; // Field type, e.g. "owner" or "signer"
202+
lockMode?: LockMode; // Current lock mode of this field
193203
}
194204
```
195205

@@ -351,6 +361,14 @@ const fields = builderRef.current?.getFields();
351361
// Returns: TemplateField[]
352362
```
353363

364+
### refresh()
365+
366+
Re-discover fields from the editor and trigger `onFieldsChange`. Use this when field data arrives asynchronously or after external changes to the document:
367+
368+
```typescript
369+
builderRef.current?.refresh();
370+
```
371+
354372
### exportTemplate()
355373

356374
Export the template as a .docx file:
@@ -387,14 +405,26 @@ Returns `SuperDoc | null`.
387405

388406
## Field type styling
389407

390-
Import the optional CSS to color-code fields by type in the editor:
408+
Use the `fieldColors` prop to color-code fields by type:
409+
410+
```tsx
411+
<SuperDocTemplateBuilder
412+
fieldColors={{
413+
owner: "#629be7",
414+
signer: "#d97706",
415+
date: "#059669",
416+
}}
417+
/>
418+
```
419+
420+
This generates scoped CSS automatically. Each color controls the field border and label in the document. Sidebar badges match.
421+
422+
For manual control, you can still import the standalone stylesheet and use CSS variables:
391423

392424
```jsx
393425
import "@superdoc-dev/template-builder/field-types.css";
394426
```
395427

396-
Override colors with CSS variables:
397-
398428
```css
399429
:root {
400430
--superdoc-field-owner-color: #629be7;
@@ -426,6 +456,7 @@ import type {
426456
SuperDocTemplateBuilderHandle,
427457
FieldDefinition,
428458
TemplateField,
459+
LockMode,
429460
TriggerEvent,
430461
ExportEvent,
431462
ExportConfig,

apps/docs/solutions/template-builder/configuration.mdx

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ Control which document is loaded and how users interact with it:
2121
**Editing mode** - Users can edit document content and insert fields.
2222
**Viewing mode** - Read-only document display, fields can still be inserted.
2323

24+
Changing `mode` at runtime switches between editing and viewing without recreating the editor — no scroll jump or content flash:
25+
26+
```tsx
27+
<SuperDocTemplateBuilder
28+
document={{ source: file, mode: isEditing ? "editing" : "viewing" }}
29+
/>
30+
```
31+
2432
## Field system
2533

2634
### Available fields
@@ -65,24 +73,30 @@ Tag fields with a `fieldType` to distinguish roles:
6573
/>
6674
```
6775

68-
Import the optional CSS to color-code fields in the editor:
76+
Color-code fields in the document and sidebar with `fieldColors`:
6977

70-
```jsx
71-
import "@superdoc-dev/template-builder/field-types.css";
78+
```tsx
79+
<SuperDocTemplateBuilder
80+
fields={{
81+
available: [
82+
{ id: "1", label: "Company Name", fieldType: "owner" },
83+
{ id: "2", label: "Signer Name", fieldType: "signer" },
84+
{ id: "3", label: "Effective Date", fieldType: "date" },
85+
],
86+
}}
87+
fieldColors={{
88+
owner: "#629be7",
89+
signer: "#d97706",
90+
date: "#059669",
91+
}}
92+
/>
7293
```
7394

74-
Customize colors with CSS variables:
95+
Each color controls the field's border and label in the document. The sidebar badges match automatically. Custom field types beyond `owner` and `signer` work the same way — just add them to `fieldColors`.
7596

76-
```css
77-
:root {
78-
--superdoc-field-owner-color: #629be7;
79-
--superdoc-field-signer-color: #d97706;
80-
}
81-
```
82-
83-
<Important>
84-
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.
85-
</Important>
97+
<Tip>
98+
You can also import `@superdoc-dev/template-builder/field-types.css` directly and use CSS variables instead. The `fieldColors` prop is the recommended approach — no extra imports needed.
99+
</Tip>
86100

87101
The `fieldType` value flows through all callbacks (`onFieldInsert`, `onFieldsChange`, `onExport`, etc.) and is stored in the SDT tag metadata.
88102

@@ -110,6 +124,26 @@ Allow users to create new fields while building templates:
110124

111125
When enabled, the field menu shows a "Create New Field" option with inputs for name, mode (inline/block), and field type (owner/signer).
112126

127+
### Field locking
128+
129+
Make fields read-only so end users can't edit their content:
130+
131+
```tsx
132+
<SuperDocTemplateBuilder
133+
defaultLockMode="contentLocked"
134+
fields={{
135+
available: [
136+
{ id: "1", label: "Customer Name" },
137+
{ id: "2", label: "Notes", lockMode: "unlocked" }, // this one stays editable
138+
],
139+
}}
140+
/>
141+
```
142+
143+
Per-field `lockMode` overrides `defaultLockMode`. The default field creation form includes a "Locked" checkbox that sets `contentLocked`.
144+
145+
Template authors can always delete and manage fields regardless of lock mode — locking only affects end users interacting with the document. For advanced use cases, see [lock modes](/extensions/structured-content#lock-modes).
146+
113147
### Linked fields
114148

115149
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.

packages/template-builder/demo/src/App.css

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,6 @@ header {
255255
background: #000000;
256256
}
257257

258-
/* Field type color overrides — just set the CSS variables */
259-
:root {
260-
--superdoc-field-owner-color: #1bb754;
261-
--superdoc-field-signer-color: #d81313;
262-
}
263-
264258
/* Responsive */
265259
@media (max-width: 768px) {
266260
.header-content {

packages/template-builder/demo/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
ExportEvent,
99
} from '@superdoc-dev/template-builder';
1010
import 'superdoc/style.css';
11-
import '@superdoc-dev/template-builder/field-types.css';
1211
import './App.css';
1312

1413
const availableFields: FieldDefinition[] = [
@@ -253,6 +252,10 @@ export function App() {
253252
fields={fieldsConfig}
254253
list={listConfig}
255254
toolbar={true}
255+
fieldColors={{
256+
owner: '#629be7',
257+
signer: '#d97706',
258+
}}
256259
telemetry={{
257260
enabled: true,
258261
metadata: {

packages/template-builder/src/defaults/FieldList.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const FieldItem: FC<{
1313
onDelete: (id: string | number) => void;
1414
isSelected: boolean;
1515
isGrouped?: boolean;
16-
}> = ({ field, onSelect, onDelete, isSelected, isGrouped = false }) => {
16+
fieldColors?: Record<string, string>;
17+
}> = ({ field, onSelect, onDelete, isSelected, isGrouped = false, fieldColors }) => {
1718
return (
1819
<div
1920
onClick={() => onSelect(field)}
@@ -117,20 +118,35 @@ const FieldItem: FC<{
117118
fontSize: '9px',
118119
padding: '2px 5px',
119120
borderRadius: '3px',
120-
...getFieldTypeStyle(field.fieldType),
121+
...getFieldTypeStyle(field.fieldType, fieldColors),
121122
fontWeight: '500',
122123
}}
123124
>
124125
{field.fieldType}
125126
</span>
126127
)}
128+
{field.lockMode && field.lockMode !== 'unlocked' && (
129+
<span
130+
style={{
131+
fontSize: '9px',
132+
padding: '2px 5px',
133+
borderRadius: '3px',
134+
background: '#fef2f2',
135+
color: '#991b1b',
136+
fontWeight: '500',
137+
}}
138+
title={field.lockMode}
139+
>
140+
🔒
141+
</span>
142+
)}
127143
</div>
128144
</div>
129145
</div>
130146
);
131147
};
132148

133-
export const FieldList: FC<FieldListProps> = ({ fields, onSelect, onDelete, selectedFieldId }) => {
149+
export const FieldList: FC<FieldListProps> = ({ fields, onSelect, onDelete, selectedFieldId, fieldColors }) => {
134150
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
135151

136152
const { groupedFields, ungroupedFields } = useMemo(() => {
@@ -196,6 +212,7 @@ export const FieldList: FC<FieldListProps> = ({ fields, onSelect, onDelete, sele
196212
onSelect={onSelect}
197213
onDelete={onDelete}
198214
isSelected={selectedFieldId === field.id}
215+
fieldColors={fieldColors}
199216
/>
200217
))}
201218

@@ -258,6 +275,7 @@ export const FieldList: FC<FieldListProps> = ({ fields, onSelect, onDelete, sele
258275
onDelete={onDelete}
259276
isSelected={selectedFieldId === field.id}
260277
isGrouped
278+
fieldColors={fieldColors}
261279
/>
262280
))}
263281
</div>

packages/template-builder/src/defaults/FieldMenu.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
1515
onCreateField,
1616
existingFields = [],
1717
onSelectExisting,
18+
fieldColors,
1819
}) => {
1920
const [isCreating, setIsCreating] = useState(false);
2021
const [newFieldName, setNewFieldName] = useState('');
2122
const [fieldMode, setFieldMode] = useState<'inline' | 'block'>('inline');
2223
const [fieldType, setFieldType] = useState<string>('owner');
24+
const [fieldLocked, setFieldLocked] = useState(false);
2325
const [existingExpanded, setExistingExpanded] = useState(true);
2426
const [availableExpanded, setAvailableExpanded] = useState(true);
2527

@@ -29,6 +31,7 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
2931
setNewFieldName('');
3032
setFieldMode('inline');
3133
setFieldType('owner');
34+
setFieldLocked(false);
3235
}
3336
}, [isVisible]);
3437

@@ -69,6 +72,7 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
6972
label: trimmedName,
7073
mode: fieldMode,
7174
fieldType: fieldType,
75+
lockMode: fieldLocked ? ('contentLocked' as const) : ('unlocked' as const),
7276
};
7377

7478
try {
@@ -83,6 +87,7 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
8387
setNewFieldName('');
8488
setFieldMode('inline');
8589
setFieldType('owner');
90+
setFieldLocked(false);
8691
}
8792
};
8893

@@ -131,6 +136,7 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
131136
setIsCreating(false);
132137
setNewFieldName('');
133138
setFieldMode('inline');
139+
setFieldLocked(false);
134140
}
135141
}}
136142
autoFocus
@@ -227,6 +233,19 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
227233
Signer
228234
</label>
229235
</div>
236+
<div
237+
style={{
238+
marginTop: '8px',
239+
display: 'flex',
240+
gap: '12px',
241+
fontSize: '13px',
242+
}}
243+
>
244+
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer' }}>
245+
<input type='checkbox' checked={fieldLocked} onChange={(e) => setFieldLocked(e.target.checked)} />
246+
Locked
247+
</label>
248+
</div>
230249
<div
231250
style={{
232251
marginTop: '8px',
@@ -254,6 +273,7 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
254273
setNewFieldName('');
255274
setFieldMode('inline');
256275
setFieldType('owner');
276+
setFieldLocked(false);
257277
}}
258278
style={{
259279
padding: '4px 12px',
@@ -370,7 +390,7 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
370390
padding: '2px 6px',
371391
borderRadius: '3px',
372392
textTransform: 'capitalize',
373-
...getFieldTypeStyle(entry.fieldType),
393+
...getFieldTypeStyle(entry.fieldType, fieldColors),
374394
fontWeight: 500,
375395
}}
376396
>
@@ -482,7 +502,7 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
482502
padding: '2px 6px',
483503
borderRadius: '3px',
484504
textTransform: 'capitalize',
485-
...getFieldTypeStyle(field.fieldType),
505+
...getFieldTypeStyle(field.fieldType, fieldColors),
486506
fontWeight: 500,
487507
}}
488508
>

0 commit comments

Comments
 (0)