Repository: sillsdev/languageforge-lexbox
Pull request #1693: Shadcn low-hanging fruit follow-up
Author: login: myieye
Created: 2025-05-21T14:56:59Z | Merged: 2025-05-23T06:47:09Z

The most visual changes:

"I don't see my project" accidently got inside the bordered list if projects. I moved it out:
![image](https://github.com/user-attachments/assets/27caf727-6042-442b-844d-74c0cf479230)

![image](https://github.com/user-attachments/assets/a6d9a8b6-1aa3-4197-9904-3edf81380759)

Scroll to top and focus first field:
https://github.com/user-attachments/assets/34c5520a-66b0-4f92-8ce2-3d4e5d754a8e

Tabbing:
https://github.com/user-attachments/assets/dab3ce70-0bd7-4d67-8786-cdb4fb1a0d6e

FieldWorks and Lexbox icons now share the Icon component (i.e. sizing), so alignment should always just work:
![image](https://github.com/user-attachments/assets/2a7da479-901c-40ab-ac33-a0915acf5af4)

Autofocusing first field of new senses and examples:

https://github.com/user-attachments/assets/8a0506f4-ca37-4315-bdbc-bbab88a50b83




Diff:
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index ee787e95ba..b0c91c4bd8 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -381,6 +381,9 @@ importers:
       svelte-ux:
         specifier: ^0.76.0
         version: 0.76.0(@babel/core@7.26.8)(postcss-load-config@4.0.2(postcss@8.5.2))(postcss@8.5.2)(svelte@5.28.2)
+      tabbable:
+        specifier: ^6.2.0
+        version: 6.2.0
       type-fest:
         specifier: ^4.18.2
         version: 4.34.1
@@ -8987,7 +8990,7 @@ snapshots:
       sirv: 3.0.0
       tinyglobby: 0.2.10
       tinyrainbow: 2.0.0
-      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@20.17.32)(@vitest/ui@3.0.5)(happy-dom@17.1.0)(jiti@2.4.2)(yaml@2.7.1)
+      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.15.3)(@vitest/ui@3.0.5)(happy-dom@17.1.0)(jiti@2.4.2)(yaml@2.7.1)
 
   '@vitest/utils@3.0.5':
     dependencies:
diff --git a/frontend/viewer/package.json b/frontend/viewer/package.json
index 2ec9d6d7b9..bf4681d155 100644
--- a/frontend/viewer/package.json
+++ b/frontend/viewer/package.json
@@ -97,6 +97,7 @@
     "svelte-preprocess": "catalog:",
     "svelte-routing": "^2.12.0",
     "svelte-ux": "^0.76.0",
+    "tabbable": "^6.2.0",
     "type-fest": "^4.18.2"
   }
 }
diff --git a/frontend/viewer/src/home/HomeView.svelte b/frontend/viewer/src/home/HomeView.svelte
index 96e80fc889..a38285c43e 100644
--- a/frontend/viewer/src/home/HomeView.svelte
+++ b/frontend/viewer/src/home/HomeView.svelte
@@ -29,6 +29,7 @@
   import {Button} from '$lib/components/ui/button';
   import {mode} from 'mode-watcher';
   import * as ResponsiveMenu from '$lib/components/responsive-menu';
+  import {Icon} from '$lib/components/ui/icon';
 
   const projectsService = useProjectsService();
   const importFwdataService = useImportFwdataService();
@@ -104,7 +105,7 @@
 
 <AppBar title={$t`Dictionaries`} class="bg-primary/15 min-h-12 shadow-md justify-between" menuIcon={null}>
   <div slot="title" class="text-lg flex gap-2 items-center">
-    <img src={mode.current === 'dark' ? logoLight : logoDark} alt={$t`Lexbox logo`} class="h-6 shrink-0" />
+    <Icon src={mode.current === 'dark' ? logoLight : logoDark} alt={$t`Lexbox logo`} />
     <h3>{$t`Dictionaries`}</h3>
   </div>
   <div slot="actions" class="flex">
@@ -222,7 +223,7 @@
                 <ButtonListItem href={`/fwdata/${project.code}`}>
                   <ListItem classes={{root: 'dark:bg-muted/50 bg-muted/80 hover:bg-muted/30 hover:dark:bg-muted' }}>
                     <ProjectTitle slot="title" {project}/>
-                    <img slot="avatar" src={flexLogo} alt={$t`FieldWorks logo`} class="h-6 shrink-0" />
+                    <Icon slot="avatar" src={flexLogo} alt={$t`FieldWorks logo`} />
                     <div slot="actions" class="shrink-0">
                       <DevContent invisible>
                         <UxButton
diff --git a/frontend/viewer/src/home/Server.svelte b/frontend/viewer/src/home/Server.svelte
index 0f34070a99..9ef9ff8c95 100644
--- a/frontend/viewer/src/home/Server.svelte
+++ b/frontend/viewer/src/home/Server.svelte
@@ -123,12 +123,12 @@
             </ButtonListItem>
           {/if}
         {/each}
-        <div class="text-center py-2">
-          <Button variant="link" target="_blank" href="{server?.authority}/wheresMyProject">
-            {$t`I don't see my project`}
-            <Icon icon="i-mdi-open-in-new" class="size-4" />
-          </Button>
-        </div>
+      </div>
+      <div class="text-center pt-2">
+        <Button variant="link" target="_blank" href="{server?.authority}/wheresMyProject">
+          {$t`I don't see my project`}
+          <Icon icon="i-mdi-open-in-new" class="size-4" />
+        </Button>
       </div>
     {/if}
   </div>
diff --git a/frontend/viewer/src/lib/OpenInFieldWorksButton.svelte b/frontend/viewer/src/lib/OpenInFieldWorksButton.svelte
index a8c9081363..cd98558986 100644
--- a/frontend/viewer/src/lib/OpenInFieldWorksButton.svelte
+++ b/frontend/viewer/src/lib/OpenInFieldWorksButton.svelte
@@ -8,6 +8,7 @@
   import {t} from 'svelte-i18n-lingui';
   import {mergeProps} from 'bits-ui';
   import {cn} from './utils';
+  import {Icon} from './components/ui/icon';
 
   type Props = {
     entry: IEntry
@@ -52,6 +53,6 @@
 
 <!--button must be a link otherwise it won't follow the redirect to a protocol handler-->
 <button class={cn(buttonVariants({ variant: 'ghost'}), className)} {...mergedProps}>
-  <img src={flexLogo} alt={$t`FieldWorks logo`} class="h-6 max-w-fit"/>
+  <Icon src={flexLogo} alt={$t`FieldWorks logo`} />
   {$t`Open in FieldWorks`}
 </button>
diff --git a/frontend/viewer/src/lib/components/editor/editor-root.svelte b/frontend/viewer/src/lib/components/editor/editor-root.svelte
index fb1e6b464c..94d14ec138 100644
--- a/frontend/viewer/src/lib/components/editor/editor-root.svelte
+++ b/frontend/viewer/src/lib/components/editor/editor-root.svelte
@@ -6,7 +6,7 @@
 
   type EditorRootProps = WithElementRef<HTMLAttributes<HTMLDivElement>>;
 
-  const {
+  let {
     class: className,
     children,
     ref = $bindable(null),
@@ -14,6 +14,6 @@
   }: EditorRootProps = $props();
 </script>
 
-<div class={cn('@container/editor', className)} {...restProps}>
+<div class={cn('@container/editor', className)} {...restProps} bind:this={ref}>
   {@render children?.()}
 </div>
diff --git a/frontend/viewer/src/lib/components/editor/editor-sub-grid.svelte b/frontend/viewer/src/lib/components/editor/editor-sub-grid.svelte
index 648921f866..96146b0bcc 100644
--- a/frontend/viewer/src/lib/components/editor/editor-sub-grid.svelte
+++ b/frontend/viewer/src/lib/components/editor/editor-sub-grid.svelte
@@ -3,7 +3,7 @@
   import type {WithElementRef} from 'bits-ui';
   import type {HTMLAttributes} from 'svelte/elements';
 
-  type EditorSubGridProps = WithElementRef<HTMLAttributes<HTMLDivElement>>;
+  export type EditorSubGridProps = WithElementRef<HTMLAttributes<HTMLDivElement>>;
 
   const {
     class: className,
diff --git a/frontend/viewer/src/lib/components/reorderer/reorderer-item-list.svelte b/frontend/viewer/src/lib/components/reorderer/reorderer-item-list.svelte
index ea90d581fc..27459283e5 100644
--- a/frontend/viewer/src/lib/components/reorderer/reorderer-item-list.svelte
+++ b/frontend/viewer/src/lib/components/reorderer/reorderer-item-list.svelte
@@ -43,7 +43,7 @@
 <DropdownMenu.Group {...mergedProps}>
   {#each displayItems as item, i}
     {@const reorderName = getDisplayName(item) || '–'}
-    <DropdownMenu.Item class="grid grid-cols-subgrid col-span-full justify-items-start items-center cursor-pointer"
+    <DropdownMenu.Item class="grid grid-cols-subgrid col-span-full justify-items-start items-center"
       onmouseover={() => displayIndex = i}
       onfocus={() => displayIndex = i}
       onSelect={() => {
diff --git a/frontend/viewer/src/lib/components/responsive-menu/responsive-menu-item.svelte b/frontend/viewer/src/lib/components/responsive-menu/responsive-menu-item.svelte
index 8eae0e2403..261db33d52 100644
--- a/frontend/viewer/src/lib/components/responsive-menu/responsive-menu-item.svelte
+++ b/frontend/viewer/src/lib/components/responsive-menu/responsive-menu-item.svelte
@@ -4,19 +4,20 @@
   import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
   import * as ContextMenu from '$lib/components/ui/context-menu';
   import { IsMobile } from '$lib/hooks/is-mobile.svelte';
-  import Button, {buttonVariants} from '$lib/components/ui/button/button.svelte';
+  import {buttonVariants} from '$lib/components/ui/button/button.svelte';
   import type {Snippet} from 'svelte';
   import {useResponsiveMenuItemList} from './responsive-menu.svelte';
-  import type {ButtonProps} from 'node_modules/bits-ui/dist/bits/toolbar/exports';
-  import {mergeProps, type ContextMenuItemProps} from 'bits-ui';
+  import type {ContextMenuItemProps} from 'bits-ui';
+  import type {DrawerCloseProps} from 'vaul-svelte';
   import {cn} from '$lib/utils';
+  import {DrawerClose} from '../ui/drawer';
 
   type Props = {
     children?: Snippet;
     icon?: IconClass;
     onSelect?: () => void;
     href?: string;
-  } & ContextMenuItemProps & ButtonProps;
+  } & Omit<ContextMenuItemProps & DrawerCloseProps, 'onclick'>;
 
   let {
     icon,
@@ -28,14 +29,6 @@
   }: Props = $props();
 
   const state = useResponsiveMenuItemList();
-
-  const buttonProps = $derived({
-    variant: 'ghost',
-    class: cn(buttonVariants({ variant: 'ghost', class: 'w-full justify-start gap-2' }), className),
-    onclick: onSelect,
-  } as const);
-
-  const mergedProps = $derived(mergeProps(buttonProps, rest));
 </script>
 
 {#snippet content()}
@@ -52,21 +45,21 @@
 {/snippet}
 
 {#if state.contextMenu}
-  <ContextMenu.Item class={cn('cursor-pointer gap-2 w-full', className)} onclick={onSelect} bind:ref
+  <ContextMenu.Item class={cn('gap-2 w-full', className)} {onSelect} bind:ref
     child={rest.href ? anchorChild : undefined} {...rest}>
     {@render content()}
   </ContextMenu.Item>
 {:else if !IsMobile.value}
-  <DropdownMenu.Item class={cn('cursor-pointer gap-2 w-full', className)} {onSelect} bind:ref
+  <DropdownMenu.Item class={cn('gap-2 w-full', className)} {onSelect} bind:ref
     child={rest.href ? anchorChild : undefined} {...rest}>
     {@render content()}
   </DropdownMenu.Item>
-{:else if rest.child}
-  {@render rest.child({ props: mergedProps})}
 {:else}
-  <Button
-    {...buttonProps}
+  <DrawerClose
+    class={cn(buttonVariants({ variant: 'ghost', class: 'w-full justify-start gap-2' }), className)}
+    onclick={onSelect}
+    {...rest}
     bind:ref>
     {@render content()}
-  </Button>
+  </DrawerClose>
 {/if}
diff --git a/frontend/viewer/src/lib/components/ui/button/index.ts b/frontend/viewer/src/lib/components/ui/button/index.ts
index 28523d63c0..9146495347 100644
--- a/frontend/viewer/src/lib/components/ui/button/index.ts
+++ b/frontend/viewer/src/lib/components/ui/button/index.ts
@@ -8,10 +8,13 @@ import Root, {
 import XButton from './x-button.svelte';
 
 export {
-  //
-  Root as Button,
+  Root,
   XButton,
-  buttonVariants, Root, type ButtonProps,
+  type ButtonProps,
   type ButtonSize,
-  type ButtonVariant, type ButtonProps as Props
+  type ButtonVariant,
+  buttonVariants,
+  //
+  Root as Button,
+  type ButtonProps as Props
 };
diff --git a/frontend/viewer/src/lib/components/ui/context-menu/context-menu-item.svelte b/frontend/viewer/src/lib/components/ui/context-menu/context-menu-item.svelte
index 046e681585..6ec587f5ca 100644
--- a/frontend/viewer/src/lib/components/ui/context-menu/context-menu-item.svelte
+++ b/frontend/viewer/src/lib/components/ui/context-menu/context-menu-item.svelte
@@ -15,7 +15,7 @@
 <ContextMenuPrimitive.Item
 	bind:ref
 	class={cn(
-		'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
+		'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
 		inset && 'pl-8',
 		className
 	)}
diff --git a/frontend/viewer/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/viewer/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
index 0128dd3467..a49b4a67fa 100644
--- a/frontend/viewer/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
+++ b/frontend/viewer/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
@@ -15,7 +15,7 @@
 <DropdownMenuPrimitive.Item
   bind:ref
   class={cn(
-    'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
+    'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
     inset && 'pl-8',
     className,
   )}
diff --git a/frontend/viewer/src/lib/components/ui/icon/icon.svelte b/frontend/viewer/src/lib/components/ui/icon/icon.svelte
index 449e7b4e56..26779f050b 100644
--- a/frontend/viewer/src/lib/components/ui/icon/icon.svelte
+++ b/frontend/viewer/src/lib/components/ui/icon/icon.svelte
@@ -2,18 +2,34 @@
   import type {IconClass} from '$lib/icon-class';
   import {cn} from '$lib/utils';
   import type {WithElementRef, WithoutChildren} from 'bits-ui';
-  import type {HTMLAttributes} from 'svelte/elements';
+  import type {HTMLAttributes, HTMLImgAttributes} from 'svelte/elements';
+  import {tv} from 'tailwind-variants';
 
-  export type IconProps = WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> & {
-    icon: IconClass;
-  };
+  export const iconVariants = tv({
+    base: 'inline-block shrink-0',
+    variants: {
+      size: {
+        default: 'size-6'
+      }
+    },
+    defaultVariants: {
+      size: 'default',
+    },
+  });
+
+  export type IconProps = WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> &
+    ({ icon: IconClass; } | ({ src: string, alt: string } & HTMLImgAttributes));
 </script>
 
 <script lang="ts">
-  const { icon, class: className, ...restProps }: IconProps = $props();
+  const { class: className, ...restProps }: IconProps = $props();
 </script>
 
-<span {...restProps} class={cn('size-6 inline-block shrink-0', className, icon)}>
-  <!-- ensures that baseline alignment works for consumers of this component -->
-  &nbsp;
-</span>
+{#if 'src' in restProps}
+  <img {...restProps} class={cn(iconVariants(), className)} />
+{:else}
+  <span {...restProps} class={cn(iconVariants(), className, restProps.icon)}>
+    <!-- ensures that baseline alignment works for consumers of this component -->
+    &nbsp;
+  </span>
+{/if}
diff --git a/frontend/viewer/src/lib/components/ui/icon/index.ts b/frontend/viewer/src/lib/components/ui/icon/index.ts
index c2af5a9723..f3af4636dc 100644
--- a/frontend/viewer/src/lib/components/ui/icon/index.ts
+++ b/frontend/viewer/src/lib/components/ui/icon/index.ts
@@ -1,4 +1,10 @@
-import Icon from './icon.svelte';
-
-export {Icon};
+import Icon, {
+  type IconProps,
+  iconVariants,
+} from './icon.svelte';
 
+export {
+  Icon,
+  type IconProps,
+  iconVariants
+};
diff --git a/frontend/viewer/src/lib/components/ui/input/composable-input.svelte b/frontend/viewer/src/lib/components/ui/input/composable-input.svelte
index 4bbcffaea1..8903fc73bf 100644
--- a/frontend/viewer/src/lib/components/ui/input/composable-input.svelte
+++ b/frontend/viewer/src/lib/components/ui/input/composable-input.svelte
@@ -22,10 +22,12 @@
     before?: Snippet,
     after?: Snippet,
   } = $props();
+
+  const focusRingClass = 'has-[.real-input:focus-visible]:ring-ring has-[.real-input:focus-visible]:outline-none has-[.real-input:focus-visible]:ring-2 has-[.real-input:focus-visible]:ring-offset-2';
 </script>
 
-<InputShell bind:ref class={className} {...restProps}>
+<InputShell bind:ref {focusRingClass} class={className} {...restProps}>
   {@render before?.()}
-  <Input variant="ghost" {placeholder} class="grow" bind:ref={inputRef} bind:value />
+  <Input variant="ghost" {placeholder} class="grow real-input" bind:ref={inputRef} bind:value />
   {@render after?.()}
 </InputShell>
diff --git a/frontend/viewer/src/lib/components/ui/input/input-shell.svelte b/frontend/viewer/src/lib/components/ui/input/input-shell.svelte
index 48207ee9ba..5ed7731285 100644
--- a/frontend/viewer/src/lib/components/ui/input/input-shell.svelte
+++ b/frontend/viewer/src/lib/components/ui/input/input-shell.svelte
@@ -4,11 +4,16 @@
   import type {HTMLAttributes} from 'svelte/elements';
   import {inputVariants} from './input.svelte';
 
-  type Props = WithElementRef<HTMLAttributes<HTMLDivElement>>;
+  interface Props extends WithElementRef<HTMLAttributes<HTMLDivElement>> {
+    focusRingClass?: string;
+  }
+
+  const anyChildHasFocusRing = 'has-[:focus-visible]:ring-ring has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-offset-2';
 
   let {
     ref = $bindable(null),
     class: className,
+    focusRingClass = anyChildHasFocusRing,
     children,
     ...restProps
   }: Props = $props();
@@ -16,7 +21,7 @@
 
 <div
   bind:this={ref}
-  class={cn(inputVariants({ variant: 'shell' }), className)}
+  class={cn(inputVariants({ variant: 'shell' }), focusRingClass, className)}
   {...restProps}
 >
   {@render children?.()}
diff --git a/frontend/viewer/src/lib/components/ui/input/input.svelte b/frontend/viewer/src/lib/components/ui/input/input.svelte
index 3c49e872bd..4fef5d6be2 100644
--- a/frontend/viewer/src/lib/components/ui/input/input.svelte
+++ b/frontend/viewer/src/lib/components/ui/input/input.svelte
@@ -11,7 +11,7 @@
         default: 'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
         ghost: 'outline-none min-w-0 bg-transparent',
         // shell variant has a dedicated component
-        shell: 'border-input bg-background ring-offset-background placeholder:text-muted-foreground has-[:focus-visible]:ring-ring flex h-10 w-full rounded-md border text-base has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-offset-2 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 md:text-sm flex gap-2 items-center justify-between',
+        shell: 'border-input bg-background ring-offset-background placeholder:text-muted-foreground flex h-10 w-full rounded-md border text-base has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 md:text-sm flex gap-2 items-center justify-between',
       },
     },
     defaultVariants: {
diff --git a/frontend/viewer/src/lib/components/ui/scroll-area/scroll-area.svelte b/frontend/viewer/src/lib/components/ui/scroll-area/scroll-area.svelte
index 43f47d887c..848a183eba 100644
--- a/frontend/viewer/src/lib/components/ui/scroll-area/scroll-area.svelte
+++ b/frontend/viewer/src/lib/components/ui/scroll-area/scroll-area.svelte
@@ -6,6 +6,7 @@
 
   let {
     ref = $bindable(null),
+    viewportRef = $bindable(null),
     class: className,
     type: explicitType,
     orientation = 'vertical',
@@ -17,13 +18,14 @@
     orientation?: 'vertical' | 'horizontal' | 'both' | undefined;
     scrollbarXClasses?: string | undefined;
     scrollbarYClasses?: string | undefined;
+    viewportRef?: HTMLElement | null;
   } = $props();
 
   const type = $derived(explicitType ?? (IsMobile.value ? 'scroll' : 'auto'));
 </script>
 
 <ScrollAreaPrimitive.Root {type} bind:ref {...restProps} class={cn('relative overflow-hidden', className)}>
-  <ScrollAreaPrimitive.Viewport class="h-full w-full rounded-[inherit]">
+  <ScrollAreaPrimitive.Viewport bind:ref={viewportRef} class="h-full w-full rounded-[inherit]">
     {@render children?.()}
   </ScrollAreaPrimitive.Viewport>
   {#if orientation === 'vertical' || orientation === 'both'}
diff --git a/frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte b/frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte
index 662512487e..95dde9e0ff 100644
--- a/frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte
+++ b/frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte
@@ -19,11 +19,13 @@
   {...rest}
   getDisplayName={getHeadword}>
   {#snippet menuItems(entry)}
-    <DropdownMenu.Item>
-      <Link to="browse?entryId={getEntryId(entry)}">
-        <Icon icon="i-mdi-book-outline" />
-        {$t`Go to ${getHeadword(entry) || '–'}`}
-      </Link>
+    <DropdownMenu.Item class="cursor-pointer">
+      {#snippet child({props})}
+        <Link {...props} to="browse?entryId={getEntryId(entry)}">
+          <Icon icon="i-mdi-book-outline" />
+          {$t`Go to ${getHeadword(entry) || '–'}`}
+        </Link>
+      {/snippet}
     </DropdownMenu.Item>
   {/snippet}
 </ItemList>
diff --git a/frontend/viewer/src/lib/entry-editor/ItemListItem.svelte b/frontend/viewer/src/lib/entry-editor/ItemListItem.svelte
index 5af905bd39..3e3d0378bb 100644
--- a/frontend/viewer/src/lib/entry-editor/ItemListItem.svelte
+++ b/frontend/viewer/src/lib/entry-editor/ItemListItem.svelte
@@ -68,7 +68,7 @@
             </Reorderer.Root>
           </DropdownMenu.Sub>
           {/if}
-          <DropdownMenu.Item onSelect={() => remove(item)}>
+          <DropdownMenu.Item class="cursor-pointer" onSelect={() => remove(item)}>
             <Icon icon="i-mdi-trash-can-outline" />
             {$t`Remove`}
           </DropdownMenu.Item>
diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte
index 1142521913..0be71011e6 100644
--- a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte
+++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte
@@ -7,6 +7,7 @@
     canAddExample?: boolean;
     onchange?: (changed: { entry: IEntry, sense?: ISense, example?: IExampleSentence }) => void;
     ondelete?: (deleted: { entry: IEntry, sense?: ISense, example?: IExampleSentence }) => void;
+    ref?: HTMLElement | null;
   };
 </script>
 <script lang="ts">
@@ -27,9 +28,11 @@
   import {watch} from 'runed';
   import FabContainer from '$lib/components/fab/fab-container.svelte';
   import {IsMobile} from '$lib/hooks/is-mobile.svelte';
+  import {findFirstTabbable} from '$lib/utils/tabbable';
 
   let {
     entry = $bindable(),
+    ref = $bindable(null),
     readonly = false,
     modalMode = false,
     canAddSense = true,
@@ -47,14 +50,14 @@
 
   function addSense() {
     const sense = defaultSense(entry.id);
-    highlightedEntity = sense;
+    highlighted = { entity: sense, autofocus: true };
     entry.senses = [...entry.senses, sense];
     newSenses = [...newSenses, sense];
   }
 
   function addExample(sense: ISense) {
     const sentence = defaultExampleSentence(sense.id);
-    highlightedEntity = sentence;
+    highlighted = { entity: sentence, autofocus: true };
     sense.exampleSentences = [...sense.exampleSentences, sentence];
     entry = entry; // examples counts are not updated without this
     newExamples = [...newExamples, sentence];
@@ -74,7 +77,7 @@
     entry.senses.splice(entry.senses.indexOf(sense), 1);
     entry.senses.splice(i, 0, sense);
     onSenseChange(sense);
-    highlightedEntity = sense;
+    highlighted = { entity: sense };
   }
 
   async function deleteExample(sense: ISense, example: IExampleSentence) {
@@ -93,7 +96,7 @@
     sense.exampleSentences.splice(sense.exampleSentences.indexOf(example), 1);
     sense.exampleSentences.splice(i, 0, example);
     onExampleChange(sense, example);
-    highlightedEntity = example;
+    highlighted = { entity: example };
     entry = entry; // examples are not updated without this
   }
 
@@ -107,17 +110,21 @@
   }
 
   let editorElem: HTMLDivElement | null = $state(null);
-  let highlightedEntity = $state<IExampleSentence | ISense | undefined>();
+  let highlighted = $state<{ entity: IExampleSentence | ISense; autofocus?: boolean}>();
   let highlightTimeout: ReturnType<typeof setTimeout>;
+  const ENTITY_FIELD_CONTAINER_CLASS = 'entity-field-container';
 
-  watch(() => highlightedEntity, () => {
-    if (highlightedEntity) {
+  watch(() => highlighted, () => {
+    if (highlighted) {
       clearTimeout(highlightTimeout);
-      highlightTimeout = setTimeout(() => highlightedEntity = undefined, 3000);
+      highlightTimeout = setTimeout(() => highlighted = undefined, 3000);
       // wait for rendering
       setTimeout(() => {
         const newEntityElem = editorElem?.querySelector('.highlight');
         if (newEntityElem) {
+          if (highlighted?.autofocus && !IsMobile.value)
+            findFirstTabbable(newEntityElem?.querySelector(`.${ENTITY_FIELD_CONTAINER_CLASS}`))?.focus();
+
           const _isBottomInViewport = isBottomInView(newEntityElem);
           const _isTopInViewport = isTopInView(newEntityElem);
           if (!_isBottomInViewport && !_isTopInViewport)
@@ -147,12 +154,12 @@
   const currentView = useCurrentView();
 </script>
 
-<Editor.Root>
+<Editor.Root bind:ref>
   <Editor.Grid bind:ref={editorElem}>
-    <EntryEditorPrimitive bind:entry {readonly} {modalMode} onchange={(entry) => onchange?.({entry})} />
+    <EntryEditorPrimitive class={ENTITY_FIELD_CONTAINER_CLASS} bind:entry {readonly} {modalMode} onchange={(entry) => onchange?.({entry})} />
 
     {#each entry.senses as sense, i (sense.id)}
-      <Editor.SubGrid class={cn(sense.id === highlightedEntity?.id && 'highlight')}>
+      <Editor.SubGrid class={cn(sense.id === highlighted?.entity.id && 'highlight')}>
         <div id="sense{i + 1}"></div> <!-- shouldn't be in the sticky header -->
         <div class="col-span-full flex items-center py-2 mb-1 sticky top-0 bg-background z-[1] w-[calc(100%+2px)] pr-[2px] animate-fade-out animation-scroll">
           <h2 class="text-lg text-muted-foreground">{pt($t`Sense`, $t`Meaning`, $currentView)} {i + 1}</h2>
@@ -165,12 +172,12 @@
               ondelete={() => deleteSense(sense)} id={sense.id} />
         </div>
 
-        <SenseEditorPrimitive bind:sense={entry.senses[i]} {readonly} onchange={() => onSenseChange(sense)}/>
+        <SenseEditorPrimitive class={ENTITY_FIELD_CONTAINER_CLASS} bind:sense={entry.senses[i]} {readonly} onchange={() => onSenseChange(sense)}/>
 
         {#if sense.exampleSentences.length}
           <Editor.SubGrid class="border-l border-dashed pl-4 mt-4 space-y-4 rounded-lg">
             {#each sense.exampleSentences as example, j (example.id)}
-              <Editor.SubGrid class={cn(example.id === highlightedEntity?.id && 'highlight')}>
+              <Editor.SubGrid class={cn(example.id === highlighted?.entity.id && 'highlight')}>
                 <div id="example{i + 1}-{j + 1}"></div> <!-- shouldn't be in the sticky header -->
                 <div class="col-span-full flex items-center mb-2">
                   <h3 class="text-muted-foreground">{$t`Example`} {j + 1}</h3>
@@ -189,6 +196,7 @@
                 </div>
 
                 <ExampleEditorPrimitive
+                  class={ENTITY_FIELD_CONTAINER_CLASS}
                   bind:example={sense.exampleSentences[j]}
                   {readonly}
                   onchange={() => onExampleChange(sense, example)}
diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte
index d36234c3d9..05e81464cc 100644
--- a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte
+++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte
@@ -11,8 +11,10 @@
   import {useComplexFormTypes} from '$lib/complex-form-types';
   import ComplexFormComponents from '../field-editors/ComplexFormComponents.svelte';
   import ComplexForms from '../field-editors/ComplexForms.svelte';
+  import type {EditorSubGridProps} from '$lib/components/editor/editor-sub-grid.svelte';
+  import {mergeProps} from 'bits-ui';
 
-  type Props = {
+  interface Props extends Omit<EditorSubGridProps, 'onchange'> {
     entry: IEntry;
     readonly?: boolean;
     modalMode?: boolean;
@@ -24,6 +26,7 @@
     onchange,
     readonly = false,
     modalMode = false,
+    ...rest
   }: Props = $props();
 
   const writingSystemService = useWritingSystemService();
@@ -35,7 +38,7 @@
   }
 </script>
 
-<Editor.SubGrid class="gap-2" style="grid-template-areas: {objectTemplateAreas($currentView, entry)}">
+<Editor.SubGrid {...mergeProps(rest, { class: 'gap-2', style: { gridTemplateAreas: objectTemplateAreas($currentView, entry) } })}>
   <Editor.Field.Root style="grid-area: lexemeForm" class={cn($currentView.fields.lexemeForm.show || 'hidden')}>
     <Editor.Field.Title name={vt($t`Lexeme form`, $t`Word`)} helpId={fieldData.lexemeForm.helpId} />
     <Editor.Field.Body subGrid>
diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte
index 62d5ad4a16..1a9377e404 100644
--- a/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte
+++ b/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte
@@ -8,8 +8,10 @@
   import {vt} from '$lib/views/view-text';
   import {t} from 'svelte-i18n-lingui';
   import {MultiWsInput, WsInput} from '$lib/components/field-editors';
+  import type {EditorSubGridProps} from '$lib/components/editor/editor-sub-grid.svelte';
+  import {mergeProps} from 'bits-ui';
 
-  type Props = {
+  interface Props extends Omit<EditorSubGridProps, 'onchange'> {
     example: IExampleSentence;
     readonly?: boolean;
     onchange?: (sense: IExampleSentence, field: FieldId) => void;
@@ -19,6 +21,7 @@
     example = $bindable(),
     readonly = false,
     onchange,
+    ...rest
   }: Props = $props();
 
   const writingSystemService = useWritingSystemService();
@@ -29,7 +32,7 @@
   }
 </script>
 
-<Editor.SubGrid class="gap-2" style="grid-template-areas: {objectTemplateAreas($currentView, example)}">
+<Editor.SubGrid {...mergeProps(rest, { class: 'gap-2', style: { gridTemplateAreas: objectTemplateAreas($currentView, example) } })}>
   <Editor.Field.Root style="grid-area: sentence" class={cn($currentView.fields.sentence.show || 'hidden')}>
     <Editor.Field.Title name={vt($t`Sentence`)} helpId={fieldData.sentence.helpId} />
     <Editor.Field.Body subGrid>
diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte
index 118a18f197..77eac63ac1 100644
--- a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte
+++ b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte
@@ -10,17 +10,20 @@
   import {MultiSelect, MultiWsInput, Select} from '$lib/components/field-editors';
   import {fieldData, type FieldId} from '../field-data';
   import {cn} from '$lib/utils';
+  import type {EditorSubGridProps} from '$lib/components/editor/editor-sub-grid.svelte';
+  import {mergeProps} from 'bits-ui';
 
-  type Props = {
+  interface Props extends Omit<EditorSubGridProps, 'onchange'> {
     sense: ISense;
     readonly?: boolean;
     onchange?: (sense: ISense, field: FieldId) => void;
-  }
+  };
 
   const {
     sense = $bindable(),
     readonly = false,
     onchange,
+    ...rest
   }: Props = $props();
 
   const writingSystemService = useWritingSystemService();
@@ -33,7 +36,7 @@
   }
 </script>
 
-<Editor.SubGrid class="gap-2" style="grid-template-areas: {objectTemplateAreas($currentView, sense)}">
+<Editor.SubGrid {...mergeProps(rest, { class: 'gap-2', style: { gridTemplateAreas: objectTemplateAreas($currentView, sense) } })}>
   <Editor.Field.Root style="grid-area: gloss" class={cn($currentView.fields.gloss.show || 'hidden')}>
     <Editor.Field.Title name={$t`Gloss`} helpId={fieldData.gloss.helpId} />
     <Editor.Field.Body subGrid>
diff --git a/frontend/viewer/src/lib/utils/tabbable.ts b/frontend/viewer/src/lib/utils/tabbable.ts
new file mode 100644
index 0000000000..beab17883c
--- /dev/null
+++ b/frontend/viewer/src/lib/utils/tabbable.ts
@@ -0,0 +1,17 @@
+import {isTabbable} from 'tabbable';
+
+export function findFirstTabbable(container: Element | null | undefined): HTMLElement | undefined {
+  if (!container) {
+    return undefined;
+  }
+  const walker = document.createTreeWalker(
+    container,
+    NodeFilter.SHOW_ELEMENT,
+    {
+      acceptNode: node => isTabbable(node as HTMLElement, {displayCheck: 'full'})
+        ? NodeFilter.FILTER_ACCEPT
+        : NodeFilter.FILTER_SKIP
+    }
+  );
+  return walker.nextNode() as HTMLElement;
+}
diff --git a/frontend/viewer/src/project/ProjectDropdown.svelte b/frontend/viewer/src/project/ProjectDropdown.svelte
index d153295eba..dd266fe074 100644
--- a/frontend/viewer/src/project/ProjectDropdown.svelte
+++ b/frontend/viewer/src/project/ProjectDropdown.svelte
@@ -62,6 +62,14 @@
   }
 </script>
 
+{#snippet projectIcon(isCrdt: boolean)}
+  {#if isCrdt}
+    <Icon icon="i-mdi-book-edit-outline" />
+  {:else}
+    <Icon src={flexLogo} alt={$t`FieldWorks logo`} />
+  {/if}
+{/snippet}
+
 <Popover.Root bind:open onOpenChange={handleOpen}>
   <Popover.Trigger bind:ref={triggerRef}>
     {#snippet child({ props })}
@@ -73,7 +81,7 @@
         {...props}
       >
         <div class="flex items-center gap-2 overflow-hidden">
-          <Icon icon="i-mdi-book" class="size-4" />
+          {@render projectIcon(isCrdt)}
           <span class="x-ellipsis">
             {projectName}
           </span>
@@ -89,7 +97,6 @@
     <Command.Root>
       <Command.Input placeholder={$t`Search Dictionaries`} />
       <Command.List>
-        <Command.Empty>{$t`No Dictionaries found`}</Command.Empty>
         {#if projectsResource.loading}
           <Command.Loading>
             <div class="flex items-center justify-center p-4">
@@ -98,17 +105,14 @@
             </div>
           </Command.Loading>
         {:else}
+          <Command.Empty>{$t`No Dictionaries found`}</Command.Empty>
           {#each projectsResource.current ?? [] as project}
             <Command.Item
               value={project.name + project.crdt}
               onSelect={() => handleSelect(project)}
               class={cn('cursor-pointer', (project.name === projectName || project.code === projectName) && project.crdt === isCrdt && 'bg-secondary')}
             >
-              {#if project.fwdata}
-                <img src={flexLogo} alt={$t`FieldWorks logo`} class="h-6 shrink-0"/>
-              {:else}
-                <Icon icon="i-mdi-book-edit-outline"/>
-              {/if}
+              {@render projectIcon(project.crdt)}
               <ProjectTitle {project} />
             </Command.Item>
           {/each}
diff --git a/frontend/viewer/src/project/browse/BrowseView.svelte b/frontend/viewer/src/project/browse/BrowseView.svelte
index e8f5067373..8d95b0671f 100644
--- a/frontend/viewer/src/project/browse/BrowseView.svelte
+++ b/frontend/viewer/src/project/browse/BrowseView.svelte
@@ -4,7 +4,7 @@
   import EntryView from './EntryView.svelte';
   import SearchFilter from './SearchFilter.svelte';
   import EntriesList from './EntriesList.svelte';
-  import { Badge } from '$lib/components/ui/badge';
+  import { badgeVariants } from '$lib/components/ui/badge';
   import { Icon } from '$lib/components/ui/icon';
   import { t } from 'svelte-i18n-lingui';
   import SidebarPrimaryAction from '../SidebarPrimaryAction.svelte';
@@ -56,14 +56,13 @@
           <div class="md:mr-3">
             <SearchFilter bind:search bind:gridifyFilter />
             <div class="my-2 flex items-center justify-between">
-              <Badge
-                variant="secondary"
-                class="cursor-pointer"
+              <button
+                class={badgeVariants({ variant: 'secondary' })}
                 onclick={toggleSort}
               >
                 <Icon icon={sortDirection === 'asc' ? 'i-mdi-sort-alphabetical-ascending' : 'i-mdi-sort-alphabetical-descending'} class="h-4 w-4" />
                 {$t`Headword`}
-              </Badge>
+              </button>
               <ResponsivePopup title={$t`List mode`}>
                 {#snippet trigger({props})}
                   <Button {...props} size="xs-icon" variant="ghost" icon="i-mdi-format-list-text" />
diff --git a/frontend/viewer/src/project/browse/EntriesList.svelte b/frontend/viewer/src/project/browse/EntriesList.svelte
index c8dca968c5..80c441ff56 100644
--- a/frontend/viewer/src/project/browse/EntriesList.svelte
+++ b/frontend/viewer/src/project/browse/EntriesList.svelte
@@ -67,6 +67,9 @@
       }
       return miniLcmApi.getEntries(queryOptions);
     },
+    {
+      debounce: 300,
+    }
   );
   const entries = $derived(entriesResource.current ?? []);
   const loading = new Debounced(() => entriesResource.loading, 50);
diff --git a/frontend/viewer/src/project/browse/EntryView.svelte b/frontend/viewer/src/project/browse/EntryView.svelte
index 7b8160909a..4b68394b0b 100644
--- a/frontend/viewer/src/project/browse/EntryView.svelte
+++ b/frontend/viewer/src/project/browse/EntryView.svelte
@@ -2,7 +2,7 @@
   import { Icon } from '$lib/components/ui/icon';
   import EntryEditor from '$lib/entry-editor/object-editors/EntryEditor.svelte';
   import { useViewSettings } from '$lib/views/view-service';
-  import {resource, Debounced} from 'runed';
+  import {resource, Debounced, watch} from 'runed';
   import { useMiniLcmApi } from '$lib/services/service-provider';
   import { fade } from 'svelte/transition';
   import ViewPicker from './ViewPicker.svelte';
@@ -17,6 +17,8 @@
   import type {IEntry} from '$lib/dotnet-types';
   import {EntryPersistence} from '$lib/entry-editor/entry-persistence.svelte';
   import {useProjectEventBus} from '$lib/services/event-bus';
+  import {IsMobile} from '$lib/hooks/is-mobile.svelte';
+  import {findFirstTabbable} from '$lib/utils/tabbable';
 
   const viewSettings = useViewSettings();
   const writingSystemService = useWritingSystemService();
@@ -40,7 +42,7 @@
   );
   eventBus.onEntryUpdated((e) => {
     if (e.id === entryId) {
-      entryResource.refetch();
+      void entryResource.refetch();
     }
   });
   const entry = $derived(entryResource.current ?? undefined);
@@ -51,6 +53,14 @@
 
   let readonly = $state(false);
   const entryPersistence = new EntryPersistence(() => entry, () => void entryResource.refetch());
+
+  const loadedEntryId = $derived(entry?.id);
+  let entryScrollViewportRef: HTMLElement | null = $state(null);
+  let editorRef: HTMLElement | null = $state(null);
+  watch([() => [loadedEntryId, entryScrollViewportRef, editorRef]], () =>{
+    entryScrollViewportRef?.scrollTo({ top: 0, left: 0 });
+    if (!IsMobile.value) findFirstTabbable(editorRef)?.focus();
+  });
 </script>
 
 {#snippet preview(entry: IEntry)}
@@ -85,12 +95,12 @@
         </div>
       {/if}
     </header>
-    <ScrollArea class={cn('grow md:pr-2', !$viewSettings.showEmptyFields && 'hide-unused')}>
+    <ScrollArea bind:viewportRef={entryScrollViewportRef} class={cn('grow md:pr-2', !$viewSettings.showEmptyFields && 'hide-unused')}>
       {#if dictionaryPreview === 'show'}
         {@render preview(entry)}
       {/if}
       <div class="max-md:p-2 md:pr-2">
-        <EntryEditor {entry} {readonly} {...entryPersistence.entryEditorProps} />
+        <EntryEditor bind:ref={editorRef} {entry} {readonly} {...entryPersistence.entryEditorProps} />
       </div>
     </ScrollArea>
   {/if}
diff --git a/frontend/viewer/src/project/browse/SearchFilter.svelte b/frontend/viewer/src/project/browse/SearchFilter.svelte
index a8548ed1ee..e41d162724 100644
--- a/frontend/viewer/src/project/browse/SearchFilter.svelte
+++ b/frontend/viewer/src/project/browse/SearchFilter.svelte
@@ -7,6 +7,7 @@
   import {Switch} from '$lib/components/ui/switch';
   import {Toggle} from '$lib/components/ui/toggle';
   import {cn} from '$lib/utils';
+  import {mergeProps} from 'bits-ui';
 
   let {
     search = $bindable(),
@@ -47,9 +48,11 @@
       {/snippet}
       {#snippet after()}
         <Collapsible.Trigger>
-          <Toggle aria-label={$t`Toggle filters`} class="aspect-square" size="xs">
-            <Icon icon={gridifyFilter ? 'i-mdi-filter' : 'i-mdi-filter-outline'} class="size-5" />
-          </Toggle>
+          {#snippet child({props})}
+            <Toggle {...mergeProps(props, { class: 'aspect-square' })} aria-label={$t`Toggle filters`} size="xs">
+              <Icon icon={gridifyFilter ? 'i-mdi-filter' : 'i-mdi-filter-outline'} class="size-5" />
+            </Toggle>
+          {/snippet}
         </Collapsible.Trigger>
       {/snippet}
     </ComposableInput>


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>frontend/viewer/src/lib/components/reorderer/reorderer-item-list.svelte (1)</summary><blockquote>

`46-46`: **Confirm removal of pointer cursor for reorder items.**
Dropping `cursor-pointer` changes the hover feedback on draggable items. If intentional, ensure this aligns with the UX guidelines. Alternatively, consider using a `cursor-grab` or `cursor-move` to indicate draggable functionality.

</blockquote></details>
<details>
<summary>frontend/viewer/src/project/browse/EntryMenu.svelte (1)</summary><blockquote>

`66-66`: **Simplify conditional logic**

The current condition uses `|| true` which effectively makes the condition only dependent on `features.openWithFlex`. This approach adds unnecessary complexity.

```diff
-    {#if features.openWithFlex && (appLauncher || !IsMobile.value || true)}
+    {#if features.openWithFlex}
```

</blockquote></details>
<details>
<summary>frontend/viewer/src/lib/utils/tabbable.ts (1)</summary><blockquote>

`1-17`: **Well-implemented focus utility function**

This utility function is a clean implementation for finding the first tabbable element in a container. It properly handles null/undefined containers and uses the efficient TreeWalker API for DOM traversal.


Consider adding JSDoc comments to document the purpose and usage of this function, especially since it's likely to be used across multiple components.

```diff
+/**
+ * Finds the first tabbable element within a container.
+ * @param container - The container element to search within
+ * @returns The first tabbable HTMLElement or undefined if none found
+ */
 export function findFirstTabbable(container: Element | null | undefined): HTMLElement | undefined {
```

</blockquote></details>
<details>
<summary>frontend/viewer/src/lib/components/ui/icon/icon.svelte (1)</summary><blockquote>

`25-25`: **Consider explicitly extracting the size prop.**

The `size` variant is defined in `iconVariants` but not explicitly extracted from props.

```diff
- const { class: className, ...restProps }: IconProps = $props();
+ const { class: className, size = "default", ...restProps }: IconProps = $props();
```

Then use it when applying variants:
```diff
- <img {...restProps} class={cn(iconVariants(), className)} />
+ <img {...restProps} class={cn(iconVariants({ size }), className)} />
```
(And similar for the span element)

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7040f153f9dc5ac5ce018807539b3a2584c14406 and 5452b93a037be02a0482de4a8869888b3d8e9f43.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `frontend/pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (26)</summary>

* `frontend/viewer/package.json` (1 hunks)
* `frontend/viewer/src/home/HomeView.svelte` (3 hunks)
* `frontend/viewer/src/home/Server.svelte` (1 hunks)
* `frontend/viewer/src/lib/OpenInFieldWorksButton.svelte` (2 hunks)
* `frontend/viewer/src/lib/components/editor/editor-root.svelte` (1 hunks)
* `frontend/viewer/src/lib/components/reorderer/reorderer-item-list.svelte` (1 hunks)
* `frontend/viewer/src/lib/components/responsive-menu/responsive-menu-item.svelte` (2 hunks)
* `frontend/viewer/src/lib/components/ui/button/index.ts` (1 hunks)
* `frontend/viewer/src/lib/components/ui/context-menu/context-menu-item.svelte` (1 hunks)
* `frontend/viewer/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte` (1 hunks)
* `frontend/viewer/src/lib/components/ui/icon/icon.svelte` (1 hunks)
* `frontend/viewer/src/lib/components/ui/icon/index.ts` (1 hunks)
* `frontend/viewer/src/lib/components/ui/scroll-area/scroll-area.svelte` (2 hunks)
* `frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte` (1 hunks)
* `frontend/viewer/src/lib/entry-editor/ItemListItem.svelte` (1 hunks)
* `frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte` (9 hunks)
* `frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte` (1 hunks)
* `frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte` (1 hunks)
* `frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte` (1 hunks)
* `frontend/viewer/src/lib/utils/tabbable.ts` (1 hunks)
* `frontend/viewer/src/project/ProjectDropdown.svelte` (3 hunks)
* `frontend/viewer/src/project/browse/BrowseView.svelte` (2 hunks)
* `frontend/viewer/src/project/browse/EntriesList.svelte` (1 hunks)
* `frontend/viewer/src/project/browse/EntryMenu.svelte` (1 hunks)
* `frontend/viewer/src/project/browse/EntryView.svelte` (5 hunks)
* `frontend/viewer/src/project/browse/SearchFilter.svelte` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Build API / publish-api
* GitHub Check: Build UI / publish-ui
* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: check-and-lint
* GitHub Check: Build FW Lite and run tests
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (43)</summary><blockquote>

<details>
<summary>frontend/viewer/package.json (1)</summary>

`100-100`: **Appropriate dependency addition for focus management**

Adding the "tabbable" library aligns with the PR objective of improving keyboard navigation and enhancing tabbing behavior. This library helps identify and manage focus on interactive elements.

</details>
<details>
<summary>frontend/viewer/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte (1)</summary>

`18-18`: **Improved usability with cursor-pointer**

Changing from `cursor-default` to `cursor-pointer` provides better visual feedback to users that the dropdown items are interactive. This is consistent with standard UI patterns and aligns with the visual improvements mentioned in the PR objectives.

</details>
<details>
<summary>frontend/viewer/src/lib/components/editor/editor-root.svelte (2)</summary>

`9-14`: **Appropriate change from const to let for DOM binding**

The change from `const` to `let` for the destructured props is necessary to support the DOM element binding added in line 17. This is standard practice in Svelte when variables need to be reassigned.

---

`17-17`: **Improved accessibility with ref binding**

Binding the ref to the root div element enables parent components to access this DOM element directly, supporting the PR objectives of improved focus management and scrolling behavior.

</details>
<details>
<summary>frontend/viewer/src/lib/components/ui/scroll-area/scroll-area.svelte (2)</summary>

`9-9`: **Well-implemented viewport reference property**

Adding the viewportRef bindable property with proper typing enables external components to access the viewport DOM element, supporting the scrolling and focus management improvements mentioned in the PR objectives.



Also applies to: 21-21

---

`28-28`: **Proper DOM binding implementation**

The binding between the viewportRef prop and the ScrollAreaPrimitive.Viewport element is correctly implemented, allowing parent components to control scrolling behavior programmatically.

</details>
<details>
<summary>frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte (1)</summary>

`38-38`: **Verify presence of `editor-primitive` CSS definition.**
You added the `editor-primitive` class to the editor grid container to unify styling and focus behavior. Please ensure this class is defined in your global or component stylesheet (e.g., in a shared CSS module or Tailwind config) so that it has the intended effect.

</details>
<details>
<summary>frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte (1)</summary>

`32-32`: **Consistent addition of `editor-primitive` class.**
This aligns the example editor primitive with others, enabling uniform styling and focus behavior. Ensure that your focus management logic (e.g., `findFirstTabbable`) targets this class appropriately.

</details>
<details>
<summary>frontend/viewer/src/lib/entry-editor/ItemListItem.svelte (1)</summary>

`71-71`: **Enhanced click affordance by adding pointer cursor.**
The `cursor-pointer` class clearly indicates interactivity on the Remove item, improving usability and consistency with other menu items. Great enhancement!

</details>
<details>
<summary>frontend/viewer/src/lib/components/ui/context-menu/context-menu-item.svelte (1)</summary>

`18-18`: **Switch to pointer cursor for better affordance.**
Replacing `cursor-default` with `cursor-pointer` improves the visual feedback on hover, signaling that the item is clickable. This aligns with similar updates in dropdown menus.

</details>
<details>
<summary>frontend/viewer/src/lib/OpenInFieldWorksButton.svelte (2)</summary>

`11-11`: **Good import addition for icon standardization.**

Adding the Icon component import aligns with the PR objective of unifying icons across the application.

---

`56-56`: **Improved icon rendering approach.**

Replacing direct image usage with the Icon component ensures consistent sizing and alignment for the FieldWorks logo, as mentioned in the PR objectives.

</details>
<details>
<summary>frontend/viewer/src/project/browse/EntriesList.svelte (1)</summary>

`70-72`: **Good optimization with debounce.**

Adding a 300ms debounce to the resource call prevents excessive API requests when search parameters change rapidly, improving performance and user experience.

</details>
<details>
<summary>frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte (1)</summary>

`36-36`: **Good addition for focus management.**

Adding the `editor-primitive` class standardizes styling across editor components and enables improved focus management with the new tabbable utility, supporting the PR's goal of enhancing keyboard navigation.

</details>
<details>
<summary>frontend/viewer/src/project/browse/BrowseView.svelte (2)</summary>

`7-7`: **Good refactoring of badge import.**

Switching from the Badge component to badgeVariants function import follows a more flexible styling utility approach.

---

`59-65`: **Improved button implementation.**

Replacing the Badge component with a native button element styled using badgeVariants provides better semantics while maintaining consistent styling. This approach is more flexible and aligns with modern UI development patterns.

</details>
<details>
<summary>frontend/viewer/src/lib/components/ui/icon/index.ts (1)</summary>

`1-10`: **Improved component exports with proper type information**

The Icon component is now properly exported along with its type definitions and variants, following best practices for component modularity and reuse.

</details>
<details>
<summary>frontend/viewer/src/home/HomeView.svelte (3)</summary>

`32-32`: **LGTM: Icon component import added**

The Icon component is now imported to be used for standardizing icon rendering throughout the file.

---

`108-108`: **Standardized logo rendering**

Replacing the direct `<img>` with the Icon component improves consistency across the UI.

---

`226-226`: **Consistent FieldWorks logo implementation**

The FieldWorks logo is now also using the Icon component, ensuring consistent rendering and styling.

</details>
<details>
<summary>frontend/viewer/src/home/Server.svelte (1)</summary>

`126-132`: **Improved UI structure for helper button**

Moving the "I don't see my project" button outside the project list container creates a better visual hierarchy and ensures the button appears only once regardless of the number of projects.

</details>
<details>
<summary>frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte (1)</summary>

`22-28`: **Improved menu item interactivity and composition**

The addition of `cursor-pointer` class makes the dropdown item's clickability more evident to users. The use of the `{#snippet child({props})}` pattern enables better component composition by allowing props to be passed down to the Link component.

</details>
<details>
<summary>frontend/viewer/src/project/browse/SearchFilter.svelte (1)</summary>

`10-10`: **Enhanced component composition with prop merging**

The addition of `mergeProps` and the `{#snippet child({props})}` pattern creates a more composable UI by allowing props to be passed down and merged with existing ones. This approach ensures the Toggle maintains its required properties while accepting additional ones from its parent.



Also applies to: 51-55

</details>
<details>
<summary>frontend/viewer/src/project/browse/EntryView.svelte (4)</summary>

`5-5`: **Added necessary imports for enhanced user experience features**

The new imports support the added functionality for improved focus management and viewport scrolling.



Also applies to: 20-21

---

`45-45`: **Properly handled promise with void operator**

Good practice to use the `void` operator to explicitly ignore the promise returned by `refetch()`.

---

`57-63`: **Enhanced user experience with automatic scrolling and focus**

This watch effect is well-implemented to improve the user experience when navigating between entries by:
1. Automatically scrolling to the top of the entry
2. Setting focus on the first interactive element (skipping this on mobile devices)

The conditional check for mobile is a good UX consideration as automatic focus can be disruptive on touch devices.

---

`98-98`: **Proper binding of DOM references**

The component references are properly bound to enable the scrolling and focus functionality implemented in the watch effect.



Also applies to: 103-103

</details>
<details>
<summary>frontend/viewer/src/project/ProjectDropdown.svelte (3)</summary>

`65-71`: **Well-organized icon rendering logic!**

Creating a reusable snippet for the project icon rendering is a great DRY improvement that centralizes the conditional logic in one place.

---

`84-84`: **Clean implementation of the icon snippet**

Replacing the duplicated icon rendering logic with the snippet calls makes the code more maintainable and consistent.



Also applies to: 115-115

---

`108-108`: **Proper placement of Command.Empty**

Moving the Command.Empty inside the else block ensures it only appears after loading completes and when no projects are found, which is the correct behavior.

</details>
<details>
<summary>frontend/viewer/src/lib/components/ui/button/index.ts (1)</summary>

`10-20`: **Improved export organization**

The reorganized exports with additional aliases provide more flexible and consistent API usage patterns across the UI components.

</details>
<details>
<summary>frontend/viewer/src/lib/components/responsive-menu/responsive-menu-item.svelte (4)</summary>

`7-13`: **Well-structured imports**

The reorganized imports properly separate direct component imports from type imports, which improves code organization.

---

`20-20`: **Improved type definition**

The Props type now correctly combines ContextMenuItemProps and DrawerCloseProps while avoiding conflicts with the onclick handler.

---

`48-49`: **Consistent event binding**

Direct binding of the onSelect handler is more consistent with the component's API design.



Also applies to: 53-54

---

`58-64`: **Semantic improvement with DrawerClose**

Replacing Button with DrawerClose for the mobile view is semantically more appropriate and provides better integration with the drawer component.

</details>
<details>
<summary>frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte (5)</summary>

`10-10`: **Good component API enhancement**

Adding the ref prop and binding it to the root element allows external components to access and manipulate the editor's DOM element when needed.



Also applies to: 35-35, 156-156

---

`31-31`: **Improved focus management setup**

The findFirstTabbable utility import and enhanced highlightedEntity state structure provide better support for focus management.



Also applies to: 113-113

---

`53-53`: **Good distinction between highlight and focus behaviors**

The code now properly distinguishes between operations that should trigger autofocus (adding new items) and those that should only highlight (moving items).



Also applies to: 60-60, 80-80, 99-99

---

`124-126`: **Well-implemented autofocus logic**

The conditional focus handling respects mobile devices (where focus can be problematic) and uses the findFirstTabbable utility to locate the appropriate element to focus.

---

`161-161`: **Updated class bindings**

The class bindings have been correctly updated to work with the new highlightedEntity structure.



Also applies to: 179-179

</details>
<details>
<summary>frontend/viewer/src/lib/components/ui/icon/icon.svelte (3)</summary>

`8-18`: **Good implementation of tailwind-variants for consistent styling.**

The use of `tailwind-variants` for centralizing icon styling is a good approach. The base styles and size variants are well-defined, making it easy to maintain consistent styling across different usages of the component.

---

`20-21`: **Strong type definition for polymorphic component.**

The union type approach for `IconProps` is a good solution to support both icon classes and image sources in a type-safe manner. This ensures proper type checking for both usage patterns.

---

`30-34`: **Good technique for baseline alignment.**

The non-breaking space in the span element is a clever way to ensure consistent baseline alignment. The descriptive comment is also helpful for future maintainers.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T15:00:38Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T04:15:53Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T04:18:02Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T04:28:51Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T04:30:29Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T04:32:00Z
user: login: hahn-kev
body: looks good, I left a few things to discuss
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T04:37:02Z
user: login: myieye
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T06:30:58Z
user: login: myieye
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T12:24:18Z
user: login: myieye
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T12:41:44Z

Review comments:
diff_hunk: @@ -2,18 +2,34 @@
   import type {IconClass} from '$lib/icon-class';
   import {cn} from '$lib/utils';
   import type {WithElementRef, WithoutChildren} from 'bits-ui';
-  import type {HTMLAttributes} from 'svelte/elements';
+  import type {HTMLAttributes, HTMLImgAttributes} from 'svelte/elements';
+  import {tv} from 'tailwind-variants';
 
-  export type IconProps = WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> & {
-    icon: IconClass;
-  };
+  export const iconVariants = tv({
+    base: 'inline-block shrink-0',
+    variants: {
+      size: {
+        default: 'size-6'
+      }
+    },
+    defaultVariants: {
+      size: 'default',
+    },
+  });
+
+  export type IconProps = WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> &
+    ({ icon: IconClass; } | ({ src: string} & HTMLImgAttributes));
 </script>
 
 <script lang="ts">
-  const { icon, class: className, ...restProps }: IconProps = $props();
+  const { class: className, ...restProps }: IconProps = $props();
 </script>
 
-<span {...restProps} class={cn('size-6 inline-block shrink-0', className, icon)}>
-  <!-- ensures that baseline alignment works for consumers of this component -->
-  &nbsp;
-</span>
+{#if 'src' in restProps}
+  <img {...restProps} class={cn(iconVariants(), className)} />
+{:else}
+  <span {...restProps} class={cn(iconVariants(), className, restProps.icon)}>
+    <!-- ensures that baseline alignment works for consumers of this component -->
+    &nbsp;
+  </span>
+{/if}
path: frontend/viewer/src/lib/components/ui/icon/icon.svelte
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Verify alt text is provided for img elements.**

When rendering as an `<img>`, ensure that an `alt` attribute is always provided for accessibility. Consider adding a default or warning if it's missing.

---

🏁 Script executed:

```shell
#!/bin/bash
# Find usages of the Icon component with src but no alt attribute
rg -U "<Icon\s+[^>]*src\s*=\s*['\"][^'\"]*['\"][^>]*>" frontend/viewer/src --iglob "*.svelte" | rg -v "alt\s*="
```

Length of output: 119

---



</details>

**Ensure `alt` is always present on `<img>` in Icon component**

To meet accessibility requirements, the `<img>` rendered by `Icon.svelte` must always include an `alt` attribute (even if it’s an empty string for purely decorative icons). Update `frontend/viewer/src/lib/components/ui/icon/icon.svelte` as follows:

- Destructure `alt` (with a default) instead of relying solely on `$$restProps`.
- Always render `alt`, falling back to `''` when no value is provided.
- Optionally log a warning when `alt` is left empty, to remind developers to supply meaningful text where appropriate.

Example change:

```diff
--- a/frontend/viewer/src/lib/components/ui/icon/icon.svelte
+++ b/frontend/viewer/src/lib/components/ui/icon/icon.svelte
@@ <script> section
- export let className = '';
- export let icon;
+ export let className = '';
+ export let icon;
+ // When using an external SVG via src, alt should default to '' if not provided
+ export let alt: string = '';

@@ component template
-{#if 'src' in restProps}
-  <img {...restProps} class={cn(iconVariants(), className)} />
+{#if 'src' in restProps}
+  <img
+    {...restProps}
+    alt={alt}
+    class={cn(iconVariants(), className)}
+  />
```

- If you’d like additional guidance, you can emit a `console.warn()` when `alt === ''` to flag potentially missing descriptive text.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
<script lang="ts">
  import { cn, iconVariants } from '...';  // existing imports

  export let className = '';
  export let icon;
  // When using an external SVG via src, alt should default to '' if not provided
  export let alt: string = '';
</script>

{#if 'src' in restProps}
  <img
    {...restProps}
    alt={alt}
    class={cn(iconVariants(), className)}
  />
{:else}
  <span {...restProps} class={cn(iconVariants(), className, restProps.icon)}>
    <!-- ensures that baseline alignment works for consumers of this component -->
    &nbsp;
  </span>
{/if}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In frontend/viewer/src/lib/components/ui/icon/icon.svelte around lines 28 to 35,
the <img> element rendered when 'src' is present does not guarantee an alt
attribute, which is required for accessibility. To fix this, destructure an alt
prop with a default empty string from the component's props, then always include
the alt attribute on the <img> element using this value. Optionally, add a
console.warn when alt is an empty string to remind developers to provide
meaningful alt text.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T15:00:37Z
author_association: NONE
diff_hunk: @@ -147,12 +153,12 @@
   const currentView = useCurrentView();
 </script>
 
-<Editor.Root>
+<Editor.Root bind:ref>
   <Editor.Grid bind:ref={editorElem}>
     <EntryEditorPrimitive bind:entry {readonly} {modalMode} onchange={(entry) => onchange?.({entry})} />
 
     {#each entry.senses as sense, i (sense.id)}
-      <Editor.SubGrid class={cn(sense.id === highlightedEntity?.id && 'highlight')}>
+      <Editor.SubGrid class={cn(sense.id === highlightedEntity?.entity.id && 'highlight')}>
path: frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte
user: login: hahn-kev
body: should we maybe change the `highlightedEntity` name to just `highlighted` now that it has an entity property on it?
created_at: 2025-05-22T04:15:52Z
author_association: COLLABORATOR
diff_hunk: @@ -35,7 +35,7 @@
   }
 </script>
 
-<Editor.SubGrid class="gap-2" style="grid-template-areas: {objectTemplateAreas($currentView, entry)}">
+<Editor.SubGrid class="gap-2 editor-primitive" style="grid-template-areas: {objectTemplateAreas($currentView, entry)}">
path: frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte
user: login: hahn-kev
body: I don't know how but we should document why these are here as it's very non obvious, I could easily see us removing them because there isn't any associated styles. Maybe we just use a const string value and reference it from here and in EntryEditor?
created_at: 2025-05-22T04:18:02Z
author_association: COLLABORATOR
diff_hunk: @@ -118,6 +121,9 @@
       setTimeout(() => {
         const newEntityElem = editorElem?.querySelector('.highlight');
         if (newEntityElem) {
+          if (highlightedEntity?.autofocus && !IsMobile.value)
+            findFirstTabbable(newEntityElem?.querySelector('.editor-primitive'))?.focus();
path: frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte
user: login: hahn-kev
body: the tabbable library has an isFocusable check, should we maybe be using that instead considering we want to focus on something?
created_at: 2025-05-22T04:28:51Z
author_association: COLLABORATOR
diff_hunk: @@ -63,7 +63,7 @@
         {$t`Open in new Window`}
       </ResponsiveMenu.Item>
     {/if}
-    {#if features.openWithFlex && (appLauncher || !IsMobile.value)}
+    {#if features.openWithFlex && (appLauncher || !IsMobile.value || true)}
path: frontend/viewer/src/project/browse/EntryMenu.svelte
user: login: hahn-kev
body: did you mean to leave this in?
created_at: 2025-05-22T04:30:29Z
author_association: COLLABORATOR
diff_hunk: @@ -51,6 +53,14 @@
 
   let readonly = $state(false);
   const entryPersistence = new EntryPersistence(() => entry, () => void entryResource.refetch());
+
+  const loadedEntryId = $derived(entry?.id);
+  let entryScrollViewportRef: HTMLElement | null = $state(null);
+  let editorRef: HTMLElement | null = $state(null);
+  watch([() => loadedEntryId, () => entryScrollViewportRef, () => editorRef], () =>{
path: frontend/viewer/src/project/browse/EntryView.svelte
user: login: hahn-kev
body: might I suggest `watch(() => [loadedEntryId, entryScrollViewportRef, editorRef], () => {...}))`, not sure why they have this option as it's so noisy.
created_at: 2025-05-22T04:31:59Z
author_association: COLLABORATOR
diff_hunk: @@ -63,7 +63,7 @@
         {$t`Open in new Window`}
       </ResponsiveMenu.Item>
     {/if}
-    {#if features.openWithFlex && (appLauncher || !IsMobile.value)}
+    {#if features.openWithFlex && (appLauncher || !IsMobile.value || true)}
path: frontend/viewer/src/project/browse/EntryMenu.svelte
user: login: myieye
body: oooooops
created_at: 2025-05-22T06:30:58Z
author_association: COLLABORATOR
diff_hunk: @@ -118,6 +121,9 @@
       setTimeout(() => {
         const newEntityElem = editorElem?.querySelector('.highlight');
         if (newEntityElem) {
+          if (highlightedEntity?.autofocus && !IsMobile.value)
+            findFirstTabbable(newEntityElem?.querySelector('.editor-primitive'))?.focus();
path: frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte
user: login: myieye
body: Well, we're actually doing this for tabbing purposes. `isFocusable` is almost the same, but it includes things that we don't want e.g. things with `tabindex="-1"`.
created_at: 2025-05-22T12:24:18Z
author_association: COLLABORATOR
diff_hunk: @@ -35,7 +35,7 @@
   }
 </script>
 
-<Editor.SubGrid class="gap-2" style="grid-template-areas: {objectTemplateAreas($currentView, entry)}">
+<Editor.SubGrid class="gap-2 editor-primitive" style="grid-template-areas: {objectTemplateAreas($currentView, entry)}">
path: frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte
user: login: myieye
body: I defined a const in `EntryEditor` and pass it in, so everything's explicit in the same file.
created_at: 2025-05-22T12:41:44Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-21T14:57:06Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto incremental reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

This update introduces a new `tabbable` dependency and utility for improved focus management, centralizes and enhances icon rendering via a polymorphic `<Icon>` component, and standardizes cursor and button behaviors across menu components. Refactoring includes improved focus handling in editors, prop typing adjustments, and minor UI consistency changes for menus, buttons, and project icons.

## Changes

| Files/Groups                                                                                      | Change Summary |
|---------------------------------------------------------------------------------------------------|---------------|
| `frontend/viewer/package.json`                                                                    | Added the `tabbable` package to dependencies. |
| `frontend/viewer/src/home/HomeView.svelte`, `OpenInFieldWorksButton.svelte`                       | Replaced `<img>` elements for logos/avatars with the `<Icon>` component. |
| `frontend/viewer/src/home/Server.svelte`                                                          | Moved "I don't see my project" button outside the projects loop and adjusted padding. |
| `frontend/viewer/src/lib/components/editor/editor-root.svelte`                                    | Changed props destructuring to `let`; bound `ref` to root `<div>` for DOM access. |
| `frontend/viewer/src/lib/components/reorderer/reorderer-item-list.svelte`                         | Removed `cursor-pointer` class from dropdown menu items. |
| `frontend/viewer/src/lib/components/responsive-menu/responsive-menu-item.svelte`                  | Swapped `Button` for `DrawerClose` in fallback UI; updated prop types and event binding. |
| `frontend/viewer/src/lib/components/ui/button/index.ts`                                           | Reorganized and duplicated exports for clarity; added `Props` alias for `ButtonProps`. |
| `frontend/viewer/src/lib/components/ui/context-menu/context-menu-item.svelte`,<br>`dropdown-menu/dropdown-menu-item.svelte` | Changed cursor style from default to pointer for menu items. |
| `frontend/viewer/src/lib/components/ui/icon/icon.svelte`                                          | Made `<Icon>` polymorphic: renders `<span>` for icon classes or `<img>` for images; added `iconVariants` and updated prop types. |
| `frontend/viewer/src/lib/components/ui/icon/index.ts`                                             | Now exports `IconProps` and `iconVariants` alongside `Icon`. |
| `frontend/viewer/src/lib/components/ui/scroll-area/scroll-area.svelte`                            | Added bindable `viewportRef` prop for accessing the viewport DOM element. |
| `frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte`,<br>`ItemListItem.svelte`      | Added or adjusted `cursor-pointer` class on dropdown items; wrapped `Link` with snippet for prop spreading. |
| `frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte`                          | Enhanced highlighted entity state with autofocus flag; improved focus logic and added `ref` prop. |
| `frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte`,<br>`ExampleEditorPrimitive.svelte`,<br>`SenseEditorPrimitive.svelte` | Added `editor-primitive` class to editor grid containers. |
| `frontend/viewer/src/lib/utils/tabbable.ts`                                                       | Added `findFirstTabbable` utility to locate the first tabbable element in a container. |
| `frontend/viewer/src/project/ProjectDropdown.svelte`                                              | Centralized project icon rendering with a reusable snippet; adjusted empty state rendering. |
| `frontend/viewer/src/project/browse/BrowseView.svelte`                                            | Replaced `Badge` component with a button styled via `badgeVariants` for sort toggle. |
| `frontend/viewer/src/project/browse/EntriesList.svelte`                                           | Added 300ms debounce to entries resource fetch. |
| `frontend/viewer/src/project/browse/EntryMenu.svelte`                                             | Made the "Open in FieldWorks" menu item always display if the feature is enabled. |
| `frontend/viewer/src/project/browse/EntryView.svelte`                                             | Added focus and scroll-to-top logic on entry load using new refs and `findFirstTabbable`; bound refs to `ScrollArea` and `EntryEditor`. |
| `frontend/viewer/src/project/browse/SearchFilter.svelte`                                          | Used `mergeProps` to dynamically compose props for the `Toggle` component in filter UI. |

## Poem

> In the warren of code, a new path appears,  
> With icons that shimmer and focus that steers.  
> A tabbable hop, a pointery dance,  
> Editors now twinkle with every glance.  
> Menus feel snappier, buttons more bright—  
> This rabbit approves: the UI feels right!  
> 🐇✨

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=sillsdev/languageforge-lexbox&utm_content=1693):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-21T14:57:50Z
body: ## UI unit Tests
12 tests   12 :white_check_mark:  0s :stopwatch:
 4 suites   0 :zzz:
 1 files     0 :x:

Results for commit 8f5e9b01.

[test-results]:data:application/gzip;base64,H4sIADkiL2gC/12MzQ6DIBAGX8Vw7mElQqEvY3CBZFOVBuFk+u6l9A9725n9MjvzNLuNXbr+1LEtU6owFLA5mkRhLQgFyyPVHf/AuGXEP3OlW7sfvaH5IFyMIb5NzOu3+LwPwZf49So3ucptDcOyUCrAlBdOT9BrMRmruJHWAzg+KDRSnFELAJTaWXZ/ANbzL+H/AAAA

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-21T14:59:25Z
body: ## C# Unit Tests
116 tests  ±0   116 :white_check_mark: ±0   10s :stopwatch: ±0s
 17 suites ±0     0 :zzz: ±0 
  1 files   ±0     0 :x: ±0 

Results for commit 8f5e9b01. ± Comparison against base commit 7040f153.

[test-results]:data:application/gzip;base64,H4sIALYiL2gC/13MTQ6DIBCG4asY1l2MpiD0MgYHSCZVafhZNb17EduqXT7fZN4nczTZyG5Ne2lYzJQ29EUmB53IL6uhuJxSPbbiqyFmxP/pTo8y/T4Gp2k6DTYEHz5LyMveXHFObsterD4Eq4899PNMqYBJx60aoVV81EZ2WhgHYLurRC14j4oDoFDWsNcbpQa9lwUBAAA=

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: hahn-kev
created_at: 2025-05-22T04:43:00Z
body: we're fixing nitpicks so here's one:
![image](https://github.com/user-attachments/assets/ae6438b6-d11f-44e5-ae92-cf1be4957bc2)
click the search input, and reverse tab, we probably shouldn't have 2 focus rings, the same thing happens when I tab to the filter icon.
author_association: COLLABORATOR