From ea40a6ef8e0ed48d5cb4f4a7b4d39cfcd11515af Mon Sep 17 00:00:00 2001 From: eti Date: Fri, 21 Aug 2026 13:10:53 +0200 Subject: [PATCH] web/reporting: visuals enhancements & nits Signed-off-by: eti --- web/.storybook/MockAuth.svelte | 13 ++- web/src/app.css | 12 +-- .../comment/CommentCard.stories.svelte | 23 +++++ .../lib/components/comment/CommentCard.svelte | 1 + .../moderation/ReportDialog.stories.svelte | 50 ++++++++-- .../components/moderation/ReportDialog.svelte | 42 ++++---- .../moderation/ReportMenu.stories.svelte | 48 ++++++++++ .../components/moderation/ReportMenu.svelte | 44 ++++++--- .../repo/issues/IssueThreadView.svelte | 18 ++-- .../repo/tickets/Ticket.stories.svelte | 39 ++++++++ .../lib/components/repo/tickets/Ticket.svelte | 16 +++- web/src/lib/components/ui/Button.svelte | 11 ++- web/src/lib/components/ui/DropdownItem.svelte | 2 +- web/src/lib/components/ui/Modal.svelte | 96 +++++++++++++++---- web/src/lib/popoverExit.ts | 8 +- 15 files changed, 342 insertions(+), 81 deletions(-) diff --git a/web/.storybook/MockAuth.svelte b/web/.storybook/MockAuth.svelte index 9cdaac2d9..ffb281ec1 100644 --- a/web/.storybook/MockAuth.svelte +++ b/web/.storybook/MockAuth.svelte @@ -11,6 +11,8 @@ accounts?: number | { did: string; handle: string }[]; // dids whose session is expired: agentFor rejects and drops the account expired?: string[]; + // answer xrpc calls instead of failing them, so a story can reach a success state + respond?: (pathname: string, init: RequestInit) => Response | Promise; } // absent: no auth context at all, like a story with no provider. @@ -20,9 +22,9 @@ @@ -35,12 +55,24 @@ name="Failed submission" play={async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.selectOptions(canvas.getByLabelText(/reason/i), [ - "tools.ozone.report.defs#reasonMisleadingSpam" - ]); - await userEvent.click(canvas.getByRole("button", { name: /^report$/i })); + await userEvent.selectOptions(canvas.getByLabelText(/reason/i), [spam]); + await userEvent.click(canvas.getByRole("button", { name: /^submit report$/i })); await waitFor(() => expect(canvas.getByRole("alert")).toHaveTextContent(/network request failed/i) ); }} /> + + { + const canvas = within(canvasElement); + await userEvent.selectOptions(canvas.getByLabelText(/reason/i), [spam]); + await userEvent.type(canvas.getByLabelText(/details/i), details); + await userEvent.click(canvas.getByRole("button", { name: /^submit report$/i })); + await waitFor(() => expect(canvas.getByText(/report sent/i)).toBeVisible()); + await expect(canvas.getByRole("button", { name: /done/i })).toBeVisible(); + await expect(canvas.queryByLabelText(/reason/i)).toBeNull(); + }} +/> diff --git a/web/src/lib/components/moderation/ReportDialog.svelte b/web/src/lib/components/moderation/ReportDialog.svelte index db0121b61..d181b3c23 100644 --- a/web/src/lib/components/moderation/ReportDialog.svelte +++ b/web/src/lib/components/moderation/ReportDialog.svelte @@ -1,7 +1,7 @@ @@ -72,3 +80,34 @@ Knot will mint service-auth to access spindle as gitrepo's authority. This way, undefined} ondelete={async () => undefined} /> {/snippet} + + + + {#snippet template(args)} + + {/snippet} + + + { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button", { name: "More actions" })); + const report = await canvas.findByRole("menuitem", { name: /report pull request/i }); + await waitFor(() => expect(report).toBeVisible()); + }} +> + {#snippet template(args)} + + {/snippet} + diff --git a/web/src/lib/components/repo/tickets/Ticket.svelte b/web/src/lib/components/repo/tickets/Ticket.svelte index 21152cc44..c6eab5165 100644 --- a/web/src/lib/components/repo/tickets/Ticket.svelte +++ b/web/src/lib/components/repo/tickets/Ticket.svelte @@ -3,6 +3,7 @@ import Trash2 from "$icon/trash-2"; import Button from "$lib/components/ui/Button.svelte"; import ErrorAlert from "$lib/components/ui/Error.svelte"; + import ReportMenu from "$lib/components/moderation/ReportMenu.svelte"; import Spinner from "$lib/components/ui/Spinner.svelte"; import TicketBody from "./TicketBody.svelte"; import TicketForm from "./TicketForm.svelte"; @@ -43,6 +44,7 @@ const auth = getAuth(); const isAuthor = $derived(auth.currentDid === ticket.author.did); + const isPull = $derived(ticket.targetBranch !== undefined); let editing = $state(false); @@ -95,7 +97,7 @@
- +
+ + +
diff --git a/web/src/lib/components/ui/Button.svelte b/web/src/lib/components/ui/Button.svelte index 4c96038a3..cd7a231b6 100644 --- a/web/src/lib/components/ui/Button.svelte +++ b/web/src/lib/components/ui/Button.svelte @@ -38,19 +38,19 @@ "border border-border-default bg-background-default text-foreground-danger", "hover:bg-background-subtle", "focus-visible:outline-foreground-danger", - "disabled:text-foreground-danger-disabled aria-disabled:text-foreground-danger-disabled" + "disabled:border-border-subtle disabled:text-foreground-danger-disabled aria-disabled:text-foreground-danger-disabled" ], success: [ "border border-border-default bg-background-default text-foreground-success", "hover:bg-background-subtle", "focus-visible:outline-foreground-success", - "disabled:text-foreground-success-disabled aria-disabled:text-foreground-success-disabled" + "disabled:border-border-subtle disabled:text-foreground-success-disabled aria-disabled:text-foreground-success-disabled" ], warning: [ "border border-border-default bg-background-default text-foreground-warning", "hover:bg-background-subtle", "focus-visible:outline-foreground-warning", - "disabled:text-foreground-warning-disabled aria-disabled:text-foreground-warning-disabled" + "disabled:border-border-subtle disabled:text-foreground-warning-disabled aria-disabled:text-foreground-warning-disabled" ] }, // the min-h is the button's real height (32/36/40 in Figma), so py has to stay @@ -110,7 +110,10 @@ } from "svelte/elements"; import Spinner from "./Spinner.svelte"; - interface Props extends Omit { + interface Props extends Omit< + HTMLAnchorAttributes & HTMLButtonAttributes, + "class" | "children" + > { variant?: ButtonVariants["variant"]; size?: ButtonVariants["size"]; insetShadow?: boolean; diff --git a/web/src/lib/components/ui/DropdownItem.svelte b/web/src/lib/components/ui/DropdownItem.svelte index e5becf0a2..7b1a6b65d 100644 --- a/web/src/lib/components/ui/DropdownItem.svelte +++ b/web/src/lib/components/ui/DropdownItem.svelte @@ -6,7 +6,7 @@ variants: { danger: { false: "", - true: "text-foreground-danger hover:bg-background-danger-subtle" + true: "text-foreground-danger" } }, defaultVariants: { diff --git a/web/src/lib/components/ui/Modal.svelte b/web/src/lib/components/ui/Modal.svelte index c1c35c7e9..bd4e0485f 100644 --- a/web/src/lib/components/ui/Modal.svelte +++ b/web/src/lib/components/ui/Modal.svelte @@ -3,9 +3,11 @@ export const modal = tv({ slots: { - panel: "m-auto w-[95%] popover-transition space-y-4 rounded-lg border border-border-default bg-background-default p-4 text-foreground-default shadow-lg backdrop:bg-black/40", - title: "typography-paragraph-large font-semibold", - description: "typography-paragraph-small text-foreground-subtle", + panel: "modal-panel m-auto flex w-[95%] popover-transition flex-col gap-4 overscroll-none rounded-lg border border-border-default bg-background-default p-4 text-foreground-default shadow-lg", + title: "flex flex-row items-center gap-2 px-0.5 typography-paragraph-large font-medium", + titleIcon: "size-4.5 text-foreground-muted", + description: "text-foreground-muted", + header: "flex flex-col gap-2", footer: "flex gap-2" }, variants: { @@ -23,11 +25,14 @@ -
(open = false)} + onclick={onClick} + aria-labelledby={title ? titleId : undefined} + tabindex={-1} class={classes.panel({ class: className })} + {@attach popoverExit()} > {#if title || description} -
- {#if title}

{title}

{/if} +
+ {#if title} +

+ + {title} +

+ {/if} {#if description}

{description}

{/if}
{/if} @@ -79,4 +108,37 @@ {#if footer}
{@render footer()}
{/if} -
+ + + diff --git a/web/src/lib/popoverExit.ts b/web/src/lib/popoverExit.ts index 63e418786..a9d602ea1 100644 --- a/web/src/lib/popoverExit.ts +++ b/web/src/lib/popoverExit.ts @@ -1,13 +1,13 @@ import type { Attachment } from "svelte/attachments"; /** - * gives a closing popover the frames its exit transition needs. + * gives a closing popover or modal dialog the frames its exit transition needs. * * chromium transitions `display` and `overlay` as discrete properties, so the * popover-transition utility in app.css fades a closing panel out by itself. - * firefox has no `overlay` yet, so a closing popover leaves the top layer right - * away and the UA's `[popover]:not(:popover-open) { display: none }` lands on - * the same frame — the exit never gets to run. this whole file goes away once + * firefox has no `overlay` yet, so a closing element leaves the top layer right + * away and the UA's `display: none` for a hidden popover or a closed dialog lands + * on the same frame — the exit never gets to run. this whole file goes away once * firefox ships `overlay`. * * holding the open `display` inline is what keeps the panel painting, but on its -- 2.51.2