@revenexx/studio-shared 0.3.0 → 0.3.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/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.vue +90 -34
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +23 -2
- package/dist/runtime/components/DataTable/DataTable.vue +199 -100
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +23 -2
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +5 -3
- package/dist/runtime/components/DataTable/FilterMenu.vue +32 -36
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +5 -3
- package/dist/runtime/components/DataTable/types.d.ts +15 -2
- package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +55 -0
- package/dist/runtime/components/Shared/FloatingPanel.vue +118 -0
- package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +55 -0
- package/dist/runtime/composables/useFloatingPanel.d.ts +69 -0
- package/dist/runtime/composables/useFloatingPanel.js +112 -0
- package/dist/runtime/composables/useTalkbackChannels.d.ts +21 -6
- package/dist/runtime/composables/useTalkbackChannels.js +16 -12
- package/dist/runtime/lib/columnOrder.d.ts +45 -0
- package/dist/runtime/lib/columnOrder.js +28 -0
- package/dist/runtime/lib/columnVisibility.d.ts +41 -0
- package/dist/runtime/lib/columnVisibility.js +12 -0
- package/dist/runtime/lib/dataTableFilters.d.ts +83 -0
- package/dist/runtime/lib/dataTableFilters.js +23 -0
- package/dist/runtime/lib/i18nBridge.d.ts +7 -2
- package/dist/types.d.mts +2 -0
- package/package.json +2 -2
|
@@ -20,6 +20,7 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
|
|
|
20
20
|
filter?: boolean | {
|
|
21
21
|
width?: string;
|
|
22
22
|
active?: boolean;
|
|
23
|
+
count?: number;
|
|
23
24
|
};
|
|
24
25
|
/** Built-in status/dimension filter pills (static mode). One dimension, or
|
|
25
26
|
* several (each renders as its own sub-menu). */
|
|
@@ -32,6 +33,26 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
|
|
|
32
33
|
viewModes?: DataTableViewMode[] | null;
|
|
33
34
|
/** Collapse handle for the surface's left rail (see `ListSurface`). */
|
|
34
35
|
rail?: boolean | DataTableRail;
|
|
36
|
+
/**
|
|
37
|
+
* Scroll the ROWS inside the table instead of the page.
|
|
38
|
+
*
|
|
39
|
+
* The toolbar, the header and the footer stay put and only the data moves,
|
|
40
|
+
* so paging through a long list keeps the search box, the filters and the
|
|
41
|
+
* "showing 20 of 400" in view — you stay in the table's context instead of
|
|
42
|
+
* scrolling the whole surface away and losing the controls that scope it.
|
|
43
|
+
*
|
|
44
|
+
* OPT-IN. A short list scrolling inside a box shorter than the list is worse
|
|
45
|
+
* than a page that scrolls, and there is no way for this component to know
|
|
46
|
+
* which it is looking at.
|
|
47
|
+
*
|
|
48
|
+
* The header row moves INSIDE the scroll container and sticks, rather than
|
|
49
|
+
* sitting above it. That is not a detail: a scrollbar takes width from the
|
|
50
|
+
* scrolling box, so a header outside it stays wider than the rows and every
|
|
51
|
+
* column drifts by however wide the scrollbar is.
|
|
52
|
+
*/
|
|
53
|
+
scrollBody?: boolean | {
|
|
54
|
+
maxHeight?: string;
|
|
55
|
+
};
|
|
35
56
|
pagination?: boolean | {
|
|
36
57
|
pageSize?: number;
|
|
37
58
|
};
|
|
@@ -61,12 +82,12 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
|
|
|
61
82
|
}) => any) | undefined;
|
|
62
83
|
} & {
|
|
63
84
|
'filter-menu'?: (props: {}) => any;
|
|
85
|
+
} & {
|
|
86
|
+
scope?: (props: {}) => any;
|
|
64
87
|
} & {
|
|
65
88
|
'toolbar-left'?: (props: {}) => any;
|
|
66
89
|
} & {
|
|
67
90
|
'toolbar-right'?: (props: {}) => any;
|
|
68
|
-
} & {
|
|
69
|
-
scope?: (props: {}) => any;
|
|
70
91
|
} & {
|
|
71
92
|
actions?: (props: {}) => any;
|
|
72
93
|
} & {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
/** Popover width class (a wide panel of inputs needs more than the default). */
|
|
3
3
|
width?: string;
|
|
4
|
-
/**
|
|
4
|
+
/** The trigger goes primary while at least one filter is set. */
|
|
5
5
|
active?: boolean;
|
|
6
|
+
/** How many are set, for the badge. */
|
|
7
|
+
count?: number;
|
|
6
8
|
};
|
|
7
|
-
declare var
|
|
9
|
+
declare var __VLS_22: {};
|
|
8
10
|
type __VLS_Slots = {} & {
|
|
9
|
-
default?: (props: typeof
|
|
11
|
+
default?: (props: typeof __VLS_22) => any;
|
|
10
12
|
};
|
|
11
13
|
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
12
14
|
active: boolean;
|
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
Button,
|
|
5
|
-
DropdownMenu,
|
|
6
|
-
DropdownMenuContent,
|
|
7
|
-
DropdownMenuItem,
|
|
8
|
-
DropdownMenuLabel,
|
|
9
|
-
DropdownMenuSeparator,
|
|
10
|
-
DropdownMenuTrigger
|
|
11
|
-
} from "@revenexx/studio";
|
|
2
|
+
import { Filter } from "#solar-icons";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger, ToolbarFilterTrigger } from "@revenexx/studio";
|
|
12
4
|
defineProps({
|
|
13
|
-
width: { type: String, required: false, default: "w-
|
|
14
|
-
active: { type: Boolean, required: false, default: false }
|
|
5
|
+
width: { type: String, required: false, default: "w-72" },
|
|
6
|
+
active: { type: Boolean, required: false, default: false },
|
|
7
|
+
count: { type: Number, required: false }
|
|
15
8
|
});
|
|
16
9
|
const t = useT();
|
|
17
10
|
</script>
|
|
18
11
|
|
|
19
12
|
<template>
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:
|
|
27
|
-
:aria-label="t('shared.datatable.filtermenu.aria')"
|
|
13
|
+
<Popover>
|
|
14
|
+
<PopoverTrigger as-child>
|
|
15
|
+
<ToolbarFilterTrigger
|
|
16
|
+
icon-only
|
|
17
|
+
:active="active"
|
|
18
|
+
:count="count"
|
|
19
|
+
:label="t('shared.datatable.filtermenu.aria')"
|
|
28
20
|
>
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
</
|
|
33
|
-
</
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
<template #icon>
|
|
22
|
+
<Filter :size="14" weight="BoldDuotone" />
|
|
23
|
+
</template>
|
|
24
|
+
</ToolbarFilterTrigger>
|
|
25
|
+
</PopoverTrigger>
|
|
26
|
+
|
|
27
|
+
<PopoverContent align="start" :class="['p-0', width]">
|
|
28
|
+
<p class="px-4 pt-3 pb-1 text-sm text-muted-foreground">
|
|
36
29
|
{{ t("shared.datatable.filtermenu.heading") }}
|
|
37
|
-
</
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
30
|
+
</p>
|
|
31
|
+
<!-- The surface's controls, stacked. Full width, because a filter panel
|
|
32
|
+
holding a half-width select looks like something failed to load. -->
|
|
33
|
+
<div class="space-y-2.5 p-4 pt-2 [&_[data-slot=select-trigger]]:w-full">
|
|
34
|
+
<slot>
|
|
35
|
+
<p class="py-4 text-center text-sm text-muted-foreground">
|
|
36
|
+
{{ t("shared.datatable.filtermenu.empty") }}
|
|
37
|
+
</p>
|
|
38
|
+
</slot>
|
|
39
|
+
</div>
|
|
40
|
+
</PopoverContent>
|
|
41
|
+
</Popover>
|
|
46
42
|
</template>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
/** Popover width class (a wide panel of inputs needs more than the default). */
|
|
3
3
|
width?: string;
|
|
4
|
-
/**
|
|
4
|
+
/** The trigger goes primary while at least one filter is set. */
|
|
5
5
|
active?: boolean;
|
|
6
|
+
/** How many are set, for the badge. */
|
|
7
|
+
count?: number;
|
|
6
8
|
};
|
|
7
|
-
declare var
|
|
9
|
+
declare var __VLS_22: {};
|
|
8
10
|
type __VLS_Slots = {} & {
|
|
9
|
-
default?: (props: typeof
|
|
11
|
+
default?: (props: typeof __VLS_22) => any;
|
|
10
12
|
};
|
|
11
13
|
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
12
14
|
active: boolean;
|
|
@@ -46,8 +46,21 @@ export interface DataTableFilter<T = unknown> {
|
|
|
46
46
|
key?: string;
|
|
47
47
|
/** Sub-menu title, e.g. "Status" (default `Status`). */
|
|
48
48
|
label?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The row's value(s) for this dimension.
|
|
51
|
+
*
|
|
52
|
+
* A string for anything a row has exactly one of — `r => r.status`. An ARRAY
|
|
53
|
+
* where a row can have several, and that is not a convenience: modelled as
|
|
54
|
+
* one value, `r => r.locales.join(', ')` makes the SET the value, so the
|
|
55
|
+
* options become combinations (four languages produce up to fifteen, six up
|
|
56
|
+
* to sixty-three) and "which of these have Polish?" cannot be asked at all —
|
|
57
|
+
* the answer is spread across three options and none of the counts is it.
|
|
58
|
+
*
|
|
59
|
+
* With an array the row lands in every bucket it belongs to. The counts then
|
|
60
|
+
* overlap rather than sum to the row total, which is correct and looks like a
|
|
61
|
+
* bug; see `runtime/lib/dataTableFilters.ts`.
|
|
62
|
+
*/
|
|
63
|
+
getter: (row: T) => string | string[] | null | undefined;
|
|
51
64
|
/** The selectable values + their labels. */
|
|
52
65
|
options: {
|
|
53
66
|
value: string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type Mode = 'docked-left' | 'docked-right' | 'floating';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** What `useFloatingPanel` returned, passed through whole. */
|
|
4
|
+
panel: {
|
|
5
|
+
mode: {
|
|
6
|
+
value: Mode;
|
|
7
|
+
};
|
|
8
|
+
collapsed: {
|
|
9
|
+
value: boolean;
|
|
10
|
+
};
|
|
11
|
+
floatingStyle: {
|
|
12
|
+
value: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
dragging: {
|
|
15
|
+
value: boolean;
|
|
16
|
+
};
|
|
17
|
+
startDrag: (ev: PointerEvent, el: HTMLElement | null) => void;
|
|
18
|
+
dock: (side: 'left' | 'right') => void;
|
|
19
|
+
floatHere: (el: HTMLElement | null) => void;
|
|
20
|
+
setCollapsed: (v: boolean) => void;
|
|
21
|
+
};
|
|
22
|
+
title: string;
|
|
23
|
+
/** Room the docked panel leaves at the bottom, for a floating control bar. */
|
|
24
|
+
bottomClass?: string;
|
|
25
|
+
/** Tailwind width, matching the `width` given to the composable. */
|
|
26
|
+
widthClass?: string;
|
|
27
|
+
/** Labels, so the package carries no copy of its own. */
|
|
28
|
+
labels: {
|
|
29
|
+
dockLeft: string;
|
|
30
|
+
dockRight: string;
|
|
31
|
+
floatHere: string;
|
|
32
|
+
collapse: string;
|
|
33
|
+
expand: string;
|
|
34
|
+
placement: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
declare var __VLS_5: {}, __VLS_67: {}, __VLS_69: {};
|
|
38
|
+
type __VLS_Slots = {} & {
|
|
39
|
+
'pill-icon'?: (props: typeof __VLS_5) => any;
|
|
40
|
+
} & {
|
|
41
|
+
default?: (props: typeof __VLS_67) => any;
|
|
42
|
+
} & {
|
|
43
|
+
footer?: (props: typeof __VLS_69) => any;
|
|
44
|
+
};
|
|
45
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
46
|
+
bottomClass: string;
|
|
47
|
+
widthClass: string;
|
|
48
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
49
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
50
|
+
export default _default;
|
|
51
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
52
|
+
new (): {
|
|
53
|
+
$slots: S;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { HamburgerMenu, MenuDots, Minimize } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger
|
|
10
|
+
} from "@revenexx/studio";
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
panel: { type: Object, required: true },
|
|
13
|
+
title: { type: String, required: true },
|
|
14
|
+
bottomClass: { type: String, required: false, default: "bottom-16" },
|
|
15
|
+
widthClass: { type: String, required: false, default: "w-80" },
|
|
16
|
+
labels: { type: Object, required: true }
|
|
17
|
+
});
|
|
18
|
+
const shell = ref(null);
|
|
19
|
+
const shellClass = computed(() => {
|
|
20
|
+
if (props.panel.mode.value === "docked-left") return `left-3 top-3 ${props.bottomClass}`;
|
|
21
|
+
if (props.panel.mode.value === "docked-right") return `right-3 top-3 ${props.bottomClass}`;
|
|
22
|
+
return "max-h-[70vh]";
|
|
23
|
+
});
|
|
24
|
+
const pillClass = computed(() => {
|
|
25
|
+
if (props.panel.mode.value === "docked-left") return "left-3 top-3";
|
|
26
|
+
if (props.panel.mode.value === "docked-right") return "right-3 top-3";
|
|
27
|
+
return "";
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<Transition
|
|
33
|
+
enter-active-class="transition duration-150 ease-out"
|
|
34
|
+
enter-from-class="opacity-0 scale-95"
|
|
35
|
+
enter-to-class="opacity-100 scale-100"
|
|
36
|
+
leave-active-class="transition duration-100 ease-in"
|
|
37
|
+
leave-from-class="opacity-100 scale-100"
|
|
38
|
+
leave-to-class="opacity-0 scale-95"
|
|
39
|
+
>
|
|
40
|
+
<div
|
|
41
|
+
v-if="panel.collapsed.value"
|
|
42
|
+
key="pill"
|
|
43
|
+
class="absolute z-10"
|
|
44
|
+
:class="pillClass"
|
|
45
|
+
:style="panel.mode.value === 'floating' ? panel.floatingStyle.value : void 0"
|
|
46
|
+
>
|
|
47
|
+
<!-- Two-layer frame: outer glass ring, inner white pane — so the pill is
|
|
48
|
+
recognisably the same object as the panel it came from. -->
|
|
49
|
+
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="flex h-9 cursor-pointer items-center gap-1.5 rounded-lg bg-white px-3 text-sm font-medium transition hover:bg-muted/75"
|
|
53
|
+
:title="labels.expand"
|
|
54
|
+
@click="panel.setCollapsed(false)"
|
|
55
|
+
>
|
|
56
|
+
<slot name="pill-icon" />
|
|
57
|
+
{{ title }}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div
|
|
63
|
+
v-else
|
|
64
|
+
key="panel"
|
|
65
|
+
ref="shell"
|
|
66
|
+
class="absolute z-10 flex flex-col rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm"
|
|
67
|
+
:class="[shellClass, widthClass]"
|
|
68
|
+
:style="panel.mode.value === 'floating' ? panel.floatingStyle.value : void 0"
|
|
69
|
+
>
|
|
70
|
+
<div class="relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-white bg-white">
|
|
71
|
+
<div
|
|
72
|
+
class="flex shrink-0 touch-none select-none items-center gap-1 border-b border-black/[0.06] bg-white px-2 py-1.5"
|
|
73
|
+
:class="panel.dragging.value ? 'cursor-grabbing' : 'cursor-grab'"
|
|
74
|
+
@pointerdown="panel.startDrag($event, shell)"
|
|
75
|
+
>
|
|
76
|
+
<HamburgerMenu :size="14" class="shrink-0 text-muted-foreground/60" />
|
|
77
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ title }}</span>
|
|
78
|
+
|
|
79
|
+
<DropdownMenu>
|
|
80
|
+
<DropdownMenuTrigger as-child>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
84
|
+
:title="labels.placement"
|
|
85
|
+
>
|
|
86
|
+
<MenuDots :size="14" />
|
|
87
|
+
</button>
|
|
88
|
+
</DropdownMenuTrigger>
|
|
89
|
+
<DropdownMenuContent align="end" class="w-36 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
|
|
90
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.dock('left')">{{ labels.dockLeft }}</DropdownMenuItem>
|
|
91
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.dock('right')">{{ labels.dockRight }}</DropdownMenuItem>
|
|
92
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.floatHere(shell)">{{ labels.floatHere }}</DropdownMenuItem>
|
|
93
|
+
<DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
|
|
94
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.setCollapsed(true)">{{ labels.collapse }}</DropdownMenuItem>
|
|
95
|
+
</DropdownMenuContent>
|
|
96
|
+
</DropdownMenu>
|
|
97
|
+
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
101
|
+
:title="labels.collapse"
|
|
102
|
+
@click="panel.setCollapsed(true)"
|
|
103
|
+
>
|
|
104
|
+
<Minimize :size="14" />
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
109
|
+
<slot />
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div v-if="$slots.footer" class="shrink-0 border-t border-black/[0.06] px-3 py-2 text-xs text-muted-foreground">
|
|
113
|
+
<slot name="footer" />
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</Transition>
|
|
118
|
+
</template>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type Mode = 'docked-left' | 'docked-right' | 'floating';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** What `useFloatingPanel` returned, passed through whole. */
|
|
4
|
+
panel: {
|
|
5
|
+
mode: {
|
|
6
|
+
value: Mode;
|
|
7
|
+
};
|
|
8
|
+
collapsed: {
|
|
9
|
+
value: boolean;
|
|
10
|
+
};
|
|
11
|
+
floatingStyle: {
|
|
12
|
+
value: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
dragging: {
|
|
15
|
+
value: boolean;
|
|
16
|
+
};
|
|
17
|
+
startDrag: (ev: PointerEvent, el: HTMLElement | null) => void;
|
|
18
|
+
dock: (side: 'left' | 'right') => void;
|
|
19
|
+
floatHere: (el: HTMLElement | null) => void;
|
|
20
|
+
setCollapsed: (v: boolean) => void;
|
|
21
|
+
};
|
|
22
|
+
title: string;
|
|
23
|
+
/** Room the docked panel leaves at the bottom, for a floating control bar. */
|
|
24
|
+
bottomClass?: string;
|
|
25
|
+
/** Tailwind width, matching the `width` given to the composable. */
|
|
26
|
+
widthClass?: string;
|
|
27
|
+
/** Labels, so the package carries no copy of its own. */
|
|
28
|
+
labels: {
|
|
29
|
+
dockLeft: string;
|
|
30
|
+
dockRight: string;
|
|
31
|
+
floatHere: string;
|
|
32
|
+
collapse: string;
|
|
33
|
+
expand: string;
|
|
34
|
+
placement: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
declare var __VLS_5: {}, __VLS_67: {}, __VLS_69: {};
|
|
38
|
+
type __VLS_Slots = {} & {
|
|
39
|
+
'pill-icon'?: (props: typeof __VLS_5) => any;
|
|
40
|
+
} & {
|
|
41
|
+
default?: (props: typeof __VLS_67) => any;
|
|
42
|
+
} & {
|
|
43
|
+
footer?: (props: typeof __VLS_69) => any;
|
|
44
|
+
};
|
|
45
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
46
|
+
bottomClass: string;
|
|
47
|
+
widthClass: string;
|
|
48
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
49
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
50
|
+
export default _default;
|
|
51
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
52
|
+
new (): {
|
|
53
|
+
$slots: S;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Ref } from 'vue';
|
|
2
|
+
export type FloatingPanelDock = 'left' | 'right';
|
|
3
|
+
export type FloatingPanelMode = 'docked-left' | 'docked-right' | 'floating';
|
|
4
|
+
export interface FloatingPanelOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Where this panel's placement is remembered. One key per panel — two panels
|
|
7
|
+
* sharing a key would drag each other around.
|
|
8
|
+
*/
|
|
9
|
+
storageKey: string;
|
|
10
|
+
/** Which side it starts on, before anybody has moved it. */
|
|
11
|
+
initialMode?: FloatingPanelMode;
|
|
12
|
+
/** The panel's width, in px. Used to clamp it inside the canvas. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** Room to keep clear at the bottom, for a floating control bar. */
|
|
15
|
+
bottomClearance?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A panel that can be docked to either side of a canvas, floated anywhere on
|
|
19
|
+
* it, or collapsed into a pill — and dragged between those states by its own
|
|
20
|
+
* header.
|
|
21
|
+
*
|
|
22
|
+
* Lifted from the integration studio's workflow editor, where this shape was
|
|
23
|
+
* worked out and where it has been in daily use: `usePaletteLayout`, with the
|
|
24
|
+
* storage key and the dimensions made arguments so a second editor can have
|
|
25
|
+
* its own panels rather than fighting over one remembered position.
|
|
26
|
+
*
|
|
27
|
+
* ## Why pointer events rather than HTML5 drag
|
|
28
|
+
*
|
|
29
|
+
* The panel's CONTENTS are draggable — the workflow editor drags nodes out of
|
|
30
|
+
* its palette onto the canvas, and the mail builder drags blocks. HTML5 drag
|
|
31
|
+
* on the shell would swallow those gestures. The header moves the panel with
|
|
32
|
+
* plain pointer events, so the two never compete: grab the bar, or grab a row,
|
|
33
|
+
* and each does the one thing.
|
|
34
|
+
*
|
|
35
|
+
* ## The magnet
|
|
36
|
+
*
|
|
37
|
+
* Coming within `DOCK_ZONE` of the left or right edge while dragging arms
|
|
38
|
+
* `dockPreview`, which the host renders as a ghost of the dock slot. Releasing
|
|
39
|
+
* inside the zone snaps into that dock; anywhere else leaves the panel where it
|
|
40
|
+
* was dropped. Docking is a state, not a position, so a docked panel keeps its
|
|
41
|
+
* side when the window resizes.
|
|
42
|
+
*
|
|
43
|
+
* Placement is persisted (guarded — private mode and quota must not throw) and
|
|
44
|
+
* clamped to the canvas on restore, because the window may have shrunk since.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useFloatingPanel(canvasRef: Ref<HTMLElement | null>, options: FloatingPanelOptions): {
|
|
47
|
+
mode: Ref<FloatingPanelMode, FloatingPanelMode>;
|
|
48
|
+
collapsed: Ref<boolean, boolean>;
|
|
49
|
+
floating: Ref<{
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
}, {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
} | {
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
}>;
|
|
59
|
+
floatingStyle: import("vue").ComputedRef<{
|
|
60
|
+
left: string;
|
|
61
|
+
top: string;
|
|
62
|
+
}>;
|
|
63
|
+
dragging: Ref<boolean, boolean>;
|
|
64
|
+
dockPreview: Ref<FloatingPanelDock | null, FloatingPanelDock | null>;
|
|
65
|
+
startDrag: (ev: PointerEvent, shellEl: HTMLElement | null) => void;
|
|
66
|
+
dock: (side: FloatingPanelDock) => void;
|
|
67
|
+
floatHere: (shellEl: HTMLElement | null) => void;
|
|
68
|
+
setCollapsed: (v: boolean) => void;
|
|
69
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
2
|
+
const MARGIN = 12;
|
|
3
|
+
const DOCK_ZONE = 48;
|
|
4
|
+
function clampNum(v, lo, hi) {
|
|
5
|
+
return Math.min(hi, Math.max(lo, v));
|
|
6
|
+
}
|
|
7
|
+
export function useFloatingPanel(canvasRef, options) {
|
|
8
|
+
const width = options.width ?? 320;
|
|
9
|
+
const bottomClearance = options.bottomClearance ?? 64;
|
|
10
|
+
const mode = ref(options.initialMode ?? "docked-left");
|
|
11
|
+
const collapsed = ref(false);
|
|
12
|
+
const floating = ref({ x: MARGIN, y: MARGIN });
|
|
13
|
+
const dragging = ref(false);
|
|
14
|
+
const dockPreview = ref(null);
|
|
15
|
+
function persist() {
|
|
16
|
+
try {
|
|
17
|
+
const payload = { mode: mode.value, floating: floating.value, collapsed: collapsed.value };
|
|
18
|
+
localStorage.setItem(options.storageKey, JSON.stringify(payload));
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function restore() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = localStorage.getItem(options.storageKey);
|
|
25
|
+
if (!raw) return;
|
|
26
|
+
const saved = JSON.parse(raw);
|
|
27
|
+
if (saved.mode === "docked-left" || saved.mode === "docked-right" || saved.mode === "floating") {
|
|
28
|
+
mode.value = saved.mode;
|
|
29
|
+
}
|
|
30
|
+
if (typeof saved.floating?.x === "number" && typeof saved.floating?.y === "number") {
|
|
31
|
+
floating.value = { x: saved.floating.x, y: saved.floating.y };
|
|
32
|
+
}
|
|
33
|
+
collapsed.value = saved.collapsed === true;
|
|
34
|
+
clampFloating();
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function clampFloating() {
|
|
39
|
+
const rect = canvasRef.value?.getBoundingClientRect();
|
|
40
|
+
if (!rect || rect.width === 0) return;
|
|
41
|
+
floating.value = {
|
|
42
|
+
x: clampNum(floating.value.x, MARGIN, Math.max(MARGIN, rect.width - width - MARGIN)),
|
|
43
|
+
y: clampNum(floating.value.y, MARGIN, Math.max(MARGIN, rect.height - bottomClearance - 160))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
onMounted(restore);
|
|
47
|
+
let grabDX = 0;
|
|
48
|
+
let grabDY = 0;
|
|
49
|
+
let panelHeight = 0;
|
|
50
|
+
function startDrag(ev, shellEl) {
|
|
51
|
+
if (ev.button !== 0 || dragging.value) return;
|
|
52
|
+
if (ev.target?.closest("button")) return;
|
|
53
|
+
const canvas = canvasRef.value;
|
|
54
|
+
if (!canvas || !shellEl) return;
|
|
55
|
+
const cRect = canvas.getBoundingClientRect();
|
|
56
|
+
const pRect = shellEl.getBoundingClientRect();
|
|
57
|
+
floating.value = { x: pRect.left - cRect.left, y: pRect.top - cRect.top };
|
|
58
|
+
mode.value = "floating";
|
|
59
|
+
grabDX = ev.clientX - pRect.left;
|
|
60
|
+
grabDY = ev.clientY - pRect.top;
|
|
61
|
+
panelHeight = Math.min(pRect.height, window.innerHeight * 0.7);
|
|
62
|
+
dragging.value = true;
|
|
63
|
+
ev.preventDefault();
|
|
64
|
+
window.addEventListener("pointermove", onDragMove);
|
|
65
|
+
window.addEventListener("pointerup", onDragEnd);
|
|
66
|
+
window.addEventListener("pointercancel", onDragEnd);
|
|
67
|
+
}
|
|
68
|
+
function onDragMove(ev) {
|
|
69
|
+
const rect = canvasRef.value?.getBoundingClientRect();
|
|
70
|
+
if (!rect) return;
|
|
71
|
+
const x = clampNum(ev.clientX - rect.left - grabDX, MARGIN, Math.max(MARGIN, rect.width - width - MARGIN));
|
|
72
|
+
const y = clampNum(ev.clientY - rect.top - grabDY, MARGIN, Math.max(MARGIN, rect.height - bottomClearance - panelHeight));
|
|
73
|
+
floating.value = { x, y };
|
|
74
|
+
dockPreview.value = x <= DOCK_ZONE ? "left" : x + width >= rect.width - DOCK_ZONE ? "right" : null;
|
|
75
|
+
}
|
|
76
|
+
function stopListeners() {
|
|
77
|
+
window.removeEventListener("pointermove", onDragMove);
|
|
78
|
+
window.removeEventListener("pointerup", onDragEnd);
|
|
79
|
+
window.removeEventListener("pointercancel", onDragEnd);
|
|
80
|
+
}
|
|
81
|
+
function onDragEnd() {
|
|
82
|
+
if (dockPreview.value) mode.value = dockPreview.value === "left" ? "docked-left" : "docked-right";
|
|
83
|
+
dockPreview.value = null;
|
|
84
|
+
dragging.value = false;
|
|
85
|
+
stopListeners();
|
|
86
|
+
persist();
|
|
87
|
+
}
|
|
88
|
+
onBeforeUnmount(stopListeners);
|
|
89
|
+
function dock(side) {
|
|
90
|
+
mode.value = side === "left" ? "docked-left" : "docked-right";
|
|
91
|
+
persist();
|
|
92
|
+
}
|
|
93
|
+
function floatHere(shellEl) {
|
|
94
|
+
if (mode.value !== "floating") {
|
|
95
|
+
const cRect = canvasRef.value?.getBoundingClientRect();
|
|
96
|
+
const pRect = shellEl?.getBoundingClientRect();
|
|
97
|
+
if (cRect && pRect) floating.value = { x: pRect.left - cRect.left, y: pRect.top - cRect.top };
|
|
98
|
+
mode.value = "floating";
|
|
99
|
+
clampFloating();
|
|
100
|
+
}
|
|
101
|
+
persist();
|
|
102
|
+
}
|
|
103
|
+
function setCollapsed(v) {
|
|
104
|
+
collapsed.value = v;
|
|
105
|
+
persist();
|
|
106
|
+
}
|
|
107
|
+
const floatingStyle = computed(() => ({
|
|
108
|
+
left: `${floating.value.x}px`,
|
|
109
|
+
top: `${floating.value.y}px`
|
|
110
|
+
}));
|
|
111
|
+
return { mode, collapsed, floating, floatingStyle, dragging, dockPreview, startDrag, dock, floatHere, setCollapsed };
|
|
112
|
+
}
|
|
@@ -21,12 +21,27 @@ import type { MaybeRefOrGetter } from '@revenexx/talkback-js/vue';
|
|
|
21
21
|
* with the caller's scope — which is also what gives the delegate's own
|
|
22
22
|
* `onScopeDispose(release)` a scope to bind to.
|
|
23
23
|
*
|
|
24
|
-
* 3.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* 3. **An attach that cannot name a channel yet is retried, as long as what is
|
|
25
|
+
* missing is reactive.** `useChannel` attaches EAGERLY and only then installs
|
|
26
|
+
* the watch that would re-attach, and the eager call has no `try` around
|
|
27
|
+
* `resolveName()` while the watch's source does. So a name the grammar rejects
|
|
28
|
+
* at that moment — overwhelmingly a tenant that has not been loaded yet, since
|
|
29
|
+
* it is positionally first in every channel name — throws straight out of the
|
|
30
|
+
* composable, before anything exists that could try again. Swallowing that (as
|
|
31
|
+
* this wrapper once did) turns "not yet" into "never": the subscription is lost
|
|
32
|
+
* for the life of the tab while the socket connects perfectly well, which is
|
|
33
|
+
* the shape realtime silently missing takes. The attach therefore runs inside
|
|
34
|
+
* an effect, so the reads that got as far as throwing are tracked and the
|
|
35
|
+
* attempt is repeated when one of them changes.
|
|
36
|
+
*
|
|
37
|
+
* THE QUALIFIER IS LOAD-BEARING, and `useTalkbackUser` is where it bites:
|
|
38
|
+
* `userChannel(tenant, '')` throws too, but the subject is a plain `let userId`
|
|
39
|
+
* in `createPlatformTalkback` — cached from the last mint so it can answer
|
|
40
|
+
* synchronously — so the tenant is the only tracked read and a subject that
|
|
41
|
+
* fills in later re-runs nothing. That is not new (the throw was swallowed
|
|
42
|
+
* before as well, and there was no channel either way), and a null token is the
|
|
43
|
+
* ordinary state for up to fifteen seconds per `usePlatformTalkback`. Making
|
|
44
|
+
* `userId` reactive is what would close it.
|
|
30
45
|
*
|
|
31
46
|
* With realtime off (`talkbackHost` empty) every one of these is a no-op that
|
|
32
47
|
* reports a null channel, so a call site needs no `if (enabled)`.
|