* add pb models * api rules has updated to view public * pb setup is completed * add migrate script * add pb migrations * ignore storage * add pocketbase SDK dependency * change Tag.id and PageCardDto.id to string for PocketBase * add DocV1 content block types * add PB client singleton and PB record types * rewrite factory for PocketBase record types * rewrite api.ts for PocketBase SDK * remove unused Strapi files, add PB to config * mark Task 7 complete in plan * feat: verify build, tests, and lint pass with PocketBase API layer * move completed plan: 2026-04-06-pocketbase-api-layer.md * scaffold hudozka-writer with Vite 8, React 19, TS 5.9, Tiptap * add PocketBase client and DocV1 types to writer * add Login component for PocketBase superuser auth * add App shell with auth gate and page loader * add DocV1 <-> Tiptap serialization layer * mark Task 5 complete in writer redesign plan * add custom Tiptap nodes for DocV1 block types * add Tiptap editor with toolbar and save to PocketBase * add image picker with browse and upload * feat: verify writer build and complete integration task * move completed plan: 2026-04-06-hudozka-writer-redesign.md * add local env * rm textblock from writer * add writer editor improvements design spec * add writer editor improvements implementation plan * feat: use @tiptap/markdown for text block serialization * feat: add global markdown/rendered toggle * feat: add BlockMenu component for block type selection * feat: add hover + button for block insertion * feat: add slash commands for block insertion * feat: add interactive file block editor with FilePicker * feat: show resolved page titles in card grid block * move completed plan: 2026-04-07-writer-editor-improvements.md * writer ui improved * pages migration * spec: draft/publish workflow for hudozka-writer * spec: rename draft_doc to draft in spec * plan: writer draft/publish implementation * plan has moved * gitignores * plans upd * feat(writer): update PbPage type for draft/publish workflow * feat(writer): add save/publish/discard handlers with dirty tracking * feat(writer): add DraftBanner component * feat(writer): add Publish button and update toolbar layout * feat(writer): add published toggle and update duplicate for new schema * docs: mark manual verification steps as skipped in writer draft/publish plan * move completed plan: 20260407-writer-draft-publish.md * feat(writer): add BlockActions component for inline block menus * feat(writer): add draggable NodeViews for text blocks * feat(writer): use BlockActions in custom block NodeViews * feat(writer): replace floating BlockInsert with inline BlockActions * fix(writer): ensure all block wrappers have position: relative for BlockActions * chore: mark manual verification tasks as skipped in writer block actions plan * move completed plan: 20260408-writer-block-actions.md * writer update * refactor(writer): simplify BlockActions to always-visible delete + drag handle * feat(writer): add BlockInsert component for inserting blocks between nodes * feat(writer): redesign block sidebar with always-visible actions and insert button * move completed plan: 20260408-writer-block-actions-redesign.md * add spec: textblock container redesign for writer editor * feat(writer): add TextBlock container node definition * feat(writer): replace DraggableXxx wrappers with TextBlock container * feat(writer): update Editor extensions for TextBlock container * feat(writer): add split button to BlockActions for text blocks * feat(writer): update serialization for TextBlock container * feat(writer): update BlockMenu and SlashCommands for TextBlock * fix(writer): use Fragment.from() for replaceWith calls in BlockActions and SlashCommands * move completed plan: 20260408-textblock-container.md * add air
27 KiB
Writer Block Actions Redesign
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Redesign the block sidebar to show a persistent left-side menu with delete + drag handle on every block, a persistent [+] insert button between blocks, and an outline on the active block.
Architecture: Replace the current hover-triggered BlockActions (3 buttons: drag, add, delete) with two separate components: BlockActions (always-visible vertical sidebar with delete + drag handle) and BlockInsert (always-visible [+] button between blocks). Each NodeViewWrapper uses a flex row layout: sidebar on the left, block content on the right. DnD uses only the drag handle element, not the full block wrapper. Active block gets a CSS outline.
Tech Stack: React, Tiptap, CSS
File Map
| Action | File | Responsibility |
|---|---|---|
| Modify | modules/hudozka-writer/src/components/BlockActions.tsx |
Simplify to delete + drag handle, always visible |
| Rewrite | modules/hudozka-writer/src/components/BlockActions.css |
Vertical sidebar layout, always visible |
| Create | modules/hudozka-writer/src/components/BlockInsert.tsx |
[+] button that opens BlockMenu |
| Create | modules/hudozka-writer/src/components/BlockInsert.css |
Insert button styles |
| Modify | modules/hudozka-writer/src/nodes/textBlocks.tsx |
Add BlockInsert, use flex wrapper layout |
| Modify | modules/hudozka-writer/src/nodes/ImageBlock.tsx |
Remove data-drag-handle, add BlockInsert, flex layout |
| Modify | modules/hudozka-writer/src/nodes/DocumentBlock.tsx |
Remove data-drag-handle, add BlockInsert, flex layout |
| Modify | modules/hudozka-writer/src/nodes/EmbedBlock.tsx |
Add BlockInsert, flex layout |
| Modify | modules/hudozka-writer/src/nodes/CardGridBlock.tsx |
Add BlockInsert, flex layout |
| Modify | modules/hudozka-writer/src/components/Editor.css |
Active block outline styles |
Task 1: Simplify BlockActions to delete + drag handle
Files:
-
Modify:
modules/hudozka-writer/src/components/BlockActions.tsx -
Rewrite:
modules/hudozka-writer/src/components/BlockActions.css -
Step 1: Rewrite BlockActions.tsx
Replace the entire file with a simplified component — no active state tracking, no menu, always visible, just delete and drag handle:
import type { Editor } from "@tiptap/react"
import "./BlockActions.css"
export type BlockActionsProps = {
editor: Editor
getPos: () => number | undefined
}
export function BlockActions({ editor, getPos }: BlockActionsProps) {
const handleDelete = (e: React.MouseEvent) => {
e.preventDefault()
const pos = getPos()
if (typeof pos !== "number") return
const node = editor.state.doc.nodeAt(pos)
if (!node) return
editor.chain().focus().deleteRange({
from: pos,
to: pos + node.nodeSize,
}).run()
}
return (
<div className="block-actions" contentEditable={false}>
<button
className="block-action-btn block-action-delete"
title="Delete block"
onClick={handleDelete}
>
×
</button>
<div
className="block-action-btn block-drag-handle"
data-drag-handle
draggable
title="Drag to reorder"
>
⠇
</div>
</div>
)
}
- Step 2: Rewrite BlockActions.css
Replace the entire file. The sidebar is a vertical flex column, always visible, positioned left of the block content via the parent flex row:
.block-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
flex-shrink: 0;
width: 28px;
padding-top: 2px;
user-select: none;
}
.block-action-btn {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid transparent;
background: none;
cursor: pointer;
font-size: 16px;
line-height: 1;
color: #ccc;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
padding: 0;
}
.block-action-btn:hover {
border-color: #ddd;
color: #666;
background: #f5f5f5;
}
.block-action-delete:hover {
border-color: #c00;
color: #c00;
background: #fff0f0;
}
.block-drag-handle {
cursor: grab;
}
.block-drag-handle:active {
cursor: grabbing;
}
- Step 3: Verify the editor loads without errors (skipped - manual browser verification)
Run: open the writer in browser, check there are no console errors. Blocks should show delete and drag buttons to the left (layout will be finalized in Task 3).
- Step 4: Commit
git add modules/hudozka-writer/src/components/BlockActions.tsx modules/hudozka-writer/src/components/BlockActions.css
git commit -m "refactor(writer): simplify BlockActions to always-visible delete + drag handle"
Task 2: Create BlockInsert component
Files:
-
Create:
modules/hudozka-writer/src/components/BlockInsert.tsx -
Create:
modules/hudozka-writer/src/components/BlockInsert.css -
Step 1: Create BlockInsert.tsx
A [+] button that sits between blocks and opens a BlockMenu dropdown:
import { useState } from "react"
import type { Editor } from "@tiptap/react"
import { BlockMenu } from "./BlockMenu"
import "./BlockInsert.css"
export type BlockInsertProps = {
editor: Editor
getPos: () => number | undefined
}
export function BlockInsert({ editor, getPos }: BlockInsertProps) {
const [menuOpen, setMenuOpen] = useState(false)
const insertPos = (() => {
const pos = getPos()
if (typeof pos !== "number") return 0
const node = editor.state.doc.nodeAt(pos)
return node ? pos + node.nodeSize : 0
})()
const handleToggle = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setMenuOpen(!menuOpen)
}
const handleClose = () => {
setMenuOpen(false)
}
return (
<div className="block-insert" contentEditable={false}>
<button
className="block-insert-btn"
title="Add block"
onClick={handleToggle}
>
+
</button>
{menuOpen && (
<div className="block-insert-menu">
<BlockMenu
editor={editor}
position={insertPos}
onClose={handleClose}
/>
</div>
)}
</div>
)
}
- Step 2: Create BlockInsert.css
.block-insert {
position: relative;
height: 0;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 28px;
}
.block-insert-btn {
position: relative;
top: -1px;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid transparent;
background: none;
cursor: pointer;
font-size: 18px;
line-height: 1;
color: #ccc;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
padding: 0;
z-index: 10;
}
.block-insert-btn:hover {
border-color: #ddd;
color: #666;
background: #f5f5f5;
}
.block-insert-menu {
position: absolute;
top: 12px;
left: 56px;
z-index: 50;
}
- Step 3: Commit
git add modules/hudozka-writer/src/components/BlockInsert.tsx modules/hudozka-writer/src/components/BlockInsert.css
git commit -m "feat(writer): add BlockInsert component for inserting blocks between nodes"
Task 3: Update all node views with new layout
Files:
- Modify:
modules/hudozka-writer/src/nodes/textBlocks.tsx - Modify:
modules/hudozka-writer/src/nodes/ImageBlock.tsx - Modify:
modules/hudozka-writer/src/nodes/DocumentBlock.tsx - Modify:
modules/hudozka-writer/src/nodes/EmbedBlock.tsx - Modify:
modules/hudozka-writer/src/nodes/CardGridBlock.tsx - Modify:
modules/hudozka-writer/src/components/Editor.css - Modify:
modules/hudozka-writer/src/components/BlockActions.css
Every block view gets a consistent flex row layout:
<NodeViewWrapper className="node-view-block">
<BlockActions ... />
<div className="node-view-content">
...block body...
</div>
</NodeViewWrapper>
<BlockInsert ... />
- Step 1: Add shared layout styles to BlockActions.css
Append these rules to the end of BlockActions.css (they use the [data-node-view-wrapper] selector which is already in this file):
[data-node-view-wrapper] {
position: relative;
}
.node-view-block {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 4px;
}
.node-view-content {
flex: 1;
min-width: 0;
}
Note: remove the existing [data-node-view-wrapper] { position: relative; } rule at the bottom of the file — the new one replaces it.
- Step 2: Add active block outline to Editor.css
Append to Editor.css:
.editor-content .tiptap [data-node-view-wrapper].active-block > .node-view-content {
outline: 2px solid #d0d5dd;
outline-offset: 2px;
border-radius: 4px;
}
- Step 3: Add active block tracking to BlockActions
We need BlockActions to set a class on its parent NodeViewWrapper when the block is active. Update BlockActions.tsx to add/remove active-block class. Replace the file with:
import { useEffect, useCallback } from "react"
import type { Editor } from "@tiptap/react"
import "./BlockActions.css"
export type BlockActionsProps = {
editor: Editor
getPos: () => number | undefined
}
export function BlockActions({ editor, getPos }: BlockActionsProps) {
const getWrapper = useCallback(() => {
const pos = getPos()
if (typeof pos !== "number") return null
const dom = editor.view.nodeDOM(pos)
if (!dom || !(dom instanceof HTMLElement)) return null
return dom.closest("[data-node-view-wrapper]") as HTMLElement | null
}, [editor, getPos])
useEffect(() => {
const checkActive = () => {
const wrapper = getWrapper()
if (!wrapper) return
const pos = getPos()
if (typeof pos !== "number") {
wrapper.classList.remove("active-block")
return
}
const { from } = editor.state.selection
const resolved = editor.state.doc.resolve(from)
if (resolved.depth < 1) {
wrapper.classList.remove("active-block")
return
}
const blockPos = resolved.before(1)
if (blockPos === pos) {
wrapper.classList.add("active-block")
} else {
wrapper.classList.remove("active-block")
}
}
const handleBlur = () => {
const wrapper = getWrapper()
wrapper?.classList.remove("active-block")
}
checkActive()
editor.on("selectionUpdate", checkActive)
editor.on("focus", checkActive)
editor.on("blur", handleBlur)
return () => {
editor.off("selectionUpdate", checkActive)
editor.off("focus", checkActive)
editor.off("blur", handleBlur)
const wrapper = getWrapper()
wrapper?.classList.remove("active-block")
}
}, [editor, getPos, getWrapper])
const handleDelete = (e: React.MouseEvent) => {
e.preventDefault()
const pos = getPos()
if (typeof pos !== "number") return
const node = editor.state.doc.nodeAt(pos)
if (!node) return
editor.chain().focus().deleteRange({
from: pos,
to: pos + node.nodeSize,
}).run()
}
return (
<div className="block-actions" contentEditable={false}>
<button
className="block-action-btn block-action-delete"
title="Delete block"
onClick={handleDelete}
>
×
</button>
<div
className="block-action-btn block-drag-handle"
data-drag-handle
draggable
title="Drag to reorder"
>
⠇
</div>
</div>
)
}
- Step 4: Update textBlocks.tsx
Replace the entire file. All views get the flex layout with BlockInsert after:
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from "@tiptap/react"
import type { NodeViewProps } from "@tiptap/react"
import Paragraph from "@tiptap/extension-paragraph"
import Heading from "@tiptap/extension-heading"
import BulletList from "@tiptap/extension-bullet-list"
import OrderedList from "@tiptap/extension-ordered-list"
import Blockquote from "@tiptap/extension-blockquote"
import CodeBlock from "@tiptap/extension-code-block"
import HorizontalRule from "@tiptap/extension-horizontal-rule"
import { BlockActions } from "../components/BlockActions"
import { BlockInsert } from "../components/BlockInsert"
function ParagraphView({ editor, getPos }: NodeViewProps) {
return (
<>
<NodeViewWrapper className="node-view-block">
<BlockActions editor={editor} getPos={getPos} />
<NodeViewContent<"p"> className="node-view-content" as="p" />
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
function HeadingView({ editor, getPos, node }: NodeViewProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tag = `h${node.attrs.level}` as any
return (
<>
<NodeViewWrapper className="node-view-block">
<BlockActions editor={editor} getPos={getPos} />
<NodeViewContent className="node-view-content" as={tag} />
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
function BulletListView({ editor, getPos }: NodeViewProps) {
return (
<>
<NodeViewWrapper className="node-view-block">
<BlockActions editor={editor} getPos={getPos} />
<NodeViewContent<"ul"> className="node-view-content" as="ul" />
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
function OrderedListView({ editor, getPos }: NodeViewProps) {
return (
<>
<NodeViewWrapper className="node-view-block">
<BlockActions editor={editor} getPos={getPos} />
<NodeViewContent<"ol"> className="node-view-content" as="ol" />
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
function BlockquoteView({ editor, getPos }: NodeViewProps) {
return (
<>
<NodeViewWrapper className="node-view-block">
<BlockActions editor={editor} getPos={getPos} />
<NodeViewContent<"blockquote"> className="node-view-content" as="blockquote" />
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
function CodeBlockView({ editor, getPos }: NodeViewProps) {
return (
<>
<NodeViewWrapper className="node-view-block" as="pre">
<BlockActions editor={editor} getPos={getPos} />
<NodeViewContent<"code"> className="node-view-content" as="code" />
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
function HorizontalRuleView({ editor, getPos }: NodeViewProps) {
return (
<>
<NodeViewWrapper className="node-view-block">
<BlockActions editor={editor} getPos={getPos} />
<div className="node-view-content">
<hr />
</div>
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
export const DraggableParagraph = Paragraph.extend({
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(ParagraphView)
},
})
export const DraggableHeading = Heading.extend({
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(HeadingView)
},
})
export const DraggableBulletList = BulletList.extend({
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(BulletListView)
},
})
export const DraggableOrderedList = OrderedList.extend({
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(OrderedListView)
},
})
export const DraggableBlockquote = Blockquote.extend({
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(BlockquoteView)
},
})
export const DraggableCodeBlock = CodeBlock.extend({
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(CodeBlockView)
},
})
export const DraggableHorizontalRule = HorizontalRule.extend({
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(HorizontalRuleView)
},
})
- Step 5: Update ImageBlock.tsx
In ImageBlockView, wrap in flex layout, remove data-drag-handle, add BlockInsert:
// Change the return in ImageBlockView to:
return (
<>
<NodeViewWrapper className="node-view-block node-image-block">
<BlockActions editor={editor} getPos={getPos} />
<div className="node-view-content">
<div className="node-block-label">Image</div>
{thumbnail ? (
<img
src={thumbnail}
alt={caption || "image"}
className="node-image-thumb"
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
/>
) : (
<div
className="node-image-placeholder"
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
{imageId ? "Loading..." : "Click to select image"}
</div>
)}
<div className="node-image-controls">
<label>
<input
type="checkbox"
checked={wide}
onChange={(e) => updateAttributes({ wide: e.target.checked })}
/>
Wide
</label>
<input
type="text"
placeholder="Caption"
value={caption || ""}
onChange={(e) => updateAttributes({ caption: e.target.value })}
className="node-image-caption"
/>
<button
className="node-image-pick-btn"
onClick={() => setShowPicker(true)}
>
Pick
</button>
</div>
{showPicker && (
<ImagePicker
onSelect={handleSelect}
onClose={() => setShowPicker(false)}
/>
)}
</div>
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
Add import at top: import { BlockInsert } from "../components/BlockInsert"
- Step 6: Update DocumentBlock.tsx
Same pattern. Change the return in DocumentBlockView:
return (
<>
<NodeViewWrapper className="node-view-block node-document-block">
<BlockActions editor={editor} getPos={getPos} />
<div className="node-view-content">
<div className="node-block-label">File</div>
{fileId ? (
<div
className="node-document-file"
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
📎 {filename ?? fileId}
</div>
) : (
<div
className="node-document-placeholder"
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
Click to select file
</div>
)}
<div className="node-document-controls">
<input
type="text"
placeholder="Title"
value={title || ""}
onChange={(e) => updateAttributes({ title: e.target.value })}
className="node-document-title"
/>
<button
className="node-image-pick-btn"
onClick={() => setShowPicker(true)}
>
Pick
</button>
</div>
{showPicker && (
<FilePicker
onSelect={handleSelect}
onClose={() => setShowPicker(false)}
/>
)}
</div>
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
Add import at top: import { BlockInsert } from "../components/BlockInsert"
- Step 7: Update EmbedBlock.tsx
function EmbedBlockView({ node, editor, getPos }: NodeViewProps) {
return (
<>
<NodeViewWrapper className="node-view-block node-readonly-block">
<BlockActions editor={editor} getPos={getPos} />
<div className="node-view-content">
<div className="node-block-label">Embed</div>
<div className="node-block-info">
<code>{node.attrs.src}</code>
</div>
</div>
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
Add import: import { BlockInsert } from "../components/BlockInsert"
- Step 8: Update CardGridBlock.tsx
return (
<>
<NodeViewWrapper className="node-view-block node-readonly-block">
<BlockActions editor={editor} getPos={getPos} />
<div className="node-view-content">
<div className="node-block-label">
Card Grid ({items.length})
</div>
{loading ? (
<div className="node-block-info">Loading...</div>
) : (
<ul className="node-cardgrid-list">
{resolved.map((item) => (
<li key={item.page} className="node-cardgrid-item">
{item.title ?? item.page}
</li>
))}
</ul>
)}
</div>
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
Add import: import { BlockInsert } from "../components/BlockInsert"
- Step 9: Update nodes.css — remove position: relative from block cards
The card blocks (.node-image-block, .node-readonly-block, .node-document-block) previously had position: relative for absolute-positioned BlockActions. That's no longer needed since BlockActions uses flex layout. Remove position: relative from these selectors in nodes.css. The margin: 8px 0 should also be removed since spacing is handled by the tiptap > * + * rule.
In nodes.css, change:
.node-image-block,
.node-readonly-block {
position: relative;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
margin: 8px 0;
background: #fafafa;
}
to:
.node-image-block,
.node-readonly-block {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
background: #fafafa;
}
And change:
.node-document-block {
position: relative;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
margin: 8px 0;
background: #fafafa;
}
to:
.node-document-block {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
background: #fafafa;
}
Note: these classes are now on NodeViewWrapper alongside node-view-block, so the border/padding/background applies to the entire flex row. Move these visual styles to .node-view-content inside those blocks instead. Actually — since these classes are on the NodeViewWrapper, the border will wrap the sidebar too. Better approach: move the visual card styles to a nested selector. Change the above to:
.node-image-block > .node-view-content,
.node-readonly-block > .node-view-content,
.node-document-block > .node-view-content {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
background: #fafafa;
}
Remove the old .node-image-block, .node-readonly-block and .node-document-block rules entirely.
- Step 10: Verify in browser (skipped - manual browser verification)
Open the writer, check:
- Every block has delete + drag handle on the left
- [+] button visible between all blocks
- Active block has outline
- Drag handle works for all block types
- Delete button removes the block
- [+] opens menu and inserts a new block
- Step 11: Commit
git add modules/hudozka-writer/src/nodes/textBlocks.tsx modules/hudozka-writer/src/nodes/ImageBlock.tsx modules/hudozka-writer/src/nodes/DocumentBlock.tsx modules/hudozka-writer/src/nodes/EmbedBlock.tsx modules/hudozka-writer/src/nodes/CardGridBlock.tsx modules/hudozka-writer/src/nodes/nodes.css modules/hudozka-writer/src/components/BlockActions.tsx modules/hudozka-writer/src/components/BlockActions.css modules/hudozka-writer/src/components/Editor.css
git commit -m "feat(writer): redesign block sidebar with always-visible actions and insert button"