Skip to content

Commit 7a4b9f5

Browse files
myieyehahn-kev
andauthored
Finalize shadcn buttons (#1670)
* Fix delete dialog close detection * Wire up sidebar and hide unimplemented content * Migrate home-page app-bar buttons to shadcn * Use function instead of prop to open troubleshooting dialog * Fix broken enter animation due to troubleshooting dialog resize after open. * Introduce responsive-menu for entry-menu * Add Open in FieldWorks button * Fix logo doesn't respond to dark-mode * Condense home-view app-bar * Make radio buttons bigger on mobile * only show Open In Flex button when supported --------- Co-authored-by: Kevin Hahn <kevin_hahn@sil.org>
1 parent 533b687 commit 7a4b9f5

22 files changed

Lines changed: 515 additions & 295 deletions

backend/FwLite/FwLiteWeb/Routes/FwIntegrationRoutes.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static IEndpointConventionBuilder MapFwIntegrationRoutes(this WebApplicat
2020
});
2121
return operation;
2222
});
23-
group.MapGet("/open/entry/{id}",
23+
group.MapGet("/link/entry/{id}",
2424
async ([FromServices] FwDataProjectContext context,
2525
[FromServices] IHubContext<FwDataMiniLcmHub, ILexboxHubClient> hubContext,
2626
[FromServices] FwDataFactory factory,
@@ -29,8 +29,8 @@ public static IEndpointConventionBuilder MapFwIntegrationRoutes(this WebApplicat
2929
if (context.Project is null) return Results.BadRequest("No project is set in the context");
3030
await hubContext.Clients.Group(context.Project.Name).OnProjectClosed(CloseReason.Locked);
3131
factory.CloseProject(context.Project);
32-
//need to use redirect as a way to not trigger flex until after we have closed the project
33-
return Results.Redirect(FwLink.ToEntry(id, context.Project.Name));
32+
var link = FwLink.ToEntry(id, context.Project.Name);
33+
return Results.Text(link, "text/plain");
3434
});
3535
return group;
3636
}

frontend/viewer/src/home/HomeView.svelte

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
mdiBookArrowLeftOutline,
44
mdiBookEditOutline,
55
mdiBookPlusOutline,
6-
mdiChatQuestion,
76
mdiChevronRight,
87
mdiDelete,
9-
mdiFaceAgent,
10-
mdiRefresh,
118
mdiTestTube,
129
} from '@mdi/js';
1310
import {AppBar, Button as UxButton, ListItem, TextField} from 'svelte-ux';
@@ -30,6 +27,8 @@
3027
import type {IProjectModel} from '$lib/dotnet-types';
3128
import ThemePicker from '$lib/ThemePicker.svelte';
3229
import {Button} from '$lib/components/ui/button';
30+
import {mode} from 'mode-watcher';
31+
import * as ResponsiveMenu from '$lib/components/responsive-menu';
3332
3433
const projectsService = useProjectsService();
3534
const importFwdataService = useImportFwdataService();
@@ -99,45 +98,39 @@
9998
}
10099
101100
const supportsTroubleshooting = useTroubleshootingService();
102-
let showTroubleshooting = false;
101+
let troubleshootDialog: TroubleshootDialog | undefined;
103102
104103
</script>
105104

106-
<AppBar title={$t`Dictionaries`} class="bg-primary/25 min-h-12 shadow-md justify-between" menuIcon={null}>
105+
<AppBar title={$t`Dictionaries`} class="bg-primary/15 min-h-12 shadow-md justify-between" menuIcon={null}>
107106
<div slot="title" class="text-lg flex gap-2 items-center">
108-
<picture>
109-
<source srcset={logoLight} media="(prefers-color-scheme: dark)" />
110-
<source srcset={logoDark} media="(prefers-color-scheme: light)" />
111-
<img src={logoDark} alt={$t`Lexbox logo`} class="h-6" />
112-
</picture>
107+
<img src={mode.current === 'dark' ? logoLight : logoDark} alt={$t`Lexbox logo`} class="h-6 shrink-0" />
113108
<h3>{$t`Dictionaries`}</h3>
114109
</div>
115-
<div slot="actions" class="flex gap-2">
116-
<UxButton href={fwLiteConfig.feedbackUrl}
117-
target="_blank"
118-
size="sm"
119-
variant="outline"
120-
icon={mdiChatQuestion}
121-
classes={{root: 'hover:bg-muted'}}>
122-
{$t`Feedback`}
123-
</UxButton>
124-
{#if supportsTroubleshooting}
125-
<UxButton
126-
size="sm"
127-
variant="outline"
128-
classes={{root: 'hover:bg-muted'}}
129-
icon={mdiFaceAgent}
130-
title={$t`Troubleshoot`}
131-
iconOnly={false}
132-
on:click={() => (showTroubleshooting = !showTroubleshooting)}
133-
></UxButton>
134-
<TroubleshootDialog bind:open={showTroubleshooting} />
135-
{/if}
110+
<div slot="actions" class="flex">
136111
<DevContent>
137-
<UxButton href="/sandbox" size="sm" variant="outline" icon={mdiTestTube} class="hover:bg-muted">Sandbox</UxButton>
112+
<Button href="/sandbox" variant="ghost" size="icon" icon="i-mdi-test-tube" />
138113
</DevContent>
139114
<LocalizationPicker/>
140115
<ThemePicker />
116+
<ResponsiveMenu.Root>
117+
<ResponsiveMenu.Trigger />
118+
<ResponsiveMenu.Content>
119+
<ResponsiveMenu.Item href={fwLiteConfig.feedbackUrl} icon="i-mdi-chat-question">
120+
{$t`Feedback`}
121+
</ResponsiveMenu.Item>
122+
{#if supportsTroubleshooting}
123+
<ResponsiveMenu.Item
124+
icon="i-mdi-face-agent"
125+
onSelect={() => troubleshootDialog?.open()}>
126+
{$t`Troubleshoot`}
127+
</ResponsiveMenu.Item>
128+
{/if}
129+
</ResponsiveMenu.Content>
130+
</ResponsiveMenu.Root>
131+
{#if supportsTroubleshooting}
132+
<TroubleshootDialog bind:this={troubleshootDialog} />
133+
{/if}
141134
</div>
142135
</AppBar>
143136
<div class="mx-auto md:w-full md:py-4 max-w-2xl">
Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,57 @@
11
<script lang="ts">
22
import flexLogo from '$lib/assets/flex-logo.png';
33
import {AppNotification} from '$lib/notifications/notifications';
4-
import {useAppLauncher} from '$lib/services/service-provider';
5-
import {Button} from 'svelte-ux';
6-
import {useProjectViewState} from '$lib/views/project-view-state-service';
4+
import {useAppLauncherService} from './services/app-launcher-service';
5+
import {useProjectContext} from './project-context.svelte';
6+
import type {IEntry} from './dotnet-types';
7+
import {buttonVariants, type ButtonProps} from './components/ui/button';
8+
import {t} from 'svelte-i18n-lingui';
9+
import {mergeProps} from 'bits-ui';
10+
import {cn} from './utils';
711
8-
const projectViewState = useProjectViewState();
9-
const appLauncher = useAppLauncher();
12+
type Props = {
13+
entry: IEntry
14+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
15+
} & ButtonProps;
1016
11-
export let show = false;
12-
export let entryId: string;
13-
export let projectName: string;
17+
const {
18+
entry,
19+
class: className,
20+
...rest
21+
}: Props = $props();
1422
15-
async function openInFlex(e: Event) {
23+
const appLauncher = useAppLauncherService();
24+
const projectContext = useProjectContext();
25+
26+
async function openInFlex() {
27+
let opened = false;
1628
if (appLauncher) {
17-
e.preventDefault();//don't follow the link
18-
await appLauncher.openInFieldWorks(entryId, projectName);
29+
opened = await appLauncher.openInFieldWorks(entry.id, projectContext.projectName);
30+
} else {
31+
// a 302 redirect to the protocol handler works, but sends the user to the home page 🤷
32+
const fieldWorksUrlResponse = await fetch(`/api/fw/${projectContext.projectName}/link/entry/${entry.id}`);
33+
if (fieldWorksUrlResponse.ok) {
34+
opened = true;
35+
window.location.href = await fieldWorksUrlResponse.text();
36+
}
37+
}
38+
if (opened) {
39+
AppNotification.displayAction($t`This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen.`, 'warning', {
40+
label: $t`Reopen`,
41+
callback: () => window.location.reload()
42+
});
43+
} else {
44+
AppNotification.display($t`Unable to open in FieldWorks`, 'error');
1945
}
20-
AppNotification.displayAction('The project is open in FieldWorks. Please close it to reopen.', 'warning', {
21-
label: 'Open',
22-
callback: () => window.location.reload()
23-
});
2446
}
47+
48+
const mergedProps = $derived(mergeProps({
49+
onclick: openInFlex,
50+
}, rest));
2551
</script>
26-
{#if show}
52+
2753
<!--button must be a link otherwise it won't follow the redirect to a protocol handler-->
28-
<Button
29-
href={`/api/fw/${projectName}/open/entry/${entryId}`}
30-
on:click={openInFlex}
31-
variant="fill-light"
32-
color="info"
33-
size="sm">
34-
<img src={flexLogo} alt="FieldWorks logo" class="h-6 max-w-fit"/>
35-
<div class="sm-form:hidden" class:hidden={$projectViewState.rightToolbarCollapsed}>
36-
Open in FieldWorks
37-
</div>
38-
</Button>
39-
{/if}
54+
<button class={cn(buttonVariants({ variant: 'ghost'}), className)} {...mergedProps}>
55+
<img src={flexLogo} alt={$t`FieldWorks logo`} class="h-6 max-w-fit"/>
56+
{$t`Open in FieldWorks`}
57+
</button>

frontend/viewer/src/lib/components/reorderer/reorderer.svelte

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
<script lang="ts" module>
2-
import type {Snippet} from 'svelte';
32
import {Context} from 'runed';
43
5-
export type ReordererProps<T> = {
6-
item: T;
7-
items: T[];
8-
direction?: 'horizontal' | 'vertical';
9-
getDisplayName: (item: T) => string | undefined;
10-
onchange?: (value: T[], fromIndex: number, toIndex: number) => void;
11-
children?: Snippet<[{first: boolean, last: boolean}]>;
12-
};
13-
144
type ReordererRootStateProps<T = unknown> = {
155
readonly item: T;
166
items: T[];
@@ -42,10 +32,20 @@
4232
</script>
4333

4434
<script lang="ts" generics="T">
35+
import type {Snippet} from 'svelte';
4536
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
4637
import ReordererTrigger from './reorderer-trigger.svelte';
4738
import ReordererItemList from './reorderer-item-list.svelte';
4839
40+
type ReordererProps<T> = {
41+
item: T;
42+
items: T[];
43+
direction?: 'horizontal' | 'vertical';
44+
getDisplayName: (item: T) => string | undefined;
45+
onchange?: (value: T[], fromIndex: number, toIndex: number) => void;
46+
children?: Snippet<[{first: boolean, last: boolean}]>;
47+
};
48+
4949
let {
5050
item,
5151
items = $bindable(),
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Content from './responsive-menu-content.svelte';
2+
import Item from './responsive-menu-item.svelte';
3+
import Root from './responsive-menu.svelte';
4+
import Trigger from './responsive-menu-trigger.svelte';
5+
6+
export {
7+
Root,
8+
Item,
9+
Trigger,
10+
Content,
11+
//
12+
Root as ResponsiveMenu,
13+
Item as ResponsiveMenuItem,
14+
Trigger as ResponsiveMenuTrigger,
15+
Content as ResponsiveMenuContent,
16+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<script lang="ts">
2+
import { Icon } from '$lib/components/ui/icon';
3+
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
4+
import {buttonVariants} from '$lib/components/ui/button/button.svelte';
5+
import {useResponsiveMenuContent} from './responsive-menu.svelte';
6+
import {ContextMenuContent} from '../ui/context-menu';
7+
import {DropdownMenuContent} from '../ui/dropdown-menu';
8+
import * as Drawer from '../ui/drawer';
9+
import type {Snippet} from 'svelte';
10+
import type {ContextMenuContentProps, DropdownMenuContentProps} from 'bits-ui';
11+
import type {DrawerContentProps} from 'vaul-svelte';
12+
13+
type Props = {
14+
children?: Snippet;
15+
} & ContextMenuContentProps & DropdownMenuContentProps & DrawerContentProps;
16+
17+
let {
18+
children,
19+
ref = $bindable(null),
20+
...rest
21+
}: Props = $props();
22+
23+
const state = useResponsiveMenuContent();
24+
</script>
25+
26+
{#if state.contextMenu}
27+
<ContextMenuContent {...rest} bind:ref>
28+
{@render children?.()}
29+
</ContextMenuContent>
30+
{:else if !IsMobile.value}
31+
<DropdownMenuContent align="end" {...rest} bind:ref>
32+
{@render children?.()}
33+
</DropdownMenuContent>
34+
{:else}
35+
<Drawer.Content {...rest} bind:ref>
36+
<div class="mx-auto w-full max-w-sm p-4">
37+
<Drawer.Close class={buttonVariants({ variant: 'ghost', size: 'icon', class: 'absolute top-4 right-4 z-10' })}>
38+
<Icon icon="i-mdi-close" />
39+
</Drawer.Close>
40+
<Drawer.Header class="justify-items-end">
41+
</Drawer.Header>
42+
<div class="flex flex-col gap-2">
43+
{@render children?.()}
44+
</div>
45+
</div>
46+
</Drawer.Content>
47+
{/if}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<script lang="ts">
2+
import { Icon } from '$lib/components/ui/icon';
3+
import { type IconClass } from '$lib/icon-class';
4+
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
5+
import * as ContextMenu from '$lib/components/ui/context-menu';
6+
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
7+
import Button, {buttonVariants} from '$lib/components/ui/button/button.svelte';
8+
import type {Snippet} from 'svelte';
9+
import {useResponsiveMenuItemList} from './responsive-menu.svelte';
10+
import type {ButtonProps} from 'node_modules/bits-ui/dist/bits/toolbar/exports';
11+
import {mergeProps, type ContextMenuItemProps} from 'bits-ui';
12+
import {cn} from '$lib/utils';
13+
14+
type Props = {
15+
children?: Snippet;
16+
icon?: IconClass;
17+
onSelect?: () => void;
18+
href?: string;
19+
} & ContextMenuItemProps & ButtonProps;
20+
21+
let {
22+
icon,
23+
onSelect,
24+
children,
25+
ref = $bindable(null),
26+
class: className,
27+
...rest
28+
}: Props = $props();
29+
30+
const state = useResponsiveMenuItemList();
31+
32+
const buttonProps = $derived({
33+
variant: 'ghost',
34+
class: cn(buttonVariants({ variant: 'ghost', class: 'w-full justify-start gap-2' }), className),
35+
onclick: onSelect,
36+
} as const);
37+
38+
const mergedProps = $derived(mergeProps(buttonProps, rest));
39+
</script>
40+
41+
{#snippet content()}
42+
{#if icon}
43+
<Icon {icon} />
44+
{/if}
45+
{@render children?.()}
46+
{/snippet}
47+
48+
{#snippet anchorChild({ props }: { props: Record<string, unknown> })}
49+
<a {...props}>
50+
{@render content()}
51+
</a>
52+
{/snippet}
53+
54+
{#if state.contextMenu}
55+
<ContextMenu.Item class={cn('cursor-pointer gap-2 w-full', className)} onclick={onSelect} bind:ref
56+
child={rest.href ? anchorChild : undefined} {...rest}>
57+
{@render content()}
58+
</ContextMenu.Item>
59+
{:else if !IsMobile.value}
60+
<DropdownMenu.Item class={cn('cursor-pointer gap-2 w-full', className)} {onSelect} bind:ref
61+
child={rest.href ? anchorChild : undefined} {...rest}>
62+
{@render content()}
63+
</DropdownMenu.Item>
64+
{:else if rest.child}
65+
{@render rest.child({ props: mergedProps})}
66+
{:else}
67+
<Button
68+
{...buttonProps}
69+
bind:ref>
70+
{@render content()}
71+
</Button>
72+
{/if}

0 commit comments

Comments
 (0)