Skip to content
Draft
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
3 changes: 0 additions & 3 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"devalue": "^5.6.1",
"electron-serve": "^3.0.0",
"electron-squirrel-startup": "^1.0.1",
"embla-carousel-auto-height": "^8.6.0",
"embla-carousel-svelte": "^8.6.0",
"exif-parser": "^0.1.12",
"fetch-progress": "git+https://github.com/gwennlbh/fetch-progress#explicit-import-extensions",
Expand Down Expand Up @@ -1285,8 +1284,6 @@

"embla-carousel": ["[email protected]", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],

"embla-carousel-auto-height": ["[email protected]", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ=="],

"embla-carousel-reactive-utils": ["[email protected]", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="],

"embla-carousel-svelte": ["[email protected]", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "svelte": "^3.49.0 || ^4.0.0 || ^5.0.0" } }, "sha512-ZDsKk8Sdv+AUTygMYcwZjfRd1DTh+JSUzxkOo8b9iKAkYjg+39mzbY/lwHsE3jXSpKxdKWS69hPSNuzlOGtR2Q=="],
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@
"devalue": "^5.6.1",
"electron-serve": "^3.0.0",
"electron-squirrel-startup": "^1.0.1",
"embla-carousel-auto-height": "^8.6.0",
"embla-carousel-svelte": "^8.6.0",
"exif-parser": "^0.1.12",
"fetch-progress": "git+https://github.com/gwennlbh/fetch-progress#explicit-import-extensions",
Expand Down
9 changes: 5 additions & 4 deletions src/lib/ButtonSecondary.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ Available CSS variables:
* @property {undefined | ((e: MouseEvent, signals: { loadingStarted: () => void, loadingEnded: () => void }) => Promise<void> |void)} onclick
* @property {boolean} [disabled=false]
* @property {boolean} [tight=false] limit the height of the button
* @property {string} [help]
* @property {Parameters<typeof tooltip>[1]} [help]
* @property {string} [keyboard] keyboard shortcut hint to display
* @property {string|undefined} [testid] add a data-testid attribute to the button
* @property {boolean} [aria-pressed]
* @property {boolean |"always"} [loading] show a loading state while the onlick handler is running. set to "always" to always show the loading state.
* @property {boolean} [danger=false] use a red color scheme for dangerous actions
* @property {boolean} [submits=false] if true, the button acts as a submit button in a form context
* @property {string} [aria-label] accessible label for the button
*/
</script>

Expand All @@ -49,7 +50,7 @@ Available CSS variables:
testid,
loading = false,
tight = false,
'aria-pressed': ariaPressed
...aria
} = $props();

let isLoading = $state(false);
Expand All @@ -60,7 +61,7 @@ Available CSS variables:
disabled={disabled || isLoading}
class:tight
class:danger
aria-pressed={ariaPressed}
{...aria}
onclick={async (e) => {
if (!onclick) return;

Expand All @@ -81,7 +82,7 @@ Available CSS variables:
if (loading) isLoading = false;
}
}}
use:tooltip={help ? { text: help, keyboard } : undefined}
use:tooltip={typeof help === 'string' && keyboard ? { text: help, keyboard } : help}
data-testid={testid || undefined}
>
{#if isLoading}
Expand Down
3 changes: 3 additions & 0 deletions src/lib/Card.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Available CSS variables:
* @type {object}
* @property {import('svelte').Snippet} [children]
* @property {(e: MouseEvent) => void} [onclick]
* @property {() => void} [ondoubleclick]
* @property {'article' | 'li' | 'div'} [tag=article] - HTML tag to use for the card container
* @property {string} [tooltip] - Tooltip text to show on hover (only if clickable)
* @property {string} [testid]
Expand All @@ -30,6 +31,7 @@ Available CSS variables:
const {
children = undefined,
onclick,
ondoubleclick,
tag = 'article',
tooltip: tooltipText,
testid,
Expand All @@ -47,6 +49,7 @@ Available CSS variables:
data-testid={testid}
use:tooltip={clickable && tooltipText ? tooltipText : undefined}
class="card"
ondblclick={ondoubleclick}
onclick={async (e) => {
try {
loading = true;
Expand Down
3 changes: 3 additions & 0 deletions src/lib/CardMedia.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @type {object}
* @property {(e: MouseEvent, set: (props: { status?: Status, loadingStatusText?: string }) => void) => void} [onclick]
* @property {() => void} [onstacksizeclick]
* @property {() => void} [ondoubleclick]
* @property {() => void} [ondelete]
* @property {() => void} [onretry]
* @property {string | undefined} [tooltip] tooltip to show
Expand Down Expand Up @@ -46,6 +47,7 @@
/** @type {Props & Omit<Record<string, unknown>, keyof Props>}*/
let {
onclick,
ondoubleclick,
onstacksizeclick,
ondelete,
onretry,
Expand Down Expand Up @@ -95,6 +97,7 @@
<!-- use () => {} instead of undefined so that the hover/focus styles still apply -->
<Card
tag="div"
{ondoubleclick}
onclick={(e) => {
if (loading || errored) return;
if (!(e instanceof MouseEvent)) return;
Expand Down
78 changes: 68 additions & 10 deletions src/lib/Carousel.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<script module lang="ts">
export interface Props<T = unknown> {
items: T[];
item: Snippet<[T]>;
scrollers?: {
next: () => void;
prev: () => void;
};
}
</script>

<script lang="ts" generics="T">
import type { EmblaCarouselType } from 'embla-carousel';
import AutoHeight from 'embla-carousel-auto-height';
import embla from 'embla-carousel-svelte';
import type { Snippet } from 'svelte';

Expand All @@ -9,21 +19,26 @@

import ButtonIcon from './ButtonIcon.svelte';

interface Props {
items: T[];
item: Snippet<[T]>;
}

const { item: itemSnippet, items }: Props = $props();
let { item: itemSnippet, items, scrollers = $bindable() }: Props<T> = $props();

let canScrollNext = $state(true);
let canScrollPrev = $state(false);
let currentIndex = $state(0);
let carousel: EmblaCarouselType | undefined = $state();

$effect(() => {
if (!carousel) return;
scrollers = {
next: () => carousel?.scrollNext(),
prev: () => carousel?.scrollPrev()
};
});

$effect(() => {
function onSettle() {
canScrollPrev = carousel?.canScrollPrev() ?? false;
canScrollNext = carousel?.canScrollNext() ?? false;
currentIndex = carousel?.selectedScrollSnap() ?? 0;
}

carousel?.on('settle', onSettle);
Expand All @@ -45,6 +60,18 @@
>
<IconPrev />
</ButtonIcon>
<div class="dots">
{#each items as _, i (i)}
<button
class="dot"
aria-label="Aller à l'image {i + 1}"
class:active={currentIndex === i}
onclick={() => {
carousel?.scrollTo(i);
}}
></button>
{/each}
</div>
<ButtonIcon
help="Image suivante"
class="embla_next"
Expand All @@ -60,8 +87,7 @@
<div
class="embla__viewport"
use:embla={{
options: { duration: 15 },
plugins: [AutoHeight()]
options: { duration: 15 }
}}
onemblaInit={(event: CustomEvent) => {
carousel = event.detail;
Expand All @@ -81,27 +107,59 @@
<style>
.embla {
flex-shrink: 0;
position: relative;
height: 100%;
}

.embla__viewport {
overflow: hidden;
height: 100%;
}

.embla__container {
align-items: flex-start;
display: flex;
height: 100%;
}

.embla__slide {
flex: 0 0 100%;
min-width: 0;
height: 100%;
}

nav {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
gap: 1rem;
position: absolute;
bottom: 0;
padding-bottom: 1em;
padding-top: 2em;
z-index: 10;
width: 100%;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100%);
}

.dots {
display: flex;
align-items: center;
justify-content: center;
gap: 0.7rem;
}

.dots button {
width: 0.7rem;
height: 0.7rem;
border-radius: 50%;
background-color: white;
border: none;
padding: 0;
cursor: pointer;
}

.dots button:not(:hover):not(:focus-visible):not(.active) {
opacity: 0.5;
}
</style>
19 changes: 15 additions & 4 deletions src/lib/ConfidencePercentage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

/**
* @typedef {object} Props
* @property {number} value
* @property {number|undefined} value if undefined, the element shows a fallback "--%" text
* @property {(percent: `${number}%`) => string} [tooltip] - text to show when hovering the percentage
* @property {import('svelte').Snippet} [children] optional content to put before the percentage, useful to make it under the tooltip activation area
*/
Expand All @@ -18,22 +18,33 @@
} = $props();

const color = $derived(
gradientedColor(value, 'fg-error', 'fg-warning', 'fg-neutral', 'fg-success')
value ? gradientedColor(value, 'fg-error', 'fg-warning', 'fg-neutral', 'fg-success') : ''
);

const decimals = $derived(value && Number((value * 100).toFixed(1)) < 1 ? 1 : 0);
</script>

{#if value && value > 0 && value < 1}
<span class="confidence" use:tooltip={help(percent(value, 4))}>
{@render children?.()}
<code class="confidence" style:color>
{percent(value, value < 0.01 ? 1 : 0, { pad: 'nbsp' })}
<code class="figure" style:color>
{percent(value, decimals, { pad: 'nbsp', length: 4 })}
</code>
</span>
{:else}
<span class="confidence empty">
{@render children?.()}
<code class="figure">&nbsp;--%</code>
</span>
{/if}

<style>
span {
display: inline-flex;
align-items: center;
}

code {
white-space: pre;
}
</style>
24 changes: 23 additions & 1 deletion src/lib/CroppedImg.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
* @property {string} [class] - Additional classes to apply to the div wrapper.
* @property {import('./BoundingBoxes.svelte').Rect} box - The bounding box to crop the image to, in relative (0-1), top-left coordinates.
* @property {boolean} [blurfill] fill empty space with a blurred version of the image.
* @property {boolean} [background] show the full image as the background (darkened)
*/

/** @type {Props & Record<string, unknown>} */
const { src, box, blurfill, class: klass = '', ...rest } = $props();
const { src, box, blurfill, background, class: klass = '', ...rest } = $props();
const corners = $derived(toCorners(box));

const aspectRatio = $derived(box.width / box.height);
Expand Down Expand Up @@ -49,6 +50,20 @@
{#if blurfill}
<img data-is-blur="true" class="blur" {src} alt="" aria-hidden="true" />
{/if}

{#if background}
<img
style:transform-origin={percents(corners.topleft)}
style:translate={percents(translate)}
style:scale="{scale * 100}%"
{src}
alt=""
aria-hidden="true"
data-is-background="true"
class="background"
/>
{/if}

<img
style:transform-origin={percents(corners.topleft)}
style:translate={percents(translate)}
Expand All @@ -70,6 +85,7 @@
picture {
overflow: hidden;
position: relative;
display: block;
}

img {
Expand All @@ -85,6 +101,12 @@
scale: 1.5;
}

img.background {
position: absolute;
inset: 0;
filter: brightness(0.25);
}

/**
Hugely scaled-up image causes global body overflow even though <picture> is set to overflow: hidden.
*/
Expand Down
33 changes: 33 additions & 0 deletions src/lib/LearnMoreLink.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import IconArrowRight from '~icons/ri/arrow-right-line';

const { href }: { href: string } = $props();
</script>

<a {href} rel="external" target="_blank" class="learn-more">
<IconArrowRight />
<div class="text">
<span>En savoir plus</span>
<code class="domain">{new URL(href).hostname}</code>
</div>
</a>

<style>
.learn-more {
display: flex;
align-items: center;
color: var(--fg-primary);
text-decoration: none;
gap: 1em;
}

.text {
display: flex;
flex-direction: column;
}

.domain {
font-size: 0.8em;
color: var(--gay);
}
</style>
Loading
Loading