mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-11-15 16:10:38 +08:00
Redesign the entire app with a new UI kit. Typography, colors, icons, avatars, and border radii have all been updated for a more polished, consistent appearance that matches our other products like radicle-explorer. - Replace blockies-based avatars with a new DID-based avatar system - Move Settings and Create Issue into fullscreen modals - Change the default theme to follow the system theme - Remove window decorations on macOS for a cleaner, more native feel - Update the app icon - Fix popovers and tooltips Navigation: - Rewrite app navigation and layouts - Add a sidebar that gives you access to all your repos - Change the startup screen to open directly to the notification inbox - Add a getting started guide shown to new users on first launch Settings: - Add a setting to disable the notification badge - Add a code font setting Bug fixes: - Fix reactions and edits incorrectly targeting the root comment instead of the intended reply - Restrict issue/patch state, labels and assignees to delegates only
145 lines
3.3 KiB
Svelte
145 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import type { Placement } from "@floating-ui/dom";
|
|
import type { ComponentProps } from "svelte";
|
|
|
|
import {
|
|
autoUpdate,
|
|
computePosition,
|
|
flip,
|
|
offset,
|
|
shift,
|
|
} from "@floating-ui/dom";
|
|
import debounce from "lodash/debounce";
|
|
|
|
import { writeToClipboard } from "@app/lib/invoke";
|
|
import { portal } from "@app/lib/portal";
|
|
import { formatOid } from "@app/lib/utils";
|
|
|
|
import Icon from "@app/components/Icon.svelte";
|
|
|
|
let icon: ComponentProps<typeof Icon>["name"] = $state("copy");
|
|
const text = "Click to copy";
|
|
let tooltip = $state(text);
|
|
|
|
const restoreIcon = debounce(() => {
|
|
icon = "copy";
|
|
tooltip = text;
|
|
}, 1000);
|
|
|
|
async function copy() {
|
|
await writeToClipboard(clipboard);
|
|
icon = "checkmark";
|
|
tooltip = "Copied to clipboard";
|
|
restoreIcon();
|
|
}
|
|
|
|
let visible: boolean = $state(false);
|
|
let anchorEl: HTMLDivElement | undefined = $state();
|
|
let floatingEl: HTMLDivElement | undefined = $state();
|
|
|
|
interface Props {
|
|
ariaLabel?: string;
|
|
clipboard: string;
|
|
id: string;
|
|
placement?: Placement;
|
|
shorten?: boolean;
|
|
}
|
|
|
|
const {
|
|
ariaLabel,
|
|
clipboard,
|
|
id,
|
|
placement = "top-start",
|
|
shorten = true,
|
|
}: Props = $props();
|
|
|
|
const setVisible = debounce((value: boolean) => {
|
|
visible = value;
|
|
}, 50);
|
|
|
|
$effect(() => {
|
|
// Re-run when tooltip text changes so the wider "Copied to clipboard"
|
|
// text is repositioned and doesn't overflow the viewport edge.
|
|
void tooltip;
|
|
if (floatingEl && anchorEl) {
|
|
const cleanup = autoUpdate(anchorEl, floatingEl, () => {
|
|
void computePosition(anchorEl!, floatingEl!, {
|
|
placement,
|
|
middleware: [offset(6), flip(), shift({ padding: 8 })],
|
|
}).then(({ x, y }) => {
|
|
if (floatingEl) {
|
|
floatingEl.style.left = `${x}px`;
|
|
floatingEl.style.top = `${y}px`;
|
|
floatingEl.style.visibility = "visible";
|
|
}
|
|
});
|
|
});
|
|
return cleanup;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.container {
|
|
display: inline-block;
|
|
}
|
|
.popover {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
visibility: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-direction: row;
|
|
gap: 0.5rem;
|
|
justify-content: center;
|
|
z-index: 20;
|
|
background: var(--color-surface-subtle);
|
|
color: var(--color-text-primary);
|
|
border: 1px solid var(--color-border-subtle);
|
|
border-radius: var(--border-radius-md);
|
|
box-shadow: var(--elevation-low);
|
|
font: var(--txt-body-m-regular);
|
|
white-space: nowrap;
|
|
width: max-content;
|
|
padding: 0.25rem 0.5rem;
|
|
}
|
|
.txt-id:hover {
|
|
color: var(--color-text-primary);
|
|
}
|
|
</style>
|
|
|
|
<div class="container" bind:this={anchorEl}>
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<div
|
|
onmouseenter={() => {
|
|
setVisible(true);
|
|
}}
|
|
onmouseleave={() => {
|
|
setVisible(false);
|
|
}}
|
|
class="txt-id"
|
|
style:cursor="pointer"
|
|
aria-label={ariaLabel}
|
|
onclick={async event => {
|
|
event.stopPropagation();
|
|
await copy();
|
|
setVisible(true);
|
|
}}
|
|
role="button"
|
|
tabindex="0">
|
|
{#if shorten}
|
|
{formatOid(id)}
|
|
{:else}
|
|
{id}
|
|
{/if}
|
|
</div>
|
|
|
|
{#if visible}
|
|
<div use:portal bind:this={floatingEl} class="popover">
|
|
<Icon name={icon} />
|
|
{tooltip}
|
|
</div>
|
|
{/if}
|
|
</div>
|