Skip to content
Merged
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
12 changes: 10 additions & 2 deletions src/components/settings/AboutSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Responsibility: About group — asymmetric layout (one large version block + a column of narrow link rows), breaking the default three-equal-card look.
import { ChevronRight, Code, FileText } from "lucide-solid";
import { ChevronRight, Code, FileText, ScrollText } from "lucide-solid";
import { createResource, type JSXElement } from "solid-js";
import {
Item,
Expand All @@ -16,6 +16,8 @@ import { SettingsGroup } from "./SettingsGroup";
import { UpdateCheck } from "./UpdateCheck";

const SOURCE_URL = "https://github.com/dwall-rs/dwall";
/** The release list, not one tag: nightly builds are published under their own release. */
const CHANGELOG_URL = `${SOURCE_URL}/releases`;

export function AboutSection() {
const [info] = createResource(getAppInfo);
Expand All @@ -29,7 +31,7 @@ export function AboutSection() {
delay={200}
>
<div class="grid grid-cols-[1.5fr_1fr] gap-3">
<div class="relative row-span-2 flex flex-col gap-2.5 overflow-hidden rounded-[15px] border border-border bg-linear-to-br from-primary/10 to-card p-5 shadow-[0_1px_2px_rgba(0,0,0,.05),0_8px_24px_rgba(0,0,0,.06)]">
<div class="relative row-span-3 flex flex-col gap-2.5 overflow-hidden rounded-[15px] border border-border bg-linear-to-br from-primary/10 to-card p-5 shadow-[0_1px_2px_rgba(0,0,0,.05),0_8px_24px_rgba(0,0,0,.06)]">
<img
src={Logo}
alt={t("settings.about.logoAlt")}
Expand All @@ -43,6 +45,12 @@ export function AboutSection() {
</div>
<UpdateCheck />
</div>
<LinkRow
icon={<ScrollText class="size-3.5" />}
title={t("settings.about.changelog")}
sub={t("settings.about.changelogSub")}
onClick={() => void openUrl(CHANGELOG_URL)}
/>
<LinkRow
icon={<Code class="size-3.5" />}
title={t("settings.about.sourceCode")}
Expand Down
73 changes: 73 additions & 0 deletions src/components/settings/ReleaseNotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Responsibility: renders the parsed release notes as elements — headings, lists, paragraphs and
// inline code — with no HTML sink anywhere.
import { For, Match, Switch, type JSX } from "solid-js";
import {
parseReleaseNoteSpans,
parseReleaseNotes,
type ReleaseNoteBlock,
} from "@/lib/release-notes";

const asHeading = (block: ReleaseNoteBlock) =>
block.kind === "heading" ? block : undefined;

const asList = (block: ReleaseNoteBlock) =>
block.kind === "list" ? block : undefined;

const asParagraph = (block: ReleaseNoteBlock) =>
block.kind === "paragraph" ? block : undefined;

function Spans(props: { text: string }): JSX.Element {
return (
<For each={parseReleaseNoteSpans(props.text)}>
{(span) =>
span.code ? (
<code class="rounded bg-foreground/8 px-1 py-px font-mono text-[0.92em]">
{span.text}
</code>
) : (
span.text
)
}
</For>
);
}

export function ReleaseNotes(props: { markdown: string }) {
return (
<div class="flex flex-col gap-2.5 text-[12.5px] leading-relaxed text-muted-foreground">
<For each={parseReleaseNotes(props.markdown)}>
{(block) => (
<Switch>
<Match when={asHeading(block)}>
{(heading) => (
<h3 class="font-display text-[13px] font-bold text-foreground">
{heading().text}
</h3>
)}
</Match>
<Match when={asList(block)}>
{(list) => (
<ul class="flex list-disc flex-col gap-1 pl-4 marker:text-primary/60">
<For each={list().items}>
{(item) => (
<li>
<Spans text={item} />
</li>
)}
</For>
</ul>
)}
</Match>
<Match when={asParagraph(block)}>
{(paragraph) => (
<p>
<Spans text={paragraph().text} />
</p>
)}
</Match>
</Switch>
)}
</For>
</div>
);
}
38 changes: 36 additions & 2 deletions src/components/settings/UpdateCheck.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
// Responsibility: Update check widget — idle / checking / up to date / update available / downloading / awaiting restart / failed.
import { Match, Switch } from "solid-js";
// Responsibility: Update check widget — idle / checking / up to date / update available (with its notes in a dialog) / downloading / awaiting restart / failed.
import { Match, Show, Switch } from "solid-js";
import { t } from "@/i18n";
import { check, install, restart, updateStore } from "@/store/update.store";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Check,
Download,
LoaderCircle,
RefreshCw,
TriangleAlert,
} from "lucide-solid";
import { ReleaseNotes } from "./ReleaseNotes";

export function UpdateCheck() {
const percent = () =>
Expand All @@ -29,6 +37,32 @@ export function UpdateCheck() {
version: updateStore.version ?? "",
})}
</span>
{/* The notes are the release body the updater already fetched. They open in a
dialog rather than inside this card: a changelog is longer than the card and
would push the install button off the screen. */}
<Show when={updateStore.notes}>
<Dialog>
<DialogTrigger
variant="ghost"
size="xs"
class="ml-auto h-auto shrink-0 px-1.5 py-0 text-[11px] font-normal text-primary hover:bg-primary/10"
>
{t("settings.about.whatsNew")}
</DialogTrigger>
<DialogContent class="sm:max-w-2xl!">
<DialogHeader class="pr-8">
<DialogTitle>
{t("settings.about.whatsNewFor", {
version: updateStore.version ?? "",
})}
</DialogTitle>
</DialogHeader>
<div class="max-h-[60vh] overflow-y-auto pr-1">
<ReleaseNotes markdown={updateStore.notes ?? ""} />
</div>
</DialogContent>
</Dialog>
</Show>
</div>
<Button
size="sm"
Expand Down
19 changes: 19 additions & 0 deletions src/components/ui/alert-dialog/AlertDialog/AlertDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Dialog } from "~/components/ui/dialog";
import type { AlertDialogProps } from "./AlertDialog.types";

/**
* AlertDialog 复用 Dialog 的实现(受控/非受控状态机、Portal、Overlay、动画、
* Title/Description 的 id 注入、滚动锁定),只在内容层保留自己的差异:
* `role="alertdialog"`、无关闭按钮、点击外部不关闭、Escape 也不关闭
* (alert dialog 要求用户做出显式选择,对齐 Base UI)。
* 调用方仍可显式传 dismissOnEscape 覆盖。
*/
export const AlertDialog = (props: AlertDialogProps) => {
return (
<Dialog
{...props}
dismissOnEscape={props.dismissOnEscape ?? false}
data-slot="alert-dialog"
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { DialogProps } from "~/components/ui/dialog";

/**
* AlertDialog 复用 Dialog 的 props:
* 受控 `open` / `defaultOpen` / `onOpenChange`、`lockScroll` 等一并可用。
*/
export type AlertDialogProps = DialogProps;
2 changes: 2 additions & 0 deletions src/components/ui/alert-dialog/AlertDialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AlertDialog } from "./AlertDialog";
export type { AlertDialogProps } from "./AlertDialog.types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { AlertDialogActionProps } from "./AlertDialogAction.types";
import { Button } from "~/components/ui/button";
import { useDialogContext } from "~/components/ui/dialog";

export const AlertDialogAction = (props: AlertDialogActionProps) => {
const { setOpen } = useDialogContext();

return (
<Button
{...props}
data-slot="alert-dialog-action"
onClick={(event) => {
props.onClick?.(event);
setOpen(false);
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { ButtonProps } from "~/components/ui/button";

export type AlertDialogActionProps = ButtonProps<"button">;
1 change: 1 addition & 0 deletions src/components/ui/alert-dialog/AlertDialogAction/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AlertDialogAction } from "./AlertDialogAction";
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { mergeProps, splitProps } from "solid-js";
import type { AlertDialogCancelProps } from "./AlertDialogCancel.types";
import { Button } from "~/components/ui/button";
import { useDialogContext } from "~/components/ui/dialog";

export const AlertDialogCancel = (props: AlertDialogCancelProps) => {
const { setOpen } = useDialogContext();

const merged = mergeProps({ variant: "outline", size: "md" } as const, props);
const [local, others] = splitProps(merged, ["onClick"]);

return (
<Button
{...others}
data-slot="alert-dialog-cancel"
onClick={(event) => {
// 与 AlertDialogAction 一致:先转发调用方回调再关闭,
// 否则用户的 onClick 会被这一行静默替换掉
local.onClick?.(event);
setOpen(false);
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { ButtonProps } from "~/components/ui/button";

export type AlertDialogCancelProps = ButtonProps<"button">;
1 change: 1 addition & 0 deletions src/components/ui/alert-dialog/AlertDialogCancel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AlertDialogCancel } from "./AlertDialogCancel";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const classes =
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-[open=true]:animate-in data-[open=true]:fade-in-0 data-[open=true]:zoom-in-95 data-[open=false]:animate-out data-[open=false]:fade-out-0 data-[open=false]:zoom-out-95";

export default classes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { mergeProps, splitProps } from "solid-js";
import type { AlertDialogContentProps } from "./AlertDialogContent.types";
import { clsx } from "~/utils";
import { DialogSurface } from "~/components/ui/dialog";
import classes from "./AlertDialogContent.styles";

export const AlertDialogContent = (props: AlertDialogContentProps) => {
const merged = mergeProps({ size: "default" } as const, props);

const [local, others] = splitProps(merged, ["size", "class", "classList"]);

return (
<DialogSurface
{...others}
role="alertdialog"
dismissOnOverlayClick={false}
data-slot="alert-dialog-content"
data-size={local.size}
class={clsx(classes, local.class)}
classList={local.classList}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { DialogSurfaceProps } from "~/components/ui/dialog";
import type { BaseProps } from "~/types";

/**
* AlertDialogContent 复用 DialogSurface 的浮层能力(Portal + Overlay + 定位),
* 因此必须把 `DialogSurfaceProps` 一并带上:`overlayClass` /
* `dismissOnOverlayClick` 在运行时本来就会透传,漏掉类型等于让调用方无法使用。
*/
export type AlertDialogContentProps = DialogSurfaceProps &
BaseProps & {
size?: "default" | "sm";
};
2 changes: 2 additions & 0 deletions src/components/ui/alert-dialog/AlertDialogContent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AlertDialogContent } from "./AlertDialogContent";
export type { AlertDialogContentProps } from "./AlertDialogContent.types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const classes =
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground";

export default classes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createUniqueId, onMount, splitProps } from "solid-js";
import type { AlertDialogDescriptionProps } from "./AlertDialogDescription.types";
import { clsx } from "~/utils";
import classes from "./AlertDialogDescription.styles";
import { useDialogContentContext } from "~/components/ui/dialog";

export const AlertDialogDescription = (props: AlertDialogDescriptionProps) => {
const { setDescriptionID } = useDialogContentContext();

const [local, others] = splitProps(props, ["class", "classList"]);

const id = createUniqueId();

onMount(() => {
setDescriptionID(id);
});

return (
<p
id={id}
data-slot="alert-dialog-description"
class={clsx(classes, local.class)}
classList={local.classList}
{...others}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { BaseProps, PolymorphicProps } from "~/types";

export type AlertDialogDescriptionProps = PolymorphicProps<
"p",
BaseProps,
false
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AlertDialogDescription } from "./AlertDialogDescription";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const classes =
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end";

export default classes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { splitProps } from "solid-js";
import type { AlertDialogFooterProps } from "./AlertDialogFooter.types";
import { clsx } from "~/utils";
import classes from "./AlertDialogFooter.styles";

export const AlertDialogFooter = (props: AlertDialogFooterProps) => {
const [local, others] = splitProps(props, ["class", "classList"]);

return (
<div
data-slot="alert-dialog-footer"
class={clsx(classes, local.class)}
classList={local.classList}
{...others}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { BaseProps, PolymorphicProps } from "~/types";

export type AlertDialogFooterProps = PolymorphicProps<"div", BaseProps, false>;
1 change: 1 addition & 0 deletions src/components/ui/alert-dialog/AlertDialogFooter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AlertDialogFooter } from "./AlertDialogFooter";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const classes =
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]";

export default classes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { splitProps } from "solid-js";
import type { AlertDialogHeaderProps } from "./AlertDialogHeader.types";
import { clsx } from "~/utils";
import classes from "./AlertDialogHeader.styles";

export const AlertDialogHeader = (props: AlertDialogHeaderProps) => {
const [local, others] = splitProps(props, ["class", "classList"]);

return (
<div
data-slot="alert-dialog-header"
class={clsx(classes, local.class)}
classList={local.classList}
{...others}
/>
);
};
Loading
Loading