@beyondwork/docx-react-component 1.0.30 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +6 -0
  2. package/package.json +1 -1
  3. package/src/api/public-types.ts +16 -1
  4. package/src/api/session-state.ts +2 -0
  5. package/src/io/docx-session.ts +16 -3
  6. package/src/io/ooxml/parse-footnotes.ts +23 -33
  7. package/src/io/ooxml/parse-headers-footers.ts +20 -21
  8. package/src/io/ooxml/workflow-payload.ts +311 -8
  9. package/src/model/snapshot.ts +113 -1
  10. package/src/runtime/document-runtime.ts +207 -33
  11. package/src/runtime/surface-projection.ts +156 -7
  12. package/src/ui/WordReviewEditor.tsx +13 -5
  13. package/src/ui/editor-surface-controller.tsx +2 -0
  14. package/src/ui/headless/selection-tool-resolver.ts +4 -1
  15. package/src/ui/headless/selection-tool-types.ts +1 -2
  16. package/src/ui/workflow-surface-blocked-rails.ts +19 -1
  17. package/src/ui-tailwind/chrome/responsive-chrome.ts +46 -0
  18. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +4 -4
  19. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +5 -5
  20. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +3 -3
  21. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +4 -4
  22. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +14 -9
  23. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +4 -5
  24. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +5 -5
  25. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +6 -6
  26. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +9 -9
  27. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +173 -124
  28. package/src/ui-tailwind/editor-surface/pm-decorations.ts +88 -14
  29. package/src/ui-tailwind/editor-surface/pm-schema.ts +29 -0
  30. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -1
  31. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +3 -3
  32. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +20 -0
  33. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +26 -0
  34. package/src/ui-tailwind/review/tw-review-rail.tsx +9 -1
  35. package/src/ui-tailwind/theme/editor-theme.css +8 -0
  36. package/src/ui-tailwind/toolbar/toolbar-layout.ts +47 -0
  37. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +367 -22
  38. package/src/ui-tailwind/tw-review-workspace.tsx +131 -4
@@ -1,7 +1,6 @@
1
1
  import React from "react";
2
2
 
3
3
  import type {
4
- StyleCatalogSnapshot,
5
4
  TableOperationCapabilitySnapshot,
6
5
  TableStructureContextSnapshot,
7
6
  } from "../../api/public-types";
@@ -10,7 +9,7 @@ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-edi
10
9
  export interface TwTableContextToolbarProps {
11
10
  disabled: boolean;
12
11
  tableContext: TableStructureContextSnapshot | null;
13
- tableStyles: StyleCatalogSnapshot["tables"];
12
+ tableStyles: Array<{ styleId: string; displayName: string }>;
14
13
  onSetTableStyle?: (styleId: string) => void;
15
14
  onAddRowBefore?: () => void;
16
15
  onAddRowAfter?: () => void;
@@ -34,138 +33,188 @@ const CELL_COLORS = [
34
33
  ] as const;
35
34
 
36
35
  export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
36
+ const tableContext = props.tableContext;
37
+ const tableSizeLabel = tableContext ? `${tableContext.rowCount} x ${tableContext.columnCount}` : null;
38
+ const selectionLabel = tableContext
39
+ ? tableContext.selectedCellCount > 1
40
+ ? `${tableContext.selectedCellCount} cells`
41
+ : `R${tableContext.currentCell.rowIndex + 1} C${tableContext.currentCell.columnIndex + 1}`
42
+ : null;
43
+
37
44
  return (
38
45
  <div
39
46
  data-testid="table-context-toolbar"
40
- className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
47
+ className="flex max-w-[min(30rem,calc(100vw-1.5rem))] flex-wrap items-start gap-1.5 rounded-lg border border-border bg-canvas px-2.5 py-1.5 shadow-sm"
41
48
  >
42
- <span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
49
+ <span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
43
50
  Table
44
51
  </span>
52
+ {tableSizeLabel ? <ToolbarBadge>{tableSizeLabel}</ToolbarBadge> : null}
53
+ {selectionLabel ? <ToolbarBadge>{selectionLabel}</ToolbarBadge> : null}
54
+ {tableContext?.currentCell.isHeader ? <ToolbarBadge tone="accent">Header row</ToolbarBadge> : null}
55
+
56
+ <ToolbarSection label="Style">
57
+ <select
58
+ aria-label="Table style"
59
+ className="h-7 min-w-[9rem] rounded-md border border-border bg-canvas px-2 text-[11px] text-primary disabled:opacity-40"
60
+ disabled={
61
+ props.disabled ||
62
+ props.tableStyles.length === 0 ||
63
+ !props.onSetTableStyle ||
64
+ !tableContext?.operations.setTableStyle.enabled
65
+ }
66
+ onMouseDown={preserveEditorSelectionMouseDown}
67
+ onChange={(event) => props.onSetTableStyle?.(event.target.value)}
68
+ value={tableContext?.currentStyleId ?? ""}
69
+ title={tableContext?.operations.setTableStyle.reason}
70
+ >
71
+ <option value="" disabled>Table style</option>
72
+ {props.tableStyles.map((style) => (
73
+ <option key={style.styleId} value={style.styleId}>
74
+ {style.displayName}
75
+ </option>
76
+ ))}
77
+ </select>
78
+ </ToolbarSection>
79
+
80
+ <ToolbarSection label="Rows">
81
+ <ToolbarButton
82
+ ariaLabel="Add row above"
83
+ capability={tableContext?.operations.addRowBefore}
84
+ disabled={props.disabled}
85
+ onClick={props.onAddRowBefore}
86
+ >
87
+ Above
88
+ </ToolbarButton>
89
+ <ToolbarButton
90
+ ariaLabel="Add row below"
91
+ capability={tableContext?.operations.addRowAfter}
92
+ disabled={props.disabled}
93
+ onClick={props.onAddRowAfter}
94
+ >
95
+ Below
96
+ </ToolbarButton>
97
+ <ToolbarButton
98
+ ariaLabel="Delete row"
99
+ capability={tableContext?.operations.deleteRow}
100
+ disabled={props.disabled}
101
+ onClick={props.onDeleteRow}
102
+ >
103
+ Delete row
104
+ </ToolbarButton>
105
+ </ToolbarSection>
45
106
 
46
- {props.tableContext?.currentCell.isHeader ? (
47
- <span className="rounded bg-accent-soft px-2 py-1 text-[10px] font-medium text-accent">
48
- Header row
49
- </span>
50
- ) : null}
107
+ <ToolbarSection label="Columns">
108
+ <ToolbarButton
109
+ ariaLabel="Add column left"
110
+ capability={tableContext?.operations.addColumnBefore}
111
+ disabled={props.disabled}
112
+ onClick={props.onAddColumnBefore}
113
+ >
114
+ Left
115
+ </ToolbarButton>
116
+ <ToolbarButton
117
+ ariaLabel="Add column right"
118
+ capability={tableContext?.operations.addColumnAfter}
119
+ disabled={props.disabled}
120
+ onClick={props.onAddColumnAfter}
121
+ >
122
+ Right
123
+ </ToolbarButton>
124
+ <ToolbarButton
125
+ ariaLabel="Delete column"
126
+ capability={tableContext?.operations.deleteColumn}
127
+ disabled={props.disabled}
128
+ onClick={props.onDeleteColumn}
129
+ >
130
+ Delete column
131
+ </ToolbarButton>
132
+ </ToolbarSection>
51
133
 
52
- <select
53
- aria-label="Table style"
54
- className="h-8 rounded-md border border-border bg-canvas px-2 text-xs text-primary disabled:opacity-40"
55
- disabled={
56
- props.disabled ||
57
- props.tableStyles.length === 0 ||
58
- !props.onSetTableStyle ||
59
- !props.tableContext?.operations.setTableStyle.enabled
60
- }
61
- onMouseDown={preserveEditorSelectionMouseDown}
62
- onChange={(event) => props.onSetTableStyle?.(event.target.value)}
63
- value={props.tableContext?.currentStyleId ?? ""}
64
- title={props.tableContext?.operations.setTableStyle.reason}
65
- >
66
- <option value="" disabled>Table style</option>
67
- {props.tableStyles.map((style) => (
68
- <option key={style.styleId} value={style.styleId}>
69
- {style.displayName}
70
- </option>
71
- ))}
72
- </select>
134
+ <ToolbarSection label="Cells">
135
+ <ToolbarButton
136
+ ariaLabel="Merge cells"
137
+ capability={tableContext?.operations.mergeCells}
138
+ disabled={props.disabled}
139
+ onClick={props.onMergeCells}
140
+ >
141
+ Merge
142
+ </ToolbarButton>
143
+ <ToolbarButton
144
+ ariaLabel="Split cell"
145
+ capability={tableContext?.operations.splitCell}
146
+ disabled={props.disabled}
147
+ onClick={props.onSplitCell}
148
+ >
149
+ Split
150
+ </ToolbarButton>
151
+ </ToolbarSection>
73
152
 
74
- <ToolbarButton
75
- ariaLabel="Add row above"
76
- capability={props.tableContext?.operations.addRowBefore}
77
- disabled={props.disabled}
78
- onClick={props.onAddRowBefore}
79
- >
80
- Row above
81
- </ToolbarButton>
82
- <ToolbarButton
83
- ariaLabel="Add row below"
84
- capability={props.tableContext?.operations.addRowAfter}
85
- disabled={props.disabled}
86
- onClick={props.onAddRowAfter}
87
- >
88
- Row below
89
- </ToolbarButton>
90
- <ToolbarButton
91
- ariaLabel="Delete row"
92
- capability={props.tableContext?.operations.deleteRow}
93
- disabled={props.disabled}
94
- onClick={props.onDeleteRow}
95
- >
96
- Delete row
97
- </ToolbarButton>
98
- <ToolbarButton
99
- ariaLabel="Add column left"
100
- capability={props.tableContext?.operations.addColumnBefore}
101
- disabled={props.disabled}
102
- onClick={props.onAddColumnBefore}
103
- >
104
- Column left
105
- </ToolbarButton>
106
- <ToolbarButton
107
- ariaLabel="Add column right"
108
- capability={props.tableContext?.operations.addColumnAfter}
109
- disabled={props.disabled}
110
- onClick={props.onAddColumnAfter}
111
- >
112
- Column right
113
- </ToolbarButton>
114
- <ToolbarButton
115
- ariaLabel="Delete column"
116
- capability={props.tableContext?.operations.deleteColumn}
117
- disabled={props.disabled}
118
- onClick={props.onDeleteColumn}
119
- >
120
- Delete column
121
- </ToolbarButton>
122
- <ToolbarButton
123
- ariaLabel="Merge cells"
124
- capability={props.tableContext?.operations.mergeCells}
125
- disabled={props.disabled}
126
- onClick={props.onMergeCells}
127
- >
128
- Merge
129
- </ToolbarButton>
130
- <ToolbarButton
131
- ariaLabel="Split cell"
132
- capability={props.tableContext?.operations.splitCell}
133
- disabled={props.disabled}
134
- onClick={props.onSplitCell}
135
- >
136
- Split
137
- </ToolbarButton>
153
+ <ToolbarSection label="Fill">
154
+ <div className="flex items-center gap-1">
155
+ {CELL_COLORS.map((color) => (
156
+ <button
157
+ key={color}
158
+ type="button"
159
+ aria-label={`Set cell fill ${color}`}
160
+ disabled={
161
+ props.disabled ||
162
+ !props.onSetCellBackground ||
163
+ !tableContext?.operations.setCellBackground.enabled
164
+ }
165
+ onMouseDown={preserveEditorSelectionMouseDown}
166
+ onClick={() => props.onSetCellBackground?.(color)}
167
+ className="h-5 w-5 rounded border border-border disabled:opacity-40"
168
+ style={{ backgroundColor: color }}
169
+ title={tableContext?.operations.setCellBackground.reason}
170
+ />
171
+ ))}
172
+ </div>
173
+ </ToolbarSection>
138
174
 
139
- <div className="flex items-center gap-1">
140
- <span className="text-[11px] text-secondary">Fill</span>
141
- {CELL_COLORS.map((color) => (
142
- <button
143
- key={color}
144
- type="button"
145
- aria-label={`Set cell fill ${color}`}
146
- disabled={
147
- props.disabled ||
148
- !props.onSetCellBackground ||
149
- !props.tableContext?.operations.setCellBackground.enabled
150
- }
151
- onMouseDown={preserveEditorSelectionMouseDown}
152
- onClick={() => props.onSetCellBackground?.(color)}
153
- className="h-6 w-6 rounded border border-border disabled:opacity-40"
154
- style={{ backgroundColor: color }}
155
- title={props.tableContext?.operations.setCellBackground.reason}
156
- />
157
- ))}
158
- </div>
175
+ <ToolbarSection label="Table">
176
+ <ToolbarButton
177
+ ariaLabel="Delete table"
178
+ capability={tableContext?.operations.deleteTable}
179
+ danger
180
+ disabled={props.disabled}
181
+ onClick={props.onDeleteTable}
182
+ >
183
+ Delete table
184
+ </ToolbarButton>
185
+ </ToolbarSection>
186
+ </div>
187
+ );
188
+ }
189
+
190
+ function ToolbarBadge(props: {
191
+ children: React.ReactNode;
192
+ tone?: "neutral" | "accent";
193
+ }) {
194
+ return (
195
+ <span
196
+ className={[
197
+ "rounded-full px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-[0.08em]",
198
+ props.tone === "accent"
199
+ ? "bg-accent-soft text-accent"
200
+ : "bg-surface text-secondary",
201
+ ].join(" ")}
202
+ >
203
+ {props.children}
204
+ </span>
205
+ );
206
+ }
159
207
 
160
- <ToolbarButton
161
- ariaLabel="Delete table"
162
- capability={props.tableContext?.operations.deleteTable}
163
- danger
164
- disabled={props.disabled}
165
- onClick={props.onDeleteTable}
166
- >
167
- Delete table
168
- </ToolbarButton>
208
+ function ToolbarSection(props: {
209
+ label: string;
210
+ children: React.ReactNode;
211
+ }) {
212
+ return (
213
+ <div className="flex flex-wrap items-center gap-1 rounded-md bg-surface/60 px-1.5 py-1 ring-1 ring-border/35">
214
+ <span className="text-[9px] font-semibold uppercase tracking-[0.08em] text-tertiary">
215
+ {props.label}
216
+ </span>
217
+ <div className="flex flex-wrap items-center gap-1">{props.children}</div>
169
218
  </div>
170
219
  );
171
220
  }
@@ -188,7 +237,7 @@ function ToolbarButton(props: {
188
237
  onMouseDown={preserveEditorSelectionMouseDown}
189
238
  onClick={props.onClick}
190
239
  title={title}
191
- className={`inline-flex h-8 items-center rounded-md px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
240
+ className={`inline-flex h-7 items-center rounded-md px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
192
241
  props.danger
193
242
  ? "text-danger hover:bg-danger/10"
194
243
  : "text-primary hover:bg-surface"
@@ -9,6 +9,7 @@ import type {
9
9
  EditorStoryTarget,
10
10
  WorkflowBlockedCommandReason,
11
11
  WorkflowCandidateRange,
12
+ WorkflowLockedZone,
12
13
  WorkflowMetadataMarkup,
13
14
  WorkflowScope,
14
15
  } from "../../api/public-types";
@@ -81,16 +82,16 @@ function getWorkflowMetadataInlineClass(): string {
81
82
 
82
83
  function getWorkflowBlockedInlineClass(reason: WorkflowBlockedCommandReason): string {
83
84
  if (reason.code === "workflow_blocked_import") {
84
- return "wre-workflow-inline wre-workflow-inline-blocked-import";
85
+ return "wre-workflow-inline wre-workflow-inline-locked-zone wre-workflow-inline-blocked-import";
85
86
  }
86
- return "wre-workflow-inline wre-workflow-inline-preserve-only";
87
+ return "wre-workflow-inline wre-workflow-inline-locked-zone wre-workflow-inline-preserve-only";
87
88
  }
88
89
 
89
90
  function getWorkflowBlockedRailClass(reason: WorkflowBlockedCommandReason): string {
90
91
  if (reason.code === "workflow_blocked_import") {
91
- return "wre-workflow-rail wre-workflow-rail-blocked-import";
92
+ return "wre-workflow-rail wre-workflow-rail-locked-zone wre-workflow-rail-blocked-import";
92
93
  }
93
- return "wre-workflow-rail wre-workflow-rail-preserve-only";
94
+ return "wre-workflow-rail wre-workflow-rail-locked-zone wre-workflow-rail-preserve-only";
94
95
  }
95
96
 
96
97
  function hasBlockChildren(node: PMNode): boolean {
@@ -172,6 +173,56 @@ function buildAnchorPmRange(
172
173
  };
173
174
  }
174
175
 
176
+ function collectLockedPmRanges(
177
+ lockedZones: readonly WorkflowLockedZone[] | undefined,
178
+ activeStory: EditorStoryTarget,
179
+ positionMap: PositionMap,
180
+ ): Array<{ from: number; to: number; zone: WorkflowLockedZone }> {
181
+ if (!lockedZones || lockedZones.length === 0) {
182
+ return [];
183
+ }
184
+ const ranges: Array<{ from: number; to: number; zone: WorkflowLockedZone }> = [];
185
+ for (const zone of lockedZones) {
186
+ const zoneStoryTarget = zone.storyTarget ?? MAIN_STORY_TARGET;
187
+ if (!storyTargetsEqual(zoneStoryTarget, activeStory)) {
188
+ continue;
189
+ }
190
+ const pmRange = buildAnchorPmRange(zone.anchor, positionMap);
191
+ if (!pmRange || !pmRange.allowInline || pmRange.from >= pmRange.to) {
192
+ continue;
193
+ }
194
+ ranges.push({ from: pmRange.from, to: pmRange.to, zone });
195
+ }
196
+ return ranges;
197
+ }
198
+
199
+ function subtractInlineOverlaps(
200
+ baseRange: { from: number; to: number },
201
+ blockedRanges: Array<{ from: number; to: number }>,
202
+ ): Array<{ from: number; to: number }> {
203
+ let segments = [baseRange];
204
+ for (const blockedRange of blockedRanges) {
205
+ const nextSegments: Array<{ from: number; to: number }> = [];
206
+ for (const segment of segments) {
207
+ if (blockedRange.to <= segment.from || blockedRange.from >= segment.to) {
208
+ nextSegments.push(segment);
209
+ continue;
210
+ }
211
+ if (blockedRange.from > segment.from) {
212
+ nextSegments.push({ from: segment.from, to: blockedRange.from });
213
+ }
214
+ if (blockedRange.to < segment.to) {
215
+ nextSegments.push({ from: blockedRange.to, to: segment.to });
216
+ }
217
+ }
218
+ segments = nextSegments;
219
+ if (segments.length === 0) {
220
+ break;
221
+ }
222
+ }
223
+ return segments.filter((segment) => segment.from < segment.to);
224
+ }
225
+
175
226
  function pushRailDecorations(
176
227
  decorations: Decoration[],
177
228
  doc: PMNode,
@@ -214,6 +265,7 @@ export function buildDecorations(
214
265
  activeStory: EditorStoryTarget = MAIN_STORY_TARGET,
215
266
  workflowCandidates?: readonly WorkflowCandidateRange[],
216
267
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
268
+ workflowLockedZones?: readonly WorkflowLockedZone[],
217
269
  activeWorkflowWorkItemId?: string | null,
218
270
  activeWorkflowScopeIds?: readonly string[],
219
271
  workflowMetadata?: readonly WorkflowMetadataMarkup[],
@@ -221,6 +273,7 @@ export function buildDecorations(
221
273
  const decorations: Decoration[] = [];
222
274
  const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
223
275
  const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
276
+ const lockedPmRanges = collectLockedPmRanges(workflowLockedZones, activeStory, positionMap);
224
277
 
225
278
  // Walk comment threads and create inline decorations
226
279
  if (commentModel) {
@@ -307,15 +360,21 @@ export function buildDecorations(
307
360
  );
308
361
 
309
362
  if (pmRange.allowInline && pmRange.from < pmRange.to) {
310
- decorations.push(
311
- Decoration.inline(pmRange.from, pmRange.to, {
312
- class: getWorkflowInlineClass(scope, isActiveWorkItem, isSelectionZone),
313
- "data-workflow-scope-id": scope.scopeId,
314
- "data-workflow-scope-mode": scope.mode,
315
- "data-workflow-active": isActiveWorkItem ? "true" : "false",
316
- ...(isSelectionZone ? { "data-workflow-zone": "selection" } : {}),
317
- }),
363
+ const visibleScopeSegments = subtractInlineOverlaps(
364
+ { from: pmRange.from, to: pmRange.to },
365
+ lockedPmRanges.filter((range) => range.to > pmRange.from && range.from < pmRange.to),
318
366
  );
367
+ for (const visibleSegment of visibleScopeSegments) {
368
+ decorations.push(
369
+ Decoration.inline(visibleSegment.from, visibleSegment.to, {
370
+ class: getWorkflowInlineClass(scope, isActiveWorkItem, isSelectionZone),
371
+ "data-workflow-scope-id": scope.scopeId,
372
+ "data-workflow-scope-mode": scope.mode,
373
+ "data-workflow-active": isActiveWorkItem ? "true" : "false",
374
+ ...(isSelectionZone ? { "data-workflow-zone": "selection" } : {}),
375
+ }),
376
+ );
377
+ }
319
378
  }
320
379
 
321
380
  pushRailDecorations(decorations, doc, pmRange.from, pmRange.to, {
@@ -375,8 +434,19 @@ export function buildDecorations(
375
434
  }
376
435
  }
377
436
 
378
- if (workflowBlockedReasons) {
379
- for (const reason of workflowBlockedReasons) {
437
+ if ((workflowLockedZones && workflowLockedZones.length > 0) || workflowBlockedReasons) {
438
+ const blockedReasonsToRender = workflowLockedZones && workflowLockedZones.length > 0
439
+ ? workflowLockedZones.map((zone) => ({
440
+ code: zone.code,
441
+ message: zone.detail,
442
+ anchor: zone.anchor,
443
+ storyTarget: zone.storyTarget,
444
+ fragmentId: zone.fragmentId,
445
+ label: zone.label,
446
+ detail: zone.detail,
447
+ }))
448
+ : workflowBlockedReasons ?? [];
449
+ for (const reason of blockedReasonsToRender) {
380
450
  if (
381
451
  reason.code !== "workflow_preserve_only" &&
382
452
  reason.code !== "workflow_blocked_import"
@@ -393,6 +463,8 @@ export function buildDecorations(
393
463
  Decoration.inline(pmRange.from, pmRange.to, {
394
464
  class: getWorkflowBlockedInlineClass(reason),
395
465
  "data-workflow-blocked-code": reason.code,
466
+ ...(reason.fragmentId ? { "data-workflow-fragment-id": reason.fragmentId } : {}),
467
+ "data-workflow-zone": "locked",
396
468
  }),
397
469
  );
398
470
  }
@@ -402,6 +474,8 @@ export function buildDecorations(
402
474
  className: getWorkflowBlockedRailClass(reason),
403
475
  attrs: {
404
476
  "data-workflow-blocked-code": reason.code,
477
+ ...(reason.fragmentId ? { "data-workflow-fragment-id": reason.fragmentId } : {}),
478
+ "data-workflow-zone": "locked",
405
479
  },
406
480
  }, railRangeCache);
407
481
  }
@@ -489,6 +489,7 @@ export const editorSchema = new Schema({
489
489
  label: { default: "Locked" },
490
490
  detail: { default: "" },
491
491
  presentation: { default: "inline-chip" },
492
+ displayText: { default: null },
492
493
  },
493
494
  toDOM(node) {
494
495
  const presentation = node.attrs.presentation as string;
@@ -505,6 +506,34 @@ export const editorSchema = new Schema({
505
506
  },
506
507
  ];
507
508
  }
509
+ if (presentation === "text-box") {
510
+ return [
511
+ "span",
512
+ {
513
+ class: "mx-0.5 inline-flex max-w-full whitespace-pre-wrap rounded border border-slate-300 bg-slate-50 px-2 py-1 align-top text-sm leading-snug text-slate-700 shadow-sm",
514
+ "data-node-type": "opaque_inline",
515
+ "data-inline-presentation": "text-box",
516
+ contenteditable: "false",
517
+ title: node.attrs.detail as string,
518
+ "aria-label": node.attrs.label as string,
519
+ },
520
+ (node.attrs.displayText as string | null) ?? (node.attrs.label as string),
521
+ ];
522
+ }
523
+ if (presentation === "checkbox") {
524
+ return [
525
+ "span",
526
+ {
527
+ class: "mx-0.5 inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded border border-slate-300 bg-white px-1 align-text-bottom text-sm leading-none text-slate-700",
528
+ "data-node-type": "opaque_inline",
529
+ "data-inline-presentation": "checkbox",
530
+ contenteditable: "false",
531
+ title: node.attrs.detail as string,
532
+ "aria-label": node.attrs.label as string,
533
+ },
534
+ (node.attrs.displayText as string | null) ?? "☐",
535
+ ];
536
+ }
508
537
  return [
509
538
  "span",
510
539
  {
@@ -34,7 +34,7 @@ export function createPMStateFromSnapshot(
34
34
  selection: SelectionSnapshot,
35
35
  plugins: Plugin[],
36
36
  mediaPreviews: Record<string, MediaPreviewDescriptor> = {},
37
- showUnsupportedObjectPreviews = true,
37
+ showUnsupportedObjectPreviews = false,
38
38
  ): PMStateResult {
39
39
  const doc = buildPMDoc(surface, mediaPreviews, showUnsupportedObjectPreviews);
40
40
  const positionMap = buildPositionMap(surface);
@@ -508,6 +508,17 @@ function buildOpaqueInlineOrComplexAtom(
508
508
  const label = segment.label;
509
509
  const detail = segment.detail;
510
510
 
511
+ if (segment.presentation === "text-box" || segment.presentation === "checkbox") {
512
+ return editorSchema.nodes.opaque_inline.create({
513
+ fragmentId: segment.fragmentId,
514
+ warningId: segment.warningId,
515
+ label,
516
+ detail,
517
+ presentation: segment.presentation,
518
+ displayText: segment.displayText ?? null,
519
+ });
520
+ }
521
+
511
522
  if (showUnsupportedObjectPreviews && label === "Embedded chart") {
512
523
  return editorSchema.nodes.chart_atom.create({ detail });
513
524
  }
@@ -548,6 +559,7 @@ function buildOpaqueInlineOrComplexAtom(
548
559
  label,
549
560
  detail,
550
561
  presentation: segment.presentation ?? "inline-chip",
562
+ displayText: segment.displayText ?? null,
551
563
  });
552
564
  }
553
565
 
@@ -30,7 +30,7 @@ export function createSurfaceDocumentBuildKey(input: {
30
30
  : getSurfaceIdentity(input.surface),
31
31
  activeStory: input.activeStory,
32
32
  mediaPreviewKey: input.mediaPreviewKey,
33
- showUnsupportedObjectPreviews: input.showUnsupportedObjectPreviews ?? true,
33
+ showUnsupportedObjectPreviews: input.showUnsupportedObjectPreviews ?? false,
34
34
  });
35
35
  }
36
36
 
@@ -43,10 +43,10 @@ export function createSurfaceDecorationKey(input: {
43
43
  workflowScopeSignature?: string;
44
44
  workflowCandidateSignature?: string;
45
45
  workflowBlockedSignature?: string;
46
+ workflowLockedZoneSignature?: string;
46
47
  workflowMetadataSignature?: string;
47
48
  activeWorkflowWorkItemId?: string | null;
48
49
  activeWorkflowScopeIds?: readonly string[];
49
- suggestionsEnabled?: boolean;
50
50
  }): string {
51
51
  return JSON.stringify({
52
52
  markupDisplay: input.markupDisplay,
@@ -57,9 +57,9 @@ export function createSurfaceDecorationKey(input: {
57
57
  workflowScopeSignature: input.workflowScopeSignature ?? null,
58
58
  workflowCandidateSignature: input.workflowCandidateSignature ?? null,
59
59
  workflowBlockedSignature: input.workflowBlockedSignature ?? null,
60
+ workflowLockedZoneSignature: input.workflowLockedZoneSignature ?? null,
60
61
  workflowMetadataSignature: input.workflowMetadataSignature ?? null,
61
62
  activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
62
63
  activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
63
- suggestionsEnabled: input.suggestionsEnabled ?? false,
64
64
  });
65
65
  }
@@ -106,6 +106,26 @@ export function TwInlineToken(props: TwInlineTokenProps) {
106
106
  );
107
107
  }
108
108
 
109
+ if (segment.presentation === "checkbox") {
110
+ return (
111
+ <button
112
+ type="button"
113
+ tabIndex={-1}
114
+ onMouseDown={(e) => {
115
+ e.preventDefault();
116
+ props.onSelectionChange?.(createSelectionSnapshot(segment.from, segment.to));
117
+ }}
118
+ className={`inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded border border-slate-300 bg-white px-1 text-sm leading-none text-slate-700 ${commentClass} ${selected ? "ring-1 ring-accent/30" : ""} ${focusRingClass}`}
119
+ title={segment.detail}
120
+ data-inline-presentation="checkbox"
121
+ >
122
+ {renderTwCaret(selection, segment.from)}
123
+ {segment.displayText ?? "☐"}
124
+ {renderTwCaret(selection, segment.to)}
125
+ </button>
126
+ );
127
+ }
128
+
109
129
  return (
110
130
  <button
111
131
  type="button"