@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.
Files changed (62) hide show
  1. package/dist/chunks/{LpAppShell-BmMYcEVx.js → LpAppShell-DcSXik4a.js} +1 -1
  2. package/dist/chunks/{LpAvatar-Cb1jtOVv.js → LpAvatar-LTvy0OkE.js} +1 -1
  3. package/dist/chunks/{LpCard-BdxkMXjo.js → LpCard-DbjO5yaF.js} +1 -1
  4. package/dist/chunks/{LpCheckbox-Bkj13CbW.js → LpCheckbox-DvVYeOD-.js} +2 -2
  5. package/dist/chunks/{LpColorPicker-BLqPX5Vw.js → LpColorPicker-D1sQqtXS.js} +1 -1
  6. package/dist/chunks/{LpContextMenu-1cKew_Dc.js → LpContextMenu-EF_90u3g.js} +5 -1
  7. package/dist/chunks/{LpFileTree-bk5VjBey.js → LpFileTree-3E4w4Rjr.js} +1 -1
  8. package/dist/chunks/{LpFileTreeNode-BvMSBM7X.js → LpFileTreeNode-B4uB8Ks_.js} +2 -2
  9. package/dist/chunks/{LpLogViewer-BDFXzvcl.js → LpLogViewer-BfgtXVKm.js} +1 -1
  10. package/dist/chunks/{LpModalNav-BAxC9et7.js → LpModalNav-aWwc_5CD.js} +1 -1
  11. package/dist/chunks/{LpNotificationBell-DsqSyMwS.js → LpNotificationBell-wZ3meeim.js} +1 -1
  12. package/dist/chunks/LpOtpInput-PbeNJ5In.js +131 -0
  13. package/dist/chunks/{LpPie-BdSnta11.js → LpPie-B7ti5qCH.js} +1 -1
  14. package/dist/chunks/{LpRadio-B5AfqTpZ.js → LpRadio-CWA5Eb3M.js} +2 -2
  15. package/dist/chunks/{LpRadioGroup-DmEW0l78.js → LpRadioGroup-CCn-Q2_c.js} +2 -2
  16. package/dist/chunks/{LpSidebar-DpEiGi8L.js → LpSidebar-C2oolgwy.js} +1 -1
  17. package/dist/chunks/{LpSidebarNav-BUuxipX-.js → LpSidebarNav-C9dHu-2b.js} +1 -1
  18. package/dist/chunks/{LpSwitch-OrcOJX9m.js → LpSwitch-DG2AhXIL.js} +2 -2
  19. package/dist/chunks/{LpTableFilter-BV8PpLM9.js → LpTableFilter-CrW7_Rj4.js} +1 -1
  20. package/dist/chunks/{LpTabs-UM0FQmPA.js → LpTabs-BMSBB78h.js} +44 -17
  21. package/dist/chunks/{LpThemeSwitcher-BbYFFT5w.js → LpThemeSwitcher-CPmhYOjX.js} +1 -1
  22. package/dist/components/LpAppShell.vue.js +1 -1
  23. package/dist/components/LpAvatar.vue.js +1 -1
  24. package/dist/components/LpCard.vue.js +1 -1
  25. package/dist/components/LpChart.vue.js +1 -1
  26. package/dist/components/LpCheckbox.vue.js +1 -1
  27. package/dist/components/LpColorPicker.vue.js +1 -1
  28. package/dist/components/LpCommandPalette.vue.js +1 -1
  29. package/dist/components/LpContextMenu.vue.d.ts +2 -2
  30. package/dist/components/LpContextMenu.vue.js +1 -1
  31. package/dist/components/LpFileTree.vue.js +1 -1
  32. package/dist/components/LpFileTreeNode.vue.js +1 -1
  33. package/dist/components/LpLogViewer.vue.js +1 -1
  34. package/dist/components/LpModalNav.vue.js +1 -1
  35. package/dist/components/LpNotificationBell.vue.js +1 -1
  36. package/dist/components/LpOtpInput.vue.d.ts +17 -0
  37. package/dist/components/LpOtpInput.vue.js +1 -1
  38. package/dist/components/LpPaletteHeader.vue.js +1 -1
  39. package/dist/components/LpPie.vue.js +1 -1
  40. package/dist/components/LpRadio.vue.js +1 -1
  41. package/dist/components/LpRadioGroup.vue.js +1 -1
  42. package/dist/components/LpSidebar.vue.js +1 -1
  43. package/dist/components/LpSidebarNav.vue.js +1 -1
  44. package/dist/components/LpSwitch.vue.js +1 -1
  45. package/dist/components/LpTable.vue.js +3 -3
  46. package/dist/components/LpTableFilter.vue.js +1 -1
  47. package/dist/components/LpTabs.vue.d.ts +10 -2
  48. package/dist/components/LpTabs.vue.js +1 -1
  49. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  50. package/dist/components/otp.d.ts +2 -0
  51. package/dist/components/otp.js +17 -1
  52. package/dist/index.js +18 -18
  53. package/package.json +1 -1
  54. package/src/components/LpCheckbox.vue +2 -2
  55. package/src/components/LpContextMenu.vue +18 -1
  56. package/src/components/LpOtpInput.vue +84 -28
  57. package/src/components/LpRadio.vue +2 -2
  58. package/src/components/LpRadioGroup.vue +2 -2
  59. package/src/components/LpSwitch.vue +2 -2
  60. package/src/components/LpTabs.vue +60 -21
  61. package/src/components/otp.ts +29 -0
  62. 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
- class="max-w-full gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
123
- :class="[
124
- block ? 'flex w-full' : 'inline-flex',
125
- variant === 'contained' ? 'rounded-control border border-line bg-surface-soft p-1' : '',
126
- variant === 'underline' ? 'border-b border-line' : '',
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 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
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
- block ? 'flex-1' : '',
137
- variant === 'underline' ? 'rounded-t-md px-3 pt-1.5 pb-2' : 'rounded-md px-3 py-1.5',
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 bottom bar in "underline", a filled pill otherwise.
145
- Both slide between tabs via the shared motion layoutId. -->
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
- 'absolute inset-0 z-0 rounded-md shadow-sm',
157
- accent
158
- ? 'border border-brand/35 bg-brand/12'
159
- : 'border border-line bg-surface-raised',
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
 
@@ -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
- };