From f34b36308c934978ae409607e440d830459efc66 Mon Sep 17 00:00:00 2001 From: Greg Shear Date: Wed, 1 Jul 2026 11:18:40 -0400 Subject: [PATCH 1/5] Add code-health PR check: dead-code delta vs base --- .github/scripts/knip-classify.mjs | 151 ++++ .github/scripts/knip-delta.mjs | 228 ++++++ .github/workflows/health.yml | 120 ++++ knip.json | 6 + package-lock.json | 1107 ++++++++++++++++++++++++++--- package.json | 4 +- 6 files changed, 1534 insertions(+), 82 deletions(-) create mode 100644 .github/scripts/knip-classify.mjs create mode 100644 .github/scripts/knip-delta.mjs create mode 100644 .github/workflows/health.yml create mode 100644 knip.json diff --git a/.github/scripts/knip-classify.mjs b/.github/scripts/knip-classify.mjs new file mode 100644 index 000000000..e3483a15d --- /dev/null +++ b/.github/scripts/knip-classify.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +// Tag each unused exported symbol in a Knip JSON report with whether it's still +// referenced privately within its own file — beyond its own declaration. This +// separates an *unnecessary export* (used privately — just drop `export`, a +// clean diff) from a *dead exported symbol* (used nowhere — the export was only +// hiding it from the unused-var linter, so delete it). +// +// We parse each file with the TypeScript compiler and ask whether any reference +// to the symbol falls outside its own declaration's subtree. A name-count regex +// can't answer that: it reads a recursive call or a self-referencing type as a +// "use" and mislabels genuinely dead code as merely unnecessary. +// +// Must run while the report's ref is checked out, since it reads source files. +// Rewrites the report in place. Usage: node knip-classify.mjs + +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const REPO = process.env.GITHUB_WORKSPACE || process.cwd(); + +// TypeScript ships as CommonJS; import it by absolute path so this script +// resolves it even when run from outside the repo (CI copies it to a temp dir, +// which has no node_modules to walk up into). If TS can't load, leave the report +// unclassified — knip-delta detects that and posts its own alert. +let ts; +try { + const url = pathToFileURL( + path.join(REPO, 'node_modules/typescript/lib/typescript.js') + ).href; + ts = (await import(url)).default; +} catch { + process.exit(0); +} + +const jsonPath = process.argv[2]; +if (!jsonPath) { + console.error('usage: knip-classify.mjs '); + process.exit(1); +} + +const SYM = ['exports', 'types', 'nsExports', 'nsTypes']; + +let data; +try { + data = JSON.parse(readFileSync(jsonPath, 'utf8')); +} catch { + process.exit(0); // nothing to classify; delta script degrades gracefully +} +if (!data || !Array.isArray(data.issues)) process.exit(0); + +// Parse each source file once. Returns the SourceFile, or null if unreadable. +const cache = new Map(); +const parse = (file) => { + if (!cache.has(file)) { + try { + const text = readFileSync(path.join(REPO, file), 'utf8'); + // Pass the real filename so .tsx is parsed as JSX. + cache.set( + file, + ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true) + ); + } catch { + cache.set(file, null); + } + } + return cache.get(file); +}; + +// An identifier that names a member/binding rather than *referencing* the +// symbol: the `b` in `a.b`, the key in `{ b: … }`, a class/interface member +// name, an enum member, an import/export specifier. Shorthand `{ b }` is a real +// reference, so it is intentionally not excluded here. +const isNonReference = (id) => { + const p = id.parent; + if (!p) return false; + if (ts.isPropertyAccessExpression(p)) return p.name === id; + if (ts.isQualifiedName(p)) return p.right === id; + if ( + ts.isPropertyAssignment(p) || + ts.isPropertySignature(p) || + ts.isPropertyDeclaration(p) || + ts.isMethodSignature(p) || + ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) || + ts.isEnumMember(p) || + ts.isImportSpecifier(p) || + ts.isExportSpecifier(p) + ) + return p.name === id; + return false; +}; + +// The top-level statement enclosing a node — the declaration's own subtree. +const topLevelStatement = (node, sf) => { + let n = node; + while (n.parent && n.parent !== sf) n = n.parent; + return n; +}; + +// privateUse: is the symbol referenced anywhere outside its own declaration? +// We find the declaration by the identifier nearest knip's reported `pos`, then +// look for any other reference to the same name outside that declaration's range. +const isPrivatelyUsed = (sf, name, pos) => { + const refs = []; + let decl = null; + let bestDist = Infinity; + const visit = (node) => { + if ( + ts.isIdentifier(node) && + node.text === name && + !isNonReference(node) + ) { + const start = node.getStart(sf); + refs.push(start); + if (typeof pos === 'number') { + const dist = Math.abs(start - pos); + if (dist < bestDist) { + bestDist = dist; + decl = node; + } + } + } + ts.forEachChild(node, visit); + }; + visit(sf); + + // Couldn't anchor to a declaration (missing pos, re-export, generated name): + // fall back to "more than one reference exists" — imperfect, but no worse + // than the old count heuristic. + if (!decl) return refs.length >= 2; + + const stmt = topLevelStatement(decl, sf); + const lo = stmt.getStart(sf); + const hi = stmt.getEnd(); + return refs.some((start) => start < lo || start >= hi); +}; + +for (const issue of data.issues) { + for (const cat of SYM) { + for (const s of issue[cat] ?? []) { + const sf = parse(issue.file); + s.privateUse = sf + ? isPrivatelyUsed(sf, String(s.name), s.pos) + : false; + } + } +} + +writeFileSync(jsonPath, JSON.stringify(data)); diff --git a/.github/scripts/knip-delta.mjs b/.github/scripts/knip-delta.mjs new file mode 100644 index 000000000..82161fcf7 --- /dev/null +++ b/.github/scripts/knip-delta.mjs @@ -0,0 +1,228 @@ +#!/usr/bin/env node +// Diff two Knip JSON reports (base vs head) into a PR "code-health" delta: which +// dead-code issues this PR introduces vs. removes, across the categories Knip +// reports (unused files, exports, dependencies, …). +// +// The comparison is against the PR's base, so the bar ratchets forward on its +// own as main improves — no committed baseline to maintain. +// +// Usage: node knip-delta.mjs +// Always exits 0 (informational). Emits `introduced` / `fixed` as step outputs +// so a future version can gate on `introduced > 0`. +import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'; + +const [, , basePath, headPath, outPath] = process.argv; +const MARKER = ''; +const CAP = 10; // max offenders listed per category before "…and N more" + +// Raw Knip issue arrays we read from each file. `files` is the file itself being +// unused; the rest are arrays of symbols/names on a file. (unresolved imports +// and duplicate exports are intentionally omitted for now — add back here.) +const RAW_CATS = [ + 'files', + 'dependencies', + 'devDependencies', + 'optionalPeerDependencies', + 'unlisted', + 'binaries', + 'exports', + 'types', + 'nsExports', + 'nsTypes', + 'enumMembers', + 'classMembers', +]; +const EXPORTED = new Set(['exports', 'types', 'nsExports', 'nsTypes']); + +// Fold raw categories into the display categories used in the report: +// - deps / devDeps / optional peers collapse into one "unused dependencies" row; +// - an unused export splits by whether the symbol is still used privately in its +// own file — an *unnecessary export* (just drop `export`, clean diff) — or +// unused everywhere — a *dead exported symbol* the export was hiding from the +// linter (delete it). `privateUse` is tagged per-symbol by knip-classify.mjs +// while each ref is checked out; if absent, fall back to one "unused exports". +function displayCat(cat, entry) { + if (EXPORTED.has(cat)) return entry.privateUse ? 'unnecessary' : 'dead'; + if (cat === 'devDependencies' || cat === 'optionalPeerDependencies') + return 'dependencies'; + return cat; +} + +// Display order + labels. +const LABELS = { + files: 'Dead files', + dead: 'Dead symbols', + enumMembers: 'Dead enum members', + unnecessary: 'Unnecessary exports', + dependencies: 'Unused dependencies', + unlisted: 'Unlisted dependencies', + classMembers: 'Unused class members', + binaries: 'Unused binaries', +}; + +// One-line explainer per display category, shown when its section is expanded. +const EXPLAIN = { + files: 'File imported nowhere — delete (or import) it.', + dead: 'Symbol used nowhere — `export` hides it from the linter; delete it', + enumMembers: 'An enum member referenced nowhere', + unnecessary: + 'Exported but only used within its own file — just drop `export`', + dependencies: 'In package.json but never imported', + unlisted: 'Imported but missing from package.json', + classMembers: 'A class member referenced nowhere', + binaries: 'A referenced binary that is not installed', +}; + +function load(path) { + try { + const data = JSON.parse(readFileSync(path, 'utf8')); + if (!data || !Array.isArray(data.issues)) return null; + return data; + } catch { + return null; + } +} + +// Map every issue to a stable key so we can diff the *sets* (not just counts): +// a PR that fixes one issue and adds another nets zero but still introduced one. +function keyset(report) { + const map = new Map(); // key -> {cat, file, name} (cat = display category) + for (const issue of report.issues) { + const file = issue.file ?? '(root)'; + for (const cat of RAW_CATS) { + for (const entry of issue[cat] ?? []) { + const name = + typeof entry === 'string' + ? entry + : (entry?.name ?? JSON.stringify(entry)); + const dcat = displayCat(cat, entry || {}); + const key = + dcat === 'files' + ? `files ${name}` + : `${dcat} ${file} ${name}`; + map.set(key, { cat: dcat, file, name }); + } + } + } + return map; +} + +// Inner text for an offender; wrapped in backticks (monospace) by the caller. +function fmt(entry) { + return entry.cat === 'files' ? entry.name : `${entry.file} : ${entry.name}`; +} + +// A report is "classified" if every exported symbol carries a privateUse tag +// (added by knip-classify.mjs); without it the unnecessary/dead split is a lie, +// so we fail loudly rather than mislabel. +function isClassified(report) { + for (const issue of report.issues) + for (const cat of EXPORTED) + for (const entry of issue[cat] ?? []) + if (entry && entry.privateUse === undefined) return false; + return true; +} + +const base = load(basePath); +const head = load(headPath); + +let body; +if (!base || !head) { + body = `${MARKER}\n## ⚠️ Code Health\n\nCouldn't compute the delta — Knip failed to produce a report on ${!base ? 'the base' : 'this branch'}. (Check the job logs.)`; + writeFileSync(outPath, body); + console.log(body); + process.exit(1); +} + +if (!isClassified(head) || !isClassified(base)) { + body = `${MARKER}\n## ⚠️ Code Health\n\nThe classify step (knip-classify) didn't run, so unnecessary vs. dead exports can't be split. Failing the check — see the job logs.`; + writeFileSync(outPath, body); + console.log(body); + process.exit(1); +} + +const baseKeys = keyset(base); +const headKeys = keyset(head); +const introduced = [...headKeys] + .filter(([k]) => !baseKeys.has(k)) + .map(([, v]) => v); +const fixed = [...baseKeys].filter(([k]) => !headKeys.has(k)).map(([, v]) => v); +const carried = [...headKeys] // "old": present in both base and head (total − new) + .filter(([k]) => baseKeys.has(k)) + .map(([, v]) => v); + +const groupByCat = (items) => { + const m = {}; + for (const i of items) (m[i.cat] ??= []).push(i); + return m; +}; +const newG = groupByCat(introduced); +const fixedG = groupByCat(fixed); +const oldG = groupByCat(carried); + +// Offenders go in a blockquote (normal markdown, so both LaTeX math and inline +// code render). A math-colored ± marker gives new = red `+` / gone = green `-`, +// matching the header on BOTH sign and color — which a ```diff block couldn't, +// since its color is tied to the sign. Paths stay monospace via backticks (the +// marker is colored; the path keeps the default code color). +const MARK = { + new: '$\\textcolor{red}{+}$ ', + fixed: '$\\textcolor{green}{-}$ ', + old: ' '.repeat(5), // non-breaking spaces: won't collapse in the HTML +}; +const blocks = []; +for (const cat of Object.keys(LABELS)) { + const nw = newG[cat] ?? []; + const fx = fixedG[cat] ?? []; + const od = oldG[cat] ?? []; + const total = nw.length + od.length; + if (!total && !fx.length) continue; + + const deltas = []; + if (nw.length) deltas.push(`$\\textcolor{red}{+${nw.length}}$`); + if (fx.length) deltas.push(`$\\textcolor{green}{-${fx.length}}$`); + const summary = + `${total} ${LABELS[cat]}` + + (deltas.length ? ` ${deltas.join(' ')}` : ''); + + const items = [ + ...nw.map((i) => `${MARK.new}\`${fmt(i)}\``), + ...fx.map((i) => `${MARK.fixed}\`${fmt(i)}\``), + ...od.map((i) => `${MARK.old}\`${fmt(i)}\``), + ]; + const lines = items.slice(0, CAP); + if (items.length > CAP) lines.push(`…and ${items.length - CAP} more`); + const list = lines.join('\n'); + + blocks.push( + `${summary}\n\n> ${EXPLAIN[cat]}\n\n${list}\n\n` + ); +} + +// ❌ any dead code added or newly orphaned (even if the PR also cleaned some up); +// ✅ only removals; ⚪ no change. (⚠️ is reserved for a failed check, above.) +let emoji, headline; +if (introduced.length > 0) { + emoji = '❌'; + headline = `Introduces $\\textcolor{red}{${introduced.length}}$ dead-code issue${introduced.length > 1 ? 's' : ''}${fixed.length ? ` (removes $\\textcolor{green}{${fixed.length}}$)` : ''}.`; +} else if (fixed.length > 0) { + emoji = '✅'; + headline = `Removes $\\textcolor{green}{${fixed.length}}$ dead-code issue${fixed.length > 1 ? 's' : ''}, introduces none.`; +} else { + emoji = '⚪'; + headline = 'No change to the dead-code surface.'; +} + +body = + `${MARKER}\n## ${emoji} Code Health\n\n${headline}\n\n` + blocks.join('\n'); + +writeFileSync(outPath, body); +console.log(body); + +if (process.env.GITHUB_STEP_SUMMARY) + appendFileSync(process.env.GITHUB_STEP_SUMMARY, body + '\n'); +if (process.env.GITHUB_OUTPUT) + appendFileSync( + process.env.GITHUB_OUTPUT, + `introduced=${introduced.length}\nfixed=${fixed.length}\n` + ); diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml new file mode 100644 index 000000000..329c80600 --- /dev/null +++ b/.github/workflows/health.yml @@ -0,0 +1,120 @@ +name: Code Health + +# Informational dead-code delta for each PR. Runs Knip on the PR head and on the +# base, then comments the difference. The bar is always "the base branch," so it +# ratchets forward on its own as main improves — no committed baseline needed. + +permissions: + contents: read + packages: read # `@estuary/flow-web` install + pull-requests: write # post the delta comment + +on: + pull_request: + +concurrency: + group: health-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + knip-delta: + name: Dead-code delta + runs-on: ubuntu-latest + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 22.22.0 + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + registry-url: 'https://npm.pkg.github.com' + scope: '@estuary' + + - name: Get Deps + run: npm ci + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Both runs use the head branch's installed Knip/toolchain, so only the + # source differs between them. Knip exits non-zero when it finds issues + # (expected on a dirty tree), so tolerate it and let the script read JSON. + - name: Knip on PR head + run: | + git checkout --force --quiet ${{ github.event.pull_request.head.sha }} + # Stash the classifier so it survives the base checkout below + # (base doesn't have it on a PR that introduces this check). + cp .github/scripts/knip-classify.mjs "$RUNNER_TEMP/classify.mjs" + node_modules/.bin/knip --reporter json > "$RUNNER_TEMP/head.json" || true + node "$RUNNER_TEMP/classify.mjs" "$RUNNER_TEMP/head.json" + + - name: Knip on base (measured with the head's config) + run: | + git checkout --force --quiet ${{ github.event.pull_request.base.sha }} + # Pin the HEAD's Knip config for the base run too, so a PR that + # only changes config (like the one that introduces this check) + # doesn't manufacture a phantom delta. The ruler is constant; + # only source differences register. Falls back to base's config. + git checkout --quiet ${{ github.event.pull_request.head.sha }} -- knip.json 2>/dev/null || true + node_modules/.bin/knip --reporter json > "$RUNNER_TEMP/base.json" || true + # Classify against the base's checked-out source (files present now). + node "$RUNNER_TEMP/classify.mjs" "$RUNNER_TEMP/base.json" + + - name: Restore head + run: git checkout --force --quiet ${{ github.event.pull_request.head.sha }} + + - name: Compute delta + id: delta + run: node .github/scripts/knip-delta.mjs "$RUNNER_TEMP/base.json" "$RUNNER_TEMP/head.json" "$RUNNER_TEMP/health.md" + + # always(): the delta step exits non-zero (fails the check) when the + # tool itself is broken — knip produced no report, or the classify + # step didn't run — and we still want its alert comment posted. + - name: Upsert PR comment + if: always() + uses: actions/github-script@v7 + env: + HEALTH_MD: ${{ runner.temp }}/health.md + with: + script: | + const fs = require('fs'); + // No report file → an earlier step failed before the delta ran; nothing to post. + if (!fs.existsSync(process.env.HEALTH_MD)) return; + const body = fs.readFileSync(process.env.HEALTH_MD, 'utf8'); + const marker = ''; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + }); + const existing = comments.find( + (c) => c.body && c.body.includes(marker) + ); + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } + + # Once you trust the signal, un-comment to make new dead code block the PR: + # - name: Enforce no new dead code + # if: steps.delta.outputs.introduced != '0' + # run: | + # echo "::error::This PR introduces ${{ steps.delta.outputs.introduced }} new dead-code issue(s). See the Code-health comment." + # exit 1 diff --git a/knip.json b/knip.json new file mode 100644 index 000000000..c8f55f61a --- /dev/null +++ b/knip.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "tags": ["-lintignore"], + "project": ["src/**/*.{ts,tsx}"], + "ignore": ["src/gql-types/**", "playwright-tests/**"] +} diff --git a/package-lock.json b/package-lock.json index 8a2370e4f..61a73db10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,6 +120,7 @@ "eslint-plugin-storybook": "^10.3.4", "eslint-plugin-unused-imports": "^4.3.0", "jsdom": "^24.0.0", + "knip": "^6.23.0", "licensee": "^11.1.1", "msw": "^2.0.11", "prettier": "^3.5.3", @@ -636,10 +637,33 @@ "react": ">=16.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -4143,6 +4167,25 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -4873,79 +4916,725 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", - "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/resources": "2.2.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz", + "integrity": "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz", + "integrity": "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz", + "integrity": "sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz", + "integrity": "sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz", + "integrity": "sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz", + "integrity": "sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz", + "integrity": "sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz", + "integrity": "sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz", + "integrity": "sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz", + "integrity": "sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz", + "integrity": "sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz", + "integrity": "sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz", + "integrity": "sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz", + "integrity": "sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz", + "integrity": "sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz", + "integrity": "sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz", + "integrity": "sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz", + "integrity": "sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz", + "integrity": "sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.137.0.tgz", + "integrity": "sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", + "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", + "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", + "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", + "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", + "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", + "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", + "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", + "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", + "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", + "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", + "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", + "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", + "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", + "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", + "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", - "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", - "license": "Apache-2.0", + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", + "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", + "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=14.0.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", - "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", - "license": "Apache-2.0", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/resources": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", - "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", - "license": "Apache-2.0", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "tslib": "^2.4.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", - "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", + "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz", + "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", @@ -5971,6 +6660,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -6151,12 +6851,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", - "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~8.3.0" } }, "node_modules/@types/normalize-package-data": { @@ -10641,6 +11341,26 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, + "node_modules/fd-package-json/node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -10824,6 +11544,22 @@ "node": ">= 6" } }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -11015,6 +11751,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -12472,9 +13221,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -12790,6 +13539,89 @@ "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", "dev": true }, + "node_modules/knip": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.23.0.tgz", + "integrity": "sha512-2DvAOX2pZWiG4SLvRRxOAU0aWGEn1ZoVblI541xIoXFdHqq2THMZXy66/qcY5WGuW3TXhb9T1x1zd/Hd1u+yqg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "formatly": "^0.3.0", + "get-tsconfig": "4.14.0", + "jiti": "^2.7.0", + "oxc-parser": "^0.137.0", + "oxc-resolver": "11.21.3", + "picomatch": "^4.0.4", + "smol-toml": "^1.6.1", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.1", + "yaml": "^2.9.0", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -14580,6 +15412,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-parser": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.137.0.tgz", + "integrity": "sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.137.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.137.0", + "@oxc-parser/binding-android-arm64": "0.137.0", + "@oxc-parser/binding-darwin-arm64": "0.137.0", + "@oxc-parser/binding-darwin-x64": "0.137.0", + "@oxc-parser/binding-freebsd-x64": "0.137.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.137.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.137.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.137.0", + "@oxc-parser/binding-linux-arm64-musl": "0.137.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.137.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.137.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.137.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.137.0", + "@oxc-parser/binding-linux-x64-gnu": "0.137.0", + "@oxc-parser/binding-linux-x64-musl": "0.137.0", + "@oxc-parser/binding-openharmony-arm64": "0.137.0", + "@oxc-parser/binding-wasm32-wasi": "0.137.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.137.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.137.0", + "@oxc-parser/binding-win32-x64-msvc": "0.137.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz", + "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.3", + "@oxc-resolver/binding-android-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-x64": "11.21.3", + "@oxc-resolver/binding-freebsd-x64": "11.21.3", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-musl": "11.21.3", + "@oxc-resolver/binding-openharmony-arm64": "11.21.3", + "@oxc-resolver/binding-wasm32-wasi": "11.21.3", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" + } + }, "node_modules/p-limit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", @@ -16181,6 +17082,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -16946,6 +17857,19 @@ "npm": ">= 3.0.0" } }, + "node_modules/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -17833,14 +18757,14 @@ "dev": true }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -18242,6 +19166,16 @@ "node": ">=14.17" } }, + "node_modules/unbash": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.2.tgz", + "integrity": "sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -18272,9 +19206,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/unicode-emoji-utils": { @@ -19453,10 +20387,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -19538,6 +20473,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zrender": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", diff --git a/package.json b/package.json index d14beb502..eb369cc43 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "build-storybook": "storybook build", "codegen": "graphql-codegen", "codegen:local": "LOCAL=true graphql-codegen", - "codegen:check": "graphql-codegen --check" + "codegen:check": "graphql-codegen --check", + "knip": "knip" }, "dependencies": { "@date-fns/utc": "^2.1.1", @@ -157,6 +158,7 @@ "eslint-plugin-storybook": "^10.3.4", "eslint-plugin-unused-imports": "^4.3.0", "jsdom": "^24.0.0", + "knip": "^6.23.0", "licensee": "^11.1.1", "msw": "^2.0.11", "prettier": "^3.5.3", From cbec2a5a110744b2a4852a46bdac747e9d995667 Mon Sep 17 00:00:00 2001 From: Greg Shear Date: Wed, 1 Jul 2026 17:12:17 -0400 Subject: [PATCH 2/5] Convert unnecessary drawer navigation to flex Box --- .gitignore | 1 + src/app/Layout.tsx | 32 ++----------- src/components/navigation/Navigation.tsx | 58 +++++++++--------------- src/context/Theme.tsx | 9 ++-- 4 files changed, 31 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index a5ab5c90b..be2f650dd 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ storybook-static .playwright-mcp/ .mcp.json +AGENTS.md diff --git a/src/app/Layout.tsx b/src/app/Layout.tsx index fa58df8c8..3d41879bc 100644 --- a/src/app/Layout.tsx +++ b/src/app/Layout.tsx @@ -6,37 +6,20 @@ import { useShallow } from 'zustand/react/shallow'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import { Outlet } from 'react-router'; -import { useLocalStorage } from 'react-use'; import { Toast } from 'src/components/AgentSkills/Toast'; -import Navigation from 'src/components/navigation/Navigation'; +import { Navigation } from 'src/components/navigation/Navigation'; import ErrorBoundryWrapper from 'src/components/shared/ErrorBoundryWrapper'; import PageContainer from 'src/components/shared/PageContainer'; import DocsSidePanel from 'src/components/sidePanelDocs/SidePanel'; import { useShowSidePanelDocs } from 'src/context/SidePanelDocs'; -import { NavWidths } from 'src/context/Theme'; import { useSidePanelDocsStore } from 'src/stores/SidePanelDocs/Store'; -import { LocalStorageKeys } from 'src/utils/localStorage-utils'; import { hasLength } from 'src/utils/misc-utils'; function AppLayout() { const theme = useTheme(); const belowMd = useMediaQuery(theme.breakpoints.down('md')); - const [navigationConfig, setNavigationConfig] = useLocalStorage( - LocalStorageKeys.NAVIGATION_SETTINGS, - { open: true } - ); - - const navigationOpen = navigationConfig?.open ?? true; - const navigationWidth: NavWidths = navigationConfig?.open - ? NavWidths.FULL - : NavWidths.RAIL; - - const toggleNavigationDrawer = () => { - setNavigationConfig({ open: !navigationOpen }); - }; - // Splitter for the side panel docs const [leftPaneFlex, setLeftPaneFlex] = useState(0.0); const [rightPaneFlex, setRightPaneFlex] = useState(0.0); @@ -75,20 +58,15 @@ function AppLayout() { }; return ( - - - - + + diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index 82ef0714b..324195e5e 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -10,7 +10,6 @@ import { Tooltip, useTheme, } from '@mui/material'; -import MuiDrawer, { drawerClasses } from '@mui/material/Drawer'; import { CloudDownload, @@ -21,49 +20,36 @@ import { Settings, } from 'iconoir-react'; import { useIntl } from 'react-intl'; +import { useLocalStorage } from 'react-use'; import { authenticatedRoutes } from 'src/app/routes'; import ListItemLink from 'src/components/navigation/ListItemLink'; import ModeSwitch from 'src/components/navigation/ModeSwitch'; -import { paperBackground } from 'src/context/Theme'; +import { NavWidths, paperBackground } from 'src/context/Theme'; +import { LocalStorageKeys as Keys } from 'src/utils/localStorage-utils'; -interface NavigationProps { - open: boolean; - width: number; - onNavigationToggle: Function; -} - -const Navigation = ({ open, width, onNavigationToggle }: NavigationProps) => { +export const Navigation = () => { const intl = useIntl(); const theme = useTheme(); - const openNavigation = () => { - onNavigationToggle(true); - }; - - const closeNavigation = () => { - onNavigationToggle(false); - }; + const [nav, setNav] = useLocalStorage<{ open: boolean }>( + Keys.NAVIGATION_SETTINGS + ); + const open = nav?.open ?? true; return ( - - `${paperTheme.transitions.duration.shortest}ms`, - width, - border: 0, - background: paperBackground[theme.palette.mode], - }, - transition: (drawerTheme) => - `${drawerTheme.transitions.duration.shortest}ms`, - width, + height: '100%', + width: open ? NavWidths.FULL : NavWidths.RAIL, + flexShrink: 0, + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + background: paperBackground[theme.palette.mode], + boxSizing: 'border-box', + transition: (boxTheme) => + `${boxTheme.transitions.duration.shortest}ms`, }} > @@ -129,7 +115,7 @@ const Navigation = ({ open, width, onNavigationToggle }: NavigationProps) => { > setNav({ open: !open })} sx={{ minHeight: 45, px: 1.5, @@ -160,8 +146,6 @@ const Navigation = ({ open, width, onNavigationToggle }: NavigationProps) => { - + ); }; - -export default Navigation; diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index 62174da64..426961ed2 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -92,11 +92,10 @@ declare module '@mui/material/Typography' { } // Navigation Width -export enum NavWidths { - MOBILE = 0, - RAIL = 48, - FULL = 200, -} +export const NavWidths = { + RAIL: 48, + FULL: 200, +}; // Colors export const sample_blue = { From cb9661b4f1bebd95979639a57d8683724fbf5392 Mon Sep 17 00:00:00 2001 From: Greg Shear Date: Wed, 1 Jul 2026 17:47:14 -0400 Subject: [PATCH 3/5] some PR feedback --- src/components/navigation/Navigation.tsx | 8 ++++++-- src/context/Theme.tsx | 6 ------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index 324195e5e..ec5d04802 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -25,9 +25,14 @@ import { useLocalStorage } from 'react-use'; import { authenticatedRoutes } from 'src/app/routes'; import ListItemLink from 'src/components/navigation/ListItemLink'; import ModeSwitch from 'src/components/navigation/ModeSwitch'; -import { NavWidths, paperBackground } from 'src/context/Theme'; +import { paperBackground } from 'src/context/Theme'; import { LocalStorageKeys as Keys } from 'src/utils/localStorage-utils'; +const NavWidths = { + RAIL: 48, + FULL: 200, +} as const; + export const Navigation = () => { const intl = useIntl(); const theme = useTheme(); @@ -114,7 +119,6 @@ export const Navigation = () => { enterDelay={open ? 1000 : undefined} > setNav({ open: !open })} sx={{ minHeight: 45, diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index 426961ed2..e374d889f 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -91,12 +91,6 @@ declare module '@mui/material/Typography' { } } -// Navigation Width -export const NavWidths = { - RAIL: 48, - FULL: 200, -}; - // Colors export const sample_blue = { 100: '#DCE6FE', From 5e3230c15e88edea13273a631e2c2056dfb813db Mon Sep 17 00:00:00 2001 From: Greg Shear Date: Thu, 2 Jul 2026 13:40:58 -0400 Subject: [PATCH 4/5] move topbar items to sidebar, restyle sidebar and page content area this is the precursor UI work that makes room for the dashboard agent feature --- src/app/Layout.tsx | 16 +- .../AgentSkills/{HeaderPill.tsx => Pill.tsx} | 78 +++---- src/components/AgentSkills/Toast.tsx | 59 ++--- src/components/AgentSkills/shared.ts | 5 - src/components/graphics/CompanyLogo.tsx | 4 +- src/components/graphics/CompanyMark.tsx | 22 ++ src/components/menus/HelpMenu.tsx | 33 ++- src/components/menus/UserMenu.tsx | 97 -------- src/components/navigation/ListItemLink.tsx | 66 ------ src/components/navigation/ModeSwitch.tsx | 52 ----- src/components/navigation/NavItems.tsx | 75 +++++++ src/components/navigation/Navigation.tsx | 211 +++++++++--------- src/components/navigation/PageTitle.tsx | 41 ---- src/components/navigation/TopBar.tsx | 59 ----- src/components/navigation/User.tsx | 142 ++++++++++++ src/components/shared/PageContainer.tsx | 61 ++++- src/components/shared/RelativeAppBar.tsx | 17 -- src/components/sidePanelDocs/SidePanel.tsx | 2 - src/components/tables/EntityTable/index.tsx | 10 +- src/context/Theme.tsx | 38 +++- src/lang/en-US/AgentSkills.ts | 10 - src/lang/en-US/Navigation.ts | 15 -- src/lang/en-US/index.ts | 2 - src/stores/useNavigationStore.ts | 35 +++ 24 files changed, 558 insertions(+), 592 deletions(-) rename src/components/AgentSkills/{HeaderPill.tsx => Pill.tsx} (69%) create mode 100644 src/components/graphics/CompanyMark.tsx delete mode 100644 src/components/menus/UserMenu.tsx delete mode 100644 src/components/navigation/ListItemLink.tsx delete mode 100644 src/components/navigation/ModeSwitch.tsx create mode 100644 src/components/navigation/NavItems.tsx delete mode 100644 src/components/navigation/PageTitle.tsx delete mode 100644 src/components/navigation/TopBar.tsx create mode 100644 src/components/navigation/User.tsx delete mode 100644 src/components/shared/RelativeAppBar.tsx delete mode 100644 src/lang/en-US/AgentSkills.ts create mode 100644 src/stores/useNavigationStore.ts diff --git a/src/app/Layout.tsx b/src/app/Layout.tsx index 3d41879bc..79223ea40 100644 --- a/src/app/Layout.tsx +++ b/src/app/Layout.tsx @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react'; -import { Box, Toolbar, useMediaQuery, useTheme } from '@mui/material'; +import { Box, useMediaQuery, useTheme } from '@mui/material'; import { useShallow } from 'zustand/react/shallow'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import { Outlet } from 'react-router'; -import { Toast } from 'src/components/AgentSkills/Toast'; +import { Toast as AgentSkillsToast } from 'src/components/AgentSkills/Toast'; import { Navigation } from 'src/components/navigation/Navigation'; import ErrorBoundryWrapper from 'src/components/shared/ErrorBoundryWrapper'; import PageContainer from 'src/components/shared/PageContainer'; @@ -60,14 +60,19 @@ function AppLayout() { return ( - - + { + // Hide the toast while the docs panel is open so it cannot cover the + // cookie-consent banner that renders inside the docs. + !displaySidePanel && + } @@ -81,9 +86,8 @@ function AppLayout() { : undefined, }} > - + - diff --git a/src/components/AgentSkills/HeaderPill.tsx b/src/components/AgentSkills/Pill.tsx similarity index 69% rename from src/components/AgentSkills/HeaderPill.tsx rename to src/components/AgentSkills/Pill.tsx index b15bf6e35..f0e9b73f7 100644 --- a/src/components/AgentSkills/HeaderPill.tsx +++ b/src/components/AgentSkills/Pill.tsx @@ -2,12 +2,9 @@ import { Box, Link, Paper, Tooltip, Typography, useTheme } from '@mui/material'; import { usePostHog } from '@posthog/react'; import { NavArrowRight } from 'iconoir-react'; -import { useIntl } from 'react-intl'; import { AGENT_SKILLS_URL, - BG_GRADIENT, - GRADIENT, LINK_COLOR, SECONDARY_TEXT_COLOR, SHIMMER_STYLES, @@ -18,7 +15,6 @@ import { SparkleIcon } from 'src/components/AgentSkills/SparkleIcon'; function TooltipContent({ onClick }: { onClick: () => void }) { const theme = useTheme(); const mode = theme.palette.mode; - const intl = useIntl(); return ( void }) { mb: 0.5, }} > - {intl.formatMessage({ - id: 'agentSkills.title', - })} + Meet Estuary Agent Skills void }) { mb: '10px', }} > - {intl.formatMessage({ - id: 'agentSkills.description', - })} + Give any AI agent the skills to build connectors, debug + pipelines, and check stats on Estuary, from wherever you + work. void }) { gap: 0.5, }} > - {intl.formatMessage({ - id: 'agentSkills.cta', - })} - + Read the docs + ); } -export function HeaderPill() { - const theme = useTheme(); - const mode = theme.palette.mode; - const intl = useIntl(); +export function Pill() { const postHog = usePostHog(); const toastDismissed = useAgentSkillsStore((s) => s.toastDismissed); @@ -113,9 +102,9 @@ export function HeaderPill() { }} /> } - placement="bottom-end" + placement="bottom-start" enterDelay={200} - leaveDelay={150} + leaveDelay={100} slotProps={{ tooltip: { sx: { @@ -137,54 +126,49 @@ export function HeaderPill() { }) } sx={{ - 'display': 'inline-flex', + 'display': 'flex', + 'width': '100%', 'alignItems': 'center', + 'justifyContent': 'flex-start', 'gap': 1, - 'height': 36, - 'px': '14px', - 'pl': '10px', - 'borderRadius': '999px', - 'background': BG_GRADIENT[mode], - 'border': '1px solid rgba(46,100,235,0.22)', + 'py': '6px', + 'px': '10px', + 'borderRadius': '8px', + 'color': 'text.primary', 'fontSize': 13, - 'fontWeight': 600, - 'transition': 'transform 200ms ease', + 'fontWeight': 400, + 'cursor': 'pointer', + 'transition': + 'background 180ms ease, transform 180ms ease, box-shadow 180ms ease', '&:hover': { background: 'linear-gradient(135deg, rgba(46,100,235,0.14) 0%, rgba(54,197,176,0.16) 100%)', - borderColor: 'rgba(46,100,235,0.42)', boxShadow: '0 6px 16px -6px rgba(46,100,235,0.35)', - transform: 'translateY(-1px)', }, }} > - - - + /> - {intl.formatMessage({ id: 'agentSkills.pill.label' })} + Agent Skills diff --git a/src/components/AgentSkills/Toast.tsx b/src/components/AgentSkills/Toast.tsx index 8a1b4eefd..d752843b0 100644 --- a/src/components/AgentSkills/Toast.tsx +++ b/src/components/AgentSkills/Toast.tsx @@ -2,7 +2,6 @@ import type { MouseEvent } from 'react'; import { Box, - Chip, IconButton, keyframes, Link, @@ -12,7 +11,6 @@ import { import { usePostHog } from '@posthog/react'; import { NavArrowRight, Xmark } from 'iconoir-react'; -import { useIntl } from 'react-intl'; import { AGENT_SKILLS_URL, @@ -23,28 +21,20 @@ import { useAgentSkillsStore, } from 'src/components/AgentSkills/shared'; import { SparkleIcon } from 'src/components/AgentSkills/SparkleIcon'; -import { toastIndex } from 'src/context/Theme'; const toastIn = keyframes` 0% { opacity: 0; transform: translateY(16px) scale(0.98); } 100% { opacity: 1; transform: translateY(0) scale(1); } `; -interface ToastProps { - docsPanelOpen?: boolean; -} - -export function Toast({ docsPanelOpen }: ToastProps) { +export function Toast() { const theme = useTheme(); const mode = theme.palette.mode; - const intl = useIntl(); const postHog = usePostHog(); const toastDismissed = useAgentSkillsStore((s) => s.toastDismissed); const dismissToast = useAgentSkillsStore((s) => s.dismissToast); - // If the docs panel is open just hide the toast. That way it cannot cover up - // the cookie consent banner in the docs. - if (toastDismissed || docsPanelOpen) { + if (toastDismissed) { return null; } @@ -71,7 +61,7 @@ export function Toast({ docsPanelOpen }: ToastProps) { 'display': 'block', 'animation': `${toastIn} 750ms cubic-bezier(.2,.9,.25,1) 1s both`, 'transition': 'transform 200ms ease, box-shadow 200ms ease', - 'zIndex': toastIndex, + 'zIndex': theme.zIndex.snackbar, '&:hover': { transform: 'translateY(-2px)', boxShadow: @@ -127,21 +117,22 @@ export function Toast({ docsPanelOpen }: ToastProps) { mb: 0.5, }} > - + > + New + - {intl.formatMessage({ - id: 'agentSkills.eyebrow', - })} + For Claude, Cursor, Codex and more @@ -166,7 +155,7 @@ export function Toast({ docsPanelOpen }: ToastProps) { mb: 0.5, }} > - {intl.formatMessage({ id: 'agentSkills.title' })} + Meet Estuary Agent Skills - {intl.formatMessage({ id: 'agentSkills.description' })} + Give any AI agent the skills to build connectors, debug + pipelines, and check stats on Estuary, from wherever you + work. - {intl.formatMessage({ id: 'agentSkills.cta' })} + Read the docs @@ -216,9 +207,7 @@ export function Toast({ docsPanelOpen }: ToastProps) { }); dismissToast(); }} - aria-label={intl.formatMessage({ - id: 'agentSkills.dismiss', - })} + aria-label="Dismiss" sx={{ 'width': 20, 'height': 20, @@ -227,7 +216,7 @@ export function Toast({ docsPanelOpen }: ToastProps) { '&:hover': { color: '#475569', background: 'none' }, }} > - + diff --git a/src/components/AgentSkills/shared.ts b/src/components/AgentSkills/shared.ts index e1b0495ad..ebc5d42ec 100644 --- a/src/components/AgentSkills/shared.ts +++ b/src/components/AgentSkills/shared.ts @@ -35,11 +35,6 @@ export const GRADIENT = { dark: 'linear-gradient(135deg, #1a4bcb 0%, #3a6fd4 45%, #2a9e8c 100%)', }; -export const BG_GRADIENT = { - light: 'linear-gradient(135deg, rgba(46,100,235,0.08) 0%, rgba(54,197,176,0.10) 100%)', - dark: 'linear-gradient(180deg, rgba(46,100,235,0.12) 0%, rgba(54,197,176,0.16) 100%)', -}; - export const SECONDARY_TEXT_COLOR = { light: '#475569', dark: '#94a3b8', diff --git a/src/components/graphics/CompanyLogo.tsx b/src/components/graphics/CompanyLogo.tsx index 0be8ed03a..2645d90f9 100644 --- a/src/components/graphics/CompanyLogo.tsx +++ b/src/components/graphics/CompanyLogo.tsx @@ -12,8 +12,8 @@ function CompanyLogo() { return ( {intl.formatMessage({ ); diff --git a/src/components/graphics/CompanyMark.tsx b/src/components/graphics/CompanyMark.tsx new file mode 100644 index 000000000..e4aa95743 --- /dev/null +++ b/src/components/graphics/CompanyMark.tsx @@ -0,0 +1,22 @@ +import { useTheme } from '@mui/material'; + +import { useIntl } from 'react-intl'; + +import darkMark from 'src/images/pictorial-marks/pictorial-mark__multi-blue.png'; +import lightMark from 'src/images/pictorial-marks/pictorial-mark__white.png'; + +function CompanyMark() { + const intl = useIntl(); + const theme = useTheme(); + + return ( + {intl.formatMessage({ + ); +} + +export default CompanyMark; diff --git a/src/components/menus/HelpMenu.tsx b/src/components/menus/HelpMenu.tsx index c1855ce0a..ffbdb02e7 100644 --- a/src/components/menus/HelpMenu.tsx +++ b/src/components/menus/HelpMenu.tsx @@ -1,18 +1,31 @@ -import { HelpCircle } from 'iconoir-react'; +import { Menu } from '@mui/material'; + import { FormattedMessage, useIntl } from 'react-intl'; -import IconMenu from 'src/components/menus/IconMenu'; import ExternalLinkMenuItem from 'src/components/shared/ExternalLinkMenuItem'; -function HelpMenu() { +interface HelpMenuProps { + anchorEl: HTMLElement | null; + onClose: () => void; +} + +export function HelpMenu({ anchorEl, onClose }: HelpMenuProps) { const intl = useIntl(); return ( - } - identifier="help-menu" - tooltip={intl.formatMessage({ id: 'helpMenu.tooltip' })} + - + ); } - -export default HelpMenu; diff --git a/src/components/menus/UserMenu.tsx b/src/components/menus/UserMenu.tsx deleted file mode 100644 index 7bf6a3ccd..000000000 --- a/src/components/menus/UserMenu.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition */ -import type { SxProps } from '@mui/material'; - -import { Stack, Typography } from '@mui/material'; -import Divider from '@mui/material/Divider'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import MenuItem from '@mui/material/MenuItem'; - -import { useShallow } from 'zustand/react/shallow'; - -import { LogOut, Mail, ProfileCircle } from 'iconoir-react'; -import { FormattedMessage, useIntl } from 'react-intl'; - -import IconMenu from 'src/components/menus/IconMenu'; -import UserAvatar from 'src/components/shared/UserAvatar'; -import { supabaseClient } from 'src/context/GlobalProviders'; -import { useUserStore } from 'src/context/User/useUserContextStore'; - -interface Props { - iconColor: string; -} - -const nonInteractiveMenuStyling: SxProps = { - '&:hover': { - cursor: 'revert', - }, -}; - -const UserMenu = ({ iconColor }: Props) => { - const intl = useIntl(); - const userDetails = useUserStore(useShallow((state) => state.userDetails)); - - const handlers = { - logout: async () => { - await supabaseClient.auth.signOut(); - }, - }; - - if (userDetails) { - const { avatar, email, emailVerified, userName } = userDetails; - return ( - - } - identifier="account-menu" - tooltip={intl.formatMessage({ id: 'accountMenu.tooltip' })} - > - - - - - {userName} - - - - - - - - - {email} - - {emailVerified ? ( - - - - ) : null} - - - - - - { - void handlers.logout(); - }} - > - - - - - - - - ); - } else { - return null; - } -}; - -export default UserMenu; diff --git a/src/components/navigation/ListItemLink.tsx b/src/components/navigation/ListItemLink.tsx deleted file mode 100644 index c55533c5d..000000000 --- a/src/components/navigation/ListItemLink.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { ReactElement } from 'react'; - -import { - ListItemButton, - ListItemIcon, - ListItemText, - Tooltip, -} from '@mui/material'; - -import { useIntl } from 'react-intl'; -import { Link, useMatch, useResolvedPath } from 'react-router-dom'; - -interface Props { - icon: ReactElement; - title: string; - link: string; -} - -export const ListItemLink = ({ icon, title, link }: Props) => { - const resolved = useResolvedPath(link); - const selected = Boolean( - useMatch({ - path: resolved.pathname, - end: false, // `end: false` matches nested routes e.g. `/admin/billing` - }) - ); - - return ( -
  • - - - theme.palette.text.primary, - }} - > - {icon} - - - - - -
  • - ); -}; - -/** @deprecated Prefer the named `ListItemLink` export */ -const ListItemLinkWrapper = ({ title, ...props }: Props) => { - const intl = useIntl(); - - return ( - - ); -}; - -export default ListItemLinkWrapper; diff --git a/src/components/navigation/ModeSwitch.tsx b/src/components/navigation/ModeSwitch.tsx deleted file mode 100644 index 8328dd40f..000000000 --- a/src/components/navigation/ModeSwitch.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - ListItemButton, - ListItemIcon, - ListItemText, - Tooltip, - useTheme, -} from '@mui/material'; - -import { HalfMoon, SunLight } from 'iconoir-react'; -import { FormattedMessage, useIntl } from 'react-intl'; - -import { useColorMode } from 'src/context/Theme'; - -function ModeSwitch() { - const intl = useIntl(); - const theme = useTheme(); - const colorMode = useColorMode(); - - return ( - - - - {theme.palette.mode === 'dark' ? ( - - ) : ( - - )} - - - - - - - - ); -} - -export default ModeSwitch; diff --git a/src/components/navigation/NavItems.tsx b/src/components/navigation/NavItems.tsx new file mode 100644 index 000000000..c967d423a --- /dev/null +++ b/src/components/navigation/NavItems.tsx @@ -0,0 +1,75 @@ +import type { MouseEvent, ReactElement } from 'react'; + +import { + ListItemIcon, + ListItemText, + ListItemButton as MuiListItemButton, + Tooltip, +} from '@mui/material'; + +import { useIntl } from 'react-intl'; +import { Link, useMatch, useResolvedPath } from 'react-router-dom'; + +interface LinkProps { + icon: ReactElement; + title: string; + link: string; + isOpen?: boolean; +} + +const NavLink = ({ icon, title, link, isOpen }: LinkProps) => { + const resolved = useResolvedPath(link); + const selected = Boolean(useMatch({ path: resolved.pathname, end: false })); + + return ( +
  • + + + {icon} + + + + +
  • + ); +}; + +interface ButtonProps { + icon: ReactElement; + title: string; + onClick: (event: MouseEvent) => void; + isOpen?: boolean; +} + +export const NavButton = ({ + icon, + title, + onClick, + isOpen, +}: ButtonProps) => { + return ( +
  • + + + {icon} + + + + +
  • + ); +}; + +/** @deprecated Prefer the named `NavLink` export with a pre-translated title */ +const NavLinkWrapper = ({ title, ...props }: LinkProps) => { + const intl = useIntl(); + + return ; +}; + +export default NavLinkWrapper; diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index ec5d04802..b48d410e6 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -1,46 +1,43 @@ -//TODO (UI / UX) - These icons are not final -import { - Box, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Stack, - Toolbar, - Tooltip, - useTheme, -} from '@mui/material'; +import React from 'react'; + +import { Box, List, Stack, useTheme } from '@mui/material'; import { CloudDownload, CloudUpload, DatabaseScript, FastArrowLeft, + HelpCircle, HomeSimple, Settings, } from 'iconoir-react'; -import { useIntl } from 'react-intl'; -import { useLocalStorage } from 'react-use'; - import { authenticatedRoutes } from 'src/app/routes'; -import ListItemLink from 'src/components/navigation/ListItemLink'; -import ModeSwitch from 'src/components/navigation/ModeSwitch'; -import { paperBackground } from 'src/context/Theme'; -import { LocalStorageKeys as Keys } from 'src/utils/localStorage-utils'; +import { Pill as AgentSkillsPill } from 'src/components/AgentSkills/Pill'; +import CompanyLogo from 'src/components/graphics/CompanyLogo'; +import CompanyMark from 'src/components/graphics/CompanyMark'; +import { HelpMenu } from 'src/components/menus/HelpMenu'; +import NavLink, { NavButton } from 'src/components/navigation/NavItems'; +import { UserButton, UserMenu } from 'src/components/navigation/User'; +import { useNavigationStore } from 'src/stores/useNavigationStore'; const NavWidths = { - RAIL: 48, + RAIL: 58, FULL: 200, } as const; export const Navigation = () => { - const intl = useIntl(); const theme = useTheme(); - const [nav, setNav] = useLocalStorage<{ open: boolean }>( - Keys.NAVIGATION_SETTINGS + const [menuAnchor, setMenuAnchor] = React.useState( + null + ); + + const [helpAnchor, setHelpAnchor] = React.useState( + null ); - const open = nav?.open ?? true; + + const open = useNavigationStore((state) => state.open); + const toggleOpen = useNavigationStore((state) => state.toggleOpen); return ( { overflowY: 'auto', display: 'flex', flexDirection: 'column', - background: paperBackground[theme.palette.mode], + background: theme.palette.background.default, boxSizing: 'border-box', transition: (boxTheme) => `${boxTheme.transitions.duration.shortest}ms`, }} > - - - - - } - title={authenticatedRoutes.home.title} - link={authenticatedRoutes.home.path} - /> - } - title={authenticatedRoutes.captures.title} - link={authenticatedRoutes.captures.path} - /> - } - title={authenticatedRoutes.collections.title} - link={authenticatedRoutes.collections.path} - /> - } - title={authenticatedRoutes.materializations.title} - link={authenticatedRoutes.materializations.path} - /> - } - title={authenticatedRoutes.admin.title} - link={authenticatedRoutes.admin.path} - /> - + + {open ? : } - - - + + } + title={authenticatedRoutes.home.title} + link={authenticatedRoutes.home.path} + isOpen={open} + /> + } + title={authenticatedRoutes.captures.title} + link={authenticatedRoutes.captures.path} + isOpen={open} + /> + } + title={authenticatedRoutes.collections.title} + link={authenticatedRoutes.collections.path} + isOpen={open} + /> + } + title={authenticatedRoutes.materializations.title} + link={authenticatedRoutes.materializations.path} + isOpen={open} + /> + } + title={authenticatedRoutes.admin.title} + link={authenticatedRoutes.admin.path} + isOpen={open} + /> + + + + + + + + } + title="Help" + onClick={(e) => setHelpAnchor(e.currentTarget)} + isOpen={open} + /> + setHelpAnchor(null)} + /> - - setNav({ open: !open })} - sx={{ - minHeight: 45, - px: 1.5, - whiteSpace: 'nowrap', + - - - + /> + } + title="Collapse" + onClick={toggleOpen} + isOpen={open} + /> - - - - + setMenuAnchor(e.currentTarget)} + isOpen={open} + /> + setMenuAnchor(null)} + /> diff --git a/src/components/navigation/PageTitle.tsx b/src/components/navigation/PageTitle.tsx deleted file mode 100644 index ac16c3db5..000000000 --- a/src/components/navigation/PageTitle.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Stack, Typography } from '@mui/material'; - -import { useShallow } from 'zustand/react/shallow'; - -import { FormattedMessage } from 'react-intl'; - -import ExternalLink from 'src/components/shared/ExternalLink'; -import { useTopBarStore } from 'src/stores/TopBar/Store'; - -function PageTitle() { - const [header, headerLink] = useTopBarStore( - useShallow((state) => [state.header, state.headerLink]) - ); - - // If there isn't a header don't show anything. You cannot display JUST the doc links - if (!header) { - return null; - } - - return ( - - - - - - {headerLink ? ( - - - - ) : null} - - ); -} - -export default PageTitle; diff --git a/src/components/navigation/TopBar.tsx b/src/components/navigation/TopBar.tsx deleted file mode 100644 index 5d35e1a12..000000000 --- a/src/components/navigation/TopBar.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Divider, Stack, Toolbar } from '@mui/material'; -import MuiAppBar from '@mui/material/AppBar'; -import { useTheme } from '@mui/material/styles'; - -import { HeaderPill } from 'src/components/AgentSkills/HeaderPill'; -import CompanyLogo from 'src/components/graphics/CompanyLogo'; -import HelpMenu from 'src/components/menus/HelpMenu'; -import UserMenu from 'src/components/menus/UserMenu'; -import PageTitle from 'src/components/navigation/PageTitle'; -import SidePanelDocsOpenButton from 'src/components/sidePanelDocs/OpenButton'; -import { UpdateAlert } from 'src/components/UpdateAlert'; -import { zIndexIncrement } from 'src/context/Theme'; - -const Topbar = () => { - const theme = useTheme(); - - return ( - - - } - > - - - - - - - - - - - - - - - - - - - ); -}; - -export default Topbar; diff --git a/src/components/navigation/User.tsx b/src/components/navigation/User.tsx new file mode 100644 index 000000000..ae65b0671 --- /dev/null +++ b/src/components/navigation/User.tsx @@ -0,0 +1,142 @@ +import type { MouseEvent } from 'react'; + +import { + Divider, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + ListItemButton as MuiListItemButton, + Stack, + Typography, + useTheme, +} from '@mui/material'; + +import { useShallow } from 'zustand/react/shallow'; + +import { HalfMoon, LogOut, MoreHoriz, SunLight } from 'iconoir-react'; + +import UserAvatar from 'src/components/shared/UserAvatar'; +import { supabaseClient } from 'src/context/GlobalProviders'; +import { useColorMode } from 'src/context/Theme'; +import { useUserStore } from 'src/context/User/useUserContextStore'; + +interface UserButtonProps { + onClick: (event: MouseEvent) => void; + isOpen?: boolean; +} + +export const UserButton = ({ onClick, isOpen }: UserButtonProps) => { + const theme = useTheme(); + const userDetails = useUserStore(useShallow((state) => state.userDetails)); + + if (!userDetails) { + return null; + } + + return ( + + + + {isOpen ? ( + + ) : null} + + ); +}; + +interface UserMenuProps { + anchorEl: HTMLElement | null; + onClose: () => void; +} + +export const UserMenu = ({ anchorEl, onClose }: UserMenuProps) => { + const theme = useTheme(); + const colorMode = useColorMode(); + const userDetails = useUserStore(useShallow((state) => state.userDetails)); + + if (!userDetails) { + return null; + } + + return ( + + + + + {userDetails.userName ?? userDetails.email} + + + {userDetails.email} + + + + + + + colorMode.toggleColorMode()}> + + {theme.palette.mode === 'dark' ? ( + + ) : ( + + )} + + {theme.palette.mode === 'dark' ? `Dark Mode` : `Light Mode`} + + + + { + void supabaseClient.auth.signOut(); + }} + > + + + + Logout + + + ); +}; diff --git a/src/components/shared/PageContainer.tsx b/src/components/shared/PageContainer.tsx index a33157194..9431f7f85 100644 --- a/src/components/shared/PageContainer.tsx +++ b/src/components/shared/PageContainer.tsx @@ -3,14 +3,18 @@ import type { Notification } from 'src/stores/NotificationStore'; import { useEffect, useMemo, useState } from 'react'; -import { Container, Paper, Snackbar, useTheme } from '@mui/material'; +import { Box, Paper, Snackbar, Typography, useTheme } from '@mui/material'; + +import { useIntl } from 'react-intl'; -import Topbar from 'src/components/navigation/TopBar'; import AlertBox from 'src/components/shared/AlertBox'; +import SidePanelDocsOpenButton from 'src/components/sidePanelDocs/OpenButton'; import { paperBackground } from 'src/context/Theme'; +import { useNavigationStore } from 'src/stores/useNavigationStore'; import useNotificationStore, { notificationStoreSelectors, } from 'src/stores/NotificationStore'; +import { useTopBarStore } from 'src/stores/TopBar/Store'; interface Props { children: ReactNode | ReactNode[]; @@ -18,7 +22,10 @@ interface Props { } function PageContainer({ children, hideBackground }: Props) { + const intl = useIntl(); const theme = useTheme(); + const header = useTopBarStore((state) => state.header); + const navigationOpen = useNavigationStore((state) => state.open); const notification = useNotificationStore( notificationStoreSelectors.notification @@ -77,10 +84,12 @@ function PageContainer({ children, hideBackground }: Props) { }, [notification]); return ( - {notification && alertBody ? ( @@ -112,20 +121,54 @@ function PageContainer({ children, hideBackground }: Props) { ) : null} - + {header ? ( + + + {intl.formatMessage({ id: header })} + + + + + + + ) : null} + `padding ${t.transitions.duration.shortest}ms`, + py: 2, + flex: 1, + minHeight: 0, + overflow: 'auto', + overscrollBehavior: 'none', width: '100%', + mb: 1, boxShadow: boxShadowMixin, - borderRadius: 3, + borderRadius: header ? '0 0 16px 16px' : 8, background: backgroundMixin, }} > {children} - + ); } diff --git a/src/components/shared/RelativeAppBar.tsx b/src/components/shared/RelativeAppBar.tsx deleted file mode 100644 index ed85e6e16..000000000 --- a/src/components/shared/RelativeAppBar.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { ReactNode } from 'react'; - -import { AppBar, Toolbar } from '@mui/material'; - -interface Props { - children: ReactNode; -} - -function RelativeAppBar({ children }: Props) { - return ( - - {children} - - ); -} - -export default RelativeAppBar; diff --git a/src/components/sidePanelDocs/SidePanel.tsx b/src/components/sidePanelDocs/SidePanel.tsx index 53b55f024..aab4af305 100644 --- a/src/components/sidePanelDocs/SidePanel.tsx +++ b/src/components/sidePanelDocs/SidePanel.tsx @@ -36,8 +36,6 @@ function DocsSidePanel({ show }: Props) { }} open={show} > - - {hideHeaderAndFooter || (!showToolbar && hideFilter && !ExportComponent) ? null : ( - - - {showToolbar ? ( - - ) : null} - </Stack> - + <Box> <Toolbar disableGutters sx={{ @@ -297,7 +291,7 @@ function EntityTable({ </Box> )} - <Box sx={hideHeaderAndFooter ? {} : { mb: 2, mx: 2 }}> + <Box sx={hideHeaderAndFooter ? {} : { mb: 2 }}> <TableContainer component={Box}> <Table size="small" diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index e374d889f..bbdf092d4 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -337,9 +337,6 @@ const accordionButton = zIndexIncrement * 5; // Need to make the sticky header be on top export const headerLinkIndex = zIndexIncrement * 30; -// Want to make sure it is near the top but right under the screen disable overlay -export const toastIndex = zIndexIncrement * 35; - // Popper component z index must be greater than 100, the z index of the reflex splitter component. export const popperIndex = zIndexIncrement * 500; @@ -1152,6 +1149,41 @@ const ThemeProvider = ({ children }: BaseComponentProps) => { }, }, }, + MuiDrawer: { + // Docs side panel is the only drawer component right now + styleOverrides: { + paper: { + background: palette.background?.default, + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + gap: 8, + whiteSpace: 'nowrap', + padding: '6px 10px', + borderRadius: 8, + }, + }, + }, + MuiListItemIcon: { + styleOverrides: { + root: { + minWidth: 'auto', + color: 'inherit', + }, + }, + }, + MuiListItemText: { + styleOverrides: { + primary: { + fontSize: 13, + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, + }, MuiDialog: { styleOverrides: { paper: { diff --git a/src/lang/en-US/AgentSkills.ts b/src/lang/en-US/AgentSkills.ts deleted file mode 100644 index 3d006e6e3..000000000 --- a/src/lang/en-US/AgentSkills.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const AgentSkills = { - 'agentSkills.pill.label': 'Agent Skills', - 'agentSkills.badge': 'New', - 'agentSkills.eyebrow': 'For Claude, Cursor, Codex and more', - 'agentSkills.title': 'Meet Estuary Agent Skills', - 'agentSkills.description': - 'Give any AI agent the skills to build connectors, debug pipelines, and check stats on Estuary, from wherever you work.', - 'agentSkills.cta': 'Read the docs', - 'agentSkills.dismiss': 'Dismiss', -}; diff --git a/src/lang/en-US/Navigation.ts b/src/lang/en-US/Navigation.ts index 304f759a8..af807ce8b 100644 --- a/src/lang/en-US/Navigation.ts +++ b/src/lang/en-US/Navigation.ts @@ -2,15 +2,6 @@ import { CommonMessages } from 'src/lang/en-US/CommonMessages'; import { CTAs } from 'src/lang/en-US/CTAs'; export const Navigation: Record<string, string> = { - 'navigation.toggle.ariaLabel': `Toggle Navigation`, - 'navigation.expand': `Expand Navigation`, - 'navigation.collapse': `Collapse Navigation`, - - // Header - 'mainMenu.tooltip': `Open Main Menu`, - - 'helpMenu.ariaLabel': `Open Help Menu`, - 'helpMenu.tooltip': `Helpful Links`, 'helpMenu.docs': `Docs`, 'helpMenu.docs.link': `https://docs.estuary.dev/`, 'helpMenu.slack': `Estuary Slack`, @@ -23,12 +14,6 @@ export const Navigation: Record<string, string> = { 'helpMenu.status': `Status`, 'helpMenu.status.link': `https://status.estuary.dev/`, - 'accountMenu.ariaLabel': `Open Account Menu`, - 'accountMenu.tooltip': `My Account`, - 'accountMenu.emailVerified': `verified`, - - 'modeSwitch.label': `Toggle Color Mode`, - 'updateAlert.cta': `Update`, 'updateAlert.title': `Dashboard Updated`, 'updateAlert.message': `An updated version of the UI was released. Reload this page to get the latest changes.`, diff --git a/src/lang/en-US/index.ts b/src/lang/en-US/index.ts index 64e6959ec..64bc75021 100644 --- a/src/lang/en-US/index.ts +++ b/src/lang/en-US/index.ts @@ -1,6 +1,5 @@ import { AccessGrants } from 'src/lang/en-US/AccessGrants'; import { AdminPage } from 'src/lang/en-US/AdminPage'; -import { AgentSkills } from 'src/lang/en-US/AgentSkills'; import { Alerts } from 'src/lang/en-US/Alerts'; import { Authentication } from 'src/lang/en-US/Authentication'; import { Captures } from 'src/lang/en-US/Captures'; @@ -32,7 +31,6 @@ import { Workflows } from 'src/lang/en-US/Workflows'; // Since we only set messages as strings we can just use this and be safe const enUSMessages: Record<string, string> = { ...AccessGrants, - ...AgentSkills, ...Alerts, ...AdminPage, ...Captures, diff --git a/src/stores/useNavigationStore.ts b/src/stores/useNavigationStore.ts new file mode 100644 index 000000000..8861b8d90 --- /dev/null +++ b/src/stores/useNavigationStore.ts @@ -0,0 +1,35 @@ +import { create } from 'zustand'; +import { devtools, persist } from 'zustand/middleware'; + +import { LocalStorageKeys } from 'src/utils/localStorage-utils'; +import { devtoolsOptions } from 'src/utils/store-utils'; + +interface NavigationState { + // Whether the side navigation is expanded (true) or collapsed to the rail. + open: boolean; + setOpen: (open: boolean) => void; + toggleOpen: () => void; +} + +export const useNavigationStore = create<NavigationState>()( + persist( + devtools( + (set) => ({ + open: true, + setOpen: (open) => set({ open }, false, 'setOpen'), + toggleOpen: () => + set( + (state) => ({ open: !state.open }), + false, + 'toggleOpen' + ), + }), + devtoolsOptions(LocalStorageKeys.NAVIGATION_SETTINGS) + ), + { + name: LocalStorageKeys.NAVIGATION_SETTINGS, + version: 0, + partialize: (state) => ({ open: state.open }), + } + ) +); From f281daa2a82978705b9d6bd9859da5a42572d98f Mon Sep 17 00:00:00 2001 From: Greg Shear <greg@estuary.dev> Date: Thu, 2 Jul 2026 16:07:54 -0400 Subject: [PATCH 5/5] move update button to sidebar, too --- src/components/UpdateAlert/Actions.tsx | 14 +++--- src/components/UpdateAlert/index.tsx | 55 ++++++++++++++---------- src/components/navigation/Navigation.tsx | 4 ++ src/lang/en-US/Navigation.ts | 5 --- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/components/UpdateAlert/Actions.tsx b/src/components/UpdateAlert/Actions.tsx index 9da64649f..5051626c3 100644 --- a/src/components/UpdateAlert/Actions.tsx +++ b/src/components/UpdateAlert/Actions.tsx @@ -1,14 +1,11 @@ import { Button, Divider, Stack, Typography } from '@mui/material'; import { ReloadWindow } from 'iconoir-react'; -import { useIntl } from 'react-intl'; import { logRocketEvent } from 'src/services/shared'; import { CustomEvents } from 'src/services/types'; function Actions() { - const intl = useIntl(); - const reloadPage = () => { logRocketEvent(CustomEvents.UPDATE_AVAILABLE, { reloadClicked: true, @@ -17,15 +14,16 @@ function Actions() { }; return ( - <Stack spacing={2} sx={{ maxWidth: 275 }}> + <Stack spacing={2} sx={{ maxWidth: 275, p: 2 }}> <Typography variant="h6" component="span"> - {intl.formatMessage({ id: 'updateAlert.title' })} + Dashboard Updated </Typography> <Typography variant="body1"> - {intl.formatMessage({ id: 'updateAlert.message' })} + An updated version of the UI was released. Reload this page to + get the latest changes. </Typography> <Typography variant="body2"> - {intl.formatMessage({ id: 'updateAlert.warning' })} + Unsaved changes could be lost. </Typography> <Divider /> <Button @@ -36,7 +34,7 @@ function Actions() { maxWidth: 'fit-content', }} > - {intl.formatMessage({ id: 'cta.reload' })} + Reload </Button> </Stack> ); diff --git a/src/components/UpdateAlert/index.tsx b/src/components/UpdateAlert/index.tsx index 4e8e2b542..1b1b7003c 100644 --- a/src/components/UpdateAlert/index.tsx +++ b/src/components/UpdateAlert/index.tsx @@ -1,19 +1,25 @@ import { useEffect, useState } from 'react'; -import { Box, Divider } from '@mui/material'; +import { Popover } from '@mui/material'; -import { NavArrowDown } from 'iconoir-react'; -import { useIntl } from 'react-intl'; +import { ReloadWindow } from 'iconoir-react'; import useSWR from 'swr'; import { getLatestVersionDetails } from 'src/api/meta'; -import ButtonWithPopper from 'src/components/shared/buttons/ButtonWithPopper'; import Actions from 'src/components/UpdateAlert/Actions'; +import { NavButton } from 'src/components/navigation/NavItems'; import { logRocketEvent } from 'src/services/shared'; import { CustomEvents } from 'src/services/types'; -export function UpdateAlert() { - const intl = useIntl(); +interface UpdateAlertProps { + isOpen?: boolean; +} + +// Sidebar item that appears when the served meta.json commit no longer +// matches the commit baked into this bundle, meaning a newer version of +// the dashboard has been deployed since this tab loaded. +export function UpdateAlert({ isOpen }: UpdateAlertProps) { + const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); const [hasLatest, setHasLatest] = useState<boolean>(true); @@ -44,25 +50,28 @@ export function UpdateAlert() { } return ( - <Box sx={{ display: 'flex' }}> - <ButtonWithPopper - popper={<Actions />} - buttonProps={{ - endIcon: <NavArrowDown style={{ fontSize: 13 }} />, - size: 'small', - sx: { - alignItems: 'center', - p: 1, - }, - variant: 'text', + <> + <NavButton + icon={<ReloadWindow />} + title="Update" + onClick={(e) => setAnchorEl(e.currentTarget)} + isOpen={isOpen} + /> + <Popover + anchorEl={anchorEl} + open={Boolean(anchorEl)} + onClose={() => setAnchorEl(null)} + anchorOrigin={{ + horizontal: 'left', + vertical: 'top', }} - popperProps={{ - placement: 'bottom-end', + transformOrigin={{ + horizontal: 'left', + vertical: 'bottom', }} > - {intl.formatMessage({ id: 'updateAlert.cta' })} - </ButtonWithPopper> - <Divider orientation="vertical" flexItem sx={{ ml: 1, mr: 2 }} /> - </Box> + <Actions /> + </Popover> + </> ); } diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index b48d410e6..80eb6a2b6 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -11,6 +11,7 @@ import { HomeSimple, Settings, } from 'iconoir-react'; + import { authenticatedRoutes } from 'src/app/routes'; import { Pill as AgentSkillsPill } from 'src/components/AgentSkills/Pill'; import CompanyLogo from 'src/components/graphics/CompanyLogo'; @@ -18,6 +19,7 @@ import CompanyMark from 'src/components/graphics/CompanyMark'; import { HelpMenu } from 'src/components/menus/HelpMenu'; import NavLink, { NavButton } from 'src/components/navigation/NavItems'; import { UserButton, UserMenu } from 'src/components/navigation/User'; +import { UpdateAlert } from 'src/components/UpdateAlert'; import { useNavigationStore } from 'src/stores/useNavigationStore'; const NavWidths = { @@ -110,6 +112,8 @@ export const Navigation = () => { </List> <Box sx={{ mt: 'auto', pb: 1 }}> + <UpdateAlert isOpen={open} /> + <Box sx={{ mx: 1, my: 0.25 }}> <AgentSkillsPill /> </Box> diff --git a/src/lang/en-US/Navigation.ts b/src/lang/en-US/Navigation.ts index af807ce8b..cc6675502 100644 --- a/src/lang/en-US/Navigation.ts +++ b/src/lang/en-US/Navigation.ts @@ -13,9 +13,4 @@ export const Navigation: Record<string, string> = { 'helpMenu.about': `About ${CommonMessages.productName}`, 'helpMenu.status': `Status`, 'helpMenu.status.link': `https://status.estuary.dev/`, - - 'updateAlert.cta': `Update`, - 'updateAlert.title': `Dashboard Updated`, - 'updateAlert.message': `An updated version of the UI was released. Reload this page to get the latest changes.`, - 'updateAlert.warning': `Unsaved changes could be lost.`, };