Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ For formatted values you can do this:
Add a new component with this:

```bash
pnpx shadcn-svelte@next add context-menu
npx shadcn-svelte@next add context-menu
```
8 changes: 4 additions & 4 deletions frontend/viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@
"tailwindcss": "catalog:",
"tailwindcss-animate": "^1.0.7",
"tslib": "catalog:",
"typescript-eslint": "catalog:",
"tw-colors": "^3.3.2",
"typescript": "catalog:",
"typescript-eslint": "catalog:",
"vaul-svelte": "1.0.0-next.6",
"vite": "catalog:",
"vite-plugin-webfont-dl": "^3.11.1",
Expand All @@ -85,15 +85,15 @@
"@vitejs/plugin-basic-ssl": "catalog:"
},
"dependencies": {
"@ffmpeg/core": "0.12.10",
"@ffmpeg/ffmpeg": "0.12.15",
"@ffmpeg/util": "0.12.2",
"@formatjs/intl-durationformat": "^0.7.4",
"@lingui/core": "^5.3.3",
"@microsoft/dotnet-js-interop": "^8.0.0",
"@microsoft/signalr": "^8.0.7",
"autoprefixer": "^10.4.21",
"fast-json-patch": "^3.1.1",
"@ffmpeg/ffmpeg": "0.12.15",
"@ffmpeg/util": "0.12.2",
"@ffmpeg/core": "0.12.10",
"jsdom": "^26.1.0",
"just-throttle": "^4.2.0",
"postcss": "catalog:",
Expand Down
4 changes: 4 additions & 0 deletions frontend/viewer/src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
.app {
@apply bg-background text-foreground;
}

.loading-text {
@apply bg-shimmer animate-shimmer rounded-md text-transparent select-none pointer-events-none;
}
}

.font-inter {
Expand Down
2 changes: 1 addition & 1 deletion frontend/viewer/src/css-breakpoints.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// used in the is-mobile.svelte hook
// used in the is-mobile.svelte hook and for the tailwind md breakpoint
export const MOBILE_BREAKPOINT = 768;
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,35 @@
import * as Dialog from '$lib/components/ui/dialog';
import * as Drawer from '$lib/components/ui/drawer';
import { buttonVariants } from '$lib/components/ui/button';
import type {PopoverTriggerProps, WithChildren} from 'bits-ui';
import type {PopoverTriggerProps, WithChildren, WithoutChildren} from 'bits-ui';
import {Icon} from '../ui/icon';
import type {ComponentProps} from 'svelte';
import {cn} from '$lib/utils';

type TriggerSnippet = PopoverTriggerProps['child'];
type ContentProps = WithoutChildren<ComponentProps<typeof Dialog.Content>>;
type Props = ComponentProps<typeof Drawer.Root> & {
open?: boolean,
title: string,
trigger?: TriggerSnippet,
contentProps?: ContentProps,
};


let {
open = $bindable(false),
children,
title,
trigger,
contentProps,
...rest
}: WithChildren<Props> = $props();
</script>

{#if !IsMobile.value}
<Dialog.Root bind:open {...rest}>
<Dialog.Trigger child={trigger} />
<Dialog.Content class="min-h-auto">
<Dialog.Content {...contentProps} class={cn('min-h-auto', contentProps?.class)}>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
Expand All @@ -36,7 +41,7 @@
{:else}
<Drawer.Root bind:open {...rest}>
<Drawer.Trigger child={trigger} />
<Drawer.Content>
<Drawer.Content {...contentProps}>
<Drawer.Close class={buttonVariants({ variant: 'ghost', size: 'icon', class: 'absolute top-4 right-4 z-10' })}>
<Icon icon="i-mdi-close" />
</Drawer.Close>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import { Accordion as AccordionPrimitive, type WithoutChild } from 'bits-ui';
import { cn } from '$lib/utils.js';

let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
</script>

<AccordionPrimitive.Content
bind:ref
class={cn(
'data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm transition-all',
className,
)}
{...restProps}
>
<div class="pb-4 pt-0">
{@render children?.()}
</div>
</AccordionPrimitive.Content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';

let { ref = $bindable(null), class: className, ...restProps }: AccordionPrimitive.ItemProps = $props();
</script>

<AccordionPrimitive.Item bind:ref class={cn('border-b', className)} {...restProps} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
import { Accordion as AccordionPrimitive, type WithoutChild } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { Icon } from '../icon';

let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
level?: AccordionPrimitive.HeaderProps['level'];
} = $props();
</script>

<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
bind:ref
class={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>.i-mdi-chevron-down]:rotate-180',
className,
)}
{...restProps}
>
{@render children?.()}
<Icon icon="i-mdi-chevron-down" class="size-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
18 changes: 18 additions & 0 deletions frontend/viewer/src/lib/components/ui/accordion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Accordion as AccordionPrimitive} from 'bits-ui';
import Content from './accordion-content.svelte';
import Item from './accordion-item.svelte';
import Trigger from './accordion-trigger.svelte';
// eslint-disable-next-line @typescript-eslint/naming-convention
const Root = AccordionPrimitive.Root;

export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {locale} from 'svelte-i18n-lingui';

const currentLocale = fromStore(locale);
type Duration = Pick<Intl.DurationLike, 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'>;
export type SmallestUnit = 'hours' | 'minutes' | 'seconds' | 'milliseconds';

function limitDurationUnits(duration: Duration, maxUnits: number): Duration {
const units: (keyof Duration)[] = ['days', 'hours', 'minutes', 'seconds', 'milliseconds'];
Expand Down Expand Up @@ -52,17 +53,17 @@ export function formatDigitalDuration(value: Duration) {
});
}

export function formatDuration(value: Duration, smallestUnit?: 'hours' | 'minutes' | 'seconds' | 'milliseconds', options?: Intl.DurationFormatOptions, maxUnits?: number) {
export function formatDuration(value: Duration, smallestUnit?: SmallestUnit, options?: Intl.DurationFormatOptions, maxUnits?: number) {
const formatter = new Intl.DurationFormat(currentLocale.current, options);//has been polyfilled in main.ts
const normalized = normalizeDuration(value, smallestUnit);
const limitedDuration = maxUnits ? limitDurationUnits(normalized, maxUnits) : normalized;
return formatter.format(limitedDuration);
}

export function normalizeDuration(value: Duration, smallestUnit?: 'hours' | 'minutes' | 'seconds' | 'milliseconds'): Duration
export function normalizeDuration(value: Duration, smallestUnit?: SmallestUnit): Duration
export function normalizeDuration(value: Duration, smallestUnit: 'seconds'): Omit<Duration, 'milliseconds'>
export function normalizeDuration(value: Duration): Duration
export function normalizeDuration(value: Duration, smallestUnit?: 'hours' | 'minutes' | 'seconds' | 'milliseconds'): Duration {
export function normalizeDuration(value: Duration, smallestUnit?: SmallestUnit): Duration {
const msPerHour = 3_600_000;
const msPerMinute = 60_000;
const msPerSecond = 1_000;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {formatDuration} from './format-duration';
import {locale} from 'svelte-i18n-lingui';
import {SvelteDate} from 'svelte/reactivity';
import {formatDuration, type SmallestUnit} from './format-duration';
import {fromStore} from 'svelte/store';
import {gt} from 'svelte-i18n-lingui';
import {SvelteDate} from 'svelte/reactivity';
import {locale} from 'svelte-i18n-lingui';

const currentLocale = fromStore(locale);
type Config = {
defaultValue: string,
now: Date,
maxUnits?: number,
smallestUnit?: SmallestUnit,
}

export function formatRelativeDate(value: Date | string | undefined | null, options?: Intl.DurationFormatOptions, config: Config = {defaultValue: '', now: new SvelteDate()}): string {
Expand All @@ -20,7 +21,7 @@ export function formatRelativeDate(value: Date | string | undefined | null, opti
const isPast = diffMs < 0;
const absDiffMs = Math.abs(diffMs);

const duration = formatDuration({milliseconds: absDiffMs}, undefined, options, config.maxUnits);
const duration = formatDuration({milliseconds: absDiffMs}, config.smallestUnit, options, config.maxUnits);
if (!duration) return config.defaultValue;

return isPast ? gt`${duration} ago` : gt`in ${duration}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import type {HTMLAttributes} from 'svelte/elements';
import {SvelteDate} from 'svelte/reactivity';
import Icon from '../icon/icon.svelte';
import * as Popover from '../popover/index.js';
import * as Popover from '../popover';
import type {SmallestUnit} from './format-duration';

type Props = HTMLAttributes<HTMLTimeElement> & {
date: Date | string | undefined | null;
Expand All @@ -14,6 +15,8 @@
showActualDate?: boolean;
actualDateOptions?: Intl.DateTimeFormatOptions;
maxUnits?: number;
smallestUnit?: SmallestUnit,
loading?: boolean;
};

const {
Expand All @@ -24,6 +27,8 @@
showActualDate = false,
actualDateOptions,
maxUnits = 2,
smallestUnit = 'seconds',
loading,
...restProps
}: Props = $props();

Expand All @@ -50,7 +55,7 @@
});

const formattedRelativeDate = $derived.by(() => {
return formatRelativeDate(date, options, {defaultValue: defaultValue || '', now, maxUnits});
return formatRelativeDate(date, options, {defaultValue: defaultValue || '', now, maxUnits, smallestUnit});
});

const actualFormattedDate = $derived.by(() => {
Expand All @@ -60,10 +65,10 @@
</script>

{#if showActualDate && actualFormattedDate}
<span class="inline-flex items-center gap-1">
<time {...restProps}>{formattedRelativeDate}</time>
<span class:loading-text={loading} class="inline-flex items-center gap-1">
<Popover.Root>
<Popover.Trigger>
<time {...restProps}>{formattedRelativeDate}</time>
<Icon icon="i-mdi-information-outline" class="size-4 text-muted-foreground hover:text-foreground" />
</Popover.Trigger>
<Popover.Content class="w-auto">
Expand All @@ -73,5 +78,5 @@
</Popover.Root>
</span>
{:else}
<time {...restProps}>{formattedRelativeDate}</time>
<time class:loading-text={loading} {...restProps}>{formattedRelativeDate}</time>
{/if}
6 changes: 4 additions & 2 deletions frontend/viewer/src/lib/utils/url.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {createSubscriber, SvelteURL} from 'svelte/reactivity';
import {SvelteURL, createSubscriber} from 'svelte/reactivity';

import {queueHistoryChange} from './history';
import {useLocation} from 'svelte-routing';

Expand Down Expand Up @@ -48,7 +49,8 @@ export class QueryParamState {
if (this.config.replaceOnDefaultValue && isDefault) {
if (this.config.allowBack) {
if (!this.isOnTopOfHistoryStack()) {
console.warn(`${this.fullKey}: wanted to pop history, but not on top of history stack, ignoring, history entry not removed.`);
console.warn(`${this.fullKey}: wanted to pop history, but not on top of history stack. Pushing new state instead so change is applied.`);
history.pushState(null, '', currentUrl.href);
return;
}
//the last history event was pushed by us so we need to just go back otherwise the next back will do nothing
Expand Down
Loading
Loading