@livestorm/ui-vue 2.0.0 → 2.0.2

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,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,9 +1,75 @@
1
1
  import { reactive, computed } from 'vue'
2
2
  import generateFlags from './devices'
3
+ import { BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL_PLUS } from './breakpoints'
3
4
 
4
5
  let responsive
5
6
 
7
+ // Under SSR there is no viewport: the server renders a desktop-sized static
8
+ // snapshot. To keep hydration safe, the client's lazy singleton starts from
9
+ // the SAME snapshot (identical trees during the hydration pass) and syncs to
10
+ // the real viewport in a microtask, which runs after hydration completes but
11
+ // before the first paint.
12
+ const SSR_WIDTH = 1280
13
+ const SSR_HEIGHT = 800
14
+
15
+ // Default flags, derived from the design system breakpoints. An app installing
16
+ // the Responsive plugin can override any of them via `options.computed`.
17
+ const DEFAULT_FLAGS = {
18
+ mobile: state => state.width < BREAKPOINT_MD,
19
+ tablet: state => state.width < BREAKPOINT_LG,
20
+ smallDesktop: state => state.width < BREAKPOINT_XL_PLUS,
21
+ desktop: state => state.width >= BREAKPOINT_LG,
22
+ }
23
+
24
+ function createResponsive (customComputed = {}, { deferViewportSync = false } = {}) {
25
+ const isClient = typeof window !== 'undefined'
26
+ const readViewportNow = isClient && !deferViewportSync
27
+
28
+ const state = reactive({
29
+ width: readViewportNow ? window.innerWidth : SSR_WIDTH,
30
+ height: readViewportNow ? window.innerHeight : SSR_HEIGHT,
31
+ })
32
+
33
+ const derived = { ...DEFAULT_FLAGS, ...customComputed }
34
+ for (const key in derived) {
35
+ // Assigning a computed ref onto a reactive object stores it unwrapped:
36
+ // consumers read plain booleans while reactivity is preserved.
37
+ state[key] = computed(() => derived[key](state))
38
+ }
39
+
40
+ if (isClient) {
41
+ const sync = () => {
42
+ state.width = window.innerWidth
43
+ state.height = window.innerHeight
44
+ }
45
+ if (deferViewportSync) {
46
+ // Runs after the synchronous hydration pass but before the first
47
+ // paint: server HTML and client hydration tree stay identical, then a
48
+ // normal reactive update applies the real viewport with no visible
49
+ // flash for client-only apps either.
50
+ queueMicrotask(sync)
51
+ }
52
+ window.addEventListener('resize', sync)
53
+ }
54
+
55
+ return state
56
+ }
57
+
58
+ /**
59
+ * Viewport state shared by the whole app. Lazily created on first use, so
60
+ * components work with no plugin registration: installing the Responsive
61
+ * plugin is only needed to customize the flags or to read `$responsive`
62
+ * in templates.
63
+ *
64
+ * @returns {import('./responsive.types').ResponsiveGlobalComponentProperty}
65
+ */
6
66
  export function useResponsive () {
67
+ if (!responsive) {
68
+ // Deferred viewport sync keeps SSR hydration safe; the plugin path below
69
+ // keeps its historical immediate initialization (it installs client-side
70
+ // at app boot, before anything rendered).
71
+ responsive = createResponsive({}, { deferViewportSync: true })
72
+ }
7
73
  return responsive
8
74
  }
9
75
 
@@ -13,11 +79,6 @@ export default {
13
79
  computed: {},
14
80
  }, options)
15
81
 
16
- const computedProps = {}
17
- for (const key in finalOptions.computed) {
18
- computedProps[key] = computed(() => finalOptions.computed[key](responsive))
19
- }
20
-
21
82
  if (import.meta.env.RESPONSIVE_V2) {
22
83
  let device
23
84
  const defaultUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36'
@@ -44,18 +105,11 @@ export default {
44
105
  }
45
106
  app.config.globalProperties.$responsive = responsive
46
107
  } else {
47
- responsive = reactive({
48
- width: window.innerWidth,
49
- height: window.innerHeight,
50
- ...computedProps,
51
- })
52
-
108
+ // The plugin-created instance replaces the lazy singleton so that
109
+ // useResponsive() and $responsive stay the same object, with the
110
+ // app's custom computed applied on top of the default flags.
111
+ responsive = createResponsive(finalOptions.computed)
53
112
  app.config.globalProperties.$responsive = responsive
54
-
55
- window.addEventListener('resize', () => {
56
- responsive.width = window.innerWidth
57
- responsive.height = window.innerHeight
58
- })
59
113
  }
60
114
  },
61
115
  }
@@ -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
- };