@leavepulse/ui 0.59.0 → 0.61.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/README.md +8 -0
- package/dist/assets/{LpCommandPalette-BM_IyzwB.css → LpCommandPalette-DA5pmuPi.css} +21 -21
- package/dist/assets/{LpTable-B_QCQCbB.css → LpTable-DpXyPzs8.css} +19 -19
- package/dist/chunks/{ContextMenuItems-CioDScj3.js → ContextMenuItems-qXReFtQW.js} +1 -1
- package/dist/chunks/{LayoutCanvas-BcGLJVBq.js → LayoutCanvas-t7LWFCEn.js} +1 -1
- package/dist/chunks/{LayoutNode-BkBn6dvM.js → LayoutNode-DHsE6GwM.js} +1 -1
- package/dist/chunks/{LpActionBar-DCN-_pZP.js → LpActionBar-BsSp66S_.js} +2 -2
- package/dist/chunks/{LpAddressInput-D-H94m4q.js → LpAddressInput-CzdZguBo.js} +7 -7
- package/dist/chunks/{LpAlert-CXMn1GYE.js → LpAlert--KLL4NLk.js} +1 -1
- package/dist/chunks/{LpAppShell-BeMuKEjh.js → LpAppShell-BXMNDEpF.js} +3 -3
- package/dist/chunks/{LpAutocomplete-hJ_6CHlh.js → LpAutocomplete-D9mY5Tqk.js} +102 -37
- package/dist/chunks/{LpAvatar-CeW4Z5CL.js → LpAvatar-K6MOMfgn.js} +2 -2
- package/dist/chunks/{LpBreadcrumbs-BLzOEgAF.js → LpBreadcrumbs-CndzPYiG.js} +1 -1
- package/dist/chunks/{LpButton-vhlwmpBy.js → LpButton-CJSD7N0Z.js} +1 -1
- package/dist/chunks/{LpCalendar-BLoE8Qm7.js → LpCalendar-CXVDbb2M.js} +1 -1
- package/dist/chunks/{LpCard-Gbr8a1xK.js → LpCard-D9HVgX8Y.js} +1 -1
- package/dist/chunks/{LpCheckbox-B_C8sOoT.js → LpCheckbox-DV9W6T3H.js} +1 -1
- package/dist/chunks/{LpCodeBlock-D6zhp30a.js → LpCodeBlock-t6MLA3fb.js} +1 -1
- package/dist/chunks/{LpColorPicker-CVXwjEtX.js → LpColorPicker-BO1ZTY51.js} +2 -2
- package/dist/chunks/{LpConfirmDialog-DkgIT-Xg.js → LpConfirmDialog-tkgyY3eu.js} +3 -3
- package/dist/chunks/{LpConfirmHost-CYzXWFOI.js → LpConfirmHost-CoYIcUdl.js} +1 -1
- package/dist/chunks/{LpContextMenu-DRFd-iyc.js → LpContextMenu-Cv_F2iCG.js} +20 -5
- package/dist/chunks/{LpDatePicker-B-VMHMgy.js → LpDatePicker-DxdrVNCG.js} +3 -3
- package/dist/chunks/{LpDateRangePicker-B68KPKPl.js → LpDateRangePicker-DjeaqikC.js} +2 -2
- package/dist/chunks/{LpDisclosure-Ct-VBHxt.js → LpDisclosure-s8cQ7KQq.js} +1 -1
- package/dist/chunks/{LpDrawer-Rc6eAJrr.js → LpDrawer-C97yoWmh.js} +1 -1
- package/dist/chunks/{LpDropdownMenu-BoJQsjzt.js → LpDropdownMenu-CbyYu4Rw.js} +11 -4
- package/dist/chunks/{LpEmptyState-CBwwkkdA.js → LpEmptyState-CX8fdGSU.js} +1 -1
- package/dist/chunks/{LpFieldNotice-9hH4ITZa.js → LpFieldNotice-B--RzKfj.js} +1 -1
- package/dist/chunks/{LpFileTree-Bs57DhVa.js → LpFileTree-DTwUj0L-.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-XgJW65v5.js → LpFileTreeNode-D-XLPZ6T.js} +3 -3
- package/dist/chunks/{LpFormField-BpQ6WJPe.js → LpFormField-DMPPL1jF.js} +1 -1
- package/dist/chunks/{LpHintLayer-B5-6FyEf.js → LpHintLayer-DsclGYcD.js} +1 -1
- package/dist/chunks/{LpHints-BqoZXHZk.js → LpHints-BTpvisgP.js} +10 -4
- package/dist/chunks/{LpIcon-B5guGKrW.js → LpIcon-CTuDVTYv.js} +1 -1
- package/dist/chunks/{LpInput-WvyP02ID.js → LpInput-C7FHxzj2.js} +2 -2
- package/dist/chunks/{LpLightbox-Dq-e36uT.js → LpLightbox-DiksFa4L.js} +1 -1
- package/dist/chunks/{LpLogViewer-B92bAftG.js → LpLogViewer-D9UgNz2m.js} +2 -2
- package/dist/chunks/{LpMapPicker-BH5csaBt.js → LpMapPicker-DybIlWnF.js} +1 -1
- package/dist/chunks/{LpMediaGallery-B1kDypJ-.js → LpMediaGallery-Cic9UHsa.js} +2 -2
- package/dist/chunks/{LpModal-DvGD6kEe.js → LpModal-QFfctZsj.js} +1 -1
- package/dist/chunks/{LpModalNav-B-_zRTS-.js → LpModalNav-D4rmflQn.js} +4 -4
- package/dist/chunks/{LpNotificationBell-63pvatBj.js → LpNotificationBell-CyylOlZR.js} +4 -4
- package/dist/chunks/{LpNumberField-CJPjmoDz.js → LpNumberField-BFSRVGkJ.js} +1 -1
- package/dist/chunks/{LpPagination-DehAaJLY.js → LpPagination-BFl0I7tL.js} +1 -1
- package/dist/chunks/{LpPasswordInput-BK25_vNG.js → LpPasswordInput-C1G4k_r0.js} +2 -2
- package/dist/chunks/{LpPhoneInput-Bxg7a_qD.js → LpPhoneInput-CZrpVxla.js} +2 -2
- package/dist/chunks/{LpPie-Coa2GMHt.js → LpPie-B2deu8CI.js} +2 -2
- package/dist/chunks/{LpRangeCalendar-D5sDID9W.js → LpRangeCalendar-COeOYF52.js} +1 -1
- package/dist/chunks/{LpSection-B19VPYmr.js → LpSection-BAkbf0OK.js} +1 -1
- package/dist/chunks/{LpSegmented-DbrUjuSA.js → LpSegmented-DslFku0N.js} +1 -1
- package/dist/chunks/{LpSelect-DynjWK1A.js → LpSelect-C4OXN_q3.js} +1 -1
- package/dist/chunks/{LpSidebar-CKamWtPo.js → LpSidebar-D8tub3bt.js} +3 -3
- package/dist/chunks/{LpSidebarNav-Y4LV0ZEn.js → LpSidebarNav-BBSRg34G.js} +2 -2
- package/dist/chunks/{LpStat-BS2TEXYK.js → LpStat-oYk6V1LZ.js} +1 -1
- package/dist/chunks/{LpStepper-BSglWF45.js → LpStepper-YgHGzgfD.js} +1 -1
- package/dist/chunks/LpSwap-D82jiecV.js +38 -0
- package/dist/chunks/{LpTableFilter-CwHXVxJt.js → LpTableFilter-Ba-rS4H1.js} +2 -2
- package/dist/chunks/{LpTabs-D01b8jBI.js → LpTabs-00v3VQx4.js} +1 -1
- package/dist/chunks/{LpTagsInput-CHbQoAAA.js → LpTagsInput-Sc56iy_j.js} +3 -3
- package/dist/chunks/{LpTextarea-Bz2IWhTH.js → LpTextarea-CQZccQI3.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-CbzL6ipO.js → LpThemeSwitcher-ZFFftBQn.js} +2 -2
- package/dist/chunks/{LpTimeField-DvGXgdES.js → LpTimeField-BTEt0kvL.js} +1 -1
- package/dist/chunks/{LpToaster-C9EacEfR.js → LpToaster-D3OisETH.js} +2 -2
- package/dist/chunks/{LpTour-D7m9ZxNk.js → LpTour-C29-1wha.js} +4 -1
- package/dist/chunks/{MenuTitle-B9HdaBwE.js → MenuTitle-FeC6I24N.js} +1 -1
- package/dist/chunks/{TourCard-DCHOjr6e.js → TourCard-AjN0uhs1.js} +2 -2
- package/dist/chunks/{icons-QqBscG5I.js → icons-CuO3W46H.js} +1 -1
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.d.ts +24 -0
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +2 -2
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +30 -19
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.d.ts +7 -2
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.d.ts +7 -2
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +4 -2
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +1 -1
- package/dist/components/LpPaletteHeader.vue.js +2 -2
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.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/LpStepper.vue.js +1 -1
- package/dist/components/LpSwap.vue.d.ts +19 -0
- package/dist/components/LpSwap.vue.js +4 -0
- package/dist/components/LpTable.vue.js +13 -11
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTour.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/composables/useHintSystem.d.ts +67 -0
- package/dist/composables/useHintSystem.js +110 -12
- package/dist/composables/useToast.d.ts +13 -4
- package/dist/composables/useToast.js +13 -2
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +101 -95
- package/package.json +2 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpAutocomplete.vue +110 -24
- package/src/components/LpCommandPalette.vue +26 -4
- package/src/components/LpContextMenu.vue +22 -1
- package/src/components/LpDropdownMenu.vue +13 -2
- package/src/components/LpHints.vue +7 -1
- package/src/components/LpSwap.vue +45 -0
- package/src/components/LpTable.vue +2 -1
- package/src/components/LpToaster.vue +1 -1
- package/src/components/LpTour.vue +3 -0
- package/src/composables/useHintSystem.ts +152 -12
- package/src/composables/useToast.ts +32 -5
- package/src/index.ts +5 -2
- package/src/kit-icons.ts +1 -1
- package/src/tokens/selection.css +20 -0
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
ComboboxRoot,
|
|
26
26
|
ComboboxViewport,
|
|
27
27
|
} from "reka-ui"
|
|
28
|
-
import { computed, ref, useAttrs, watch } from "vue"
|
|
28
|
+
import { computed, nextTick, ref, useAttrs, watch } from "vue"
|
|
29
29
|
import { useFieldControl } from "../composables/useField"
|
|
30
30
|
import { useInputFilter } from "../composables/useInputFilter"
|
|
31
31
|
import { useLabels } from "../composables/useLpConfig"
|
|
@@ -36,6 +36,15 @@ export interface AutocompleteOption {
|
|
|
36
36
|
value: string
|
|
37
37
|
label?: string
|
|
38
38
|
description?: string
|
|
39
|
+
/** A picture beside the option: an avatar, an icon of a server. */
|
|
40
|
+
image?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What the field's value stands for: a name for an id, with an optional muted hint and picture. */
|
|
44
|
+
export interface AutocompleteSelection {
|
|
45
|
+
label: string
|
|
46
|
+
hint?: string
|
|
47
|
+
image?: string
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
const props = withDefaults(
|
|
@@ -65,6 +74,14 @@ const props = withDefaults(
|
|
|
65
74
|
pattern?: RegExp | string
|
|
66
75
|
/** Allowed-character RegExp; hard-blocks other input (see LpInput). */
|
|
67
76
|
restrict?: RegExp
|
|
77
|
+
/**
|
|
78
|
+
* What the current value stands for, shown in the field itself while it is
|
|
79
|
+
* not being edited: a picked id reads as a person (avatar in place of the
|
|
80
|
+
* icon, name, muted hint) instead of digits nobody recognises, and nothing
|
|
81
|
+
* appears or jumps below the field. Focusing it brings the raw text back.
|
|
82
|
+
* When room runs short the hint gives way first; the name stays.
|
|
83
|
+
*/
|
|
84
|
+
selected?: AutocompleteSelection | null
|
|
68
85
|
}>(),
|
|
69
86
|
{
|
|
70
87
|
placeholder: "Type…",
|
|
@@ -136,6 +153,12 @@ const filtered = computed(() => {
|
|
|
136
153
|
|
|
137
154
|
const open = ref(false)
|
|
138
155
|
const focused = ref(false)
|
|
156
|
+
const showSelected = computed(() => !!props.selected?.label && !focused.value)
|
|
157
|
+
// Focusing a picked value starts a new search from its name, not from the raw
|
|
158
|
+
// value: the name is put in the box selected, so typing replaces it, and
|
|
159
|
+
// leaving without typing keeps the pick as it was.
|
|
160
|
+
const draft = ref<string | null>(null)
|
|
161
|
+
const showImage = computed(() => !!props.selected?.image && (!focused.value || draft.value !== null))
|
|
139
162
|
|
|
140
163
|
// Threshold met AND something to show → the list is allowed to open.
|
|
141
164
|
const canOpen = computed(
|
|
@@ -174,19 +197,31 @@ const anchorSize = {
|
|
|
174
197
|
function onInput(event: Event) {
|
|
175
198
|
filterInput(event)
|
|
176
199
|
typing.value = true
|
|
200
|
+
draft.value = null
|
|
177
201
|
text.value = (event.target as HTMLInputElement).value
|
|
178
202
|
}
|
|
179
203
|
|
|
180
|
-
function onFocus() {
|
|
204
|
+
function onFocus(event: FocusEvent) {
|
|
181
205
|
// Arriving at a field that already holds a value shows every option, not just
|
|
182
206
|
// the one it currently holds.
|
|
183
207
|
typing.value = false
|
|
184
208
|
focused.value = true
|
|
209
|
+
if (props.selected?.label) {
|
|
210
|
+
draft.value = props.selected.label
|
|
211
|
+
const input = event.target as HTMLInputElement
|
|
212
|
+
void nextTick(() => input.select())
|
|
213
|
+
}
|
|
185
214
|
if (canOpen.value) open.value = true
|
|
186
215
|
}
|
|
187
216
|
|
|
217
|
+
function onBlur() {
|
|
218
|
+
focused.value = false
|
|
219
|
+
draft.value = null
|
|
220
|
+
}
|
|
221
|
+
|
|
188
222
|
function choose(opt: AutocompleteOption) {
|
|
189
223
|
typing.value = false
|
|
224
|
+
draft.value = null
|
|
190
225
|
emit("update:modelValue", opt.value)
|
|
191
226
|
emit("select", opt.value)
|
|
192
227
|
open.value = false
|
|
@@ -194,6 +229,7 @@ function choose(opt: AutocompleteOption) {
|
|
|
194
229
|
|
|
195
230
|
function clear() {
|
|
196
231
|
typing.value = false
|
|
232
|
+
draft.value = null
|
|
197
233
|
emit("update:modelValue", "")
|
|
198
234
|
open.value = false
|
|
199
235
|
}
|
|
@@ -220,7 +256,28 @@ function clear() {
|
|
|
220
256
|
: 'border-line focus-within:border-brand',
|
|
221
257
|
]"
|
|
222
258
|
>
|
|
223
|
-
|
|
259
|
+
<!-- Picture and icon share one fixed box and cross-fade, so the text
|
|
260
|
+
beside them never moves when one gives way to the other. -->
|
|
261
|
+
<span
|
|
262
|
+
v-if="icon || selected?.image"
|
|
263
|
+
class="relative grid shrink-0 place-items-center"
|
|
264
|
+
:class="selected?.image ? 'size-5' : 'size-4'"
|
|
265
|
+
>
|
|
266
|
+
<LpIcon
|
|
267
|
+
v-if="icon"
|
|
268
|
+
:name="icon"
|
|
269
|
+
:size="16"
|
|
270
|
+
class="col-start-1 row-start-1 text-muted transition-[opacity,scale] duration-[var(--duration-fast)] motion-reduce:transition-none"
|
|
271
|
+
:class="showImage ? 'scale-75 opacity-0' : 'opacity-100'"
|
|
272
|
+
/>
|
|
273
|
+
<img
|
|
274
|
+
v-if="selected?.image"
|
|
275
|
+
:src="selected.image"
|
|
276
|
+
alt=""
|
|
277
|
+
class="col-start-1 row-start-1 size-5 rounded-full object-cover transition-[opacity,scale] duration-[var(--duration-fast)] motion-reduce:transition-none"
|
|
278
|
+
:class="showImage ? 'opacity-100' : 'scale-75 opacity-0'"
|
|
279
|
+
/>
|
|
280
|
+
</span>
|
|
224
281
|
<!--
|
|
225
282
|
Bind value one-way and update from the raw DOM event, NOT via reka's
|
|
226
283
|
ComboboxInput v-model: reka defers the value/search update to nextTick
|
|
@@ -228,23 +285,49 @@ function clear() {
|
|
|
228
285
|
keystroke lag by one char. Reading event.target.value here is synchronous
|
|
229
286
|
so `text` (and the reactive `open`) track the field exactly.
|
|
230
287
|
-->
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
288
|
+
<div class="relative flex min-w-0 flex-1 self-stretch">
|
|
289
|
+
<ComboboxInput
|
|
290
|
+
v-bind="inputAttrs"
|
|
291
|
+
:id="field.id"
|
|
292
|
+
:model-value="draft ?? text"
|
|
293
|
+
:placeholder="placeholder"
|
|
294
|
+
:disabled="field.disabled"
|
|
295
|
+
:required="field.required || undefined"
|
|
296
|
+
:aria-invalid="isInvalidState || undefined"
|
|
297
|
+
:aria-describedby="field.describedBy"
|
|
298
|
+
class="lp-autofill min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted"
|
|
299
|
+
:class="showSelected && 'text-transparent placeholder:text-transparent'"
|
|
300
|
+
@beforeinput="onBeforeInput"
|
|
301
|
+
@paste="onPaste"
|
|
302
|
+
@compositionend="onCompositionEnd"
|
|
303
|
+
@input="onInput"
|
|
304
|
+
@focus="onFocus"
|
|
305
|
+
@blur="onBlur"
|
|
306
|
+
/>
|
|
307
|
+
<!-- Focus and blur do not animate the name: it sits exactly where the
|
|
308
|
+
input's own text sits, so that swap cannot be seen. The hint, which
|
|
309
|
+
exists in one state alone, eases in and out beside it, and the
|
|
310
|
+
whole line fades when a pick appears or is cleared. -->
|
|
311
|
+
<Transition
|
|
312
|
+
enter-active-class="transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none"
|
|
313
|
+
leave-active-class="transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none"
|
|
314
|
+
enter-from-class="opacity-0"
|
|
315
|
+
leave-to-class="opacity-0"
|
|
316
|
+
>
|
|
317
|
+
<span
|
|
318
|
+
v-if="selected?.label"
|
|
319
|
+
class="pointer-events-none absolute inset-0 flex min-w-0 items-center gap-1.5"
|
|
320
|
+
aria-hidden="true"
|
|
321
|
+
>
|
|
322
|
+
<span class="max-w-full shrink-0 truncate" :class="showSelected ? '' : 'invisible'">{{ selected.label }}</span>
|
|
323
|
+
<span
|
|
324
|
+
v-if="selected.hint"
|
|
325
|
+
class="min-w-0 truncate text-sm text-muted transition-[opacity,translate] duration-[var(--duration-fast)] motion-reduce:transition-none"
|
|
326
|
+
:class="showSelected ? 'opacity-100' : '-translate-x-1 opacity-0'"
|
|
327
|
+
>{{ selected.hint }}</span>
|
|
328
|
+
</span>
|
|
329
|
+
</Transition>
|
|
330
|
+
</div>
|
|
248
331
|
<button
|
|
249
332
|
v-if="clearable && hasValue"
|
|
250
333
|
type="button"
|
|
@@ -285,10 +368,13 @@ function clear() {
|
|
|
285
368
|
:class="[OPTION_ITEM, 'justify-between']"
|
|
286
369
|
@select.prevent="choose(opt)"
|
|
287
370
|
>
|
|
288
|
-
<span class="flex min-w-0
|
|
289
|
-
<
|
|
290
|
-
<span
|
|
291
|
-
{{ opt.
|
|
371
|
+
<span class="flex min-w-0 items-center gap-2.5">
|
|
372
|
+
<img v-if="opt.image" :src="opt.image" alt="" class="size-6 shrink-0 rounded-full object-cover" />
|
|
373
|
+
<span class="flex min-w-0 flex-col">
|
|
374
|
+
<span class="truncate">{{ opt.label ?? opt.value }}</span>
|
|
375
|
+
<span v-if="opt.description" class="truncate text-xs text-muted">
|
|
376
|
+
{{ opt.description }}
|
|
377
|
+
</span>
|
|
292
378
|
</span>
|
|
293
379
|
</span>
|
|
294
380
|
</ComboboxItem>
|
|
@@ -44,6 +44,8 @@ import { useAsyncTask } from "../composables/useAsyncTask"
|
|
|
44
44
|
import { useGlass } from "../composables/useGlass"
|
|
45
45
|
import { useClipboard } from "../composables/useClipboard"
|
|
46
46
|
import { useHotkeys } from "../composables/useHotkeys"
|
|
47
|
+
import { hints } from "../composables/useHints"
|
|
48
|
+
import { HINT_SIGNALS, runningHintSystem } from "../composables/useHintSystem"
|
|
47
49
|
import { useModalLayer } from "./modalDepth"
|
|
48
50
|
import { useLabels } from "../composables/useLpConfig"
|
|
49
51
|
import {
|
|
@@ -516,9 +518,20 @@ function onTab(event: KeyboardEvent) {
|
|
|
516
518
|
event.stopPropagation()
|
|
517
519
|
const actionsMode: Command = { id: `${c.id}:actions`, label: c.label, icon: c.icon, children: actions }
|
|
518
520
|
syntheticModes.add(actionsMode)
|
|
521
|
+
hints.use(HINT_SIGNALS.paletteActions)
|
|
519
522
|
enter(actionsMode)
|
|
520
523
|
}
|
|
521
524
|
|
|
525
|
+
// A returning user who never pressed Tab gets the footer key lit up, nothing
|
|
526
|
+
// more: the palette is the one place they already came to on purpose.
|
|
527
|
+
const PALETTE_ACTIONS_AFTER = 3
|
|
528
|
+
const showActionsKey = computed(() => {
|
|
529
|
+
const system = runningHintSystem()
|
|
530
|
+
return !!system?.state.loaded
|
|
531
|
+
&& system.count(HINT_SIGNALS.palette) >= PALETTE_ACTIONS_AFTER
|
|
532
|
+
&& system.count(HINT_SIGNALS.paletteActions) === 0
|
|
533
|
+
})
|
|
534
|
+
|
|
522
535
|
/*
|
|
523
536
|
* A mode holds the command it was entered through, and a static `children`
|
|
524
537
|
* list is a snapshot of that moment. When the app rebuilds its commands (a
|
|
@@ -1162,8 +1175,10 @@ function onEscape(event: KeyboardEvent) {
|
|
|
1162
1175
|
watch(
|
|
1163
1176
|
() => props.open,
|
|
1164
1177
|
async (o) => {
|
|
1165
|
-
if (o)
|
|
1166
|
-
|
|
1178
|
+
if (o) {
|
|
1179
|
+
claim()
|
|
1180
|
+
hints.use(HINT_SIGNALS.palette)
|
|
1181
|
+
} else release()
|
|
1167
1182
|
cancel()
|
|
1168
1183
|
query.value = ""
|
|
1169
1184
|
select(0)
|
|
@@ -1461,8 +1476,15 @@ useHotkeys(() =>
|
|
|
1461
1476
|
</span>
|
|
1462
1477
|
<!-- Only inside a mode: at the root Backspace just edits the query,
|
|
1463
1478
|
and advertising it there would be a lie. -->
|
|
1464
|
-
<span
|
|
1465
|
-
|
|
1479
|
+
<span
|
|
1480
|
+
v-if="selectedActions.length"
|
|
1481
|
+
class="flex items-center gap-1 transition-colors"
|
|
1482
|
+
:class="showActionsKey && 'font-medium text-brand'"
|
|
1483
|
+
>
|
|
1484
|
+
<kbd
|
|
1485
|
+
class="rounded-small border px-1 text-[10px] transition-colors"
|
|
1486
|
+
:class="showActionsKey ? 'border-brand' : 'border-line'"
|
|
1487
|
+
>tab</kbd>
|
|
1466
1488
|
{{ text.actions }}
|
|
1467
1489
|
</span>
|
|
1468
1490
|
<span v-if="trail.length" class="flex items-center gap-1">
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ContextMenuContent, ContextMenuPortal, ContextMenuRoot, ContextMenuTrigger } from "reka-ui"
|
|
10
10
|
import { computed } from "vue"
|
|
11
|
+
import { hints } from "../composables/useHints"
|
|
12
|
+
import { HINT_SIGNALS, noteContextTarget } from "../composables/useHintSystem"
|
|
11
13
|
import ContextMenuItems from "./ContextMenuItems.vue"
|
|
12
14
|
import MenuTitle from "./MenuTitle.vue"
|
|
13
15
|
import { POPOVER_PANEL } from "./dropdown"
|
|
@@ -52,6 +54,11 @@ const props = defineProps<{
|
|
|
52
54
|
/** An icon beside the title, or `titleImage` for a picture (an avatar, a logo). */
|
|
53
55
|
titleIcon?: string
|
|
54
56
|
titleImage?: string
|
|
57
|
+
/**
|
|
58
|
+
* Do not count this element as "has a right-click menu" for hint tips: for
|
|
59
|
+
* menus that only hold generic items (a whole table without row actions).
|
|
60
|
+
*/
|
|
61
|
+
noHint?: boolean
|
|
55
62
|
}>()
|
|
56
63
|
|
|
57
64
|
// With no items the trigger is disabled, so the element keeps the browser's
|
|
@@ -59,6 +66,7 @@ const props = defineProps<{
|
|
|
59
66
|
// per element. `always` overrides this so a late-populated menu is ready on the
|
|
60
67
|
// very first click.
|
|
61
68
|
const enabled = computed(() => props.always || props.items.length > 0)
|
|
69
|
+
const hinted = computed(() => enabled.value && !props.noHint)
|
|
62
70
|
|
|
63
71
|
const PANEL = `${POPOVER_PANEL} z-(--z-popover) min-w-48 p-1 outline-none`
|
|
64
72
|
|
|
@@ -91,6 +99,17 @@ function keepContextMenu(event: Event) {
|
|
|
91
99
|
if (enabled.value) event.stopPropagation()
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
function opened(open: boolean) {
|
|
103
|
+
if (open) hints.use(HINT_SIGNALS.contextMenu)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function leftClick(event: MouseEvent) {
|
|
107
|
+
if (!hinted.value) return
|
|
108
|
+
hints.use(HINT_SIGNALS.contextTarget)
|
|
109
|
+
const el = event.currentTarget as Element
|
|
110
|
+
noteContextTarget((event.target as Element).closest?.("[data-lp-context-row]") ?? el)
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
function returnFocus(event: Event) {
|
|
95
114
|
const saved = field
|
|
96
115
|
field = null
|
|
@@ -106,10 +125,12 @@ function returnFocus(event: Event) {
|
|
|
106
125
|
the reka trigger remounted the wrapped content whenever items arrived
|
|
107
126
|
late, losing its state and focus. A disabled trigger leaves the browser's
|
|
108
127
|
own menu in place. -->
|
|
109
|
-
<ContextMenuRoot>
|
|
128
|
+
<ContextMenuRoot @update:open="opened">
|
|
110
129
|
<ContextMenuTrigger
|
|
111
130
|
as-child
|
|
112
131
|
:disabled="!enabled"
|
|
132
|
+
:data-lp-context-menu="hinted ? '' : undefined"
|
|
133
|
+
@click.capture="leftClick"
|
|
113
134
|
@contextmenu.capture="rememberField"
|
|
114
135
|
@contextmenu="keepContextMenu"
|
|
115
136
|
>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { hints } from "../composables/useHints"
|
|
3
|
+
import { HINT_SIGNALS } from "../composables/useHintSystem"
|
|
2
4
|
import {
|
|
3
5
|
DropdownMenuContent,
|
|
4
6
|
DropdownMenuItem,
|
|
@@ -20,18 +22,27 @@ export interface MenuItem {
|
|
|
20
22
|
onSelect?: () => void
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
defineProps<{
|
|
25
|
+
const props = defineProps<{
|
|
24
26
|
items: MenuItem[]
|
|
25
27
|
/** A heading naming what the menu acts on; see LpContextMenu's `title`. */
|
|
26
28
|
title?: string
|
|
27
29
|
subtitle?: string
|
|
28
30
|
titleIcon?: string
|
|
29
31
|
titleImage?: string
|
|
32
|
+
/**
|
|
33
|
+
* The same items are also on a right click (a row's "…" next to its context
|
|
34
|
+
* menu). Opening it then counts toward the hint system's right-click tip.
|
|
35
|
+
*/
|
|
36
|
+
rightClickToo?: boolean
|
|
30
37
|
}>()
|
|
38
|
+
|
|
39
|
+
function opened(open: boolean) {
|
|
40
|
+
if (open && props.rightClickToo) hints.use(HINT_SIGNALS.menuButton)
|
|
41
|
+
}
|
|
31
42
|
</script>
|
|
32
43
|
|
|
33
44
|
<template>
|
|
34
|
-
<DropdownMenuRoot>
|
|
45
|
+
<DropdownMenuRoot @update:open="opened">
|
|
35
46
|
<DropdownMenuTrigger as-child>
|
|
36
47
|
<slot name="trigger" />
|
|
37
48
|
</DropdownMenuTrigger>
|
|
@@ -68,6 +68,7 @@ const title = computed(() => {
|
|
|
68
68
|
const o = offer.value
|
|
69
69
|
if (!o) return ""
|
|
70
70
|
if (o.kind === "nudge") return notes.value[o.nudge.feature]?.title ?? ""
|
|
71
|
+
if (o.kind === "tip") return o.tip.title
|
|
71
72
|
if (o.kind === "news") return fill(labels.value.newsTitle)
|
|
72
73
|
return fill(o.kind === "assigned" ? labels.value.assignedTitle : labels.value.tourTitle)
|
|
73
74
|
})
|
|
@@ -76,6 +77,7 @@ const description = computed(() => {
|
|
|
76
77
|
const o = offer.value
|
|
77
78
|
if (!o) return ""
|
|
78
79
|
if (o.kind === "nudge") return o.nudge.text
|
|
80
|
+
if (o.kind === "tip") return o.tip.keys ? `${o.tip.text} ${o.tip.keys}` : o.tip.text
|
|
79
81
|
if (o.kind === "news") return o.items.map((n) => n.text).join(" ")
|
|
80
82
|
return o.kind === "assigned" ? labels.value.assignedText : labels.value.tourText
|
|
81
83
|
})
|
|
@@ -86,6 +88,10 @@ const actions = computed<ActionBarAction[]>(() => {
|
|
|
86
88
|
const answer = (choice: "show" | "later" | "never") => () => props.system.answer(choice)
|
|
87
89
|
const show: ActionBarAction = { label: labels.value.show, icon: "lucide:sparkles", onClick: answer("show") }
|
|
88
90
|
if (o.kind === "nudge") return [{ label: labels.value.noThanks, variant: "ghost", onClick: answer("never") }, show]
|
|
91
|
+
if (o.kind === "tip") {
|
|
92
|
+
const later: ActionBarAction = { label: labels.value.gotIt, variant: o.tip.step ? "ghost" : undefined, onClick: answer("later") }
|
|
93
|
+
return [{ label: labels.value.never, variant: "ghost", onClick: answer("never") }, later, ...(o.tip.step ? [show] : [])]
|
|
94
|
+
}
|
|
89
95
|
if (o.kind === "news") return [{ label: labels.value.gotIt, variant: "ghost", onClick: answer("later") }, show]
|
|
90
96
|
return [
|
|
91
97
|
...(o.kind === "tour" ? [{ label: labels.value.never, variant: "ghost" as const, onClick: answer("never") }] : []),
|
|
@@ -109,7 +115,7 @@ const actions = computed<ActionBarAction[]>(() => {
|
|
|
109
115
|
:open="!!offer"
|
|
110
116
|
:title="title"
|
|
111
117
|
:description="description"
|
|
112
|
-
:icon="offer?.kind === 'news' ? 'lucide:gift' : 'lucide:compass'"
|
|
118
|
+
:icon="offer?.kind === 'news' ? 'lucide:gift' : offer?.kind === 'tip' ? 'lucide:lightbulb' : 'lucide:compass'"
|
|
113
119
|
:actions="actions"
|
|
114
120
|
/>
|
|
115
121
|
</template>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* One piece of content standing in for another: the section picked in a side
|
|
4
|
+
* nav, the step of a wizard, the view chosen in a segmented control. Without
|
|
5
|
+
* it the old content vanishes and the new one appears in the same frame, and
|
|
6
|
+
* everything below jumps to the new height.
|
|
7
|
+
*
|
|
8
|
+
* <LpSidebar v-model="section" :items="items" />
|
|
9
|
+
* <LpSwap :swap-key="section">
|
|
10
|
+
* <SectionBody :name="section" />
|
|
11
|
+
* </LpSwap>
|
|
12
|
+
*
|
|
13
|
+
* The outgoing content fades out quickly, the incoming one rises in the way
|
|
14
|
+
* LpTabs panels do (`tab-in`), and the box eases between the two heights
|
|
15
|
+
* through LpShift, so what sits under it glides instead of jolting.
|
|
16
|
+
*
|
|
17
|
+
* `swapKey` is what decides that the content is "another one". Change it per
|
|
18
|
+
* section, not per record: a key that changes on every reload replays the
|
|
19
|
+
* entrance for data that merely refreshed.
|
|
20
|
+
*/
|
|
21
|
+
import LpShift from "./LpShift.vue"
|
|
22
|
+
|
|
23
|
+
defineProps<{
|
|
24
|
+
/** Identity of the current content; a new value plays the swap. */
|
|
25
|
+
swapKey: string | number
|
|
26
|
+
/** Turn the motion off (the content still swaps). */
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
}>()
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<LpShift axis="height" :disabled="disabled">
|
|
33
|
+
<Transition
|
|
34
|
+
mode="out-in"
|
|
35
|
+
:css="!disabled"
|
|
36
|
+
enter-active-class="animate-[tab-in_220ms_var(--ease-emphasized)] motion-reduce:animate-none"
|
|
37
|
+
leave-active-class="transition-opacity duration-[var(--duration-fast)] ease-out motion-reduce:transition-none"
|
|
38
|
+
leave-to-class="opacity-0"
|
|
39
|
+
>
|
|
40
|
+
<div :key="swapKey">
|
|
41
|
+
<slot />
|
|
42
|
+
</div>
|
|
43
|
+
</Transition>
|
|
44
|
+
</LpShift>
|
|
45
|
+
</template>
|
|
@@ -1157,7 +1157,7 @@ const barInsetTop = computed(() =>
|
|
|
1157
1157
|
<!-- The table-wide menu, which is what a right-click on empty header space
|
|
1158
1158
|
reaches: rows and headers carry their own and take the event first, so
|
|
1159
1159
|
this is the fallback rather than a competitor. -->
|
|
1160
|
-
<LpContextMenu :items="contextMenu" always>
|
|
1160
|
+
<LpContextMenu :items="contextMenu" always :no-hint="!rowMenu">
|
|
1161
1161
|
<table ref="tableEl" class="w-full border-collapse text-sm">
|
|
1162
1162
|
<!-- The background belongs on `thead` itself, not only on its row:
|
|
1163
1163
|
`border-collapse` collapses the borders, and with a sticky header the
|
|
@@ -1293,6 +1293,7 @@ const barInsetTop = computed(() =>
|
|
|
1293
1293
|
:class="selectedSet.has(keyFor(row, index)) ? 'bg-brand-soft/40' : ''"
|
|
1294
1294
|
v-bind="roving.itemProps(String(keyFor(row, index)))"
|
|
1295
1295
|
:data-paint-key="selectable ? String(keyFor(row, index)) : undefined"
|
|
1296
|
+
:data-lp-context-row="rowMenu ? '' : undefined"
|
|
1296
1297
|
@click="onRowClick(row, keyFor(row, index))"
|
|
1297
1298
|
@pointerdown="onRowPointerDown($event, keyFor(row, index))"
|
|
1298
1299
|
@keydown="onRowKeydown"
|
|
@@ -225,7 +225,7 @@ let copiedTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
225
225
|
const copyable = (t: ToastItem) => t.variant === "error" || t.variant === "warning"
|
|
226
226
|
|
|
227
227
|
async function copyToast(t: ToastItem) {
|
|
228
|
-
if (!(await copy([t.title, t.message].filter(Boolean).join("\n")))) return
|
|
228
|
+
if (!(await copy([t.title, t.message, t.details].filter(Boolean).join("\n")))) return
|
|
229
229
|
copiedId.value = tag(t)
|
|
230
230
|
clearTimeout(copiedTimer)
|
|
231
231
|
copiedTimer = setTimeout(() => (copiedId.value = null), 1500)
|
|
@@ -270,10 +270,12 @@ watch(
|
|
|
270
270
|
if (value) {
|
|
271
271
|
direction = 1
|
|
272
272
|
document.addEventListener("click", onTargetClick, true)
|
|
273
|
+
document.addEventListener("contextmenu", onTargetClick, true)
|
|
273
274
|
frame ||= requestAnimationFrame(track)
|
|
274
275
|
void show()
|
|
275
276
|
} else {
|
|
276
277
|
document.removeEventListener("click", onTargetClick, true)
|
|
278
|
+
document.removeEventListener("contextmenu", onTargetClick, true)
|
|
277
279
|
end()
|
|
278
280
|
}
|
|
279
281
|
},
|
|
@@ -286,6 +288,7 @@ watch(index, () => {
|
|
|
286
288
|
|
|
287
289
|
onBeforeUnmount(() => {
|
|
288
290
|
document.removeEventListener("click", onTargetClick, true)
|
|
291
|
+
document.removeEventListener("contextmenu", onTargetClick, true)
|
|
289
292
|
if (open.value) end()
|
|
290
293
|
})
|
|
291
294
|
|