@leavepulse/ui 0.78.1 → 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-CLLhd3WB.js → DropzoneLinkField-CyE7TJ5f.js} +1 -1
- package/dist/chunks/LpAddressInput-ft8fQOGx.js +401 -0
- package/dist/chunks/{LpAppShell-BKbYCHkx.js → LpAppShell-DkvU6IBB.js} +1 -1
- package/dist/chunks/LpAutocomplete-CrZbd0uv.js +310 -0
- package/dist/chunks/LpCheckbox-CCWCwD0y.js +129 -0
- package/dist/chunks/{LpCodeDisplay-D3BUAhV1.js → LpCodeDisplay-D59uPwRb.js} +1 -1
- package/dist/chunks/LpColorPicker-BONURBFN.js +574 -0
- package/dist/chunks/LpDatePicker-C2AOtxgF.js +171 -0
- package/dist/chunks/{LpDateRangePicker-CNQXdTVv.js → LpDateRangePicker-CiXdIAyh.js} +43 -35
- package/dist/chunks/{LpFileTree-C-rJKzEQ.js → LpFileTree-CpNurwZp.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-BgYn4U1J.js → LpFileTreeNode-DdF8Cq-s.js} +1 -1
- package/dist/chunks/{LpInput-D0O6dpuA.js → LpInput-CbI7z5fQ.js} +3 -4
- package/dist/chunks/{LpLogInspector-BdQlX6S-.js → LpLogInspector-CcSJSGan.js} +1 -1
- package/dist/chunks/{LpLogViewer-umCZBapN.js → LpLogViewer-CWu49Dko.js} +2 -2
- package/dist/chunks/{LpModalNav-gOi1Giz1.js → LpModalNav-DI3BW5ff.js} +1 -1
- package/dist/chunks/LpNumberField-C8LfUz_R.js +154 -0
- package/dist/chunks/LpOtpInput-B1X9l9no.js +140 -0
- package/dist/chunks/{LpPaletteTheme-BIXpZLGe.js → LpPaletteTheme-B-xuZiKK.js} +2 -2
- package/dist/chunks/{LpPasswordInput-D4HPhqfV.js → LpPasswordInput-Duc_3wPv.js} +1 -1
- package/dist/chunks/{LpPhoneInput-BSL7NX4z.js → LpPhoneInput-CctcRK9K.js} +124 -118
- package/dist/chunks/LpRadioGroup-DTM0nd9G.js +103 -0
- package/dist/chunks/LpRangeSlider-DJsEZXad.js +97 -0
- package/dist/chunks/LpRepeater-Czib3rmv.js +193 -0
- package/dist/chunks/{LpSecretField-B2roc4Iz.js → LpSecretField-BtlCk3IH.js} +1 -1
- package/dist/chunks/LpSegmented-sskrJKJJ.js +128 -0
- package/dist/chunks/LpSelect-JFZehOkd.js +515 -0
- package/dist/chunks/{LpSidebar-DDNJzb69.js → LpSidebar-CvRvVBTt.js} +1 -1
- package/dist/chunks/LpSlider-C4ilCtZX.js +76 -0
- package/dist/chunks/LpSwitch-DyAYTchz.js +122 -0
- package/dist/chunks/{LpTableFilter-B_XMOeAh.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-ChU0_Ga8.js → LpThemeStudio-R-wohbCY.js} +2 -2
- package/dist/chunks/{LpThemeSwitcher-DmCZ21P_.js → LpThemeSwitcher-Bzy15MCP.js} +1 -1
- package/dist/chunks/LpTimeField-B5n0m8m0.js +115 -0
- package/dist/chunks/{LpTopologyCanvas-jqz1KCq2.js → LpTopologyCanvas-BNgw9RyZ.js} +2 -2
- package/dist/chunks/{LpWorkspaceSwitcher-BeNHCLd5.js → LpWorkspaceSwitcher-ugv-fyig.js} +1 -1
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- 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 +1 -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/LpCheckbox.vue.d.ts +4 -0
- package/dist/components/LpCheckbox.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/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.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/LpImageEditor.vue.d.ts +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLogInspector.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNumberField.vue.d.ts +4 -0
- 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/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/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/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/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/LpShinyText.vue.d.ts +1 -1
- package/dist/components/LpSidebar.vue.d.ts +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSlider.vue.d.ts +4 -0
- package/dist/components/LpSlider.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 +2 -2
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +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/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
- package/dist/components/calendar.d.ts +2 -0
- 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/index.js +34 -34
- package/package.json +1 -1
- package/src/components/LpAddressInput.vue +8 -4
- package/src/components/LpAutocomplete.vue +11 -5
- package/src/components/LpCheckbox.vue +8 -6
- package/src/components/LpColorPicker.vue +7 -4
- package/src/components/LpDatePicker.vue +8 -4
- package/src/components/LpDateRangePicker.vue +8 -4
- package/src/components/LpInput.vue +3 -4
- package/src/components/LpNumberField.vue +11 -6
- package/src/components/LpOtpInput.vue +8 -6
- 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/LpSegmented.vue +8 -4
- package/src/components/LpSelect.vue +8 -7
- package/src/components/LpSlider.vue +8 -4
- 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/calendar.ts +2 -0
- package/src/composables/useField.ts +52 -0
- package/src/composables/useMergedClass.ts +11 -5
- package/dist/chunks/LpAddressInput-BSS49jpm.js +0 -393
- package/dist/chunks/LpAutocomplete-DkY6CSyD.js +0 -302
- package/dist/chunks/LpCheckbox-D6RuDW4X.js +0 -124
- package/dist/chunks/LpColorPicker-COHzV68R.js +0 -567
- package/dist/chunks/LpDatePicker-DwSDY0mt.js +0 -163
- package/dist/chunks/LpNumberField-Csg-1Zh_.js +0 -148
- 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
|
@@ -36,6 +36,7 @@ export type PhoneInputLabels = {
|
|
|
36
36
|
* is what a form should store. Until the library arrives the field takes digits
|
|
37
37
|
* as before, unformatted.
|
|
38
38
|
*/
|
|
39
|
+
import FieldWrapper from "./FieldWrapper.vue"
|
|
39
40
|
import { CONTROL_SIZE } from "./controlSize"
|
|
40
41
|
import {
|
|
41
42
|
ComboboxAnchor,
|
|
@@ -48,8 +49,8 @@ import {
|
|
|
48
49
|
ComboboxTrigger,
|
|
49
50
|
ComboboxViewport,
|
|
50
51
|
} from "reka-ui"
|
|
51
|
-
import { computed, onMounted, ref, shallowRef } from "vue"
|
|
52
|
-
import { useField,
|
|
52
|
+
import { computed, onMounted, ref, shallowRef, useAttrs } from "vue"
|
|
53
|
+
import { useField, useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
53
54
|
import { useFieldNotice, type FieldNoticeOptions } from "../composables/useFieldNotice"
|
|
54
55
|
import { useInputFilter } from "../composables/useInputFilter"
|
|
55
56
|
import { useLabels } from "../composables/useLpConfig"
|
|
@@ -101,6 +102,8 @@ const props = withDefaults(
|
|
|
101
102
|
placeholder?: string
|
|
102
103
|
disabled?: boolean
|
|
103
104
|
invalid?: boolean
|
|
105
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
106
|
+
error?: string
|
|
104
107
|
size?: "sm" | "md" | "lg"
|
|
105
108
|
emptyText?: string
|
|
106
109
|
searchPlaceholder?: string
|
|
@@ -322,7 +325,8 @@ const sizeClass = CONTROL_SIZE
|
|
|
322
325
|
// placing it in a narrow column has to change, so `class="w-48"` must win.
|
|
323
326
|
// style lays out the frame; every other attr (name, autocomplete, @blur…)
|
|
324
327
|
// belongs to the number <input>.
|
|
325
|
-
const
|
|
328
|
+
const attrs = useAttrs()
|
|
329
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, invalid: props.invalid || flagged.value, disabled: props.disabled }))
|
|
326
330
|
const text = useLabels<PhoneInputLabels>("phoneInput", {
|
|
327
331
|
selectCountry: "Select country",
|
|
328
332
|
placeholder: "Phone number",
|
|
@@ -345,8 +349,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
345
349
|
? "border-danger focus-within:border-danger focus-within:ring-danger-soft"
|
|
346
350
|
: "border-line focus-within:border-brand",
|
|
347
351
|
field.value.disabled ? "cursor-not-allowed opacity-55" : "",
|
|
348
|
-
].join(" "),
|
|
349
|
-
)
|
|
352
|
+
].join(" "), () => ownAttrs())
|
|
350
353
|
const inputAttrs = computed(() => {
|
|
351
354
|
const { style: _s, ...others } = rest.value
|
|
352
355
|
return others
|
|
@@ -360,10 +363,11 @@ const guardOutside = useOutsideGuard()
|
|
|
360
363
|
</script>
|
|
361
364
|
|
|
362
365
|
<template>
|
|
366
|
+
<FieldWrapper v-bind="wrapper">
|
|
363
367
|
<div
|
|
364
368
|
data-lp-ring-owner
|
|
365
369
|
:class="rootClass"
|
|
366
|
-
:style="attrs.style as never"
|
|
370
|
+
:style="ownAttrs() ? (attrs.style as never) : undefined"
|
|
367
371
|
>
|
|
368
372
|
<!-- reset-search-term-on-{select,blur}: as in LpSelect, the trigger shows
|
|
369
373
|
the selection and the box only filters, so reka seeding the chosen value
|
|
@@ -460,4 +464,5 @@ const guardOutside = useOutsideGuard()
|
|
|
460
464
|
/>
|
|
461
465
|
<LpFieldNotice v-if="ownNotice" :notice="notice" :id="noticeId" />
|
|
462
466
|
</div>
|
|
467
|
+
</FieldWrapper>
|
|
463
468
|
</template>
|
|
@@ -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 { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot } from "reka-ui"
|
|
11
12
|
import { useId } from "vue"
|
|
12
|
-
import {
|
|
13
|
+
import { useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
13
14
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
14
15
|
import LpIcon from "./LpIcon.vue"
|
|
15
16
|
import type { LpOptionBase } from "./options"
|
|
@@ -22,21 +23,23 @@ export interface RadioOption extends LpOptionBase<string> {
|
|
|
22
23
|
// a disabled option stays visible but cannot be picked. For anything richer
|
|
23
24
|
// (badges, custom layout) pass <LpRadio> children in the default slot instead —
|
|
24
25
|
// they share the same RadioGroupRoot, so selection still flows through v-model.
|
|
25
|
-
const props = withDefaults(defineProps<{ palette?: PaletteControlOptions; modelValue?: string; options?: RadioOption[]; disabled?: boolean
|
|
26
|
+
const props = withDefaults(defineProps<{ palette?: PaletteControlOptions; modelValue?: string; options?: RadioOption[]; disabled?: boolean
|
|
27
|
+
/** A message under the group, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
28
|
+
error?: string }>(), { palette: undefined })
|
|
26
29
|
defineEmits<{ (e: "update:modelValue", value: string): void }>()
|
|
27
30
|
|
|
28
|
-
const field =
|
|
31
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, disabled: props.disabled }))
|
|
29
32
|
const uid = useId()
|
|
30
33
|
|
|
31
34
|
// A radio group stacks with a fixed gap; a dense settings list needs it smaller, and a horizontal layout needs it gone.
|
|
32
35
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
33
|
-
"flex flex-col gap-2",
|
|
34
|
-
)
|
|
36
|
+
"flex flex-col gap-2", () => ownAttrs())
|
|
35
37
|
|
|
36
38
|
usePaletteControl({ disabled: () => field.value.disabled })
|
|
37
39
|
</script>
|
|
38
40
|
|
|
39
41
|
<template>
|
|
42
|
+
<FieldWrapper v-bind="wrapper">
|
|
40
43
|
<RadioGroupRoot
|
|
41
44
|
:model-value="modelValue"
|
|
42
45
|
:disabled="field.disabled"
|
|
@@ -82,4 +85,5 @@ usePaletteControl({ disabled: () => field.value.disabled })
|
|
|
82
85
|
</label>
|
|
83
86
|
</slot>
|
|
84
87
|
</RadioGroupRoot>
|
|
88
|
+
</FieldWrapper>
|
|
85
89
|
</template>
|
|
@@ -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 SliderRail from "./SliderRail.vue"
|
|
10
11
|
import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
|
|
11
12
|
/*
|
|
@@ -16,7 +17,7 @@ import { usePaletteControl, type PaletteControlOptions } from "../composables/us
|
|
|
16
17
|
* focused or dragged, so the numbers are readable without a second readout.
|
|
17
18
|
*/
|
|
18
19
|
import { SliderRoot, SliderThumb } from "reka-ui"
|
|
19
|
-
import {
|
|
20
|
+
import { useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
20
21
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
21
22
|
import { SLIDER_GLIDE, useSliderDrag } from "./slider"
|
|
22
23
|
|
|
@@ -31,6 +32,8 @@ const props = withDefaults(
|
|
|
31
32
|
/** The least distance the two ends keep between them, in model units. */
|
|
32
33
|
minGap?: number
|
|
33
34
|
disabled?: boolean
|
|
35
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
36
|
+
error?: string
|
|
34
37
|
/** Accessible names for the two thumbs when there's no visible label. */
|
|
35
38
|
ariaLabels?: [string, string]
|
|
36
39
|
/** Label over each thumb while it is in use: `(value) => "1:05"`. */
|
|
@@ -58,11 +61,10 @@ function onCommit(v: number[] | undefined) {
|
|
|
58
61
|
if (value) emit("change", value)
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
const field =
|
|
64
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, disabled: props.disabled }))
|
|
62
65
|
|
|
63
66
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
64
|
-
`relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`,
|
|
65
|
-
)
|
|
67
|
+
`relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55 ${SLIDER_GLIDE}`, () => ownAttrs())
|
|
66
68
|
const { dragging, onPointerDown } = useSliderDrag()
|
|
67
69
|
|
|
68
70
|
const thumbClass =
|
|
@@ -74,6 +76,7 @@ usePaletteControl({ disabled: () => field.value.disabled })
|
|
|
74
76
|
</script>
|
|
75
77
|
|
|
76
78
|
<template>
|
|
79
|
+
<FieldWrapper v-bind="wrapper">
|
|
77
80
|
<SliderRoot
|
|
78
81
|
:model-value="[props.modelValue[0], props.modelValue[1]]"
|
|
79
82
|
:min="min"
|
|
@@ -101,4 +104,5 @@ usePaletteControl({ disabled: () => field.value.disabled })
|
|
|
101
104
|
<span v-if="format" :class="labelClass">{{ format(props.modelValue[index]) }}</span>
|
|
102
105
|
</SliderThumb>
|
|
103
106
|
</SliderRoot>
|
|
107
|
+
</FieldWrapper>
|
|
104
108
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T extends object">
|
|
2
|
+
import FieldWrapper from "./FieldWrapper.vue"
|
|
2
3
|
import { nextTick, ref } from "vue"
|
|
3
|
-
import {
|
|
4
|
+
import { useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
4
5
|
import { useLabels } from "../composables/useLpConfig"
|
|
5
6
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
6
7
|
import { useRepeater, type RepeaterItemKey } from "../composables/useRepeater"
|
|
@@ -17,6 +18,8 @@ const props = defineProps<{
|
|
|
17
18
|
itemKey?: RepeaterItemKey<T>
|
|
18
19
|
label?: string
|
|
19
20
|
disabled?: boolean
|
|
21
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
22
|
+
error?: string
|
|
20
23
|
labels?: Partial<{ add: string; remove: string; empty: string }>
|
|
21
24
|
}>()
|
|
22
25
|
const model = defineModel<T[]>({ default: () => [] })
|
|
@@ -25,7 +28,7 @@ defineSlots<{
|
|
|
25
28
|
empty?(): unknown
|
|
26
29
|
}>()
|
|
27
30
|
|
|
28
|
-
const field =
|
|
31
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, disabled: props.disabled }))
|
|
29
32
|
const text = useLabels("repeater", { add: "Add item", remove: "Remove item {index}", empty: "No items" }, () => props.labels)
|
|
30
33
|
const list = useRepeater({
|
|
31
34
|
value: () => model.value,
|
|
@@ -78,11 +81,19 @@ async function remove(index: number) {
|
|
|
78
81
|
else (addButton.value?.$el as HTMLElement | undefined)?.focus({ preventScroll: true })
|
|
79
82
|
}
|
|
80
83
|
defineExpose({ add, remove, update: list.update })
|
|
81
|
-
const { class: rootClass, attrs: rest } = useMergedAttrs("flex min-w-0 flex-col gap-2")
|
|
84
|
+
const { class: rootClass, attrs: rest } = useMergedAttrs("flex min-w-0 flex-col gap-2", () => ownAttrs())
|
|
82
85
|
</script>
|
|
83
86
|
|
|
84
87
|
<template>
|
|
85
|
-
<
|
|
88
|
+
<FieldWrapper v-bind="wrapper">
|
|
89
|
+
<!-- A group, named by its label, so the field's message is read with it. -->
|
|
90
|
+
<div
|
|
91
|
+
v-bind="rest"
|
|
92
|
+
role="group"
|
|
93
|
+
:aria-label="label || undefined"
|
|
94
|
+
:aria-describedby="mergeDescribedBy(rest['aria-describedby'], field.describedBy)"
|
|
95
|
+
:class="rootClass"
|
|
96
|
+
>
|
|
86
97
|
<div class="flex items-center justify-between gap-2">
|
|
87
98
|
<span v-if="label" class="text-sm font-medium text-ink">{{ label }}</span>
|
|
88
99
|
<LpButton ref="addButton" size="xs" variant="ghost" :disabled="field.disabled" @click="add">
|
|
@@ -105,6 +116,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs("flex min-w-0 flex-col
|
|
|
105
116
|
</div>
|
|
106
117
|
</LpShift>
|
|
107
118
|
</div>
|
|
119
|
+
</FieldWrapper>
|
|
108
120
|
</template>
|
|
109
121
|
|
|
110
122
|
<style>
|
|
@@ -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 { isHTMLElement } from "../composables/useView"
|
|
11
12
|
import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
|
|
@@ -19,7 +20,7 @@ import { usePaletteControl, type PaletteControlOptions } from "../composables/us
|
|
|
19
20
|
import { Motion } from "motion-v"
|
|
20
21
|
import { ToggleGroupItem, ToggleGroupRoot } from "reka-ui"
|
|
21
22
|
import { ref, useId } from "vue"
|
|
22
|
-
import {
|
|
23
|
+
import { useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
23
24
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
24
25
|
import { usePillTransition } from "../composables/usePillTransition"
|
|
25
26
|
import AttentionDot from "./AttentionDot.vue"
|
|
@@ -47,6 +48,8 @@ const props = withDefaults(
|
|
|
47
48
|
/** Stretch to fill the container with equal-width segments. */
|
|
48
49
|
block?: boolean
|
|
49
50
|
disabled?: boolean
|
|
51
|
+
/** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
|
|
52
|
+
error?: string
|
|
50
53
|
/**
|
|
51
54
|
* The pill glides under the hovered segment and springs back on leave (the
|
|
52
55
|
* default, as in LpTabs). False pins it to the selected segment and hover
|
|
@@ -80,10 +83,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
80
83
|
// `w-fit`, not left to the parent: in a column a flex item stretches, and
|
|
81
84
|
// the options sat on the left of an empty bar (the theme studio met it).
|
|
82
85
|
props.block ? "flex w-full" : "w-fit",
|
|
83
|
-
].join(" "),
|
|
84
|
-
)
|
|
86
|
+
].join(" "), () => ownAttrs())
|
|
85
87
|
|
|
86
|
-
const field =
|
|
88
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, disabled: props.disabled }))
|
|
87
89
|
|
|
88
90
|
const pillId = `lp-segmented-${useId()}`
|
|
89
91
|
const pillTransition = usePillTransition()
|
|
@@ -114,6 +116,7 @@ usePaletteControl({ disabled: () => field.value.disabled })
|
|
|
114
116
|
</script>
|
|
115
117
|
|
|
116
118
|
<template>
|
|
119
|
+
<FieldWrapper v-bind="wrapper">
|
|
117
120
|
<ToggleGroupRoot
|
|
118
121
|
:model-value="modelValue"
|
|
119
122
|
type="single"
|
|
@@ -152,4 +155,5 @@ usePaletteControl({ disabled: () => field.value.disabled })
|
|
|
152
155
|
<AttentionDot v-if="opt.dot && opt.label" class="mt-1.5" />
|
|
153
156
|
</ToggleGroupItem>
|
|
154
157
|
</ToggleGroupRoot>
|
|
158
|
+
</FieldWrapper>
|
|
155
159
|
</template>
|
|
@@ -39,7 +39,8 @@ import {
|
|
|
39
39
|
ComboboxViewport,
|
|
40
40
|
} from "reka-ui"
|
|
41
41
|
import { computed, nextTick, reactive, ref, useAttrs, useId, watch } from "vue"
|
|
42
|
-
import
|
|
42
|
+
import FieldWrapper from "./FieldWrapper.vue"
|
|
43
|
+
import { useFieldBlur, useOwnField, mergeDescribedBy } from "../composables/useField"
|
|
43
44
|
import { useLabels } from "../composables/useLpConfig"
|
|
44
45
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
45
46
|
import { OPTION_ITEM, optionStagger, POPOVER_PANEL } from "./dropdown"
|
|
@@ -75,6 +76,8 @@ const props = withDefaults(
|
|
|
75
76
|
clearable?: boolean
|
|
76
77
|
disabled?: boolean
|
|
77
78
|
invalid?: boolean
|
|
79
|
+
/** A message under the select, as on LpInput: it marks the select invalid (what `form.field()` hands it). */
|
|
80
|
+
error?: string
|
|
78
81
|
emptyText?: string
|
|
79
82
|
// Server-side search: bind with `v-model:searchTerm`. When provided, the
|
|
80
83
|
// parent owns filtering (fetches matching `options` per term), so the
|
|
@@ -110,11 +113,7 @@ const props = withDefaults(
|
|
|
110
113
|
const SIZE_HEIGHT = CONTROL_SIZE
|
|
111
114
|
const anchorHeight = computed(() => SIZE_HEIGHT[props.size])
|
|
112
115
|
|
|
113
|
-
const field =
|
|
114
|
-
wrapped: false,
|
|
115
|
-
invalid: props.invalid,
|
|
116
|
-
disabled: props.disabled,
|
|
117
|
-
}))
|
|
116
|
+
const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, invalid: props.invalid, disabled: props.disabled }))
|
|
118
117
|
const labels = useLabels<SelectLabels>(
|
|
119
118
|
"select",
|
|
120
119
|
{
|
|
@@ -136,7 +135,7 @@ const emptyLabel = computed(() => props.emptyText ?? labels.value.empty)
|
|
|
136
135
|
const loadingLabel = computed(() => props.loadingText ?? labels.value.loading)
|
|
137
136
|
|
|
138
137
|
// The consumer's `class="w-40"` has to beat the root's own layout.
|
|
139
|
-
const { class: rootClass, attrs: rootAttrs } = useMergedAttrs("relative")
|
|
138
|
+
const { class: rootClass, attrs: rootAttrs } = useMergedAttrs("relative", ownAttrs)
|
|
140
139
|
|
|
141
140
|
const emit = defineEmits<{
|
|
142
141
|
// Full v-model state (single value, array for multiple, or empty).
|
|
@@ -450,6 +449,7 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
|
|
|
450
449
|
the seeding left the list pinned to one option until it was cleared by
|
|
451
450
|
hand. Both flags default true and reach the same writer, so both must go:
|
|
452
451
|
we own the term (`query`) and reka must not write to it. -->
|
|
452
|
+
<FieldWrapper v-bind="wrapper">
|
|
453
453
|
<ComboboxRoot
|
|
454
454
|
:model-value="comboModel"
|
|
455
455
|
:multiple="multiple"
|
|
@@ -635,4 +635,5 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
|
|
|
635
635
|
</ComboboxContent>
|
|
636
636
|
</ComboboxPortal>
|
|
637
637
|
</ComboboxRoot>
|
|
638
|
+
</FieldWrapper>
|
|
638
639
|
</template>
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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
|
|
@@ -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
|
}
|