Add svelte runes

This commit is contained in:
Sebastian Martinez
2024-11-05 10:54:09 +01:00
parent 737be7935a
commit 58891de06f
43 changed files with 821 additions and 505 deletions
+1 -1
View File
@@ -59,7 +59,7 @@
}
});
$: document.documentElement.setAttribute("data-theme", $theme);
$effect(() => document.documentElement.setAttribute("data-theme", $theme));
</script>
{#if $activeRouteStore.resource === "booting"}
+5 -1
View File
@@ -1,7 +1,11 @@
<script lang="ts">
import { createIcon } from "@app/lib/blockies";
export let publicKey: string;
const {
publicKey,
}: {
publicKey: string;
} = $props();
function createContainer(source: string) {
const seed = source.toLowerCase();
+32 -15
View File
@@ -1,23 +1,40 @@
<script lang="ts">
import type { Snippet } from "svelte";
export let children: Snippet;
export let variant: "primary" | "secondary" | "ghost" | "float" | "danger";
export let hoverable: boolean = false;
export let onclick: (() => void) | undefined = undefined;
interface Props {
children: Snippet;
variant: "primary" | "secondary" | "ghost" | "float" | "danger";
hoverable?: boolean;
onclick?: () => void;
stylePadding?: string;
styleHeight?: string;
styleMinHeight?: string;
styleWidth?: string;
styleCursor?: "default" | "pointer" | "text";
styleGap?: string;
styleOverflow?: string;
flatTop?: boolean;
}
export let stylePadding: string | undefined = undefined;
export let styleHeight: string | undefined = undefined;
export let styleMinHeight: string | undefined = undefined;
export let styleWidth: string | undefined = undefined;
export let styleCursor: "default" | "pointer" | "text" = "default";
export let styleGap: string = "0.5rem";
export let styleOverflow: string | undefined = undefined;
export let flatTop: boolean = false;
const {
children,
variant,
hoverable = false,
onclick,
stylePadding,
styleHeight,
styleMinHeight,
styleWidth,
styleCursor = "default",
styleGap = "0.5rem",
styleOverflow,
flatTop = false,
}: Props = $props();
$: style =
const style = $derived(
`--local-button-color-1: var(--color-fill-${variant});` +
`--local-hover-background-color: ${hoverable ? "var(--color-background-float)" : "var(--color-background-default)"}`;
`--local-hover-background-color: ${hoverable ? "var(--color-background-float)" : "var(--color-background-default)"}`,
);
</script>
<style>
@@ -170,7 +187,7 @@
}
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
style:width={styleWidth}
style:cursor={styleCursor}
+25 -14
View File
@@ -1,22 +1,33 @@
<script lang="ts">
import type { Snippet } from "svelte";
export let children: Snippet;
export let variant: "primary" | "secondary" | "ghost";
export let onclick: (() => void) | undefined = undefined;
interface Props {
children: Snippet;
variant: "primary" | "secondary" | "ghost";
onclick?: () => void;
disabled?: boolean;
active?: boolean;
flatLeft?: boolean;
flatRight?: boolean;
}
export let disabled: boolean = false;
export let active: boolean = false;
const {
children,
variant,
onclick = undefined,
disabled = false,
active = false,
flatLeft = false,
flatRight = false,
}: Props = $props();
export let flatLeft: boolean = false;
export let flatRight: boolean = false;
$: style =
const style = $derived(
`--button-color-1: var(--color-fill-${variant});` +
`--button-color-2: var(--color-fill-${variant}-hover);` +
`--button-color-3: var(--color-fill-${variant}-shade);` +
// The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
`--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`;
`--button-color-2: var(--color-fill-${variant}-hover);` +
`--button-color-3: var(--color-fill-${variant}-shade);` +
// The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
`--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`,
);
</script>
<style>
@@ -347,7 +358,7 @@
}
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="container active"
style:cursor={!disabled ? "pointer" : "default"}
+37 -22
View File
@@ -18,26 +18,41 @@
import ReactionSelector from "@app/components/ReactionSelector.svelte";
import Reactions from "@app/components/Reactions.svelte";
export let actions: Snippet | undefined = undefined;
export let id: string | undefined = undefined;
export let rid: string;
export let author: Author;
export let body: string;
export let reactions: Reaction[] | undefined = undefined;
export let embeds: Map<string, Embed> | undefined = undefined;
export let caption = "commented";
export let timestamp: number;
export let lastEdit: Edit | undefined = undefined;
export let disallowEmptyBody: boolean = false;
interface Props {
actions?: Snippet;
id?: string;
rid: string;
author: Author;
body: string;
reactions?: Reaction[];
embeds?: Map<string, Embed>;
caption?: string;
timestamp: number;
lastEdit?: Edit;
disallowEmptyBody?: boolean;
editComment?: (body: string, embeds: Embed[]) => Promise<void>;
reactOnComment?: (authors: Author[], reaction: string) => Promise<void>;
}
export let editComment:
| ((body: string, embeds: Embed[]) => Promise<void>)
| undefined = undefined;
export let reactOnComment:
| ((authors: Author[], reaction: string) => Promise<void>)
| undefined = undefined;
/* eslint-disable prefer-const */
let {
actions,
id,
rid,
author,
body = $bindable(),
reactions,
embeds,
caption = "commented",
timestamp,
lastEdit,
disallowEmptyBody = false,
editComment,
reactOnComment,
}: Props = $props();
/* eslint-enable prefer-const */
let state: "read" | "edit" | "submit" = "read";
let state: "read" | "edit" | "submit" = $state("read");
async function toggleEdit() {
if (state === "read") {
@@ -136,7 +151,7 @@
popoverPositionRight="0"
popoverPositionBottom="1.5rem"
{reactions}
on:select={async ({ detail: { authors, emoji } }) => {
select={async ({ authors, emoji }) => {
try {
await reactOnComment(authors, emoji);
} finally {
@@ -169,7 +184,7 @@
submitInProgress={state === "submit"}
submitCaption="Save"
placeholder="Leave a comment"
on:submit={async ({ detail: { comment, embeds } }) => {
submit={async ({ comment, embeds }) => {
state = "submit";
try {
await editComment(comment, Array.from(embeds.values()));
@@ -177,7 +192,7 @@
state = "read";
}
}}
on:close={async () => {
close={async () => {
body = body;
await tick();
state = "read";
@@ -199,7 +214,7 @@
popoverPositionLeft="0"
popoverPositionBottom="1.5rem"
{reactions}
on:select={async ({ detail: { authors, emoji } }) => {
select={async ({ authors, emoji }) => {
try {
await reactOnComment(authors, emoji);
} finally {
+27 -11
View File
@@ -4,17 +4,33 @@
import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
import Border from "./Border.svelte";
export let rid: string;
export let placeholder: string | undefined = undefined;
export let focus: boolean = false;
export let submit: (comment: string, embeds: Embed[]) => Promise<void>;
export let onclose: (() => void) | undefined = undefined;
export let onexpand: (() => void) | undefined = undefined;
export let disallowEmptyBody: boolean = false;
interface Props {
rid: string;
placeholder?: string;
focus?: boolean;
submit: (comment: string, embeds: Embed[]) => Promise<void>;
onclose?: () => void;
onexpand?: () => void;
disallowEmptyBody?: boolean;
}
let state: "collapsed" | "expanded" | "submit";
/* eslint-disable prefer-const */
let {
rid,
placeholder,
focus = false,
submit,
onclose,
onexpand,
disallowEmptyBody = false,
}: Props = $props();
/* eslint-enable prefer-const */
$: state = onclose !== undefined ? "expanded" : "collapsed";
let state: "collapsed" | "expanded" | "submit" | undefined = $state();
$effect(() => {
state = onclose !== undefined ? "expanded" : "collapsed";
});
</script>
<style>
@@ -33,14 +49,14 @@
submitInProgress={state === "submit"}
{focus}
stylePadding="0.5rem 0.75rem"
on:close={() => {
close={() => {
if (onclose !== undefined) {
onclose();
} else {
state = "collapsed";
}
}}
on:submit={async ({ detail: { comment, embeds } }) => {
submit={async ({ comment, embeds }) => {
try {
state = "submit";
await submit(comment, Array.from(embeds.values()));
+6 -2
View File
@@ -6,9 +6,13 @@
import Icon from "./Icon.svelte";
export let id: string;
const {
id,
}: {
id: string;
} = $props();
let icon: ComponentProps<Icon>["name"] = "copy";
let icon: ComponentProps<typeof Icon>["name"] = $state("copy");
const restoreIcon = debounce(() => {
icon = "copy";
+5 -1
View File
@@ -1,7 +1,11 @@
<script lang="ts">
import type { Stats } from "@bindings/cob/Stats";
export let stats: Stats;
interface Props {
stats: Stats;
}
const { stats }: Props = $props();
</script>
<style>
+8 -4
View File
@@ -1,10 +1,14 @@
<script lang="ts" generics="T">
import type { Snippet } from "svelte";
export let item: Snippet<[T]>;
export let empty: Snippet | undefined = undefined;
export let items: T[];
export let styleDropdownMinWidth: string | undefined = undefined;
interface Props {
item: Snippet<[T]>;
empty?: Snippet;
items: T[];
styleDropdownMinWidth?: string;
}
const { item, empty, items, styleDropdownMinWidth }: Props = $props();
</script>
<style>
+19 -7
View File
@@ -1,11 +1,23 @@
<script lang="ts">
import type { Snippet } from "svelte";
export let children: Snippet;
export let selected: boolean;
export let disabled: boolean = false;
export let title: string | undefined = undefined;
export let style: string | undefined = undefined;
interface Props {
children: Snippet;
selected: boolean;
onclick: () => void;
disabled?: boolean;
title?: string;
style?: string;
}
const {
onclick,
children,
selected,
disabled = false,
title,
style,
}: Props = $props();
</script>
<style>
@@ -46,7 +58,7 @@
}
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
@@ -55,6 +67,6 @@
class:disabled
{style}
{title}
on:click>
{onclick}>
{@render children()}
</div>
+51 -34
View File
@@ -2,8 +2,6 @@
import type { ComponentProps } from "svelte";
import type { Embed } from "@bindings/cob/thread/Embed";
import { createEventDispatcher } from "svelte";
import * as utils from "@app/lib/utils";
import Button from "./Button.svelte";
@@ -12,39 +10,58 @@
import Textarea from "./Textarea.svelte";
import OutlineButton from "./OutlineButton.svelte";
export let rid: string;
export let placeholder: string = "Leave your comment";
export let submitCaption: string = "Comment";
export let focus: boolean = false;
export let inline: boolean = false;
export let body: string = "";
export let embeds: Map<string, Embed> = new Map();
export let submitInProgress: boolean = false;
export let disallowEmptyBody: boolean = false;
export let isValid: () => boolean = () => {
return true;
};
export let stylePadding: string | undefined = undefined;
export let borderVariant: ComponentProps<Textarea>["borderVariant"] = "float";
interface Props {
rid: string;
placeholder?: string;
submitCaption?: string;
focus?: boolean;
inline?: boolean;
body?: string;
embeds?: Map<string, Embed>;
submitInProgress?: boolean;
disallowEmptyBody?: boolean;
isValid?: () => boolean;
stylePadding?: string;
borderVariant?: ComponentProps<typeof Textarea>["borderVariant"];
submit: (opts: {
comment: string;
embeds: Map<string, Embed>;
}) => Promise<void>;
close: () => void;
}
let preview: boolean = false;
let selectionStart = 0;
let selectionEnd = 0;
let inputFiles: FileList | undefined = undefined;
/* eslint-disable prefer-const */
let {
rid,
placeholder = "Leave your comment",
submitCaption = "Comment",
focus = false,
inline = false,
body = $bindable(""),
embeds = new Map(),
submitInProgress = false,
disallowEmptyBody = false,
isValid = () => true,
stylePadding,
borderVariant = "float",
submit,
close,
}: Props = $props();
/* eslint-enable prefer-const */
let preview: boolean = $state(false);
let selectionStart = $state(0);
let selectionEnd = $state(0);
let inputFiles: FileList | undefined = $state(undefined);
const inputId = `input-label-${crypto.randomUUID()}`;
const dispatch = createEventDispatcher<{
submit: {
comment: string;
embeds: Map<string, Embed>;
};
close: null;
}>();
function submit() {
dispatch("submit", { comment: body, embeds });
preview = false;
function submitFn() {
void submit({ comment: body, embeds })
.then(() => (preview = false))
.catch(e => {
console.error(e);
});
}
</script>
@@ -106,7 +123,7 @@
bind:selectionEnd
bind:selectionStart
{focus}
on:submit={submit}
submit={() => submit({ comment: body, embeds })}
bind:value={body}
{placeholder} />
{/if}
@@ -116,7 +133,7 @@
variant="ghost"
onclick={() => {
preview = false;
dispatch("close");
close();
}}>
<Icon name="cross" />Discard
</OutlineButton>
@@ -138,7 +155,7 @@
disabled={!isValid() ||
submitInProgress ||
(disallowEmptyBody && body.trim() === "")}
onclick={submit}>
onclick={submitFn}>
<Icon name="checkmark" />
{#if submitInProgress}
Saving…
+7 -3
View File
@@ -11,9 +11,13 @@
import Popover from "./Popover.svelte";
import ThemeSwitch from "./ThemeSwitch.svelte";
export let breadcrumbs: Snippet;
export let iconLeft: Snippet | undefined = undefined;
export let center: Snippet | undefined = undefined;
interface Props {
breadcrumbs: Snippet;
iconLeft?: Snippet;
center?: Snippet;
}
const { breadcrumbs, iconLeft, center }: Props = $props();
</script>
<style>
+48 -40
View File
@@ -1,45 +1,53 @@
<script lang="ts">
import { unreachable } from "@app/lib/utils";
export let size: "16" | "32" = "16";
export let onclick: (() => void) | undefined = undefined;
export let styleCursor: "default" | "pointer" = "default";
interface Props {
size?: "16" | "32";
onclick?: () => void;
styleCursor?: "default" | "pointer";
name:
| "arrow-left"
| "arrow-right"
| "checkmark"
| "chevron-down"
| "chevron-right"
| "comment"
| "copy"
| "cross"
| "dashboard"
| "delegate"
| "diff"
| "eye"
| "face"
| "file"
| "inbox"
| "issue"
| "lock"
| "markdown"
| "moon"
| "more-vertical"
| "offline"
| "online"
| "patch"
| "pen"
| "plus"
| "reply"
| "repo"
| "revision"
| "seedling"
| "seedling-filled"
| "settings"
| "sidebar"
| "sun"
| "warning";
}
export let name:
| "arrow-left"
| "arrow-right"
| "checkmark"
| "chevron-down"
| "chevron-right"
| "comment"
| "copy"
| "cross"
| "dashboard"
| "delegate"
| "diff"
| "eye"
| "face"
| "file"
| "inbox"
| "issue"
| "lock"
| "markdown"
| "moon"
| "more-vertical"
| "offline"
| "online"
| "patch"
| "pen"
| "plus"
| "reply"
| "repo"
| "revision"
| "seedling"
| "seedling-filled"
| "settings"
| "sidebar"
| "sun"
| "warning";
const {
size = "16",
onclick = undefined,
styleCursor = "default",
name,
}: Props = $props();
</script>
<style>
@@ -52,8 +60,8 @@
}
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<svg
style:cursor={styleCursor}
role="img"
+23 -12
View File
@@ -8,16 +8,9 @@
import Icon from "./Icon.svelte";
export let children: Snippet | undefined = undefined;
export let id: string;
export let clipboard: string = id;
export let shorten: boolean = true;
export let variant: "oid" | "commit" | "none";
export let ariaLabel: string | undefined = undefined;
let icon: ComponentProps<Icon>["name"] = "copy";
let icon: ComponentProps<typeof Icon>["name"] = $state("copy");
const text = "Click to copy";
let tooltip = text;
let tooltip = $state(text);
const restoreIcon = debounce(() => {
icon = "copy";
@@ -31,8 +24,26 @@
restoreIcon();
}
let visible: boolean = false;
export let debounceTimeout = 50;
let visible: boolean = $state(false);
interface Props {
children?: Snippet;
id: string;
clipboard?: string;
shorten?: boolean;
variant: "oid" | "commit" | "none";
ariaLabel?: string;
debounceTimeout?: number;
}
const {
children,
id,
clipboard = id,
shorten = true,
variant,
ariaLabel,
debounceTimeout = 50,
}: Props = $props();
const setVisible = debounce((value: boolean) => {
visible = value;
@@ -73,7 +84,7 @@
</style>
<div class="container">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
onmouseenter={() => {
setVisible(true);
+6 -3
View File
@@ -2,9 +2,12 @@
import dompurify from "dompurify";
import escape from "lodash/escape";
export let content: string;
export let fontSize: "tiny" | "small" | "regular" | "medium" | "large" =
"small";
interface Props {
content: string;
fontSize?: "tiny" | "small" | "regular" | "medium" | "large";
}
const { content, fontSize = "small" }: Props = $props();
function formatInlineTitle(input: string): string {
return input.replaceAll(/`([^`]+)`/g, "<code>$1</code>");
+5 -1
View File
@@ -7,7 +7,11 @@
import IssueStateBadge from "@app/components/IssueStateBadge.svelte";
import NodeId from "@app/components/NodeId.svelte";
export let issue: Issue;
interface Props {
issue: Issue;
}
const { issue }: Props = $props();
</script>
<style>
+5 -1
View File
@@ -4,7 +4,11 @@
import capitalize from "lodash/capitalize";
import { issueStatusColor } from "@app/lib/utils";
export let state: Issue["state"];
interface Props {
state: Issue["state"];
}
const { state }: Props = $props();
</script>
<div
+14 -6
View File
@@ -12,8 +12,13 @@
import Icon from "@app/components/Icon.svelte";
import Popover from "@app/components/Popover.svelte";
export let state: State;
export let save: (state: State) => Promise<void>;
const {
save,
...rest
}: {
state: State;
save: (state: State) => Promise<void>;
} = $props();
const actions: { caption: string; state: State }[] = [
{ caption: "Reopen", state: { status: "open" } },
@@ -25,7 +30,9 @@
];
// Pick a default for the action button when the issue state changes.
$: selectedAction = state.status === "open" ? actions[1] : actions[0];
let selectedAction = $state(
rest.state.status === "open" ? actions[1] : actions[0],
);
</script>
<style>
@@ -40,7 +47,7 @@
<Button
variant="secondary"
flatRight
onclick={() => void save(selectedAction["state"])}>
onclick={() => void save($state.snapshot(selectedAction["state"]))}>
{selectedAction["caption"]}
</Button>
@@ -57,11 +64,12 @@
{/snippet}
{#snippet popover()}
<Border variant="ghost">
<DropdownList items={actions.filter(a => !isEqual(a.state, state))}>
<DropdownList
items={actions.filter(a => !isEqual(a.state, rest.state))}>
{#snippet item(action)}
<DropdownListItem
selected={isEqual(selectedAction, action)}
on:click={() => {
onclick={() => {
selectedAction = action;
closeFocused();
}}>
+6 -2
View File
@@ -14,8 +14,12 @@
import InlineTitle from "./InlineTitle.svelte";
import NodeId from "./NodeId.svelte";
export let issue: Issue;
export let rid: string;
interface Props {
issue: Issue;
rid: string;
}
const { issue, rid }: Props = $props();
</script>
<style>
@@ -7,7 +7,11 @@
import IssueStateBadge from "@app/components/IssueStateBadge.svelte";
import NodeId from "@app/components/NodeId.svelte";
export let operation: Extract<Operation, { type: "lifecycle" }>;
interface Props {
operation: Extract<Operation, { type: "lifecycle" }>;
}
const { operation }: Props = $props();
</script>
<Border variant="float" stylePadding="1rem">
+13 -4
View File
@@ -4,10 +4,19 @@
import { push, routeToPath } from "@app/lib/router";
export let children: Snippet;
export let route: Route;
export let disabled: boolean = false;
export let variant: "active" | "regular" | "tab" = "regular";
interface Props {
children: Snippet;
route: Route;
disabled?: boolean;
variant?: "active" | "regular" | "tab";
}
const {
children,
route,
disabled = false,
variant = "regular",
}: Props = $props();
function navigateToRoute(event: MouseEvent): void {
event.preventDefault();
+90 -87
View File
@@ -1,34 +1,35 @@
<script lang="ts">
import dompurify from "dompurify";
import matter from "@radicle/gray-matter";
import { afterUpdate } from "svelte";
import { toDom } from "hast-util-to-dom";
import { Renderer, markdownWithExtensions } from "@app/lib/markdown";
import { highlight } from "@app/lib/syntax";
import { twemoji, scrollIntoView, isCommit } from "@app/lib/utils";
import { invoke } from "@app/lib/invoke";
import { tick } from "svelte";
export let rid: string;
export let content: string;
// If true, add <br> on a single line break
export let breaks: boolean = false;
interface Props {
rid: string;
content: string;
// If true, add <br> on a single line break
breaks?: boolean;
}
const { rid, content, breaks = false }: Props = $props();
let container: HTMLElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let frontMatter: [string, any][] | undefined = undefined;
$: {
const doc = $derived(matter(content));
const frontMatter = $derived.by(() => {
try {
const doc = matter(content);
content = doc.content;
frontMatter = Object.entries(doc.data).filter(
return Object.entries(doc.data).filter(
([, val]) => typeof val === "string" || typeof val === "number",
);
} catch (error) {
console.error("Not able to parse frontmatter: ", error);
}
}
});
function render(content: string): string {
return dompurify.sanitize(
@@ -39,85 +40,86 @@
);
}
afterUpdate(async () => {
for (const e of container.querySelectorAll("a")) {
try {
const url = new URL(e.href);
if (url.origin !== window.origin) {
e.target = "_blank";
$effect(() => {
void tick().then(() => {
for (const e of container.querySelectorAll("a")) {
try {
const url = new URL(e.href);
if (url.origin !== window.origin) {
e.target = "_blank";
}
} catch (e) {
console.warn("Not able to parse url", e);
}
// Don't underline <a> tags that contain images.
// Make an exception for emojis.
if (
e.firstElementChild instanceof HTMLImageElement &&
!e.firstElementChild.classList.contains("txt-emoji")
) {
e.classList.add("no-underline");
}
} catch (e) {
console.warn("Not able to parse url", e);
}
// Don't underline <a> tags that contain images.
// Make an exception for emojis.
if (
e.firstElementChild instanceof HTMLImageElement &&
!e.firstElementChild.classList.contains("txt-emoji")
) {
e.classList.add("no-underline");
// Replace standard HTML checkboxes with our custom radicle-icon-small element
for (const i of container.querySelectorAll('input[type="checkbox"]')) {
i.parentElement?.classList.add("task-item");
const checkbox = document.createElement("radicle-icon-small");
const checked = i.getAttribute("checked");
checkbox.setAttribute(
"name",
checked === null ? "checkbox-unchecked" : "checkbox-checked",
);
i.insertAdjacentElement("beforebegin", checkbox);
i.remove();
}
}
// Replace standard HTML checkboxes with our custom radicle-icon-small element
for (const i of container.querySelectorAll('input[type="checkbox"]')) {
i.parentElement?.classList.add("task-item");
// Iterate over all images, and replace the source with a canonicalized URL
// pointing at the repos /raw endpoint.
for (const i of container.querySelectorAll("img")) {
const imagePath = i.getAttribute("src");
const checkbox = document.createElement("radicle-icon-small");
const checked = i.getAttribute("checked");
checkbox.setAttribute(
"name",
checked === null ? "checkbox-unchecked" : "checkbox-checked",
);
i.insertAdjacentElement("beforebegin", checkbox);
i.remove();
}
// Iterate over all images, and replace the source with a canonicalized URL
// pointing at the repos /raw endpoint.
for (const i of container.querySelectorAll("img")) {
const imagePath = i.getAttribute("src");
// If the image is an oid embed
if (imagePath && isCommit(imagePath)) {
const base64Content = await invoke<string>("get_file_by_oid", {
rid,
oid: imagePath,
});
i.setAttribute("src", `data:image/jpeg;base64,${base64Content}`);
continue;
// If the image is an oid embed
if (imagePath && isCommit(imagePath)) {
void invoke<string>("get_file_by_oid", {
rid,
oid: imagePath,
}).then(base64Content =>
i.setAttribute("src", `data:image/jpeg;base64,${base64Content}`),
);
}
}
}
// Replaces code blocks in the background with highlighted code.
const prefix = "language-";
const nodes = Array.from(document.body.querySelectorAll("pre code"));
// Replaces code blocks in the background with highlighted code.
const prefix = "language-";
const nodes = Array.from(document.body.querySelectorAll("pre code"));
const treeChanges: Promise<void>[] = [];
const treeChanges: Promise<void>[] = [];
for (const node of nodes) {
const className = Array.from(node.classList).find(name =>
name.startsWith(prefix),
);
if (!className) continue;
for (const node of nodes) {
const className = Array.from(node.classList).find(name =>
name.startsWith(prefix),
);
if (!className) continue;
treeChanges.push(
highlight(node.textContent ?? "", className.slice(prefix.length))
.then(tree => {
if (tree) {
node.replaceChildren(toDom(tree, { fragment: true }));
}
})
.catch(e => console.warn("Not able to highlight code block", e)),
);
}
treeChanges.push(
highlight(node.textContent ?? "", className.slice(prefix.length))
.then(tree => {
if (tree) {
node.replaceChildren(toDom(tree, { fragment: true }));
}
})
.catch(e => console.warn("Not able to highlight code block", e)),
);
}
await Promise.allSettled(treeChanges);
void Promise.allSettled(treeChanges);
if (window.location.hash) {
scrollIntoView(window.location.hash.substring(1));
}
if (window.location.hash) {
scrollIntoView(window.location.hash.substring(1));
}
});
});
</script>
@@ -385,17 +387,18 @@
{#if frontMatter && frontMatter.length > 0}
<div class="front-matter">
<table>
{#each frontMatter as [key, val]}
<!-- svelte-ignore node_invalid_placement_ssr -->
<tr>
<td><span class="txt-bold">{key}</span></td>
<td>{val}</td>
</tr>
{/each}
<tbody>
{#each frontMatter as [key, val]}
<tr>
<td><span class="txt-bold">{key}</span></td>
<td>{val}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<div class="markdown" bind:this={container} use:twemoji={{ exclude: ["21a9"] }}>
{@html render(content)}
{@html render(doc.content)}
</div>
+14 -9
View File
@@ -1,16 +1,21 @@
<script lang="ts">
import type { Snippet } from "svelte";
export let children: Snippet;
export let variant: "primary" | "secondary" | "ghost";
export let onclick: (() => void) | undefined = undefined;
interface Props {
children: Snippet;
variant: "primary" | "secondary" | "ghost";
onclick?: () => void;
}
$: style =
const { children, variant, onclick }: Props = $props();
const style = $derived(
`--button-color-1: var(--color-fill-${variant});` +
`--button-color-2: var(--color-fill-${variant}-hover);` +
`--button-color-3: var(--color-fill-${variant}-shade);` +
// The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
`--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`;
`--button-color-2: var(--color-fill-${variant}-hover);` +
`--button-color-3: var(--color-fill-${variant}-shade);` +
// The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
`--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`,
);
</script>
<style>
@@ -197,7 +202,7 @@
}
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="container" {onclick} role="button" tabindex="0" {style}>
<div class="pixel p1-1"></div>
<div class="pixel p1-2"></div>
+12 -5
View File
@@ -3,12 +3,19 @@
import Avatar from "./Avatar.svelte";
export let publicKey: string;
export let alias: string | undefined = undefined;
interface Props {
publicKey: string;
alias?: string;
styleFontSize?: string;
styleFontFamily?: string;
}
export let styleFontSize: string | undefined = "var(--font-size-small)";
export let styleFontFamily: string | undefined =
"var(--font-family-monospace)";
const {
publicKey,
alias,
styleFontSize = "var(--font-size-small)",
styleFontFamily = "var(--font-family-monospace)",
}: Props = $props();
</script>
<style>
+15 -10
View File
@@ -1,17 +1,22 @@
<script lang="ts">
import type { Snippet } from "svelte";
export let children: Snippet;
export let variant: "primary" | "secondary" | "ghost";
export let onclick: (() => void) | undefined = undefined;
export let disabled: boolean = false;
interface Props {
children: Snippet;
variant: "primary" | "secondary" | "ghost";
onclick?: () => void;
disabled?: boolean;
}
$: style =
const { children, variant, onclick, disabled = false }: Props = $props();
const style = $derived(
`--button-color-1: var(--color-fill-${variant});` +
`--button-color-2: var(--color-fill-${variant}-hover);` +
`--button-color-3: var(--color-fill-${variant}-shade);` +
// The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
`--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`;
`--button-color-2: var(--color-fill-${variant}-hover);` +
`--button-color-3: var(--color-fill-${variant}-shade);` +
// The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
`--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`,
);
</script>
<style>
@@ -241,7 +246,7 @@
}
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="container"
style:cursor={!disabled ? "pointer" : "default"}
+7 -3
View File
@@ -18,7 +18,7 @@
import NodeId from "./NodeId.svelte";
import Id from "./Id.svelte";
let stats: Stats | undefined = undefined;
let stats: Stats | undefined = $state(undefined);
onMount(async () => {
stats = await invoke<Stats>("diff_stats", {
@@ -28,8 +28,12 @@
});
});
export let patch: Patch;
export let rid: string;
interface Props {
patch: Patch;
rid: string;
}
const { patch, rid }: Props = $props();
</script>
<style>
+29 -11
View File
@@ -10,17 +10,33 @@
<script lang="ts">
import type { Snippet } from "svelte";
export let toggle: Snippet<[() => void]>;
export let popover: Snippet;
export let popoverContainerMinWidth: string | undefined = undefined;
export let popoverPadding: string | undefined = undefined;
export let popoverPositionBottom: string | undefined = undefined;
export let popoverPositionLeft: string | undefined = undefined;
export let popoverPositionRight: string | undefined = undefined;
export let popoverPositionTop: string | undefined = undefined;
interface Props {
toggle: Snippet<[() => void]>;
popover: Snippet;
popoverContainerMinWidth?: string;
popoverPadding?: string;
popoverPositionBottom?: string;
popoverPositionLeft?: string;
popoverPositionRight?: string;
popoverPositionTop?: string;
expanded?: boolean;
}
export let expanded = false;
let thisComponent: HTMLDivElement;
/* eslint-disable prefer-const */
let {
toggle,
popover,
popoverContainerMinWidth,
popoverPadding,
popoverPositionBottom,
popoverPositionLeft,
popoverPositionRight,
popoverPositionTop,
expanded = $bindable(false),
}: Props = $props();
/* eslint-enable prefer-const */
let thisComponent: HTMLDivElement | undefined = $state();
function clickOutside(ev: MouseEvent | TouchEvent) {
if ($focused && !ev.composedPath().includes($focused)) {
@@ -37,7 +53,9 @@
}
}
$: expanded = $focused === thisComponent;
$effect(() => {
expanded = $focused === thisComponent;
});
</script>
<style>
+17 -16
View File
@@ -1,22 +1,27 @@
<script lang="ts">
import type { Reaction } from "@bindings/cob/Reaction";
import { createEventDispatcher } from "svelte";
import Border from "./Border.svelte";
import Icon from "./Icon.svelte";
import Popover from "./Popover.svelte";
export let reactions: Reaction[] | undefined = undefined;
export let popoverPositionBottom: string | undefined = undefined;
export let popoverPositionRight: string | undefined = undefined;
export let popoverPositionLeft: string | undefined = undefined;
interface Props {
reactions?: Reaction[];
popoverPositionBottom?: string;
popoverPositionRight?: string;
popoverPositionLeft?: string;
select: (reaction: Reaction) => Promise<void>;
}
const {
reactions,
popoverPositionBottom,
popoverPositionRight,
popoverPositionLeft,
select,
}: Props = $props();
const availableReactions = ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"];
const dispatch = createEventDispatcher<{
select: Reaction;
}>();
</script>
<style>
@@ -58,12 +63,8 @@
)}
<button
class:active={Boolean(lookedUpReaction)}
onclick={() => {
dispatch(
"select",
lookedUpReaction || { emoji: reaction, authors: [] },
);
}}>
onclick={() =>
select(lookedUpReaction || { emoji: reaction, authors: [] })}>
{reaction}
</button>
{/each}
+6 -4
View File
@@ -2,10 +2,12 @@
import type { Author } from "@bindings/cob/Author";
import type { Reaction } from "@bindings/cob/Reaction";
export let reactions: Reaction[];
export let handleReaction:
| ((authors: Author[], reaction: string) => Promise<void>)
| undefined;
interface Props {
reactions: Reaction[];
handleReaction?: (authors: Author[], reaction: string) => Promise<void>;
}
const { reactions, handleReaction }: Props = $props();
function authorsToTooltip(authors: Author[]) {
return authors.map(a => a.alias ?? a.did).join("\n");
+8 -4
View File
@@ -8,11 +8,15 @@
import RepoHeader from "./RepoHeader.svelte";
import Id from "./Id.svelte";
export let repo: RepoInfo;
export let selfDid: string;
export let onclick: (() => void) | undefined = undefined;
interface Props {
repo: RepoInfo;
selfDid: string;
onclick?: () => void;
}
$: project = repo.payloads["xyz.radicle.project"]!;
const { repo, selfDid, onclick }: Props = $props();
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>
+14 -5
View File
@@ -3,12 +3,21 @@
import Icon from "./Icon.svelte";
export let repo: RepoInfo;
export let selfDid: string;
export let emphasizedTitle: boolean = true;
export let showLabels: boolean = true;
interface Props {
repo: RepoInfo;
selfDid: string;
emphasizedTitle?: boolean;
showLabels?: boolean;
}
$: project = repo.payloads["xyz.radicle.project"]!;
const {
repo,
selfDid,
emphasizedTitle = true,
showLabels = true,
}: Props = $props();
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>
+34 -15
View File
@@ -2,20 +2,38 @@
import { onMount } from "svelte";
import Border from "./Border.svelte";
import type { FormEventHandler } from "svelte/elements";
export let name: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let value: string | undefined = undefined;
interface Props {
name?: string;
placeholder?: string;
value?: string;
autofocus?: boolean;
autoselect?: boolean;
disabled?: boolean;
onSubmit?: () => void;
onDismiss?: () => void;
valid?: boolean;
oninput?: FormEventHandler<HTMLInputElement>;
}
export let autofocus: boolean = false;
export let autoselect: boolean = false;
export let disabled: boolean = false;
export let onSubmit: (() => void) | undefined = undefined;
export let onDismiss: (() => void) | undefined = undefined;
export let valid: boolean = true;
/* eslint-disable prefer-const */
let {
name,
placeholder,
value = $bindable(undefined),
autofocus = false,
autoselect = false,
disabled = false,
onSubmit,
onDismiss,
valid = true,
oninput,
}: Props = $props();
/* eslint-enable prefer-const */
let inputElement: HTMLInputElement | undefined = undefined;
let focussed = false;
let inputElement: HTMLInputElement | undefined = $state(undefined);
let focussed = $state(false);
onMount(() => {
if (inputElement === undefined) {
@@ -31,6 +49,7 @@
});
function handleKeydown(event: KeyboardEvent) {
event.stopPropagation();
if (event.key === "Enter" && valid && onSubmit) {
onSubmit();
}
@@ -72,10 +91,10 @@
variant={valid ? (focussed ? "secondary" : "ghost") : "danger"}
styleWidth="100%">
<input
on:focus={() => {
onfocus={() => {
focussed = true;
}}
on:blur={() => {
onblur={() => {
focussed = false;
}}
bind:this={inputElement}
@@ -86,6 +105,6 @@
bind:value
autocomplete="off"
spellcheck="false"
on:keydown|stopPropagation={handleKeydown}
on:input />
onkeydown={handleKeydown}
{oninput} />
</Border>
+65 -43
View File
@@ -1,67 +1,89 @@
<script lang="ts">
import type { FormEventHandler } from "svelte/elements";
import type { ComponentProps } from "svelte";
import { afterUpdate, beforeUpdate, createEventDispatcher } from "svelte";
import { tick } from "svelte";
import * as utils from "@app/lib/utils";
import Border from "./Border.svelte";
export let value: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let focus: boolean = false;
export let size: "grow" | "resizable" | "fixed-height" = "grow";
export let styleMinHeight: string | undefined = undefined;
export let stylePadding: string = "0.75rem";
export let borderVariant: ComponentProps<Border>["variant"] = "float";
interface Props {
borderVariant?: ComponentProps<typeof Border>["variant"];
focus?: boolean;
oninput?: FormEventHandler<HTMLTextAreaElement>;
onkeypress?: FormEventHandler<HTMLTextAreaElement>;
placeholder?: string;
selectionEnd?: number;
selectionStart?: number;
size?: "grow" | "resizable" | "fixed-height";
styleMinHeight?: string;
stylePadding?: string;
submit: () => Promise<void>;
value?: string;
}
// Defaulting selectionStart and selectionEnd to 0, since no full support yet.
export let selectionStart: number = 0;
export let selectionEnd: number = 0;
/* eslint-disable prefer-const */
let {
borderVariant = "float",
focus = false,
oninput,
onkeypress,
placeholder = undefined,
// Defaulting selectionStart and selectionEnd to 0, since no full support yet.
selectionEnd = $bindable(0),
selectionStart = $bindable(0),
size = "grow",
styleMinHeight = undefined,
stylePadding = "0.75rem",
submit,
value = $bindable(undefined),
}: Props = $props();
/* eslint-enable prefer-const */
let textareaElement: HTMLTextAreaElement | undefined = undefined;
let focussed = false;
let textareaElement: HTMLTextAreaElement | undefined = $state(undefined);
let focussed = $state(false);
// We either auto-grow the textarea, or allow the user to resize it. These
// options are mutually exclusive because a user resized textarea would
// automatically shrink upon text input otherwise.
$: if (textareaElement && size === "grow") {
// React to changes to the textarea content.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
value;
$effect(() => {
if (textareaElement && size === "grow") {
// React to changes to the textarea content.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
value;
// Reset height to 0px on every value change so that the textarea
// immediately shrinks when all text is deleted.
textareaElement.style.height = `0px`;
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
}
// Reset height to 0px on every value change so that the textarea
// immediately shrinks when all text is deleted.
textareaElement.style.height = `0px`;
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
}
if (textareaElement && focus) {
textareaElement.focus();
focussed = false;
}
});
$: if (textareaElement && focus) {
textareaElement.focus();
focus = false;
}
beforeUpdate(() => {
$effect.pre(() => {
if (textareaElement) {
({ selectionStart, selectionEnd } = textareaElement);
}
});
afterUpdate(() => {
if (textareaElement && focus) {
textareaElement.setSelectionRange(selectionStart, selectionEnd);
textareaElement.focus();
}
$effect(() => {
void tick().then(() => {
if (textareaElement && focus) {
textareaElement.setSelectionRange(selectionStart, selectionEnd);
textareaElement.focus();
}
});
});
const dispatch = createEventDispatcher<{
submit: null;
}>();
function handleKeydown(event: KeyboardEvent) {
event.stopPropagation();
const auxiliarKey = utils.isMac() ? event.metaKey : event.ctrlKey;
if (auxiliarKey && event.key === "Enter") {
dispatch("submit");
void submit();
}
if (event.key === "Escape") {
textareaElement?.blur();
@@ -117,10 +139,10 @@
? "scroll"
: undefined}
{placeholder}
on:input
on:focus={() => (focussed = true)}
on:blur={() => (focussed = false)}
on:keydown|stopPropagation={handleKeydown}
on:keypress>
{oninput}
{onkeypress}
onfocus={() => (focussed = true)}
onblur={() => (focussed = false)}
onkeydown={handleKeydown}>
</textarea>
</Border>
+41 -28
View File
@@ -13,25 +13,38 @@
import ExtendedTextarea from "./ExtendedTextarea.svelte";
import Icon from "@app/components/Icon.svelte";
export let thread: {
root: Comment;
replies: Comment[];
};
export let rid: string;
export let canEditComment: (author: string) => true | undefined;
export let editComment:
| ((commentId: string, body: string, embeds: Embed[]) => Promise<void>)
| undefined;
export let createReply:
| ((commentId: string, comment: string, embeds: Embed[]) => Promise<void>)
| undefined;
export let reactOnComment:
| ((
commentId: string,
authors: Author[],
reaction: string,
) => Promise<void>)
| undefined;
interface Props {
thread: {
root: Comment;
replies: Comment[];
};
rid: string;
canEditComment: (author: string) => true | undefined;
editComment?: (
commentId: string,
body: string,
embeds: Embed[],
) => Promise<void>;
createReply?: (
commentId: string,
comment: string,
embeds: Embed[],
) => Promise<void>;
reactOnComment?: (
commentId: string,
authors: Author[],
reaction: string,
) => Promise<void>;
}
const {
thread,
rid,
canEditComment,
editComment,
createReply,
reactOnComment,
}: Props = $props();
async function toggleReply() {
showReplyForm = !showReplyForm;
@@ -46,16 +59,16 @@
});
}
let showReplyForm = false;
let submitInProgress = false;
let showReplyForm = $state(false);
let submitInProgress = $state(false);
$: root = thread.root;
$: replies = thread.replies;
$: style =
const root = $derived(thread.root);
const replies = $derived(thread.replies);
const style = $derived(
replies.length > 0
? "--local-clip-path: var(--2px-top-corner-fill)"
: "--local-clip-path: var(--2px-corner-fill)";
: "--local-clip-path: var(--2px-corner-fill)",
);
</script>
<style>
@@ -135,8 +148,8 @@
submitCaption="Reply"
focus
stylePadding="0.5rem 0.75rem"
on:close={() => (showReplyForm = false)}
on:submit={async ({ detail: { comment, embeds } }) => {
close={() => (showReplyForm = false)}
submit={async ({ comment, embeds }) => {
try {
submitInProgress = true;
await createReply(
+1 -1
View File
@@ -153,7 +153,7 @@ export const patchStatusBackgroundColor: Record<
merged: "var(--color-fill-delegate)",
};
export function authorForNodeId(author: Author): ComponentProps<NodeId> {
export function authorForNodeId(author: Author): ComponentProps<typeof NodeId> {
return { publicKey: publicKeyFromDid(author.did), alias: author.alias };
}
+6 -2
View File
@@ -1,8 +1,12 @@
<script lang="ts">
import Icon from "@app/components/Icon.svelte";
export let error: string;
export let hint: string | undefined = undefined;
interface Props {
error: string;
hint?: string;
}
const { error, hint }: Props = $props();
</script>
<style>
+6 -2
View File
@@ -9,8 +9,12 @@
import RepoCard from "@app/components/RepoCard.svelte";
import NodeId from "@app/components/NodeId.svelte";
export let repos: RepoInfo[];
export let config: Config;
interface Props {
repos: RepoInfo[];
config: Config;
}
const { repos, config }: Props = $props();
</script>
<style>
+12 -7
View File
@@ -21,13 +21,17 @@
import Textarea from "@app/components/Textarea.svelte";
import Markdown from "@app/components/Markdown.svelte";
export let repo: RepoInfo;
export let issues: Issue[];
export let config: Config;
interface Props {
repo: RepoInfo;
issues: Issue[];
config: Config;
}
let title: string = "";
let description: string = "";
let preview: boolean = false;
const { repo, issues, config }: Props = $props();
let title: string = $state("");
let description: string = $state("");
let preview: boolean = $state(false);
const announce = false;
const labels: string[] = [];
@@ -47,7 +51,7 @@
});
}
$: project = repo.payloads["xyz.radicle.project"]!;
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>
@@ -176,6 +180,7 @@
placeholder="Description"
bind:value={description}
size="fixed-height"
submit={createIssue}
styleMinHeight="100%" />
{/if}
<div
+42 -33
View File
@@ -35,48 +35,57 @@
import Layout from "./Layout.svelte";
export let repo: RepoInfo;
export let issue: Issue;
export let issues: Issue[];
export let config: Config;
interface Props {
repo: RepoInfo;
issue: Issue;
issues: Issue[];
config: Config;
}
let topLevelReplyOpen = false;
let editingTitle = false;
let updatedTitle: string = issue.title;
/* eslint-disable prefer-const */
let { repo, issue, issues: initialIssues, config }: Props = $props();
/* eslint-enable prefer-const */
const issues = $state(initialIssues);
let topLevelReplyOpen = $state(false);
let editingTitle = $state(false);
let updatedTitle = $state(issue.title);
// The view doesn't get destroyed when we switch between different issues in
// the sidebar and because of that the top-level state gets retained when the
// issue changes. This reactive statement makes sure we always load the new
// issue and reset the state to defaults.
let issueId = issue.id;
$: if (issueId !== issue.id) {
issueId = issue.id;
topLevelReplyOpen = false;
editingTitle = false;
updatedTitle = issue.title;
void loadActivity();
}
$effect(() => {
if (issueId !== issue.id) {
issueId = issue.id;
topLevelReplyOpen = false;
editingTitle = false;
updatedTitle = issue.title;
void loadActivity();
}
});
$: project = repo.payloads["xyz.radicle.project"]!;
const project = $derived(repo.payloads["xyz.radicle.project"]!);
const issueBody = $derived(issue.discussion[0]);
const threads = $derived(
issue.discussion
.filter(
comment =>
(comment.id !== issueBody.id && !comment.replyTo) ||
comment.replyTo === issueBody.id,
)
.map(thread => {
return {
root: thread,
replies: issue.discussion
.filter(comment => comment.replyTo === thread.id)
.sort((a, b) => a.edits[0].timestamp - b.edits[0].timestamp),
};
}, []),
);
$: issueBody = issue.discussion[0];
$: threads = issue.discussion
.filter(
comment =>
(comment.id !== issueBody.id && !comment.replyTo) ||
comment.replyTo === issueBody.id,
)
.map(thread => {
return {
root: thread,
replies: issue.discussion
.filter(comment => comment.replyTo === thread.id)
.sort((a, b) => a.edits[0].timestamp - b.edits[0].timestamp),
};
}, []);
let activity: Operation[];
let activity = $state<Operation[]>([]);
async function loadActivity() {
activity = await invoke("activity_by_id", {
+9 -5
View File
@@ -17,12 +17,16 @@
import RepoHeader from "@app/components/RepoHeader.svelte";
import Button from "@app/components/Button.svelte";
export let repo: RepoInfo;
export let issues: Issue[];
export let config: Config;
export let status: IssueStatus;
interface Props {
repo: RepoInfo;
issues: Issue[];
config: Config;
status: IssueStatus;
}
$: project = repo.payloads["xyz.radicle.project"]!;
const { repo, issues, config, status }: Props = $props();
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>
+18 -7
View File
@@ -7,20 +7,31 @@
import Icon from "@app/components/Icon.svelte";
import NakedButton from "@app/components/NakedButton.svelte";
export let children: Snippet;
export let breadcrumbs: Snippet;
export let headerCenter: Snippet | undefined = undefined;
export let sidebar: Snippet;
export let loadMore: (() => Promise<void>) | undefined = undefined;
interface Props {
children: Snippet;
breadcrumbs: Snippet;
headerCenter?: Snippet;
sidebar: Snippet;
loadMore?: () => Promise<void>;
}
let hidden = false;
let listElement: HTMLElement;
const {
children,
breadcrumbs,
headerCenter = undefined,
sidebar,
loadMore = undefined,
}: Props = $props();
let hidden = $state(false);
let listElement: HTMLElement | undefined = $state();
let loading = false;
onMount(() => {
if (listElement && loadMore) {
listElement.addEventListener("scroll", async () => {
if (
listElement &&
listElement.scrollTop + listElement.clientHeight >=
listElement.scrollHeight - 600 &&
loading === false
+9 -15
View File
@@ -9,7 +9,6 @@
formatTimestamp,
patchStatusColor,
} from "@app/lib/utils";
import { invoke } from "@app/lib/invoke";
import Border from "@app/components/Border.svelte";
import CopyableId from "@app/components/CopyableId.svelte";
@@ -21,22 +20,17 @@
import Markdown from "@app/components/Markdown.svelte";
import Id from "@app/components/Id.svelte";
export let repo: RepoInfo;
export let patch: Patch;
export let patches: Patch[];
export let revisions: Revision[];
export let config: Config;
interface Props {
repo: RepoInfo;
patch: Patch;
patches: Patch[];
revisions: Revision[];
config: Config;
}
$: void invoke("get_diff", {
rid: repo.rid,
options: {
base: revisions[0].base,
head: revisions[0].head,
unified: 10,
},
}).then(console.log);
const { repo, patch, patches, revisions, config }: Props = $props();
$: project = repo.payloads["xyz.radicle.project"]!;
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>
+18 -8
View File
@@ -16,14 +16,24 @@
import PatchTeaser from "@app/components/PatchTeaser.svelte";
import RepoHeader from "@app/components/RepoHeader.svelte";
export let repo: RepoInfo;
export let patches: PaginatedQuery<Patch[]>;
export let config: Config;
export let status: PatchStatus | undefined = undefined;
interface Props {
repo: RepoInfo;
patches: PaginatedQuery<Patch[]>;
config: Config;
status?: PatchStatus;
}
$: items = patches.content;
$: more = patches.more;
$: cursor = patches.cursor;
const { repo, patches, config, status }: Props = $props();
let items = $state(patches.content);
let cursor = patches.cursor;
let more = patches.more;
$effect(() => {
items = patches.content;
cursor = patches.cursor;
more = patches.more;
});
async function loadMore() {
if (more) {
@@ -40,7 +50,7 @@
}
}
$: project = repo.payloads["xyz.radicle.project"]!;
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>