11<script setup lang="ts">
22import type { DevframeClientCommand , DevframeCommandEntry } from ' @devframes/hub'
33import type { DocksContext } from ' @devframes/hub/client'
4+ import type { PaletteCrumb , PaletteFlatItem } from ' ../../state/palette'
45import Fuse from ' fuse.js'
56import { computed , nextTick , ref , useTemplateRef , watch } from ' vue'
7+ import { flattenPaletteCommands , paletteScopeTrail } from ' ../../state/palette'
68import BrandWordmark from ' ../icons/BrandWordmark.vue'
79import CommandPaletteItem from ' ./CommandPaletteItem.vue'
810
@@ -23,36 +25,14 @@ const listContainer = useTemplateRef<HTMLElement>('listContainer')
2325const visible = ref (false )
2426
2527// Breadcrumb stack for sub-command drill-down
26- const breadcrumb = ref <Array <{ title : string , items : DevframeCommandEntry [] }> >([])
28+ const breadcrumb = ref <PaletteCrumb [] >([])
2729
28- // Flattened items for top-level search (includes children with parent prefix)
29- interface FlatItem {
30- entry: DevframeCommandEntry
31- parentTitle? : string
32- searchTitle: string
33- }
34-
35- const flattenedItems = computed <FlatItem []>(() => {
36- const result: FlatItem [] = []
37- for (const cmd of commandsCtx .value .paletteCommands ) {
38- result .push ({ entry: cmd , searchTitle: cmd .title })
39- if (cmd .children && cmd .showInPalette !== ' without-children' ) {
40- for (const child of cmd .children ) {
41- if (child .showInPalette === false )
42- continue
43- result .push ({
44- entry: child as DevframeCommandEntry ,
45- parentTitle: cmd .title ,
46- searchTitle: ` ${cmd .title } > ${child .title } ` ,
47- })
48- }
49- }
50- }
51- return result
52- })
30+ const flattenedItems = computed <PaletteFlatItem []>(
31+ () => flattenPaletteCommands (commandsCtx .value .paletteCommands ),
32+ )
5333
5434// Current items: either drilled-down sub-items or root items
55- const currentFlatItems = computed <FlatItem []>(() => {
35+ const currentFlatItems = computed <PaletteFlatItem []>(() => {
5636 if (breadcrumb .value .length > 0 ) {
5737 const current = breadcrumb .value .at (- 1 )!
5838 return current .items .map (entry => ({ entry , searchTitle: entry .title }))
@@ -62,7 +42,7 @@ const currentFlatItems = computed<FlatItem[]>(() => {
6242
6343// Dynamic sub-items from action() return
6444const dynamicItems = ref <DevframeClientCommand [] | undefined >()
65- const activeItems = computed <FlatItem []>(() => {
45+ const activeItems = computed <PaletteFlatItem []>(() => {
6646 if (dynamicItems .value ) {
6747 return dynamicItems .value .map (entry => ({ entry , searchTitle: entry .title }))
6848 }
@@ -85,12 +65,17 @@ watch(search, () => {
8565 selectedIndex .value = 0
8666})
8767
68+ /** Show the rows at `scopeId`'s level, from a fresh search. */
69+ function showScope(scopeId : string | null ) {
70+ search .value = ' '
71+ selectedIndex .value = 0
72+ dynamicItems .value = undefined
73+ breadcrumb .value = paletteScopeTrail (commandsCtx .value .paletteCommands , scopeId )
74+ }
75+
8876watch (show , (v ) => {
8977 if (v ) {
90- search .value = ' '
91- selectedIndex .value = 0
92- breadcrumb .value = []
93- dynamicItems .value = undefined
78+ showScope (commandsCtx .value .paletteScopeId )
9479 // Trigger enter animation
9580 requestAnimationFrame (() => {
9681 visible .value = true
@@ -99,9 +84,22 @@ watch(show, (v) => {
9984 }
10085 else {
10186 visible .value = false
87+ // Every close path funnels through `show` — Escape, the backdrop, running a
88+ // command, and a bare `paletteOpen` toggle — so the scope is dropped here
89+ // once rather than in each of them. A later Mod+K then opens at the root
90+ // instead of resurrecting the group it was last scoped to.
91+ commandsCtx .value .paletteScopeId = null
10292 }
10393})
10494
95+ // A scope also arrives while the palette is already open — activating a dock
96+ // group picked from the root list, say. `show` stays `true` throughout, so the
97+ // drill-down follows the scope itself rather than the open transition.
98+ watch (() => commandsCtx .value .paletteScopeId , (scopeId ) => {
99+ if (show .value && scopeId )
100+ showScope (scopeId )
101+ })
102+
105103function moveSelected(delta : number ) {
106104 const len = filtered .value .length
107105 if (len === 0 )
@@ -121,7 +119,7 @@ function scrollToItem() {
121119
122120const loadingId = ref <string | null >(null )
123121
124- async function enterItem(flatItem : FlatItem ) {
122+ async function enterItem(flatItem : PaletteFlatItem ) {
125123 const entry = flatItem .entry
126124
127125 // If has static children, drill down
@@ -195,13 +193,32 @@ function goBack() {
195193 }
196194 if (breadcrumb .value .length > 0 ) {
197195 breadcrumb .value .pop ()
196+ dropScopeAtRoot ()
198197 search .value = ' '
199198 selectedIndex .value = 0
200199 return
201200 }
202201 close ()
203202}
204203
204+ /** Jump to the level the crumb at `index` sits above. */
205+ function goToCrumb(index : number ) {
206+ breadcrumb .value .splice (index )
207+ dropScopeAtRoot ()
208+ search .value = ' '
209+ selectedIndex .value = 0
210+ }
211+
212+ /**
213+ * Stepping back out to the root list leaves the palette unscoped, so the
214+ * shortcut that scoped it drills back in instead of reading as "press again to
215+ * close".
216+ */
217+ function dropScopeAtRoot() {
218+ if (breadcrumb .value .length === 0 )
219+ commandsCtx .value .paletteScopeId = null
220+ }
221+
205222function onKeyDown(e : KeyboardEvent ) {
206223 if (e .key === ' Backspace' && ! search .value && (breadcrumb .value .length > 0 || dynamicItems .value )) {
207224 e .preventDefault ()
@@ -275,7 +292,7 @@ function getKeybindings(id: string) {
275292 v-for =" (crumb, i) in breadcrumb"
276293 :key =" i"
277294 class =" text-xs op60 hover:op80 mr-1 flex items-center gap-0.5"
278- @click =" breadcrumb.splice (i); search = ''; selectedIndex = 0 "
295+ @click =" goToCrumb (i)"
279296 >
280297 {{ crumb.title }}
281298 <span class =" op40" >&rsaquo ; </span >
0 commit comments