@leavepulse/ui 0.78.0 → 0.78.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.
- package/dist/chunks/{DropzoneLinkField-DXIr0eFz.js → DropzoneLinkField-CyE7TJ5f.js} +1 -1
- package/dist/chunks/FieldLeading-mr81aEAP.js +27 -0
- package/dist/chunks/LpAddressInput-ft8fQOGx.js +401 -0
- package/dist/chunks/{LpAppShell-BNAVTQsY.js → LpAppShell-DkvU6IBB.js} +23 -4
- package/dist/chunks/LpAutocomplete-CrZbd0uv.js +310 -0
- package/dist/chunks/LpCheckbox-CCWCwD0y.js +129 -0
- package/dist/chunks/{LpCodeBlock-DuzmnIHl.js → LpCodeBlock-DBqy4RLd.js} +1 -1
- package/dist/chunks/{LpCodeDisplay-CO8fngxa.js → LpCodeDisplay-D59uPwRb.js} +3 -3
- package/dist/chunks/LpColorPicker-BONURBFN.js +574 -0
- 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/LpDatePicker-C2AOtxgF.js +171 -0
- package/dist/chunks/{LpDateRangePicker-CNQXdTVv.js → LpDateRangePicker-CiXdIAyh.js} +43 -35
- package/dist/chunks/{LpDrawer-Bt9UUqt_.js → LpDrawer-DXNQFpAA.js} +1 -1
- package/dist/chunks/{LpFileTree-BSx1kBoV.js → LpFileTree-CpNurwZp.js} +3 -3
- package/dist/chunks/{LpFileTreeNode-4dVmFHb3.js → LpFileTreeNode-DdF8Cq-s.js} +2 -2
- 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-CbI7z5fQ.js} +21 -25
- 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-CcSJSGan.js} +1 -1
- package/dist/chunks/{LpLogViewer-Dvb1E9_G.js → LpLogViewer-CWu49Dko.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-DI3BW5ff.js} +2 -2
- package/dist/chunks/{LpNotificationBell-D_wklO1m.js → LpNotificationBell-DUbVyK6R.js} +3 -3
- package/dist/chunks/LpNumberField-C8LfUz_R.js +154 -0
- package/dist/chunks/LpOtpInput-B1X9l9no.js +140 -0
- package/dist/chunks/LpPageScrollbar-D2O0xhJw.js +164 -0
- package/dist/chunks/{LpPaletteTheme-dVtxHx-F.js → LpPaletteTheme-B-xuZiKK.js} +2 -2
- package/dist/chunks/{LpPasswordInput-B-zD6tUS.js → LpPasswordInput-Duc_3wPv.js} +2 -2
- package/dist/chunks/{LpPhoneInput-BSL7NX4z.js → LpPhoneInput-CctcRK9K.js} +124 -118
- 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/LpRadioGroup-DTM0nd9G.js +103 -0
- package/dist/chunks/LpRangeSlider-DJsEZXad.js +97 -0
- package/dist/chunks/{LpRelativeTime-BovUk8MP.js → LpRelativeTime-0zI-44ve.js} +1 -1
- package/dist/chunks/LpRepeater-Czib3rmv.js +193 -0
- package/dist/chunks/{LpScrollArea-ay4Xo_hg.js → LpScrollArea-CqvwfZs2.js} +9 -12
- package/dist/chunks/{LpSecretField-DBvU6iAw.js → LpSecretField-BtlCk3IH.js} +4 -4
- package/dist/chunks/LpSegmented-sskrJKJJ.js +128 -0
- package/dist/chunks/LpSelect-JFZehOkd.js +515 -0
- package/dist/chunks/{LpServiceNode-D8uJ8sGk.js → LpServiceNode-Cln2ntdw.js} +17 -0
- package/dist/chunks/{LpSidebar-kSkpPh3-.js → LpSidebar-CvRvVBTt.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/LpSlider-C4ilCtZX.js +76 -0
- package/dist/chunks/{LpStat-pMNZgFcq.js → LpStat-DPjc6_fX.js} +17 -4
- package/dist/chunks/LpSwitch-DyAYTchz.js +122 -0
- package/dist/chunks/{LpTableFilter-DgARTsA1.js → LpTableFilter-CJg5eIcV.js} +2 -2
- package/dist/chunks/{LpTagsInput-Dt0mteSj.js → LpTagsInput-CkNxpNYa.js} +4 -3
- package/dist/chunks/{LpTextarea-Co0b9PVx.js → LpTextarea-CFK_5QB8.js} +3 -2
- package/dist/chunks/{LpThemeStudio-DzYfE-5k.js → LpThemeStudio-R-wohbCY.js} +3 -3
- package/dist/chunks/{LpThemeSwitcher-CFLkdbBL.js → LpThemeSwitcher-Bzy15MCP.js} +2 -2
- package/dist/chunks/LpTimeField-B5n0m8m0.js +115 -0
- 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-BNgw9RyZ.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-ugv-fyig.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.d.ts +4 -0
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.d.ts +3 -3
- package/dist/components/LpAppShell.vue.d.ts +16 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.d.ts +4 -0
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpBadge.vue.d.ts +2 -2
- package/dist/components/LpChart.vue.d.ts +1 -1
- package/dist/components/LpChart.vue.js +1 -1
- package/dist/components/LpCheckbox.vue.d.ts +4 -0
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpCollapse.vue.d.ts +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +5 -3
- 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/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.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/LpImageEditor.vue.d.ts +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 +17 -2
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.d.ts +4 -0
- package/dist/components/LpOtpInput.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/LpPhoneInput.vue.d.ts +4 -0
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.d.ts +4 -0
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeSlider.vue.d.ts +4 -0
- package/dist/components/LpRangeSlider.vue.js +1 -1
- package/dist/components/LpRelativeTime.vue.js +1 -1
- package/dist/components/LpRepeater.vue.d.ts +2 -0
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +1 -1
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpSegmented.vue.d.ts +4 -0
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.d.ts +2 -0
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpServiceNode.vue.d.ts +8 -2
- package/dist/components/LpServiceNode.vue.js +1 -1
- package/dist/components/LpShinyText.vue.d.ts +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.d.ts +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/LpSlider.vue.d.ts +4 -0
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpStat.vue.d.ts +16 -0
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpSwitch.vue.d.ts +4 -0
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.js +5 -5
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +1 -1
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.d.ts +4 -0
- package/dist/components/LpTimeField.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/calendar.d.ts +2 -0
- 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/useField.d.ts +50 -0
- package/dist/composables/useField.js +31 -2
- package/dist/composables/useMergedClass.d.ts +6 -2
- package/dist/composables/useMergedClass.js +6 -6
- package/dist/composables/usePaletteCommands.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +170 -168
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/FieldLeading.vue +17 -0
- package/src/components/LpAddressInput.vue +8 -4
- package/src/components/LpAppShell.vue +23 -0
- package/src/components/LpAutocomplete.vue +11 -5
- package/src/components/LpCheckbox.vue +8 -6
- package/src/components/LpColorPicker.vue +7 -4
- package/src/components/LpContextMenu.vue +7 -0
- package/src/components/LpDatePicker.vue +8 -4
- package/src/components/LpDateRangePicker.vue +8 -4
- package/src/components/LpHoverCard.vue +7 -0
- package/src/components/LpInfraNode.vue +4 -3
- package/src/components/LpInput.vue +7 -7
- package/src/components/LpLaneNode.vue +5 -1
- package/src/components/LpNumberField.vue +18 -7
- package/src/components/LpOtpInput.vue +8 -6
- package/src/components/LpPageScrollbar.vue +195 -0
- package/src/components/LpPhoneInput.vue +11 -6
- package/src/components/LpRadioGroup.vue +9 -5
- package/src/components/LpRangeSlider.vue +8 -4
- package/src/components/LpRepeater.vue +16 -4
- package/src/components/LpScrollArea.vue +27 -23
- package/src/components/LpSegmented.vue +8 -4
- package/src/components/LpSelect.vue +8 -7
- package/src/components/LpServiceNode.vue +4 -2
- package/src/components/LpSlider.vue +8 -4
- package/src/components/LpStat.vue +25 -5
- package/src/components/LpSwitch.vue +8 -5
- package/src/components/LpTagsInput.vue +4 -3
- package/src/components/LpTextarea.vue +3 -2
- package/src/components/LpTimeField.vue +8 -6
- package/src/components/LpTooltip.vue +7 -0
- package/src/components/LpTopologyCanvas.vue +3 -1
- package/src/components/calendar.ts +2 -0
- package/src/components/scrollbar.ts +71 -0
- package/src/composables/propCheck.ts +20 -5
- package/src/composables/useField.ts +52 -0
- package/src/composables/useMergedClass.ts +11 -5
- package/src/composables/usePaletteCommands.ts +5 -2
- package/src/index.ts +1 -0
- package/dist/chunks/LpAddressInput-ZqoZ-N2Y.js +0 -393
- package/dist/chunks/LpAutocomplete-DkY6CSyD.js +0 -302
- package/dist/chunks/LpCheckbox-D6RuDW4X.js +0 -124
- package/dist/chunks/LpColorPicker-BxAHEnR5.js +0 -567
- package/dist/chunks/LpDatePicker-DwSDY0mt.js +0 -163
- package/dist/chunks/LpNumberField-CNHsmSDZ.js +0 -134
- package/dist/chunks/LpOtpInput-BRpLBbOD.js +0 -132
- package/dist/chunks/LpRadioGroup-BrazjLY_.js +0 -95
- package/dist/chunks/LpRangeSlider-fz61tzc-.js +0 -89
- package/dist/chunks/LpRepeater-BkJHSPI1.js +0 -180
- package/dist/chunks/LpSegmented-DbEJhbv-.js +0 -123
- package/dist/chunks/LpSelect-Y-iuSymW.js +0 -512
- package/dist/chunks/LpSlider-A4ixovgS.js +0 -68
- package/dist/chunks/LpSwitch-DnC2-0aV.js +0 -115
- package/dist/chunks/LpTimeField-DeOcKWJq.js +0 -110
|
@@ -6,12 +6,13 @@ export default { inheritAttrs: false }
|
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
|
+
import FieldWrapper from "./FieldWrapper.vue"
|
|
9
10
|
import SliderRail from "./SliderRail.vue"
|
|
10
11
|
import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
|
|
11
12
|
// Single-value slider (reka Slider). reka models value as an array; we expose
|
|
12
13
|
// a plain number for the common single-thumb case. Themed via tokens.
|
|
13
14
|
import { SliderRoot, SliderThumb } from "reka-ui"
|
|
14
|
-
import {
|
|
15
|
+
import { useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
15
16
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
16
17
|
import { SLIDER_GLIDE, useSliderDrag } from "./slider"
|
|
17
18
|
|
|
@@ -24,6 +25,8 @@ const props = withDefaults(
|
|
|
24
25
|
max?: number
|
|
25
26
|
step?: number
|
|
26
27
|
disabled?: boolean
|
|
28
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
29
|
+
error?: string
|
|
27
30
|
/** Accessible name for the thumb when there's no visible label. */
|
|
28
31
|
ariaLabel?: string
|
|
29
32
|
}>(),
|
|
@@ -44,18 +47,18 @@ function onCommit(v: number[] | undefined) {
|
|
|
44
47
|
if (v && v.length) emit("change", v[0])
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
const field =
|
|
50
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, disabled: props.disabled }))
|
|
48
51
|
|
|
49
52
|
// A slider is full-width by default; putting one in a toolbar means constraining it, and a fixed h-5 has to yield to a denser row.
|
|
50
53
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
51
|
-
`relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`,
|
|
52
|
-
)
|
|
54
|
+
`relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`, () => ownAttrs())
|
|
53
55
|
const { dragging, onPointerDown } = useSliderDrag()
|
|
54
56
|
|
|
55
57
|
usePaletteControl({ disabled: () => field.value.disabled })
|
|
56
58
|
</script>
|
|
57
59
|
|
|
58
60
|
<template>
|
|
61
|
+
<FieldWrapper v-bind="wrapper">
|
|
59
62
|
<SliderRoot
|
|
60
63
|
:model-value="[props.modelValue]"
|
|
61
64
|
:min="min"
|
|
@@ -78,4 +81,5 @@ usePaletteControl({ disabled: () => field.value.disabled })
|
|
|
78
81
|
class="block size-4 rounded-pill border-2 border-brand bg-surface shadow outline-none transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:scale-110 active:scale-95 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hover:scale-100 motion-reduce:active:scale-100"
|
|
79
82
|
/>
|
|
80
83
|
</SliderRoot>
|
|
84
|
+
</FieldWrapper>
|
|
81
85
|
</template>
|
|
@@ -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
|
>
|
|
@@ -6,10 +6,11 @@ export default { inheritAttrs: false }
|
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
|
+
import FieldWrapper from "./FieldWrapper.vue"
|
|
9
10
|
import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
|
|
10
11
|
import { computed, useId, useSlots } from "vue"
|
|
11
12
|
import { SwitchRoot, SwitchThumb } from "reka-ui"
|
|
12
|
-
import { mergeDescribedBy,
|
|
13
|
+
import { mergeDescribedBy, useOwnField } from "../composables/useField"
|
|
13
14
|
import { useOptimisticToggle } from "../composables/useOptimisticToggle"
|
|
14
15
|
import LpLoader from "./LpLoader.vue"
|
|
15
16
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
@@ -20,6 +21,8 @@ const props = withDefaults(
|
|
|
20
21
|
palette?: PaletteControlOptions
|
|
21
22
|
modelValue?: boolean
|
|
22
23
|
disabled?: boolean
|
|
24
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
25
|
+
error?: string
|
|
23
26
|
label?: string
|
|
24
27
|
/** Secondary line under the label. */
|
|
25
28
|
description?: string
|
|
@@ -67,13 +70,11 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() => {
|
|
|
67
70
|
return props.labelPosition === "start"
|
|
68
71
|
? "flex w-full cursor-pointer items-center justify-between gap-3 text-sm text-ink"
|
|
69
72
|
: "inline-flex cursor-pointer items-center gap-2 text-sm text-ink"
|
|
70
|
-
})
|
|
73
|
+
}, () => ownAttrs())
|
|
71
74
|
|
|
72
75
|
// Whatever is not class (name, aria-*, @focus…) belongs to the switch itself,
|
|
73
76
|
// labelled or not: on the label row it would land on the <label>.
|
|
74
|
-
const field =
|
|
75
|
-
wrapped: false,
|
|
76
|
-
id: typeof rest.value.id === "string" ? rest.value.id : undefined,
|
|
77
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, id: typeof rest.value.id === "string" ? rest.value.id : undefined,
|
|
77
78
|
disabled: props.disabled,
|
|
78
79
|
}))
|
|
79
80
|
const descriptionId = `${useId()}-description`
|
|
@@ -89,6 +90,7 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
|
|
|
89
90
|
</script>
|
|
90
91
|
|
|
91
92
|
<template>
|
|
93
|
+
<FieldWrapper v-bind="wrapper">
|
|
92
94
|
<component :is="labelled ? 'label' : 'span'" :class="labelled ? [rootClass, field.disabled && 'cursor-not-allowed'] : 'contents'">
|
|
93
95
|
<span v-if="labelled && labelPosition === 'start'" class="flex min-w-0 flex-col">
|
|
94
96
|
<slot>{{ label }}</slot>
|
|
@@ -121,4 +123,5 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
|
|
|
121
123
|
<span v-if="description" :id="descriptionId" class="text-xs text-muted">{{ description }}</span>
|
|
122
124
|
</span>
|
|
123
125
|
</component>
|
|
126
|
+
</FieldWrapper>
|
|
124
127
|
</template>
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
TagsInputRoot,
|
|
40
40
|
} from "reka-ui"
|
|
41
41
|
import { computed, ref, useAttrs, useId, watch } from "vue"
|
|
42
|
-
import { fieldWrapperBinds, fieldMessageId, useField, useFieldControl, mergeDescribedBy } from "../composables/useField"
|
|
42
|
+
import { wrapsItself, fieldWrapperBinds, fieldMessageId, useField, useFieldControl, mergeDescribedBy } from "../composables/useField"
|
|
43
43
|
import { prefersReducedMotion } from "../composables/easing"
|
|
44
44
|
import { useFieldNotice, type FieldNoticeOptions } from "../composables/useFieldNotice"
|
|
45
45
|
import { useShift } from "../composables/useShift"
|
|
@@ -137,7 +137,8 @@ const text = useLabels<TagsInputLabels>(
|
|
|
137
137
|
)
|
|
138
138
|
|
|
139
139
|
const attrs = useAttrs()
|
|
140
|
-
const
|
|
140
|
+
const outerFieldOf = useField()
|
|
141
|
+
const hasField = computed(() => wrapsItself(props, outerFieldOf))
|
|
141
142
|
/*
|
|
142
143
|
* Notes go to the field that frames the control, which fades its own hint out
|
|
143
144
|
* under them: this component's own LpFormField when it has a label or hint,
|
|
@@ -145,7 +146,7 @@ const hasField = computed(() => !!props.label || !!props.hint || !!props.error)
|
|
|
145
146
|
* alone.
|
|
146
147
|
*/
|
|
147
148
|
const notice = useFieldNotice()
|
|
148
|
-
const outerField =
|
|
149
|
+
const outerField = outerFieldOf
|
|
149
150
|
type FieldHandle = { notify: (text: string, options?: FieldNoticeOptions) => void; clearNotice: () => void }
|
|
150
151
|
const ownField = ref<FieldHandle | null>(null)
|
|
151
152
|
function bindField(c: unknown): void {
|
|
@@ -6,7 +6,7 @@ export default { inheritAttrs: false }
|
|
|
6
6
|
import { nodeWindow } from "../composables/realm"
|
|
7
7
|
import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
|
|
8
8
|
import { computed, nextTick, onMounted, ref, useAttrs, watch } from "vue"
|
|
9
|
-
import { fieldWrapperBinds, useFieldControl, mergeDescribedBy } from "../composables/useField"
|
|
9
|
+
import { wrapsItself, useField, fieldWrapperBinds, useFieldControl, mergeDescribedBy } from "../composables/useField"
|
|
10
10
|
import { mergeClass } from "../composables/useMergedClass"
|
|
11
11
|
import FieldWrapper from "./FieldWrapper.vue"
|
|
12
12
|
|
|
@@ -37,7 +37,8 @@ const props = withDefaults(defineProps<{
|
|
|
37
37
|
|
|
38
38
|
defineEmits<{ (e: "update:modelValue", value: string): void }>()
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const outerFieldOf = useField()
|
|
41
|
+
const hasField = computed(() => wrapsItself(props, outerFieldOf))
|
|
41
42
|
|
|
42
43
|
// class/style lay out the field when there is one, and style the <textarea>
|
|
43
44
|
// when it stands alone; every other attr (name, maxlength, autofocus, @blur…)
|
|
@@ -6,6 +6,7 @@ export default { inheritAttrs: false }
|
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
|
+
import FieldWrapper from "./FieldWrapper.vue"
|
|
9
10
|
import { CONTROL_SIZE } from "./controlSize"
|
|
10
11
|
import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
|
|
11
12
|
/*
|
|
@@ -22,7 +23,7 @@ import { usePaletteControl, type PaletteControlOptions } from "../composables/us
|
|
|
22
23
|
import { Time } from "@internationalized/date"
|
|
23
24
|
import { TimeFieldInput, TimeFieldRoot } from "reka-ui"
|
|
24
25
|
import { computed } from "vue"
|
|
25
|
-
import { focusLeft,
|
|
26
|
+
import { focusLeft, useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
26
27
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
27
28
|
import LpIcon from "./LpIcon.vue"
|
|
28
29
|
|
|
@@ -34,6 +35,8 @@ const props = withDefaults(
|
|
|
34
35
|
modelValue?: string
|
|
35
36
|
disabled?: boolean
|
|
36
37
|
invalid?: boolean
|
|
38
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
39
|
+
error?: string
|
|
37
40
|
size?: "sm" | "md" | "lg"
|
|
38
41
|
/** 24 or 12-hour clock, independent of the browser locale. */
|
|
39
42
|
hourCycle?: 12 | 24
|
|
@@ -68,9 +71,7 @@ function onUpdate(next: unknown) {
|
|
|
68
71
|
emit("update:modelValue", `${pad(next.hour)}:${pad(next.minute)}`)
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
const field =
|
|
72
|
-
wrapped: false,
|
|
73
|
-
invalid: props.invalid,
|
|
74
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, invalid: props.invalid,
|
|
74
75
|
disabled: props.disabled,
|
|
75
76
|
}))
|
|
76
77
|
|
|
@@ -82,13 +83,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
82
83
|
"flex w-full items-center gap-0.5 rounded-control border bg-surface-soft px-3 text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
|
|
83
84
|
shellSize[props.size],
|
|
84
85
|
field.value.invalid ? "border-danger focus-within:ring-danger-soft" : "border-line focus-within:border-brand",
|
|
85
|
-
].join(" "),
|
|
86
|
-
)
|
|
86
|
+
].join(" "), () => ownAttrs())
|
|
87
87
|
|
|
88
88
|
usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.value.invalid })
|
|
89
89
|
</script>
|
|
90
90
|
|
|
91
91
|
<template>
|
|
92
|
+
<FieldWrapper v-bind="wrapper">
|
|
92
93
|
<TimeFieldRoot
|
|
93
94
|
v-slot="{ segments }"
|
|
94
95
|
:model-value="value"
|
|
@@ -121,4 +122,5 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
|
|
|
121
122
|
</TimeFieldInput>
|
|
122
123
|
</template>
|
|
123
124
|
</TimeFieldRoot>
|
|
125
|
+
</FieldWrapper>
|
|
124
126
|
</template>
|
|
@@ -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>
|
|
@@ -52,6 +52,8 @@ export interface PickerFieldProps {
|
|
|
52
52
|
clearable?: boolean
|
|
53
53
|
disabled?: boolean
|
|
54
54
|
invalid?: boolean
|
|
55
|
+
/** A message under the picker, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
56
|
+
error?: string
|
|
55
57
|
size?: "sm" | "md" | "lg"
|
|
56
58
|
/** Locale of the shown date and the calendar; defaults to reka's ConfigProvider, then the browser. */
|
|
57
59
|
locale?: string
|
|
@@ -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?]> = {
|
|
@@ -154,6 +154,58 @@ export function fieldWrapperBinds(
|
|
|
154
154
|
return { wrapped, id: field.id, label, hint, error, required, class: attrs.class, style: attrs.style }
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Whether a control with these props draws its own LpFormField. A label or a
|
|
159
|
+
* hint is its own; an error alone is, except inside an outer LpFormField, which
|
|
160
|
+
* owns the label (its `for` points at the control) and the message line: a
|
|
161
|
+
* second field in it showed the message twice and took the label's control id.
|
|
162
|
+
* There the error only marks the control invalid.
|
|
163
|
+
*/
|
|
164
|
+
export function wrapsItself(
|
|
165
|
+
own: { label?: string; hint?: string; error?: string },
|
|
166
|
+
outer: ComputedRef<FieldContext> | null,
|
|
167
|
+
): boolean {
|
|
168
|
+
if (own.label || own.hint) return true
|
|
169
|
+
return !!own.error && !outer?.value
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A control that takes `label`, `hint` and `error` the way LpInput does: given
|
|
174
|
+
* any of them it wraps itself in LpFormField (FieldWrapper, bound to
|
|
175
|
+
* `wrapper`), an error marks it invalid, and the consumer's class and style lay
|
|
176
|
+
* out that field instead of the control (`ownAttrs` says whether the control's
|
|
177
|
+
* root still takes them). Without them it is the bare control, as before. So
|
|
178
|
+
* `v-bind="form.field('x')"`, which hands every control `error`, shows the
|
|
179
|
+
* message on any of them.
|
|
180
|
+
* Inside an outer LpFormField an error alone does not wrap (wrapsItself).
|
|
181
|
+
*/
|
|
182
|
+
export function useOwnField(
|
|
183
|
+
state: () => { label?: string; hint?: string; error?: string; required?: boolean; invalid?: boolean; disabled?: boolean; id?: string },
|
|
184
|
+
) {
|
|
185
|
+
const attrs = useAttrs()
|
|
186
|
+
const outer = useField()
|
|
187
|
+
const wrapped = computed(() => wrapsItself(state(), outer))
|
|
188
|
+
const field = useFieldControl(() => {
|
|
189
|
+
const own = state()
|
|
190
|
+
return {
|
|
191
|
+
wrapped: wrapped.value,
|
|
192
|
+
hasMessage: !!own.error || !!own.hint,
|
|
193
|
+
id: own.id ?? (typeof attrs.id === "string" ? attrs.id : undefined),
|
|
194
|
+
invalid: !!own.invalid || !!own.error,
|
|
195
|
+
required: own.required,
|
|
196
|
+
disabled: own.disabled,
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
const wrapper = computed(() => fieldWrapperBinds(wrapped.value, field.value, state(), attrs))
|
|
200
|
+
/* The attributes for the control itself: all of them, less the class and style the wrapper takes. */
|
|
201
|
+
const controlAttrs = computed(() => {
|
|
202
|
+
if (!wrapped.value) return attrs
|
|
203
|
+
const { class: _class, style: _style, ...rest } = attrs
|
|
204
|
+
return rest
|
|
205
|
+
})
|
|
206
|
+
return { wrapped, field, wrapper, controlAttrs, ownAttrs: () => !wrapped.value }
|
|
207
|
+
}
|
|
208
|
+
|
|
157
209
|
/** The state of a control that takes its id from the app and draws no wrapper of its own. */
|
|
158
210
|
export function useAttrsField(state: () => { invalid?: boolean; disabled?: boolean }) {
|
|
159
211
|
const attrs = useAttrs()
|
|
@@ -62,11 +62,13 @@ function merge(...classes: (string | undefined | null | false)[]): string {
|
|
|
62
62
|
*/
|
|
63
63
|
export function useMergedClass(
|
|
64
64
|
own: MaybeRefOrGetter<string | undefined>,
|
|
65
|
+
/** False while something else takes the consumer's class (a field wrapper). */
|
|
66
|
+
takesAttrs: () => boolean = () => true,
|
|
65
67
|
): ComputedRef<string> {
|
|
66
68
|
const attrs = useAttrs()
|
|
67
69
|
// `cn` returns undefined when everything it was given was empty; a class
|
|
68
70
|
// binding is happier with "" than with undefined.
|
|
69
|
-
return computed(() => merge(toValue(own), attrs.class as string | undefined))
|
|
71
|
+
return computed(() => merge(toValue(own), takesAttrs() ? (attrs.class as string | undefined) : undefined))
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -85,16 +87,20 @@ export function useMergedClass(
|
|
|
85
87
|
* Vue appends that second `class` to the merged one, and stylesheet order —
|
|
86
88
|
* not the caller — decides which wins.
|
|
87
89
|
*/
|
|
88
|
-
export function useMergedAttrs(
|
|
90
|
+
export function useMergedAttrs(
|
|
91
|
+
own: MaybeRefOrGetter<string | undefined>,
|
|
92
|
+
/** False while a field wrapper takes the consumer's class and style (useOwnField's `ownAttrs`). */
|
|
93
|
+
takesAttrs: () => boolean = () => true,
|
|
94
|
+
): {
|
|
89
95
|
class: ComputedRef<string>
|
|
90
96
|
attrs: ComputedRef<Record<string, unknown>>
|
|
91
97
|
} {
|
|
92
98
|
const attrs = useAttrs()
|
|
93
99
|
return {
|
|
94
|
-
class: useMergedClass(own),
|
|
100
|
+
class: useMergedClass(own, takesAttrs),
|
|
95
101
|
attrs: computed(() => {
|
|
96
|
-
const { class: _omitted, ...rest } = attrs
|
|
97
|
-
return rest
|
|
102
|
+
const { class: _omitted, style, ...rest } = attrs
|
|
103
|
+
return takesAttrs() ? { ...rest, ...(style === undefined ? {} : { style }) } : rest
|
|
98
104
|
}),
|
|
99
105
|
}
|
|
100
106
|
}
|
|
@@ -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,
|
package/src/index.ts
CHANGED
|
@@ -198,6 +198,7 @@ export { default as LpRadioGroup } from "./components/LpRadioGroup.vue"
|
|
|
198
198
|
export type { RadioOption } from "./components/LpRadioGroup.vue"
|
|
199
199
|
export { default as LpRollingText } from "./components/LpRollingText.vue"
|
|
200
200
|
export { default as LpScrollArea } from "./components/LpScrollArea.vue"
|
|
201
|
+
export { default as LpPageScrollbar } from "./components/LpPageScrollbar.vue"
|
|
201
202
|
export { default as LpSegmented } from "./components/LpSegmented.vue"
|
|
202
203
|
export type { SegmentedOption } from "./components/LpSegmented.vue"
|
|
203
204
|
export { default as LpSection } from "./components/LpSection.vue"
|