@leavepulse/ui 0.53.0 → 0.54.0
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/{LpAppShell-BmMYcEVx.js → LpAppShell-DcSXik4a.js} +1 -1
- package/dist/chunks/{LpAvatar-Cb1jtOVv.js → LpAvatar-LTvy0OkE.js} +1 -1
- package/dist/chunks/{LpCard-BdxkMXjo.js → LpCard-DbjO5yaF.js} +1 -1
- package/dist/chunks/{LpCheckbox-Bkj13CbW.js → LpCheckbox-DvVYeOD-.js} +2 -2
- package/dist/chunks/{LpColorPicker-BLqPX5Vw.js → LpColorPicker-D1sQqtXS.js} +1 -1
- package/dist/chunks/{LpContextMenu-1cKew_Dc.js → LpContextMenu-EF_90u3g.js} +5 -1
- package/dist/chunks/{LpFileTree-bk5VjBey.js → LpFileTree-3E4w4Rjr.js} +1 -1
- package/dist/chunks/{LpFileTreeNode-BvMSBM7X.js → LpFileTreeNode-B4uB8Ks_.js} +2 -2
- package/dist/chunks/{LpLogViewer-BDFXzvcl.js → LpLogViewer-BfgtXVKm.js} +1 -1
- package/dist/chunks/{LpModalNav-BAxC9et7.js → LpModalNav-aWwc_5CD.js} +1 -1
- package/dist/chunks/{LpNotificationBell-DsqSyMwS.js → LpNotificationBell-wZ3meeim.js} +1 -1
- package/dist/chunks/LpOtpInput-PbeNJ5In.js +131 -0
- package/dist/chunks/{LpPie-BdSnta11.js → LpPie-B7ti5qCH.js} +1 -1
- package/dist/chunks/{LpRadio-B5AfqTpZ.js → LpRadio-CWA5Eb3M.js} +2 -2
- package/dist/chunks/{LpRadioGroup-DmEW0l78.js → LpRadioGroup-CCn-Q2_c.js} +2 -2
- package/dist/chunks/{LpSidebar-DpEiGi8L.js → LpSidebar-C2oolgwy.js} +1 -1
- package/dist/chunks/{LpSidebarNav-BUuxipX-.js → LpSidebarNav-C9dHu-2b.js} +1 -1
- package/dist/chunks/{LpSwitch-OrcOJX9m.js → LpSwitch-DG2AhXIL.js} +2 -2
- package/dist/chunks/{LpTableFilter-BV8PpLM9.js → LpTableFilter-CrW7_Rj4.js} +1 -1
- package/dist/chunks/{LpTabs-UM0FQmPA.js → LpTabs-BMSBB78h.js} +44 -17
- package/dist/chunks/{LpThemeSwitcher-BbYFFT5w.js → LpThemeSwitcher-CPmhYOjX.js} +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +1 -1
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.d.ts +2 -2
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.d.ts +17 -0
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPaletteHeader.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpRadio.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.js +3 -3
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +10 -2
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/otp.d.ts +2 -0
- package/dist/components/otp.js +17 -1
- package/dist/index.js +18 -18
- package/package.json +1 -1
- package/src/components/LpCheckbox.vue +2 -2
- package/src/components/LpContextMenu.vue +18 -1
- package/src/components/LpOtpInput.vue +84 -28
- package/src/components/LpRadio.vue +2 -2
- package/src/components/LpRadioGroup.vue +2 -2
- package/src/components/LpSwitch.vue +2 -2
- package/src/components/LpTabs.vue +60 -21
- package/src/components/otp.ts +29 -0
- package/dist/chunks/LpOtpInput-CziPF3Oq.js +0 -99
|
@@ -10,6 +10,8 @@ export interface TabItem {
|
|
|
10
10
|
label: string
|
|
11
11
|
/** Optional iconify name (e.g. "lucide:palette"), shown left of the label. */
|
|
12
12
|
icon?: string
|
|
13
|
+
/** A small dot on the tab: something behind it wants a look (unread, a problem). */
|
|
14
|
+
dot?: boolean
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const props = withDefaults(
|
|
@@ -24,15 +26,23 @@ const props = withDefaults(
|
|
|
24
26
|
* sliding pill marking the active one (for nav bars over a custom surface).
|
|
25
27
|
* "underline" is a flat row over a hairline baseline, marking the active
|
|
26
28
|
* tab with a sliding bottom bar instead of a pill (page-section nav style).
|
|
29
|
+
* "bottom" is a phone's bottom navigation: full width, equal-share tabs
|
|
30
|
+
* with the icon over the label, a sliding bar along the top edge marking
|
|
31
|
+
* the active one, and padding for the gesture strip and side insets of an
|
|
32
|
+
* edge-to-edge screen. Place it as the last row of the layout.
|
|
27
33
|
*/
|
|
28
|
-
variant?: "contained" | "plain" | "underline"
|
|
34
|
+
variant?: "contained" | "plain" | "underline" | "bottom"
|
|
29
35
|
/** Tint the active pill + label with the brand colour (nav-bar style). */
|
|
30
36
|
accent?: boolean
|
|
31
37
|
/** Stretch the bar to full width with equal-share triggers. */
|
|
32
38
|
block?: boolean
|
|
39
|
+
/** Accessible name of the tab list, e.g. "Main" for an app's navigation. */
|
|
40
|
+
label?: string
|
|
33
41
|
}>(),
|
|
34
42
|
{ variant: "contained", accent: false, block: false },
|
|
35
43
|
)
|
|
44
|
+
|
|
45
|
+
const bottom = computed(() => props.variant === "bottom")
|
|
36
46
|
const emit = defineEmits<{ (e: "update:modelValue", value: string): void }>()
|
|
37
47
|
|
|
38
48
|
// Held here rather than left to reka, so the active marker follows the active
|
|
@@ -88,7 +98,11 @@ const panelLayoutTransition = computed(() =>
|
|
|
88
98
|
: { type: "spring" as const, stiffness: 520, damping: 42, mass: 0.9 },
|
|
89
99
|
)
|
|
90
100
|
|
|
101
|
+
// A bottom bar marks only the active tab: on a phone a tap is the hover, and
|
|
102
|
+
// a bar that chased the mouse across a desktop window would point at a tab
|
|
103
|
+
// nobody chose.
|
|
91
104
|
function pillUnder(value: string, active?: string): boolean {
|
|
105
|
+
if (bottom.value) return active === value
|
|
92
106
|
return hovered.value ? hovered.value === value : active === value
|
|
93
107
|
}
|
|
94
108
|
|
|
@@ -119,30 +133,38 @@ function releaseFocusAfterPointer(event: PointerEvent) {
|
|
|
119
133
|
>
|
|
120
134
|
<TabsList
|
|
121
135
|
ref="list"
|
|
122
|
-
|
|
123
|
-
:class="
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
:aria-label="label"
|
|
137
|
+
:class="bottom
|
|
138
|
+
? 'grid w-full auto-cols-fr grid-flow-col border-t border-line bg-surface pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]'
|
|
139
|
+
: [
|
|
140
|
+
'max-w-full gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
|
|
141
|
+
block ? 'flex w-full' : 'inline-flex',
|
|
142
|
+
variant === 'contained' ? 'rounded-control border border-line bg-surface-soft p-1' : '',
|
|
143
|
+
variant === 'underline' ? 'border-b border-line' : '',
|
|
144
|
+
]"
|
|
128
145
|
@pointerleave="hovered = null"
|
|
129
146
|
>
|
|
130
147
|
<TabsTrigger
|
|
131
148
|
v-for="item in items"
|
|
132
149
|
:key="item.value"
|
|
133
150
|
:value="item.value"
|
|
134
|
-
class="relative inline-flex
|
|
151
|
+
class="relative inline-flex items-center justify-center outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
|
|
135
152
|
:class="[
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
bottom
|
|
154
|
+
? 'min-h-14 min-w-0 flex-col gap-0.5 px-1 py-1.5 text-[11px] font-medium focus-visible:ring-inset'
|
|
155
|
+
: 'shrink-0 gap-1.5 whitespace-nowrap text-sm',
|
|
156
|
+
block && !bottom ? 'flex-1' : '',
|
|
157
|
+
variant === 'underline' ? 'rounded-t-md px-3 pt-1.5 pb-2' : '',
|
|
158
|
+
variant === 'contained' || variant === 'plain' ? 'rounded-md px-3 py-1.5' : '',
|
|
138
159
|
accent ? 'data-[state=active]:text-brand' : 'data-[state=active]:text-ink',
|
|
139
|
-
hovered === item.value ? 'text-ink' : 'text-muted',
|
|
160
|
+
hovered === item.value && !bottom ? 'text-ink' : 'text-muted',
|
|
140
161
|
]"
|
|
141
162
|
@pointerenter="hovered = item.value"
|
|
142
163
|
@pointerup="releaseFocusAfterPointer"
|
|
143
164
|
>
|
|
144
|
-
<!-- Active marker: a
|
|
145
|
-
|
|
165
|
+
<!-- Active marker: a bar under the tab in "underline", over it in
|
|
166
|
+
"bottom", a filled pill otherwise. All slide between tabs via the
|
|
167
|
+
shared motion layoutId. -->
|
|
146
168
|
<Motion
|
|
147
169
|
v-if="pillUnder(item.value, current)"
|
|
148
170
|
:layout-id="pillId"
|
|
@@ -152,15 +174,32 @@ function releaseFocusAfterPointer(event: PointerEvent) {
|
|
|
152
174
|
'absolute inset-x-0 -bottom-px z-0 h-0.5 rounded-full',
|
|
153
175
|
accent ? 'bg-brand' : 'bg-ink',
|
|
154
176
|
]
|
|
155
|
-
:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
? '
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
: bottom
|
|
178
|
+
? [
|
|
179
|
+
'absolute inset-x-[22%] top-0 z-0 h-0.5 rounded-b-full',
|
|
180
|
+
accent ? 'bg-brand' : 'bg-ink',
|
|
181
|
+
]
|
|
182
|
+
: [
|
|
183
|
+
'absolute inset-0 z-0 rounded-md shadow-sm',
|
|
184
|
+
accent
|
|
185
|
+
? 'border border-brand/35 bg-brand/12'
|
|
186
|
+
: 'border border-line bg-surface-raised',
|
|
187
|
+
]"
|
|
188
|
+
/>
|
|
189
|
+
<span v-if="item.icon" class="relative z-10 inline-flex">
|
|
190
|
+
<LpIcon :name="item.icon" :size="bottom ? 22 : 14" />
|
|
191
|
+
<span
|
|
192
|
+
v-if="item.dot"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
class="absolute -right-1 -top-0.5 size-2 rounded-full bg-accent"
|
|
195
|
+
/>
|
|
196
|
+
</span>
|
|
197
|
+
<span class="relative z-10" :class="bottom ? 'max-w-full truncate' : ''">{{ item.label }}</span>
|
|
198
|
+
<span
|
|
199
|
+
v-if="item.dot && !item.icon"
|
|
200
|
+
aria-hidden="true"
|
|
201
|
+
class="relative z-10 size-1.5 self-start rounded-full bg-accent"
|
|
161
202
|
/>
|
|
162
|
-
<LpIcon v-if="item.icon" :name="item.icon" :size="14" class="relative z-10" />
|
|
163
|
-
<span class="relative z-10">{{ item.label }}</span>
|
|
164
203
|
</TabsTrigger>
|
|
165
204
|
</TabsList>
|
|
166
205
|
|
package/src/components/otp.ts
CHANGED
|
@@ -14,3 +14,32 @@ export function joinCells(cells: OtpCells): string {
|
|
|
14
14
|
export function splitCode(code: string | undefined, length: number): string[] {
|
|
15
15
|
return (code ?? "").split("").slice(0, length)
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* Upper-case every filled cell, keeping the holes where they are.
|
|
20
|
+
*
|
|
21
|
+
* A phone opens its keyboard in lower case while backup and pairing codes are
|
|
22
|
+
* printed in capitals, so a code typed letter by letter came out "ab12cd"
|
|
23
|
+
* against an "AB12CD" on screen: a difference the reader cannot fix by looking,
|
|
24
|
+
* and one a server comparing exactly rejects.
|
|
25
|
+
*/
|
|
26
|
+
export function upperCells<T extends OtpCells>(cells: T): T {
|
|
27
|
+
const out = [] as unknown as T
|
|
28
|
+
cells.forEach((c, i) => {
|
|
29
|
+
out[i] = typeof c === "string" ? c.toUpperCase() : c
|
|
30
|
+
})
|
|
31
|
+
out.length = cells.length
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* The cell indexes a group separator follows: after every `group` cells, never
|
|
37
|
+
* after the last. Eight characters are read and copied as two blocks of four,
|
|
38
|
+
* which is how such codes are printed, far more easily than as one row.
|
|
39
|
+
*/
|
|
40
|
+
export function groupBreaks(length: number, group: number | undefined): Set<number> {
|
|
41
|
+
const breaks = new Set<number>()
|
|
42
|
+
if (!group || group < 1 || group >= length) return breaks
|
|
43
|
+
for (let i = group; i < length; i += group) breaks.add(i - 1)
|
|
44
|
+
return breaks
|
|
45
|
+
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { defineComponent, ref, watch, computed, openBlock, createBlock, unref, mergeProps, withCtx, createElementBlock, Fragment, renderList, normalizeClass } from "vue";
|
|
2
|
-
import { PinInputRoot, PinInputInput } from "reka-ui";
|
|
3
|
-
import { useFieldControl } from "../composables/useField.js";
|
|
4
|
-
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
5
|
-
import { splitCode, joinCells } from "../components/otp.js";
|
|
6
|
-
const __default__ = { inheritAttrs: false };
|
|
7
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
8
|
-
...__default__,
|
|
9
|
-
__name: "LpOtpInput",
|
|
10
|
-
props: {
|
|
11
|
-
modelValue: {},
|
|
12
|
-
length: { default: 6 },
|
|
13
|
-
disabled: { type: Boolean },
|
|
14
|
-
mask: { type: Boolean },
|
|
15
|
-
alphanumeric: { type: Boolean, default: false },
|
|
16
|
-
invalid: { type: Boolean },
|
|
17
|
-
size: { default: "md" }
|
|
18
|
-
},
|
|
19
|
-
emits: ["update:modelValue", "complete"],
|
|
20
|
-
setup(__props, { emit: __emit }) {
|
|
21
|
-
const props = __props;
|
|
22
|
-
const emit = __emit;
|
|
23
|
-
const local = ref(splitCode(props.modelValue, props.length));
|
|
24
|
-
let emitted = props.modelValue ?? "";
|
|
25
|
-
watch(
|
|
26
|
-
() => props.modelValue ?? "",
|
|
27
|
-
(value) => {
|
|
28
|
-
if (value === emitted) return;
|
|
29
|
-
emitted = value;
|
|
30
|
-
local.value = splitCode(value, props.length);
|
|
31
|
-
}
|
|
32
|
-
);
|
|
33
|
-
watch(
|
|
34
|
-
() => props.length,
|
|
35
|
-
(length) => {
|
|
36
|
-
local.value = local.value.slice(0, length);
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
const cells = computed({
|
|
40
|
-
get: () => local.value,
|
|
41
|
-
set: (arr) => {
|
|
42
|
-
local.value = arr;
|
|
43
|
-
emitted = joinCells(arr);
|
|
44
|
-
emit("update:modelValue", emitted);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
const field = useFieldControl(() => ({
|
|
48
|
-
wrapped: false,
|
|
49
|
-
invalid: props.invalid,
|
|
50
|
-
disabled: props.disabled
|
|
51
|
-
}));
|
|
52
|
-
function onComplete(arr) {
|
|
53
|
-
emit("complete", joinCells(arr));
|
|
54
|
-
}
|
|
55
|
-
const cellSize = {
|
|
56
|
-
sm: "size-9 text-base",
|
|
57
|
-
md: "size-11 text-lg",
|
|
58
|
-
lg: "size-13 text-xl"
|
|
59
|
-
};
|
|
60
|
-
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
61
|
-
"flex items-center gap-2"
|
|
62
|
-
);
|
|
63
|
-
return (_ctx, _cache) => {
|
|
64
|
-
return openBlock(), createBlock(unref(PinInputRoot), mergeProps({
|
|
65
|
-
modelValue: cells.value,
|
|
66
|
-
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => cells.value = $event),
|
|
67
|
-
type: __props.alphanumeric ? "text" : "number",
|
|
68
|
-
mask: __props.mask,
|
|
69
|
-
disabled: unref(field).disabled,
|
|
70
|
-
otp: "",
|
|
71
|
-
class: unref(rootClass)
|
|
72
|
-
}, unref(rest), { onComplete }), {
|
|
73
|
-
default: withCtx(() => [
|
|
74
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.length, (i) => {
|
|
75
|
-
return openBlock(), createBlock(unref(PinInputInput), {
|
|
76
|
-
key: i,
|
|
77
|
-
index: i - 1,
|
|
78
|
-
id: i === 1 ? unref(field).id : void 0,
|
|
79
|
-
"aria-invalid": unref(field).invalid || void 0,
|
|
80
|
-
"aria-describedby": unref(field).describedBy,
|
|
81
|
-
class: normalizeClass([
|
|
82
|
-
cellSize[__props.size],
|
|
83
|
-
"rounded-control border bg-surface-soft text-center font-semibold text-ink caret-brand",
|
|
84
|
-
"outline-none transition-colors duration-[var(--duration-fast)]",
|
|
85
|
-
"focus:border-brand focus:ring-2 focus:ring-ring",
|
|
86
|
-
"disabled:cursor-not-allowed disabled:opacity-55",
|
|
87
|
-
unref(field).invalid ? "border-danger focus:border-danger focus:ring-danger-soft" : "border-line"
|
|
88
|
-
])
|
|
89
|
-
}, null, 8, ["index", "id", "aria-invalid", "aria-describedby", "class"]);
|
|
90
|
-
}), 128))
|
|
91
|
-
]),
|
|
92
|
-
_: 1
|
|
93
|
-
}, 16, ["modelValue", "type", "mask", "disabled", "class"]);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
export {
|
|
98
|
-
_sfc_main as _
|
|
99
|
-
};
|