From 8f53f806293dfc69781c65e561760c17f4437423 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:45 +0000 Subject: [PATCH 1/9] feat(yasgui): in-app dialogs and toasts instead of alert, confirm and prompt Native dialogs block the page, ignore the theme and are suppressed in sandboxed iframes, where confirm() returns false and the unsaved-changes flow silently loses work. dialogs.ts adds showAlert, showConfirm, showChoice, showCopyValue and showToast, rendered inside the Yasgui root with focus trapping, Escape to cancel and focus returned to the opener. All 41 alert/confirm/prompt calls use them; save-modal validation is shown on the field itself. Also fixes saving a Git query after a stale version tag: when the automatic retry succeeded, the code fell through to the conflict error and skipped updating the saved-version metadata. --- packages/yasgui/src/CommandPalette.ts | 3 +- packages/yasgui/src/Tab.ts | 123 ++++---- packages/yasgui/src/TabContextMenu.ts | 5 +- packages/yasgui/src/TabSettingsModal.ts | 69 +++-- packages/yasgui/src/ai/AiSettingsForm.ts | 12 +- packages/yasgui/src/ai/aiSettings.ts | 2 +- packages/yasgui/src/dialogs.scss | 228 ++++++++++++++ packages/yasgui/src/dialogs.ts | 287 ++++++++++++++++++ .../src/queryManagement/QueryBrowser.ts | 3 +- .../queryManagement/SaveManagedQueryModal.ts | 22 +- 10 files changed, 660 insertions(+), 94 deletions(-) create mode 100644 packages/yasgui/src/dialogs.scss create mode 100644 packages/yasgui/src/dialogs.ts diff --git a/packages/yasgui/src/CommandPalette.ts b/packages/yasgui/src/CommandPalette.ts index e1573115..8b932cc9 100644 --- a/packages/yasgui/src/CommandPalette.ts +++ b/packages/yasgui/src/CommandPalette.ts @@ -1,3 +1,4 @@ +import { showToast } from "./dialogs"; import { addClass } from "@matdata/yasgui-utils"; import type { Yasgui } from "./"; import { icon, IconName, modKeyLabel } from "./icons"; @@ -184,7 +185,7 @@ export default class CommandPalette { this.close(false); Promise.resolve() .then(() => item.run()) - .catch((error) => window.alert(error?.message || String(error))); + .catch((error) => showToast(error?.message || String(error), { host: this.yasgui.rootEl, kind: "error" })); } /** List the saved queries of every workspace, at most once a minute */ diff --git a/packages/yasgui/src/Tab.ts b/packages/yasgui/src/Tab.ts index 5f91f898..e3e071fe 100644 --- a/packages/yasgui/src/Tab.ts +++ b/packages/yasgui/src/Tab.ts @@ -1,3 +1,4 @@ +import { showAlert, showChoice } from "./dialogs"; import { EventEmitter } from "events"; import { addClass, removeClass, getAsValue } from "@matdata/yasgui-utils"; import { TabListEl } from "./TabElements"; @@ -241,7 +242,7 @@ export class Tab extends EventEmitter { const workspace = this.yasgui.persistentConfig.getWorkspace(meta.workspaceId); if (!workspace) { - window.alert("Selected workspace no longer exists"); + void showAlert("Selected workspace no longer exists", { host: this.yasgui.rootEl }); return; } @@ -263,35 +264,38 @@ export class Tab extends EventEmitter { await backend.writeQuery(queryId, this.getQueryTextForSave(), { expectedVersionTag }); } catch (e) { const err = asWorkspaceBackendError(e); - if (err.code === "CONFLICT") { - if (meta.backendType === "git") { - // Best-effort self-heal: some providers use a file sha for optimistic concurrency. - // If our stored version tag is stale/incorrect but the remote content is unchanged, - // refresh the tag and retry once. - try { - const latest = await backend.readQuery(queryId); - const latestHash = hashQueryText(latest.queryText); - if (meta.lastSavedTextHash && meta.lastSavedTextHash === latestHash && latest.versionTag) { - await backend.writeQuery(queryId, this.getQueryTextForSave(), { expectedVersionTag: latest.versionTag }); - } else { - window.alert( - "Save conflict. Resolve the conflict externally (e.g., pull/rebase/merge) and then try saving again.", - ); - return; - } - } catch { - window.alert( - "Save conflict. Resolve the conflict externally (e.g., pull/rebase/merge) and then try saving again.", - ); - return; - } - } else { - window.alert("Save conflict. Refresh the query and try again."); + if (err.code !== "CONFLICT") { + void showAlert(err.message, { host: this.yasgui.rootEl, title: "Could not save the query" }); + return; + } + if (meta.backendType !== "git") { + void showAlert("Someone else changed this query since you opened it. Refresh the query and try again.", { + host: this.yasgui.rootEl, + title: "Save conflict", + }); + return; + } + // Best-effort self-heal: some providers use a file sha for optimistic concurrency. + // If our stored version tag is stale/incorrect but the remote content is unchanged, + // refresh the tag and retry once. + const gitConflict = () => + showAlert( + "The file changed in the repository since you opened it. Resolve the conflict there (pull, rebase or merge), then save again.", + { host: this.yasgui.rootEl, title: "Save conflict" }, + ); + try { + const latest = await backend.readQuery(queryId); + const latestHash = hashQueryText(latest.queryText); + if (!(meta.lastSavedTextHash && meta.lastSavedTextHash === latestHash && latest.versionTag)) { + void gitConflict(); return; } + await backend.writeQuery(queryId, this.getQueryTextForSave(), { expectedVersionTag: latest.versionTag }); + // Saved on the retry: carry on as after a normal save + } catch { + void gitConflict(); + return; } - window.alert(err.message); - return; } const read = await backend.readQuery(queryId); @@ -309,7 +313,9 @@ export class Tab extends EventEmitter { public async saveAsManagedQuery(): Promise { if (getWritableWorkspaces(this.yasgui.persistentConfig.getWorkspaces()).length === 0) { - window.alert("There is no workspace to save to. Read-only workspaces cannot be saved to."); + void showAlert("There is no workspace to save to. Read-only workspaces cannot be saved to.", { + host: this.yasgui.rootEl, + }); return; } @@ -343,11 +349,11 @@ export class Tab extends EventEmitter { const workspace = this.yasgui.persistentConfig.getWorkspace(result.workspaceId); if (!workspace) { - window.alert("Selected workspace no longer exists"); + void showAlert("Selected workspace no longer exists", { host: this.yasgui.rootEl }); return; } if (isWorkspaceReadOnly(workspace)) { - window.alert("This workspace is read-only"); + void showAlert("This workspace is read-only", { host: this.yasgui.rootEl }); return; } @@ -389,7 +395,7 @@ export class Tab extends EventEmitter { } catch (e) { modal.notifySaveComplete(); const err = asWorkspaceBackendError(e); - window.alert(err.message); + void showAlert(err.message, { host: this.yasgui.rootEl }); } } @@ -547,19 +553,26 @@ export class Tab extends EventEmitter { public close() { if (this.isManagedQueryTab() && this.hasUnsavedManagedChanges()) { - const wantsSave = window.confirm("This managed query has unsaved changes. Save before closing?"); - if (wantsSave) { - void this.saveManagedQueryOrSaveAsManagedQuery().then(() => { - // Only close if we actually saved (metadata updated so no longer dirty) - if (!this.hasUnsavedManagedChanges()) { - this.closeNow(); - } - }); - return; - } - - const discard = window.confirm("Discard changes and close the tab?"); - if (!discard) return; + void showChoice( + "This saved query has unsaved changes.", + [ + { label: "Cancel", value: "cancel" }, + { label: "Discard changes", value: "discard", kind: "danger" }, + { label: "Save and close", value: "save", kind: "primary" }, + ], + "cancel", + { host: this.yasgui.rootEl, title: `Close “${this.getName()}”?` }, + ).then((choice) => { + if (choice === "discard") { + this.closeNow(); + } else if (choice === "save") { + return this.saveManagedQueryOrSaveAsManagedQuery().then(() => { + // Only close if we actually saved (metadata updated so no longer dirty) + if (!this.hasUnsavedManagedChanges()) this.closeNow(); + }); + } + }); + return; } this.closeNow(); @@ -1142,7 +1155,7 @@ export class Tab extends EventEmitter { const workspace = this.yasgui.persistentConfig.getWorkspace(meta.workspaceId); if (!workspace) { - window.alert("Selected workspace no longer exists"); + void showAlert("Selected workspace no longer exists", { host: this.yasgui.rootEl }); return; } @@ -1158,7 +1171,7 @@ export class Tab extends EventEmitter { const queryId = this.getManagedQueryIdFromMetadata(meta); if (!queryId) return; if (!backend.renameQuery) { - window.alert("This workspace does not support renaming queries"); + void showAlert("This workspace does not support renaming queries", { host: this.yasgui.rootEl }); return; } @@ -1169,7 +1182,7 @@ export class Tab extends EventEmitter { this.yasgui.queryBrowser.invalidateAndRefresh(meta.workspaceId); } catch (e) { const err = asWorkspaceBackendError(e); - window.alert(err.message); + void showAlert(err.message, { host: this.yasgui.rootEl }); } finally { this.getTabListEl().setAsRenaming(false); } @@ -1204,7 +1217,9 @@ export class Tab extends EventEmitter { }); if (!backend.deleteQuery) { - window.alert("This workspace does not support deleting queries, so the old file could not be removed."); + void showAlert("This workspace does not support deleting queries, so the old file could not be removed.", { + host: this.yasgui.rootEl, + }); } else { await backend.deleteQuery(oldPath); } @@ -1224,7 +1239,7 @@ export class Tab extends EventEmitter { this.yasgui.queryBrowser.invalidateAndRefresh(meta.workspaceId); } catch (e) { const err = asWorkspaceBackendError(e); - window.alert(err.message); + void showAlert(err.message, { host: this.yasgui.rootEl }); } finally { this.getTabListEl().setAsRenaming(false); } @@ -1355,15 +1370,17 @@ export class Tab extends EventEmitter { } catch (error) { console.error("Failed to refresh OAuth 2.0 token:", error); // Token refresh failed, user needs to re-authenticate - alert( - "Your OAuth 2.0 session has expired and could not be refreshed. Please re-authenticate by clicking the Settings button (gear icon) and selecting the SPARQL Endpoints tab.", + void showAlert( + "Your OAuth 2.0 session for this endpoint has expired and could not be refreshed. Sign in again under Settings, Endpoints.", + { host: this.yasgui.rootEl, title: "Sign in again" }, ); return false; } } else { // No refresh token available, user needs to re-authenticate - alert( - "Your OAuth 2.0 session has expired. Please re-authenticate by clicking the Settings button (gear icon) and selecting the SPARQL Endpoints tab.", + void showAlert( + "Your OAuth 2.0 session for this endpoint has expired. Sign in again under Settings, Endpoints.", + { host: this.yasgui.rootEl, title: "Sign in again" }, ); return false; } diff --git a/packages/yasgui/src/TabContextMenu.ts b/packages/yasgui/src/TabContextMenu.ts index 432bcd2f..de828244 100644 --- a/packages/yasgui/src/TabContextMenu.ts +++ b/packages/yasgui/src/TabContextMenu.ts @@ -1,3 +1,4 @@ +import { showCopyValue } from "./dialogs"; import { addClass } from "@matdata/yasgui-utils"; import { default as Yasgui, getRandomId } from "./"; import Tab from "./Tab"; @@ -127,7 +128,7 @@ export default class TabContextMenu { this.saveAsRqFileEl.onclick = () => { if (!tab) return; - tab.downloadAsRqFile(); + tab.downloadAsRqFile().catch((error) => console.error("Failed to download the query", error)); this.closeConfigMenu(); }; @@ -150,7 +151,7 @@ export default class TabContextMenu { try { await navigator.clipboard.writeText(uri); } catch { - window.prompt("Copy this URI:", uri); + void showCopyValue("Copy the URI of this query", uri, { host: this.yasgui.rootEl }); } this.closeConfigMenu(); }; diff --git a/packages/yasgui/src/TabSettingsModal.ts b/packages/yasgui/src/TabSettingsModal.ts index 3f35ceb2..d5c9d3b0 100644 --- a/packages/yasgui/src/TabSettingsModal.ts +++ b/packages/yasgui/src/TabSettingsModal.ts @@ -1,3 +1,4 @@ +import { showAlert, showConfirm, showToast } from "./dialogs"; import { addClass, removeClass } from "@matdata/yasgui-utils"; import "./TabSettingsModal.scss"; import Tab from "./Tab"; @@ -473,10 +474,16 @@ export default class TabSettingsModal { onDeleteRequested: (workspaceId) => { const w = this.tab.yasgui.persistentConfig.getWorkspace(workspaceId); const name = w?.label || workspaceId; - if (confirm(`Delete workspace "${name}"? This only removes the local configuration.`)) { + void showConfirm("This only removes the workspace from this browser. Its queries stay where they are.", { + host: this.tab.yasgui.rootEl, + title: `Remove workspace “${name}”?`, + confirmLabel: "Remove", + danger: true, + }).then((confirmed) => { + if (!confirmed) return; this.tab.yasgui.persistentConfig.deleteWorkspace(workspaceId); form.render(); - } + }); }, }); @@ -1009,11 +1016,17 @@ export default class TabSettingsModal { deleteButton.textContent = "Delete"; addClass(deleteButton, "deleteEndpointButton"); deleteButton.onclick = () => { - if (confirm(`Delete endpoint "${config.endpoint}"?`)) { + void showConfirm(config.endpoint, { + host: this.tab.yasgui.rootEl, + title: "Delete this endpoint and its saved credentials?", + confirmLabel: "Delete", + danger: true, + }).then((confirmed) => { + if (!confirmed) return; this.tab.yasgui.persistentConfig.deleteEndpointConfig(config.endpoint); this.renderEndpointsList(container); this.tab.refreshEndpointButtons(); - } + }); }; actionsCell.appendChild(deleteButton); addClass(actionsCell, "centerCell"); @@ -1050,28 +1063,30 @@ export default class TabSettingsModal { const endpoint = endpointInput.value.trim(); if (!endpoint) { - alert("Please enter an endpoint URL."); + void showAlert("Please enter an endpoint URL.", { host: this.tab.yasgui.rootEl }); return; } // Validate URL format // Check for supported protocol first if (!/^https?:\/\//i.test(endpoint)) { - alert("Endpoint URL must start with http:// or https://"); + void showAlert("Endpoint URL must start with http:// or https://", { host: this.tab.yasgui.rootEl }); return; } try { new URL(endpoint); } catch (e) { // Show the error message if available, otherwise a generic one - alert(e instanceof Error && e.message ? "Malformed URL: " + e.message : "Please enter a valid URL."); + void showAlert(e instanceof Error && e.message ? "Malformed URL: " + e.message : "Please enter a valid URL.", { + host: this.tab.yasgui.rootEl, + }); return; } // Check if endpoint already exists const existing = this.tab.yasgui.persistentConfig.getEndpointConfig(endpoint); if (existing) { - alert("This endpoint is already in the list."); + void showAlert("This endpoint is already in the list.", { host: this.tab.yasgui.rootEl }); return; } @@ -1442,7 +1457,7 @@ export default class TabSettingsModal { }); closeModalAndRefresh(); } else { - alert("Please enter both username and password."); + void showAlert("Please enter both username and password.", { host: this.tab.yasgui.rootEl }); } } else if (authType === "bearer") { const token = tokenInput.value.trim(); @@ -1456,7 +1471,7 @@ export default class TabSettingsModal { }); closeModalAndRefresh(); } else { - alert("Please enter a bearer token."); + void showAlert("Please enter a bearer token.", { host: this.tab.yasgui.rootEl }); } } else if (authType === "apiKey") { const headerName = headerNameInput.value.trim(); @@ -1472,7 +1487,7 @@ export default class TabSettingsModal { }); closeModalAndRefresh(); } else { - alert("Please enter both header name and API key."); + void showAlert("Please enter both header name and API key.", { host: this.tab.yasgui.rootEl }); } } else if (authType === "oauth2") { const clientId = clientIdInput.value.trim(); @@ -1482,7 +1497,9 @@ export default class TabSettingsModal { const scope = scopeInput.value.trim(); if (!clientId || !authorizationEndpoint || !tokenEndpoint) { - alert("Please enter Client ID, Authorization Endpoint, and Token Endpoint."); + void showAlert("Please enter Client ID, Authorization Endpoint, and Token Endpoint.", { + host: this.tab.yasgui.rootEl, + }); return; } @@ -1537,11 +1554,11 @@ export default class TabSettingsModal { }); closeModalAndRefresh(); - alert("OAuth 2.0 authentication successful!"); + showToast("Signed in with OAuth 2.0", { host: this.tab.yasgui.rootEl, kind: "success" }); }) .catch((error) => { console.error("OAuth 2.0 authentication failed:", error); - alert("OAuth 2.0 authentication failed: " + error.message); + void showAlert(error.message, { host: this.tab.yasgui.rootEl, title: "OAuth 2.0 sign-in failed" }); saveButton.disabled = false; saveButton.textContent = "Save & Authenticate"; }); @@ -2005,8 +2022,12 @@ export default class TabSettingsModal { const parsedConfig = ConfigExportImport.parseFromTurtle(turtleContent); // Confirm with user before importing - const confirmMsg = `This will replace your current configuration with ${parsedConfig.tabs?.length || 0} tab(s). Continue?`; - if (!confirm(confirmMsg)) { + const tabCount = parsedConfig.tabs?.length || 0; + const confirmed = await showConfirm( + `Your current tabs and settings are replaced by the ${tabCount} tab${tabCount === 1 ? "" : "s"} and settings of the file.`, + { host: this.tab.yasgui.rootEl, title: "Import this configuration?", confirmLabel: "Import" }, + ); + if (!confirmed) { return; } @@ -2152,7 +2173,7 @@ export default class TabSettingsModal { const clearStorageButton = document.createElement("button"); clearStorageButton.textContent = "Clear Persistent Storage"; addClass(clearStorageButton, "dangerButton"); - clearStorageButton.onclick = () => this.clearPersistentStorage(); + clearStorageButton.onclick = () => void this.clearPersistentStorage(); storageSection.appendChild(clearStorageButton); container.appendChild(storageSection); @@ -2179,16 +2200,10 @@ export default class TabSettingsModal { return linkContainer; } - private clearPersistentStorage() { - const confirmed = confirm( - "Are you sure you want to clear all persistent storage?\n\n" + - "This will delete:\n" + - "- All saved tabs and queries\n" + - "- Endpoint configurations and history\n" + - "- Authentication settings\n" + - "- Editor preferences\n" + - "- Prefixes\n\n" + - "This action cannot be undone and the page will reload.", + private async clearPersistentStorage() { + const confirmed = await showConfirm( + "This deletes all saved tabs and queries, endpoints and their credentials, endpoint history, editor preferences and prefixes from this browser, then reloads the page. It cannot be undone.", + { host: this.tab.yasgui.rootEl, title: "Clear all stored data?", confirmLabel: "Clear and reload", danger: true }, ); if (confirmed) { diff --git a/packages/yasgui/src/ai/AiSettingsForm.ts b/packages/yasgui/src/ai/AiSettingsForm.ts index c47656af..a492b979 100644 --- a/packages/yasgui/src/ai/AiSettingsForm.ts +++ b/packages/yasgui/src/ai/AiSettingsForm.ts @@ -1,3 +1,4 @@ +import { showConfirm } from "../dialogs"; /** * "AI" page of the settings: the AI services as cards (the site's, the user's own, and "No AI service") with the * default one picked, and a form to add or edit one of the user's own services: a provider, its base URL (filled in, @@ -193,7 +194,7 @@ export default class AiSettingsForm { const remove = el("button", "yasgui-aiSettings__linkButton danger", "Remove"); remove.type = "button"; remove.setAttribute("aria-label", `Remove ${serviceName}`); - remove.addEventListener("click", () => this.removeService(service)); + remove.addEventListener("click", () => void this.removeService(service)); const meta = [ service.model || config.defaultModel || "no model chosen", hostOf(baseUrl), @@ -290,8 +291,13 @@ export default class AiSettingsForm { this.render(); } - private removeService(service: AiServiceSettings) { - if (!confirm(`Remove the AI service “${this.serviceName(service)}” and its API key from this browser?`)) return; + private async removeService(service: AiServiceSettings) { + const confirmed = await showConfirm("Its API key is deleted from this browser too.", { + title: `Remove the AI service “${this.serviceName(service)}”?`, + confirmLabel: "Remove", + danger: true, + }); + if (!confirmed) return; const services = (this.settings().services || []).filter((candidate) => candidate.id !== service.id); const changes: Parameters[0] = { services: services.length ? services : undefined, diff --git a/packages/yasgui/src/ai/aiSettings.ts b/packages/yasgui/src/ai/aiSettings.ts index 78c8b978..25f029f7 100644 --- a/packages/yasgui/src/ai/aiSettings.ts +++ b/packages/yasgui/src/ai/aiSettings.ts @@ -57,7 +57,7 @@ function siteConnection(proxy: AiProxyConfig, index: number): AiConnection { label: proxy.label || hostOf(proxy.url), privacyUrl: proxy.privacyUrl, withCredentials: proxy.withCredentials, - getHeaders: headers ? async () => (typeof headers === "function" ? await headers() : headers) : undefined, + getHeaders: headers ? async () => (typeof headers === "function" ? headers() : headers) : undefined, }; } diff --git a/packages/yasgui/src/dialogs.scss b/packages/yasgui/src/dialogs.scss new file mode 100644 index 00000000..15a04b93 --- /dev/null +++ b/packages/yasgui/src/dialogs.scss @@ -0,0 +1,228 @@ +// In-app alert/confirm dialogs and toasts (dialogs.ts) + +// `.yasgui > div` lays out the tab panels as a column; these layers are not part of it +.yasgui > div.yasgui-dialog:not(.tabsList) { + flex: none; + flex-direction: row; + overflow: visible; + min-height: 0; +} + +.yasgui > div.yasgui-toasts:not(.tabsList) { + flex: none; + flex-direction: column; + overflow: visible; + min-height: 0; +} + +.yasgui-dialog { + position: fixed; + inset: 0; + // Above the settings modal, the What's new dialog and the command palette + z-index: 10100; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 96px 16px 16px; + box-sizing: border-box; + background: var(--yasgui-scrim, rgba(13, 20, 28, 0.32)); + font-family: var(--yasgui-font-family, inherit); + font-size: 13px; + color: var(--yasgui-ink, #1c2633); + + &__box { + width: 440px; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + box-sizing: border-box; + border-radius: 12px; + background: var(--yasgui-surface-raised, #ffffff); + box-shadow: var(--yasgui-shadow-overlay, 0 24px 48px rgba(13, 20, 28, 0.18), 0 0 0 1px rgba(13, 20, 28, 0.06)); + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 600; + letter-spacing: -0.01em; + } + + &__message { + margin: 0; + line-height: 1.5; + color: var(--yasgui-ink-body, #3a4552); + white-space: pre-line; + overflow-wrap: anywhere; + } + + &__field { + width: 100%; + box-sizing: border-box; + padding: 7px 10px; + border: 1px solid var(--yasgui-line, #dfe5ec); + border-radius: var(--yasgui-radius-sm, 6px); + background: var(--yasgui-surface, #f1f4f8); + color: var(--yasgui-ink, #1c2633); + font-family: var(--yasgui-font-mono, monospace); + font-size: 12px; + } + + &__actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + margin-top: 4px; + } + + &__button { + min-height: 32px; + padding: 0 14px; + border: 1px solid var(--yasgui-line, #dfe5ec); + border-radius: var(--yasgui-radius-button, 8px); + background: var(--yasgui-surface-raised, #ffffff); + color: var(--yasgui-ink, #1c2633); + font: inherit; + font-weight: 500; + cursor: pointer; + + &:hover { + background: var(--yasgui-surface-sunken, #edf0f4); + } + + &:focus-visible { + outline: 2px solid var(--yasgui-focus, #044a8c); + outline-offset: 2px; + } + + &--primary { + border-color: transparent; + background: var(--yasgui-brand-fill, #0563bb); + color: var(--yasgui-on-brand, #ffffff); + + &:hover { + background: var(--yasgui-brand-fill-strong, #044a8c); + } + } + + &--danger { + border-color: transparent; + background: var(--yasgui-danger, #b42318); + color: #ffffff; + + &:hover { + filter: brightness(0.92); + background: var(--yasgui-danger, #b42318); + } + } + } +} + +.yasgui-toasts { + position: fixed; + left: 50%; + bottom: 24px; + transform: translateX(-50%); + z-index: 10110; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: max-content; + max-width: calc(100vw - 32px); + pointer-events: none; + font-family: var(--yasgui-font-family, inherit); + font-size: 13px; +} + +.yasgui-toast { + pointer-events: auto; + display: flex; + align-items: center; + gap: 10px; + max-width: 560px; + padding: 8px 8px 8px 12px; + border-radius: 10px; + background: var(--yasgui-surface-raised, #ffffff); + color: var(--yasgui-ink, #1c2633); + box-shadow: var(--yasgui-shadow-overlay, 0 16px 48px rgba(28, 38, 51, 0.22), 0 0 0 1px rgba(28, 38, 51, 0.06)); + animation: yasgui-toast-in 160ms ease-out; + + &__icon { + display: flex; + flex-shrink: 0; + color: var(--yasgui-brand, #0563bb); + } + + &--success &__icon { + color: var(--yasgui-success, #067647); + } + + &--warning &__icon { + color: var(--yasgui-highlight-ink, #6e4d00); + } + + &--error &__icon { + color: var(--yasgui-danger, #b42318); + } + + &__message { + line-height: 1.45; + overflow-wrap: anywhere; + } + + &__action, + &__close { + flex-shrink: 0; + border: none; + border-radius: var(--yasgui-radius-button, 8px); + background: transparent; + font: inherit; + cursor: pointer; + + &:focus-visible { + outline: 2px solid var(--yasgui-focus, #044a8c); + outline-offset: 1px; + } + } + + &__action { + padding: 4px 10px; + color: var(--yasgui-brand, #0563bb); + font-weight: 600; + + &:hover { + background: var(--yasgui-brand-soft, #e3eefa); + } + } + + &__close { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + color: var(--yasgui-ink-muted, #56606c); + + &:hover { + background: var(--yasgui-surface-sunken, #edf0f4); + } + } +} + +@keyframes yasgui-toast-in { + from { + opacity: 0; + transform: translateY(8px); + } +} + +@media (prefers-reduced-motion: reduce) { + .yasgui-toast { + animation: none; + } +} diff --git a/packages/yasgui/src/dialogs.ts b/packages/yasgui/src/dialogs.ts new file mode 100644 index 00000000..36b9af8b --- /dev/null +++ b/packages/yasgui/src/dialogs.ts @@ -0,0 +1,287 @@ +/** + * In-app replacements for window.alert, window.confirm and window.prompt, plus toasts. + * + * Native dialogs block the page, can't follow the theme, and are suppressed in sandboxed iframes (where + * `confirm()` silently returns false). These render inside the Yasgui root, so they get the theme tokens. + */ +import { addClass } from "@matdata/yasgui-utils"; +import { icon } from "./icons"; +import "./dialogs.scss"; + +let defaultHost: HTMLElement | undefined; + +/** The Yasgui root that dialogs open in when a call doesn't name one (the last Yasgui instance created) */ +export function setDialogHost(host: HTMLElement) { + defaultHost = host; +} + +function resolveHost(host?: HTMLElement): HTMLElement { + if (host && host.isConnected) { + return (host.closest(".yasgui") as HTMLElement | null) || host; + } + if (defaultHost && defaultHost.isConnected) return defaultHost; + return (document.querySelector(".yasgui") as HTMLElement | null) || document.body; +} + +export interface DialogOptions { + /** Element inside the Yasgui instance the dialog belongs to (defaults to the last Yasgui instance) */ + host?: HTMLElement; + title?: string; +} + +export interface ConfirmOptions extends DialogOptions { + confirmLabel?: string; + cancelLabel?: string; + /** Style the confirm button as destructive */ + danger?: boolean; +} + +interface DialogButton { + label: string; + value: string; + kind?: "primary" | "danger" | "secondary"; +} + +let dialogCounter = 0; + +function openDialog( + message: string, + buttons: DialogButton[], + options: DialogOptions & { cancelValue: string; extra?: HTMLElement; focus?: HTMLElement }, +): Promise { + const host = resolveHost(options.host); + const previouslyFocused = document.activeElement as HTMLElement | null; + const id = `yasgui-dialog-${++dialogCounter}`; + + const overlay = document.createElement("div"); + addClass(overlay, "yasgui-dialog"); + + const dialog = document.createElement("div"); + addClass(dialog, "yasgui-dialog__box"); + dialog.setAttribute("role", buttons.length > 1 ? "alertdialog" : "dialog"); + dialog.setAttribute("aria-modal", "true"); + dialog.setAttribute("aria-describedby", `${id}-message`); + + if (options.title) { + const title = document.createElement("h2"); + addClass(title, "yasgui-dialog__title"); + title.id = `${id}-title`; + title.textContent = options.title; + dialog.setAttribute("aria-labelledby", title.id); + dialog.appendChild(title); + } + + const body = document.createElement("p"); + addClass(body, "yasgui-dialog__message"); + body.id = `${id}-message`; + body.textContent = message; + dialog.appendChild(body); + + if (options.extra) dialog.appendChild(options.extra); + + const actions = document.createElement("div"); + addClass(actions, "yasgui-dialog__actions"); + dialog.appendChild(actions); + overlay.appendChild(dialog); + + return new Promise((resolve) => { + let done = false; + const finish = (value: string) => { + if (done) return; + done = true; + document.removeEventListener("keydown", onKeydown, true); + overlay.remove(); + if (previouslyFocused && previouslyFocused.isConnected && typeof previouslyFocused.focus === "function") { + previouslyFocused.focus(); + } + resolve(value); + }; + + const buttonEls: HTMLButtonElement[] = []; + for (const button of buttons) { + const el = document.createElement("button"); + el.type = "button"; + addClass(el, "yasgui-dialog__button", `yasgui-dialog__button--${button.kind || "secondary"}`); + el.textContent = button.label; + el.addEventListener("click", () => finish(button.value)); + actions.appendChild(el); + buttonEls.push(el); + } + + const focusables = () => + Array.from( + dialog.querySelectorAll( + "button, input, textarea, select, a[href], [tabindex]:not([tabindex='-1'])", + ), + ); + + const onKeydown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + event.stopPropagation(); + finish(options.cancelValue); + } else if (event.key === "Tab") { + // Keep focus inside the dialog + const items = focusables(); + if (!items.length) return; + const first = items[0]; + const last = items[items.length - 1]; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } + }; + document.addEventListener("keydown", onKeydown, true); + overlay.addEventListener("mousedown", (event) => { + if (event.target === overlay) finish(options.cancelValue); + }); + + host.appendChild(overlay); + const primary = buttonEls.find((_el, i) => buttons[i].kind === "primary" || buttons[i].kind === "danger"); + (options.focus || primary || buttonEls[buttonEls.length - 1])?.focus(); + }); +} + +/** Replacement for window.alert */ +export function showAlert(message: string, options: DialogOptions = {}): Promise { + return openDialog(message, [{ label: "OK", value: "ok", kind: "primary" }], { + ...options, + cancelValue: "ok", + }).then(() => undefined); +} + +/** Replacement for window.confirm: resolves true when the user confirms */ +export function showConfirm(message: string, options: ConfirmOptions = {}): Promise { + return openDialog( + message, + [ + { label: options.cancelLabel || "Cancel", value: "cancel" }, + { label: options.confirmLabel || "OK", value: "confirm", kind: options.danger ? "danger" : "primary" }, + ], + { ...options, cancelValue: "cancel" }, + ).then((value) => value === "confirm"); +} + +/** + * A choice between more than two buttons, such as "Save", "Discard" and "Cancel". Resolves with the value of + * the button that was clicked, or `cancelValue` on Escape. + */ +export function showChoice( + message: string, + choices: Array<{ label: string; value: V; kind?: "primary" | "danger" | "secondary" }>, + cancelValue: V, + options: DialogOptions = {}, +): Promise { + return openDialog(message, choices, { ...options, cancelValue }) as Promise; +} + +/** Replacement for `window.prompt("Copy this:", value)`: shows the value selected, with a Copy button */ +export function showCopyValue(message: string, value: string, options: DialogOptions = {}): Promise { + const field = document.createElement("input"); + field.type = "text"; + field.readOnly = true; + field.value = value; + addClass(field, "yasgui-dialog__field"); + field.setAttribute("aria-label", message); + field.addEventListener("focus", () => field.select()); + const copy = () => + navigator.clipboard + ?.writeText(value) + .then(() => showToast("Copied to clipboard", { kind: "success", host: options.host })) + .catch(() => field.select()); + const promise = openDialog( + message, + [ + { label: "Close", value: "close" }, + { label: "Copy", value: "copy", kind: "primary" }, + ], + { ...options, cancelValue: "close", extra: field, focus: field }, + ).then((result) => { + if (result === "copy") void copy(); + }); + return promise; +} + +export type ToastKind = "info" | "success" | "warning" | "error"; + +export interface ToastOptions { + host?: HTMLElement; + kind?: ToastKind; + /** Milliseconds before it hides; 0 keeps it until closed (default: 4000, or 0 when there is an action) */ + duration?: number; + action?: { label: string; onClick: () => void }; +} + +const TOAST_ICONS: Record[0]> = { + info: "help", + success: "check", + warning: "alert", + error: "alert", +}; + +function toastStack(host: HTMLElement): HTMLElement { + let stack = host.querySelector(":scope > .yasgui-toasts"); + if (!stack) { + stack = document.createElement("div"); + addClass(stack, "yasgui-toasts"); + stack.setAttribute("role", "region"); + stack.setAttribute("aria-label", "Notifications"); + host.appendChild(stack); + } + return stack; +} + +/** A short message at the bottom of the screen, optionally with one action button */ +export function showToast(message: string, options: ToastOptions = {}): { close: () => void } { + const host = resolveHost(options.host); + const kind = options.kind || "info"; + const toast = document.createElement("div"); + addClass(toast, "yasgui-toast", `yasgui-toast--${kind}`); + toast.setAttribute("role", kind === "error" || kind === "warning" ? "alert" : "status"); + + const iconEl = document.createElement("span"); + addClass(iconEl, "yasgui-toast__icon"); + iconEl.innerHTML = icon(TOAST_ICONS[kind], 16); + toast.appendChild(iconEl); + + const text = document.createElement("span"); + addClass(text, "yasgui-toast__message"); + text.textContent = message; + toast.appendChild(text); + + let timer: number | undefined; + const close = () => { + if (timer !== undefined) window.clearTimeout(timer); + toast.remove(); + }; + + if (options.action) { + const action = document.createElement("button"); + action.type = "button"; + addClass(action, "yasgui-toast__action"); + action.textContent = options.action.label; + const onClick = options.action.onClick; + action.addEventListener("click", () => { + close(); + onClick(); + }); + toast.appendChild(action); + } + + const closeBtn = document.createElement("button"); + closeBtn.type = "button"; + addClass(closeBtn, "yasgui-toast__close"); + closeBtn.setAttribute("aria-label", "Dismiss"); + closeBtn.innerHTML = icon("x", 14); + closeBtn.addEventListener("click", close); + toast.appendChild(closeBtn); + + toastStack(host).appendChild(toast); + const duration = options.duration ?? (options.action ? 0 : 4000); + if (duration > 0) timer = window.setTimeout(close, duration); + return { close }; +} diff --git a/packages/yasgui/src/queryManagement/QueryBrowser.ts b/packages/yasgui/src/queryManagement/QueryBrowser.ts index 08444017..5342ed39 100644 --- a/packages/yasgui/src/queryManagement/QueryBrowser.ts +++ b/packages/yasgui/src/queryManagement/QueryBrowser.ts @@ -1,3 +1,4 @@ +import { showCopyValue } from "../dialogs"; import type Yasgui from "../index"; import { addClass, removeClass } from "@matdata/yasgui-utils"; import type { WorkspaceConfig, FolderEntry } from "./types"; @@ -800,7 +801,7 @@ export default class QueryBrowser { try { await navigator.clipboard.writeText(uri); } catch { - window.prompt("Copy this URI:", uri); + void showCopyValue("Copy the URI of this query", uri, { host: this.yasgui.rootEl }); } }), ); diff --git a/packages/yasgui/src/queryManagement/SaveManagedQueryModal.ts b/packages/yasgui/src/queryManagement/SaveManagedQueryModal.ts index e816261d..9a72f605 100644 --- a/packages/yasgui/src/queryManagement/SaveManagedQueryModal.ts +++ b/packages/yasgui/src/queryManagement/SaveManagedQueryModal.ts @@ -416,7 +416,7 @@ export default class SaveManagedQueryModal { const isSparql = workspace?.type === "sparql"; if (!workspaceId) { - window.alert("Please select a workspace"); + this.reportInvalid(this.workspaceSelectEl, "Select a workspace to save to"); return; } @@ -426,7 +426,7 @@ export default class SaveManagedQueryModal { if (isGit) { resolvedFilename = resolvedFilename.replace(/\.(rq|sparql)$/i, ""); if (!resolvedFilename) { - window.alert("Please enter a filename"); + this.reportInvalid(this.filenameEl, "Enter a filename"); return; } // Derive a tab label for convenience. @@ -434,18 +434,18 @@ export default class SaveManagedQueryModal { resolvedName = base || resolvedFilename; } else if (isSparql) { if (!resolvedName) { - window.alert("Please enter a name"); + this.reportInvalid(this.nameEl, "Enter a name"); return; } resolvedFilename = this.suggestFilenameFromName(resolvedName) || normalizeQueryFilename(resolvedName); } else { // Fallback (should not happen): require both. if (!resolvedFilename) { - window.alert("Please enter a filename"); + this.reportInvalid(this.filenameEl, "Enter a filename"); return; } if (!resolvedName) { - window.alert("Please enter a name"); + this.reportInvalid(this.nameEl, "Enter a name"); return; } } @@ -521,12 +521,22 @@ export default class SaveManagedQueryModal { this.nameEl.focus(); } - return await new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } + /** Show a validation message on the field itself, rather than in a separate dialog */ + private reportInvalid(field: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, message: string) { + field.setCustomValidity(message); + field.reportValidity(); + field.focus(); + const reset = () => field.setCustomValidity(""); + field.addEventListener("input", reset, { once: true }); + field.addEventListener("change", reset, { once: true }); + } + private suggestFilenameFromName(name: string): string | undefined { const trimmed = name.trim(); if (!trimmed) return undefined; From 7f092b776ebd72e13f470333bfa3db09e3c68fd4 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:45 +0000 Subject: [PATCH 2/9] fix: keep user data when the browser storage is full Storage.set called the quota callback and then threw anyway, and the Yasgui callback cleared every tab, endpoint and credential. The callback now replaces the throw. Yasgui retries without the query results kept with the tabs and otherwise keeps working from memory with one message; Yasr and Yasqe only skip the write. Quota errors are recognised by name in current browsers, and the expired-key sweep (which parses all of localStorage) runs at most once a minute, with get() checking expiry itself. Also adds Yasqe requestSendsCredentials(), used by the share-link check. --- packages/utils/src/Storage.ts | 83 +++++++++++++------------ packages/yasgui/src/PersistentConfig.ts | 35 +++++++++-- packages/yasqe/src/index.ts | 14 ++++- packages/yasr/src/index.ts | 10 ++- test/unit/storage-test.ts | 46 ++++++++++++++ 5 files changed, 137 insertions(+), 51 deletions(-) create mode 100644 test/unit/storage-test.ts diff --git a/packages/utils/src/Storage.ts b/packages/utils/src/Storage.ts index 29fff422..04485c06 100644 --- a/packages/utils/src/Storage.ts +++ b/packages/utils/src/Storage.ts @@ -9,12 +9,21 @@ export interface ItemWrapper { interface QuotaExceededError extends Error { quotaExceeded?: boolean; } + +/** Sweeping expired keys parses every value in localStorage, so do it at most this often */ +const EXPIRY_SWEEP_INTERVAL_MS = 60 * 1000; +let lastExpirySweep = 0; + export default class Storage { private namespace: string; constructor(namespace: string) { this.namespace = namespace; } - public set(key: string | undefined, val: any, expInSeconds: number, onQuotaExceeded: (e: any) => void) { + /** + * Store a value. When the storage quota is exceeded and `onQuotaExceeded` is given, that callback handles it + * and nothing is thrown; other errors are thrown. + */ + public set(key: string | undefined, val: any, expInSeconds: number, onQuotaExceeded?: (e: any) => void) { if (!store.enabled) return; //this is probably in private mode. Don't run, as we might get Js errors this.removeExpiredKeys(); if (key && val !== undefined) { @@ -28,15 +37,12 @@ export default class Storage { time: new Date().getTime() / 1000, }); } catch (e) { - if (e instanceof Error) { - const quotaExceededError = e as QuotaExceededError; - quotaExceededError.quotaExceeded = isQuotaExceeded(e); - if (quotaExceededError.quotaExceeded && onQuotaExceeded) { - onQuotaExceeded(e); - } else { - throw quotaExceededError; - } + const quotaExceeded = isQuotaExceeded(e); + if (quotaExceeded && onQuotaExceeded) { + onQuotaExceeded(e); + return; } + if (e instanceof Error) (e as QuotaExceededError).quotaExceeded = quotaExceeded; throw e; } } @@ -45,56 +51,51 @@ export default class Storage { if (!store.enabled) return; //this is probably in private mode. Don't run, as we might get Js errors if (key) store.remove(key); } - removeExpiredKeys() { + /** + * Remove the keys whose expiry time has passed. Runs at most once a minute unless `force` is set, since it + * reads and parses everything in localStorage. + */ + removeExpiredKeys(force = false) { if (!store.enabled) return; + const now = Date.now(); + if (!force && now - lastExpirySweep < EXPIRY_SWEEP_INTERVAL_MS) return; + lastExpirySweep = now; store.each((value: ItemWrapper, key: string) => { - if (value && value.exp && new Date().getTime() / 1000 - value.time > value.exp) { + if (value && value.exp && now / 1000 - value.time > value.exp) { this.remove(key); } }); } + /** Remove every key stored in this namespace */ removeAll() { - if (!store.enabled) return; //this is probably in private mode. Don't run, as we might get Js errors + this.removeNamespace(); } removeNamespace() { + if (!store.enabled) return; + const keys: string[] = []; store.each((value: ItemWrapper, key: string) => { - if (value.namespace && value.namespace === this.namespace) this.remove(key); + if (value && value.namespace && value.namespace === this.namespace) keys.push(key); }); + keys.forEach((key) => this.remove(key)); } get(key?: string): V | undefined { if (!store.enabled) return; //this is probably in private mode. Don't run, as we might get Js errors if (!key) return; this.removeExpiredKeys(); - if (key) { - var info: ItemWrapper = store.get(key); - if (!info) { - return; - } - return info.val; - } else { + const info: ItemWrapper | undefined = store.get(key); + if (!info) return; + if (info.exp && Date.now() / 1000 - info.time > info.exp) { + this.remove(key); return; } + return info.val; } } -function isQuotaExceeded(e: any) { - var quotaExceeded = false; - if (e) { - if (e.code) { - switch (e.code) { - case 22: - quotaExceeded = true; - break; - case 1014: - // Firefox - if (e.name === "NS_ERROR_DOM_QUOTA_REACHED") { - quotaExceeded = true; - } - break; - } - } else if (e.number === -2147024882) { - // Internet Explorer 8 - quotaExceeded = true; - } - } - return quotaExceeded; + +export function isQuotaExceeded(e: any): boolean { + if (!e) return false; + // Current browsers + if (e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED") return true; + // Older WebKit/Blink (22) and Firefox (1014) + return e.code === 22 || e.code === 1014; } diff --git a/packages/yasgui/src/PersistentConfig.ts b/packages/yasgui/src/PersistentConfig.ts index d4b4bdc2..3f68ac23 100644 --- a/packages/yasgui/src/PersistentConfig.ts +++ b/packages/yasgui/src/PersistentConfig.ts @@ -1,3 +1,4 @@ +import { showToast } from "./dialogs"; import { Storage as YStorage } from "@matdata/yasgui-utils"; import Yasgui, { EndpointButton, EndpointConfig } from "./"; import * as Tab from "./Tab"; @@ -141,6 +142,35 @@ export default class PersistentConfig { this.handleLocalStorageQuotaFull, ); } + + /** + * The browser storage is full. Query results kept with the tabs are by far the largest part, so save the + * configuration without them; if even that doesn't fit, keep working from memory and tell the user, without + * deleting anything they saved. + */ + private handleLocalStorageQuotaFull = (_e: any) => { + const tabConfig: PersistedJson["tabConfig"] = {}; + for (const id in this.persistedJson.tabConfig) { + const tab = this.persistedJson.tabConfig[id]; + tabConfig[id] = tab && tab.yasr ? { ...tab, yasr: { ...tab.yasr, response: undefined } } : tab; + } + const withoutResponses: PersistedJson = { ...this.persistedJson, lastClosedTab: undefined, tabConfig }; + let saved = true; + this.storage.set(this.storageId, withoutResponses, this.yasgui.config.persistencyExpire, () => (saved = false)); + if (saved) { + console.warn("Local storage is full: saved the configuration without the query results of the tabs"); + return; + } + console.warn("Local storage is full: the configuration could not be saved"); + if (!this.warnedStorageFull) { + this.warnedStorageFull = true; + showToast( + "Your browser's storage for this site is full, so changes to tabs and settings are not being saved. Close tabs you don't need, or export your configuration from the settings.", + { host: this.yasgui.rootEl, kind: "error", duration: 0 }, + ); + } + }; + private warnedStorageFull = false; private fromStorage(): PersistedJson { const stored = this.storage.get(this.storageId); this.hadStoredConfig = !!stored; @@ -154,11 +184,6 @@ export default class PersistentConfig { return this.persistedJson; } - private handleLocalStorageQuotaFull(_e: any) { - console.warn("Localstorage quota exceeded. Clearing all Matgui configurations"); - PersistentConfig.clear(); - } - /** False on a first visit, before anything was stored */ public hasStoredConfig(): boolean { return this.hadStoredConfig; diff --git a/packages/yasqe/src/index.ts b/packages/yasqe/src/index.ts index ab835b8c..9a75dd96 100644 --- a/packages/yasqe/src/index.ts +++ b/packages/yasqe/src/index.ts @@ -1614,9 +1614,9 @@ export class Yasqe extends EditorFacade { addClass(this.queryBtn, "state-normal"); } } + /** The browser storage is full: keep the query in the editor, rather than deleting every saved query */ public handleLocalStorageQuotaFull(_e: any) { - console.warn("Localstorage quota exceeded. Clearing all queries"); - Yasqe.clearStorage(); + console.warn("Local storage quota exceeded: the query was not saved"); } public saveQuery() { @@ -2354,6 +2354,16 @@ export class Yasqe extends EditorFacade { return ajaxConfig ? Sparql.hasAuthenticationCredentials(ajaxConfig) : false; } + /** + * True when a request from this editor would carry credentials: an Authorization or API key header, or + * cookies (`withCredentials`) + */ + public requestSendsCredentials(config?: Sparql.YasqeAjaxConfig): boolean { + const ajaxConfig = Sparql.getAjaxConfig(this, config); + if (!ajaxConfig) return false; + return Sparql.hasAuthenticationCredentials(ajaxConfig) || !!ajaxConfig.withCredentials; + } + /** * True while a query started from this editor is running */ diff --git a/packages/yasr/src/index.ts b/packages/yasr/src/index.ts index 372f2fc3..52f90568 100644 --- a/packages/yasr/src/index.ts +++ b/packages/yasr/src/index.ts @@ -750,9 +750,9 @@ export class Yasr extends EventEmitter { } } + /** The browser storage is full: keep working without persisting, rather than deleting what was saved */ public handleLocalStorageQuotaFull(_e: any) { - console.warn("Localstorage quota exceeded. Clearing all queries"); - Yasr.clearStorage(); + console.warn("Local storage quota exceeded: the results view settings were not saved"); } public getResponseFromStorage() { @@ -798,7 +798,11 @@ export class Yasr extends EventEmitter { if (storageId && this.results) { const storeObj = this.results.getAsStoreObject(this.config.maxPersistentResponseSize); if (storeObj && !storeObj.error) { - this.storage.set(storageId, storeObj, this.config.persistencyExpire, this.handleLocalStorageQuotaFull); + this.storage.set(storageId, storeObj, this.config.persistencyExpire, () => { + // Too large to keep: drop the previous stored response too, so it isn't shown for this query + console.warn("Local storage quota exceeded: the query response was not saved"); + this.storage.remove(storageId); + }); } else { //remove old string; this.storage.remove(storageId); diff --git a/test/unit/storage-test.ts b/test/unit/storage-test.ts new file mode 100644 index 00000000..1f7681e4 --- /dev/null +++ b/test/unit/storage-test.ts @@ -0,0 +1,46 @@ +import * as chai from "chai"; +import { describe, it, beforeEach, afterEach } from "mocha"; +import store from "store"; + +import Storage, { isQuotaExceeded } from "../../packages/utils/src/Storage.js"; + +const expect = chai.expect; + +describe("Storage", () => { + let original: typeof store.set; + beforeEach(() => { + original = store.set; + }); + afterEach(() => { + store.set = original; + }); + + it("recognises quota errors from current and older browsers", () => { + expect(isQuotaExceeded({ name: "QuotaExceededError", code: 22 })).to.equal(true); + expect(isQuotaExceeded({ name: "NS_ERROR_DOM_QUOTA_REACHED", code: 1014 })).to.equal(true); + expect(isQuotaExceeded({ name: "TypeError" })).to.equal(false); + expect(isQuotaExceeded(undefined)).to.equal(false); + }); + + it("lets the quota callback handle a full storage without throwing", function () { + if (!store.enabled) this.skip(); + (store as any).set = () => { + const error = new Error("full"); + error.name = "QuotaExceededError"; + throw error; + }; + let handled = 0; + const storage = new Storage("test"); + expect(() => storage.set("key", { a: 1 }, 60, () => handled++)).to.not.throw(); + expect(handled).to.equal(1); + }); + + it("still throws errors that are not about the quota", function () { + if (!store.enabled) this.skip(); + (store as any).set = () => { + throw new TypeError("circular"); + }; + const storage = new Storage("test"); + expect(() => storage.set("key", { a: 1 }, 60, () => {})).to.throw(TypeError); + }); +}); From f047cf517ae57734d9f5fe0eea6d1543bc48b72f Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:46 +0000 Subject: [PATCH 3/9] fix(yasgui): don't run risky share-link queries on load A link could name any query and endpoint, and it ran at once with the credentials saved for that endpoint, so a crafted link could run an update as whoever opened it. Updates never run from a link now; queries with syntax errors, or that would send an Authorization/API key header or cookies, wait for the user to click Run query in a toast. New autoRunSharedQueries option: safe (default), always, never. Malformed headers/outputSettings JSON in a link is ignored instead of throwing in the constructor. Yasgui.dialogs exposes the dialogs to plugins and embedders. Browser tests can use another port with TEST_PORT. --- .changeset/review-hardening.md | 17 +++ packages/yasgui/src/index.ts | 44 ++++++- packages/yasgui/src/linkUtils.ts | 10 +- packages/yasgui/src/shareLinkSafety.ts | 53 +++++++++ test/run.ts | 1 + test/share-link-browser.ts | 159 +++++++++++++++++++++++++ test/unit/share-link-safety-test.ts | 60 ++++++++++ test/utils.ts | 3 +- 8 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 .changeset/review-hardening.md create mode 100644 packages/yasgui/src/shareLinkSafety.ts create mode 100644 test/share-link-browser.ts create mode 100644 test/unit/share-link-safety-test.ts diff --git a/.changeset/review-hardening.md b/.changeset/review-hardening.md new file mode 100644 index 00000000..5a1f2732 --- /dev/null +++ b/.changeset/review-hardening.md @@ -0,0 +1,17 @@ +--- +"@matdata/yasgui": minor +"@matdata/yasqe": patch +"@matdata/yasr": patch +"@matdata/yasgui-utils": patch +--- + +Safer share links, storage and dialogs: + +- A query opened from a share link no longer runs on its own when it is an update, has syntax errors, or would send saved credentials or cookies; a message asks first. New `autoRunSharedQueries` option ("safe" by default, "always", "never"). +- A malformed `headers` or `outputSettings` parameter in a link is ignored instead of breaking the page. +- When the browser storage is full, MatGUI saves the configuration without query results, and otherwise keeps working and says so. It no longer deletes every tab, endpoint and credential. +- In-app dialogs and toasts replace every `alert`, `confirm` and `prompt`, so they follow the theme and work in sandboxed iframes; available to plugins and embedders as `Yasgui.dialogs`. +- Configuration import reads the Turtle file with a real parser, so tabs without an endpoint or with quotes in their name import correctly. +- Saving a Git query after a stale version tag no longer reports a conflict when the automatic retry succeeded. +- The OAuth 2.0 popup only accepts its result from the origin of the redirect URI. +- Yasqe `requestSendsCredentials()`. diff --git a/packages/yasgui/src/index.ts b/packages/yasgui/src/index.ts index 58b1c1f6..ac9aa42b 100644 --- a/packages/yasgui/src/index.ts +++ b/packages/yasgui/src/index.ts @@ -16,6 +16,8 @@ import GeoPlugin from "@matdata/yasgui-geo-plugin"; import GraphPlugin from "@matdata/yasgui-graph-plugin"; import TablePlugin from "@matdata/yasgui-table-plugin"; import { moveTabIdToFront, removeTabId } from "./tabNavigationHistory"; +import * as dialogs from "./dialogs"; +import { setDialogHost, showToast } from "./dialogs"; import "@matdata/yasgui-chart-plugin/dist/yasgui-chart-plugin.min.css"; import "@matdata/yasgui-explain-plugin/dist/yasgui-explain-plugin.min.css"; import "@matdata/yasgui-graph-plugin/dist/yasgui-graph-plugin.min.css"; @@ -103,6 +105,13 @@ export interface Config { endpointButtons?: EndpointButton[]; //The function allows us to modify the config before we pass it on to a tab populateFromUrl: boolean | ((configFromUrl: PersistedTabJson) => PersistedTabJson); + /** + * Whether a query opened from a share link runs on load (default: "safe"). + * "safe": runs, unless the request would carry saved credentials (auth headers or cookies) or the query is + * invalid; then the user is asked to review it first. "always": runs unless it is an update. "never": loads + * the query without running it. Update queries never run from a link. + */ + autoRunSharedQueries?: shareLink.AutoRunSharedQueries; autoAddOnInit: boolean; persistenceId: ((yasr: Yasgui) => string) | string | null; persistenceLabelConfig: string; @@ -245,11 +254,14 @@ export class Yasgui extends EventEmitter { private navigationSnapshot: string[] | null = null; private navigationCursor = 0; public static Tab = Tab; + /** In-app alert, confirm and toast, in the MatGUI style, for plugins and embedding pages */ + public static dialogs = dialogs; constructor(parent: HTMLElement, config: PartialConfig) { super(); this.rootEl = document.createElement("div"); addClass(this.rootEl, "yasgui"); parent.appendChild(this.rootEl); + setDialogHost(this.rootEl); this.config = merge({}, Yasgui.defaults, config); @@ -356,7 +368,7 @@ export class Yasgui extends EventEmitter { this.markTabSelected(activeTabId); this.recordTabInRecentHistory(activeTabId); if (executeIdAfterInit && executeIdAfterInit === activeTabId) { - (this.getTab(activeTabId) as Tab).query().catch(() => {}); + this.runSharedQuery(this.getTab(activeTabId) as Tab); } // } } @@ -376,6 +388,36 @@ export class Yasgui extends EventEmitter { }, 300); } } + /** + * Run the query of a tab opened from a share link, unless that would be unsafe without the user reviewing it + */ + private runSharedQuery(tab: Tab) { + const yasqe = tab.getYasqe(); + if (!yasqe) return; + yasqe.checkSyntax(); + let sendsCredentials = false; + try { + sendsCredentials = yasqe.requestSendsCredentials(); + } catch { + // A failing request config is reported when the query runs; treat it as credentialed to be safe + sendsCredentials = true; + } + const blocker = shareLink.getSharedQueryRunBlocker( + { queryMode: yasqe.getQueryMode(), queryValid: yasqe.queryValid, sendsCredentials }, + this.config.autoRunSharedQueries, + ); + if (!blocker) { + tab.query().catch(() => {}); + return; + } + const canRun = blocker === "credentials" || blocker === "disabled"; + showToast(shareLink.sharedQueryBlockedMessage(blocker, tab.getEndpoint()), { + host: this.rootEl, + kind: blocker === "disabled" ? "info" : "warning", + duration: canRun ? 0 : 10000, + action: canRun ? { label: "Run query", onClick: () => tab.query().catch(() => {}) } : undefined, + }); + } public hasFullscreen(fullscreen: boolean) { if (fullscreen) { this.emit("fullscreen-enter", this); diff --git a/packages/yasgui/src/linkUtils.ts b/packages/yasgui/src/linkUtils.ts index fc838e90..2425c3bd 100644 --- a/packages/yasgui/src/linkUtils.ts +++ b/packages/yasgui/src/linkUtils.ts @@ -4,6 +4,8 @@ import { default as Tab, PersistedJson } from "./Tab"; import Yasr from "@matdata/yasr"; import { PlainRequestConfig } from "@matdata/yasqe"; import { getAsValue } from "@matdata/yasgui-utils"; +import { parseJsonParam } from "./shareLinkSafety"; +export * from "./shareLinkSafety"; var getUrlParams = function (_url?: string) { var urlFromWindow = false; if (!_url) { @@ -127,7 +129,7 @@ export function getConfigFromUrl(defaults: PersistedJson, _url?: string): Persis console.warn(`Output format plugin "${value}" not found`); } } else if (key == "outputSettings") { - pluginsConfig = JSON.parse(value); + pluginsConfig = parseJsonParam("outputSettings", value); } else if (key == "contentTypeConstruct") { options.requestConfig.acceptHeaderGraph = value; } else if (key == "contentTypeSelect") { @@ -145,8 +147,10 @@ export function getConfigFromUrl(defaults: PersistedJson, _url?: string): Persis if (!Array.isArray(options.requestConfig.defaultGraphs)) options.requestConfig.defaultGraphs = []; options.requestConfig.defaultGraphs.push(value); } else if (key == "headers") { - if (!options.requestConfig.headers) options.requestConfig.headers = {}; - options.requestConfig.headers = JSON.parse(value); + const headers = parseJsonParam("headers", value); + if (headers && typeof headers === "object" && !Array.isArray(headers)) { + options.requestConfig.headers = headers; + } } else { //regular arguments. So store them as regular arguments if (!options.requestConfig.args) options.requestConfig.args = []; diff --git a/packages/yasgui/src/shareLinkSafety.ts b/packages/yasgui/src/shareLinkSafety.ts new file mode 100644 index 00000000..2cf67f16 --- /dev/null +++ b/packages/yasgui/src/shareLinkSafety.ts @@ -0,0 +1,53 @@ +/** + * Why a query that came in through a share link must not run on its own, if it must not. + * + * A link can name any query and endpoint. Running it on load would let a crafted link run an update, or a + * query that carries the credentials this browser saved for that endpoint, as the person who opens it. + */ +export type SharedQueryRunBlocker = "update" | "invalid" | "credentials"; + +export interface SharedQueryRunInfo { + queryMode: "update" | "query"; + queryValid: boolean; + /** The request would carry an Authorization or API key header, or cookies */ + sendsCredentials: boolean; +} + +export type AutoRunSharedQueries = "safe" | "always" | "never"; + +export function getSharedQueryRunBlocker( + info: SharedQueryRunInfo, + mode: AutoRunSharedQueries = "safe", +): SharedQueryRunBlocker | "disabled" | undefined { + if (mode === "never") return "disabled"; + // Updates never run from a link, whatever the mode + if (info.queryMode === "update") return "update"; + if (mode === "always") return undefined; + // An invalid query may be an update the tokenizer couldn't classify + if (!info.queryValid) return "invalid"; + if (info.sendsCredentials) return "credentials"; + return undefined; +} + +export function sharedQueryBlockedMessage(blocker: SharedQueryRunBlocker | "disabled", endpoint: string): string { + switch (blocker) { + case "update": + return "This link holds an update query, so it was not run. Review it, then press Run if you want to change the data."; + case "credentials": + return `This link was not run because it would use your saved credentials for ${endpoint}. Review the query first.`; + case "invalid": + return "This link holds a query with syntax errors, so it was not run."; + case "disabled": + return "Query loaded from the link. Press Run to execute it."; + } +} + +/** Parse a JSON-valued share link parameter, ignoring a malformed or truncated value */ +export function parseJsonParam(key: string, value: string): T | undefined { + try { + return JSON.parse(value); + } catch { + console.warn(`Ignoring the "${key}" parameter of the link: it is not valid JSON`); + return undefined; + } +} diff --git a/test/run.ts b/test/run.ts index c0875b86..17d7d38f 100644 --- a/test/run.ts +++ b/test/run.ts @@ -15,6 +15,7 @@ import "./quick-start-browser.js"; import "./ai-assistant-browser.js"; import "./sparql-anything-browser.js"; import "./whats-new-browser.js"; +import "./share-link-browser.js"; declare var window: Window & { Yasqe: typeof Yasqe; diff --git a/test/share-link-browser.ts b/test/share-link-browser.ts new file mode 100644 index 00000000..0a679d12 --- /dev/null +++ b/test/share-link-browser.ts @@ -0,0 +1,159 @@ +import * as path from "path"; +import * as http from "http"; +import * as chai from "chai"; +import * as puppeteer from "puppeteer"; +import { it, describe, before, beforeEach, after, afterEach } from "mocha"; +import { setup, destroy, closePage, getPage, TEST_ORIGIN } from "./utils.js"; + +const expect = chai.expect; + +// Requests to this endpoint get a 404 from the test server; we only count them +const ENDPOINT = `${TEST_ORIGIN}/share-link-endpoint`; + +function linkHash(params: { [key: string]: string }) { + return "#" + new URLSearchParams(params).toString(); +} + +describe("Share links", function () { + let browser: puppeteer.Browser; + let page: puppeteer.Page; + let server: http.Server | undefined; + let endpointRequests: number; + + before(async function () { + const refs = await setup(this, path.resolve("./build")); + browser = refs.browser; + server = refs.server; + }); + + beforeEach(async function () { + this.timeout(30000); + page = await getPage(browser, "yasgui.html"); + await page.evaluate(() => localStorage.clear()); + endpointRequests = 0; + page.on("request", (request) => { + if (request.url().startsWith(ENDPOINT)) endpointRequests++; + }); + }); + + afterEach(async () => { + await closePage(this, page); + }); + + after(async function () { + return destroy(browser, server); + }); + + async function open(params: { [key: string]: string }) { + // Through a blank page, so the link loads as a new document rather than as a hash change + await page.goto("about:blank"); + await page.goto(`${TEST_ORIGIN}/yasgui.html?noExternalServices=true${linkHash(params)}`, { + waitUntil: "networkidle2", + }); + await page.waitForFunction(() => "yasgui" in window); + // Give a query that would run on load the time to start + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + it("runs a read query without credentials", async function () { + await open({ query: "SELECT * WHERE { ?s ?p ?o } LIMIT 1", endpoint: ENDPOINT }); + expect(endpointRequests).to.be.greaterThan(0); + expect(await page.$(".yasgui-toast")).to.equal(null); + }); + + it("never runs an update query", async function () { + await open({ query: "INSERT DATA { }", endpoint: ENDPOINT }); + expect(endpointRequests).to.equal(0); + const toast = await page.$eval(".yasgui-toast", (el) => el.textContent || ""); + expect(toast).to.contain("update query"); + // The query is loaded for review + const value = await page.evaluate(() => (window as any).yasgui.getTab().getQuery()); + expect(value).to.contain("INSERT DATA"); + }); + + it("asks before running a query that would send saved credentials", async function () { + await page.evaluate((endpoint) => { + (window as any).yasgui.persistentConfig.addOrUpdateEndpoint(endpoint, { + authentication: { type: "basic", username: "u", password: "p" }, + }); + }, ENDPOINT); + await open({ query: "SELECT * WHERE { ?s ?p ?o } LIMIT 1", endpoint: ENDPOINT }); + expect(endpointRequests).to.equal(0); + const toast = await page.$eval(".yasgui-toast", (el) => el.textContent || ""); + expect(toast).to.contain("saved credentials"); + await page.click(".yasgui-toast__action"); + await page.waitForFunction(() => !document.querySelector(".yasgui-toast")); + await new Promise((resolve) => setTimeout(resolve, 300)); + expect(endpointRequests).to.be.greaterThan(0); + }); + + it("loads a link with malformed JSON parameters", async function () { + await open({ query: "SELECT * WHERE { ?s ?p ?o } LIMIT 1", endpoint: ENDPOINT, headers: '{"X-Broken":' }); + const value = await page.evaluate(() => (window as any).yasgui.getTab().getQuery()); + expect(value).to.contain("SELECT"); + }); +}); + +describe("In-app dialogs", function () { + let browser: puppeteer.Browser; + let page: puppeteer.Page; + let server: http.Server | undefined; + + before(async function () { + const refs = await setup(this, path.resolve("./build")); + browser = refs.browser; + server = refs.server; + }); + + beforeEach(async function () { + this.timeout(30000); + page = await getPage(browser, "yasgui.html"); + }); + + afterEach(async () => { + await closePage(this, page); + }); + + after(async function () { + return destroy(browser, server); + }); + + it("resolves a confirm with the button that was clicked, and false on Escape", async function () { + await page.evaluate(() => { + (window as any).confirmResult = (window as any).Yasgui.dialogs.showConfirm("Delete it?", { + title: "Delete", + confirmLabel: "Delete", + danger: true, + }); + }); + await page.waitForSelector(".yasgui-dialog [role=alertdialog]"); + // The confirm button has focus + const focused = await page.evaluate(() => document.activeElement?.textContent); + expect(focused).to.equal("Delete"); + await page.click(".yasgui-dialog__button--danger"); + expect(await page.evaluate(() => (window as any).confirmResult)).to.equal(true); + expect(await page.$(".yasgui-dialog")).to.equal(null); + + await page.evaluate(() => { + (window as any).confirmResult = (window as any).Yasgui.dialogs.showConfirm("Delete it?"); + }); + await page.waitForSelector(".yasgui-dialog"); + await page.keyboard.press("Escape"); + expect(await page.evaluate(() => (window as any).confirmResult)).to.equal(false); + }); + + it("closes a tab holding unsaved saved-query changes only after the choice", async function () { + const tabCount = () => page.evaluate(() => (window as any).yasgui.persistentConfig.getTabs().length); + const before = await tabCount(); + await page.evaluate(() => { + const tab = (window as any).yasgui.getTab(); + tab.isManagedQueryTab = () => true; + tab.hasUnsavedManagedChanges = () => true; + tab.close(); + }); + await page.waitForSelector(".yasgui-dialog"); + expect(await tabCount()).to.equal(before); + await page.keyboard.press("Escape"); + expect(await tabCount()).to.equal(before); + }); +}); diff --git a/test/unit/share-link-safety-test.ts b/test/unit/share-link-safety-test.ts new file mode 100644 index 00000000..501e3ec4 --- /dev/null +++ b/test/unit/share-link-safety-test.ts @@ -0,0 +1,60 @@ +import * as chai from "chai"; +import { describe, it } from "mocha"; + +import { + getSharedQueryRunBlocker, + parseJsonParam, + sharedQueryBlockedMessage, +} from "../../packages/yasgui/src/shareLinkSafety.js"; + +const expect = chai.expect; + +const select = { queryMode: "query" as const, queryValid: true, sendsCredentials: false }; + +describe("Share link auto-run", () => { + it("runs a valid read query without credentials", () => { + expect(getSharedQueryRunBlocker(select)).to.equal(undefined); + }); + + it("never runs an update, in any mode", () => { + const update = { ...select, queryMode: "update" as const }; + expect(getSharedQueryRunBlocker(update)).to.equal("update"); + expect(getSharedQueryRunBlocker(update, "always")).to.equal("update"); + expect(getSharedQueryRunBlocker(update, "never")).to.equal("disabled"); + }); + + it("asks before sending saved credentials", () => { + expect(getSharedQueryRunBlocker({ ...select, sendsCredentials: true })).to.equal("credentials"); + expect(getSharedQueryRunBlocker({ ...select, sendsCredentials: true }, "always")).to.equal(undefined); + }); + + it("does not run an invalid query, which may be an unrecognised update", () => { + expect(getSharedQueryRunBlocker({ ...select, queryValid: false })).to.equal("invalid"); + }); + + it("runs nothing when disabled", () => { + expect(getSharedQueryRunBlocker(select, "never")).to.equal("disabled"); + }); + + it("names the endpoint when credentials are involved", () => { + expect(sharedQueryBlockedMessage("credentials", "https://example.org/sparql")).to.contain( + "https://example.org/sparql", + ); + }); +}); + +describe("Share link JSON parameters", () => { + it("parses valid JSON", () => { + expect(parseJsonParam("headers", '{"X-Test":"1"}')).to.deep.equal({ "X-Test": "1" }); + }); + + it("ignores a truncated value instead of throwing", () => { + const warn = console.warn; + console.warn = () => {}; + try { + expect(parseJsonParam("outputSettings", '{"pageSize":')).to.equal(undefined); + } finally { + console.warn = warn; + } + }); +}); diff --git a/test/utils.ts b/test/utils.ts index 308080e4..6d788d86 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -5,7 +5,8 @@ import * as path from "path"; import * as Mocha from "mocha"; import _ from "lodash"; // eslint-disable-line const TEST_ON_DEV_BUILD = !!process.env["TEST_ON_DEV_BUILD"]; -const PORT = TEST_ON_DEV_BUILD ? 4000 : 40001; +const PORT = Number(process.env["TEST_PORT"]) || (TEST_ON_DEV_BUILD ? 4000 : 40001); +export const TEST_ORIGIN = `http://localhost:${PORT}`; const MIME_TYPES: { [ext: string]: string } = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", From b868f078225c3e69998ef9bd8f5d48e6d07000bc Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:46 +0000 Subject: [PATCH 4/9] fix(yasgui): accept the OAuth 2.0 callback only from the redirect URI origin The postMessage handler took a callback from any window. The code verifier and state are no longer written to sessionStorage (only the closure reads them), and unexpected errors while polling the popup now reject the flow instead of being swallowed. --- packages/yasgui/src/OAuth2Utils.ts | 31 ++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/yasgui/src/OAuth2Utils.ts b/packages/yasgui/src/OAuth2Utils.ts index e7e6bf92..167a0a73 100644 --- a/packages/yasgui/src/OAuth2Utils.ts +++ b/packages/yasgui/src/OAuth2Utils.ts @@ -52,6 +52,15 @@ export interface OAuth2TokenResponse { token_type: string; } +/** Origin of a URL, or of the current page for a relative one */ +export function originOf(url: string): string { + try { + return new URL(url, window.location.href).origin; + } catch { + return window.location.origin; + } +} + /** * Check if a token has expired */ @@ -76,9 +85,8 @@ export async function startOAuth2Flow(config: OAuth2Config): Promise { clearInterval(checkInterval); window.removeEventListener("message", messageHandler); - // Clean up sessionStorage - sessionStorage.removeItem(`oauth2_code_verifier_${flowId}`); - sessionStorage.removeItem(`oauth2_state_${flowId}`); }; const checkInterval = setInterval(() => { @@ -169,17 +174,23 @@ export async function startOAuth2Flow(config: OAuth2Config): Promise { - // Validate origin if needed (should match redirect URI origin) + // Only the redirect page, on the origin of the redirect URI, may report the result + if (event.origin !== redirectOrigin) return; if (event.data && event.data.type === "oauth2_callback" && !flowCompleted) { flowCompleted = true; cleanup(); From d1bcd6d9076284dd7db24d77987f0f99276c2c35 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:46 +0000 Subject: [PATCH 5/9] fix(yasgui): parse imported configuration with N3 The import matched literals with regular expressions and paired them by position, so a tab without an endpoint shifted the endpoints of the next tabs, quotes in names broke parsing, and a tab orientation could become the global one. Adds an export/import round-trip test. --- packages/yasgui/src/ConfigExportImport.ts | 237 ++++++++++------------ test/unit/config-export-import-test.ts | 85 ++++++++ 2 files changed, 197 insertions(+), 125 deletions(-) create mode 100644 test/unit/config-export-import-test.ts diff --git a/packages/yasgui/src/ConfigExportImport.ts b/packages/yasgui/src/ConfigExportImport.ts index 6e879165..8ccaa988 100644 --- a/packages/yasgui/src/ConfigExportImport.ts +++ b/packages/yasgui/src/ConfigExportImport.ts @@ -3,7 +3,8 @@ * Handles serialization and deserialization of Matgui configuration to/from RDF Turtle format */ -import { PersistedJson } from "./PersistentConfig"; +import * as N3 from "n3"; +import type { PersistedJson } from "./PersistentConfig"; // Matgui Configuration Ontology export const YASGUI_NS = "https://yasgui.matdata.eu/ontology#"; @@ -178,26 +179,8 @@ function escapeTurtleString(str: string): string { } /** - * Unescape Turtle string - */ -function unescapeTurtleString(str: string): string { - return str - .replace(/\\n/g, "\n") - .replace(/\\r/g, "\r") - .replace(/\\t/g, "\t") - .replace(/\\"/g, '"') - .replace(/\\\\/g, "\\"); -} - -/** - * Parse Turtle format back to configuration - * This is a simplified parser focused on the structure we generate. - * - * Note: This parser is designed to handle the specific Turtle format - * produced by serializeToTurtle(). For production use with arbitrary - * Turtle input, consider using a robust RDF library like N3.js. - * The current implementation uses regex patterns that work well for - * our serialization output but may not handle all valid Turtle syntax. + * Parse Turtle back to configuration, with a real Turtle parser: the export of any version, or a file edited by + * hand, is read by its triples rather than by the layout of the text. */ export function parseFromTurtle(turtle: string): Partial { const config: Partial = { @@ -207,124 +190,128 @@ export function parseFromTurtle(turtle: string): Partial { customEndpointButtons: [], }; + let quads: N3.Quad[]; try { - // Extract endpoint history - const endpointHistoryMatch = turtle.match(/yasgui:endpointHistory\s*\(([\s\S]*?)\)/); - if (endpointHistoryMatch) { - const endpoints = endpointHistoryMatch[1].match(/"([^"]*)"/g); - if (endpoints) { - config.endpointHistory = endpoints.map((e) => unescapeTurtleString(e.slice(1, -1))); - } - } + quads = new N3.Parser().parse(turtle); + } catch (error) { + console.error("Error parsing Turtle configuration:", error); + throw new Error( + `Failed to parse configuration. Please check the format${error instanceof Error ? ` (${error.message})` : ""}.`, + ); + } - // Extract active tab - const activeTabMatch = turtle.match(/yasgui:activeTab\s+"([^"]*)"/); - if (activeTabMatch) { - config.active = unescapeTurtleString(activeTabMatch[1]); + const bySubject = new Map(); + for (const quad of quads) { + const key = termKey(quad.subject); + const list = bySubject.get(key); + if (list) list.push(quad); + else bySubject.set(key, [quad]); + } + const objects = (subject: N3.Term, predicate: string): N3.Term[] => + (bySubject.get(termKey(subject)) || []).filter((q) => q.predicate.value === predicate).map((q) => q.object); + const literal = (subject: N3.Term, predicate: string): string | undefined => { + const object = objects(subject, predicate)[0]; + return object && object.termType === "Literal" ? object.value : undefined; + }; + const list = (head: N3.Term | undefined): N3.Term[] => { + const items: N3.Term[] = []; + const seen = new Set(); + let node = head; + while (node && node.value !== RDF_NS + "nil" && !seen.has(termKey(node))) { + seen.add(termKey(node)); + const first = objects(node, RDF_NS + "first")[0]; + if (first) items.push(first); + node = objects(node, RDF_NS + "rest")[0]; } + return items; + }; - // Extract prefixes - const prefixesMatch = turtle.match(/yasgui:prefixesValue\s+"""([\s\S]*?)"""/); - if (prefixesMatch) { - config.prefixes = unescapeTurtleString(prefixesMatch[1]); - } + const root = quads.find( + (q) => q.predicate.value === RDF_NS + "type" && q.object.value === YASGUI_NS + "Configuration", + )?.subject; + if (!root) { + throw new Error("Failed to parse configuration: no yasgui:Configuration found."); + } - // Extract auto capture enabled - const autoCaptureMatch = turtle.match(/yasgui:autoCaptureEnabled\s+"([^"]*)"/); - if (autoCaptureMatch) { - config.autoCaptureEnabled = autoCaptureMatch[1] === "true"; - } + config.endpointHistory = list(objects(root, YASGUI_NS + "endpointHistory")[0]) + .filter((term) => term.termType === "Literal") + .map((term) => term.value); - // Extract theme - const themeMatch = turtle.match(/yasgui:theme\s+"([^"]*)"/); - if (themeMatch && (themeMatch[1] === "light" || themeMatch[1] === "dark")) { - config.theme = themeMatch[1] as "light" | "dark"; - } + const active = literal(root, YASGUI_NS + "activeTab"); + if (active !== undefined) config.active = active; - // Extract orientation - const orientationMatch = turtle.match(/yasgui:orientation\s+"([^"]*)"/); - if (orientationMatch && (orientationMatch[1] === "vertical" || orientationMatch[1] === "horizontal")) { - config.orientation = orientationMatch[1] as "vertical" | "horizontal"; - } + const prefixes = literal(root, YASGUI_NS + "prefixesValue"); + if (prefixes !== undefined) config.prefixes = prefixes; - // Extract custom endpoint buttons - const buttonPattern = - /yasgui:customEndpointButton\s+\[([\s\S]*?)rdfs:label\s+"([^"]*)"\s*;\s*sd:endpoint\s+"([^"]*)"/g; - let buttonMatch; - while ((buttonMatch = buttonPattern.exec(turtle)) !== null) { - config.customEndpointButtons!.push({ - label: unescapeTurtleString(buttonMatch[2]), - endpoint: unescapeTurtleString(buttonMatch[3]), - }); - } + const autoCapture = literal(root, YASGUI_NS + "autoCaptureEnabled"); + if (autoCapture !== undefined) config.autoCaptureEnabled = autoCapture === "true"; - // Extract tabs - simplified parsing - const tabPattern = - /dcterms:identifier\s+"([^"]*)"\s*;\s*rdfs:label\s+"([^"]*)"\s*;[\s\S]*?(?=dcterms:identifier|yasgui:customEndpointButton|\]\.|\]\s*,\s*\[)/g; - const tabBlocks = turtle.match(/yasgui:tab\s+\[([\s\S]*)\]\s*\./); - - if (tabBlocks) { - const tabsContent = tabBlocks[1]; - const tabIdMatches = [...tabsContent.matchAll(/dcterms:identifier\s+"([^"]*)"/g)]; - const tabNameMatches = [...tabsContent.matchAll(/rdfs:label\s+"([^"]*)"/g)]; - const queryMatches = [...tabsContent.matchAll(/sp:text\s+"""([\s\S]*?)"""/g)]; - const endpointMatches = [...tabsContent.matchAll(/sd:endpoint\s+"([^"]*)"/g)]; - const methodMatches = [...tabsContent.matchAll(/yasgui:requestMethod\s+"([^"]*)"/g)]; - - // Extract tab-level orientation - const tabOrientationMatches = [...tabsContent.matchAll(/yasgui:orientation\s+"([^"]*)"/g)]; - - tabIdMatches.forEach((match, index) => { - const tabId = unescapeTurtleString(match[1]); - const tabName = tabNameMatches[index] ? unescapeTurtleString(tabNameMatches[index][1]) : "Query"; - const query = queryMatches[index] ? unescapeTurtleString(queryMatches[index][1]) : ""; - const endpoint = endpointMatches[index] ? unescapeTurtleString(endpointMatches[index][1]) : ""; - const method = methodMatches[index] ? unescapeTurtleString(methodMatches[index][1]) : "POST"; - const orientation = tabOrientationMatches[index] - ? (unescapeTurtleString(tabOrientationMatches[index][1]) as "vertical" | "horizontal") - : undefined; - - config.tabs!.push(tabId); - config.tabConfig![tabId] = { - id: tabId, - name: tabName, - yasqe: { - value: query, - }, - requestConfig: { - queryArgument: undefined, - endpoint: endpoint, - method: method as "GET" | "POST", - acceptHeaderGraph: "text/turtle", - acceptHeaderSelect: "application/sparql-results+json", - acceptHeaderUpdate: "application/sparql-results+json", - namedGraphs: [], - defaultGraphs: [], - args: [], - headers: {}, - withCredentials: false, - adjustQueryBeforeRequest: false, - basicAuth: undefined, - bearerAuth: undefined, - apiKeyAuth: undefined, - oauth2Auth: undefined, - }, - yasr: { - settings: {}, - response: undefined, - }, - orientation: orientation, - }; - }); - } - } catch (error) { - console.error("Error parsing Turtle configuration:", error); - throw new Error("Failed to parse configuration. Please check the format."); + const theme = literal(root, YASGUI_NS + "theme"); + if (theme === "light" || theme === "dark") config.theme = theme; + + const orientation = literal(root, YASGUI_NS + "orientation"); + if (orientation === "vertical" || orientation === "horizontal") config.orientation = orientation; + + for (const button of objects(root, YASGUI_NS + "customEndpointButton")) { + const label = literal(button, RDFS_NS + "label"); + const endpoint = literal(button, SD_NS + "endpoint"); + if (label !== undefined && endpoint !== undefined) config.customEndpointButtons!.push({ label, endpoint }); } + // Tabs are blank nodes without an order of their own: keep the order in which they appear in the file + objects(root, YASGUI_NS + "tab").forEach((tab, index) => { + const tabId = literal(tab, DCTERMS_NS + "identifier") || `imported-${index + 1}`; + if (config.tabConfig![tabId]) return; + const tabOrientation = literal(tab, YASGUI_NS + "orientation"); + const acceptHeader = (predicate: string) => { + const header = objects(tab, predicate)[0]; + return header ? literal(header, HTTP_NS + "headerValue") : undefined; + }; + const method = literal(tab, YASGUI_NS + "requestMethod"); + const editorHeight = literal(tab, SCHEMA_NS + "height"); + const selectedPlugin = literal(tab, YASGUI_NS + "selectedPlugin"); + + config.tabs!.push(tabId); + config.tabConfig![tabId] = { + id: tabId, + name: literal(tab, RDFS_NS + "label") || "Query", + yasqe: { + value: literal(tab, SP_NS + "text") || "", + ...(editorHeight ? { editorHeight } : {}), + }, + requestConfig: { + queryArgument: undefined, + endpoint: literal(tab, SD_NS + "endpoint") || "", + method: method === "GET" ? "GET" : "POST", + acceptHeaderGraph: acceptHeader(YASGUI_NS + "acceptHeaderGraph") || "text/turtle", + acceptHeaderSelect: acceptHeader(YASGUI_NS + "acceptHeaderSelect") || "application/sparql-results+json", + acceptHeaderUpdate: "application/sparql-results+json", + namedGraphs: [], + defaultGraphs: [], + args: [], + headers: {}, + withCredentials: false, + adjustQueryBeforeRequest: false, + basicAuth: undefined, + bearerAuth: undefined, + apiKeyAuth: undefined, + oauth2Auth: undefined, + }, + yasr: { + settings: selectedPlugin ? { selectedPlugin } : {}, + response: undefined, + }, + orientation: tabOrientation === "vertical" || tabOrientation === "horizontal" ? tabOrientation : undefined, + }; + }); + return config; } +function termKey(term: N3.Term): string { + return `${term.termType}:${term.value}`; +} + /** * Download configuration as a file */ diff --git a/test/unit/config-export-import-test.ts b/test/unit/config-export-import-test.ts new file mode 100644 index 00000000..072f3827 --- /dev/null +++ b/test/unit/config-export-import-test.ts @@ -0,0 +1,85 @@ +import * as chai from "chai"; +import { describe, it } from "mocha"; + +import { parseFromTurtle, serializeToTurtle } from "../../packages/yasgui/src/ConfigExportImport.js"; + +const expect = chai.expect; + +function tab(id: string, name: string, value: string, endpoint?: string, extra: any = {}) { + return { + id, + name, + yasqe: { value }, + requestConfig: { endpoint, method: "GET", acceptHeaderSelect: "application/sparql-results+xml" }, + yasr: { settings: { selectedPlugin: "Graph" }, response: undefined }, + ...extra, + }; +} + +describe("Configuration export and import", () => { + const config: any = { + endpointHistory: ["https://a.example/sparql", 'https://b.example/sparql?x="1"'], + tabs: ["t1", "t2", "t3"], + active: "t2", + tabConfig: { + t1: tab( + "t1", + 'Say "hi"', + 'SELECT * WHERE { ?s ?p "a \\"quoted\\" value" }\n# second line', + "https://a.example/sparql", + { + orientation: "horizontal", + }, + ), + // No endpoint: must not shift the endpoints of the other tabs + t2: tab("t2", "No endpoint", "ASK {}"), + t3: tab("t3", "Third", 'SELECT """long""" {}', "https://c.example/sparql"), + }, + prefixes: "PREFIX ex: \n", + autoCaptureEnabled: true, + theme: "dark", + customEndpointButtons: [{ label: "A", endpoint: "https://a.example/sparql" }], + }; + + it("round-trips tabs with quotes, newlines and a missing endpoint", () => { + const parsed = parseFromTurtle(serializeToTurtle(config)); + expect(parsed.tabs).to.deep.equal(["t1", "t2", "t3"]); + expect(parsed.active).to.equal("t2"); + expect(parsed.tabConfig!.t1.name).to.equal('Say "hi"'); + expect(parsed.tabConfig!.t1.yasqe.value).to.equal(config.tabConfig.t1.yasqe.value); + expect(parsed.tabConfig!.t1.requestConfig.endpoint).to.equal("https://a.example/sparql"); + expect(parsed.tabConfig!.t1.requestConfig.method).to.equal("GET"); + expect(parsed.tabConfig!.t1.requestConfig.acceptHeaderSelect).to.equal("application/sparql-results+xml"); + expect(parsed.tabConfig!.t1.orientation).to.equal("horizontal"); + expect(parsed.tabConfig!.t1.yasr.settings.selectedPlugin).to.equal("Graph"); + expect(parsed.tabConfig!.t2.requestConfig.endpoint).to.equal(""); + expect(parsed.tabConfig!.t3.requestConfig.endpoint).to.equal("https://c.example/sparql"); + expect(parsed.tabConfig!.t3.yasqe.value).to.equal('SELECT """long""" {}'); + }); + + it("keeps the configuration-level settings apart from the tab settings", () => { + const parsed = parseFromTurtle(serializeToTurtle(config)); + expect(parsed.endpointHistory).to.deep.equal(config.endpointHistory); + expect(parsed.prefixes).to.equal(config.prefixes); + expect(parsed.autoCaptureEnabled).to.equal(true); + expect(parsed.theme).to.equal("dark"); + // Only a tab has an orientation here, so the configuration has none + expect(parsed.orientation).to.equal(undefined); + expect(parsed.customEndpointButtons).to.deep.equal(config.customEndpointButtons); + }); + + it("never imports credentials", () => { + const parsed = parseFromTurtle(serializeToTurtle(config)); + const req: any = parsed.tabConfig!.t1.requestConfig; + expect(req.basicAuth).to.equal(undefined); + expect(req.bearerAuth).to.equal(undefined); + }); + + it("reports malformed Turtle", () => { + expect(() => parseFromTurtle("this is not turtle {")).to.throw(/Failed to parse configuration/); + }); + + it("reports Turtle that is not a configuration", () => { + expect(() => parseFromTurtle(" .")).to.throw(/no yasgui:Configuration/); + }); +}); From 79436ce580324fa1eeef490b3b7d4557a6ca53c2 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:46 +0000 Subject: [PATCH 6/9] ci: make unit tests, type-check and lint fail the build The unit-test script ended in '|| exit 0', util:validateTs (failing on yasqe tooltip.ts) and lint were never run in CI, and the lint glob pointed at a folder that no longer exists. build.yml now type-checks, lints, builds, checks bundle sizes against a budget (npm run util:bundleSize, which also lists what each bundle is made of) and runs all tests; test.yaml ran the same steps a second time and is removed. Dependabot covers npm, the website and GitHub Actions. Dependencies: query-string 10 (drops decode-uri-component, which had a high-severity advisory; npm audit is now clean). Removes packages nothing imports (choices.js, blueimp-md5, es6-object-assign in yasgui; jquery and its dead helpers, datatables.net, column-resizer, colors and free-solid-svg-icons in yasr), lists the five plugins yasgui imports as its dependencies, and moves test and build tooling to devDependencies. --- .eslintrc.cjs | 2 +- .github/dependabot.yml | 36 ++ .github/workflows/build.yml | 10 + .github/workflows/test.yaml | 29 -- .gitignore | 1 + esbuild.config.js | 3 + package-lock.json | 397 ++++++++++++----------- package.json | 21 +- packages/yasgui/package.json | 15 +- packages/yasqe/package.json | 2 +- packages/yasqe/src/tooltip.ts | 4 +- packages/yasr/package.json | 7 - packages/yasr/src/jquery/extendJquery.js | 1 - packages/yasr/src/jquery/tableToCsv.js | 88 ----- scripts/bundle-size.mjs | 78 +++++ 15 files changed, 355 insertions(+), 339 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/test.yaml delete mode 100644 packages/yasr/src/jquery/extendJquery.js delete mode 100644 packages/yasr/src/jquery/tableToCsv.js create mode 100644 scripts/bundle-size.mjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0d549b19..4fc7d950 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -9,7 +9,7 @@ module.exports = { parserOptions: { ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features sourceType: "module", // Allows for the use of imports, - project: expensive ? "./tsconfig-validate.json" : undefined, + project: expensive ? ["./tsconfig-validate.json", "./tsconfig-test.json"] : undefined, tsconfigRootDir: expensive ? "." : undefined, }, plugins: ["@typescript-eslint", "jest", "lodash"], diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..426495b9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,36 @@ +version: 2 +updates: + # Monorepo packages (packages/*) and build tooling + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + codemirror: + patterns: + - "@codemirror/*" + - "@lezer/*" + matdata-plugins: + patterns: + - "@matdata/*" + dev-tooling: + dependency-type: development + update-types: + - minor + - patch + + # Documentation website + - package-ecosystem: npm + directory: /website + schedule: + interval: monthly + groups: + docusaurus: + patterns: + - "@docusaurus/*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15c80292..5d227c6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - main + workflow_dispatch: jobs: build: @@ -28,9 +29,18 @@ jobs: - name: Install dependencies run: npm ci + - name: Type-check + run: npm run util:validateTs + + - name: Lint + run: npm run util:lint + - name: Build project run: npm run build + - name: Bundle size + run: npm run util:bundleSize + - name: Run tests run: npm run test diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index feaaf831..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Run tests - -on: push - -jobs: - tests: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Set up NodeJS - uses: actions/setup-node@v6 - with: - node-version: "lts/*" - cache: npm - - - name: Disable AppArmor - run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Run tests - run: npm run test diff --git a/.gitignore b/.gitignore index 7eb4e25b..71f8d8cb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ website/node_modules website/build website/.docusaurus +bundle-stats diff --git a/esbuild.config.js b/esbuild.config.js index 142c2142..a183d573 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -146,6 +146,9 @@ async function buildPackage(name, entryPoint, globalName) { }, }); extractCss(iifeResult, name); + // Bundle composition, for `npm run util:bundleSize` (kept out of build/ so it isn't distributed) + fs.mkdirSync("bundle-stats", { recursive: true }); + fs.writeFileSync(`bundle-stats/${name}.json`, JSON.stringify(iifeResult.metafile)); // ESM build: consumed by bundlers (vite/webpack/esbuild) and Node via `import`. // Uses the `.mjs` extension so it is always treated as an ES module. diff --git a/package-lock.json b/package-lock.json index e94648e5..20c61f15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,20 +16,18 @@ "@matdata/yasgui-explain-plugin": "^1.0.0", "@matdata/yasgui-geo-plugin": "^1.3.0", "@matdata/yasgui-graph-plugin": "^1.7.0", - "@matdata/yasgui-table-plugin": "^1.7.1", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", - "autoprefixer": "^10.4.21", - "better-npm-run": "^0.1.1", - "chai": "^6.2.0", - "fs-extra": "^11.3.2", - "mocha": "^12.0.3" + "@matdata/yasgui-table-plugin": "^1.7.1" }, "devDependencies": { "@changesets/cli": "^2.29.7", "@types/chai": "^5.2.3", "@types/fs-extra": "^11.0.4", "@types/mocha": "^10.0.10", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "autoprefixer": "^10.4.21", + "better-npm-run": "^0.1.1", + "chai": "^6.2.0", "cross-env": "^7.0.3", "esbuild": "^0.28.2", "esbuild-sass-plugin": "^3.3.1", @@ -37,8 +35,10 @@ "eslint-config-prettier": "^6.10.0", "eslint-plugin-jest": "^23.8.1", "eslint-plugin-lodash": "^7.1.0", + "fs-extra": "^11.3.2", "husky": "^9.1.7", "lint-staged": "^16.2.6", + "mocha": "^12.0.3", "postcss": "^8.4.49", "prettier": "^3.6.2", "puppeteer": "^25.16.0", @@ -53,6 +53,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1112,6 +1113,7 @@ "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -1130,6 +1132,7 @@ "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1139,6 +1142,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1162,6 +1166,7 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1172,6 +1177,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -1184,20 +1190,12 @@ "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz", - "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@fortawesome/fontawesome-free": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", @@ -1207,23 +1205,12 @@ "node": ">=6" } }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz", - "integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "7.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", + "dev": true, "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", @@ -1238,6 +1225,7 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1248,6 +1236,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -1260,6 +1249,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -1274,6 +1264,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@inquirer/external-editor": { @@ -1609,6 +1600,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1622,6 +1614,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1631,6 +1624,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2498,13 +2492,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/blueimp-md5": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/@types/blueimp-md5/-/blueimp-md5-2.18.2.tgz", - "integrity": "sha512-dJ9yRry9Olt5GAWlgCtE5dK9d/Dfhn/V7hna86eEO2Pn76+E8Y0S0n61iEUEGhWXXgtKtHxtZLVNwL8X+vLHzg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -2547,16 +2534,6 @@ "license": "MIT", "peer": true }, - "node_modules/@types/jquery": { - "version": "3.5.33", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz", - "integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2655,13 +2632,6 @@ "htmlparser2": "^4.1.0" } }, - "node_modules/@types/sizzle": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", - "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2673,6 +2643,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", @@ -2837,6 +2808,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", @@ -2865,6 +2837,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.18.0", @@ -2882,6 +2855,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "7.18.0", @@ -2909,6 +2883,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2922,6 +2897,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "7.18.0", @@ -2950,6 +2926,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -2972,6 +2949,7 @@ "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.18.0", @@ -2989,6 +2967,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, "license": "ISC" }, "node_modules/abort-controller": { @@ -3007,6 +2986,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3019,6 +2999,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -3028,6 +3009,7 @@ "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -3070,6 +3052,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3079,6 +3062,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3094,12 +3078,14 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3119,6 +3105,7 @@ "version": "10.4.23", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3164,6 +3151,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -3190,6 +3178,7 @@ "version": "2.11.29", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3202,6 +3191,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/better-npm-run/-/better-npm-run-0.1.1.tgz", "integrity": "sha512-SBBYsUsb6bYcUMF9QUWy39GX5kzD4CoRBP11gx/k5jYkUr4Tr+irAokIeQX5FgfCRz0Q27rt8U0J4D2TlRgQFA==", + "dev": true, "license": "MIT", "dependencies": { "commander": "^2.9.0", @@ -3235,16 +3225,11 @@ "@types/geojson": "^7946.0.16" } }, - "node_modules/blueimp-md5": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3254,6 +3239,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3266,12 +3252,14 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, "license": "ISC" }, "node_modules/browserslist": { "version": "4.29.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3344,6 +3332,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3353,6 +3342,7 @@ "version": "1.0.30001816", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3373,6 +3363,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3382,6 +3373,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3401,17 +3393,6 @@ "dev": true, "license": "MIT" }, - "node_modules/choices.js": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/choices.js/-/choices.js-9.1.0.tgz", - "integrity": "sha512-6NnqiE/MNnNAiMzdW7phJ49nMQylkKMQ6La6PAS1+h1VhrGt38MOPnjzEJ3cRaECieqaGpl9eFGtI2icW27r8A==", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2", - "fuse.js": "^3.4.6", - "redux": "^4.1.2" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -3555,6 +3536,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3567,6 +3549,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -3584,31 +3567,11 @@ "license": "MIT", "peer": true }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/column-resizer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/column-resizer/-/column-resizer-1.4.0.tgz", - "integrity": "sha512-KM5Jh/UBKwVUr01oEGN/OvxF6gZIEn4c1Qde4iHSqNru9hxq93ao3u93qb9N1E1TZ2Sxjh4x7OHGe8v/P8FgkA==", - "license": "BSD-3-Clause", - "dependencies": { - "string-hash": "~1.1.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, "license": "MIT" }, "node_modules/component-emitter": { @@ -3628,6 +3591,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/crelt": { @@ -3659,6 +3623,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3669,29 +3634,11 @@ "node": ">= 8" } }, - "node_modules/datatables.net": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.5.tgz", - "integrity": "sha512-Qrwc+vuw8GHo42u1usWTuriNAMW0VvLPSW3j8g3GxvatiD8wS/ZGW32VAYLLfmF4Hz0C/fo2KB3xZBfcpqqVTQ==", - "license": "MIT", - "dependencies": { - "jquery": ">=1.7" - } - }, - "node_modules/datatables.net-dt": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-2.3.5.tgz", - "integrity": "sha512-JJkMNM03RUkv4jwAqyXsDHqUTEvti3+15466QiauzqRCmBOSwrHoglt1sRbvBwsv2isrCObp50JemgfG8R3wEQ==", - "license": "MIT", - "dependencies": { - "datatables.net": "2.3.5", - "jquery": ">=1.7" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3705,30 +3652,13 @@ } } }, - "node_modules/decode-uri-component": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.5.0.tgz", - "integrity": "sha512-1BiQVoK8C9gUbQU6NzAtO/tkz2qOFpEObMWpcFvhx4fYnj4Oc5yzaJN/LD36ihkVUdXyh5ZekzX+yM+ty/SrPg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -3760,6 +3690,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -3769,6 +3700,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -3781,6 +3713,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -3893,6 +3826,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-2.0.0.tgz", "integrity": "sha512-Y+zZAmv7p2zOdpyZcSIA+aIxohsyfTcNaMeh3YJn9exq85bQhso65Wz9IhjYYNB4zyvXnfi7Ae+FuygARljVJw==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/echarts": { @@ -3915,6 +3849,7 @@ "version": "1.5.459", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.459.tgz", "integrity": "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -3961,12 +3896,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -4029,6 +3958,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4038,6 +3968,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4051,6 +3982,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -4154,6 +4086,7 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -4196,6 +4129,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4208,6 +4142,7 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -4218,6 +4153,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -4234,6 +4170,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -4249,6 +4186,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4261,6 +4199,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -4276,6 +4215,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -4291,6 +4231,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", @@ -4322,6 +4263,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -4334,6 +4276,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -4346,6 +4289,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -4355,6 +4299,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -4402,6 +4347,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4418,6 +4364,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4430,18 +4377,21 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, "license": "MIT" }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -4451,6 +4401,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" @@ -4463,6 +4414,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4472,12 +4424,12 @@ } }, "node_modules/filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-6.1.0.tgz", + "integrity": "sha512-xdMtCAODmPloU9qtmPcdBV9Kd27NtMse+4ayThxqIHUES5Z2S6bGpap5PpdmNM56ub7y3i1eyr+vJJIIgWGKmA==", "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4501,6 +4453,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4513,6 +4466,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -4527,12 +4481,14 @@ "version": "3.4.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, "license": "ISC" }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -4546,6 +4502,7 @@ "version": "11.3.2", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -4560,6 +4517,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -4587,15 +4545,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuse.js": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", - "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==", - "license": "Apache-2.0", - "engines": { - "node": ">=6" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4633,6 +4582,7 @@ "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -4650,6 +4600,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -4662,6 +4613,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -4671,6 +4623,7 @@ "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4683,6 +4636,7 @@ "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.8" @@ -4698,6 +4652,7 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.20.2" @@ -4713,6 +4668,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, "license": "MIT", "dependencies": { "array-union": "^2.1.0", @@ -4733,18 +4689,21 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, "license": "MIT" }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4849,6 +4808,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -4865,6 +4825,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -4881,6 +4842,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -4900,6 +4862,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -4910,6 +4873,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/is-core-module": { @@ -4932,6 +4896,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4957,6 +4922,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4969,6 +4935,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4978,6 +4945,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5000,6 +4968,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5022,18 +4991,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, "funding": [ { "type": "github", @@ -5056,24 +5021,28 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, "license": "MIT" }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -5101,6 +5070,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -5136,6 +5106,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -5514,6 +5485,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, "license": "MIT" }, "node_modules/lodash.startcase": { @@ -5576,6 +5548,7 @@ "version": "11.5.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5585,6 +5558,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -5600,6 +5574,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5626,6 +5601,7 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -5641,6 +5617,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -5657,6 +5634,7 @@ "version": "12.0.3", "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.3.tgz", "integrity": "sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==", + "dev": true, "license": "MIT", "dependencies": { "browser-stdout": "^1.3.1", @@ -5687,6 +5665,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", "integrity": "sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==", + "dev": true, "funding": [ { "type": "github", @@ -5703,6 +5682,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -5712,6 +5692,7 @@ "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -5724,6 +5705,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -5739,6 +5721,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5751,6 +5734,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", + "dev": true, "funding": [ { "type": "github", @@ -5773,6 +5757,7 @@ "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.8" @@ -5788,6 +5773,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -5801,6 +5787,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14.16" @@ -5813,6 +5800,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -5848,6 +5836,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/n3": { @@ -5867,6 +5856,7 @@ "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, "funding": [ { "type": "github", @@ -5885,6 +5875,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, "license": "MIT" }, "node_modules/node-addon-api": { @@ -5899,6 +5890,7 @@ "version": "2.0.59", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.59.tgz", "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -5908,6 +5900,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5917,6 +5910,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5942,6 +5936,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -6044,6 +6039,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -6056,6 +6052,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6065,6 +6062,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6074,6 +6072,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6090,6 +6089,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -6106,6 +6106,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6115,12 +6116,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -6143,6 +6146,7 @@ "version": "8.5.30", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -6171,12 +6175,14 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -6224,6 +6230,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6287,17 +6294,16 @@ "license": "MIT" }, "node_modules/query-string": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.5.1.tgz", - "integrity": "sha512-/zO3RwuRCMTIcEgq6YMv4OrtEE1XzBG7w5N6zc6ydYnkWYOWsnLI/5894hYEzfESfMOT4cHCsRTIdxsSl1KjGg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-10.0.0.tgz", + "integrity": "sha512-QYodx/Ywk8lCcOmsyupXVN91yWPAdjq0xVqE2FE2BHFYnma71C8txR8JCBptjzoUpkfAK7Qjua9VjYRTru4LYg==", "license": "MIT", "dependencies": { - "decode-uri-component": "^0.5.0", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" + "filter-obj": "^6.1.0", + "split-on-first": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6307,6 +6313,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -6448,15 +6455,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/remove-accents": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.4.tgz", @@ -6515,6 +6513,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -6533,6 +6532,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -6548,6 +6548,7 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6559,6 +6560,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -6579,6 +6581,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6625,6 +6628,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -7099,6 +7103,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7111,6 +7116,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=20.0.0" @@ -7120,6 +7126,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7132,6 +7139,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7154,6 +7162,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7209,6 +7218,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7247,12 +7257,12 @@ } }, "node_modules/split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-4.0.0.tgz", + "integrity": "sha512-pyrgNqiEBHA9KlEtg9quYGagtrlTyukL3ryQ+RmjRn9SYmcNQmfa+sWIsI+hlcL4WrmFdrt7IfM6o8ktBAooDQ==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7293,12 +7303,6 @@ "node": ">=0.6.19" } }, - "node_modules/string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", - "license": "CC0-1.0" - }, "node_modules/string-width": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", @@ -7349,6 +7353,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7371,6 +7376,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7389,6 +7395,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -7458,6 +7465,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, "license": "MIT" }, "node_modules/tinyexec": { @@ -7522,6 +7530,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -7534,6 +7543,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -7585,6 +7595,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -7597,6 +7608,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -7616,6 +7628,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7635,6 +7648,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -7644,6 +7658,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7674,6 +7689,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -7860,6 +7876,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7884,6 +7901,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7893,6 +7911,7 @@ "version": "10.0.3", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/wrap-ansi": { @@ -7977,6 +7996,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -8105,6 +8125,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8170,17 +8191,17 @@ "version": "4.6.1", "license": "MIT", "dependencies": { + "@matdata/yasgui-chart-plugin": "^1.2.0", + "@matdata/yasgui-explain-plugin": "^1.0.0", + "@matdata/yasgui-geo-plugin": "^1.3.0", "@matdata/yasgui-graph-plugin": "^1.7.0", - "@matdata/yasgui-table-plugin": "^1.4.0", + "@matdata/yasgui-table-plugin": "^1.7.1", "@matdata/yasgui-utils": "^4.6.1", "@matdata/yasqe": "^4.6.1", "@matdata/yasr": "^4.6.1", "@tarekraafat/autocomplete.js": "^7.2.0", "autosuggest-highlight": "^3.1.1", - "blueimp-md5": "^2.12.0", - "choices.js": "^9.0.1", "dompurify": "^3.4.17", - "es6-object-assign": "^1.1.0", "jsuri": "^1.3.1", "lodash-es": "^4.17.15", "n3": "^1.3.5", @@ -8188,7 +8209,6 @@ }, "devDependencies": { "@types/autosuggest-highlight": "^3.1.0", - "@types/blueimp-md5": "^2.7.0", "@types/jsuri": "^1.3.30", "@types/lodash-es": "^4.17.3", "@types/node": "^22.5.4" @@ -8225,7 +8245,7 @@ "@matdata/yasgui-utils": "^4.6.1", "leaflet": "^1.9.4", "lodash-es": "^4.17.15", - "query-string": "^9.5.1" + "query-string": "^10.0.0" }, "devDependencies": { "@types/leaflet": "^1.9.21", @@ -8263,24 +8283,17 @@ "@codemirror/legacy-modes": "^6.5.3", "@codemirror/state": "^6.7.2", "@codemirror/view": "^6.43.10", - "@fortawesome/free-solid-svg-icons": "^7.1.0", "@json2csv/plainjs": "^7.0.4", "@lezer/highlight": "^1.2.3", "@matdata/yasgui-utils": "^4.6.1", "@matdata/yasqe": "^4.6.1", "codemirror-lang-turtle": "^0.0.2", - "colors": "^1.4.0", - "column-resizer": "^1.4.0", - "datatables.net": "^2.0.5", - "datatables.net-dt": "^2.0.5", "dompurify": "^3.4.17", - "jquery": "^3.7.1", "lodash-es": "^4.17.15", "n3": "^1.3.5", "papaparse": "^5.3.1" }, "devDependencies": { - "@types/jquery": "^3.5.32", "@types/lodash-es": "^4.17.3", "@types/n3": "^1.1.5", "@types/node": "^22.5.4", diff --git a/package.json b/package.json index 625e6f34..9f1e5a91 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,16 @@ "prepare": "husky", "build": "better-npm-run build", "dev": "better-npm-run dev", - "util:lint": "cross-env ESLINT_STRICT=true eslint --ext .tsx --ext .ts \"packages/*/src/**/*.{ts,tsx}\" \"packages/*/test/**/*.{ts,tsx}\" \"packages/*/grammar/**/*.{ts,tsx}\"", + "util:lint": "cross-env ESLINT_STRICT=true eslint --ext .tsx --ext .ts \"packages/*/src/**/*.{ts,tsx}\" \"test/**/*.ts\" \"packages/*/grammar/**/*.{ts,tsx}\"", "util:validateTs": "tsc -p ./tsconfig-validate.json --noEmit", "util:fetchLov": "node fetchLovData.js", + "util:bundleSize": "node scripts/bundle-size.mjs", "util:prettify": "prettier --parser typescript --write \"packages/*/src/**/*.{ts,tsx}\" && prettier --parser css --write \"packages/*/src/**/*.{css,scss}\"", "pretest": "node --input-type=module -e \"import fs from 'fs'; if (!fs.existsSync('./build') || fs.readdirSync('./build').length === 0) { console.log('Run \\\"npm run build\\\" before running a test'); process.exit(1); }\"", "test:dev": "cross-env TEST_ON_DEV_BUILD=1 npm run test", "test": "npm run puppeteer-test && npm run unit-test", "test-live": "cross-env TEST_ON_DEV_BUILD=1 npm run test", - "unit-test": "tsc -p ./tsconfig-test.json && node ./test/fix-esm-imports.mjs && node ./node_modules/mocha/bin/mocha.js \"build/test/**/*-test.js\" --require source-map-support/register || exit 0", + "unit-test": "tsc -p ./tsconfig-test.json && node ./test/fix-esm-imports.mjs && node ./node_modules/mocha/bin/mocha.js \"build/test/**/*-test.js\" --require source-map-support/register", "puppeteer-test": "tsc -p ./tsconfig-test.json && node ./test/fix-esm-imports.mjs && node ./node_modules/mocha/bin/mocha.js --timeout 30000 ./build/test/test/run.js --require source-map-support/register", "prerelease": "npm run build", "release": "changeset publish" @@ -57,20 +58,18 @@ "@matdata/yasgui-explain-plugin": "^1.0.0", "@matdata/yasgui-geo-plugin": "^1.3.0", "@matdata/yasgui-graph-plugin": "^1.7.0", - "@matdata/yasgui-table-plugin": "^1.7.1", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", - "autoprefixer": "^10.4.21", - "better-npm-run": "^0.1.1", - "chai": "^6.2.0", - "fs-extra": "^11.3.2", - "mocha": "^12.0.3" + "@matdata/yasgui-table-plugin": "^1.7.1" }, "devDependencies": { "@changesets/cli": "^2.29.7", "@types/chai": "^5.2.3", "@types/fs-extra": "^11.0.4", "@types/mocha": "^10.0.10", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "autoprefixer": "^10.4.21", + "better-npm-run": "^0.1.1", + "chai": "^6.2.0", "cross-env": "^7.0.3", "esbuild": "^0.28.2", "esbuild-sass-plugin": "^3.3.1", @@ -78,8 +77,10 @@ "eslint-config-prettier": "^6.10.0", "eslint-plugin-jest": "^23.8.1", "eslint-plugin-lodash": "^7.1.0", + "fs-extra": "^11.3.2", "husky": "^9.1.7", "lint-staged": "^16.2.6", + "mocha": "^12.0.3", "postcss": "^8.4.49", "prettier": "^3.6.2", "puppeteer": "^25.16.0", diff --git a/packages/yasgui/package.json b/packages/yasgui/package.json index 2b110a8e..02c2d2c1 100644 --- a/packages/yasgui/package.json +++ b/packages/yasgui/package.json @@ -35,25 +35,24 @@ "directory": "packages/yasgui" }, "dependencies": { - "@tarekraafat/autocomplete.js": "^7.2.0", + "@matdata/yasgui-chart-plugin": "^1.2.0", + "@matdata/yasgui-explain-plugin": "^1.0.0", + "@matdata/yasgui-geo-plugin": "^1.3.0", + "@matdata/yasgui-graph-plugin": "^1.7.0", + "@matdata/yasgui-table-plugin": "^1.7.1", "@matdata/yasgui-utils": "^4.6.1", "@matdata/yasqe": "^4.6.1", "@matdata/yasr": "^4.6.1", + "@tarekraafat/autocomplete.js": "^7.2.0", "autosuggest-highlight": "^3.1.1", - "blueimp-md5": "^2.12.0", - "choices.js": "^9.0.1", "dompurify": "^3.4.17", - "es6-object-assign": "^1.1.0", "jsuri": "^1.3.1", "lodash-es": "^4.17.15", "n3": "^1.3.5", - "sortablejs": "^1.10.2", - "@matdata/yasgui-graph-plugin": "^1.7.0", - "@matdata/yasgui-table-plugin": "^1.4.0" + "sortablejs": "^1.10.2" }, "devDependencies": { "@types/autosuggest-highlight": "^3.1.0", - "@types/blueimp-md5": "^2.7.0", "@types/jsuri": "^1.3.30", "@types/lodash-es": "^4.17.3", "@types/node": "^22.5.4" diff --git a/packages/yasqe/package.json b/packages/yasqe/package.json index fe0b37c6..a0d1484e 100644 --- a/packages/yasqe/package.json +++ b/packages/yasqe/package.json @@ -49,7 +49,7 @@ "@matdata/yasgui-utils": "^4.6.1", "leaflet": "^1.9.4", "lodash-es": "^4.17.15", - "query-string": "^9.5.1" + "query-string": "^10.0.0" }, "devDependencies": { "@types/leaflet": "^1.9.21", diff --git a/packages/yasqe/src/tooltip.ts b/packages/yasqe/src/tooltip.ts index f74e9602..47aeb4ce 100644 --- a/packages/yasqe/src/tooltip.ts +++ b/packages/yasqe/src/tooltip.ts @@ -12,7 +12,7 @@ export default function tooltip(_yasqe: Yasqe, parent: HTMLElement, html: string // escapes the `overflow` clipping of the CodeMirror gutter/scroller, which would // otherwise hide the tooltip behind the editor. let tooltip: HTMLDivElement | undefined; - parent.onmouseover = function (this: HTMLElement) { + parent.onmouseover = function (this: GlobalEventHandlers) { if (!tooltip) { tooltip = document.createElement("div"); tooltip.className = "yasqe_tooltip"; @@ -22,7 +22,7 @@ export default function tooltip(_yasqe: Yasqe, parent: HTMLElement, html: string tooltip.style.display = "block"; // `this` is the element the handler fires on. The gutter marker is rendered as a clone of // `parent`, so position relative to `this` rather than the (possibly detached) `parent`. - const rect = this.getBoundingClientRect(); + const rect = (this as HTMLElement).getBoundingClientRect(); tooltip.style.position = "fixed"; tooltip.style.left = `${rect.right + 6}px`; tooltip.style.top = `${rect.top}px`; diff --git a/packages/yasr/package.json b/packages/yasr/package.json index 64e952b9..bf500b99 100644 --- a/packages/yasr/package.json +++ b/packages/yasr/package.json @@ -45,24 +45,17 @@ "@codemirror/legacy-modes": "^6.5.3", "@codemirror/state": "^6.7.2", "@codemirror/view": "^6.43.10", - "@fortawesome/free-solid-svg-icons": "^7.1.0", "@json2csv/plainjs": "^7.0.4", "@lezer/highlight": "^1.2.3", "@matdata/yasgui-utils": "^4.6.1", "@matdata/yasqe": "^4.6.1", "codemirror-lang-turtle": "^0.0.2", - "colors": "^1.4.0", - "column-resizer": "^1.4.0", - "datatables.net": "^2.0.5", - "datatables.net-dt": "^2.0.5", "dompurify": "^3.4.17", - "jquery": "^3.7.1", "lodash-es": "^4.17.15", "n3": "^1.3.5", "papaparse": "^5.3.1" }, "devDependencies": { - "@types/jquery": "^3.5.32", "@types/lodash-es": "^4.17.3", "@types/n3": "^1.1.5", "@types/node": "^22.5.4", diff --git a/packages/yasr/src/jquery/extendJquery.js b/packages/yasr/src/jquery/extendJquery.js deleted file mode 100644 index 73930078..00000000 --- a/packages/yasr/src/jquery/extendJquery.js +++ /dev/null @@ -1 +0,0 @@ -import "./tableToCsv.js"; diff --git a/packages/yasr/src/jquery/tableToCsv.js b/packages/yasr/src/jquery/tableToCsv.js deleted file mode 100644 index edf337ba..00000000 --- a/packages/yasr/src/jquery/tableToCsv.js +++ /dev/null @@ -1,88 +0,0 @@ -"use strict"; -import $ from "jquery"; - -$.fn.tableToCsv = function (config) { - var csvString = ""; - config = $.extend( - { - quote: '"', - delimiter: ",", - lineBreak: "\n", - }, - config, - ); - - var needToQuoteString = function (value) { - //quote when it contains whitespace or the delimiter - var needQuoting = false; - if (value.match("[\\w|" + config.delimiter + "|" + config.quote + "]")) { - needQuoting = true; - } - return needQuoting; - }; - var addValueToString = function (value) { - //Quotes in the string need to be escaped - value.replace(config.quote, config.quote + config.quote); - if (needToQuoteString(value)) { - value = config.quote + value + config.quote; - } - csvString += " " + value + " " + config.delimiter; - }; - - var addRowToString = function (rowArray) { - rowArray.forEach(function (val) { - addValueToString(val); - }); - csvString += config.lineBreak; - }; - - var tableArrays = []; - var $el = $(this); - var rowspans = {}; - - var totalColCount = 0; - $el.find("tr:first *").each(function () { - if ($(this).attr("colspan")) { - totalColCount += +$(this).attr("colspan"); - } else { - totalColCount++; - } - }); - - $el.find("tr").each(function (rowId, tr) { - var $tr = $(tr); - var rowArray = []; - - var htmlColId = 0; - var actualColId = 0; - while (actualColId < totalColCount) { - if (rowspans[actualColId]) { - rowArray.push(rowspans[actualColId].text); - rowspans[actualColId].rowSpan--; - if (!rowspans[actualColId].rowSpan) rowspans[actualColId] = null; - actualColId++; - continue; - } - - var $cell = $tr.find(":nth-child(" + (htmlColId + 1) + ")"); - if (!$cell) break; - var colspan = $cell.attr("colspan") || 1; - var rowspan = $cell.attr("rowspan") || 1; - - for (var i = 0; i < colspan; i++) { - rowArray.push($cell.text()); - if (rowspan > 1) { - rowspans[actualColId] = { - rowSpan: rowspan - 1, - text: $cell.text(), - }; - } - actualColId++; - } - htmlColId++; - } - addRowToString(rowArray); - }); - - return csvString; -}; diff --git a/scripts/bundle-size.mjs b/scripts/bundle-size.mjs new file mode 100644 index 00000000..00893c12 --- /dev/null +++ b/scripts/bundle-size.mjs @@ -0,0 +1,78 @@ +// Reports what the browser bundles (build/.min.js) are made of, and fails when one grows past its budget. +// +// npm run util:bundleSize report + budget check +// npm run util:bundleSize -- --top 40 +// +// Budgets are in bytes of minified, uncompressed JavaScript. Raise one deliberately, in the PR that needs it. +import fs from "fs"; +import path from "path"; +import zlib from "zlib"; + +const BUDGETS = { + yasgui: 5_400_000, + yasqe: 1_800_000, + yasr: 650_000, + utils: 60_000, +}; + +const topArg = process.argv.indexOf("--top"); +const TOP = topArg > -1 ? Number(process.argv[topArg + 1]) || 20 : 15; + +const kb = (bytes) => `${(bytes / 1024).toFixed(0).padStart(6)} KB`; + +/** Group an input path by npm package, or by source folder for our own code */ +function groupOf(input) { + const parts = input.split(/[\\/]/); + const nm = parts.lastIndexOf("node_modules"); + if (nm > -1) { + const name = parts[nm + 1].startsWith("@") ? `${parts[nm + 1]}/${parts[nm + 2]}` : parts[nm + 1]; + return name; + } + const pkg = parts.indexOf("packages"); + if (pkg > -1) { + const file = parts.slice(pkg + 1).join("/"); + if (file.endsWith(".json")) return file; + return `${parts[pkg + 1]} (${parts.slice(pkg + 3, pkg + 4).join("/") || "src"})`; + } + return input; +} + +let failed = false; +for (const [name, budget] of Object.entries(BUDGETS)) { + const bundle = path.join("build", `${name}.min.js`); + const statsFile = path.join("bundle-stats", `${name}.json`); + if (!fs.existsSync(bundle)) { + console.error(`${bundle} not found: run "npm run build" first`); + process.exit(1); + } + const size = fs.statSync(bundle).size; + const gzip = zlib.gzipSync(fs.readFileSync(bundle), { level: 9 }).length; + const over = size > budget; + failed ||= over; + console.log( + `\n${name}.min.js ${kb(size)} minified, ${kb(gzip)} gzipped budget ${kb(budget)} ${over ? "OVER BUDGET" : "ok"}`, + ); + + if (!fs.existsSync(statsFile)) continue; + const meta = JSON.parse(fs.readFileSync(statsFile, "utf8")); + const output = Object.entries(meta.outputs).find(([file]) => file.endsWith(".js")); + if (!output) continue; + const groups = new Map(); + for (const [input, { bytesInOutput }] of Object.entries(output[1].inputs)) { + const group = groupOf(input); + groups.set(group, (groups.get(group) || 0) + bytesInOutput); + } + [...groups.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, TOP) + .forEach(([group, bytes]) => + console.log(` ${kb(bytes)} ${((bytes / size) * 100).toFixed(1).padStart(5)}% ${group}`), + ); +} + +if (failed) { + console.error( + "\nA bundle is over its budget. Look at what grew above, or raise the budget in scripts/bundle-size.mjs.", + ); + process.exit(1); +} From 6017a03b27a493513178dcb92eed7f6764222496 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:46 +0000 Subject: [PATCH 7/9] build(docker): serve with gzip and cache headers, build on Node 24 The image used the default nginx server: no compression for a 5 MB bundle and no cache policy. docker/nginx.conf gzips text assets (about 1.5 MB transferred), caches the hashed fonts and images for a year, revalidates yasgui.min.js/css, and sets nosniff and a referrer policy. The build stage moves from Node 20 to 24, as in the publish workflow. --- docker/Dockerfile | 3 ++- docker/nginx.conf | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docker/nginx.conf diff --git a/docker/Dockerfile b/docker/Dockerfile index 4dd709ad..ef93d473 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # Use the official Node.js image as the base image -FROM node:20 +FROM node:24 # Set the working directory WORKDIR /app @@ -36,6 +36,7 @@ COPY docker/src/ /usr/share/nginx/html/ USER root +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf COPY docker/change-default-endpoint.sh /docker-entrypoint.d/ RUN chmod +x /docker-entrypoint.d/change-default-endpoint.sh RUN chown 1001:0 -R /usr/share/nginx/html/ diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 00000000..34224d25 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,36 @@ +# MatGUI static site. Replaces the default server of nginx-unprivileged (which listens on 8080). +server { + listen 8080; + listen [::]:8080; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # The bundle is about 5 MB of JavaScript; gzip brings the transfer down to about 1.5 MB + gzip on; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_vary on; + gzip_proxied any; + gzip_types text/css application/javascript text/javascript application/json application/manifest+json image/svg+xml text/plain; + + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Fonts and images emitted by the build carry a content hash in their name + location ~* "-[A-Z0-9]{8}\.(woff2?|ttf|eot|png|jpg|svg)$" { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header X-Content-Type-Options "nosniff" always; + } + + # yasgui.min.js and .css keep their name across releases: let browsers revalidate them + location ~* \.(js|css)$ { + add_header Cache-Control "no-cache"; + add_header X-Content-Type-Options "nosniff" always; + } + + location / { + try_files $uri $uri/ /index.html; + } +} From e284c886ea2033a04c88d12dd8c9fd9e9dcc5147 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:34:46 +0000 Subject: [PATCH 8/9] docs: share-link auto-run, storage-full behaviour and Yasgui.dialogs Also points the README documentation links at matgui-doc.matdata.eu, the domain the app links to, and aligns the Docusaurus url with it. --- README.md | 4 ++-- docs/developer-guide.md | 38 ++++++++++++++++++++++++++++++++++++ docs/user-guide.md | 8 ++++++++ website/docusaurus.config.ts | 2 +- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e7d40abd..d3ab03c1 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,12 @@ The **documentation for Matgui is hosted on GitHub Pages**: -- **📚 Documentation Website**: [https://matgui-doc.matdata.eu/](https://matdata-eu.github.io/Yasgui/) +- **📚 Documentation Website**: [https://matgui-doc.matdata.eu/](https://matgui-doc.matdata.eu/) - User Guide, Developer Guide, API Reference - Built with Docusaurus - Version-tagged with the repository -- **🚀 Development Build**: [https://matgui-doc.matdata.eu/dev/main/](https://matdata-eu.github.io/Yasgui/dev/main/) +- **🚀 Development Build**: [https://matgui-doc.matdata.eu/dev/main/](https://matgui-doc.matdata.eu/dev/main/) - Live build from the main branch - Updated automatically with every commit - Test latest features before release diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 962b7dba..bac79faa 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -664,6 +664,13 @@ interface Config { // Populate config from URL parameters populateFromUrl: boolean | ((config: TabJson) => TabJson); // default: true + // Whether a query opened from a share link runs on load. Update queries never do. + // "safe": runs, unless the request would carry credentials (Authorization or API key + // header, or withCredentials) or the query is invalid; then a message asks first + // "always": runs unless it is an update + // "never": loads the query without running it + autoRunSharedQueries?: "safe" | "always" | "never"; // default: "safe" + // Auto-create first tab on init autoAddOnInit: boolean; // default: true @@ -2577,6 +2584,37 @@ yasgui.helpMenu.removeItem("contact"); `yasgui.whatsNew` (unless `showWhatsNew: false`) has `open()`, `close()`, `hasUnseen()` and `getLatestVersion()`. The release entries live in `packages/yasgui/src/whatsNew/releases.ts`, newest first; add the next version there when preparing a release. The newest version a user has opened is stored as `whatsNewSeen` in the persisted config. +#### Dialogs and notifications + +`Yasgui.dialogs` shows alerts, confirmations and toasts in the MatGUI style, inside the Yasgui root so they follow the theme. Unlike `window.alert` and `window.confirm` they don't block the page, and they keep working when MatGUI is embedded in a sandboxed iframe (where `confirm()` silently returns `false`). Plugins and embedding pages can use them too. + +```javascript +const { showAlert, showConfirm, showChoice, showToast, showCopyValue } = Yasgui.dialogs; + +await showAlert("The query was saved.", { title: "Saved" }); + +if (await showConfirm("Its queries stay in the repository.", { title: "Remove workspace?", confirmLabel: "Remove", danger: true })) { + // ... +} + +const choice = await showChoice( + "This query has unsaved changes.", + [ + { label: "Cancel", value: "cancel" }, + { label: "Discard", value: "discard", kind: "danger" }, + { label: "Save", value: "save", kind: "primary" }, + ], + "cancel", // returned on Escape or a click outside +); + +showToast("Copied", { kind: "success" }); // kind: "info" | "success" | "warning" | "error" +showToast("Results are outdated", { action: { label: "Run again", onClick: () => yasgui.getTab().query() } }); + +await showCopyValue("Copy the URI of this query", "https://example.org/query/1"); +``` + +Each function takes a `host` option: an element inside the Yasgui instance the dialog belongs to. Without it, the dialog opens in the last Yasgui instance created on the page. + ### Tab Class Represents a query tab. diff --git a/docs/user-guide.md b/docs/user-guide.md index 890bb24f..c37a75af 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -1277,6 +1277,8 @@ _OAuth 2.0:_ - **Be cautious on shared computers**: Clear your browser data when using Matgui on shared or public computers - **OAuth 2.0 tokens**: Access tokens are automatically refreshed when expired (if refresh token is available) - **Token security**: OAuth 2.0 uses secure PKCE flow (Proof Key for Code Exchange) for enhanced security +- **Shared links don't use your credentials on their own**: a link that would query an endpoint with your saved credentials asks before it runs (see [Share Queries](#share-queries)) +- **When the browser storage is full**, MatGUI first stops keeping query results with the tabs; if that isn't enough it keeps working without saving and tells you. It never deletes your tabs or credentials to make room **How Authentication Works:** @@ -1645,6 +1647,12 @@ Matgui supports URL-based query sharing: - `endpoint`: The endpoint URL - Other tab configurations +**Opening a shared link:** the query of the link opens in a tab and runs straight away, except in these cases, where MatGUI shows a message and waits for you: + +- **Update queries** (INSERT, DELETE, LOAD, CLEAR, DROP...) never run from a link. Review the query and press **Run** yourself if you want to change the data. +- **Queries that would use your saved credentials** for that endpoint (or cookies) wait until you click **Run query** in the message, so a link someone sends you can't run a query as you without you seeing it. +- **Queries with syntax errors** are loaded without running. + **Command-Line Formats:** All command-line formats (cURL, PowerShell, wget) include: diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 451c2752..a57f835b 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -16,7 +16,7 @@ const config: Config = { }, // Set the production url of your site here - url: "https://matgui-docs.matdata.eu", + url: "https://matgui-doc.matdata.eu", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl: "/", From 141fed1bd8db12f2a6b0b349ed9b63636b3e13c1 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 31 Oct 2026 03:37:35 +0000 Subject: [PATCH 9/9] ci: type-check and lint after the build packages/yasgui/src/version.ts is generated by the build and git-ignored, so util:validateTs failed on a fresh checkout before it. --- .github/workflows/build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d227c6d..a9d08c8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,15 +29,16 @@ jobs: - name: Install dependencies run: npm ci + - name: Build project + run: npm run build + + # After the build, which generates packages/yasgui/src/version.ts - name: Type-check run: npm run util:validateTs - name: Lint run: npm run util:lint - - name: Build project - run: npm run build - - name: Bundle size run: npm run util:bundleSize