@jackuait/blok 0.10.0-beta.8 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-CRvF-xVm.mjs → blok-BfcBwAfE.mjs} +1211 -1159
  3. package/dist/chunks/{constants-BOZ5plBi.mjs → constants-QNVyXALL.mjs} +49 -48
  4. package/dist/chunks/{tools-CnqCfv2L.mjs → tools-DHtzbrxy.mjs} +1411 -1220
  5. package/dist/full.mjs +3 -3
  6. package/dist/react.mjs +2 -2
  7. package/dist/tools.mjs +2 -2
  8. package/package.json +3 -5
  9. package/src/cli/commands/convert-gdocs/index.ts +26 -0
  10. package/src/cli/commands/convert-html/block-builder.ts +392 -0
  11. package/src/cli/commands/convert-html/id-generator.ts +11 -0
  12. package/src/cli/commands/convert-html/index.ts +23 -0
  13. package/src/cli/commands/convert-html/preprocessor.ts +422 -0
  14. package/src/cli/commands/convert-html/sanitizer.ts +93 -0
  15. package/src/cli/commands/convert-html/types.ts +15 -0
  16. package/src/cli/index.ts +56 -5
  17. package/src/components/block/index.ts +44 -10
  18. package/src/components/constants/data-attributes.ts +10 -0
  19. package/src/components/icons/index.ts +16 -0
  20. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
  21. package/src/components/modules/blockManager/hierarchy.ts +4 -1
  22. package/src/components/modules/readonly.ts +46 -0
  23. package/src/components/modules/rectangleSelection.ts +25 -5
  24. package/src/components/modules/toolbar/index.ts +96 -19
  25. package/src/components/modules/toolbar/styles.ts +0 -2
  26. package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
  27. package/src/components/tools/block.ts +10 -0
  28. package/src/components/utils/placeholder.ts +9 -2
  29. package/src/styles/main.css +16 -0
  30. package/src/tools/callout/constants.ts +2 -1
  31. package/src/tools/callout/dom-builder.ts +13 -1
  32. package/src/tools/callout/index.ts +21 -7
  33. package/src/tools/code/constants.ts +9 -1
  34. package/src/tools/code/dom-builder.ts +90 -54
  35. package/src/tools/code/index.ts +73 -31
  36. package/src/tools/divider/index.ts +5 -0
  37. package/src/tools/header/index.ts +47 -1
  38. package/src/tools/list/dom-builder.ts +3 -1
  39. package/src/tools/list/index.ts +55 -3
  40. package/src/tools/list/list-helpers.ts +2 -2
  41. package/src/tools/nested-blocks.ts +25 -0
  42. package/src/tools/paragraph/index.ts +47 -6
  43. package/src/tools/quote/index.ts +43 -8
  44. package/src/tools/stub/index.ts +10 -0
  45. package/src/tools/table/index.ts +238 -6
  46. package/src/tools/table/table-add-controls.ts +37 -5
  47. package/src/tools/table/table-cell-blocks.ts +87 -18
  48. package/src/tools/table/table-core.ts +2 -0
  49. package/src/tools/table/table-corner-drag.ts +247 -0
  50. package/src/tools/table/table-operations.ts +45 -9
  51. package/src/tools/toggle/dom-builder.ts +1 -0
  52. package/src/tools/toggle/index.ts +25 -0
  53. package/src/tools/toggle/toggle-lifecycle.ts +5 -4
  54. package/src/types-internal/jsdom.d.ts +9 -0
  55. package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
  56. package/types/tools/block-tool.d.ts +10 -0
  57. package/bin/blok.mjs +0 -10
  58. package/dist/cli.mjs +0 -37
  59. package/src/tools/code/language-picker.ts +0 -241
@@ -16,6 +16,7 @@ import type { CalloutData, CalloutConfig } from './types';
16
16
  import { buildCalloutDOM, type CalloutDOMRefs } from './dom-builder';
17
17
  import { saveCallout } from './block-operations';
18
18
  import { handleCalloutFirstChildBackspace } from './callout-keyboard';
19
+ import { mountChildBlocks } from '../nested-blocks';
19
20
  import { createColorPicker, type ColorPickerHandle } from '../../components/shared/color-picker';
20
21
  import { colorVarName } from '../../components/shared/color-presets';
21
22
  import { mapToNearestPresetName } from '../../components/utils/color-mapping';
@@ -60,11 +61,12 @@ const VARIANT_TO_BG_PRESET: Record<string, string | null> = {
60
61
 
61
62
  export class CalloutTool implements BlockTool {
62
63
  private readonly api: API;
63
- private readonly readOnly: boolean;
64
+ private readOnly: boolean;
64
65
  private _data: CalloutData;
65
66
  private _dom: CalloutDOMRefs | null = null;
66
67
  private _emojiPicker: EmojiPicker | null = null;
67
68
  private _colorPicker: ColorPickerHandle | null = null;
69
+ private _dragZone: HTMLElement | null = null;
68
70
  private blockId?: string;
69
71
 
70
72
  constructor({ data, api, readOnly, block }: BlockToolConstructorOptions<CalloutData, CalloutConfig>) {
@@ -108,6 +110,10 @@ export class CalloutTool implements BlockTool {
108
110
  }
109
111
 
110
112
  public render(): HTMLElement {
113
+ if (this._dom) {
114
+ return this._dom.wrapper;
115
+ }
116
+
111
117
  const dom = buildCalloutDOM({
112
118
  emoji: this._data.emoji,
113
119
  readOnly: this.readOnly,
@@ -115,6 +121,7 @@ export class CalloutTool implements BlockTool {
115
121
  });
116
122
 
117
123
  this._dom = dom;
124
+ this._dragZone = dom.dragZone;
118
125
  this.applyColors();
119
126
 
120
127
  if (!this.readOnly) {
@@ -144,12 +151,7 @@ export class CalloutTool implements BlockTool {
144
151
 
145
152
  const children = this.api.blocks.getChildren(this.blockId);
146
153
 
147
- // Append existing children to the container
148
- for (const child of children) {
149
- if (child.holder.parentElement !== this._dom.childContainer) {
150
- this._dom.childContainer.appendChild(child.holder);
151
- }
152
- }
154
+ mountChildBlocks(this._dom.childContainer, children);
153
155
 
154
156
  // Auto-create initial paragraph child when callout has no children
155
157
  if (children.length === 0) {
@@ -252,6 +254,18 @@ export class CalloutTool implements BlockTool {
252
254
  // No-op — no subscriptions to clean up
253
255
  }
254
256
 
257
+ public setReadOnly(state: boolean): void {
258
+ this.readOnly = state;
259
+
260
+ if (this._dom) {
261
+ this._dom.emojiButton.disabled = state;
262
+ }
263
+ }
264
+
265
+ public get dragZone(): HTMLElement | null {
266
+ return this._dragZone;
267
+ }
268
+
255
269
  private syncPickerActiveColors(): void {
256
270
  if (this._colorPicker === null) {
257
271
  return;
@@ -53,7 +53,7 @@ export const LANGUAGES: LanguageEntry[] = [
53
53
  ];
54
54
 
55
55
  // CSS — Tailwind classes
56
- export const WRAPPER_STYLES = 'flex flex-col rounded-lg bg-bg-secondary overflow-hidden my-1';
56
+ export const WRAPPER_STYLES = 'flex flex-col rounded-lg border border-border-secondary bg-bg-secondary overflow-hidden my-1';
57
57
  export const HEADER_STYLES = 'flex items-center gap-1 px-3 py-1.5 border-b border-border-primary text-xs text-gray-text';
58
58
  export const LANGUAGE_BUTTON_STYLES = 'px-1.5 py-0.5 rounded cursor-pointer bg-transparent border-0 text-xs text-gray-text font-medium transition-colors can-hover:hover:bg-item-hover-bg select-none';
59
59
  export const HEADER_BUTTON_STYLES = 'p-1 rounded cursor-pointer bg-transparent border-0 text-gray-text transition-colors can-hover:hover:bg-item-hover-bg flex items-center justify-center';
@@ -73,6 +73,14 @@ export const TAB_ACTIVE_STYLES = 'bg-blue-500 text-white';
73
73
  export const TAB_INACTIVE_STYLES = 'bg-transparent text-gray-text can-hover:hover:bg-item-hover-bg';
74
74
  export const PREVIEW_AREA_STYLES = 'px-4 py-3 overflow-x-auto min-h-[1.5em] flex justify-center';
75
75
 
76
+ // i18n key — preview toggle
77
+ export const PREVIEW_TOGGLE_KEY = 'tools.code.previewToggle';
78
+
79
+ // CSS — more menu dropdown
80
+ export const MORE_MENU_STYLES = 'absolute right-0 top-full mt-1 min-w-[10rem] rounded-lg bg-bg-secondary border border-border-secondary shadow-lg p-1 z-10';
81
+ export const MORE_MENU_ITEM_STYLES = 'flex items-center gap-2 w-full px-2.5 py-1.5 rounded text-xs text-gray-text cursor-pointer bg-transparent border-0 transition-colors can-hover:hover:bg-item-hover-bg select-none';
82
+ export const MORE_MENU_ITEM_ACTIVE_STYLES = 'text-blue-500';
83
+
76
84
  // Shiki theme names for syntax highlighting
77
85
  export const SHIKI_LIGHT_THEME = 'one-light';
78
86
  export const SHIKI_DARK_THEME = 'vitesse-dark';
@@ -4,15 +4,14 @@ import {
4
4
  LANGUAGE_BUTTON_STYLES,
5
5
  HEADER_BUTTON_STYLES,
6
6
  CODE_AREA_STYLES,
7
- TAB_STYLES,
8
- TAB_ACTIVE_STYLES,
9
- TAB_INACTIVE_STYLES,
10
7
  PREVIEW_AREA_STYLES,
11
8
  CODE_BODY_STYLES,
12
9
  GUTTER_STYLES,
13
10
  GUTTER_LINE_STYLES,
11
+ MORE_MENU_STYLES,
12
+ MORE_MENU_ITEM_STYLES,
14
13
  } from './constants';
15
- import { IconCopy, IconWrap, IconLineNumbers } from '../../components/icons';
14
+ import { IconCopy, IconCode, IconChevronDown, IconEllipsis, IconWrap, IconLineNumbers } from '../../components/icons';
16
15
 
17
16
  export interface CodeDOMRefs {
18
17
  wrapper: HTMLElement;
@@ -23,9 +22,10 @@ export interface CodeDOMRefs {
23
22
  preElement: HTMLPreElement;
24
23
  codeElement: HTMLElement;
25
24
  gutterElement: HTMLElement;
26
- codeTab: HTMLButtonElement | null;
27
- previewTab: HTMLButtonElement | null;
25
+ previewToggleButton: HTMLButtonElement | null;
28
26
  previewElement: HTMLDivElement | null;
27
+ moreButton: HTMLButtonElement;
28
+ moreMenu: HTMLElement;
29
29
  }
30
30
 
31
31
  export interface BuildCodeDOMOptions {
@@ -36,38 +36,30 @@ export interface BuildCodeDOMOptions {
36
36
  wrapLabel: string;
37
37
  lineNumbersLabel?: string;
38
38
  previewable?: boolean;
39
- codeTabLabel?: string;
40
- previewTabLabel?: string;
39
+ previewToggleLabel?: string;
41
40
  }
42
41
 
43
42
  function buildPreviewElements(
44
- codeTabLabel?: string,
45
- previewTabLabel?: string,
46
- ): { codeTab: HTMLButtonElement; previewTab: HTMLButtonElement; previewElement: HTMLDivElement } {
47
- const codeTab = document.createElement('button');
43
+ previewToggleLabel?: string,
44
+ ): { previewToggleButton: HTMLButtonElement; previewElement: HTMLDivElement } {
45
+ const previewToggleButton = document.createElement('button');
48
46
 
49
- codeTab.type = 'button';
50
- codeTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
51
- codeTab.textContent = codeTabLabel ?? 'Code';
52
- codeTab.setAttribute('data-blok-testid', 'code-code-tab');
53
-
54
- const previewTab = document.createElement('button');
55
-
56
- previewTab.type = 'button';
57
- previewTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
58
- previewTab.textContent = previewTabLabel ?? 'Preview';
59
- previewTab.setAttribute('data-blok-testid', 'code-preview-tab');
47
+ previewToggleButton.type = 'button';
48
+ previewToggleButton.className = HEADER_BUTTON_STYLES;
49
+ previewToggleButton.innerHTML = IconCode;
50
+ previewToggleButton.setAttribute('aria-label', previewToggleLabel ?? 'Preview');
51
+ previewToggleButton.setAttribute('data-blok-testid', 'code-preview-toggle-btn');
60
52
 
61
53
  const previewElement = document.createElement('div');
62
54
 
63
55
  previewElement.className = PREVIEW_AREA_STYLES;
64
56
  previewElement.setAttribute('data-blok-testid', 'code-preview');
65
57
 
66
- return { codeTab, previewTab, previewElement };
58
+ return { previewToggleButton, previewElement };
67
59
  }
68
60
 
69
61
  export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
70
- const { code, languageName, readOnly, copyLabel, wrapLabel, lineNumbersLabel, previewable, codeTabLabel, previewTabLabel } = options;
62
+ const { code, languageName, readOnly, copyLabel, wrapLabel, lineNumbersLabel, previewable, previewToggleLabel } = options;
71
63
 
72
64
  // Wrapper
73
65
  const wrapper = document.createElement('div');
@@ -77,38 +69,30 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
77
69
  const header = document.createElement('div');
78
70
  header.className = HEADER_STYLES;
79
71
 
80
- // Language button (opens language picker)
72
+ // Language button (opens language picker) — includes text + chevron icon
81
73
  const languageButton = document.createElement('button');
82
74
  languageButton.type = 'button';
83
75
  languageButton.className = LANGUAGE_BUTTON_STYLES;
84
- languageButton.textContent = languageName;
85
76
  languageButton.setAttribute('aria-haspopup', 'listbox');
86
77
  languageButton.setAttribute('data-blok-testid', 'code-language-btn');
87
78
 
79
+ const langText = document.createElement('span');
80
+ langText.textContent = languageName;
81
+ languageButton.appendChild(langText);
82
+
83
+ const chevronSpan = document.createElement('span');
84
+ chevronSpan.className = 'inline-flex items-center ml-0.5 -mr-0.5';
85
+ chevronSpan.innerHTML = IconChevronDown;
86
+ languageButton.appendChild(chevronSpan);
87
+
88
88
  // Spacer
89
89
  const spacer = document.createElement('div');
90
90
  spacer.className = 'flex-1';
91
91
 
92
- // Tab buttons (only when previewable)
93
- const { codeTab, previewTab, previewElement } = previewable
94
- ? buildPreviewElements(codeTabLabel, previewTabLabel)
95
- : { codeTab: null, previewTab: null, previewElement: null };
96
-
97
- // Wrap toggle button
98
- const wrapButton = document.createElement('button');
99
- wrapButton.type = 'button';
100
- wrapButton.className = HEADER_BUTTON_STYLES;
101
- wrapButton.innerHTML = IconWrap;
102
- wrapButton.setAttribute('aria-label', wrapLabel);
103
- wrapButton.setAttribute('data-blok-testid', 'code-wrap-btn');
104
-
105
- // Line numbers toggle button
106
- const lineNumbersButton = document.createElement('button');
107
- lineNumbersButton.type = 'button';
108
- lineNumbersButton.className = HEADER_BUTTON_STYLES;
109
- lineNumbersButton.innerHTML = IconLineNumbers;
110
- lineNumbersButton.setAttribute('aria-label', lineNumbersLabel ?? 'Line numbers');
111
- lineNumbersButton.setAttribute('data-blok-testid', 'code-line-numbers-btn');
92
+ // Preview toggle button (only when previewable and not read-only)
93
+ const { previewToggleButton, previewElement } = previewable
94
+ ? buildPreviewElements(previewToggleLabel)
95
+ : { previewToggleButton: null, previewElement: null };
112
96
 
113
97
  // Copy button
114
98
  const copyButton = document.createElement('button');
@@ -118,6 +102,54 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
118
102
  copyButton.setAttribute('aria-label', copyLabel);
119
103
  copyButton.setAttribute('data-blok-testid', 'code-copy-btn');
120
104
 
105
+ // More button (ellipsis)
106
+ const moreButton = document.createElement('button');
107
+ moreButton.type = 'button';
108
+ moreButton.className = HEADER_BUTTON_STYLES;
109
+ moreButton.innerHTML = IconEllipsis;
110
+ moreButton.setAttribute('aria-label', 'More');
111
+ moreButton.setAttribute('aria-haspopup', 'true');
112
+ moreButton.setAttribute('data-blok-testid', 'code-more-btn');
113
+
114
+ // More menu dropdown
115
+ const moreMenu = document.createElement('div');
116
+ moreMenu.className = MORE_MENU_STYLES;
117
+ moreMenu.hidden = true;
118
+ moreMenu.setAttribute('data-blok-testid', 'code-more-menu');
119
+
120
+ // Line numbers toggle (inside more menu)
121
+ const lineNumbersButton = document.createElement('button');
122
+ lineNumbersButton.type = 'button';
123
+ lineNumbersButton.className = MORE_MENU_ITEM_STYLES;
124
+ lineNumbersButton.setAttribute('data-blok-testid', 'code-line-numbers-btn');
125
+
126
+ const lineNumIconSpan = document.createElement('span');
127
+ lineNumIconSpan.className = 'flex items-center justify-center w-5 h-5';
128
+ lineNumIconSpan.innerHTML = IconLineNumbers;
129
+ lineNumbersButton.appendChild(lineNumIconSpan);
130
+
131
+ const lineNumText = document.createElement('span');
132
+ lineNumText.textContent = lineNumbersLabel ?? 'Line numbers';
133
+ lineNumbersButton.appendChild(lineNumText);
134
+
135
+ // Wrap toggle (inside more menu)
136
+ const wrapButton = document.createElement('button');
137
+ wrapButton.type = 'button';
138
+ wrapButton.className = MORE_MENU_ITEM_STYLES;
139
+ wrapButton.setAttribute('data-blok-testid', 'code-wrap-btn');
140
+
141
+ const wrapIconSpan = document.createElement('span');
142
+ wrapIconSpan.className = 'flex items-center justify-center w-5 h-5';
143
+ wrapIconSpan.innerHTML = IconWrap;
144
+ wrapButton.appendChild(wrapIconSpan);
145
+
146
+ const wrapText = document.createElement('span');
147
+ wrapText.textContent = wrapLabel;
148
+ wrapButton.appendChild(wrapText);
149
+
150
+ moreMenu.appendChild(lineNumbersButton);
151
+ moreMenu.appendChild(wrapButton);
152
+
121
153
  // Code area
122
154
  const codeElement = document.createElement('code');
123
155
  codeElement.className = CODE_AREA_STYLES;
@@ -146,19 +178,23 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
146
178
  gutterElement.appendChild(lineEl);
147
179
  });
148
180
 
149
- // Assemble header
181
+ // Assemble header: [language] [spacer] [preview toggle?] [copy] [more ▸ menu]
150
182
  header.appendChild(languageButton);
151
183
  header.appendChild(spacer);
152
184
 
153
- if (codeTab && previewTab) {
154
- header.appendChild(codeTab);
155
- header.appendChild(previewTab);
185
+ if (previewToggleButton) {
186
+ header.appendChild(previewToggleButton);
156
187
  }
157
188
 
158
- header.appendChild(lineNumbersButton);
159
- header.appendChild(wrapButton);
160
189
  header.appendChild(copyButton);
161
190
 
191
+ // More wrapper (relative position anchor for absolute dropdown)
192
+ const moreWrapper = document.createElement('div');
193
+ moreWrapper.className = 'relative';
194
+ moreWrapper.appendChild(moreButton);
195
+ moreWrapper.appendChild(moreMenu);
196
+ header.appendChild(moreWrapper);
197
+
162
198
  // Pre wrapper for semantic HTML
163
199
  const preElement = document.createElement('pre');
164
200
  preElement.appendChild(codeElement);
@@ -177,5 +213,5 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
177
213
  wrapper.appendChild(previewElement);
178
214
  }
179
215
 
180
- return { wrapper, languageButton, lineNumbersButton, copyButton, wrapButton, preElement, codeElement, gutterElement, codeTab, previewTab, previewElement };
216
+ return { wrapper, languageButton, lineNumbersButton, copyButton, wrapButton, preElement, codeElement, gutterElement, previewToggleButton, previewElement, moreButton, moreMenu };
181
217
  }
@@ -14,7 +14,8 @@ import { IconCodeBlock } from '../../components/icons';
14
14
  import { buildCodeDOM } from './dom-builder';
15
15
  import type { CodeDOMRefs } from './dom-builder';
16
16
  import { handleCodeKeydown } from './code-keyboard';
17
- import { LanguagePicker } from './language-picker';
17
+ import { PopoverDesktop } from '../../components/utils/popover';
18
+ import type { PopoverItemParams } from '@/types/utils/popover/popover-item';
18
19
  import {
19
20
  DEFAULT_LANGUAGE,
20
21
  LANGUAGES,
@@ -24,13 +25,10 @@ import {
24
25
  LINE_NUMBERS_KEY,
25
26
  COPIED_KEY,
26
27
  LANGUAGE_KEY,
28
+ SEARCH_LANGUAGE_KEY,
27
29
  COPIED_FEEDBACK_STYLES,
28
30
  PREVIEWABLE_LANGUAGES,
29
- CODE_TAB_KEY,
30
- PREVIEW_TAB_KEY,
31
- TAB_STYLES,
32
- TAB_ACTIVE_STYLES,
33
- TAB_INACTIVE_STYLES,
31
+ PREVIEW_TOGGLE_KEY,
34
32
  PREVIEW_AREA_STYLES,
35
33
  GUTTER_LINE_STYLES,
36
34
  } from './constants';
@@ -48,7 +46,7 @@ export class CodeTool implements BlockTool {
48
46
  private _dom: CodeDOMRefs | null = null;
49
47
  private _wrapping = true;
50
48
  private _lineNumbers = true;
51
- private _picker: LanguagePicker | null = null;
49
+ private _picker: PopoverDesktop | null = null;
52
50
  private _previewActive = false;
53
51
  private _previewContainer: HTMLElement | null = null;
54
52
  private _disposeHighlights: (() => void) | null = null;
@@ -76,8 +74,7 @@ export class CodeTool implements BlockTool {
76
74
  wrapLabel: this.api.i18n.t(WRAP_LINES_KEY),
77
75
  lineNumbersLabel: this.api.i18n.t(LINE_NUMBERS_KEY),
78
76
  previewable: this.readOnly ? false : isPreviewable,
79
- codeTabLabel: this.api.i18n.t(CODE_TAB_KEY),
80
- previewTabLabel: this.api.i18n.t(PREVIEW_TAB_KEY),
77
+ previewToggleLabel: this.api.i18n.t(PREVIEW_TOGGLE_KEY),
81
78
  });
82
79
 
83
80
  this._dom = dom;
@@ -86,7 +83,10 @@ export class CodeTool implements BlockTool {
86
83
  dom.gutterElement.hidden = !this._lineNumbers;
87
84
  dom.lineNumbersButton.addEventListener('click', () => this.toggleLineNumbers());
88
85
 
89
- // Read-only + previewable: show preview only, hide code, no tabs
86
+ // More menu toggle
87
+ dom.moreButton.addEventListener('click', () => this.toggleMoreMenu());
88
+
89
+ // Read-only + previewable: show preview only, hide code, no toggle
90
90
  if (this.readOnly && isPreviewable) {
91
91
  const previewEl = document.createElement('div');
92
92
 
@@ -99,8 +99,8 @@ export class CodeTool implements BlockTool {
99
99
  void this.renderPreview();
100
100
  }
101
101
 
102
- // Edit mode + previewable: show tabs, default to preview
103
- if (!this.readOnly && isPreviewable && dom.codeTab && dom.previewTab && dom.previewElement) {
102
+ // Edit mode + previewable: show preview toggle, default to preview
103
+ if (!this.readOnly && isPreviewable && dom.previewToggleButton && dom.previewElement) {
104
104
  this._previewActive = true;
105
105
  dom.preElement.hidden = true;
106
106
  dom.gutterElement.hidden = true;
@@ -108,8 +108,7 @@ export class CodeTool implements BlockTool {
108
108
  this._previewContainer = dom.previewElement;
109
109
  void this.renderPreview();
110
110
 
111
- dom.codeTab.addEventListener('click', () => this.showCode());
112
- dom.previewTab.addEventListener('click', () => this.showPreview());
111
+ dom.previewToggleButton.addEventListener('click', () => this.togglePreview());
113
112
  }
114
113
 
115
114
  if (!this.readOnly) {
@@ -135,17 +134,28 @@ export class CodeTool implements BlockTool {
135
134
  dom.wrapButton.addEventListener('click', () => this.toggleWrap());
136
135
 
137
136
  if (!this.readOnly) {
138
- this._picker = new LanguagePicker({
139
- languages: LANGUAGES,
140
- onSelect: (id: string) => this.setLanguage(id),
141
- i18n: this.api.i18n,
142
- activeLanguageId: this._data.language,
137
+ const languageItems: PopoverItemParams[] = LANGUAGES.map((lang) => ({
138
+ title: lang.name,
139
+ name: lang.id,
140
+ toggle: 'language',
141
+ isActive: (): boolean => this._data.language === lang.id,
142
+ closeOnActivate: true,
143
+ onActivate: (): void => this.setLanguage(lang.id),
144
+ }));
145
+
146
+ this._picker = new PopoverDesktop({
147
+ items: languageItems,
148
+ trigger: dom.languageButton,
149
+ leftAlignElement: dom.wrapper,
150
+ searchable: true,
151
+ width: '200px',
152
+ messages: {
153
+ search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
154
+ },
143
155
  });
144
156
 
145
- document.body.appendChild(this._picker.getElement());
146
-
147
157
  dom.languageButton.addEventListener('click', () => {
148
- this._picker?.open(dom.languageButton);
158
+ this._picker?.show();
149
159
  });
150
160
  }
151
161
 
@@ -156,8 +166,16 @@ export class CodeTool implements BlockTool {
156
166
  void this.highlightCode();
157
167
  }
158
168
 
169
+ private togglePreview(): void {
170
+ if (this._previewActive) {
171
+ this.showCode();
172
+ } else {
173
+ this.showPreview();
174
+ }
175
+ }
176
+
159
177
  private showCode(): void {
160
- if (!this._dom?.previewElement || !this._dom.codeTab || !this._dom.previewTab) {
178
+ if (!this._dom?.previewElement || !this._dom.previewToggleButton) {
161
179
  return;
162
180
  }
163
181
 
@@ -165,12 +183,10 @@ export class CodeTool implements BlockTool {
165
183
  this._dom.preElement.hidden = false;
166
184
  this._dom.gutterElement.hidden = !this._lineNumbers;
167
185
  this._dom.previewElement.hidden = true;
168
- this._dom.codeTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
169
- this._dom.previewTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
170
186
  }
171
187
 
172
188
  private showPreview(): void {
173
- if (!this._dom?.previewElement || !this._dom.codeTab || !this._dom.previewTab) {
189
+ if (!this._dom?.previewElement || !this._dom.previewToggleButton) {
174
190
  return;
175
191
  }
176
192
 
@@ -178,13 +194,19 @@ export class CodeTool implements BlockTool {
178
194
  this._dom.preElement.hidden = true;
179
195
  this._dom.gutterElement.hidden = true;
180
196
  this._dom.previewElement.hidden = false;
181
- this._dom.codeTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
182
- this._dom.previewTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
183
197
 
184
198
  // Re-render preview with current code content
185
199
  void this.renderPreview();
186
200
  }
187
201
 
202
+ private toggleMoreMenu(): void {
203
+ if (!this._dom) {
204
+ return;
205
+ }
206
+
207
+ this._dom.moreMenu.hidden = !this._dom.moreMenu.hidden;
208
+ }
209
+
188
210
  private async renderPreview(): Promise<void> {
189
211
  if (!this._previewContainer) {
190
212
  return;
@@ -198,6 +220,22 @@ export class CodeTool implements BlockTool {
198
220
  this._previewContainer.innerHTML = rendered;
199
221
  }
200
222
 
223
+ public setReadOnly(state: boolean): void {
224
+ this.readOnly = state;
225
+
226
+ if (!this._dom) {
227
+ return;
228
+ }
229
+
230
+ if (state) {
231
+ this._dom.codeElement.setAttribute('contenteditable', 'false');
232
+ this._dom.codeElement.removeAttribute('spellcheck');
233
+ } else {
234
+ this._dom.codeElement.setAttribute('contenteditable', 'plaintext-only');
235
+ this._dom.codeElement.setAttribute('spellcheck', 'false');
236
+ }
237
+ }
238
+
201
239
  public save(_blockContent: HTMLElement): CodeData {
202
240
  return {
203
241
  code: this._dom?.codeElement.textContent ?? '',
@@ -265,10 +303,14 @@ export class CodeTool implements BlockTool {
265
303
  this._data.language = id;
266
304
 
267
305
  if (this._dom) {
268
- this._dom.languageButton.textContent = this.getLanguageName(id);
306
+ // Update the text span inside the language button (first child)
307
+ const textSpan = this._dom.languageButton.querySelector('span');
308
+
309
+ if (textSpan) {
310
+ textSpan.textContent = this.getLanguageName(id);
311
+ }
269
312
  }
270
313
 
271
- this._picker?.setActiveLanguage(id);
272
314
  void this.highlightCode();
273
315
  }
274
316
 
@@ -419,7 +461,7 @@ export class CodeTool implements BlockTool {
419
461
  }
420
462
 
421
463
  if (this._picker) {
422
- this._picker.getElement().remove();
464
+ this._picker.destroy();
423
465
  this._picker = null;
424
466
  }
425
467
  }
@@ -96,6 +96,11 @@ export class DividerTool implements BlockTool {
96
96
  */
97
97
  public onPaste(_event: PasteEvent): void {}
98
98
 
99
+ /**
100
+ * Toggle read-only mode in place. Divider is purely presentational — no-op.
101
+ */
102
+ public setReadOnly(_state: boolean): void {}
103
+
99
104
  /**
100
105
  * Nothing to sanitize — no HTML content
101
106
  */
@@ -111,6 +111,12 @@ export class Header implements BlockTool {
111
111
  */
112
112
  private readOnly: boolean;
113
113
 
114
+ /**
115
+ * Cleanup function for the placeholder, returned by setupPlaceholder().
116
+ * Stored so we can tear down the placeholder when entering read-only mode.
117
+ */
118
+ private placeholderCleanup: (() => void) | null = null;
119
+
114
120
  /**
115
121
  * Tool's settings passed from Editor
116
122
  */
@@ -302,6 +308,45 @@ export class Header implements BlockTool {
302
308
  this._element.removeEventListener('keydown', this.handleKeyDown);
303
309
  }
304
310
 
311
+ /**
312
+ * Toggle read-only mode in-place without re-rendering the DOM element.
313
+ * Manages contentEditable, keydown listener (for toggle headings),
314
+ * placeholder setup/teardown, and body placeholder click handler.
315
+ *
316
+ * @param state - true to enter read-only mode, false to exit
317
+ */
318
+ public setReadOnly(state: boolean): void {
319
+ if (!this._element) {
320
+ return;
321
+ }
322
+
323
+ this.readOnly = state;
324
+
325
+ if (state) {
326
+ this._element.contentEditable = 'false';
327
+
328
+ if (this._data.isToggleable) {
329
+ this._element.removeEventListener('keydown', this.handleKeyDown);
330
+ }
331
+
332
+ if (this.placeholderCleanup) {
333
+ this.placeholderCleanup();
334
+ this.placeholderCleanup = null;
335
+ }
336
+ } else {
337
+ this._element.contentEditable = 'true';
338
+
339
+ if (this._data.isToggleable) {
340
+ this._element.addEventListener('keydown', this.handleKeyDown);
341
+ }
342
+
343
+ const translatedName = this.api.i18n.t(this.currentLevel.nameKey);
344
+ const placeholderText = this.resolvePlaceholderText(translatedName);
345
+
346
+ this.placeholderCleanup = setupPlaceholder(this._element, placeholderText);
347
+ }
348
+ }
349
+
305
350
  /**
306
351
  * Expand the toggle heading (no-op if not toggleable or already expanded).
307
352
  * Can be called externally via block.call('expand').
@@ -679,7 +724,7 @@ export class Header implements BlockTool {
679
724
  const placeholderText = this.resolvePlaceholderText(translatedName);
680
725
 
681
726
  if (!this.readOnly) {
682
- setupPlaceholder(tag, placeholderText);
727
+ this.placeholderCleanup = setupPlaceholder(tag, placeholderText);
683
728
  } else {
684
729
  tag.setAttribute('data-placeholder', placeholderText);
685
730
  }
@@ -750,6 +795,7 @@ export class Header implements BlockTool {
750
795
  // pl-8 (32px) matches the heading's left padding so children align with the title text start.
751
796
  childContainer.className = 'pl-8';
752
797
  childContainer.setAttribute(TOGGLE_ATTR.toggleChildren, '');
798
+ childContainer.setAttribute(DATA_ATTR.nestedBlocks, '');
753
799
  // Block DOM mutations inside the children container from triggering the header tool's
754
800
  // didMutated → syncBlockDataToYjs path (same rationale as the toggle list tool).
755
801
  childContainer.setAttribute('data-blok-mutation-free', 'true');
@@ -111,7 +111,9 @@ export const buildListItem = (context: DOMBuilderContext): BuildResult => {
111
111
  // Extract element references for the result
112
112
  const markerElement = itemContent.querySelector<HTMLElement>('[data-list-marker]');
113
113
  const checkboxElement = itemContent.querySelector<HTMLInputElement>('input[type="checkbox"]');
114
- const contentElement = itemContent.querySelector<HTMLElement>('[contenteditable]');
114
+ const contentElement = data.style === 'checklist'
115
+ ? itemContent.querySelector<HTMLElement>(`[data-blok-testid="${LIST_TEST_IDS.checklistContent}"]`)
116
+ : itemContent.querySelector<HTMLElement>(`[data-blok-testid="${LIST_TEST_IDS.contentContainer}"]`);
115
117
 
116
118
  return {
117
119
  wrapper,