diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 682eff8..e6369ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -219,8 +219,18 @@ jobs: VERSION: ${{ needs.prepare.outputs.version }} run: | mkdir -p tauri-update - gh release download "v${VERSION}" --pattern latest.json --dir tauri-update - gh api "repos/${GITHUB_REPOSITORY}/releases/tags/v${VERSION}" > tauri-update/release.json + release_id=$( + gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ + --jq ".[] | select(.tag_name == \"v${VERSION}\") | .id" + ) + test -n "${release_id}" + gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" > tauri-update/release.json + asset_id=$(node -p 'require("./tauri-update/release.json").assets.find(({ name }) => name === "latest.json")?.id ?? ""') + test -n "${asset_id}" + gh api \ + -H "Accept: application/octet-stream" \ + "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" \ + > tauri-update/latest.json node scripts/release/normalize-tauri-update.mjs \ --manifest tauri-update/latest.json \ --release tauri-update/release.json \ diff --git a/Cargo.lock b/Cargo.lock index 35fd3a9..9dac26b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1128,7 +1128,7 @@ checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" [[package]] name = "cursor-byok-desktop" -version = "0.1.2" +version = "0.1.3" dependencies = [ "axum", "cursor-server", diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index 1592a76..fe3f47f 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "cursor-byok-desktop", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cursor-byok-desktop", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.8.0", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 61dc3f2..259cbed 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "cursor-byok-desktop", - "version": "0.1.2", + "version": "0.1.3", "description": "Cursor BYOK desktop management application", "type": "module", "scripts": { diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 9f65084..84ca450 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cursor-byok-desktop" -version = "0.1.2" +version = "0.1.3" edition = "2021" publish = false diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 44c55e0..9f04274 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Cursor BYOK", - "version": "0.1.2", + "version": "0.1.3", "identifier": "dev.cursorbyok.desktop", "build": { "beforeDevCommand": "npm run dev", diff --git a/apps/desktop/src/api.ts b/apps/desktop/src/api.ts index c29562a..8226b99 100644 --- a/apps/desktop/src/api.ts +++ b/apps/desktop/src/api.ts @@ -291,7 +291,8 @@ export const api = { importV0049Models: () => request("/models/import-v0049", { method: "POST" }), updateModel: (hash: string, model: ModelInput) => request(`/models/${hash}`, { method: "PUT", body: JSON.stringify(model) }), deleteModel: (hash: string) => request(`/models/${hash}`, { method: "DELETE" }), - testModel: (hash: string) => request(`/models/${hash}/test`, { method: "POST" }), + testModel: (hash: string, testId: string, signal?: AbortSignal) => request(`/models/${hash}/test/${encodeURIComponent(testId)}`, { method: "POST", signal }), + cancelModelTest: (hash: string, testId: string) => request(`/models/${hash}/test/${encodeURIComponent(testId)}`, { method: "DELETE" }), overview: (filter?: { startMs: number; endMs: number; modelHashes?: string[] }) => { const params = new URLSearchParams(); if (filter) { diff --git a/apps/desktop/src/components/charts/ContributionCalendarChart.tsx b/apps/desktop/src/components/charts/ContributionCalendarChart.tsx index aa45b40..f0ca418 100644 --- a/apps/desktop/src/components/charts/ContributionCalendarChart.tsx +++ b/apps/desktop/src/components/charts/ContributionCalendarChart.tsx @@ -1,5 +1,7 @@ import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { init, Rect, type ElementEvent } from "zrender"; +import type { Locale } from "../../i18n/runtime"; +import { useI18n } from "../../i18n/store"; import { Tooltip, type TooltipAnchor } from "../ui/Tooltip"; import styles from "./ContributionCalendarChart.module.scss"; @@ -54,8 +56,6 @@ const CALENDAR_CONFIG = { axisLabelGap: 8, axisLabelWidth: 28, } as const; -const tokenFormatter = new Intl.NumberFormat("zh-CN"); - function parseDate(date: string) { return new Date(`${date}T00:00:00Z`); } @@ -72,9 +72,10 @@ function isCellExtra(value: unknown): value is CellExtra { return typeof value === "object" && value !== null && (value as CellExtra).kind === "calendar-cell"; } -function buildCalendarLayout(data: ContributionDay[]) { +function buildCalendarLayout(data: ContributionDay[], locale: Locale) { if (data.length === 0) return null; + const monthFormatter = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" }); const maximum = Math.max(1, ...data.map(({ tokens }) => tokens)); const firstDate = parseDate(data[0].date); const calendarStart = new Date(firstDate.getTime() - mondayIndex(firstDate) * DAY_IN_MS); @@ -88,20 +89,22 @@ function buildCalendarLayout(data: ContributionDay[]) { const monthTicks = cells.reduce>((ticks, cell) => { const date = parseDate(cell.date); const key = `${date.getUTCFullYear()}-${date.getUTCMonth()}`; - if (ticks.at(-1)?.key !== key) ticks.push({ key, text: `${date.getUTCMonth() + 1}月`, column: cell.column }); + if (ticks.at(-1)?.key !== key) ticks.push({ key, text: monthFormatter.format(date), column: cell.column }); return ticks; }, []); return { cells, columnCount, monthTicks }; } export function ContributionCalendarChart({ data }: ContributionCalendarChartProps) { + const { locale } = useI18n(); const scrollerRef = useRef(null); const canvasRef = useRef(null); const layoutRef = useRef>(null); const scheduleDrawRef = useRef<() => void>(() => undefined); const [tooltip, setTooltip] = useState(null); const [axisLabels, setAxisLabels] = useState([]); - const layout = useMemo(() => buildCalendarLayout(data), [data]); + const layout = useMemo(() => buildCalendarLayout(data, locale), [data, locale]); + const tokenFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]); layoutRef.current = layout; useLayoutEffect(() => { diff --git a/apps/desktop/src/components/cursor/CursorModelCards.tsx b/apps/desktop/src/components/cursor/CursorModelCards.tsx index 9072672..58bf035 100644 --- a/apps/desktop/src/components/cursor/CursorModelCards.tsx +++ b/apps/desktop/src/components/cursor/CursorModelCards.tsx @@ -1,25 +1,26 @@ +import type { IconifyIcon } from "@iconify/react/offline"; import { useEffect, useRef } from "react"; import Sortable from "sortablejs"; import type { Model } from "../../api"; import { Button } from "../ui/Button"; import { Card } from "../ui/Card"; import { Icon } from "../ui/Icon"; -import { claudeIcon, dragIcon, openAiIcon } from "../ui/icons"; +import { claudeIcon, dragIcon, flatColorOrganizationIcon, openAiIcon } from "../ui/icons"; import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult"; import styles from "./CursorSettings.module.scss"; -export function CursorModelCards({ - models, - disabled, - testingModelHashes, - testResults, - onTest, - onEdit, - onDuplicate, - onDelete, - onReorder, -}: { +export type CursorModelGrouping = "flat" | "provider" | "type"; + +export type CursorModelGroup = { + key: string; + label: string; + icon: IconifyIcon; models: Model[]; +}; + +type CursorModelCardsProps = { + models: Model[]; + grouping: CursorModelGrouping; disabled: boolean; testingModelHashes: Set; testResults: Map; @@ -28,7 +29,54 @@ export function CursorModelCards({ onDuplicate: (model: Model) => void; onDelete: (model: Model) => void; onReorder: (modelHashes: string[]) => void; -}) { +}; + +type ModelGridProps = Omit & { + sortable: boolean; +}; + +export function cursorModelGroups(models: Model[], grouping: Exclude): CursorModelGroup[] { + const groups = new Map(); + for (const model of models) { + const descriptor = grouping === "provider" ? providerGroup(model) : typeGroup(model); + const group = groups.get(descriptor.key); + if (group) { + group.models.push(model); + } else { + groups.set(descriptor.key, { ...descriptor, models: [model] }); + } + } + return [...groups.values()]; +} + +export function CursorModelCards(props: CursorModelCardsProps) { + if (props.grouping === "flat") return
+ +
; + + return
+ {cursorModelGroups(props.models, props.grouping).map((group) =>
+
+ + {group.label} +
+ +
)} +
; +} + +function ModelGrid({ + models, + sortable: sortableEnabled, + disabled, + testingModelHashes, + testResults, + onTest, + onEdit, + onDuplicate, + onDelete, + onReorder, +}: ModelGridProps) { const grid = useRef(null); const sortable = useRef(null); const currentModels = useRef(models); @@ -37,7 +85,7 @@ export function CursorModelCards({ reorder.current = onReorder; useEffect(() => { - if (!grid.current) return; + if (!sortableEnabled || !grid.current) return; sortable.current = Sortable.create(grid.current, { animation: 160, dataIdAttr: "data-model-hash", @@ -72,7 +120,7 @@ export function CursorModelCards({ sortable.current?.destroy(); sortable.current = null; }; - }, []); + }, [sortableEnabled]); useEffect(() => { sortable.current?.option("disabled", disabled); @@ -84,14 +132,14 @@ export function CursorModelCards({ const result = testResults.get(model.model_hash); const testing = testingModelHashes.has(model.model_hash); return - + }
- {model.display_name} - {model.model_id} + {model.display_name} + {model.model_id}
@@ -102,7 +150,7 @@ export function CursorModelCards({
- + @@ -112,3 +160,27 @@ export function CursorModelCards({ })}
; } + +function providerGroup(model: Model) { + const label = providerDomain(model.base_url); + return { key: label, label, icon: flatColorOrganizationIcon }; +} + +function providerDomain(baseUrl: string) { + const value = baseUrl.trim(); + try { + return new URL(value).hostname.toLowerCase() || value; + } catch { + try { + return new URL(`https://${value}`).hostname.toLowerCase() || value; + } catch { + return value; + } + } +} + +function typeGroup(model: Model) { + if (model.type === "anthropic") return { key: "anthropic", label: "Anthropic", icon: claudeIcon }; + if (model.openai_endpoint === "/v1/chat/completions") return { key: "openai-chat", label: "OpenAI Chat", icon: openAiIcon }; + return { key: "openai-responses", label: "OpenAI Responses", icon: openAiIcon }; +} diff --git a/apps/desktop/src/components/cursor/CursorModelEditor.tsx b/apps/desktop/src/components/cursor/CursorModelEditor.tsx index b3c14c5..2618faa 100644 --- a/apps/desktop/src/components/cursor/CursorModelEditor.tsx +++ b/apps/desktop/src/components/cursor/CursorModelEditor.tsx @@ -26,7 +26,7 @@ export const emptyCursorModelDraft = (): CursorModelDraft => ({ base_url: "", use_full_url: false, api_key: "", - tooltip_data: t("备注"), + tooltip_data: "", model_id: "", reasoning_effort: null, openai_endpoint: "/v1/responses", @@ -142,17 +142,17 @@ export function CursorModelEditor({ draft, modelOptions, discovering, onChange, setModel({ api_key: event.target.value })} /> {discovering ? t("获取中…") : t("获取模型")}} onChange={(model_id) => setModel({ model_id, display_name: draft.model.display_name || model_id })} /> - setModel({ display_name: event.target.value })} /> - setModel({ tooltip_data: event.target.value })} /> + setModel({ display_name: event.target.value })} /> + setModel({ tooltip_data: event.target.value })} /> - setModel({ context_window_tokens: numberValue(event.target.value) })} /> + setModel({ context_window_tokens: numberValue(event.target.value) })} /> {draft.model.type === "openai" ? <> - setModel({ max_completion_tokens: numberValue(event.target.value) })} /> + setModel({ max_completion_tokens: numberValue(event.target.value) })} /> setModel({ anthropic_thinking_effort })} /> - setModel({ thinking_budget_tokens: numberValue(event.target.value) })} /> + setModel({ thinking_budget_tokens: numberValue(event.target.value) })} /> } {t("测试中…")}
; if (!state) return
{t("未测试")}
; + if (state.status === "cancelled") return
{t("测试已取消")}
; const success = state.status === "success"; const summary = success diff --git a/apps/desktop/src/components/cursor/CursorSettings.module.scss b/apps/desktop/src/components/cursor/CursorSettings.module.scss index a90eaf1..7dbe326 100644 --- a/apps/desktop/src/components/cursor/CursorSettings.module.scss +++ b/apps/desktop/src/components/cursor/CursorSettings.module.scss @@ -5,6 +5,29 @@ gap: 16px; } +.groupActions { + display: flex; + align-items: center; + overflow: hidden; + border-radius: 5px; + + button { + height: 25px; + padding: 0 8px; + color: var(--vscode-descriptionForeground); + background: transparent; + border: 0; + border-right: 1px solid var(--vscode-sideBar-border); + font-size: type.$font-size-xs; + white-space: nowrap; + + &:last-child { border-right: 0; } + &:hover:not(:disabled) { color: var(--vscode-foreground); background: var(--vscode-list-hoverBackground); } + &[aria-pressed="true"] { color: var(--vscode-list-activeSelectionForeground); background: var(--vscode-list-activeSelectionBackground); } + &:disabled { opacity: 0.5; cursor: not-allowed; } + } +} + .gateActions { display: flex; align-items: center; @@ -39,13 +62,34 @@ display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; + +} +.modelGroups { + display: flex; + flex-direction: column; + gap: 20px; + padding-top: 10px; +} +.modelGroup { + display: flex; + flex-direction: column; + gap: 10px; +} +.modelGroupHeader { + display: flex; + align-items: center; + gap: 6px; + color: var(--vscode-foreground); + font-size: type.$font-size-base; + opacity: 0.6; + padding-left: 6px; } .modelCard { position: relative; padding: 16px; } .modelCardContent { - height: 150px; + min-height: 120px; display: flex; flex-direction: column; justify-content: space-between; @@ -64,19 +108,19 @@ flex-direction: column; gap: 4px; - strong, - span { + .modelCardNameText, + .modelCardModelId { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - strong { + .modelCardNameText { color: var(--vscode-foreground); font-size: type.$font-size-base; } - span { + .modelCardModelId { color: var(--vscode-descriptionForeground); - font-size: type.$font-size-xs; + font-size: type.$font-size-2xs; } } .modelTypeBadge { diff --git a/apps/desktop/src/components/layout/PageContent.module.scss b/apps/desktop/src/components/layout/PageContent.module.scss index 0d14941..dd14014 100644 --- a/apps/desktop/src/components/layout/PageContent.module.scss +++ b/apps/desktop/src/components/layout/PageContent.module.scss @@ -15,8 +15,8 @@ top: var(--app-content-top); left: max(var(--app-page-padding), calc((100% - var(--app-page-max-width)) / 2)); margin: 0; - padding-left: 10px; - font-size: type.$font-size-lg; + padding-left:0; + font-size: type.$font-size-xxl; font-weight: bolder; transform: translateY(-150%); -webkit-user-select: none; diff --git a/apps/desktop/src/components/layout/VirtualPage.module.scss b/apps/desktop/src/components/layout/VirtualPage.module.scss index cf01f6b..91ebcbe 100644 --- a/apps/desktop/src/components/layout/VirtualPage.module.scss +++ b/apps/desktop/src/components/layout/VirtualPage.module.scss @@ -26,8 +26,8 @@ top: 0; margin: 0; - padding-left: 10px; - font-size: type.$font-size-lg; + padding-left: 0; + font-size: type.$font-size-xxl; font-weight: bolder; transform: translateY(-150%); } diff --git a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.module.scss b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.module.scss index 31d58a3..dfcf225 100644 --- a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.module.scss +++ b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.module.scss @@ -18,7 +18,7 @@ border-radius: 5px; button { - height: 30px; + height: 25px; padding: 0 8px; color: var(--vscode-descriptionForeground); background: transparent; diff --git a/apps/desktop/src/components/scrollable/ScrollArea.tsx b/apps/desktop/src/components/scrollable/ScrollArea.tsx index 0c5dd24..ccf2afb 100644 --- a/apps/desktop/src/components/scrollable/ScrollArea.tsx +++ b/apps/desktop/src/components/scrollable/ScrollArea.tsx @@ -97,6 +97,7 @@ export const ScrollArea = forwardRef( const hostRef = useRef(null) const localViewportRef = useRef(null) + const localContentRef = useRef(null) const trackRef = useRef(null) const thumbRef = useRef(null) const dragRef = useRef<{ @@ -107,6 +108,7 @@ export const ScrollArea = forwardRef( const { state, api, metricsRef, scrollToNow } = useScrollableModel({ viewportRef: localViewportRef, + contentRef: localContentRef, trackRef, thumbRef, contentHeight, @@ -132,7 +134,10 @@ export const ScrollArea = forwardRef( ) const setContentRef = useCallback( - (node: HTMLDivElement | null) => assignRef(contentRef, node), + (node: HTMLDivElement | null) => { + localContentRef.current = node + assignRef(contentRef, node) + }, [contentRef] ) diff --git a/apps/desktop/src/components/scrollable/useScrollableModel.ts b/apps/desktop/src/components/scrollable/useScrollableModel.ts index 7e5c067..a12ff04 100644 --- a/apps/desktop/src/components/scrollable/useScrollableModel.ts +++ b/apps/desktop/src/components/scrollable/useScrollableModel.ts @@ -33,6 +33,7 @@ export interface ScrollMetrics extends ScrollAreaState { interface UseScrollableModelOptions { viewportRef: RefObject + contentRef: RefObject trackRef: RefObject thumbRef: RefObject contentHeight?: number @@ -131,6 +132,7 @@ function applyThumbStyle( export function useScrollableModel(options: UseScrollableModelOptions) { const { viewportRef, + contentRef, trackRef, thumbRef, contentHeight, @@ -319,28 +321,30 @@ export function useScrollableModel(options: UseScrollableModelOptions) { useEffect(() => { const element = viewportRef.current + const content = contentRef.current const track = trackRef.current - if (!element || !track) return + if (!element || !content || !track) return const resizeObserver = new ResizeObserver((entries) => { - const entry = entries[0] - if (entry) { + const viewportEntry = entries.find((entry) => entry.target === element) + if (viewportEntry) { onViewportResizeRef.current?.({ - width: entry.contentRect.width, - height: entry.contentRect.height, + width: viewportEntry.contentRect.width, + height: viewportEntry.contentRect.height, }) } snapshot(false) }) resizeObserver.observe(element) + resizeObserver.observe(content) resizeObserver.observe(track) snapshot(false) return () => { resizeObserver.disconnect() } - }, [snapshot, trackRef, viewportRef]) + }, [contentRef, snapshot, trackRef, viewportRef]) useEffect(() => { snapshot(false) diff --git a/apps/desktop/src/components/ui/Checkbox.module.scss b/apps/desktop/src/components/ui/Checkbox.module.scss index d315f59..e285641 100644 --- a/apps/desktop/src/components/ui/Checkbox.module.scss +++ b/apps/desktop/src/components/ui/Checkbox.module.scss @@ -8,6 +8,7 @@ color: var(--vscode-foreground); font-size: type.$font-size-xs; cursor: pointer; + flex-shrink: 0; input { position: absolute; diff --git a/apps/desktop/src/components/ui/Controls.module.scss b/apps/desktop/src/components/ui/Controls.module.scss index 3a00096..e7df9e1 100644 --- a/apps/desktop/src/components/ui/Controls.module.scss +++ b/apps/desktop/src/components/ui/Controls.module.scss @@ -34,8 +34,8 @@ } .iconButton { - width: 30px; - height: 30px; + width: 25px; + height: 25px; display: grid; place-items: center; padding: 0; diff --git a/apps/desktop/src/components/ui/FormControls.module.scss b/apps/desktop/src/components/ui/FormControls.module.scss index 36ba7b1..5547c8e 100644 --- a/apps/desktop/src/components/ui/FormControls.module.scss +++ b/apps/desktop/src/components/ui/FormControls.module.scss @@ -47,6 +47,16 @@ .input { height: 34px; padding: 0 10px; + + &[type="number"] { + appearance: textfield; + } + + &[type="number"]::-webkit-inner-spin-button, + &[type="number"]::-webkit-outer-spin-button { + margin: 0; + appearance: none; + } } .secret { diff --git a/apps/desktop/src/components/ui/Modal.module.scss b/apps/desktop/src/components/ui/Modal.module.scss index 8f7c931..073a0f4 100644 --- a/apps/desktop/src/components/ui/Modal.module.scss +++ b/apps/desktop/src/components/ui/Modal.module.scss @@ -34,12 +34,14 @@ box-shadow: 0 20px 50px rgb(0 0 0 / 45%); header { + flex: 0 0 auto; padding: 10px 18px; border-bottom: 1px solid var(--vscode-editorWidget-border); font-size: type.$font-size-base; font-weight: 630; } footer { + flex: 0 0 auto; display: flex; justify-content: flex-end; gap: 8px; @@ -52,6 +54,10 @@ width: min(1080px, calc(100vw - 48px)); } +.fullHeight { + height: calc(100vh - var(--window-header-height) - 24px); +} + .banner { flex: 0 0 auto; padding: 12px 18px; @@ -59,14 +65,8 @@ } .body { - flex: 0 1 auto; + flex: 1 1 auto; min-height: 0; - max-height: calc(100vh - var(--window-header-height) - 112px); -} - -.bodyViewport { - height: auto; - max-height: calc(100vh - var(--window-header-height) - 112px); } .bodyContent { diff --git a/apps/desktop/src/components/ui/Modal.tsx b/apps/desktop/src/components/ui/Modal.tsx index d4a7679..b25e0ee 100644 --- a/apps/desktop/src/components/ui/Modal.tsx +++ b/apps/desktop/src/components/ui/Modal.tsx @@ -12,6 +12,7 @@ type ModalProps = { banner?: ReactNode; busy?: boolean; wide?: boolean; + fullHeight?: boolean; role?: "dialog" | "alertdialog"; ariaDescribedBy?: string; initialFocus?: "first" | "submit"; @@ -36,7 +37,7 @@ function focusableElements(root: HTMLElement) { .filter((element) => element.getClientRects().length > 0); } -export function Modal({ id, open, title, children, banner, busy, wide, role = "dialog", ariaDescribedBy, initialFocus = "first", onClose, onSubmit, secondaryAction, closeLabel = t("取消"), submitLabel = t("保存") }: ModalProps) { +export function Modal({ id, open, title, children, banner, busy, wide, fullHeight, role = "dialog", ariaDescribedBy, initialFocus = "first", onClose, onSubmit, secondaryAction, closeLabel = t("取消"), submitLabel = t("保存") }: ModalProps) { const dialog = useRef(null); const submitButton = useRef(null); const closeRef = useRef(onClose); @@ -87,10 +88,10 @@ export function Modal({ id, open, title, children, banner, busy, wide, role = "d if (!open) return null; return createPortal(