4141 </button >
4242 </div >
4343 <div v-if =" !sidebarCollapsed" class =" sidebar-content" >
44+ <div class =" shape-search" >
45+ <input
46+ type =" text"
47+ v-model =" shapeSearchQuery"
48+ placeholder =" Filter shapes..."
49+ class =" shape-search-input"
50+ />
51+ <span
52+ v-if =" shapeSearchQuery"
53+ class =" shape-search-clear"
54+ @click =" shapeSearchQuery = ''"
55+ >× ; </span >
56+ </div >
57+
58+ <!-- Recent edited shapes -->
59+ <div v-if =" recentEditedShapes.length > 0 && !shapeSearchQuery" class =" recent-shapes" >
60+ <div class =" recent-shapes-header" >Recent</div >
61+ <div
62+ v-for =" shapeId in recentEditedShapes"
63+ :key =" 'recent-' + shapeId"
64+ class =" recent-shape-item"
65+ :class =" { active: currentShapeId === shapeId }"
66+ @click =" selectShape(shapeId)"
67+ >
68+ {{ shapeId }}
69+ </div >
70+ </div >
71+
4472 <div class =" sidebar-buttons" >
4573 <button
4674 class =" btn btn-primary btn-grow"
167195 @shape-label-change =" updateShapeLabel"
168196 @description-change =" updateDescription"
169197 @resource-uri-change =" updateResourceURI"
198+ @navigate-to-shape =" navigateToShape"
170199 />
171200 </main >
172201 </div >
@@ -427,18 +456,47 @@ export default defineComponent({
427456 const startingPointFileInput = ref <HTMLInputElement | null >(null );
428457 const importingStartingPoints = ref (false );
429458
459+ // Shape search
460+ const shapeSearchQuery = ref (' ' );
461+
462+ // Recent edited shapes (session only, max 5)
463+ const recentEditedShapeIds = ref <string []>([]);
464+ const recentEditedShapes = computed (() => {
465+ // Only show shapes that still exist
466+ return recentEditedShapeIds .value .filter (id => shapes .value .some (s => s .shapeId === id ));
467+ });
468+
469+ function trackRecentShape(shapeId : string ) {
470+ const list = recentEditedShapeIds .value ;
471+ const idx = list .indexOf (shapeId );
472+ if (idx !== - 1 ) {
473+ list .splice (idx , 1 );
474+ }
475+ list .unshift (shapeId );
476+ if (list .length > 5 ) {
477+ list .pop ();
478+ }
479+ }
480+
481+ function shapeMatchesSearch(shape : Shape ): boolean {
482+ if (! shapeSearchQuery .value ) return true ;
483+ const query = shapeSearchQuery .value .toLowerCase ();
484+ return shape .shapeId .toLowerCase ().includes (query ) ||
485+ (shape .shapeLabel || ' ' ).toLowerCase ().includes (query );
486+ }
487+
430488 // Shapes at root level (no folder)
431489 const rootShapes = computed (() => {
432490 return [... shapes .value ]
433- .filter (s => s .folderId === null )
491+ .filter (s => s .folderId === null && shapeMatchesSearch ( s ) )
434492 .sort ((a , b ) => a .shapeId .localeCompare (b .shapeId ));
435493 });
436494
437495 // Shapes grouped by folder
438496 const shapesByFolder = computed (() => {
439497 const map = new Map <number , Shape []>();
440498 for (const shape of shapes .value ) {
441- if (shape .folderId !== null ) {
499+ if (shape .folderId !== null && shapeMatchesSearch ( shape ) ) {
442500 if (! map .has (shape .folderId )) {
443501 map .set (shape .folderId , []);
444502 }
@@ -452,9 +510,11 @@ export default defineComponent({
452510 return map ;
453511 });
454512
455- // Sorted folders
513+ // Sorted folders - hide empty folders when searching
456514 const sortedFolders = computed (() => {
457- return [... folders .value ].sort ((a , b ) => a .name .localeCompare (b .name ));
515+ const sorted = [... folders .value ].sort ((a , b ) => a .name .localeCompare (b .name ));
516+ if (! shapeSearchQuery .value ) return sorted ;
517+ return sorted .filter (f => (shapesByFolder .value .get (f .id )?.length || 0 ) > 0 );
458518 });
459519
460520 const sortedShapes = computed (() => {
@@ -534,9 +594,17 @@ export default defineComponent({
534594
535595 function selectShape(shapeId : string ) {
536596 currentShapeId .value = shapeId ;
597+ trackRecentShape (shapeId );
537598 router .replace ({ name: ' shape' , params: { id: props .id , shapeId } });
538599 }
539600
601+ function navigateToShape(shapeId : string ) {
602+ const shape = shapes .value .find (s => s .shapeId === shapeId );
603+ if (shape ) {
604+ selectShape (shapeId );
605+ }
606+ }
607+
540608 async function createShape() {
541609 if (! newShapeId .value .trim ()) return ;
542610 try {
@@ -700,6 +768,10 @@ export default defineComponent({
700768 }
701769
702770 function isFolderCollapsed(folderId : number ): boolean {
771+ // Auto-expand folders with matches when searching
772+ if (shapeSearchQuery .value && (shapesByFolder .value .get (folderId )?.length || 0 ) > 0 ) {
773+ return false ;
774+ }
703775 return ! expandedFolders .value .has (folderId );
704776 }
705777
@@ -823,6 +895,7 @@ export default defineComponent({
823895 showNamespaceManager ,
824896 goHome ,
825897 selectShape ,
898+ navigateToShape ,
826899 createShape ,
827900 editShape ,
828901 doEditShape ,
@@ -847,6 +920,10 @@ export default defineComponent({
847920 isFolderCollapsed ,
848921 createFolder ,
849922 deleteFolder ,
923+ // Shape search
924+ shapeSearchQuery ,
925+ // Recent edited shapes
926+ recentEditedShapes ,
850927 // Drag and drop
851928 draggedShapeId ,
852929 dragOverFolderId ,
@@ -1171,6 +1248,77 @@ export default defineComponent({
11711248 flex : 1 ;
11721249}
11731250
1251+ /* Shape search */
1252+ .shape-search {
1253+ position : relative ;
1254+ margin-bottom : 0.75rem ;
1255+ }
1256+
1257+ .shape-search-input {
1258+ width : 100% ;
1259+ padding : 0.4rem 1.75rem 0.4rem 0.5rem ;
1260+ border : 1px solid #ddd ;
1261+ border-radius : 4px ;
1262+ font-size : 0.85rem ;
1263+ box-sizing : border-box ;
1264+ }
1265+
1266+ .shape-search-input :focus {
1267+ outline : none ;
1268+ border-color : #3498db ;
1269+ }
1270+
1271+ .shape-search-clear {
1272+ position : absolute ;
1273+ right : 6px ;
1274+ top : 50% ;
1275+ transform : translateY (-50% );
1276+ cursor : pointer ;
1277+ color : #999 ;
1278+ font-size : 1.1rem ;
1279+ line-height : 1 ;
1280+ }
1281+
1282+ .shape-search-clear :hover {
1283+ color : #333 ;
1284+ }
1285+
1286+ /* Recent edited shapes */
1287+ .recent-shapes {
1288+ margin-bottom : 0.75rem ;
1289+ padding-bottom : 0.5rem ;
1290+ border-bottom : 1px solid #e0e0e0 ;
1291+ }
1292+
1293+ .recent-shapes-header {
1294+ font-size : 0.75rem ;
1295+ font-weight : 600 ;
1296+ color : #888 ;
1297+ text-transform : uppercase ;
1298+ letter-spacing : 0.5px ;
1299+ margin-bottom : 0.25rem ;
1300+ }
1301+
1302+ .recent-shape-item {
1303+ padding : 0.3rem 0.5rem ;
1304+ border-radius : 4px ;
1305+ cursor : pointer ;
1306+ font-size : 0.85rem ;
1307+ color : #2c3e50 ;
1308+ white-space : nowrap ;
1309+ overflow : hidden ;
1310+ text-overflow : ellipsis ;
1311+ }
1312+
1313+ .recent-shape-item :hover {
1314+ background : #f0f0f0 ;
1315+ }
1316+
1317+ .recent-shape-item.active {
1318+ background : #e3f2fd ;
1319+ font-weight : 500 ;
1320+ }
1321+
11741322/* Root drop zone */
11751323.root-drop-zone {
11761324 min-height : 20px ;
0 commit comments