@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.
Files changed (67) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-CdxHhr5i.mjs → blok-BmlbETK7.mjs} +2119 -2013
  3. package/dist/chunks/{constants-C_H9o9Ao.mjs → constants-WhLyFkza.mjs} +260 -223
  4. package/dist/chunks/{i18next-loader-D5HxE5ZQ.mjs → i18next-loader-CZARkla1.mjs} +1 -1
  5. package/dist/chunks/{lightweight-i18n-Safdy0ua.mjs → lightweight-i18n-BQa0F2X6.mjs} +9 -0
  6. package/dist/chunks/{tools-B0YXCZFW.mjs → tools-BCb5bMO3.mjs} +973 -843
  7. package/dist/full.mjs +3 -3
  8. package/dist/locales.mjs +9 -0
  9. package/dist/react.mjs +2 -2
  10. package/dist/tools.mjs +2 -2
  11. package/package.json +2 -2
  12. package/src/components/block/style-manager.ts +1 -1
  13. package/src/components/blocks.ts +26 -54
  14. package/src/components/constants/data-attributes.ts +0 -2
  15. package/src/components/i18n/locales/en/messages.json +9 -0
  16. package/src/components/icons/index.ts +34 -6
  17. package/src/components/inline-tools/inline-tool-link.ts +202 -5
  18. package/src/components/inline-tools/inline-tool-marker.ts +166 -23
  19. package/src/components/inline-tools/utils/formatting-range-utils.ts +10 -1
  20. package/src/components/modules/blockManager/blockManager.ts +2 -2
  21. package/src/components/modules/blockManager/operations.ts +2 -2
  22. package/src/components/modules/blockManager/repository.ts +1 -9
  23. package/src/components/modules/blockManager/types.ts +1 -1
  24. package/src/components/modules/drag/operations/DragOperations.ts +45 -6
  25. package/src/components/modules/paste/google-docs-preprocessor.ts +69 -2
  26. package/src/components/modules/paste/handlers/blok-data-handler.ts +96 -19
  27. package/src/components/modules/renderer.ts +2 -0
  28. package/src/components/modules/toolbar/blockSettings.ts +1 -1
  29. package/src/components/modules/toolbar/index.ts +21 -0
  30. package/src/components/modules/toolbar/plus-button.ts +15 -5
  31. package/src/components/selection/fake-background/index.ts +9 -10
  32. package/src/components/shared/color-picker.ts +108 -95
  33. package/src/components/shared/color-presets.ts +30 -2
  34. package/src/components/ui/toolbox.ts +36 -7
  35. package/src/components/utils/color-mapping.ts +43 -1
  36. package/src/components/utils/color-migration.ts +37 -0
  37. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +4 -3
  38. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +5 -39
  39. package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts +2 -2
  40. package/src/components/utils/popover/components/popover-item/popover-item.ts +11 -0
  41. package/src/components/utils/popover/components/search-input/search-input.const.ts +2 -3
  42. package/src/components/utils/popover/components/search-input/search-input.ts +1 -32
  43. package/src/components/utils/popover/popover-abstract.ts +2 -4
  44. package/src/components/utils/popover/popover-desktop.ts +1 -16
  45. package/src/components/utils/popover/popover-inline.ts +1 -2
  46. package/src/components/utils/popover/popover-mobile.ts +2 -2
  47. package/src/components/utils/popover/popover.const.ts +1 -1
  48. package/src/stories/Table.stories.ts +15 -9
  49. package/src/styles/main.css +312 -14
  50. package/src/tools/header/index.ts +5 -5
  51. package/src/tools/list/constants.ts +11 -4
  52. package/src/tools/list/depth-validator.ts +13 -1
  53. package/src/tools/list/dom-builder.ts +5 -3
  54. package/src/tools/list/index.ts +3 -2
  55. package/src/tools/paragraph/index.ts +2 -2
  56. package/src/tools/table/table-cell-color-picker.ts +1 -1
  57. package/src/tools/table/table-cell-selection.ts +1 -2
  58. package/src/tools/table/table-core.ts +2 -2
  59. package/src/tools/table/table-grip-visuals.ts +13 -5
  60. package/src/tools/table/table-heading-toggle.ts +15 -9
  61. package/src/tools/table/table-row-col-controls.ts +17 -11
  62. package/src/tools/table/table-row-col-drag.ts +26 -3
  63. package/src/tools/toggle/constants.ts +5 -5
  64. package/src/tools/toggle/index.ts +1 -1
  65. package/types/tools/hook-events.d.ts +6 -0
  66. package/types/utils/popover/popover-item.d.ts +6 -0
  67. package/CHANGELOG.md +0 -119
@@ -167,6 +167,13 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
167
167
  */
168
168
  private isInsideTableCell = false;
169
169
 
170
+ /**
171
+ * Whether the toolbox was opened in slash-search mode (via "/" key or existing slash paragraph).
172
+ * When false (opened via plus button), the input filter uses the full block text as the query
173
+ * instead of requiring a leading "/" and does not close on missing slash.
174
+ */
175
+ private openedWithSlash = true;
176
+
170
177
  /**
171
178
  * Toolbox constructor
172
179
  * @param options - available parameters
@@ -280,8 +287,10 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
280
287
 
281
288
  /**
282
289
  * Open Toolbox with Tools
290
+ * @param withSlash - When true (default), inline search requires "/" and closes on its removal.
291
+ * When false (plus button), the full block text is used as the filter query.
283
292
  */
284
- public open(): void {
293
+ public open(withSlash = true): void {
285
294
  if (this.isEmpty) {
286
295
  return;
287
296
  }
@@ -318,8 +327,20 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
318
327
  const caretRect = SelectionUtils.rect;
319
328
 
320
329
  this.popover.updatePosition(caretRect);
330
+ } else if (!withSlash && this.popover instanceof PopoverDesktop) {
331
+ /**
332
+ * When opened without slash (via plus button), the trigger element (plus button)
333
+ * is at the top of the block. Position the popover below the block's bottom edge
334
+ * instead, so it doesn't overlap the block's placeholder text.
335
+ */
336
+ const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
337
+
338
+ if (currentBlock) {
339
+ this.popover.updatePosition(currentBlock.holder.getBoundingClientRect());
340
+ }
321
341
  }
322
342
 
343
+ this.openedWithSlash = withSlash;
323
344
  this.opened = true;
324
345
  this.emit(ToolboxEvent.Opened);
325
346
  this.startListeningToBlockInput();
@@ -725,7 +746,12 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
725
746
 
726
747
  /**
727
748
  * Handles input events on the block to filter the toolbox.
728
- * Extracts text after "/" and applies it as a filter query.
749
+ *
750
+ * In slash mode (default): extracts text after "/" and filters by it.
751
+ * Closes if "/" is removed.
752
+ *
753
+ * In no-slash mode (opened via plus button): uses full block text as the
754
+ * filter query and does not close on missing "/".
729
755
  */
730
756
  private handleBlockInput = (): void => {
731
757
  if (this.currentContentEditable === null) {
@@ -733,15 +759,18 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
733
759
  }
734
760
 
735
761
  const text = this.currentContentEditable.textContent || '';
736
- const slashIndex = text.lastIndexOf('/');
737
762
 
738
- if (slashIndex === -1) {
739
- this.close();
763
+ if (this.openedWithSlash) {
764
+ const slashIndex = text.lastIndexOf('/');
740
765
 
741
- return;
766
+ if (slashIndex === -1) {
767
+ this.close();
768
+
769
+ return;
770
+ }
742
771
  }
743
772
 
744
- const query = text.slice(slashIndex + 1);
773
+ const query = this.openedWithSlash ? text.slice(text.lastIndexOf('/') + 1) : text;
745
774
 
746
775
  if (this.currentContentEditable instanceof HTMLElement) {
747
776
  this.currentContentEditable.setAttribute(
@@ -1,4 +1,4 @@
1
- import { COLOR_PRESETS } from '../shared/color-presets';
1
+ import { COLOR_PRESETS, COLOR_PRESETS_DARK } from '../shared/color-presets';
2
2
 
3
3
  /**
4
4
  * Convert an HSL color (H in degrees, S and L as 0-100 percentages) to an RGB tuple.
@@ -239,3 +239,45 @@ export function mapToNearestPresetColor(cssColor: string, mode: 'text' | 'bg'):
239
239
 
240
240
  return best.color;
241
241
  }
242
+
243
+ /**
244
+ * Map an arbitrary CSS color to the name of the nearest Blok preset color.
245
+ *
246
+ * Searches both light and dark presets to correctly recover the semantic name
247
+ * from hex values that originated in either theme. First match wins on ties.
248
+ *
249
+ * @param cssColor - CSS color string (hex or rgb)
250
+ * @param mode - 'text' for text color presets, 'bg' for background presets
251
+ * @returns the nearest preset name (e.g. 'red'), or null if unparseable
252
+ */
253
+ export function mapToNearestPresetName(cssColor: string, mode: 'text' | 'bg'): string | null {
254
+ const rgb = parseColor(cssColor);
255
+
256
+ if (rgb === null) {
257
+ return null;
258
+ }
259
+
260
+ const hsl = rgbToHsl(rgb);
261
+ const allPresets = [...COLOR_PRESETS, ...COLOR_PRESETS_DARK];
262
+
263
+ const best = allPresets.reduce<{ name: string; distance: number } | null>(
264
+ (acc, preset) => {
265
+ const presetRgb = parseColor(preset[mode]);
266
+
267
+ if (presetRgb === null) {
268
+ return acc;
269
+ }
270
+
271
+ const distance = hslDistance(hsl, rgbToHsl(presetRgb));
272
+
273
+ if (acc === null || distance < acc.distance) {
274
+ return { name: preset.name, distance };
275
+ }
276
+
277
+ return acc;
278
+ },
279
+ null
280
+ );
281
+
282
+ return best?.name ?? null;
283
+ }
@@ -0,0 +1,37 @@
1
+ import { colorVarName } from '../shared/color-presets';
2
+ import { mapToNearestPresetName } from './color-mapping';
3
+
4
+ const PROPS = ['color', 'background-color'] as const;
5
+ type Prop = typeof PROPS[number];
6
+
7
+ const PROP_MODE: Record<Prop, 'text' | 'bg'> = {
8
+ 'color': 'text',
9
+ 'background-color': 'bg',
10
+ };
11
+
12
+ /**
13
+ * Scan all <mark> elements inside container and replace raw hex color/background-color
14
+ * inline style values with their corresponding CSS custom property references.
15
+ *
16
+ * Safe to call multiple times — already-migrated var() values and 'transparent' are
17
+ * left unchanged.
18
+ *
19
+ * @param container - Root element to search within (e.g. the editor redactor node)
20
+ */
21
+ export function migrateMarkColors(container: Element): void {
22
+ container.querySelectorAll<HTMLElement>('mark').forEach((el) => {
23
+ for (const prop of PROPS) {
24
+ const value = el.style.getPropertyValue(prop);
25
+
26
+ if (!value || value === 'transparent' || value.startsWith('var(')) {
27
+ continue;
28
+ }
29
+
30
+ const name = mapToNearestPresetName(value, PROP_MODE[prop]);
31
+
32
+ if (name !== null) {
33
+ el.style.setProperty(prop, colorVarName(name, PROP_MODE[prop]));
34
+ }
35
+ }
36
+ });
37
+ }
@@ -10,7 +10,7 @@ export const css = {
10
10
  * Note: noHover state is handled via [data-blok-popover-item-no-hover] which disables hover
11
11
  * Priority order: active < hover < focus (focus wins when navigating with keyboard)
12
12
  */
13
- item: 'flex items-center select-none border-none bg-transparent rounded-lg px-2 py-1 text-text-primary mb-px outline-hidden transition-[color,background-color,border-color,opacity,max-height,padding,margin] duration-150 max-h-9 overflow-hidden data-blok-popover-item-active:bg-icon-active-bg data-blok-popover-item-active:text-icon-active-text can-hover:hover:cursor-pointer can-hover:hover:bg-item-hover-bg data-blok-force-hover:cursor-pointer data-blok-force-hover:bg-item-hover-bg data-[blok-focused="true"]:bg-item-focus-bg data-blok-popover-item-no-hover:hover:bg-transparent data-blok-popover-item-no-hover:cursor-default can-hover:data-blok-popover-item-destructive:hover:text-item-destructive-text can-hover:data-blok-popover-item-destructive:hover:bg-item-destructive-hover-bg [&[data-blok-popover-item-destructive][data-blok-force-hover]]:text-item-destructive-text [&[data-blok-popover-item-destructive][data-blok-force-hover]]:bg-item-destructive-hover-bg [&[data-blok-popover-item-destructive][data-blok-focused="true"]]:text-item-destructive-text [&[data-blok-popover-item-destructive][data-blok-focused="true"]]:bg-item-destructive-hover-bg',
13
+ item: 'flex items-center select-none border-none bg-transparent rounded-lg px-2 py-1 text-text-primary mb-px outline-hidden max-h-9 overflow-hidden data-blok-popover-item-active:bg-icon-active-bg data-blok-popover-item-active:text-icon-active-text can-hover:hover:cursor-pointer can-hover:hover:bg-item-hover-bg data-blok-force-hover:cursor-pointer data-blok-force-hover:bg-item-hover-bg data-[blok-focused="true"]:bg-item-focus-bg data-blok-popover-item-no-hover:hover:bg-transparent data-blok-popover-item-no-hover:cursor-default can-hover:data-blok-popover-item-destructive:hover:text-item-destructive-text can-hover:data-blok-popover-item-destructive:hover:bg-item-destructive-hover-bg [&[data-blok-popover-item-destructive][data-blok-force-hover]]:text-item-destructive-text [&[data-blok-popover-item-destructive][data-blok-force-hover]]:bg-item-destructive-hover-bg [&[data-blok-popover-item-destructive][data-blok-focused="true"]]:text-item-destructive-text [&[data-blok-popover-item-destructive][data-blok-focused="true"]]:bg-item-destructive-hover-bg',
14
14
 
15
15
  /**
16
16
  * Item disabled state
@@ -21,7 +21,7 @@ export const css = {
21
21
  /**
22
22
  * Icon container styles
23
23
  */
24
- icon: 'flex items-center justify-center w-7 h-7 shrink-0 rounded-lg bg-popover-icon-bg transition-colors duration-150 [&_svg]:w-icon [&_svg]:h-icon',
24
+ icon: 'flex items-center justify-center w-7 h-7 shrink-0 rounded-lg bg-popover-icon-bg [&_svg]:w-icon [&_svg]:h-icon',
25
25
 
26
26
  /**
27
27
  * Focused state class for DomIterator/Flipper keyboard navigation.
@@ -37,7 +37,8 @@ export const cssInline = {
37
37
  /**
38
38
  * Item in inline context - more compact styling
39
39
  */
40
- item: 'rounded p-1',
40
+ item: 'rounded-lg p-1',
41
+ itemIconOnly: 'aspect-square justify-center',
41
42
  };
42
43
 
43
44
  /**
@@ -305,7 +305,7 @@ export class PopoverItemDefault extends PopoverItem {
305
305
  private createIconElement(icon: string, iconWithGap: boolean, isInline: boolean, isNestedInline: boolean): HTMLElement {
306
306
  const iconEl = document.createElement('div');
307
307
 
308
- iconEl.className = this.getIconClass(iconWithGap, isInline, isNestedInline, false);
308
+ iconEl.className = this.getIconClass(iconWithGap, isInline, isNestedInline);
309
309
  iconEl.setAttribute(DATA_ATTR.popoverItemIcon, '');
310
310
  iconEl.setAttribute('data-blok-testid', 'popover-item-icon');
311
311
  iconEl.innerHTML = icon;
@@ -328,6 +328,7 @@ export class PopoverItemDefault extends PopoverItem {
328
328
  css.item,
329
329
  !isInline && !isNestedInline && 'pl-2 pr-3',
330
330
  isInline && cssInline.item,
331
+ isInline && this.params.icon && cssInline.itemIconOnly,
331
332
  isNestedInline && cssNestedInline.item,
332
333
  this.params.isDisabled && css.itemDisabled
333
334
  );
@@ -336,15 +337,14 @@ export class PopoverItemDefault extends PopoverItem {
336
337
  /**
337
338
  * Gets the icon class based on context
338
339
  */
339
- private getIconClass(iconWithGap: boolean, isInline: boolean, isNestedInline: boolean, isWobbling: boolean): string {
340
+ private getIconClass(iconWithGap: boolean, isInline: boolean, isNestedInline: boolean): string {
340
341
  return twMerge(
341
342
  css.icon,
342
343
  isInline && 'w-auto h-auto bg-transparent [&_svg]:w-icon [&_svg]:h-icon mobile:[&_svg]:w-icon-mobile mobile:[&_svg]:h-icon-mobile',
343
344
  isNestedInline && 'w-toolbox-btn h-toolbox-btn',
344
345
  iconWithGap && 'mr-3',
345
346
  iconWithGap && isInline && 'shadow-none mr-0!',
346
- iconWithGap && isNestedInline && 'mr-2!',
347
- isWobbling && 'animate-wobble'
347
+ iconWithGap && isNestedInline && 'mr-2!'
348
348
  );
349
349
  }
350
350
 
@@ -639,47 +639,13 @@ export class PopoverItemDefault extends PopoverItem {
639
639
  item.onActivate?.(item);
640
640
  this.disableConfirmationMode();
641
641
  } catch {
642
- this.animateError();
642
+ // onActivate threw an error
643
643
  }
644
644
  } else {
645
645
  this.enableConfirmationMode(item.confirmation);
646
646
  }
647
647
  }
648
648
 
649
- /**
650
- * Animates item which symbolizes that error occurred while executing 'onActivate()' callback
651
- */
652
- private animateError(): void {
653
- this.triggerWobble();
654
- }
655
-
656
- /**
657
- * Triggers wobble animation on the icon
658
- */
659
- private triggerWobble(): void {
660
- if (!this.nodes.icon) {
661
- return;
662
- }
663
-
664
- const isInline = this.renderParams?.isInline ?? false;
665
- const isNestedInline = this.renderParams?.isNestedInline ?? false;
666
- const iconWithGap = this.renderParams?.iconWithGap ?? true;
667
-
668
- // Add wobble class
669
- this.nodes.icon.setAttribute(DATA_ATTR.popoverItemWobble, 'true');
670
- this.nodes.icon.className = this.getIconClass(iconWithGap, isInline, isNestedInline, true);
671
-
672
- // Remove wobble after animation ends
673
- const handleAnimationEnd = (): void => {
674
- if (this.nodes.icon) {
675
- this.nodes.icon.removeAttribute(DATA_ATTR.popoverItemWobble);
676
- this.nodes.icon.className = this.getIconClass(iconWithGap, isInline, isNestedInline, false);
677
- }
678
- };
679
-
680
- this.nodes.icon.addEventListener('animationend', handleAnimationEnd, { once: true });
681
- }
682
-
683
649
  /**
684
650
  * Gets reference to the icon element
685
651
  */
@@ -2,7 +2,7 @@
2
2
  * Tailwind CSS classes for popover separator component
3
3
  */
4
4
  export const css = {
5
- container: 'py-1.5 px-2 transition-[opacity,max-height,padding] duration-150 max-h-5 overflow-hidden',
5
+ container: 'py-1.5 px-2 max-h-5 overflow-hidden',
6
6
  containerHidden: 'opacity-0 max-h-0! py-0!',
7
7
  line: 'h-px w-full bg-popover-border/60',
8
8
  };
@@ -12,7 +12,7 @@ export const css = {
12
12
  */
13
13
  export const cssInline = {
14
14
  // Inline context: horizontal separator
15
- container: 'px-1 py-0',
15
+ container: 'px-1 py-0 max-h-none',
16
16
  line: 'h-full w-px',
17
17
  // Nested inline context: back to vertical separator
18
18
  nestedContainer: 'py-1 px-[3px]',
@@ -170,6 +170,17 @@ export abstract class PopoverItem {
170
170
  return this.params.children?.width;
171
171
  }
172
172
 
173
+ /**
174
+ * Returns the min-width for children popover, if specified
175
+ */
176
+ public get childrenMinWidth(): string | undefined {
177
+ if (this.params === undefined || !('children' in this.params)) {
178
+ return undefined;
179
+ }
180
+
181
+ return this.params.children?.minWidth;
182
+ }
183
+
173
184
  /**
174
185
  * True if popover should close once item is activated
175
186
  */
@@ -2,8 +2,7 @@
2
2
  * CSS class names to be used in popover search input class
3
3
  */
4
4
  export const css = {
5
- wrapper: 'bg-search-input-bg border border-search-input-border rounded-lg p-1 grid grid-cols-[auto_auto_1fr] grid-rows-[auto] transition-all duration-200 focus-within:bg-popover-bg focus-within:border-search-input-focus-border focus-within:shadow-[0_0_0_2px_rgba(35,131,226,0.08)]',
5
+ wrapper: 'bg-search-input-bg border border-search-input-border rounded-lg p-1 grid grid-cols-[auto_1fr] grid-rows-[auto] focus-within:border-search-input-focus-border focus-within:shadow-[0_0_0_2px_rgba(35,131,226,0.15)]',
6
6
  icon: 'w-toolbox-btn h-toolbox-btn flex items-center justify-center mr-2 [&_svg]:w-icon [&_svg]:h-icon [&_svg]:text-gray-text',
7
- input: "text-sm outline-hidden font-medium font-inherit border-0 bg-transparent m-0 p-0 leading-[22px] min-w-[calc(100%-(--spacing(6))-10px)] placeholder:text-gray-text/60 placeholder:font-normal",
8
- clearButton: 'flex items-center justify-center w-toolbox-btn h-toolbox-btn cursor-pointer border-0 bg-transparent rounded p-0 opacity-0 pointer-events-none transition-opacity duration-150 [&_svg]:w-3 [&_svg]:h-3 [&_svg]:text-gray-text can-hover:hover:[&_svg]:text-dark',
7
+ input: "text-sm outline-hidden font-medium font-inherit border-0 bg-transparent m-0 p-0 leading-[22px] min-w-[calc(100%-(--spacing(6))-10px)] text-text-primary placeholder:text-gray-text/80 placeholder:font-normal",
9
8
  };
@@ -1,5 +1,5 @@
1
1
  import { Dom } from '../../../../dom';
2
- import { IconCross, IconSearch } from '../../../../icons';
2
+ import { IconSearch } from '../../../../icons';
3
3
  import { EventsDispatcher } from '../../../events';
4
4
  import { Listeners } from '../../../listeners';
5
5
 
@@ -23,11 +23,6 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
23
23
  */
24
24
  private input: HTMLInputElement;
25
25
 
26
- /**
27
- * Clear button element
28
- */
29
- private clearButton: HTMLButtonElement;
30
-
31
26
  /**
32
27
  * The instance of the Listeners util
33
28
  */
@@ -78,15 +73,8 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
78
73
  this.input.setAttribute('data-blok-flipper-navigation-target', 'true');
79
74
  this.input.setAttribute('data-blok-testid', 'popover-search-input');
80
75
 
81
- this.clearButton = Dom.make('button', css.clearButton, {
82
- type: 'button',
83
- innerHTML: IconCross,
84
- }) as HTMLButtonElement;
85
- this.clearButton.setAttribute('aria-label', 'Clear search');
86
-
87
76
  this.wrapper.appendChild(iconWrapper);
88
77
  this.wrapper.appendChild(this.input);
89
- this.wrapper.appendChild(this.clearButton);
90
78
 
91
79
  this.overrideValueProperty();
92
80
 
@@ -95,11 +83,6 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
95
83
  eventsToHandle.forEach((eventName) => {
96
84
  this.listeners.on(this.input, eventName, this.handleValueChange);
97
85
  });
98
-
99
- this.listeners.on(this.clearButton, 'click', () => {
100
- this.clear();
101
- this.input.focus();
102
- });
103
86
  }
104
87
 
105
88
  /**
@@ -121,14 +104,12 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
121
104
  */
122
105
  public clear(): void {
123
106
  this.input.value = '';
124
- this.updateClearButtonVisibility();
125
107
  }
126
108
 
127
109
  /**
128
110
  * Handles value changes for the input element
129
111
  */
130
112
  private handleValueChange = (): void => {
131
- this.updateClearButtonVisibility();
132
113
  this.applySearch(this.input.value);
133
114
  };
134
115
 
@@ -178,18 +159,6 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
178
159
  });
179
160
  }
180
161
 
181
- /**
182
- * Shows or hides the clear button based on whether the input has a value
183
- */
184
- private updateClearButtonVisibility(): void {
185
- const visible = this.input.value.length > 0;
186
-
187
- this.clearButton.classList.toggle('opacity-0', !visible);
188
- this.clearButton.classList.toggle('pointer-events-none', !visible);
189
- this.clearButton.classList.toggle('opacity-100', visible);
190
- this.clearButton.classList.toggle('pointer-events-auto', visible);
191
- }
192
-
193
162
  /**
194
163
  * Clears memory
195
164
  */
@@ -391,11 +391,9 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
391
391
  protected toggleNothingFoundMessage(isDisplayed: boolean): void {
392
392
  if (isDisplayed) {
393
393
  this.nodes.nothingFoundMessage.classList.remove('hidden');
394
- this.nodes.nothingFoundMessage.classList.add('animate-[fade-in_150ms_ease_forwards]');
395
394
  this.nodes.nothingFoundMessage.setAttribute(DATA_ATTR.nothingFoundDisplayed, 'true');
396
395
  } else {
397
396
  this.nodes.nothingFoundMessage.classList.add('hidden');
398
- this.nodes.nothingFoundMessage.classList.remove('animate-[fade-in_150ms_ease_forwards]');
399
397
  this.nodes.nothingFoundMessage.removeAttribute(DATA_ATTR.nothingFoundDisplayed);
400
398
  }
401
399
  }
@@ -461,14 +459,14 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
461
459
  // Create popover container
462
460
  const popoverContainer = document.createElement('div');
463
461
  popoverContainer.className = css.popoverContainer;
464
- popoverContainer.style.boxShadow = '0 0 0 1px var(--blok-popover-border), 0 4px 16px rgba(0, 0, 0, 0.1), 0 16px 40px -8px rgba(0, 0, 0, 0.08)';
462
+ popoverContainer.style.boxShadow = 'var(--blok-popover-box-shadow)';
465
463
  popoverContainer.setAttribute(DATA_ATTR.popoverContainer, '');
466
464
  popoverContainer.setAttribute('data-blok-testid', 'popover-container');
467
465
 
468
466
  // Create nothing found message
469
467
  const nothingFoundMessage = document.createElement('div');
470
468
  nothingFoundMessage.className = twMerge(
471
- 'cursor-default text-[13px] leading-5 font-normal whitespace-nowrap overflow-hidden text-ellipsis text-gray-text/70 px-3 py-4 text-center',
469
+ 'cursor-default text-[13px] leading-5 font-normal whitespace-nowrap overflow-hidden text-ellipsis text-gray-text px-3 py-4 text-center',
472
470
  'hidden'
473
471
  );
474
472
  nothingFoundMessage.setAttribute('data-blok-testid', 'popover-nothing-found');
@@ -562,6 +562,7 @@ export class PopoverDesktop extends PopoverAbstract {
562
562
  messages: this.messages,
563
563
  onNavigateBack: this.destroyNestedPopoverIfExists.bind(this),
564
564
  width: item.childrenWidth,
565
+ minWidth: item.childrenMinWidth,
565
566
  handleContentEditableNavigation: handleContentEditable,
566
567
  autoFocusFirstItem: this.params.autoFocusFirstItem,
567
568
  });
@@ -1035,15 +1036,6 @@ export class PopoverDesktop extends PopoverAbstract {
1035
1036
 
1036
1037
  const isNothingFound = matchingTopLevel.length === 0 && data.promotedItems.length === 0;
1037
1038
 
1038
- /**
1039
- * When nothing is found, disable transitions so items hide instantly.
1040
- */
1041
- if (isNothingFound) {
1042
- this.items.forEach(item => {
1043
- item.getElement()?.style.setProperty('transition-duration', '0s');
1044
- });
1045
- }
1046
-
1047
1039
  this.items
1048
1040
  .forEach((item) => {
1049
1041
  const isDefaultItem = item instanceof PopoverItemDefault;
@@ -1055,13 +1047,6 @@ export class PopoverDesktop extends PopoverAbstract {
1055
1047
  item.toggleHidden(isHidden);
1056
1048
  });
1057
1049
 
1058
- if (isNothingFound) {
1059
- this.nodes.popoverContainer.offsetHeight;
1060
- this.items.forEach(item => {
1061
- item.getElement()?.style.removeProperty('transition-duration');
1062
- });
1063
- }
1064
-
1065
1050
  // Reorder top-level DOM elements to reflect ranking
1066
1051
  if (!isEmptyQuery && matchingTopLevel.length > 0) {
1067
1052
  this.reorderItemsByRank(matchingTopLevel);
@@ -204,8 +204,7 @@ export class PopoverInline extends PopoverDesktop {
204
204
  this.nodes.popoverContainer.className = twMerge(
205
205
  css.popoverContainer,
206
206
  css.popoverContainerOpened,
207
- cssInline.popoverContainer,
208
- 'animate-none'
207
+ cssInline.popoverContainer
209
208
  );
210
209
 
211
210
  // Set height based on screen
@@ -90,7 +90,7 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
90
90
  */
91
91
  public show(): void {
92
92
  this.nodes.overlay.removeAttribute(DATA_ATTR.overlayHidden);
93
- this.nodes.overlay.className = twMerge(css.popoverOverlay, 'fixed inset-0 block visible z-3 opacity-50 transition-opacity duration-120 ease-in will-change-[opacity]');
93
+ this.nodes.overlay.className = twMerge(css.popoverOverlay, 'fixed inset-0 block visible z-3 opacity-50');
94
94
 
95
95
  super.show();
96
96
 
@@ -102,7 +102,7 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
102
102
  css.popoverContainer,
103
103
  css.popoverContainerMobile,
104
104
  css.popoverContainerOpened,
105
- 'max-h-none z-4 animate-[panelShowingMobile_250ms_ease]'
105
+ 'max-h-none z-4'
106
106
  );
107
107
 
108
108
  this.scrollLocker.lock();
@@ -12,7 +12,7 @@ export const css = {
12
12
  popoverContainerMobile: 'fixed max-w-none rounded-[10px] min-w-[calc(100%-var(--offset)*2)] left-auto top-auto inset-[auto_var(--offset)_calc(var(--offset)+env(safe-area-inset-bottom))_var(--offset)]',
13
13
 
14
14
  // Popover container - opened state
15
- popoverContainerOpened: 'opacity-100 pointer-events-auto p-1.5 max-h-(--max-height) border-none animate-panel-showing',
15
+ popoverContainerOpened: 'opacity-100 pointer-events-auto p-1.5 max-h-(--max-height) border-none',
16
16
 
17
17
  // Popover overlay
18
18
  popoverOverlay: 'hidden bg-dark',
@@ -7,6 +7,8 @@ import { createEditorContainer, defaultTools, simulateClick, waitForToolbar } fr
7
7
  import type { EditorFactoryOptions } from './helpers';
8
8
 
9
9
  import type { OutputData, ToolSettings } from '@/types';
10
+ import { GRIP_ACTIVE_CLASSES, GRIP_IDLE_CLASSES, GRIP_VISIBLE_CLASSES } from '../tools/table/table-row-col-controls';
11
+ import { GRIP_HOVER_SIZE } from '../tools/table/table-grip-visuals';
10
12
 
11
13
  // ── Constants ────────────────────────────────────────────────────────
12
14
 
@@ -77,20 +79,24 @@ const waitForTable = async (canvas: HTMLElement): Promise<void> => {
77
79
  const forceGripsVisible = (grips: NodeListOf<Element>): void => {
78
80
  grips.forEach((grip) => {
79
81
  grip.setAttribute('data-blok-table-grip-visible', '');
80
- grip.classList.remove('opacity-0', 'pointer-events-none');
81
- grip.classList.add('opacity-100', 'pointer-events-auto');
82
+ GRIP_IDLE_CLASSES.forEach(cls => grip.classList.remove(cls));
83
+ GRIP_VISIBLE_CLASSES.forEach(cls => grip.classList.add(cls));
82
84
  });
83
85
  };
84
86
 
85
- const forceGripActive = (grip: Element, expanded: { width: string; height: string }): void => {
87
+ const forceGripActive = (grip: Element): void => {
86
88
  grip.setAttribute('data-blok-table-grip-visible', '');
87
- grip.classList.remove('opacity-0', 'pointer-events-none', 'bg-gray-300');
88
- grip.classList.add('opacity-100', 'pointer-events-auto', 'bg-blue-500', 'text-white');
89
+ GRIP_IDLE_CLASSES.forEach(cls => grip.classList.remove(cls));
90
+ GRIP_ACTIVE_CLASSES.forEach(cls => grip.classList.add(cls));
89
91
 
90
92
  const el = grip as HTMLElement;
93
+ const isCol = grip.hasAttribute('data-blok-table-grip-col');
91
94
 
92
- el.style.width = expanded.width;
93
- el.style.height = expanded.height;
95
+ if (isCol) {
96
+ el.style.height = `${GRIP_HOVER_SIZE}px`;
97
+ } else {
98
+ el.style.width = `${GRIP_HOVER_SIZE}px`;
99
+ }
94
100
 
95
101
  const svg = grip.querySelector('svg');
96
102
 
@@ -701,7 +707,7 @@ export const ColumnGripActive: Story = {
701
707
  const grip = canvasElement.querySelector(GRIP_COL_SELECTOR);
702
708
 
703
709
  if (grip) {
704
- forceGripActive(grip, { width: '24px', height: '16px' });
710
+ forceGripActive(grip);
705
711
  }
706
712
 
707
713
  await waitFor(
@@ -749,7 +755,7 @@ export const RowGripActive: Story = {
749
755
  const grip = canvasElement.querySelector(GRIP_ROW_SELECTOR);
750
756
 
751
757
  if (grip) {
752
- forceGripActive(grip, { width: '16px', height: '20px' });
758
+ forceGripActive(grip);
753
759
  }
754
760
 
755
761
  await waitFor(