@leavepulse/ui 0.39.1 → 0.40.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.
@@ -0,0 +1,64 @@
1
+
2
+ /*
3
+ * The dialog changes width and height when the preview pane comes and goes;
4
+ * eased, so the list does not jump sideways under the pointer.
5
+ */
6
+ .lp-cmd-resizing[data-v-79f73da4] {
7
+ transition:
8
+ width 260ms var(--ease-emphasized, ease),
9
+ min-height 260ms var(--ease-emphasized, ease);
10
+ }
11
+ .lp-cmd-pane-enter-active[data-v-79f73da4] {
12
+ transition:
13
+ opacity 220ms ease 60ms,
14
+ transform 260ms var(--ease-emphasized, ease);
15
+ }
16
+ .lp-cmd-pane-leave-active[data-v-79f73da4] {
17
+ transition: opacity 120ms ease;
18
+ }
19
+ .lp-cmd-pane-enter-from[data-v-79f73da4] {
20
+ opacity: 0;
21
+ transform: translateX(16px);
22
+ }
23
+ .lp-cmd-pane-leave-to[data-v-79f73da4] {
24
+ opacity: 0;
25
+ }
26
+ .lp-cmd-para[data-v-79f73da4] {
27
+ animation: lp-cmd-para-in-79f73da4 260ms ease-out both;
28
+ }
29
+ @keyframes lp-cmd-para-in-79f73da4 {
30
+ from {
31
+ opacity: 0;
32
+ transform: translateY(4px);
33
+ }
34
+ }
35
+ @media (prefers-reduced-motion: reduce) {
36
+ .lp-cmd-resizing[data-v-79f73da4],
37
+ .lp-cmd-pane-enter-active[data-v-79f73da4],
38
+ .lp-cmd-pane-leave-active[data-v-79f73da4] {
39
+ transition: none;
40
+ }
41
+ .lp-cmd-para[data-v-79f73da4] {
42
+ animation: none;
43
+ }
44
+ }
45
+ .lp-cmd-excerpt[data-v-79f73da4] {
46
+ display: -webkit-box;
47
+ -webkit-box-orient: vertical;
48
+ overflow: hidden;
49
+ }
50
+ /* Each result eases in; honours reduced motion. */
51
+ .lp-cmd-item[data-v-79f73da4] {
52
+ animation: lp-cmd-in-79f73da4 140ms ease-out both;
53
+ }
54
+ @keyframes lp-cmd-in-79f73da4 {
55
+ from {
56
+ opacity: 0;
57
+ transform: translateY(3px);
58
+ }
59
+ }
60
+ @media (prefers-reduced-motion: reduce) {
61
+ .lp-cmd-item[data-v-79f73da4] {
62
+ animation: none;
63
+ }
64
+ }
@@ -90,8 +90,11 @@ export interface PaletteLabels {
90
90
  copyLink: string;
91
91
  /** Toast after "copy link"; empty for none. */
92
92
  linkCopied: string;
93
- /** The preview pane when the highlighted row has nothing to show. */
94
- noPreview: string;
93
+ /**
94
+ * @deprecated The pane now closes for a row with nothing to show, so this
95
+ * text is never displayed. Kept so existing `labels` objects still type-check.
96
+ */
97
+ noPreview?: string;
95
98
  }
96
99
  type __VLS_Props = {
97
100
  open?: boolean;
@@ -146,14 +149,14 @@ type __VLS_Props = {
146
149
  * A pane beside the list showing the highlighted result in full — its
147
150
  * `preview` text, or the `preview` slot. For palettes that search
148
151
  * content rather than commands: the excerpt says why a row matched, the
149
- * pane lets it be read without leaving the page. Opt-in and fixed while
150
- * open, so the dialog does not change width as results come and go.
152
+ * pane lets it be read without leaving the page. It opens only for a row
153
+ * with `preview` or `excerpt` (any row, with the slot): the dialog eases
154
+ * wider, the pane slides in, and the text arrives paragraph by paragraph.
151
155
  * Hidden below the md breakpoint, where there is no room beside the list.
152
156
  *
153
- * The dialog takes one width class or the other, never a base width plus
154
- * an `md:` override: a consumer's Tailwind build can emit the two in an
155
- * order where the base wins, and the pane opened squeezed into 40rem.
156
- * Both widths are 92vw on a narrow screen, so no breakpoint is needed.
157
+ * The width is an inline style rather than a pair of classes: a base
158
+ * width plus an `md:` override could be emitted by a consumer's Tailwind
159
+ * build in an order where the base won, and the pane opened squeezed.
157
160
  */
158
161
  previewPane?: boolean;
159
162
  /**
@@ -1,4 +1,4 @@
1
- import { defineComponent, computed, ref, useId, shallowRef, watch, nextTick, openBlock, createBlock, unref, withCtx, createVNode, normalizeClass, withModifiers, createTextVNode, createElementVNode, createElementBlock, Fragment, renderList, createCommentVNode, toDisplayString, withDirectives, withKeys, vModelText, resolveDynamicComponent, normalizeStyle, renderSlot } from "vue";
1
+ import { defineComponent, computed, ref, useId, shallowRef, useSlots, watch, nextTick, openBlock, createBlock, unref, withCtx, createVNode, normalizeClass, withModifiers, normalizeStyle, createTextVNode, createElementVNode, createElementBlock, Fragment, renderList, createCommentVNode, toDisplayString, withDirectives, withKeys, vModelText, resolveDynamicComponent, Transition, renderSlot } from "vue";
2
2
  import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from "reka-ui";
3
3
  import { _ as _sfc_main$1 } from "../chunks/LpIcon-CCnX5_2j.js";
4
4
  import { _ as _sfc_main$2 } from "../chunks/LpScrollArea-D8xyKXcF.js";
@@ -6,7 +6,7 @@ import { useAsyncTask } from "../composables/useAsyncTask.js";
6
6
  import { useClipboard } from "../composables/useClipboard.js";
7
7
  import { useHotkeys } from "../composables/useHotkeys.js";
8
8
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
9
- import '../assets/LpCommandPalette-DKL4O4JP.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
9
+ import '../assets/LpCommandPalette-B5eZBHox.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
10
10
  const _hoisted_2 = ["title", "onClick"];
11
11
  const _hoisted_3 = ["aria-activedescendant", "placeholder", "onKeydown"];
12
12
  const _hoisted_4 = { class: "flex min-h-0 flex-1" };
@@ -42,39 +42,33 @@ const _hoisted_15 = {
42
42
  key: 0,
43
43
  class: "hidden min-h-0 w-[56%] shrink-0 flex-col border-l border-line md:flex"
44
44
  };
45
- const _hoisted_16 = { class: "flex items-center gap-2 text-ink" };
45
+ const _hoisted_16 = { class: "lp-cmd-para flex items-center gap-2 text-ink" };
46
46
  const _hoisted_17 = { class: "min-w-0 text-base font-semibold" };
47
47
  const _hoisted_18 = {
48
48
  key: 0,
49
- class: "mt-0.5 text-xs text-muted"
49
+ class: "lp-cmd-para mt-0.5 text-xs text-muted"
50
50
  };
51
51
  const _hoisted_19 = {
52
- key: 1,
53
- class: "mt-3 whitespace-pre-line text-sm leading-relaxed text-ink/90"
54
- };
55
- const _hoisted_20 = {
56
52
  key: 0,
57
53
  class: "rounded-xs bg-brand/25 text-ink"
58
54
  };
59
- const _hoisted_21 = {
60
- key: 2,
61
- class: "mt-3 text-sm text-muted"
62
- };
63
- const _hoisted_22 = { class: "flex shrink-0 items-center gap-4 border-t border-line px-4 py-2.5 text-[11px] text-muted" };
64
- const _hoisted_23 = { class: "flex items-center gap-1" };
65
- const _hoisted_24 = { class: "flex items-center gap-1" };
66
- const _hoisted_25 = {
55
+ const _hoisted_20 = { class: "flex shrink-0 items-center gap-4 border-t border-line px-4 py-2.5 text-[11px] text-muted" };
56
+ const _hoisted_21 = { class: "flex items-center gap-1" };
57
+ const _hoisted_22 = { class: "flex items-center gap-1" };
58
+ const _hoisted_23 = {
67
59
  key: 0,
68
60
  class: "flex items-center gap-1"
69
61
  };
70
- const _hoisted_26 = {
62
+ const _hoisted_24 = {
71
63
  key: 1,
72
64
  class: "flex items-center gap-1"
73
65
  };
74
- const _hoisted_27 = { class: "ml-auto flex items-center gap-1" };
66
+ const _hoisted_25 = { class: "ml-auto flex items-center gap-1" };
75
67
  const LATIN = "qwertyuiop[]asdfghjkl;'zxcvbnm,.`";
76
68
  const CYRILLIC = "йцукенгшщзхъфывапролджэячсмитьбюё";
77
69
  const TYPO_MIN = 5;
70
+ const STAGGER_MS = 45;
71
+ const STAGGER_MAX = 10;
78
72
  const _sfc_main = /* @__PURE__ */ defineComponent({
79
73
  __name: "LpCommandPalette",
80
74
  props: {
@@ -312,15 +306,22 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
312
306
  return [...items.slice(0, limit), more];
313
307
  }
314
308
  const flat = computed(() => grouped.value.flatMap((g) => g.items));
309
+ const slots = useSlots();
315
310
  const active = computed(() => {
316
311
  const c = flat.value[selectedIndex.value];
317
312
  return c && !moreOf.has(c) ? c : void 0;
318
313
  });
319
314
  const previewRef = ref(null);
315
+ const paneOpen = computed(
316
+ () => props.previewPane && !!active.value && (!!slots.preview || !!(active.value.preview || active.value.excerpt))
317
+ );
318
+ const paragraphs = computed(
319
+ () => (active.value?.preview ?? active.value?.excerpt ?? "").split(/\n{2,}/).map((part) => part.trim()).filter(Boolean)
320
+ );
320
321
  watch(
321
322
  () => active.value?.id,
322
323
  async () => {
323
- if (!props.previewPane) return;
324
+ if (!paneOpen.value) return;
324
325
  await nextTick();
325
326
  const viewport = previewRef.value?.viewportEl;
326
327
  if (!viewport) return;
@@ -484,10 +485,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
484
485
  class: normalizeClass([{ "lp-scrim--light": __props.glass }, "lp-scrim fixed inset-0 z-(--z-overlay) data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"])
485
486
  }, null, 8, ["class"]),
486
487
  createVNode(unref(DialogContent), {
487
- class: normalizeClass(["fixed left-1/2 top-[12vh] z-(--z-modal) flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]", [
488
- __props.glass ? "lp-glass-overlay" : "bg-surface-raised",
489
- __props.previewPane ? "w-[min(92vw,64rem)] md:h-[min(76vh,36rem)]" : "w-[min(92vw,40rem)]"
490
- ]]),
488
+ class: normalizeClass(["fixed left-1/2 top-[12vh] z-(--z-modal) flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]", [__props.glass ? "lp-glass-overlay" : "bg-surface-raised", __props.previewPane && "lp-cmd-resizing"]]),
489
+ style: normalizeStyle({
490
+ width: paneOpen.value ? "min(92vw, 64rem)" : "min(92vw, 40rem)",
491
+ minHeight: paneOpen.value ? "min(76vh, 36rem)" : "0px"
492
+ }),
491
493
  onOpenAutoFocus: _cache[2] || (_cache[2] = withModifiers(() => {
492
494
  }, ["prevent"]))
493
495
  }, {
@@ -687,71 +689,83 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
687
689
  ]),
688
690
  _: 1
689
691
  }, 512),
690
- __props.previewPane ? (openBlock(), createElementBlock("aside", _hoisted_15, [
691
- createVNode(_sfc_main$2, {
692
- ref_key: "previewRef",
693
- ref: previewRef,
694
- class: "min-h-0 flex-1",
695
- "content-class": "px-5 py-4"
696
- }, {
697
- default: withCtx(() => [
698
- active.value ? renderSlot(_ctx.$slots, "preview", {
699
- key: 0,
700
- command: active.value
701
- }, () => [
702
- createElementVNode("div", _hoisted_16, [
703
- active.value.icon ? (openBlock(), createBlock(_sfc_main$1, {
704
- key: 0,
705
- name: active.value.icon,
706
- size: 16,
707
- class: "shrink-0 text-muted"
708
- }, null, 8, ["name"])) : createCommentVNode("", true),
709
- createElementVNode("h3", _hoisted_17, toDisplayString(active.value.label), 1)
692
+ createVNode(Transition, { name: "lp-cmd-pane" }, {
693
+ default: withCtx(() => [
694
+ paneOpen.value ? (openBlock(), createElementBlock("aside", _hoisted_15, [
695
+ createVNode(_sfc_main$2, {
696
+ ref_key: "previewRef",
697
+ ref: previewRef,
698
+ class: "min-h-0 flex-1",
699
+ "content-class": "px-5 py-4"
700
+ }, {
701
+ default: withCtx(() => [
702
+ active.value ? (openBlock(), createElementBlock("div", {
703
+ key: active.value.id
704
+ }, [
705
+ renderSlot(_ctx.$slots, "preview", { command: active.value }, () => [
706
+ createElementVNode("div", _hoisted_16, [
707
+ active.value.icon ? (openBlock(), createBlock(_sfc_main$1, {
708
+ key: 0,
709
+ name: active.value.icon,
710
+ size: 16,
711
+ class: "shrink-0 text-muted"
712
+ }, null, 8, ["name"])) : createCommentVNode("", true),
713
+ createElementVNode("h3", _hoisted_17, toDisplayString(active.value.label), 1)
714
+ ]),
715
+ active.value.description ? (openBlock(), createElementBlock("p", _hoisted_18, toDisplayString(active.value.description), 1)) : createCommentVNode("", true),
716
+ (openBlock(true), createElementBlock(Fragment, null, renderList(paragraphs.value, (para, i) => {
717
+ return openBlock(), createElementBlock("p", {
718
+ key: i,
719
+ class: "lp-cmd-para mt-3 whitespace-pre-line text-sm leading-relaxed text-ink/90",
720
+ style: normalizeStyle({ animationDelay: `${Math.min(i + 1, STAGGER_MAX) * STAGGER_MS}ms` })
721
+ }, [
722
+ (openBlock(true), createElementBlock(Fragment, null, renderList(highlight(para, active.value.highlight), (p, pi) => {
723
+ return openBlock(), createElementBlock(Fragment, {
724
+ key: `p${pi}`
725
+ }, [
726
+ p.hit ? (openBlock(), createElementBlock("mark", _hoisted_19, toDisplayString(p.text), 1)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
727
+ createTextVNode(toDisplayString(p.text), 1)
728
+ ], 64))
729
+ ], 64);
730
+ }), 128))
731
+ ], 4);
732
+ }), 128))
733
+ ], true)
734
+ ])) : createCommentVNode("", true)
710
735
  ]),
711
- active.value.description ? (openBlock(), createElementBlock("p", _hoisted_18, toDisplayString(active.value.description), 1)) : createCommentVNode("", true),
712
- active.value.preview || active.value.excerpt ? (openBlock(), createElementBlock("p", _hoisted_19, [
713
- (openBlock(true), createElementBlock(Fragment, null, renderList(highlight(active.value.preview ?? active.value.excerpt ?? "", active.value.highlight), (p, pi) => {
714
- return openBlock(), createElementBlock(Fragment, {
715
- key: `p${pi}`
716
- }, [
717
- p.hit ? (openBlock(), createElementBlock("mark", _hoisted_20, toDisplayString(p.text), 1)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
718
- createTextVNode(toDisplayString(p.text), 1)
719
- ], 64))
720
- ], 64);
721
- }), 128))
722
- ])) : (openBlock(), createElementBlock("p", _hoisted_21, toDisplayString(text.value.noPreview), 1))
723
- ], true) : createCommentVNode("", true)
724
- ]),
725
- _: 3
726
- }, 512)
727
- ])) : createCommentVNode("", true)
736
+ _: 3
737
+ }, 512)
738
+ ])) : createCommentVNode("", true)
739
+ ]),
740
+ _: 3
741
+ })
728
742
  ]),
729
- createElementVNode("div", _hoisted_22, [
730
- createElementVNode("span", _hoisted_23, [
743
+ createElementVNode("div", _hoisted_20, [
744
+ createElementVNode("span", _hoisted_21, [
731
745
  _cache[7] || (_cache[7] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "↑", -1)),
732
746
  _cache[8] || (_cache[8] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "↓", -1)),
733
747
  createTextVNode(" " + toDisplayString(text.value.navigate), 1)
734
748
  ]),
735
- createElementVNode("span", _hoisted_24, [
749
+ createElementVNode("span", _hoisted_22, [
736
750
  _cache[9] || (_cache[9] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "↵", -1)),
737
751
  createTextVNode(" " + toDisplayString(text.value.select), 1)
738
752
  ]),
739
- selectedActions.value.length ? (openBlock(), createElementBlock("span", _hoisted_25, [
753
+ selectedActions.value.length ? (openBlock(), createElementBlock("span", _hoisted_23, [
740
754
  _cache[10] || (_cache[10] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "tab", -1)),
741
755
  createTextVNode(" " + toDisplayString(text.value.actions), 1)
742
756
  ])) : createCommentVNode("", true),
743
- trail.value.length ? (openBlock(), createElementBlock("span", _hoisted_26, [
757
+ trail.value.length ? (openBlock(), createElementBlock("span", _hoisted_24, [
744
758
  _cache[11] || (_cache[11] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "⌫", -1)),
745
759
  createTextVNode(" " + toDisplayString(text.value.back), 1)
746
760
  ])) : createCommentVNode("", true),
747
- createElementVNode("span", _hoisted_27, [
761
+ createElementVNode("span", _hoisted_25, [
748
762
  _cache[12] || (_cache[12] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "esc", -1)),
749
763
  createTextVNode(" " + toDisplayString(text.value.close), 1)
750
764
  ])
751
765
  ])
752
766
  ]),
753
767
  _: 3
754
- }, 8, ["class"])
768
+ }, 8, ["class", "style"])
755
769
  ]),
756
770
  _: 3
757
771
  })
@@ -761,7 +775,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
761
775
  };
762
776
  }
763
777
  });
764
- const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-fcb38dbd"]]);
778
+ const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-79f73da4"]]);
765
779
  export {
766
780
  LpCommandPalette as default
767
781
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -136,8 +136,11 @@ export interface PaletteLabels {
136
136
  copyLink: string
137
137
  /** Toast after "copy link"; empty for none. */
138
138
  linkCopied: string
139
- /** The preview pane when the highlighted row has nothing to show. */
140
- noPreview: string
139
+ /**
140
+ * @deprecated The pane now closes for a row with nothing to show, so this
141
+ * text is never displayed. Kept so existing `labels` objects still type-check.
142
+ */
143
+ noPreview?: string
141
144
  }
142
145
 
143
146
  const props = withDefaults(
@@ -194,14 +197,14 @@ const props = withDefaults(
194
197
  * A pane beside the list showing the highlighted result in full — its
195
198
  * `preview` text, or the `preview` slot. For palettes that search
196
199
  * content rather than commands: the excerpt says why a row matched, the
197
- * pane lets it be read without leaving the page. Opt-in and fixed while
198
- * open, so the dialog does not change width as results come and go.
200
+ * pane lets it be read without leaving the page. It opens only for a row
201
+ * with `preview` or `excerpt` (any row, with the slot): the dialog eases
202
+ * wider, the pane slides in, and the text arrives paragraph by paragraph.
199
203
  * Hidden below the md breakpoint, where there is no room beside the list.
200
204
  *
201
- * The dialog takes one width class or the other, never a base width plus
202
- * an `md:` override: a consumer's Tailwind build can emit the two in an
203
- * order where the base wins, and the pane opened squeezed into 40rem.
204
- * Both widths are 92vw on a narrow screen, so no breakpoint is needed.
205
+ * The width is an inline style rather than a pair of classes: a base
206
+ * width plus an `md:` override could be emitted by a consumer's Tailwind
207
+ * build in an order where the base won, and the pane opened squeezed.
205
208
  */
206
209
  previewPane?: boolean
207
210
  /**
@@ -605,7 +608,7 @@ function folded(name: string, items: Command[]): Command[] {
605
608
  // Flat list in render order — what arrow keys walk through.
606
609
  const flat = computed(() => grouped.value.flatMap((g) => g.items))
607
610
 
608
- defineSlots<{
611
+ const slots = defineSlots<{
609
612
  /** Body of the preview pane for the highlighted command. */
610
613
  preview?: (props: { command: Command }) => unknown
611
614
  }>()
@@ -616,6 +619,30 @@ const active = computed<Command | undefined>(() => {
616
619
  })
617
620
  const previewRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
618
621
 
622
+ /*
623
+ * The pane opens only for a row that has something to show: an empty pane
624
+ * beside a list of commands is a column of dead space. With the slot the app
625
+ * decides what to show, so any highlighted row opens it.
626
+ */
627
+ const paneOpen = computed(
628
+ () => props.previewPane && !!active.value && (!!slots.preview || !!(active.value.preview || active.value.excerpt)),
629
+ )
630
+
631
+ /*
632
+ * The text in paragraphs, so they can arrive one after another. A blank line
633
+ * separates them, as in the `preview` the consumer wrote.
634
+ */
635
+ const paragraphs = computed(() =>
636
+ (active.value?.preview ?? active.value?.excerpt ?? "")
637
+ .split(/\n{2,}/)
638
+ .map((part) => part.trim())
639
+ .filter(Boolean),
640
+ )
641
+
642
+ /** Stagger between paragraphs, capped so a long section does not trickle in. */
643
+ const STAGGER_MS = 45
644
+ const STAGGER_MAX = 10
645
+
619
646
  /*
620
647
  * A preview is opened for the match, so the pane lands on it: the first
621
648
  * highlighted word is brought into view, and a result without one starts
@@ -624,7 +651,7 @@ const previewRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
624
651
  watch(
625
652
  () => active.value?.id,
626
653
  async () => {
627
- if (!props.previewPane) return
654
+ if (!paneOpen.value) return
628
655
  await nextTick()
629
656
  const viewport = previewRef.value?.viewportEl
630
657
  if (!viewport) return
@@ -842,10 +869,11 @@ useHotkeys(() =>
842
869
  />
843
870
  <DialogContent
844
871
  class="fixed left-1/2 top-[12vh] z-(--z-modal) flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]"
845
- :class="[
846
- glass ? 'lp-glass-overlay' : 'bg-surface-raised',
847
- previewPane ? 'w-[min(92vw,64rem)] md:h-[min(76vh,36rem)]' : 'w-[min(92vw,40rem)]',
848
- ]"
872
+ :class="[glass ? 'lp-glass-overlay' : 'bg-surface-raised', previewPane && 'lp-cmd-resizing']"
873
+ :style="{
874
+ width: paneOpen ? 'min(92vw, 64rem)' : 'min(92vw, 40rem)',
875
+ minHeight: paneOpen ? 'min(76vh, 36rem)' : '0px',
876
+ }"
849
877
  @open-auto-focus.prevent
850
878
  >
851
879
  <DialogTitle class="sr-only">Command palette</DialogTitle>
@@ -1007,35 +1035,41 @@ useHotkeys(() =>
1007
1035
  </button>
1008
1036
  </div>
1009
1037
  </LpScrollArea>
1010
- <aside
1011
- v-if="previewPane"
1012
- class="hidden min-h-0 w-[56%] shrink-0 flex-col border-l border-line md:flex"
1013
- >
1014
- <LpScrollArea ref="previewRef" class="min-h-0 flex-1" content-class="px-5 py-4">
1015
- <template v-if="active">
1016
- <slot name="preview" :command="active">
1017
- <div class="flex items-center gap-2 text-ink">
1018
- <LpIcon v-if="active.icon" :name="active.icon" :size="16" class="shrink-0 text-muted" />
1019
- <h3 class="min-w-0 text-base font-semibold">{{ active.label }}</h3>
1020
- </div>
1021
- <p v-if="active.description" class="mt-0.5 text-xs text-muted">{{ active.description }}</p>
1022
- <p
1023
- v-if="active.preview || active.excerpt"
1024
- class="mt-3 whitespace-pre-line text-sm leading-relaxed text-ink/90"
1025
- >
1026
- <template
1027
- v-for="(p, pi) in highlight(active.preview ?? active.excerpt ?? '', active.highlight)"
1028
- :key="`p${pi}`"
1038
+ <!-- Slides in beside the list while the dialog widens to make room,
1039
+ and out again for a row with nothing to show. -->
1040
+ <Transition name="lp-cmd-pane">
1041
+ <aside
1042
+ v-if="paneOpen"
1043
+ class="hidden min-h-0 w-[56%] shrink-0 flex-col border-l border-line md:flex"
1044
+ >
1045
+ <LpScrollArea ref="previewRef" class="min-h-0 flex-1" content-class="px-5 py-4">
1046
+ <!-- Keyed by the row, so moving the highlight replays the
1047
+ entrance rather than swapping text in place. -->
1048
+ <div v-if="active" :key="active.id">
1049
+ <slot name="preview" :command="active">
1050
+ <div class="lp-cmd-para flex items-center gap-2 text-ink">
1051
+ <LpIcon v-if="active.icon" :name="active.icon" :size="16" class="shrink-0 text-muted" />
1052
+ <h3 class="min-w-0 text-base font-semibold">{{ active.label }}</h3>
1053
+ </div>
1054
+ <p v-if="active.description" class="lp-cmd-para mt-0.5 text-xs text-muted">
1055
+ {{ active.description }}
1056
+ </p>
1057
+ <p
1058
+ v-for="(para, i) in paragraphs"
1059
+ :key="i"
1060
+ class="lp-cmd-para mt-3 whitespace-pre-line text-sm leading-relaxed text-ink/90"
1061
+ :style="{ animationDelay: `${Math.min(i + 1, STAGGER_MAX) * STAGGER_MS}ms` }"
1029
1062
  >
1030
- <mark v-if="p.hit" class="rounded-xs bg-brand/25 text-ink">{{ p.text }}</mark>
1031
- <template v-else>{{ p.text }}</template>
1032
- </template>
1033
- </p>
1034
- <p v-else class="mt-3 text-sm text-muted">{{ text.noPreview }}</p>
1035
- </slot>
1036
- </template>
1037
- </LpScrollArea>
1038
- </aside>
1063
+ <template v-for="(p, pi) in highlight(para, active.highlight)" :key="`p${pi}`">
1064
+ <mark v-if="p.hit" class="rounded-xs bg-brand/25 text-ink">{{ p.text }}</mark>
1065
+ <template v-else>{{ p.text }}</template>
1066
+ </template>
1067
+ </p>
1068
+ </slot>
1069
+ </div>
1070
+ </LpScrollArea>
1071
+ </aside>
1072
+ </Transition>
1039
1073
  </div>
1040
1074
 
1041
1075
  <!-- Footer hints -->
@@ -1072,6 +1106,49 @@ useHotkeys(() =>
1072
1106
  </template>
1073
1107
 
1074
1108
  <style scoped>
1109
+ /*
1110
+ * The dialog changes width and height when the preview pane comes and goes;
1111
+ * eased, so the list does not jump sideways under the pointer.
1112
+ */
1113
+ .lp-cmd-resizing {
1114
+ transition:
1115
+ width 260ms var(--ease-emphasized, ease),
1116
+ min-height 260ms var(--ease-emphasized, ease);
1117
+ }
1118
+ .lp-cmd-pane-enter-active {
1119
+ transition:
1120
+ opacity 220ms ease 60ms,
1121
+ transform 260ms var(--ease-emphasized, ease);
1122
+ }
1123
+ .lp-cmd-pane-leave-active {
1124
+ transition: opacity 120ms ease;
1125
+ }
1126
+ .lp-cmd-pane-enter-from {
1127
+ opacity: 0;
1128
+ transform: translateX(16px);
1129
+ }
1130
+ .lp-cmd-pane-leave-to {
1131
+ opacity: 0;
1132
+ }
1133
+ .lp-cmd-para {
1134
+ animation: lp-cmd-para-in 260ms ease-out both;
1135
+ }
1136
+ @keyframes lp-cmd-para-in {
1137
+ from {
1138
+ opacity: 0;
1139
+ transform: translateY(4px);
1140
+ }
1141
+ }
1142
+ @media (prefers-reduced-motion: reduce) {
1143
+ .lp-cmd-resizing,
1144
+ .lp-cmd-pane-enter-active,
1145
+ .lp-cmd-pane-leave-active {
1146
+ transition: none;
1147
+ }
1148
+ .lp-cmd-para {
1149
+ animation: none;
1150
+ }
1151
+ }
1075
1152
  .lp-cmd-excerpt {
1076
1153
  display: -webkit-box;
1077
1154
  -webkit-box-orient: vertical;
@@ -1,21 +0,0 @@
1
-
2
- .lp-cmd-excerpt[data-v-fcb38dbd] {
3
- display: -webkit-box;
4
- -webkit-box-orient: vertical;
5
- overflow: hidden;
6
- }
7
- /* Each result eases in; honours reduced motion. */
8
- .lp-cmd-item[data-v-fcb38dbd] {
9
- animation: lp-cmd-in-fcb38dbd 140ms ease-out both;
10
- }
11
- @keyframes lp-cmd-in-fcb38dbd {
12
- from {
13
- opacity: 0;
14
- transform: translateY(3px);
15
- }
16
- }
17
- @media (prefers-reduced-motion: reduce) {
18
- .lp-cmd-item[data-v-fcb38dbd] {
19
- animation: none;
20
- }
21
- }