@leavepulse/ui 0.78.0 → 0.78.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.
- package/dist/chunks/{DropzoneLinkField-DXIr0eFz.js → DropzoneLinkField-CLLhd3WB.js} +1 -1
- package/dist/chunks/FieldLeading-mr81aEAP.js +27 -0
- package/dist/chunks/{LpAddressInput-ZqoZ-N2Y.js → LpAddressInput-BSS49jpm.js} +2 -2
- package/dist/chunks/{LpAppShell-BNAVTQsY.js → LpAppShell-BKbYCHkx.js} +23 -4
- package/dist/chunks/{LpCodeBlock-DuzmnIHl.js → LpCodeBlock-DBqy4RLd.js} +1 -1
- package/dist/chunks/{LpCodeDisplay-CO8fngxa.js → LpCodeDisplay-D3BUAhV1.js} +2 -2
- package/dist/chunks/{LpColorPicker-BxAHEnR5.js → LpColorPicker-COHzV68R.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-6oCIJTDa.js → LpConfirmDialog-CnrmzkuN.js} +1 -1
- package/dist/chunks/{LpConfirmHost-DgHvfT6s.js → LpConfirmHost-CwbObVGH.js} +1 -1
- package/dist/chunks/{LpContextMenu--eMGIql6.js → LpContextMenu-Bx_IhQDS.js} +5 -5
- package/dist/chunks/{LpCopyButton-7EOSIuwa.js → LpCopyButton-sAMvWeLP.js} +1 -1
- package/dist/chunks/{LpDataList-DgBeM01y.js → LpDataList-CaVfsozH.js} +2 -2
- package/dist/chunks/{LpDrawer-Bt9UUqt_.js → LpDrawer-DXNQFpAA.js} +1 -1
- package/dist/chunks/{LpFileTree-BSx1kBoV.js → LpFileTree-C-rJKzEQ.js} +3 -3
- package/dist/chunks/{LpFileTreeNode-4dVmFHb3.js → LpFileTreeNode-BgYn4U1J.js} +1 -1
- package/dist/chunks/{LpHoverCard-DanH3Nz1.js → LpHoverCard-CHE9jAJG.js} +6 -6
- package/dist/chunks/{LpInfraNode-BAU40Km2.js → LpInfraNode-Bb5nbuFY.js} +17 -0
- package/dist/chunks/{LpInput-DL6HEJdi.js → LpInput-D0O6dpuA.js} +18 -21
- package/dist/chunks/{LpInspector-CuoMAhiw.js → LpInspector-DO3fTCQ1.js} +2 -2
- package/dist/chunks/{LpLaneNode-HtnQf2FF.js → LpLaneNode-Bqcdm8Rf.js} +19 -1
- package/dist/chunks/{LpLightbox-xvpQ64MM.js → LpLightbox-CtDTQV01.js} +1 -1
- package/dist/chunks/{LpLogInspector-6cNnXqp4.js → LpLogInspector-BdQlX6S-.js} +1 -1
- package/dist/chunks/{LpLogViewer-Dvb1E9_G.js → LpLogViewer-umCZBapN.js} +4 -4
- package/dist/chunks/{LpMediaGallery-Df-tiWeq.js → LpMediaGallery-BYEwySoF.js} +2 -2
- package/dist/chunks/{LpModal-DrtQ_1A2.js → LpModal-DDvrSAnf.js} +1 -1
- package/dist/chunks/{LpModalNav-DkmGCnZE.js → LpModalNav-gOi1Giz1.js} +2 -2
- package/dist/chunks/{LpNotificationBell-D_wklO1m.js → LpNotificationBell-DUbVyK6R.js} +3 -3
- package/dist/chunks/{LpNumberField-CNHsmSDZ.js → LpNumberField-Csg-1Zh_.js} +20 -6
- package/dist/chunks/LpPageScrollbar-D2O0xhJw.js +164 -0
- package/dist/chunks/{LpPaletteTheme-dVtxHx-F.js → LpPaletteTheme-BIXpZLGe.js} +2 -2
- package/dist/chunks/{LpPasswordInput-B-zD6tUS.js → LpPasswordInput-D4HPhqfV.js} +2 -2
- package/dist/chunks/{LpPie-9t_guO49.js → LpPie-oVNHjsKD.js} +2 -2
- package/dist/chunks/{LpPipeline-rjXvEDRJ.js → LpPipeline-BSXT6oUJ.js} +1 -1
- package/dist/chunks/{LpRelativeTime-BovUk8MP.js → LpRelativeTime-0zI-44ve.js} +1 -1
- package/dist/chunks/{LpScrollArea-ay4Xo_hg.js → LpScrollArea-CqvwfZs2.js} +9 -12
- package/dist/chunks/{LpSecretField-DBvU6iAw.js → LpSecretField-B2roc4Iz.js} +4 -4
- package/dist/chunks/{LpServiceNode-D8uJ8sGk.js → LpServiceNode-Cln2ntdw.js} +17 -0
- package/dist/chunks/{LpSidebar-kSkpPh3-.js → LpSidebar-DDNJzb69.js} +4 -4
- package/dist/chunks/{LpSidebarNav-C2mvqrY2.js → LpSidebarNav-CE0jQS8i.js} +2 -2
- package/dist/chunks/{LpSidebarRow-eRP4Q31q.js → LpSidebarRow-DOTg1txp.js} +2 -2
- package/dist/chunks/{LpStat-pMNZgFcq.js → LpStat-DPjc6_fX.js} +17 -4
- package/dist/chunks/{LpTableFilter-DgARTsA1.js → LpTableFilter-B_XMOeAh.js} +1 -1
- package/dist/chunks/{LpThemeStudio-DzYfE-5k.js → LpThemeStudio-ChU0_Ga8.js} +2 -2
- package/dist/chunks/{LpThemeSwitcher-CFLkdbBL.js → LpThemeSwitcher-DmCZ21P_.js} +2 -2
- package/dist/chunks/{LpToaster-QFj0P-5m.js → LpToaster-Ct_ZOhCh.js} +1 -1
- package/dist/chunks/{LpTooltip-BJf3lZMq.js → LpTooltip-Bz_rrq5S.js} +5 -5
- package/dist/chunks/{LpTopologyCanvas-BOHHHZ_6.js → LpTopologyCanvas-jqz1KCq2.js} +14 -9
- package/dist/chunks/{LpTruncate-C6u3gjc6.js → LpTruncate-ellq9RYM.js} +2 -2
- package/dist/chunks/{LpUptimeBar-BnYmR2rj.js → LpUptimeBar-BoxcCDVY.js} +1 -1
- package/dist/chunks/{LpWorkspaceSwitcher-D0e9upAy.js → LpWorkspaceSwitcher-BeNHCLd5.js} +2 -2
- package/dist/chunks/{RevealToggle-CCPDE8Mv.js → RevealToggle-D_iyrB8d.js} +1 -1
- package/dist/component-names.d.ts +1 -1
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- package/dist/components/FieldLeading.vue.d.ts +16 -0
- package/dist/components/FieldLeading.vue.js +4 -0
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.d.ts +15 -0
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpChart.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpCopyButton.vue.js +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDock.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpHoverCard.vue.js +1 -1
- package/dist/components/LpInfraNode.vue.d.ts +11 -2
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpInspector.vue.js +1 -1
- package/dist/components/LpLaneNode.vue.d.ts +2 -3
- package/dist/components/LpLaneNode.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogInspector.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.d.ts +13 -2
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpPageScrollbar.vue.d.ts +10 -0
- package/dist/components/LpPageScrollbar.vue.js +4 -0
- package/dist/components/LpPaletteTheme.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpRelativeTime.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpServiceNode.vue.d.ts +8 -2
- package/dist/components/LpServiceNode.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSidebarRow.vue.js +1 -1
- package/dist/components/LpStat.vue.d.ts +16 -0
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpTable.vue.js +4 -4
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.d.ts +2 -2
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpTruncate.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
- package/dist/components/RevealToggle.vue.js +1 -1
- package/dist/components/menuWhen.js +1 -1
- package/dist/components/scrollbar.d.ts +34 -0
- package/dist/components/scrollbar.js +47 -0
- package/dist/composables/propCheck.d.ts +1 -1
- package/dist/composables/propCheck.js +15 -4
- package/dist/composables/usePaletteCommands.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +162 -160
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/FieldLeading.vue +17 -0
- package/src/components/LpAppShell.vue +23 -0
- package/src/components/LpContextMenu.vue +7 -0
- package/src/components/LpHoverCard.vue +7 -0
- package/src/components/LpInfraNode.vue +4 -3
- package/src/components/LpInput.vue +4 -3
- package/src/components/LpLaneNode.vue +5 -1
- package/src/components/LpNumberField.vue +7 -1
- package/src/components/LpPageScrollbar.vue +195 -0
- package/src/components/LpScrollArea.vue +27 -23
- package/src/components/LpServiceNode.vue +4 -2
- package/src/components/LpStat.vue +25 -5
- package/src/components/LpTooltip.vue +7 -0
- package/src/components/LpTopologyCanvas.vue +3 -1
- package/src/components/scrollbar.ts +71 -0
- package/src/composables/propCheck.ts +20 -5
- package/src/composables/usePaletteCommands.ts +5 -2
- package/src/index.ts +1 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `@source` over the kit emits these classes — scoped CSS in a published
|
|
9
9
|
* component doesn't reach apps that only import the kit's token css.
|
|
10
10
|
*/
|
|
11
|
-
import { Handle, Position } from "@vue-flow/core"
|
|
11
|
+
import { Handle, Position, type NodeProps } from "@vue-flow/core"
|
|
12
12
|
import { computed } from "vue"
|
|
13
13
|
import LpBadge from "./LpBadge.vue"
|
|
14
14
|
import LpStatus from "./LpStatus.vue"
|
|
@@ -66,8 +66,9 @@ export interface InfraNodeLabels {
|
|
|
66
66
|
old?: string
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// Vue Flow hands a custom node all of NodeProps. Declared, they stop falling
|
|
70
|
+
// through onto the node's root as attributes (position="[object Object]").
|
|
71
|
+
const props = defineProps<Omit<NodeProps<InfraNodeData>, "selected"> & {
|
|
71
72
|
selected?: boolean
|
|
72
73
|
/** Extra/overriding role styles, merged over the defaults below. */
|
|
73
74
|
roles?: Record<string, InfraRoleStyle>
|
|
@@ -13,6 +13,7 @@ import { useInputFilter } from "../composables/useInputFilter"
|
|
|
13
13
|
import { DEFAULT_MASK_TOKENS, useInputMask, type MaskSpec, type MaskTokens } from "../composables/useInputMask"
|
|
14
14
|
import { mergeClass } from "../composables/useMergedClass"
|
|
15
15
|
import FieldWrapper from "./FieldWrapper.vue"
|
|
16
|
+
import FieldLeading from "./FieldLeading.vue"
|
|
16
17
|
import LpIcon from "./LpIcon.vue"
|
|
17
18
|
|
|
18
19
|
// Single source of truth for the field shell — bare input and adorned input
|
|
@@ -189,9 +190,9 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
|
|
|
189
190
|
:class="mergeClass(shellClass, hasLeading ? 'pl-2.5' : '', hasTrailing ? 'pr-1.5' : '', hasField ? '' : (attrs.class as string))"
|
|
190
191
|
:style="hasField ? undefined : attrs.style"
|
|
191
192
|
>
|
|
192
|
-
<
|
|
193
|
-
<
|
|
194
|
-
</
|
|
193
|
+
<FieldLeading :icon="icon">
|
|
194
|
+
<template v-if="$slots.leading" #default><slot name="leading" /></template>
|
|
195
|
+
</FieldLeading>
|
|
195
196
|
<input
|
|
196
197
|
ref="inputEl"
|
|
197
198
|
v-bind="inputAttrs"
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* tab stays clickable-looking but is inert. Tailwind utilities only so styles
|
|
11
11
|
* reach consumer apps via @source.
|
|
12
12
|
*/
|
|
13
|
+
import type { NodeProps } from "@vue-flow/core"
|
|
14
|
+
|
|
13
15
|
export interface LaneNodeData {
|
|
14
16
|
label: string
|
|
15
17
|
/** Optional accent colour token for the lane border + label. */
|
|
@@ -18,7 +20,9 @@ export interface LaneNodeData {
|
|
|
18
20
|
height: number
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
// Vue Flow hands a custom node all of NodeProps. Declared, they stop falling
|
|
24
|
+
// through onto the node's root as attributes (position="[object Object]").
|
|
25
|
+
const props = defineProps<NodeProps<LaneNodeData>>()
|
|
22
26
|
</script>
|
|
23
27
|
|
|
24
28
|
<template>
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
NumberFieldRoot,
|
|
17
17
|
} from "reka-ui"
|
|
18
18
|
import { computed, useId } from "vue"
|
|
19
|
+
import FieldLeading from "./FieldLeading.vue"
|
|
19
20
|
import LpIcon from "./LpIcon.vue"
|
|
20
21
|
import { focusLeft, useAttrsField, mergeDescribedBy } from "../composables/useField"
|
|
21
22
|
import { useLabels } from "../composables/useLpConfig"
|
|
@@ -67,6 +68,8 @@ const props = withDefaults(defineProps<{
|
|
|
67
68
|
// "Depth", "Weight" tells the reader nothing about which one wants grams.
|
|
68
69
|
// Not part of the value: it is never typed, selected, or submitted.
|
|
69
70
|
unit?: string
|
|
71
|
+
/** A leading icon, as on LpInput; the `leading` slot replaces it. */
|
|
72
|
+
icon?: string
|
|
70
73
|
invalid?: boolean
|
|
71
74
|
/** Control height, matching LpInput. */
|
|
72
75
|
size?: "sm" | "md" | "lg"
|
|
@@ -151,6 +154,9 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
|
|
|
151
154
|
@update:model-value="(v) => emit('update:modelValue', Number.isNaN(v) ? null : (v ?? null))"
|
|
152
155
|
@focusout="onFocusOut"
|
|
153
156
|
>
|
|
157
|
+
<FieldLeading :icon="icon" class="pl-2.5">
|
|
158
|
+
<template v-if="$slots.leading" #default><slot name="leading" /></template>
|
|
159
|
+
</FieldLeading>
|
|
154
160
|
<NumberFieldDecrement
|
|
155
161
|
v-if="steppers"
|
|
156
162
|
:aria-label="text.decrease"
|
|
@@ -168,7 +174,7 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
|
|
|
168
174
|
:aria-describedby="describedBy"
|
|
169
175
|
:aria-invalid="field.invalid || undefined"
|
|
170
176
|
class="min-w-0 flex-1 bg-transparent text-ink outline-none placeholder:text-muted [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
|
|
171
|
-
:class="steppers ? 'text-center @max-[8.5rem]:px-3 @max-[8.5rem]:text-left' : 'px-3'"
|
|
177
|
+
:class="steppers ? 'text-center @max-[8.5rem]:px-3 @max-[8.5rem]:text-left' : icon || $slots.leading ? 'pl-2 pr-3' : 'px-3'"
|
|
172
178
|
/>
|
|
173
179
|
<!-- The unit sits between the value and the increment button, so a long one
|
|
174
180
|
narrows the field rather than covering the digits. Not focusable and
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* The kit's overlay scrollbar for the document itself, for a page that scrolls
|
|
4
|
+
* the window (LpAppShell's default): the same bar as LpScrollArea's, drawn by
|
|
5
|
+
* the kit over the content instead of the browser's beside it.
|
|
6
|
+
*
|
|
7
|
+
* <LpPageScrollbar top="4rem" />
|
|
8
|
+
*
|
|
9
|
+
* The browser's own bar is hidden while this is mounted, so the page no longer
|
|
10
|
+
* loses its width to a gutter, nor changes width when it starts to scroll. The
|
|
11
|
+
* scrolling stays the browser's: wheel, keyboard, touch, find-in-page, anchors,
|
|
12
|
+
* scroll restoration, sticky, a full-page screenshot and printing are untouched
|
|
13
|
+
* (the bar is not printed, and at rest it is not drawn). What the kit adds is
|
|
14
|
+
* the drawing, a thumb that drags, and a press beside it that pages.
|
|
15
|
+
*
|
|
16
|
+
* Where the browser's bars already float over the content (phones, macOS set
|
|
17
|
+
* to show them while scrolling) it draws nothing and leaves the browser's.
|
|
18
|
+
*
|
|
19
|
+
* `top` and `bottom` keep the bar clear of what covers the window's edges: a
|
|
20
|
+
* sticky header, a bottom bar. Any CSS length, a variable included, and a
|
|
21
|
+
* change eases.
|
|
22
|
+
*/
|
|
23
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
|
|
24
|
+
import { mergeClass } from "../composables/useMergedClass"
|
|
25
|
+
import { nodeWindow } from "../composables/useView"
|
|
26
|
+
import { BAR_FADE, BAR_Y, BAR_Y_SIZE, hideNativeBar, pageToward, SCROLL_HIDE_DELAY_MS, THUMB_Y, THUMB_Y_ROUND } from "./scrollbar"
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<{ top?: string; bottom?: string }>(), { top: "0px", bottom: "0px" })
|
|
29
|
+
|
|
30
|
+
/* Shorter than this the thumb is hard to catch; reka's bar holds the same floor. */
|
|
31
|
+
const MIN_THUMB_PX = 18
|
|
32
|
+
|
|
33
|
+
const barEl = ref<HTMLElement | null>(null)
|
|
34
|
+
const active = ref(false)
|
|
35
|
+
const thumb = ref({ top: 0, height: 0 })
|
|
36
|
+
const scrollable = ref(false)
|
|
37
|
+
const scrolling = ref(false)
|
|
38
|
+
const hovered = ref(false)
|
|
39
|
+
const dragging = ref(false)
|
|
40
|
+
// Unset until the bar first shows: a bar mounting "hidden" would play its fade-out.
|
|
41
|
+
const state = computed(() => (scrolling.value || hovered.value || dragging.value ? "visible" : shownOnce.value ? "hidden" : undefined))
|
|
42
|
+
const shownOnce = ref(false)
|
|
43
|
+
|
|
44
|
+
let releaseNative: (() => void) | undefined
|
|
45
|
+
let doc: Document | undefined
|
|
46
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined
|
|
47
|
+
let frame = 0
|
|
48
|
+
let sizes: ResizeObserver | undefined
|
|
49
|
+
|
|
50
|
+
function scroller(): HTMLElement | null {
|
|
51
|
+
return (doc?.scrollingElement as HTMLElement | null) ?? null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* The width the browser's bar takes from the layout: 0 where it floats over the content. */
|
|
55
|
+
function nativeBarWidth(d: Document): number {
|
|
56
|
+
const probe = d.createElement("div")
|
|
57
|
+
probe.style.cssText = "position:absolute;top:-9999px;width:100px;height:100px;overflow:scroll"
|
|
58
|
+
d.body.append(probe)
|
|
59
|
+
const width = probe.offsetWidth - probe.clientWidth
|
|
60
|
+
probe.remove()
|
|
61
|
+
return width
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function measure() {
|
|
65
|
+
frame = 0
|
|
66
|
+
const view = scroller()
|
|
67
|
+
const bar = barEl.value
|
|
68
|
+
if (!view || !bar) return
|
|
69
|
+
const range = view.scrollHeight - view.clientHeight
|
|
70
|
+
scrollable.value = range > 1
|
|
71
|
+
if (!scrollable.value) return
|
|
72
|
+
const track = bar.clientHeight
|
|
73
|
+
const height = Math.max(MIN_THUMB_PX, Math.round((track * view.clientHeight) / view.scrollHeight))
|
|
74
|
+
thumb.value = { height, top: Math.round(((track - height) * view.scrollTop) / range) }
|
|
75
|
+
}
|
|
76
|
+
function measureSoon() {
|
|
77
|
+
if (!frame && doc) frame = nodeWindow(doc).requestAnimationFrame(measure)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onScroll() {
|
|
81
|
+
measureSoon()
|
|
82
|
+
scrolling.value = true
|
|
83
|
+
shownOnce.value = true
|
|
84
|
+
clearTimeout(hideTimer)
|
|
85
|
+
hideTimer = setTimeout(() => (scrolling.value = false), SCROLL_HIDE_DELAY_MS)
|
|
86
|
+
}
|
|
87
|
+
function onEnter() {
|
|
88
|
+
hovered.value = true
|
|
89
|
+
shownOnce.value = true
|
|
90
|
+
}
|
|
91
|
+
function onLeave() {
|
|
92
|
+
hideTimer && clearTimeout(hideTimer)
|
|
93
|
+
hideTimer = setTimeout(() => {
|
|
94
|
+
hovered.value = false
|
|
95
|
+
scrolling.value = false
|
|
96
|
+
}, SCROLL_HIDE_DELAY_MS)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* A dialog that locks the page locks the bar with it. */
|
|
100
|
+
function locked(): boolean {
|
|
101
|
+
return !!doc && nodeWindow(doc).getComputedStyle(doc.body).overflow === "hidden"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function onTrackPointerDown(event: PointerEvent) {
|
|
105
|
+
const view = scroller()
|
|
106
|
+
const thumbEl = barEl.value?.querySelector<HTMLElement>("[data-lp-thumb]")
|
|
107
|
+
if (!view || !thumbEl || event.button !== 0 || thumbEl.contains(event.target as Node) || locked()) return
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
pageToward(view, "y", event.clientY < thumbEl.getBoundingClientRect().top)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onThumbPointerDown(event: PointerEvent) {
|
|
113
|
+
const view = scroller()
|
|
114
|
+
const bar = barEl.value
|
|
115
|
+
if (!view || !bar || event.button !== 0 || locked()) return
|
|
116
|
+
// Not a text selection across the page, and the release is heard wherever it lands.
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
const handle = event.currentTarget as HTMLElement
|
|
119
|
+
handle.setPointerCapture(event.pointerId)
|
|
120
|
+
dragging.value = true
|
|
121
|
+
const startY = event.clientY
|
|
122
|
+
const startTop = view.scrollTop
|
|
123
|
+
const perPixel = (view.scrollHeight - view.clientHeight) / Math.max(1, bar.clientHeight - thumb.value.height)
|
|
124
|
+
const move = (e: PointerEvent) => view.scrollTo({ top: startTop + (e.clientY - startY) * perPixel, behavior: "instant" })
|
|
125
|
+
const stop = () => {
|
|
126
|
+
dragging.value = false
|
|
127
|
+
handle.removeEventListener("pointermove", move)
|
|
128
|
+
handle.removeEventListener("pointerup", stop)
|
|
129
|
+
handle.removeEventListener("pointercancel", stop)
|
|
130
|
+
handle.removeEventListener("lostpointercapture", stop)
|
|
131
|
+
}
|
|
132
|
+
handle.addEventListener("pointermove", move)
|
|
133
|
+
handle.addEventListener("pointerup", stop)
|
|
134
|
+
handle.addEventListener("pointercancel", stop)
|
|
135
|
+
handle.addEventListener("lostpointercapture", stop)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onMounted(() => {
|
|
139
|
+
const host = barEl.value ?? undefined
|
|
140
|
+
doc = host?.ownerDocument
|
|
141
|
+
if (!doc || nativeBarWidth(doc) === 0) return
|
|
142
|
+
active.value = true
|
|
143
|
+
releaseNative = hideNativeBar(doc)
|
|
144
|
+
const win = nodeWindow(doc)
|
|
145
|
+
win.addEventListener("scroll", onScroll, { passive: true })
|
|
146
|
+
win.addEventListener("resize", measureSoon)
|
|
147
|
+
sizes = new win.ResizeObserver(measureSoon)
|
|
148
|
+
sizes.observe(doc.body)
|
|
149
|
+
if (host) sizes.observe(host)
|
|
150
|
+
measure()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
onBeforeUnmount(() => {
|
|
154
|
+
clearTimeout(hideTimer)
|
|
155
|
+
sizes?.disconnect()
|
|
156
|
+
if (!doc || !active.value) return
|
|
157
|
+
const win = nodeWindow(doc)
|
|
158
|
+
if (frame) win.cancelAnimationFrame(frame)
|
|
159
|
+
win.removeEventListener("scroll", onScroll)
|
|
160
|
+
win.removeEventListener("resize", measureSoon)
|
|
161
|
+
releaseNative?.()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const barClass = computed(() =>
|
|
165
|
+
mergeClass(
|
|
166
|
+
BAR_Y,
|
|
167
|
+
BAR_Y_SIZE.wall,
|
|
168
|
+
BAR_FADE,
|
|
169
|
+
"fixed right-0 z-(--z-sticky) transition-[top,bottom] duration-[var(--duration-medium)] ease-[var(--ease-settle)] print:hidden",
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<template>
|
|
175
|
+
<!-- Always in the DOM, so it can be measured; drawn only where the kit owns the bar. -->
|
|
176
|
+
<div
|
|
177
|
+
ref="barEl"
|
|
178
|
+
aria-hidden="true"
|
|
179
|
+
data-lp-page-scrollbar
|
|
180
|
+
:class="barClass"
|
|
181
|
+
:data-state="active && scrollable ? state : undefined"
|
|
182
|
+
:style="{ top: props.top, bottom: props.bottom, visibility: active && scrollable ? undefined : 'hidden' }"
|
|
183
|
+
@pointerenter="onEnter"
|
|
184
|
+
@pointerleave="onLeave"
|
|
185
|
+
@pointerdown="onTrackPointerDown"
|
|
186
|
+
>
|
|
187
|
+
<div
|
|
188
|
+
data-lp-thumb
|
|
189
|
+
class="absolute right-px"
|
|
190
|
+
:class="[THUMB_Y, THUMB_Y_ROUND.wall]"
|
|
191
|
+
:style="{ top: `${thumb.top}px`, height: `${thumb.height}px` }"
|
|
192
|
+
@pointerdown.stop="onThumbPointerDown"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
@@ -13,8 +13,20 @@ import {
|
|
|
13
13
|
ScrollAreaThumb,
|
|
14
14
|
ScrollAreaViewport,
|
|
15
15
|
} from "reka-ui"
|
|
16
|
-
import { prefersReducedMotion } from "../composables/easing"
|
|
17
16
|
import { nodeWindow } from "../composables/useView"
|
|
17
|
+
import {
|
|
18
|
+
BAR_FADE,
|
|
19
|
+
BAR_X,
|
|
20
|
+
BAR_X_SIZE,
|
|
21
|
+
BAR_Y,
|
|
22
|
+
BAR_Y_SIZE,
|
|
23
|
+
pageToward,
|
|
24
|
+
SCROLL_HIDE_DELAY_MS,
|
|
25
|
+
THUMB_X,
|
|
26
|
+
THUMB_X_ROUND,
|
|
27
|
+
THUMB_Y,
|
|
28
|
+
THUMB_Y_ROUND,
|
|
29
|
+
} from "./scrollbar"
|
|
18
30
|
|
|
19
31
|
// `fade` softens whichever of the top/bottom edges currently has content beyond
|
|
20
32
|
// it, so a clipped list reads as continuing rather than as cut off. On by
|
|
@@ -80,8 +92,7 @@ function onViewportBlur() {
|
|
|
80
92
|
// `opacity-0` + the hidden-state fade is what keeps the bar invisible at rest:
|
|
81
93
|
// with force-mount the element stays in the DOM, so a one-shot fade-out alone
|
|
82
94
|
// would snap back to visible once the animation finished.
|
|
83
|
-
const
|
|
84
|
-
"opacity-0 data-[state=visible]:animate-[fade-in_180ms_ease] data-[state=visible]:opacity-100 data-[state=hidden]:animate-[fade-out_240ms_ease]"
|
|
95
|
+
const edge = computed(() => (props.floating ? "floating" : "wall"))
|
|
85
96
|
|
|
86
97
|
// Dragging the thumb pulls the pointer off the bar and across the content, and
|
|
87
98
|
// the browser reads that as a text selection — the rows highlight blue while
|
|
@@ -197,13 +208,9 @@ onBeforeUnmount(() => {
|
|
|
197
208
|
})
|
|
198
209
|
|
|
199
210
|
/*
|
|
200
|
-
* A press on the bar beside the thumb
|
|
201
|
-
*
|
|
202
|
-
* so a press that
|
|
203
|
-
* thumb, and a press further away teleported it. Like a native bar, a press
|
|
204
|
-
* off the thumb now pages one viewport towards the pointer, smoothly; the
|
|
205
|
-
* thumb itself has a hit area as wide as the bar (see its `before:`), so a
|
|
206
|
-
* press that meant to grab it does.
|
|
211
|
+
* A press on the bar beside the thumb pages towards the pointer (pageToward)
|
|
212
|
+
* instead of reka's jump; the thumb's hit area is as wide as the bar (its
|
|
213
|
+
* `before:`), so a press that meant to grab it does.
|
|
207
214
|
*/
|
|
208
215
|
function onTrackPointerDown(event: PointerEvent, axis: "x" | "y") {
|
|
209
216
|
const el = viewportEl.value
|
|
@@ -213,10 +220,7 @@ function onTrackPointerDown(event: PointerEvent, axis: "x" | "y") {
|
|
|
213
220
|
event.preventDefault()
|
|
214
221
|
event.stopImmediatePropagation()
|
|
215
222
|
const rect = thumb.getBoundingClientRect()
|
|
216
|
-
|
|
217
|
-
const step = (axis === "y" ? el.clientHeight : el.clientWidth) * 0.85 * (before ? -1 : 1)
|
|
218
|
-
const behavior = prefersReducedMotion() ? "auto" : "smooth"
|
|
219
|
-
el.scrollBy(axis === "y" ? { top: step, behavior } : { left: step, behavior })
|
|
223
|
+
pageToward(el, axis, axis === "y" ? event.clientY < rect.top : event.clientX < rect.left)
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
let stopThumbDrag: (() => void) | null = null
|
|
@@ -253,7 +257,7 @@ function onThumbPointerDown(event: PointerEvent) {
|
|
|
253
257
|
class="relative flex flex-col overflow-hidden"
|
|
254
258
|
:class="dragging ? 'select-none' : undefined"
|
|
255
259
|
type="hover"
|
|
256
|
-
:scroll-hide-delay="
|
|
260
|
+
:scroll-hide-delay="SCROLL_HIDE_DELAY_MS"
|
|
257
261
|
>
|
|
258
262
|
<!-- The bar itself is a pointer target far wider than it looks: the visible
|
|
259
263
|
rail is the thumb, sized below, while the track pads it out to a
|
|
@@ -273,8 +277,8 @@ function onThumbPointerDown(event: PointerEvent) {
|
|
|
273
277
|
<ScrollAreaScrollbar
|
|
274
278
|
orientation="vertical"
|
|
275
279
|
force-mount
|
|
276
|
-
class="
|
|
277
|
-
:class="[
|
|
280
|
+
class="z-1"
|
|
281
|
+
:class="[BAR_Y, BAR_FADE, BAR_Y_SIZE[edge]]"
|
|
278
282
|
:style="barInsetTop ? { marginTop: barInsetTop } : undefined"
|
|
279
283
|
@pointerdown.capture="onTrackPointerDown($event, 'y')"
|
|
280
284
|
>
|
|
@@ -286,22 +290,22 @@ function onThumbPointerDown(event: PointerEvent) {
|
|
|
286
290
|
the positioning reka owns. -->
|
|
287
291
|
<ScrollAreaThumb
|
|
288
292
|
data-lp-thumb
|
|
289
|
-
class="relative
|
|
290
|
-
:class="
|
|
293
|
+
class="relative"
|
|
294
|
+
:class="[THUMB_Y, THUMB_Y_ROUND[edge]]"
|
|
291
295
|
@pointerdown="onThumbPointerDown"
|
|
292
296
|
/>
|
|
293
297
|
</ScrollAreaScrollbar>
|
|
294
298
|
|
|
295
299
|
<ScrollAreaScrollbar
|
|
296
300
|
orientation="horizontal"
|
|
297
|
-
class="
|
|
298
|
-
:class="[
|
|
301
|
+
class="z-1"
|
|
302
|
+
:class="[BAR_X, BAR_FADE, BAR_X_SIZE[edge]]"
|
|
299
303
|
@pointerdown.capture="onTrackPointerDown($event, 'x')"
|
|
300
304
|
>
|
|
301
305
|
<ScrollAreaThumb
|
|
302
306
|
data-lp-thumb
|
|
303
|
-
class="relative
|
|
304
|
-
:class="
|
|
307
|
+
class="relative"
|
|
308
|
+
:class="[THUMB_X, THUMB_X_ROUND[edge]]"
|
|
305
309
|
@pointerdown="onThumbPointerDown"
|
|
306
310
|
/>
|
|
307
311
|
</ScrollAreaScrollbar>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* depends_on edges connect these across host frames. Tailwind utilities only
|
|
6
6
|
* (no scoped CSS) so styles reach consumer apps via @source.
|
|
7
7
|
*/
|
|
8
|
-
import { Handle, Position } from "@vue-flow/core"
|
|
8
|
+
import { Handle, Position, type NodeProps } from "@vue-flow/core"
|
|
9
9
|
import { computed } from "vue"
|
|
10
10
|
|
|
11
11
|
export interface ServiceNodeData {
|
|
@@ -16,7 +16,9 @@ export interface ServiceNodeData {
|
|
|
16
16
|
dimmed?: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Vue Flow hands a custom node all of NodeProps. Declared, they stop falling
|
|
20
|
+
// through onto the node's root as attributes (position="[object Object]").
|
|
21
|
+
const props = defineProps<Omit<NodeProps<ServiceNodeData>, "selected"> & { selected?: boolean }>()
|
|
20
22
|
|
|
21
23
|
const kindVar: Record<string, string> = {
|
|
22
24
|
service: "var(--color-brand)",
|
|
@@ -15,7 +15,7 @@ import LpSkeleton from "./LpSkeleton.vue"
|
|
|
15
15
|
import LpStatus from "./LpStatus.vue"
|
|
16
16
|
import { useFitScale } from "../composables/useFitScale"
|
|
17
17
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
18
|
-
import { TONES } from "./tone"
|
|
18
|
+
import { TONES, toneOf, type ToneName } from "./tone"
|
|
19
19
|
|
|
20
20
|
const props = withDefaults(
|
|
21
21
|
defineProps<{
|
|
@@ -59,10 +59,26 @@ const props = withDefaults(
|
|
|
59
59
|
* (it names the tile, it is not data); an empty one pulses too.
|
|
60
60
|
*/
|
|
61
61
|
loading?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Colour of the figure, from the kit's tones (`danger` for offenders,
|
|
64
|
+
* `success`, `warning`…). Unset, it is ink. The delta keeps its own colour.
|
|
65
|
+
*/
|
|
66
|
+
tone?: ToneName
|
|
67
|
+
/** Size of the figure: `lg` (the tile's own, text-3xl), `md` or `sm` for a dense row. */
|
|
68
|
+
size?: "sm" | "md" | "lg"
|
|
62
69
|
}>(),
|
|
63
|
-
{ trend: "auto", decimals: 0, animateOnMount: true },
|
|
70
|
+
{ trend: "auto", decimals: 0, animateOnMount: true, size: "lg" },
|
|
64
71
|
)
|
|
65
72
|
|
|
73
|
+
/* The figure's type, and the variable its fit to the tile scales from. */
|
|
74
|
+
const FIGURE_SIZE = {
|
|
75
|
+
sm: { class: "text-lg", font: "var(--text-lg, 1.125rem)", line: "var(--text-lg--line-height, 1.55)" },
|
|
76
|
+
md: { class: "text-2xl", font: "var(--text-2xl, 1.5rem)", line: "var(--text-2xl--line-height, 1.33)" },
|
|
77
|
+
lg: { class: "text-3xl", font: "var(--text-3xl, 1.875rem)", line: "var(--text-3xl--line-height, 1.2)" },
|
|
78
|
+
} as const
|
|
79
|
+
const figureSize = computed(() => FIGURE_SIZE[props.size])
|
|
80
|
+
const figureColor = computed(() => (props.tone ? TONES[toneOf(props.tone)].text : "text-ink"))
|
|
81
|
+
|
|
66
82
|
// Resolve the trend direction: explicit wins; "auto" reads a numeric delta.
|
|
67
83
|
const dir = computed<"up" | "down" | "flat">(() => {
|
|
68
84
|
if (props.trend !== "auto") return props.trend
|
|
@@ -136,7 +152,11 @@ const FIT_MIN = 0.6
|
|
|
136
152
|
const row = ref<HTMLElement | null>(null)
|
|
137
153
|
const figure = ref<HTMLElement | null>(null)
|
|
138
154
|
const fit = useFitScale(row, figure, FIT_MIN)
|
|
139
|
-
const figureStyle = computed(() =>
|
|
155
|
+
const figureStyle = computed(() => {
|
|
156
|
+
if (fit.scale.value >= 1) return undefined
|
|
157
|
+
const { font, line } = figureSize.value
|
|
158
|
+
return { fontSize: `calc(${font} * ${fit.scale.value})`, lineHeight: `calc(${font} * ${line})` }
|
|
159
|
+
})
|
|
140
160
|
const figureTitle = computed(() => (fit.clipped.value && !props.loading ? `${props.prefix ?? ""}${props.value}${props.valueSuffix ?? ""}` : undefined))
|
|
141
161
|
|
|
142
162
|
// A stat tile spaces its label and value on a fixed gap; a dense KPI row needs that tighter.
|
|
@@ -168,8 +188,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
168
188
|
is the real one: the tile cannot change height when the figure lands. -->
|
|
169
189
|
<span
|
|
170
190
|
ref="figure"
|
|
171
|
-
class="max-w-full whitespace-nowrap font-mono
|
|
172
|
-
:class="fit.clipped.value ? 'inline-block overflow-hidden text-ellipsis align-bottom' : ''"
|
|
191
|
+
class="max-w-full whitespace-nowrap font-mono font-medium tracking-tight"
|
|
192
|
+
:class="[figureSize.class, figureColor, fit.clipped.value ? 'inline-block overflow-hidden text-ellipsis align-bottom' : '']"
|
|
173
193
|
:style="figureStyle"
|
|
174
194
|
:title="figureTitle"
|
|
175
195
|
>
|
|
@@ -296,6 +296,12 @@ const portalTo = usePortalTarget()
|
|
|
296
296
|
// What happens in another document (the page around a frame, or a frame in
|
|
297
297
|
// the page) is not "outside" for this layer.
|
|
298
298
|
const guardOutside = useOutsideGuard()
|
|
299
|
+
|
|
300
|
+
// Several roots (trigger and portal), so Vue cannot hand attributes on by
|
|
301
|
+
// itself and dropped them: a trigger wrapped around this one from outside (an
|
|
302
|
+
// LpPopover's, as-child) lost its id, ARIA state and click, and its popover
|
|
303
|
+
// never opened. They go to the trigger, which merges them onto the slot's element.
|
|
304
|
+
defineOptions({ inheritAttrs: false })
|
|
299
305
|
</script>
|
|
300
306
|
|
|
301
307
|
<template>
|
|
@@ -318,6 +324,7 @@ const guardOutside = useOutsideGuard()
|
|
|
318
324
|
lands on the real button rather than on a wrapper. -->
|
|
319
325
|
<TooltipTrigger
|
|
320
326
|
as-child
|
|
327
|
+
v-bind="$attrs"
|
|
321
328
|
:aria-label="label ? content : undefined"
|
|
322
329
|
@pointermove="onPointerMove"
|
|
323
330
|
>
|
|
@@ -30,6 +30,7 @@ import { useResizeObserver } from "../composables/useResizeObserver"
|
|
|
30
30
|
import { isHTMLElement, nodeWindow } from "../composables/useView"
|
|
31
31
|
import { backgroundOf, resolveCssColor, svgToPng } from "./exportData"
|
|
32
32
|
import LpButton from "./LpButton.vue"
|
|
33
|
+
import LpIcon from "./LpIcon.vue"
|
|
33
34
|
import LpInfraNode, { type InfraNodeData } from "./LpInfraNode.vue"
|
|
34
35
|
import LpInput from "./LpInput.vue"
|
|
35
36
|
import LpLaneNode from "./LpLaneNode.vue"
|
|
@@ -962,9 +963,10 @@ defineExpose({
|
|
|
962
963
|
:key="action.id"
|
|
963
964
|
size="sm"
|
|
964
965
|
variant="outline"
|
|
965
|
-
:icon="action.icon"
|
|
966
966
|
@click="emit('selection-action', { action: action.id, ids: selectedIds })"
|
|
967
967
|
>
|
|
968
|
+
<!-- LpButton has no icon prop: an :icon here fell through and drew nothing. -->
|
|
969
|
+
<LpIcon v-if="action.icon" :name="action.icon" :size="14" />
|
|
968
970
|
{{ action.label }}
|
|
969
971
|
</LpButton>
|
|
970
972
|
</Panel>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { prefersReducedMotion } from "../composables/easing"
|
|
2
|
+
import { nodeWindow } from "../composables/useView"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* The kit's overlay scrollbar, shared by LpScrollArea (reka's bar over its own
|
|
6
|
+
* viewport) and LpPageScrollbar (the document's, drawn by the kit), so the two
|
|
7
|
+
* look and answer alike.
|
|
8
|
+
*
|
|
9
|
+
* The bar is a pointer target far wider than it looks: the visible rail is the
|
|
10
|
+
* thumb, while the bar pads it out to a grabbable strip (a 4px-wide hit area is
|
|
11
|
+
* near-impossible to catch with a mouse). Padding, not width, carries the room
|
|
12
|
+
* so the thumb stays flush against the wall. The thumb is sized with
|
|
13
|
+
* min-width/min-height: reka sets an inline width from a variable it never
|
|
14
|
+
* defines for a vertical bar, and a minimum wins over that.
|
|
15
|
+
*/
|
|
16
|
+
export const BAR_Y = "group flex touch-none select-none justify-end pl-2.5"
|
|
17
|
+
export const BAR_X = "group flex flex-col touch-none select-none justify-end pt-2.5"
|
|
18
|
+
/* Against a wall, and inside a card or panel (a gap from the edge). */
|
|
19
|
+
export const BAR_Y_SIZE = { wall: "w-3.5 py-px pr-px", floating: "w-4 py-1 pr-1" }
|
|
20
|
+
export const BAR_X_SIZE = { wall: "h-3.5 px-px pb-px", floating: "h-4 px-1 pb-1" }
|
|
21
|
+
|
|
22
|
+
export const THUMB_Y =
|
|
23
|
+
"min-w-1 bg-brand/40 before:absolute before:inset-y-0 before:right-0 before:-left-2.5 before:content-[''] transition-[min-width,background-color] duration-[var(--duration-fast)] group-hover:min-w-1.5 group-hover:bg-brand"
|
|
24
|
+
export const THUMB_X =
|
|
25
|
+
"min-h-1 bg-brand/40 before:absolute before:inset-x-0 before:bottom-0 before:-top-2.5 before:content-[''] transition-[min-height,background-color] duration-[var(--duration-fast)] group-hover:min-h-1.5 group-hover:bg-brand"
|
|
26
|
+
/* Rounded on the outer edge only against a wall; at both ends inside a card. */
|
|
27
|
+
export const THUMB_Y_ROUND = { wall: "rounded-l-pill", floating: "rounded-pill" }
|
|
28
|
+
export const THUMB_X_ROUND = { wall: "rounded-t-pill", floating: "rounded-pill" }
|
|
29
|
+
|
|
30
|
+
/* In and out by the bar's `data-state` (visible | hidden), as reka sets it. */
|
|
31
|
+
export const BAR_FADE =
|
|
32
|
+
"opacity-0 data-[state=visible]:animate-[fade-in_180ms_ease] data-[state=visible]:opacity-100 data-[state=hidden]:animate-[fade-out_240ms_ease]"
|
|
33
|
+
|
|
34
|
+
/* How long the bar stays after the last scroll or the pointer leaving it. */
|
|
35
|
+
export const SCROLL_HIDE_DELAY_MS = 500
|
|
36
|
+
|
|
37
|
+
/* A press beside the thumb pages this much of the view towards the pointer. */
|
|
38
|
+
const PAGE_FRACTION = 0.85
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A press on the bar beside the thumb. Like a native bar it pages one view
|
|
42
|
+
* towards the pointer, smoothly, rather than putting the thumb's middle under
|
|
43
|
+
* it (reka's own answer, which yanked the content by half a thumb on a near
|
|
44
|
+
* miss and teleported it on a far press).
|
|
45
|
+
*/
|
|
46
|
+
export function pageToward(scroller: Element, axis: "x" | "y", before: boolean): void {
|
|
47
|
+
const view = axis === "y" ? scroller.clientHeight : scroller.clientWidth
|
|
48
|
+
const step = view * PAGE_FRACTION * (before ? -1 : 1)
|
|
49
|
+
const behavior = prefersReducedMotion(nodeWindow(scroller)) ? "auto" : "smooth"
|
|
50
|
+
scroller.scrollBy(axis === "y" ? { top: step, behavior } : { left: step, behavior })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Owners per document, so two page bars cannot give the browser's bar back early. */
|
|
54
|
+
const nativeHidden = new WeakMap<Document, number>()
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hide the document's own scrollbar (`lp-scrollbar-none` on <html>) while the
|
|
58
|
+
* kit draws one. Scrolling is untouched. Returns the release.
|
|
59
|
+
*/
|
|
60
|
+
export function hideNativeBar(doc: Document): () => void {
|
|
61
|
+
nativeHidden.set(doc, (nativeHidden.get(doc) ?? 0) + 1)
|
|
62
|
+
doc.documentElement.classList.add("lp-scrollbar-none")
|
|
63
|
+
let released = false
|
|
64
|
+
return () => {
|
|
65
|
+
if (released) return
|
|
66
|
+
released = true
|
|
67
|
+
const left = (nativeHidden.get(doc) ?? 1) - 1
|
|
68
|
+
nativeHidden.set(doc, left)
|
|
69
|
+
if (!left) doc.documentElement.classList.remove("lp-scrollbar-none")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Teleport, type ComponentPublicInstance, type Plugin, type VNode } from "vue"
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* Development check for a misspelt prop on a kit component.
|
|
@@ -61,11 +61,26 @@ export function unknownAttributes(
|
|
|
61
61
|
})
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/*
|
|
65
|
+
* The elements a component rendered at its top. Not `$el`: with several roots
|
|
66
|
+
* (a trigger and its portal) that is the fragment's empty text anchor, which
|
|
67
|
+
* made every attribute look unknown. A teleport's content is left out, since
|
|
68
|
+
* nothing a caller passes lands there.
|
|
69
|
+
*/
|
|
70
|
+
function rootElements(vnode: VNode | null | undefined, out: Element[] = []): Element[] {
|
|
71
|
+
if (!vnode || vnode.type === Teleport) return out
|
|
72
|
+
if (vnode.component) return rootElements(vnode.component.subTree, out)
|
|
73
|
+
const el = vnode.el as Node | null
|
|
74
|
+
if (el?.nodeType === 1) {
|
|
75
|
+
out.push(el as Element)
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(vnode.children)) for (const child of vnode.children) if (child && typeof child === "object") rootElements(child as VNode, out)
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
function renderedElements(instance: ComponentPublicInstance): Element[] {
|
|
65
|
-
|
|
66
|
-
if (root?.nodeType !== 1) return []
|
|
67
|
-
const el = root as Element
|
|
68
|
-
return [el, ...el.querySelectorAll(FORWARD_TARGETS)]
|
|
83
|
+
return rootElements(instance.$.subTree).flatMap((el) => [el, ...el.querySelectorAll(FORWARD_TARGETS)])
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
export const lpPropCheck: Plugin<[PropCheckOptions?]> = {
|
|
@@ -218,8 +218,11 @@ export function usePaletteControl(options: {
|
|
|
218
218
|
h(raw.label ? "h3" : "label", { id: `${editorId}-label`, for: raw.label ? undefined : editorId, class: "text-sm font-semibold text-ink" }, label()),
|
|
219
219
|
h(instance.type as Component, {
|
|
220
220
|
...raw, id: undefined, name: undefined, form: undefined, autofocus: undefined, palette: false,
|
|
221
|
-
disabled: options.disabled?.() || !writable(),
|
|
222
|
-
|
|
221
|
+
disabled: options.disabled?.() || !writable(),
|
|
222
|
+
// Only to a control that declares them: elsewhere they fell through
|
|
223
|
+
// as attributes onto its root (a switch's button took `required`).
|
|
224
|
+
...("invalid" in instance.props ? { invalid: options.invalid?.() } : {}),
|
|
225
|
+
...("required" in instance.props ? { required: field?.value.required ?? instance.props.required } : {}),
|
|
223
226
|
"aria-label": label(), class: "w-full", style: undefined,
|
|
224
227
|
}, instance.slots),
|
|
225
228
|
error() || hint() ? h("p", { id: `${editorId}-message`, class: error() ? "text-xs text-danger" : "text-xs text-muted", role: error() ? "alert" : undefined }, error() || hint()) : null,
|