@rokkit/ui 1.0.0-next.153 → 1.0.0-next.154
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/package.json +1 -1
- package/src/components/Dropdown.svelte +1 -1
- package/src/components/Menu.svelte +1 -1
- package/src/components/MultiSelect.svelte +1 -1
- package/src/components/Select.svelte +1 -1
- package/src/components/Tooltip.svelte +121 -0
- package/src/components/UploadTarget.svelte +6 -2
- package/src/components/index.ts +1 -0
- package/src/index.ts +2 -1
- package/src/types/index.ts +3 -0
- package/src/types/tooltip.ts +21 -0
- package/src/types/upload-target.ts +3 -0
package/package.json
CHANGED
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
$effect(() => {
|
|
151
151
|
if (!isOpen || !dropdownRef) return
|
|
152
152
|
const dir = getComputedStyle(dropdownRef).direction || 'ltr'
|
|
153
|
-
const nav = new Navigator(dropdownRef, wrapper, { collapsible, dir })
|
|
153
|
+
const nav = new Navigator(dropdownRef, wrapper, { collapsible, dir, containScroll: true })
|
|
154
154
|
return () => nav.destroy()
|
|
155
155
|
})
|
|
156
156
|
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
$effect(() => {
|
|
205
205
|
if (!isOpen || !dropdownRef) return
|
|
206
206
|
const dir = getComputedStyle(dropdownRef).direction || 'ltr'
|
|
207
|
-
const nav = new Navigator(dropdownRef, wrapper, { dir })
|
|
207
|
+
const nav = new Navigator(dropdownRef, wrapper, { dir, containScroll: true })
|
|
208
208
|
return () => nav.destroy()
|
|
209
209
|
})
|
|
210
210
|
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
$effect(() => {
|
|
251
251
|
if (!isOpen || !dropdownRef) return
|
|
252
252
|
const dir = getComputedStyle(dropdownRef).direction || 'ltr'
|
|
253
|
-
const nav = new Navigator(dropdownRef, wrapper, { dir })
|
|
253
|
+
const nav = new Navigator(dropdownRef, wrapper, { dir, containScroll: true })
|
|
254
254
|
return () => nav.destroy()
|
|
255
255
|
})
|
|
256
256
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Tooltip — contextual text revealed on hover or focus.
|
|
4
|
+
*
|
|
5
|
+
* Wraps any trigger element. Tooltip appears after a configurable delay
|
|
6
|
+
* on mouseenter/focusin, hides immediately on mouseleave/focusout/Escape.
|
|
7
|
+
* Position auto-flips when the preferred side would overflow the viewport.
|
|
8
|
+
*
|
|
9
|
+
* Data attributes:
|
|
10
|
+
* data-tooltip-root — wrapper element (position: relative)
|
|
11
|
+
* data-tooltip-content — tooltip bubble
|
|
12
|
+
* data-tooltip-position — actual position after auto-flip
|
|
13
|
+
* data-tooltip-visible — "true" while visible
|
|
14
|
+
*/
|
|
15
|
+
import type { TooltipProps } from '../types/tooltip.js'
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
content = '',
|
|
19
|
+
position = 'top',
|
|
20
|
+
delay = 300,
|
|
21
|
+
class: className,
|
|
22
|
+
children,
|
|
23
|
+
tooltipContent
|
|
24
|
+
}: TooltipProps = $props()
|
|
25
|
+
|
|
26
|
+
const id = `tt-${Math.random().toString(36).slice(2, 9)}`
|
|
27
|
+
let visible = $state(false)
|
|
28
|
+
// positionOverride is set by auto-flip during show; null resets to the position prop
|
|
29
|
+
let positionOverride = $state<typeof position | null>(null)
|
|
30
|
+
const resolvedPosition = $derived(positionOverride ?? position)
|
|
31
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
32
|
+
let rootEl = $state<HTMLElement | null>(null)
|
|
33
|
+
let tooltipEl = $state<HTMLElement | null>(null)
|
|
34
|
+
|
|
35
|
+
const GAP = 6
|
|
36
|
+
|
|
37
|
+
function resolveFlip(
|
|
38
|
+
triggerRect: DOMRect,
|
|
39
|
+
tooltipRect: DOMRect,
|
|
40
|
+
preferred: typeof position
|
|
41
|
+
): typeof position {
|
|
42
|
+
const vw = window.innerWidth
|
|
43
|
+
const vh = window.innerHeight
|
|
44
|
+
const fits: Record<string, boolean> = {
|
|
45
|
+
top: triggerRect.top >= tooltipRect.height + GAP,
|
|
46
|
+
bottom: triggerRect.bottom + tooltipRect.height + GAP <= vh,
|
|
47
|
+
left: triggerRect.left >= tooltipRect.width + GAP,
|
|
48
|
+
right: triggerRect.right + tooltipRect.width + GAP <= vw
|
|
49
|
+
}
|
|
50
|
+
if (fits[preferred]) return preferred
|
|
51
|
+
const flip: Record<string, typeof position> = {
|
|
52
|
+
top: 'bottom',
|
|
53
|
+
bottom: 'top',
|
|
54
|
+
left: 'right',
|
|
55
|
+
right: 'left'
|
|
56
|
+
}
|
|
57
|
+
if (fits[flip[preferred]]) return flip[preferred]
|
|
58
|
+
return (Object.keys(fits).find((p) => fits[p]) as typeof position) ?? preferred
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function updatePosition() {
|
|
62
|
+
if (!rootEl || !tooltipEl) return
|
|
63
|
+
const triggerRect = rootEl.getBoundingClientRect()
|
|
64
|
+
const tooltipRect = tooltipEl.getBoundingClientRect()
|
|
65
|
+
positionOverride = resolveFlip(triggerRect, tooltipRect, position)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function show() {
|
|
69
|
+
updatePosition()
|
|
70
|
+
visible = true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hide() {
|
|
74
|
+
if (timer) clearTimeout(timer)
|
|
75
|
+
visible = false
|
|
76
|
+
positionOverride = null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onMouseEnter() {
|
|
80
|
+
timer = setTimeout(show, delay)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onMouseLeave() {
|
|
84
|
+
if (timer) clearTimeout(timer)
|
|
85
|
+
hide()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onKeydown(e: KeyboardEvent) {
|
|
89
|
+
if (e.key === 'Escape') hide()
|
|
90
|
+
}
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
94
|
+
<span
|
|
95
|
+
data-tooltip-root
|
|
96
|
+
role="group"
|
|
97
|
+
aria-describedby={id}
|
|
98
|
+
class={className || undefined}
|
|
99
|
+
onmouseenter={onMouseEnter}
|
|
100
|
+
onmouseleave={onMouseLeave}
|
|
101
|
+
onfocusin={show}
|
|
102
|
+
onfocusout={hide}
|
|
103
|
+
onkeydown={onKeydown}
|
|
104
|
+
bind:this={rootEl}
|
|
105
|
+
>
|
|
106
|
+
{@render children?.()}
|
|
107
|
+
<div
|
|
108
|
+
data-tooltip-content
|
|
109
|
+
data-tooltip-position={resolvedPosition}
|
|
110
|
+
data-tooltip-visible={visible ? 'true' : 'false'}
|
|
111
|
+
{id}
|
|
112
|
+
role="tooltip"
|
|
113
|
+
bind:this={tooltipEl}
|
|
114
|
+
>
|
|
115
|
+
{#if tooltipContent}
|
|
116
|
+
{@render tooltipContent()}
|
|
117
|
+
{:else}
|
|
118
|
+
{content}
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
121
|
+
</span>
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
maxSize = Infinity,
|
|
22
22
|
multiple = false,
|
|
23
23
|
disabled = false,
|
|
24
|
+
size = 'full',
|
|
24
25
|
labels = {} as Record<string, string>,
|
|
25
26
|
onfiles,
|
|
26
27
|
onerror,
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
|
|
88
89
|
<div
|
|
89
90
|
data-upload-target
|
|
91
|
+
data-size={size}
|
|
90
92
|
data-disabled={disabled || undefined}
|
|
91
93
|
data-dragging={dragging || undefined}
|
|
92
94
|
role="button"
|
|
@@ -106,8 +108,10 @@
|
|
|
106
108
|
{@render content(dragging)}
|
|
107
109
|
{:else}
|
|
108
110
|
<span data-upload-icon class="i-lucide:upload" aria-hidden="true"></span>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
{#if size !== 'small'}
|
|
112
|
+
<p>Drop files here or click to browse</p>
|
|
113
|
+
<button type="button" data-upload-button {disabled}>Browse</button>
|
|
114
|
+
{/if}
|
|
111
115
|
{/if}
|
|
112
116
|
|
|
113
117
|
<input
|
package/src/components/index.ts
CHANGED
|
@@ -45,3 +45,4 @@ export { default as Divider } from './Divider.svelte'
|
|
|
45
45
|
export { default as Stack } from './Stack.svelte'
|
|
46
46
|
export { default as Badge } from './Badge.svelte'
|
|
47
47
|
export { default as Avatar } from './Avatar.svelte'
|
|
48
|
+
export { default as Tooltip } from './Tooltip.svelte'
|
package/src/index.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte'
|
|
2
|
+
|
|
3
|
+
export interface TooltipProps {
|
|
4
|
+
/** Tooltip text (alternative: use tooltipContent snippet for rich content) */
|
|
5
|
+
content?: string
|
|
6
|
+
|
|
7
|
+
/** Preferred position — auto-flips when the preferred side overflows the viewport */
|
|
8
|
+
position?: 'top' | 'bottom' | 'left' | 'right'
|
|
9
|
+
|
|
10
|
+
/** Show delay in ms (default: 300) */
|
|
11
|
+
delay?: number
|
|
12
|
+
|
|
13
|
+
/** Additional CSS classes on the root wrapper */
|
|
14
|
+
class?: string
|
|
15
|
+
|
|
16
|
+
/** Trigger element(s) */
|
|
17
|
+
children?: Snippet
|
|
18
|
+
|
|
19
|
+
/** Rich tooltip content — overrides the content string */
|
|
20
|
+
tooltipContent?: Snippet
|
|
21
|
+
}
|
|
@@ -60,6 +60,9 @@ export interface UploadTargetProps {
|
|
|
60
60
|
/** Called for each file that fails validation */
|
|
61
61
|
onerror?: (err: UploadError) => void
|
|
62
62
|
|
|
63
|
+
/** Size variant — full (default) fills available area; small shows only border + icon */
|
|
64
|
+
size?: 'full' | 'small'
|
|
65
|
+
|
|
63
66
|
/** Additional CSS classes */
|
|
64
67
|
class?: string
|
|
65
68
|
|