@livestorm/ui-vue 2.0.0 → 2.0.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.
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
  import { Label, useId } from 'reka-ui'
4
+ import UiTooltip from '../UiTooltip/UiTooltip.vue'
4
5
 
5
6
  interface Props {
6
7
  id?: string
@@ -48,13 +49,14 @@ defineExpose({ fieldId })
48
49
  >
49
50
  {{ label }}{{ required ? '*' : '' }}
50
51
  </Label>
51
- <div
52
+ <UiTooltip
52
53
  v-if="labelInfo"
53
- v-tooltip="labelInfo"
54
- class="flex items-center justify-center size-4 text-text-neutral-secondary cursor-help"
54
+ :content="labelInfo"
55
55
  >
56
- <span class="i-ls-info size-4" />
57
- </div>
56
+ <div class="flex items-center justify-center size-4 text-text-neutral-secondary cursor-help">
57
+ <span class="i-ls-info size-4" />
58
+ </div>
59
+ </UiTooltip>
58
60
  </div>
59
61
 
60
62
  <!-- Input -->
@@ -2,6 +2,7 @@
2
2
  import { computed, ref, watch, nextTick } from 'vue'
3
3
  import { Label, useId } from 'reka-ui'
4
4
  import type { CheckboxSize } from './types'
5
+ import UiTooltip from '../UiTooltip/UiTooltip.vue'
5
6
 
6
7
  interface UiInputCheckboxProps {
7
8
  /**
@@ -390,18 +391,21 @@ defineExpose({
390
391
  </Label>
391
392
 
392
393
  <!-- icon -->
393
- <div
394
+ <UiTooltip
394
395
  v-if="labelInfo"
395
- v-tooltip="labelInfo"
396
- class="flex items-center justify-center size-4 text-text-neutral-primary cursor-help shrink-0"
397
- :class="{
398
- 'text-text-neutral-base': !error && !isChecked,
399
- 'text-text-danger-base': error,
400
- 'text-text-primary-base': isChecked && !error,
401
- }"
396
+ :content="labelInfo"
402
397
  >
403
- <span class="i-ls-info-fill size-4" />
404
- </div>
398
+ <div
399
+ class="flex items-center justify-center size-4 text-text-neutral-primary cursor-help shrink-0"
400
+ :class="{
401
+ 'text-text-neutral-base': !error && !isChecked,
402
+ 'text-text-danger-base': error,
403
+ 'text-text-primary-base': isChecked && !error,
404
+ }"
405
+ >
406
+ <span class="i-ls-info-fill size-4" />
407
+ </div>
408
+ </UiTooltip>
405
409
  </div>
406
410
 
407
411
  <span
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from 'vue'
3
+ import UiTooltip from '../UiTooltip/UiTooltip.vue'
3
4
 
4
5
  interface Props {
5
6
  label?: string | null
@@ -21,14 +22,15 @@ withDefaults(defineProps<Props>(), {
21
22
 
22
23
  <template>
23
24
  <div class="mb-1 flex items-center text-content-legends-bold-md text-text-neutral-tertiary">
24
- <label
25
- v-tooltip="tooltip"
26
- :for="forId"
27
- :class="labelClass"
28
- :style="{
29
- color: labelColor,
30
- }"
31
- v-html="required ? `${label}*` : label"
32
- />
25
+ <UiTooltip :content="tooltip">
26
+ <label
27
+ :for="forId"
28
+ :class="labelClass"
29
+ :style="{
30
+ color: labelColor,
31
+ }"
32
+ v-html="required ? `${label}*` : label"
33
+ />
34
+ </UiTooltip>
33
35
  </div>
34
36
  </template>
@@ -1,16 +1,68 @@
1
1
  <!-- UiTooltip.vue -->
2
2
  <script setup lang="ts">
3
- import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
3
+ import { computed, getCurrentInstance } from 'vue'
4
+ import type { TooltipContentProps, TooltipRootEmits, TooltipRootProps } from 'reka-ui'
4
5
  import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
6
+ import { reactiveOmit } from '@vueuse/core'
7
+ import UiTooltipProvider from './UiTooltipProvider.vue'
8
+ import UiTooltipTrigger from './UiTooltipTrigger.vue'
9
+ import UiTooltipContent from './UiTooltipContent.vue'
5
10
 
6
- const props = defineProps<TooltipRootProps>()
11
+ export interface UiTooltipProps extends TooltipRootProps {
12
+ /**
13
+ * Simple mode: the tooltip text. When `:content` is bound, UiTooltip is
14
+ * fully self-contained — the default slot becomes the trigger (merged
15
+ * via as-child) and no surrounding UiTooltipProvider is required:
16
+ *
17
+ * <UiTooltip content="Delete this event"><UiButton … /></UiTooltip>
18
+ *
19
+ * A bound-but-empty content renders the slot untouched (no tooltip, no
20
+ * overhead) — so wrappers can always bind an optional string.
21
+ *
22
+ * When `:content` is NOT bound, UiTooltip is the composable root: use it
23
+ * inside a UiTooltipProvider with UiTooltipTrigger/UiTooltipContent
24
+ * children.
25
+ */
26
+ content?: string | null
27
+ /** Simple mode only: which side the tooltip appears on. */
28
+ side?: TooltipContentProps['side']
29
+ }
30
+
31
+ defineOptions({ inheritAttrs: false })
32
+
33
+ const props = defineProps<UiTooltipProps>()
7
34
  const emits = defineEmits<TooltipRootEmits>()
8
35
 
9
- const forwarded = useForwardPropsEmits(props, emits)
36
+ // Simple vs composed mode is decided by the BINDING, not the value:
37
+ // reka's TooltipRoot requires a provider, so the composed branch must only
38
+ // render when the caller actually composes (no `content` bound).
39
+ const hasContentBinding = computed(() => {
40
+ const vnodeProps = getCurrentInstance()?.vnode.props
41
+ return !!vnodeProps && 'content' in vnodeProps
42
+ })
43
+
44
+ // content/side are ours — only forward the reka root props
45
+ const forwarded = useForwardPropsEmits(reactiveOmit(props, 'content', 'side'), emits)
10
46
  </script>
11
47
 
12
48
  <template>
13
- <TooltipRoot v-bind="{ ...forwarded, ...$attrs }">
49
+ <template v-if="hasContentBinding">
50
+ <UiTooltipProvider v-if="content">
51
+ <TooltipRoot v-bind="{ ...forwarded, ...$attrs }">
52
+ <UiTooltipTrigger as-child>
53
+ <slot />
54
+ </UiTooltipTrigger>
55
+ <UiTooltipContent :side="side">
56
+ {{ content }}
57
+ </UiTooltipContent>
58
+ </TooltipRoot>
59
+ </UiTooltipProvider>
60
+ <slot v-else />
61
+ </template>
62
+ <TooltipRoot
63
+ v-else
64
+ v-bind="{ ...forwarded, ...$attrs }"
65
+ >
14
66
  <slot />
15
67
  </TooltipRoot>
16
68
  </template>
@@ -1,7 +1,83 @@
1
- import { computed } from 'vue'
1
+ import { computed, createApp, h } from 'vue'
2
2
  import { useClipboard, usePermission } from '@vueuse/core'
3
3
  import i18n from 'i18next'
4
- import { createTooltip, destroyTooltip } from 'floating-vue'
4
+
5
+ import UiTooltipProvider from '../components/UiTooltip/UiTooltipProvider.vue'
6
+ import UiTooltip from '../components/UiTooltip/UiTooltip.vue'
7
+ import UiTooltipTrigger from '../components/UiTooltip/UiTooltipTrigger.vue'
8
+ import UiTooltipContent from '../components/UiTooltip/UiTooltipContent.vue'
9
+
10
+ let hideActiveFlash = null
11
+
12
+ /**
13
+ * Transient "Copied!" flash above an element, rendered with the design
14
+ * system's own UiTooltip family so the bubble, arrow and positioning stay
15
+ * identical to every other tooltip. The copy() API is imperative and
16
+ * receives a raw DOM element, so the tooltip cannot wrap the caller's
17
+ * markup: instead a throwaway Vue app drives a controlled-open tooltip
18
+ * whose invisible trigger is overlaid on the element's current rect.
19
+ */
20
+ function flashTooltip (el, text) {
21
+ // A copy triggered before the previous flash ended replaces it instead of
22
+ // stacking a second bubble on top.
23
+ if (hideActiveFlash) {
24
+ hideActiveFlash()
25
+ }
26
+
27
+ const rect = el.getBoundingClientRect()
28
+ const host = document.createElement('div')
29
+ document.body.appendChild(host)
30
+
31
+ const app = createApp({
32
+ render: () => h(UiTooltipProvider, null, () =>
33
+ h(UiTooltip, { open: true, delayDuration: 0 }, () => [
34
+ h(UiTooltipTrigger, { asChild: true }, () =>
35
+ h('span', {
36
+ 'aria-hidden': 'true',
37
+ style: {
38
+ position: 'fixed',
39
+ left: `${rect.left}px`,
40
+ top: `${rect.top}px`,
41
+ width: `${rect.width}px`,
42
+ height: `${rect.height}px`,
43
+ pointerEvents: 'none',
44
+ },
45
+ })),
46
+ h(UiTooltipContent, { side: 'top' }, () => text),
47
+ // The bubble lives in reka's portal where attrs like role are not
48
+ // forwardable; announce the feedback from a visually hidden live
49
+ // region instead so screen readers hear the copy confirmation.
50
+ h('span', {
51
+ role: 'status',
52
+ style: {
53
+ position: 'absolute',
54
+ width: '1px',
55
+ height: '1px',
56
+ overflow: 'hidden',
57
+ clipPath: 'inset(50%)',
58
+ },
59
+ }, text),
60
+ ])),
61
+ })
62
+ app.mount(host)
63
+
64
+ // Idempotent: the replace-guard above may spend a flash's hide before its
65
+ // own copy() timer fires it a second time.
66
+ let hidden = false
67
+ const hide = () => {
68
+ if (hidden) {
69
+ return
70
+ }
71
+ hidden = true
72
+ if (hideActiveFlash === hide) {
73
+ hideActiveFlash = null
74
+ }
75
+ app.unmount()
76
+ host.remove()
77
+ }
78
+ hideActiveFlash = hide
79
+ return hide
80
+ }
5
81
 
6
82
  export function useCopyToClipboard () {
7
83
  const { isSupported, copy: _copy } = useClipboard()
@@ -14,19 +90,10 @@ export function useCopyToClipboard () {
14
90
 
15
91
  if (el) {
16
92
  el.classList.add('copied')
17
- const tooltip = createTooltip(el, {
18
- triggers: [],
19
- content: tooltipText,
20
- delay: 0,
21
- })
22
- tooltip.show()
93
+ const hideTooltip = flashTooltip(el, tooltipText)
23
94
  setTimeout(() => {
24
95
  el.classList.remove('copied')
25
- tooltip.hide()
26
- // Transition
27
- setTimeout(() => {
28
- destroyTooltip(el)
29
- }, 400)
96
+ hideTooltip()
30
97
  }, 600)
31
98
  }
32
99
  }
@@ -1,66 +0,0 @@
1
- import { ref as k, computed as u, watch as r, onUnmounted as S, getCurrentInstance as y, onMounted as g, nextTick as x } from "vue";
2
- import { onKeyDown as w } from "@vueuse/core";
3
- let T = 0;
4
- const a = k([]), b = u(() => a.value.length > 0);
5
- function A(o, s) {
6
- const n = T++, t = u(() => a.value.findIndex((e) => e.id === n));
7
- function f() {
8
- a.value.some((e) => e.id === n) || a.value.push({
9
- id: n,
10
- type: s
11
- });
12
- }
13
- function i() {
14
- t.value !== -1 && a.value.splice(t.value, 1);
15
- }
16
- r(o, (e) => {
17
- e ? f() : i();
18
- }, {
19
- immediate: !0
20
- }), S(() => {
21
- i();
22
- });
23
- const d = u(() => n === a.value[a.value.length - 1]?.id), m = u(() => t.value > 0), c = u(() => a.value.filter((e) => e.type === s)), p = u(() => c.value.findIndex((e) => e.id === n) < c.value.length - 1), h = u(() => c.value.findIndex((e) => e.id === n) > 0), l = [];
24
- function v(e) {
25
- l.push(e);
26
- }
27
- return w("Escape", () => {
28
- d.value && l.forEach((e) => e());
29
- }), {
30
- isActiveStacked: d,
31
- hasStackedBehind: m,
32
- isBehindSameType: p,
33
- hasStackedSameTypeBehind: h,
34
- stackIndex: t,
35
- onClose: v
36
- };
37
- }
38
- typeof document < "u" && r(b, (o) => {
39
- o ? document.documentElement.classList.add("overflow-hidden") : document.documentElement.classList.remove("overflow-hidden");
40
- }, {
41
- immediate: !0
42
- });
43
- function C() {
44
- const o = y();
45
- let s = null;
46
- g(async () => {
47
- await x();
48
- const n = o.proxy.$el;
49
- if (!n?.querySelector) return;
50
- let t = n.querySelector("[data-stackview-autofocus]");
51
- t ? ["INPUT", "A", "BUTTON", "TEXTAREA"].includes(t.tagName) || (t = t.querySelector("input, a, button, textarea") || t) : (t = n.querySelector('button[type="submit"]'), console.warn(
52
- "%cDEV",
53
- "background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;",
54
- "A element should have the data-stackview-autofocus attribute in this component. Fallback to submit button."
55
- )), clearTimeout(s), s = setTimeout(() => t?.focus(), 150);
56
- });
57
- }
58
- function q(o) {
59
- o.preventDefault(), (o.target.closest('[role="menu"]') || o.target).querySelector("a")?.focus();
60
- }
61
- export {
62
- C as a,
63
- q as f,
64
- b as h,
65
- A as u
66
- };