diff --git a/.storybook/preview.ts b/.storybook/preview.ts index ce70a1b..93156b1 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -5,6 +5,11 @@ import "./preview.css"; const preview: Preview = { parameters: { + options: { + storySort: { + order: ["Overview", ["Showcase", "Showcase (EN)"], "Components"], + }, + }, controls: { matchers: { color: /(background|color)$/i, diff --git a/README.md b/README.md index 97ef565..513803c 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,20 @@ self-host Archivo (weights 400/600/700). Buttons & actions: `Button`, `ButtonLink`, `BigIconButton`, `ToolButton` · Inputs: `TextField`, `Search`, `Select`, `Checkbox`, `Radio`, `Switch`, `Suggestion` · Feedback: `Callout`, `Toast`, `Tooltip`, `Spinner`, -`CheckCircle`, `Stepper` · Surfaces: `Card`, `Dialog`, `Popover` · Chrome: -`Toolbar`, `StatusBar`, `Tag`, `Logo` · Archives: `ArchiveProgress`, -`ArchiveTabs`, `ArchiveView`. +`CheckCircle`, `Stepper` · Surfaces: `Card`, `CardTool`, `Dialog`, `Popover` · Chrome: +`AppHeader`, `Toolbar`, `StatusBar`, `Tag`, `Logo` · Archives: `ArchiveProgress`, +`ArchiveTabs`, `ArchiveView` · Workflow: `WorkflowStepLayout`, `PageTitle`, +`AppFooter`. + +`CardTool` owns the shared feature-card visuals, including its disabled state. +Consumers keep navigation in their router: wrap enabled cards in a link and +render disabled placeholders directly, so unavailable tools never become +interactive elements. + +`WorkflowStepLayout` owns the common viewport, content width and single-scroll +page structure. Consumers provide `AppHeader`, `AppFooter`, back controls and +page actions through its slots; routing, step state and domain logic remain in +the application. Browse every component and variant in Storybook (`pnpm storybook`). diff --git a/package.json b/package.json index ee3b7f4..45245d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aymurai/ui", - "version": "0.4.3", + "version": "0.5.0", "description": "AymurAI shared React component library, extracted from the Figma UI Library and authored with Panda CSS.", "license": "MIT", "type": "module", @@ -38,6 +38,7 @@ "build-storybook": "storybook build", "lint": "biome check", "lint:fix": "biome check --write", + "test": "vitest run", "typecheck": "tsc -p tsconfig.build.json --noEmit", "validate": "pnpm lint && pnpm typecheck", "release": "pnpm build && pnpm pack --pack-destination ." @@ -47,12 +48,20 @@ "react-dom": "^18 || ^19" }, "dependencies": { + "@phosphor-icons/react": "^2.1.10", + "@radix-ui/react-context-menu": "^2.3.7", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", - "phosphor-react": "^1.4.1", + "@tiptap/core": "^3.28.0", + "@tiptap/extension-highlight": "^3.28.0", + "@tiptap/extension-table": "^3.28.0", + "@tiptap/markdown": "^3.28.0", + "@tiptap/pm": "^3.28.0", + "@tiptap/react": "^3.28.0", + "@tiptap/starter-kit": "^3.28.0", "react-hot-toast": "^2.6.0" }, "devDependencies": { @@ -60,17 +69,22 @@ "@pandacss/dev": "^1.8.1", "@storybook/react": "^8.6.18", "@storybook/react-vite": "^8.4.0", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.5.0", "husky": "^9.1.7", + "jsdom": "^29.1.1", "lint-staged": "^17.0.8", "react": "^19", "react-dom": "^19", "storybook": "^8.4.0", "typescript": "^5.9.2", "vite": "^6.3.5", - "vite-plugin-dts": "^4.3.0" + "vite-plugin-dts": "^4.3.0", + "vitest": "^4.1.10" }, "repository": { "type": "git", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de32498..ac9eb95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-context-menu': + specifier: ^2.3.7 + version: 2.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -23,9 +29,27 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - phosphor-react: - specifier: ^1.4.1 - version: 1.4.1(react@19.2.7) + '@tiptap/core': + specifier: ^3.28.0 + version: 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/extension-highlight': + specifier: ^3.28.0 + version: 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-table': + specifier: ^3.28.0 + version: 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/markdown': + specifier: ^3.28.0 + version: 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/pm': + specifier: ^3.28.0 + version: 3.28.0 + '@tiptap/react': + specifier: ^3.28.0 + version: 3.28.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tiptap/starter-kit': + specifier: ^3.28.0 + version: 3.28.0 react-hot-toast: specifier: ^2.6.0 version: 2.6.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -35,13 +59,22 @@ importers: version: 2.5.3 '@pandacss/dev': specifier: ^1.8.1 - version: 1.11.3(typescript@5.9.3) + version: 1.11.3(jsdom@29.1.1)(typescript@5.9.3) '@storybook/react': specifier: ^8.6.18 version: 8.6.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@8.6.18(prettier@3.2.5))(typescript@5.9.3) '@storybook/react-vite': specifier: ^8.4.0 version: 8.6.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@8.6.18(prettier@3.2.5))(typescript@5.9.3)(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0)) + '@testing-library/jest-dom': + specifier: ^7.0.0 + version: 7.0.0(@testing-library/dom@10.4.1) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/react': specifier: ^19 version: 19.2.17 @@ -54,6 +87,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + jsdom: + specifier: ^29.1.1 + version: 29.1.1 lint-staged: specifier: ^17.0.8 version: 17.0.8 @@ -75,9 +111,30 @@ importers: vite-plugin-dts: specifier: ^4.3.0 version: 4.5.4(rollup@4.61.1)(typescript@5.9.3)(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0)) + vitest: + specifier: ^4.1.10 + version: 4.1.10(jsdom@29.1.1)(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0)) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -149,6 +206,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -218,12 +279,52 @@ packages: cpu: [x64] os: [win32] + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@csstools/postcss-cascade-layers@5.0.2': resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==} engines: {node: '>=18'} @@ -392,6 +493,15 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -558,6 +668,13 @@ packages: resolution: {integrity: sha512-tUbxa3WzoCuulP6kcMn7f0uNgLBvu81JAg4sfoUpz6PZE36VHA3k9n6ROEwziTbr7vqgeCj5DYQCUldrVNbEGQ==} engines: {node: '>=20'} + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -568,6 +685,22 @@ packages: '@radix-ui/primitive@1.1.4': resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} + + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.9': resolution: {integrity: sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig==} peerDependencies: @@ -581,6 +714,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.15': + resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.9': resolution: {integrity: sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ==} peerDependencies: @@ -603,6 +749,28 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.3.7': + resolution: {integrity: sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-context@1.1.4': resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} peerDependencies: @@ -612,6 +780,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.16': resolution: {integrity: sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw==} peerDependencies: @@ -634,6 +811,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-direction@1.1.4': + resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.12': resolution: {integrity: sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg==} peerDependencies: @@ -647,6 +833,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.19': + resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.4': resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} peerDependencies: @@ -656,6 +855,28 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-guards@1.1.6': + resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.16': + resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-scope@1.1.9': resolution: {integrity: sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ==} peerDependencies: @@ -678,6 +899,28 @@ packages: '@types/react': optional: true + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-menu@2.1.24': + resolution: {integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popover@1.1.16': resolution: {integrity: sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw==} peerDependencies: @@ -704,6 +947,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.11': resolution: {integrity: sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw==} peerDependencies: @@ -717,6 +973,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.10': + resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.6': resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} peerDependencies: @@ -730,6 +1012,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.5': resolution: {integrity: sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==} peerDependencies: @@ -743,6 +1038,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.19': + resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-select@2.3.0': resolution: {integrity: sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g==} peerDependencies: @@ -765,6 +1073,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.3.3': + resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.3.0': resolution: {integrity: sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA==} peerDependencies: @@ -800,6 +1117,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.4': + resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.2.3': resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} peerDependencies: @@ -809,6 +1135,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-effect-event@0.0.3': resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} peerDependencies: @@ -818,6 +1153,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.2': resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} peerDependencies: @@ -827,6 +1171,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-is-hydrated@0.1.3': + resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.2': resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} peerDependencies: @@ -836,6 +1189,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-previous@1.1.2': resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} peerDependencies: @@ -854,6 +1216,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-size@1.1.2': resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} peerDependencies: @@ -863,6 +1234,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-visually-hidden@1.2.5': resolution: {integrity: sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg==} peerDependencies: @@ -879,6 +1259,9 @@ packages: '@radix-ui/rect@1.1.2': resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1059,6 +1442,9 @@ packages: '@rushstack/ts-command-line@5.3.9': resolution: {integrity: sha512-GIHqU+sRGQ3LGWAZu1O+9Yh++qwtyNIIGuNbcWHJjBTm2qRez0cwINUHZ+pQLR8UuzZDcMajrDaNbUYoaL/XtQ==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@storybook/builder-vite@8.6.18': resolution: {integrity: sha512-XLqnOv4C36jlTd4uC8xpWBxv+7GV4/05zWJ0wAcU4qflorropUTirt4UQPGkwIzi+BVAhs9pJj+m4k0IWJtpHg==} peerDependencies: @@ -1136,12 +1522,213 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@7.0.0': + resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tiptap/core@3.28.0': + resolution: {integrity: sha512-gUuD5WAYfbDxNSSJya/emh2KSzXZXLUYKW4fEnc1AQ5FE2twzh4LJ9UlKFIawigrUCAksWI5Fy1hRbv+5m4ZdQ==} + peerDependencies: + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-blockquote@3.28.0': + resolution: {integrity: sha512-y8Xi6Z3AQLXedz0J8Z3kDsu7SKBmL50gKVXx3RK1Oo1cuCGhNChm3syChkAz3PLAbrPUM5rvJDSZdF2jUrDnng==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-bold@3.28.0': + resolution: {integrity: sha512-JhZQmr0AU741bOwjVMfuwJdK4g0TQwwPbeca9aqKHv5zvZw4i4G9G6fESVyMFc3Yag1ffpnq5EtNldHKTnMhlw==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-bubble-menu@3.28.0': + resolution: {integrity: sha512-7AUNoHj2K4XLKCgW4uspeD/ENejPu2BeHvLTsiOoIO+XXDNSi2j0bbaIS8f2s/qnFirscwp/sbznp1qph/76qg==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-bullet-list@3.28.0': + resolution: {integrity: sha512-dSVuNiH7PFMmWGkNER9vfUb5bXUHDARvHbJ3l+APEb+ZiusVN1KFdM3nd/RsW/Rt5Gd3XwlIEU/c2Uf7cxYDcw==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-code-block@3.28.0': + resolution: {integrity: sha512-bHITDzT0umTLh+SiEVQzIXkCMt/TzbPM1+HQy9ZxgQDHAJj/vdmfNAnP3RCOrz4lETXyhNQ2b6kxeu3lWckxyA==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-code@3.28.0': + resolution: {integrity: sha512-AUw2Acof3CQE6Q6Y22saK4lyg0nkeJ4XXniU65TdAi/9TE66TGiO4NQJJNNPgu8+VMEwl5j8VsIFK8sfTcNYtg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-document@3.28.0': + resolution: {integrity: sha512-5SAKtlB1Tr/eZFhISL86HqmUup6rItvPtuPyRjmZo/VgbMwXEwiyAh1sMgFp5VNaeSMM14vJSyQpjhSaOmaBqg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-dropcursor@3.28.0': + resolution: {integrity: sha512-JA+dXQjjfJBpD0nVsZeddGVXRsmanESM9+8CtM35hI9wJnYMXay/B+5GUhswO20zhT3zhB2ZOTGe9knu6A8nIQ==} + peerDependencies: + '@tiptap/extensions': 3.28.0 + + '@tiptap/extension-floating-menu@3.28.0': + resolution: {integrity: sha512-59SECvJq3pQfeJBuydEdhQqrpl0oDlk8N1Ovs1Si3/fJa6rEQAJB2zIvcpBHky9Z+5JodI2eueDnv8eimiyGbg==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-gapcursor@3.28.0': + resolution: {integrity: sha512-Wo73kM4q8z4Va9i4+0n1cO0UEMVUVBHPqM6cAqEYXjB4T48LQkKjRsiQydCezm185rQAsmm1dyi72gtvVQfdMg==} + peerDependencies: + '@tiptap/extensions': 3.28.0 + + '@tiptap/extension-hard-break@3.28.0': + resolution: {integrity: sha512-JHIFjX1luJgQ4VFEkIeXZpbc54Qiw+69P8FmlazYTh7AIJ6iLCpMFgQFELnivEAZ+/vS0lMPQubg0rCeM3UrHw==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-heading@3.28.0': + resolution: {integrity: sha512-rKjGG/Ik2lNaXBNVmONEDm5DBfGDLM1NWgSf5RRfBISrH8Lm1xqFruOB9tIaB0YUoSV3SBOiwTF9svmzvKSoRA==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-highlight@3.28.0': + resolution: {integrity: sha512-T8uvQ1aZs+u496XxycMk5zAIwA0DxN+SCdSAFWCU7bqCAen3ulzOsk6RXJ+Jx40GqTV/ShPyIcEuaJVNurXxbg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-horizontal-rule@3.28.0': + resolution: {integrity: sha512-neBCMWprkppQUoLWyeJZDHqBw+xKX2oNhLD9MbFlhKIr092zgfP4mpCvQnSkn1OHl156KzZMp070+NyLBjgQ7A==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-italic@3.28.0': + resolution: {integrity: sha512-Yur/ELz6dNVKQC7m8wGjtoLFxamGjJmA0rUEEOacTH6J39aFmcSxYkEGNDkYHbZFyHFYqbej6eI3VE0h08O0mg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-link@3.28.0': + resolution: {integrity: sha512-hy/PqSeTyl317yseFcHGE+3XVfQKQYaL4uZuj27xlrcO7MZ15drt1h9sUZy1FTBF3mr8676pQu7aoEm/Ww4ZRA==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-list-item@3.28.0': + resolution: {integrity: sha512-GbXrnMac6knSetZY33NHwOrgXFPqH28uCklQqmOZQlsrdGqezDKk31qoEMr4GsKW9n/lViAeuc07oIzwLwCMng==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-list-keymap@3.28.0': + resolution: {integrity: sha512-u9Wks8c/eQAv0RkULNggOyTKDD69WVbcV9H5cbasPDUbq5XbEFVa9ajxhEGfMmQ5et3EX0QL8qBi50K0GqbIjA==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-list@3.28.0': + resolution: {integrity: sha512-zQ66i5DuhVOndmZ0d7H475Y5Yb+BMx+zfCjFkCzEc6WVef5MumtxBVTkGLQ0ibxUaD71s4QQZbjHWagpaTg0eQ==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-ordered-list@3.28.0': + resolution: {integrity: sha512-OZNYrKKL9D01ySOujlNbKlLS9/3wGgKNYeoHZUg0e+Ta8WPVvL3W1zH6TgiU5sF2ZSGUBoq3w7mdaO/iROlUkw==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-paragraph@3.28.0': + resolution: {integrity: sha512-8UMlQIjzqf6r2hSJ/VeJ00RqaOrOB1J5rTk02Vcw2pYeHkOqp+B0ff0HFu4abT9x1q4oXrBAgMsxRtgh/kR14Q==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-strike@3.28.0': + resolution: {integrity: sha512-VVj2ZZU9QYqiHLcjqMqRYvuHSsokj/AgUl+6TzLrKjlWwyZ18D4H2vkyI0g70iVTKnUpZ3sEy8WA/rqjgBjqBg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-table@3.28.0': + resolution: {integrity: sha512-SX2Uwn/bFyrqjbBo2Hg9wfLT/ofByHm4f80BuG0h5/m89Ve78CgtaK2lGz9jHZRM7rKTG6eqel+PUZQUwcSzOg==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-text@3.28.0': + resolution: {integrity: sha512-Jqp1LgfY1mnp4TQHoy+vnHyfn6qnnAM6nHgGwbY5zA8b/Xf1Etll6pziTx9p1J1qcfAgSrnjOyAmA++H7VQqww==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-underline@3.28.0': + resolution: {integrity: sha512-rwxCS6vTh2DJkNIYQX7JSrrRSmm76e2y48aZeuZO5ShkvLWMuJ3/zqDHRxAQVDiJhuE2Cp8NrTmPYlUc9YrT0A==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extensions@3.28.0': + resolution: {integrity: sha512-DJT1khCK+O/pT1gQlAnoKAx6zwDkgv7GtnhSfkqm1/4KHC3x+SSJnBIgBl9oGHymA+DxWbW1EULU4V3d/kT2uQ==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/markdown@3.28.0': + resolution: {integrity: sha512-AefLVBgpjmBDbGDx8i0UDr4KyPxfsDnpIGYROTDSyGJswovGZDzCm3YjMGm8roxP31Opw4UCtts4WkFzWPI0og==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/pm@3.28.0': + resolution: {integrity: sha512-ALcpwZMUdat9gjJKlpscpoqXStoLhU246LPEVBDvJdIsoUKvUu3MrzfXik2Y8mtSGfhjtm9O2TRkWxQiFVMwsQ==} + + '@tiptap/react@3.28.0': + resolution: {integrity: sha512-BxzSAqaDEldQ97K/v6+GizU1/Dxx9VWkRh7PLQql6bXlAMiz6T1G3dGt25vvv1hQ1FoMt5ExWY5NkrPmR8G8ew==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.28.0': + resolution: {integrity: sha512-pqvFpValV+ONtlu3l4s73ROm/tEjoGMmt6uHmSJaLbqTcMHkdWuR3flJ/5brr4IDHe1foxmNicMbRxlje7yBYw==} + '@ts-morph/common@0.29.0': resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1154,6 +1741,12 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -1174,12 +1767,44 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1278,6 +1903,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1289,6 +1918,17 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -1317,6 +1957,9 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -1368,6 +2011,10 @@ packages: caniuse-lite@1.0.30001797: resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1434,6 +2081,13 @@ packages: resolution: {integrity: sha512-ju88BYCQ2uvjO2bR+SsgLSTwTSctU+6Vp2ePbKPgSCZyy4MWZxYsT738DlKVRE5utUjobjPRm1MkTYKJxCmpTA==} engines: {node: '>=14.9.0'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1442,6 +2096,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -1454,6 +2112,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1466,6 +2127,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1481,6 +2146,12 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1515,6 +2186,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -1527,6 +2202,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -1556,6 +2234,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1575,6 +2256,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} @@ -1591,6 +2276,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.1: + resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1733,6 +2422,10 @@ packages: resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} engines: {node: '>=16.9.0'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1750,6 +2443,10 @@ packages: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1802,6 +2499,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -1839,6 +2539,15 @@ packages: resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} engines: {node: '>=12.0.0'} + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1939,6 +2648,9 @@ packages: resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + lint-staged@17.0.8: resolution: {integrity: sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==} engines: {node: '>=22.22.1'} @@ -1972,9 +2684,17 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} @@ -1982,10 +2702,18 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@17.0.6: + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -2017,6 +2745,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.3: resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} engines: {node: 18 || 20 || >=22} @@ -2070,6 +2802,10 @@ packages: resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} engines: {node: '>= 10.12.0'} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2085,6 +2821,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} @@ -2106,6 +2845,9 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2137,12 +2879,6 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - phosphor-react@1.4.1: - resolution: {integrity: sha512-gO5j7U0xZrdglTAYDYPACU4xDOFBTJmptrrB/GeR+tHhCZF3nUMyGmV/0hnloKjuTrOmpSFlbfOY78H39rgjUQ==} - engines: {node: '>=10'} - peerDependencies: - react: '>=16' - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2225,14 +2961,61 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.3: + resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.42.1: + resolution: {integrity: sha512-rRqzZnRgkyh69XoOMrfFJHwauHscLBmHbq772kwbic1ymQAM8gXjzEbJse5j1ep2UO2HRIAQL0bY3kZ/RoqjVw==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -2272,6 +3055,9 @@ packages: react: '>=16' react-dom: '>=16' + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -2318,6 +3104,10 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2346,6 +3136,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -2360,6 +3153,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2416,6 +3213,9 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2446,10 +3246,16 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + storybook@8.6.18: resolution: {integrity: sha512-p8seiSI6FiVY6P3V0pG+5v7c8pDMehMAFRWEhG5XqIBSQszzOjDnW2rNvm3odoLKfo3V3P6Cs6Hv9ILzymULyQ==} hasBin: true @@ -2491,6 +3297,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -2503,6 +3313,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -2510,6 +3323,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} @@ -2518,6 +3334,17 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.9: + resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} + + tldts@7.4.9: + resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2526,6 +3353,14 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -2570,6 +3405,10 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2608,6 +3447,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2667,12 +3511,72 @@ packages: yaml: optional: true + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which-typed-array@1.1.22: resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} @@ -2682,6 +3586,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wordwrapjs@5.1.1: resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} engines: {node: '>=12.17'} @@ -2717,6 +3626,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2743,6 +3659,28 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2832,6 +3770,8 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 + '@babel/runtime@7.29.7': {} + '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -2890,6 +3830,10 @@ snapshots: '@biomejs/cli-win32-x64@2.5.3': optional: true + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -2901,6 +3845,30 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.14)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) @@ -2989,6 +3957,8 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@exodus/bytes@1.15.1': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -3148,14 +4118,14 @@ snapshots: postcss-selector-parser: 7.1.1 ts-pattern: 5.9.0 - '@pandacss/dev@1.11.3(typescript@5.9.3)': + '@pandacss/dev@1.11.3(jsdom@29.1.1)(typescript@5.9.3)': dependencies: '@clack/prompts': 0.11.0 '@pandacss/config': 1.11.3 '@pandacss/logger': 1.11.3 - '@pandacss/mcp': 1.11.3(typescript@5.9.3) - '@pandacss/node': 1.11.3(typescript@5.9.3) - '@pandacss/postcss': 1.11.3(typescript@5.9.3) + '@pandacss/mcp': 1.11.3(jsdom@29.1.1)(typescript@5.9.3) + '@pandacss/node': 1.11.3(jsdom@29.1.1)(typescript@5.9.3) + '@pandacss/postcss': 1.11.3(jsdom@29.1.1)(typescript@5.9.3) '@pandacss/preset-base': 1.11.3 '@pandacss/preset-panda': 1.11.3 '@pandacss/shared': 1.11.3 @@ -3168,10 +4138,10 @@ snapshots: - supports-color - typescript - '@pandacss/extractor@1.11.3(typescript@5.9.3)': + '@pandacss/extractor@1.11.3(jsdom@29.1.1)(typescript@5.9.3)': dependencies: '@pandacss/shared': 1.11.3 - ts-evaluator: 1.2.0(typescript@5.9.3) + ts-evaluator: 1.2.0(jsdom@29.1.1)(typescript@5.9.3) ts-morph: 28.0.0 transitivePeerDependencies: - jsdom @@ -3198,12 +4168,12 @@ snapshots: '@pandacss/types': 1.11.3 kleur: 4.1.5 - '@pandacss/mcp@1.11.3(typescript@5.9.3)': + '@pandacss/mcp@1.11.3(jsdom@29.1.1)(typescript@5.9.3)': dependencies: '@clack/prompts': 0.11.0 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) '@pandacss/logger': 1.11.3 - '@pandacss/node': 1.11.3(typescript@5.9.3) + '@pandacss/node': 1.11.3(jsdom@29.1.1)(typescript@5.9.3) '@pandacss/token-dictionary': 1.11.3 '@pandacss/types': 1.11.3 zod: 4.4.3 @@ -3213,13 +4183,13 @@ snapshots: - supports-color - typescript - '@pandacss/node@1.11.3(typescript@5.9.3)': + '@pandacss/node@1.11.3(jsdom@29.1.1)(typescript@5.9.3)': dependencies: '@pandacss/config': 1.11.3 '@pandacss/core': 1.11.3 '@pandacss/generator': 1.11.3 '@pandacss/logger': 1.11.3 - '@pandacss/parser': 1.11.3(typescript@5.9.3) + '@pandacss/parser': 1.11.3(jsdom@29.1.1)(typescript@5.9.3) '@pandacss/plugin-lightningcss': 1.11.3 '@pandacss/plugin-svelte': 1.11.3 '@pandacss/plugin-vue': 1.11.3 @@ -3251,11 +4221,11 @@ snapshots: - jsdom - typescript - '@pandacss/parser@1.11.3(typescript@5.9.3)': + '@pandacss/parser@1.11.3(jsdom@29.1.1)(typescript@5.9.3)': dependencies: '@pandacss/config': 1.11.3 '@pandacss/core': 1.11.3 - '@pandacss/extractor': 1.11.3(typescript@5.9.3) + '@pandacss/extractor': 1.11.3(jsdom@29.1.1)(typescript@5.9.3) '@pandacss/logger': 1.11.3 '@pandacss/shared': 1.11.3 '@pandacss/types': 1.11.3 @@ -3283,9 +4253,9 @@ snapshots: '@vue/compiler-sfc': 3.5.25 magic-string: 0.30.21 - '@pandacss/postcss@1.11.3(typescript@5.9.3)': + '@pandacss/postcss@1.11.3(jsdom@29.1.1)(typescript@5.9.3)': dependencies: - '@pandacss/node': 1.11.3(typescript@5.9.3) + '@pandacss/node': 1.11.3(jsdom@29.1.1)(typescript@5.9.3) postcss: 8.5.14 transitivePeerDependencies: - jsdom @@ -3321,6 +4291,11 @@ snapshots: '@pandacss/types@1.11.3': {} + '@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + '@pkgjs/parseargs@0.11.0': optional: true @@ -3328,6 +4303,17 @@ snapshots: '@radix-ui/primitive@1.1.4': {} + '@radix-ui/primitive@1.1.7': {} + + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-arrow@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -3337,6 +4323,18 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-collection@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) @@ -3355,12 +4353,37 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-context-menu@2.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-context@1.2.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -3389,6 +4412,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-direction@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-dismissable-layer@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -3402,12 +4431,42 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-focus-scope@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) @@ -3426,6 +4485,39 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-id@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-popover@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -3467,6 +4559,24 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-portal@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -3477,6 +4587,25 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) @@ -3486,6 +4615,15 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-primitive@2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) @@ -3495,6 +4633,25 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-select@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/number': 1.1.2 @@ -3532,6 +4689,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-slot@1.3.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-switch@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -3573,29 +4737,63 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: @@ -3614,6 +4812,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/rect': 1.1.3 + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) @@ -3621,6 +4826,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-visually-hidden@1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -3632,6 +4844,8 @@ snapshots: '@radix-ui/rect@1.1.2': {} + '@radix-ui/rect@1.1.3': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.4.0(rollup@4.61.1)': @@ -3750,6 +4964,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@standard-schema/spec@1.1.0': {} + '@storybook/builder-vite@8.6.18(storybook@8.6.18(prettier@3.2.5))(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0))': dependencies: '@storybook/csf-plugin': 8.6.18(storybook@8.6.18(prettier@3.2.5)) @@ -3842,6 +5058,229 @@ snapshots: dependencies: storybook: 8.6.18(prettier@3.2.5) + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tiptap/core@3.28.0(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-blockquote@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-bold@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-bubble-menu@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + optional: true + + '@tiptap/extension-bullet-list@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-code-block@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-code@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-document@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-dropcursor@3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extensions': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-floating-menu@3.28.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + optional: true + + '@tiptap/extension-gapcursor@3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extensions': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-hard-break@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-heading@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-highlight@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-horizontal-rule@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-italic@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-link@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-list-keymap@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-ordered-list@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-paragraph@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-strike@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-table@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-text@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-underline@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/markdown@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + marked: 17.0.6 + + '@tiptap/pm@3.28.0': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.3 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + + '@tiptap/react@3.28.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-floating-menu': 3.28.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.28.0': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/extension-blockquote': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-bold': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-bullet-list': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-code': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-code-block': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-document': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-dropcursor': 3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-gapcursor': 3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-hard-break': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-heading': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-horizontal-rule': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-italic': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-link': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-list-item': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-list-keymap': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-ordered-list': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-paragraph': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-strike': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-text': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-underline': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extensions': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + '@ts-morph/common@0.29.0': dependencies: minimatch: 10.2.5 @@ -3850,6 +5289,8 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -3871,6 +5312,13 @@ snapshots: dependencies: '@babel/types': 7.29.7 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} '@types/estree@1.0.9': {} @@ -3887,6 +5335,8 @@ snapshots: '@types/resolve@1.20.6': {} + '@types/use-sync-external-store@0.0.6': {} + '@vitejs/plugin-react@4.7.0(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 @@ -3899,6 +5349,47 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.3(lightningcss@1.31.1)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/language-core@2.4.28': dependencies: '@volar/source-map': 2.4.28 @@ -4025,6 +5516,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} argparse@1.0.10: @@ -4035,6 +5528,14 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -4055,6 +5556,10 @@ snapshots: dependencies: open: 8.4.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -4122,6 +5627,8 @@ snapshots: caniuse-lite@1.0.30001797: {} + chai@6.2.2: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -4176,16 +5683,32 @@ snapshots: dependencies: '@types/node': 17.0.45 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + cssesc@3.0.0: {} csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + de-indent@1.0.2: {} debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -4196,6 +5719,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -4206,6 +5731,10 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4230,12 +5759,16 @@ snapshots: entities@7.0.1: {} + entities@8.0.0: {} + environment@1.1.0: {} es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.3.1: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -4284,6 +5817,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} etag@1.8.1: {} @@ -4296,6 +5833,8 @@ snapshots: dependencies: eventsource-parser: 3.1.0 + expect-type@1.4.0: {} + express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 @@ -4338,6 +5877,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.1: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4481,6 +6022,12 @@ snapshots: hono@4.12.25: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -4497,6 +6044,8 @@ snapshots: import-lazy@4.0.0: {} + indent-string@4.0.0: {} + inherits@2.0.4: {} ip-address@10.2.0: {} @@ -4538,6 +6087,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -4573,6 +6124,32 @@ snapshots: jsdoc-type-pratt-parser@4.8.0: {} + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 7.28.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-schema-traverse@1.0.0: {} @@ -4640,6 +6217,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + linkifyjs@4.3.3: {} + lint-staged@17.0.8: dependencies: listr2: 10.2.2 @@ -4683,10 +6262,14 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lz-string@1.5.0: {} + magic-string@0.27.0: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4695,8 +6278,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@17.0.6: {} + math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -4718,6 +6305,8 @@ snapshots: mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@10.2.3: dependencies: brace-expansion: 5.0.6 @@ -4757,6 +6346,8 @@ snapshots: object-path@0.11.8: {} + obug@2.1.4: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -4775,6 +6366,8 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + orderedmap@2.1.1: {} + outdent@0.8.0: {} p-limit@3.1.0: @@ -4793,6 +6386,10 @@ snapshots: package-manager-detector@1.6.0: {} + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -4814,10 +6411,6 @@ snapshots: perfect-debounce@1.0.0: {} - phosphor-react@1.4.1(react@19.2.7): - dependencies: - react: 19.2.7 - picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -4893,13 +6486,95 @@ snapshots: prettier@3.2.5: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + process@0.11.10: {} + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.3: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.1 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.11: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.11 + + prosemirror-view@1.42.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.2: dependencies: side-channel: 1.1.1 @@ -4948,6 +6623,8 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + react-is@17.0.2: {} + react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): @@ -4989,6 +6666,11 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + require-from-string@2.0.2: {} resolve-pkg-maps@1.0.0: {} @@ -5040,6 +6722,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.1 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + router@2.2.0: dependencies: debug: 4.4.3 @@ -5062,6 +6746,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} semver@6.3.1: {} @@ -5140,6 +6828,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} sisteransi@1.0.5: {} @@ -5166,8 +6856,12 @@ snapshots: sprintf-js@1.0.3: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.2.0: {} + storybook@8.6.18(prettier@3.2.5): dependencies: '@storybook/core': 8.6.18(prettier@3.2.5)(storybook@8.6.18(prettier@3.2.5)) @@ -5213,6 +6907,10 @@ snapshots: strip-bom@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-indent@4.1.1: {} supports-color@8.1.1: @@ -5221,6 +6919,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} + table@6.9.0: dependencies: ajv: 8.20.0 @@ -5231,6 +6931,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@1.2.4: {} tinyglobby@0.2.17: @@ -5238,20 +6940,38 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@3.1.0: {} + + tldts-core@7.4.9: {} + + tldts@7.4.9: + dependencies: + tldts-core: 7.4.9 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toidentifier@1.0.1: {} + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.9 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-dedent@2.2.0: {} - ts-evaluator@1.2.0(typescript@5.9.3): + ts-evaluator@1.2.0(jsdom@29.1.1)(typescript@5.9.3): dependencies: ansi-colors: 4.1.3 crosspath: 2.0.0 object-path: 0.11.8 typescript: 5.9.3 + optionalDependencies: + jsdom: 29.1.1 ts-morph@28.0.0: dependencies: @@ -5280,6 +7000,8 @@ snapshots: ufo@1.6.4: {} + undici@7.28.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -5316,6 +7038,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + util-deprecate@1.0.2: {} util@0.12.5: @@ -5360,10 +7086,55 @@ snapshots: lightningcss: 1.31.1 yaml: 2.9.0 + vitest@4.1.10(jsdom@29.1.1)(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@6.4.3(lightningcss@1.31.1)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 6.4.3(lightningcss@1.31.1)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + vscode-uri@3.1.0: {} + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which-typed-array@1.1.22: dependencies: available-typed-arrays: 1.0.7 @@ -5378,6 +7149,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wordwrapjs@5.1.1: {} wrap-ansi@10.0.0: @@ -5408,6 +7184,10 @@ snapshots: ws@8.21.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} yaml@2.9.0: diff --git a/src/components/app-footer/AppFooter.stories.tsx b/src/components/app-footer/AppFooter.stories.tsx new file mode 100644 index 0000000..f833abf --- /dev/null +++ b/src/components/app-footer/AppFooter.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { css } from "@/styled/css"; +import { Button } from "../button"; +import { AppFooter } from "./AppFooter"; + +const meta = { + title: "Components/AppFooter", + component: AppFooter, + parameters: { layout: "fullscreen" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** Stand-in for desktop-app's own `BuiltBy` — DataGénero branding stays there. */ +function BuiltByPlaceholder() { + return ( +
+ + Plataforma hecha por + + + datagénero + +
+ ); +} + +export const Default: Story = { + args: { + leading: , + actions: , + }, +}; + +export const TwoActions: Story = { + args: { + leading: , + actions: ( + <> + + + + ), + }, +}; + +export const NoLeading: Story = { + args: { actions: }, +}; + +/** Narrow viewport — actions wrap onto their own line, still right-aligned. */ +export const NarrowViewport: Story = { + args: { + leading: , + actions: ( + <> + + + + ), + }, + parameters: { viewport: { defaultViewport: "mobile1" } }, +}; + +/** Sticky-footer pattern: last child of a flex column page wrapper. */ +export const StickyToBottom: Story = { + render: (args) => ( +
+
Contenido de la página (corto).
+ +
+ ), + args: { + leading: , + actions: , + }, +}; diff --git a/src/components/app-footer/AppFooter.tsx b/src/components/app-footer/AppFooter.tsx new file mode 100644 index 0000000..5d4e140 --- /dev/null +++ b/src/components/app-footer/AppFooter.tsx @@ -0,0 +1,74 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { css, cx } from "@/styled/css"; + +/** + * AppFooter — bottom bar shared across every pipeline (Dataset, Anonimizador, + * Voz a Texto). AymurAI UI Library "bar" node 40002579:88245 (bottom bar of + * the "Anonimizador - Preview" screen). + * + * `leading` and `actions` are pre-built content — the library only owns the + * shell (height, border, background, padding, slot placement, wrapping). + * `leading` is desktop-app's `BuiltBy` (DataGénero branding belongs to the + * product, not the library); `actions` is usually one or two `Button`s. + * + * `mt: "auto"` pushes the footer to the bottom when it's the last child of + * a `display:flex; flexDirection:column; minH:100vh` page wrapper (the + * standard sticky-footer pattern) — the library can't force this on its + * own without controlling the whole page. + */ + +const root = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + flexWrap: "wrap", + gap: "4", // 16px — matches the Figma gap between leading/actions when they fit on one line + minH: "[99px]", + px: "12", // 48px + py: "6", // 24px + bg: "bg.secondary", + borderTopWidth: "[1px]", + borderTopStyle: "solid", + borderTopColor: "[#BCBAB8]", // border.primary colour, no bare token + w: "full", + flexShrink: "0", + mt: "auto", +}); + +const leadingWrap = css({ + display: "flex", + alignItems: "center", + minW: "[0px]", +}); + +const actionsWrap = css({ + display: "flex", + alignItems: "center", + gap: "4", // 16px + flexWrap: "wrap", + justifyContent: "flex-end", + // Keeps actions right-aligned even when they wrap onto their own line; + // a no-op when `root` still has room to lay both slots out side by side. + ml: "auto", +}); + +export interface AppFooterProps extends HTMLAttributes { + leading?: ReactNode; + actions?: ReactNode; +} + +export function AppFooter({ + leading, + actions, + className, + ...props +}: AppFooterProps) { + return ( +
+ {leading &&
{leading}
} + {actions &&
{actions}
} +
+ ); +} + +export default AppFooter; diff --git a/src/components/app-footer/index.ts b/src/components/app-footer/index.ts new file mode 100644 index 0000000..fee4808 --- /dev/null +++ b/src/components/app-footer/index.ts @@ -0,0 +1 @@ +export { AppFooter, type AppFooterProps, default } from "./AppFooter"; diff --git a/src/components/app-header/AppHeader.stories.tsx b/src/components/app-header/AppHeader.stories.tsx index 02253a6..268bc51 100644 --- a/src/components/app-header/AppHeader.stories.tsx +++ b/src/components/app-header/AppHeader.stories.tsx @@ -45,6 +45,11 @@ export const WithoutProgress: Story = { args: { featureName: "Anonimizador" }, }; +/** Home screens can show the full AymurAI wordmark without a feature name. */ +export const HomeWordmark: Story = { + args: { logoVariant: "logo" }, +}; + /** * Slots wrap the defaults without rebuilding their styles: an anchor for the * logo, a Radix PopoverTrigger for help, and a DialogTrigger for apps. diff --git a/src/components/app-header/AppHeader.tsx b/src/components/app-header/AppHeader.tsx index 504c413..f6983f1 100644 --- a/src/components/app-header/AppHeader.tsx +++ b/src/components/app-header/AppHeader.tsx @@ -1,4 +1,4 @@ -import { DotsNine, Question } from "phosphor-react"; +import { DotsNineIcon, QuestionIcon } from "@phosphor-icons/react"; import type { ReactElement, ReactNode } from "react"; import { css, cx } from "@/styled/css"; import { BigIconButton } from "../big-icon-button"; @@ -23,8 +23,10 @@ import { Stepper } from "../stepper"; * default logo/buttons without recreating their styles or focus behaviour. * * The top-bar variant is 1440×96px with 48px horizontal padding. Its three - * layout regions are 203px (logo), 332px (stepper), and 203px (actions), so - * the stepper remains centered even when the logo content changes width. + * layout regions are min 203px (logo — grows for a long `featureName`, + * e.g. "Resumen de Documento"), 332px (stepper), and 203px (actions), so the + * stepper stays centered for the Figma-sized case and only shifts if a + * longer feature name pushes past 203px. */ const root = css({ display: "flex", @@ -37,18 +39,31 @@ const root = css({ borderBottomWidth: "[1px]", borderBottomStyle: "solid", borderBottomColor: "[#BCBAB8]", // border.primary colour, no bare token + flexShrink: "0", + w: "full", + // Anchor for stepperWrap, which centers on the header itself rather than + // on the (variable-width) space between logo and actions. + position: "relative", }); const logoWrap = css({ display: "flex", alignItems: "center", - w: "[203px]", + minW: "[203px]", flexShrink: "0", }); const stepperWrap = css({ + // Absolutely centered on `root` so a long featureName growing logoWrap + // past 203px can't push the stepper off-center — space-between would + // otherwise shift it toward whichever side has more room. + position: "absolute", + left: "[50%]", + top: "[50%]", + transform: "[translate(-50%, -50%)]", display: "flex", alignItems: "center", + justifyContent: "center", w: "[332px]", h: "[52px]", flexShrink: "0", @@ -66,7 +81,7 @@ const actionsWrap = css({ const helpIcon = css({ color: "text.lighter" }); export interface AppHeaderSlots { - /** Wrap or replace the default Logo while preserving the header layout. */ + /** Wrap only the iso mark; wordmark/divider/feature name remain non-interactive. */ logo?: (defaultElement: ReactElement) => ReactNode; /** Wrap or replace the default help Button (for example with PopoverTrigger). */ help?: (defaultElement: ReactElement) => ReactNode; @@ -77,6 +92,8 @@ export interface AppHeaderSlots { interface AppHeaderBaseProps { /** Feature name shown next to the logo (e.g. "Voz a Texto"). Omit for the bare iso mark. */ featureName?: string; + /** Brand shown when featureName is omitted. Defaults to the bare iso mark. */ + logoVariant?: "logo" | "iso"; onHelp?: () => void; onOpenApps?: () => void; helpLabel?: string; @@ -103,6 +120,7 @@ export type AppHeaderProps = AppHeaderBaseProps & AppHeaderProgressProps; export function AppHeader({ featureName, + logoVariant = "iso", steps, current, onHelp, @@ -117,10 +135,12 @@ export function AppHeader({ ? steps.map((label, i) => ({ label: i === current ? label : "" })) : []; - const defaultLogo = featureName ? ( - - ) : ( - + const defaultLogo = ( + ); const defaultHelp = ( ); const defaultApps = ( @@ -141,15 +161,13 @@ export function AppHeader({ onClick={onOpenApps} style={{ padding: 2, borderRadius: 4 }} > - + ); return (
-
- {slots?.logo ? slots.logo(defaultLogo) : defaultLogo} -
+
{defaultLogo}
{hasProgress && (
diff --git a/src/components/archives/ArchiveProgress.stories.tsx b/src/components/archives/ArchiveProgress.stories.tsx index 8b8424f..5f7fb88 100644 --- a/src/components/archives/ArchiveProgress.stories.tsx +++ b/src/components/archives/ArchiveProgress.stories.tsx @@ -20,9 +20,6 @@ export const Default50: Story = { args: { status: "default", progress: 50 } }; /** Figma: Status=Stoped */ export const Stopped: Story = { args: { status: "stopped", progress: 20 } }; -/** Figma: Status=Replace archive */ -export const Replace: Story = { args: { status: "replace", progress: 0 } }; - /** Figma: Status=Error */ export const ErrorState: Story = { args: { status: "error", progress: 0 } }; @@ -52,12 +49,11 @@ export const Matrix: Story = { progress={20} fileName="Archivo 3.doc" /> - - +
), diff --git a/src/components/archives/ArchiveProgress.tsx b/src/components/archives/ArchiveProgress.tsx index 955b381..f1ed8e3 100644 --- a/src/components/archives/ArchiveProgress.tsx +++ b/src/components/archives/ArchiveProgress.tsx @@ -1,4 +1,8 @@ -import { ArrowsClockwise, CheckCircle, Stop, X } from "phosphor-react"; +import { + ArrowsClockwiseIcon, + CheckCircleIcon, + StopIcon, +} from "@phosphor-icons/react"; import { Button } from "@/components/button/Button"; import { css, cx } from "@/styled/css"; @@ -9,17 +13,21 @@ import { css, cx } from "@/styled/css"; * Default 20% — active progress bar at 20%, "Detener" (stop) button * Default 50% — active progress bar at 50% * Stopped — disabled bar (grey), disabled "Detener" button - * Replace archive — error state, "Reemplazar" button * Error — error text + error bar (system.error-secondary bg), "Reemplazar" button * Completed — full bar (action.alt-default), CheckCircle + "Carga finalizada 100%" * + * Figma also defines a "Replace archive" state (identical to Error minus the + * error styling) and a dismiss (X) action on every state; product decided + * against exposing either — "Reemplazar" only shows for the real failure + * case (Error), and dismiss isn't exposed at all. Deliberately not + * implemented here. + * * Props: * fileName — file name string * progress — 0–100 number - * status — one of the 6 states + * status — one of the 4 states * onStop — called when "Detener" clicked - * onReplace — called when "Reemplazar" clicked - * onDismiss — called when X (dismiss) clicked + * onReplace — called when "Reemplazar" clicked (Error state only) * * Tokens: * action.progress = #CED1F4 (active progress fill, Figma-exact) @@ -38,7 +46,6 @@ import { css, cx } from "@/styled/css"; export type ArchiveProgressStatus = | "default" | "stopped" - | "replace" | "error" | "completed"; @@ -49,7 +56,6 @@ export interface ArchiveProgressProps { status?: ArchiveProgressStatus; onStop?: () => void; onReplace?: () => void; - onDismiss?: () => void; className?: string; } @@ -166,7 +172,7 @@ function ProgressLabel({ color: "brand.primary", })} > - + - Detuviste el procesamiento de este archivo - - ); - } - return ( - + Detener )} @@ -341,34 +327,12 @@ export function ArchiveProgress({ className={css({ px: "4", py: "2", gap: "1" })} style={{ height: 41 }} > - + Reemplazar )}
)} - - {/* Dismiss X */} - {showDismiss && ( - - )} ); } diff --git a/src/components/archives/ArchiveRow.stories.tsx b/src/components/archives/ArchiveRow.stories.tsx new file mode 100644 index 0000000..25700dd --- /dev/null +++ b/src/components/archives/ArchiveRow.stories.tsx @@ -0,0 +1,110 @@ +import { FileIcon, PlayIcon, TrashIcon } from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { css } from "@/styled/css"; +import { Button } from "../button"; +import { ArchiveRow } from "./ArchiveRow"; +import { ArchiveView } from "./ArchiveView"; + +const meta = { + title: "Components/Archives/ArchiveRow", + component: ArchiveRow, + tags: ["autodocs"], + parameters: { layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function TrashButton() { + return ( + + ); +} + +/** Single-document preview file row (Figma node 40002579:88220) — no leading action. */ +export const Default: Story = { + args: { + icon: , + title: "Archivonombrelargolarguisimo.doc", + description: "11 pag. - 21.5 mb", + trailingAction: , + }, + render: (args) => ( +
+ +
+ ), +}; + +/** Voz a Texto file list — leading play button + trailing delete. */ +export const WithLeadingAction: Story = { + args: { + variant: "outlined", + title: "audiencia.wav", + description: "11 seg. · 344 kb", + leadingAction: ( + + ), + trailingAction: , + }, + render: (args) => ( +
+ +
+ ), +}; + +/** Long filenames ellipsize instead of pushing the trailing action out. */ +export const LongTitle: Story = { + args: { + icon: , + title: "Un-nombre-de-archivo-extremadamente-largo-que-no-entra.docx", + description: "34 pag. - 8.1 mb", + trailingAction: , + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * Full single-document preview composition: `ArchiveView size="lg"` stacked + * above `ArchiveRow` — the outer card/title chrome stays in desktop-app. + */ +export const DocumentPreviewComposition: Story = { + render: () => ( +
+ + } + title="Archivonombrelargolarguisimo.doc" + description="11 pag. - 21.5 mb" + trailingAction={} + /> +
+ ), +}; diff --git a/src/components/archives/ArchiveRow.tsx b/src/components/archives/ArchiveRow.tsx new file mode 100644 index 0000000..07575de --- /dev/null +++ b/src/components/archives/ArchiveRow.tsx @@ -0,0 +1,114 @@ +import type { ReactNode } from "react"; +import { css, cva, cx } from "@/styled/css"; + +/** + * ArchiveRow — horizontal file presentation: icon, title, description, and + * optional leading/trailing actions (e.g. a play button, a delete button). + * + * Formalizes the file-info row from the single-document preview screen + * (Figma node 40002579:88220, frame "Frame 1244833316" — icon container + + * title/description + trailing Trash) and doubles as the row shape Voz a + * Texto already uses for its file list (icon/play button + name + duration + * + delete), so both consumers share one implementation. + * + * Actions are passed as fully-built elements (e.g. ``) — this component only places them, + * it doesn't know what they do. + */ + +const iconContainer = css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + w: "[40px]", + h: "[40px]", + p: "2", // 8px + rounded: "md", + bg: "bg.primary-alternative", + color: "text.default", + flexShrink: "0", + "& svg": { w: "full", h: "full" }, +}); + +const content = css({ + display: "flex", + flexDir: "column", + gap: "[2px]", + flex: "1", + minW: "[0px]", +}); + +const titleStyle = css({ + margin: "0", // no preflight —

keeps the UA default margin otherwise + textStyle: "label.md.default", + color: "text.default", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", +}); + +const descriptionStyle = css({ + margin: "0", + textStyle: "subtitle.sm.default", + color: "text.lighter", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", +}); + +const root = cva({ + base: { + display: "flex", + alignItems: "center", + gap: "4", // 16px + w: "full", + }, + variants: { + variant: { + plain: {}, + outlined: { + p: "6", + rounded: "[8px]", + borderWidth: "[4px]", + borderStyle: "solid", + borderColor: "[#BCBAB8]", + bg: "bg.secondary", + }, + }, + }, + defaultVariants: { variant: "plain" }, +}); + +export interface ArchiveRowProps { + icon?: ReactNode; + title: string; + description: string; + variant?: "plain" | "outlined"; + leadingAction?: ReactNode; + trailingAction?: ReactNode; + className?: string; +} + +export function ArchiveRow({ + icon, + title, + description, + variant = "plain", + leadingAction, + trailingAction, + className, +}: ArchiveRowProps) { + return ( +

+ {leadingAction} + {icon && {icon}} +
+

{title}

+

{description}

+
+ {trailingAction} +
+ ); +} + +export default ArchiveRow; diff --git a/src/components/archives/ArchiveTabs.tsx b/src/components/archives/ArchiveTabs.tsx index 72845da..64d19f0 100644 --- a/src/components/archives/ArchiveTabs.tsx +++ b/src/components/archives/ArchiveTabs.tsx @@ -1,4 +1,4 @@ -import { CheckCircle } from "phosphor-react"; +import { CheckCircleIcon } from "@phosphor-icons/react"; import { cva, cx } from "@/styled/css"; /** @@ -85,7 +85,7 @@ export function ArchiveTabs({ className={cx(archiveTab({ status }), className)} > {label} - {status === "completed" && } + {status === "completed" && } ); } diff --git a/src/components/archives/ArchiveView.stories.tsx b/src/components/archives/ArchiveView.stories.tsx index 9a041dd..0d48b19 100644 --- a/src/components/archives/ArchiveView.stories.tsx +++ b/src/components/archives/ArchiveView.stories.tsx @@ -28,14 +28,31 @@ export const PreviewSelected: Story = { args: { type: "preview", selected: true }, }; +/** Formalizes desktop-app's ad-hoc `loadingPreview` spinner box. */ +export const PreviewLoading: Story = { args: { type: "preview-loading" } }; + +/** Replaces desktop-app's `withoutSelection` CSS hack (hides the checkbox via a prop). */ +export const PreviewNotSelectable: Story = { + args: { type: "preview", selectable: false }, +}; + +/** + * `size="lg"` — the single-document preview (Figma node 40002579:88220), + * shared by Dataset/Anonimizador. No caption: pair it with `ArchiveRow`. + */ +export const LargePreview: Story = { + args: { type: "preview", size: "lg", selectable: false }, +}; + /** Matrix */ export const Matrix: Story = { render: () => (
- - - + + + +
), }; diff --git a/src/components/archives/ArchiveView.tsx b/src/components/archives/ArchiveView.tsx index 7a9b7cc..5764f1b 100644 --- a/src/components/archives/ArchiveView.tsx +++ b/src/components/archives/ArchiveView.tsx @@ -1,11 +1,12 @@ -import { CheckCircle, XCircle } from "phosphor-react"; -import { css, cx } from "@/styled/css"; +import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react"; +import { css, cva, cx } from "@/styled/css"; +import { Spinner } from "../spinner"; /** * ArchiveView — AymurAI UI Library "archive-view" family node 222:21065. * * Figma types: - * Previsualitation — thumbnail image preview (157×192px), filename below, + * Previsualitation — thumbnail image preview, filename below, * checkbox overlay (top-right corner) * Previsualitation Error — same thumbnail but error border (#DC582E) + pink tint, * "Error de carga." italic message below filename @@ -14,6 +15,18 @@ import { css, cx } from "@/styled/css"; * Document Error — error coloured placeholder + XCircle icon (48px), * "Error de guardado." italic message below filename * + * `size="lg"` (367×426px) has no dedicated Figma type — it formalizes the + * single-document preview from the "Anonimizador - Preview" screen (node + * 40002579:88220), shared by every flow that reads one docx/pdf at a time + * (Dataset, Anonimizador). That screen pairs the thumbnail with a separate + * {@link ArchiveRow} for the filename/size/delete action, so `size="lg"` + * never renders its own caption — only `size="sm"` (default, 157×192px, + * the original grid thumbnail) does. + * + * `type="preview-loading"` also has no Figma type — it formalizes + * desktop-app's own ad-hoc `loadingPreview` spinner box, shown while a + * dropped file is still being parsed. + * * Tokens: * border/primary = #BCBAB8 (preview border) * bg/primary = #F6F5F7 (document placeholder bg) @@ -28,91 +41,96 @@ import { css, cx } from "@/styled/css"; export type ArchiveViewType = | "preview" + | "preview-loading" | "preview-error" | "document-ok" | "document-error"; +export type ArchiveViewSize = "sm" | "lg"; + export interface ArchiveViewProps { fileName?: string; type?: ArchiveViewType; + size?: ArchiveViewSize; /** Optional thumbnail image URL (used for preview variants) */ src?: string; + /** Whether the selection checkbox renders at all (preview variants only). */ + selectable?: boolean; /** Whether file is selected (shows checked checkbox overlay) */ selected?: boolean; onSelect?: (selected: boolean) => void; className?: string; } -// Thumbnail frame styles per type -const frameStyles: Record = { - preview: css({ - position: "relative", - w: "[157px]", - h: "[192px]", - rounded: "md", - borderWidth: "[4px]", - borderStyle: "solid", - borderColor: "[#BCBAB8]", - boxShadow: "[0px_0px_4px_rgba(0,0,0,0.1)]", - overflow: "hidden", - bg: "bg.secondary", - flexShrink: "0", - }), - "preview-error": css({ - position: "relative", - w: "[157px]", - h: "[192px]", - rounded: "md", - borderWidth: "[4px]", - borderStyle: "solid", - borderColor: "system.error", - boxShadow: "[0px_0px_4px_rgba(0,0,0,0.1)]", - overflow: "hidden", - bg: "bg.secondary", - flexShrink: "0", - }), - "document-ok": css({ +const frame = cva({ + base: { position: "relative", - display: "flex", - alignItems: "center", - justifyContent: "center", - w: "[157px]", - h: "[192px]", rounded: "md", borderWidth: "[4px]", borderStyle: "solid", - borderColor: "[#BCBAB8]", - filter: "[drop-shadow(0px_0px_2px_rgba(0,0,0,0.1))]", - bg: "bg.primary", flexShrink: "0", - }), - "document-error": css({ - position: "relative", - display: "flex", - alignItems: "center", - justifyContent: "center", - w: "[157px]", - h: "[192px]", - rounded: "md", - borderWidth: "[4px]", - borderStyle: "solid", - borderColor: "system.error", - filter: "[drop-shadow(0px_0px_2px_rgba(0,0,0,0.1))]", - bg: "system.error-secondary", - flexShrink: "0", - }), -}; + }, + variants: { + size: { + sm: { w: "[157px]", h: "[192px]" }, + lg: { w: "[367px]", h: "[426px]" }, + }, + type: { + preview: { + overflow: "hidden", + borderColor: "[#BCBAB8]", + boxShadow: "[0px_0px_4px_rgba(0,0,0,0.1)]", + bg: "bg.secondary", + }, + "preview-loading": { + display: "flex", + alignItems: "center", + justifyContent: "center", + borderColor: "[#BCBAB8]", + boxShadow: "[0px_0px_4px_rgba(0,0,0,0.1)]", + bg: "bg.secondary", + }, + "preview-error": { + overflow: "hidden", + borderColor: "system.error", + boxShadow: "[0px_0px_4px_rgba(0,0,0,0.1)]", + bg: "bg.secondary", + }, + "document-ok": { + display: "flex", + alignItems: "center", + justifyContent: "center", + borderColor: "[#BCBAB8]", + filter: "[drop-shadow(0px_0px_2px_rgba(0,0,0,0.1))]", + bg: "bg.primary", + }, + "document-error": { + display: "flex", + alignItems: "center", + justifyContent: "center", + borderColor: "system.error", + filter: "[drop-shadow(0px_0px_2px_rgba(0,0,0,0.1))]", + bg: "system.error-secondary", + }, + }, + }, + defaultVariants: { size: "sm", type: "preview" }, +}); export function ArchiveView({ fileName = "Archivo 1.doc", type = "preview", + size = "sm", src, + selectable = true, selected = false, onSelect, className, }: ArchiveViewProps) { const isPreview = type === "preview" || type === "preview-error"; const isError = type === "preview-error" || type === "document-error"; + const isLoading = type === "preview-loading"; + const showCaption = size !== "lg"; return (
{/* Thumbnail / placeholder frame */} -
+
{isPreview && src && ( )} - {!isPreview && ( + {isLoading && } + {!isPreview && !isLoading && (
{isError ? ( - + ) : ( - + )}
)} {/* Selection checkbox (preview variants only) */} - {isPreview && ( + {isPreview && selectable && ( )} diff --git a/src/components/big-icon-button/BigIconButton.stories.tsx b/src/components/big-icon-button/BigIconButton.stories.tsx index 0ded366..b13e66b 100644 --- a/src/components/big-icon-button/BigIconButton.stories.tsx +++ b/src/components/big-icon-button/BigIconButton.stories.tsx @@ -1,5 +1,9 @@ +import { + MagnifyingGlassIcon, + PencilIcon, + PlusCircleIcon, +} from "@phosphor-icons/react"; import type { Meta, StoryObj } from "@storybook/react"; -import { MagnifyingGlass, Pencil, PlusCircle } from "phosphor-react"; import { BigIconButton } from "./BigIconButton"; const meta = { @@ -30,7 +34,7 @@ export const PrimaryBig: Story = { args: { variant: "primary", size: "big", - children: , + children: , }, }; @@ -39,7 +43,7 @@ export const PrimarySmall: Story = { args: { variant: "primary", size: "small", - children: , + children: , }, }; @@ -48,7 +52,7 @@ export const SecondaryBig: Story = { args: { variant: "secondary", size: "big", - children: , + children: , }, }; @@ -57,7 +61,7 @@ export const SecondarySmall: Story = { args: { variant: "secondary", size: "small", - children: , + children: , }, }; @@ -66,7 +70,7 @@ export const TertiaryBig: Story = { args: { variant: "tertiary", size: "big", - children: , + children: , }, }; @@ -75,7 +79,7 @@ export const TertiarySmall: Story = { args: { variant: "tertiary", size: "small", - children: , + children: , }, }; @@ -102,7 +106,7 @@ export const DisabledBig: Story = { variant: "primary", size: "big", disabled: true, - children: , + children: , }, }; @@ -118,34 +122,34 @@ export const Matrix: Story = { }} > - + - + - + - + - + - + - + - + - +
), diff --git a/src/components/button-link/ButtonLink.stories.tsx b/src/components/button-link/ButtonLink.stories.tsx index e164672..7c15af1 100644 --- a/src/components/button-link/ButtonLink.stories.tsx +++ b/src/components/button-link/ButtonLink.stories.tsx @@ -1,5 +1,5 @@ +import { ArrowLeftIcon, CaretDownIcon } from "@phosphor-icons/react"; import type { Meta, StoryObj } from "@storybook/react"; -import { ArrowLeft, CaretDown } from "phosphor-react"; import { ButtonLink } from "./ButtonLink"; const meta = { @@ -27,8 +27,8 @@ export const SizeMDefault: Story = { size: "M", type: "Default", children: "call to action", - iconLeft: , - iconRight: , + iconLeft: , + iconRight: , }, }; @@ -38,8 +38,8 @@ export const SizeMAlternative: Story = { size: "M", type: "Alternative", children: "call to action", - iconLeft: , - iconRight: , + iconLeft: , + iconRight: , }, parameters: { backgrounds: { default: "dark" } }, }; @@ -51,7 +51,7 @@ export const SizeSDefault: Story = { size: "S", type: "Default", children: "call to action", - iconLeft: , + iconLeft: , }, }; @@ -62,7 +62,7 @@ export const SizeSAlternative: Story = { size: "S", type: "Alternative", children: "call to action", - iconLeft: , + iconLeft: , }, parameters: { backgrounds: { default: "dark" } }, }; @@ -82,8 +82,8 @@ export const Matrix: Story = { } - iconRight={} + iconLeft={} + iconRight={} > Size M / Default @@ -93,14 +93,18 @@ export const Matrix: Story = { } - iconRight={} + iconLeft={} + iconRight={} > Size M / Alternative
{/* Size=S: built-in CaretDown replaces iconRight */} - }> + } + > Size S / Default
} + iconLeft={} > Size S / Alternative diff --git a/src/components/button-link/ButtonLink.tsx b/src/components/button-link/ButtonLink.tsx index fa6a4b6..4276766 100644 --- a/src/components/button-link/ButtonLink.tsx +++ b/src/components/button-link/ButtonLink.tsx @@ -1,4 +1,4 @@ -import { CaretDown } from "phosphor-react"; +import { CaretDownIcon } from "@phosphor-icons/react"; import type { AnchorHTMLAttributes, ReactNode } from "react"; import { cva, cx, type RecipeVariantProps } from "@/styled/css"; @@ -100,7 +100,7 @@ export function ButtonLink({ as the exclusive right slot. For Size=M the caller controls iconRight. */} {size === "S" ? ( ) : ( iconRight && diff --git a/src/components/button/Button.stories.tsx b/src/components/button/Button.stories.tsx index f78cca9..10e9fa5 100644 --- a/src/components/button/Button.stories.tsx +++ b/src/components/button/Button.stories.tsx @@ -1,5 +1,5 @@ +import { CaretDownIcon } from "@phosphor-icons/react"; import type { Meta, StoryObj } from "@storybook/react"; -import { CaretDown } from "phosphor-react"; import { Button } from "./Button"; const meta = { @@ -48,7 +48,7 @@ export const Matrix: Story = { Disabled
))} diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 4871636..2d8c688 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -1,4 +1,4 @@ -import { CircleNotch } from "phosphor-react"; +import { CircleNotchIcon } from "@phosphor-icons/react"; import type { ButtonHTMLAttributes } from "react"; import { css, cva, cx, type RecipeVariantProps } from "@/styled/css"; @@ -153,7 +153,7 @@ export function Button({ disabled={disabled || isLoading} > {isLoading ? ( - + ) : ( children )} diff --git a/src/components/callout/Callout.stories.tsx b/src/components/callout/Callout.stories.tsx index 25ffb37..5788b29 100644 --- a/src/components/callout/Callout.stories.tsx +++ b/src/components/callout/Callout.stories.tsx @@ -1,9 +1,10 @@ -import type { Meta, StoryObj } from "@storybook/react"; import { - CheckCircle, - WarningCircle, - Warning as WarningIcon, -} from "phosphor-react"; + CheckCircleIcon, + InfoIcon, + WarningCircleIcon, + WarningIcon, +} from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; import { Callout } from "./Callout"; const meta = { @@ -19,7 +20,7 @@ export const Info: Story = { args: { variant: "info" } }; export const Success: Story = { args: { variant: "success", - icon: CheckCircle, + icon: CheckCircleIcon, message: "Operation completed successfully.", }, }; @@ -33,7 +34,7 @@ export const Warning: Story = { export const ErrorState: Story = { args: { variant: "error", - icon: WarningCircle, + icon: WarningCircleIcon, message: "An error occurred.", }, }; @@ -44,6 +45,17 @@ export const NoBorder: Story = { args: { variant: "success", noBorder: true }, }; +/** Voz a Texto processing notice — formalizes the local hand-rolled version. */ +export const Compact: Story = { + args: { + size: "compact", + variant: "info", + icon: InfoIcon, + noBorder: true, + message: "Transcribiendo audio…", + }, +}; + /** Full variant matrix */ export const Matrix: Story = { render: () => ( @@ -51,7 +63,7 @@ export const Matrix: Story = { { message: string; variant?: CalloutVariant; + size?: CalloutSize; noBorder?: boolean; onDismiss?: () => void; icon?: Icon; @@ -99,16 +113,17 @@ export interface CalloutProps extends HTMLAttributes { export function Callout({ message, variant = "info", + size = "normal", noBorder = false, onDismiss, - icon: IconComponent = Bell, + icon: IconComponent = BellIcon, className, ...props }: CalloutProps) { const accent = accentRecipe({ variant }); return (
@@ -131,7 +146,7 @@ export function Callout({ lineHeight="[0]" className={accent} > -
diff --git a/src/components/card-tool/CardTool.stories.tsx b/src/components/card-tool/CardTool.stories.tsx new file mode 100644 index 0000000..954ebb7 --- /dev/null +++ b/src/components/card-tool/CardTool.stories.tsx @@ -0,0 +1,22 @@ +import { ArticleIcon } from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { CardTool } from "./CardTool"; + +const meta = { + title: "Components/CardTool", + component: CardTool, + args: { + icon: , + title: "Resumen de documentos", + description: "Resumen automático de documentos", + interactive: true, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; +export const Disabled: Story = { + args: { disabled: true, interactive: false }, +}; diff --git a/src/components/card-tool/CardTool.tsx b/src/components/card-tool/CardTool.tsx new file mode 100644 index 0000000..c0e777c --- /dev/null +++ b/src/components/card-tool/CardTool.tsx @@ -0,0 +1,88 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { css, cva, cx } from "@/styled/css"; +import { Card } from "../card"; + +const iconContainer = cva({ + base: { + display: "flex", + alignItems: "center", + justifyContent: "center", + w: "[70px]", + h: "[70px]", + p: "[14px]", + rounded: "[14px]", + flexShrink: "0", + "& svg": { w: "full", h: "full" }, + }, + variants: { + disabled: { + true: { bg: "bg.secondary", color: "text.lighter" }, + false: { bg: "bg.primary-alternative", color: "text.default" }, + }, + }, + defaultVariants: { disabled: false }, +}); + +const content = css({ + display: "flex", + flexDir: "column", + alignItems: "flex-start", + gap: "4", +}); + +const copy = css({ + display: "flex", + flexDir: "column", + gap: "1", + minW: "0", +}); + +const titleStyle = css({ + margin: "0", // no preflight —

/

keep the UA default margin otherwise + textStyle: "subtitle.md.strong", +}); +const descriptionStyle = css({ + margin: "0", + textStyle: "subtitle.sm.default", + color: "text.lighter", +}); + +export interface CardToolProps + extends Omit, "title"> { + icon: ReactNode; + title: ReactNode; + description: ReactNode; + disabled?: boolean; + interactive?: boolean; +} + +/** Feature-launch card content. Wrap enabled cards with the consumer's router link. */ +export function CardTool({ + icon, + title, + description, + disabled = false, + interactive = false, + className, + ...props +}: CardToolProps) { + return ( + +

+
{icon}
+
+

{title}

+

{description}

+
+
+ + ); +} + +export default CardTool; diff --git a/src/components/card-tool/index.ts b/src/components/card-tool/index.ts new file mode 100644 index 0000000..0e21034 --- /dev/null +++ b/src/components/card-tool/index.ts @@ -0,0 +1 @@ +export { CardTool, type CardToolProps, default } from "./CardTool"; diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index a2d5e01..6852b34 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -1,3 +1,4 @@ +import type { HTMLAttributes } from "react"; import { cva, cx } from "@/styled/css"; /** @@ -64,8 +65,7 @@ const styles = cva({ }, }); -export interface CardProps { - children?: React.ReactNode; +export interface CardProps extends HTMLAttributes { disabled?: boolean; size?: "lg" | "sm"; clickable?: boolean; @@ -78,9 +78,14 @@ export function Card({ clickable = false, children, className, + ...props }: CardProps) { const classes = styles({ size, disabled, clickable }); - return
{children}
; + return ( +
+ {children} +
+ ); } export default Card; diff --git a/src/components/check-circle/CheckCircle.tsx b/src/components/check-circle/CheckCircle.tsx index ba0a1ea..f24f0c9 100644 --- a/src/components/check-circle/CheckCircle.tsx +++ b/src/components/check-circle/CheckCircle.tsx @@ -1,13 +1,14 @@ -import { useId } from "react"; import { cx } from "@/styled/css"; /** * CheckCircle — AymurAI UI Library. * - * Figma family: 1568:25593 (success/complete state icon). A full ring, NOT a tick. - * Stroke is a 3-stop gradient matching `gradients.primary` token: - * 249.5deg — #C5CAFF (action.default) → #8591E8 → #3F479D (action.alt-default). - * Completed/success state. + * Figma node 1568:25588 (the "Spinner" frame's 5th/completed state — sibling + * nodes named "CheckCircle" in that same frame are actually spinner rotation + * frames with no tick; this is the only one with a checkmark). Solid stroke, + * #3F479D (action.alt-default) — NOT a gradient; a prior version of this + * component drew an invented 3-stop gradient and omitted the tick entirely, + * matching neither Figma reference. */ export type CheckCircleProps = { @@ -22,9 +23,6 @@ export function CheckCircle({ className, "aria-label": ariaLabel = "Success", }: CheckCircleProps) { - // Unique gradient id per instance to avoid collisions when several - // CheckCircle elements are rendered on the same page. - const gradId = useId(); return ( - {/* Gradient circle — gradients.primary token: 249.5deg, #C5CAFF → #8591E8 → #3F479D */} + - - {/* - Matches Figma `gradients.primary`: - linear-gradient(249.5deg, #C5CAFF -33.26%, #8591E8 28.49%, #3F479D 82.99%) - SVG coords derived from 249.5deg on a 48×48 userSpaceOnUse canvas. - x1/y1 = light end (C5CAFF), x2/y2 = dark end (3F479D). - */} - - - - - - ); } diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx index bea196a..0eb7807 100644 --- a/src/components/checkbox/Checkbox.tsx +++ b/src/components/checkbox/Checkbox.tsx @@ -1,4 +1,4 @@ -import { Check } from "phosphor-react"; +import { CheckIcon } from "@phosphor-icons/react"; import type { InputHTMLAttributes, ReactNode } from "react"; import { css, cva, cx, type RecipeVariantProps } from "@/styled/css"; @@ -180,7 +180,7 @@ export function Checkbox({ disabled, })} > - {(checked || defaultChecked) && } + {(checked || defaultChecked) && } {children} diff --git a/src/components/context-menu/ContextMenu.tsx b/src/components/context-menu/ContextMenu.tsx new file mode 100644 index 0000000..86803db --- /dev/null +++ b/src/components/context-menu/ContextMenu.tsx @@ -0,0 +1,104 @@ +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; +import type { ComponentPropsWithoutRef } from "react"; +import { css, cx } from "@/styled/css"; + +/** + * ContextMenu — Radix UI context menu with AymurAI styling. + * + * Right-click menu, not a Popover: Radix's context-menu primitive owns + * cursor-position anchoring, keyboard nav, and dismiss-on-outside-click for + * this exact "menu at the click point" shape, which Popover isn't built for. + * + * `ContextMenuTrigger`'s own `disabled` prop is the mechanism for "only show + * our menu sometimes" (e.g. only while the caret is inside a table) — when + * disabled, the native browser context menu shows through instead. + */ +export const ContextMenu = ContextMenuPrimitive.Root; +export const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const contentStyles = css({ + zIndex: 50, + minW: "[200px]", + bg: "bg.secondary", + border: "primary", + rounded: "sm", + boxShadow: "popover", + py: "2", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +export function ContextMenuContent({ + className, + children, + ...props +}: ComponentPropsWithoutRef) { + return ( + + + {children} + + + ); +} + +const itemStyles = css({ + display: "flex", + alignItems: "center", + gap: "2", + px: "4", + py: "2", + textStyle: "label.md.default", + color: "text.default", + cursor: "pointer", + outline: "none", + userSelect: "none", + + "&[data-highlighted]": { + bg: "bg.primary-alternative", + }, + "&[data-disabled]": { + cursor: "not-allowed", + color: "text.lighter", + }, +}); + +export function ContextMenuItem({ + className, + ...props +}: ComponentPropsWithoutRef) { + return ( + + ); +} + +const separatorStyles = css({ + h: "[1px]", + my: "1", + bg: "[#BCBAB8]", +}); + +export function ContextMenuSeparator({ + className, + ...props +}: ComponentPropsWithoutRef) { + return ( + + ); +} + +export default ContextMenu; diff --git a/src/components/context-menu/index.ts b/src/components/context-menu/index.ts new file mode 100644 index 0000000..5a8f865 --- /dev/null +++ b/src/components/context-menu/index.ts @@ -0,0 +1,8 @@ +export { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, + default, +} from "./ContextMenu"; diff --git a/src/components/dialog/Dialog.stories.tsx b/src/components/dialog/Dialog.stories.tsx index 79bd0da..0eed634 100644 --- a/src/components/dialog/Dialog.stories.tsx +++ b/src/components/dialog/Dialog.stories.tsx @@ -91,6 +91,187 @@ export const Default: Story = { ), }; +function sizeTrigger(label: string) { + return ( + + + + ); +} + +/** Confirmations — e.g. "¿Eliminar esta etiqueta?" */ +export const SizeSm: Story = { + render: () => ( + + {sizeTrigger("Open sm")} + + + + ¿Eliminar esta etiqueta? + + + + + + + + + + + ), +}; + +/** Forms — e.g. entity/label resolution */ +export const SizeMd: Story = { + render: () => ( + + {sizeTrigger("Open md")} + + + + Resolver entidad + + + + Elegí la entidad correcta para el término seleccionado. + + + + + + + + + + ), +}; + +/** Tutorials — the 2×2 "¿Cómo funciona?" step grid */ +export const SizeLg: Story = { + render: () => ( + + {sizeTrigger("Open lg")} + + + + ¿Cómo funciona? + + + + + +
+ {[1, 2, 3, 4].map((step) => ( +
+
+ + Paso {step} + +
+ ))} +
+ +
+ ), +}; + +/** Complex screens that need real vertical room */ +export const SizeFull: Story = { + render: () => ( + + {sizeTrigger("Open full")} + + + + Pantalla completa + + + + + +

+ Ocupa el alto disponible (90vh) en vez de ajustarse al contenido — + para flujos complejos con su propio scroll interno. +

+
+
+ ), +}; + export const WithLongContent: Story = { render: () => ( diff --git a/src/components/dialog/Dialog.tsx b/src/components/dialog/Dialog.tsx index de8eb8e..ad5aa93 100644 --- a/src/components/dialog/Dialog.tsx +++ b/src/components/dialog/Dialog.tsx @@ -1,10 +1,18 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"; import type { ComponentPropsWithoutRef, HTMLAttributes } from "react"; -import { css, cx } from "@/styled/css"; +import { css, cva, cx } from "@/styled/css"; /** * Dialog — Radix UI dialog with AymurAI styling. * Ported from desktop-app/src/renderer/src/components/ui/dialog.tsx + * + * No Figma component backs `DialogContent`'s `size` variants — this design + * system file has no Dialog/Modal family at all. Breakpoints are lifted + * from desktop-app's real per-usage overrides instead (confirmations ~420px, + * forms ~520px, the "¿Cómo funciona?" tutorial ~900–1024px via a + * `minWidth:900px` + `maxW:5xl!` hack). Every size stays viewport-bound + * (`min(px, vw)`) instead of the old hard `minWidth`, which is what let the + * tutorial dialog overflow narrow windows. */ export const Dialog = DialogPrimitive.Root; export const DialogTrigger = DialogPrimitive.Trigger; @@ -26,27 +34,61 @@ const overlayStyles = css({ }, }); -const contentStyles = css({ - position: "fixed", - inset: "[0]", - margin: "auto", - zIndex: 50, - - bg: "bg.secondary", - rounded: "sm", - p: "6", - boxShadow: "dialog", - - width: "[90vw]", - minW: "[300px]", - maxW: "[700px]", - h: "[fit-content]", - - "&[data-state='open']": { - animation: "fadeIn", +export type DialogContentSize = "sm" | "md" | "lg" | "full"; + +const contentStyles = cva({ + base: { + position: "fixed", + inset: "[0]", + margin: "auto", + zIndex: 50, + + bg: "bg.secondary", + rounded: "sm", + p: "6", + boxShadow: "dialog", + + minW: "[300px]", + maxH: "[90vh]", + overflowY: "auto", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, }, - "&[data-state='closed']": { - animation: "fadeOut", + variants: { + size: { + // Confirmations — e.g. "¿Eliminar esta etiqueta?" + sm: { + width: "[min(420px,90vw)]", + maxW: "[420px]", + h: "[fit-content]", + }, + // Forms — e.g. entity/label resolution + md: { + width: "[min(520px,92vw)]", + maxW: "[520px]", + h: "[fit-content]", + }, + // Tutorials — e.g. the 2×2 "¿Cómo funciona?" step grid + lg: { + width: "[min(1024px,92vw)]", + maxW: "[1024px]", + h: "[fit-content]", + }, + // Complex screens that need real vertical room, not just fit-content + full: { + width: "[min(1440px,96vw)]", + maxW: "[1440px]", + h: "[90vh]", + }, + }, + }, + defaultVariants: { + size: "md", }, }); @@ -80,19 +122,22 @@ export function DialogOverlay({ className, ...props }: DialogOverlayProps) { export interface DialogContentProps extends ComponentPropsWithoutRef { container?: HTMLElement; + /** Confirmations (sm) · forms (md, default) · tutorials (lg) · complex screens (full) */ + size?: DialogContentSize; } export function DialogContent({ className, container, children, + size = "md", ...props }: DialogContentProps) { return ( {children} diff --git a/src/components/features-menu/FeaturesMenu.stories.tsx b/src/components/features-menu/FeaturesMenu.stories.tsx new file mode 100644 index 0000000..8cde714 --- /dev/null +++ b/src/components/features-menu/FeaturesMenu.stories.tsx @@ -0,0 +1,110 @@ +import { + ArticleIcon, + DatabaseIcon, + DetectiveIcon, + FileAudioIcon, + GearIcon, +} from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Popover, PopoverContent, PopoverTrigger } from "../popover"; +import { FeaturesMenu } from "./FeaturesMenu"; +import { FeaturesMenuItem } from "./FeaturesMenuItem"; + +const meta = { + title: "Components/FeaturesMenu", + component: FeaturesMenu, + tags: ["autodocs"], + parameters: { layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** The three real pipelines, plus the "Resumen" placeholder — not yet released. */ +export const Default: Story = { + render: () => ( + + } + label="Set de Datos" + /> + } + label="Anonimizador" + /> + } + label="Voz a Texto" + /> + } label="Resumen" /> + + ), +}; + +/** + * The real header dropdown: three pipelines + "Configuración" (no Figma + * reference for this row — desktop-app-only, kept because it's needed). + */ +export const WithSettings: Story = { + render: () => ( + + } + label="Set de Datos" + /> + } + label="Anonimizador" + /> + } + label="Voz a Texto" + /> + } label="Resumen" /> + } + label="Configuración" + fullWidth + /> + + ), +}; + +/** Anchored inside a real Popover — overrides Popover's own bg/radius/shadow/padding so the two don't double up. */ +export const InsidePopover: Story = { + render: () => ( + + + + + + + } + label="Set de Datos" + /> + } + label="Anonimizador" + /> + } + label="Voz a Texto" + /> + } label="Resumen" /> + } + label="Configuración" + fullWidth + /> + + + + ), +}; diff --git a/src/components/features-menu/FeaturesMenu.tsx b/src/components/features-menu/FeaturesMenu.tsx new file mode 100644 index 0000000..733bb3a --- /dev/null +++ b/src/components/features-menu/FeaturesMenu.tsx @@ -0,0 +1,34 @@ +import type { HTMLAttributes } from "react"; +import { css, cx } from "@/styled/css"; + +/** + * FeaturesMenu — the grid chrome for a set of {@link FeaturesMenuItem}s. + * AymurAI UI Library "Menu" node 40000732:79289. + * + * Two columns, matching Figma exactly — this is sized for a small anchored + * popover, not a full-page layout, so it doesn't need to reflow at + * viewport breakpoints the way a page-level grid would. + * + * Purely a container: place ``s (optionally wrapped in + * the consumer's own router link/onClick) as children. A trailing item + * that doesn't share its row (e.g. "Configuración") uses + * `` to span both columns. + */ + +const grid = css({ + display: "grid", + gridTemplateColumns: "[repeat(2,140px)]", + gap: "2", // 8px + bg: "bg.primary", + rounded: "md", // 8px + p: "4", // 16px + boxShadow: "menu", +}); + +export interface FeaturesMenuProps extends HTMLAttributes {} + +export function FeaturesMenu({ className, ...props }: FeaturesMenuProps) { + return
; +} + +export default FeaturesMenu; diff --git a/src/components/features-menu/FeaturesMenuItem.tsx b/src/components/features-menu/FeaturesMenuItem.tsx new file mode 100644 index 0000000..bc17fe1 --- /dev/null +++ b/src/components/features-menu/FeaturesMenuItem.tsx @@ -0,0 +1,118 @@ +import type { ReactNode } from "react"; +import { css, cva, cx } from "@/styled/css"; + +/** + * FeaturesMenuItem — a single entry in {@link FeaturesMenu}: icon + label, + * no description. AymurAI UI Library "Menu" node 40000732:79289 ("item + * menu" sub-frame). + * + * Figma specs this label in Inter SemiBold — every other text style in this + * library is Archivo. Kept on the shared `label.md.strong` token (Archivo) + * instead, for visual consistency with the rest of the app; treated as a + * likely Figma inconsistency rather than a deliberate exception. + * + * Disabled styling mirrors {@link Card}'s convention (bg.primary/text.lighter + * on the card) plus {@link CardTool}'s icon-box convention (bg.secondary/ + * text.lighter) — this is a distinct, smaller card shape from both, but + * reuses their established disabled treatment for consistency. + */ + +const item = cva({ + base: { + display: "flex", + flexDir: "column", + alignItems: "center", + justifyContent: "center", + gap: "3", // 12px + w: "full", + bg: "bg.secondary", + border: "primary", + rounded: "md", // 8px + p: "4", // 16px + cursor: "pointer", + transitionProperty: "[border, box-shadow]", + transitionDuration: "normal", + transitionTimingFunction: "default", + "&:hover:enabled": { + border: "primary-alt", + boxShadow: "card-hover", + }, + "&:focus-visible:enabled": { + outline: "primary-alt", + outlineWidth: "[2px]", + boxShadow: "focus", + }, + "&:disabled": { + cursor: "default", + bg: "bg.primary", + color: "text.lighter", + }, + }, + variants: { + fullWidth: { + true: { gridColumn: "1 / -1" }, + false: {}, + }, + }, + defaultVariants: { fullWidth: false }, +}); + +const iconContainer = cva({ + base: { + display: "flex", + alignItems: "center", + justifyContent: "center", + w: "[40px]", + h: "[40px]", + p: "2", // 8px + rounded: "md", // 8px + flexShrink: "0", + "& svg": { w: "full", h: "full" }, + }, + variants: { + disabled: { + true: { bg: "bg.secondary", color: "text.lighter" }, + false: { bg: "bg.primary-alternative", color: "text.default" }, + }, + }, + defaultVariants: { disabled: false }, +}); + +const label = css({ + margin: "0", // no preflight —

keeps the UA default margin otherwise + textStyle: "label.md.strong", + whiteSpace: "nowrap", +}); + +export interface FeaturesMenuItemProps { + icon: ReactNode; + label: string; + disabled?: boolean; + /** Spans both grid columns — e.g. a trailing "Configuración" row. */ + fullWidth?: boolean; + onClick?: () => void; + className?: string; +} + +export function FeaturesMenuItem({ + icon, + label: labelText, + disabled = false, + fullWidth = false, + onClick, + className, +}: FeaturesMenuItemProps) { + return ( + + ); +} + +export default FeaturesMenuItem; diff --git a/src/components/features-menu/index.ts b/src/components/features-menu/index.ts new file mode 100644 index 0000000..8375913 --- /dev/null +++ b/src/components/features-menu/index.ts @@ -0,0 +1,5 @@ +export { default, FeaturesMenu, type FeaturesMenuProps } from "./FeaturesMenu"; +export { + FeaturesMenuItem, + type FeaturesMenuItemProps, +} from "./FeaturesMenuItem"; diff --git a/src/components/file-drop-zone/FileDropZone.stories.tsx b/src/components/file-drop-zone/FileDropZone.stories.tsx new file mode 100644 index 0000000..f5bc758 --- /dev/null +++ b/src/components/file-drop-zone/FileDropZone.stories.tsx @@ -0,0 +1,43 @@ +import { FileAudioIcon } from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { FileDropZone } from "./FileDropZone"; + +const meta = { + title: "Components/FileDropZone", + component: FileDropZone, + args: { + icon: , + title: "Selecciona o arrastra el archivo para\ntranscribir", + description: "Formatos válidos: .mp3, .wav, .m4a, .webm, .ogg o .flac", + }, + parameters: { layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Dragging: Story = { args: { dragging: true } }; + +export const Disabled: Story = { args: { disabled: true } }; + +/** Single-line copy — e.g. Dataset/Anonimizador, which don't need a line break. */ +export const OneLineCopy: Story = { + args: { + title: "Selecciona o arrastra el archivo para agregar al set de datos", + description: "Formatos válidos: .docx, .pdf", + }, +}; + +/** Narrow viewport — padding/gap shrink, the surface stays full-width. */ +export const NarrowViewport: Story = { + parameters: { viewport: { defaultViewport: "mobile1" } }, + decorators: [ + (Story) => ( +

+ +
+ ), + ], +}; diff --git a/src/components/file-drop-zone/FileDropZone.tsx b/src/components/file-drop-zone/FileDropZone.tsx new file mode 100644 index 0000000..c167ec3 --- /dev/null +++ b/src/components/file-drop-zone/FileDropZone.tsx @@ -0,0 +1,182 @@ +import type { DragEvent, ReactNode } from "react"; +import { useRef, useState } from "react"; +import { css, cva, cx } from "@/styled/css"; +import { stack } from "@/styled/patterns"; + +/** + * FileDropZone — file-selection surface shared by Dataset, Anonimizador and + * Voz a Texto onboarding. No Figma frame documents this component; it + * formalizes desktop-app's own `DropArea` (already byte-identical across + * the three flows) instead — including its two real gaps: no `disabled` + * state existed before (styled from scratch, following the codebase's + * `opacity: 0.5` disabled convention, e.g. `Search`'s prev/next buttons), + * and no `:focus-visible` styling existed before (added here, matching + * `Button`'s outline + shadow treatment). + * + * Purely presentational: it never inspects file names or extensions. + * `onDrop` hands the consumer the raw dropped `File[]`; filtering by + * extension, single/multiple selection, and the hidden `` + * all stay in desktop-app. + * + * `dragging` is self-managed by default (native dragenter/dragleave with a + * counter, ported from `DropArea`, so nested children don't cause flicker) + * but accepts a controlled override — mainly so Storybook can force the + * state without simulating a real OS drag gesture. + */ + +const zone = cva({ + base: { + display: "flex", + flexDir: "column", + alignItems: "center", + justifyContent: "center", + gap: { base: "6", sm: "8" }, + w: "full", + minH: "[335px]", + p: { base: "4", sm: "8" }, + rounded: "sm", + border: "primary", + bg: "[#F3F4FF]", // token gap: idle drop-zone tint, distinct from bg.primary-alternative (#E5E8FF) + boxShadow: "[0px_4px_20px_rgba(0,0,0,0.05)]", + cursor: "pointer", + transitionProperty: "[border-color, background-color, box-shadow]", + transitionDuration: "normal", + transitionTimingFunction: "default", + + "&:focus-visible:enabled": { + outline: "primary-alt", + outlineWidth: "[2px]", + boxShadow: "focus", + }, + "&:disabled": { + cursor: "not-allowed", + opacity: "0.5", + }, + }, + variants: { + dragging: { + true: { + borderColor: "brand.primary", + bg: "bg.primary-alternative", + boxShadow: "[0px_0px_15px_0px_#3F479D66]", + }, + false: {}, + }, + }, + defaultVariants: { dragging: false }, +}); + +const iconContainer = cva({ + base: { + display: "flex", + alignItems: "center", + justifyContent: "center", + w: "[70px]", + h: "[70px]", + p: "[14px]", + rounded: "[14px]", + flexShrink: "0", + bg: "bg.primary-alternative", + color: "text.lighter", + "& svg": { w: "full", h: "full" }, + }, +}); + +const titleStyle = css({ + margin: "0", // no preflight —

keeps the UA default margin otherwise + textStyle: "subtitle.md.default", + color: "text.default", + textAlign: "center", + whiteSpace: "pre-line", // titles carry real line breaks, e.g. "...para\ntranscribir" + maxW: "[361px]", +}); + +const descriptionStyle = css({ + margin: "0", + textStyle: "subtitle.sm.default", + color: "text.lighter", + textAlign: "center", +}); + +export interface FileDropZoneProps { + icon: ReactNode; + title: string; + description: string; + /** Forces the dragging visual; omit to let the component track real drag events. */ + dragging?: boolean; + disabled?: boolean; + onClick?: () => void; + /** Receives the raw dropped files — extension/type filtering stays in the consumer. */ + onDrop?: (files: File[]) => void; + className?: string; +} + +export function FileDropZone({ + icon, + title, + description, + dragging, + disabled = false, + onClick, + onDrop, + className, +}: FileDropZoneProps) { + const [internalDragging, setInternalDragging] = useState(false); + const dragCounter = useRef(0); + const isDragging = dragging ?? internalDragging; + + function handleDragEnter(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + if (disabled) return; + dragCounter.current += 1; + setInternalDragging(true); + } + + function handleDragLeave(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + if (disabled) return; + dragCounter.current = Math.max(0, dragCounter.current - 1); + if (dragCounter.current === 0) setInternalDragging(false); + } + + function handleDragOver(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + } + + function handleDrop(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + dragCounter.current = 0; + setInternalDragging(false); + if (disabled) return; + if (event.dataTransfer.files.length > 0) { + onDrop?.(Array.from(event.dataTransfer.files)); + } + } + + return ( + + ); +} + +export default FileDropZone; diff --git a/src/components/file-drop-zone/index.ts b/src/components/file-drop-zone/index.ts new file mode 100644 index 0000000..89829e5 --- /dev/null +++ b/src/components/file-drop-zone/index.ts @@ -0,0 +1 @@ +export { default, FileDropZone, type FileDropZoneProps } from "./FileDropZone"; diff --git a/src/components/logo/Logo.tsx b/src/components/logo/Logo.tsx index ab19adf..2b17cec 100644 --- a/src/components/logo/Logo.tsx +++ b/src/components/logo/Logo.tsx @@ -1,4 +1,4 @@ -import type { HTMLAttributes } from "react"; +import type { HTMLAttributes, ReactElement, ReactNode } from "react"; import { css, cx } from "@/styled/css"; /** @@ -128,14 +128,21 @@ export interface LogoProps extends HTMLAttributes { * Falls back to "Aymurai" when omitted. */ featureName?: string; + /** Wrap only the iso mark, leaving the wordmark/divider/feature label outside. */ + markSlot?: (defaultMark: ReactElement) => ReactNode; } export function Logo({ variant = "logo", featureName = "Anonimizador", + markSlot, className, ...props }: LogoProps) { + const mark = markSlot ? markSlot() : ; + const logoA11yProps = (label: string) => + markSlot ? {} : { role: "img", "aria-label": label }; + if (variant === "iso") { return (

- + {mark}
); } @@ -170,10 +176,9 @@ export function Logo({ }), className, )} - role="img" - aria-label={`AymurAI ${featureName}`} + {...logoA11yProps(`AymurAI ${featureName}`)} > - + {mark} {/* Vertical divider */}
- + {mark}
); diff --git a/src/components/option/Option.tsx b/src/components/option/Option.tsx index 73cfd2d..09d1e11 100644 --- a/src/components/option/Option.tsx +++ b/src/components/option/Option.tsx @@ -1,4 +1,4 @@ -import { XCircle } from "phosphor-react"; +import { XCircleIcon } from "@phosphor-icons/react"; import type { HTMLAttributes, MouseEventHandler } from "react"; import { css, cva, cx, type RecipeVariantProps } from "@/styled/css"; @@ -80,7 +80,7 @@ export function Option({ className={removeButton} onClick={onRemove} > - +
); diff --git a/src/components/page-title/PageTitle.stories.tsx b/src/components/page-title/PageTitle.stories.tsx new file mode 100644 index 0000000..35348df --- /dev/null +++ b/src/components/page-title/PageTitle.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { PageTitle } from "./PageTitle"; + +const meta = { + title: "Components/PageTitle", + component: PageTitle, + args: { + children: "1. Selección de archivo", + }, + parameters: { layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const LongTitle: Story = { + args: { + children: "Revisá y validá la información extraída del documento", + }, + parameters: { viewport: { defaultViewport: "mobile1" } }, +}; diff --git a/src/components/page-title/PageTitle.tsx b/src/components/page-title/PageTitle.tsx new file mode 100644 index 0000000..058685e --- /dev/null +++ b/src/components/page-title/PageTitle.tsx @@ -0,0 +1,25 @@ +import type { HTMLAttributes } from "react"; +import { css, cx } from "@/styled/css"; + +/** + * PageTitle — semantic heading shared by the main sections of AymurAI flows. + * + * The visual contract comes from the repeated `title.md.strong` heading used + * by onboarding, preview, processing, validation and finish screens. It stays + * deliberately small: hierarchy and copy belong to the consumer, while the + * library owns the common typography and reset. + */ +const titleStyle = css({ + m: "0", + color: "text.default", + textStyle: "title.md.strong", + minW: "[0px]", +}); + +export interface PageTitleProps extends HTMLAttributes {} + +export function PageTitle({ className, ...props }: PageTitleProps) { + return

; +} + +export default PageTitle; diff --git a/src/components/page-title/index.ts b/src/components/page-title/index.ts new file mode 100644 index 0000000..3678965 --- /dev/null +++ b/src/components/page-title/index.ts @@ -0,0 +1 @@ +export { default, PageTitle, type PageTitleProps } from "./PageTitle"; diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 6e1fc0d..1a4b06a 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -1,9 +1,9 @@ import { - ArrowClockwise, - ArrowCounterClockwise, - Pause, - Play, -} from "phosphor-react"; + ArrowClockwiseIcon, + ArrowCounterClockwiseIcon, + PauseIcon, + PlayIcon, +} from "@phosphor-icons/react"; import { type MouseEvent, type ReactNode, @@ -295,7 +295,7 @@ export function Player({ aria-label={t.rewind5s} onClick={rewind5s} > - + 5 @@ -305,7 +305,7 @@ export function Player({ aria-label={isPlaying ? t.pause : t.play} onClick={togglePlay} > - {isPlaying ? : } + {isPlaying ? : } - + 5 diff --git a/src/components/popover/Popover.stories.tsx b/src/components/popover/Popover.stories.tsx index 8947c0a..f5db83a 100644 --- a/src/components/popover/Popover.stories.tsx +++ b/src/components/popover/Popover.stories.tsx @@ -74,6 +74,41 @@ export const WithArrow: Story = { ), }; +export const NoSurface: Story = { + render: () => ( + + + + + +
+ Content owns its own background/radius/shadow — this shell only + positions and animates it. +
+
+
+ ), +}; + export const BottomPlacement: Story = { render: () => ( diff --git a/src/components/popover/Popover.tsx b/src/components/popover/Popover.tsx index 4dd170f..90c5e11 100644 --- a/src/components/popover/Popover.tsx +++ b/src/components/popover/Popover.tsx @@ -11,11 +11,8 @@ export const PopoverTrigger = PopoverPrimitive.Trigger; export const PopoverAnchor = PopoverPrimitive.Anchor; export const PopoverClose = PopoverPrimitive.Close; -const contentStyles = css({ +const baseStyles = css({ zIndex: 50, - bg: "bg.primary", - rounded: "lg", - boxShadow: "popover", "&[data-state='open']": { animation: "fadeIn", @@ -25,6 +22,16 @@ const contentStyles = css({ }, }); +// The default look: a light surface, rounded corners, and the standard +// popover shadow. `surface={false}` skips this for consumers that render +// their own fully-styled surface (a custom menu grid, a colored toolbar) +// and only want this shell for positioning/animation/focus-trap behavior. +const surfaceStyles = css({ + bg: "bg.primary", + rounded: "lg", + boxShadow: "popover", +}); + const arrowStyles = css({ // Match the popover surface so the caret reads as one shape (was bg.secondary // = #FFFFFF against a bg.primary #F6F5F7 body → visible seam). @@ -35,6 +42,15 @@ export interface PopoverContentProps extends ComponentPropsWithoutRef { showArrow?: boolean; container?: HTMLElement; + /** + * Set to `false` when the content renders its own fully-styled surface + * (background, radius, shadow) and this shell should stay inert apart + * from positioning/animation — e.g. FeaturesMenu's grid or an + * already-colored toolbar. Defaults to `true` (the standard light-surface + * look). Pair with `showArrow={false}` when disabling the surface, since + * the default arrow is colored to match it. + */ + surface?: boolean; } export function PopoverContent({ @@ -43,13 +59,14 @@ export function PopoverContent({ showArrow = false, container, children, + surface = true, ...props }: PopoverContentProps) { return ( {children} diff --git a/src/components/rich-text-editor/RichTextEditor.stories.tsx b/src/components/rich-text-editor/RichTextEditor.stories.tsx new file mode 100644 index 0000000..34c8f06 --- /dev/null +++ b/src/components/rich-text-editor/RichTextEditor.stories.tsx @@ -0,0 +1,135 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { JSONContent } from "@tiptap/core"; +import { useState } from "react"; +import { documentFromMarkdown } from "@/utils/rich-text/markdown"; +import { RichTextEditor } from "./RichTextEditor"; + +const sampleDoc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "El presente caso tramita ante el Juzgado en lo Penal, ", + }, + { + type: "text", + text: "Contravencional y de Faltas", + marks: [{ type: "bold" }], + }, + { type: "text", text: " N.º 10." }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Se dispusieron medidas de protección urgentes.", + marks: [{ type: "highlight", attrs: { color: "#FFF2C6" } }], + }, + ], + }, + ], +}; + +const meta: Meta = { + title: "Components/RichTextEditor", + component: RichTextEditor, +}; +export default meta; + +type Story = StoryObj; + +export const Editable: Story = { + render: () => { + const [doc, setDoc] = useState(sampleDoc); + const [title, setTitle] = useState("Resumen 10/04/2025"); + return ( + + ); + }, +}; + +export const ReadOnlyPreview: Story = { + args: { + document: sampleDoc, + title: "Resumen 10/04/2025", + readOnly: true, + }, +}; + +export const Empty: Story = { + args: { + document: { type: "doc", content: [] }, + title: "Resumen", + }, +}; + +export const Lists: Story = { + render: () => { + const [doc, setDoc] = useState({ + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "• Primer punto" }], + }, + { + type: "paragraph", + content: [{ type: "text", text: "• Segundo punto" }], + }, + { + type: "paragraph", + content: [{ type: "text", text: "1. Paso uno" }], + }, + { + type: "paragraph", + content: [{ type: "text", text: "2. Paso dos" }], + }, + ], + }); + return ; + }, +}; + +const SAMPLE_MARKDOWN = `## Resumen del documento + +El presente caso tramita ante el **Juzgado en lo Penal, Contravencional y de Faltas N.º 10**. + +1. **Hechos relevantes** + - Hecho relevante uno + - Hecho relevante dos +2. **Decisión adoptada** + - Se dispusieron *medidas de protección* urgentes. + +--- + +## Entidades relevantes + +| Medida | Plazo | +| --- | --- | +| Exclusión del hogar | Inmediato | +| Prohibición de acercamiento | 180 días |`; + +export const FromMarkdown: Story = { + render: () => { + const [doc, setDoc] = useState(() => + documentFromMarkdown(SAMPLE_MARKDOWN), + ); + return ( + + ); + }, +}; diff --git a/src/components/rich-text-editor/RichTextEditor.test.tsx b/src/components/rich-text-editor/RichTextEditor.test.tsx new file mode 100644 index 0000000..6315e08 --- /dev/null +++ b/src/components/rich-text-editor/RichTextEditor.test.tsx @@ -0,0 +1,1087 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { JSONContent } from "@tiptap/core"; +import { describe, expect, it, vi } from "vitest"; +import { RichTextEditor } from "./RichTextEditor"; + +/** Narrows a possibly-null/undefined test lookup without a non-null assertion. */ +function mustExist(value: T | null | undefined, what: string): T { + if (value == null) throw new Error(`expected ${what} to exist`); + return value; +} + +const doc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "hola " }, + { type: "text", text: "mundo", marks: [{ type: "bold" }] }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "segundo párrafo", + marks: [{ type: "italic" }], + }, + ], + }, + ], +}; + +describe("RichTextEditor", () => { + it("renders each paragraph and applies bold/italic styling per run", () => { + render(); + expect(screen.getByText("mundo").tagName).toBe("STRONG"); + expect(screen.getByText("segundo párrafo").tagName).toBe("EM"); + }); + + it("variant=embedded renders the formatted content without the panel/toolbar chrome", () => { + render(); + expect(screen.getByText("mundo").tagName).toBe("STRONG"); + expect( + screen.queryByTestId("rich-text-editor-panel"), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId("rich-text-editor-toolbar"), + ).not.toBeInTheDocument(); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("renders underline and highlight runs with and ", () => { + const marksDoc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "subrayado", + marks: [{ type: "underline" }], + }, + { + type: "text", + text: "resaltado", + marks: [ + { + type: "highlight", + attrs: { color: "category.yellow-light" }, + }, + ], + }, + ], + }, + ], + }; + render(); + expect(screen.getByText("subrayado").tagName).toBe("U"); + expect(screen.getByText("resaltado").tagName).toBe("MARK"); + }); + + it("renders the title and calls onTitleChange when edited", () => { + const onTitleChange = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByRole("button", { name: /editar título/i })); + const input = screen.getByDisplayValue("Resumen 10/04/2025"); + fireEvent.change(input, { target: { value: "Nuevo título" } }); + fireEvent.blur(input); + expect(onTitleChange).toHaveBeenCalledWith("Nuevo título"); + }); + + it("makes the body non-editable and hides the title's edit button in readOnly mode", () => { + render(); + expect( + screen.queryByRole("button", { name: /editar título/i }), + ).not.toBeInTheDocument(); + const body = screen.getByRole("textbox"); + expect(body).toHaveAttribute("contenteditable", "false"); + }); + + it("wraps the body in a bordered, rounded, shadowed card inside a gray panel", () => { + render(); + const panel = screen.getByTestId("rich-text-editor-panel"); + const card = screen.getByTestId("rich-text-editor-card"); + expect(panel).toContainElement(card); + expect(card).toContainElement(screen.getByRole("textbox")); + }); + + it("defaults the body card's max height to 532px", () => { + render(); + const card = screen.getByTestId("rich-text-editor-card"); + expect(card.style.maxHeight).toBe("532px"); + }); + + it("lets a consumer override the body card's max height via maxBodyHeight", () => { + render( + , + ); + const card = screen.getByTestId("rich-text-editor-card"); + expect(card.style.maxHeight).toBe("800px"); + }); + + it("renders a
fallback for a paragraph with no runs, instead of an empty

", () => { + const emptyParagraphDoc: JSONContent = { + type: "doc", + content: [{ type: "paragraph" }], + }; + const { container } = render( + , + ); + // ProseMirror (Tiptap's rendering engine) has no paragraph-id concept — + // it injects its own trailing
into an otherwise-empty text block so + // the caret can still be hosted there, on whichever

is empty. + const paragraphEl = container.querySelector("p"); + expect(paragraphEl?.querySelector("br")).toBeTruthy(); + }); + + it("does not call onChange when the document prop changes externally (not from user typing)", async () => { + const onChange = vi.fn(); + const { rerender } = render( + , + ); + expect(screen.getByText("mundo")).toBeInTheDocument(); + + const nextDoc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "otro resumen cargado externo" }], + }, + ], + }; + rerender(); + + // The external content sync is applied via a `useEffect`, so wait for it + // to flush before asserting. + await waitFor(() => + expect( + screen.getByText("otro resumen cargado externo"), + ).toBeInTheDocument(), + ); + expect(onChange).not.toHaveBeenCalled(); + }); +}); + +describe("RichTextEditor — toolbar", () => { + it("hides the formatting toolbar in readOnly mode (Copy remains)", () => { + render( + , + ); + expect( + screen.queryByRole("button", { name: /negrita/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /cursiva/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /subrayado/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /resaltar/i }), + ).not.toBeInTheDocument(); + // Copy is an export action, not an editing affordance — it stays visible. + expect(screen.getByRole("button", { name: /copiar/i })).toBeInTheDocument(); + }); + + it("renders formatting buttons in Bold/Italic/Underline/Highlight order, with a divider before Copy, right-aligned above the title", () => { + render( + , + ); + const buttons = screen.getAllByRole("button", { + name: /subrayado|cursiva|negrita|resaltar|copiar/i, + }); + expect(buttons.map((b) => b.getAttribute("aria-label"))).toEqual([ + "Negrita", + "Cursiva", + "Subrayado", + "Resaltar", + "Copiar", + ]); + expect( + screen.getByTestId("rich-text-editor-toolbar-divider"), + ).toBeInTheDocument(); + + const panel = screen.getByTestId("rich-text-editor-panel"); + const toolbar = screen.getByTestId("rich-text-editor-toolbar"); + const titleRow = mustExist( + screen + .getByText("Resumen") + .closest('[data-testid="rich-text-editor-title-row"]'), + "title row", + ); + const allChildren = Array.from(panel.querySelectorAll("[data-testid]")); + const toolbarIndex = allChildren.indexOf(toolbar); + const titleIndex = allChildren.indexOf(titleRow as Element); + expect(toolbarIndex).toBeLessThan(titleIndex); + }); + + it("toggles bold on the current selection and calls onChange", async () => { + const onChange = vi.fn(); + const singleRunDoc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "hola mundo" }], + }, + ], + }; + render(); + + const paragraphEl = mustExist( + screen.getByText("hola mundo").closest("p"), + "paragraph element", + ); + const range = document.createRange(); + range.selectNodeContents(paragraphEl); + // The Tiptap/ProseMirror view only reacts to a "selectionchange" event + // when its contentEditable root is the focused element (see + // prosemirror-view's `hasFocusAndSelection`) — a plain DOM Range/ + // Selection without focus is invisible to it. jsdom also dispatches + // "selectionchange" as a queued (async) task, so a microtask tick is + // needed before the editor's internal selection reflects it. + (screen.getByRole("textbox") as HTMLElement).focus(); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + fireEvent.select(paragraphEl); + await waitFor(() => { + expect(window.getSelection()?.isCollapsed).toBe(false); + }); + + fireEvent.click(screen.getByRole("button", { name: /negrita/i })); + + expect(onChange).toHaveBeenCalledWith({ + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "hola mundo", marks: [{ type: "bold" }] }, + ], + }, + ], + }); + }); + + it("toggles bold via the toolbar and reflects active state", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + const doc = { + type: "doc", + content: [ + { type: "paragraph", content: [{ type: "text", text: "Hello" }] }, + ], + }; + render(); + + const editorEl = screen.getByTestId("rich-text-editor-card"); + // Select "Hello" via a native selection so the toolbar acts on it. The + // contentEditable must be focused first — ProseMirror's view only reacts + // to a "selectionchange" event when its own DOM root has focus. + const textNode = mustExist( + mustExist(editorEl.querySelector("p"), "paragraph element").firstChild, + "paragraph text node", + ); + const range = document.createRange(); + range.selectNodeContents(textNode); + (screen.getByRole("textbox") as HTMLElement).focus(); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + + await user.click(screen.getByRole("button", { name: "Negrita" })); + + expect(handleChange).toHaveBeenCalled(); + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].content[0].marks).toEqual([{ type: "bold" }]); + expect(screen.getByRole("button", { name: "Negrita" })).toHaveAttribute( + "data-state", + "on", + ); + }); + + it("toggles italic via the toolbar and reflects active state", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + const doc = { + type: "doc", + content: [ + { type: "paragraph", content: [{ type: "text", text: "Hello" }] }, + ], + }; + render(); + + const editorEl = screen.getByTestId("rich-text-editor-card"); + const textNode = mustExist( + mustExist(editorEl.querySelector("p"), "paragraph element").firstChild, + "paragraph text node", + ); + const range = document.createRange(); + range.selectNodeContents(textNode); + (screen.getByRole("textbox") as HTMLElement).focus(); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + + await user.click(screen.getByRole("button", { name: "Cursiva" })); + + expect(handleChange).toHaveBeenCalled(); + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].content[0].marks).toEqual([{ type: "italic" }]); + expect(screen.getByRole("button", { name: "Cursiva" })).toHaveAttribute( + "data-state", + "on", + ); + }); + + it("toggles underline via the toolbar and reflects active state", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + const doc = { + type: "doc", + content: [ + { type: "paragraph", content: [{ type: "text", text: "Hello" }] }, + ], + }; + render(); + + const editorEl = screen.getByTestId("rich-text-editor-card"); + const textNode = mustExist( + mustExist(editorEl.querySelector("p"), "paragraph element").firstChild, + "paragraph text node", + ); + const range = document.createRange(); + range.selectNodeContents(textNode); + (screen.getByRole("textbox") as HTMLElement).focus(); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + + await user.click(screen.getByRole("button", { name: "Subrayado" })); + + expect(handleChange).toHaveBeenCalled(); + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].content[0].marks).toEqual([ + { type: "underline" }, + ]); + expect(screen.getByRole("button", { name: "Subrayado" })).toHaveAttribute( + "data-state", + "on", + ); + }); + + it("toggles a bullet list via the toolbar", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + const listDoc: JSONContent = { + type: "doc", + content: [ + { type: "paragraph", content: [{ type: "text", text: "Item" }] }, + ], + }; + render(); + + await user.click(screen.getByRole("button", { name: "Lista con viñetas" })); + + expect(handleChange).toHaveBeenCalled(); + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].type).toBe("bulletList"); + expect( + screen.getByRole("button", { name: "Lista con viñetas" }), + ).toHaveAttribute("data-state", "on"); + }); + + it("toggles an ordered list via the toolbar", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + const listDoc: JSONContent = { + type: "doc", + content: [ + { type: "paragraph", content: [{ type: "text", text: "Item" }] }, + ], + }; + render(); + + await user.click(screen.getByRole("button", { name: "Lista numerada" })); + + expect(handleChange).toHaveBeenCalled(); + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].type).toBe("orderedList"); + expect( + screen.getByRole("button", { name: "Lista numerada" }), + ).toHaveAttribute("data-state", "on"); + }); +}); + +describe("RichTextEditor — highlight + copy", () => { + it("renders exactly the 12 Figma-specified highlight swatches with human Spanish labels", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: /resaltar/i })); + + const expectedLabels = [ + "Azul claro", + "Azul", + "Verde claro", + "Verde", + "Naranja claro", + "Naranja", + "Rosa claro", + "Rosa", + "Rojo claro", + "Rojo", + "Amarillo claro", + "Amarillo", + ]; + for (const label of expectedLabels) { + expect( + screen.getByRole("button", { name: new RegExp(`^${label}$`, "i") }), + ).toBeInTheDocument(); + } + expect( + screen.queryByRole("button", { name: /violeta/i }), + ).not.toBeInTheDocument(); + }); + + it("applies the correct highlight color for a solid-shade swatch click", async () => { + const onChange = vi.fn(); + const singleRunDoc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "hola mundo" }], + }, + ], + }; + render(); + + const paragraphEl = mustExist( + screen.getByText("hola mundo").closest("p"), + "paragraph element", + ); + const range = document.createRange(); + range.selectNodeContents(paragraphEl); + // The contentEditable must be focused first — ProseMirror's view only + // reacts to a "selectionchange" event when its own DOM root has focus + // (see the "toggles bold via toolbar" test above for the same pattern). + (screen.getByRole("textbox") as HTMLElement).focus(); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + fireEvent.select(paragraphEl); + await waitFor(() => { + expect(window.getSelection()?.isCollapsed).toBe(false); + }); + + fireEvent.click(screen.getByRole("button", { name: /resaltar/i })); + fireEvent.click(screen.getByRole("button", { name: /^azul$/i })); + + expect(onChange).toHaveBeenCalledWith({ + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "hola mundo", + marks: [{ type: "highlight", attrs: { color: "#A2E2F9" } }], + }, + ], + }, + ], + }); + }); + + it("copies the serialized plain text to the clipboard", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + value: { writeText }, + configurable: true, + }); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /copiar/i })); + expect(writeText).toHaveBeenCalledWith("hola mundo"); + }); + + it("shows a border only on the swatch matching the current selection's highlight, and none by default", () => { + const onChange = vi.fn(); + const highlightDoc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "hola", + marks: [{ type: "highlight", attrs: { color: "#A2E2F9" } }], + }, + ], + }, + ], + }; + render(); + + const paragraphEl = mustExist( + screen.getByText("hola").closest("p"), + "paragraph element", + ); + const range = document.createRange(); + range.selectNodeContents(paragraphEl); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + fireEvent.select(paragraphEl); + + fireEvent.click(screen.getByRole("button", { name: /resaltar/i })); + const blueSwatch = screen.getByRole("button", { name: /^azul$/i }); + const greenSwatch = screen.getByRole("button", { name: /^verde$/i }); + + // jsdom's computed-style engine can't resolve a `border` shorthand whose + // value is a CSS custom property (our design tokens compile to + // `border: var(--aym-borders-primary-alt)`), so `toHaveStyle` always + // reports "none" here regardless of which class is applied — a jsdom/ + // cssstyle limitation, not a real bug. Asserting on the generated + // utility class name is the reliable way to check which swatch got the + // active border class within this test environment. + expect(blueSwatch.className).toMatch(/(?:^|\s)aym-bd_primary-alt(?:\s|$)/); + expect(greenSwatch.className).not.toMatch( + /(?:^|\s)aym-bd_primary-alt(?:\s|$)/, + ); + }); + + it("clicking the already-active swatch again removes the highlight and its border", async () => { + const onChange = vi.fn(); + const plainDoc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "hola" }], + }, + ], + }; + render(); + + const paragraphEl = mustExist( + screen.getByText("hola").closest("p"), + "paragraph element", + ); + const range = document.createRange(); + range.selectNodeContents(paragraphEl); + // The contentEditable must be focused first — ProseMirror's view only + // reacts to a "selectionchange" event when its own DOM root has focus + // (see the "toggles bold via toolbar" test above for the same pattern). + (screen.getByRole("textbox") as HTMLElement).focus(); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + fireEvent.select(paragraphEl); + await waitFor(() => { + expect(window.getSelection()?.isCollapsed).toBe(false); + }); + + fireEvent.click(screen.getByRole("button", { name: /resaltar/i })); + const blueSwatch = screen.getByRole("button", { name: /^azul$/i }); + + fireEvent.click(blueSwatch); + expect(blueSwatch.className).toMatch(/(?:^|\s)aym-bd_primary-alt(?:\s|$)/); + + fireEvent.click(blueSwatch); + expect(blueSwatch.className).not.toMatch( + /(?:^|\s)aym-bd_primary-alt(?:\s|$)/, + ); + }); + + it("copies to the clipboard in readOnly mode", () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + value: { writeText }, + configurable: true, + }); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /copiar/i })); + expect(writeText).toHaveBeenCalledWith("solo lectura"); + }); + + it("does not apply a highlight or show a swatch as active when clicked with no active selection", () => { + const onChange = vi.fn(); + render( + , + ); + + // No selection is ever made — open the popover and click a swatch cold. + fireEvent.click(screen.getByRole("button", { name: /resaltar/i })); + const blueSwatch = screen.getByRole("button", { name: /^azul$/i }); + fireEvent.click(blueSwatch); + + expect(onChange).not.toHaveBeenCalled(); + expect(blueSwatch.className).not.toMatch( + /(?:^|\s)aym-bd_primary-alt(?:\s|$)/, + ); + }); +}); + +// StarterKit bundles BulletList/OrderedList/ListItem/ListKeymap (backed by +// @tiptap/extension-list) with zero extra configuration. Verified empirically +// here rather than assumed: typing "- "/"1. " autoformats into real list +// nodes, Enter continues/exits the list, and — critically — the pre-Tiptap +// hand-rolled-paragraph bug where typing into the paragraph produced by +// exiting an empty list item landed in the *previous* paragraph instead +// (documented in an earlier plan's task-7-report.md) does not reproduce with +// real ProseMirror list nodes. +// +// Each scenario is driven as a single, uninterrupted `userEvent.type(...)` +// call starting from a fresh empty paragraph, rather than programmatically +// setting a DOM Range/Selection mid-sequence and then continuing to type. +// That's a deliberate choice, not a style preference: jsdom has no real +// layout engine, so once a re-render moves the selection, userEvent's own +// pointer-position-based caret tracking (which falls back to a stubbed +// `elementFromPoint`) can desync from the Range we set by hand, and the +// following keystrokes land in the wrong node. Typing the whole scenario in +// one call lets userEvent track the caret itself the whole way through, +// which is reliable in this environment; splitting it up is not. +describe("RichTextEditor — lists", () => { + it("autoformats '- ' into a real bullet list item while typing", async () => { + const handleChange = vi.fn(); + const doc: JSONContent = { type: "doc", content: [{ type: "paragraph" }] }; + render(); + const user = userEvent.setup(); + const editable = screen.getByRole("textbox"); + await user.click(editable); + await user.type(editable, "- Primer punto"); + + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].type).toBe("bulletList"); + expect(lastCall.content[0].content[0].type).toBe("listItem"); + expect(lastCall.content[0].content[0].content[0].content[0].text).toBe( + "Primer punto", + ); + }); + + it("autoformats '1. ' into a real ordered list item while typing", async () => { + const handleChange = vi.fn(); + const doc: JSONContent = { type: "doc", content: [{ type: "paragraph" }] }; + render(); + const user = userEvent.setup(); + const editable = screen.getByRole("textbox"); + await user.click(editable); + await user.type(editable, "1. Paso uno"); + + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].type).toBe("orderedList"); + expect(lastCall.content[0].content[0].type).toBe("listItem"); + expect(lastCall.content[0].content[0].content[0].content[0].text).toBe( + "Paso uno", + ); + }); + + it("continues an ordered list on Enter, adding a sibling item", async () => { + const handleChange = vi.fn(); + const doc: JSONContent = { type: "doc", content: [{ type: "paragraph" }] }; + render(); + const user = userEvent.setup(); + const editable = screen.getByRole("textbox"); + await user.click(editable); + await user.type(editable, "1. Paso uno{Enter}Paso dos"); + + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + const items = lastCall.content[0].content; + expect(lastCall.content[0].type).toBe("orderedList"); + expect(items).toHaveLength(2); + expect(items[0].content[0].content[0].text).toBe("Paso uno"); + expect(items[1].content[0].content[0].text).toBe("Paso dos"); + }); + + it("exits the list when Enter is pressed on an empty list item", async () => { + const handleChange = vi.fn(); + const doc: JSONContent = { type: "doc", content: [{ type: "paragraph" }] }; + render(); + const user = userEvent.setup(); + const editable = screen.getByRole("textbox"); + await user.click(editable); + // "- Primer punto" autoformats to a bullet item; the first Enter + // continues the list with a new empty item; the second Enter, pressed + // on that now-empty item, exits the list instead of adding another one. + await user.type(editable, "- Primer punto{Enter}{Enter}"); + + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect(lastCall.content[0].type).toBe("bulletList"); + expect(lastCall.content[0].content).toHaveLength(1); + expect(lastCall.content[0].content[0].content[0].content[0].text).toBe( + "Primer punto", + ); + expect(lastCall.content[1].type).toBe("paragraph"); + }); + + it("regression: typing into the paragraph produced by exiting an empty list item lands there, not in the previous item (task-7-report.md bug)", async () => { + const handleChange = vi.fn(); + const doc: JSONContent = { type: "doc", content: [{ type: "paragraph" }] }; + render(); + const user = userEvent.setup(); + const editable = screen.getByRole("textbox"); + await user.click(editable); + await user.type(editable, "- Primer punto{Enter}{Enter}texto"); + + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + // The bulletList item's own text must be untouched — this is exactly + // the failure mode from the pre-Tiptap bug, where the typed text was + // misrouted into the previous (list) paragraph instead of the new one. + expect(lastCall.content[0].content[0].content[0].content[0].text).toBe( + "Primer punto", + ); + const typedNode = lastCall.content.find( + (node: JSONContent) => node.type === "paragraph" && node.content, + ); + expect(typedNode?.content?.[0].text).toBe("texto"); + }); +}); + +// `EditableRecoveryBoundary` was carried over from the pre-Tiptap, +// hand-rolled-contentEditable implementation to catch a React +// "Failed to execute 'removeChild'" crash: React's fiber for a paragraph's +// child text/mark nodes drifting out of sync with the live DOM after a +// native contentEditable mutation (e.g. select-all-and-delete) touched those +// exact nodes directly. The Final whole-branch review flagged that this +// boundary's continued necessity was never empirically verified against the +// new Tiptap/ProseMirror-backed engine — only "we didn't touch it" was +// recorded. These tests do that verification. +// +// Empirical finding: the crash class is NOT reachable anymore, and this is +// provable structurally, not just by absence of a crash in these particular +// tests. `@tiptap/react`'s `EditorContent` (see `PureEditorContent.render` in +// `@tiptap/react/dist/index.js`) renders a bare `

` with NO +// `children` prop — React's fiber for that div has no child nodes to +// reconcile, ever. All of the actual editor DOM (paragraphs, marks, text) +// is appended into that div imperatively by ProseMirror's own view +// (`element.append(...editor.view.dom.parentNode.childNodes)` in +// `componentDidMount`/`componentDidUpdate`), entirely outside React's +// reconciliation. Directly mutating nodes inside `.ProseMirror` therefore +// cannot desync a React fiber from the live DOM the way it could in the old +// hand-rolled implementation, because React was never tracking those nodes +// as its own children to begin with — ProseMirror owns and re-diffs that +// subtree itself on every transaction. +describe("RichTextEditor — EditableRecoveryBoundary stress test", () => { + it("survives rapid successive userEvent.type() calls without crashing", async () => { + const handleChange = vi.fn(); + const doc: JSONContent = { type: "doc", content: [{ type: "paragraph" }] }; + render(); + const user = userEvent.setup(); + const editable = screen.getByRole("textbox"); + await user.click(editable); + + // Several back-to-back, uninterrupted typing bursts — the closest + // analogue to "rapid" native input a test environment can drive. + await user.type(editable, "primera ráfaga de texto"); + await user.type(editable, " segunda ráfaga"); + await user.type(editable, " tercera ráfaga final"); + + expect(screen.getByTestId("rich-text-editor-panel")).toBeInTheDocument(); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); + + it("does not crash (and the boundary never has to recover) when the ProseMirror-owned DOM is mutated directly, simulating the old native contentEditable desync bug", async () => { + const consoleError = vi + .spyOn(console, "error") + .mockImplementation(() => {}); + const handleChange = vi.fn(); + const doc: JSONContent = { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "hola mundo" }], + }, + ], + }; + render(); + + const proseMirrorRoot = screen.getByRole("textbox"); + expect(proseMirrorRoot.className).toMatch(/ProseMirror/); + const paragraphEl = mustExist( + proseMirrorRoot.querySelector("p"), + "paragraph element", + ); + + // This is exactly the class of native DOM mutation the boundary's + // comment describes: ripping out the exact text node the (in the old + // implementation) React fiber still expected to be there, bypassing + // React/ProseMirror's own transaction pipeline entirely. + while (paragraphEl.firstChild) { + paragraphEl.firstChild.remove(); + } + paragraphEl.textContent = ""; + + // Force a re-render of the surrounding React tree the way a consumer + // naturally would after such an edit (e.g. toggling readOnly, or any + // other prop change) — this is the moment the old implementation threw + // "Failed to execute 'removeChild': the node to be removed is not a + // child of this node" while reconciling. + const user = userEvent.setup(); + await user.click(screen.getByRole("button", { name: /negrita/i })); + await user.type(proseMirrorRoot, "nuevo texto"); + + // No crash reached `EditableRecoveryBoundary` — the panel and its + // children are still the original mounted instance, not a post-recovery + // remount (which would have reset the toolbar/body via the `key` bump). + expect(screen.getByTestId("rich-text-editor-panel")).toBeInTheDocument(); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + const removeChildErrors = consoleError.mock.calls.filter(([msg]) => + String(msg).includes("removeChild"), + ); + expect(removeChildErrors).toHaveLength(0); + + consoleError.mockRestore(); + }); +}); + +describe("RichTextEditor — table context menu", () => { + const tableDoc: JSONContent = { + type: "doc", + content: [ + { + type: "table", + content: [ + { + type: "tableRow", + content: [ + { + type: "tableHeader", + content: [ + { type: "paragraph", content: [{ type: "text", text: "A" }] }, + ], + }, + { + type: "tableHeader", + content: [ + { type: "paragraph", content: [{ type: "text", text: "B" }] }, + ], + }, + ], + }, + { + type: "tableRow", + content: [ + { + type: "tableCell", + content: [ + { type: "paragraph", content: [{ type: "text", text: "1" }] }, + ], + }, + { + type: "tableCell", + content: [ + { type: "paragraph", content: [{ type: "text", text: "2" }] }, + ], + }, + ], + }, + ], + }, + ], + }; + + /** Finds the / with the given text and puts the editor's real + * selection inside it — the same DOM-Range-based technique the bold/ + * italic toolbar tests already rely on to drive ProseMirror's own + * selection from a test. */ + function selectCell(editorEl: HTMLElement, cellText: string): HTMLElement { + const cell = mustExist( + Array.from(editorEl.querySelectorAll("td, th")).find( + (candidate) => candidate.textContent === cellText, + ) as HTMLElement | undefined, + `cell "${cellText}"`, + ); + const textNode = mustExist( + mustExist(cell.querySelector("p"), "cell paragraph").firstChild, + `cell "${cellText}" text node`, + ); + const range = document.createRange(); + range.selectNodeContents(textNode); + (screen.getByRole("textbox") as HTMLElement).focus(); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + return cell; + } + + it("shows row/column/table controls when right-clicking inside a table cell", async () => { + render(); + const editorEl = screen.getByTestId("rich-text-editor-card"); + const cell = selectCell(editorEl, "1"); + + fireEvent.contextMenu(cell); + + expect(await screen.findByText("Eliminar fila")).toBeInTheDocument(); + expect(screen.getByText("Agregar fila arriba")).toBeInTheDocument(); + expect(screen.getByText("Agregar fila abajo")).toBeInTheDocument(); + expect( + screen.getByText("Agregar columna a la izquierda"), + ).toBeInTheDocument(); + expect( + screen.getByText("Agregar columna a la derecha"), + ).toBeInTheDocument(); + expect(screen.getByText("Eliminar columna")).toBeInTheDocument(); + expect(screen.getByText("Eliminar tabla")).toBeInTheDocument(); + }); + + it("does not show table controls when right-clicking outside a table", () => { + const plainDoc: JSONContent = { + type: "doc", + content: [ + { type: "paragraph", content: [{ type: "text", text: "Hola" }] }, + ], + }; + render(); + fireEvent.contextMenu(screen.getByTestId("rich-text-editor-card")); + expect(screen.queryByText("Eliminar fila")).not.toBeInTheDocument(); + }); + + it("does not show table controls in readOnly mode", () => { + render(); + const editorEl = screen.getByTestId("rich-text-editor-card"); + fireEvent.contextMenu(mustExist(editorEl.querySelector("td"), "cell")); + expect(screen.queryByText("Eliminar fila")).not.toBeInTheDocument(); + }); + + it("deletes the current row via 'Eliminar fila'", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + render(); + const editorEl = screen.getByTestId("rich-text-editor-card"); + const cell = selectCell(editorEl, "1"); + fireEvent.contextMenu(cell); + + await user.click(await screen.findByText("Eliminar fila")); + + expect(handleChange).toHaveBeenCalled(); + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + // Only the header row is left. + expect(lastCall.content[0].content).toHaveLength(1); + }); + + it("deletes the whole table via 'Eliminar tabla'", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + render(); + const editorEl = screen.getByTestId("rich-text-editor-card"); + const cell = selectCell(editorEl, "1"); + fireEvent.contextMenu(cell); + + await user.click(await screen.findByText("Eliminar tabla")); + + expect(handleChange).toHaveBeenCalled(); + const lastCall = mustExist( + handleChange.mock.calls.at(-1), + "onChange call", + )[0]; + expect( + lastCall.content.some((node: JSONContent) => node.type === "table"), + ).toBe(false); + }); +}); diff --git a/src/components/rich-text-editor/RichTextEditor.tsx b/src/components/rich-text-editor/RichTextEditor.tsx new file mode 100644 index 0000000..61dd505 --- /dev/null +++ b/src/components/rich-text-editor/RichTextEditor.tsx @@ -0,0 +1,691 @@ +import { + CopyIcon, + HighlighterIcon, + ListBulletsIcon, + ListNumbersIcon, + PencilSimpleLineIcon, + TextBIcon, + TextItalicIcon, + TextUnderlineIcon, +} from "@phosphor-icons/react"; +import type { JSONContent } from "@tiptap/core"; +import Highlight from "@tiptap/extension-highlight"; +import { TableKit } from "@tiptap/extension-table"; +import { EditorContent, useEditor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { + Component, + type ReactElement, + type ReactNode, + useEffect, + useState, +} from "react"; +import { Button } from "@/components/button"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/context-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/popover"; +import { css, cx } from "@/styled/css"; +import { HStack, Stack } from "@/styled/jsx"; + +const titleRow = css({ + display: "flex", + alignItems: "center", + gap: "2", +}); + +const titleText = css({ textStyle: "title.md.strong", color: "text.default" }); + +const titleInput = css({ + textStyle: "title.md.strong", + color: "text.default", + border: "primary-alt", + rounded: "sm", + px: "2", + py: "1", + bg: "bg.secondary", +}); + +const body = css({ + textStyle: "paragraph.md.default", + color: "text.default", + outline: "none", + // `EditorContent`'s own className only reaches the wrapper div it renders + // — the actual contentEditable element is the nested `.ProseMirror` div + // ProseMirror creates itself, which browsers give a default focus outline + // (shows as a black ring while editing) unless targeted directly. + "& .ProseMirror": { outline: "none" }, + "& p": { margin: "0" }, + // Panda's preflight resets every heading to `font-size/font-weight: + // inherit` and every `ul`/`ol`/`menu` to `list-style: none` (so apps style + // typography explicitly instead of relying on UA defaults) — without + // overriding both here, markdown-parsed `# Heading`/`- item`/`1. item` + // text rendered visually identical to a plain paragraph, with no numbers + // or bullets at all. Sizes/markers are a reasonable default scale, not a + // re-verified Figma measurement. + "& h1, & h2, & h3, & h4, & h5, & h6": { + fontWeight: "[700]", + marginBottom: "2", + }, + "& h1": { fontSize: "[28px]" }, + "& h2": { fontSize: "[24px]" }, + "& h3": { fontSize: "[20px]" }, + "& h4, & h5, & h6": { fontSize: "[16px]" }, + "& ul": { listStyle: "[disc]", paddingLeft: "[1.25rem]", margin: "0" }, + "& ol": { listStyle: "[decimal]", paddingLeft: "[1.25rem]", margin: "0" }, + "& li + li": { marginTop: "4" }, + // Nested lists (an `- item` under a `1. item`) sit inside a parent
  • — + // the parent's own bottom margin (from the general sibling-spacing rule + // below) already separates it from the next top-level block, so it + // doesn't need one of its own before its first line of content. + "& li > ul, & li > ol": { marginTop: "1" }, + // General breathing room between any two top-level blocks (paragraph, + // heading, list, blockquote, table, hr) — not just "paragraph after + // paragraph" — so a heading or `---` isn't flush against whatever + // follows it. Headings get a bit more space above since they start a new + // section; the first block never gets a leading gap. + "& .ProseMirror > * + *": { marginTop: "4" }, + "& .ProseMirror > h1, & .ProseMirror > h2, & .ProseMirror > h3, & .ProseMirror > h4, & .ProseMirror > h5, & .ProseMirror > h6": + { marginTop: "8" }, + "& .ProseMirror > :first-child": { marginTop: "[0px]" }, + "& blockquote": { + borderLeft: "primary", + paddingLeft: "4", + color: "text.lighter", + fontStyle: "italic", + }, + "& hr": { marginY: "2" }, + "& table": { + width: "full", + }, + "& th, & td": { + border: "primary", + px: "3", + py: "2", + textAlign: "left", + }, + "& th": { + fontWeight: "[700]", + bg: "bg.secondary", + }, +}); + +// Figma (node 40002572:59897, "Main-Content"): pt:42px, px:48px, pb:32px — +// asymmetric, not the uniform p:8 (32px) this used to be. The extra +// horizontal padding is what keeps the card from stretching edge-to-edge. +const panel = css({ + display: "flex", + flexDirection: "column", + gap: "10", + bg: "bg.primary", + pt: "[42px]", + px: "12", + pb: "8", + height: "full", + minHeight: "[0]", + overflow: "hidden", +}); + +const card = css({ + bg: "bg.secondary", + border: "card", + rounded: "[12px]", + boxShadow: "card", + p: "8", + overflowY: "auto", + flex: "[1]", + minHeight: "[0]", +}); + +const toolbar = css({ justifyContent: "flex-end", pb: "3" }); + +// `variant="embedded"` skips the panel/toolbar/title chrome above entirely — +// the consumer supplies its own frame (border, shadow, fixed height) and +// just wants the formatted content inside it, scrollable within whatever +// height that frame gives it. Padding lives here (not on the consumer's +// frame) so it scrolls along with the content, like margins on a page. +const embeddedBody = css({ + height: "full", + overflowY: "auto", + p: "6", +}); + +const divider = css({ + w: "[1px]", + h: "6", + bg: "[#BCBAB8]", +}); + +// `ContextMenuTrigger asChild` needs a real DOM element to clone its own +// props (onContextMenu, ref) onto — EditorContent's rendered div already +// carries load-bearing classes/style/testid of its own (asserted on +// directly by existing tests), so it's wrapped in this element instead of +// being the trigger itself. `display: contents` keeps the wrapper invisible +// to layout, so it doesn't disturb EditorContent's own flex/sizing. +const tableMenuWrapper = css({ display: "contents" }); + +// Figma (node 40002573:72715): hover/active/pressed-open state for the +// format toggle buttons is a light-purple fill, not the default Button +// variants (which "none" deliberately opts out of) — bg.primary-alternative +// = #E5E8FF. +const formatButton = css({ + rounded: "[4px]", + "&:hover:enabled": { + bg: "bg.primary-alternative", + }, + "&:active:enabled": { + bg: "bg.primary-alternative", + }, + "&[data-state='open']": { + bg: "bg.primary-alternative", + }, + "&[data-state='on']": { + bg: "bg.primary-alternative", + }, +}); + +// Figma (node 40002573:62458): the title-edit pencil is action/alt-default +// (#3F479D), not the button's default onbutton-default color. +const editTitleButton = css({ + color: "action.alt-default", +}); + +// 6 hues x 2 shades (light + solid), matching Figma's swatch popover exactly +// (verified via get_variable_defs on the popover node — no violet, no +// single-shade-only hues). Order: light-then-solid per hue, hues in +// blue/green/orange/pink/red/yellow order — a stable default; the Figma +// screenshot's exact on-screen grid order could not be pixel-verified at the +// available render resolution. +// Real hex values, not `category.*` design-token paths — Tiptap's Highlight +// mark (multicolor mode) writes `attrs.color` straight into an inline +// `style="background-color: ..."` on the rendered `` (see +// @tiptap/extension-highlight's `renderHTML`), so it has to already be a +// value the browser can parse as a CSS color. A token path like +// "category.yellow-light" is invalid CSS — the browser silently drops it +// and every highlight fell back to the `` UA-stylesheet default +// (plain yellow) regardless of which swatch was picked. Values mirror +// `theme.semanticTokens.colors.category` in src/preset.ts. +export const RICH_TEXT_HIGHLIGHT_COLORS = [ + "#CBF1FF", // blue-light + "#A2E2F9", // blue + "#D1F4E2", // green-light + "#94FFC8", // green + "#FFE2C4", // orange-light + "#F7C693", // orange + "#FFD6FA", // pink-light + "#F8AEEF", // pink + "#FFE2D9", // red-light + "#F69FA6", // red + "#FFF2C6", // yellow-light + "#FDE27B", // yellow +]; + +// Human-readable Spanish names for the highlight swatches, so screen readers +// announce "Azul claro" instead of reading a raw hex value. +const HIGHLIGHT_COLOR_LABELS: Record = { + "#CBF1FF": "Azul claro", + "#A2E2F9": "Azul", + "#D1F4E2": "Verde claro", + "#94FFC8": "Verde", + "#FFE2C4": "Naranja claro", + "#F7C693": "Naranja", + "#FFD6FA": "Rosa claro", + "#F8AEEF": "Rosa", + "#FFE2D9": "Rojo claro", + "#F69FA6": "Rojo", + "#FFF2C6": "Amarillo claro", + "#FDE27B": "Amarillo", +}; + +// `bg` is deliberately not set via Panda's css() here: Panda's bracket +// escape (`[value]`) is resolved by static analysis at build time, so it +// can't express a value that's only known at runtime (one of 12 possible +// swatch colors, picked dynamically). The background is applied as a plain +// inline style instead — see the `style` prop where this is used. +const swatch = (active: boolean) => + css({ + w: "6", + h: "6", + rounded: "full", + border: active ? "primary-alt" : "none", + cursor: "pointer", + }); + +const swatchGrid = css({ + display: "grid", + gridTemplateColumns: "repeat(4, 1fr)", + gap: "2", + p: "3", +}); + +export interface RichTextEditorProps { + document: JSONContent; + onChange?: (next: JSONContent) => void; + readOnly?: boolean; + title?: string; + onTitleChange?: (next: string) => void; + highlightColors?: string[]; + "aria-label"?: string; + /** + * Maximum height of the scrollable body card, as a CSS length (e.g. + * "532px") or Panda token. Defaults to Figma's static mockup dimension + * ("532px") so existing behavior is unchanged unless a consumer opts into + * a different value — e.g. a taller real layout that has more vertical + * space available than the Figma mock did. Pass "none" to remove the cap + * entirely and let the card fill its flex parent instead (it already + * flex-grows and clips its own overflow) — the right choice whenever the + * consumer's layout gives this editor a real, non-static height to fill. + */ + maxBodyHeight?: string; + /** + * "panel" (default) — the full Validación screen chrome: toolbar + * (formatting controls + copy), optional title row, and the + * padded/bordered/shadowed card. + * + * "embedded" — just the formatted content, no toolbar, no title row, no + * card border/shadow of its own. For consumers that already have their own + * frame (e.g. the Resumen finish screen's preview panel) and only want the + * document's real formatting (headings, bold, lists…) inside it, instead + * of building a second, separate read-only renderer. + */ + variant?: "panel" | "embedded"; +} + +interface EditableRecoveryBoundaryProps { + onRecover: () => void; + children: ReactNode; +} + +/** + * A native contentEditable edit (e.g. selecting all text and deleting it) + * mutates the DOM directly — removing the exact //text nodes + * React's fiber still expects to be there. The next re-render then throws + * "Failed to execute 'removeChild': the node to be removed is not a child of + * this node" while trying to patch a subtree the browser already changed out + * from under it. There's no partial patch once fiber and the live DOM have + * drifted apart — only a full remount from the current `document` model + * recovers cleanly, so this boundary catches the crash and asks the parent + * to bump a `key` to force one. + * + * Empirically stress-tested against the Tiptap/ProseMirror-backed engine + * (see the "EditableRecoveryBoundary stress test" describe block in + * RichTextEditor.test.tsx, including a test that directly rips out DOM nodes + * inside `.ProseMirror` the same way the original bug did) and found to no + * longer be load-bearing: `@tiptap/react`'s `EditorContent` renders a bare + * `
    ` with no `children` prop, so React's fiber for it has nothing of + * the editor's internal DOM to reconcile — ProseMirror owns and re-diffs + * that subtree itself, entirely outside React. This boundary is kept as a + * low-probability defense-in-depth safety net, not because the new engine + * requires it. + */ +class EditableRecoveryBoundary extends Component< + EditableRecoveryBoundaryProps, + { hasError: boolean } +> { + state = { hasError: false }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + + componentDidCatch() { + this.props.onRecover(); + } + + render() { + if (this.state.hasError) return null; + return this.props.children; + } +} + +export function RichTextEditor({ + document: doc, + onChange, + readOnly = false, + title, + onTitleChange, + highlightColors = RICH_TEXT_HIGHLIGHT_COLORS, + "aria-label": ariaLabel, + maxBodyHeight = "532px", + variant = "panel", +}: RichTextEditorProps) { + const [editingTitle, setEditingTitle] = useState(false); + const [draftTitle, setDraftTitle] = useState(title ?? ""); + const [bodyEpoch, setBodyEpoch] = useState(0); + const [, forceToolbarUpdate] = useState(0); + + const commitTitle = () => { + setEditingTitle(false); + if (draftTitle !== title) onTitleChange?.(draftTitle); + }; + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ link: false }), + Highlight.configure({ multicolor: true }), + TableKit.configure({ table: { resizable: false } }), + ], + content: doc, + editable: !readOnly, + immediatelyRender: false, + editorProps: { + attributes: { + role: "textbox", + "aria-label": ariaLabel ?? "Resumen", + "aria-multiline": "true", + }, + }, + onUpdate: ({ editor: updatedEditor }) => { + onChange?.(updatedEditor.getJSON()); + }, + }); + + // `editable` can change after mount (e.g. a consumer flips readOnly), so + // it's kept in sync imperatively rather than only being read once at + // construction time. Guarded by an equality check because + // `Editor#setEditable` unconditionally emits a synthetic "update" event + // (see @tiptap/core's `setEditable`) — calling it on every render (e.g. + // the initial mount, where `editable` already matches `useEditor`'s + // config) would fire a spurious `onChange` with unchanged content. + useEffect(() => { + if (!editor || editor.isEditable === !readOnly) return; + editor.setEditable(!readOnly); + }, [editor, readOnly]); + + // The incoming `document` prop can also change from outside (e.g. loading + // a different summary). Only push it into the editor when it actually + // differs from the editor's own current content, so we don't clobber the + // user's in-progress typing on every render. `emitUpdate: false` is + // required here for the same reason the `editable`-sync effect above + // guards its own call — without it, `setContent` fires a synthetic + // "update" event and this externally-driven sync would immediately call + // `onChange` back with the very content it was just given, which can loop + // in a controlled-component usage (e.g. autosave wiring that writes + // `document` on every `onChange`). + useEffect(() => { + if (!editor) return; + const current = JSON.stringify(editor.getJSON()); + const next = JSON.stringify(doc); + if (current !== next) { + editor.commands.setContent(doc, { emitUpdate: false }); + } + }, [editor, doc]); + + // `editor.isActive(...)` reflects live editor state but isn't itself + // reactive React state, so the toolbar (which reads it during render via + // `editor?.isActive("bold")` etc.) won't re-render on its own when the + // selection moves into/out of a mark. Force a re-render on the events that + // can change that state. + useEffect(() => { + if (!editor) return; + const rerender = () => forceToolbarUpdate((n) => n + 1); + editor.on("selectionUpdate", rerender); + editor.on("transaction", rerender); + return () => { + editor.off("selectionUpdate", rerender); + editor.off("transaction", rerender); + }; + }, [editor]); + + // Right-click table controls, editable mode only — read-only previews + // (the Resumen finish screen, "embedded" variant) never mutate content, so + // the browser's native context menu is left alone there. + function withTableContextMenu(children: ReactElement) { + if (readOnly) return children; + return ( + + +
    {children}
    +
    + + editor?.chain().focus().addRowBefore().run()} + > + Agregar fila arriba + + editor?.chain().focus().addRowAfter().run()} + > + Agregar fila abajo + + editor?.chain().focus().deleteRow().run()} + > + Eliminar fila + + + editor?.chain().focus().addColumnBefore().run()} + > + Agregar columna a la izquierda + + editor?.chain().focus().addColumnAfter().run()} + > + Agregar columna a la derecha + + editor?.chain().focus().deleteColumn().run()} + > + Eliminar columna + + + editor?.chain().focus().deleteTable().run()} + > + Eliminar tabla + + +
    + ); + } + + if (variant === "embedded") { + return ( + setBodyEpoch((epoch) => epoch + 1)} + > + {withTableContextMenu( + , + )} + + ); + } + + return ( +
    + + {/* Copy stays available in both modes — the readOnly export preview + (Finalización) treats copy-to-clipboard as a core action. The + formatting controls below are editing affordances and stay hidden + when readOnly. */} + + {!readOnly && ( + <> + + + + + + + + + + +
    + {highlightColors.map((color) => { + const active = + editor?.isActive("highlight", { color }) ?? false; + return ( +
    +
    +
    +
    + + )} + + + + {title !== undefined && ( +
    + {editingTitle ? ( + setDraftTitle(e.target.value)} + onBlur={commitTitle} + onKeyDown={(e) => { + if (e.key === "Enter") commitTitle(); + }} + // biome-ignore lint/a11y/noAutofocus: replaces an inline click-to-edit label, not a dialog + autoFocus + /> + ) : ( + {title} + )} + {!readOnly && !editingTitle && ( + + )} +
    + )} + + setBodyEpoch((epoch) => epoch + 1)} + > + {withTableContextMenu( + , + )} + + +
    + ); +} + +export default RichTextEditor; diff --git a/src/components/rich-text-editor/index.ts b/src/components/rich-text-editor/index.ts new file mode 100644 index 0000000..a3e04be --- /dev/null +++ b/src/components/rich-text-editor/index.ts @@ -0,0 +1,5 @@ +export { + RICH_TEXT_HIGHLIGHT_COLORS, + RichTextEditor, + type RichTextEditorProps, +} from "./RichTextEditor"; diff --git a/src/components/search/Search.tsx b/src/components/search/Search.tsx index 614cb9d..6d7414f 100644 --- a/src/components/search/Search.tsx +++ b/src/components/search/Search.tsx @@ -1,4 +1,9 @@ -import { CaretDown, CaretUp, MagnifyingGlass, X } from "phosphor-react"; +import { + CaretDownIcon, + CaretUpIcon, + MagnifyingGlassIcon, + XIcon, +} from "@phosphor-icons/react"; import type { InputHTMLAttributes } from "react"; import { css, cx, sva } from "@/styled/css"; @@ -11,9 +16,7 @@ import { css, cx, sva } from "@/styled/css"; * bg.secondary = #FFFFFF (input background) * border/primary = 1px solid #BCBAB8 (Default border) * border/primary-alt = 1px solid #110041 (Focus border) - * [1px_solid_#9F99A5] = Field + Suggestion border (border/secondary in Figma; - * promote to preset as `border.tertiary` or rename - * existing `border.secondary` which is currently #EDF2F7) + * border/secondary = 1px solid #9F99A5 (Field + Suggestion border) * text.lighter = #625C68 (placeholder, result counter) * text.default = #110041 (typed text / suggestion text) * [#2D3748] = Suggestion typed+caret colour (promote as text.secondary) @@ -147,7 +150,7 @@ export function Search({ // Field + Suggestion: border/secondary in Figma (#9F99A5) (isField || isSuggestion) && css({ - border: "[1px_solid_#9F99A5]", + border: "secondary", }), // Suggestion: also gets the drop-shadow (same as Focus) isSuggestion && @@ -169,7 +172,7 @@ export function Search({ hasValue && css({ color: "text.default" }), )} > - +
    - + )} {resultCount && ( @@ -301,7 +304,7 @@ export function Search({ "&:disabled": { cursor: "not-allowed", opacity: "0.5" }, })} > - +
    )} diff --git a/src/components/select/Select.tsx b/src/components/select/Select.tsx index 3c306f5..6977f1c 100644 --- a/src/components/select/Select.tsx +++ b/src/components/select/Select.tsx @@ -1,9 +1,26 @@ +import { + CaretDownIcon, + CaretUpIcon, + CheckIcon, + XCircleIcon, +} from "@phosphor-icons/react"; import * as RadixSelect from "@radix-ui/react-select"; -import { CaretDown, CaretUp, Check } from "phosphor-react"; -import { type Ref, useId, useImperativeHandle } from "react"; +import { + type Ref, + useEffect, + useId, + useImperativeHandle, + useRef, + useState, +} from "react"; import { Suggestion } from "@/components/suggestion/Suggestion"; -import { sva } from "@/styled/css"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/tooltip/Tooltip"; +import { css, sva } from "@/styled/css"; import { styled } from "@/styled/jsx"; import { stack } from "@/styled/patterns"; @@ -21,7 +38,7 @@ const Affix = styled("span", { }, }); -export type SelectOption = { id: string; text: string }; +export type SelectOption = { id: string; text: string; description?: string }; export type SelectSuggestion = { id: string; text?: string }; export interface SelectProps { @@ -36,6 +53,8 @@ export interface SelectProps { placeholder?: string; disabled?: boolean; size?: "md" | "sm"; + /** Show a clear (×) control once an option is selected. Default: true. */ + clearable?: boolean; ref?: Ref<{ value: string | undefined }>; } @@ -47,6 +66,71 @@ function orderByPriority(options: SelectOption[], priority: string[] = []) { return [...preferred, ...filtered]; } +// Matches Radix Tooltip's own default hover delay. Kept separate from focus: +// Radix opens a tooltip instantly on focus (correct for keyboard users), but +// Select auto-focuses the current value when the list opens, which would +// otherwise pop its tooltip immediately with no hover involved. Driving +// `open` ourselves from pointer events only (ignoring focus) avoids that. +const OPTION_TOOLTIP_DELAY_MS = 700; + +function SelectItem({ + id, + text, + description, + itemClassName, + itemIndicatorClassName, +}: { + id: string; + text: string; + description?: string; + itemClassName?: string; + itemIndicatorClassName?: string; +}) { + const [tooltipOpen, setTooltipOpen] = useState(false); + const timeoutRef = useRef | undefined>( + undefined, + ); + + const item = ( + { + timeoutRef.current = setTimeout( + () => setTooltipOpen(true), + OPTION_TOOLTIP_DELAY_MS, + ); + } + : undefined + } + onPointerLeave={ + description + ? () => { + clearTimeout(timeoutRef.current); + setTooltipOpen(false); + } + : undefined + } + > + + + + {text} + + ); + + if (!description) return item; + + return ( + {}}> + {item} + {description} + + ); +} + function secureSuggestion( suggestion: SelectSuggestion | undefined, options: SelectOption[], @@ -152,12 +236,12 @@ const select = sva({ outline: "none", userSelect: "none", + // Only the hovered/keyboard-focused option is highlighted — the + // selected option is already marked by its check indicator, so it + // doesn't need to stay highlighted too once the list is open. "&[data-highlighted]": { bg: "bg.primary-alternative", }, - "&[data-state='checked']": { - bg: "bg.primary-alternative", - }, "&[data-disabled]": { cursor: "not-allowed", color: "text.lighter", @@ -202,6 +286,7 @@ export function Select({ placeholder = "", disabled = false, size = "md", + clearable = true, ref, }: SelectProps) { const triggerId = useId(); @@ -210,9 +295,28 @@ export function Select({ const orderedOptions = orderByPriority(options, priorityOrder); const securedSuggestion = secureSuggestion(suggestion, options); - useImperativeHandle(ref, () => ({ value }), [value]); + // Self-managed selection. Works both ways: + // - Controlled: parent passes `value` + `onChange` and re-renders (e.g. the + // Voz a Texto export format select). The effect keeps us in sync. + // - Uncontrolled/ref: consumers that only read the selection back through + // `ref` and never re-render on change (the dataset validation forms' + // register/useForm pattern). Here `value` is just the initial seed, so the + // component must own the selection or picking an option would revert. + // Seed with "" (never undefined) so Radix stays controlled throughout and + // doesn't emit an uncontrolled→controlled warning on first selection. + const [selectedValue, setSelectedValue] = useState(value ?? ""); + + useEffect(() => { + setSelectedValue(value ?? ""); + }, [value]); + + // Expose the live selection (not the initial `value`) so ref-based consumers + // capture user changes — useImperativeHandle re-runs when it changes, which is + // what re-fires the forms' registration callback ref. + useImperativeHandle(ref, () => ({ value: selectedValue }), [selectedValue]); const handleChange = (id: string) => { + setSelectedValue(id); const option = options.find((o) => o.id === id); if (option) onChange?.(option); }; @@ -234,7 +338,7 @@ export function Select({ {label && ( {label} @@ -242,7 +346,7 @@ export function Select({ )} @@ -252,7 +356,7 @@ export function Select({ {prefix && } - {!value && securedSuggestion ? ( + {!selectedValue && securedSuggestion ? ( + )} + {/* Caret sits at the trailing edge — matches Figma layout */} -