@jackuait/blok 0.7.3-beta.4 → 0.7.3
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.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-CdxHhr5i.mjs → blok-BmlbETK7.mjs} +2119 -2013
- package/dist/chunks/{constants-C_H9o9Ao.mjs → constants-WhLyFkza.mjs} +260 -223
- package/dist/chunks/{i18next-loader-D5HxE5ZQ.mjs → i18next-loader-CZARkla1.mjs} +1 -1
- package/dist/chunks/{lightweight-i18n-Safdy0ua.mjs → lightweight-i18n-BQa0F2X6.mjs} +9 -0
- package/dist/chunks/{tools-B0YXCZFW.mjs → tools-BCb5bMO3.mjs} +973 -843
- package/dist/full.mjs +3 -3
- package/dist/locales.mjs +9 -0
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +2 -2
- package/src/components/block/style-manager.ts +1 -1
- package/src/components/blocks.ts +26 -54
- package/src/components/constants/data-attributes.ts +0 -2
- package/src/components/i18n/locales/en/messages.json +9 -0
- package/src/components/icons/index.ts +34 -6
- package/src/components/inline-tools/inline-tool-link.ts +202 -5
- package/src/components/inline-tools/inline-tool-marker.ts +166 -23
- package/src/components/inline-tools/utils/formatting-range-utils.ts +10 -1
- package/src/components/modules/blockManager/blockManager.ts +2 -2
- package/src/components/modules/blockManager/operations.ts +2 -2
- package/src/components/modules/blockManager/repository.ts +1 -9
- package/src/components/modules/blockManager/types.ts +1 -1
- package/src/components/modules/drag/operations/DragOperations.ts +45 -6
- package/src/components/modules/paste/google-docs-preprocessor.ts +69 -2
- package/src/components/modules/paste/handlers/blok-data-handler.ts +96 -19
- package/src/components/modules/renderer.ts +2 -0
- package/src/components/modules/toolbar/blockSettings.ts +1 -1
- package/src/components/modules/toolbar/index.ts +21 -0
- package/src/components/modules/toolbar/plus-button.ts +15 -5
- package/src/components/selection/fake-background/index.ts +9 -10
- package/src/components/shared/color-picker.ts +108 -95
- package/src/components/shared/color-presets.ts +30 -2
- package/src/components/ui/toolbox.ts +36 -7
- package/src/components/utils/color-mapping.ts +43 -1
- package/src/components/utils/color-migration.ts +37 -0
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +4 -3
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +5 -39
- package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts +2 -2
- package/src/components/utils/popover/components/popover-item/popover-item.ts +11 -0
- package/src/components/utils/popover/components/search-input/search-input.const.ts +2 -3
- package/src/components/utils/popover/components/search-input/search-input.ts +1 -32
- package/src/components/utils/popover/popover-abstract.ts +2 -4
- package/src/components/utils/popover/popover-desktop.ts +1 -16
- package/src/components/utils/popover/popover-inline.ts +1 -2
- package/src/components/utils/popover/popover-mobile.ts +2 -2
- package/src/components/utils/popover/popover.const.ts +1 -1
- package/src/stories/Table.stories.ts +15 -9
- package/src/styles/main.css +312 -14
- package/src/tools/header/index.ts +5 -5
- package/src/tools/list/constants.ts +11 -4
- package/src/tools/list/depth-validator.ts +13 -1
- package/src/tools/list/dom-builder.ts +5 -3
- package/src/tools/list/index.ts +3 -2
- package/src/tools/paragraph/index.ts +2 -2
- package/src/tools/table/table-cell-color-picker.ts +1 -1
- package/src/tools/table/table-cell-selection.ts +1 -2
- package/src/tools/table/table-core.ts +2 -2
- package/src/tools/table/table-grip-visuals.ts +13 -5
- package/src/tools/table/table-heading-toggle.ts +15 -9
- package/src/tools/table/table-row-col-controls.ts +17 -11
- package/src/tools/table/table-row-col-drag.ts +26 -3
- package/src/tools/toggle/constants.ts +5 -5
- package/src/tools/toggle/index.ts +1 -1
- package/types/tools/hook-events.d.ts +6 -0
- package/types/utils/popover/popover-item.d.ts +6 -0
- package/CHANGELOG.md +0 -119
|
@@ -127,9 +127,19 @@ export class BlokDataHandler extends BasePasteHandler implements PasteHandler {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
* Insert Blok JSON blocks
|
|
131
|
-
*
|
|
132
|
-
*
|
|
130
|
+
* Insert Blok JSON blocks using a two-pass approach:
|
|
131
|
+
*
|
|
132
|
+
* Pass 1 — child blocks (those whose parentId is within the pasted set) are
|
|
133
|
+
* inserted first. They receive new IDs, which are recorded in a map.
|
|
134
|
+
*
|
|
135
|
+
* Pass 2 — root/container blocks (e.g. tables) are inserted with their data
|
|
136
|
+
* remapped so that any old child-block ID references are replaced by the new
|
|
137
|
+
* IDs from Pass 1. This prevents container tools (TableCellBlocks) from
|
|
138
|
+
* resolving old IDs that still exist in the editor and stealing blocks from
|
|
139
|
+
* the original table.
|
|
140
|
+
*
|
|
141
|
+
* After both passes the parent-child hierarchy is re-established using the
|
|
142
|
+
* accumulated old→new ID mapping.
|
|
133
143
|
*/
|
|
134
144
|
private insertBlokBlocks(
|
|
135
145
|
blocks: BlokClipboardBlock[],
|
|
@@ -142,31 +152,70 @@ export class BlokDataHandler extends BasePasteHandler implements PasteHandler {
|
|
|
142
152
|
this.config.sanitizer
|
|
143
153
|
);
|
|
144
154
|
|
|
155
|
+
// Capture replace intent before any insertions move the current block pointer.
|
|
156
|
+
const shouldReplaceFirst =
|
|
157
|
+
canReplace &&
|
|
158
|
+
Boolean(BlockManager.currentBlock?.tool.isDefault) &&
|
|
159
|
+
Boolean(BlockManager.currentBlock?.isEmpty);
|
|
160
|
+
|
|
161
|
+
// Set of old IDs present in this paste, used to identify parent-child pairs.
|
|
162
|
+
const pastedOldIds = new Set(blocks.map(b => b.id));
|
|
163
|
+
|
|
164
|
+
type Entry = { sanitized: (typeof sanitizedBlocks)[number]; original: BlokClipboardBlock };
|
|
165
|
+
const children: Entry[] = [];
|
|
166
|
+
const roots: Entry[] = [];
|
|
167
|
+
|
|
168
|
+
sanitizedBlocks.forEach((sanitizedBlock, i) => {
|
|
169
|
+
const original = blocks[i];
|
|
170
|
+
|
|
171
|
+
if (original === undefined) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const isChild =
|
|
176
|
+
original.parentId !== undefined &&
|
|
177
|
+
original.parentId !== null &&
|
|
178
|
+
pastedOldIds.has(original.parentId);
|
|
179
|
+
|
|
180
|
+
(isChild ? children : roots).push({ sanitized: sanitizedBlock, original });
|
|
181
|
+
});
|
|
182
|
+
|
|
145
183
|
/**
|
|
146
184
|
* Map from original (old) block ID to the newly inserted Block instance.
|
|
147
|
-
* Used
|
|
185
|
+
* Used to remap data and restore hierarchy after both passes.
|
|
148
186
|
*/
|
|
149
187
|
const oldIdToEntry = new Map<string, { newBlock: Block; original: BlokClipboardBlock }>();
|
|
150
188
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
canReplace &&
|
|
155
|
-
Boolean(BlockManager.currentBlock?.tool.isDefault) &&
|
|
156
|
-
Boolean(BlockManager.currentBlock?.isEmpty);
|
|
189
|
+
// Pass 1: insert children first so they exist with new IDs before the parent.
|
|
190
|
+
children.forEach(({ sanitized, original }) => {
|
|
191
|
+
const block = BlockManager.insert({ tool: sanitized.tool, data: sanitized.data });
|
|
157
192
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
replace: needToReplaceCurrentBlock,
|
|
162
|
-
});
|
|
193
|
+
oldIdToEntry.set(original.id, { newBlock: block, original });
|
|
194
|
+
Caret.setToBlock(block, Caret.positions.END);
|
|
195
|
+
});
|
|
163
196
|
|
|
164
|
-
|
|
197
|
+
// Build old→new string map for remapping ID references inside parent data.
|
|
198
|
+
const oldIdToNewId = new Map<string, string>();
|
|
165
199
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
200
|
+
for (const [oldId, { newBlock }] of oldIdToEntry) {
|
|
201
|
+
oldIdToNewId.set(oldId, newBlock.id);
|
|
202
|
+
}
|
|
169
203
|
|
|
204
|
+
// Pass 2: insert root blocks with child IDs remapped in their data.
|
|
205
|
+
// Skip replace when children were pre-inserted to avoid replacing a
|
|
206
|
+
// just-inserted child paragraph rather than the original empty block.
|
|
207
|
+
roots.forEach(({ sanitized, original }, idx) => {
|
|
208
|
+
const remappedData = oldIdToNewId.size > 0
|
|
209
|
+
? remapIds(sanitized.data, oldIdToNewId) as typeof sanitized.data
|
|
210
|
+
: sanitized.data;
|
|
211
|
+
|
|
212
|
+
const block = BlockManager.insert({
|
|
213
|
+
tool: sanitized.tool,
|
|
214
|
+
data: remappedData,
|
|
215
|
+
replace: idx === 0 && shouldReplaceFirst && children.length === 0,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
oldIdToEntry.set(original.id, { newBlock: block, original });
|
|
170
219
|
Caret.setToBlock(block, Caret.positions.END);
|
|
171
220
|
});
|
|
172
221
|
|
|
@@ -193,3 +242,31 @@ export class BlokDataHandler extends BasePasteHandler implements PasteHandler {
|
|
|
193
242
|
}
|
|
194
243
|
}
|
|
195
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Recursively walks `value` and replaces any string found as a key in `idMap`
|
|
248
|
+
* with its mapped value. Used to remap old block IDs to new IDs within a
|
|
249
|
+
* tool's data object before insertion, so that container blocks (e.g. tables)
|
|
250
|
+
* reference the correct newly-inserted child block IDs.
|
|
251
|
+
*/
|
|
252
|
+
function remapIds(value: unknown, idMap: Map<string, string>): unknown {
|
|
253
|
+
if (typeof value === 'string') {
|
|
254
|
+
return idMap.get(value) ?? value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (Array.isArray(value)) {
|
|
258
|
+
return value.map(item => remapIds(item, idMap));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (value !== null && typeof value === 'object') {
|
|
262
|
+
const result: Record<string, unknown> = {};
|
|
263
|
+
|
|
264
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
265
|
+
result[k] = remapIds(v, idMap);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
shouldExpandToHierarchical,
|
|
11
11
|
type DataFormatAnalysis,
|
|
12
12
|
} from '../utils/data-model-transform';
|
|
13
|
+
import { migrateMarkColors } from '../utils/color-migration';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Module that responsible for rendering Blocks on blok initialization
|
|
@@ -157,6 +158,7 @@ export class Renderer extends Module {
|
|
|
157
158
|
* Insert batch of Blocks
|
|
158
159
|
*/
|
|
159
160
|
BlockManager.insertMany(blocks);
|
|
161
|
+
migrateMarkColors(this.Blok.UI.nodes.redactor);
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
/**
|
|
@@ -140,6 +140,7 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
140
140
|
{
|
|
141
141
|
getToolboxOpened: () => this.toolbox.opened ?? false,
|
|
142
142
|
openToolbox: () => this.toolbox.open(),
|
|
143
|
+
openToolboxWithoutSlash: () => this.toolbox.openWithoutSlash(),
|
|
143
144
|
closeToolbox: () => this.toolbox.close(),
|
|
144
145
|
moveAndOpenToolbar: (block, target) => this.moveAndOpen(block, target),
|
|
145
146
|
}
|
|
@@ -202,6 +203,7 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
202
203
|
opened: boolean | undefined; // undefined is for the case when Toolbox is not initialized yet
|
|
203
204
|
close: () => void;
|
|
204
205
|
open: () => void;
|
|
206
|
+
openWithoutSlash: () => void;
|
|
205
207
|
toggle: () => void;
|
|
206
208
|
hasFocus: () => boolean | undefined;
|
|
207
209
|
} {
|
|
@@ -210,6 +212,25 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
210
212
|
close: () => {
|
|
211
213
|
this.toolboxInstance?.close();
|
|
212
214
|
},
|
|
215
|
+
openWithoutSlash: () => {
|
|
216
|
+
if (this.toolboxInstance === null) {
|
|
217
|
+
log('toolbox.openWithoutSlash() called before initialization is finished', 'warn');
|
|
218
|
+
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.hoveredBlock && !this.hoveredBlockIsFromTableCell) {
|
|
223
|
+
const currentBlock = this.Blok.BlockManager.currentBlock;
|
|
224
|
+
const isCurrentBlockInsideTableCell = currentBlock !== undefined
|
|
225
|
+
&& currentBlock.holder.closest('[data-blok-table-cell-blocks]') !== null;
|
|
226
|
+
|
|
227
|
+
if (!isCurrentBlockInsideTableCell) {
|
|
228
|
+
this.Blok.BlockManager.currentBlock = this.hoveredBlock;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.toolboxInstance.open(false);
|
|
233
|
+
},
|
|
213
234
|
open: () => {
|
|
214
235
|
/**
|
|
215
236
|
* If Toolbox is not initialized yet, do nothing
|
|
@@ -27,10 +27,15 @@ export class PlusButtonHandler {
|
|
|
27
27
|
private getToolboxOpened: () => boolean;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Callback to open the toolbox
|
|
30
|
+
* Callback to open the toolbox in slash-search mode
|
|
31
31
|
*/
|
|
32
32
|
private openToolbox: () => void;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Callback to open the toolbox in no-slash mode (used when clicking the plus button)
|
|
36
|
+
*/
|
|
37
|
+
private openToolboxWithoutSlash: () => void;
|
|
38
|
+
|
|
34
39
|
/**
|
|
35
40
|
* Callback to close the toolbox
|
|
36
41
|
*/
|
|
@@ -50,6 +55,7 @@ export class PlusButtonHandler {
|
|
|
50
55
|
callbacks: {
|
|
51
56
|
getToolboxOpened: () => boolean;
|
|
52
57
|
openToolbox: () => void;
|
|
58
|
+
openToolboxWithoutSlash: () => void;
|
|
53
59
|
closeToolbox: () => void;
|
|
54
60
|
moveAndOpenToolbar: (block?: Block | null, target?: Element | null) => void;
|
|
55
61
|
}
|
|
@@ -57,6 +63,7 @@ export class PlusButtonHandler {
|
|
|
57
63
|
this.getBlok = getBlok;
|
|
58
64
|
this.getToolboxOpened = callbacks.getToolboxOpened;
|
|
59
65
|
this.openToolbox = callbacks.openToolbox;
|
|
66
|
+
this.openToolboxWithoutSlash = callbacks.openToolboxWithoutSlash;
|
|
60
67
|
this.closeToolbox = callbacks.closeToolbox;
|
|
61
68
|
this.moveAndOpenToolbar = callbacks.moveAndOpenToolbar;
|
|
62
69
|
}
|
|
@@ -196,14 +203,17 @@ export class PlusButtonHandler {
|
|
|
196
203
|
hoveredBlock?.holder.after(targetBlock.holder);
|
|
197
204
|
}
|
|
198
205
|
|
|
199
|
-
//
|
|
206
|
+
// Position caret and open toolbox
|
|
200
207
|
if (startsWithSlash) {
|
|
208
|
+
// Block already has "/" - keep slash-search mode, position after the slash
|
|
201
209
|
Caret.setToBlock(targetBlock, Caret.positions.DEFAULT, 1);
|
|
210
|
+
this.moveAndOpenToolbar(targetBlock);
|
|
211
|
+
this.openToolbox();
|
|
202
212
|
} else {
|
|
213
|
+
// New empty block - open toolbox directly without inserting "/"
|
|
203
214
|
Caret.setToBlock(targetBlock, Caret.positions.START);
|
|
204
|
-
|
|
215
|
+
this.moveAndOpenToolbar(targetBlock);
|
|
216
|
+
this.openToolboxWithoutSlash();
|
|
205
217
|
}
|
|
206
|
-
this.moveAndOpenToolbar(targetBlock);
|
|
207
|
-
this.openToolbox();
|
|
208
218
|
}
|
|
209
219
|
}
|
|
@@ -119,18 +119,17 @@ export class FakeBackgroundManager {
|
|
|
119
119
|
* Unwraps the highlight spans and restores the selection
|
|
120
120
|
*/
|
|
121
121
|
removeFakeBackground(): void {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
if (this.selectionUtils.isFakeBackgroundEnabled) {
|
|
123
|
+
// Remove highlight spans first while they still exist in the DOM,
|
|
124
|
+
// so that removeHighlightSpans() can reconstruct savedSelectionRange
|
|
125
|
+
// as a text-node-based range before the spans are gone.
|
|
126
|
+
this.removeHighlightSpans();
|
|
127
|
+
this.selectionUtils.isFakeBackgroundEnabled = false;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
this.selectionUtils.isFakeBackgroundEnabled = false;
|
|
130
|
+
// Clean up any remaining/orphaned fake background elements
|
|
131
|
+
// (handles undo/redo restoring spans, backwards compat, etc.)
|
|
132
|
+
this.removeOrphanedFakeBackgroundElements();
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
/**
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import type { I18n } from '../../../types/api';
|
|
2
2
|
import { parseColor } from '../utils/color-mapping';
|
|
3
|
+
import { onHover } from '../utils/tooltip';
|
|
3
4
|
import { twMerge } from '../utils/tw';
|
|
4
|
-
import { COLOR_PRESETS } from './color-presets';
|
|
5
|
+
import { COLOR_PRESETS, COLOR_PRESETS_DARK } from './color-presets';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the appropriate preset array for the current theme.
|
|
9
|
+
* Checks the data-blok-theme attribute first (explicit override),
|
|
10
|
+
* then falls back to the prefers-color-scheme media query.
|
|
11
|
+
*/
|
|
12
|
+
function getActivePresets(): typeof COLOR_PRESETS {
|
|
13
|
+
const theme = document.documentElement.getAttribute('data-blok-theme');
|
|
14
|
+
|
|
15
|
+
if (theme === 'dark') return COLOR_PRESETS_DARK;
|
|
16
|
+
if (theme === 'light') return COLOR_PRESETS;
|
|
17
|
+
|
|
18
|
+
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
|
19
|
+
return COLOR_PRESETS_DARK;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return COLOR_PRESETS;
|
|
23
|
+
}
|
|
5
24
|
|
|
6
25
|
/**
|
|
7
26
|
* Compare two CSS color strings for equality by their parsed RGB tuples.
|
|
@@ -23,7 +42,7 @@ function colorsEqual(a: string, b: string): boolean {
|
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
/**
|
|
26
|
-
* Describes one
|
|
45
|
+
* Describes one section in the color picker (e.g. "Text" or "Background")
|
|
27
46
|
*/
|
|
28
47
|
export interface ColorPickerMode {
|
|
29
48
|
key: string;
|
|
@@ -37,7 +56,6 @@ export interface ColorPickerMode {
|
|
|
37
56
|
export interface ColorPickerOptions {
|
|
38
57
|
i18n: I18n;
|
|
39
58
|
modes: [ColorPickerMode, ColorPickerMode];
|
|
40
|
-
defaultModeIndex?: number;
|
|
41
59
|
testIdPrefix: string;
|
|
42
60
|
onColorSelect: (color: string | null, modeKey: string) => void;
|
|
43
61
|
}
|
|
@@ -49,163 +67,158 @@ export interface ColorPickerHandle {
|
|
|
49
67
|
element: HTMLDivElement;
|
|
50
68
|
/**
|
|
51
69
|
* Set the currently active color for visual indication on the matching swatch.
|
|
52
|
-
* Pass null to clear any active indicator.
|
|
70
|
+
* Pass null to clear any active indicator for that section.
|
|
53
71
|
* @param color - CSS color value or null to clear
|
|
54
|
-
* @param modeKey - The mode key (e.g. 'color', 'background-color') to match the correct
|
|
72
|
+
* @param modeKey - The mode key (e.g. 'color', 'background-color') to match the correct section
|
|
55
73
|
*/
|
|
56
74
|
setActiveColor: (color: string | null, modeKey: string) => void;
|
|
57
75
|
/**
|
|
58
|
-
* Reset the picker state (
|
|
59
|
-
* Call this when the picker is reopened to ensure consistent initial state.
|
|
76
|
+
* Reset the picker state back to defaults (clear all active colors).
|
|
60
77
|
*/
|
|
61
78
|
reset: () => void;
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/**
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
const TAB_BASE_CLASSES = 'flex-1 py-1.5 text-xs text-center rounded-md cursor-pointer border-none outline-hidden transition-colors';
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Neutral background for text-mode swatches so they render as visible buttons
|
|
82
|
+
* Neutral background for text-mode swatches so they render as visible buttons.
|
|
83
|
+
* Uses a CSS variable so it adapts to dark mode.
|
|
71
84
|
*/
|
|
72
|
-
const SWATCH_NEUTRAL_BG = '
|
|
85
|
+
const SWATCH_NEUTRAL_BG = 'var(--blok-swatch-neutral-bg)';
|
|
73
86
|
|
|
74
87
|
/**
|
|
75
|
-
* Creates a color picker element with two
|
|
76
|
-
* a 5-column swatch grid
|
|
88
|
+
* Creates a color picker element with two always-visible sections (e.g. Text / Background),
|
|
89
|
+
* each containing a 5-column swatch grid with a "Default" reset swatch at position 0.
|
|
77
90
|
*
|
|
78
91
|
* Shared between the marker inline tool and the table cell color popover.
|
|
79
92
|
*/
|
|
80
93
|
export function createColorPicker(options: ColorPickerOptions): ColorPickerHandle {
|
|
81
94
|
const { i18n, modes, testIdPrefix, onColorSelect } = options;
|
|
82
|
-
const
|
|
83
|
-
|
|
95
|
+
const state = {
|
|
96
|
+
activeColors: Object.fromEntries(modes.map((m) => [m.key, null])) as Record<string, string | null>,
|
|
97
|
+
};
|
|
84
98
|
|
|
85
99
|
const wrapper = document.createElement('div');
|
|
86
100
|
|
|
87
101
|
wrapper.setAttribute('data-blok-testid', `${testIdPrefix}-picker`);
|
|
88
|
-
wrapper.className = 'flex flex-col gap-
|
|
102
|
+
wrapper.className = 'flex flex-col gap-3 p-2';
|
|
89
103
|
|
|
90
104
|
/**
|
|
91
|
-
*
|
|
105
|
+
* One grid element per section, stored so we can re-render independently.
|
|
92
106
|
*/
|
|
93
|
-
const
|
|
107
|
+
const sectionGrids: HTMLDivElement[] = [];
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Build sections once; re-render only the grids on state changes.
|
|
111
|
+
*/
|
|
112
|
+
modes.forEach((mode) => {
|
|
113
|
+
const section = document.createElement('div');
|
|
96
114
|
|
|
97
|
-
|
|
115
|
+
section.setAttribute('data-blok-testid', `${testIdPrefix}-section-${mode.key}`);
|
|
116
|
+
section.className = 'flex flex-col gap-1';
|
|
98
117
|
|
|
99
|
-
|
|
100
|
-
const tab = document.createElement('button');
|
|
118
|
+
const title = document.createElement('div');
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
tab.addEventListener('click', () => {
|
|
105
|
-
state.modeIndex = modeIndex;
|
|
106
|
-
updateTabs();
|
|
107
|
-
renderSwatches();
|
|
108
|
-
});
|
|
109
|
-
tabButtons.push(tab);
|
|
110
|
-
tabRow.appendChild(tab);
|
|
111
|
-
});
|
|
120
|
+
title.className = 'text-xs font-medium text-text-primary/60 px-0.5';
|
|
121
|
+
title.textContent = i18n.t(mode.labelKey);
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
const grid = document.createElement('div');
|
|
124
|
+
|
|
125
|
+
grid.className = 'grid gap-1';
|
|
126
|
+
grid.style.gridTemplateColumns = 'repeat(5, 2.25rem)';
|
|
127
|
+
sectionGrids.push(grid);
|
|
128
|
+
|
|
129
|
+
section.appendChild(title);
|
|
130
|
+
section.appendChild(grid);
|
|
131
|
+
wrapper.appendChild(section);
|
|
132
|
+
});
|
|
121
133
|
|
|
122
134
|
/**
|
|
123
|
-
*
|
|
135
|
+
* Render the swatches for one section.
|
|
124
136
|
*/
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
const renderSection = (modeIndex: number): void => {
|
|
138
|
+
const grid = sectionGrids[modeIndex];
|
|
139
|
+
const mode = modes[modeIndex];
|
|
140
|
+
const presets = getActivePresets();
|
|
129
141
|
|
|
130
|
-
const renderSwatches = (): void => {
|
|
131
142
|
grid.innerHTML = '';
|
|
132
143
|
|
|
133
|
-
const
|
|
144
|
+
const activeColorForSection = state.activeColors[mode.key];
|
|
145
|
+
|
|
146
|
+
// Default swatch (first position) — clears the active color for this section
|
|
147
|
+
const defaultSwatch = document.createElement('button');
|
|
148
|
+
const isDefaultActive = activeColorForSection === null;
|
|
149
|
+
|
|
150
|
+
defaultSwatch.setAttribute('data-blok-testid', `${testIdPrefix}-swatch-${mode.key}-default`);
|
|
151
|
+
defaultSwatch.className = twMerge(
|
|
152
|
+
'w-9 h-9 rounded-md cursor-pointer border-none outline-hidden',
|
|
153
|
+
'flex items-center justify-center text-sm font-semibold',
|
|
154
|
+
'transition-[box-shadow,transform] ring-inset hover:ring-2 hover:ring-swatch-ring-hover active:scale-90',
|
|
155
|
+
isDefaultActive && 'ring-2 ring-swatch-ring-hover'
|
|
156
|
+
);
|
|
157
|
+
defaultSwatch.textContent = mode.presetField === 'text' ? 'A' : '';
|
|
158
|
+
|
|
159
|
+
if (mode.presetField === 'text') {
|
|
160
|
+
defaultSwatch.style.color = 'var(--blok-text-primary)';
|
|
161
|
+
defaultSwatch.style.backgroundColor = SWATCH_NEUTRAL_BG;
|
|
162
|
+
} else {
|
|
163
|
+
defaultSwatch.style.backgroundColor = SWATCH_NEUTRAL_BG;
|
|
164
|
+
}
|
|
165
|
+
defaultSwatch.addEventListener('click', () => {
|
|
166
|
+
onColorSelect(null, mode.key);
|
|
167
|
+
});
|
|
168
|
+
onHover(defaultSwatch, `${i18n.t('tools.marker.default')} ${i18n.t(mode.labelKey).toLowerCase()}`, { placement: 'top' });
|
|
169
|
+
grid.appendChild(defaultSwatch);
|
|
134
170
|
|
|
135
|
-
for (const preset of
|
|
171
|
+
for (const preset of presets) {
|
|
136
172
|
const swatch = document.createElement('button');
|
|
137
|
-
const swatchColor =
|
|
138
|
-
const isActive =
|
|
173
|
+
const swatchColor = mode.presetField === 'text' ? preset.text : preset.bg;
|
|
174
|
+
const isActive = activeColorForSection !== null && colorsEqual(swatchColor, activeColorForSection);
|
|
139
175
|
|
|
140
|
-
swatch.setAttribute('data-blok-testid', `${testIdPrefix}-swatch-${preset.name}`);
|
|
176
|
+
swatch.setAttribute('data-blok-testid', `${testIdPrefix}-swatch-${mode.key}-${preset.name}`);
|
|
141
177
|
swatch.className = twMerge(
|
|
142
|
-
'w-
|
|
178
|
+
'w-9 h-9 rounded-md cursor-pointer border-none outline-hidden',
|
|
143
179
|
'flex items-center justify-center text-sm font-semibold',
|
|
144
|
-
'transition-shadow ring-inset hover:ring-2 hover:ring-
|
|
145
|
-
isActive && 'ring-2 ring-
|
|
180
|
+
'transition-[box-shadow,transform] ring-inset hover:ring-2 hover:ring-swatch-ring-hover active:scale-90',
|
|
181
|
+
isActive && 'ring-2 ring-swatch-ring-hover'
|
|
146
182
|
);
|
|
147
|
-
swatch.textContent = 'A';
|
|
183
|
+
swatch.textContent = mode.presetField === 'text' ? 'A' : '';
|
|
148
184
|
|
|
149
|
-
if (
|
|
185
|
+
if (mode.presetField === 'text') {
|
|
150
186
|
swatch.style.color = preset.text;
|
|
151
187
|
swatch.style.backgroundColor = SWATCH_NEUTRAL_BG;
|
|
152
188
|
} else {
|
|
153
|
-
swatch.style.color = '';
|
|
189
|
+
swatch.style.color = presets === COLOR_PRESETS_DARK ? preset.text : '#37352f';
|
|
154
190
|
swatch.style.backgroundColor = preset.bg;
|
|
155
191
|
}
|
|
156
192
|
|
|
157
193
|
swatch.addEventListener('click', () => {
|
|
158
|
-
onColorSelect(swatchColor,
|
|
194
|
+
onColorSelect(swatchColor, mode.key);
|
|
159
195
|
});
|
|
196
|
+
onHover(swatch, `${i18n.t('tools.colorPicker.color.' + preset.name)} ${i18n.t(mode.labelKey).toLowerCase()}`, { placement: 'top' });
|
|
160
197
|
grid.appendChild(swatch);
|
|
161
198
|
}
|
|
162
199
|
};
|
|
163
200
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const defaultBtn = document.createElement('button');
|
|
168
|
-
|
|
169
|
-
defaultBtn.setAttribute('data-blok-testid', `${testIdPrefix}-default-btn`);
|
|
170
|
-
defaultBtn.className = twMerge(
|
|
171
|
-
'w-full py-1.5 text-xs text-center rounded-md cursor-pointer',
|
|
172
|
-
'bg-transparent border-none outline-hidden hover:bg-item-hover-bg',
|
|
173
|
-
'mt-0.5 transition-colors'
|
|
174
|
-
);
|
|
175
|
-
defaultBtn.textContent = i18n.t('tools.marker.default');
|
|
176
|
-
defaultBtn.addEventListener('click', () => {
|
|
177
|
-
onColorSelect(null, modes[state.modeIndex].key);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Assemble
|
|
182
|
-
*/
|
|
183
|
-
updateTabs();
|
|
184
|
-
renderSwatches();
|
|
201
|
+
const renderAll = (): void => {
|
|
202
|
+
modes.forEach((_, i) => renderSection(i));
|
|
203
|
+
};
|
|
185
204
|
|
|
186
|
-
|
|
187
|
-
wrapper.appendChild(grid);
|
|
188
|
-
wrapper.appendChild(defaultBtn);
|
|
205
|
+
renderAll();
|
|
189
206
|
|
|
190
207
|
return {
|
|
191
208
|
element: wrapper,
|
|
192
209
|
setActiveColor: (color: string | null, modeKey: string) => {
|
|
193
|
-
state.activeColor = color;
|
|
194
|
-
|
|
195
210
|
const matchingIndex = modes.findIndex((m) => m.key === modeKey);
|
|
196
211
|
|
|
197
212
|
if (matchingIndex !== -1) {
|
|
198
|
-
state.
|
|
199
|
-
|
|
213
|
+
state.activeColors[modeKey] = color;
|
|
214
|
+
renderSection(matchingIndex);
|
|
200
215
|
}
|
|
201
|
-
|
|
202
|
-
renderSwatches();
|
|
203
216
|
},
|
|
204
217
|
reset: () => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
for (const mode of modes) {
|
|
219
|
+
state.activeColors[mode.key] = null;
|
|
220
|
+
}
|
|
221
|
+
renderAll();
|
|
209
222
|
},
|
|
210
223
|
};
|
|
211
224
|
}
|
|
@@ -8,7 +8,7 @@ export interface ColorPreset {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Ten Notion-style color presets.
|
|
11
|
+
* Ten Notion-style color presets for light mode.
|
|
12
12
|
* `text` is used for foreground (text-color mode), `bg` for background swatches.
|
|
13
13
|
*/
|
|
14
14
|
export const COLOR_PRESETS: ColorPreset[] = [
|
|
@@ -17,9 +17,37 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
|
|
17
17
|
{ name: 'orange', text: '#d9730d', bg: '#fbecdd' },
|
|
18
18
|
{ name: 'yellow', text: '#cb9b00', bg: '#fbf3db' },
|
|
19
19
|
{ name: 'green', text: '#448361', bg: '#edf3ec' },
|
|
20
|
-
{ name: 'teal', text: '#2b9a8f', bg: '#e4f5f3' },
|
|
21
20
|
{ name: 'blue', text: '#337ea9', bg: '#e7f3f8' },
|
|
22
21
|
{ name: 'purple', text: '#9065b0', bg: '#f6f3f9' },
|
|
23
22
|
{ name: 'pink', text: '#c14c8a', bg: '#f9f0f5' },
|
|
24
23
|
{ name: 'red', text: '#d44c47', bg: '#fdebec' },
|
|
25
24
|
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Dark-mode adapted presets. Text colors are lightened for readability on dark
|
|
28
|
+
* swatch backgrounds; background colors are deep/muted to integrate with dark UI.
|
|
29
|
+
* All pairs achieve at least 3.8:1 WCAG contrast. Backgrounds are equalized at
|
|
30
|
+
* ~L19% HSL with increased saturation so each hue family is clearly identifiable.
|
|
31
|
+
*/
|
|
32
|
+
export const COLOR_PRESETS_DARK: ColorPreset[] = [
|
|
33
|
+
{ name: 'gray', text: '#9b9b9b', bg: '#2f2f2f' },
|
|
34
|
+
{ name: 'brown', text: '#c59177', bg: '#452a1c' },
|
|
35
|
+
{ name: 'orange', text: '#dc8c47', bg: '#4d2f14' },
|
|
36
|
+
{ name: 'yellow', text: '#d4ab49', bg: '#544012' },
|
|
37
|
+
{ name: 'green', text: '#5db184', bg: '#1e432f' },
|
|
38
|
+
{ name: 'blue', text: '#5c9fcc', bg: '#123a54' },
|
|
39
|
+
{ name: 'purple', text: '#a67dca', bg: '#341d49' },
|
|
40
|
+
{ name: 'pink', text: '#d45e99', bg: '#4b1b33' },
|
|
41
|
+
{ name: 'red', text: '#dd5e5a', bg: '#4e1a18' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Construct a CSS custom property reference for a named preset color.
|
|
46
|
+
*
|
|
47
|
+
* @param name - The color preset name (e.g. 'red', 'blue')
|
|
48
|
+
* @param mode - 'text' for foreground, 'bg' for background
|
|
49
|
+
* @returns CSS var reference, e.g. `var(--blok-color-red-text)`
|
|
50
|
+
*/
|
|
51
|
+
export function colorVarName(name: string, mode: 'text' | 'bg'): string {
|
|
52
|
+
return `var(--blok-color-${name}-${mode})`;
|
|
53
|
+
}
|