@leavepulse/ui 0.70.0 → 0.70.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/README.md +12 -0
- package/dist/assets/LpRepeater-C1Cw27hj.css +21 -0
- package/dist/assets/{LpRollingText-BGnE6q9j.css → LpRollingText-dyOXedGO.css} +7 -0
- package/dist/chunks/{ContextMenuItems-B5uE8Znw.js → ContextMenuItems-6kxEivBI.js} +10 -4
- package/dist/chunks/{LpAddressInput-CNnLTDZg.js → LpAddressInput-Chv_Xf8q.js} +1 -1
- package/dist/chunks/{LpAppShell-CEUAB8kK.js → LpAppShell-BiWByLmt.js} +1 -1
- package/dist/chunks/{LpBadge-HOEPBYmi.js → LpBadge-BBq1Er9T.js} +1 -1
- package/dist/chunks/{LpBreadcrumbs-DYoPc3P4.js → LpBreadcrumbs-JKEi_yFF.js} +1 -1
- package/dist/chunks/{LpCodeDisplay-CIKKT2iW.js → LpCodeDisplay-CbJMs-IK.js} +2 -2
- package/dist/chunks/{LpColorPicker-DFugXNUi.js → LpColorPicker-B7HSIFmn.js} +2 -2
- package/dist/chunks/{LpContextMenu-COwOwRgI.js → LpContextMenu-Cj96ENDC.js} +5 -4
- package/dist/chunks/{LpCountdown-Dtq1ihlJ.js → LpCountdown-BQnmB4gq.js} +1 -1
- package/dist/chunks/{LpDataList-B-5nHfNw.js → LpDataList-CHNYQw7h.js} +4 -3
- package/dist/chunks/{LpDatePicker-BUe1VUC0.js → LpDatePicker-ac1_6o5P.js} +4 -3
- package/dist/chunks/{LpDateRangePicker-C_k65vGR.js → LpDateRangePicker-B15gYrZJ.js} +4 -3
- package/dist/chunks/{LpDropdownMenu-CRgrhdAH.js → LpDropdownMenu-R4aYVtVV.js} +5 -2
- package/dist/chunks/{LpEditable-DTqLt2lT.js → LpEditable-Ws3DNQHn.js} +5 -3
- package/dist/chunks/{LpFileTree-DMISXVHC.js → LpFileTree-DDNg34tJ.js} +1 -1
- package/dist/chunks/{LpFileTreeNode-CPXB_Yy3.js → LpFileTreeNode-DmPuYNZ_.js} +2 -2
- package/dist/chunks/{LpGauge-Bk8TwJj0.js → LpGauge-C14Z9503.js} +1 -1
- package/dist/chunks/{LpHintLayer-p4hqzdyA.js → LpHintLayer-HvwUA5Ec.js} +1 -1
- package/dist/chunks/{LpHints-CCrhRfy9.js → LpHints-DxbPKOzQ.js} +2 -2
- package/dist/chunks/{LpInfraNode-CUC3t4na.js → LpInfraNode-D64hywJI.js} +2 -2
- package/dist/chunks/{LpLogViewer-elZHbj85.js → LpLogViewer-DKK4ySZl.js} +2 -2
- package/dist/chunks/{LpMediaGallery-D5swlmZ_.js → LpMediaGallery-rmI1DjKN.js} +1 -1
- package/dist/chunks/{LpModalNav-D4Z0wVHR.js → LpModalNav-DiyFCHiZ.js} +1 -1
- package/dist/chunks/{LpNotificationBell-DPZmP5dM.js → LpNotificationBell-D-WhE5m2.js} +5 -5
- package/dist/chunks/{LpPasswordInput-DTV18FIh.js → LpPasswordInput-GL2KOFK8.js} +10 -6
- package/dist/chunks/{LpPie-CFuV7pZv.js → LpPie-BgXZ3xmm.js} +2 -2
- package/dist/chunks/{LpPipeline-D81yh_NX.js → LpPipeline-B31h41Mu.js} +2 -2
- package/dist/chunks/{LpPopover-BEIcqb9p.js → LpPopover-DmZ3wUtP.js} +6 -5
- package/dist/chunks/{LpQuota-2Sw_gqcJ.js → LpQuota-DaL8TgNe.js} +1 -1
- package/dist/chunks/{LpRelativeTime-CKzVqAo9.js → LpRelativeTime-yin9uQP7.js} +1 -1
- package/dist/chunks/LpRepeater-D0S7jjwr.js +180 -0
- package/dist/chunks/{LpRollingText-DB7upf-5.js → LpRollingText-DiL717LK.js} +4 -3
- package/dist/chunks/{LpSecretField-DMfdJ1cQ.js → LpSecretField-CfWyHv9m.js} +19 -10
- package/dist/chunks/{LpSelect-B4f_yRi5.js → LpSelect-C_8cJtj6.js} +10 -15
- package/dist/chunks/{LpSidebar-D-M0Y_7U.js → LpSidebar-CZRXtvem.js} +1 -1
- package/dist/chunks/{LpSidebarNav-DF-TYxOV.js → LpSidebarNav-DBg1U1oW.js} +2 -2
- package/dist/chunks/{LpStat-B2ix1Q7t.js → LpStat-DFKlPJgy.js} +1 -1
- package/dist/chunks/{LpStatus-C7kx5PKE.js → LpStatus-BsGLrpor.js} +2 -2
- package/dist/chunks/{LpStepMark-DK-5BKAt.js → LpStepMark-TrzOioa9.js} +1 -1
- package/dist/chunks/{LpStepper-DRFfdJaq.js → LpStepper-BbnS5jUr.js} +1 -1
- package/dist/chunks/{LpTagsInput-DgpBFRV2.js → LpTagsInput-DZg80W-B.js} +1 -1
- package/dist/chunks/{LpThemeStudio-DguwHdGt.js → LpThemeStudio-DxVZMtzN.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-D5uz0MhK.js → LpThemeSwitcher-CadVBTBn.js} +3 -3
- package/dist/chunks/{LpTopologyCanvas-ZhKthfgz.js → LpTopologyCanvas-C17A1XCb.js} +1 -1
- package/dist/chunks/{LpTour-NfRJNEIP.js → LpTour-CY2bFXge.js} +1 -1
- package/dist/chunks/{LpUptimeBar-DWegV2p6.js → LpUptimeBar-Tew1Ykx4.js} +1 -1
- package/dist/chunks/{PickerField-CHRS1h7K.js → PickerField-DoHv0WMH.js} +25 -16
- package/dist/chunks/{TourCard-BXuVHsfn.js → TourCard-BJ8i7dk0.js} +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpBadge.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpChart.vue.js +26 -24
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpCountdown.vue.js +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.d.ts +2 -0
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.d.ts +2 -0
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpEditable.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpGauge.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +1 -1
- package/dist/components/LpPaletteStats.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.d.ts +7 -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/LpPopover.vue.js +1 -1
- package/dist/components/LpQuota.vue.js +1 -1
- package/dist/components/LpRelativeTime.vue.js +1 -1
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpRollingText.vue.d.ts +2 -0
- package/dist/components/LpRollingText.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStatus.vue.js +1 -1
- package/dist/components/LpStepMark.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpTable.vue.js +4 -4
- package/dist/components/LpTabs.vue.js +3 -3
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpTour.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/PickerField.vue.d.ts +5 -3
- package/dist/components/PickerField.vue.js +1 -1
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/components/menuWhen.js +1 -1
- package/dist/composables/useField.d.ts +5 -0
- package/dist/composables/useField.js +21 -1
- package/dist/composables/useForm.js +13 -4
- package/dist/composables/useView.js +4 -1
- package/dist/index.js +43 -43
- package/package.json +4 -1
- package/src/components/ContextMenuItems.vue +5 -2
- package/src/components/LpChart.vue +19 -17
- package/src/components/LpContextMenu.vue +4 -3
- package/src/components/LpDataList.vue +2 -2
- package/src/components/LpDatePicker.vue +2 -0
- package/src/components/LpDateRangePicker.vue +2 -0
- package/src/components/LpDropdownMenu.vue +1 -1
- package/src/components/LpEditable.vue +3 -1
- package/src/components/LpPasswordInput.vue +10 -5
- package/src/components/LpPopover.vue +4 -0
- package/src/components/LpRepeater.vue +64 -13
- package/src/components/LpRollingText.vue +12 -1
- package/src/components/LpSecretField.vue +15 -8
- package/src/components/LpSelect.vue +4 -9
- package/src/components/PickerField.vue +5 -4
- package/src/composables/useField.ts +26 -1
- package/src/composables/useForm.ts +14 -5
- package/src/composables/useView.ts +3 -0
- package/dist/chunks/LpRepeater-D0VjmD6y.js +0 -140
|
@@ -4,8 +4,10 @@ import { useFieldControl } from "../composables/useField"
|
|
|
4
4
|
import { useLabels } from "../composables/useLpConfig"
|
|
5
5
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
6
6
|
import { useRepeater, type RepeaterItemKey } from "../composables/useRepeater"
|
|
7
|
+
import { usePrefersReducedMotion } from "../composables/useReducedMotion"
|
|
7
8
|
import LpButton from "./LpButton.vue"
|
|
8
9
|
import LpIcon from "./LpIcon.vue"
|
|
10
|
+
import LpShift from "./LpShift.vue"
|
|
9
11
|
|
|
10
12
|
defineOptions({ inheritAttrs: false })
|
|
11
13
|
const props = defineProps<{
|
|
@@ -33,11 +35,31 @@ const list = useRepeater({
|
|
|
33
35
|
disabled: () => field.value.disabled,
|
|
34
36
|
})
|
|
35
37
|
const { rows } = list
|
|
36
|
-
const
|
|
38
|
+
const rowsHost = ref<HTMLElement | null>(null)
|
|
39
|
+
const reducedMotion = usePrefersReducedMotion()
|
|
37
40
|
const addButton = ref<InstanceType<typeof LpButton> | null>(null)
|
|
38
41
|
const focusable = 'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex="0"], [contenteditable="true"]'
|
|
39
42
|
function focusRow(index: number) {
|
|
40
|
-
|
|
43
|
+
rowAt(index)?.querySelector<HTMLElement>(focusable)?.focus({ preventScroll: true })
|
|
44
|
+
}
|
|
45
|
+
function rowAt(index: number): HTMLElement | undefined {
|
|
46
|
+
return Array.from(rowsHost.value?.children ?? []).filter((node): node is HTMLElement =>
|
|
47
|
+
node.hasAttribute("data-lp-repeater-row") && !node.hasAttribute("inert"),
|
|
48
|
+
)[index]
|
|
49
|
+
}
|
|
50
|
+
function beforeLeave(el: Element) {
|
|
51
|
+
const row = el as HTMLElement
|
|
52
|
+
row.style.width = `${row.offsetWidth}px`
|
|
53
|
+
row.style.top = `${row.offsetTop}px`
|
|
54
|
+
row.setAttribute("inert", "")
|
|
55
|
+
row.setAttribute("aria-hidden", "true")
|
|
56
|
+
}
|
|
57
|
+
function beforeEnter(el: Element) {
|
|
58
|
+
const row = el as HTMLElement
|
|
59
|
+
row.removeAttribute("inert")
|
|
60
|
+
row.removeAttribute("aria-hidden")
|
|
61
|
+
row.style.removeProperty("top")
|
|
62
|
+
row.style.removeProperty("width")
|
|
41
63
|
}
|
|
42
64
|
async function add() {
|
|
43
65
|
if (field.value.disabled) return
|
|
@@ -47,34 +69,63 @@ async function add() {
|
|
|
47
69
|
}
|
|
48
70
|
async function remove(index: number) {
|
|
49
71
|
if (field.value.disabled) return
|
|
50
|
-
const row =
|
|
72
|
+
const row = rowAt(index)
|
|
51
73
|
const restore = row?.contains(row.ownerDocument.activeElement)
|
|
52
74
|
list.remove(index)
|
|
53
75
|
if (!restore) return
|
|
54
76
|
await nextTick()
|
|
55
77
|
if (model.value.length) focusRow(Math.min(index, model.value.length - 1))
|
|
56
|
-
else (addButton.value?.$el as HTMLElement | undefined)?.focus()
|
|
78
|
+
else (addButton.value?.$el as HTMLElement | undefined)?.focus({ preventScroll: true })
|
|
57
79
|
}
|
|
58
80
|
defineExpose({ add, remove, update: list.update })
|
|
59
81
|
const { class: rootClass, attrs: rest } = useMergedAttrs("flex min-w-0 flex-col gap-2")
|
|
60
82
|
</script>
|
|
61
83
|
|
|
62
84
|
<template>
|
|
63
|
-
<div
|
|
85
|
+
<div v-bind="rest" :class="rootClass">
|
|
64
86
|
<div class="flex items-center justify-between gap-2">
|
|
65
87
|
<span v-if="label" class="text-sm font-medium text-ink">{{ label }}</span>
|
|
66
88
|
<LpButton ref="addButton" size="xs" variant="ghost" :disabled="field.disabled" @click="add">
|
|
67
89
|
<LpIcon name="lucide:plus" :size="14" />{{ text.add }}
|
|
68
90
|
</LpButton>
|
|
69
91
|
</div>
|
|
70
|
-
<
|
|
71
|
-
<div class="min-w-0 flex-
|
|
72
|
-
<
|
|
92
|
+
<LpShift axis="height">
|
|
93
|
+
<div ref="rowsHost" class="relative flex min-w-0 flex-col gap-2">
|
|
94
|
+
<TransitionGroup name="lp-repeater" :css="!reducedMotion" @before-enter="beforeEnter" @before-leave="beforeLeave">
|
|
95
|
+
<div v-for="({ item, key }, index) in rows" :key="key" data-lp-repeater-row class="flex min-w-0 items-start gap-2">
|
|
96
|
+
<div class="min-w-0 flex-1">
|
|
97
|
+
<slot :item="item" :index="index" :disabled="field.disabled" :update="(next: T) => list.update(index, next)" :remove="() => remove(index)" />
|
|
98
|
+
</div>
|
|
99
|
+
<LpButton variant="ghost" size="sm" square :disabled="field.disabled" :aria-label="text.remove.replace('{index}', String(index + 1))" @click="remove(index)">
|
|
100
|
+
<LpIcon name="lucide:x" :size="14" />
|
|
101
|
+
</LpButton>
|
|
102
|
+
</div>
|
|
103
|
+
<div v-if="!rows.length" key="empty" class="text-xs text-muted"><slot name="empty">{{ text.empty }}</slot></div>
|
|
104
|
+
</TransitionGroup>
|
|
73
105
|
</div>
|
|
74
|
-
|
|
75
|
-
<LpIcon name="lucide:x" :size="14" />
|
|
76
|
-
</LpButton>
|
|
77
|
-
</div>
|
|
78
|
-
<div v-if="!rows.length" class="text-xs text-muted"><slot name="empty">{{ text.empty }}</slot></div>
|
|
106
|
+
</LpShift>
|
|
79
107
|
</div>
|
|
80
108
|
</template>
|
|
109
|
+
|
|
110
|
+
<style>
|
|
111
|
+
.lp-repeater-move,
|
|
112
|
+
.lp-repeater-enter-active,
|
|
113
|
+
.lp-repeater-leave-active {
|
|
114
|
+
transition: transform var(--duration-fast) var(--ease-emphasized), opacity var(--duration-fast) var(--ease-emphasized);
|
|
115
|
+
}
|
|
116
|
+
.lp-repeater-leave-active {
|
|
117
|
+
position: absolute;
|
|
118
|
+
}
|
|
119
|
+
.lp-repeater-enter-from,
|
|
120
|
+
.lp-repeater-leave-to {
|
|
121
|
+
opacity: 0;
|
|
122
|
+
transform: translateY(-0.5rem);
|
|
123
|
+
}
|
|
124
|
+
@media (prefers-reduced-motion: reduce) {
|
|
125
|
+
.lp-repeater-move,
|
|
126
|
+
.lp-repeater-enter-active,
|
|
127
|
+
.lp-repeater-leave-active {
|
|
128
|
+
transition: none;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -35,6 +35,8 @@ const props = withDefaults(
|
|
|
35
35
|
* width — a value that grows shouldn't shove its neighbours.
|
|
36
36
|
*/
|
|
37
37
|
align?: "start" | "center" | "end"
|
|
38
|
+
/** Wrap to the available width instead of keeping a single line. */
|
|
39
|
+
wrap?: boolean
|
|
38
40
|
}>(),
|
|
39
41
|
{ mode: "slide", direction: "up", duration: 320, align: "center" },
|
|
40
42
|
)
|
|
@@ -63,7 +65,7 @@ const style = computed(() => ({
|
|
|
63
65
|
<template>
|
|
64
66
|
<span
|
|
65
67
|
class="lp-roll relative inline-flex overflow-hidden align-bottom"
|
|
66
|
-
:class="justify"
|
|
68
|
+
:class="[justify, wrap && 'lp-roll--wrap']"
|
|
67
69
|
:style="style"
|
|
68
70
|
>
|
|
69
71
|
<Transition :name="name">
|
|
@@ -93,6 +95,15 @@ const style = computed(() => ({
|
|
|
93
95
|
backface-visibility: hidden;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
.lp-roll--wrap {
|
|
99
|
+
max-width: 100%;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.lp-roll--wrap .lp-roll__item {
|
|
103
|
+
white-space: normal;
|
|
104
|
+
overflow-wrap: anywhere;
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
/* The outgoing copy leaves the flow so the incoming one keeps the baseline. */
|
|
97
108
|
.lp-roll-up-leave-active,
|
|
98
109
|
.lp-roll-down-leave-active,
|
|
@@ -37,6 +37,7 @@ import { computed, onBeforeUnmount, onMounted, ref, useId, watch } from "vue"
|
|
|
37
37
|
import { prefersReducedMotion } from "../composables/easing"
|
|
38
38
|
import { useScramble } from "../composables/useScramble"
|
|
39
39
|
import { useLabels } from "../composables/useLpConfig"
|
|
40
|
+
import { useFieldControl } from "../composables/useField"
|
|
40
41
|
import { mergeClass } from "../composables/useMergedClass"
|
|
41
42
|
import FieldWrapper from "./FieldWrapper.vue"
|
|
42
43
|
import LpButton from "./LpButton.vue"
|
|
@@ -91,7 +92,11 @@ const emit = defineEmits<{
|
|
|
91
92
|
}>()
|
|
92
93
|
|
|
93
94
|
/** Shown or hidden; bind it to drive the reveal from outside. */
|
|
94
|
-
const
|
|
95
|
+
const revealedModel = defineModel<boolean>("revealed", { default: false })
|
|
96
|
+
const revealed = computed({
|
|
97
|
+
get: () => props.revealable && revealedModel.value,
|
|
98
|
+
set: (value: boolean) => { revealedModel.value = value },
|
|
99
|
+
})
|
|
95
100
|
|
|
96
101
|
const text = useLabels<SecretFieldLabels>(
|
|
97
102
|
"secretField",
|
|
@@ -107,6 +112,7 @@ const text = useLabels<SecretFieldLabels>(
|
|
|
107
112
|
)
|
|
108
113
|
|
|
109
114
|
const secret = computed(() => (props.input ? props.modelValue : props.value) ?? "")
|
|
115
|
+
const control = useFieldControl(() => ({ wrapped: false, disabled: props.disabled }))
|
|
110
116
|
|
|
111
117
|
// Revealing decrypts the value into place, and a new value (a regenerated key)
|
|
112
118
|
// decrypts over the old one: the change is seen happening, not just swapped.
|
|
@@ -167,7 +173,7 @@ const shell = computed(() =>
|
|
|
167
173
|
mergeClass(
|
|
168
174
|
"relative flex w-full items-center gap-1 overflow-hidden rounded-control border bg-surface-soft text-ink transition-colors duration-(--duration-fast)",
|
|
169
175
|
props.error ? "border-danger" : "border-line",
|
|
170
|
-
|
|
176
|
+
control.value.disabled ? "opacity-55" : "",
|
|
171
177
|
props.wrap ? "py-1.5" : "",
|
|
172
178
|
{ sm: "min-h-(--size-control-sm) pl-2.5 pr-1 text-xs", md: "min-h-(--size-control-md) pl-3 pr-1.5 text-sm", lg: "min-h-(--size-control-lg) pl-3.5 pr-1.5 text-sm" }[props.size],
|
|
173
179
|
),
|
|
@@ -184,7 +190,8 @@ const shell = computed(() =>
|
|
|
184
190
|
class="[&_input]:font-mono"
|
|
185
191
|
:model-value="modelValue"
|
|
186
192
|
:placeholder="placeholder"
|
|
187
|
-
:disabled="disabled"
|
|
193
|
+
:disabled="control.disabled"
|
|
194
|
+
:revealable="revealable"
|
|
188
195
|
:label="label"
|
|
189
196
|
:hint="hint"
|
|
190
197
|
:error="error"
|
|
@@ -199,7 +206,7 @@ const shell = computed(() =>
|
|
|
199
206
|
:value="secret"
|
|
200
207
|
size="xs"
|
|
201
208
|
class="size-7"
|
|
202
|
-
:disabled="disabled"
|
|
209
|
+
:disabled="control.disabled"
|
|
203
210
|
@copied="(t: string) => emit('copied', t)"
|
|
204
211
|
/>
|
|
205
212
|
<slot name="actions" />
|
|
@@ -212,7 +219,7 @@ const shell = computed(() =>
|
|
|
212
219
|
:aria-label="text.regenerate"
|
|
213
220
|
:title="text.regenerate"
|
|
214
221
|
:action="regenerate"
|
|
215
|
-
:disabled="disabled"
|
|
222
|
+
:disabled="control.disabled"
|
|
216
223
|
>
|
|
217
224
|
<LpIcon name="lucide:refresh-cw" :size="iconSize" />
|
|
218
225
|
</LpButton>
|
|
@@ -264,7 +271,7 @@ const shell = computed(() =>
|
|
|
264
271
|
:icon-size="iconSize"
|
|
265
272
|
:show-label="text.show"
|
|
266
273
|
:hide-label="text.hide"
|
|
267
|
-
:disabled="disabled"
|
|
274
|
+
:disabled="control.disabled"
|
|
268
275
|
:controls="id"
|
|
269
276
|
class="size-7"
|
|
270
277
|
/>
|
|
@@ -273,7 +280,7 @@ const shell = computed(() =>
|
|
|
273
280
|
:value="secret"
|
|
274
281
|
:size="copySize"
|
|
275
282
|
class="size-7 text-muted hover:text-ink"
|
|
276
|
-
:disabled="disabled"
|
|
283
|
+
:disabled="control.disabled"
|
|
277
284
|
@copied="(t: string) => emit('copied', t)"
|
|
278
285
|
/>
|
|
279
286
|
<slot name="actions" />
|
|
@@ -284,7 +291,7 @@ const shell = computed(() =>
|
|
|
284
291
|
square
|
|
285
292
|
class="size-7 text-muted hover:text-ink"
|
|
286
293
|
:action="regenerate"
|
|
287
|
-
:disabled="disabled"
|
|
294
|
+
:disabled="control.disabled"
|
|
288
295
|
>
|
|
289
296
|
<LpIcon name="lucide:refresh-cw" :size="iconSize" />
|
|
290
297
|
</LpButton>
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
ComboboxViewport,
|
|
37
37
|
} from "reka-ui"
|
|
38
38
|
import { computed, nextTick, reactive, ref, useAttrs, useId, watch } from "vue"
|
|
39
|
-
import { useFieldControl } from "../composables/useField"
|
|
39
|
+
import { useFieldBlur, useFieldControl } from "../composables/useField"
|
|
40
40
|
import { useLabels } from "../composables/useLpConfig"
|
|
41
41
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
42
42
|
import { OPTION_ITEM, optionStagger, POPOVER_PANEL } from "./dropdown"
|
|
@@ -178,12 +178,7 @@ function onOpenChange(open: boolean) {
|
|
|
178
178
|
// are marked with one id, because the list is portalled away from the trigger
|
|
179
179
|
// and a check on the trigger alone either missed the search box or took a
|
|
180
180
|
// step into it for leaving.
|
|
181
|
-
const
|
|
182
|
-
function onFocusOut(event: FocusEvent) {
|
|
183
|
-
const next = event.relatedTarget as Element | null
|
|
184
|
-
const inside = !!next && typeof next.closest === "function" && !!next.closest(`[data-lp-select-part="${partOf}"]`)
|
|
185
|
-
if (!inside) emit("blur", event)
|
|
186
|
-
}
|
|
181
|
+
const { partId, onFocusOut } = useFieldBlur((event) => emit("blur", event))
|
|
187
182
|
|
|
188
183
|
/*
|
|
189
184
|
* The trigger's name. reka hard-codes aria-label="Show popup" on it, which hid
|
|
@@ -396,7 +391,7 @@ const guardOutside = useOutsideGuard()
|
|
|
396
391
|
@update:open="onOpenChange"
|
|
397
392
|
>
|
|
398
393
|
<ComboboxAnchor
|
|
399
|
-
:data-lp-
|
|
394
|
+
:data-lp-field-part="partId"
|
|
400
395
|
@focusout="onFocusOut"
|
|
401
396
|
:class="[
|
|
402
397
|
anchorHeight,
|
|
@@ -528,7 +523,7 @@ const guardOutside = useOutsideGuard()
|
|
|
528
523
|
|
|
529
524
|
<ComboboxPortal :to="portalTo">
|
|
530
525
|
<ComboboxContent
|
|
531
|
-
:data-lp-
|
|
526
|
+
:data-lp-field-part="partId"
|
|
532
527
|
@focusout="onFocusOut"
|
|
533
528
|
@interact-outside="guardOutside"
|
|
534
529
|
@escape-key-down="guardOutside"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useAttrs } from "vue"
|
|
3
|
-
import type
|
|
3
|
+
import { useFieldBlur, type useFieldControl } from "../composables/useField"
|
|
4
4
|
import { mergeClass } from "../composables/useMergedClass"
|
|
5
5
|
import { PICKER_CLEAR, PICKER_PRESET, PICKER_PRESETS, PICKER_SIZE, PICKER_TRIGGER, pickerTone } from "./calendar"
|
|
6
6
|
import LpIcon from "./LpIcon.vue"
|
|
@@ -21,13 +21,14 @@ withDefaults(defineProps<{
|
|
|
21
21
|
presetsClass: string
|
|
22
22
|
}>(), { size: "md" })
|
|
23
23
|
const open = defineModel<boolean>("open")
|
|
24
|
-
defineEmits<{ clear: [] }>()
|
|
24
|
+
const emit = defineEmits<{ clear: []; blur: [event: FocusEvent] }>()
|
|
25
|
+
const { partId, onFocusOut } = useFieldBlur((event) => emit("blur", event))
|
|
25
26
|
const attrs = useAttrs()
|
|
26
27
|
</script>
|
|
27
28
|
|
|
28
29
|
<template>
|
|
29
|
-
<div :class="mergeClass('relative w-full', attrs.class as string)" :style="attrs.style as never">
|
|
30
|
-
<LpPopover v-model:open="open" align="start" panel-class="flex w-auto max-w-[calc(100vw-1rem)] flex-col rounded-card p-0 sm:flex-row">
|
|
30
|
+
<div :data-lp-field-part="partId" @focusout="onFocusOut" :class="mergeClass('relative w-full', attrs.class as string)" :style="attrs.style as never">
|
|
31
|
+
<LpPopover v-model:open="open" :data-lp-field-part="partId" @focusout="onFocusOut" align="start" panel-class="flex w-auto max-w-[calc(100vw-1rem)] flex-col rounded-card p-0 sm:flex-row">
|
|
31
32
|
<template #trigger>
|
|
32
33
|
<button
|
|
33
34
|
v-bind="{ ...attrs, class: undefined, style: undefined }"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, inject, provide, useId, type ComputedRef, type InjectionKey } from "vue"
|
|
1
|
+
import { computed, inject, onScopeDispose, provide, useId, type ComputedRef, type InjectionKey } from "vue"
|
|
2
2
|
import type { FieldNoticeOptions } from "./useFieldNotice"
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -93,3 +93,28 @@ export function focusLeft(event: FocusEvent): boolean {
|
|
|
93
93
|
const next = event.relatedTarget
|
|
94
94
|
return !(next instanceof Node && (event.currentTarget as Node | null)?.contains(next))
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/** Trigger and portalled content share a boundary for the control's public blur. */
|
|
98
|
+
export function useFieldBlur(blur: (event: FocusEvent) => void) {
|
|
99
|
+
const partId = `lp-field-${useId()}`
|
|
100
|
+
let cancelPending: (() => void) | undefined
|
|
101
|
+
const inside = (target: EventTarget | null) =>
|
|
102
|
+
!!(target as Element | null)?.closest?.(`[data-lp-field-part="${partId}"]`)
|
|
103
|
+
|
|
104
|
+
function onFocusOut(event: FocusEvent) {
|
|
105
|
+
cancelPending?.()
|
|
106
|
+
if (inside(event.relatedTarget)) return
|
|
107
|
+
if (event.relatedTarget) return blur(event)
|
|
108
|
+
// A removed calendar cell can lose focus before its trigger gets it back.
|
|
109
|
+
const doc = (event.target as Node).ownerDocument!
|
|
110
|
+
const view = doc.defaultView!
|
|
111
|
+
const frame = view.requestAnimationFrame(() => {
|
|
112
|
+
cancelPending = undefined
|
|
113
|
+
if (!inside(doc.activeElement)) blur(event)
|
|
114
|
+
})
|
|
115
|
+
cancelPending = () => view.cancelAnimationFrame(frame)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onScopeDispose(() => cancelPending?.())
|
|
119
|
+
return { partId, onFocusOut }
|
|
120
|
+
}
|
|
@@ -119,7 +119,8 @@ export function useForm<T extends object>(options: UseFormOptions<T>): UseForm<T
|
|
|
119
119
|
const validating = ref(false)
|
|
120
120
|
const submitting = ref(false)
|
|
121
121
|
const submitCount = ref(0)
|
|
122
|
-
const runs = new Map<keyof T | "form",
|
|
122
|
+
const runs = new Map<keyof T | "form", symbol>()
|
|
123
|
+
let validationEpoch = 0
|
|
123
124
|
// What the form-level check set last, so its next run can take back what no
|
|
124
125
|
// longer holds without touching an error a field rule set on the same key.
|
|
125
126
|
const fromForm = new Map<keyof T | "form", string>()
|
|
@@ -148,7 +149,7 @@ export function useForm<T extends object>(options: UseFormOptions<T>): UseForm<T
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
async function runField(name: keyof T): Promise<boolean> {
|
|
151
|
-
const run = (
|
|
152
|
+
const run = Symbol()
|
|
152
153
|
runs.set(name, run)
|
|
153
154
|
const found = await checkField(name)
|
|
154
155
|
if (runs.get(name) !== run) return !errors[name]
|
|
@@ -159,7 +160,7 @@ export function useForm<T extends object>(options: UseFormOptions<T>): UseForm<T
|
|
|
159
160
|
|
|
160
161
|
async function runForm(): Promise<boolean> {
|
|
161
162
|
if (!options.validate) return true
|
|
162
|
-
const run = (
|
|
163
|
+
const run = Symbol()
|
|
163
164
|
runs.set("form", run)
|
|
164
165
|
const found = (await options.validate(plain())) ?? {}
|
|
165
166
|
if (runs.get("form") !== run) return true
|
|
@@ -177,12 +178,17 @@ export function useForm<T extends object>(options: UseFormOptions<T>): UseForm<T
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
async function validate(name?: keyof T): Promise<boolean> {
|
|
181
|
+
const epoch = validationEpoch
|
|
180
182
|
validating.value = true
|
|
181
183
|
try {
|
|
182
|
-
if (name !== undefined)
|
|
184
|
+
if (name !== undefined) {
|
|
185
|
+
const valid = await runField(name)
|
|
186
|
+
return epoch === validationEpoch && valid
|
|
187
|
+
}
|
|
183
188
|
const fields = await Promise.all(keys().map(runField))
|
|
189
|
+
if (epoch !== validationEpoch) return false
|
|
184
190
|
const form = await runForm()
|
|
185
|
-
return fields.every(Boolean) && form && !Object.values(external).some(Boolean)
|
|
191
|
+
return epoch === validationEpoch && fields.every(Boolean) && form && !Object.values(external).some(Boolean)
|
|
186
192
|
} finally {
|
|
187
193
|
validating.value = false
|
|
188
194
|
}
|
|
@@ -202,6 +208,7 @@ export function useForm<T extends object>(options: UseFormOptions<T>): UseForm<T
|
|
|
202
208
|
}
|
|
203
209
|
|
|
204
210
|
function clearState(): void {
|
|
211
|
+
validationEpoch++
|
|
205
212
|
for (const k of Object.keys(errors)) delete errors[k as keyof T]
|
|
206
213
|
for (const k of Object.keys(external)) delete external[k as keyof T]
|
|
207
214
|
for (const k of Object.keys(touched)) delete touched[k as keyof T]
|
|
@@ -242,7 +249,9 @@ export function useForm<T extends object>(options: UseFormOptions<T>): UseForm<T
|
|
|
242
249
|
// and runs the form-level check again once it has run: a cross-field error
|
|
243
250
|
// ("passwords differ") must clear when either field is fixed.
|
|
244
251
|
async function recheck(names: (keyof T)[]): Promise<void> {
|
|
252
|
+
const epoch = validationEpoch
|
|
245
253
|
await Promise.all(names.map((name) => validate(name)))
|
|
254
|
+
if (epoch !== validationEpoch) return
|
|
246
255
|
if (fromForm.size || submitCount.value) await runForm()
|
|
247
256
|
}
|
|
248
257
|
|
|
@@ -64,6 +64,9 @@ export function forwardViewEvents(from: Document, to: Document): () => void {
|
|
|
64
64
|
const Copy = event.constructor as new (type: string, init: Event) => Event
|
|
65
65
|
const copy = new Copy(event.type, event) as Marked
|
|
66
66
|
copy[ORIGIN] = from
|
|
67
|
+
const path = event.composedPath()
|
|
68
|
+
Object.defineProperty(copy, "target", { value: event.target })
|
|
69
|
+
copy.composedPath = () => path
|
|
67
70
|
to.dispatchEvent(copy)
|
|
68
71
|
}
|
|
69
72
|
for (const type of FORWARDED) from.addEventListener(type, forward)
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { defineComponent, useModel, ref, openBlock, createElementBlock, mergeProps, unref, createElementVNode, toDisplayString, createCommentVNode, createVNode, withCtx, createTextVNode, Fragment, renderList, renderSlot, mergeModels, nextTick } from "vue";
|
|
2
|
-
import { useFieldControl } from "../composables/useField.js";
|
|
3
|
-
import { useLabels } from "../composables/useLpConfig.js";
|
|
4
|
-
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
5
|
-
import { useRepeater } from "../composables/useRepeater.js";
|
|
6
|
-
import { _ as _sfc_main$1 } from "./LpButton-CVxBfGjd.js";
|
|
7
|
-
import { _ as _sfc_main$2 } from "./LpIcon-HJP5xMz4.js";
|
|
8
|
-
const _hoisted_1 = { class: "flex items-center justify-between gap-2" };
|
|
9
|
-
const _hoisted_2 = {
|
|
10
|
-
key: 0,
|
|
11
|
-
class: "text-sm font-medium text-ink"
|
|
12
|
-
};
|
|
13
|
-
const _hoisted_3 = { class: "min-w-0 flex-1" };
|
|
14
|
-
const _hoisted_4 = {
|
|
15
|
-
key: 0,
|
|
16
|
-
class: "text-xs text-muted"
|
|
17
|
-
};
|
|
18
|
-
const focusable = 'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex="0"], [contenteditable="true"]';
|
|
19
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
20
|
-
...{ inheritAttrs: false },
|
|
21
|
-
__name: "LpRepeater",
|
|
22
|
-
props: /* @__PURE__ */ mergeModels({
|
|
23
|
-
createItem: { type: Function },
|
|
24
|
-
itemKey: { type: Function },
|
|
25
|
-
label: {},
|
|
26
|
-
disabled: { type: Boolean },
|
|
27
|
-
labels: {}
|
|
28
|
-
}, {
|
|
29
|
-
"modelValue": { default: () => [] },
|
|
30
|
-
"modelModifiers": {}
|
|
31
|
-
}),
|
|
32
|
-
emits: ["update:modelValue"],
|
|
33
|
-
setup(__props, { expose: __expose }) {
|
|
34
|
-
const props = __props;
|
|
35
|
-
const model = useModel(__props, "modelValue");
|
|
36
|
-
const field = useFieldControl(() => ({ wrapped: false, disabled: props.disabled }));
|
|
37
|
-
const text = useLabels("repeater", { add: "Add item", remove: "Remove item {index}", empty: "No items" }, () => props.labels);
|
|
38
|
-
const list = useRepeater({
|
|
39
|
-
value: () => model.value,
|
|
40
|
-
set: (items) => {
|
|
41
|
-
model.value = items;
|
|
42
|
-
},
|
|
43
|
-
createItem: () => props.createItem(),
|
|
44
|
-
itemKey: () => props.itemKey,
|
|
45
|
-
disabled: () => field.value.disabled
|
|
46
|
-
});
|
|
47
|
-
const { rows } = list;
|
|
48
|
-
const root = ref(null);
|
|
49
|
-
const addButton = ref(null);
|
|
50
|
-
function focusRow(index) {
|
|
51
|
-
root.value?.querySelectorAll("[data-lp-repeater-row]")[index]?.querySelector(focusable)?.focus();
|
|
52
|
-
}
|
|
53
|
-
async function add() {
|
|
54
|
-
if (field.value.disabled) return;
|
|
55
|
-
list.add();
|
|
56
|
-
await nextTick();
|
|
57
|
-
focusRow(model.value.length - 1);
|
|
58
|
-
}
|
|
59
|
-
async function remove(index) {
|
|
60
|
-
if (field.value.disabled) return;
|
|
61
|
-
const row = root.value?.querySelectorAll("[data-lp-repeater-row]")[index];
|
|
62
|
-
const restore = row?.contains(row.ownerDocument.activeElement);
|
|
63
|
-
list.remove(index);
|
|
64
|
-
if (!restore) return;
|
|
65
|
-
await nextTick();
|
|
66
|
-
if (model.value.length) focusRow(Math.min(index, model.value.length - 1));
|
|
67
|
-
else addButton.value?.$el?.focus();
|
|
68
|
-
}
|
|
69
|
-
__expose({ add, remove, update: list.update });
|
|
70
|
-
const { class: rootClass, attrs: rest } = useMergedAttrs("flex min-w-0 flex-col gap-2");
|
|
71
|
-
return (_ctx, _cache) => {
|
|
72
|
-
return openBlock(), createElementBlock("div", mergeProps({
|
|
73
|
-
ref_key: "root",
|
|
74
|
-
ref: root
|
|
75
|
-
}, unref(rest), { class: unref(rootClass) }), [
|
|
76
|
-
createElementVNode("div", _hoisted_1, [
|
|
77
|
-
__props.label ? (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(__props.label), 1)) : createCommentVNode("", true),
|
|
78
|
-
createVNode(_sfc_main$1, {
|
|
79
|
-
ref_key: "addButton",
|
|
80
|
-
ref: addButton,
|
|
81
|
-
size: "xs",
|
|
82
|
-
variant: "ghost",
|
|
83
|
-
disabled: unref(field).disabled,
|
|
84
|
-
onClick: add
|
|
85
|
-
}, {
|
|
86
|
-
default: withCtx(() => [
|
|
87
|
-
createVNode(_sfc_main$2, {
|
|
88
|
-
name: "lucide:plus",
|
|
89
|
-
size: 14
|
|
90
|
-
}),
|
|
91
|
-
createTextVNode(toDisplayString(unref(text).add), 1)
|
|
92
|
-
]),
|
|
93
|
-
_: 1
|
|
94
|
-
}, 8, ["disabled"])
|
|
95
|
-
]),
|
|
96
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(rows), ({ item, key }, index) => {
|
|
97
|
-
return openBlock(), createElementBlock("div", {
|
|
98
|
-
key,
|
|
99
|
-
"data-lp-repeater-row": "",
|
|
100
|
-
class: "flex min-w-0 items-start gap-2"
|
|
101
|
-
}, [
|
|
102
|
-
createElementVNode("div", _hoisted_3, [
|
|
103
|
-
renderSlot(_ctx.$slots, "default", {
|
|
104
|
-
item,
|
|
105
|
-
index,
|
|
106
|
-
disabled: unref(field).disabled,
|
|
107
|
-
update: (next) => unref(list).update(index, next),
|
|
108
|
-
remove: () => remove(index)
|
|
109
|
-
})
|
|
110
|
-
]),
|
|
111
|
-
createVNode(_sfc_main$1, {
|
|
112
|
-
variant: "ghost",
|
|
113
|
-
size: "sm",
|
|
114
|
-
square: "",
|
|
115
|
-
disabled: unref(field).disabled,
|
|
116
|
-
"aria-label": unref(text).remove.replace("{index}", String(index + 1)),
|
|
117
|
-
onClick: ($event) => remove(index)
|
|
118
|
-
}, {
|
|
119
|
-
default: withCtx(() => [
|
|
120
|
-
createVNode(_sfc_main$2, {
|
|
121
|
-
name: "lucide:x",
|
|
122
|
-
size: 14
|
|
123
|
-
})
|
|
124
|
-
]),
|
|
125
|
-
_: 1
|
|
126
|
-
}, 8, ["disabled", "aria-label", "onClick"])
|
|
127
|
-
]);
|
|
128
|
-
}), 128)),
|
|
129
|
-
!unref(rows).length ? (openBlock(), createElementBlock("div", _hoisted_4, [
|
|
130
|
-
renderSlot(_ctx.$slots, "empty", {}, () => [
|
|
131
|
-
createTextVNode(toDisplayString(unref(text).empty), 1)
|
|
132
|
-
])
|
|
133
|
-
])) : createCommentVNode("", true)
|
|
134
|
-
], 16);
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
export {
|
|
139
|
-
_sfc_main as _
|
|
140
|
-
};
|