@invopop/popui 0.1.98 → 0.1.100
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/MenuItem.svelte
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
let highlight = $state(false)
|
|
42
42
|
let leaveHoverTimeout: ReturnType<typeof setTimeout> | null = null
|
|
43
43
|
let rowStyles = $derived(
|
|
44
|
-
clsx('flex items-center rounded-lg
|
|
44
|
+
clsx('flex items-center rounded-lg', {
|
|
45
45
|
'py-1 pr-1': !collapsedSidebar,
|
|
46
46
|
'pl-2': !collapsedSidebar && !imageUrl,
|
|
47
47
|
'pl-[10px]': !collapsedSidebar && imageUrl,
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
</span>
|
|
166
166
|
</button>
|
|
167
167
|
{#if !collapsedSidebar && action}
|
|
168
|
-
<span class="shrink-0" data-menu-item-action>
|
|
168
|
+
<span class="shrink-0 flex items-center" data-menu-item-action>
|
|
169
169
|
{@render action()}
|
|
170
170
|
</span>
|
|
171
171
|
{/if}
|
|
@@ -9,11 +9,13 @@ declare class SidebarState {
|
|
|
9
9
|
open: boolean;
|
|
10
10
|
openMobile: boolean;
|
|
11
11
|
width: string;
|
|
12
|
+
isResizing: boolean;
|
|
12
13
|
setOpen: SidebarStateProps['setOpen'];
|
|
13
14
|
state: string;
|
|
14
15
|
constructor(props: SidebarStateProps);
|
|
15
16
|
get isMobile(): boolean;
|
|
16
17
|
setWidth: (px: number) => void;
|
|
18
|
+
resetWidth: () => void;
|
|
17
19
|
handleShortcutKeydown: (e: KeyboardEvent) => void;
|
|
18
20
|
setOpenMobile: (value: boolean) => void;
|
|
19
21
|
toggle: () => boolean | void;
|
|
@@ -6,6 +6,7 @@ class SidebarState {
|
|
|
6
6
|
open = $derived.by(() => this.props.open());
|
|
7
7
|
openMobile = $state(false);
|
|
8
8
|
width = $state(SIDEBAR_WIDTH);
|
|
9
|
+
isResizing = $state(false);
|
|
9
10
|
setOpen;
|
|
10
11
|
#isMobile;
|
|
11
12
|
state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
|
|
@@ -34,6 +35,17 @@ class SidebarState {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
};
|
|
38
|
+
resetWidth = () => {
|
|
39
|
+
this.width = SIDEBAR_WIDTH;
|
|
40
|
+
if (typeof localStorage !== 'undefined') {
|
|
41
|
+
try {
|
|
42
|
+
localStorage.removeItem(SIDEBAR_WIDTH_STORAGE_KEY);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// ignore private-mode errors
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
37
49
|
handleShortcutKeydown = (e) => {
|
|
38
50
|
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
|
|
39
51
|
e.preventDefault();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { HTMLAttributes } from 'svelte/elements'
|
|
2
|
+
import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements'
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from 'bits-ui'
|
|
3
4
|
import ShortcutWrapper from '../ShortcutWrapper.svelte'
|
|
5
|
+
import TooltipContent from '../tooltip/tooltip-content.svelte'
|
|
4
6
|
import { cn, type WithElementRef } from '../utils.js'
|
|
5
|
-
import { SIDEBAR_DRAG_THRESHOLD_PX } from './constants.js'
|
|
7
|
+
import { SIDEBAR_DRAG_THRESHOLD_PX, SIDEBAR_MIN_WIDTH_PX } from './constants.js'
|
|
6
8
|
import { useSidebar } from './context.svelte.js'
|
|
7
9
|
|
|
8
10
|
let {
|
|
@@ -19,50 +21,77 @@
|
|
|
19
21
|
let dragMoved = false
|
|
20
22
|
let dragDirection: 1 | -1 = 1
|
|
21
23
|
|
|
22
|
-
let isHovering = $state(false)
|
|
23
24
|
let isDragging = $state(false)
|
|
24
|
-
let
|
|
25
|
-
let
|
|
26
|
-
let
|
|
27
|
-
let tooltipWidth = $state(0)
|
|
28
|
-
let tooltipHeight = $state(0)
|
|
25
|
+
let tooltipOpen = $state(false)
|
|
26
|
+
let cursorX = $state(0)
|
|
27
|
+
let cursorY = $state(0)
|
|
29
28
|
|
|
29
|
+
const TOOLTIP_HOVER_DELAY_MS = 700
|
|
30
30
|
const TOOLTIP_CURSOR_OFFSET = 12
|
|
31
|
-
const TOOLTIP_VIEWPORT_PADDING = 8
|
|
32
|
-
|
|
33
|
-
let tooltipLeft = $derived.by(() => {
|
|
34
|
-
if (tooltipWidth === 0 || typeof window === 'undefined') return tooltipX
|
|
35
|
-
const desired = tooltipX - tooltipWidth / 2
|
|
36
|
-
const minX = TOOLTIP_VIEWPORT_PADDING
|
|
37
|
-
const maxX = window.innerWidth - tooltipWidth - TOOLTIP_VIEWPORT_PADDING
|
|
38
|
-
return Math.max(minX, Math.min(maxX, desired))
|
|
39
|
-
})
|
|
40
31
|
|
|
41
|
-
let
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
let cursorAnchor = $derived.by(() => {
|
|
33
|
+
const x = cursorX
|
|
34
|
+
const y = cursorY
|
|
35
|
+
return {
|
|
36
|
+
getBoundingClientRect: () => DOMRect.fromRect({ x, y, width: 0, height: 0 })
|
|
37
|
+
}
|
|
46
38
|
})
|
|
47
39
|
|
|
48
|
-
function updateTooltipPosition(e: PointerEvent) {
|
|
49
|
-
tooltipX = e.clientX
|
|
50
|
-
tooltipY = e.clientY
|
|
51
|
-
}
|
|
52
|
-
|
|
53
40
|
function onPointerEnter(e: PointerEvent) {
|
|
54
41
|
const sidebarRoot = (e.currentTarget as HTMLElement).closest('[data-slot="sidebar"]')
|
|
55
42
|
dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
cursorX = e.clientX
|
|
44
|
+
cursorY = e.clientY
|
|
58
45
|
}
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
const COLLAPSE_DRAG_OVERSHOOT_PX = 100
|
|
48
|
+
const POST_DRAG_CLICK_GUARD_MS = 250
|
|
49
|
+
let dragEndTime = 0
|
|
50
|
+
|
|
51
|
+
function onPointerMove(e: PointerEvent) {
|
|
52
|
+
cursorX = e.clientX
|
|
53
|
+
cursorY = e.clientY
|
|
54
|
+
const button = e.currentTarget as HTMLButtonElement
|
|
55
|
+
if (!button.hasPointerCapture(e.pointerId)) return
|
|
56
|
+
const delta = (e.clientX - dragStartX) * dragDirection
|
|
57
|
+
if (Math.abs(delta) > SIDEBAR_DRAG_THRESHOLD_PX) {
|
|
58
|
+
dragMoved = true
|
|
59
|
+
isDragging = true
|
|
60
|
+
sidebar.isResizing = true
|
|
61
|
+
tooltipOpen = false
|
|
62
|
+
}
|
|
63
|
+
if (!dragMoved) return
|
|
64
|
+
if (sidebar.state === 'collapsed') {
|
|
65
|
+
if (delta > 0) {
|
|
66
|
+
button.releasePointerCapture(e.pointerId)
|
|
67
|
+
document.body.style.cursor = ''
|
|
68
|
+
document.body.style.userSelect = ''
|
|
69
|
+
isDragging = false
|
|
70
|
+
sidebar.isResizing = false
|
|
71
|
+
dragMoved = false
|
|
72
|
+
dragEndTime = Date.now()
|
|
73
|
+
sidebar.setOpen(true)
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
const targetWidth = dragStartWidthPx + delta
|
|
78
|
+
if (targetWidth < SIDEBAR_MIN_WIDTH_PX - COLLAPSE_DRAG_OVERSHOOT_PX) {
|
|
79
|
+
button.releasePointerCapture(e.pointerId)
|
|
80
|
+
document.body.style.cursor = ''
|
|
81
|
+
document.body.style.userSelect = ''
|
|
82
|
+
isDragging = false
|
|
83
|
+
sidebar.isResizing = false
|
|
84
|
+
dragMoved = false
|
|
85
|
+
dragEndTime = Date.now()
|
|
86
|
+
sidebar.resetWidth()
|
|
87
|
+
sidebar.setOpen(false)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
sidebar.setWidth(targetWidth)
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
function onPointerDown(e: PointerEvent) {
|
|
65
|
-
if (sidebar.
|
|
94
|
+
if (sidebar.isMobile) return
|
|
66
95
|
const button = e.currentTarget as HTMLButtonElement
|
|
67
96
|
const sidebarRoot = button.closest('[data-slot="sidebar"]')
|
|
68
97
|
dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1
|
|
@@ -76,76 +105,105 @@
|
|
|
76
105
|
document.body.style.userSelect = 'none'
|
|
77
106
|
}
|
|
78
107
|
|
|
79
|
-
function onPointerMove(e: PointerEvent) {
|
|
80
|
-
if (tooltipVisible) updateTooltipPosition(e)
|
|
81
|
-
const button = e.currentTarget as HTMLButtonElement
|
|
82
|
-
if (!button.hasPointerCapture(e.pointerId)) return
|
|
83
|
-
const delta = (e.clientX - dragStartX) * dragDirection
|
|
84
|
-
if (Math.abs(delta) > SIDEBAR_DRAG_THRESHOLD_PX) {
|
|
85
|
-
dragMoved = true
|
|
86
|
-
isDragging = true
|
|
87
|
-
}
|
|
88
|
-
if (dragMoved) sidebar.setWidth(dragStartWidthPx + delta)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
108
|
function onPointerUp(e: PointerEvent) {
|
|
92
109
|
const button = e.currentTarget as HTMLButtonElement
|
|
93
110
|
if (button.hasPointerCapture(e.pointerId)) button.releasePointerCapture(e.pointerId)
|
|
94
111
|
document.body.style.cursor = ''
|
|
95
112
|
document.body.style.userSelect = ''
|
|
96
113
|
isDragging = false
|
|
114
|
+
sidebar.isResizing = false
|
|
115
|
+
if (dragMoved) {
|
|
116
|
+
dragEndTime = Date.now()
|
|
117
|
+
dragMoved = false
|
|
118
|
+
}
|
|
97
119
|
}
|
|
98
120
|
|
|
121
|
+
const DOUBLE_CLICK_DELAY_MS = 300
|
|
122
|
+
let pendingClickTimer: ReturnType<typeof setTimeout> | undefined
|
|
123
|
+
|
|
99
124
|
function onClick(e: MouseEvent) {
|
|
100
|
-
if (
|
|
125
|
+
if (Date.now() - dragEndTime < POST_DRAG_CLICK_GUARD_MS) {
|
|
101
126
|
e.preventDefault()
|
|
102
127
|
e.stopPropagation()
|
|
103
|
-
dragMoved = false
|
|
104
128
|
return
|
|
105
129
|
}
|
|
106
|
-
|
|
130
|
+
clearTimeout(pendingClickTimer)
|
|
131
|
+
pendingClickTimer = setTimeout(() => {
|
|
132
|
+
pendingClickTimer = undefined
|
|
133
|
+
sidebar.toggle()
|
|
134
|
+
}, DOUBLE_CLICK_DELAY_MS)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function onDoubleClick() {
|
|
138
|
+
if (sidebar.isMobile) return
|
|
139
|
+
clearTimeout(pendingClickTimer)
|
|
140
|
+
pendingClickTimer = undefined
|
|
141
|
+
sidebar.setOpen(true)
|
|
142
|
+
sidebar.resetWidth()
|
|
107
143
|
}
|
|
108
144
|
</script>
|
|
109
145
|
|
|
110
|
-
<
|
|
111
|
-
bind:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
aria-label="Toggle Sidebar"
|
|
115
|
-
tabindex={-1}
|
|
116
|
-
type="button"
|
|
117
|
-
onpointerenter={onPointerEnter}
|
|
118
|
-
onpointerleave={onPointerLeave}
|
|
119
|
-
onpointerdown={onPointerDown}
|
|
120
|
-
onpointermove={onPointerMove}
|
|
121
|
-
onpointerup={onPointerUp}
|
|
122
|
-
onpointercancel={onPointerUp}
|
|
123
|
-
onclick={onClick}
|
|
124
|
-
class={cn(
|
|
125
|
-
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
|
126
|
-
'after:absolute after:inset-y-0 after:left-1/2 after:w-[2px]',
|
|
127
|
-
'hover:after:bg-sidebar-border',
|
|
128
|
-
'group-data-[side=left]:cursor-w-resize group-data-[side=right]:cursor-e-resize',
|
|
129
|
-
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
|
130
|
-
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
|
131
|
-
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
|
132
|
-
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
|
133
|
-
className
|
|
134
|
-
)}
|
|
135
|
-
{...restProps}
|
|
146
|
+
<TooltipPrimitive.Root
|
|
147
|
+
bind:open={tooltipOpen}
|
|
148
|
+
delayDuration={TOOLTIP_HOVER_DELAY_MS}
|
|
149
|
+
disableHoverableContent
|
|
136
150
|
>
|
|
137
|
-
{
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
<TooltipPrimitive.Trigger disabled={isDragging}>
|
|
152
|
+
{#snippet child({ props })}
|
|
153
|
+
{@const buttonProps = props as HTMLButtonAttributes}
|
|
154
|
+
<button
|
|
155
|
+
bind:this={ref}
|
|
156
|
+
{...buttonProps}
|
|
157
|
+
onpointerenter={(e) => {
|
|
158
|
+
buttonProps.onpointerenter?.(e)
|
|
159
|
+
onPointerEnter(e)
|
|
160
|
+
}}
|
|
161
|
+
onpointermove={(e) => {
|
|
162
|
+
buttonProps.onpointermove?.(e)
|
|
163
|
+
onPointerMove(e)
|
|
164
|
+
}}
|
|
165
|
+
onpointerdown={(e) => {
|
|
166
|
+
buttonProps.onpointerdown?.(e)
|
|
167
|
+
onPointerDown(e)
|
|
168
|
+
}}
|
|
169
|
+
onpointerup={(e) => {
|
|
170
|
+
buttonProps.onpointerup?.(e)
|
|
171
|
+
onPointerUp(e)
|
|
172
|
+
}}
|
|
173
|
+
onpointercancel={onPointerUp}
|
|
174
|
+
onclick={(e) => {
|
|
175
|
+
buttonProps.onclick?.(e)
|
|
176
|
+
onClick(e)
|
|
177
|
+
}}
|
|
178
|
+
ondblclick={onDoubleClick}
|
|
179
|
+
data-sidebar="rail"
|
|
180
|
+
data-slot="sidebar-rail"
|
|
181
|
+
aria-label="Toggle Sidebar"
|
|
182
|
+
tabindex={-1}
|
|
183
|
+
type="button"
|
|
184
|
+
class={cn(
|
|
185
|
+
'absolute inset-y-0 z-50 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
|
186
|
+
'after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:transition-colors after:duration-150',
|
|
187
|
+
'hover:after:delay-150 hover:after:bg-background-accent-default',
|
|
188
|
+
'group-data-[side=left]:cursor-w-resize group-data-[side=right]:cursor-e-resize',
|
|
189
|
+
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
|
190
|
+
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
|
191
|
+
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
|
192
|
+
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
|
193
|
+
className
|
|
194
|
+
)}
|
|
195
|
+
{...restProps}
|
|
196
|
+
>
|
|
197
|
+
{@render children?.()}
|
|
198
|
+
</button>
|
|
199
|
+
{/snippet}
|
|
200
|
+
</TooltipPrimitive.Trigger>
|
|
201
|
+
<TooltipContent
|
|
202
|
+
customAnchor={cursorAnchor}
|
|
203
|
+
side="bottom"
|
|
204
|
+
align="center"
|
|
205
|
+
sideOffset={TOOLTIP_CURSOR_OFFSET}
|
|
206
|
+
class="px-3 py-2"
|
|
149
207
|
>
|
|
150
208
|
<div class="flex flex-col gap-1.5">
|
|
151
209
|
{#if sidebar.state === 'expanded'}
|
|
@@ -161,5 +219,5 @@
|
|
|
161
219
|
</div>
|
|
162
220
|
</div>
|
|
163
221
|
</div>
|
|
164
|
-
</
|
|
165
|
-
|
|
222
|
+
</TooltipContent>
|
|
223
|
+
</TooltipPrimitive.Root>
|
|
@@ -67,12 +67,13 @@
|
|
|
67
67
|
data-collapsible={sidebar.state === 'collapsed' ? collapsible : ''}
|
|
68
68
|
data-variant={variant}
|
|
69
69
|
data-side={side}
|
|
70
|
+
data-resizing={sidebar.isResizing ? 'true' : undefined}
|
|
70
71
|
data-slot="sidebar"
|
|
71
72
|
>
|
|
72
73
|
<div
|
|
73
74
|
data-slot="sidebar-gap"
|
|
74
75
|
class={cn(
|
|
75
|
-
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
|
76
|
+
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear group-data-[resizing=true]:transition-none',
|
|
76
77
|
'group-data-[collapsible=offcanvas]:w-0',
|
|
77
78
|
'group-data-[side=right]:rotate-180',
|
|
78
79
|
variant === 'floating' || variant === 'inset'
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
<div
|
|
84
85
|
data-slot="sidebar-container"
|
|
85
86
|
class={cn(
|
|
86
|
-
'fixed inset-y-0 z-
|
|
87
|
+
'fixed inset-y-0 z-50 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear group-data-[resizing=true]:transition-none md:flex',
|
|
87
88
|
side === 'left'
|
|
88
89
|
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
|
89
90
|
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
side = 'top',
|
|
9
9
|
children,
|
|
10
10
|
showArrow = false,
|
|
11
|
-
arrowClasses
|
|
11
|
+
arrowClasses,
|
|
12
|
+
...rest
|
|
12
13
|
}: TooltipPrimitive.ContentProps & {
|
|
13
14
|
showArrow?: boolean
|
|
14
15
|
arrowClasses?: string
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
data-slot="tooltip-content"
|
|
22
23
|
{sideOffset}
|
|
23
24
|
{side}
|
|
25
|
+
{...rest}
|
|
24
26
|
class={cn(
|
|
25
27
|
'bg-background-default-negative border border-border-inverse z-[1002] rounded-md px-2 py-1 text-sm font-medium text-foreground-inverse leading-5 tracking-tight shadow-md',
|
|
26
28
|
className
|