diff --git a/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/.openspec.yaml b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/.openspec.yaml
new file mode 100644
index 0000000000..8803b473ec
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-12
diff --git a/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/design.md b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/design.md
new file mode 100644
index 0000000000..1076cbebc3
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/design.md
@@ -0,0 +1,176 @@
+## Context
+
+Rich text widget renders ordered (`
`) and unordered (`
`) lists using Tiptap's StarterKit extensions. Current SCSS (`RichText.scss` lines 158-162) only sets basic padding and margin:
+
+```scss
+ul,
+ol {
+ padding-left: 1.5em;
+ margin: 0.5em 0;
+}
+```
+
+All nesting levels use browser defaults:
+
+- Ordered lists: `list-style-type: decimal` (1, 2, 3) at all levels
+- Unordered lists: `list-style-type: disc` (●) at all levels
+
+Standard word processors (Word, Google Docs, Notion) auto-cycle list styles by depth for visual hierarchy. Users coming from these tools expect similar behavior.
+
+Recent change `fix-list-tab-indent` added Tab key support for nesting lists, making this enhancement more valuable.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Auto-cycle ordered list styles: decimal → lower-alpha → lower-roman (repeat)
+- Auto-cycle unordered list styles: disc → circle → square (repeat)
+- CSS-only implementation (zero JavaScript)
+- Preserve inline style overrides (user-set `style="list-style-type: ..."`)
+- Support 6+ nesting levels
+
+**Non-Goals:**
+
+- User controls for changing list style (Phase 2 scope)
+- Toolbar UI changes
+- Extending Tiptap list extensions
+- Data model changes (no new attributes)
+- RTL-specific list markers
+
+## Decisions
+
+### Decision 1: Pure CSS nested selectors
+
+**Choice**: Use CSS descendant combinators (`ol ol { ... }`) to target nested lists.
+
+**Rationale**:
+
+- Zero JavaScript overhead
+- Standard CSS, widely supported
+- Works immediately with existing HTML structure
+- No Tiptap extension modifications needed
+- Inline styles naturally override via CSS specificity
+
+**Alternatives considered**:
+
+- Data attributes (`data-list-level="2"`) → Requires Tiptap extension to inject attributes, adds complexity
+- JavaScript depth calculation → Performance overhead, unnecessary for static styling
+- CSS `:nth-child()` or counters → Incorrect semantic (targets item position, not nesting depth)
+
+### Decision 2: Cycle sequence matches Word
+
+**Choice**:
+
+- Ordered: decimal (1, 2, 3) → lower-alpha (a, b, c) → lower-roman (i, ii, iii)
+- Unordered: disc (●) → circle (○) → square (■)
+
+**Rationale**:
+
+- Industry standard (Microsoft Word, Google Docs use same sequence)
+- User familiarity reduces cognitive load
+- Lower-case styles preferred for nested content (less visual weight than uppercase)
+
+**Alternatives considered**:
+
+- Upper-case roman/alpha at level 1 → Too visually heavy for running text
+- Custom sequence → No benefit, breaks user expectations
+
+### Decision 3: Define 6 nesting levels explicitly
+
+**Choice**: Write CSS rules for 6 levels of nesting (2 full cycles).
+
+**Rationale**:
+
+- Covers 95%+ of real-world use cases
+- Cost is ~30 lines of CSS (cheap)
+- Deeper nesting (7+) falls back to browser defaults (acceptable edge case)
+
+**Alternatives considered**:
+
+- 3 levels only → Breaks at 4th level (incomplete cycle)
+- 9 levels → Unnecessary (deeply nested lists are rare, unreadable)
+- Infinite via preprocessor loops → Adds build complexity for marginal gain
+
+### Decision 4: Preserve inline style precedence
+
+**Choice**: CSS specificity naturally prioritizes inline styles. No `!important` needed for base rules.
+
+**Rationale**:
+
+- Users who manually set `style="list-style-type: upper-roman;"` keep their choice
+- Supports future Phase 2 (user overrides via toolbar)
+- Standard CSS behavior, no surprises
+
+**CSS specificity**:
+
+- Inline style: 1000 points (highest)
+- `ol ol ol`: 3 points
+- Inline wins automatically
+
+## Risks / Trade-offs
+
+**[Risk]** Users with existing nested lists see visual change after upgrade → **Mitigation**: Document in CHANGELOG as enhancement. Not breaking (only affects appearance, not functionality).
+
+**[Risk]** Deeply nested lists (7+ levels) don't cycle correctly → **Mitigation**: Define extra levels if needed (cheap). Fallback to browser default (decimal/disc) is acceptable.
+
+**[Risk]** User expectations from non-Word apps → **Mitigation**: Word/Google Docs are dominant, setting de facto standard. Other tools vary, but decimal→alpha→roman is most common.
+
+**[Trade-off]** No per-list customization in Phase 1 → Accepted. Phase 2 adds toolbar controls for manual override.
+
+**[Trade-off]** CSS file size increases ~40 lines → Negligible (gzipped impact <0.5KB).
+
+## Implementation
+
+**File modified**: `packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss`
+
+**Change location**: Lines 158-162 (existing `ul, ol` rules)
+
+**Strategy**: Replace simple `ul, ol` selector with nested rules:
+
+```scss
+// Ordered list auto-cycling (6 levels = 2 full cycles)
+ol {
+ list-style-type: decimal;
+ ol {
+ list-style-type: lower-alpha;
+ ol {
+ list-style-type: lower-roman;
+ ol {
+ list-style-type: decimal; // Cycle repeats
+ ol {
+ list-style-type: lower-alpha;
+ ol {
+ list-style-type: lower-roman;
+ }
+ }
+ }
+ }
+ }
+}
+
+// Unordered list auto-cycling (6 levels = 2 full cycles)
+ul {
+ list-style-type: disc;
+ ul {
+ list-style-type: circle;
+ ul {
+ list-style-type: square;
+ ul {
+ list-style-type: disc; // Cycle repeats
+ ul {
+ list-style-type: circle;
+ ul {
+ list-style-type: square;
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+Keep existing padding/margin rules intact.
+
+## Open Questions
+
+None. Design is complete and straightforward.
diff --git a/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/proposal.md b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/proposal.md
new file mode 100644
index 0000000000..1d9e132d06
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/proposal.md
@@ -0,0 +1,48 @@
+## Why
+
+Nested lists in rich text widget currently use same numbering style at all levels (1, 2, 3 everywhere). Standard word processors (Word, Google Docs) automatically cycle through different styles for visual hierarchy (decimal → lower-alpha → lower-roman). This improves readability of complex nested lists.
+
+## What Changes
+
+- Ordered lists (``) will auto-cycle numbering styles based on nesting depth:
+ - Level 1: decimal (1, 2, 3)
+ - Level 2: lower-alpha (a, b, c)
+ - Level 3: lower-roman (i, ii, iii)
+ - Level 4+: cycle repeats (decimal → lower-alpha → lower-roman)
+- Unordered lists (`
`) will auto-cycle bullet styles based on nesting depth:
+ - Level 1: disc (●)
+ - Level 2: circle (○)
+ - Level 3: square (■)
+ - Level 4+: cycle repeats (disc → circle → square)
+- CSS-only implementation (no JavaScript changes)
+- No toolbar changes or user override controls (Phase 1 scope)
+
+## Capabilities
+
+### New Capabilities
+
+- `list-style-auto-cycle`: Nested ordered and unordered lists automatically cycle through numbering/bullet styles based on depth
+
+### Modified Capabilities
+
+
+
+## Impact
+
+**Files affected**:
+
+- `packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss` — Add nested selector rules for `ol` and `ul`
+
+**User-facing changes**:
+
+- Nested lists will display different numbering/bullet styles automatically
+- Matches standard word processor behavior
+- Existing lists with manual `style="list-style-type: ..."` inline styles unchanged (inline styles override CSS)
+- No breaking changes — pure visual enhancement
+
+**Testing scope**:
+
+- Visual verification of 6 nesting levels for ordered lists
+- Visual verification of 4 nesting levels for unordered lists
+- Mixed list types (ordered inside unordered, vice versa)
+- Task lists (`ul[data-type="taskList"]`) should remain unaffected
diff --git a/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/specs/list-style-auto-cycle/spec.md b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/specs/list-style-auto-cycle/spec.md
new file mode 100644
index 0000000000..2cf6cdef03
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/specs/list-style-auto-cycle/spec.md
@@ -0,0 +1,96 @@
+## ADDED Requirements
+
+### Requirement: Ordered lists cycle through numbering styles by nesting depth
+
+Ordered lists (``) SHALL automatically cycle through numbering styles based on nesting level using CSS descendant selectors.
+
+#### Scenario: Level 1 ordered list uses decimal
+
+- **WHEN** an ordered list is at the top level (not nested)
+- **THEN** list items display with decimal numbering (1, 2, 3)
+
+#### Scenario: Level 2 ordered list uses lower-alpha
+
+- **WHEN** an ordered list is nested one level deep inside another ordered list
+- **THEN** list items display with lower-alpha numbering (a, b, c)
+
+#### Scenario: Level 3 ordered list uses lower-roman
+
+- **WHEN** an ordered list is nested two levels deep
+- **THEN** list items display with lower-roman numbering (i, ii, iii)
+
+#### Scenario: Level 4 ordered list cycles back to decimal
+
+- **WHEN** an ordered list is nested three levels deep
+- **THEN** list items display with decimal numbering (1, 2, 3)
+
+#### Scenario: Level 5 ordered list cycles to lower-alpha
+
+- **WHEN** an ordered list is nested four levels deep
+- **THEN** list items display with lower-alpha numbering (a, b, c)
+
+#### Scenario: Level 6 ordered list cycles to lower-roman
+
+- **WHEN** an ordered list is nested five levels deep
+- **THEN** list items display with lower-roman numbering (i, ii, iii)
+
+### Requirement: Unordered lists cycle through bullet styles by nesting depth
+
+Unordered lists (`
`) SHALL automatically cycle through bullet styles based on nesting level using CSS descendant selectors.
+
+#### Scenario: Level 1 unordered list uses disc
+
+- **WHEN** an unordered list is at the top level (not nested)
+- **THEN** list items display with disc bullets (●)
+
+#### Scenario: Level 2 unordered list uses circle
+
+- **WHEN** an unordered list is nested one level deep inside another unordered list
+- **THEN** list items display with circle bullets (○)
+
+#### Scenario: Level 3 unordered list uses square
+
+- **WHEN** an unordered list is nested two levels deep
+- **THEN** list items display with square bullets (■)
+
+#### Scenario: Level 4 unordered list cycles back to disc
+
+- **WHEN** an unordered list is nested three levels deep
+- **THEN** list items display with disc bullets (●)
+
+### Requirement: Inline styles override CSS defaults
+
+When a list element has an inline `style="list-style-type: ..."` attribute, that style SHALL take precedence over CSS auto-cycle rules.
+
+#### Scenario: Manually styled list preserves inline style
+
+- **WHEN** an ordered list has `style="list-style-type: upper-roman;"`
+- **THEN** list displays with upper-roman (I, II, III) regardless of nesting depth
+
+#### Scenario: Nested list without inline style uses auto-cycle
+
+- **WHEN** parent list has inline style but nested child list has no inline style
+- **THEN** child list uses CSS auto-cycle rules based on its depth
+
+### Requirement: Task lists remain unstyled
+
+Task lists with `data-type="taskList"` SHALL remain unstyled with `list-style: none`, unaffected by auto-cycle rules.
+
+#### Scenario: Task list ignores auto-cycle
+
+- **WHEN** a list has `data-type="taskList"` attribute
+- **THEN** list displays with no bullet/number markers (checkboxes only)
+
+### Requirement: Mixed list types cycle independently
+
+When ordered and unordered lists are mixed (e.g., `
` inside ``), each list type SHALL follow its own cycle sequence.
+
+#### Scenario: Ordered list inside unordered list starts at decimal
+
+- **WHEN** an `` is nested inside a `
`
+- **THEN** ordered list uses decimal (1, 2, 3) regardless of parent's depth
+
+#### Scenario: Unordered list inside ordered list starts at disc
+
+- **WHEN** a `
` is nested inside an ``
+- **THEN** unordered list uses disc (●) regardless of parent's depth
diff --git a/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/tasks.md b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/tasks.md
new file mode 100644
index 0000000000..89ed5d5756
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/tasks.md
@@ -0,0 +1,17 @@
+## 1. Update SCSS for list style cycling
+
+- [x] 1.1 Add nested selector rules for ordered lists (ol) with 6 levels: decimal → lower-alpha → lower-roman → decimal → lower-alpha → lower-roman
+- [x] 1.2 Add nested selector rules for unordered lists (ul) with 6 levels: disc → circle → square → disc → circle → square
+- [x] 1.3 Preserve existing padding-left and margin rules for ul/ol
+
+## 2. Manual testing
+
+- [x] 2.1 Test ordered list nesting: verify 6 levels show correct cycle (1 → a → i → 1 → a → i)
+- [x] 2.2 Test unordered list nesting: verify 4 levels show correct cycle (● → ○ → ■ → ●)
+- [x] 2.3 Test mixed list types: ordered inside unordered starts at decimal, unordered inside ordered starts at disc
+- [x] 2.4 Test inline style override: list with style="list-style-type: upper-roman;" displays upper-roman at any depth
+- [x] 2.5 Test task lists remain unaffected: ul[data-type="taskList"] shows no bullets/numbers
+
+## 3. Documentation
+
+- [x] 3.1 Add entry to CHANGELOG.md describing auto-cycle enhancement
diff --git a/openspec/changes/archive/2026-07-12-fix-list-tab-indent/.openspec.yaml b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/.openspec.yaml
new file mode 100644
index 0000000000..eb5fa80e61
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-10
diff --git a/openspec/changes/archive/2026-07-12-fix-list-tab-indent/design.md b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/design.md
new file mode 100644
index 0000000000..420b25f323
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/design.md
@@ -0,0 +1,109 @@
+## Context
+
+Rich text widget uses Tiptap editor with custom `Indent` extension (`src/extensions/Indent.ts`). Current implementation:
+
+- Targets `["paragraph", "heading", "blockquote"]` node types
+- Tab handler intercepts Tab key when focus is inside editor
+- Calls `increaseIndent()`/`decreaseIndent()` commands
+- Applies margin-based indentation via `margin-left` style or `indent-*` class
+
+Tiptap's StarterKit includes list extensions with built-in commands:
+
+- `sinkListItem(typeOrName)` — increases list nesting (wraps in sublist)
+- `liftListItem(typeOrName)` — decreases list nesting
+- `listItem` node type for both ordered and unordered lists
+
+Problem: Tab handler doesn't detect list item context, applies margin-based indent to paragraph inside list item instead of structural nesting.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Tab/Shift+Tab nest/unnest list items structurally
+- Preserve existing Tab behavior for paragraphs, headings, blockquotes
+- Maintain focus handling (only intercept Tab inside editor content)
+- Work for both ordered and unordered lists
+
+**Non-Goals:**
+
+- Modifying toolbar indent buttons behavior (still call `increaseIndent`/`decreaseIndent`)
+- Adding list item support to `updateIndentLevel()` function
+- Changing maximum nesting depth limits
+- Supporting mixed content selection (list + paragraph)
+
+## Decisions
+
+### Decision 1: Modify Tab handler, not indent commands
+
+**Choice**: Add list detection to `handleKeyDown` in `addProseMirrorPlugins()`, call Tiptap's built-in list commands directly.
+
+**Rationale**:
+
+- Simplest change — Tab handler already exists and checks focus
+- Avoids modifying `increaseIndent()`/`decreaseIndent()` commands
+- Toolbar buttons unchanged (acceptable limitation for v1)
+- Leverages Tiptap's proven list nesting logic
+
+**Alternatives considered**:
+
+- Add `listItem` to `types` array and implement list logic in `updateIndentLevel()` → More complex, mixes structural changes with style changes, would affect toolbar buttons (out of scope)
+- Create separate list indent commands → Unnecessary duplication of Tiptap functionality
+
+### Decision 2: Use editor.isActive('listItem') for detection
+
+**Choice**: Check `editor.isActive('listItem')` to determine if cursor is in list.
+
+**Rationale**:
+
+- Standard Tiptap API for node detection
+- Works regardless of cursor position within list item
+- Single check covers both ordered and unordered lists
+
+**Alternatives considered**:
+
+- Check node type directly via ProseMirror state → More verbose, no benefit
+- Check parent node chain → Unnecessary complexity
+
+### Decision 3: Single typeOrName parameter 'listItem'
+
+**Choice**: Pass `'listItem'` to `sinkListItem()` and `liftListItem()` commands.
+
+**Rationale**:
+
+- `listItem` is the node type for both ordered and unordered lists in Tiptap
+- Commands handle list type conversion automatically
+- Simpler than checking `bulletList` vs `orderedList`
+
+**Alternatives considered**:
+
+- Detect list type and pass `bulletList` or `orderedList` → Unnecessary, commands work on `listItem` directly
+
+### Decision 4: Short-circuit return when in list
+
+**Choice**: When `isActive('listItem')` returns true, call list command and return result immediately (don't fall through to indent commands).
+
+**Rationale**:
+
+- List and paragraph indent are mutually exclusive behaviors
+- Prevents calling both list command and indent command
+- Clear control flow
+
+## Risks / Trade-offs
+
+**[Risk]** Toolbar indent buttons won't work on lists → **Mitigation**: Acceptable for v1, document as known limitation. Can be addressed in future by extending indent commands.
+
+**[Risk]** User muscle memory for existing Tab behavior in lists → **Mitigation**: New behavior matches industry standard (Google Docs, Word, Notion). Empty paragraph below list can still be indented.
+
+**[Risk]** Selection spanning multiple list items may behave unexpectedly → **Mitigation**: Tiptap's `sinkListItem`/`liftListItem` handle ranges. If issues arise, can add selection check.
+
+**[Trade-off]** Two separate indent systems (structure-based for lists, margin-based for paragraphs) → Accepted trade-off for simplicity. Unifying would require significant refactor.
+
+## Implementation Plan
+
+1. Modify `handleKeyDown` in `Indent.ts` `addProseMirrorPlugins()` section
+2. After Tab key check, before focus check, add list detection
+3. If `editor.isActive('listItem')`, call `sinkListItem('listItem')` or `liftListItem('listItem')`
+4. Return result immediately (don't fall through)
+5. Existing logic remains for non-list content
+
+Code location: `packages/pluggableWidgets/rich-text-web/src/extensions/Indent.ts`, lines 125-153 (handleKeyDown function).
diff --git a/openspec/changes/archive/2026-07-12-fix-list-tab-indent/proposal.md b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/proposal.md
new file mode 100644
index 0000000000..9e02a67b28
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/proposal.md
@@ -0,0 +1,38 @@
+## Why
+
+Rich text widget users cannot nest list items using Tab key. When cursor is in ordered/unordered list and Tab is pressed, content inside list item gets indented (margin added to paragraph) instead of nesting the list item structurally. Expected behavior: Tab should increase list nesting level (convert flat list to nested sublists).
+
+## What Changes
+
+- Tab key in list items will nest the list item structurally (using Tiptap's `sinkListItem` command)
+- Shift+Tab in list items will outdent/lift the list item (using Tiptap's `liftListItem` command)
+- Tab key in non-list content (paragraphs, headings) continues to work as before (margin-based indentation)
+- Toolbar indent buttons unchanged (still call `increaseIndent`/`decreaseIndent` commands)
+
+## Capabilities
+
+### New Capabilities
+
+- `list-tab-indent`: Tab/Shift+Tab keys nest and unnest list items in ordered and unordered lists
+
+### Modified Capabilities
+
+
+
+## Impact
+
+**Files affected**:
+
+- `packages/pluggableWidgets/rich-text-web/src/extensions/Indent.ts` — Tab handler logic modified
+
+**User-facing changes**:
+
+- Tab key behavior in lists now matches standard rich text editor expectations (Google Docs, Word, Notion)
+- No breaking changes — adds missing functionality
+
+**Testing scope**:
+
+- Keyboard navigation in ordered lists
+- Keyboard navigation in unordered lists
+- Mixed list types (switching between ordered/unordered)
+- Tab key in non-list content (should not regress)
diff --git a/openspec/changes/archive/2026-07-12-fix-list-tab-indent/specs/list-tab-indent/spec.md b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/specs/list-tab-indent/spec.md
new file mode 100644
index 0000000000..92e1fbdddd
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/specs/list-tab-indent/spec.md
@@ -0,0 +1,77 @@
+## ADDED Requirements
+
+### Requirement: Tab key increases list item nesting
+
+When cursor is inside a list item (ordered or unordered), pressing Tab key SHALL nest the list item one level deeper by calling Tiptap's `sinkListItem` command.
+
+#### Scenario: Tab in ordered list item
+
+- **WHEN** cursor is in an ordered list item and user presses Tab
+- **THEN** list item is nested one level deeper (becomes a sublist)
+
+#### Scenario: Tab in unordered list item
+
+- **WHEN** cursor is in an unordered list item and user presses Tab
+- **THEN** list item is nested one level deeper (becomes a sublist)
+
+#### Scenario: Tab in nested list item
+
+- **WHEN** cursor is in an already-nested list item and user presses Tab
+- **THEN** list item is nested one additional level deeper
+
+#### Scenario: Tab with cursor at any position in list line
+
+- **WHEN** cursor is anywhere in the list item text (start, middle, end) and user presses Tab
+- **THEN** entire list item is nested (not just the text)
+
+### Requirement: Shift+Tab decreases list item nesting
+
+When cursor is inside a nested list item, pressing Shift+Tab SHALL lift the list item one level up by calling Tiptap's `liftListItem` command.
+
+#### Scenario: Shift+Tab in nested ordered list
+
+- **WHEN** cursor is in a nested ordered list item and user presses Shift+Tab
+- **THEN** list item is lifted one level up (reduced nesting)
+
+#### Scenario: Shift+Tab in nested unordered list
+
+- **WHEN** cursor is in a nested unordered list item and user presses Shift+Tab
+- **THEN** list item is lifted one level up (reduced nesting)
+
+#### Scenario: Shift+Tab at top level has no effect
+
+- **WHEN** cursor is in a top-level list item (not nested) and user presses Shift+Tab
+- **THEN** no change occurs (cannot lift beyond list boundary)
+
+### Requirement: Tab in non-list content uses existing indent behavior
+
+When cursor is in paragraph, heading, or blockquote (non-list content), pressing Tab SHALL apply margin-based indentation as before.
+
+#### Scenario: Tab in paragraph
+
+- **WHEN** cursor is in a paragraph and user presses Tab
+- **THEN** paragraph receives increased margin-left indentation
+
+#### Scenario: Tab in heading
+
+- **WHEN** cursor is in a heading and user presses Tab
+- **THEN** heading receives increased margin-left indentation
+
+#### Scenario: Tab in blockquote
+
+- **WHEN** cursor is in a blockquote and user presses Tab
+- **THEN** blockquote receives increased margin-left indentation
+
+### Requirement: Tab only captures when focus is inside editor content
+
+Tab key SHALL only be intercepted for indentation when document focus is inside the editor content area, not when focus is on toolbar buttons or other UI elements.
+
+#### Scenario: Tab on toolbar button
+
+- **WHEN** focus is on a toolbar button and user presses Tab
+- **THEN** focus moves to next focusable element (standard browser Tab behavior)
+
+#### Scenario: Tab in editor content
+
+- **WHEN** focus is inside editor content area and user presses Tab
+- **THEN** Tab is intercepted for indentation (does not move focus)
diff --git a/openspec/changes/archive/2026-07-12-fix-list-tab-indent/tasks.md b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/tasks.md
new file mode 100644
index 0000000000..8134f7d6b8
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-fix-list-tab-indent/tasks.md
@@ -0,0 +1,22 @@
+## 1. Modify Tab handler in Indent extension
+
+- [x] 1.1 Add list item detection in handleKeyDown (check `editor.isActive('listItem')`)
+- [x] 1.2 When in list item and Shift+Tab pressed, call `editor.commands.liftListItem('listItem')` and return result
+- [x] 1.3 When in list item and Tab pressed, call `editor.commands.sinkListItem('listItem')` and return result
+- [x] 1.4 Ensure existing non-list Tab behavior preserved (paragraphs, headings, blockquotes)
+
+## 2. Manual testing
+
+- [x] 2.1 Test Tab in ordered list (1, 2, 3) creates nested sublist
+- [x] 2.2 Test Tab in unordered list (bullets) creates nested sublist
+- [x] 2.3 Test Shift+Tab in nested list item lifts it one level
+- [x] 2.4 Test Shift+Tab at top-level list item has no effect
+- [x] 2.5 Test Tab with cursor at start, middle, and end of list line
+- [x] 2.6 Test Tab in paragraph still applies margin indentation
+- [x] 2.7 Test Tab on toolbar button moves focus (not intercepted)
+- [x] 2.8 Test multiple Tab presses nest multiple levels
+- [x] 2.9 Test switching between ordered and unordered lists preserves nesting
+
+## 3. Documentation
+
+- [x] 3.1 Add entry to CHANGELOG.md describing new Tab behavior in lists
diff --git a/openspec/changes/archive/2026-07-12-manual-list-style-override/.openspec.yaml b/openspec/changes/archive/2026-07-12-manual-list-style-override/.openspec.yaml
new file mode 100644
index 0000000000..8803b473ec
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-manual-list-style-override/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-12
diff --git a/openspec/changes/archive/2026-07-12-manual-list-style-override/design.md b/openspec/changes/archive/2026-07-12-manual-list-style-override/design.md
new file mode 100644
index 0000000000..f7507e65de
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-manual-list-style-override/design.md
@@ -0,0 +1,250 @@
+## Context
+
+Rich text widget uses Tiptap editor with StarterKit's `OrderedList` extension. Recent Phase 1 (`auto-cycle-list-styles`) added CSS-based automatic style cycling:
+
+- Level 1: decimal → Level 2: lower-alpha → Level 3: lower-roman (repeats)
+
+Current limitations:
+
+- No way for users to manually override list style
+- All top-level lists must be decimal
+- Cannot start document with lower-alpha or lower-roman lists
+
+Widget supports two output modes via `styleDataFormat` prop:
+
+- `inline`: Outputs `style="..."` attributes
+- `class`: Outputs `data-*` attributes + CSS classes (CSP-compliant)
+
+Existing toolbar has "Numbered List" button (toggles ordered list on/off, no style control).
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Add toolbar dropdown for manual list style selection (lower-alpha, lower-roman, decimal)
+- Store style choice in `listStyleType` attribute on `` element
+- Support both inline and class-based output modes
+- Cycle offset behavior — manual styles continue cycle for nested children
+- Dropdown visible only when cursor in ordered list
+- Convert entire `` when style selected
+
+**Non-Goals:**
+
+- Upper-alpha / upper-roman styles (future Phase 3)
+- Unordered list style overrides (disc/circle/square remain auto-only)
+- Per-item styling (apply per `
` instead of per ``)
+- Split-button UI (icon + chevron in single button)
+- Keyboard shortcuts for style cycling
+- Preview of styles before selection
+
+## Decisions
+
+### Decision 1: Extend OrderedList vs create wrapper
+
+**Choice**: Create `OrderedListStyled` that extends Tiptap's `OrderedList`, add `listStyleType` attribute.
+
+**Rationale**:
+
+- Inherits all existing functionality (toggle, keyboard shortcuts, paste handling)
+- Adds single attribute without duplicating logic
+- Standard Tiptap extension pattern
+- Easy to maintain when StarterKit updates
+
+**Alternatives considered**:
+
+- Wrap OrderedList in higher-order component → More complex, loses access to extension internals
+- Modify StarterKit directly → Breaks future updates, not maintainable
+
+### Decision 2: Separate dropdown button vs split button
+
+**Choice**: Add new dropdown button "Numbering Style" next to existing "Numbered List" button. Dropdown only visible when in ordered list (`canExecute` check).
+
+**Rationale**:
+
+- Uses existing `action: "dropdown"` pattern (matches font family, font size)
+- Simpler implementation (no new component needed)
+- Better accessibility (single focus target per button)
+- Conditional visibility keeps toolbar clean when not applicable
+
+**Alternatives considered**:
+
+- Split button (icon | chevron) → Requires new component, harder accessibility, complex focus management
+- Always-visible dropdown → Clutters toolbar, confusing when not in list
+
+### Decision 3: Cycle offset for nested lists
+
+**Choice**: Manual `listStyleType` acts as "cycle position marker". Children without attribute continue cycle from that position.
+
+Example:
+
+- Parent has `listStyleType="lower-alpha"` (position 2) → children start at lower-roman (position 3)
+- Parent has `listStyleType="lower-roman"` (position 3) → children start at decimal (position 4)
+
+**CSS Implementation**:
+
+```scss
+ol[data-list-style="lower-alpha"] {
+ list-style-type: lower-alpha !important;
+
+ > li > ol:not([data-list-style]) {
+ list-style-type: lower-roman; // Continue from position 3
+ }
+}
+```
+
+**Rationale**:
+
+- Intuitive for users — "a, b, c" nests to "i, ii, iii"
+- Consistent with auto-cycle mental model
+- Predictable across nesting depths
+
+**Alternatives considered**:
+
+- Reset children to decimal → Breaks visual hierarchy, confusing
+- All children inherit same style → No nesting distinction, defeats purpose
+- No cycle offset (children use auto-cycle from level 1) → "a" → "1" → weird jump
+
+### Decision 4: Dropdown options
+
+**Choice**: 3 options:
+
+1. "1, 2, 3" (decimal) — removes attribute, uses auto-cycle
+2. "a, b, c" (lower-alpha) — sets `listStyleType="lower-alpha"`
+3. "i, ii, iii" (lower-roman) — sets `listStyleType="lower-roman"`
+
+Option 1 acts as "reset to auto" (removes attribute).
+
+**Rationale**:
+
+- Covers 95% of use cases (legal docs, formal outlines)
+- Keeps UI simple (3 choices, not overwhelming)
+- Decimal option allows intentional override (force decimal at level 2+)
+
+**Alternatives considered**:
+
+- 5 options (add upper-alpha, upper-roman) → Too many, clutters dropdown, rare use case
+- 2 options (only lower-alpha, lower-roman) → No way to reset to auto-cycle
+- No decimal option → Can't intentionally set decimal at non-top levels
+
+### Decision 5: Attribute storage format
+
+**Choice**: Use `listStyleType` attribute name (matches CSS property). Output format depends on `styleDataFormat`:
+
+- **Inline mode**: ``
+- **Class mode**: ``
+
+**parseHTML** accepts both formats (backward compat).
+
+**Rationale**:
+
+- Inline mode: Direct CSS, works immediately without stylesheet changes
+- Class mode: CSP-compliant, separates content from presentation
+- Dual parsing supports mixed content (e.g., pasted from other editors)
+
+**Alternatives considered**:
+
+- Always use inline style → Fails strict CSP policies
+- Always use class → Harder debugging, requires CSS class definitions
+- Custom attribute name (`data-mx-list-style`) → Diverges from web standards
+
+## Risks / Trade-offs
+
+**[Risk]** Users create lower-alpha at top level, expect children to be lower-roman, get decimal instead → **Mitigation**: Cycle offset CSS ensures lower-alpha (pos 2) → lower-roman (pos 3). Only issue if user expects "b" → "ii" (which would be wrong).
+
+**[Risk]** CSS specificity conflicts with existing auto-cycle rules → **Mitigation**: Use `!important` on manual overrides (`ol[data-list-style]` rules). Specificity: attribute selector (11 points) beats nested selectors (3 points).
+
+**[Risk]** Copy-paste from Word might have incompatible `list-style-type` values → **Mitigation**: `parseHTML` filters to allowed values (decimal, lower-alpha, lower-roman). Unknown values ignored, fall back to auto-cycle.
+
+**[Risk]** Users forget they set manual style, confused why nesting behaves differently → **Mitigation**: Dropdown checkmark shows current style. Decimal option allows reset.
+
+**[Trade-off]** Adding `OrderedListStyled` increases bundle size (~2KB) → Acceptable for functionality gain.
+
+**[Trade-off]** Toolbar gets more crowded with new dropdown → Mitigated by conditional visibility (only shows when in ordered list).
+
+**[Trade-off]** Two buttons for ordered lists ("Numbered List" + "Numbering Style") might confuse users → Acceptable, matches other dual-button patterns (e.g., "Bold" + "Font Family").
+
+## Implementation
+
+### Component Architecture
+
+```
+Editor.tsx
+ ├─ StarterKit (without OrderedList)
+ ├─ OrderedListStyled (replaces OrderedList)
+ │ ├─ Inherits: toggle, keyboard, paste
+ │ ├─ Adds: listStyleType attribute
+ │ ├─ Commands: setOrderedListStyle()
+ │ └─ parseHTML/renderHTML: dual-mode support
+ └─ Other extensions...
+
+ToolbarConfig.ts
+ ├─ orderedList button (existing)
+ └─ orderedListStyle dropdown (NEW)
+ ├─ canExecute: editor.isActive('orderedList')
+ ├─ getCurrentValue: reads listStyleType attribute
+ └─ dropdownOptions: decimal/lower-alpha/lower-roman
+```
+
+### Data Flow
+
+1. User clicks dropdown option "a, b, c"
+2. Calls `editor.commands.setOrderedListStyle('lower-alpha')`
+3. Command uses `updateAttributes('orderedList', { listStyleType: 'lower-alpha' })`
+4. `renderHTML` hook outputs:
+ - Inline mode: ``
+ - Class mode: ``
+5. CSS rule `ol[data-list-style="lower-alpha"]` applies `list-style-type: lower-alpha !important`
+6. Nested children match `ol[data-list-style="lower-alpha"] > li > ol:not([data-list-style])` → get `list-style-type: lower-roman`
+
+### CSS Structure
+
+```scss
+// Base auto-cycle rules (existing, from Phase 1)
+ol {
+ list-style-type: decimal;
+}
+ol ol {
+ list-style-type: lower-alpha;
+}
+ol ol ol {
+ list-style-type: lower-roman;
+}
+
+// Manual override rules (NEW)
+ol[data-list-style="lower-alpha"] {
+ list-style-type: lower-alpha !important;
+
+ > li > ol:not([data-list-style]) {
+ list-style-type: lower-roman; // Cycle offset
+ > li > ol:not([data-list-style]) {
+ list-style-type: decimal;
+ }
+ }
+}
+
+ol[data-list-style="lower-roman"] {
+ list-style-type: lower-roman !important;
+
+ > li > ol:not([data-list-style]) {
+ list-style-type: decimal; // Cycle offset
+ > li > ol:not([data-list-style]) {
+ list-style-type: lower-alpha;
+ }
+ }
+}
+
+// Class mode classes (NEW)
+.list-style-lower-alpha {
+ list-style-type: lower-alpha !important;
+}
+.list-style-lower-roman {
+ list-style-type: lower-roman !important;
+}
+.list-style-decimal {
+ list-style-type: decimal !important;
+}
+```
+
+## Open Questions
+
+None. Design is complete and validated through exploration phase.
diff --git a/openspec/changes/archive/2026-07-12-manual-list-style-override/proposal.md b/openspec/changes/archive/2026-07-12-manual-list-style-override/proposal.md
new file mode 100644
index 0000000000..2268e9c311
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-manual-list-style-override/proposal.md
@@ -0,0 +1,60 @@
+## Why
+
+Phase 1 auto-cycle provides good defaults, but users sometimes need specific numbering styles for documents with formal conventions (legal docs use lower-roman, outlines use lower-alpha at specific levels). Adding manual override gives users control while preserving automatic cycling where not overridden.
+
+## What Changes
+
+- New `OrderedListStyled` Tiptap extension replaces default `OrderedList` from StarterKit
+ - Adds `listStyleType` attribute to store manual style choice (lower-alpha, lower-roman, or null for auto-cycle)
+ - Supports both inline (`style="list-style-type: ..."`) and class-based (`data-list-style="..."` + class) modes for CSP compliance
+- New toolbar dropdown button "Numbering Style" visible only when cursor is in ordered list
+ - 3 options: "1, 2, 3" (decimal/auto-cycle), "a, b, c" (lower-alpha), "i, ii, iii" (lower-roman)
+ - Checkmark shows current style
+ - Selecting option converts entire `` to that style
+- CSS cycle offset behavior: manually styled lists continue cycle from their position
+ - lower-alpha (level 2 in cycle) → children start at lower-roman (level 3)
+ - lower-roman (level 3 in cycle) → children start at decimal (level 4)
+ - Selecting decimal removes override, reverts to auto-cycle
+- Regular "Numbered List" button unchanged — creates decimal list with no attribute (uses auto-cycle)
+
+## Capabilities
+
+### New Capabilities
+
+- `manual-list-style`: Users can manually override ordered list numbering style via toolbar dropdown (lower-alpha and lower-roman)
+
+### Modified Capabilities
+
+
+
+## Impact
+
+**Files affected**:
+
+- `packages/pluggableWidgets/rich-text-web/src/extensions/OrderedListStyled.ts` — NEW extension extending OrderedList
+- `packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx` — Replace OrderedList import with OrderedListStyled
+- `packages/pluggableWidgets/rich-text-web/src/components/toolbars/ToolbarConfig.ts` — Add dropdown button config
+- `packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss` — Add CSS override rules for manual styles (~60 lines)
+
+**Dependencies**:
+
+- No new npm packages
+- Extends existing Tiptap `OrderedList` extension
+- Uses existing dropdown component pattern
+
+**User-facing changes**:
+
+- New toolbar button appears when in ordered list
+- Users can create/convert lists to lower-alpha or lower-roman styles
+- Manual styles persist in HTML as `data-list-style` attribute (class mode) or inline style
+- Backward compatible — existing lists without attribute use auto-cycle
+
+**Testing scope**:
+
+- Create list with manual style from dropdown
+- Convert existing decimal list to lower-alpha/lower-roman
+- Verify cycle offset (nested children continue from parent's position)
+- Dropdown state (checkmark on active style)
+- Class vs inline mode output
+- Copy-paste preserves manual style
+- Inline style overrides (user-edited HTML)
diff --git a/openspec/changes/archive/2026-07-12-manual-list-style-override/specs/manual-list-style/spec.md b/openspec/changes/archive/2026-07-12-manual-list-style-override/specs/manual-list-style/spec.md
new file mode 100644
index 0000000000..6ce7af6219
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-manual-list-style-override/specs/manual-list-style/spec.md
@@ -0,0 +1,129 @@
+## ADDED Requirements
+
+### Requirement: Users can manually set ordered list style via toolbar dropdown
+
+When cursor is in an ordered list, a toolbar dropdown SHALL appear with options to set the list's numbering style to decimal (auto-cycle), lower-alpha, or lower-roman.
+
+#### Scenario: Dropdown visible when in ordered list
+
+- **WHEN** cursor is inside an ordered list
+- **THEN** "Numbering Style" dropdown button is visible in toolbar
+
+#### Scenario: Dropdown hidden when not in ordered list
+
+- **WHEN** cursor is in paragraph, unordered list, or task list
+- **THEN** "Numbering Style" dropdown button is hidden
+
+#### Scenario: Dropdown shows three style options
+
+- **WHEN** user clicks "Numbering Style" dropdown
+- **THEN** dropdown displays options: "1, 2, 3" (decimal), "a, b, c" (lower-alpha), "i, ii, iii" (lower-roman)
+
+#### Scenario: Dropdown shows checkmark on current style
+
+- **WHEN** ordered list has no `listStyleType` attribute
+- **THEN** dropdown shows checkmark next to "1, 2, 3" (decimal/auto-cycle default)
+
+#### Scenario: Dropdown shows checkmark on manual style
+
+- **WHEN** ordered list has `listStyleType="lower-alpha"` attribute
+- **THEN** dropdown shows checkmark next to "a, b, c" (lower-alpha)
+
+### Requirement: Users can convert ordered list to lower-alpha via dropdown
+
+Selecting "a, b, c" from dropdown SHALL convert entire ordered list to lower-alpha numbering style.
+
+#### Scenario: Convert decimal list to lower-alpha
+
+- **WHEN** cursor is in decimal list (1, 2, 3) and user selects "a, b, c" from dropdown
+- **THEN** entire list converts to lower-alpha numbering (a, b, c)
+
+#### Scenario: Lower-alpha attribute stored in HTML
+
+- **WHEN** list is converted to lower-alpha
+- **THEN** `` element has `listStyleType="lower-alpha"` attribute stored as `data-list-style="lower-alpha"` (class mode) or inline style (inline mode)
+
+### Requirement: Users can convert ordered list to lower-roman via dropdown
+
+Selecting "i, ii, iii" from dropdown SHALL convert entire ordered list to lower-roman numbering style.
+
+#### Scenario: Convert decimal list to lower-roman
+
+- **WHEN** cursor is in decimal list (1, 2, 3) and user selects "i, ii, iii" from dropdown
+- **THEN** entire list converts to lower-roman numbering (i, ii, iii)
+
+#### Scenario: Lower-roman attribute stored in HTML
+
+- **WHEN** list is converted to lower-roman
+- **THEN** `` element has `listStyleType="lower-roman"` attribute stored as `data-list-style="lower-roman"` (class mode) or inline style (inline mode)
+
+### Requirement: Users can revert manual style to auto-cycle via dropdown
+
+Selecting "1, 2, 3" from dropdown SHALL remove manual style override and revert list to auto-cycle behavior.
+
+#### Scenario: Revert lower-alpha to auto-cycle
+
+- **WHEN** cursor is in lower-alpha list and user selects "1, 2, 3" from dropdown
+- **THEN** `listStyleType` attribute is removed and list uses auto-cycle (decimal at top level)
+
+#### Scenario: Auto-cycle applies after revert
+
+- **WHEN** manual style is removed from nested list at level 2
+- **THEN** list displays with lower-alpha (auto-cycle for level 2)
+
+### Requirement: Manual styles use cycle offset for nested lists
+
+When an ordered list has manual `listStyleType` attribute, nested children SHALL continue cycle from parent's position in sequence.
+
+#### Scenario: Lower-alpha nested children start at lower-roman
+
+- **WHEN** ordered list has `listStyleType="lower-alpha"` (cycle position 2) and contains nested ordered list without attribute
+- **THEN** nested list displays with lower-roman (cycle position 3)
+
+#### Scenario: Lower-roman nested children start at decimal
+
+- **WHEN** ordered list has `listStyleType="lower-roman"` (cycle position 3) and contains nested ordered list without attribute
+- **THEN** nested list displays with decimal (cycle position 4, cycle repeats)
+
+#### Scenario: Decimal nested children start at lower-alpha
+
+- **WHEN** ordered list has explicit `listStyleType="decimal"` (cycle position 1) and contains nested ordered list without attribute
+- **THEN** nested list displays with lower-alpha (cycle position 2)
+
+### Requirement: Manual styles support both inline and class-based modes
+
+OrderedListStyled extension SHALL output manual styles as inline `style="list-style-type: ..."` or class-based `data-list-style="..." class="list-style-..."` depending on widget configuration.
+
+#### Scenario: Inline mode outputs inline style
+
+- **WHEN** widget configured with `styleDataFormat="inline"` and list has `listStyleType="lower-alpha"`
+- **THEN** HTML output is ``
+
+#### Scenario: Class mode outputs data attribute and class
+
+- **WHEN** widget configured with `styleDataFormat="class"` and list has `listStyleType="lower-alpha"`
+- **THEN** HTML output is ``
+
+#### Scenario: Parse inline style on load
+
+- **WHEN** HTML contains ``
+- **THEN** extension parses and stores `listStyleType="lower-roman"` attribute
+
+#### Scenario: Parse data attribute on load
+
+- **WHEN** HTML contains ``
+- **THEN** extension parses and stores `listStyleType="lower-alpha"` attribute
+
+### Requirement: Regular numbered list button creates decimal list without attribute
+
+The existing "Numbered List" toolbar button SHALL continue creating ordered lists with no `listStyleType` attribute (uses auto-cycle).
+
+#### Scenario: Numbered list button creates decimal list
+
+- **WHEN** cursor is in paragraph and user clicks "Numbered List" button
+- **THEN** ordered list is created with decimal numbering (1, 2, 3) and no `listStyleType` attribute
+
+#### Scenario: Dropdown shows decimal as active for attribute-less list
+
+- **WHEN** ordered list has no `listStyleType` attribute
+- **THEN** dropdown displays checkmark next to "1, 2, 3" (decimal)
diff --git a/openspec/changes/archive/2026-07-12-manual-list-style-override/tasks.md b/openspec/changes/archive/2026-07-12-manual-list-style-override/tasks.md
new file mode 100644
index 0000000000..d5c3a4e146
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-manual-list-style-override/tasks.md
@@ -0,0 +1,48 @@
+## 1. Create OrderedListStyled extension
+
+- [x] 1.1 Create new file `src/extensions/OrderedListStyled.ts`
+- [x] 1.2 Extend Tiptap's OrderedList with listStyleType attribute (default: null)
+- [x] 1.3 Implement parseHTML to read from inline style or data-list-style attribute
+- [x] 1.4 Implement renderHTML to output based on styleDataFormat option (inline vs class mode)
+- [x] 1.5 Add setOrderedListStyle command that calls updateAttributes with styleType parameter
+
+## 2. Update Editor to use OrderedListStyled
+
+- [x] 2.1 Import OrderedListStyled extension in Editor.tsx
+- [x] 2.2 Configure StarterKit to disable built-in OrderedList extension
+- [x] 2.3 Add OrderedListStyled to extensions array with styleDataFormat option from props
+- [x] 2.4 Verify ordered list toggle still works (inherited behavior)
+
+## 3. Add toolbar dropdown button
+
+- [x] 3.1 Add orderedListStyle dropdown config to ToolbarConfig.ts in list group
+- [x] 3.2 Configure 3 dropdown options: "1, 2, 3" (null), "a, b, c" (lower-alpha), "i, ii, iii" (lower-roman)
+- [x] 3.3 Implement getCurrentValue function to read listStyleType from editor.getAttributes('orderedList')
+- [x] 3.4 Add canExecute check: only show when editor.isActive('orderedList')
+- [x] 3.5 Map dropdown selections to setOrderedListStyle command with correct styleType
+
+## 4. Add CSS override rules
+
+- [x] 4.1 Add manual override rule for ol[data-list-style="lower-alpha"] with cycle offset (children start at lower-roman)
+- [x] 4.2 Add manual override rule for ol[data-list-style="lower-roman"] with cycle offset (children start at decimal)
+- [x] 4.3 Add manual override rule for ol[data-list-style="decimal"] (optional, for explicit decimal at any level)
+- [x] 4.4 Add class mode CSS: .list-style-lower-alpha, .list-style-lower-roman, .list-style-decimal
+- [x] 4.5 Verify !important priority works (manual overrides beat auto-cycle rules)
+
+## 5. Manual testing
+
+- [x] 5.1 Test dropdown appears when cursor in ordered list, hidden otherwise
+- [x] 5.2 Test create lower-alpha list via dropdown, verify a, b, c numbering
+- [x] 5.3 Test create lower-roman list via dropdown, verify i, ii, iii numbering
+- [x] 5.4 Test convert decimal list to lower-alpha via dropdown
+- [x] 5.5 Test convert lower-alpha list back to decimal (revert to auto-cycle)
+- [x] 5.6 Test cycle offset: lower-alpha parent → lower-roman children → decimal grandchildren
+- [x] 5.7 Test cycle offset: lower-roman parent → decimal children → lower-alpha grandchildren
+- [x] 5.8 Test dropdown checkmark shows on correct option based on current listStyleType
+- [x] 5.9 Test inline mode outputs style="list-style-type: ..."
+- [x] 5.10 Test class mode outputs data-list-style + class
+- [x] 5.11 Test copy-paste preserves manual style attribute
+
+## 6. Documentation
+
+- [x] 6.1 Add entry to CHANGELOG.md describing manual list style override feature (Phase 2)
diff --git a/openspec/changes/archive/2026-07-12-ordered-list-split-button/.openspec.yaml b/openspec/changes/archive/2026-07-12-ordered-list-split-button/.openspec.yaml
new file mode 100644
index 0000000000..8803b473ec
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-ordered-list-split-button/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-12
diff --git a/openspec/changes/archive/2026-07-12-ordered-list-split-button/design.md b/openspec/changes/archive/2026-07-12-ordered-list-split-button/design.md
new file mode 100644
index 0000000000..06759d9860
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-ordered-list-split-button/design.md
@@ -0,0 +1,465 @@
+# Design: Ordered List Split Button
+
+## Context
+
+Current toolbar implementation:
+
+- Separate `ToolbarButton` and `ToolbarDropdown` components
+- Button actions: `toggle`, `command`, `custom`, `dropdown`, `colorPicker`, `dialog`, `tableGrid`, `codeView`, `configurationDropdown`
+- List group has 4 buttons: bulletList, orderedList, taskList, orderedListStyle
+- `orderedListStyle` dropdown only enabled when OL active (`canExecute: editor => editor.isActive("orderedList")`)
+- Icons exist in RichTextIcons.scss: `List-numbers`, `List-lower-alpha`, `List-roman`
+- Toolbar config driven by `TOOLBAR_GROUPS` array in `ToolbarConfig.ts`
+- Factory pattern in `Toolbar.tsx` renders buttons based on action type
+
+Current limitation:
+
+- No composite button pattern (split button, segmented control)
+- No per-button state tracking (style preference)
+- Dropdown options only support text labels, no icons
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Create reusable `ToolbarSplitButton` component for composite button patterns
+- Merge OL toggle + style picker into single UI element
+- Dynamic icon based on active/last-used style
+- Sticky state for style preference across toggles
+- Keyboard navigation following ARIA authoring practices
+- Zero breaking changes to existing toolbar buttons
+
+**Non-Goals:**
+
+- Bullet list split button (future work)
+- Per-editor instance state isolation (module-level acceptable for MVP)
+- LocalStorage persistence (session-scoped sufficient)
+- Refactor existing button components (reuse where possible)
+
+## Decisions
+
+### 1. Component architecture: New vs Composite
+
+**Decision**: Create new `ToolbarSplitButton` component, not composite wrapper.
+
+**Rationale**:
+
+- Split button has distinct interaction model (two focus targets, shared active state)
+- Keyboard nav differs from separate buttons (arrow keys move between parts)
+- ARIA requirements specific to split button pattern (role="group", coordinated aria-pressed/expanded)
+- Reusability for future split buttons (bullet styles, etc)
+
+**Alternative considered**: Wrap existing `ToolbarButton` + `ToolbarDropdown` in container
+
+- **Rejected**: Focus management and keyboard nav would be complex to coordinate externally. Interaction logic belongs in component.
+
+### 2. State management: Where to store lastOrderedListStyle
+
+**Decision**: Module-level variable in component file.
+
+```typescript
+// ToolbarSplitButton.tsx
+let lastOrderedListStyle: "decimal" | "lower-alpha" | "lower-roman" = "decimal";
+```
+
+**Rationale**:
+
+- Simplest implementation, no extra infrastructure
+- Multiple editor instances on same page is rare edge case
+- Easy to migrate to per-editor storage later if needed
+
+**Alternatives considered**:
+
+| Approach | Pro | Con | Verdict |
+| -------------- | ------------------------ | ------------------------------------------ | ---------------- |
+| Editor.storage | Per-editor isolation | Need to update OrderedListStyled extension | Overkill for MVP |
+| React Context | React-idiomatic | More boilerplate, context provider changes | Overengineered |
+| LocalStorage | Persists across sessions | Unnecessary complexity, sync issues | Out of scope |
+
+### 3. Icon update mechanism: Pull vs Push
+
+**Decision**: Component pulls icon on render via `getCurrentIcon()` helper.
+
+```typescript
+const getCurrentIcon = (): string => {
+ if (editor?.isActive("orderedList")) {
+ const attrs = editor.getAttributes("orderedList");
+ const style = attrs.listStyleType || "decimal";
+ return STYLE_ICON_MAP[style];
+ }
+ return STYLE_ICON_MAP[lastOrderedListStyle];
+};
+```
+
+**Rationale**:
+
+- Consistent with existing button patterns (re-render on editor.on("selectionUpdate"))
+- No need to track icon state separately
+- Single source of truth: editor attributes when active, sticky state when inactive
+
+**Alternative considered**: Push updates via editor events
+
+- **Rejected**: Would require tracking previous style, detecting changes, syncing with component state. Pull is simpler.
+
+### 4. Dropdown behavior when OL inactive
+
+**Decision**: Dropdown always enabled, selecting option enables OL + applies style.
+
+**Rationale**:
+
+- Matches Word/Office pattern (dropdown accessible before list enabled)
+- Improves discoverability (user can preview style options)
+- Eliminates confusion of disabled dropdown
+
+**Alternative considered**: Dropdown disabled when inactive (current behavior)
+
+- **Rejected**: Forces two-step workflow (enable OL, then pick style). Poor UX.
+
+### 5. Command strategy: New vs Compose
+
+**Decision**: Compose existing commands in component handlers, don't add new editor command.
+
+```typescript
+// Main button click
+editor.chain().focus().toggleOrderedList().updateAttributes("orderedList", { listStyleType: style }).run();
+
+// Dropdown option click
+if (isActive) {
+ editor.chain().focus().setOrderedListStyle(style).run();
+} else {
+ editor.chain().focus().toggleOrderedList().updateAttributes("orderedList", { listStyleType: style }).run();
+}
+```
+
+**Rationale**:
+
+- Existing commands cover all operations
+- Logic belongs in component (UI concern, not editor operation)
+- Easier to test (no editor extension changes)
+
+**Alternative considered**: Add `toggleOrderedListWithStyle` command to OrderedListStyled extension
+
+- **Rejected**: Mixes UI state (sticky preference) with editor logic. Commands should be stateless.
+
+### 6. Keyboard navigation model
+
+**Decision**: Two-stop tab model with arrow key navigation between parts.
+
+```
+Tab → Focus whole split button (outline both parts)
+Enter/Space → Execute focused part's action
+ArrowRight → Move focus from main to dropdown
+ArrowLeft → Move focus from dropdown to main
+ArrowDown → Open dropdown (from either part)
+```
+
+**Rationale**:
+
+- Follows WCAG ARIA authoring practices for split button
+- Efficient keyboard workflow (Tab once, arrows to navigate parts)
+- Matches Office/Windows split button behavior
+
+**Alternative considered**: Separate tab stops for main/dropdown
+
+- **Rejected**: Doubles tab stops in toolbar, slows keyboard navigation. Split button should behave as single control.
+
+### 7. ToolbarConfig changes: Extend vs Replace
+
+**Decision**: Add `icon` field to `ToolbarDropdownOption`, add `"splitButton"` action type, keep existing config structure.
+
+```typescript
+export interface ToolbarDropdownOption {
+ label: string;
+ value: string;
+ command: string;
+ attrs?: Record;
+ icon?: string; // NEW
+}
+
+export type ToolbarActionType =
+ | "toggle"
+ | "command"
+ | "custom"
+ | "heading"
+ | "dropdown"
+ | "splitButton" // NEW
+ | "tableGrid"
+ | "colorPicker"
+ | "dialog"
+ | "codeView"
+ | "configurationDropdown";
+```
+
+**Rationale**:
+
+- Backward compatible (icon optional, splitButton only used by OL)
+- Follows existing action type pattern
+- Dropdown options reusable by ToolbarDropdown (icon rendering conditional)
+
+### 8. SCSS structure: BEM vs Nested
+
+**Decision**: Nested selectors under `.split-button` parent class.
+
+```scss
+.split-button {
+ .split-button-main {
+ /* ... */
+ }
+ .split-button-dropdown {
+ /* ... */
+ }
+ &.is-active {
+ /* ... */
+ }
+}
+```
+
+**Rationale**:
+
+- Matches existing toolbar SCSS structure (`.toolbar-group`, `.toolbar-dropdown-button`)
+- Scoping prevents class name collisions
+- Active state applies to container, styling inherited by parts
+
+## Component API
+
+### ToolbarSplitButton Props
+
+```typescript
+interface ToolbarSplitButtonProps {
+ config: ToolbarButtonConfig; // Reuse existing config type
+}
+
+// Config shape for split button:
+{
+ name: "orderedList",
+ title: "Numbered List",
+ icon: "List-numbers", // Default icon (overridden by getCurrentIcon)
+ action: "splitButton",
+ command: "toggleOrderedList", // Main button command
+ isActive: (editor) => editor.isActive("orderedList"),
+ dropdownOptions: [
+ {
+ label: "1, 2, 3",
+ value: "decimal",
+ command: "setOrderedListStyle",
+ attrs: { styleType: null },
+ icon: "List-numbers"
+ },
+ // ... more options
+ ],
+ getCurrentValue: (editor) => {
+ const attrs = editor.getAttributes("orderedList");
+ return attrs.listStyleType || "decimal";
+ }
+}
+```
+
+### Internal State
+
+```typescript
+const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+const [focusedPart, setFocusedPart] = useState<"main" | "dropdown">("main");
+const mainButtonRef = useRef(null);
+const dropdownButtonRef = useRef(null);
+```
+
+### Event Handlers
+
+```typescript
+handleMainClick(): void
+ // Toggle OL with sticky style when inactive
+ // Toggle OL off when active
+
+handleDropdownClick(): void
+ // Toggle dropdown open/closed
+
+handleOptionSelect(option: ToolbarDropdownOption): void
+ // Update sticky state
+ // Apply style + enable OL if inactive
+ // Just apply style if active
+ // Close dropdown
+
+handleKeyDown(e: KeyboardEvent, part: "main" | "dropdown"): void
+ // ArrowRight/Left: move focus between parts
+ // ArrowDown: open dropdown
+ // Enter/Space: execute focused part action
+```
+
+## Accessibility
+
+### ARIA attributes
+
+```tsx
+
+
+
+
+
;
+
+{
+ isDropdownOpen && (
+
+
+
+ );
+}
+```
+
+### Focus management
+
+- Split button container has visual focus indicator when either part focused
+- Focus outline on active part (`:focus-visible`)
+- Arrow keys move focus without triggering actions
+- Dropdown opens with first option pre-highlighted (via `aria-activedescendant`)
+
+### Screen reader announcements
+
+- Main button: "Toggle numbered list, button, pressed" (when active)
+- Dropdown button: "Numbering style options, button, collapsed/expanded"
+- Menu item: "1, 2, 3, menu item, selected" (when active style)
+
+## File changes
+
+### New files
+
+- `src/components/toolbars/components/ToolbarSplitButton.tsx` - Split button component
+- `src/components/toolbars/helpers/listHelpers.ts` - Icon mapping utilities
+
+### Modified files
+
+- `src/components/toolbars/ToolbarConfig.ts`
+ - Add `icon?: string` to `ToolbarDropdownOption`
+ - Add `"splitButton"` to `ToolbarActionType`
+ - Update `orderedList` button config in `TOOLBAR_GROUPS`
+ - Remove `orderedListStyle` standalone button
+ - Add icon to dropdown options
+
+- `src/components/toolbars/Toolbar.tsx`
+ - Import `ToolbarSplitButton`
+ - Add `case "splitButton"` to `ToolbarButtonFactory`
+
+- `src/components/toolbars/Toolbar.scss`
+ - Add `.split-button` styles
+ - Update `.toolbar-dropdown-item` for icon + text layout
+
+- `src/components/toolbars/components/ToolbarDropdown.tsx` (optional enhancement)
+ - Conditionally render option icons if present
+
+## Risks / Trade-offs
+
+### Risk: Multi-editor instance state collision
+
+**Scenario**: Page has two RichText widgets, user selects lower-alpha in widget A, then clicks OL in widget B. Widget B gets lower-alpha instead of decimal.
+
+**Mitigation**:
+
+- Acceptable for MVP (rare use case)
+- If reported, migrate to `editor.storage.orderedListStyled.lastUsedStyle`
+
+**Trade-off**: Simple implementation now vs perfect isolation
+
+---
+
+### Risk: Touch target size on mobile
+
+**Scenario**: Split button parts too small for touch (iOS Safari needs 44x44pt minimum)
+
+**Mitigation**:
+
+- Combined split button width: 56px (adequate)
+- Main button: 32px min-width
+- Dropdown: 24px min-width
+- Vertical height: 32px (matches other toolbar buttons)
+
+**Verification**: Test on iPhone during QA
+
+**Trade-off**: Desktop compactness vs mobile usability
+
+---
+
+### Risk: Keyboard nav complexity
+
+**Scenario**: Users confused by arrow key behavior (moves focus instead of navigating toolbar)
+
+**Mitigation**:
+
+- Standard split button pattern (Word, Office, Windows)
+- Arrow keys only active when split button focused
+- Tab continues to next toolbar button (normal flow)
+
+**Trade-off**: Richer interaction vs learning curve
+
+---
+
+### Risk: Icon not updating after style change
+
+**Scenario**: Component doesn't re-render after dropdown selection
+
+**Mitigation**:
+
+- `useEffect` on editor "selectionUpdate" and "transaction" events (same as existing buttons)
+- `getCurrentIcon()` recalculates on every render
+- Force re-render with state trigger: `setUpdateTrigger(prev => prev + 1)`
+
+**Trade-off**: Extra re-renders vs guaranteed sync
+
+---
+
+### Risk: Dropdown positioning with Floating UI
+
+**Scenario**: Dropdown anchored to whole split button, not dropdown part
+
+**Mitigation**:
+
+- Pass `dropdownButtonRef.current` to `useDropdown` hook (not container ref)
+- Floating UI anchors to dropdown part
+
+**Verification**: Test with scrolling, overflow containers
+
+## Migration Plan
+
+No user-facing migration needed (UI change only).
+
+### Deployment steps
+
+1. Deploy changes to dev environment
+2. Manual testing:
+ - Verify icon changes (decimal → alpha → roman)
+ - Verify sticky state across toggles
+ - Keyboard nav (Tab, arrows, Enter, ArrowDown)
+ - Touch on mobile (iPad/iPhone)
+ - Multiple editor instances (if possible)
+3. Run unit tests + E2E test
+4. Deploy to staging
+5. Notify QA team for UX verification
+6. Deploy to production
+
+### Rollback strategy
+
+If critical issue found:
+
+1. Revert PR (single PR includes all changes)
+2. Split button reverts to separate buttons
+3. No data loss (OrderedListStyled extension unchanged)
+
+## Open Questions
+
+None. Design ready for implementation.
diff --git a/openspec/changes/archive/2026-07-12-ordered-list-split-button/proposal.md b/openspec/changes/archive/2026-07-12-ordered-list-split-button/proposal.md
new file mode 100644
index 0000000000..7f8e0dbe87
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-ordered-list-split-button/proposal.md
@@ -0,0 +1,160 @@
+# Proposal: Ordered List Split Button
+
+## Problem
+
+Current toolbar has separate buttons for ordered list toggle and style picker:
+
+```
+[Bullets] [Numbers] [Checklist] [▼ Style]
+```
+
+Issues:
+
+- Style dropdown separated from list toggle (poor spatial grouping)
+- Style dropdown disabled when no OL active (discovery problem)
+- No visual feedback of current style when OL inactive
+- Doesn't match Word/Office UX pattern users expect
+
+## Solution
+
+Merge ordered list toggle and style picker into single split button:
+
+```
+[Bullets ▼] [Numbers ▼] [Checklist]
+```
+
+Split button behavior:
+
+- **Main button**: Toggle OL on/off
+- **Dropdown arrow**: Open style picker
+- **Icon**: Dynamic based on active/last-used style (1, a, i)
+- **Sticky state**: Remember last used style across toggles
+
+### Word-like interaction flow
+
+**When OL inactive:**
+
+- Main click → Enable OL with last-used style (default: decimal)
+- Dropdown click → Show styles, selecting applies style + enables OL
+- Icon shows last-used style
+
+**When OL active:**
+
+- Main click → Disable OL
+- Dropdown click → Show styles, selecting changes style
+- Icon shows current active style
+
+**Dropdown options (icon + text):**
+
+- [#] 1, 2, 3 (decimal)
+- [a] a, b, c (lower-alpha)
+- [i] i, ii, iii (lower-roman)
+
+## Benefits
+
+- **Spatial proximity**: Style control attached to list trigger
+- **Discoverability**: Dropdown always visible, not hidden when inactive
+- **Visual feedback**: Icon changes to reflect style (1 → a → i)
+- **Familiar UX**: Matches Word/Office split button pattern
+- **Less toolbar clutter**: 4 buttons → 3 buttons
+
+## Implementation approach
+
+### 1. New component
+
+Create `ToolbarSplitButton.tsx`:
+
+- Two buttons in group (main + dropdown)
+- Independent click handlers
+- Shared active state styling
+- Keyboard nav: arrows move between buttons, ArrowDown opens menu
+- ARIA: `role="group"`, `aria-pressed`, `aria-expanded`
+
+### 2. Config changes
+
+Update `ToolbarConfig.ts`:
+
+- Add `"splitButton"` action type
+- Add `icon` field to `ToolbarDropdownOption` interface
+- Update `orderedList` button config with dropdown options
+- Remove standalone `orderedListStyle` button
+- Add icon mapping helper
+
+### 3. State management
+
+Module-level sticky state:
+
+```typescript
+let lastOrderedListStyle: "decimal" | "lower-alpha" | "lower-roman" = "decimal";
+```
+
+Updated on style selection, used when toggling OL while inactive.
+
+### 4. Icon mapping
+
+```typescript
+const STYLE_ICON_MAP = {
+ decimal: "List-numbers",
+ "lower-alpha": "List-lower-alpha",
+ "lower-roman": "List-roman"
+};
+```
+
+Icons already exist in `RichTextIcons.scss`.
+
+### 5. Dropdown options
+
+Add icons to dropdown items:
+
+```tsx
+
+```
+
+### 6. SCSS
+
+`.split-button` wrapper with:
+
+- `.split-button-main` (left side, border-right)
+- `.split-button-dropdown` (right side, chevron)
+- `&.is-active` highlighting
+- Focus indicators on both buttons
+
+## Out of scope
+
+- Bullet list split button (future enhancement for bullet styles)
+- Multi-instance isolation (module-level state acceptable for MVP)
+- Persistent storage (session-scoped sticky state sufficient)
+
+## Risks
+
+- **Multi-editor instances**: Shared module state across instances on same page. Mitigation: Rare use case, can enhance later with per-editor storage.
+- **Touch targets**: 56px combined width adequate for touch, verify on mobile.
+- **Keyboard complexity**: Split button nav more complex than single button. Mitigation: Follow ARIA authoring practices.
+
+## Testing
+
+Unit tests:
+
+- Icon changes based on active style
+- Sticky state persists across toggles
+- Main button toggles OL with correct style
+- Dropdown options apply style + toggle when inactive
+- Dropdown options change style only when active
+- Keyboard navigation
+
+E2E test:
+
+- Full workflow: select style via dropdown, toggle off/on, verify sticky
+
+## Success criteria
+
+- [ ] Ordered list button merged with style picker
+- [ ] Icon updates dynamically (List-numbers, List-lower-alpha, List-roman)
+- [ ] Last-used style remembered across toggles
+- [ ] Dropdown accessible when OL inactive
+- [ ] Keyboard navigation works
+- [ ] Tests pass
+- [ ] Visual match to Word split button pattern
diff --git a/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/ordered-list-interaction/spec.md b/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/ordered-list-interaction/spec.md
new file mode 100644
index 0000000000..99186f1508
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/ordered-list-interaction/spec.md
@@ -0,0 +1,139 @@
+# Ordered List Interaction Specification
+
+## ADDED Requirements
+
+### Requirement: Sticky state tracks last-used list style
+
+The system SHALL remember the last-used ordered list style across toggle operations.
+
+#### Scenario: Default style is decimal
+
+- **WHEN** user first interacts with ordered list
+- **THEN** sticky state defaults to `"decimal"`
+
+#### Scenario: Sticky state updates on style selection
+
+- **WHEN** user selects lower-alpha from dropdown
+- **THEN** sticky state updates to `"lower-alpha"`
+
+#### Scenario: Sticky state persists after toggle off
+
+- **WHEN** user has lower-alpha active
+- **WHEN** user toggles ordered list off
+- **WHEN** user toggles ordered list on again
+- **THEN** ordered list uses lower-alpha style
+
+### Requirement: Main button toggles OL with sticky style
+
+Clicking the main button SHALL toggle ordered list on/off, using sticky style when enabling.
+
+#### Scenario: Enable OL with sticky style when inactive
+
+- **WHEN** ordered list is inactive
+- **WHEN** sticky state is lower-alpha
+- **WHEN** user clicks main button
+- **THEN** ordered list enables with lower-alpha style
+
+#### Scenario: Enable OL with decimal on first use
+
+- **WHEN** ordered list is inactive
+- **WHEN** sticky state is default (decimal)
+- **WHEN** user clicks main button
+- **THEN** ordered list enables with decimal style (null attribute)
+
+#### Scenario: Disable OL when active
+
+- **WHEN** ordered list is active
+- **WHEN** user clicks main button
+- **THEN** ordered list disables
+- **THEN** sticky state remains unchanged
+
+### Requirement: Dropdown selection applies style and enables OL
+
+Selecting a style from dropdown SHALL apply that style and enable OL if inactive.
+
+#### Scenario: Apply style when OL inactive
+
+- **WHEN** ordered list is inactive
+- **WHEN** user selects lower-roman from dropdown
+- **THEN** ordered list enables with lower-roman style
+- **THEN** sticky state updates to lower-roman
+
+#### Scenario: Change style when OL active
+
+- **WHEN** ordered list is active with decimal style
+- **WHEN** user selects lower-alpha from dropdown
+- **THEN** ordered list style changes to lower-alpha
+- **THEN** ordered list remains active
+- **THEN** sticky state updates to lower-alpha
+
+### Requirement: Icon reflects current or sticky state
+
+The split button icon SHALL dynamically change to reflect the active style or sticky state.
+
+#### Scenario: Icon shows active style
+
+- **WHEN** ordered list is active with lower-alpha
+- **THEN** icon displays List-lower-alpha
+
+#### Scenario: Icon shows sticky state when inactive
+
+- **WHEN** ordered list is inactive
+- **WHEN** sticky state is lower-roman
+- **THEN** icon displays List-roman
+
+#### Scenario: Icon updates after style change
+
+- **WHEN** user changes style from decimal to lower-alpha
+- **THEN** icon updates from List-numbers to List-lower-alpha
+
+### Requirement: Dropdown is accessible when OL inactive
+
+The dropdown button SHALL be enabled and functional even when ordered list is inactive.
+
+#### Scenario: Dropdown button is not disabled
+
+- **WHEN** ordered list is inactive
+- **THEN** dropdown button is enabled
+- **THEN** user can open dropdown menu
+
+#### Scenario: Style options are selectable when inactive
+
+- **WHEN** ordered list is inactive
+- **WHEN** dropdown menu is open
+- **THEN** all style options are clickable
+- **THEN** selecting option enables OL with that style
+
+### Requirement: Active style is visually indicated in dropdown
+
+The dropdown menu SHALL highlight the currently active or sticky style.
+
+#### Scenario: Active style is highlighted
+
+- **WHEN** ordered list is active with lower-alpha
+- **WHEN** dropdown menu is open
+- **THEN** lower-alpha option has `is-active` class
+
+#### Scenario: Sticky style is highlighted when inactive
+
+- **WHEN** ordered list is inactive
+- **WHEN** sticky state is lower-roman
+- **WHEN** dropdown menu is open
+- **THEN** lower-roman option has `is-active` class
+
+### Requirement: Sticky state is module-scoped
+
+The sticky state SHALL be stored at module level and shared across editor instances.
+
+#### Scenario: State persists during editor session
+
+- **WHEN** user selects lower-alpha
+- **WHEN** user navigates to different content area
+- **WHEN** user returns and toggles OL
+- **THEN** ordered list uses lower-alpha style
+
+#### Scenario: State resets on page reload
+
+- **WHEN** user selects lower-roman
+- **WHEN** page reloads
+- **THEN** sticky state resets to default (decimal)
diff --git a/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/split-button-component/spec.md b/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/split-button-component/spec.md
new file mode 100644
index 0000000000..f9094a7d88
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/split-button-component/spec.md
@@ -0,0 +1,185 @@
+# Split Button Component Specification
+
+## ADDED Requirements
+
+### Requirement: Split button renders main and dropdown parts
+
+The component SHALL render two interactive button elements grouped together: a main action button and a dropdown trigger button.
+
+#### Scenario: Component renders with both parts
+
+- **WHEN** split button is mounted in toolbar
+- **THEN** main button displays configured icon
+- **THEN** dropdown button displays chevron-down icon
+- **THEN** both buttons are grouped in single container with `role="group"`
+
+#### Scenario: Container has accessible label
+
+- **WHEN** split button is rendered
+- **THEN** container has `aria-label` matching button title
+
+### Requirement: Main button executes primary action
+
+The main button SHALL execute the configured command when clicked.
+
+#### Scenario: Main button click triggers command
+
+- **WHEN** user clicks main button
+- **THEN** configured command executes via editor chain
+- **THEN** focus returns to editor
+
+#### Scenario: Main button respects active state
+
+- **WHEN** configured `isActive` function returns true
+- **THEN** main button has `aria-pressed="true"`
+- **THEN** split button container has `is-active` class
+
+### Requirement: Dropdown button opens style menu
+
+The dropdown button SHALL toggle the dropdown menu when clicked.
+
+#### Scenario: Dropdown button opens menu
+
+- **WHEN** user clicks dropdown button
+- **THEN** dropdown menu becomes visible
+- **THEN** dropdown button has `aria-expanded="true"`
+
+#### Scenario: Dropdown button closes menu
+
+- **WHEN** dropdown menu is open
+- **WHEN** user clicks dropdown button again
+- **THEN** dropdown menu becomes hidden
+- **THEN** dropdown button has `aria-expanded="false"`
+
+### Requirement: Dropdown options display icon and text
+
+Each dropdown option SHALL render both an icon and text label.
+
+#### Scenario: Option renders with icon
+
+- **WHEN** dropdown menu is open
+- **THEN** each option displays icon from config `icon` field
+- **THEN** each option displays text from config `label` field
+
+#### Scenario: Active option is highlighted
+
+- **WHEN** dropdown menu is open
+- **WHEN** current value matches option value
+- **THEN** option has `is-active` class
+
+### Requirement: Selecting dropdown option updates state
+
+When user selects a dropdown option, the component SHALL execute the option's command and close the menu.
+
+#### Scenario: Option selection executes command
+
+- **WHEN** user clicks dropdown option
+- **THEN** option's command executes with configured attrs
+- **THEN** dropdown menu closes
+- **THEN** focus returns to editor
+
+### Requirement: Icon updates dynamically
+
+The main button icon SHALL change based on editor state and last-used value.
+
+#### Scenario: Icon reflects active state
+
+- **WHEN** editor has active state matching button config
+- **THEN** icon updates to reflect current state value
+
+#### Scenario: Icon reflects last-used value when inactive
+
+- **WHEN** editor does not have active state
+- **THEN** icon shows last-used value from sticky state
+
+### Requirement: Keyboard navigation between parts
+
+The component SHALL support arrow key navigation between main and dropdown buttons.
+
+#### Scenario: ArrowRight moves focus to dropdown
+
+- **WHEN** main button has focus
+- **WHEN** user presses ArrowRight
+- **THEN** focus moves to dropdown button
+
+#### Scenario: ArrowLeft moves focus to main
+
+- **WHEN** dropdown button has focus
+- **WHEN** user presses ArrowLeft
+- **THEN** focus moves to main button
+
+#### Scenario: ArrowDown opens dropdown
+
+- **WHEN** either button has focus
+- **WHEN** user presses ArrowDown
+- **THEN** dropdown menu opens
+
+### Requirement: Tab navigation treats as single control
+
+The split button SHALL act as single tab stop in toolbar navigation.
+
+#### Scenario: Tab focuses split button once
+
+- **WHEN** user tabs through toolbar
+- **THEN** split button receives focus as single stop
+- **THEN** next tab moves to next toolbar control
+
+#### Scenario: Shift+Tab navigates backward
+
+- **WHEN** split button has focus
+- **WHEN** user presses Shift+Tab
+- **THEN** focus moves to previous toolbar control
+
+### Requirement: Enter and Space execute focused part
+
+The component SHALL execute the action of whichever part has focus when Enter or Space is pressed.
+
+#### Scenario: Enter on main button executes command
+
+- **WHEN** main button has focus
+- **WHEN** user presses Enter
+- **THEN** main button command executes
+
+#### Scenario: Space on dropdown button opens menu
+
+- **WHEN** dropdown button has focus
+- **WHEN** user presses Space
+- **THEN** dropdown menu opens
+
+### Requirement: Click outside closes dropdown
+
+The dropdown menu SHALL close when user clicks outside the component.
+
+#### Scenario: Outside click closes menu
+
+- **WHEN** dropdown menu is open
+- **WHEN** user clicks outside split button
+- **THEN** dropdown menu closes
+
+### Requirement: Focus indicators are visible
+
+Both button parts SHALL display visible focus indicators for keyboard navigation.
+
+#### Scenario: Focus outline on main button
+
+- **WHEN** main button has focus
+- **THEN** main button displays focus outline
+
+#### Scenario: Focus outline on dropdown button
+
+- **WHEN** dropdown button has focus
+- **THEN** dropdown button displays focus outline
+
+### Requirement: Component updates on editor events
+
+The component SHALL re-render when editor state changes to keep icon and active state synchronized.
+
+#### Scenario: Update on selection change
+
+- **WHEN** editor `selectionUpdate` event fires
+- **THEN** component re-renders with current state
+
+#### Scenario: Update on content change
+
+- **WHEN** editor `transaction` event fires
+- **THEN** component re-renders with current state
diff --git a/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/toolbar-configuration/spec.md b/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/toolbar-configuration/spec.md
new file mode 100644
index 0000000000..a256e2b4bc
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/toolbar-configuration/spec.md
@@ -0,0 +1,91 @@
+# Toolbar Configuration Specification
+
+## ADDED Requirements
+
+### Requirement: Toolbar config supports splitButton action type
+
+The toolbar configuration SHALL support `"splitButton"` as a valid action type for button definitions.
+
+#### Scenario: Split button action type is recognized
+
+- **WHEN** button config has `action: "splitButton"`
+- **THEN** toolbar factory renders ToolbarSplitButton component
+
+#### Scenario: Split button config includes dropdown options
+
+- **WHEN** button config has `action: "splitButton"`
+- **THEN** button config includes `dropdownOptions` array
+- **THEN** each option includes `label`, `value`, `command`, `attrs`, and `icon` fields
+
+### Requirement: Dropdown options support icon field
+
+The ToolbarDropdownOption interface SHALL include an optional `icon` field for visual representation.
+
+#### Scenario: Icon field is optional
+
+- **WHEN** dropdown option is defined
+- **THEN** `icon` field may be omitted without error
+
+#### Scenario: Icon field is used when present
+
+- **WHEN** dropdown option includes `icon` field
+- **THEN** rendered option displays icon before label
+
+### Requirement: Ordered list button uses split button configuration
+
+The ordered list toolbar button SHALL be configured as a split button with style options.
+
+#### Scenario: Ordered list button has split button action
+
+- **WHEN** toolbar groups are loaded
+- **THEN** orderedList button has `action: "splitButton"`
+
+#### Scenario: Ordered list dropdown includes style options
+
+- **WHEN** orderedList button config is accessed
+- **THEN** dropdownOptions includes decimal, lower-alpha, and lower-roman
+- **THEN** each option has corresponding icon (List-numbers, List-lower-alpha, List-roman)
+
+#### Scenario: Decimal option uses null styleType
+
+- **WHEN** decimal option is selected
+- **THEN** `attrs.styleType` is `null` (browser default)
+
+#### Scenario: Lower-alpha option uses styleType attribute
+
+- **WHEN** lower-alpha option is selected
+- **THEN** `attrs.styleType` is `"lower-alpha"`
+
+#### Scenario: Lower-roman option uses styleType attribute
+
+- **WHEN** lower-roman option is selected
+- **THEN** `attrs.styleType` is `"lower-roman"`
+
+### Requirement: Standalone orderedListStyle button is removed
+
+The toolbar configuration SHALL NOT include standalone `orderedListStyle` dropdown button.
+
+#### Scenario: List group has three buttons
+
+- **WHEN** list toolbar group is rendered
+- **THEN** group includes bulletList, orderedList (split), and taskList
+- **THEN** group does NOT include standalone orderedListStyle button
+
+### Requirement: Icon mapping helper is available
+
+The toolbar configuration SHALL provide helper function to map list style values to icon names.
+
+#### Scenario: Decimal maps to List-numbers
+
+- **WHEN** helper function receives `"decimal"` or `null`
+- **THEN** function returns `"List-numbers"`
+
+#### Scenario: Lower-alpha maps to List-lower-alpha
+
+- **WHEN** helper function receives `"lower-alpha"`
+- **THEN** function returns `"List-lower-alpha"`
+
+#### Scenario: Lower-roman maps to List-roman
+
+- **WHEN** helper function receives `"lower-roman"`
+- **THEN** function returns `"List-roman"`
diff --git a/openspec/changes/archive/2026-07-12-ordered-list-split-button/tasks.md b/openspec/changes/archive/2026-07-12-ordered-list-split-button/tasks.md
new file mode 100644
index 0000000000..8c380f7c91
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-ordered-list-split-button/tasks.md
@@ -0,0 +1,97 @@
+# Implementation Tasks
+
+## 1. Type and Config Updates
+
+- [x] 1.1 Add `icon?: string` field to `ToolbarDropdownOption` interface in `ToolbarConfig.ts`
+- [x] 1.2 Add `"splitButton"` to `ToolbarActionType` union type in `ToolbarConfig.ts`
+- [x] 1.3 Create `listHelpers.ts` with `getIconForOrderedListStyle()` function and `STYLE_ICON_MAP` constant
+
+## 2. Toolbar Configuration Changes
+
+- [x] 2.1 Update `orderedList` button config in `TOOLBAR_GROUPS` to use `action: "splitButton"`
+- [x] 2.2 Add `dropdownOptions` array to `orderedList` config with decimal, lower-alpha, lower-roman options
+- [x] 2.3 Add `icon` field to each dropdown option (List-numbers, List-lower-alpha, List-roman)
+- [x] 2.4 Add `getCurrentValue` function to `orderedList` config to return active style
+- [x] 2.5 Remove standalone `orderedListStyle` button from list group buttons array
+
+## 3. ToolbarSplitButton Component
+
+- [x] 3.1 Create `ToolbarSplitButton.tsx` component file with props interface
+- [x] 3.2 Add module-level `lastOrderedListStyle` sticky state variable
+- [x] 3.3 Implement `getCurrentIcon()` helper function using active/sticky state
+- [x] 3.4 Add component state: `isDropdownOpen`, `focusedPart` with refs for both buttons
+- [x] 3.5 Implement `useDropdown` hook integration for floating menu positioning
+- [x] 3.6 Add `useEffect` to re-render on editor `selectionUpdate` and `transaction` events
+
+## 4. Split Button Event Handlers
+
+- [x] 4.1 Implement `handleMainClick()` to toggle OL with sticky style when inactive, disable when active
+- [x] 4.2 Implement `handleDropdownClick()` to toggle dropdown open/closed state
+- [x] 4.3 Implement `handleOptionSelect()` to update sticky state, apply style, enable OL if needed, close dropdown
+- [x] 4.4 Implement `handleKeyDown()` with ArrowRight/Left focus movement between buttons
+- [x] 4.5 Add ArrowDown handling to open dropdown from either button
+- [x] 4.6 Add Enter/Space handling to execute focused part's action
+
+## 5. Split Button Render
+
+- [x] 5.1 Render container div with `role="group"` and `aria-label`
+- [x] 5.2 Render main button with dynamic icon, `aria-pressed`, and click/keydown handlers
+- [x] 5.3 Render dropdown button with chevron icon, `aria-expanded`, `aria-haspopup="menu"`
+- [x] 5.4 Conditionally render dropdown menu with Floating UI positioning when open
+- [x] 5.5 Render dropdown options with icon + text, `role="menuitem"`, active class
+- [x] 5.6 Apply `is-active` class to split button container when OL active
+
+## 6. Toolbar Factory Integration
+
+- [x] 6.1 Import `ToolbarSplitButton` component in `Toolbar.tsx`
+- [x] 6.2 Add `case "splitButton"` to `ToolbarButtonFactory` switch statement
+- [x] 6.3 Return `` in split button case
+
+## 7. Dropdown Option Icon Support
+
+- [x] 7.1 Update `ToolbarDropdown.tsx` to conditionally render icon if `option.icon` present
+- [x] 7.2 Update dropdown item layout to support icon + text (flex with gap)
+
+## 8. SCSS Styling
+
+- [x] 8.1 Add `.split-button` base styles with inline-flex layout in `Toolbar.scss`
+- [x] 8.2 Add `.split-button-main` styles with border-right separator
+- [x] 8.3 Add `.split-button-dropdown` styles with chevron rotation on `aria-expanded="true"`
+- [x] 8.4 Add `.split-button.is-active` styles for active state highlighting
+- [x] 8.5 Add focus indicator styles for both button parts (`:focus-visible`)
+- [x] 8.6 Update `.toolbar-dropdown-item` to support icon + text layout (flex with gap)
+- [x] 8.7 Add hover states for main and dropdown buttons
+
+## 9. Unit Tests
+
+- [ ] 9.1 Create `orderedListSplitButton.spec.tsx` test file
+- [ ] 9.2 Test: Split button renders with both parts and correct icons
+- [ ] 9.3 Test: Main button click toggles OL with sticky style when inactive
+- [ ] 9.4 Test: Main button click disables OL when active
+- [ ] 9.5 Test: Dropdown button opens/closes menu
+- [ ] 9.6 Test: Dropdown option selection updates sticky state and applies style
+- [ ] 9.7 Test: Dropdown option enables OL + applies style when inactive
+- [ ] 9.8 Test: Dropdown option only changes style when active
+- [ ] 9.9 Test: Icon updates based on active style
+- [ ] 9.10 Test: Icon shows sticky state when inactive
+- [ ] 9.11 Test: Sticky state persists after toggle off/on
+- [ ] 9.12 Test: Keyboard ArrowRight/Left moves focus between buttons
+- [ ] 9.13 Test: Keyboard ArrowDown opens dropdown
+- [ ] 9.14 Test: ARIA attributes (role, aria-pressed, aria-expanded, aria-haspopup)
+
+## 10. E2E Test
+
+- [ ] 10.1 Create `orderedListSplitButton.spec.js` E2E test file
+- [ ] 10.2 Test: Click dropdown, select lower-alpha, verify OL enabled with a,b,c style
+- [ ] 10.3 Test: Toggle off via main button, verify OL disabled
+- [ ] 10.4 Test: Click main button, verify OL re-enabled with lower-alpha (sticky)
+- [ ] 10.5 Test: Open dropdown, select lower-roman, verify icon updates
+- [ ] 10.6 Test: Verify dropdown accessible when OL inactive
+
+## 11. Manual Verification
+
+- [ ] 11.1 Test on desktop: Click workflow, keyboard navigation, icon updates
+- [ ] 11.2 Test on mobile/tablet: Touch targets adequate (56px combined width)
+- [ ] 11.3 Test screen reader: ARIA announcements for button states
+- [ ] 11.4 Test with multiple editor instances (if available): Verify sticky state behavior
+- [ ] 11.5 Verify visual match to Word split button pattern
diff --git a/openspec/changes/archive/2026-07-13-add-link-bubble-menu/.openspec.yaml b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/.openspec.yaml
new file mode 100644
index 0000000000..b119b63505
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-13
diff --git a/openspec/changes/archive/2026-07-13-add-link-bubble-menu/design.md b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/design.md
new file mode 100644
index 0000000000..b55298d9aa
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/design.md
@@ -0,0 +1,89 @@
+## Context
+
+The rich text widget (`@mendix/rich-text-web`) runs tiptap v3. Links come from StarterKit's link mark, configured with `openOnClick: false` so clicking a link places the caret instead of navigating. Link insertion today goes through the toolbar's `DialogToolbarButton` → `LinkDialog`, which reads `getAttributes("link")` and already renders an "Edit Link" variant. There is no affordance to edit or remove an _existing_ link without reselecting it from the toolbar.
+
+`@tiptap/react/menus` exposes `BubbleMenu` (backed by `@tiptap/extension-bubble-menu@3.27.1`, already resolved transitively). `BubbleMenu` manages its own floating position via a ProseMirror plugin and accepts `editor`, `shouldShow`, `pluginKey`, `updateDelay`, plus standard div attributes. `shouldShow` receives `{ editor, element, view, state, from, to }`.
+
+Reference: tiptap menus docs — https://tiptap.dev/docs/examples/advanced/menus
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Contextual Edit/Remove menu that appears on any link in an editable editor.
+- Reuse the existing `LinkDialog` for editing (prefilled, in-place update).
+- Correct behavior from a bare caret inside a link (no selection required).
+
+**Non-Goals:**
+
+- No change to toolbar link insertion.
+- No new bubble menus for other marks (bold, image, etc.) — link only.
+- No new npm dependency.
+
+## Decisions
+
+### Render `LinkBubbleMenu` inside `EditorInner`
+
+The bubble menu lives as a sibling to `EditorContent`, within the existing `EditorContextProvider`, so it can read `editor` via `useCurrentEditor()`. Buttons reuse `ToolbarDefaultButton`.
+
+```tsx
+
+
+
+;
+{
+ isEditing && ;
+}
+```
+
+### `shouldShow` = active link, editable, not editing
+
+```ts
+shouldShow = ({ editor }) => editor.isEditable && editor.isActive("link") && !isEditing;
+```
+
+Covers three spec requirements at once: appears on link, hidden off-link, hidden when read-only, and suppressed while the dialog is open (single floating layer).
+
+_Alternative considered:_ require a non-empty selection over the link. Rejected — less discoverable; the caret-inside case is the common one.
+
+### Full-range selection before Edit and Remove
+
+Both actions first run `editor.chain().focus().extendMarkRange("link")`. This is the crux fix:
+
+- **Edit:** `LinkDialog`'s submit branches on `selectedText`. With a bare caret, `selectedText === ""` → it takes the "insert new text" branch and **duplicates** the link. Selecting the whole link range first populates `selectedText` → correct "apply to existing selection" branch.
+- **Remove:** without `extendMarkRange`, `unsetLink()` only clears from the caret; the rest of the link survives. Extending first strips the entire link.
+
+```ts
+removeLink = () => editor.chain().focus().extendMarkRange("link").unsetLink().run();
+openEdit = () => {
+ editor.chain().focus().extendMarkRange("link").run();
+ setLinkEl(resolveLinkEl());
+ setIsEditing(true);
+};
+```
+
+### Anchor the dialog to the link DOM element
+
+`LinkDialog` takes a `referenceElement`. Anchoring to the actual ``/`.tiptap-link` element (resolved from `editor.view.domAtPos(selection.from)`, walking up to the nearest anchor) keeps the dialog positioned even after the bubble menu hides on focus loss, and reads as "editing _this_ link."
+
+_Alternative considered:_ anchor to the bubble menu container ref. Rejected — the bubble hides when the editor blurs (dialog input steals focus), orphaning the dialog.
+
+## Risks / Trade-offs
+
+- **Icon names unknown** → the icon font names for edit/trash must be confirmed against the existing icon set (config uses names like `Text-bold`). Resolve during implementation; pick the closest existing glyph.
+- **Focus loss hides the bubble mid-interaction** → mitigated by anchoring the dialog to the link element (not the bubble) and by `!isEditing` in `shouldShow`, so the dialog owns the interaction once open.
+- **KeyboardNavigation extension** intercepts wrapper/toolbar keys → verify Tab/Escape inside the bubble buttons and dialog don't conflict; the bubble buttons are plain `ToolbarDefaultButton`s so Escape/click-outside close is handled by `LinkDialog`'s `useDropdown`.
+- **`extendMarkRange` changes the user's selection** → acceptable and expected; the caret ends on the whole link, which is the intent for both edit and remove.
+
+## Migration Plan
+
+1. Add `LinkBubbleMenu.tsx` (menu + edit/remove + link-element resolution + dialog).
+2. Render it in `EditorInner` inside `tiptap-wrapper`.
+3. Add `.link-bubble-menu` container styling; reuse toolbar button styles.
+4. Unit-test the edit/remove commands and `shouldShow` gating; manual check in Studio Pro.
+
+Rollback: remove the render in `EditorInner`; the new file becomes dead and can be deleted.
+
+## Open Questions
+
+- Exact icon glyph names for Edit and Remove — resolve against the shipped icon font during implementation.
diff --git a/openspec/changes/archive/2026-07-13-add-link-bubble-menu/proposal.md b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/proposal.md
new file mode 100644
index 0000000000..6bd926ad0d
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/proposal.md
@@ -0,0 +1,32 @@
+## Why
+
+Today a user can insert a link via the toolbar, but there is no quick way to edit or remove an _existing_ link — they must reselect the text and reopen the toolbar dialog, and the toolbar's link dialog even duplicates the link when invoked on a bare cursor inside a link. A contextual bubble menu that appears on any link gives an in-place Edit/Remove affordance, matching the standard rich text editor UX.
+
+## What Changes
+
+- Add a `LinkBubbleMenu` component using tiptap's `BubbleMenu` from `@tiptap/react/menus` (already resolved transitively — no new dependency).
+- The menu appears whenever the caret is inside or a selection covers a link (`editor.isActive("link")`) and the editor is editable.
+- Menu contains two `ToolbarDefaultButton` controls:
+ - **Edit** — selects the whole link (`extendMarkRange("link")`) then opens the existing `LinkDialog`, prefilled, anchored to the link's DOM element.
+ - **Remove** — `extendMarkRange("link").unsetLink()` to strip the link across its full range, turning it back into normal text.
+- While the dialog is open, the bubble menu is suppressed (`shouldShow` returns false during editing) so only one floating layer shows at a time.
+- Render `LinkBubbleMenu` inside `EditorInner`, as a sibling to `EditorContent`, within the existing `EditorContextProvider`.
+
+## Capabilities
+
+### New Capabilities
+
+- `rich-text-link-bubble-menu`: Contextual bubble menu shown on links, offering in-place Edit and Remove actions, including the full-link selection behavior that keeps editing and removal correct regardless of caret position.
+
+### Modified Capabilities
+
+
+
+## Impact
+
+- Package: `@mendix/rich-text-web`
+- New file: `src/components/LinkBubbleMenu.tsx`
+- Modified: `src/components/Editor.tsx` (`EditorInner` renders the bubble menu)
+- Reuses: `LinkDialog` (existing edit path), `ToolbarDefaultButton`, `useCurrentEditor`.
+- Styling: new `.link-bubble-menu` container class; reuses existing toolbar button styles.
+- No XML, no runtime API change, no new dependency.
diff --git a/openspec/changes/archive/2026-07-13-add-link-bubble-menu/specs/rich-text-link-bubble-menu/spec.md b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/specs/rich-text-link-bubble-menu/spec.md
new file mode 100644
index 0000000000..598d67d1a8
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/specs/rich-text-link-bubble-menu/spec.md
@@ -0,0 +1,58 @@
+## ADDED Requirements
+
+### Requirement: Contextual link bubble menu
+
+The rich text editor SHALL display a floating bubble menu whenever the caret is inside a link or a selection covers a link, and the editor is editable. The menu SHALL contain an Edit action and a Remove action.
+
+#### Scenario: Menu appears on a link
+
+- **WHEN** the caret is placed inside link text in an editable editor
+- **THEN** a bubble menu with Edit and Remove buttons appears anchored to the link
+
+#### Scenario: Menu hidden away from links
+
+- **WHEN** the caret is in text that is not part of a link
+- **THEN** the bubble menu is not shown
+
+#### Scenario: Menu suppressed when not editable
+
+- **WHEN** the editor is read-only
+- **THEN** the bubble menu is not shown even if the caret is inside a link
+
+### Requirement: Edit an existing link
+
+The Edit action SHALL select the entire link range before opening the link dialog, so the dialog is prefilled with the link's current attributes and updates the existing link in place rather than inserting a duplicate.
+
+#### Scenario: Edit from a bare caret inside a link
+
+- **WHEN** the caret is inside a link with no text selected and the user activates Edit
+- **THEN** the full link range is selected and the link dialog opens prefilled with the current URL, text, title, and target
+- **AND** submitting the dialog updates the existing link without duplicating its text
+
+#### Scenario: Dialog anchored to the link
+
+- **WHEN** the Edit action opens the link dialog
+- **THEN** the dialog is positioned relative to the link's DOM element and remains positioned there while editing
+
+### Requirement: Remove a link
+
+The Remove action SHALL strip the link mark across its entire range, converting the linked text back into normal text.
+
+#### Scenario: Remove from a bare caret inside a link
+
+- **WHEN** the caret is inside a link with no text selected and the user activates Remove
+- **THEN** the link mark is removed from the whole link range and the text remains as plain text
+
+### Requirement: Single floating layer while editing
+
+While the link dialog is open, the bubble menu SHALL be suppressed so that only one floating layer is visible at a time.
+
+#### Scenario: Bubble hides during edit
+
+- **WHEN** the link dialog is open via the Edit action
+- **THEN** the bubble menu is not shown
+
+#### Scenario: Bubble returns after editing
+
+- **WHEN** the link dialog is closed and the caret is still inside a link
+- **THEN** the bubble menu reappears
diff --git a/openspec/changes/archive/2026-07-13-add-link-bubble-menu/tasks.md b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/tasks.md
new file mode 100644
index 0000000000..d19c8cc7b8
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-add-link-bubble-menu/tasks.md
@@ -0,0 +1,30 @@
+## 1. Create the bubble menu component
+
+- [x] 1.1 Add `src/components/LinkBubbleMenu.tsx` importing `BubbleMenu` from `@tiptap/react/menus`, reading `editor` via `useCurrentEditor()`
+- [x] 1.2 Add local `isEditing` state and a `linkEl` state for the resolved link DOM element
+- [x] 1.3 Implement `shouldShow = ({ editor }) => editor.isEditable && editor.isActive("link") && !isEditing`
+- [x] 1.4 Render `` wrapping two `ToolbarDefaultButton`s (Edit, Remove) in a `.link-bubble-menu` container
+
+## 2. Wire the actions
+
+- [x] 2.1 `removeLink`: `editor.chain().focus().extendMarkRange("link").unsetLink().run()`
+- [x] 2.2 `resolveLinkEl`: from `editor.view.domAtPos(editor.state.selection.from)`, walk up to nearest `` / `.tiptap-link` element
+- [x] 2.3 `openEdit`: run `extendMarkRange("link")`, set `linkEl` from `resolveLinkEl`, set `isEditing = true`
+- [x] 2.4 Render `{isEditing && setIsEditing(false)} />}`
+- [x] 2.5 Choose icons from the shipped font (`src/ui/RichTextIcons.scss`) — no pencil glyph exists; use `Hyperlink` for Edit and `Erase` (or `Delete`) for Remove, or add a new glyph if a dedicated edit icon is wanted
+
+## 3. Integrate into the editor
+
+- [x] 3.1 Render `` inside `EditorInner` in `Editor.tsx`, as a sibling to `EditorContent` within `tiptap-wrapper`
+- [x] 3.2 Confirm it only mounts when not in code view (bubble is irrelevant in the HTML code editor)
+
+## 4. Styling
+
+- [x] 4.1 Add `.link-bubble-menu` container style (inline-flex, padding, background, shadow); reuse existing toolbar `.icon-button` styles for the buttons
+
+## 5. Verify
+
+- [x] 5.1 Unit test: `shouldShow` returns true on active link + editable, false off-link, false when read-only, false while editing
+- [x] 5.2 Unit test: Remove strips the whole link from a bare caret; Edit selects the full range so `LinkDialog` prefills and updates in place (no duplicate)
+- [x] 5.3 Run package unit tests (Jest + RTL) — all pass
+- [ ] 5.4 Manual check in Studio Pro (`pnpm start` with `MX_PROJECT_PATH`): click a link → bubble shows → Edit prefills + updates, Remove unlinks; bubble hidden while dialog open and in read-only mode
diff --git a/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/.openspec.yaml b/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/.openspec.yaml
new file mode 100644
index 0000000000..8803b473ec
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-12
diff --git a/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/design.md b/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/design.md
new file mode 100644
index 0000000000..6c7971da89
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/design.md
@@ -0,0 +1,93 @@
+## Context
+
+The rich text toolbar (`@mendix/rich-text-web`) renders every control through `ToolbarButtonFactory` in `Toolbar.tsx`, which dispatches to 7 components. Eight `