diff --git a/.github/workflows/companion-build.yml b/.github/workflows/companion-build.yml index be48f2d6..e731c6cc 100644 --- a/.github/workflows/companion-build.yml +++ b/.github/workflows/companion-build.yml @@ -22,7 +22,28 @@ jobs: target: aarch64-apple-darwin runs-on: ${{ matrix.platform }} steps: + # pnpm-workspace.yaml globs '../packages/@nself/*', i.e. the + # nself-org/packages repo as a SIBLING directory. desktop/package.json + # depends on five of them as workspace:*, and tauri.conf.json's + # beforeBuildCommand runs `pnpm build` before cargo builds the shell. + # nclaw goes in a subdir so the sibling lands beside it, matching the + # layout the workspace file expects (same pattern as desktop-e2e.yml). - uses: actions/checkout@v7 + with: + path: nclaw + - uses: actions/checkout@v7 + with: + repository: nself-org/packages + path: packages + + - uses: actions/setup-node@v7 + with: { node-version: '20' } + - uses: pnpm/action-setup@v6 + with: { version: '9' } + + - name: Install frontend dependencies + working-directory: nclaw + run: pnpm install --frozen-lockfile - uses: dtolnay/rust-toolchain@stable @@ -40,7 +61,7 @@ jobs: patchelf - name: Build Tauri app - working-directory: desktop/src-tauri + working-directory: nclaw/desktop/src-tauri run: cargo tauri build - name: Upload Linux artifacts @@ -49,8 +70,8 @@ jobs: with: name: companion-${{ matrix.target }} path: | - desktop/src-tauri/target/release/bundle/appimage/*.AppImage - desktop/src-tauri/target/release/bundle/deb/*.deb + nclaw/desktop/src-tauri/target/release/bundle/appimage/*.AppImage + nclaw/desktop/src-tauri/target/release/bundle/deb/*.deb - name: Upload Windows artifacts if: matrix.platform == 'windows-latest' @@ -58,8 +79,8 @@ jobs: with: name: companion-${{ matrix.target }} path: | - desktop/src-tauri/target/release/bundle/msi/*.msi - desktop/src-tauri/target/release/bundle/nsis/*.exe + nclaw/desktop/src-tauri/target/release/bundle/msi/*.msi + nclaw/desktop/src-tauri/target/release/bundle/nsis/*.exe - name: Upload macOS artifacts if: matrix.platform == 'macos-latest' @@ -67,4 +88,4 @@ jobs: with: name: companion-${{ matrix.target }} path: | - desktop/src-tauri/target/release/bundle/dmg/*.dmg + nclaw/desktop/src-tauri/target/release/bundle/dmg/*.dmg diff --git a/desktop/package.json b/desktop/package.json index 7bcced7f..190dabea 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -19,6 +19,7 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.0.0", "@ai-sdk/react": "^3.0.0", + "@fontsource/inter": "^5.0.0", "@formatjs/intl-localematcher": "^0.5.0", "@sentry/react": "^8.0.0", "@nself/auth-core": "workspace:*", @@ -49,6 +50,7 @@ "devDependencies": { "@formatjs/cli": "^6.0.0", "@playwright/test": "^1.48", + "@tailwindcss/postcss": "^4.0.0", "@tauri-apps/cli": "^2.3.0", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", @@ -57,8 +59,11 @@ "@types/react-syntax-highlighter": "^15.0.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^3.2.6", + "autoprefixer": "^10.4.0", "jsdom": "^24.0.0", "playwright": "^1.48", + "postcss": "^8.4.0", + "tailwindcss": "^4.0.0", "typescript": "^5.4.5", "vite": "^6.4.2", "vitest": "^3.2.6" diff --git a/desktop/postcss.config.js b/desktop/postcss.config.js index 2b75bd8a..d9234117 100644 --- a/desktop/postcss.config.js +++ b/desktop/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { - tailwindcss: {}, + '@tailwindcss/postcss': {}, autoprefixer: {} } } diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index 1809482f..37ce3d26 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -2,7 +2,8 @@ * Purpose: ɳClaw desktop root component. Wraps the app with shared providers and * renders the main shell. Auth state drives the top-level view. * Inputs: Auth context from NselfAuthProvider (via useAuth hook). - * Outputs: Renders i18n-wrapped shell. Unauthenticated state shows a sign-in prompt. + * Outputs: Renders i18n-wrapped shell. Authenticated state renders the chat UI; + * unauthenticated state shows a sign-in prompt. * Constraints: * - NselfI18nProvider wraps all children so useNselfTranslation() works everywhere. * - Do NOT lift graphql queries here — let page components own their queries. @@ -15,6 +16,7 @@ import * as Sentry from '@sentry/react'; import { NselfI18nProvider, isRTL, useNselfTranslation, useTranslation } from '@nself/i18n'; import { useAuth } from '@nself/auth-core'; import { initObservability } from '@nself/observability'; +import { ChatContainer } from './components/chat/ChatContainer'; // Initialize Sentry error reporting (runs at module load, before first render) // Vite exposes build-time env via import.meta.env, not the Node-only `process` @@ -59,6 +61,28 @@ function Shell(): React.ReactElement { const { t } = useNselfTranslation(); useDocumentDir(); + // Authenticated is the only state with content below the title bar — render + // it full-height so ChatContainer's own flex layout can fill the window. + if (status === 'authenticated') { + return ( +
+
+ {t('desktop.nclaw.title')} +
+
+ +
+
+ ); + } + return (
{ - const body = await req.json() as { messages: ChatMessage[] }; +export async function chatTransport( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + const bodyText = input instanceof Request ? await input.text() : ((init?.body as string) ?? '{}'); + const body = JSON.parse(bodyText) as { messages: ChatMessage[] }; const reply = await invoke('stream_chat', { messages: body.messages }); const encoder = new TextEncoder(); + const messageId = crypto.randomUUID(); + const chunk = (part: Record) => + encoder.encode(`data: ${JSON.stringify(part)}\n\n`); + const stream = new ReadableStream({ start(controller) { - controller.enqueue( - encoder.encode(`data: ${JSON.stringify({ content: reply })}\n\n`) - ); + controller.enqueue(chunk({ type: 'text-start', id: messageId })); + controller.enqueue(chunk({ type: 'text-delta', id: messageId, delta: reply })); + controller.enqueue(chunk({ type: 'text-end', id: messageId })); controller.enqueue(encoder.encode('data: [DONE]\n\n')); controller.close(); }, }); return new Response(stream, { - headers: { 'Content-Type': 'text/event-stream' }, + headers: { + 'Content-Type': 'text/event-stream', + 'x-vercel-ai-ui-message-stream': 'v1', + }, }); } diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx index 0ae67e8e..483db16a 100644 --- a/desktop/src/main.tsx +++ b/desktop/src/main.tsx @@ -4,6 +4,12 @@ * Inputs: VITE_NSELF_SENTRY_DSN, VITE_APP_ENV, VITE_APP_VERSION from build env. * Outputs: React app mounted on #root, Sentry + OTel registered, i18next initialised. * Constraints: + * - styles/tokens.css and styles/globals.css (which pulls in Tailwind via + * @import 'tailwindcss') MUST be imported here — Vite only emits CSS that's reachable + * from the module graph. Without this import every Tailwind utility class + * in the tree is inert (elements keep className but get no rules), which + * silently collapses flex layouts (e.g. ChatList's scroll container) to + * zero height. * - initObservability() MUST be called before ReactDOM.createRoot() so Sentry catches * any errors thrown during React hydration. * - initializeI18next() runs synchronously at module level — must complete before render. @@ -24,6 +30,8 @@ import { Provider as UrqlProvider } from 'urql'; import App from './App'; import { authStrategy } from './lib/auth'; import { graphqlClient } from './lib/graphql'; +import './styles/tokens.css'; +import './styles/globals.css'; // ─── i18n init (module level — before first render) ────────────────────────── initializeI18next(); diff --git a/desktop/src/styles/globals.css b/desktop/src/styles/globals.css index 080f01d2..6e5e66fd 100644 --- a/desktop/src/styles/globals.css +++ b/desktop/src/styles/globals.css @@ -1,8 +1,10 @@ @import '@fontsource/inter'; +@import 'tailwindcss'; -@tailwind base; -@tailwind components; -@tailwind utilities; +/* Tailwind v4 is CSS-first, but tailwind.config.js still carries this app's + custom colors (bg-surface, accent) and darkMode setting — @config keeps + that file authoritative instead of duplicating theme values here. */ +@config '../../tailwind.config.js'; body { @apply bg-surface text-slate-100 font-sans antialiased; diff --git a/desktop/tests/e2e/chat.spec.ts b/desktop/tests/e2e/chat.spec.ts index 50b94b21..a5499b3f 100644 --- a/desktop/tests/e2e/chat.spec.ts +++ b/desktop/tests/e2e/chat.spec.ts @@ -1,15 +1,46 @@ import { test, expect } from '@playwright/test'; +import { mockAuthenticatedSession } from './fixtures/auth'; + +// Both tests require an authenticated session: App.tsx only mounts +// ChatContainer (the composer + message list) once useAuth() resolves to +// 'authenticated' (see desktop/src/App.tsx Shell()). A fresh, unauthenticated +// page has no chat UI at all, so every test here seeds a session first. test('user can send a message and see a stub reply', async ({ page }) => { + await mockAuthenticatedSession(page); await page.goto('/'); - const input = page.getByPlaceholder(/Type a message/i); + + // Real placeholder string is @nself/i18n's desktop.nclaw.messagePlaceholder + // ("Message ɳClaw…"), wired in InputArea.tsx via useNselfTranslation(). + const input = page.getByPlaceholder(/Message ɳClaw/i); await input.waitFor({ timeout: 5000 }); await input.fill('Hello'); await page.keyboard.press('Enter'); + + // stream_chat's real Rust command still returns NotImplemented pending + // S15.T17 (desktop/src-tauri/src/commands/chat.rs); the mocked Tauri + // bridge in fixtures/auth.ts stands in for it with the same placeholder + // text so this test exercises the real send -> render pipeline (useChat, + // ChatContainer, ChatList, MessageBubble) without depending on a native + // Tauri host, which Playwright cannot drive. await expect(page.locator('text=(stub response)')).toBeVisible({ timeout: 10000 }); }); test('markdown renders in messages', async ({ page }) => { + await mockAuthenticatedSession(page); await page.goto('/'); - await expect(page.locator('article, [role="article"], .markdown, .prose').first()).toBeAttached({ timeout: 10000 }); + + const input = page.getByPlaceholder(/Message ɳClaw/i); + await input.waitFor({ timeout: 5000 }); + await input.fill('**bold text**'); + await page.keyboard.press('Enter'); + + // MessageBubble renders message content through react-markdown + // (remark-gfm), which turns **bold text** into a real element. + // This fails again if MessageBubble stops rendering markdown (e.g. reverts + // to a plain text node), unlike a generic container-class selector that + // could match unrelated markup. + await expect(page.locator('strong', { hasText: 'bold text' })).toBeAttached({ + timeout: 10000, + }); }); diff --git a/desktop/tests/e2e/fixtures/auth.ts b/desktop/tests/e2e/fixtures/auth.ts new file mode 100644 index 00000000..e252b6ce --- /dev/null +++ b/desktop/tests/e2e/fixtures/auth.ts @@ -0,0 +1,97 @@ +/** + * Purpose: Seed an authenticated session for nclaw desktop E2E tests. + * Inputs: Playwright Page. + * Outputs: Injects a TokenPair into localStorage (the same keys + * NativeAuthStrategy/SecureStoreInterface read from — see + * desktop/src/lib/auth.ts and @nself/auth-core/src/native.helpers.ts + * SECURE_STORE_KEYS) before the page's own scripts run, and stubs + * window.__TAURI_INTERNALS__ so invoke() resolves without a real + * Tauri binary (Playwright drives a plain Chromium window, which + * has no Tauri IPC bridge at all). + * Constraints: + * - Must run via page.addInitScript, before React mounts and before + * NativeAuthStrategy.init() reads SecureStore, or the app renders + * 'loading' -> 'unauthenticated' and the chat UI never appears. + * - expiresAt is set far in the future so the proactive refresh loop + * (DEFAULT_REFRESH_BUFFER_MS before expiry) never fires mid-test and + * tries to hit a real auth server that doesn't exist in CI. + * - The JWT signature is not verified client-side (decodeUserFromJwt only + * reads the payload), so any syntactically valid three-part token works. + */ + +import type { Page } from '@playwright/test'; + +// Mirrors @nself/auth-core SECURE_STORE_KEYS (native.helpers.ts) — desktop's +// localSecureStore (desktop/src/lib/auth.ts) is a thin localStorage wrapper +// around the same keys. +const SECURE_STORE_KEYS = { + ACCESS_TOKEN: '@nself/auth-core/accessToken', + REFRESH_TOKEN: '@nself/auth-core/refreshToken', + EXPIRES_AT: '@nself/auth-core/expiresAt', +} as const; + +/** + * Seed a valid, far-from-expiry TokenPair and stub the Tauri IPC bridge so + * the app boots into the 'authenticated' state and can call stream_chat + * without a native Tauri host. Call before page.goto('/'). + */ +export async function mockAuthenticatedSession(page: Page): Promise { + await page.addInitScript( + ({ accessKey, refreshKey, expiresKey }) => { + function base64url(obj: Record): string { + return btoa(JSON.stringify(obj)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + } + + const header = base64url({ alg: 'HS256', typ: 'JWT' }); + const payload = base64url({ + sub: 'e2e-test-user-id', + email: 'e2e@example.test', + display_name: 'E2E Test User', + 'https://hasura.io/jwt/claims': { + 'x-hasura-allowed-roles': ['user'], + 'x-hasura-default-role': 'user', + }, + }); + const fakeJwt = `${header}.${payload}.e2e-test-signature`; + + window.localStorage.setItem(accessKey, fakeJwt); + window.localStorage.setItem(refreshKey, 'e2e-test-refresh-token'); + // 24h out — well past DEFAULT_REFRESH_BUFFER_MS (60s), so the refresh + // loop schedules for tomorrow and never fires during the test run. + window.localStorage.setItem(expiresKey, String(Date.now() + 24 * 60 * 60 * 1000)); + + // Stub the Tauri IPC bridge. Real command implementations are Rust + // (desktop/src-tauri/src/commands/*.rs); Playwright drives a plain + // browser window with no native host, so invoke() would otherwise + // throw "window.__TAURI_INTERNALS__ is undefined" before React can + // even render the composer. + const w = window as unknown as Record; + w.__TAURI_INTERNALS__ = { + transformCallback: (callback: (value: unknown) => void): number => { + const id = Math.floor(Math.random() * 1e9); + (w as Record)[`_tauriCb_${id}`] = callback; + return id; + }, + invoke: async (cmd: string): Promise => { + if (cmd === 'stream_chat') { + // stream_chat's real Rust implementation is a stub returning + // NotImplemented (see desktop/src-tauri/src/commands/chat.rs, + // awaiting S15.T17). Mirror that shape here rather than a + // hand-picked success value, so this fixture does not claim + // more of the backend works than actually does. + return '(stub response)'; + } + return null; + }, + }; + }, + { + accessKey: SECURE_STORE_KEYS.ACCESS_TOKEN, + refreshKey: SECURE_STORE_KEYS.REFRESH_TOKEN, + expiresKey: SECURE_STORE_KEYS.EXPIRES_AT, + }, + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e8561e9..cd42604c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -499,6 +499,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.0.0 version: 3.2.2(react@19.2.7) + '@fontsource/inter': + specifier: ^5.0.0 + version: 5.3.0 '@formatjs/intl-localematcher': specifier: ^0.5.0 version: 0.5.10 @@ -584,6 +587,9 @@ importers: '@playwright/test': specifier: ^1.48 version: 1.61.0 + '@tailwindcss/postcss': + specifier: ^4.0.0 + version: 4.3.1 '@tauri-apps/cli': specifier: ^2.3.0 version: 2.11.2 @@ -608,12 +614,21 @@ importers: '@vitest/coverage-v8': specifier: ^3.2.6 version: 3.2.6(vitest@3.2.6) + autoprefixer: + specifier: ^10.4.0 + version: 10.5.4(postcss@8.5.26) jsdom: specifier: ^24.0.0 version: 24.1.3 playwright: specifier: ^1.48 version: 1.61.0 + postcss: + specifier: '>=8.5.18' + version: 8.5.26 + tailwindcss: + specifier: ^4.0.0 + version: 4.3.1 typescript: specifier: ^5.4.5 version: 5.8.3 @@ -1825,6 +1840,9 @@ packages: '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fontsource/inter@5.3.0': + resolution: {integrity: sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==} + '@formatjs/cli-native-darwin-arm64@1.1.5': resolution: {integrity: sha512-zGt7OenQakyh8a+eAgCx/zUInbQXYT0UNzeOaYlnaKmU9yr78wsVAGGhPc85gchnQ7gIXSWOKhwIzvfSMFGU7w==} cpu: [arm64] @@ -4579,6 +4597,13 @@ packages: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + autoprefixer@10.5.4: + resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: '>=8.5.18' + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -4668,6 +4693,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + baseline-browser-mapping@2.11.19: + resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==} + engines: {node: '>=6.0.0'} + hasBin: true + better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -4716,6 +4746,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -4782,6 +4817,9 @@ packages: caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5313,6 +5351,9 @@ packages: electron-to-chromium@1.5.374: resolution: {integrity: sha512-HCF5i7izveksHSGqa7mhDh6tr3Uz9Dar2RAjwuh69bw3QGPVObjQIgLwQWeO/Rxp9/r0KdboKy9RbpQDl97fjg==} + electron-to-chromium@1.5.415: + resolution: {integrity: sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==} + emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -5837,6 +5878,9 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + framer-motion@11.18.2: resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} peerDependencies: @@ -7522,6 +7566,10 @@ packages: resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} engines: {node: '>=18'} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + normalize-path@2.1.1: resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} engines: {node: '>=0.10.0'} @@ -9093,6 +9141,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -10862,6 +10916,8 @@ snapshots: '@fastify/busboy@3.2.0': {} + '@fontsource/inter@5.3.0': {} + '@formatjs/cli-native-darwin-arm64@1.1.5': optional: true @@ -13967,6 +14023,15 @@ snapshots: auto-bind@5.0.1: {} + autoprefixer@10.5.4(postcss@8.5.26): + dependencies: + browserslist: 4.28.8 + caniuse-lite: 1.0.30001810 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -14107,6 +14172,8 @@ snapshots: baseline-browser-mapping@2.10.37: {} + baseline-browser-mapping@2.11.19: {} + better-opn@3.0.2: dependencies: open: 8.4.2 @@ -14158,6 +14225,14 @@ snapshots: node-releases: 2.0.47 update-browserslist-db: 1.2.3(browserslist@4.28.2) + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.19 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.415 + node-releases: 2.0.54 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -14215,6 +14290,8 @@ snapshots: caniuse-lite@1.0.30001799: {} + caniuse-lite@1.0.30001810: {} + ccount@2.0.1: {} chai@5.3.3: @@ -14723,6 +14800,8 @@ snapshots: electron-to-chromium@1.5.374: {} + electron-to-chromium@1.5.415: {} + emittery@0.13.1: {} emoji-regex@10.6.0: {} @@ -15434,6 +15513,8 @@ snapshots: dependencies: fetch-blob: 3.2.0 + fraction.js@5.3.4: {} + framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: motion-dom: 11.18.1 @@ -17937,6 +18018,8 @@ snapshots: node-releases@2.0.47: {} + node-releases@2.0.54: {} + normalize-path@2.1.1: dependencies: remove-trailing-separator: 1.1.0 @@ -19705,6 +19788,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1