@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/ui",
3
- "version": "1.0.0-next.153",
3
+ "version": "1.0.0-next.154",
4
4
  "description": "Data driven UI components for Rokkit applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -120,7 +120,7 @@
120
120
 
121
121
  $effect(() => {
122
122
  if (!isOpen || !panelRef) return
123
- const nav = new Navigator(panelRef, wrapper, {})
123
+ const nav = new Navigator(panelRef, wrapper, { containScroll: true })
124
124
  return () => nav.destroy()
125
125
  })
126
126
 
@@ -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
- <p>Drop files here or click to browse</p>
110
- <button type="button" data-upload-button {disabled}>Browse</button>
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
@@ -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
@@ -44,7 +44,8 @@ export {
44
44
  Divider,
45
45
  Stack,
46
46
  Badge,
47
- Avatar
47
+ Avatar,
48
+ Tooltip
48
49
  } from './components/index.js'
49
50
 
50
51
  export { default as MarkdownRenderer } from './MarkdownRenderer.svelte'
@@ -53,3 +53,6 @@ export * from './floating-navigation.js'
53
53
  export * from './upload-target.js'
54
54
  export * from './upload-file-status.js'
55
55
  export * from './upload-progress.js'
56
+
57
+ // Tooltip Types
58
+ export * from './tooltip.js'
@@ -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