@beehexa/vue-components 0.0.0-stage → 2611.5.1

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/README.md CHANGED
@@ -1,3 +1,151 @@
1
- # Temporary Holding Version
1
+ # @beehexa/vue-components
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Source-owned Vue 3 components from beehexa.design, independent of Ant Design Vue or
4
+ any other UI framework. `ResizablePanel` is the first component. The private
5
+ `0.0.0` source package is for local development; staging does not authorize a
6
+ release or substitution of an invented registry version.
7
+
8
+ ## Install and use
9
+
10
+ Use the actual local workspace/package reference until an authorized version is
11
+ published. Vue 3.5 or newer is a peer dependency. The only package dependency is
12
+ `@beehexa/design-tokens` at the matching local `0.0.0` version.
13
+
14
+ ```vue
15
+ <script setup lang="ts">
16
+ import { ref } from 'vue';
17
+ import { ResizablePanel, type ResizablePanelResizeEvent } from '@beehexa/vue-components';
18
+ import '@beehexa/vue-components/css';
19
+
20
+ const ratio = ref(0.6);
21
+ function onResize(event: ResizablePanelResizeEvent) {
22
+ // DOM layout is complete: ask an editor/chart/canvas to measure its retained host.
23
+ // event.first and event.second contain actual rendered width and height.
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <div style="height: 32rem; min-height: 0; min-width: 0">
29
+ <ResizablePanel v-model:ratio="ratio" separator-label="Resize source and preview" @resize="onResize">
30
+ <template #first><SourceEditor /></template>
31
+ <template #second><PreviewCanvas /></template>
32
+ </ResizablePanel>
33
+ </div>
34
+ </template>
35
+ ```
36
+
37
+ The CSS entry has an export declaration for strict TypeScript side-effect imports
38
+ and imports the source design-token CSS. Import it once. No app-owned AntD adapter,
39
+ SFC compiler, global registration or global body style is required. Runtime imports
40
+ are safe during SSR; observation, pointer capture and layout reads begin on mount.
41
+ IDs use Vue's SSR-safe `useId` rather than a module-global instance counter.
42
+
43
+ ## Public exports
44
+
45
+ - `ResizablePanel`: Vue component, slots `first` and `second`.
46
+ - `ResizablePanelDirection = 'horizontal' | 'vertical'`.
47
+ - `ResizablePanelResizeEvent`: the event contract below.
48
+ - `@beehexa/vue-components/css`: typed side-effect CSS entry.
49
+
50
+ ### Props
51
+
52
+ | Prop | Type | Default | Behavior |
53
+ | --- | --- | --- | --- |
54
+ | `direction` | `ResizablePanelDirection` | `'horizontal'` | Horizontal is left/right panes with a vertical separator. Vertical is top/bottom panes with a horizontal separator. |
55
+ | `ratio` | `number` | `0.5` | Requested first-pane fraction, supports `v-model:ratio`. Pointer and keyboard changes emit a bounded ratio. |
56
+ | `min` | `number` | `100` | Minimum CSS pixels for **each** pane on the split axis. Use `0` for real endpoint collapse. |
57
+ | `max` | `number` | `Infinity` | First-pane maximum in CSS pixels. |
58
+ | `minRatio` | `number` | `0` | Lower first-pane fraction, constrained to `[0, 1]`. |
59
+ | `maxRatio` | `number` | `1` | Upper first-pane fraction, constrained to `[0, 1]`. |
60
+ | `secondVisible` | `boolean` | `true` | Hides the second pane without unmounting either slot. First fills the host; separator is hidden. Restores the requested ratio when shown. |
61
+ | `resizerSize` | `string` | `'90%'` | Reactive CSS length/percentage for the visible divider **line**, centered on the cross axis and capped at host length. The centered handle remains visible; the hit target spans the cross axis. |
62
+ | `separatorLabel` | `string` | `'Resize panels'` | Accessible name. Consumer supplies the translated/contextual label. |
63
+
64
+ Both panes stay mounted when one reaches zero or the second is hidden. Child
65
+ state, Monaco models, diagram zoom and fullscreen state are not reset by ratio,
66
+ direction or visibility changes. Pane wrappers clip ordinary overflowing content;
67
+ the physical gutter keeps the complete handle clear of their scroll targets.
68
+ At rest root and panes do not create stacking contexts: nested fixed overlays share
69
+ their actual overlay owner's stack above the outer divider. Root isolation and
70
+ pane stacking apply only during a pointer gesture, beneath its local iframe shield.
71
+ The component adds no heading, chrome, app breakpoint or global overlay layer.
72
+ The consumer computes `direction` from its existing breakpoint policy and provides
73
+ a bounded host (`min-width: 0`, `min-height: 0` in surrounding flex/grid layouts).
74
+
75
+ Pane wrappers are shrinkable column flex containers. Fill-layout Cards, editors
76
+ and canvases can consume the available pane height without relying on an
77
+ intrinsic content height or a consumer patch to the wrapper.
78
+
79
+ Pixel minima yield symmetrically when a host is too small to fit both, never
80
+ forcing overflow. Explicit ratio/first-pane maximums take precedence over
81
+ contradictory minima. A temporarily small host clamps **effective layout**, not the
82
+ stored requested ratio; growing the host restores it. External model updates also
83
+ request a ratio subject to current bounds. `resize.ratio` reports that effective
84
+ split fraction (retained while the second pane is hidden); pane dimensions report
85
+ the actual visible layout. A fixed interval (`minRatio === maxRatio`, including
86
+ `0` or `1` with `min=0`) hides/disables the separator and cancels an active gesture.
87
+
88
+ ### Events
89
+
90
+ `update:ratio` supplies the new numeric first-pane fraction for user resizing.
91
+ `resize-start`, `resize` and `resize-end` carry:
92
+
93
+ ```ts
94
+ interface ResizablePanelResizeEvent {
95
+ ratio: number;
96
+ direction: 'horizontal' | 'vertical';
97
+ first: { width: number; height: number };
98
+ second: { width: number; height: number };
99
+ source: 'pointer' | 'keyboard' | 'container' | 'programmatic';
100
+ }
101
+ ```
102
+
103
+ Dimensions are actual `getBoundingClientRect()` sizes, read after Vue's DOM update
104
+ and the next animation frame. A hidden second pane reports zero dimensions.
105
+ Movement notifications coalesce within a frame; lifecycle ordering is retained.
106
+ Parents should use `resize` to update editor/chart/canvas host layout, not change a
107
+ child key, replace its model, reset zoom or toggle it out of the render tree.
108
+ A parent's normal `v-model` echo retains the original pointer/keyboard source.
109
+
110
+ - Pointer down starts; movement resizes; up/cancel/lost capture/window blur ends.
111
+ - Keyboard changes start on the first supported keydown; repeats resize; keyup or
112
+ focus loss ends. Pointer cancellation keeps the last valid ratio.
113
+ - ResizeObserver changes emit `resize` with `source: 'container'` after initial
114
+ mount and whenever the host's dimensions change.
115
+ - Model, constraints, visibility and direction changes emit `resize` with
116
+ `source: 'programmatic'`; they do not fabricate an interactive start/end.
117
+ - Active gestures end before a direction/visibility/constraint change is reported.
118
+ Listeners, capture, observers and pending animation work are removed on unmount.
119
+
120
+ ### Keyboard and accessibility
121
+
122
+ The focusable separator has a localized name, `role="separator"`, the actual
123
+ separator's `aria-orientation`, `aria-controls` referencing the first pane, and
124
+ percentage `aria-valuemin`, `aria-valuemax`, `aria-valuenow`. Hidden or fixed-bound
125
+ separators are neither visible nor keyboard reachable.
126
+
127
+ - Left/Right change a horizontal split; Up/Down change a vertical split.
128
+ - Arrow changes are 1 percentage point; Shift+Arrow changes 10.
129
+ - Home/End move to live lower/upper bounds.
130
+ - Enter or separator double-click resets to the **initial** ratio, within live
131
+ bounds, with a complete keyboard/pointer resize lifecycle. Programmatic model,
132
+ direction and container changes do not redefine this reset point.
133
+ - Pointer/touch uses a source-token target, expanded for coarse pointers. Pointer
134
+ capture, a pane-local drag shield and gesture-scoped selection prevention avoid
135
+ iframe/editor interception or accidental text selection without mutating body
136
+ styles or arbitrary framework controls.
137
+
138
+ ### Theme and presentation
139
+
140
+ Resize geometry, colors, grip, active background and focus use the `resize.*` and
141
+ border/spacing source tokens. The default line is centered at 90% cross-axis
142
+ length (5% inset at either end), with a handle centered at 50%. Both orientations
143
+ keep that handle outside the panes' overflow and above positioned pane children;
144
+ only local stacking levels are used. Native rem token values respect reader text
145
+ scaling. Source `data-theme="dark"` changes apply live; no component theme or
146
+ locale singleton is introduced. Geometry does not animate; reduced-motion and
147
+ forced-colors behavior retain usable focus/contrast.
148
+
149
+ Complete active usage, code, examples, accessibility and changelog rules live at
150
+ `src/content/components/resizable-panel/` in beehexa.design and are distributed by
151
+ `@beehexa/design-rules` under stable `en/components/resizable-panel/*` IDs.
@@ -0,0 +1,122 @@
1
+ import { type PropType } from 'vue';
2
+ export type ResizablePanelDirection = 'horizontal' | 'vertical';
3
+ export interface ResizablePanelResizeEvent {
4
+ ratio: number;
5
+ direction: ResizablePanelDirection;
6
+ first: {
7
+ width: number;
8
+ height: number;
9
+ };
10
+ second: {
11
+ width: number;
12
+ height: number;
13
+ };
14
+ source: 'pointer' | 'keyboard' | 'container' | 'programmatic';
15
+ }
16
+ /** A bounded two-pane layout. Children stay mounted when a pane is hidden. */
17
+ export declare const ResizablePanel: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
18
+ direction: {
19
+ type: PropType<ResizablePanelDirection>;
20
+ default: string;
21
+ };
22
+ ratio: {
23
+ type: NumberConstructor;
24
+ default: number;
25
+ };
26
+ min: {
27
+ type: NumberConstructor;
28
+ default: number;
29
+ };
30
+ max: {
31
+ type: NumberConstructor;
32
+ default: number;
33
+ };
34
+ preserveMinimum: {
35
+ type: BooleanConstructor;
36
+ default: boolean;
37
+ };
38
+ minRatio: {
39
+ type: NumberConstructor;
40
+ default: number;
41
+ };
42
+ maxRatio: {
43
+ type: NumberConstructor;
44
+ default: number;
45
+ };
46
+ secondVisible: {
47
+ type: BooleanConstructor;
48
+ default: boolean;
49
+ };
50
+ resizerSize: {
51
+ type: StringConstructor;
52
+ default: string;
53
+ };
54
+ separatorLabel: {
55
+ type: StringConstructor;
56
+ default: string;
57
+ };
58
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
59
+ [key: string]: any;
60
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
61
+ 'update:ratio': (ratio: number) => boolean;
62
+ 'resize-start': (_event: ResizablePanelResizeEvent) => true;
63
+ resize: (_event: ResizablePanelResizeEvent) => true;
64
+ 'resize-end': (_event: ResizablePanelResizeEvent) => true;
65
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
66
+ direction: {
67
+ type: PropType<ResizablePanelDirection>;
68
+ default: string;
69
+ };
70
+ ratio: {
71
+ type: NumberConstructor;
72
+ default: number;
73
+ };
74
+ min: {
75
+ type: NumberConstructor;
76
+ default: number;
77
+ };
78
+ max: {
79
+ type: NumberConstructor;
80
+ default: number;
81
+ };
82
+ preserveMinimum: {
83
+ type: BooleanConstructor;
84
+ default: boolean;
85
+ };
86
+ minRatio: {
87
+ type: NumberConstructor;
88
+ default: number;
89
+ };
90
+ maxRatio: {
91
+ type: NumberConstructor;
92
+ default: number;
93
+ };
94
+ secondVisible: {
95
+ type: BooleanConstructor;
96
+ default: boolean;
97
+ };
98
+ resizerSize: {
99
+ type: StringConstructor;
100
+ default: string;
101
+ };
102
+ separatorLabel: {
103
+ type: StringConstructor;
104
+ default: string;
105
+ };
106
+ }>> & Readonly<{
107
+ "onResize-start"?: ((_event: ResizablePanelResizeEvent) => any) | undefined;
108
+ onResize?: ((_event: ResizablePanelResizeEvent) => any) | undefined;
109
+ "onResize-end"?: ((_event: ResizablePanelResizeEvent) => any) | undefined;
110
+ "onUpdate:ratio"?: ((ratio: number) => any) | undefined;
111
+ }>, {
112
+ direction: ResizablePanelDirection;
113
+ ratio: number;
114
+ min: number;
115
+ max: number;
116
+ preserveMinimum: boolean;
117
+ minRatio: number;
118
+ maxRatio: number;
119
+ secondVisible: boolean;
120
+ resizerSize: string;
121
+ separatorLabel: string;
122
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,359 @@
1
+ import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, useId, watch, } from 'vue';
2
+ import { clampRatio, panelBounds } from './geometry.js';
3
+ /** A bounded two-pane layout. Children stay mounted when a pane is hidden. */
4
+ export const ResizablePanel = defineComponent({
5
+ name: 'ResizablePanel',
6
+ props: {
7
+ direction: {
8
+ type: String,
9
+ default: 'horizontal',
10
+ },
11
+ ratio: { type: Number, default: 0.5 },
12
+ min: { type: Number, default: 100 },
13
+ max: { type: Number, default: Number.POSITIVE_INFINITY },
14
+ preserveMinimum: { type: Boolean, default: false },
15
+ minRatio: { type: Number, default: 0 },
16
+ maxRatio: { type: Number, default: 1 },
17
+ secondVisible: { type: Boolean, default: true },
18
+ resizerSize: { type: String, default: '90%' },
19
+ separatorLabel: { type: String, default: 'Resize panels' },
20
+ },
21
+ emits: {
22
+ 'update:ratio': (ratio) => Number.isFinite(ratio),
23
+ 'resize-start': (_event) => true,
24
+ resize: (_event) => true,
25
+ 'resize-end': (_event) => true,
26
+ },
27
+ setup(props, { slots, emit }) {
28
+ const id = useId();
29
+ const firstId = `bh-panel-${id}-first`;
30
+ const secondId = `bh-panel-${id}-second`;
31
+ const root = ref();
32
+ const first = ref();
33
+ const second = ref();
34
+ const separator = ref();
35
+ const width = ref(0);
36
+ const height = ref(0);
37
+ const gutter = ref(0);
38
+ // Requested ratio is retained while hidden or temporarily constrained by a small host.
39
+ const initialRatio = clampRatio(props.ratio);
40
+ const requestedRatio = ref(initialRatio);
41
+ const dragging = ref(false);
42
+ const horizontal = computed(() => props.direction === 'horizontal');
43
+ const bounds = computed(() => panelBounds(horizontal.value ? width.value : height.value, gutter.value, props.min, props.max, props.minRatio, props.maxRatio));
44
+ const actualRatio = computed(() => clampRatio(requestedRatio.value, bounds.value.min, bounds.value.max));
45
+ const gutterHidden = computed(() => !props.secondVisible ||
46
+ (bounds.value.min === bounds.value.max && (bounds.value.min === 0 || bounds.value.min === 1)));
47
+ const disabled = computed(() => !props.secondVisible || bounds.value.min === bounds.value.max ||
48
+ (horizontal.value ? width.value : height.value) <= gutter.value);
49
+ const minimumAllocation = computed(() => {
50
+ if (!props.preserveMinimum)
51
+ return undefined;
52
+ const minimum = Math.max(0, Number.isFinite(props.min) ? props.min : 100);
53
+ return gutterHidden.value
54
+ ? `${minimum}px`
55
+ : `calc(${minimum * 2}px + var(--bh-panel-hit-target))`;
56
+ });
57
+ let mounted = false;
58
+ let disposed = false;
59
+ let frame;
60
+ let observer;
61
+ let ownerWindow;
62
+ let activePointer;
63
+ let dragOffset = 0;
64
+ let dragDocument;
65
+ let keyboardActive = false;
66
+ let pending = [];
67
+ function queueResize(source, phase = 'resize') {
68
+ if (!mounted || disposed)
69
+ return;
70
+ // Keep lifecycle order while coalescing repeated moves/observer notifications.
71
+ const previous = pending[pending.length - 1];
72
+ if (phase === 'resize' && previous?.phase === 'resize') {
73
+ if (source !== 'container')
74
+ previous.source = source;
75
+ }
76
+ else {
77
+ pending.push({ phase, source });
78
+ }
79
+ if (frame !== undefined)
80
+ return;
81
+ void nextTick(() => {
82
+ if (disposed || frame !== undefined || !ownerWindow)
83
+ return;
84
+ frame = ownerWindow.requestAnimationFrame(() => {
85
+ frame = undefined;
86
+ if (!first.value || !second.value || disposed)
87
+ return;
88
+ const firstRect = first.value.getBoundingClientRect();
89
+ const secondRect = second.value.getBoundingClientRect();
90
+ const measured = {
91
+ ratio: actualRatio.value,
92
+ direction: props.direction,
93
+ first: { width: firstRect.width, height: firstRect.height },
94
+ second: { width: secondRect.width, height: secondRect.height },
95
+ };
96
+ const events = pending;
97
+ pending = [];
98
+ for (const event of events) {
99
+ const payload = { ...measured, source: event.source };
100
+ switch (event.phase) {
101
+ case 'resize-start':
102
+ emit('resize-start', payload);
103
+ break;
104
+ case 'resize':
105
+ emit('resize', payload);
106
+ break;
107
+ case 'resize-end':
108
+ emit('resize-end', payload);
109
+ break;
110
+ }
111
+ }
112
+ });
113
+ });
114
+ }
115
+ function measureContainer() {
116
+ const element = root.value;
117
+ if (!element)
118
+ return;
119
+ const nextWidth = element.clientWidth;
120
+ const nextHeight = element.clientHeight;
121
+ const nextGutter = separator.value
122
+ ? horizontal.value ? separator.value.offsetWidth : separator.value.offsetHeight
123
+ : 0;
124
+ if (width.value !== nextWidth || height.value !== nextHeight || gutter.value !== nextGutter) {
125
+ width.value = nextWidth;
126
+ height.value = nextHeight;
127
+ gutter.value = nextGutter;
128
+ queueResize('container');
129
+ }
130
+ }
131
+ function setRatio(value, source) {
132
+ const next = clampRatio(value, bounds.value.min, bounds.value.max);
133
+ if (next !== requestedRatio.value) {
134
+ requestedRatio.value = next;
135
+ emit('update:ratio', next);
136
+ queueResize(source);
137
+ }
138
+ }
139
+ function preventSelection(event) {
140
+ event.preventDefault();
141
+ }
142
+ function finishPointer() {
143
+ if (activePointer === undefined)
144
+ return;
145
+ const pointer = activePointer;
146
+ activePointer = undefined;
147
+ dragging.value = false;
148
+ dragDocument?.removeEventListener('pointermove', movePointer, true);
149
+ dragDocument?.removeEventListener('pointerup', endPointer, true);
150
+ dragDocument?.removeEventListener('pointercancel', endPointer, true);
151
+ dragDocument?.removeEventListener('selectstart', preventSelection, true);
152
+ dragDocument = undefined;
153
+ ownerWindow?.removeEventListener('blur', finishPointer);
154
+ if (separator.value?.hasPointerCapture(pointer))
155
+ separator.value.releasePointerCapture(pointer);
156
+ queueResize('pointer', 'resize-end');
157
+ }
158
+ function ratioAtPointer(event) {
159
+ if (!root.value)
160
+ return undefined;
161
+ const rect = root.value.getBoundingClientRect();
162
+ const size = horizontal.value ? rect.width : rect.height;
163
+ const layoutSize = horizontal.value ? width.value : height.value;
164
+ const visibleGutter = layoutSize > 0 ? gutter.value * size / layoutSize : 0;
165
+ const available = size - visibleGutter;
166
+ if (available <= 0)
167
+ return undefined;
168
+ const position = horizontal.value ? event.clientX - rect.left : event.clientY - rect.top;
169
+ return (position - visibleGutter / 2) / available;
170
+ }
171
+ function movePointer(event) {
172
+ if (event.pointerId !== activePointer)
173
+ return;
174
+ const ratio = ratioAtPointer(event);
175
+ if (ratio === undefined)
176
+ return;
177
+ event.preventDefault();
178
+ setRatio(ratio - dragOffset, 'pointer');
179
+ }
180
+ function endPointer(event) {
181
+ if (event.pointerId !== activePointer)
182
+ return;
183
+ if (event.type === 'pointerup')
184
+ movePointer(event);
185
+ finishPointer();
186
+ }
187
+ function beginPointer(event) {
188
+ if (disabled.value || activePointer !== undefined || event.button !== 0 || !root.value)
189
+ return;
190
+ event.preventDefault();
191
+ finishKeyboard();
192
+ measureContainer();
193
+ const ratio = ratioAtPointer(event);
194
+ if (ratio === undefined || disabled.value)
195
+ return;
196
+ dragOffset = ratio - actualRatio.value;
197
+ activePointer = event.pointerId;
198
+ dragging.value = true;
199
+ separator.value?.focus({ preventScroll: true });
200
+ separator.value?.setPointerCapture(event.pointerId);
201
+ dragDocument = root.value.ownerDocument;
202
+ dragDocument.addEventListener('pointermove', movePointer, { capture: true, passive: false });
203
+ dragDocument.addEventListener('pointerup', endPointer, true);
204
+ dragDocument.addEventListener('pointercancel', endPointer, true);
205
+ dragDocument.addEventListener('selectstart', preventSelection, true);
206
+ ownerWindow?.addEventListener('blur', finishPointer);
207
+ queueResize('pointer', 'resize-start');
208
+ }
209
+ function finishKeyboard() {
210
+ if (!keyboardActive)
211
+ return;
212
+ keyboardActive = false;
213
+ queueResize('keyboard', 'resize-end');
214
+ }
215
+ function resetRatio(source) {
216
+ if (disabled.value || activePointer !== undefined)
217
+ return;
218
+ finishKeyboard();
219
+ queueResize(source, 'resize-start');
220
+ setRatio(initialRatio, source);
221
+ queueResize(source, 'resize-end');
222
+ }
223
+ function keydown(event) {
224
+ if (disabled.value || activePointer !== undefined || event.altKey || event.ctrlKey || event.metaKey)
225
+ return;
226
+ if (event.key === 'Enter') {
227
+ event.preventDefault();
228
+ if (!event.repeat)
229
+ resetRatio('keyboard');
230
+ return;
231
+ }
232
+ const decrease = horizontal.value ? 'ArrowLeft' : 'ArrowUp';
233
+ const increase = horizontal.value ? 'ArrowRight' : 'ArrowDown';
234
+ if (![decrease, increase, 'Home', 'End'].includes(event.key))
235
+ return;
236
+ event.preventDefault();
237
+ if (!keyboardActive) {
238
+ keyboardActive = true;
239
+ queueResize('keyboard', 'resize-start');
240
+ }
241
+ const step = event.shiftKey ? 0.1 : 0.01;
242
+ const value = event.key === 'Home' ? bounds.value.min
243
+ : event.key === 'End' ? bounds.value.max
244
+ : actualRatio.value + (event.key === increase ? step : -step);
245
+ setRatio(value, 'keyboard');
246
+ }
247
+ function keyup(event) {
248
+ if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key))
249
+ finishKeyboard();
250
+ }
251
+ watch(() => props.ratio, value => {
252
+ const next = clampRatio(value);
253
+ // A parent's v-model echo must not relabel a pointer/keyboard resize.
254
+ if (next === requestedRatio.value)
255
+ return;
256
+ requestedRatio.value = next;
257
+ queueResize('programmatic');
258
+ });
259
+ watch(disabled, value => {
260
+ if (value) {
261
+ finishPointer();
262
+ finishKeyboard();
263
+ }
264
+ });
265
+ watch(() => [props.direction, props.min, props.max, props.minRatio, props.maxRatio, props.secondVisible, props.preserveMinimum], () => {
266
+ finishPointer();
267
+ finishKeyboard();
268
+ queueResize('programmatic');
269
+ });
270
+ onMounted(() => {
271
+ mounted = true;
272
+ ownerWindow = root.value?.ownerDocument.defaultView ?? undefined;
273
+ measureContainer();
274
+ const Observer = typeof ResizeObserver === 'undefined' ? undefined : ResizeObserver;
275
+ if (Observer && root.value) {
276
+ observer = new Observer(measureContainer);
277
+ observer.observe(root.value);
278
+ if (separator.value)
279
+ observer.observe(separator.value);
280
+ }
281
+ else {
282
+ ownerWindow?.addEventListener('resize', measureContainer);
283
+ }
284
+ queueResize('container');
285
+ });
286
+ onBeforeUnmount(() => {
287
+ disposed = true;
288
+ finishPointer();
289
+ finishKeyboard();
290
+ observer?.disconnect();
291
+ ownerWindow?.removeEventListener('resize', measureContainer);
292
+ if (frame !== undefined)
293
+ ownerWindow?.cancelAnimationFrame(frame);
294
+ pending = [];
295
+ });
296
+ return () => h('div', {
297
+ ref: root,
298
+ class: ['bh-resizable-panel', `bh-resizable-panel--${props.direction}`],
299
+ 'data-resizing': dragging.value ? 'true' : undefined,
300
+ style: {
301
+ '--bh-panel-resizer-size': props.resizerSize,
302
+ '--bh-panel-gutter': gutterHidden.value ? '0px' : undefined,
303
+ minWidth: horizontal.value ? minimumAllocation.value : undefined,
304
+ minHeight: horizontal.value ? undefined : minimumAllocation.value,
305
+ },
306
+ }, [
307
+ h('div', {
308
+ ref: first,
309
+ id: firstId,
310
+ class: 'bh-resizable-panel__pane bh-resizable-panel__pane--first',
311
+ inert: props.secondVisible && actualRatio.value === 0,
312
+ 'aria-hidden': props.secondVisible && actualRatio.value === 0 ? 'true' : undefined,
313
+ style: {
314
+ flexBasis: props.secondVisible
315
+ ? `calc((100% - var(--bh-panel-gutter)) * ${actualRatio.value})`
316
+ : '100%',
317
+ },
318
+ }, slots.first?.()),
319
+ h('div', {
320
+ ref: separator,
321
+ class: 'bh-resizable-panel__separator nokey',
322
+ role: 'separator',
323
+ tabindex: disabled.value ? -1 : 0,
324
+ 'aria-label': props.separatorLabel,
325
+ 'aria-orientation': horizontal.value ? 'vertical' : 'horizontal',
326
+ 'aria-controls': firstId,
327
+ 'aria-valuemin': Math.round(bounds.value.min * 100),
328
+ 'aria-valuemax': Math.round(bounds.value.max * 100),
329
+ 'aria-valuenow': Math.round(actualRatio.value * 100),
330
+ 'aria-disabled': disabled.value ? 'true' : undefined,
331
+ style: { display: gutterHidden.value ? 'none' : undefined },
332
+ onPointerdown: beginPointer,
333
+ onDblclick: (event) => {
334
+ if (disabled.value)
335
+ return;
336
+ event.preventDefault();
337
+ event.stopPropagation();
338
+ resetRatio('pointer');
339
+ },
340
+ onLostpointercapture: finishPointer,
341
+ onKeydown: keydown,
342
+ onKeyup: keyup,
343
+ onBlur: finishKeyboard,
344
+ }, [
345
+ h('span', { class: 'bh-resizable-panel__line', 'aria-hidden': 'true' }),
346
+ h('span', { class: 'bh-resizable-panel__handle', 'aria-hidden': 'true' }),
347
+ ]),
348
+ h('div', {
349
+ ref: second,
350
+ id: secondId,
351
+ class: 'bh-resizable-panel__pane bh-resizable-panel__pane--second',
352
+ style: { display: props.secondVisible ? undefined : 'none' },
353
+ inert: !props.secondVisible || actualRatio.value === 1,
354
+ 'aria-hidden': !props.secondVisible || actualRatio.value === 1 ? 'true' : undefined,
355
+ }, slots.second?.()),
356
+ dragging.value ? h('div', { class: 'bh-resizable-panel__drag-shield', 'aria-hidden': 'true' }) : null,
357
+ ]);
358
+ },
359
+ });
package/dist/css.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // CSS is imported for side effects only.
2
+ export {};
@@ -0,0 +1,9 @@
1
+ export interface PanelBounds {
2
+ min: number;
3
+ max: number;
4
+ }
5
+ export declare function clampRatio(value: number, min?: number, max?: number): number;
6
+ /** Pixel constraints apply to pane space after reserving the divider gutter.
7
+ * Minima yield symmetrically in undersized hosts; explicit maximums win conflicts.
8
+ */
9
+ export declare function panelBounds(size: number, gutter: number, min: number, max: number, minRatio: number, maxRatio: number): PanelBounds;
@@ -0,0 +1,18 @@
1
+ export function clampRatio(value, min = 0, max = 1) {
2
+ return Math.min(max, Math.max(min, Number.isFinite(value) ? value : 0.5));
3
+ }
4
+ /** Pixel constraints apply to pane space after reserving the divider gutter.
5
+ * Minima yield symmetrically in undersized hosts; explicit maximums win conflicts.
6
+ */
7
+ export function panelBounds(size, gutter, min, max, minRatio, maxRatio) {
8
+ const available = Math.max(0, size - gutter);
9
+ const ratioMax = clampRatio(maxRatio);
10
+ const ratioMin = Math.min(ratioMax, clampRatio(minRatio));
11
+ if (available <= 0)
12
+ return { min: ratioMin, max: ratioMax };
13
+ const minimum = Math.min(Math.max(0, Number.isFinite(min) ? min : 100), available / 2) / available;
14
+ const maximum = Number.isFinite(max) ? Math.max(0, max) / available : 1;
15
+ const upper = Math.min(ratioMax, maximum, 1);
16
+ const lower = Math.min(upper, Math.max(ratioMin, minimum));
17
+ return { min: lower, max: Math.max(lower, Math.min(upper, 1 - minimum)) };
18
+ }
@@ -0,0 +1,2 @@
1
+ export { ResizablePanel } from './ResizablePanel.js';
2
+ export type { ResizablePanelDirection, ResizablePanelResizeEvent } from './ResizablePanel.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { ResizablePanel } from './ResizablePanel.js';
@@ -0,0 +1,272 @@
1
+ @import '@beehexa/design-tokens/css';
2
+
3
+ .bh-resizable-panel {
4
+ --bh-panel-hit-target: var(--bh-resize-target);
5
+ --bh-panel-gutter: min(100%, var(--bh-panel-hit-target));
6
+ position: relative;
7
+ display: flex;
8
+ flex-direction: row;
9
+ align-items: stretch;
10
+ box-sizing: border-box;
11
+ width: 100%;
12
+ height: 100%;
13
+ min-width: 0;
14
+ min-height: 0;
15
+ max-width: 100%;
16
+ max-height: 100%;
17
+ gap: 0;
18
+ overflow: visible;
19
+ }
20
+
21
+ .bh-resizable-panel--vertical {
22
+ flex-direction: column;
23
+ }
24
+
25
+ .bh-resizable-panel__pane {
26
+ display: flex;
27
+ flex-direction: column;
28
+ position: relative;
29
+ box-sizing: border-box;
30
+ min-width: 0;
31
+ min-height: 0;
32
+ overflow: hidden;
33
+ }
34
+
35
+ .bh-resizable-panel__pane--first {
36
+ flex: 0 0 auto;
37
+ }
38
+
39
+ .bh-resizable-panel__pane--second {
40
+ flex: 1 1 0%;
41
+ }
42
+
43
+ .bh-resizable-panel__separator {
44
+ position: relative;
45
+ flex: 0 0 auto;
46
+ min-width: 0;
47
+ min-height: 0;
48
+ z-index: 1;
49
+ box-sizing: border-box;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ padding: 0;
54
+ border: 0;
55
+ outline: none;
56
+ overflow: visible;
57
+ touch-action: none;
58
+ user-select: none;
59
+ -webkit-user-select: none;
60
+ color: var(--bh-resize-grip);
61
+ }
62
+
63
+ .bh-resizable-panel--horizontal > .bh-resizable-panel__separator {
64
+ width: var(--bh-panel-gutter);
65
+ cursor: col-resize;
66
+ }
67
+
68
+ .bh-resizable-panel--vertical > .bh-resizable-panel__separator {
69
+ height: var(--bh-panel-gutter);
70
+ cursor: row-resize;
71
+ }
72
+
73
+ .bh-resizable-panel__separator[aria-disabled='true'] {
74
+ cursor: default;
75
+ }
76
+
77
+ .bh-resizable-panel__line,
78
+ .bh-resizable-panel__handle {
79
+ position: absolute;
80
+ top: 50%;
81
+ left: 50%;
82
+ box-sizing: border-box;
83
+ transform: translate(-50%, -50%);
84
+ pointer-events: none;
85
+ }
86
+
87
+ .bh-resizable-panel__line {
88
+ background: var(--bh-resize-color);
89
+ }
90
+
91
+ .bh-resizable-panel--horizontal > .bh-resizable-panel__separator > .bh-resizable-panel__line {
92
+ width: var(--bh-resize-divider);
93
+ height: clamp(0px, var(--bh-panel-resizer-size, 90%), 100%);
94
+ }
95
+
96
+ .bh-resizable-panel--vertical > .bh-resizable-panel__separator > .bh-resizable-panel__line {
97
+ width: clamp(0px, var(--bh-panel-resizer-size, 90%), 100%);
98
+ height: var(--bh-resize-divider);
99
+ }
100
+
101
+ .bh-resizable-panel__handle {
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ width: var(--bh-resize-handle-width);
106
+ height: var(--bh-resize-handle-height);
107
+ border: var(--bh-border-width-default) solid var(--bh-resize-color);
108
+ border-radius: var(--bh-resize-handle-radius);
109
+ background: var(--bh-resize-handle-background);
110
+ }
111
+
112
+ .bh-resizable-panel__handle::after {
113
+ content: '';
114
+ width: var(--bh-resize-dot-size);
115
+ height: var(--bh-resize-dot-size);
116
+ border-radius: var(--bh-radius-full);
117
+ background: currentColor;
118
+ box-shadow:
119
+ 0 calc(-1 * var(--bh-resize-dot-pitch)) 0 currentColor,
120
+ 0 var(--bh-resize-dot-pitch) 0 currentColor;
121
+ }
122
+
123
+ .bh-resizable-panel--vertical > .bh-resizable-panel__separator > .bh-resizable-panel__handle {
124
+ width: var(--bh-resize-handle-height);
125
+ height: var(--bh-resize-handle-width);
126
+ }
127
+
128
+ .bh-resizable-panel--vertical > .bh-resizable-panel__separator > .bh-resizable-panel__handle::after {
129
+ transform: rotate(90deg);
130
+ }
131
+
132
+ .bh-resizable-panel__separator:not([aria-disabled='true']):is(:hover, :focus-visible) > .bh-resizable-panel__line,
133
+ .bh-resizable-panel[data-resizing='true'] > .bh-resizable-panel__separator > .bh-resizable-panel__line {
134
+ background: var(--bh-resize-active);
135
+ }
136
+
137
+ .bh-resizable-panel__separator:not([aria-disabled='true']):is(:hover, :focus-visible) > .bh-resizable-panel__handle,
138
+ .bh-resizable-panel[data-resizing='true'] > .bh-resizable-panel__separator > .bh-resizable-panel__handle {
139
+ color: var(--bh-resize-activeGrip);
140
+ background: var(--bh-resize-handle-activeBackground);
141
+ border-color: var(--bh-resize-active);
142
+ }
143
+
144
+ .bh-resizable-panel__separator:focus-visible > .bh-resizable-panel__handle {
145
+ outline: var(--bh-border-width-focused) solid var(--bh-resize-active);
146
+ outline-offset: var(--bh-border-width-focused);
147
+ }
148
+
149
+ /* The physical gutter keeps the complete handle off pane content. At rest neither
150
+ root nor panes create stacking contexts: a nested fixed overlay must participate
151
+ in its actual overlay owner's stack, above this panel's local separator.
152
+ Only an active pointer gesture isolates panes beneath its local iframe shield. */
153
+ .bh-resizable-panel[data-resizing='true'] {
154
+ isolation: isolate;
155
+ }
156
+
157
+ .bh-resizable-panel[data-resizing='true'] > .bh-resizable-panel__pane {
158
+ z-index: 0;
159
+ }
160
+
161
+ /* Pointer capture and document listeners retain the gesture outside the host;
162
+ no permanent pane stack, global cursor, body style or high-z overlay. */
163
+ .bh-resizable-panel__drag-shield {
164
+ position: absolute;
165
+ inset: 0;
166
+ z-index: 2;
167
+ touch-action: none;
168
+ user-select: none;
169
+ -webkit-user-select: none;
170
+ }
171
+
172
+ .bh-resizable-panel--horizontal > .bh-resizable-panel__drag-shield {
173
+ cursor: col-resize;
174
+ }
175
+
176
+ .bh-resizable-panel--vertical > .bh-resizable-panel__drag-shield {
177
+ cursor: row-resize;
178
+ }
179
+
180
+ @media (pointer: coarse) {
181
+ .bh-resizable-panel {
182
+ --bh-panel-hit-target: var(--bh-resize-touchTarget);
183
+ }
184
+ }
185
+
186
+ /* Resize geometry never animates, including when reduced motion is requested. */
187
+ @media (prefers-reduced-motion: reduce) {
188
+ .bh-resizable-panel > .bh-resizable-panel__pane,
189
+ .bh-resizable-panel > .bh-resizable-panel__separator {
190
+ transition: none;
191
+ animation: none;
192
+ }
193
+ }
194
+
195
+ @media (forced-colors: active) {
196
+ .bh-resizable-panel__line {
197
+ background: CanvasText;
198
+ }
199
+
200
+ .bh-resizable-panel__handle {
201
+ color: CanvasText;
202
+ background: Canvas;
203
+ border-color: CanvasText;
204
+ }
205
+
206
+ .bh-resizable-panel__separator:focus-visible > .bh-resizable-panel__handle {
207
+ outline-color: Highlight;
208
+ }
209
+ }
210
+
211
+ /* Opt in on existing interactive renderer hosts, not on panes or static content.
212
+ The outer allocation can shrink; only its readable surface retains a minimum. */
213
+ .bh-renderer-frame,
214
+ .bh-renderer-viewport {
215
+ display: flex;
216
+ flex-direction: column;
217
+ flex: 1 1 0%;
218
+ box-sizing: border-box;
219
+ inline-size: 100%;
220
+ min-inline-size: 0;
221
+ min-block-size: 0;
222
+ overflow: auto;
223
+ }
224
+
225
+ /* A usable outer interaction strip, not the readable SDK surface minimum.
226
+ Reserve a native toolbar row, one interaction row and their source gap when
227
+ surrounding page chrome would otherwise reduce the allocation to zero. */
228
+ :root {
229
+ --bh-renderer-access-minimum-block-size: calc(var(--bh-size-control-default) * 2 + var(--bh-space-100));
230
+ }
231
+
232
+ .bh-renderer-access {
233
+ min-block-size: var(--bh-renderer-access-minimum-block-size);
234
+ }
235
+
236
+ @media (pointer: coarse) {
237
+ :root {
238
+ --bh-renderer-access-minimum-block-size: calc(var(--bh-size-target-touch) * 2 + var(--bh-space-100));
239
+ }
240
+ }
241
+
242
+ .bh-renderer-viewport__surface {
243
+ display: flex;
244
+ flex-direction: column;
245
+ /* A definite readable flex basis also resolves SDK percentage-height children
246
+ in Firefox. It still grows with the allocation, never caps the renderer. */
247
+ flex: 1 0 var(--bh-layout-renderer-minimum-block-size);
248
+ box-sizing: border-box;
249
+ min-inline-size: var(--bh-layout-renderer-minimum-inline-size);
250
+ min-block-size: var(--bh-layout-renderer-minimum-block-size);
251
+ }
252
+
253
+ /* Chrome remains outside the actual viewport. Below its readable minimum,
254
+ the existing frame owns the one access scroller across chrome and surface. */
255
+ .bh-renderer-frame > .bh-renderer-viewport {
256
+ flex: 1 0 auto;
257
+ block-size: auto;
258
+ overflow: visible;
259
+ }
260
+
261
+ /* A retained renderer inside an already scrolling document delegates access to
262
+ that document; it must not create another clipped, independently scrolling box. */
263
+ .bh-renderer-frame.bh-renderer-frame--document:not(:fullscreen):not([data-bh-overlay-owner]) {
264
+ flex: 1 0 auto;
265
+ block-size: auto;
266
+ overflow: visible;
267
+ }
268
+
269
+ .bh-renderer-frame > .bh-renderer-viewport > .bh-renderer-viewport__surface,
270
+ .bh-renderer-frame > .bh-renderer-viewport__surface {
271
+ contain: size;
272
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@beehexa/vue-components",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2611.5.1",
4
+ "description": "Accessible, source-token-backed Vue components independent of a UI framework",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/beehexacorp/beehexa.design.git",
8
+ "directory": "packages/vue-components"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "sideEffects": [
18
+ "**/*.css"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./css": {
26
+ "types": "./dist/css.d.ts",
27
+ "default": "./dist/styles.css"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "dependencies": {
32
+ "@beehexa/design-tokens": "2611.5.1"
33
+ },
34
+ "peerDependencies": {
35
+ "vue": "^3.5.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "node ../../scripts/build-runtime-packages.mjs vue-components",
42
+ "check": "node ../../scripts/build-runtime-packages.mjs vue-components --check",
43
+ "prepack": "npm run build"
44
+ },
45
+ "engines": {
46
+ "node": ">=22.22.0"
47
+ }
48
+ }