@invopop/popui 0.1.99 → 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,20 +1,12 @@
|
|
|
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
|
-
function portal(node: HTMLElement) {
|
|
9
|
-
const target = typeof document !== 'undefined' ? document.body : null
|
|
10
|
-
if (target) target.appendChild(node)
|
|
11
|
-
return {
|
|
12
|
-
destroy() {
|
|
13
|
-
if (node.parentNode) node.parentNode.removeChild(node)
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
10
|
let {
|
|
19
11
|
ref = $bindable(null),
|
|
20
12
|
class: className,
|
|
@@ -29,50 +21,77 @@
|
|
|
29
21
|
let dragMoved = false
|
|
30
22
|
let dragDirection: 1 | -1 = 1
|
|
31
23
|
|
|
32
|
-
let isHovering = $state(false)
|
|
33
24
|
let isDragging = $state(false)
|
|
34
|
-
let
|
|
35
|
-
let
|
|
36
|
-
let
|
|
37
|
-
let tooltipWidth = $state(0)
|
|
38
|
-
let tooltipHeight = $state(0)
|
|
25
|
+
let tooltipOpen = $state(false)
|
|
26
|
+
let cursorX = $state(0)
|
|
27
|
+
let cursorY = $state(0)
|
|
39
28
|
|
|
29
|
+
const TOOLTIP_HOVER_DELAY_MS = 700
|
|
40
30
|
const TOOLTIP_CURSOR_OFFSET = 12
|
|
41
|
-
const TOOLTIP_VIEWPORT_PADDING = 8
|
|
42
|
-
|
|
43
|
-
let tooltipLeft = $derived.by(() => {
|
|
44
|
-
if (tooltipWidth === 0 || typeof window === 'undefined') return tooltipX
|
|
45
|
-
const desired = tooltipX - tooltipWidth / 2
|
|
46
|
-
const minX = TOOLTIP_VIEWPORT_PADDING
|
|
47
|
-
const maxX = window.innerWidth - tooltipWidth - TOOLTIP_VIEWPORT_PADDING
|
|
48
|
-
return Math.max(minX, Math.min(maxX, desired))
|
|
49
|
-
})
|
|
50
31
|
|
|
51
|
-
let
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
56
38
|
})
|
|
57
39
|
|
|
58
|
-
function updateTooltipPosition(e: PointerEvent) {
|
|
59
|
-
tooltipX = e.clientX
|
|
60
|
-
tooltipY = e.clientY
|
|
61
|
-
}
|
|
62
|
-
|
|
63
40
|
function onPointerEnter(e: PointerEvent) {
|
|
64
41
|
const sidebarRoot = (e.currentTarget as HTMLElement).closest('[data-slot="sidebar"]')
|
|
65
42
|
dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1
|
|
66
|
-
|
|
67
|
-
|
|
43
|
+
cursorX = e.clientX
|
|
44
|
+
cursorY = e.clientY
|
|
68
45
|
}
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
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)
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
function onPointerDown(e: PointerEvent) {
|
|
75
|
-
if (sidebar.
|
|
94
|
+
if (sidebar.isMobile) return
|
|
76
95
|
const button = e.currentTarget as HTMLButtonElement
|
|
77
96
|
const sidebarRoot = button.closest('[data-slot="sidebar"]')
|
|
78
97
|
dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1
|
|
@@ -86,77 +105,105 @@
|
|
|
86
105
|
document.body.style.userSelect = 'none'
|
|
87
106
|
}
|
|
88
107
|
|
|
89
|
-
function onPointerMove(e: PointerEvent) {
|
|
90
|
-
if (tooltipVisible) updateTooltipPosition(e)
|
|
91
|
-
const button = e.currentTarget as HTMLButtonElement
|
|
92
|
-
if (!button.hasPointerCapture(e.pointerId)) return
|
|
93
|
-
const delta = (e.clientX - dragStartX) * dragDirection
|
|
94
|
-
if (Math.abs(delta) > SIDEBAR_DRAG_THRESHOLD_PX) {
|
|
95
|
-
dragMoved = true
|
|
96
|
-
isDragging = true
|
|
97
|
-
}
|
|
98
|
-
if (dragMoved) sidebar.setWidth(dragStartWidthPx + delta)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
108
|
function onPointerUp(e: PointerEvent) {
|
|
102
109
|
const button = e.currentTarget as HTMLButtonElement
|
|
103
110
|
if (button.hasPointerCapture(e.pointerId)) button.releasePointerCapture(e.pointerId)
|
|
104
111
|
document.body.style.cursor = ''
|
|
105
112
|
document.body.style.userSelect = ''
|
|
106
113
|
isDragging = false
|
|
114
|
+
sidebar.isResizing = false
|
|
115
|
+
if (dragMoved) {
|
|
116
|
+
dragEndTime = Date.now()
|
|
117
|
+
dragMoved = false
|
|
118
|
+
}
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
const DOUBLE_CLICK_DELAY_MS = 300
|
|
122
|
+
let pendingClickTimer: ReturnType<typeof setTimeout> | undefined
|
|
123
|
+
|
|
109
124
|
function onClick(e: MouseEvent) {
|
|
110
|
-
if (
|
|
125
|
+
if (Date.now() - dragEndTime < POST_DRAG_CLICK_GUARD_MS) {
|
|
111
126
|
e.preventDefault()
|
|
112
127
|
e.stopPropagation()
|
|
113
|
-
dragMoved = false
|
|
114
128
|
return
|
|
115
129
|
}
|
|
116
|
-
|
|
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()
|
|
117
143
|
}
|
|
118
144
|
</script>
|
|
119
145
|
|
|
120
|
-
<
|
|
121
|
-
bind:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
aria-label="Toggle Sidebar"
|
|
125
|
-
tabindex={-1}
|
|
126
|
-
type="button"
|
|
127
|
-
onpointerenter={onPointerEnter}
|
|
128
|
-
onpointerleave={onPointerLeave}
|
|
129
|
-
onpointerdown={onPointerDown}
|
|
130
|
-
onpointermove={onPointerMove}
|
|
131
|
-
onpointerup={onPointerUp}
|
|
132
|
-
onpointercancel={onPointerUp}
|
|
133
|
-
onclick={onClick}
|
|
134
|
-
class={cn(
|
|
135
|
-
'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',
|
|
136
|
-
'after:absolute after:inset-y-0 after:left-1/2 after:w-[2px]',
|
|
137
|
-
'hover:after:bg-sidebar-border',
|
|
138
|
-
'group-data-[side=left]:cursor-w-resize group-data-[side=right]:cursor-e-resize',
|
|
139
|
-
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
|
140
|
-
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
|
141
|
-
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
|
142
|
-
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
|
143
|
-
className
|
|
144
|
-
)}
|
|
145
|
-
{...restProps}
|
|
146
|
+
<TooltipPrimitive.Root
|
|
147
|
+
bind:open={tooltipOpen}
|
|
148
|
+
delayDuration={TOOLTIP_HOVER_DELAY_MS}
|
|
149
|
+
disableHoverableContent
|
|
146
150
|
>
|
|
147
|
-
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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"
|
|
160
207
|
>
|
|
161
208
|
<div class="flex flex-col gap-1.5">
|
|
162
209
|
{#if sidebar.state === 'expanded'}
|
|
@@ -172,5 +219,5 @@
|
|
|
172
219
|
</div>
|
|
173
220
|
</div>
|
|
174
221
|
</div>
|
|
175
|
-
</
|
|
176
|
-
|
|
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
|