@kubex/zinc 1.1.100 → 1.1.102

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.
@@ -1,6 +1,8 @@
1
1
  @use "../../wc";
2
2
 
3
3
  :host {
4
+ @include wc.scrollbars;
5
+
4
6
  --slash-menu-width: 320px;
5
7
  --slash-menu-max-height: 260px;
6
8
  --slash-menu-border-radius: 12px;
@@ -0,0 +1,100 @@
1
+ import {ResizeController} from '@lit-labs/observers/resize-controller.js';
2
+ import type {ReactiveController, ReactiveControllerHost} from 'lit';
3
+
4
+ interface ToolbarOverflowOptions {
5
+ /** The measurable group elements, in toolbar order. */
6
+ groups: () => HTMLElement[];
7
+ /** The element whose width the groups have to fit inside. */
8
+ container: () => HTMLElement | null | undefined;
9
+ /** Space to keep for the overflow trigger. Defaults to 44px. */
10
+ reserve?: number;
11
+ }
12
+
13
+ /**
14
+ * Reports how many leading toolbar groups fit the container, so the host can render the
15
+ * rest into an overflow menu. Two passes: the first ignores the trigger, because when
16
+ * everything fits there is no trigger to make room for.
17
+ */
18
+ export class ToolbarOverflowController implements ReactiveController {
19
+ visibleCount = Infinity;
20
+
21
+ private readonly host: ReactiveControllerHost & Element;
22
+ private readonly options: ToolbarOverflowOptions;
23
+ private resizeRafId = 0;
24
+
25
+ constructor(host: ReactiveControllerHost & Element, options: ToolbarOverflowOptions) {
26
+ this.host = host;
27
+ this.options = options;
28
+ host.addController(this);
29
+ // Deferred to a rAF rather than measuring inline: measure() mutates the display of
30
+ // elements inside the observed host, and doing that synchronously in the observer's own
31
+ // callback is what trips "ResizeObserver loop completed with undelivered notifications"
32
+ // (fatal in WebKit's test runner, not just a console warning). Same fix as button-menu.
33
+ // eslint-disable-next-line no-new -- ResizeController registers itself with the host.
34
+ new ResizeController(host, {
35
+ callback: () => {
36
+ if (this.resizeRafId) return;
37
+ this.resizeRafId = requestAnimationFrame(() => {
38
+ this.resizeRafId = 0;
39
+ this.measure();
40
+ });
41
+ },
42
+ });
43
+ }
44
+
45
+ hostUpdated() {
46
+ this.measure();
47
+ }
48
+
49
+ private measure() {
50
+ const groups = this.options.groups();
51
+ const container = this.options.container();
52
+ if (!groups.length || !container) return;
53
+
54
+ // clientWidth includes the container's own padding, so it has to come back out — and so
55
+ // does any non-group sibling (the raw-source toggle, or the trigger itself once it
56
+ // exists) that is really sitting in the same row and eating into the same space. Without
57
+ // this, `total <= available` can read true while a sibling's real footprint still clips
58
+ // trailing groups off-screen with no trigger rendered to reach them.
59
+ const style = getComputedStyle(container);
60
+ const paddingX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
61
+ const siblingsWidth = [...container.children]
62
+ .filter(child => !child.classList.contains('toolbar__group'))
63
+ .reduce((sum, child) => sum + child.getBoundingClientRect().width, 0);
64
+ const available = container.clientWidth - paddingX - siblingsWidth;
65
+ if (available <= 0) return;
66
+
67
+ // Show everything before measuring: a collapsed group measures 0, which would let the
68
+ // next pass re-expand it and oscillate forever. Same reason the editor toolbar resets
69
+ // display before each pass. Reads and writes stay in this frame, so nothing flickers.
70
+ groups.forEach(group => (group.style.display = ''));
71
+ const widths = groups.map(group => group.getBoundingClientRect().width);
72
+ const total = widths.reduce((sum, width) => sum + width, 0);
73
+
74
+ const next = total <= available
75
+ ? groups.length
76
+ : this.countThatFit(widths, available - (this.options.reserve ?? 44));
77
+
78
+ groups.forEach((group, index) => (group.style.display = index < next ? '' : 'none'));
79
+
80
+ if (next !== this.visibleCount) {
81
+ this.visibleCount = next;
82
+ this.host.requestUpdate();
83
+ }
84
+ }
85
+
86
+ private countThatFit(widths: number[], available: number): number {
87
+ let used = 0;
88
+ let count = 0;
89
+ for (const width of widths) {
90
+ if (used + width > available) break;
91
+ used += width;
92
+ count++;
93
+ }
94
+ // No `Math.max(count, 1)` floor here: forcing at least one group into the bar when even
95
+ // that group alone doesn't fit `available` pushed the trigger past the host's own
96
+ // `overflow: hidden` edge — clipped and unreachable, not just visually tight. Zero visible
97
+ // groups is the correct answer in that case; every action still reaches the menu.
98
+ return count;
99
+ }
100
+ }