` row-height attribute (no row extension exists; cell height achieves the visible result).
+- Fixed/clipping height — table cell height is a minimum; content still grows the cell.
+
+## Decisions
+
+### Decision 1: Cell attribute `cellHeight`, rendered on `` style
+
+**Rationale:** Mirrors `cellWidth` exactly. Unlike width, height needs **no** `` routing — `table-layout: fixed` only governs column widths, so `height` on the `| ` works directly. Simpler than width.
+
+### Decision 2: "Column Height" label (matches the existing "Column Width")
+
+**Rationale:** The width control is labeled "Column Width"; the paired control reads "Column Height" for UI symmetry, even though height technically applies to the cell/row. Terminology consistency beats pedantic accuracy here.
+
+### Decision 3: Height is a minimum; whole row follows
+
+**Rationale / behavior notes:**
+
+- Browsers treat table-cell `height` as a minimum — the cell grows if content is taller (no clipping). Consistent with the table `min-height` decision.
+- A ` | ` height stretches its entire row (sibling cells share the row height). So setting "cell height" visually behaves as row height — which is what a user asking for "column/row height" expects.
+
+## Migration Plan
+
+Additive. Existing tables (no `cellHeight`) unaffected. No data model or breaking changes.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/proposal.md
new file mode 100644
index 0000000000..c7da5c4553
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/proposal.md
@@ -0,0 +1,24 @@
+## Why
+
+Users can set column (cell) width via a CSS-size text input, but there is no equivalent control for cell height. Completing the pair lets users size table cells in both dimensions. (Requested as "column height"; height in a table applies to cells/rows, not columns — see design.md.)
+
+## What Changes
+
+- Add a `cellHeight` string attribute to the table cell (`TableCellBackgroundColor`), mirroring `cellWidth`.
+- Render it as `height: ` in the cell's inline style (and `data-cell-height` in class format).
+- Add a "Column Height" text input to the cell configuration dropdown, reusing the `textInput` type + `normalizeCssSize` validation + commit-on-blur behavior.
+
+## Capabilities
+
+### Modified Capabilities
+
+- `table-size-control`: add a cell height control alongside the existing column width control.
+
+## Impact
+
+**Affected Files:**
+
+- `src/extensions/TableCellBackgroundColor.ts` — add `cellHeight` attribute; render into cell style + `data-cell-height`.
+- `src/components/toolbars/helpers/configurationHelpers.ts` — add "Column Height" text input section.
+
+**User Impact:** additive; existing tables unaffected. No colgroup/NodeView changes — height lives directly on the cell (unlike width, which routes through ``).
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/specs/table-size-control/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/specs/table-size-control/spec.md
new file mode 100644
index 0000000000..f35b447ae9
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/specs/table-size-control/spec.md
@@ -0,0 +1,35 @@
+## ADDED Requirements
+
+### Requirement: Column height as CSS size
+
+The cell configuration dropdown SHALL include a "Column Height" text input that stores the height as a CSS size string (e.g. `100px`, `50%`) on the cell's inline style, applying a minimum height to the cell's row.
+
+#### Scenario: Setting a cell height
+
+- **WHEN** user enters a valid CSS size in the "Column Height" input for a cell
+- **THEN** the cell (and its row) renders at least that tall
+
+#### Scenario: Row grows beyond the set height
+
+- **WHEN** a cell has a height set and its content requires more vertical space
+- **THEN** the cell grows to fit the content, exceeding the set height
+
+#### Scenario: Bare number treated as pixels
+
+- **WHEN** user enters a bare number such as `80` for the cell height
+- **THEN** the value is normalized to `80px` and applied
+
+#### Scenario: Invalid or unsafe height is rejected
+
+- **WHEN** user enters a value that is not a valid CSS size
+- **THEN** the value is not applied
+
+#### Scenario: Clearing cell height
+
+- **WHEN** user clears the "Column Height" input
+- **THEN** the cell height is reset to auto (null)
+
+#### Scenario: Height serializes in the active style-data format
+
+- **WHEN** a cell height is set
+- **THEN** it is rendered as inline `height` in `inline` mode and as `data-cell-height` in `class` mode, consistent with the column width control
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/tasks.md
new file mode 100644
index 0000000000..6b9f452f23
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-cell-height/tasks.md
@@ -0,0 +1,15 @@
+## 1. Cell height attribute
+
+- [x] 1.1 Add `cellHeight` string attribute to `TableCellBackgroundColor.addAttributes()` (parse from `style.height` / `data-cell-height`; renderHTML returns {} — handled in main renderHTML), mirroring `cellWidth`
+- [x] 1.2 In `renderHTML`, append `height: ${cellHeight}` to the cell style string (both formats)
+- [x] 1.3 In class mode, emit `data-cell-height`
+
+## 2. Cell configuration control
+
+- [x] 2.1 Add "Column Height" `textInput` section after `cellWidth` in `createCellConfigurationSections()` (placeholder e.g. "Auto (e.g. 100px, 50%)")
+- [x] 2.2 `getCurrentValue` → `getCellAttributes(editor)?.cellHeight`; `onChange` → `normalizeCssSize` + `setCellAttribute("cellHeight", …)` (empty clears to null, invalid ignored)
+
+## 3. Verification
+
+- [x] 3.1 Typecheck + lint clean; unit suite passes (82 tests)
+- [x] 3.2 Manual: set cell height (px + %) → row grows to that height; content taller → grows past it; clear → auto (verified by user in Mendix editor)
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/.openspec.yaml b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/.openspec.yaml
new file mode 100644
index 0000000000..c0a8162549
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-21
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/design.md
new file mode 100644
index 0000000000..071af99be6
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/design.md
@@ -0,0 +1,83 @@
+## Context
+
+The Rich Text widget is a TipTap-based editor. Keyboard shortcuts come from three scattered sources with no central registry:
+
+- **TipTap built-ins** — bold `Mod-b`, italic `Mod-i`, etc., baked into extension packages under `node_modules` (not this repo's code).
+- **Custom extensions** — `src/extensions/Indent.ts` (`Mod-]` / `Mod-[`), `src/extensions/Fullscreen.ts` (Escape to exit fullscreen).
+- **Accessibility navigation** — `src/extensions/KeyboardNavigation.ts` (Alt+F10 focus toolbar, Alt+F11 focus status bar, Escape return to editor).
+
+TipTap exposes no runtime API to enumerate active shortcuts, so any help panel must use a hand-maintained list.
+
+The toolbar is configured declaratively in `src/components/toolbars/ToolbarConfig.ts` (`TOOLBAR_GROUPS`) and rendered by `src/components/toolbars/Toolbar.tsx` via a `ToolbarButtonFactory` switch on `button.action`. Two dialog patterns already exist: anchored floating dropdowns (`DialogToolbarButton` + `ToolbarContext`) and a centered overlay modal (`ConfirmDialog`, using `Dialog.scss`).
+
+The icon font (`src/ui/RichTextIcons.scss`) has no help/question glyph.
+
+Toolbar group toggles (`history`, `fontStyle`, … `tableBetter`) live in the "Custom toolbar" property group and are gated in Studio Pro to `preset === "custom"` via `toolbarGroupKeys` in `src/RichText.editorConfig.ts`. The `preset` (basic/standard/full/custom) and per-group booleans flow through `EditorWrapper.tsx` into `Toolbar`, which computes `filteredGroups` from them.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Add a discoverable help button that opens a centered modal listing keyboard shortcuts, similar to TinyMCE's help menu.
+- Gate the button behind a new `helpButton` property (default true) AND require the full toolbar to be shown.
+- Reuse existing modal/dialog and styling patterns; add no runtime dependencies.
+- Ship an accessible dialog (role/aria, focus, Escape).
+
+**Non-Goals:**
+
+- No runtime enumeration of TipTap shortcuts — the catalog is static and manually synced.
+- No internationalization of shortcut labels (the widget has no i18n infra today; strings stay English, matching existing toolbar titles).
+- No new icon-font glyph — use a text "?" for now.
+- No editing/customization of shortcut bindings from the modal.
+
+## Decisions
+
+### Decision 1: Centered modal, mirroring `ConfirmDialog`
+
+Use a centered overlay modal (`HelpDialog`) reusing `Dialog.scss`, rather than the anchored floating dropdown pattern. Rationale: matches TinyMCE's help UX and the user's requested style; `ConfirmDialog` already proves the overlay + click-outside + `Dialog.scss` pattern in this codebase.
+
+**Alternative considered:** anchored dropdown (`DialogToolbarButton`). Rejected — a shortcuts reference is a focused, content-heavy panel better suited to a centered modal than a small anchored popover.
+
+### Decision 2: Render outside `TOOLBAR_GROUPS`, gated on "all groups shown"
+
+The help button is appended in `Toolbar.tsx` outside the mapped `filteredGroups`, not added as a `TOOLBAR_GROUPS` entry. Rationale: it must escape the normal preset/group filtering and follow its own gating rule. Gating condition:
+
+```
+helpButton !== false && filteredGroups.length === TOOLBAR_GROUPS.length
+```
+
+`filteredGroups.length === TOOLBAR_GROUPS.length` is true for preset `full` and for `custom` with every group enabled; false for basic/standard and for custom missing any group. This directly implements "only visible when full toolbar."
+
+**Alternative considered:** a dedicated `help` group in `TOOLBAR_GROUPS` with a `presetValue`. Rejected — presetValue gating can't express "all groups present," and it would appear under partial custom configs.
+
+### Decision 3: New `helpButton` property in the custom-toolbar group
+
+Add `` after `tableBetter` in `RichText.xml`, and add `"helpButton"` to `toolbarGroupKeys` in `editorConfig.ts` so it's hidden unless `preset === "custom"`, consistent with sibling toggles. `typings/RichTextProps.d.ts` is regenerated from XML (never hand-edited). Prop is plumbed through `RichText.tsx` → `EditorWrapper.tsx` → `Editor.tsx` → `Toolbar`.
+
+**Note on gating interplay:** the property is only _editable_ under `custom`, but the button is only _rendered_ under the full toolbar. Under preset `full` the property keeps its default (`true`), so the button shows. Under `custom`, the user can both enable all groups and toggle `helpButton`.
+
+### Decision 4: Static shortcut catalog module
+
+A new module (e.g. `shortcuts.ts`) exports a grouped, static list (Formatting, Paragraph, History, Accessibility). `HelpDialog` renders it. Key combos shown generically as `Ctrl` (TinyMCE-style) to avoid OS-detection complexity.
+
+**Alternative considered:** platform detection to show Cmd on macOS. Deferred — adds branching for marginal benefit; can revisit.
+
+### Decision 5: Text "?" button via `ToolbarDefaultButton` children
+
+Render the button using `ToolbarDefaultButton` with `children="?"`, bypassing the icon-font ``. Rationale: no help glyph exists in the font; adding one requires font tooling. The `.icons` CSS hides `svg` inside toolbar buttons, so an inline SVG would need a CSS exception — text is simpler.
+
+## Risks / Trade-offs
+
+- **Static catalog drifts from real bindings** → Colocate the catalog with clear "keep in sync" comments; add a unit test asserting the catalog covers the shortcuts owned by this repo's custom extensions (Indent, Fullscreen, KeyboardNavigation). Built-in TipTap combos remain manually verified.
+- **Generic `Ctrl` labels are inaccurate on macOS** (Cmd) → Documented as a known limitation; low impact, revisitable via platform detection later.
+- **Escape key collision** — Escape already closes fullscreen (`Fullscreen.ts`) and returns focus to editor (`KeyboardNavigation.ts`). The modal's Escape handler must stop propagation / take precedence while open so it doesn't also trigger those → scope the handler to the open modal and `stopPropagation`.
+- **Text "?" visual inconsistency** with icon-font buttons → acceptable interim; style via existing button classes for alignment.
+
+## Migration Plan
+
+Additive, non-breaking. New property defaults to `true` but only surfaces under the full toolbar, so existing basic/standard configurations are unchanged. No data migration. Rollback = revert the change; no persisted state introduced.
+
+## Open Questions
+
+- Should a follow-up add a dedicated help icon glyph to the font (replacing the text "?")? Out of scope now.
+- Should platform-aware key labels (Cmd vs Ctrl) be added later? Deferred.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/proposal.md
new file mode 100644
index 0000000000..9b7f608a92
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/proposal.md
@@ -0,0 +1,35 @@
+## Why
+
+The Rich Text widget supports many keyboard shortcuts (formatting, indentation, history, and accessibility navigation), but users have no way to discover them from within the editor. TinyMCE and other editors expose a help menu listing shortcuts; the Rich Text widget lacks an equivalent, so shortcuts remain hidden and underused — hurting both productivity and keyboard/accessibility users.
+
+## What Changes
+
+- Add a help button ("?" text icon) to the Rich Text toolbar.
+- Clicking the button opens a centered modal dialog listing available keyboard shortcuts, grouped by category (Formatting, Paragraph, History, Accessibility navigation).
+- The modal follows the existing dialog pattern (overlay, click-outside to close, Escape to close) and is accessible (`role="dialog"`, `aria-modal`, focus management).
+- Add a new `helpButton` boolean widget property (default `true`), placed after `tableBetter` in the toolbar-groups configuration.
+- The help button renders only when the full set of toolbar groups is shown (preset `full`, or custom mode with all groups enabled) AND `helpButton` is not disabled.
+- The shortcut list is a static, manually-synced source (no runtime enumeration — TipTap provides no such API).
+
+No breaking changes. The property defaults to `true`, but the button only appears under the full toolbar, so basic/standard presets are unaffected.
+
+## Capabilities
+
+### New Capabilities
+
+- `rich-text-help-shortcuts`: A toolbar help button and keyboard-shortcuts modal for the Rich Text widget — its visibility gating, modal behavior, accessibility, and the catalog of shortcuts displayed.
+
+### Modified Capabilities
+
+
+
+## Impact
+
+- **Widget XML**: new `helpButton` property in `src/RichText.xml` after `tableBetter`.
+- **Generated typings**: `typings/RichTextProps.d.ts` regenerated from XML (not hand-edited).
+- **Editor config**: `src/RichText.editorConfig.ts` — add `helpButton` to `toolbarGroupKeys` so it hides in Studio Pro unless preset is `custom`, matching sibling group toggles.
+- **Props plumbing**: `src/RichText.tsx` → `src/components/EditorWrapper.tsx` → `src/components/Editor.tsx` → `Toolbar`.
+- **Toolbar rendering**: `src/components/toolbars/Toolbar.tsx` — conditional help button + "all groups shown" gating; new `HelpDialog` component and a static `shortcuts` list module.
+- **Styling**: reuse existing `Dialog.scss` (centered-modal pattern already present via `ConfirmDialog`).
+- **Tests**: unit tests for gating logic, modal open/close, and accessibility attributes.
+- No new runtime dependencies.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/specs/rich-text-help-shortcuts/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/specs/rich-text-help-shortcuts/spec.md
new file mode 100644
index 0000000000..095580383a
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/specs/rich-text-help-shortcuts/spec.md
@@ -0,0 +1,106 @@
+## ADDED Requirements
+
+### Requirement: Help button configuration property
+
+The Rich Text widget SHALL expose a `helpButton` boolean property (default `true`) that controls whether the keyboard-shortcuts help button is available. In Studio Pro this property SHALL be shown only when the toolbar `preset` is `custom`, consistent with the other toolbar-group toggles.
+
+#### Scenario: Default value
+
+- **WHEN** the widget is added to a page without changing the property
+- **THEN** `helpButton` defaults to `true`
+
+#### Scenario: Property visibility in Studio Pro
+
+- **WHEN** the toolbar `preset` is not `custom`
+- **THEN** the `helpButton` property is hidden in the Studio Pro property editor
+
+#### Scenario: Property visible under custom preset
+
+- **WHEN** the toolbar `preset` is `custom`
+- **THEN** the `helpButton` property is visible in the Studio Pro property editor
+
+### Requirement: Help button visibility gating
+
+The help button SHALL render in the toolbar only when `helpButton` is not disabled AND the full set of toolbar groups is shown (preset `full`, or `custom` preset with every toolbar group enabled). Under any smaller toolbar (basic/standard presets, or custom with fewer groups) the help button SHALL NOT render.
+
+#### Scenario: Full preset shows the button
+
+- **WHEN** the toolbar preset is `full` and `helpButton` is `true`
+- **THEN** the help button ("?") is rendered in the toolbar
+
+#### Scenario: Basic and standard presets hide the button
+
+- **WHEN** the toolbar preset is `basic` or `standard`
+- **THEN** the help button is not rendered, regardless of the `helpButton` value
+
+#### Scenario: Custom preset with all groups shows the button
+
+- **WHEN** the preset is `custom` and every toolbar group is enabled and `helpButton` is `true`
+- **THEN** the help button is rendered
+
+#### Scenario: Custom preset missing a group hides the button
+
+- **WHEN** the preset is `custom` and at least one toolbar group is disabled
+- **THEN** the help button is not rendered
+
+#### Scenario: Help button disabled
+
+- **WHEN** `helpButton` is `false`
+- **THEN** the help button is not rendered even under the full toolbar
+
+### Requirement: Keyboard-shortcuts modal
+
+Activating the help button SHALL open a centered modal dialog that lists available keyboard shortcuts. The dialog SHALL be dismissible by clicking outside it and by pressing Escape, matching the widget's existing dialog behavior.
+
+#### Scenario: Open modal
+
+- **WHEN** the user clicks the help button
+- **THEN** a centered modal dialog listing keyboard shortcuts is displayed
+
+#### Scenario: Close by clicking outside
+
+- **WHEN** the modal is open and the user clicks outside the dialog
+- **THEN** the modal closes
+
+#### Scenario: Close with Escape
+
+- **WHEN** the modal is open and the user presses Escape
+- **THEN** the modal closes and focus returns to a sensible element
+
+### Requirement: Modal accessibility
+
+The modal SHALL be accessible: it SHALL use `role="dialog"` with `aria-modal="true"`, have an accessible name (dialog title), manage focus on open, and support keyboard dismissal.
+
+#### Scenario: Dialog semantics
+
+- **WHEN** the modal is open
+- **THEN** it exposes `role="dialog"`, `aria-modal="true"`, and an accessible name referencing the dialog title
+
+#### Scenario: Focus on open
+
+- **WHEN** the modal opens
+- **THEN** focus moves into the dialog
+
+### Requirement: Shortcut catalog
+
+The modal SHALL display a static, manually maintained catalog of keyboard shortcuts grouped into categories: Formatting, Paragraph, History, and Accessibility navigation. The catalog SHALL include, at minimum: bold, italic, underline, strikethrough, superscript, subscript; indent and outdent; undo and redo; and the accessibility navigation shortcuts (focus toolbar via Alt+F10, focus status bar via Alt+F11, return to editor / exit fullscreen via Escape).
+
+#### Scenario: Formatting shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the Formatting category lists bold, italic, underline, and strikethrough with their key combinations
+
+#### Scenario: Paragraph shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the Paragraph category lists indent and outdent with their key combinations
+
+#### Scenario: History shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the History category lists undo and redo with their key combinations
+
+#### Scenario: Accessibility navigation shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the Accessibility category lists focus toolbar (Alt+F10), focus status bar (Alt+F11), and return-to-editor/exit-fullscreen (Escape)
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/tasks.md
new file mode 100644
index 0000000000..1ec65d7dc6
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-rich-text-help-shortcuts/tasks.md
@@ -0,0 +1,39 @@
+## 1. Widget property & config
+
+- [x] 1.1 Add `` (caption "Keyboard shortcuts") after `tableBetter` in `src/RichText.xml`
+- [x] 1.2 Add `"helpButton"` to `toolbarGroupKeys` in `src/RichText.editorConfig.ts` so it hides unless preset is `custom`
+- [x] 1.3 Regenerate `typings/RichTextProps.d.ts` from XML via the build (do not hand-edit); confirm `helpButton` appears in props
+
+## 2. Shortcut catalog
+
+- [x] 2.1 Create `src/components/toolbars/helpers/shortcuts.ts` exporting a static grouped catalog (Formatting, Paragraph, History, Accessibility) with "keep in sync" comment
+- [x] 2.2 Populate Formatting (bold, italic, underline, strikethrough, superscript, subscript), Paragraph (indent Ctrl+], outdent Ctrl+[), History (undo, redo), Accessibility (Alt+F10, Alt+F11, Escape)
+
+## 3. Help dialog component
+
+- [x] 3.1 Create `src/components/toolbars/components/HelpDialog.tsx` — centered modal mirroring `ConfirmDialog`, reusing `Dialog.scss`
+- [x] 3.2 Render the shortcut catalog grouped by category with key combos
+- [x] 3.3 Add accessibility: `role="dialog"`, `aria-modal="true"`, accessible name from title, focus into dialog on open
+- [x] 3.4 Dismiss on click-outside and on Escape (scope Escape to open modal, `stopPropagation` to avoid fullscreen/editor handlers)
+
+## 4. Toolbar integration
+
+- [x] 4.1 Create a help toolbar button (text "?") via `ToolbarDefaultButton` with `children="?"`, toggling the modal via local state
+- [x] 4.2 In `Toolbar.tsx`, render the help button outside `filteredGroups`, gated on `helpButton !== false && filteredGroups.length === TOOLBAR_GROUPS.length`
+- [x] 4.3 Plumb `helpButton` prop: `RichText.tsx` → `EditorWrapper.tsx` → `Editor.tsx` → `Toolbar`
+
+## 5. Styling
+
+- [x] 5.1 Ensure the "?" button aligns with icon-font buttons using existing button classes; add minimal styles for the help dialog list layout if needed
+
+## 6. Tests
+
+- [x] 6.1 Unit test: button renders under full preset / custom-all-groups, hidden under basic/standard, custom-missing-group, and `helpButton=false`
+- [x] 6.2 Unit test: modal opens on click, closes on click-outside and Escape
+- [x] 6.3 Unit test: dialog exposes `role="dialog"`, `aria-modal`, accessible name; focus moves into dialog on open
+- [x] 6.4 Unit test: catalog covers shortcuts owned by custom extensions (Indent, Fullscreen, KeyboardNavigation) — drift guard
+- [x] 6.5 Update snapshots if applicable (`pnpm run test -u`)
+
+## 7. Docs
+
+- [x] 7.1 Add a user-facing CHANGELOG.md entry describing the new help button and shortcuts modal
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/.openspec.yaml b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/.openspec.yaml
new file mode 100644
index 0000000000..c0a8162549
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-21
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/design.md
new file mode 100644
index 0000000000..b5d3d87c51
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/design.md
@@ -0,0 +1,160 @@
+## Context
+
+The rich text editor uses TipTap. The `TableBackgroundColor` extension (extends `@tiptap/extension-table`) already:
+
+- Declares custom table attributes (`backgroundColor`, `borderColor`, `borderStyle`, `borderWidth`) in both `inline` and `class` style-data formats.
+- Derives the table width from `colwidth` via `createColGroup()`: if every column has a fixed `colwidth`, the table gets `width: ΣpX`; otherwise `min-width: ΣpX`.
+- Uses a hand-written plain-DOM NodeView (`TableBackgroundColorNodeView`) when `resizable` and editable, which builds the `` and applies table styles imperatively.
+
+The table configuration dropdown (`createTableConfigurationSections()`) already reads/writes table attrs and supports `colorPicker`, `dropdown`, and `numberInput` section types. The `numberInput` control (with unit label, clear button, placeholder) was added by the `table-column-width-control` capability and is reused here.
+
+There is currently **no** stored table `width`/`height` — the footprint is entirely `colwidth`-derived, and there are no drag handles on the table.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Store explicit table `width` and `minHeight` as Table node attributes.
+- Provide numeric inputs ("Table Width", "Table Height") in the table configuration dropdown.
+- Provide drag-to-resize handles on the table, similar to image/embed resize.
+- Coexist cleanly with the existing per-column `colwidth` feature.
+- Support both `inline` and `class` style-data formats.
+- Validate/clamp inputs; clear resets to auto (null).
+
+**Non-Goals:**
+
+- Percentage or non-pixel units (pixels only, matching column-width feature).
+- Fixed (non-growing) table height — height is `min-height`, rows still grow with content.
+- Rescaling/redistributing column widths when the table is resized (Strategy C rejected).
+- Converting the table NodeView to a React NodeView.
+- Per-row height control.
+
+## Decisions
+
+### Decision 1: Store `width` / `minHeight` as Table node attributes (image-like)
+
+**Rationale:** User wants explicit table sizing behaving like image/embed, where size is a direct node attribute. Storing on the node makes it persistent, undoable, and serializable in both style-data formats.
+
+**Alternatives considered:**
+
+- Derive everything from `colwidth` (status quo): no way to set an explicit table footprint.
+- Scale columns proportionally (Strategy C): coherent but rejected — more math and couples tightly to the column-width feature.
+
+**Implementation:** Add `width` and `minHeight` attributes in `addAttributes()`, mirroring the existing `borderWidth` attribute (parse from `element.style` / `data-*`, render into the merged style string / class attrs).
+
+---
+
+### Decision 2: Table width wins; `colwidth` feeds `min-width` (Strategy A)
+
+**Rationale:** Explicit table `width` and per-column `colwidth` must coexist. Making `attrs.width` the table footprint while `colwidth` continues to emit per-column `min-width` (which `createColGroup()` already does when columns aren't all fixed) lets both features work together without one silently disabling the other.
+
+**Precedence:**
+
+```
+table.style.width = attrs.width (NEW — explicit footprint, wins)
+table.style.min-height = attrs.minHeight (NEW — no conflict, nothing derives height today)
+ = min-width from colwidth (EXISTING — unchanged)
+```
+
+`createColGroup()` logic is untouched; the new attributes are layered on top in `renderHTML()` and `updateTableStyles()`. When `attrs.width` is set it overrides the `colwidth`-derived `width`/`min-width` on the table element.
+
+**Alternatives considered:**
+
+- Strategy B (mutually exclusive): predictable but confusing ("why doesn't my table width apply?").
+- Strategy C (rescale columns): rejected per Decision 1.
+
+---
+
+### Decision 3: Height is `min-height`, not fixed height
+
+**Rationale:** Table rows are content-driven. A fixed height would force distribution across rows and risk content overflow. `min-height` gives users a floor while letting rows grow naturally. There is no existing derived table height, so there is no conflict.
+
+---
+
+### Decision 4: Add resize handles to the existing plain-DOM NodeView (no React rewrite)
+
+**Rationale:** The table uses a hand-written `TableBackgroundColorNodeView` (~180 lines managing colgroup + color/border styles). Converting it to a React NodeView (like `ImageResize.tsx`) to reuse the React resize component would rewrite working code and risk regressions in color/border rendering. Adding drag handles imperatively to the existing NodeView is contained.
+
+**Implementation:**
+
+- In the NodeView constructor, append resize-handle ``s to `this.dom` (the `tableWrapper`).
+- Attach `mousedown` → document `mousemove`/`mouseup` handlers.
+- During drag: write `this.table.style.width` / `min-height` directly (live preview), tracking the live value in an **instance field** (e.g. `this.currentWidth`) — the plain-class equivalent of the `currentSize` ref used in `ImageResize.tsx`, avoiding the stale-closure trap.
+- On `mouseup`: commit **once** via `this.view.dispatch(tr.setNodeMarkup(pos, undefined, { ...attrs, width, minHeight }))` so the change is a single undoable step.
+- Do **not** dispatch per-frame (avoids history spam and re-render churn).
+
+---
+
+### Decision 5: `setTableWidth` / `setTableMinHeight` commands mirror `setTableBorderWidth`
+
+**Rationale:** The existing border-width command walks up the selection depth to the `table` node and applies `setNodeMarkup`. Reusing this exact pattern keeps the config-dropdown `onChange` handlers consistent with `tableBorderStyle`/`tableBorderWidth`.
+
+---
+
+### Decision 6: Both UIs — numeric inputs and drag handles write the same attrs
+
+**Rationale:** Numeric input gives precision; drag gives speed. Both write `attrs.width` / `attrs.minHeight`, so they stay in sync: after dragging, the config dropdown reflects the new value, and vice-versa. Clear button resets to `null` (auto).
+
+---
+
+### Decision 7: Free-form CSS size text inputs (not clamped number inputs)
+
+**Rationale:** A `type="number"` input only accepts a bare pixel count and rejects `"100%"`, `"250px"`, `"10em"`. Users need responsive units. The width/height config sections use a `textInput` type accepting any CSS length/percentage.
+
+**Safety:** Because these values are interpolated into inline `style` (same injection surface as the color feature), input is validated via `isSafeCssSize` / normalized via `normalizeCssSize` in `utils/helpers.ts` — bare numbers become ` px`, valid units pass through, empty clears to auto, invalid/unsafe is rejected. Uses `CSS.supports("width", value)` at runtime with a regex allowlist fallback for jsdom.
+
+**Consequence:** The earlier 50–2000 / 30–1000 px clamps are dropped (meaningless for `%`/`em`).
+
+---
+
+### Decision 8: Config text inputs buffer locally and commit on blur/Enter (fix focus loss)
+
+**Problem:** Committing on every keystroke caused two focus-stealing effects: (1) each `onChange` ran `editor.chain().focus()`, yanking the caret into the editor; (2) the dispatched transaction fired `selectionUpdate`, re-rendering the toolbar subtree and resetting the input value. The field blurred after each character.
+
+**Decision:** Text/number config inputs hold a **local draft** while focused and commit to the editor only on **blur** or **Enter**. The editor is not touched during typing, so no `selectionUpdate`, no toolbar re-render, no `.focus()` steal.
+
+- `onChange` → update the draft map only (no editor dispatch).
+- `onBlur` / Enter → run the section's `onChange` (validate/normalize/commit); Enter also blurs the field.
+- **Invalid on blur/Enter → revert** the input to the last committed value (`getCurrentValue()`); no stuck bad text.
+- Escape → discard the draft entry and revert.
+- Color pickers and dropdowns are **unchanged** — they commit live (no text caret to lose). Save/Cancel buttons were considered but rejected: they add UI and their scope would be ambiguous since colors already apply live. Commit-on-blur needs no new controls.
+
+**Scope note:** This fix lives in `ConfigurationDropdown` and therefore also fixes the pre-existing column-width (`cellWidth`) input, which had the same bug.
+
+## Risks / Trade-offs
+
+### Risk: `Σcolwidth > attrs.width` overflow
+
+If columns sum to more than the explicit table width, the browser must reconcile `table.style.width` against per-column `min-width`. Columns may overflow or the wrapper may need to scroll.
+
+**Mitigation:** `.tableWrapper` likely needs `overflow-x: auto`. Verify with a manual test (e.g. 3 cols @ 300px, table width 400px) during implementation. Document the observed behavior in the spec's visual-feedback scenarios.
+
+---
+
+### Risk: `ignoreMutation` swallowing drag style writes
+
+`TableBackgroundColorNodeView.ignoreMutation()` already returns `true` for attribute mutations on `this.table`, so imperative style writes during drag won't trigger a ProseMirror re-parse. The final committed value must still flow through `view.dispatch` so history/undo captures it.
+
+**Mitigation:** Only the mouseup commit dispatches; live drag mutates DOM only. Confirm undo restores the pre-drag size in one step.
+
+---
+
+### Trade-off: Not a React NodeView
+
+The table resize code diverges stylistically from image/embed (imperative vs React). Accepted to avoid rewriting the working color/border NodeView.
+
+## Migration Plan
+
+No migration needed — purely additive:
+
+- Tables without `width`/`minHeight` attrs render exactly as today (colwidth-derived).
+- New attrs are `null` by default.
+- No data model breaking changes.
+
+Rollback: remove the two config sections and the resize-handle setup from the NodeView; the new attributes are ignored when unset.
+
+## Resolved Questions
+
+1. **Handles:** SE corner (width+height) + E edge (width-only) + S edge (height-only) — full independent control.
+2. **Clamp ranges:** table width 50–2000px, min-height 30–1000px (table-appropriate, wider than the 25–1000 column range).
+3. **Aspect ratio:** independent — width and height resize freely, no ratio lock (unlike image/embed). Correct for tables since rows/columns are independent axes.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/proposal.md
new file mode 100644
index 0000000000..10e8dedc2e
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/proposal.md
@@ -0,0 +1,42 @@
+## Why
+
+Users can style tables (background, border) and set individual column widths, but cannot control the overall table size. There is no way to set an explicit table width/height, and no drag-to-resize affordance like the one images and embeds already provide. Today the table width is only ever _derived_ from the sum of column widths (`colwidth`), so users who want a specific table footprint have no direct control.
+
+This change adds table-level sizing: numeric width/height inputs in the table configuration dropdown, plus drag-to-resize handles on the table itself — mirroring the image/embed resize experience.
+
+## What Changes
+
+- Add `width` and `minHeight` attributes to the Table node (`TableBackgroundColor` extension), rendered on the `` element in both `inline` and `class` style-data formats.
+- Add two `numberInput` sections ("Table Width", "Table Height") to the table configuration dropdown, reusing the existing `numberInput` control from the column-width feature.
+- Add `setTableWidth` / `setTableMinHeight` commands (mirroring the existing `setTableBorderWidth` command pattern).
+- Add drag-to-resize handles to the existing plain-DOM `TableBackgroundColorNodeView`: live style updates during drag, single history-committing `setNodeMarkup` on mouseup.
+- Ensure explicit table width coexists with per-column `colwidth`: table `width` wins as the table footprint, `colwidth` continues to feed per-column `min-width` (Strategy A).
+
+## Capabilities
+
+### New Capabilities
+
+- `table-size-control`: table-level width and min-height control via numeric inputs in the table configuration dropdown, with validation, clear-to-auto support, and persistence.
+- `table-drag-resize`: drag-to-resize handles on the table that update width/height interactively and commit a single undoable change on release.
+
+### Modified Capabilities
+
+
+
+## Impact
+
+**Affected Files:**
+
+- `src/extensions/TableBackgroundColor.ts` — add `width`/`minHeight` attributes; render on ``; add `setTableWidth`/`setTableMinHeight` commands; add resize handles + drag handlers to `TableBackgroundColorNodeView`.
+- `src/components/toolbars/helpers/configurationHelpers.ts` — add "Table Width" and "Table Height" `numberInput` sections to `createTableConfigurationSections()`.
+- `src/ui/*.scss` (table/format styles) — styles for resize handles and hover affordance.
+
+**User Impact:**
+
+- Positive: users gain explicit table sizing and familiar drag-to-resize, consistent with image/embed.
+- No breaking changes: existing tables (no `width`/`minHeight` attr) render exactly as today via the unchanged `colwidth`-derived sizing.
+
+**Technical Impact:**
+
+- Table width now has two layered sources: explicit `attrs.width` (new, wins) and `colwidth`-derived `min-width` (existing, unchanged). See design.md for precedence rules.
+- Resize handles are added to the existing plain-DOM NodeView (no rewrite to a React NodeView), keeping the working color/border code untouched.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/specs/table-drag-resize/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/specs/table-drag-resize/spec.md
new file mode 100644
index 0000000000..9738940d43
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/specs/table-drag-resize/spec.md
@@ -0,0 +1,62 @@
+## ADDED Requirements
+
+### Requirement: Table resize handles
+
+When the editor is editable and the table is resizable, the table SHALL display drag handles that allow the user to resize the table interactively, similar to image and embed resize.
+
+#### Scenario: Handles appear on an editable table
+
+- **WHEN** the editor is editable and a resizable table is rendered
+- **THEN** the table displays resize handle(s) for adjusting its size
+
+#### Scenario: No handles in read-only mode
+
+- **WHEN** the editor is not editable
+- **THEN** the table displays no resize handles
+
+### Requirement: Interactive drag resize
+
+The system SHALL update the table size live as the user drags a resize handle.
+
+#### Scenario: Dragging updates size live
+
+- **WHEN** user presses a resize handle and drags
+- **THEN** the table's rendered width and/or min-height update continuously to follow the pointer
+
+#### Scenario: Minimum size enforced during drag
+
+- **WHEN** user drags a handle below the minimum allowed table size
+- **THEN** the table does not shrink past the minimum size
+
+### Requirement: Single undoable commit on release
+
+The system SHALL commit the dragged size as a single document change only when the user releases the pointer.
+
+#### Scenario: Size committed on mouseup
+
+- **WHEN** user releases the resize handle after dragging
+- **THEN** the final width/min-height is written to the table node attributes as one change
+
+#### Scenario: Undo restores previous size in one step
+
+- **WHEN** user resizes a table and then triggers undo
+- **THEN** the table returns to its size from before the drag in a single undo step
+
+#### Scenario: Live drag does not spam history
+
+- **WHEN** user drags a handle across many pointer positions before releasing
+- **THEN** only one entry is added to the undo history for the whole drag
+
+### Requirement: Drag and numeric input stay in sync
+
+The drag handles and the numeric configuration inputs SHALL read and write the same table size attributes.
+
+#### Scenario: Config reflects a dragged size
+
+- **WHEN** user resizes a table by dragging and then opens the table configuration dropdown
+- **THEN** the "Table Width" and "Table Height" inputs display the dragged values
+
+#### Scenario: Table reflects a configured size
+
+- **WHEN** user sets a size via the numeric inputs
+- **THEN** the table renders at that size and subsequent drags start from that size
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/specs/table-size-control/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/specs/table-size-control/spec.md
new file mode 100644
index 0000000000..134b4c7a74
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/specs/table-size-control/spec.md
@@ -0,0 +1,154 @@
+## ADDED Requirements
+
+### Requirement: Table width input control
+
+The table configuration dropdown SHALL include a text input control labeled "Table Width" that allows users to set the overall table width using any CSS length or percentage (e.g. `250px`, `100%`, `10em`).
+
+#### Scenario: Opening table configuration shows current width
+
+- **WHEN** user selects a table and opens the table configuration dropdown
+- **THEN** the "Table Width" input displays the current table width value if set, or shows empty with an "Auto" placeholder if width is auto
+
+#### Scenario: Setting a CSS length or percentage width
+
+- **WHEN** user enters a valid CSS size such as `250px` or `100%` in the "Table Width" input
+- **THEN** the table `width` attribute is set to that value and the table resizes accordingly
+
+#### Scenario: Bare number is treated as pixels
+
+- **WHEN** user enters a bare number such as `250` in the "Table Width" input
+- **THEN** the value is normalized to `250px` before being applied
+
+#### Scenario: Invalid or unsafe width is rejected
+
+- **WHEN** user enters a value that is not a valid CSS size (e.g. non-size text or an injection payload)
+- **THEN** the value is not applied to the table
+
+#### Scenario: Clearing table width for auto sizing
+
+- **WHEN** user clicks the clear button or deletes the value from the "Table Width" input
+- **THEN** the table width is reset to auto (null) and the table falls back to its column-width-derived size
+
+### Requirement: Table height input control
+
+The table configuration dropdown SHALL include a text input control labeled "Table Height" that sets the table's minimum height using any CSS length or percentage.
+
+#### Scenario: Setting a table minimum height
+
+- **WHEN** user enters a valid CSS size in the "Table Height" input
+- **THEN** the table `min-height` is set to that value and the table is at least that tall
+
+#### Scenario: Rows still grow beyond minimum height
+
+- **WHEN** a table has a minimum height set and its content requires more vertical space
+- **THEN** the table grows to fit the content, exceeding the minimum height
+
+#### Scenario: Clearing table height
+
+- **WHEN** user clears the "Table Height" input
+- **THEN** the table `min-height` is reset to auto (null)
+
+### Requirement: Configuration text inputs retain focus while typing
+
+Text inputs in the table and cell configuration dropdowns SHALL retain focus while the user types and SHALL commit their value to the editor only on blur or Enter, so that editing is not interrupted by editor re-renders.
+
+#### Scenario: Typing does not lose focus
+
+- **WHEN** user types multiple characters into a configuration text input (e.g. "Table Width")
+- **THEN** the input retains focus for the entire entry and reflects each typed character without resetting
+
+#### Scenario: Commit on blur
+
+- **WHEN** user finishes typing a value and moves focus away from the input
+- **THEN** the value is validated and applied to the editor as a single change
+
+#### Scenario: Commit on Enter
+
+- **WHEN** user presses Enter while a configuration text input is focused
+- **THEN** the value is applied to the editor and the input blurs
+
+#### Scenario: Invalid value reverts on commit
+
+- **WHEN** user enters an invalid value and then blurs or presses Enter
+- **THEN** the input reverts to the last committed value and the editor is not changed
+
+#### Scenario: Escape discards the draft
+
+- **WHEN** user presses Escape while editing a configuration text input
+- **THEN** the draft is discarded and the input reverts to the last committed value
+
+#### Scenario: Color and dropdown controls remain live
+
+- **WHEN** user changes a color picker or a select dropdown in the configuration dropdown
+- **THEN** the change is applied to the editor immediately (these controls do not defer to blur)
+
+### Requirement: Table size validation
+
+The system SHALL validate table width and height input as safe CSS size values before applying them, to prevent CSS injection through the inline style.
+
+#### Scenario: Accepting valid CSS sizes
+
+- **WHEN** user enters a valid CSS length or percentage (e.g. `250px`, `100%`, `10em`, `auto`)
+- **THEN** the system accepts the value and applies it to the table
+
+#### Scenario: Rejecting non-size text
+
+- **WHEN** user enters text that is not a valid CSS size
+- **THEN** the system ignores the input and retains the previous value
+
+#### Scenario: Rejecting injection payloads
+
+- **WHEN** user enters a value containing CSS-breakout characters or `url(`/`expression(` sequences
+- **THEN** the system rejects the value and does not apply it to the table
+
+### Requirement: Table width and column width coexistence
+
+The system SHALL apply an explicit table width as the table footprint while continuing to honor per-column `colwidth` as column minimum widths.
+
+#### Scenario: Explicit table width with column widths set
+
+- **WHEN** a table has an explicit `width` attribute and one or more columns have `colwidth` set
+- **THEN** the table element renders with the explicit `width`, and each sized column renders with its `colwidth` as a `min-width`
+
+#### Scenario: No explicit table width falls back to derived sizing
+
+- **WHEN** a table has no `width` attribute set
+- **THEN** the table size is derived from the sum of column widths exactly as before this feature
+
+### Requirement: Column width as CSS size
+
+The column width control SHALL store the width as a CSS size string (e.g. `250px`, `50%`) on the cell and SHALL apply it to the column via the table's ``, which is authoritative under `table-layout: fixed`.
+
+#### Scenario: Setting a column width resizes the column
+
+- **WHEN** user enters a valid CSS size in the "Column Width" input for a cell
+- **THEN** the corresponding column resizes to that width
+
+#### Scenario: Percentage column width
+
+- **WHEN** user enters a percentage such as `50%` for a column width
+- **THEN** the column is sized to that percentage of the table
+
+#### Scenario: Bare number treated as pixels
+
+- **WHEN** user enters a bare number such as `120` for a column width
+- **THEN** the value is normalized to `120px` and applied
+
+#### Scenario: Clearing column width
+
+- **WHEN** user clears the "Column Width" input
+- **THEN** the column width is reset to auto
+
+### Requirement: Table size persistence
+
+The system SHALL persist table width and min-height values in the document in both inline and class style-data formats.
+
+#### Scenario: Size persists after save and reload
+
+- **WHEN** user sets a table width and height, saves the document, and reloads
+- **THEN** the table width and min-height remain and are displayed correctly in the configuration dropdown
+
+#### Scenario: Size serializes in the active style-data format
+
+- **WHEN** the widget is configured for `class` style-data format and a table size is set
+- **THEN** the size is serialized via data attributes/classes consistent with the other table attributes, and via inline `style` when configured for `inline` format
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/tasks.md
new file mode 100644
index 0000000000..45387cf9b4
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-21-add-table-size-resize/tasks.md
@@ -0,0 +1,75 @@
+## 1. Table node attributes
+
+- [x] 1.1 Add `width` attribute to `addAttributes()` in `src/extensions/TableBackgroundColor.ts` (default null; parse from `element.style.width` / `data-width`; render into merged style / class attrs — mirror `borderWidth`)
+- [x] 1.2 Add `minHeight` attribute the same way (parse from `element.style.minHeight` / `data-min-height`)
+- [x] 1.3 In `renderHTML()`, layer `attrs.width` over the `colwidth`-derived `width`/`min-width` (explicit width wins) and append `min-height` from `attrs.minHeight`, for `inline` format
+- [x] 1.4 In `renderHTML()`, emit `data-width` / `data-min-height` (and associated class) for `class` format
+- [x] 1.5 Mirror 1.3/1.4 in `TableBackgroundColorNodeView.updateTableStyles()` so the NodeView applies the same size
+
+## 2. Table size commands
+
+- [x] 2.1 Declare `setTableWidth` / `setTableMinHeight` in the `tableBackgroundColor` command module interface
+- [x] 2.2 Implement both in `addCommands()` mirroring `setTableBorderWidth` (walk selection depth to `table`, `setNodeMarkup` with new attr)
+- [x] 2.3 Support clearing to `null` (auto) when passed an empty value
+
+## 3. Table configuration dropdown
+
+- [x] 3.1 Add "Table Width" `numberInput` section to `createTableConfigurationSections()` in `configurationHelpers.ts` (min/max/step, placeholder "Auto", unit "px")
+- [x] 3.2 `getCurrentValue` reads `getTableAttributes(editor)?.width` (strip "px" to number, or null)
+- [x] 3.3 `onChange` parses/validates/clamps and calls `setTableWidth` (or clears to null on empty)
+- [x] 3.4 Add "Table Height" `numberInput` section wired to `minHeight` via `setTableMinHeight`
+
+## 4. Drag-to-resize handles (NodeView)
+
+- [x] 4.1 In `TableBackgroundColorNodeView` constructor, append resize-handle ``(s) to `this.dom` (SE corner + E + S edges per resolved design)
+- [x] 4.2 Add `mousedown` handler that records start pointer + start table rect, attaches document `mousemove`/`mouseup`
+- [x] 4.3 On `mousemove`, compute new width/min-height, enforce clamp bounds, write to `this.table.style` live, and store in instance fields (`this.currentWidth` / `this.currentMinHeight`) — avoid stale-closure trap
+- [x] 4.4 On `mouseup`, commit once via `this.view.dispatch(setNodeMarkup(pos, { ...attrs, width, minHeight }))`; remove document listeners
+- [x] 4.5 Ensure no per-frame dispatch (history stays a single step per drag)
+- [x] 4.6 Confirm `ignoreMutation()` still swallows the live style writes on `this.table` (+ handles)
+
+## 5. Styling
+
+- [x] 5.1 Add resize-handle styles (position, size, cursor) scoped to the table wrapper in `src/ui/TableStyle.scss`
+- [x] 5.2 Add hover/active affordance; handles only rendered in editable mode (NodeView only exists when editable)
+- [x] 5.3 Ensure `.tableWrapper` handles overflow (`overflow-x`) when `Σcolwidth > table width` (already had `overflow-x: auto`)
+
+## 6. Testing & verification
+
+> Section 6 = manual browser verification (drag/undo/reload). Verified by user in a running Mendix editor: table resize, config width/height inputs, and column width all working.
+
+- [x] 6.1 Set table width via text input → table resizes; value persists after reload (inline format)
+- [x] 6.2 Same for class style-data format (data attributes emitted)
+- [x] 6.3 Set table min-height → table at least that tall; add content → grows beyond minimum
+- [x] 6.4 Clear width/height → reverts to auto / colwidth-derived sizing
+- [x] 6.5 Valid CSS size (`250px`, `100%`, bare `250`→px) applies; invalid/injection text rejected
+- [x] 6.6 Coexistence: set column widths AND explicit table width → both apply
+- [x] 6.7 Drag SE handle → live resize; release → single committed change
+- [x] 6.8 Undo after drag → restores pre-drag size in one step
+- [x] 6.9 Drag then open config dropdown → inputs reflect dragged size (and vice-versa)
+- [x] 6.10 Read-only editor → no handles rendered; existing color/border behavior unchanged (regression check)
+
+## 7. Free-form CSS size text inputs (done during feedback)
+
+- [x] 7.1 Add `isSafeCssSize` / `normalizeCssSize` to `utils/helpers.ts` (CSS.supports + regex fallback; bare number → px)
+- [x] 7.2 Add `"textInput"` type to `ConfigurationSection` (both `ToolbarConfig.ts` and `ConfigurationDropdown.tsx`) + render case
+- [x] 7.3 Switch "Table Width"/"Table Height" sections to `textInput`; validate via `normalizeCssSize`; drop px clamps
+- [x] 7.4 Fix handle alignment: `.tableWrapper` `width: fit-content; max-width: 100%` so handles track table edges
+- [x] 7.5 Unit tests for `isSafeCssSize` + `normalizeCssSize`
+
+## 8. Config text inputs commit on blur/Enter (fix focus loss)
+
+- [x] 8.1 In `ConfigurationDropdown.tsx`, make `numberInput`/`textInput` `onChange` write to the local draft map ONLY (removed the per-keystroke `section.onChange(value)` call)
+- [x] 8.2 Commit on `onBlur`: run `section.onChange(draft)`, then clear the draft entry (`commitDraft`)
+- [x] 8.3 Add `onKeyDown`: Enter → commit + blur the input; Escape → discard draft + revert (`handleDraftKeyDown`)
+- [x] 8.4 On invalid commit, revert the input to `getCurrentValue()` (draft cleared after commit; invalid ignored by `configurationHelpers` onChange, so input shows committed value)
+- [x] 8.5 Remove the per-keystroke `.focus()` cause: size `onChange` handlers now only run on commit (blur/Enter), not per character
+- [x] 8.6 Leave colorPicker + dropdown branches live (unchanged)
+- [x] 8.7 Verify: type multi-char value in Table/Cell width input → focus retained, commits on blur/Enter, invalid reverts, colors still live (verified by user)
+
+## 9. Column width as cell style (CSS string, replaces numeric colwidth in config)
+
+- [x] 9.1 Add `cellWidth` string attribute to `TableCellBackgroundColor` (parse from `style.width` / `data-cell-width`; render into cell style + `data-cell-width`)
+- [x] 9.2 `createColGroup` reads first-row `cellWidth` string and emits `width` on ` ` (authoritative under `table-layout: fixed`); falls back to native `colwidth`; percent widths excluded from fixed px total
+- [x] 9.3 Switch "Column Width" config section from `numberInput`/`colwidth` to `textInput`/`cellWidth` with `normalizeCssSize` validation
+- [x] 9.4 Verified by user: altering column width resizes the column (px and %)
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/.openspec.yaml b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/.openspec.yaml
new file mode 100644
index 0000000000..c0a8162549
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-21
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/design.md
new file mode 100644
index 0000000000..9f1a4f4098
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/design.md
@@ -0,0 +1,85 @@
+## Context
+
+The Rich Text widget (`packages/pluggableWidgets/rich-text-web`) is a TipTap-based editor. Its toolbar, dropdowns, dialogs, help modal, and status bar render ~120–150 hardcoded English strings. These live in two shapes:
+
+1. **Static module consts** — `TOOLBAR_GROUPS` in `ToolbarConfig.ts` (~60 strings), `SHORTCUT_CATEGORIES` in `helpers/shortcuts.ts` (~29 strings), and section builders in `helpers/configurationHelpers.ts` (~30 strings, produced at runtime inside `customAction`).
+2. **Inline JSX** — dialog fields and menu tooltips in `LinkDialog.tsx`, `ImageDialog.tsx`, `VideoDialog.tsx`, `LinkBubbleMenu.tsx`, `StatusBar.tsx`, `HelpDialog.tsx`.
+
+Constraints: no mobx needed (bundles are static, not per-instance props); no XML/property changes (app developers do not supply translations); the repo already reads Mendix locale via `window.mx.session.getConfig().locale.code` in other widgets, but the decision for this change is to key off the page language (`document.documentElement.lang`).
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Localize every toolbar-facing UI string in the widget from bundled translations.
+- Resolve the active language from the page automatically; no configuration.
+- Keep static config serializable/static (avoid converting consts into factories that ripple through Toolbar/Editor/tests).
+- English fallback for any missing locale or missing key.
+
+**Non-Goals:**
+
+- Localizing editor _content_ or user-entered text.
+- App-developer-supplied per-instance translations (XML props) — explicitly out of scope for now.
+- Localizing font family names or font-size numeric labels.
+- Right-to-left (RTL) layout support.
+- A build-time translation extraction/CI pipeline (bundles are hand-maintained JSON).
+
+## Decisions
+
+### Decision 1: Locale source = page language, not Mendix session
+
+Resolve order: `document.documentElement.lang` → `navigator.language` → `"en"`. Normalize to a 2-letter code (`de-DE` → `de`, `pt_BR` → `pt`).
+
+- **Why**: Directed by product owner; page `lang` is simple, testable, and available in Studio Pro preview and jest (where `window.mx` is often absent).
+- **Alternative rejected**: `window.mx.session.getConfig().locale.code` (repo precedent in date-time-picker/google-tag). More authoritative but undefined in preview/tests and adds a runtime dependency. `navigator.language` retained only as second fallback for empty `lang`.
+
+### Decision 2: i18n keys over factory functions
+
+Replace `title: "Bold"` with `titleKey: "toolbar.bold"` in static config; resolve `t(key)` at the render leaf. Inline JSX uses `t()` directly.
+
+- **Why**: `TOOLBAR_GROUPS` stays a static module const — no signature changes to `Toolbar.tsx`/`Editor.tsx`, no re-memoization concerns. Smaller blast radius.
+- **Alternative rejected**: `getToolbarGroups(t)` factory — ripples through every consumer, memoization, and tests.
+
+### Decision 3: Translation delivery via React context + `useT()` hook
+
+A `TranslationProvider` (mounted once in `Editor.tsx`) resolves the locale, merges the chosen bundle over the `en` base, and exposes a `t(key, vars?)` function. Leaf components call `useT()`.
+
+- **Why**: Avoids prop-drilling `t` through the toolbar factory tree. Mirrors file-uploader's context shape but with no mobx (static data). Locale resolved once per mount.
+- **Alternative rejected**: A module-level singleton `t`. Harder to test (global state), and can't react if the provider needs props later.
+
+### Decision 4: `configurationHelpers.ts` receives `t` as an argument
+
+Section builders run inside `customAction` at runtime and return `ConfigurationSection[]`; their labels are not render-time JSX. The builder functions take `t` as a parameter, threaded from the call site (which has context access).
+
+- **Why**: These strings materialize outside React render, so a hook can't reach them; explicit `t` arg is the clean seam.
+
+### Decision 5: Bundle format and fallback
+
+One JSON file per language under `src/utils/i18n/locales/` (`en`, `nl`, `de`, `fr`, `es`), flat dot-namespaced keys (e.g. `toolbar.bold`, `dialog.link.url`, `shortcut.category.formatting`). `en.json` is the base; the active locale is shallow-merged over `en`, so any missing key falls back to English. Unknown locale → `en` only.
+
+- **Why**: Flat keys are greppable and match how strings map 1:1 to UI. Merge-over-base gives free per-key fallback with no runtime error.
+
+## Risks / Trade-offs
+
+- [Translation drift: new UI strings added without a key] → Lint/review convention: no string literals in the modified files' render paths; `en.json` is the single source of key truth. Fallback to `en` prevents runtime breakage.
+- [Empty or non-standard `document.documentElement.lang`] → `navigator.language` then `en` fallback; normalization strips region and lowercases.
+- [Snapshot test churn] → `ToolbarDefaultButton.spec.tsx` snapshots will change once titles resolve via `t`; update snapshots and add explicit locale-resolution unit tests so the behavior is asserted, not just snapshotted.
+- [Studio Pro editor preview lacks a provider] → `RichText.editorPreview.tsx` either mounts the provider or `useT` degrades to the `en` base when no provider is present (safe default in the hook).
+- [Incomplete non-English bundles] → Acceptable; missing keys render English. Bundles can be filled incrementally.
+
+## Migration Plan
+
+Internal-only, no data migration and no widget version-breaking change:
+
+1. Add `src/utils/i18n/` (resolver, loader, context, hook) and 5 locale JSON files.
+2. Swap static-config strings to keys; resolve at leaves; convert inline JSX strings to `t()`.
+3. Thread `t` into `configurationHelpers.ts` builders.
+4. Mount `TranslationProvider` in `Editor.tsx` (and preview).
+5. Update tests + snapshots; add resolver unit tests.
+
+Rollback: revert the change; no persisted state or API surface affected.
+
+## Open Questions
+
+- Should the editor preview render localized text or always English? (Leaning English base for design-time stability.)
+- Confirm final language set stays `en/nl/de/fr/es` for the first release, or trim to `en`-scaffold if translations aren't ready.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/proposal.md
new file mode 100644
index 0000000000..1e2a5b01c0
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/proposal.md
@@ -0,0 +1,29 @@
+## Why
+
+The Rich Text widget renders all toolbar tooltips, dropdown labels, dialog fields, help-shortcut text, and status-bar labels as hardcoded English strings. Mendix apps run in many languages, so non-English users see an untranslated toolbar. We want the widget UI to follow the page language automatically, with translations bundled in the widget (no per-instance configuration by app developers).
+
+## What Changes
+
+- Add a bundled internationalization (i18n) layer to the Rich Text widget covering all toolbar-facing UI text: button tooltips, heading/list dropdowns, table/cell configuration controls, link/image/video dialogs, the link bubble menu, the help dialog (shortcut categories + labels), and the status-bar aria-label.
+- Resolve the active locale at runtime from `document.documentElement.lang`, falling back to `navigator.language`, then `en`. Locale keys are normalized to 2-letter codes (`de-DE` → `de`).
+- Ship 5 language bundles: `en` (base), `nl`, `de`, `fr`, `es`. Missing keys in any bundle fall back to `en`.
+- Replace hardcoded `title`/`label` strings in static config (`ToolbarConfig.ts`, `shortcuts.ts`, `configurationHelpers.ts`) with stable i18n keys, resolved via a translation function `t(key)` at render time. Inline JSX strings in dialogs/menus use `t()` directly.
+- Provide translations through a lightweight React context + `useT()` hook (no mobx; bundles are static).
+
+## Capabilities
+
+### New Capabilities
+
+- `rich-text-i18n`: Bundled localization of the Rich Text widget's toolbar and dialog UI, including locale resolution from the page language, a keyed translation lookup with English fallback, and shipped language bundles.
+
+### Modified Capabilities
+
+- `rich-text-help-shortcuts`: Help-dialog shortcut category titles and labels become localized via i18n keys instead of hardcoded English.
+
+## Impact
+
+- **New code**: `src/utils/i18n/` (locale resolver, translation loader, context + `useT` hook) and `src/utils/i18n/locales/*.json` (~120–150 keys × 5 languages).
+- **Modified code**: `ToolbarConfig.ts`, `shortcuts.ts`, `configurationHelpers.ts` (strings → keys); leaf render components (`ToolbarButton`, `ToolbarDropdown`, `ColorPicker`, `Dialog`, `TableGrid`, `ConfigurationDropdown`, `CodeView`), `HelpDialog.tsx`, `LinkDialog.tsx`, `ImageDialog.tsx`, `VideoDialog.tsx`, `LinkBubbleMenu.tsx`, `StatusBar.tsx` (resolve keys via `t`); `Editor.tsx` (mount translation provider).
+- **Tests**: `ToolbarDefaultButton.spec.tsx` and snapshots update (titles resolved via `t`); new unit tests for locale resolution + fallback.
+- **No XML/API change**: purely internal; no new widget properties, no breaking change for app developers.
+- **Dependencies**: none added (native `Intl`/DOM only).
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/specs/rich-text-help-shortcuts/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/specs/rich-text-help-shortcuts/spec.md
new file mode 100644
index 0000000000..0258c044a4
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/specs/rich-text-help-shortcuts/spec.md
@@ -0,0 +1,32 @@
+## MODIFIED Requirements
+
+### Requirement: Shortcut catalog
+
+The modal SHALL display a static, manually maintained catalog of keyboard shortcuts grouped into categories: Formatting, Paragraph, History, and Accessibility navigation. The catalog SHALL include, at minimum: bold, italic, underline, strikethrough, superscript, subscript; indent and outdent; undo and redo; and the accessibility navigation shortcuts (focus toolbar via Alt+F10, focus status bar via Alt+F11, return to editor / exit fullscreen via Escape).
+
+The category titles and shortcut labels SHALL be localized through the widget's translation layer, resolved from the active page language with English fallback. Key combination strings (e.g. `Alt+F10`) are not translated. The set of categories, shortcuts, and their key combinations is unchanged by localization.
+
+#### Scenario: Formatting shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the Formatting category lists bold, italic, underline, and strikethrough with their key combinations
+
+#### Scenario: Paragraph shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the Paragraph category lists indent and outdent with their key combinations
+
+#### Scenario: History shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the History category lists undo and redo with their key combinations
+
+#### Scenario: Accessibility navigation shortcuts listed
+
+- **WHEN** the modal is open
+- **THEN** the Accessibility category lists focus toolbar (Alt+F10), focus status bar (Alt+F11), and return-to-editor/exit-fullscreen (Escape)
+
+#### Scenario: Catalog text is localized
+
+- **WHEN** the active page language is `nl` and the modal is open
+- **THEN** the category titles and shortcut labels render in Dutch (English fallback for any missing key), while the key combination strings remain unchanged
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/specs/rich-text-i18n/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/specs/rich-text-i18n/spec.md
new file mode 100644
index 0000000000..14e90f53b5
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/specs/rich-text-i18n/spec.md
@@ -0,0 +1,78 @@
+## ADDED Requirements
+
+### Requirement: Locale resolution from page language
+
+The widget SHALL determine its active display language at render time from `document.documentElement.lang`. When that value is empty or unset, it SHALL fall back to `navigator.language`, and when that is also unavailable it SHALL fall back to `en`. The resolved value SHALL be normalized to a lowercase 2-letter language code (region and script subtags discarded, e.g. `de-DE` → `de`, `pt_BR` → `pt`).
+
+#### Scenario: Page language drives the locale
+
+- **WHEN** `document.documentElement.lang` is `"nl"` (or `"nl-NL"`)
+- **THEN** the widget resolves the active locale to `nl`
+
+#### Scenario: Empty page language falls back to navigator
+
+- **WHEN** `document.documentElement.lang` is empty or unset
+- **AND** `navigator.language` is `"fr-FR"`
+- **THEN** the widget resolves the active locale to `fr`
+
+#### Scenario: No signal falls back to English
+
+- **WHEN** neither `document.documentElement.lang` nor `navigator.language` yields a value
+- **THEN** the widget resolves the active locale to `en`
+
+### Requirement: Keyed translation lookup with English fallback
+
+The widget SHALL expose a translation function `t(key)` that returns the string for the given dot-namespaced key from the active language bundle. When the active bundle lacks that key, or the resolved locale has no bundle, the function SHALL return the value from the `en` base bundle. The `en` bundle SHALL define every key used by the widget.
+
+#### Scenario: Active bundle provides the string
+
+- **WHEN** the active locale is `de` and its bundle defines `toolbar.bold` as `"Fett"`
+- **THEN** `t("toolbar.bold")` returns `"Fett"`
+
+#### Scenario: Missing key falls back to English
+
+- **WHEN** the active locale is `de` and its bundle does not define `toolbar.subscript`
+- **AND** the `en` bundle defines `toolbar.subscript` as `"Subscript"`
+- **THEN** `t("toolbar.subscript")` returns `"Subscript"`
+
+#### Scenario: Unknown locale uses English
+
+- **WHEN** the resolved locale is `xx` and no `xx` bundle exists
+- **THEN** every `t(key)` returns the `en` bundle value
+
+### Requirement: Bundled language coverage
+
+The widget SHALL ship bundled translations for at least `en`, `nl`, `de`, `fr`, and `es`. Translations SHALL be bundled within the widget and require no configuration by the Mendix app developer (no widget XML properties for text).
+
+#### Scenario: Shipped languages available
+
+- **WHEN** the page language is any of `en`, `nl`, `de`, `fr`, or `es`
+- **THEN** the toolbar UI renders in that language without any widget property being set
+
+### Requirement: Localized toolbar and dialog UI
+
+All toolbar-facing UI text SHALL be sourced from the translation layer rather than hardcoded English literals. This SHALL cover: toolbar button tooltips, heading and list dropdown labels, table and cell configuration controls (labels, options, placeholders), the link, image, and video dialogs (labels and placeholders), the link bubble menu tooltips, and the status-bar accessible name.
+
+#### Scenario: Button tooltip is localized
+
+- **WHEN** the active locale is `nl` and the user hovers the bold button
+- **THEN** the tooltip text is the `nl` translation of "Bold", not the English literal
+
+#### Scenario: Dialog fields are localized
+
+- **WHEN** the active locale is `de` and the user opens the insert-link dialog
+- **THEN** the field labels and placeholders render from the `de` bundle (with English fallback for any missing key)
+
+#### Scenario: Status-bar accessible name is localized
+
+- **WHEN** the active locale is `fr`
+- **THEN** the status bar's accessible name is the `fr` translation, not the English literal
+
+### Requirement: Provider-independent safe default
+
+Components that consume translations SHALL render the `en` base strings when no translation provider is mounted, so the widget never renders missing or broken text (e.g. in design-time preview or isolated tests).
+
+#### Scenario: No provider renders English
+
+- **WHEN** a toolbar component is rendered without a translation provider in scope
+- **THEN** it renders the `en` base strings without throwing
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/tasks.md
new file mode 100644
index 0000000000..c00019bc19
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-22-add-rich-text-i18n/tasks.md
@@ -0,0 +1,45 @@
+## 1. i18n core layer
+
+- [x] 1.1 Create `src/utils/i18n/resolveLocale.ts`: read `document.documentElement.lang` → `navigator.language` → `"en"`, normalize to lowercase 2-letter code
+- [x] 1.2 Create `src/utils/i18n/locales/en.json` as the base bundle with flat dot-namespaced keys for every string in the inventory
+- [x] 1.3 Create `nl.json`, `de.json`, `fr.json`, `es.json` bundles (missing keys allowed; fall back to `en`)
+- [x] 1.4 Create `src/utils/i18n/translations.ts`: load bundle by locale, shallow-merge over `en` base, expose `getBundle(locale)` and `translate(bundle, key)`
+- [x] 1.5 Create `src/utils/i18n/context.tsx`: `TranslationProvider` + `useT()` hook; `useT` returns `en`-base `t` when no provider is mounted
+- [x] 1.6 Add index barrel `src/utils/i18n/index.ts`
+
+## 2. Wire provider
+
+- [x] 2.1 Mount `TranslationProvider` once in `Editor.tsx`
+- [x] 2.2 Ensure `RichText.editorPreview.tsx` renders safely (provider or `en` base default)
+
+## 3. Static config → keys
+
+- [x] 3.1 `ToolbarConfig.ts`: replace `title` string literals with `titleKey`; replace dropdown `label` literals with `labelKey`
+- [x] 3.2 `helpers/shortcuts.ts`: replace category `title` and shortcut `label` literals with keys (leave `keys` combos untranslated)
+- [x] 3.3 `helpers/configurationHelpers.ts`: add `t` parameter to section builders; replace label/option/placeholder literals with `t(key)`; update `customAction` call sites to pass `t`
+
+## 4. Resolve keys at render leaves
+
+- [x] 4.1 `ToolbarButton.tsx` and `ToolbarDropdown.tsx`: resolve `titleKey`/`labelKey` via `useT()`
+- [x] 4.2 `ColorPicker.tsx`, `Dialog.tsx`, `TableGrid.tsx`, `ConfigurationDropdown.tsx`, `CodeView.tsx`: resolve titles via `useT()`
+- [x] 4.3 `HelpDialog.tsx`: resolve category titles, shortcut labels, and Close button via `useT()`
+
+## 5. Inline JSX strings → t()
+
+- [x] 5.1 `LinkDialog.tsx`: URL/target/window labels and placeholders
+- [x] 5.2 `ImageDialog.tsx`: title, URL, database, description/title placeholders
+- [x] 5.3 `VideoDialog.tsx`: title, URL, width/height, embed-code strings
+- [x] 5.4 `LinkBubbleMenu.tsx`: "Edit link" / "Remove link" tooltips
+- [x] 5.5 `StatusBar.tsx`: status-bar aria-label
+
+## 6. Tests
+
+- [x] 6.1 Unit test `resolveLocale`: page lang, navigator fallback, English fallback, normalization
+- [x] 6.2 Unit test translation lookup: active-bundle hit, missing-key English fallback, unknown-locale English
+- [x] 6.3 Update `ToolbarDefaultButton.spec.tsx` + snapshots for `t`-resolved titles
+- [x] 6.4 Add a render test asserting a localized tooltip/label for a non-`en` locale
+
+## 7. Verify
+
+- [x] 7.1 Run `pnpm run test` in `rich-text-web` — all green
+- [x] 7.2 Manual/build check: toolbar renders localized text for `nl`/`de` via page `lang`, English fallback otherwise
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/.openspec.yaml b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/.openspec.yaml
new file mode 100644
index 0000000000..9e5b8a1905
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-23
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/design.md
new file mode 100644
index 0000000000..c641dcd41e
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/design.md
@@ -0,0 +1,47 @@
+## Context
+
+The Rich Text widget's image dialog (`ImageDialog.tsx`) hardcodes three tabs: URL, Upload, and Entity ("Media Library"). All three always render. The widget already gates configuration in `RichText.editorConfig.ts`: `imageSourceContent` and `enableDefaultUpload` are only available when `imageSource` is set, and `enableDefaultUpload` defaults to `true`. This guarantees at least two tabs are always valid (URL is unconditional).
+
+Today `imageSourceContent` is drilled from the container through `EditorWrapper → Editor → EditorInner → Toolbar → ToolbarRow → DialogToolbarButton → ImageDialog`. The two additional signals needed for tab visibility (`enableDefaultUpload`, and whether `imageSource` is present) would follow the same path if added as props.
+
+`EditorContext` already reaches `ImageDialog` — the dialog calls `useCurrentEditor()`. The provider is created in `Editor.tsx` and currently carries only `{ editor, codeViewState, codeViewDispatch }`.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Hide the Entity tab when `imageSource` is not configured.
+- Hide the Upload tab when `enableDefaultUpload` is `false`.
+- Avoid prop drilling by delivering image dialog configuration through `EditorContext`.
+- Remove the existing multi-hop drilling of `imageSourceContent`.
+
+**Non-Goals:**
+
+- Changing the editor-config gating logic in `RichText.editorConfig.ts` (already enforces the ≥2-tab invariant).
+- Changing XML schema, property keys, or the URL/Upload/Entity behaviors themselves.
+- Handling a "zero tabs" or "URL hidden" case (not possible under current gating).
+
+## Decisions
+
+**Decision: Extend `EditorContext` with an `imageConfig` block.**
+The context value gains `imageConfig: { imageSourceContent?: ReactNode; enableDefaultUpload: boolean; hasImageSource: boolean }`. `ImageDialog` reads it via `useCurrentEditor()`.
+_Alternative considered_: add a separate dedicated context/provider higher in the tree (e.g. at `RichText.tsx`). Rejected — `EditorContext` already spans the needed range and is already consumed by `ImageDialog`; a second provider adds surface area for no benefit.
+
+**Decision: Derive `hasImageSource` at the feed point, not pass raw `ListValue` down.**
+Compute `hasImageSource = imageSource != null` where the provider is fed (in `Editor`), keeping `ImageDialog` presentational and free of Mendix runtime types.
+_Alternative considered_: key visibility off `imageSourceContent != null`. Equivalent under current gating, but `imageSource` matches the stated product intent and is the true source of truth.
+
+**Decision: Remove `imageSourceContent` from the toolbar prop chain.**
+Fold `imageSourceContent` into `imageConfig`, deleting it from `ImageDialogProps`, `DialogToolbarButtonProps`, `ToolbarProps`, `ToolbarRow`, and `EditorInnerProps`. This reduces net prop drilling even while adding two new signals.
+
+**Decision: Feed the provider from `Editor`.**
+`Editor`'s `Pick ` adds `imageSource` and `enableDefaultUpload`; `EditorWrapper` forwards them (it already holds full container props). This is a 3-hop feed (`RichText → EditorWrapper → Editor`) that is unavoidable — a context must be fed somewhere — but eliminates the deeper 5-hop consume path.
+
+**Decision: Keep `activeTab` default of `"url"`.**
+URL is always rendered, so the default active tab is always valid; no reset logic for hidden tabs is required.
+
+## Risks / Trade-offs
+
+- **Provider returns `null` while editor loads** → No new race: `ImageDialog` only mounts under a live editor, after the provider is established.
+- **Existing tests pass `imageSourceContent` as a prop** → Update tests to provide it via context; treat as part of this change.
+- **Future config where URL could be hidden** → Out of scope; current gating guarantees URL is always present, so no zero/one-tab handling is added.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/proposal.md
new file mode 100644
index 0000000000..af2e1cd83b
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/proposal.md
@@ -0,0 +1,27 @@
+## Why
+
+The Rich Text image dialog always renders all three source tabs (URL, Upload, Media Library/entity), even when the widget is configured so a mode is unavailable. The entity tab appears with no data source behind it, and the upload tab appears even when the app maker disabled default upload. Tabs should reflect the widget configuration.
+
+## What Changes
+
+- Hide the **Entity** ("Media Library") tab when no image data source is configured (`imageSource` is null/undefined).
+- Hide the **Upload** tab when `enableDefaultUpload` is `false`.
+- The **URL** tab is always shown; at least two tabs are always present (enforced by existing `RichText.editorConfig.ts` gating).
+- Move the image dialog configuration (`imageSourceContent`, `enableDefaultUpload`, and a derived "has image source" flag) into `EditorContext` so `ImageDialog` reads it directly, removing the existing multi-hop prop drilling of `imageSourceContent` through Toolbar → ToolbarRow → Dialog.
+
+## Capabilities
+
+### New Capabilities
+
+- `rich-text-image-dialog`: Configuration-driven visibility of the image source tabs (URL / Upload / Entity) in the Rich Text image insertion dialog.
+
+### Modified Capabilities
+
+
+
+## Impact
+
+- **Widget**: `packages/pluggableWidgets/rich-text-web`
+- **Files**: `EditorContext.tsx`, `Editor.tsx`, `EditorWrapper.tsx`, `Toolbar.tsx`, `components/toolbars/components/Dialog.tsx`, `components/toolbars/components/ImageDialog.tsx`, `components/toolbars/helpers/toolbarTypes.ts`
+- **Behavior**: End-user visible change in the image dialog (fewer tabs in some configurations). No XML/schema change; no breaking API change.
+- **Tests**: Unit tests for tab visibility across configurations; existing ImageDialog/RichText tests updated for the context-based prop delivery.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/specs/rich-text-image-dialog/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/specs/rich-text-image-dialog/spec.md
new file mode 100644
index 0000000000..cc9dbee8a6
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/specs/rich-text-image-dialog/spec.md
@@ -0,0 +1,37 @@
+## ADDED Requirements
+
+### Requirement: Image dialog tabs reflect widget configuration
+
+The Rich Text image dialog SHALL render only the image source tabs that are available for the current widget configuration. The URL tab SHALL always be rendered. The Upload tab SHALL be rendered only when `enableDefaultUpload` is `true`. The Entity ("Media Library") tab SHALL be rendered only when an image data source is configured (`imageSource` is not null or undefined). Hidden tabs SHALL NOT be rendered in the DOM (not merely disabled).
+
+#### Scenario: No image data source configured
+
+- **WHEN** the image dialog is opened and `imageSource` is null or undefined
+- **THEN** the Entity tab is not rendered
+- **AND** the URL and Upload tabs are rendered
+
+#### Scenario: Default upload disabled
+
+- **WHEN** the image dialog is opened, `imageSource` is configured, and `enableDefaultUpload` is `false`
+- **THEN** the Upload tab is not rendered
+- **AND** the URL and Entity tabs are rendered
+
+#### Scenario: All sources available
+
+- **WHEN** the image dialog is opened, `imageSource` is configured, and `enableDefaultUpload` is `true`
+- **THEN** the URL, Upload, and Entity tabs are all rendered
+
+#### Scenario: Default URL tab remains valid
+
+- **WHEN** the image dialog is opened in any configuration
+- **THEN** the URL tab is rendered and is the initially active tab
+
+### Requirement: Image dialog configuration delivered via editor context
+
+The image dialog configuration (image source content, default-upload flag, and whether an image data source is present) SHALL be provided to the image dialog through the shared editor context rather than passed as props through intermediate toolbar components.
+
+#### Scenario: Dialog reads configuration from context
+
+- **WHEN** the image dialog renders
+- **THEN** it obtains image source content, the default-upload flag, and the has-image-source flag from the editor context
+- **AND** intermediate toolbar components do not forward these values as props
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/tasks.md
new file mode 100644
index 0000000000..92b51ff1a1
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-23-rich-text-image-dialog-tab-visibility/tasks.md
@@ -0,0 +1,29 @@
+## 1. Extend EditorContext with image configuration
+
+- [x] 1.1 Add `ImageDialogConfig` type (`imageSourceContent?: ReactNode`, `enableDefaultUpload: boolean`, `hasImageSource: boolean`) and add `imageConfig: ImageDialogConfig` to `EditorContextValue` in `components/EditorContext.tsx`
+- [x] 1.2 Update `EditorContextProvider` to accept an `imageConfig` prop and pass it into the context value
+
+## 2. Feed configuration from the editor down to the provider
+
+- [x] 2.1 In `components/EditorWrapper.tsx`, destructure `imageSource` and `enableDefaultUpload` from props and forward them to `Editor`
+- [x] 2.2 In `components/Editor.tsx`, add `imageSource` and `enableDefaultUpload` to the `Pick` types (`EditorProps`) and thread them to where the provider is rendered
+- [x] 2.3 In `components/Editor.tsx`, build `imageConfig` (deriving `hasImageSource = imageSource != null`) and pass it to `EditorContextProvider`
+
+## 3. Remove imageSourceContent prop drilling
+
+- [x] 3.1 Remove `imageSourceContent` from `EditorInnerProps` and the `` call in `components/Editor.tsx`
+- [x] 3.2 Remove `imageSourceContent` from `ToolbarProps`, `ToolbarRow`, and the `DialogToolbarButton` usage in `components/toolbars/Toolbar.tsx`
+- [x] 3.3 Remove `imageSourceContent` from `DialogToolbarButtonProps` and the `` call in `components/toolbars/components/Dialog.tsx`
+- [x] 3.4 Remove `imageSourceContent` from `ImageDialogProps` in `components/toolbars/helpers/toolbarTypes.ts`
+
+## 4. Conditional tab rendering in ImageDialog
+
+- [x] 4.1 In `components/toolbars/components/ImageDialog.tsx`, read `imageConfig` from `useCurrentEditor()` instead of the `imageSourceContent` prop
+- [x] 4.2 Render the Upload tab button and its content only when `enableDefaultUpload` is `true`
+- [x] 4.3 Render the Entity tab button and its content only when `hasImageSource` is `true`, using `imageConfig.imageSourceContent`
+
+## 5. Tests and verification
+
+- [x] 5.1 Add/adjust unit tests covering the three configurations (no image source; upload disabled; all sources) asserting which tab buttons are rendered
+- [x] 5.2 Update existing `ImageDialog`/`RichText` tests to supply configuration via `EditorContext` instead of the removed prop
+- [x] 5.3 Run `pnpm run test` and `pnpm run lint` in `packages/pluggableWidgets/rich-text-web` and fix failures
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/.openspec.yaml b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/.openspec.yaml
new file mode 100644
index 0000000000..e8209ffaac
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-28
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/design.md
new file mode 100644
index 0000000000..9093891f98
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/design.md
@@ -0,0 +1,49 @@
+## Context
+
+The Rich Text widget's image insert dialog (`ImageDialog.tsx`) currently collects only `src`, `alt`, and `title`, then calls `editor.chain().focus().setImage(imageAttrs).run()`. The underlying `ImageResize` TipTap extension already defines `width` and `height` node attributes (both defaulting to `null`), but they are only ever populated by the drag-resize node view (`ImageResize.tsx`) after the image is on the canvas. This change surfaces those attributes at insert time.
+
+Constraints:
+
+- No node schema changes are required — `width`/`height` attributes already flow through `setImage`.
+- Dialog UI must match existing patterns (`dialog-field`, i18n via `useT`).
+- Values must be consistent with the drag-resize output, which writes pixel strings like `"300px"`.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Let users set initial width and (optionally) height when inserting an image.
+- Provide a "maintain aspect ratio" toggle that, when on, applies width only and lets the browser derive height.
+- Keep the change contained to the dialog + i18n + tests; no extension or widget-prop changes.
+
+**Non-Goals:**
+
+- Changing how drag-resize behaves (it continues to always preserve ratio).
+- Persisting "maintain aspect ratio" as a property of the image node — it is an insert-time decision only.
+- Supporting non-pixel units (`%`, `em`) or a unit picker.
+- Preloading images to read natural dimensions.
+
+## Decisions
+
+**Decision: "Maintain aspect ratio" leaves height auto (Interpretation A).**
+When checked, only `width` is applied and `height` is left `null`, so the browser renders height proportionally via `height: auto`. Chosen over computing a locked height because it requires no natural-dimension lookup (no image preloading, no async resolution differences between URL/base64/entity sources) and always stays proportional.
+
+- Alternative considered: read natural width/height and write both attributes. Rejected — adds async complexity and source-specific handling for little benefit at insert time.
+
+**Decision: Pixel-only number inputs.**
+Inputs are numeric and interpreted as pixels, applied as `"px"` strings to match the drag-resize output. A unit picker was considered but rejected as scope creep.
+
+**Decision: Checkbox defaults to ON.**
+Matches common editor behavior and avoids accidental distortion. Height input is disabled (greyed) while ON.
+
+**Decision: Non-destructive toggle.**
+Toggling the checkbox ON keeps any previously typed height value in component state; it is simply not sent on submit. Un-ticking restores the value. Avoids surprising data loss.
+
+**Decision: Apply only filled, positive-numeric values.**
+Empty or invalid (non-numeric, zero, negative) width/height are omitted from `imageAttrs`, falling back to today's natural-size behavior. Any combination of filled/empty is allowed (e.g., height only → width auto).
+
+## Risks / Trade-offs
+
+- [Ratio checkbox is insert-time only, but drag-resize always keeps ratio] → Acceptable; an image inserted with ratio unchecked (distorted) will re-derive ratio from its current box on next drag. Documented as a non-goal.
+- [Users may expect `%` or other units] → Out of scope for this change; pixel-only keeps behavior consistent with existing resize. Can be revisited later.
+- [Disabled height field showing a stale typed value could confuse] → Field is visually greyed while checkbox is ON, signalling it is inactive; value is preserved intentionally.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/proposal.md
new file mode 100644
index 0000000000..e00b3314e4
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/proposal.md
@@ -0,0 +1,30 @@
+## Why
+
+When inserting an image, users can only set its source, alt text, and title. Width and height can only be set _after_ insertion by dragging the resize handles. Users who know the dimensions they want (or need a consistent size) have no way to specify them up front, forcing an extra manual resize step for every image.
+
+## What Changes
+
+- Add a "Dimensions" section to the image insert dialog with **Width** and **Height** number inputs (pixel values).
+- Add a **Maintain aspect ratio** checkbox that defaults to checked.
+- When the checkbox is checked, the Height input is disabled and only `width` is applied on insert; the browser derives height proportionally (`height: auto`).
+- When the checkbox is unchecked, both Width and Height are applied as entered (image may be distorted — explicit user choice).
+- Width/height are optional; any combination of filled/empty is allowed, and only filled, positive-numeric values are applied. Empty or invalid values fall back to today's natural-size behavior.
+- New i18n keys for the dimension labels and checkbox across all supported locales.
+
+## Capabilities
+
+### New Capabilities
+
+
+
+### Modified Capabilities
+
+- `rich-text-image-dialog`: The image dialog gains initial width/height inputs and an aspect-ratio toggle that governs how dimensions are applied on insert.
+
+## Impact
+
+- `src/components/toolbars/components/ImageDialog.tsx`: new state (`width`, `height`, `maintainRatio`), Dimensions UI block, and submit wiring passing `width`/`height` into the existing `setImage(...)` call.
+- `src/utils/i18n/locales/*.json` (en, de, es, fr, nl): new translation keys.
+- `src/components/toolbars/components/__tests__/ImageDialog.spec.tsx`: tests for the new inputs and toggle behavior.
+- No changes to the `ImageResize` node extension or node view — `width`/`height` attributes already exist on the node.
+- No `.xml` or TypeScript widget-prop changes — this is purely in-widget dialog UI.
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/specs/rich-text-image-dialog/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/specs/rich-text-image-dialog/spec.md
new file mode 100644
index 0000000000..0cfc357269
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/specs/rich-text-image-dialog/spec.md
@@ -0,0 +1,36 @@
+## ADDED Requirements
+
+### Requirement: Image dialog supports initial dimensions and aspect-ratio toggle
+
+The Rich Text image dialog SHALL provide a Width input, a Height input, and a "Maintain aspect ratio" checkbox that let the user set an image's initial dimensions at insert time. The checkbox SHALL default to checked. While the checkbox is checked, the Height input SHALL be disabled and only the width SHALL be applied to the inserted image (height left unset so the browser derives it proportionally). While the checkbox is unchecked, both Width and Height SHALL be applied as entered. Width and Height are optional; only filled, positive-numeric values SHALL be applied, and each applied value SHALL be expressed as a pixel string (e.g., `300` becomes `300px`). Empty or non-positive/non-numeric values SHALL be omitted, preserving the image's natural size. Toggling the checkbox SHALL NOT clear a previously entered Height value.
+
+#### Scenario: Insert with width and maintained aspect ratio
+
+- **WHEN** the user enters a Width of `300`, leaves "Maintain aspect ratio" checked, and inserts the image
+- **THEN** the inserted image is given a `width` of `300px`
+- **AND** no `height` attribute is applied
+
+#### Scenario: Height input disabled while ratio maintained
+
+- **WHEN** the image dialog is open and "Maintain aspect ratio" is checked
+- **THEN** the Height input is disabled
+
+#### Scenario: Insert with explicit width and height
+
+- **WHEN** the user unchecks "Maintain aspect ratio", enters a Width of `300` and a Height of `200`, and inserts the image
+- **THEN** the inserted image is given a `width` of `300px` and a `height` of `200px`
+
+#### Scenario: Empty dimensions preserve natural size
+
+- **WHEN** the user leaves both Width and Height empty and inserts the image
+- **THEN** neither `width` nor `height` is applied to the inserted image
+
+#### Scenario: Invalid dimension values are ignored
+
+- **WHEN** the user enters a non-positive or non-numeric Width
+- **THEN** no `width` attribute is applied to the inserted image
+
+#### Scenario: Toggling ratio preserves entered height
+
+- **WHEN** the user has entered a Height value with "Maintain aspect ratio" unchecked, then checks the box, then unchecks it again
+- **THEN** the previously entered Height value is still present in the Height input
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/tasks.md
new file mode 100644
index 0000000000..46fffa5cff
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-28-add-image-insert-dimensions/tasks.md
@@ -0,0 +1,23 @@
+## 1. i18n
+
+- [x] 1.1 Add `image.width`, `image.height`, and `image.maintainRatio` keys to `src/utils/i18n/locales/en.json`
+- [x] 1.2 Add translations for the same keys to `de.json`, `es.json`, `fr.json`, and `nl.json`
+
+## 2. Dialog UI and behavior
+
+- [x] 2.1 Add `width`, `height`, and `maintainRatio` (default `true`) state to `ImageDialog.tsx`
+- [x] 2.2 Add a "Dimensions" block with numeric Width and Height inputs and a "Maintain aspect ratio" checkbox using existing `dialog-field` markup and `useT` labels
+- [x] 2.3 Disable the Height input while `maintainRatio` is checked, keeping any typed value in state (do not clear on toggle)
+- [x] 2.4 In `handleSubmit`, parse width/height, apply only positive-numeric values as `"px"` strings, and omit `height` when `maintainRatio` is checked
+
+## 3. Tests
+
+- [x] 3.1 Test inserting with width + ratio maintained applies `width` only (no `height`)
+- [x] 3.2 Test Height input is disabled while ratio is checked
+- [x] 3.3 Test inserting with ratio unchecked applies both `width` and `height`
+- [x] 3.4 Test empty and invalid (non-positive/non-numeric) values are omitted
+- [x] 3.5 Test toggling the checkbox preserves a previously entered Height value
+
+## 4. Changelog
+
+- [x] 4.1 Add a CHANGELOG.md entry for the new image insert dimension inputs
diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-08-11-fix-youtube-embed-playback/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-08-11-fix-youtube-embed-playback/design.md
new file mode 100644
index 0000000000..fe92a01b0f
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-08-11-fix-youtube-embed-playback/design.md
@@ -0,0 +1,137 @@
+## Context
+
+The rich text widget uses Tiptap 3.x. YouTube support comes from `@tiptap/extension-youtube@3.29.0`, wrapped by `src/extensions/YouTubeResize.ts` purely to attach a resizable React node view:
+
+```ts
+export const YouTubeResize = Youtube.extend({
+ addNodeView() {
+ return ReactNodeViewRenderer(YouTubeResizeComponent);
+ }
+});
+```
+
+### How the stock extension handles src
+
+Verified in `node_modules/@tiptap/extension-youtube/dist/index.js`:
+
+- `addAttributes()` declares `src`, `start`, `width`, `height`. There is no `renderHTML` on the `src` attribute, and nothing normalises it on the way in.
+- `addCommands().setYoutubeVideo(options)` only runs `isValidYoutubeUrl(options.src)` and then `insertContent({ type, attrs: options })` — the URL is stored **verbatim**.
+- `addPasteRules()` returns `nodePasteRule({ find: YOUTUBE_REGEX_GLOBAL, getAttributes: match => ({ src: match.input }) })` — also verbatim.
+- `parseHTML()` matches `div[data-youtube-video] iframe` and, via `getAttributesFromYoutubeEmbedUrl`, **normalises an embed URL back to a watch URL** (`src = https://www.youtube.com/watch?v=`).
+- `renderHTML({ HTMLAttributes })` is the single place conversion happens:
+
+```js
+const embedUrl = getEmbedUrlFromYoutubeUrl({
+ url: HTMLAttributes.src,
+ nocookie,
+ controls,
+ rel,
+ startAt: HTMLAttributes.start || 0 /* … */
+});
+HTMLAttributes.src = embedUrl;
+return ["div", { "data-youtube-video": "" }, ["iframe", mergeAttributes(/* … */)]];
+```
+
+So the invariant the extension maintains is: **`attrs.src` is always the canonical watch URL; the embed URL exists only in serialized output.** That invariant is intentional and worth preserving — `parseHTML` actively converts back to it, so storing an embed URL in `attrs` would fight the round-trip.
+
+### Why the node view breaks it
+
+`ReactNodeViewRenderer` supplies the editor's DOM for the node, bypassing `renderHTML()` entirely (`renderHTML` remains in use by `DOMSerializer` for `editor.getHTML()`, and by the markdown spec). `src/components/YouTubeResize.tsx:82` therefore renders the raw canonical URL:
+
+```tsx
+
+```
+
+YouTube responds to `/watch` with `X-Frame-Options: sameorigin` (after a redirect to `youtube.com/`), so the frame is refused. Nothing about resizing, `pointer-events`, or SCSS is involved — `.resize-handles` has `pointer-events: none` but the handles re-enable it, and the iframe itself is never covered.
+
+```
+ ┌──────────────────────── node.attrs.src (canonical watch URL) ────────────────────────┐
+ │ │
+ ┌─────────▼──────────┐ ┌───────────────▼──────────────┐
+ │ renderHTML() │ getEmbedUrlFromYoutubeUrl(...) │ YouTubeResize node view │
+ │ DOMSerializer / │ ──────────────────────────────▶ /embed/ OK │ | |