diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx
index d2047a2ebd..85740aa0d9 100644
--- a/apps/docs/document-api/available-operations.mdx
+++ b/apps/docs/document-api/available-operations.mdx
@@ -43,7 +43,7 @@ Use the tables below to see what operations are available and where each one is
| Styles | 1 | 0 | 1 | [Reference](/document-api/reference/styles/index) |
| Table of Authorities | 11 | 0 | 11 | [Reference](/document-api/reference/authorities/index) |
| Table of Contents | 10 | 0 | 10 | [Reference](/document-api/reference/toc/index) |
-| Tables | 42 | 0 | 42 | [Reference](/document-api/reference/tables/index) |
+| Tables | 45 | 0 | 45 | [Reference](/document-api/reference/tables/index) |
| Track Changes | 3 | 0 | 3 | [Reference](/document-api/reference/track-changes/index) |
| Editor method | Operation |
@@ -412,6 +412,9 @@ Use the tables below to see what operations are available and where each one is
| editor.doc.tables.setCellPadding(...) | [`tables.setCellPadding`](/document-api/reference/tables/set-cell-padding) |
| editor.doc.tables.setCellSpacing(...) | [`tables.setCellSpacing`](/document-api/reference/tables/set-cell-spacing) |
| editor.doc.tables.clearCellSpacing(...) | [`tables.clearCellSpacing`](/document-api/reference/tables/clear-cell-spacing) |
+| editor.doc.tables.applyStyle(...) | [`tables.applyStyle`](/document-api/reference/tables/apply-style) |
+| editor.doc.tables.setBorders(...) | [`tables.setBorders`](/document-api/reference/tables/set-borders) |
+| editor.doc.tables.setTableOptions(...) | [`tables.setTableOptions`](/document-api/reference/tables/set-table-options) |
| editor.doc.tables.get(...) | [`tables.get`](/document-api/reference/tables/get) |
| editor.doc.tables.getCells(...) | [`tables.getCells`](/document-api/reference/tables/get-cells) |
| editor.doc.tables.getProperties(...) | [`tables.getProperties`](/document-api/reference/tables/get-properties) |
diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json
index f71d77a9eb..8eb4076244 100644
--- a/apps/docs/document-api/reference/_generated-manifest.json
+++ b/apps/docs/document-api/reference/_generated-manifest.json
@@ -348,6 +348,7 @@
"apps/docs/document-api/reference/styles/paragraph/index.mdx",
"apps/docs/document-api/reference/styles/paragraph/set-style.mdx",
"apps/docs/document-api/reference/tables/apply-border-preset.mdx",
+ "apps/docs/document-api/reference/tables/apply-style.mdx",
"apps/docs/document-api/reference/tables/clear-border.mdx",
"apps/docs/document-api/reference/tables/clear-cell-spacing.mdx",
"apps/docs/document-api/reference/tables/clear-contents.mdx",
@@ -374,6 +375,7 @@
"apps/docs/document-api/reference/tables/move.mdx",
"apps/docs/document-api/reference/tables/set-alt-text.mdx",
"apps/docs/document-api/reference/tables/set-border.mdx",
+ "apps/docs/document-api/reference/tables/set-borders.mdx",
"apps/docs/document-api/reference/tables/set-cell-padding.mdx",
"apps/docs/document-api/reference/tables/set-cell-properties.mdx",
"apps/docs/document-api/reference/tables/set-cell-spacing.mdx",
@@ -385,6 +387,7 @@
"apps/docs/document-api/reference/tables/set-shading.mdx",
"apps/docs/document-api/reference/tables/set-style-option.mdx",
"apps/docs/document-api/reference/tables/set-style.mdx",
+ "apps/docs/document-api/reference/tables/set-table-options.mdx",
"apps/docs/document-api/reference/tables/set-table-padding.mdx",
"apps/docs/document-api/reference/tables/sort.mdx",
"apps/docs/document-api/reference/tables/split-cell.mdx",
@@ -686,6 +689,9 @@
"tables.setCellPadding",
"tables.setCellSpacing",
"tables.clearCellSpacing",
+ "tables.applyStyle",
+ "tables.setBorders",
+ "tables.setTableOptions",
"tables.get",
"tables.getCells",
"tables.getProperties",
@@ -976,5 +982,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
- "sourceHash": "5eb339719530fd6ff1e69c9c90c36637fedce9fc426b3aba84f973c73facf3e0"
+ "sourceHash": "31ad98dfc8659ebef07fbe3070add1c26fbbc8e0fbfa284e52d0b17e9fdd6b0f"
}
diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx
index 06d2367c67..f363b8c77f 100644
--- a/apps/docs/document-api/reference/capabilities/get.mdx
+++ b/apps/docs/document-api/reference/capabilities/get.mdx
@@ -1862,6 +1862,11 @@ _No fields._
| `operations.tables.applyBorderPreset.dryRun` | boolean | yes | |
| `operations.tables.applyBorderPreset.reasons` | enum[] | no | |
| `operations.tables.applyBorderPreset.tracked` | boolean | yes | |
+| `operations.tables.applyStyle` | object | yes | |
+| `operations.tables.applyStyle.available` | boolean | yes | |
+| `operations.tables.applyStyle.dryRun` | boolean | yes | |
+| `operations.tables.applyStyle.reasons` | enum[] | no | |
+| `operations.tables.applyStyle.tracked` | boolean | yes | |
| `operations.tables.clearBorder` | object | yes | |
| `operations.tables.clearBorder.available` | boolean | yes | |
| `operations.tables.clearBorder.dryRun` | boolean | yes | |
@@ -1987,6 +1992,11 @@ _No fields._
| `operations.tables.setBorder.dryRun` | boolean | yes | |
| `operations.tables.setBorder.reasons` | enum[] | no | |
| `operations.tables.setBorder.tracked` | boolean | yes | |
+| `operations.tables.setBorders` | object | yes | |
+| `operations.tables.setBorders.available` | boolean | yes | |
+| `operations.tables.setBorders.dryRun` | boolean | yes | |
+| `operations.tables.setBorders.reasons` | enum[] | no | |
+| `operations.tables.setBorders.tracked` | boolean | yes | |
| `operations.tables.setCellPadding` | object | yes | |
| `operations.tables.setCellPadding.available` | boolean | yes | |
| `operations.tables.setCellPadding.dryRun` | boolean | yes | |
@@ -2042,6 +2052,11 @@ _No fields._
| `operations.tables.setStyleOption.dryRun` | boolean | yes | |
| `operations.tables.setStyleOption.reasons` | enum[] | no | |
| `operations.tables.setStyleOption.tracked` | boolean | yes | |
+| `operations.tables.setTableOptions` | object | yes | |
+| `operations.tables.setTableOptions.available` | boolean | yes | |
+| `operations.tables.setTableOptions.dryRun` | boolean | yes | |
+| `operations.tables.setTableOptions.reasons` | enum[] | no | |
+| `operations.tables.setTableOptions.tracked` | boolean | yes | |
| `operations.tables.setTablePadding` | object | yes | |
| `operations.tables.setTablePadding.available` | boolean | yes | |
| `operations.tables.setTablePadding.dryRun` | boolean | yes | |
@@ -4013,6 +4028,11 @@ _No fields._
"dryRun": true,
"tracked": false
},
+ "tables.applyStyle": {
+ "available": true,
+ "dryRun": true,
+ "tracked": false
+ },
"tables.clearBorder": {
"available": true,
"dryRun": true,
@@ -4138,6 +4158,11 @@ _No fields._
"dryRun": true,
"tracked": false
},
+ "tables.setBorders": {
+ "available": true,
+ "dryRun": true,
+ "tracked": false
+ },
"tables.setCellPadding": {
"available": true,
"dryRun": true,
@@ -4193,6 +4218,11 @@ _No fields._
"dryRun": true,
"tracked": false
},
+ "tables.setTableOptions": {
+ "available": true,
+ "dryRun": true,
+ "tracked": false
+ },
"tables.setTablePadding": {
"available": true,
"dryRun": true,
@@ -17086,6 +17116,41 @@ _No fields._
],
"type": "object"
},
+ "tables.applyStyle": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "HELPER_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE",
+ "STYLES_PART_MISSING",
+ "COLLABORATION_ACTIVE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
"tables.clearBorder": {
"additionalProperties": false,
"properties": {
@@ -17961,6 +18026,41 @@ _No fields._
],
"type": "object"
},
+ "tables.setBorders": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "HELPER_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE",
+ "STYLES_PART_MISSING",
+ "COLLABORATION_ACTIVE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
"tables.setCellPadding": {
"additionalProperties": false,
"properties": {
@@ -18346,6 +18446,41 @@ _No fields._
],
"type": "object"
},
+ "tables.setTableOptions": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "HELPER_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE",
+ "STYLES_PART_MISSING",
+ "COLLABORATION_ACTIVE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
"tables.setTablePadding": {
"additionalProperties": false,
"properties": {
@@ -19165,6 +19300,9 @@ _No fields._
"tables.setCellPadding",
"tables.setCellSpacing",
"tables.clearCellSpacing",
+ "tables.applyStyle",
+ "tables.setBorders",
+ "tables.setTableOptions",
"tables.get",
"tables.getCells",
"tables.getProperties",
diff --git a/apps/docs/document-api/reference/content-controls/get-binding.mdx b/apps/docs/document-api/reference/content-controls/get-binding.mdx
index d799300a5a..5632caddb8 100644
--- a/apps/docs/document-api/reference/content-controls/get-binding.mdx
+++ b/apps/docs/document-api/reference/content-controls/get-binding.mdx
@@ -47,7 +47,7 @@ Returns the ContentControlBinding or null if no binding is set.
## Output fields
-### Variant 1 (storeItemId, xpath)
+### Variant 1 (required: storeItemId, xpath)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/delete.mdx b/apps/docs/document-api/reference/delete.mdx
index ce2b5f5273..7d8ff8ddb3 100644
--- a/apps/docs/document-api/reference/delete.mdx
+++ b/apps/docs/document-api/reference/delete.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the
| `target.kind` | `"selection"` | yes | Constant: `"selection"` |
| `target.start` | SelectionPoint | yes | SelectionPoint |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/apply.mdx b/apps/docs/document-api/reference/format/apply.mdx
index cf3aa0c733..fcffa4a0a1 100644
--- a/apps/docs/document-api/reference/format/apply.mdx
+++ b/apps/docs/document-api/reference/format/apply.mdx
@@ -79,7 +79,7 @@ Returns a TextMutationReceipt confirming inline styles were applied to the targe
| `target.kind` | `"selection"` | yes | Constant: `"selection"` |
| `target.start` | SelectionPoint | yes | SelectionPoint |
-### Variant 2 (ref, inline)
+### Variant 2 (required: ref, inline)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/b-cs.mdx b/apps/docs/document-api/reference/format/b-cs.mdx
index db5dc02c13..8f86a99723 100644
--- a/apps/docs/document-api/reference/format/b-cs.mdx
+++ b/apps/docs/document-api/reference/format/b-cs.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/bold.mdx b/apps/docs/document-api/reference/format/bold.mdx
index f3a9034c81..32ecaf3a41 100644
--- a/apps/docs/document-api/reference/format/bold.mdx
+++ b/apps/docs/document-api/reference/format/bold.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/border.mdx b/apps/docs/document-api/reference/format/border.mdx
index d15943c840..12d7f40580 100644
--- a/apps/docs/document-api/reference/format/border.mdx
+++ b/apps/docs/document-api/reference/format/border.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | object \\| null | yes | One of: object, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/caps.mdx b/apps/docs/document-api/reference/format/caps.mdx
index 7be24b27bd..995710f5e1 100644
--- a/apps/docs/document-api/reference/format/caps.mdx
+++ b/apps/docs/document-api/reference/format/caps.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/char-scale.mdx b/apps/docs/document-api/reference/format/char-scale.mdx
index 85f340a598..d2471a0aed 100644
--- a/apps/docs/document-api/reference/format/char-scale.mdx
+++ b/apps/docs/document-api/reference/format/char-scale.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | number \\| null | yes | One of: number, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/color.mdx b/apps/docs/document-api/reference/format/color.mdx
index 961ef26752..37ac61be42 100644
--- a/apps/docs/document-api/reference/format/color.mdx
+++ b/apps/docs/document-api/reference/format/color.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/contextual-alternates.mdx b/apps/docs/document-api/reference/format/contextual-alternates.mdx
index f9a3cd9004..3f8244af31 100644
--- a/apps/docs/document-api/reference/format/contextual-alternates.mdx
+++ b/apps/docs/document-api/reference/format/contextual-alternates.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/cs.mdx b/apps/docs/document-api/reference/format/cs.mdx
index dbfe287484..a8bebba92c 100644
--- a/apps/docs/document-api/reference/format/cs.mdx
+++ b/apps/docs/document-api/reference/format/cs.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/dstrike.mdx b/apps/docs/document-api/reference/format/dstrike.mdx
index c73e4b14f0..67979b2c18 100644
--- a/apps/docs/document-api/reference/format/dstrike.mdx
+++ b/apps/docs/document-api/reference/format/dstrike.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/east-asian-layout.mdx b/apps/docs/document-api/reference/format/east-asian-layout.mdx
index 6308b6708f..2b20e07011 100644
--- a/apps/docs/document-api/reference/format/east-asian-layout.mdx
+++ b/apps/docs/document-api/reference/format/east-asian-layout.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | object \\| null | yes | One of: object, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/em.mdx b/apps/docs/document-api/reference/format/em.mdx
index 9db16c8212..b4d67fff7a 100644
--- a/apps/docs/document-api/reference/format/em.mdx
+++ b/apps/docs/document-api/reference/format/em.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/emboss.mdx b/apps/docs/document-api/reference/format/emboss.mdx
index f8587ed878..e43699ea2b 100644
--- a/apps/docs/document-api/reference/format/emboss.mdx
+++ b/apps/docs/document-api/reference/format/emboss.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/fit-text.mdx b/apps/docs/document-api/reference/format/fit-text.mdx
index 53d677e111..0cdc8a3931 100644
--- a/apps/docs/document-api/reference/format/fit-text.mdx
+++ b/apps/docs/document-api/reference/format/fit-text.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | object \\| null | yes | One of: object, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/font-family.mdx b/apps/docs/document-api/reference/format/font-family.mdx
index c53e1a663f..114024c665 100644
--- a/apps/docs/document-api/reference/format/font-family.mdx
+++ b/apps/docs/document-api/reference/format/font-family.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/font-size-cs.mdx b/apps/docs/document-api/reference/format/font-size-cs.mdx
index 0b6581b707..8f72148aec 100644
--- a/apps/docs/document-api/reference/format/font-size-cs.mdx
+++ b/apps/docs/document-api/reference/format/font-size-cs.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | number \\| null | yes | One of: number, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/font-size.mdx b/apps/docs/document-api/reference/format/font-size.mdx
index cdf0009909..cb6621916a 100644
--- a/apps/docs/document-api/reference/format/font-size.mdx
+++ b/apps/docs/document-api/reference/format/font-size.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | number \\| null | yes | One of: number, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/highlight.mdx b/apps/docs/document-api/reference/format/highlight.mdx
index 2a5ce3dee5..eb7e312ac7 100644
--- a/apps/docs/document-api/reference/format/highlight.mdx
+++ b/apps/docs/document-api/reference/format/highlight.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/i-cs.mdx b/apps/docs/document-api/reference/format/i-cs.mdx
index 19b56be5a5..a75151e4e9 100644
--- a/apps/docs/document-api/reference/format/i-cs.mdx
+++ b/apps/docs/document-api/reference/format/i-cs.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/imprint.mdx b/apps/docs/document-api/reference/format/imprint.mdx
index 9ef71e9d89..5f528f372d 100644
--- a/apps/docs/document-api/reference/format/imprint.mdx
+++ b/apps/docs/document-api/reference/format/imprint.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/italic.mdx b/apps/docs/document-api/reference/format/italic.mdx
index d2da984e4f..3e071ad430 100644
--- a/apps/docs/document-api/reference/format/italic.mdx
+++ b/apps/docs/document-api/reference/format/italic.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/kerning.mdx b/apps/docs/document-api/reference/format/kerning.mdx
index 16d3583123..48811a6b2a 100644
--- a/apps/docs/document-api/reference/format/kerning.mdx
+++ b/apps/docs/document-api/reference/format/kerning.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | number \\| null | yes | One of: number, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/lang.mdx b/apps/docs/document-api/reference/format/lang.mdx
index 9aaa7929b0..1578406b23 100644
--- a/apps/docs/document-api/reference/format/lang.mdx
+++ b/apps/docs/document-api/reference/format/lang.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | object \\| null | yes | One of: object, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/letter-spacing.mdx b/apps/docs/document-api/reference/format/letter-spacing.mdx
index 5c72b59aa6..54eabf39fc 100644
--- a/apps/docs/document-api/reference/format/letter-spacing.mdx
+++ b/apps/docs/document-api/reference/format/letter-spacing.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | number \\| null | yes | One of: number, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/ligatures.mdx b/apps/docs/document-api/reference/format/ligatures.mdx
index 52a05eba2d..8fe08d7c47 100644
--- a/apps/docs/document-api/reference/format/ligatures.mdx
+++ b/apps/docs/document-api/reference/format/ligatures.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/num-form.mdx b/apps/docs/document-api/reference/format/num-form.mdx
index e3afbb0fdf..e761dd926e 100644
--- a/apps/docs/document-api/reference/format/num-form.mdx
+++ b/apps/docs/document-api/reference/format/num-form.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/num-spacing.mdx b/apps/docs/document-api/reference/format/num-spacing.mdx
index 8f38f713e9..6c97444b55 100644
--- a/apps/docs/document-api/reference/format/num-spacing.mdx
+++ b/apps/docs/document-api/reference/format/num-spacing.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/o-math.mdx b/apps/docs/document-api/reference/format/o-math.mdx
index 91d1a3eaa8..660dc4c254 100644
--- a/apps/docs/document-api/reference/format/o-math.mdx
+++ b/apps/docs/document-api/reference/format/o-math.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/outline.mdx b/apps/docs/document-api/reference/format/outline.mdx
index 97e31e2431..bb146e5be9 100644
--- a/apps/docs/document-api/reference/format/outline.mdx
+++ b/apps/docs/document-api/reference/format/outline.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx b/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx
index e4611ccb9a..8990ae1c9d 100644
--- a/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx
+++ b/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx
@@ -26,21 +26,21 @@ Returns a ParagraphMutationResult; reports NO_OP if all flags already match.
## Input fields
-### Variant 1 (target, contextualSpacing)
+### Variant 1 (required: contextualSpacing)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contextualSpacing` | boolean | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 2 (target, pageBreakBefore)
+### Variant 2 (required: pageBreakBefore)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `pageBreakBefore` | boolean | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 3 (target, suppressAutoHyphens)
+### Variant 3 (required: suppressAutoHyphens)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx b/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx
index c5c179f428..476bde0a5a 100644
--- a/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx
+++ b/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx
@@ -26,28 +26,28 @@ Returns a ParagraphMutationResult; reports NO_OP if indentation already matches.
## Input fields
-### Variant 1 (left)
+### Variant 1 (required: left)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `left` | integer | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 2 (right)
+### Variant 2 (required: right)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `right` | integer | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 3 (firstLine)
+### Variant 3 (required: firstLine)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `firstLine` | integer | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 4 (hanging)
+### Variant 4 (required: hanging)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx b/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx
index 63d4f14c17..e19f4b9544 100644
--- a/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx
+++ b/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx
@@ -26,21 +26,21 @@ Returns a ParagraphMutationResult; reports NO_OP if all flags already match.
## Input fields
-### Variant 1 (target, keepNext)
+### Variant 1 (required: keepNext)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `keepNext` | boolean | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 2 (target, keepLines)
+### Variant 2 (required: keepLines)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `keepLines` | boolean | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 3 (target, widowControl)
+### Variant 3 (required: widowControl)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/paragraph/set-shading.mdx b/apps/docs/document-api/reference/format/paragraph/set-shading.mdx
index 0c34eb7a05..864225965f 100644
--- a/apps/docs/document-api/reference/format/paragraph/set-shading.mdx
+++ b/apps/docs/document-api/reference/format/paragraph/set-shading.mdx
@@ -26,21 +26,21 @@ Returns a ParagraphMutationResult; reports NO_OP if the shading already matches.
## Input fields
-### Variant 1 (target, fill)
+### Variant 1 (required: fill)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `fill` | string | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 2 (target, color)
+### Variant 2 (required: color)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `color` | string | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 3 (target, pattern)
+### Variant 3 (required: pattern)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx b/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx
index a7c722c403..bd3fc4bc09 100644
--- a/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx
+++ b/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx
@@ -26,28 +26,28 @@ Returns a ParagraphMutationResult; reports NO_OP if spacing already matches.
## Input fields
-### Variant 1 (before)
+### Variant 1 (required: before)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `before` | integer | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 2 (after)
+### Variant 2 (required: after)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `after` | integer | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 3 (line)
+### Variant 3 (required: line)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `line` | integer | yes | |
| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress |
-### Variant 4 (lineRule)
+### Variant 4 (required: lineRule)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/position.mdx b/apps/docs/document-api/reference/format/position.mdx
index 2e0e5bfae2..39940d45b0 100644
--- a/apps/docs/document-api/reference/format/position.mdx
+++ b/apps/docs/document-api/reference/format/position.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | number \\| null | yes | One of: number, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/r-fonts.mdx b/apps/docs/document-api/reference/format/r-fonts.mdx
index 1f631bfb15..227190bc9c 100644
--- a/apps/docs/document-api/reference/format/r-fonts.mdx
+++ b/apps/docs/document-api/reference/format/r-fonts.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | object \\| null | yes | One of: object, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/r-style.mdx b/apps/docs/document-api/reference/format/r-style.mdx
index 248ae14608..b825018016 100644
--- a/apps/docs/document-api/reference/format/r-style.mdx
+++ b/apps/docs/document-api/reference/format/r-style.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | string \\| null | yes | One of: string, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/rtl.mdx b/apps/docs/document-api/reference/format/rtl.mdx
index 4606eeba8f..b7863791cd 100644
--- a/apps/docs/document-api/reference/format/rtl.mdx
+++ b/apps/docs/document-api/reference/format/rtl.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/shading.mdx b/apps/docs/document-api/reference/format/shading.mdx
index dde07074da..f81e7d577b 100644
--- a/apps/docs/document-api/reference/format/shading.mdx
+++ b/apps/docs/document-api/reference/format/shading.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | object \\| null | yes | One of: object, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/shadow.mdx b/apps/docs/document-api/reference/format/shadow.mdx
index 638f03d3a4..f2b2c0dfb2 100644
--- a/apps/docs/document-api/reference/format/shadow.mdx
+++ b/apps/docs/document-api/reference/format/shadow.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/small-caps.mdx b/apps/docs/document-api/reference/format/small-caps.mdx
index 58f0823d46..a987a6ba5b 100644
--- a/apps/docs/document-api/reference/format/small-caps.mdx
+++ b/apps/docs/document-api/reference/format/small-caps.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/snap-to-grid.mdx b/apps/docs/document-api/reference/format/snap-to-grid.mdx
index ca444218aa..74edd6a4a0 100644
--- a/apps/docs/document-api/reference/format/snap-to-grid.mdx
+++ b/apps/docs/document-api/reference/format/snap-to-grid.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/spec-vanish.mdx b/apps/docs/document-api/reference/format/spec-vanish.mdx
index 8468b14f9a..b211ca2047 100644
--- a/apps/docs/document-api/reference/format/spec-vanish.mdx
+++ b/apps/docs/document-api/reference/format/spec-vanish.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/strike.mdx b/apps/docs/document-api/reference/format/strike.mdx
index 1757fa4a34..e49c8c90cb 100644
--- a/apps/docs/document-api/reference/format/strike.mdx
+++ b/apps/docs/document-api/reference/format/strike.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/stylistic-sets.mdx b/apps/docs/document-api/reference/format/stylistic-sets.mdx
index bd08ae6411..29676bbd98 100644
--- a/apps/docs/document-api/reference/format/stylistic-sets.mdx
+++ b/apps/docs/document-api/reference/format/stylistic-sets.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | object[] \\| null | yes | One of: object[], null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/underline.mdx b/apps/docs/document-api/reference/format/underline.mdx
index ceffb3fe81..e1dc6a016b 100644
--- a/apps/docs/document-api/reference/format/underline.mdx
+++ b/apps/docs/document-api/reference/format/underline.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null \\| object | no | One of: boolean, null, object |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/vanish.mdx b/apps/docs/document-api/reference/format/vanish.mdx
index 49915a1898..2edf8b3bfb 100644
--- a/apps/docs/document-api/reference/format/vanish.mdx
+++ b/apps/docs/document-api/reference/format/vanish.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/vert-align.mdx b/apps/docs/document-api/reference/format/vert-align.mdx
index df83d824c1..904664afde 100644
--- a/apps/docs/document-api/reference/format/vert-align.mdx
+++ b/apps/docs/document-api/reference/format/vert-align.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | enum \\| null | yes | One of: enum, null |
-### Variant 2 (ref, value)
+### Variant 2 (required: ref, value)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/format/web-hidden.mdx b/apps/docs/document-api/reference/format/web-hidden.mdx
index 1444ccaa8b..3e30864629 100644
--- a/apps/docs/document-api/reference/format/web-hidden.mdx
+++ b/apps/docs/document-api/reference/format/web-hidden.mdx
@@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `value` | boolean \\| null | no | One of: boolean, null |
-### Variant 2 (ref)
+### Variant 2 (required: ref)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx
index b9d86a11c0..c4cea64439 100644
--- a/apps/docs/document-api/reference/index.mdx
+++ b/apps/docs/document-api/reference/index.mdx
@@ -34,7 +34,7 @@ Document API is currently alpha and subject to breaking changes.
| Mutations | 2 | 0 | 2 | [Open](/document-api/reference/mutations/index) |
| Paragraph Formatting | 17 | 0 | 17 | [Open](/document-api/reference/format/paragraph/index) |
| Paragraph Styles | 2 | 0 | 2 | [Open](/document-api/reference/styles/paragraph/index) |
-| Tables | 42 | 0 | 42 | [Open](/document-api/reference/tables/index) |
+| Tables | 45 | 0 | 45 | [Open](/document-api/reference/tables/index) |
| History | 3 | 0 | 3 | [Open](/document-api/reference/history/index) |
| Table of Contents | 10 | 0 | 10 | [Open](/document-api/reference/toc/index) |
| Images | 27 | 0 | 27 | [Open](/document-api/reference/images/index) |
@@ -319,6 +319,9 @@ The tables below are grouped by namespace.
| tables.setCellPadding | editor.doc.tables.setCellPadding(...) | Set padding on a specific table cell or cell range. |
| tables.setCellSpacing | editor.doc.tables.setCellSpacing(...) | Set the cell spacing for the target table. |
| tables.clearCellSpacing | editor.doc.tables.clearCellSpacing(...) | Remove custom cell spacing from the target table. |
+| tables.applyStyle | editor.doc.tables.applyStyle(...) | Apply a table style and/or style options in one call. |
+| tables.setBorders | editor.doc.tables.setBorders(...) | Set borders on a table using a target set or per-edge patch. |
+| tables.setTableOptions | editor.doc.tables.setTableOptions(...) | Set table-level default cell margins and/or cell spacing. |
| tables.get | editor.doc.tables.get(...) | Retrieve table structure and dimensions by locator. |
| tables.getCells | editor.doc.tables.getCells(...) | Retrieve cell information for a table, optionally filtered by row or column. |
| tables.getProperties | editor.doc.tables.getProperties(...) | Retrieve layout and style properties of a table. |
diff --git a/apps/docs/document-api/reference/replace.mdx b/apps/docs/document-api/reference/replace.mdx
index dd0d53cff7..d2f9c54be5 100644
--- a/apps/docs/document-api/reference/replace.mdx
+++ b/apps/docs/document-api/reference/replace.mdx
@@ -36,14 +36,14 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t
| `target.start` | SelectionPoint | yes | SelectionPoint |
| `text` | string | yes | |
-### Variant 1.2 (ref, text)
+### Variant 1.2 (required: ref, text)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | |
| `text` | string | yes | |
-### Variant 2.1 (target, content)
+### Variant 2.1 (required: target, content)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
@@ -52,7 +52,7 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t
| `nestingPolicy.tables` | enum | no | `"forbid"`, `"allow"` |
| `target` | BlockNodeAddress \\| SelectionTarget | yes | One of: BlockNodeAddress, SelectionTarget |
-### Variant 2.2 (ref, content)
+### Variant 2.2 (required: ref, content)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/apply-border-preset.mdx b/apps/docs/document-api/reference/tables/apply-border-preset.mdx
index 5bf31d4fd1..a530fb5bfb 100644
--- a/apps/docs/document-api/reference/tables/apply-border-preset.mdx
+++ b/apps/docs/document-api/reference/tables/apply-border-preset.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the preset is already ap
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/apply-style.mdx b/apps/docs/document-api/reference/tables/apply-style.mdx
new file mode 100644
index 0000000000..2ec9ff1f34
--- /dev/null
+++ b/apps/docs/document-api/reference/tables/apply-style.mdx
@@ -0,0 +1,328 @@
+---
+title: tables.applyStyle
+sidebarTitle: tables.applyStyle
+description: Apply a table style and/or style options in one call.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+Apply a table style and/or style options in one call.
+
+- Operation ID: `tables.applyStyle`
+- API member path: `editor.doc.tables.applyStyle(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Expected result
+
+Returns a TableMutationResult receipt; reports NO_OP if the style and all provided options already match.
+
+## Input fields
+
+### Variant 1 (target.kind="block")
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `styleId` | string | no | |
+| `styleOptions` | object | no | |
+| `styleOptions.bandedColumns` | boolean | no | |
+| `styleOptions.bandedRows` | boolean | no | |
+| `styleOptions.firstColumn` | boolean | no | |
+| `styleOptions.headerRow` | boolean | no | |
+| `styleOptions.lastColumn` | boolean | no | |
+| `styleOptions.lastRow` | boolean | no | |
+| `styleOptions.totalRow` | boolean | no | |
+| `target` | BlockNodeAddress | yes | BlockNodeAddress |
+| `target.kind` | `"block"` | yes | Constant: `"block"` |
+| `target.nodeId` | string | yes | |
+| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` |
+
+### Variant 2 (required: nodeId)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `nodeId` | string | yes | |
+| `styleId` | string | no | |
+| `styleOptions` | object | no | |
+| `styleOptions.bandedColumns` | boolean | no | |
+| `styleOptions.bandedRows` | boolean | no | |
+| `styleOptions.firstColumn` | boolean | no | |
+| `styleOptions.headerRow` | boolean | no | |
+| `styleOptions.lastColumn` | boolean | no | |
+| `styleOptions.lastRow` | boolean | no | |
+| `styleOptions.totalRow` | boolean | no | |
+
+### Example request
+
+```json
+{
+ "styleId": "style-001",
+ "styleOptions": {
+ "headerRow": true,
+ "lastRow": true
+ },
+ "target": {
+ "kind": "block",
+ "nodeId": "node-def456",
+ "nodeType": "paragraph"
+ }
+}
+```
+
+## Output fields
+
+### Variant 1 (success=true)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `success` | `true` | yes | Constant: `true` |
+| `table` | TableAddress | no | TableAddress |
+| `table.kind` | `"block"` | no | Constant: `"block"` |
+| `table.nodeId` | string | no | |
+| `table.nodeType` | `"table"` | no | Constant: `"table"` |
+| `trackedChangeRefs` | EntityAddress[] | no | |
+
+### Variant 2 (success=false)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `failure` | object | yes | |
+| `failure.code` | enum | yes | `"NO_OP"`, `"INVALID_TARGET"`, `"TARGET_NOT_FOUND"`, `"CAPABILITY_UNAVAILABLE"` |
+| `failure.details` | any | no | |
+| `failure.message` | string | yes | |
+| `success` | `false` | yes | Constant: `false` |
+
+
+When present, `result.table` is the follow-up address to reuse after this call. For non-destructive table-targeted mutations, pass `result.table.nodeId` to the next table operation instead of re-running `find()`. Destructive operations may omit `table`.
+
+
+### Example response
+
+```json
+{
+ "success": true,
+ "table": {
+ "kind": "block",
+ "nodeId": "node-def456",
+ "nodeType": "table"
+ },
+ "trackedChangeRefs": [
+ {
+ "entityId": "entity-789",
+ "entityType": "comment",
+ "kind": "entity"
+ }
+ ]
+}
+```
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `INVALID_TARGET`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+- `INVALID_TARGET`
+- `INVALID_INPUT`
+
+## Raw schemas
+
+
+```json
+{
+ "additionalProperties": false,
+ "oneOf": [
+ {
+ "required": [
+ "target"
+ ]
+ },
+ {
+ "required": [
+ "nodeId"
+ ]
+ }
+ ],
+ "properties": {
+ "nodeId": {
+ "type": "string"
+ },
+ "styleId": {
+ "type": "string"
+ },
+ "styleOptions": {
+ "additionalProperties": false,
+ "properties": {
+ "bandedColumns": {
+ "type": "boolean"
+ },
+ "bandedRows": {
+ "type": "boolean"
+ },
+ "firstColumn": {
+ "type": "boolean"
+ },
+ "headerRow": {
+ "type": "boolean"
+ },
+ "lastColumn": {
+ "type": "boolean"
+ },
+ "lastRow": {
+ "type": "boolean"
+ },
+ "totalRow": {
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
+ "target": {
+ "$ref": "#/$defs/BlockNodeAddress"
+ }
+ },
+ "type": "object"
+}
+```
+
+
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "success": {
+ "const": true
+ },
+ "table": {
+ "$ref": "#/$defs/TableAddress"
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "$ref": "#/$defs/EntityAddress"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET",
+ "TARGET_NOT_FOUND",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "success": {
+ "const": true
+ },
+ "table": {
+ "$ref": "#/$defs/TableAddress"
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "$ref": "#/$defs/EntityAddress"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET",
+ "TARGET_NOT_FOUND",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
+
diff --git a/apps/docs/document-api/reference/tables/clear-border.mdx b/apps/docs/document-api/reference/tables/clear-border.mdx
index a28294b749..27e19d7783 100644
--- a/apps/docs/document-api/reference/tables/clear-border.mdx
+++ b/apps/docs/document-api/reference/tables/clear-border.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no borders are set.
| `target.nodeId` | string | yes | |
| `target.nodeType` | enum | yes | `"table"`, `"tableCell"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx b/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx
index d236c4022e..aa5e0bb0dd 100644
--- a/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx
+++ b/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx
@@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no custom cell spacing i
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/clear-contents.mdx b/apps/docs/document-api/reference/tables/clear-contents.mdx
index 6107c30574..41d2dff794 100644
--- a/apps/docs/document-api/reference/tables/clear-contents.mdx
+++ b/apps/docs/document-api/reference/tables/clear-contents.mdx
@@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target cells are alr
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/clear-shading.mdx b/apps/docs/document-api/reference/tables/clear-shading.mdx
index f8d5c18106..4d2d0e5a03 100644
--- a/apps/docs/document-api/reference/tables/clear-shading.mdx
+++ b/apps/docs/document-api/reference/tables/clear-shading.mdx
@@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no shading is set.
| `target.nodeId` | string | yes | |
| `target.nodeType` | enum | yes | `"table"`, `"tableCell"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/clear-style.mdx b/apps/docs/document-api/reference/tables/clear-style.mdx
index 0f9960d7b9..8408100322 100644
--- a/apps/docs/document-api/reference/tables/clear-style.mdx
+++ b/apps/docs/document-api/reference/tables/clear-style.mdx
@@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no table style is applie
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/convert-from-text.mdx b/apps/docs/document-api/reference/tables/convert-from-text.mdx
index 9f5726aafd..f2268069f2 100644
--- a/apps/docs/document-api/reference/tables/convert-from-text.mdx
+++ b/apps/docs/document-api/reference/tables/convert-from-text.mdx
@@ -38,7 +38,7 @@ Returns a TableMutationResult receipt confirming text was converted into a table
| `target.nodeId` | string | yes | |
| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/convert-to-text.mdx b/apps/docs/document-api/reference/tables/convert-to-text.mdx
index ca23f428b1..acc1fe1661 100644
--- a/apps/docs/document-api/reference/tables/convert-to-text.mdx
+++ b/apps/docs/document-api/reference/tables/convert-to-text.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table has no content
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/delete-cell.mdx b/apps/docs/document-api/reference/tables/delete-cell.mdx
index b6f1a90a42..bb01403f58 100644
--- a/apps/docs/document-api/reference/tables/delete-cell.mdx
+++ b/apps/docs/document-api/reference/tables/delete-cell.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target cell does not
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/delete-column.mdx b/apps/docs/document-api/reference/tables/delete-column.mdx
index 633a9b7be1..3df8c320e8 100644
--- a/apps/docs/document-api/reference/tables/delete-column.mdx
+++ b/apps/docs/document-api/reference/tables/delete-column.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target column does n
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/delete-row.mdx b/apps/docs/document-api/reference/tables/delete-row.mdx
index 04ca453d61..78467f7cf8 100644
--- a/apps/docs/document-api/reference/tables/delete-row.mdx
+++ b/apps/docs/document-api/reference/tables/delete-row.mdx
@@ -45,7 +45,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target row does not
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 3 (nodeId, rowIndex)
+### Variant 3 (required: nodeId, rowIndex)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/delete.mdx b/apps/docs/document-api/reference/tables/delete.mdx
index 02ce3fe50a..1aadf51ab3 100644
--- a/apps/docs/document-api/reference/tables/delete.mdx
+++ b/apps/docs/document-api/reference/tables/delete.mdx
@@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table was already re
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/distribute-columns.mdx b/apps/docs/document-api/reference/tables/distribute-columns.mdx
index 63c85074f1..760df06b82 100644
--- a/apps/docs/document-api/reference/tables/distribute-columns.mdx
+++ b/apps/docs/document-api/reference/tables/distribute-columns.mdx
@@ -38,7 +38,7 @@ Returns a TableMutationResult receipt; reports NO_OP if column widths are alread
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/distribute-rows.mdx b/apps/docs/document-api/reference/tables/distribute-rows.mdx
index 25a9b20e9f..4bbbafeac4 100644
--- a/apps/docs/document-api/reference/tables/distribute-rows.mdx
+++ b/apps/docs/document-api/reference/tables/distribute-rows.mdx
@@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if row heights are already
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/get-cells.mdx b/apps/docs/document-api/reference/tables/get-cells.mdx
index 73da376e62..9fded42784 100644
--- a/apps/docs/document-api/reference/tables/get-cells.mdx
+++ b/apps/docs/document-api/reference/tables/get-cells.mdx
@@ -37,7 +37,7 @@ Returns a TablesGetCellsOutput with cell information for the requested rows and
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/get-properties.mdx b/apps/docs/document-api/reference/tables/get-properties.mdx
index 84d95fd3af..41dbc26fcd 100644
--- a/apps/docs/document-api/reference/tables/get-properties.mdx
+++ b/apps/docs/document-api/reference/tables/get-properties.mdx
@@ -22,7 +22,7 @@ Retrieve layout and style properties of a table.
## Expected result
-Returns a TablesGetPropertiesOutput with the table layout, style, border, and shading properties.
+Returns a TablesGetPropertiesOutput with direct table layout and style state, including style options, borders, default cell margins, and cell spacing when explicitly set.
## Input fields
@@ -35,7 +35,7 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
@@ -63,6 +63,19 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh
| `address.nodeType` | `"table"` | yes | Constant: `"table"` |
| `alignment` | enum | no | `"left"`, `"center"`, `"right"` |
| `autoFitMode` | enum | no | `"fixedWidth"`, `"fitContents"`, `"fitWindow"` |
+| `borders` | object | no | |
+| `borders.bottom` | object \\| null | no | One of: object, null |
+| `borders.insideH` | object \\| null | no | One of: object, null |
+| `borders.insideV` | object \\| null | no | One of: object, null |
+| `borders.left` | object \\| null | no | One of: object, null |
+| `borders.right` | object \\| null | no | One of: object, null |
+| `borders.top` | object \\| null | no | One of: object, null |
+| `cellSpacingPt` | number | no | |
+| `defaultCellMargins` | object | no | |
+| `defaultCellMargins.bottomPt` | number | no | |
+| `defaultCellMargins.leftPt` | number | no | |
+| `defaultCellMargins.rightPt` | number | no | |
+| `defaultCellMargins.topPt` | number | no | |
| `direction` | enum | no | `"ltr"`, `"rtl"` |
| `nodeId` | string | yes | |
| `preferredWidth` | number | no | |
@@ -151,6 +164,207 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh
"fitWindow"
]
},
+ "borders": {
+ "additionalProperties": false,
+ "properties": {
+ "bottom": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "insideH": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "insideV": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "left": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "right": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "top": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "cellSpacingPt": {
+ "type": "number"
+ },
+ "defaultCellMargins": {
+ "additionalProperties": false,
+ "properties": {
+ "bottomPt": {
+ "type": "number"
+ },
+ "leftPt": {
+ "type": "number"
+ },
+ "rightPt": {
+ "type": "number"
+ },
+ "topPt": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ },
"direction": {
"enum": [
"ltr",
diff --git a/apps/docs/document-api/reference/tables/get.mdx b/apps/docs/document-api/reference/tables/get.mdx
index 2c13c59ab8..3629e66b88 100644
--- a/apps/docs/document-api/reference/tables/get.mdx
+++ b/apps/docs/document-api/reference/tables/get.mdx
@@ -35,7 +35,7 @@ Returns a TablesGetOutput with the table row count, column count, and structural
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/index.mdx b/apps/docs/document-api/reference/tables/index.mdx
index 3509affd8a..c244bf335a 100644
--- a/apps/docs/document-api/reference/tables/index.mdx
+++ b/apps/docs/document-api/reference/tables/index.mdx
@@ -54,6 +54,9 @@ For non-destructive table-targeted mutations, reuse `result.table.nodeId` from t
| tables.setCellPadding | `tables.setCellPadding` | Yes | `idempotent` | No | Yes |
| tables.setCellSpacing | `tables.setCellSpacing` | Yes | `idempotent` | No | Yes |
| tables.clearCellSpacing | `tables.clearCellSpacing` | Yes | `conditional` | No | Yes |
+| tables.applyStyle | `tables.applyStyle` | Yes | `conditional` | No | Yes |
+| tables.setBorders | `tables.setBorders` | Yes | `idempotent` | No | Yes |
+| tables.setTableOptions | `tables.setTableOptions` | Yes | `conditional` | No | Yes |
| tables.get | `tables.get` | No | `idempotent` | No | No |
| tables.getCells | `tables.getCells` | No | `idempotent` | No | No |
| tables.getProperties | `tables.getProperties` | No | `idempotent` | No | No |
diff --git a/apps/docs/document-api/reference/tables/insert-cell.mdx b/apps/docs/document-api/reference/tables/insert-cell.mdx
index 35dce13e25..2f87213145 100644
--- a/apps/docs/document-api/reference/tables/insert-cell.mdx
+++ b/apps/docs/document-api/reference/tables/insert-cell.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt confirming a cell was inserted.
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/insert-column.mdx b/apps/docs/document-api/reference/tables/insert-column.mdx
index efea30a190..dc4f38fa99 100644
--- a/apps/docs/document-api/reference/tables/insert-column.mdx
+++ b/apps/docs/document-api/reference/tables/insert-column.mdx
@@ -38,7 +38,7 @@ Returns a TableMutationResult receipt confirming a column was inserted.
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/insert-row.mdx b/apps/docs/document-api/reference/tables/insert-row.mdx
index 9131f2c93e..f008ae44db 100644
--- a/apps/docs/document-api/reference/tables/insert-row.mdx
+++ b/apps/docs/document-api/reference/tables/insert-row.mdx
@@ -49,7 +49,7 @@ Returns a TableMutationResult receipt confirming a row was inserted.
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 3 (nodeId, rowIndex, position)
+### Variant 3 (required: nodeId, rowIndex, position)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/merge-cells.mdx b/apps/docs/document-api/reference/tables/merge-cells.mdx
index e0756acb6d..f73022ff4d 100644
--- a/apps/docs/document-api/reference/tables/merge-cells.mdx
+++ b/apps/docs/document-api/reference/tables/merge-cells.mdx
@@ -41,7 +41,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the cells are already me
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/move.mdx b/apps/docs/document-api/reference/tables/move.mdx
index fa327ae64c..d434d86823 100644
--- a/apps/docs/document-api/reference/tables/move.mdx
+++ b/apps/docs/document-api/reference/tables/move.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table is already at
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-alt-text.mdx b/apps/docs/document-api/reference/tables/set-alt-text.mdx
index a19e4304e3..c7e82926a3 100644
--- a/apps/docs/document-api/reference/tables/set-alt-text.mdx
+++ b/apps/docs/document-api/reference/tables/set-alt-text.mdx
@@ -37,7 +37,7 @@ Returns a TableMutationResult receipt; reports NO_OP if alt text already matches
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
| `title` | string | no | |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-border.mdx b/apps/docs/document-api/reference/tables/set-border.mdx
index 974b272b1a..5067283251 100644
--- a/apps/docs/document-api/reference/tables/set-border.mdx
+++ b/apps/docs/document-api/reference/tables/set-border.mdx
@@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if border properties alread
| `target.nodeId` | string | yes | |
| `target.nodeType` | enum | yes | `"table"`, `"tableCell"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-borders.mdx b/apps/docs/document-api/reference/tables/set-borders.mdx
new file mode 100644
index 0000000000..c2c6c3ca71
--- /dev/null
+++ b/apps/docs/document-api/reference/tables/set-borders.mdx
@@ -0,0 +1,583 @@
+---
+title: tables.setBorders
+sidebarTitle: tables.setBorders
+description: Set borders on a table using a target set or per-edge patch.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+Set borders on a table using a target set or per-edge patch.
+
+- Operation ID: `tables.setBorders`
+- API member path: `editor.doc.tables.setBorders(...)`
+- Mutates document: `yes`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Expected result
+
+Returns a TableMutationResult receipt. Does not perform NO_OP detection.
+
+## Input fields
+
+### Variant 1.1 (target.kind="block")
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `applyTo` | enum | yes | `"all"`, `"outside"`, `"inside"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"insideH"`, `"insideV"` |
+| `border` | object \\| null | yes | One of: object, null |
+| `mode` | `"applyTo"` | yes | Constant: `"applyTo"` |
+| `target` | BlockNodeAddress | yes | BlockNodeAddress |
+| `target.kind` | `"block"` | yes | Constant: `"block"` |
+| `target.nodeId` | string | yes | |
+| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` |
+
+### Variant 1.2 (mode="applyTo")
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `applyTo` | enum | yes | `"all"`, `"outside"`, `"inside"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"insideH"`, `"insideV"` |
+| `border` | object \\| null | yes | One of: object, null |
+| `mode` | `"applyTo"` | yes | Constant: `"applyTo"` |
+| `nodeId` | string | yes | |
+
+### Variant 2.1 (target.kind="block")
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `edges` | object | yes | |
+| `edges.bottom` | object \\| null | no | One of: object, null |
+| `edges.insideH` | object \\| null | no | One of: object, null |
+| `edges.insideV` | object \\| null | no | One of: object, null |
+| `edges.left` | object \\| null | no | One of: object, null |
+| `edges.right` | object \\| null | no | One of: object, null |
+| `edges.top` | object \\| null | no | One of: object, null |
+| `mode` | `"edges"` | yes | Constant: `"edges"` |
+| `target` | BlockNodeAddress | yes | BlockNodeAddress |
+| `target.kind` | `"block"` | yes | Constant: `"block"` |
+| `target.nodeId` | string | yes | |
+| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` |
+
+### Variant 2.2 (mode="edges")
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `edges` | object | yes | |
+| `edges.bottom` | object \\| null | no | One of: object, null |
+| `edges.insideH` | object \\| null | no | One of: object, null |
+| `edges.insideV` | object \\| null | no | One of: object, null |
+| `edges.left` | object \\| null | no | One of: object, null |
+| `edges.right` | object \\| null | no | One of: object, null |
+| `edges.top` | object \\| null | no | One of: object, null |
+| `mode` | `"edges"` | yes | Constant: `"edges"` |
+| `nodeId` | string | yes | |
+
+### Example request
+
+```json
+{
+ "applyTo": "all",
+ "border": {
+ "color": "example",
+ "lineStyle": "example",
+ "lineWeightPt": 12.5
+ },
+ "mode": "applyTo",
+ "target": {
+ "kind": "block",
+ "nodeId": "node-def456",
+ "nodeType": "paragraph"
+ }
+}
+```
+
+## Output fields
+
+### Variant 1 (success=true)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `success` | `true` | yes | Constant: `true` |
+| `table` | TableAddress | no | TableAddress |
+| `table.kind` | `"block"` | no | Constant: `"block"` |
+| `table.nodeId` | string | no | |
+| `table.nodeType` | `"table"` | no | Constant: `"table"` |
+| `trackedChangeRefs` | EntityAddress[] | no | |
+
+### Variant 2 (success=false)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `failure` | object | yes | |
+| `failure.code` | enum | yes | `"NO_OP"`, `"INVALID_TARGET"`, `"TARGET_NOT_FOUND"`, `"CAPABILITY_UNAVAILABLE"` |
+| `failure.details` | any | no | |
+| `failure.message` | string | yes | |
+| `success` | `false` | yes | Constant: `false` |
+
+
+When present, `result.table` is the follow-up address to reuse after this call. For non-destructive table-targeted mutations, pass `result.table.nodeId` to the next table operation instead of re-running `find()`. Destructive operations may omit `table`.
+
+
+### Example response
+
+```json
+{
+ "success": true,
+ "table": {
+ "kind": "block",
+ "nodeId": "node-def456",
+ "nodeType": "table"
+ },
+ "trackedChangeRefs": [
+ {
+ "entityId": "entity-789",
+ "entityType": "comment",
+ "kind": "entity"
+ }
+ ]
+}
+```
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `INVALID_TARGET`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+- `INVALID_INPUT`
+
+## Raw schemas
+
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "oneOf": [
+ {
+ "required": [
+ "target"
+ ]
+ },
+ {
+ "required": [
+ "nodeId"
+ ]
+ }
+ ],
+ "properties": {
+ "applyTo": {
+ "enum": [
+ "all",
+ "outside",
+ "inside",
+ "top",
+ "bottom",
+ "left",
+ "right",
+ "insideH",
+ "insideV"
+ ]
+ },
+ "border": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "mode": {
+ "const": "applyTo"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "target": {
+ "$ref": "#/$defs/BlockNodeAddress"
+ }
+ },
+ "required": [
+ "mode",
+ "applyTo",
+ "border"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "oneOf": [
+ {
+ "required": [
+ "target"
+ ]
+ },
+ {
+ "required": [
+ "nodeId"
+ ]
+ }
+ ],
+ "properties": {
+ "edges": {
+ "additionalProperties": false,
+ "properties": {
+ "bottom": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "insideH": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "insideV": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "left": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "right": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "top": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "color": {
+ "pattern": "^([0-9A-Fa-f]{6}|auto)$",
+ "type": "string"
+ },
+ "lineStyle": {
+ "type": "string"
+ },
+ "lineWeightPt": {
+ "exclusiveMinimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "lineStyle",
+ "lineWeightPt",
+ "color"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "mode": {
+ "const": "edges"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "target": {
+ "$ref": "#/$defs/BlockNodeAddress"
+ }
+ },
+ "required": [
+ "mode",
+ "edges"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "success": {
+ "const": true
+ },
+ "table": {
+ "$ref": "#/$defs/TableAddress"
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "$ref": "#/$defs/EntityAddress"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET",
+ "TARGET_NOT_FOUND",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "success": {
+ "const": true
+ },
+ "table": {
+ "$ref": "#/$defs/TableAddress"
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "$ref": "#/$defs/EntityAddress"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET",
+ "TARGET_NOT_FOUND",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
+
diff --git a/apps/docs/document-api/reference/tables/set-cell-padding.mdx b/apps/docs/document-api/reference/tables/set-cell-padding.mdx
index 937854ce8b..fe353001a0 100644
--- a/apps/docs/document-api/reference/tables/set-cell-padding.mdx
+++ b/apps/docs/document-api/reference/tables/set-cell-padding.mdx
@@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if cell padding already mat
| `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` |
| `topPt` | number | yes | |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-cell-properties.mdx b/apps/docs/document-api/reference/tables/set-cell-properties.mdx
index 77a701c2d5..9a84bcd5d0 100644
--- a/apps/docs/document-api/reference/tables/set-cell-properties.mdx
+++ b/apps/docs/document-api/reference/tables/set-cell-properties.mdx
@@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if cell properties already
| `verticalAlign` | enum | no | `"top"`, `"center"`, `"bottom"` |
| `wrapText` | boolean | no | |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-cell-spacing.mdx b/apps/docs/document-api/reference/tables/set-cell-spacing.mdx
index dfe3eb3ca3..7a15321270 100644
--- a/apps/docs/document-api/reference/tables/set-cell-spacing.mdx
+++ b/apps/docs/document-api/reference/tables/set-cell-spacing.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if cell spacing already mat
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-column-width.mdx b/apps/docs/document-api/reference/tables/set-column-width.mdx
index 5842bbd083..51bb5f8252 100644
--- a/apps/docs/document-api/reference/tables/set-column-width.mdx
+++ b/apps/docs/document-api/reference/tables/set-column-width.mdx
@@ -37,7 +37,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the column width already
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
| `widthPt` | number | yes | |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-layout.mdx b/apps/docs/document-api/reference/tables/set-layout.mdx
index be32241c68..68411d7a4b 100644
--- a/apps/docs/document-api/reference/tables/set-layout.mdx
+++ b/apps/docs/document-api/reference/tables/set-layout.mdx
@@ -40,7 +40,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table already uses t
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-row-height.mdx b/apps/docs/document-api/reference/tables/set-row-height.mdx
index dc6eec5274..c0e4e29e05 100644
--- a/apps/docs/document-api/reference/tables/set-row-height.mdx
+++ b/apps/docs/document-api/reference/tables/set-row-height.mdx
@@ -49,7 +49,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the row height already m
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 3 (nodeId, rowIndex, heightPt, rule)
+### Variant 3 (required: nodeId, rowIndex, heightPt, rule)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-row-options.mdx b/apps/docs/document-api/reference/tables/set-row-options.mdx
index 7eebbe7b27..fe01ee5457 100644
--- a/apps/docs/document-api/reference/tables/set-row-options.mdx
+++ b/apps/docs/document-api/reference/tables/set-row-options.mdx
@@ -49,7 +49,7 @@ Returns a TableMutationResult receipt; reports NO_OP if row options already matc
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 3 (nodeId, rowIndex)
+### Variant 3 (required: nodeId, rowIndex)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-shading.mdx b/apps/docs/document-api/reference/tables/set-shading.mdx
index aa78309db9..d1d673560d 100644
--- a/apps/docs/document-api/reference/tables/set-shading.mdx
+++ b/apps/docs/document-api/reference/tables/set-shading.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if shading already matches.
| `target.nodeId` | string | yes | |
| `target.nodeType` | enum | yes | `"table"`, `"tableCell"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-style-option.mdx b/apps/docs/document-api/reference/tables/set-style-option.mdx
index ab9eec6685..baa3664b2e 100644
--- a/apps/docs/document-api/reference/tables/set-style-option.mdx
+++ b/apps/docs/document-api/reference/tables/set-style-option.mdx
@@ -37,7 +37,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the style option already
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-style.mdx b/apps/docs/document-api/reference/tables/set-style.mdx
index fe50034030..109ddab6c9 100644
--- a/apps/docs/document-api/reference/tables/set-style.mdx
+++ b/apps/docs/document-api/reference/tables/set-style.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table already uses t
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/set-table-options.mdx b/apps/docs/document-api/reference/tables/set-table-options.mdx
new file mode 100644
index 0000000000..e9ae63535e
--- /dev/null
+++ b/apps/docs/document-api/reference/tables/set-table-options.mdx
@@ -0,0 +1,333 @@
+---
+title: tables.setTableOptions
+sidebarTitle: tables.setTableOptions
+description: Set table-level default cell margins and/or cell spacing.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+Set table-level default cell margins and/or cell spacing.
+
+- Operation ID: `tables.setTableOptions`
+- API member path: `editor.doc.tables.setTableOptions(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Expected result
+
+Returns a TableMutationResult receipt; reports NO_OP if the provided values already match current direct formatting.
+
+## Input fields
+
+### Variant 1 (target.kind="block")
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `cellSpacingPt` | number \\| null | no | One of: number, null |
+| `defaultCellMargins` | object | no | |
+| `defaultCellMargins.bottomPt` | number | no | |
+| `defaultCellMargins.leftPt` | number | no | |
+| `defaultCellMargins.rightPt` | number | no | |
+| `defaultCellMargins.topPt` | number | no | |
+| `target` | BlockNodeAddress | yes | BlockNodeAddress |
+| `target.kind` | `"block"` | yes | Constant: `"block"` |
+| `target.nodeId` | string | yes | |
+| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` |
+
+### Variant 2 (required: nodeId)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `cellSpacingPt` | number \\| null | no | One of: number, null |
+| `defaultCellMargins` | object | no | |
+| `defaultCellMargins.bottomPt` | number | no | |
+| `defaultCellMargins.leftPt` | number | no | |
+| `defaultCellMargins.rightPt` | number | no | |
+| `defaultCellMargins.topPt` | number | no | |
+| `nodeId` | string | yes | |
+
+### Example request
+
+```json
+{
+ "cellSpacingPt": 12.5,
+ "defaultCellMargins": {
+ "bottomPt": 12.5,
+ "leftPt": 12.5,
+ "rightPt": 12.5,
+ "topPt": 12.5
+ },
+ "target": {
+ "kind": "block",
+ "nodeId": "node-def456",
+ "nodeType": "paragraph"
+ }
+}
+```
+
+## Output fields
+
+### Variant 1 (success=true)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `success` | `true` | yes | Constant: `true` |
+| `table` | TableAddress | no | TableAddress |
+| `table.kind` | `"block"` | no | Constant: `"block"` |
+| `table.nodeId` | string | no | |
+| `table.nodeType` | `"table"` | no | Constant: `"table"` |
+| `trackedChangeRefs` | EntityAddress[] | no | |
+
+### Variant 2 (success=false)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `failure` | object | yes | |
+| `failure.code` | enum | yes | `"NO_OP"`, `"INVALID_TARGET"`, `"TARGET_NOT_FOUND"`, `"CAPABILITY_UNAVAILABLE"` |
+| `failure.details` | any | no | |
+| `failure.message` | string | yes | |
+| `success` | `false` | yes | Constant: `false` |
+
+
+When present, `result.table` is the follow-up address to reuse after this call. For non-destructive table-targeted mutations, pass `result.table.nodeId` to the next table operation instead of re-running `find()`. Destructive operations may omit `table`.
+
+
+### Example response
+
+```json
+{
+ "success": true,
+ "table": {
+ "kind": "block",
+ "nodeId": "node-def456",
+ "nodeType": "table"
+ },
+ "trackedChangeRefs": [
+ {
+ "entityId": "entity-789",
+ "entityType": "comment",
+ "kind": "entity"
+ }
+ ]
+}
+```
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `INVALID_TARGET`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+- `INVALID_TARGET`
+- `INVALID_INPUT`
+
+## Raw schemas
+
+
+```json
+{
+ "additionalProperties": false,
+ "oneOf": [
+ {
+ "required": [
+ "target"
+ ]
+ },
+ {
+ "required": [
+ "nodeId"
+ ]
+ }
+ ],
+ "properties": {
+ "cellSpacingPt": {
+ "oneOf": [
+ {
+ "minimum": 0,
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "defaultCellMargins": {
+ "additionalProperties": false,
+ "properties": {
+ "bottomPt": {
+ "minimum": 0,
+ "type": "number"
+ },
+ "leftPt": {
+ "minimum": 0,
+ "type": "number"
+ },
+ "rightPt": {
+ "minimum": 0,
+ "type": "number"
+ },
+ "topPt": {
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "topPt",
+ "rightPt",
+ "bottomPt",
+ "leftPt"
+ ],
+ "type": "object"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "target": {
+ "$ref": "#/$defs/BlockNodeAddress"
+ }
+ },
+ "type": "object"
+}
+```
+
+
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "success": {
+ "const": true
+ },
+ "table": {
+ "$ref": "#/$defs/TableAddress"
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "$ref": "#/$defs/EntityAddress"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET",
+ "TARGET_NOT_FOUND",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "success": {
+ "const": true
+ },
+ "table": {
+ "$ref": "#/$defs/TableAddress"
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "$ref": "#/$defs/EntityAddress"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET",
+ "TARGET_NOT_FOUND",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
+
diff --git a/apps/docs/document-api/reference/tables/set-table-padding.mdx b/apps/docs/document-api/reference/tables/set-table-padding.mdx
index 9a84eeba6a..6a913f2e84 100644
--- a/apps/docs/document-api/reference/tables/set-table-padding.mdx
+++ b/apps/docs/document-api/reference/tables/set-table-padding.mdx
@@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if table padding already ma
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
| `topPt` | number | yes | |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/sort.mdx b/apps/docs/document-api/reference/tables/sort.mdx
index 5789fdd466..bc886a7817 100644
--- a/apps/docs/document-api/reference/tables/sort.mdx
+++ b/apps/docs/document-api/reference/tables/sort.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt confirming rows were reordered.
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/split-cell.mdx b/apps/docs/document-api/reference/tables/split-cell.mdx
index 3addd62123..6ff84e7f54 100644
--- a/apps/docs/document-api/reference/tables/split-cell.mdx
+++ b/apps/docs/document-api/reference/tables/split-cell.mdx
@@ -37,7 +37,7 @@ Returns a TableMutationResult receipt confirming the cell was split.
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/split.mdx b/apps/docs/document-api/reference/tables/split.mdx
index 0b22848008..43bbb480f1 100644
--- a/apps/docs/document-api/reference/tables/split.mdx
+++ b/apps/docs/document-api/reference/tables/split.mdx
@@ -36,7 +36,7 @@ Returns a TableMutationResult receipt confirming the table was split at the targ
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 2 (nodeId)
+### Variant 2 (required: nodeId)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-api/reference/tables/unmerge-cells.mdx b/apps/docs/document-api/reference/tables/unmerge-cells.mdx
index 784ecd07cc..d5c850c073 100644
--- a/apps/docs/document-api/reference/tables/unmerge-cells.mdx
+++ b/apps/docs/document-api/reference/tables/unmerge-cells.mdx
@@ -26,15 +26,20 @@ Returns a TableMutationResult receipt; reports NO_OP if the cell is not merged.
## Input fields
-### Variant 1 (target.nodeType="tableCell")
+### Variant 1.1 (target.nodeType="tableCell")
| Field | Type | Required | Description |
| --- | --- | --- | --- |
-| `nodeId` | string | no | |
-| `target` | TableCellAddress | no | TableCellAddress |
-| `target.kind` | `"block"` | no | Constant: `"block"` |
-| `target.nodeId` | string | no | |
-| `target.nodeType` | `"tableCell"` | no | Constant: `"tableCell"` |
+| `target` | TableCellAddress | yes | TableCellAddress |
+| `target.kind` | `"block"` | yes | Constant: `"block"` |
+| `target.nodeId` | string | yes | |
+| `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` |
+
+### Variant 1.2 (required: nodeId)
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `nodeId` | string | yes | |
### Variant 2 (target.nodeType="table")
@@ -47,7 +52,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the cell is not merged.
| `target.nodeId` | string | yes | |
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
-### Variant 3 (nodeId, rowIndex, columnIndex)
+### Variant 3 (required: nodeId, rowIndex, columnIndex)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx
index 2b0047b7f1..9d374487f1 100644
--- a/apps/docs/document-engine/sdks.mdx
+++ b/apps/docs/document-engine/sdks.mdx
@@ -715,6 +715,9 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
| `doc.tables.setCellPadding` | `tables set-cell-padding` | Set padding on a specific table cell or cell range. |
| `doc.tables.setCellSpacing` | `tables set-cell-spacing` | Set the cell spacing for the target table. |
| `doc.tables.clearCellSpacing` | `tables clear-cell-spacing` | Remove custom cell spacing from the target table. |
+| `doc.tables.applyStyle` | `tables apply-style` | Apply a table style and/or style options in one call. |
+| `doc.tables.setBorders` | `tables set-borders` | Set borders on a table using a target set or per-edge patch. |
+| `doc.tables.setTableOptions` | `tables set-table-options` | Set table-level default cell margins and/or cell spacing. |
| `doc.tables.get` | `tables get` | Retrieve table structure and dimensions by locator. |
| `doc.tables.getCells` | `tables get-cells` | Retrieve cell information for a table, optionally filtered by row or column. |
| `doc.tables.getProperties` | `tables get-properties` | Retrieve layout and style properties of a table. |
@@ -1160,6 +1163,9 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
| `doc.tables.set_cell_padding` | `tables set-cell-padding` | Set padding on a specific table cell or cell range. |
| `doc.tables.set_cell_spacing` | `tables set-cell-spacing` | Set the cell spacing for the target table. |
| `doc.tables.clear_cell_spacing` | `tables clear-cell-spacing` | Remove custom cell spacing from the target table. |
+| `doc.tables.apply_style` | `tables apply-style` | Apply a table style and/or style options in one call. |
+| `doc.tables.set_borders` | `tables set-borders` | Set borders on a table using a target set or per-edge patch. |
+| `doc.tables.set_table_options` | `tables set-table-options` | Set table-level default cell margins and/or cell spacing. |
| `doc.tables.get` | `tables get` | Retrieve table structure and dimensions by locator. |
| `doc.tables.get_cells` | `tables get-cells` | Retrieve cell information for a table, optionally filtered by row or column. |
| `doc.tables.get_properties` | `tables get-properties` | Retrieve layout and style properties of a table. |
diff --git a/packages/document-api/scripts/lib/reference-docs-artifacts.ts b/packages/document-api/scripts/lib/reference-docs-artifacts.ts
index 3e679f6b6f..0b1b6b52dd 100644
--- a/packages/document-api/scripts/lib/reference-docs-artifacts.ts
+++ b/packages/document-api/scripts/lib/reference-docs-artifacts.ts
@@ -439,6 +439,8 @@ function buildFieldSections(schema: JsonSchema, $defs: Defs): FieldSection[] {
const { resolved } = resolveRef(schema, $defs);
// Flatten allOf first — the merged schema may itself contain oneOf/anyOf.
const flat = flattenAllOf(resolved, $defs);
+ const sharedProperties = (flat.properties as Record | undefined) ?? undefined;
+ const sharedRequired = new Set(Array.isArray(flat.required) ? (flat.required as string[]) : []);
for (const keyword of ['oneOf', 'anyOf'] as const) {
const variants = flat[keyword];
@@ -446,60 +448,57 @@ function buildFieldSections(schema: JsonSchema, $defs: Defs): FieldSection[] {
return variants.flatMap((variant, index) => {
const resolvedVariant = flattenAllOf(resolveRef(variant as JsonSchema, $defs).resolved, $defs);
- const variantProperties = resolvedVariant.properties as Record | undefined;
- const parentProperties = flat.properties as Record | undefined;
+ const variantProperties = (resolvedVariant.properties as Record | undefined) ?? undefined;
+ const variantRequired = Array.isArray(resolvedVariant.required) ? (resolvedVariant.required as string[]) : [];
+ const variantRequiredSet = new Set(variantRequired);
const hasOwnProperties = !!variantProperties && Object.keys(variantProperties).length > 0;
- const variantRequired = new Set(
- Array.isArray(resolvedVariant.required) ? (resolvedVariant.required as string[]) : [],
- );
-
- // For schemas like `{ properties: {...}, oneOf: [{required:['target']}, {required:['nodeId']}] }`,
- // inherit the parent properties into each variant so the field table shows
- // the actual payload shape instead of `_No fields._`.
const hiddenFields = new Set();
- if (parentProperties && !hasOwnProperties) {
+ if (sharedProperties && !hasOwnProperties) {
for (let otherIndex = 0; otherIndex < variants.length; otherIndex++) {
if (otherIndex === index) continue;
const otherRequired = Array.isArray((variants[otherIndex] as JsonSchema).required)
? ((variants[otherIndex] as JsonSchema).required as string[])
: [];
for (const field of otherRequired) {
- if (!variantRequired.has(field)) hiddenFields.add(field);
+ if (!variantRequiredSet.has(field)) hiddenFields.add(field);
}
}
}
-
- const variantSchema =
- parentProperties && !hasOwnProperties
+ const visibleSharedProperties = sharedProperties
+ ? Object.fromEntries(Object.entries(sharedProperties).filter(([field]) => !hiddenFields.has(field)))
+ : undefined;
+ const mergedRequired = new Set(variantRequired);
+ for (const field of sharedRequired) mergedRequired.add(field);
+ const variantSchema: JsonSchema =
+ visibleSharedProperties || variantProperties
? {
...resolvedVariant,
type: 'object',
- properties: Object.fromEntries(
- Object.entries(parentProperties).filter(([field]) => !hiddenFields.has(field)),
- ),
+ properties: {
+ ...(visibleSharedProperties ?? {}),
+ ...(variantProperties ?? {}),
+ },
additionalProperties: resolvedVariant.additionalProperties ?? flat.additionalProperties ?? false,
- required: [
- ...new Set([...(Array.isArray(flat.required) ? (flat.required as string[]) : []), ...variantRequired]),
- ],
+ ...(mergedRequired.size > 0 ? { required: [...mergedRequired] } : {}),
}
: resolvedVariant;
-
+ const variantOnlyRequired = variantRequired.filter((field) => !sharedRequired.has(field));
const discriminators = collectConstDiscriminators(variantSchema, $defs);
const preferred = preferredDiscriminator(discriminators);
const variantLabelSuffix = preferred
? `${preferred.path}=${JSON.stringify(preferred.value)}`
- : variantRequired.size > 0
- ? [...variantRequired].join(', ')
+ : variantOnlyRequired.length > 0
+ ? `required: ${variantOnlyRequired.join(', ')}`
: undefined;
const label = variantLabelSuffix ? `Variant ${index + 1} (${variantLabelSuffix})` : `Variant ${index + 1}`;
- const rows = buildFieldRows(variantSchema, $defs);
- if (rows.length === 0 && hasTopLevelUnion(variantSchema)) {
+ if (hasTopLevelUnion(variantSchema)) {
return buildFieldSections(variantSchema, $defs).map((section) => ({
title: combineVariantTitles(label, section.title),
rows: section.rows,
}));
}
+ const rows = buildFieldRows(variantSchema, $defs);
return {
title: label,
rows,
diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts
index 5abf184735..5fef67cf9f 100644
--- a/packages/document-api/src/contract/operation-definitions.ts
+++ b/packages/document-api/src/contract/operation-definitions.ts
@@ -2645,6 +2645,58 @@ export const OPERATION_DEFINITIONS = {
referenceGroup: 'tables',
},
+ // -------------------------------------------------------------------------
+ // Tables: convenience operations (SD-2129)
+ // -------------------------------------------------------------------------
+
+ 'tables.applyStyle': {
+ memberPath: 'tables.applyStyle',
+ description: 'Apply a table style and/or style options in one call.',
+ expectedResult:
+ 'Returns a TableMutationResult receipt; reports NO_OP if the style and all provided options already match.',
+ requiresDocumentContext: true,
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'INVALID_INPUT'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'tables/apply-style.mdx',
+ referenceGroup: 'tables',
+ },
+ 'tables.setBorders': {
+ memberPath: 'tables.setBorders',
+ description: 'Set borders on a table using a target set or per-edge patch.',
+ expectedResult: 'Returns a TableMutationResult receipt. Does not perform NO_OP detection.',
+ requiresDocumentContext: true,
+ metadata: mutationOperation({
+ idempotency: 'idempotent',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['INVALID_TARGET', 'INVALID_INPUT'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'tables/set-borders.mdx',
+ referenceGroup: 'tables',
+ },
+ 'tables.setTableOptions': {
+ memberPath: 'tables.setTableOptions',
+ description: 'Set table-level default cell margins and/or cell spacing.',
+ expectedResult:
+ 'Returns a TableMutationResult receipt; reports NO_OP if the provided values already match current direct formatting.',
+ requiresDocumentContext: true,
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'INVALID_INPUT'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'tables/set-table-options.mdx',
+ referenceGroup: 'tables',
+ },
+
// -------------------------------------------------------------------------
// Tables: read operations (B4 ref handoff)
// -------------------------------------------------------------------------
@@ -2676,7 +2728,8 @@ export const OPERATION_DEFINITIONS = {
'tables.getProperties': {
memberPath: 'tables.getProperties',
description: 'Retrieve layout and style properties of a table.',
- expectedResult: 'Returns a TablesGetPropertiesOutput with the table layout, style, border, and shading properties.',
+ expectedResult:
+ 'Returns a TablesGetPropertiesOutput with direct table layout and style state, including style options, borders, default cell margins, and cell spacing when explicitly set.',
requiresDocumentContext: true,
metadata: readOperation({
idempotency: 'idempotent',
diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts
index 59e1640922..a27261b581 100644
--- a/packages/document-api/src/contract/operation-registry.ts
+++ b/packages/document-api/src/contract/operation-registry.ts
@@ -378,6 +378,9 @@ import type {
TablesSetCellPaddingInput,
TablesSetCellSpacingInput,
TablesClearCellSpacingInput,
+ TablesApplyStyleInput,
+ TablesSetBordersInput,
+ TablesSetTableOptionsInput,
TableMutationResult,
TablesGetInput,
TablesGetOutput,
@@ -886,6 +889,13 @@ export interface OperationRegistry extends FormatInlineAliasOperationRegistry {
options: MutationOptions;
output: TableMutationResult;
};
+ 'tables.applyStyle': { input: TablesApplyStyleInput; options: MutationOptions; output: TableMutationResult };
+ 'tables.setBorders': { input: TablesSetBordersInput; options: MutationOptions; output: TableMutationResult };
+ 'tables.setTableOptions': {
+ input: TablesSetTableOptionsInput;
+ options: MutationOptions;
+ output: TableMutationResult;
+ };
// --- tables.* reads ---
'tables.get': { input: TablesGetInput; options: never; output: TablesGetOutput };
diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts
index 9c3cb1922c..ab20be7b90 100644
--- a/packages/document-api/src/contract/schemas.ts
+++ b/packages/document-api/src/contract/schemas.ts
@@ -1454,6 +1454,20 @@ const capabilitiesOutputSchema = objectSchema(
);
const strictEmptyObjectSchema = objectSchema({});
+const tableBorderColorPattern = '^([0-9A-Fa-f]{6}|auto)$';
+
+const tableBorderSpecSchema = objectSchema(
+ {
+ lineStyle: { type: 'string' },
+ lineWeightPt: { type: 'number', exclusiveMinimum: 0 },
+ color: { type: 'string', pattern: tableBorderColorPattern },
+ },
+ ['lineStyle', 'lineWeightPt', 'color'],
+);
+
+const nullableTableBorderSpecSchema: JsonSchema = {
+ oneOf: [tableBorderSpecSchema, { type: 'null' }],
+};
const sdFragmentSchema: JsonSchema = {
oneOf: [{ type: 'object' }, { type: 'array', items: { type: 'object' } }],
@@ -5207,6 +5221,96 @@ const operationSchemas: Record = {
failure: tableMutationFailureSchema,
},
+ // --- tables.* convenience operations (SD-2129) ---
+
+ 'tables.applyStyle': {
+ input: {
+ ...objectSchema({
+ target: blockNodeAddressSchema,
+ nodeId: { type: 'string' },
+ styleId: { type: 'string' },
+ styleOptions: objectSchema({
+ headerRow: { type: 'boolean' },
+ lastRow: { type: 'boolean' },
+ totalRow: { type: 'boolean' },
+ firstColumn: { type: 'boolean' },
+ lastColumn: { type: 'boolean' },
+ bandedRows: { type: 'boolean' },
+ bandedColumns: { type: 'boolean' },
+ }),
+ }),
+ oneOf: [{ required: ['target'] }, { required: ['nodeId'] }],
+ },
+ output: tableMutationResultSchema,
+ success: tableMutationSuccessSchema,
+ failure: tableMutationFailureSchema,
+ },
+ 'tables.setBorders': {
+ input: {
+ oneOf: [
+ {
+ ...objectSchema(
+ {
+ target: blockNodeAddressSchema,
+ nodeId: { type: 'string' },
+ mode: { const: 'applyTo' },
+ applyTo: {
+ enum: ['all', 'outside', 'inside', 'top', 'bottom', 'left', 'right', 'insideH', 'insideV'],
+ },
+ border: nullableTableBorderSpecSchema,
+ },
+ ['mode', 'applyTo', 'border'],
+ ),
+ oneOf: [{ required: ['target'] }, { required: ['nodeId'] }],
+ },
+ {
+ ...objectSchema(
+ {
+ target: blockNodeAddressSchema,
+ nodeId: { type: 'string' },
+ mode: { const: 'edges' },
+ edges: objectSchema({
+ top: nullableTableBorderSpecSchema,
+ bottom: nullableTableBorderSpecSchema,
+ left: nullableTableBorderSpecSchema,
+ right: nullableTableBorderSpecSchema,
+ insideH: nullableTableBorderSpecSchema,
+ insideV: nullableTableBorderSpecSchema,
+ }),
+ },
+ ['mode', 'edges'],
+ ),
+ oneOf: [{ required: ['target'] }, { required: ['nodeId'] }],
+ },
+ ],
+ },
+ output: tableMutationResultSchema,
+ success: tableMutationSuccessSchema,
+ failure: tableMutationFailureSchema,
+ },
+ 'tables.setTableOptions': {
+ input: {
+ ...objectSchema({
+ target: blockNodeAddressSchema,
+ nodeId: { type: 'string' },
+ defaultCellMargins: objectSchema(
+ {
+ topPt: { type: 'number', minimum: 0 },
+ rightPt: { type: 'number', minimum: 0 },
+ bottomPt: { type: 'number', minimum: 0 },
+ leftPt: { type: 'number', minimum: 0 },
+ },
+ ['topPt', 'rightPt', 'bottomPt', 'leftPt'],
+ ),
+ cellSpacingPt: { oneOf: [{ type: 'number', minimum: 0 }, { type: 'null' }] },
+ }),
+ oneOf: [{ required: ['target'] }, { required: ['nodeId'] }],
+ },
+ output: tableMutationResultSchema,
+ success: tableMutationSuccessSchema,
+ failure: tableMutationFailureSchema,
+ },
+
// --- tables.* reads (B4 ref handoff) ---
'tables.get': {
@@ -5272,6 +5376,21 @@ const operationSchemas: Record = {
bandedRows: { type: 'boolean' },
bandedColumns: { type: 'boolean' },
}),
+ borders: objectSchema({
+ top: nullableTableBorderSpecSchema,
+ bottom: nullableTableBorderSpecSchema,
+ left: nullableTableBorderSpecSchema,
+ right: nullableTableBorderSpecSchema,
+ insideH: nullableTableBorderSpecSchema,
+ insideV: nullableTableBorderSpecSchema,
+ }),
+ defaultCellMargins: objectSchema({
+ topPt: { type: 'number' },
+ rightPt: { type: 'number' },
+ bottomPt: { type: 'number' },
+ leftPt: { type: 'number' },
+ }),
+ cellSpacingPt: { type: 'number' },
},
['nodeId', 'address'],
),
diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts
index 4aebb53f77..84a97b1dc4 100644
--- a/packages/document-api/src/index.ts
+++ b/packages/document-api/src/index.ts
@@ -265,6 +265,9 @@ import type {
TablesSetCellPaddingInput,
TablesSetCellSpacingInput,
TablesClearCellSpacingInput,
+ TablesApplyStyleInput,
+ TablesSetBordersInput,
+ TablesSetTableOptionsInput,
TablesGetInput,
TablesGetOutput,
TablesGetCellsInput,
@@ -316,6 +319,9 @@ import {
executeRowLocatorOp,
executeCellOrTableScopedCellLocatorOp,
executeDocumentLevelTableOp,
+ executeTablesApplyStyle,
+ executeTablesSetBorders,
+ executeTablesSetTableOptions,
} from './tables/tables.js';
import type {
ParagraphsAdapter,
@@ -1367,6 +1373,9 @@ export interface TablesApi {
setCellPadding(input: TablesSetCellPaddingInput, options?: MutationOptions): TableMutationResult;
setCellSpacing(input: TablesSetCellSpacingInput, options?: MutationOptions): TableMutationResult;
clearCellSpacing(input: TablesClearCellSpacingInput, options?: MutationOptions): TableMutationResult;
+ applyStyle(input: TablesApplyStyleInput, options?: MutationOptions): TableMutationResult;
+ setBorders(input: TablesSetBordersInput, options?: MutationOptions): TableMutationResult;
+ setTableOptions(input: TablesSetTableOptionsInput, options?: MutationOptions): TableMutationResult;
get(input: TablesGetInput): TablesGetOutput;
getCells(input: TablesGetCellsInput): TablesGetCellsOutput;
getProperties(input: TablesGetPropertiesInput): TablesGetPropertiesOutput;
@@ -2451,6 +2460,30 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi {
options,
);
},
+ applyStyle(input, options?) {
+ return executeTablesApplyStyle(
+ 'tables.applyStyle',
+ adapters.tables.applyStyle.bind(adapters.tables),
+ input,
+ options,
+ );
+ },
+ setBorders(input, options?) {
+ return executeTablesSetBorders(
+ 'tables.setBorders',
+ adapters.tables.setBorders.bind(adapters.tables),
+ input,
+ options,
+ );
+ },
+ setTableOptions(input, options?) {
+ return executeTablesSetTableOptions(
+ 'tables.setTableOptions',
+ adapters.tables.setTableOptions.bind(adapters.tables),
+ input,
+ options,
+ );
+ },
get(input) {
return adapters.tables.get(input);
},
diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts
index 87a3594f90..14082d8c0c 100644
--- a/packages/document-api/src/invoke/invoke.ts
+++ b/packages/document-api/src/invoke/invoke.ts
@@ -246,6 +246,9 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable {
'tables.setCellPadding': (input, options) => api.tables.setCellPadding(input, options),
'tables.setCellSpacing': (input, options) => api.tables.setCellSpacing(input, options),
'tables.clearCellSpacing': (input, options) => api.tables.clearCellSpacing(input, options),
+ 'tables.applyStyle': (input, options) => api.tables.applyStyle(input, options),
+ 'tables.setBorders': (input, options) => api.tables.setBorders(input, options),
+ 'tables.setTableOptions': (input, options) => api.tables.setTableOptions(input, options),
// --- tables.* reads ---
'tables.get': (input) => api.tables.get(input),
diff --git a/packages/document-api/src/tables/tables.test.ts b/packages/document-api/src/tables/tables.test.ts
new file mode 100644
index 0000000000..05acd65faa
--- /dev/null
+++ b/packages/document-api/src/tables/tables.test.ts
@@ -0,0 +1,267 @@
+import { describe, expect, it, vi } from 'vitest';
+import { executeTablesApplyStyle, executeTablesSetBorders, executeTablesSetTableOptions } from './tables.js';
+import { DocumentApiValidationError } from '../errors.js';
+
+const MOCK_ADAPTER = vi.fn(() => ({ success: true }));
+const nodeId = 'table-1';
+
+describe('executeTablesApplyStyle validation', () => {
+ it('rejects when neither styleId nor styleOptions is provided', () => {
+ expect(() => executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId } as any)).toThrow(
+ DocumentApiValidationError,
+ );
+ });
+
+ it('rejects empty string styleId', () => {
+ expect(() => executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleId: '' } as any)).toThrow(
+ 'non-empty string',
+ );
+ });
+
+ it('rejects empty styleOptions when styleId is absent', () => {
+ expect(() =>
+ executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: {} } as any),
+ ).toThrow('at least one flag');
+ });
+
+ it('allows empty styleOptions when styleId is present', () => {
+ expect(() =>
+ executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleId: 'X', styleOptions: {} } as any),
+ ).not.toThrow();
+ });
+
+ it('rejects unknown styleOptions keys', () => {
+ expect(() =>
+ executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, {
+ nodeId,
+ styleOptions: { unknownFlag: true },
+ } as any),
+ ).toThrow('unrecognized');
+ });
+
+ it('rejects styleOptions: null with INVALID_INPUT', () => {
+ expect(() =>
+ executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: null } as any),
+ ).toThrow('plain object');
+ });
+
+ it('rejects styleOptions: true with INVALID_INPUT', () => {
+ expect(() =>
+ executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: true } as any),
+ ).toThrow('plain object');
+ });
+
+ it('rejects styleOptions: 5 with INVALID_INPUT', () => {
+ expect(() =>
+ executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: 5 } as any),
+ ).toThrow('plain object');
+ });
+
+ it('accepts valid styleId and styleOptions', () => {
+ expect(() =>
+ executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, {
+ nodeId,
+ styleId: 'TableGrid',
+ styleOptions: { headerRow: true, bandedRows: false },
+ } as any),
+ ).not.toThrow();
+ });
+});
+
+describe('executeTablesSetBorders validation', () => {
+ it('rejects missing mode', () => {
+ expect(() => executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { nodeId } as any)).toThrow('mode');
+ });
+
+ it('rejects invalid applyTo value', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'diagonal',
+ border: null,
+ } as any),
+ ).toThrow('applyTo');
+ });
+
+ it('rejects applyTo mode without border field', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ } as any),
+ ).toThrow('border is required');
+ });
+
+ it('rejects lineWeightPt: 0 in border spec', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 0, color: '000000' },
+ } as any),
+ ).toThrow('positive');
+ });
+
+ it('rejects empty lineStyle string', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: '', lineWeightPt: 1, color: '000000' },
+ } as any),
+ ).toThrow('non-empty string');
+ });
+
+ it('rejects empty color string', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '' },
+ } as any),
+ ).toThrow('non-empty string');
+ });
+
+ it('rejects non-hex color strings', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: 'red' },
+ } as any),
+ ).toThrow('6-digit hex color');
+ });
+
+ it('rejects edges mode with empty edges object', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'edges',
+ edges: {},
+ } as any),
+ ).toThrow('at least one');
+ });
+
+ it('rejects invalid nested border spec in edges mode', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'edges',
+ edges: { top: { lineStyle: 'single', lineWeightPt: 1 } }, // missing color
+ } as any),
+ ).toThrow('color');
+ });
+
+ it('accepts valid applyTo mode with null border', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: null,
+ } as any),
+ ).not.toThrow();
+ });
+
+ it('accepts color: auto', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: 'auto' },
+ } as any),
+ ).not.toThrow();
+ });
+
+ it('accepts valid edges mode', () => {
+ expect(() =>
+ executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, {
+ nodeId,
+ mode: 'edges',
+ edges: {
+ top: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ insideH: null,
+ },
+ } as any),
+ ).not.toThrow();
+ });
+});
+
+describe('executeTablesSetTableOptions validation', () => {
+ it('rejects when neither margins nor spacing is provided', () => {
+ expect(() => executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { nodeId } as any)).toThrow(
+ 'at least one',
+ );
+ });
+
+ it('rejects defaultCellMargins: null with INVALID_INPUT', () => {
+ expect(() =>
+ executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, {
+ nodeId,
+ defaultCellMargins: null,
+ } as any),
+ ).toThrow('plain object');
+ });
+
+ it('rejects defaultCellMargins: true with INVALID_INPUT', () => {
+ expect(() =>
+ executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, {
+ nodeId,
+ defaultCellMargins: true,
+ } as any),
+ ).toThrow('plain object');
+ });
+
+ it('rejects negative margin value', () => {
+ expect(() =>
+ executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, {
+ nodeId,
+ defaultCellMargins: { topPt: -1, rightPt: 0, bottomPt: 0, leftPt: 0 },
+ } as any),
+ ).toThrow('non-negative');
+ });
+
+ it('rejects negative cellSpacingPt', () => {
+ expect(() =>
+ executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, {
+ nodeId,
+ cellSpacingPt: -1,
+ } as any),
+ ).toThrow('non-negative');
+ });
+
+ it('accepts cellSpacingPt: 0', () => {
+ expect(() =>
+ executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, {
+ nodeId,
+ cellSpacingPt: 0,
+ } as any),
+ ).not.toThrow();
+ });
+
+ it('accepts cellSpacingPt: null', () => {
+ expect(() =>
+ executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, {
+ nodeId,
+ cellSpacingPt: null,
+ } as any),
+ ).not.toThrow();
+ });
+
+ it('accepts valid margins and spacing', () => {
+ expect(() =>
+ executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, {
+ nodeId,
+ defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 },
+ cellSpacingPt: 2,
+ } as any),
+ ).not.toThrow();
+ });
+});
diff --git a/packages/document-api/src/tables/tables.ts b/packages/document-api/src/tables/tables.ts
index 5817f4a4a0..35b3f8b660 100644
--- a/packages/document-api/src/tables/tables.ts
+++ b/packages/document-api/src/tables/tables.ts
@@ -1,6 +1,13 @@
import type { MutationOptions } from '../write/write.js';
import { normalizeMutationOptions } from '../write/write.js';
import { DocumentApiValidationError } from '../errors.js';
+import type {
+ TablesApplyStyleInput,
+ TablesSetBordersInput,
+ TablesSetTableOptionsInput,
+ TableBorderSpec,
+ TableStyleOptionsPatch,
+} from '../types/table-operations.types.js';
// ---------------------------------------------------------------------------
// Locator validation
@@ -194,3 +201,294 @@ export function executeDocumentLevelTableOp(
): TResult {
return adapter(input, normalizeMutationOptions(options));
}
+
+// ---------------------------------------------------------------------------
+// Convenience operation validation helpers
+// ---------------------------------------------------------------------------
+
+const VALID_STYLE_OPTION_FLAGS = new Set([
+ 'headerRow',
+ 'lastRow',
+ 'totalRow',
+ 'firstColumn',
+ 'lastColumn',
+ 'bandedRows',
+ 'bandedColumns',
+]);
+
+function validateStyleOptionsPatch(options: TableStyleOptionsPatch, operationName: string): void {
+ const keys = Object.keys(options);
+ for (const key of keys) {
+ if (!VALID_STYLE_OPTION_FLAGS.has(key)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: unrecognized style option flag "${key}".`,
+ { field: 'styleOptions', value: key },
+ );
+ }
+ if (typeof options[key as keyof TableStyleOptionsPatch] !== 'boolean') {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: style option "${key}" must be a boolean.`,
+ { field: `styleOptions.${key}` },
+ );
+ }
+ }
+}
+
+function validateBorderSpec(spec: TableBorderSpec, fieldPath: string, operationName: string): void {
+ if (typeof spec !== 'object' || spec === null) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: ${fieldPath} must be a border spec object.`,
+ );
+ }
+ if (typeof spec.lineStyle !== 'string' || spec.lineStyle.length === 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: ${fieldPath}.lineStyle must be a non-empty string.`,
+ {
+ field: `${fieldPath}.lineStyle`,
+ },
+ );
+ }
+ if (typeof spec.lineWeightPt !== 'number' || !Number.isFinite(spec.lineWeightPt) || spec.lineWeightPt <= 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: ${fieldPath}.lineWeightPt must be a positive number.`,
+ { field: `${fieldPath}.lineWeightPt` },
+ );
+ }
+ if (typeof spec.color !== 'string' || spec.color.length === 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: ${fieldPath}.color must be a non-empty string.`,
+ {
+ field: `${fieldPath}.color`,
+ },
+ );
+ }
+ if (!TABLE_BORDER_COLOR_PATTERN.test(spec.color)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: ${fieldPath}.color must be a 6-digit hex color without "#" or "auto".`,
+ {
+ field: `${fieldPath}.color`,
+ },
+ );
+ }
+}
+
+function validateBorderPatchEdge(
+ value: TableBorderSpec | null | undefined,
+ edgeName: string,
+ operationName: string,
+): void {
+ if (value === undefined || value === null) return;
+ validateBorderSpec(value, `edges.${edgeName}`, operationName);
+}
+
+const TABLE_BORDER_COLOR_PATTERN = /^([0-9A-Fa-f]{6}|auto)$/u;
+
+const VALID_APPLY_TO_VALUES = new Set([
+ 'all',
+ 'outside',
+ 'inside',
+ 'top',
+ 'bottom',
+ 'left',
+ 'right',
+ 'insideH',
+ 'insideV',
+]);
+
+const VALID_BORDER_EDGE_KEYS = new Set(['top', 'bottom', 'left', 'right', 'insideH', 'insideV']);
+
+// ---------------------------------------------------------------------------
+// Convenience operation execute wrappers
+// ---------------------------------------------------------------------------
+
+/**
+ * Validate and execute `tables.applyStyle`.
+ */
+export function executeTablesApplyStyle(
+ operationName: string,
+ adapter: (input: TablesApplyStyleInput, options?: MutationOptions) => TResult,
+ input: TablesApplyStyleInput,
+ options?: MutationOptions,
+): TResult {
+ validateTableLocator(input, operationName);
+
+ const hasStyleId = input.styleId !== undefined;
+ const hasOptions = input.styleOptions !== undefined;
+
+ if (!hasStyleId && !hasOptions) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName} requires at least one of styleId or styleOptions.`,
+ );
+ }
+
+ if (hasStyleId && typeof input.styleId !== 'string') {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: styleId must be a string.`, {
+ field: 'styleId',
+ });
+ }
+
+ if (hasStyleId && input.styleId === '') {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: styleId must be a non-empty string. Use tables.clearStyle to remove a style.`,
+ { field: 'styleId' },
+ );
+ }
+
+ if (hasOptions) {
+ if (typeof input.styleOptions !== 'object' || input.styleOptions === null || Array.isArray(input.styleOptions)) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: styleOptions must be a plain object.`, {
+ field: 'styleOptions',
+ });
+ }
+ const optionKeys = Object.keys(input.styleOptions);
+ if (!hasStyleId && optionKeys.length === 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: styleOptions must contain at least one flag when styleId is absent.`,
+ { field: 'styleOptions' },
+ );
+ }
+ if (optionKeys.length > 0) {
+ validateStyleOptionsPatch(input.styleOptions!, operationName);
+ }
+ }
+
+ return adapter(input, normalizeMutationOptions(options));
+}
+
+/**
+ * Validate and execute `tables.setBorders`.
+ */
+export function executeTablesSetBorders(
+ operationName: string,
+ adapter: (input: TablesSetBordersInput, options?: MutationOptions) => TResult,
+ input: TablesSetBordersInput,
+ options?: MutationOptions,
+): TResult {
+ validateTableLocator(input, operationName);
+
+ if (!('mode' in input) || (input.mode !== 'applyTo' && input.mode !== 'edges')) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: mode must be "applyTo" or "edges".`, {
+ field: 'mode',
+ });
+ }
+
+ if (input.mode === 'applyTo') {
+ if (!VALID_APPLY_TO_VALUES.has(input.applyTo)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: applyTo must be one of: ${[...VALID_APPLY_TO_VALUES].join(', ')}.`,
+ { field: 'applyTo' },
+ );
+ }
+ if (input.border === undefined) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: border is required when mode is "applyTo".`,
+ { field: 'border' },
+ );
+ }
+ if (input.border !== null) {
+ validateBorderSpec(input.border, 'border', operationName);
+ }
+ }
+
+ if (input.mode === 'edges') {
+ if (!input.edges || typeof input.edges !== 'object') {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: edges is required when mode is "edges".`,
+ { field: 'edges' },
+ );
+ }
+ const edgeKeys = Object.keys(input.edges);
+ const definedKeys = edgeKeys.filter(
+ (k) => VALID_BORDER_EDGE_KEYS.has(k) && input.edges[k as keyof typeof input.edges] !== undefined,
+ );
+ if (definedKeys.length === 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: edges must contain at least one defined edge.`,
+ { field: 'edges' },
+ );
+ }
+ for (const key of edgeKeys) {
+ if (!VALID_BORDER_EDGE_KEYS.has(key)) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: unrecognized edge "${key}".`, {
+ field: `edges.${key}`,
+ });
+ }
+ validateBorderPatchEdge(input.edges[key as keyof typeof input.edges], key, operationName);
+ }
+ }
+
+ return adapter(input, normalizeMutationOptions(options));
+}
+
+/**
+ * Validate and execute `tables.setTableOptions`.
+ */
+export function executeTablesSetTableOptions(
+ operationName: string,
+ adapter: (input: TablesSetTableOptionsInput, options?: MutationOptions) => TResult,
+ input: TablesSetTableOptionsInput,
+ options?: MutationOptions,
+): TResult {
+ validateTableLocator(input, operationName);
+
+ const hasMargins = input.defaultCellMargins !== undefined;
+ const hasSpacing = input.cellSpacingPt !== undefined;
+
+ if (!hasMargins && !hasSpacing) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName} requires at least one of defaultCellMargins or cellSpacingPt.`,
+ );
+ }
+
+ if (hasMargins) {
+ if (
+ typeof input.defaultCellMargins !== 'object' ||
+ input.defaultCellMargins === null ||
+ Array.isArray(input.defaultCellMargins)
+ ) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: defaultCellMargins must be a plain object with topPt, rightPt, bottomPt, leftPt.`,
+ { field: 'defaultCellMargins' },
+ );
+ }
+ const m = input.defaultCellMargins;
+ const sides = ['topPt', 'rightPt', 'bottomPt', 'leftPt'] as const;
+ for (const side of sides) {
+ if (typeof m[side] !== 'number' || !Number.isFinite(m[side]) || m[side] < 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: defaultCellMargins.${side} must be a non-negative number.`,
+ { field: `defaultCellMargins.${side}` },
+ );
+ }
+ }
+ }
+
+ if (hasSpacing && input.cellSpacingPt !== null) {
+ if (typeof input.cellSpacingPt !== 'number' || !Number.isFinite(input.cellSpacingPt) || input.cellSpacingPt < 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${operationName}: cellSpacingPt must be a non-negative number or null.`,
+ { field: 'cellSpacingPt' },
+ );
+ }
+ }
+
+ return adapter(input, normalizeMutationOptions(options));
+}
diff --git a/packages/document-api/src/types/table-operations.types.ts b/packages/document-api/src/types/table-operations.types.ts
index f6332cf52e..7059931b60 100644
--- a/packages/document-api/src/types/table-operations.types.ts
+++ b/packages/document-api/src/types/table-operations.types.ts
@@ -345,6 +345,149 @@ export interface TablesSetStyleOptionInput extends TableLocator {
enabled: boolean;
}
+// ---------------------------------------------------------------------------
+// Shared table-formatting types (used by both reads and writes)
+// ---------------------------------------------------------------------------
+
+/** Border spec for a single edge. Values are raw OOXML (line style, color). */
+export interface TableBorderSpec {
+ /** Raw OOXML `ST_Border` value (e.g., `single`, `double`, `dotted`). */
+ lineStyle: string;
+ /** Border weight in points. Must be positive (0 is rejected). */
+ lineWeightPt: number;
+ /** Uppercase hex without `#` (e.g., `000000`), or `auto`. */
+ color: string;
+}
+
+// ---------------------------------------------------------------------------
+// Write-only patch types (used by mutation inputs)
+// ---------------------------------------------------------------------------
+
+/** All four sides required when present. */
+export interface TableMargins {
+ topPt: number;
+ rightPt: number;
+ bottomPt: number;
+ leftPt: number;
+}
+
+/** Omitted flag = leave unchanged. */
+export interface TableStyleOptionsPatch {
+ headerRow?: boolean;
+ lastRow?: boolean;
+ /** @deprecated Use `lastRow` instead. */
+ totalRow?: boolean;
+ firstColumn?: boolean;
+ lastColumn?: boolean;
+ bandedRows?: boolean;
+ bandedColumns?: boolean;
+}
+
+/**
+ * Per-edge border patch for writes.
+ * - `null` = clear this edge (write explicit "no border")
+ * - Omitted = leave this edge unchanged
+ */
+export interface TableBorderPatch {
+ top?: TableBorderSpec | null;
+ bottom?: TableBorderSpec | null;
+ left?: TableBorderSpec | null;
+ right?: TableBorderSpec | null;
+ insideH?: TableBorderSpec | null;
+ insideV?: TableBorderSpec | null;
+}
+
+// ---------------------------------------------------------------------------
+// Read-only state types (used by getProperties output)
+// ---------------------------------------------------------------------------
+
+/** Absent key = no direct formatting for this flag. */
+export interface TableStyleOptionsState {
+ headerRow?: boolean;
+ lastRow?: boolean;
+ firstColumn?: boolean;
+ lastColumn?: boolean;
+ bandedRows?: boolean;
+ bandedColumns?: boolean;
+}
+
+/**
+ * Three states per edge:
+ * - Absent key = no direct formatting on this edge
+ * - `null` = explicit direct clear (overrides style-inherited borders)
+ * - `TableBorderSpec` = explicit direct border spec
+ */
+export interface TableBorderState {
+ top?: TableBorderSpec | null;
+ bottom?: TableBorderSpec | null;
+ left?: TableBorderSpec | null;
+ right?: TableBorderSpec | null;
+ insideH?: TableBorderSpec | null;
+ insideV?: TableBorderSpec | null;
+}
+
+/** Absent key = no direct formatting for this side. */
+export interface TableMarginsState {
+ topPt?: number;
+ rightPt?: number;
+ bottomPt?: number;
+ leftPt?: number;
+}
+
+// ---------------------------------------------------------------------------
+// Convenience operation inputs
+// ---------------------------------------------------------------------------
+
+/**
+ * Apply a table style and/or style options in one call.
+ * At least one of `styleId` or `styleOptions` is required.
+ */
+export interface TablesApplyStyleInput extends TableLocator {
+ /** Table style ID. Not validated against the style catalog. */
+ styleId?: string;
+ /** Style option flags to merge into `tblLook`. Omitted flags are left unchanged. */
+ styleOptions?: TableStyleOptionsPatch;
+}
+
+/** Target set for the `applyTo` mode of `setBorders`. */
+export type TableBorderApplyTo =
+ | 'all'
+ | 'outside'
+ | 'inside'
+ | 'top'
+ | 'bottom'
+ | 'left'
+ | 'right'
+ | 'insideH'
+ | 'insideV';
+
+/**
+ * Set borders on a table. Two modes:
+ * - `applyTo`: apply one border spec (or `null` to clear) to a named target set
+ * - `edges`: apply a per-edge patch
+ */
+export type TablesSetBordersInput =
+ | (TableLocator & {
+ mode: 'applyTo';
+ applyTo: TableBorderApplyTo;
+ border: TableBorderSpec | null;
+ })
+ | (TableLocator & {
+ mode: 'edges';
+ edges: TableBorderPatch;
+ });
+
+/**
+ * Set table-level default cell margins and/or cell spacing.
+ * At least one of `defaultCellMargins` or `cellSpacingPt` is required.
+ */
+export interface TablesSetTableOptionsInput extends TableLocator {
+ /** All four sides required when present. */
+ defaultCellMargins?: TableMargins;
+ /** Non-negative number, or `null` to clear. */
+ cellSpacingPt?: number | null;
+}
+
// ---------------------------------------------------------------------------
// Styling: borders
// ---------------------------------------------------------------------------
@@ -492,7 +635,13 @@ export interface TablesGetCellsOutput {
/** Input for `tables.getProperties` — locates a single table. */
export type TablesGetPropertiesInput = TableLocator;
-/** Output for `tables.getProperties` — table layout/style metadata. */
+/**
+ * Output for `tables.getProperties` — table layout/style metadata.
+ *
+ * All fields reflect **direct formatting only**. Properties inherited from
+ * the table style are not included — use `styleId` and `styleOptions` to
+ * determine which style is active.
+ */
export interface TablesGetPropertiesOutput {
nodeId: string;
address: TableAddress;
@@ -505,12 +654,12 @@ export interface TablesGetPropertiesOutput {
*/
preferredWidth?: number;
autoFitMode?: TableAutoFitMode;
- styleOptions?: {
- headerRow?: boolean;
- lastRow?: boolean;
- firstColumn?: boolean;
- lastColumn?: boolean;
- bandedRows?: boolean;
- bandedColumns?: boolean;
- };
+ /** Absent when `tblLook` has no direct formatting. Only explicitly stored flags are emitted. */
+ styleOptions?: TableStyleOptionsState;
+ /** Absent when no direct border formatting exists. Three states per edge (see `TableBorderState`). */
+ borders?: TableBorderState;
+ /** Default cell margins in points. Only sides with explicit direct formatting are included. */
+ defaultCellMargins?: TableMarginsState;
+ /** Cell spacing in points. `0` is explicit; absent = no direct formatting. */
+ cellSpacingPt?: number;
}
diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
index 5f1dc4e3db..710b037236 100644
--- a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
+++ b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
@@ -82,6 +82,9 @@ import {
tablesSetCellPaddingWrapper,
tablesSetCellSpacingWrapper,
tablesClearCellSpacingWrapper,
+ tablesApplyStyleWrapper,
+ tablesSetBordersWrapper,
+ tablesSetTableOptionsWrapper,
} from '../plan-engine/tables-wrappers.js';
import { getDocumentApiCapabilities } from '../capabilities-adapter.js';
import {
@@ -1583,6 +1586,9 @@ const IMPLEMENTED_TABLE_OPS: ReadonlySet = new Set([
'tables.setCellPadding',
'tables.setCellSpacing',
'tables.clearCellSpacing',
+ 'tables.applyStyle',
+ 'tables.setBorders',
+ 'tables.setTableOptions',
'tables.getStyles',
'tables.setDefaultStyle',
'tables.clearDefaultStyle',
@@ -6465,6 +6471,87 @@ const mutationVectors: Partial> = {
return tablesClearCellSpacingWrapper(editor, { nodeId: 'table-1' }, { changeMode: 'direct' });
},
},
+ 'tables.applyStyle': {
+ throwCase: () => {
+ const editor = makeTableEditor();
+ return tablesApplyStyleWrapper(editor, { nodeId: 'missing', styleId: 'TableGrid' }, { changeMode: 'direct' });
+ },
+ failureCase: () => {
+ const editor = makeTableEditor({}, { throwOnDispatch: true });
+ return tablesApplyStyleWrapper(editor, { nodeId: 'table-1', styleId: 'TableGrid' }, { changeMode: 'direct' });
+ },
+ applyCase: () => {
+ const editor = makeTableEditor();
+ return tablesApplyStyleWrapper(editor, { nodeId: 'table-1', styleId: 'TableGrid' }, { changeMode: 'direct' });
+ },
+ },
+ 'tables.setBorders': {
+ throwCase: () => {
+ const editor = makeTableEditor();
+ return tablesSetBordersWrapper(
+ editor,
+ {
+ nodeId: 'missing',
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ },
+ { changeMode: 'direct' },
+ );
+ },
+ failureCase: () => {
+ const editor = makeTableEditor({}, { throwOnDispatch: true });
+ return tablesSetBordersWrapper(
+ editor,
+ {
+ nodeId: 'table-1',
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ },
+ { changeMode: 'direct' },
+ );
+ },
+ applyCase: () => {
+ const editor = makeTableEditor();
+ return tablesSetBordersWrapper(
+ editor,
+ {
+ nodeId: 'table-1',
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ },
+ { changeMode: 'direct' },
+ );
+ },
+ },
+ 'tables.setTableOptions': {
+ throwCase: () => {
+ const editor = makeTableEditor();
+ return tablesSetTableOptionsWrapper(
+ editor,
+ { nodeId: 'missing', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } },
+ { changeMode: 'direct' },
+ );
+ },
+ failureCase: () => {
+ const editor = makeTableEditor({}, { throwOnDispatch: true });
+ return tablesSetTableOptionsWrapper(
+ editor,
+ { nodeId: 'table-1', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } },
+ { changeMode: 'direct' },
+ );
+ },
+ applyCase: () => {
+ const editor = makeTableEditor();
+ return tablesSetTableOptionsWrapper(
+ editor,
+ { nodeId: 'table-1', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } },
+ { changeMode: 'direct' },
+ );
+ },
+ },
'tables.setDefaultStyle': {
throwCase: () => {
// No converter → CAPABILITY_UNAVAILABLE
@@ -9479,6 +9566,44 @@ const dryRunVectors: Partial unknown>> = {
expect(dispatch).not.toHaveBeenCalled();
return result;
},
+ 'tables.applyStyle': () => {
+ const editor = makeTableEditor();
+ const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch;
+ const result = tablesApplyStyleWrapper(
+ editor,
+ { nodeId: 'table-1', styleId: 'TableGrid' },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ return result;
+ },
+ 'tables.setBorders': () => {
+ const editor = makeTableEditor();
+ const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch;
+ const result = tablesSetBordersWrapper(
+ editor,
+ {
+ nodeId: 'table-1',
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ return result;
+ },
+ 'tables.setTableOptions': () => {
+ const editor = makeTableEditor();
+ const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch;
+ const result = tablesSetTableOptionsWrapper(
+ editor,
+ { nodeId: 'table-1', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ return result;
+ },
'tables.setDefaultStyle': () => {
const editor = makeSectionsEditor();
const converter = (editor as unknown as { converter: Record }).converter;
@@ -10578,6 +10703,9 @@ describe('document-api adapter conformance', () => {
'tables.setCellPadding',
'tables.setCellSpacing',
'tables.clearCellSpacing',
+ 'tables.applyStyle',
+ 'tables.setBorders',
+ 'tables.setTableOptions',
'tables.insertCell',
'tables.deleteCell',
'tables.setDefaultStyle',
@@ -10934,6 +11062,9 @@ describe('document-api adapter conformance', () => {
args: {},
wrapperFn: (e) => tablesClearCellSpacingWrapper(e, { nodeId: 'table-1' }),
},
+ // Note: tables.applyStyle, tables.setBorders, tables.setTableOptions are
+ // intentionally excluded from parity tests — they are not yet in the
+ // step-op catalog and do not support mutations.apply (SD-2129 scope).
// create.table (ref is a dummy target — executor ignores targets for create ops)
{
op: 'create.table',
diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts
index 09a6bf56a5..b28eb1183d 100644
--- a/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts
+++ b/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts
@@ -15,9 +15,11 @@ import {
tablesSetStyleAdapter,
tablesClearStyleAdapter,
tablesSetStyleOptionAdapter,
+ tablesApplyStyleAdapter,
tablesSetCellSpacingAdapter,
tablesClearCellSpacingAdapter,
tablesSetBorderAdapter,
+ tablesSetTableOptionsAdapter,
tablesGetPropertiesAdapter,
} from '../tables-adapter.js';
@@ -451,6 +453,24 @@ describe('table setter/getter parity', () => {
});
});
+ describe('applyStyle → tblLook materialization parity', () => {
+ it('seeds Word default tblLook values before merging styleOptions onto a table with no explicit tblLook', () => {
+ const { editor, getSetNodeMarkupCalls } = makeTableEditorWithProps();
+
+ tablesApplyStyleAdapter(editor, { nodeId: 'table-1', styleOptions: { headerRow: false } });
+
+ const tp = lastWrittenAttrs(getSetNodeMarkupCalls()).tableProperties as any;
+ expect(tp.tblLook).toEqual({
+ firstRow: false,
+ lastRow: false,
+ firstColumn: true,
+ lastColumn: false,
+ noHBand: false,
+ noVBand: true,
+ });
+ });
+ });
+
describe('setCellSpacing → canonical key', () => {
it('writes tableCellSpacing (not tblCellSpacing)', () => {
const { editor, getSetNodeMarkupCalls } = makeTableEditorWithProps();
@@ -575,16 +595,16 @@ describe('getProperties reads from tableProperties', () => {
expect(result.autoFitMode).toBe('fitContents');
});
- it('reads styleOptions from tblLook', () => {
+ it('reads styleOptions from tblLook — emits only explicitly stored flags', () => {
const { editor } = makeTableEditorWithProps({
tblLook: { firstRow: true, lastRow: false, noHBand: false, noVBand: true },
});
const result = tablesGetPropertiesAdapter(editor, { nodeId: 'table-1' });
+ // Only flags explicitly stored in tblLook are emitted.
+ // firstColumn and lastColumn are absent from the OOXML, so they are omitted.
expect(result.styleOptions).toEqual({
headerRow: true,
lastRow: false,
- firstColumn: false,
- lastColumn: false,
bandedRows: true,
bandedColumns: false,
});
@@ -598,4 +618,51 @@ describe('getProperties reads from tableProperties', () => {
expect(result.styleOptions).toHaveProperty('lastRow', true);
expect(result.styleOptions).not.toHaveProperty('totalRow');
});
+
+ it('maps logical marginStart/marginEnd into defaultCellMargins', () => {
+ const { editor } = makeTableEditorWithProps({
+ cellMargins: {
+ marginTop: { value: 100, type: 'dxa' },
+ marginStart: { value: 200, type: 'dxa' },
+ marginEnd: { value: 300, type: 'dxa' },
+ marginBottom: { value: 400, type: 'dxa' },
+ },
+ });
+
+ const result = tablesGetPropertiesAdapter(editor, { nodeId: 'table-1' });
+
+ expect(result.defaultCellMargins).toEqual({
+ topPt: 5,
+ rightPt: 15,
+ bottomPt: 20,
+ leftPt: 10,
+ });
+ });
+
+ it('returns NO_OP from setTableOptions when logical margins already match the requested values', () => {
+ const { editor, getSetNodeMarkupCalls } = makeTableEditorWithProps({
+ cellMargins: {
+ marginTop: { value: 100, type: 'dxa' },
+ marginStart: { value: 200, type: 'dxa' },
+ marginEnd: { value: 300, type: 'dxa' },
+ marginBottom: { value: 400, type: 'dxa' },
+ },
+ });
+
+ const result = tablesSetTableOptionsAdapter(editor, {
+ nodeId: 'table-1',
+ defaultCellMargins: {
+ topPt: 5,
+ rightPt: 15,
+ bottomPt: 20,
+ leftPt: 10,
+ },
+ });
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.failure.code).toBe('NO_OP');
+ }
+ expect(getSetNodeMarkupCalls()).toHaveLength(0);
+ });
});
diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts
index 3cb7bb57f4..8d2ae98f8a 100644
--- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts
+++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts
@@ -160,6 +160,9 @@ import {
tablesSetCellPaddingWrapper,
tablesSetCellSpacingWrapper,
tablesClearCellSpacingWrapper,
+ tablesApplyStyleWrapper,
+ tablesSetBordersWrapper,
+ tablesSetTableOptionsWrapper,
} from './plan-engine/tables-wrappers.js';
import {
tablesGetAdapter,
@@ -522,6 +525,9 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters
setCellPadding: (input, options) => tablesSetCellPaddingWrapper(editor, input, options),
setCellSpacing: (input, options) => tablesSetCellSpacingWrapper(editor, input, options),
clearCellSpacing: (input, options) => tablesClearCellSpacingWrapper(editor, input, options),
+ applyStyle: (input, options) => tablesApplyStyleWrapper(editor, input, options),
+ setBorders: (input, options) => tablesSetBordersWrapper(editor, input, options),
+ setTableOptions: (input, options) => tablesSetTableOptionsWrapper(editor, input, options),
get: (input) => tablesGetAdapter(editor, input),
getCells: (input) => tablesGetCellsAdapter(editor, input),
getProperties: (input) => tablesGetPropertiesAdapter(editor, input),
diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts
index 7421924f35..fc31bbad9c 100644
--- a/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts
+++ b/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts
@@ -48,6 +48,9 @@ import type {
TablesSetCellPaddingInput,
TablesSetCellSpacingInput,
TablesClearCellSpacingInput,
+ TablesApplyStyleInput,
+ TablesSetBordersInput,
+ TablesSetTableOptionsInput,
} from '@superdoc/document-api';
import type { CompiledPlan } from './compiler.js';
@@ -92,6 +95,9 @@ import {
tablesSetCellPaddingAdapter,
tablesSetCellSpacingAdapter,
tablesClearCellSpacingAdapter,
+ tablesApplyStyleAdapter,
+ tablesSetBordersAdapter,
+ tablesSetTableOptionsAdapter,
} from '../tables-adapter.js';
// ---------------------------------------------------------------------------
@@ -473,3 +479,31 @@ export function tablesClearCellSpacingWrapper(
): TableMutationResult {
return executeTableCommand(editor, 'tables.clearCellSpacing', tablesClearCellSpacingAdapter, input, options);
}
+
+// ---------------------------------------------------------------------------
+// Convenience operations (SD-2129)
+// ---------------------------------------------------------------------------
+
+export function tablesApplyStyleWrapper(
+ editor: Editor,
+ input: TablesApplyStyleInput,
+ options?: MutationOptions,
+): TableMutationResult {
+ return executeTableCommand(editor, 'tables.applyStyle', tablesApplyStyleAdapter, input, options);
+}
+
+export function tablesSetBordersWrapper(
+ editor: Editor,
+ input: TablesSetBordersInput,
+ options?: MutationOptions,
+): TableMutationResult {
+ return executeTableCommand(editor, 'tables.setBorders', tablesSetBordersAdapter, input, options);
+}
+
+export function tablesSetTableOptionsWrapper(
+ editor: Editor,
+ input: TablesSetTableOptionsInput,
+ options?: MutationOptions,
+): TableMutationResult {
+ return executeTableCommand(editor, 'tables.setTableOptions', tablesSetTableOptionsAdapter, input, options);
+}
diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.convenience.test.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.convenience.test.ts
new file mode 100644
index 0000000000..7e329f3507
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/tables-adapter.convenience.test.ts
@@ -0,0 +1,546 @@
+/* @vitest-environment jsdom */
+
+import { afterEach, beforeAll, describe, expect, it } from 'vitest';
+import { initTestEditor, loadTestDataForEditorTests } from '@tests/helpers/helpers.js';
+import type { Editor } from '../core/Editor.js';
+import {
+ createTableAdapter,
+ tablesApplyStyleAdapter,
+ tablesSetBordersAdapter,
+ tablesSetTableOptionsAdapter,
+ tablesGetPropertiesAdapter,
+ tablesSetBorderAdapter,
+ tablesClearBorderAdapter,
+ tablesApplyBorderPresetAdapter,
+} from './tables-adapter.js';
+
+type LoadedDocData = Awaited>;
+
+const DIRECT = { changeMode: 'direct' } as const;
+
+function requireTableNodeId(result: { success: boolean; table?: { nodeId?: string } }, label: string): string {
+ if (!result.success) {
+ throw new Error(`${label} failed: expected success.`);
+ }
+ const nodeId = (result as { table?: { nodeId?: string } }).table?.nodeId;
+ if (!nodeId) {
+ throw new Error(`${label}: expected result.table.nodeId to be defined.`);
+ }
+ return nodeId;
+}
+
+describe('SD-2129: table convenience operations', () => {
+ let docData: LoadedDocData;
+ let editor: Editor | undefined;
+
+ beforeAll(async () => {
+ docData = await loadTestDataForEditorTests('blank-doc.docx');
+ });
+
+ afterEach(() => {
+ editor?.destroy();
+ editor = undefined;
+ });
+
+ function createEditor(): Editor {
+ const result = initTestEditor({
+ content: docData.docx,
+ media: docData.media,
+ mediaFiles: docData.mediaFiles,
+ fonts: docData.fonts,
+ useImmediateSetTimeout: false,
+ });
+ editor = result.editor;
+ return editor;
+ }
+
+ function createTableAndGetId(ed: Editor): string {
+ const createResult = createTableAdapter(ed, { rows: 3, columns: 3, at: { kind: 'documentEnd' } }, DIRECT);
+ return requireTableNodeId(createResult, 'create.table');
+ }
+
+ // ---------------------------------------------------------------------------
+ // tables.applyStyle
+ // ---------------------------------------------------------------------------
+
+ describe('tables.applyStyle', () => {
+ it('sets styleId and multiple style options in one call', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesApplyStyleAdapter(
+ ed,
+ {
+ nodeId: tableId,
+ styleId: 'TableGrid',
+ styleOptions: { headerRow: true, firstColumn: true, bandedRows: true },
+ },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'applyStyle') });
+ expect(props.styleId).toBe('TableGrid');
+ expect(props.styleOptions?.headerRow).toBe(true);
+ expect(props.styleOptions?.firstColumn).toBe(true);
+ expect(props.styleOptions?.bandedRows).toBe(true);
+ });
+
+ it('supports style-options-only updates (no styleId)', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // First set a style
+ tablesApplyStyleAdapter(ed, { nodeId: tableId, styleId: 'TableGrid' }, DIRECT);
+
+ // Then update only options
+ const result = tablesApplyStyleAdapter(
+ ed,
+ { nodeId: tableId, styleOptions: { bandedRows: false, bandedColumns: true } },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'applyStyle') });
+ // styleId should be preserved
+ expect(props.styleId).toBe('TableGrid');
+ expect(props.styleOptions?.bandedRows).toBe(false);
+ expect(props.styleOptions?.bandedColumns).toBe(true);
+ });
+
+ it('returns NO_OP when style and all options already match', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ tablesApplyStyleAdapter(ed, { nodeId: tableId, styleId: 'TableGrid', styleOptions: { headerRow: true } }, DIRECT);
+
+ const result = tablesApplyStyleAdapter(
+ ed,
+ { nodeId: tableId, styleId: 'TableGrid', styleOptions: { headerRow: true } },
+ DIRECT,
+ );
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.failure.code).toBe('NO_OP');
+ }
+ });
+
+ it('dry-run returns success without mutating', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesApplyStyleAdapter(ed, { nodeId: tableId, styleId: 'NewStyle' }, { ...DIRECT, dryRun: true });
+ expect(result.success).toBe(true);
+
+ // Should NOT have changed the style
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.styleId).toBeUndefined();
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // tables.setBorders
+ // ---------------------------------------------------------------------------
+
+ describe('tables.setBorders', () => {
+ it('applies borders to all edges via applyTo: all', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const border = { lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' };
+ const result = tablesSetBordersAdapter(ed, { nodeId: tableId, mode: 'applyTo', applyTo: 'all', border }, DIRECT);
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') });
+ expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ expect(props.borders?.bottom).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ expect(props.borders?.left).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ expect(props.borders?.right).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ expect(props.borders?.insideH).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ expect(props.borders?.insideV).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ });
+
+ it('applies borders to outside edges only (leaves inside edges unchanged)', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const border = { lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' };
+ const result = tablesSetBordersAdapter(
+ ed,
+ { nodeId: tableId, mode: 'applyTo', applyTo: 'outside', border },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') });
+ expect(props.borders?.top).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' });
+ expect(props.borders?.bottom).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' });
+ expect(props.borders?.left).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' });
+ expect(props.borders?.right).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' });
+ // Inside edges are not touched by outside-only — they retain whatever was there before
+ });
+
+ it('applies borders to inside edges only (leaves outside edges unchanged)', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const border = { lineStyle: 'single', lineWeightPt: 0.5, color: '000000' };
+ const result = tablesSetBordersAdapter(
+ ed,
+ { nodeId: tableId, mode: 'applyTo', applyTo: 'inside', border },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') });
+ expect(props.borders?.insideH).toEqual({ lineStyle: 'single', lineWeightPt: 0.5, color: '000000' });
+ expect(props.borders?.insideV).toEqual({ lineStyle: 'single', lineWeightPt: 0.5, color: '000000' });
+ // Outside edges are not touched by inside-only — they retain whatever was there before
+ });
+
+ it('applies explicit edge patch', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesSetBordersAdapter(
+ ed,
+ {
+ nodeId: tableId,
+ mode: 'edges',
+ edges: {
+ top: { lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' },
+ bottom: { lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' },
+ insideH: null,
+ insideV: null,
+ },
+ },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') });
+ expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ expect(props.borders?.bottom).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' });
+ // null = explicit clear → reads back as null
+ expect(props.borders?.insideH).toBeNull();
+ expect(props.borders?.insideV).toBeNull();
+ });
+
+ it('clears all edges via applyTo: all with null', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // First set some borders
+ tablesSetBordersAdapter(
+ ed,
+ {
+ nodeId: tableId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ },
+ DIRECT,
+ );
+
+ // Then clear all
+ const result = tablesSetBordersAdapter(
+ ed,
+ { nodeId: tableId, mode: 'applyTo', applyTo: 'all', border: null },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') });
+ // All edges should be null (explicit clear)
+ expect(props.borders?.top).toBeNull();
+ expect(props.borders?.bottom).toBeNull();
+ expect(props.borders?.left).toBeNull();
+ expect(props.borders?.right).toBeNull();
+ expect(props.borders?.insideH).toBeNull();
+ expect(props.borders?.insideV).toBeNull();
+ });
+
+ it('dry-run returns success without mutating', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // Snapshot borders before dry-run
+ const before = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+
+ const result = tablesSetBordersAdapter(
+ ed,
+ {
+ nodeId: tableId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'thick', lineWeightPt: 5, color: 'FF0000' },
+ },
+ { ...DIRECT, dryRun: true },
+ );
+ expect(result.success).toBe(true);
+
+ // Borders should be unchanged from before dry-run
+ const after = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(after.borders).toEqual(before.borders);
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // tables.setTableOptions
+ // ---------------------------------------------------------------------------
+
+ describe('tables.setTableOptions', () => {
+ it('sets default cell margins', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesSetTableOptionsAdapter(
+ ed,
+ { nodeId: tableId, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') });
+ expect(props.defaultCellMargins).toEqual({ topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 });
+ });
+
+ it('sets cell spacing', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: 2 }, DIRECT);
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') });
+ expect(props.cellSpacingPt).toBe(2);
+ });
+
+ it('sets both margins and spacing together', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesSetTableOptionsAdapter(
+ ed,
+ {
+ nodeId: tableId,
+ defaultCellMargins: { topPt: 5, rightPt: 10, bottomPt: 5, leftPt: 10 },
+ cellSpacingPt: 3,
+ },
+ DIRECT,
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') });
+ expect(props.defaultCellMargins).toEqual({ topPt: 5, rightPt: 10, bottomPt: 5, leftPt: 10 });
+ expect(props.cellSpacingPt).toBe(3);
+ });
+
+ it('clears cell spacing with null', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // First set spacing
+ tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: 2 }, DIRECT);
+
+ // Then clear it
+ const result = tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: null }, DIRECT);
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') });
+ expect(props.cellSpacingPt).toBeUndefined();
+ });
+
+ it('cellSpacingPt: 0 is explicit (distinct from absent)', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: 0 }, DIRECT);
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') });
+ expect(props.cellSpacingPt).toBe(0);
+ });
+
+ it('returns NO_OP when values already match', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ tablesSetTableOptionsAdapter(
+ ed,
+ { nodeId: tableId, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } },
+ DIRECT,
+ );
+
+ const result = tablesSetTableOptionsAdapter(
+ ed,
+ { nodeId: tableId, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } },
+ DIRECT,
+ );
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.failure.code).toBe('NO_OP');
+ }
+ });
+
+ it('dry-run returns success without mutating', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const result = tablesSetTableOptionsAdapter(
+ ed,
+ { nodeId: tableId, cellSpacingPt: 5 },
+ { ...DIRECT, dryRun: true },
+ );
+ expect(result.success).toBe(true);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.cellSpacingPt).toBeUndefined();
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // tables.getProperties — expanded fields
+ // ---------------------------------------------------------------------------
+
+ describe('tables.getProperties expanded fields', () => {
+ it('omits styleOptions when tblLook is absent', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.styleOptions).toBeUndefined();
+ });
+
+ it('returns borders when direct border formatting exists', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // Created tables may have default borders from the create adapter.
+ // After a mutation, the borders field should reflect the direct formatting.
+ tablesSetBordersAdapter(
+ ed,
+ {
+ nodeId: tableId,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ },
+ DIRECT,
+ );
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.borders).toBeDefined();
+ expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '000000' });
+ });
+
+ it('returns defaultCellMargins when direct margin formatting exists', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ tablesSetTableOptionsAdapter(
+ ed,
+ { nodeId: tableId, defaultCellMargins: { topPt: 8, rightPt: 8, bottomPt: 8, leftPt: 8 } },
+ DIRECT,
+ );
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.defaultCellMargins).toEqual({ topPt: 8, rightPt: 8, bottomPt: 8, leftPt: 8 });
+ });
+
+ it('returns borders after setBorders mutation', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ tablesSetBordersAdapter(
+ ed,
+ { nodeId: tableId, mode: 'edges', edges: { top: { lineStyle: 'single', lineWeightPt: 1.5, color: 'FF0000' } } },
+ DIRECT,
+ );
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1.5, color: 'FF0000' });
+ });
+
+ it('returns null for explicitly cleared border edge', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ tablesSetBordersAdapter(ed, { nodeId: tableId, mode: 'edges', edges: { top: null } }, DIRECT);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.borders?.top).toBeNull();
+ });
+
+ it('normalizes legacy clearBorder (val: nil) to null on read', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // clearBorder writes val: 'nil'
+ tablesClearBorderAdapter(ed, { nodeId: tableId, edge: 'top' }, DIRECT);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.borders?.top).toBeNull();
+ });
+
+ it('normalizes legacy applyBorderPreset(none) to null on read', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // applyBorderPreset('none') writes val: 'none'
+ tablesApplyBorderPresetAdapter(ed, { nodeId: tableId, preset: 'none' }, DIRECT);
+
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId });
+ expect(props.borders?.top).toBeNull();
+ expect(props.borders?.bottom).toBeNull();
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // Ref handoff chaining
+ // ---------------------------------------------------------------------------
+
+ describe('ref handoff chaining across convenience operations', () => {
+ it('chains create → applyStyle → setBorders → setTableOptions', () => {
+ const ed = createEditor();
+ const tableId = createTableAndGetId(ed);
+
+ // Step 1: applyStyle
+ const r1 = tablesApplyStyleAdapter(
+ ed,
+ { nodeId: tableId, styleId: 'TableGrid', styleOptions: { headerRow: true } },
+ DIRECT,
+ );
+ const id1 = requireTableNodeId(r1, 'applyStyle');
+
+ // Step 2: setBorders using applyStyle's ref
+ const r2 = tablesSetBordersAdapter(
+ ed,
+ {
+ nodeId: id1,
+ mode: 'applyTo',
+ applyTo: 'all',
+ border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' },
+ },
+ DIRECT,
+ );
+ const id2 = requireTableNodeId(r2, 'setBorders');
+
+ // Step 3: setTableOptions using setBorders's ref
+ const r3 = tablesSetTableOptionsAdapter(
+ ed,
+ { nodeId: id2, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 }, cellSpacingPt: 2 },
+ DIRECT,
+ );
+ const id3 = requireTableNodeId(r3, 'setTableOptions');
+
+ // Verify final state
+ const props = tablesGetPropertiesAdapter(ed, { nodeId: id3 });
+ expect(props.styleId).toBe('TableGrid');
+ expect(props.styleOptions?.headerRow).toBe(true);
+ expect(props.borders?.top).toBeDefined();
+ expect(props.defaultCellMargins).toEqual({ topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 });
+ expect(props.cellSpacingPt).toBe(2);
+ });
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.ts
index df911d0941..f44d28dea7 100644
--- a/packages/super-editor/src/document-api-adapters/tables-adapter.ts
+++ b/packages/super-editor/src/document-api-adapters/tables-adapter.ts
@@ -59,6 +59,16 @@ import type {
TablesSetDefaultStyleInput,
TablesClearDefaultStyleInput,
DocumentMutationResult,
+ TablesApplyStyleInput,
+ TablesSetBordersInput,
+ TablesSetTableOptionsInput,
+ TableBorderSpec,
+ TableBorderState,
+ TableMarginsState,
+ TableStyleOptionsPatch,
+ TableStyleOptionsState,
+ TableBorderPatch,
+ TableBorderApplyTo,
} from '@superdoc/document-api';
import type { Transaction } from 'prosemirror-state';
import { TableMap } from 'prosemirror-tables';
@@ -3491,6 +3501,441 @@ export function tablesClearCellSpacingAdapter(
}
}
+// ---------------------------------------------------------------------------
+// Convenience operation helpers (SD-2129)
+// ---------------------------------------------------------------------------
+
+/** Reverse map from OOXML tblLook key to API style-option flag. */
+const XML_KEY_TO_STYLE_OPTION: Record = {
+ firstRow: 'headerRow',
+ lastRow: 'lastRow',
+ firstColumn: 'firstColumn',
+ lastColumn: 'lastColumn',
+ noHBand: 'bandedRows',
+ noVBand: 'bandedColumns',
+};
+
+/**
+ * Read `tblLook` flags as `TableStyleOptionsState`.
+ * Emits only explicitly stored flags — absent OOXML keys stay omitted.
+ */
+function readTableLookAsState(tblLook: Record | undefined): TableStyleOptionsState | undefined {
+ if (!tblLook) return undefined;
+
+ const result: TableStyleOptionsState = {};
+ let hasAny = false;
+
+ for (const [xmlKey, apiFlag] of Object.entries(XML_KEY_TO_STYLE_OPTION)) {
+ if (xmlKey in tblLook && typeof tblLook[xmlKey] === 'boolean') {
+ const rawValue = tblLook[xmlKey] as boolean;
+ (result as Record)[apiFlag] = INVERTED_FLAGS.has(apiFlag) ? !rawValue : rawValue;
+ hasAny = true;
+ }
+ }
+
+ return hasAny ? result : undefined;
+}
+
+/**
+ * Merge API style-option flags into an existing `tblLook` object.
+ * Returns the updated tblLook (new object, original not mutated).
+ */
+function writeTableLook(
+ currentLook: Record | undefined,
+ patch: TableStyleOptionsPatch,
+): Record {
+ // Match tables.setStyleOption behavior: once tblLook is materialized,
+ // omitted flags should preserve Word's effective default mask.
+ const result = currentLook ? { ...currentLook } : { ...WORD_DEFAULT_TBL_LOOK };
+ for (const [apiFlag, value] of Object.entries(patch) as Array<[keyof TableStyleOptionsPatch, boolean | undefined]>) {
+ if (value === undefined) continue;
+ const normalizedFlag = apiFlag as TableStyleOptionFlag;
+ const xmlKey = resolveStyleOptionFlag(normalizedFlag);
+ result[xmlKey] = INVERTED_FLAGS.has(normalizedFlag) ? !value : value;
+ }
+ delete result.val;
+ return result;
+}
+
+/** Convert API `TableBorderSpec` to OOXML border storage. pt → eighths-of-a-point. */
+function normalizeBorderSpecFromApi(spec: TableBorderSpec): Record {
+ return {
+ val: spec.lineStyle,
+ size: Math.round(spec.lineWeightPt * 8),
+ color: spec.color,
+ };
+}
+
+/** Convert OOXML border storage to API `TableBorderSpec`. Eighths-of-a-point → pt. */
+function normalizeBorderSpecToApi(border: Record): TableBorderSpec {
+ const rawColor = typeof border.color === 'string' ? border.color : 'auto';
+ // `auto` stays lowercase per the public contract; hex values are uppercased.
+ const color = rawColor === 'auto' ? 'auto' : rawColor.toUpperCase();
+ return {
+ lineStyle: String(border.val ?? 'single'),
+ lineWeightPt: typeof border.size === 'number' ? border.size / 8 : 0,
+ color,
+ };
+}
+
+/** The OOXML representation of "no border" — used when API sends `null`. */
+const CLEARED_BORDER_OOXML = { val: 'none', size: 0, color: 'auto' } as const;
+
+/** Returns true if an OOXML border value represents an explicit clear. */
+function isClearedBorder(border: Record): boolean {
+ return border.val === 'none' || border.val === 'nil';
+}
+
+/** Convert OOXML border to the three-state API read model. */
+function readBorderEdge(border: unknown): TableBorderSpec | null | undefined {
+ if (!border || typeof border !== 'object') return undefined;
+ const b = border as Record;
+ if (isClearedBorder(b)) return null;
+ return normalizeBorderSpecToApi(b);
+}
+
+/** Read OOXML borders as `TableBorderState`. Returns undefined if no direct formatting. */
+function readBordersAsState(borders: unknown): TableBorderState | undefined {
+ if (!borders || typeof borders !== 'object') return undefined;
+ const b = borders as Record;
+
+ const result: TableBorderState = {};
+ let hasAny = false;
+ const edgeNames = ['top', 'bottom', 'left', 'right', 'insideH', 'insideV'] as const;
+
+ for (const edge of edgeNames) {
+ if (edge in b) {
+ const value = readBorderEdge(b[edge]);
+ if (value !== undefined) {
+ (result as Record)[edge] = value;
+ hasAny = true;
+ }
+ }
+ }
+
+ return hasAny ? result : undefined;
+}
+
+type TableCellMarginKey = 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft' | 'marginStart' | 'marginEnd';
+
+const TABLE_MARGIN_KEY_GROUPS: ReadonlyArray<{
+ keys: readonly TableCellMarginKey[];
+ apiKey: keyof TableMarginsState;
+}> = [
+ { keys: ['marginTop'], apiKey: 'topPt' },
+ { keys: ['marginRight', 'marginEnd'], apiKey: 'rightPt' },
+ { keys: ['marginBottom'], apiKey: 'bottomPt' },
+ { keys: ['marginLeft', 'marginStart'], apiKey: 'leftPt' },
+] as const;
+
+function readCellMarginEntry(
+ cellMargins: Record,
+ keys: readonly TableCellMarginKey[],
+): { value?: number } | undefined {
+ for (const key of keys) {
+ const entry = cellMargins[key] as { value?: number } | undefined;
+ if (entry && typeof entry.value === 'number') return entry;
+ }
+ return undefined;
+}
+
+/** Read OOXML cell margins as `TableMarginsState`. Returns undefined if no direct formatting. */
+function readCellMarginsAsState(cellMargins: unknown): TableMarginsState | undefined {
+ if (!cellMargins || typeof cellMargins !== 'object') return undefined;
+ const cm = cellMargins as Record;
+
+ const result: TableMarginsState = {};
+ let hasAny = false;
+
+ for (const { keys, apiKey } of TABLE_MARGIN_KEY_GROUPS) {
+ const entry = readCellMarginEntry(cm, keys);
+ if (entry && typeof entry.value === 'number') {
+ result[apiKey] = entry.value / POINTS_TO_TWIPS;
+ hasAny = true;
+ }
+ }
+
+ return hasAny ? result : undefined;
+}
+
+/** Read OOXML cell spacing as API Pt. Returns undefined if absent. */
+function readCellSpacingPt(spacing: unknown): number | undefined {
+ if (!spacing || typeof spacing !== 'object') return undefined;
+ const s = spacing as { value?: number };
+ if (typeof s.value !== 'number') return undefined;
+ return s.value / POINTS_TO_TWIPS;
+}
+
+/**
+ * Expand `applyTo` target into the concrete edge patch.
+ */
+function expandApplyToEdges(
+ applyTo: TableBorderApplyTo,
+): Array<'top' | 'bottom' | 'left' | 'right' | 'insideH' | 'insideV'> {
+ switch (applyTo) {
+ case 'all':
+ return ['top', 'bottom', 'left', 'right', 'insideH', 'insideV'];
+ case 'outside':
+ return ['top', 'bottom', 'left', 'right'];
+ case 'inside':
+ return ['insideH', 'insideV'];
+ default:
+ return [applyTo as 'top' | 'bottom' | 'left' | 'right' | 'insideH' | 'insideV'];
+ }
+}
+
+/**
+ * Build the OOXML border patch from the resolved edge map.
+ * Each edge is either a border spec (from API) or null (clear).
+ */
+function buildOoxmlBorderPatch(
+ currentBorders: Record,
+ edgePatch: TableBorderPatch,
+): Record {
+ const result = { ...currentBorders };
+ const edges = Object.entries(edgePatch) as Array<[string, TableBorderSpec | null | undefined]>;
+ for (const [edge, value] of edges) {
+ if (value === undefined) continue;
+ result[edge] = value === null ? { ...CLEARED_BORDER_OOXML } : normalizeBorderSpecFromApi(value);
+ }
+ return result;
+}
+
+/**
+ * Check if the requested style patch is already satisfied by current table properties.
+ * Compares against raw direct flag keys, not inferred defaults.
+ */
+function isStylePatchSatisfied(currentTableProps: Record, input: TablesApplyStyleInput): boolean {
+ if (input.styleId !== undefined) {
+ if (currentTableProps.tableStyleId !== input.styleId) return false;
+ }
+
+ if (input.styleOptions) {
+ const currentLook = (currentTableProps.tblLook ?? {}) as Record;
+ for (const [apiFlag, value] of Object.entries(input.styleOptions)) {
+ if (value === undefined) continue;
+ const normalizedFlag = apiFlag as TableStyleOptionFlag;
+ const xmlKey = resolveStyleOptionFlag(normalizedFlag);
+ const expectedXmlValue = INVERTED_FLAGS.has(normalizedFlag) ? !value : value;
+ if (!(xmlKey in currentLook) || currentLook[xmlKey] !== expectedXmlValue) return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Check if the requested table-options patch is already satisfied by current table properties.
+ */
+function isTableOptionsSatisfied(
+ currentTableProps: Record,
+ input: TablesSetTableOptionsInput,
+): boolean {
+ if (input.defaultCellMargins !== undefined) {
+ const cm = currentTableProps.cellMargins as Record | undefined;
+ if (!cm) return false;
+ const m = input.defaultCellMargins;
+ const pairs: Array<[readonly TableCellMarginKey[], number]> = [
+ [['marginTop'], m.topPt],
+ [['marginRight', 'marginEnd'], m.rightPt],
+ [['marginBottom'], m.bottomPt],
+ [['marginLeft', 'marginStart'], m.leftPt],
+ ];
+ for (const [ooxmlKeys, ptValue] of pairs) {
+ const entry = readCellMarginEntry(cm, ooxmlKeys);
+ if (!entry || entry.value !== Math.round(ptValue * POINTS_TO_TWIPS)) return false;
+ }
+ }
+
+ if (input.cellSpacingPt !== undefined) {
+ const spacing = currentTableProps.tableCellSpacing as { value?: number } | undefined;
+ if (input.cellSpacingPt === null) {
+ if (spacing !== undefined && spacing !== null) return false;
+ } else {
+ if (!spacing || spacing.value !== Math.round(input.cellSpacingPt * POINTS_TO_TWIPS)) return false;
+ }
+ }
+
+ return true;
+}
+
+// ---------------------------------------------------------------------------
+// Convenience adapters (SD-2129)
+// ---------------------------------------------------------------------------
+
+/**
+ * tables.applyStyle — apply a table style and/or style options in one call.
+ */
+export function tablesApplyStyleAdapter(
+ editor: Editor,
+ input: TablesApplyStyleInput,
+ options?: MutationOptions,
+): TableMutationResult {
+ rejectTrackedMode('tables.applyStyle', options);
+
+ const { candidate, address } = resolveTableLocator(editor, input, 'tables.applyStyle');
+
+ if (options?.dryRun) {
+ return buildTableSuccess(address);
+ }
+
+ try {
+ const currentAttrs = candidate.node.attrs as Record;
+ const currentTableProps = (currentAttrs.tableProperties ?? {}) as Record;
+
+ if (isStylePatchSatisfied(currentTableProps, input)) {
+ return toTableFailure('NO_OP', 'tables.applyStyle did not produce a change.');
+ }
+
+ const updatedTableProps = { ...currentTableProps };
+
+ if (input.styleId !== undefined) {
+ updatedTableProps.tableStyleId = input.styleId;
+ }
+
+ if (input.styleOptions) {
+ const currentLook = asRecord(updatedTableProps.tblLook);
+ updatedTableProps.tblLook = writeTableLook(currentLook, input.styleOptions as Record);
+ }
+
+ const tr = editor.state.tr;
+ tr.setNodeMarkup(candidate.pos, null, {
+ ...currentAttrs,
+ tableProperties: updatedTableProps,
+ ...syncExtractedTableAttrs(updatedTableProps),
+ });
+ applyDirectMutationMeta(tr);
+ editor.dispatch(tr);
+ clearIndexCache(editor);
+ return buildTableSuccess(resolvePostMutationTableAddress(editor, candidate.pos, address.nodeId, tr));
+ } catch {
+ return toTableFailure('INVALID_TARGET', 'Table style application could not be applied.');
+ }
+}
+
+/**
+ * tables.setBorders — set borders on a table using a target set or per-edge patch.
+ */
+export function tablesSetBordersAdapter(
+ editor: Editor,
+ input: TablesSetBordersInput,
+ options?: MutationOptions,
+): TableMutationResult {
+ rejectTrackedMode('tables.setBorders', options);
+
+ const { candidate, address } = resolveTableLocator(editor, input, 'tables.setBorders');
+
+ if (options?.dryRun) {
+ return buildTableSuccess(address);
+ }
+
+ try {
+ // Resolve the edge patch from the input mode
+ let edgePatch: TableBorderPatch;
+ if (input.mode === 'applyTo') {
+ const edges = expandApplyToEdges(input.applyTo);
+ edgePatch = {};
+ for (const edge of edges) {
+ (edgePatch as Record)[edge] = input.border;
+ }
+ } else {
+ edgePatch = input.edges;
+ }
+
+ const tr = editor.state.tr;
+ const currentAttrs = candidate.node.attrs as Record;
+ const currentTableProps = { ...((currentAttrs.tableProperties ?? {}) as Record) };
+ const currentBorders = (currentTableProps.borders ?? {}) as Record;
+
+ currentTableProps.borders = buildOoxmlBorderPatch(currentBorders, edgePatch);
+
+ tr.setNodeMarkup(candidate.pos, null, {
+ ...currentAttrs,
+ tableProperties: currentTableProps,
+ ...syncExtractedTableAttrs(currentTableProps),
+ });
+
+ // Propagate to cell borders for each edge in the patch
+ const patchEntries = Object.entries(edgePatch) as Array<[string, TableBorderSpec | null | undefined]>;
+ for (const [edge, value] of patchEntries) {
+ if (value === undefined) continue;
+ if (!isBoundaryEdge(edge)) continue;
+ const ooxmlSpec = value === null ? { ...CLEARED_BORDER_OOXML } : normalizeBorderSpecFromApi(value);
+ applyTableEdgeToCellBorders(tr, candidate.pos, candidate.node, edge as TableBorderEdgeForCells, ooxmlSpec);
+ }
+
+ applyDirectMutationMeta(tr);
+ editor.dispatch(tr);
+ clearIndexCache(editor);
+ return buildTableSuccess(resolvePostMutationTableAddress(editor, candidate.pos, address.nodeId, tr));
+ } catch {
+ return toTableFailure('INVALID_TARGET', 'Table border update could not be applied.');
+ }
+}
+
+/**
+ * tables.setTableOptions — set table-level default cell margins and/or cell spacing.
+ */
+export function tablesSetTableOptionsAdapter(
+ editor: Editor,
+ input: TablesSetTableOptionsInput,
+ options?: MutationOptions,
+): TableMutationResult {
+ rejectTrackedMode('tables.setTableOptions', options);
+
+ const { candidate, address } = resolveTableLocator(editor, input, 'tables.setTableOptions');
+
+ if (options?.dryRun) {
+ return buildTableSuccess(address);
+ }
+
+ try {
+ const currentAttrs = candidate.node.attrs as Record;
+ const currentTableProps = (currentAttrs.tableProperties ?? {}) as Record;
+
+ if (isTableOptionsSatisfied(currentTableProps, input)) {
+ return toTableFailure('NO_OP', 'tables.setTableOptions did not produce a change.');
+ }
+
+ const updatedTableProps = { ...currentTableProps };
+
+ if (input.defaultCellMargins !== undefined) {
+ const m = input.defaultCellMargins;
+ updatedTableProps.cellMargins = {
+ marginTop: { value: Math.round(m.topPt * POINTS_TO_TWIPS), type: 'dxa' },
+ marginRight: { value: Math.round(m.rightPt * POINTS_TO_TWIPS), type: 'dxa' },
+ marginBottom: { value: Math.round(m.bottomPt * POINTS_TO_TWIPS), type: 'dxa' },
+ marginLeft: { value: Math.round(m.leftPt * POINTS_TO_TWIPS), type: 'dxa' },
+ };
+ }
+
+ if (input.cellSpacingPt !== undefined) {
+ if (input.cellSpacingPt === null) {
+ delete updatedTableProps.tableCellSpacing;
+ delete updatedTableProps.tblCellSpacing;
+ } else {
+ updatedTableProps.tableCellSpacing = {
+ value: Math.round(input.cellSpacingPt * POINTS_TO_TWIPS),
+ type: 'dxa',
+ };
+ }
+ }
+
+ const tr = editor.state.tr;
+ tr.setNodeMarkup(candidate.pos, null, {
+ ...currentAttrs,
+ tableProperties: updatedTableProps,
+ ...syncExtractedTableAttrs(updatedTableProps),
+ });
+ applyDirectMutationMeta(tr);
+ editor.dispatch(tr);
+ clearIndexCache(editor);
+ return buildTableSuccess(resolvePostMutationTableAddress(editor, candidate.pos, address.nodeId, tr));
+ } catch {
+ return toTableFailure('INVALID_TARGET', 'Table options could not be applied.');
+ }
+}
+
// ---------------------------------------------------------------------------
// create.table
// ---------------------------------------------------------------------------
@@ -3771,17 +4216,17 @@ export function tablesGetPropertiesAdapter(editor: Editor, input: TablesGetPrope
if (preferredWidth != null) result.preferredWidth = preferredWidth;
}
- const look = resolveTableLook(tp);
- if (look) {
- result.styleOptions = {
- headerRow: look.firstRow === true,
- lastRow: look.lastRow === true,
- firstColumn: look.firstColumn === true,
- lastColumn: look.lastColumn === true,
- bandedRows: look.noHBand !== true,
- bandedColumns: look.noVBand !== true,
- };
- }
+ const styleOptions = readTableLookAsState(resolveTableLook(tp));
+ if (styleOptions) result.styleOptions = styleOptions;
+
+ const borders = readBordersAsState(tp.borders);
+ if (borders) result.borders = borders;
+
+ const defaultCellMargins = readCellMarginsAsState(tp.cellMargins);
+ if (defaultCellMargins) result.defaultCellMargins = defaultCellMargins;
+
+ const cellSpacingPt = readCellSpacingPt(tp.tableCellSpacing);
+ if (cellSpacingPt !== undefined) result.cellSpacingPt = cellSpacingPt;
return result;
}