FIX: Tabla WIP

This commit is contained in:
Juan Felipe Zapata Moreno 2025-09-30 16:22:56 -06:00
parent b2095e4559
commit 1ce1fb30fc
10 changed files with 1575 additions and 653 deletions

View File

@ -1,10 +1,10 @@
VITE_API_URL=http://backend.holos.test:8080
VITE_BASE_URL=http://frontend.holos.test
VITE_API_URL=http://localhost:8080
VITE_BASE_URL=http://localhost:3000
VITE_REVERB_APP_ID=
VITE_REVERB_APP_KEY=
VITE_REVERB_APP_SECRET=
VITE_REVERB_HOST="backend.holos.test"
VITE_REVERB_HOST="localhost"
VITE_REVERB_PORT=8080
VITE_REVERB_SCHEME=http
VITE_REVERB_ACTIVE=false

683
package-lock.json generated
View File

@ -12,6 +12,10 @@
"@tailwindcss/postcss": "^4.0.9",
"@tailwindcss/vite": "^4.0.9",
"@tiptap/extension-color": "^3.5.2",
"@tiptap/extension-table": "^3.6.2",
"@tiptap/extension-table-cell": "^3.6.2",
"@tiptap/extension-table-header": "^3.6.2",
"@tiptap/extension-table-row": "^3.6.2",
"@tiptap/extension-text-align": "^3.5.2",
"@tiptap/extension-text-style": "^3.5.2",
"@tiptap/extension-underline": "^3.5.2",
@ -26,10 +30,10 @@
"@vuepic/vue-datepicker": "^11.0.2",
"apexcharts": "^5.3.5",
"axios": "^1.8.1",
"html-to-pdfmake": "^2.5.31",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"laravel-echo": "^2.0.2",
"luxon": "^3.5.0",
"pdfmake": "^0.2.20",
"pinia": "^3.0.1",
"pusher-js": "^8.4.0",
"tailwindcss": "^4.0",
@ -577,51 +581,6 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@foliojs-fork/fontkit": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz",
"integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==",
"license": "MIT",
"dependencies": {
"@foliojs-fork/restructure": "^2.0.2",
"brotli": "^1.2.0",
"clone": "^1.0.4",
"deep-equal": "^1.0.0",
"dfa": "^1.2.0",
"tiny-inflate": "^1.0.2",
"unicode-properties": "^1.2.2",
"unicode-trie": "^2.0.0"
}
},
"node_modules/@foliojs-fork/linebreak": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz",
"integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==",
"license": "MIT",
"dependencies": {
"base64-js": "1.3.1",
"unicode-trie": "^2.0.0"
}
},
"node_modules/@foliojs-fork/pdfkit": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/@foliojs-fork/pdfkit/-/pdfkit-0.15.3.tgz",
"integrity": "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw==",
"license": "MIT",
"dependencies": {
"@foliojs-fork/fontkit": "^1.9.2",
"@foliojs-fork/linebreak": "^1.1.1",
"crypto-js": "^4.2.0",
"jpeg-exif": "^1.1.4",
"png-js": "^1.0.0"
}
},
"node_modules/@foliojs-fork/restructure": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz",
"integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==",
"license": "MIT"
},
"node_modules/@grpc/grpc-js": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz",
@ -2278,16 +2237,16 @@
}
},
"node_modules/@tiptap/core": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.6.1.tgz",
"integrity": "sha512-Ascvlh0PmScOJyPxcgPOqFLSOruY/7JjPYdTyqShOIks1S2dr3w8A07omQCmeEHI0mX9L2eZyR2qXwFlyMdfEA==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.6.2.tgz",
"integrity": "sha512-XKZYrCVFsyQGF6dXQR73YR222l/76wkKfZ+2/4LCrem5qtcOarmv5pYxjUBG8mRuBPskTTBImSFTeQltJIUNCg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "^3.6.1"
"@tiptap/pm": "^3.6.2"
}
},
"node_modules/@tiptap/extension-blockquote": {
@ -2591,6 +2550,59 @@
"@tiptap/core": "^3.6.1"
}
},
"node_modules/@tiptap/extension-table": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.6.2.tgz",
"integrity": "sha512-ozRPpxTXrYABTU/zQq3JlytUUXvQDaEcl19YUR1mL/7Ctf4zRBvSnBHCuP/1Cu+4oHX4zdako/G++Z5qJxa65A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.6.2",
"@tiptap/pm": "^3.6.2"
}
},
"node_modules/@tiptap/extension-table-cell": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.6.2.tgz",
"integrity": "sha512-0mYEVy8YtHVRD781SA6pdQN3ICnRfUaVNwLLP8BJv32qLKUX4akK4Xtd+h3XQ5PY6uqBtiVKLiEfpuLeBVpvZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-table": "^3.6.2"
}
},
"node_modules/@tiptap/extension-table-header": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.6.2.tgz",
"integrity": "sha512-D9J0fzVBgZQQds8BQXb1dm02u9CLG90NGuLWz42kWoddViNmXkzZFJeUmsksGiQmM9s1Uqq87m3KpXcFgy8uvw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-table": "^3.6.2"
}
},
"node_modules/@tiptap/extension-table-row": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.6.2.tgz",
"integrity": "sha512-2PxDZ0DjopWUoxgP9BaMy7v86DMHB158KA/QktBt976zpLUiZ9JPLHezbSqADjt+vaWXesnjZk2iHw2Kgd+zFQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-table": "^3.6.2"
}
},
"node_modules/@tiptap/extension-text": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.6.1.tgz",
@ -2658,9 +2670,9 @@
}
},
"node_modules/@tiptap/pm": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.6.1.tgz",
"integrity": "sha512-4F3qKhLQYlHGfayO0dQD3hiX3VeSCkdoKQQ+gIegNhwX5IWboQleVMueo7rcKN8WarTkysbYnILcWgx5OYcKew==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.6.2.tgz",
"integrity": "sha512-g+NXjqjbj6NfHOMl22uNWVYIu8oCq7RFfbnpohPMsSKJLaHYE8mJR++7T6P5R9FoqhIFdwizg1jTpwRU5CHqXQ==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
@ -2799,6 +2811,19 @@
"undici-types": "~7.12.0"
}
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
@ -2826,6 +2851,13 @@
"integrity": "sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==",
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@univerjs-pro/collaboration": {
"version": "0.10.9",
"resolved": "https://registry.npmjs.org/@univerjs-pro/collaboration/-/collaboration-0.10.9.tgz",
@ -5286,11 +5318,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
"license": "MIT"
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.7",
@ -5341,15 +5377,6 @@
"node": ">=8"
}
},
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browserslist": {
"version": "4.26.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
@ -5391,24 +5418,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -5422,22 +5431,6 @@
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/camel-case": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
@ -5470,6 +5463,26 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@ -5538,15 +5551,6 @@
"node": ">=12"
}
},
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -5647,6 +5651,18 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-js": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@ -5668,6 +5684,16 @@
"tiny-invariant": "^1.0.6"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
@ -5744,60 +5770,6 @@
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"license": "MIT",
"dependencies": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -5822,12 +5794,6 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@ -5892,6 +5858,16 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
@ -6201,6 +6177,17 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@ -6228,6 +6215,12 @@
}
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -6352,15 +6345,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -6448,18 +6432,6 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -6552,22 +6524,18 @@
"node": ">=12"
}
},
"node_modules/html-to-pdfmake": {
"version": "2.5.31",
"resolved": "https://registry.npmjs.org/html-to-pdfmake/-/html-to-pdfmake-2.5.31.tgz",
"integrity": "sha512-JCal2XMt3G2ndah4XQ3/WWj7OSYD7j81bhZ9070SWU3rdnVNqoAEI/JTS5GnNKF7v55Vw0S5KSwvulPbErJPAA==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=0.10.0"
"node": ">=8.0.0"
}
},
"node_modules/immediate": {
@ -6576,37 +6544,11 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-date-object": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
"integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/is-extglob": {
"version": "2.1.1",
@ -6651,24 +6593,6 @@
"node": ">=0.12.0"
}
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
@ -6708,12 +6632,6 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jpeg-exif": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
"license": "MIT"
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@ -6739,6 +6657,32 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jspdf": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz",
"integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.9",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
@ -7323,31 +7267,6 @@
"node": ">=0.10.0"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/opentype.js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz",
@ -7423,27 +7342,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/pdfmake": {
"version": "0.2.20",
"resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.20.tgz",
"integrity": "sha512-bGbxbGFP5p8PWNT3Phsu1ZcRLnRfF6jmnuKTkgmt6i5PZzSdX6JaB+NeTz9q+aocfW8SE9GUjL3o/5GroBqGcQ==",
"license": "MIT",
"dependencies": {
"@foliojs-fork/linebreak": "^1.1.2",
"@foliojs-fork/pdfkit": "^0.15.3",
"iconv-lite": "^0.6.3",
"xmldoc": "^2.0.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -7483,11 +7394,6 @@
}
}
},
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@ -7841,6 +7747,16 @@
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
"license": "ISC"
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
@ -8193,6 +8109,13 @@
"@babel/runtime": "^7.9.2"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regexp-util": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/regexp-util/-/regexp-util-2.0.3.tgz",
@ -8202,26 +8125,6 @@
"node": ">=16"
}
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
"integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"define-properties": "^1.2.1",
"es-errors": "^1.3.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"set-function-name": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@ -8265,6 +8168,16 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
@ -8345,18 +8258,6 @@
"tslib": "^2.1.0"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC"
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@ -8367,38 +8268,6 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-function-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
@ -8478,6 +8347,16 @@
"node": ">=0.10.0"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -8524,6 +8403,16 @@
"node": ">=16"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
@ -8595,6 +8484,16 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@ -8753,16 +8652,6 @@
"license": "MIT",
"peer": true
},
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-regex": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/unicode-regex/-/unicode-regex-4.2.0.tgz",
@ -8775,22 +8664,6 @@
"node": ">=16"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/unicount": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unicount/-/unicount-1.1.0.tgz",
@ -8890,6 +8763,16 @@
}
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@ -9187,18 +9070,6 @@
}
}
},
"node_modules/xmldoc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.2.tgz",
"integrity": "sha512-UiRwoSStEXS3R+YE8OqYv3jebza8cBBAI2y8g3B15XFkn3SbEOyyLnmPHjLBPZANrPJKEzxxB7A3XwcLikQVlQ==",
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",

View File

@ -14,6 +14,10 @@
"@tailwindcss/postcss": "^4.0.9",
"@tailwindcss/vite": "^4.0.9",
"@tiptap/extension-color": "^3.5.2",
"@tiptap/extension-table": "^3.6.2",
"@tiptap/extension-table-cell": "^3.6.2",
"@tiptap/extension-table-header": "^3.6.2",
"@tiptap/extension-table-row": "^3.6.2",
"@tiptap/extension-text-align": "^3.5.2",
"@tiptap/extension-text-style": "^3.5.2",
"@tiptap/extension-underline": "^3.5.2",
@ -28,10 +32,10 @@
"@vuepic/vue-datepicker": "^11.0.2",
"apexcharts": "^5.3.5",
"axios": "^1.8.1",
"html-to-pdfmake": "^2.5.31",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"laravel-echo": "^2.0.2",
"luxon": "^3.5.0",
"pdfmake": "^0.2.20",
"pinia": "^3.0.1",
"pusher-js": "^8.4.0",
"tailwindcss": "^4.0",

View File

@ -15,6 +15,7 @@ const emit = defineEmits([
"move",
"editor-active",
"table-editing",
"edit-table",
]);
const isEditing = ref(false);
@ -31,6 +32,7 @@ const elementStyles = computed(() => ({
width: `${props.element.width || 200}px`,
height: `${props.element.height || 120}px`,
position: "absolute",
zIndex: isEditing.value ? 30 : props.isSelected ? 20 : 10,
}));
watch(
@ -54,11 +56,12 @@ const handleSelect = (event) => {
const startEditing = () => {
if (!props.isSelected) emit("select", props.element.id);
if (props.element.type === "text" || props.element.type === "table") {
if (props.element.type === "text") {
isEditing.value = true;
if (props.element.type === "table") {
emit("table-editing", true);
}
}
if (props.element.type === "table") {
// Emitir evento para abrir modal
emit("edit-table", props.element.id);
}
if (props.element.type === "image") {
fileInput.value.click();
@ -69,14 +72,6 @@ const handleContentUpdate = (newContent) => {
emit("update", { id: props.element.id, content: newContent });
};
const handleCellUpdate = (rowIndex, colIndex, value) => {
const updatedData = JSON.parse(JSON.stringify(props.element.content.data));
updatedData[rowIndex][colIndex] = value;
emit("update", {
id: props.element.id,
content: { data: updatedData },
});
};
const handleEditorFocus = (editor) => {
emit("editor-active", editor);
@ -178,27 +173,15 @@ const handleFileSelect = (event) => {
event.target.value = null;
};
const textStyles = computed(() => {
if (props.element.type !== "text") return {};
return {
width: "100%",
height: "100%",
overflow: "hidden",
position: "relative",
};
});
</script>
<template>
<div
ref="elementRef"
:style="elementStyles"
class="group select-none bg-white border border-gray-300 rounded"
class="group select-none bg-white border border-gray-300 rounded transition-shadow"
:class="{
'ring-2 ring-blue-500 border-blue-500 z-10': isSelected,
'z-20': isEditing,
'shadow-md': isSelected,
'ring-2 ring-blue-500 border-blue-500 shadow-md': isSelected,
'overflow-hidden': element.type === 'image',
}"
@click="handleSelect"
@ -212,8 +195,7 @@ const textStyles = computed(() => {
<div
v-if="element.type === 'text'"
:style="textStyles"
class="text-container"
class="w-full h-full overflow-hidden"
>
<TiptapEditor
class="w-full h-full overflow-auto"
@ -242,35 +224,18 @@ const textStyles = computed(() => {
<div
v-else-if="element.type === 'table'"
class="w-full h-full overflow-auto"
@mousedown.stop
class="w-full h-full overflow-auto bg-white p-2"
>
<table class="w-full h-full border-collapse">
<tbody>
<tr
v-for="(row, rowIndex) in element.content.data"
:key="rowIndex"
class="align-top"
>
<td
v-for="(cell, colIndex) in row"
:key="colIndex"
class="border p-0 text-sm relative"
>
<TiptapEditor
:key="`${element.id}-${rowIndex}-${colIndex}`"
:model-value="cell"
:editable="isEditing"
@update:model-value="
handleCellUpdate(rowIndex, colIndex, $event)
"
@focus="handleEditorFocus"
class="w-full h-full min-h-[2.5em]"
/>
</td>
</tr>
</tbody>
</table>
<div v-html="element.content" class="table-preview"></div>
<div
v-if="!element.content || element.content.includes('<!---->')"
class="flex items-center justify-center h-full text-gray-400"
>
<div class="text-center">
<GoogleIcon name="table_chart" class="text-4xl mb-2" />
<p class="text-sm">Doble clic para editar tabla</p>
</div>
</div>
</div>
<div
@ -299,3 +264,28 @@ const textStyles = computed(() => {
/>
</div>
</template>
<style scoped>
/* Estilos para vista previa de tabla */
:deep(.table-preview table) {
border-collapse: collapse;
width: 100%;
font-size: 0.875rem;
}
:deep(.table-preview td),
:deep(.table-preview th) {
border: 1px solid #d1d5db;
padding: 0.5rem;
text-align: left;
}
:deep(.table-preview th) {
background-color: #f3f4f6;
font-weight: 600;
}
:deep(.table-preview p) {
margin: 0;
}
</style>

View File

@ -2,33 +2,17 @@
import { ref } from 'vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Propiedades */
const props = defineProps({
type: {
type: String,
required: true,
},
icon: String,
title: String,
description: String,
tag: String
type: { type: String, required: true },
icon: { type: String, required: true },
title: { type: String, required: true },
});
/** Eventos */
const emit = defineEmits(['dragstart']);
/** Referencias */
const isDragging = ref(false);
/** Métodos */
const handleDragStart = (event) => {
isDragging.value = true;
event.dataTransfer.setData('text/plain', JSON.stringify({
type: props.type,
title: props.title,
tag: props.tag
}));
emit('dragstart', props.type);
event.dataTransfer.setData('text/plain', JSON.stringify({ type: props.type }));
};
const handleDragEnd = () => {
@ -58,9 +42,6 @@ const handleDragEnd = () => {
<div class="text-xs sm:text-sm font-medium text-gray-900">
{{ title }}
</div>
<div class="text-xs text-gray-500 truncate hidden sm:block">
{{ description }}
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,354 @@
<script setup>
import { useEditor, EditorContent } from "@tiptap/vue-3";
import { StarterKit } from "@tiptap/starter-kit";
import { Table } from "@tiptap/extension-table";
import { TableRow } from "@tiptap/extension-table-row";
import { TableCell } from "@tiptap/extension-table-cell";
import { TableHeader } from "@tiptap/extension-table-header";
import { Underline } from "@tiptap/extension-underline";
import { TextStyle } from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import { FontSize } from "tiptap-extension-font-size";
import { TextAlign } from "@tiptap/extension-text-align";
import { watch } from "vue";
import GoogleIcon from "@Shared/GoogleIcon.vue";
const props = defineProps({
modelValue: { type: String, default: "" },
editable: { type: Boolean, default: true },
});
const emit = defineEmits(["update:modelValue", "focus", "blur"]);
const editor = useEditor({
content: props.modelValue,
editable: props.editable,
extensions: [
StarterKit,
Underline,
TextStyle,
Color.configure({ types: ["textStyle"] }),
FontSize.configure({ types: ["textStyle"] }),
TextAlign.configure({ types: ["heading", "paragraph"] }),
Table.configure({
resizable: true,
HTMLAttributes: {
class: 'tiptap-table',
},
}),
TableRow,
TableHeader,
TableCell,
],
onUpdate: ({ editor }) => emit("update:modelValue", editor.getHTML()),
onFocus: ({ editor }) => emit("focus", editor),
onBlur: ({ editor }) => emit("blur", editor),
});
watch(
() => props.modelValue,
(newValue) => {
if (editor.value && editor.value.getHTML() !== newValue) {
editor.value.commands.setContent(newValue, false);
}
}
);
watch(
() => props.editable,
(isEditable) => {
editor.value?.setEditable(isEditable);
}
);
// Acciones de tabla
const insertTable = () => {
editor.value?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
};
const addRowBefore = () => {
editor.value?.chain().focus().addRowBefore().run();
};
const addRowAfter = () => {
editor.value?.chain().focus().addRowAfter().run();
};
const deleteRow = () => {
editor.value?.chain().focus().deleteRow().run();
};
const addColumnBefore = () => {
editor.value?.chain().focus().addColumnBefore().run();
};
const addColumnAfter = () => {
editor.value?.chain().focus().addColumnAfter().run();
};
const deleteColumn = () => {
editor.value?.chain().focus().deleteColumn().run();
};
const deleteTable = () => {
editor.value?.chain().focus().deleteTable().run();
};
const mergeCells = () => {
editor.value?.chain().focus().mergeCells().run();
};
const splitCell = () => {
editor.value?.chain().focus().splitCell().run();
};
const toggleHeaderRow = () => {
editor.value?.chain().focus().toggleHeaderRow().run();
};
const toggleHeaderColumn = () => {
editor.value?.chain().focus().toggleHeaderColumn().run();
};
defineExpose({ editor });
</script>
<template>
<div class="table-editor-wrapper">
<!-- Toolbar de tabla -->
<div v-if="editable && editor" class="table-toolbar">
<div class="toolbar-section">
<button
@click="insertTable"
class="toolbar-btn"
title="Insertar tabla"
>
<GoogleIcon name="table_chart" />
</button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-section">
<button
@click="addRowBefore"
class="toolbar-btn"
title="Insertar fila arriba"
>
<GoogleIcon name="table_rows" />
<span class="btn-label"></span>
</button>
<button
@click="addRowAfter"
class="toolbar-btn"
title="Insertar fila abajo"
>
<GoogleIcon name="table_rows" />
<span class="btn-label"></span>
</button>
<button
@click="deleteRow"
class="toolbar-btn toolbar-btn-danger"
title="Eliminar fila"
>
<GoogleIcon name="delete" />
<span class="btn-label">Fila</span>
</button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-section">
<button
@click="addColumnBefore"
class="toolbar-btn"
title="Insertar columna a la izquierda"
>
<GoogleIcon name="view_column" />
<span class="btn-label"></span>
</button>
<button
@click="addColumnAfter"
class="toolbar-btn"
title="Insertar columna a la derecha"
>
<GoogleIcon name="view_column" />
<span class="btn-label"></span>
</button>
<button
@click="deleteColumn"
class="toolbar-btn toolbar-btn-danger"
title="Eliminar columna"
>
<GoogleIcon name="delete" />
<span class="btn-label">Col</span>
</button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-section">
<button
@click="mergeCells"
class="toolbar-btn"
title="Combinar celdas"
>
<GoogleIcon name="call_merge" />
</button>
<button
@click="splitCell"
class="toolbar-btn"
title="Dividir celda"
>
<GoogleIcon name="call_split" />
</button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-section">
<button
@click="toggleHeaderRow"
class="toolbar-btn"
:class="{ 'is-active': editor.isActive('tableHeader') }"
title="Toggle fila de encabezado"
>
<GoogleIcon name="text_fields" />
<span class="btn-label">H</span>
</button>
<button
@click="deleteTable"
class="toolbar-btn toolbar-btn-danger"
title="Eliminar tabla"
>
<GoogleIcon name="delete_forever" />
</button>
</div>
</div>
<!-- Editor -->
<EditorContent :editor="editor" class="table-editor-content" />
</div>
</template>
<style scoped>
.table-editor-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-toolbar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
flex-wrap: wrap;
}
.toolbar-section {
display: flex;
align-items: center;
gap: 0.25rem;
}
.toolbar-divider {
width: 1px;
height: 1.5rem;
background: #d1d5db;
}
.toolbar-btn {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.5rem;
background: white;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.75rem;
color: #374151;
}
.toolbar-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.toolbar-btn.is-active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.toolbar-btn-danger:hover {
background: #fee2e2;
border-color: #ef4444;
color: #dc2626;
}
.btn-label {
font-size: 0.625rem;
font-weight: 500;
}
.table-editor-content {
flex: 1;
overflow: auto;
padding: 0.5rem;
}
:deep(.ProseMirror) {
outline: none;
width: 100%;
height: 100%;
}
/* Estilos para tablas Tiptap */
:deep(.tiptap-table) {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
}
:deep(.tiptap-table td),
:deep(.tiptap-table th) {
min-width: 1em;
border: 1px solid #d1d5db;
padding: 0.5rem;
vertical-align: top;
box-sizing: border-box;
position: relative;
}
:deep(.tiptap-table th) {
font-weight: bold;
text-align: left;
background-color: #f9fafb;
}
:deep(.tiptap-table .selectedCell) {
background-color: #dbeafe;
}
:deep(.tiptap-table .column-resize-handle) {
position: absolute;
right: -2px;
top: 0;
bottom: 0;
width: 4px;
background-color: #3b82f6;
pointer-events: none;
}
:deep(.ProseMirror p) {
margin: 0;
}
</style>

View File

@ -0,0 +1,512 @@
<script setup>
import { ref, onMounted } from "vue";
import { useEditor, EditorContent } from "@tiptap/vue-3";
import { StarterKit } from "@tiptap/starter-kit";
import { Table } from "@tiptap/extension-table";
import { TableRow } from "@tiptap/extension-table-row";
import { TableCell } from "@tiptap/extension-table-cell";
import { TableHeader } from "@tiptap/extension-table-header";
import { Underline } from "@tiptap/extension-underline";
import { TextStyle } from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import { FontSize } from "tiptap-extension-font-size";
import { TextAlign } from "@tiptap/extension-text-align";
import GoogleIcon from "@Shared/GoogleIcon.vue";
const props = defineProps({
modelValue: { type: String, required: true },
});
const emit = defineEmits(["save", "cancel"]);
const editor = useEditor({
content: props.modelValue,
editable: true,
extensions: [
StarterKit,
Underline,
TextStyle,
Color.configure({ types: ["textStyle"] }),
FontSize.configure({ types: ["textStyle"] }),
TextAlign.configure({ types: ["heading", "paragraph"] }),
Table.configure({
resizable: true,
HTMLAttributes: {
class: 'tiptap-table',
},
}),
TableRow,
TableHeader,
TableCell,
],
});
onMounted(() => {
// Focus en el editor al abrir
setTimeout(() => {
editor.value?.commands.focus();
}, 100);
});
const handleSave = () => {
emit("save", editor.value?.getHTML());
};
const handleCancel = () => {
emit("cancel");
};
// Acciones de tabla
const insertTable = () => {
editor.value?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
};
const addRowBefore = () => {
editor.value?.chain().focus().addRowBefore().run();
};
const addRowAfter = () => {
editor.value?.chain().focus().addRowAfter().run();
};
const deleteRow = () => {
editor.value?.chain().focus().deleteRow().run();
};
const addColumnBefore = () => {
editor.value?.chain().focus().addColumnBefore().run();
};
const addColumnAfter = () => {
editor.value?.chain().focus().addColumnAfter().run();
};
const deleteColumn = () => {
editor.value?.chain().focus().deleteColumn().run();
};
const deleteTable = () => {
if (confirm("¿Estás seguro de eliminar toda la tabla?")) {
editor.value?.chain().focus().deleteTable().run();
}
};
const mergeCells = () => {
editor.value?.chain().focus().mergeCells().run();
};
const splitCell = () => {
editor.value?.chain().focus().splitCell().run();
};
const toggleHeaderRow = () => {
editor.value?.chain().focus().toggleHeaderRow().run();
};
const toggleHeaderColumn = () => {
editor.value?.chain().focus().toggleHeaderColumn().run();
};
// Formato de texto
const toggleBold = () => {
editor.value?.chain().focus().toggleBold().run();
};
const toggleItalic = () => {
editor.value?.chain().focus().toggleItalic().run();
};
const toggleUnderline = () => {
editor.value?.chain().focus().toggleUnderline().run();
};
const setTextColor = (color) => {
editor.value?.chain().focus().setColor(color).run();
};
const fontSize = ref('12');
const FONT_SIZES = ['10', '12', '14', '16', '18', '20', '24'];
const changeFontSize = (size) => {
editor.value?.chain().focus().setFontSize(`${size}px`).run();
};
</script>
<template>
<div class="modal-overlay" @click.self="handleCancel">
<div class="modal-container">
<!-- Header -->
<div class="modal-header">
<div class="flex items-center gap-2">
<GoogleIcon name="table_chart" class="text-blue-600 text-2xl" />
<h2 class="text-xl font-semibold text-gray-900">Editor de Tabla</h2>
</div>
<button
@click="handleCancel"
class="close-btn"
title="Cerrar (ESC)"
>
<GoogleIcon name="close" />
</button>
</div>
<!-- Toolbar -->
<div class="toolbar">
<!-- Acciones de tabla -->
<div class="toolbar-section">
<span class="section-label">Tabla:</span>
<button @click="insertTable" class="toolbar-btn" title="Insertar tabla">
<GoogleIcon name="add" />
<span>Nueva</span>
</button>
<button @click="deleteTable" class="toolbar-btn danger" title="Eliminar tabla">
<GoogleIcon name="delete_forever" />
</button>
</div>
<div class="toolbar-divider"></div>
<!-- Filas -->
<div class="toolbar-section">
<span class="section-label">Filas:</span>
<button @click="addRowBefore" class="toolbar-btn" title="Insertar fila arriba">
<GoogleIcon name="arrow_upward" />
</button>
<button @click="addRowAfter" class="toolbar-btn" title="Insertar fila abajo">
<GoogleIcon name="arrow_downward" />
</button>
<button @click="deleteRow" class="toolbar-btn danger" title="Eliminar fila">
<GoogleIcon name="remove" />
</button>
</div>
<div class="toolbar-divider"></div>
<!-- Columnas -->
<div class="toolbar-section">
<span class="section-label">Columnas:</span>
<button @click="addColumnBefore" class="toolbar-btn" title="Insertar columna izquierda">
<GoogleIcon name="arrow_back" />
</button>
<button @click="addColumnAfter" class="toolbar-btn" title="Insertar columna derecha">
<GoogleIcon name="arrow_forward" />
</button>
<button @click="deleteColumn" class="toolbar-btn danger" title="Eliminar columna">
<GoogleIcon name="remove" />
</button>
</div>
<div class="toolbar-divider"></div>
<!-- Celdas -->
<div class="toolbar-section">
<span class="section-label">Celdas:</span>
<button @click="mergeCells" class="toolbar-btn" title="Combinar celdas">
<GoogleIcon name="call_merge" />
</button>
<button @click="splitCell" class="toolbar-btn" title="Dividir celda">
<GoogleIcon name="call_split" />
</button>
<button @click="toggleHeaderRow" class="toolbar-btn" title="Toggle header fila">
<GoogleIcon name="title" />
</button>
</div>
<div class="toolbar-divider"></div>
<!-- Formato de texto -->
<div class="toolbar-section">
<span class="section-label">Formato:</span>
<button
@click="toggleBold"
class="toolbar-btn"
:class="{ active: editor?.isActive('bold') }"
title="Negrita"
>
<GoogleIcon name="format_bold" />
</button>
<button
@click="toggleItalic"
class="toolbar-btn"
:class="{ active: editor?.isActive('italic') }"
title="Cursiva"
>
<GoogleIcon name="format_italic" />
</button>
<button
@click="toggleUnderline"
class="toolbar-btn"
:class="{ active: editor?.isActive('underline') }"
title="Subrayado"
>
<GoogleIcon name="format_underlined" />
</button>
<select
v-model="fontSize"
@change="changeFontSize(fontSize)"
class="font-size-select"
>
<option v-for="size in FONT_SIZES" :key="size" :value="size">
{{ size }}px
</option>
</select>
</div>
</div>
<!-- Editor Content -->
<div class="editor-wrapper">
<EditorContent :editor="editor" class="editor-content" />
</div>
<!-- Footer -->
<div class="modal-footer">
<div class="text-sm text-gray-500">
<GoogleIcon name="info" class="inline text-base" />
Usa <kbd>Tab</kbd> para navegar entre celdas
</div>
<div class="flex gap-2">
<button @click="handleCancel" class="btn btn-secondary">
Cancelar
</button>
<button @click="handleSave" class="btn btn-primary">
<GoogleIcon name="check" />
Guardar
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 1rem;
}
.modal-container {
background: white;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 90vw;
max-height: 90vh;
width: 1200px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.close-btn {
padding: 0.5rem;
border-radius: 0.375rem;
border: none;
background: transparent;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
}
.close-btn:hover {
background: #f3f4f6;
color: #111827;
}
.toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
overflow-x: auto;
flex-wrap: wrap;
}
.toolbar-section {
display: flex;
align-items: center;
gap: 0.375rem;
}
.section-label {
font-size: 0.75rem;
font-weight: 500;
color: #6b7280;
margin-right: 0.25rem;
}
.toolbar-btn {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 0.75rem;
background: white;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.875rem;
color: #374151;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.toolbar-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.toolbar-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.toolbar-btn.danger:hover {
background: #fee2e2;
border-color: #ef4444;
color: #dc2626;
}
.toolbar-divider {
width: 1px;
height: 1.5rem;
background: #d1d5db;
}
.font-size-select {
padding: 0.375rem 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.875rem;
background: white;
cursor: pointer;
}
.editor-wrapper {
flex: 1;
overflow: auto;
padding: 1.5rem;
background: #fafafa;
}
.editor-content {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1.5rem;
min-height: 400px;
}
.modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
background: #f9fafb;
}
kbd {
padding: 0.125rem 0.375rem;
background: #e5e7eb;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.75rem;
font-family: monospace;
}
.btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-secondary:hover {
background: #f3f4f6;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
/* Estilos del editor Tiptap */
:deep(.ProseMirror) {
outline: none;
min-height: 400px;
}
:deep(.tiptap-table) {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
}
:deep(.tiptap-table td),
:deep(.tiptap-table th) {
min-width: 1em;
border: 2px solid #d1d5db;
padding: 0.75rem;
vertical-align: top;
box-sizing: border-box;
position: relative;
}
:deep(.tiptap-table th) {
font-weight: bold;
text-align: left;
background-color: #f3f4f6;
}
:deep(.tiptap-table .selectedCell) {
background-color: #dbeafe;
}
:deep(.tiptap-table .column-resize-handle) {
position: absolute;
right: -2px;
top: 0;
bottom: 0;
width: 4px;
background-color: #3b82f6;
pointer-events: none;
}
:deep(.ProseMirror p) {
margin: 0;
}
</style>

View File

@ -7,7 +7,6 @@ const props = defineProps({
selectedElement: { type: Object, default: null },
isTableEditing: { type: Boolean, default: false },
});
const emit = defineEmits(["table-action"]);
const colorGrid = ref(null);
@ -15,9 +14,6 @@ const colorGrid = ref(null);
const isTextSelected = computed(
() => props.selectedElement?.type === "text" && props.editor
);
const isTableSelected = computed(
() => props.selectedElement?.type === "table" && props.isTableEditing
);
const FONT_SIZES = ["12", "14", "16", "20", "24", "30"];
const PRIMARY_COLORS = [
@ -125,8 +121,8 @@ const textActions = [
<div
class="w-full bg-white border-b px-4 py-2 shadow-sm h-14 flex items-center gap-2"
>
<template v-if="isTextSelected || isTableSelected">
<!-- Botones de formato de texto (disponibles para texto y tabla) -->
<template v-if="isTextSelected">
<!-- Botones de formato de texto -->
<template v-if="editor">
<button
v-for="item in textActions.filter((i) => !i.type)"
@ -180,38 +176,6 @@ const textActions = [
</div>
</div>
</template>
<!-- Controles específicos de tabla -->
<div v-if="isTableSelected" class="flex items-center gap-2 ml-auto" @mousedown.stop>
<button
type="button"
@click="emit('table-action', 'addRow')"
class="px-2 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-600"
>
+ Fila
</button>
<button
type="button"
@click="emit('table-action', 'delRow')"
class="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
>
- Fila
</button>
<button
type="button"
@click="emit('table-action', 'addCol')"
class="px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600"
>
+ Col
</button>
<button
type="button"
@click="emit('table-action', 'delCol')"
class="px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700"
>
- Col
</button>
</div>
</template>
<div v-else class="text-sm text-gray-400">

View File

@ -81,7 +81,7 @@ defineExpose({ editor });
/* Prevenir que el texto se desborde horizontalmente */
:deep(.ProseMirror p) {
margin: 0 0 0.5em 0;
margin: 0 0 0.25em 0;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;

View File

@ -1,25 +1,26 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
import { jsPDF } from "jspdf";
import GoogleIcon from "@Shared/GoogleIcon.vue";
import Draggable from "@Holos/PDF/Draggable.vue";
import CanvasElement from "@Holos/PDF/Canvas.vue";
import PDFViewport from "@Holos/PDF/PDFViewport.vue";
import TextFormatter from "@Holos/PDF/TextFormatter.vue";
import htmlToPdfMake from "html-to-pdfmake";
import TableEditorModal from "@Holos/PDF/TableEditorModal.vue";
import autoTable from "jspdf-autotable";
/** Estado Reactivo */
const pages = ref([{ id: 1, elements: [] }]);
const selectedElementId = ref(null);
const elementCounter = ref(0);
const documentTitle = ref("Documento sin título");
const currentPage = ref(1);
const pageIdCounter = ref(1);
const isExporting = ref(false);
const currentPageSize = ref("A4");
const elementRefs = ref({});
const activeEditor = ref(null);
const isTableEditing = ref(false);
const isTableModalOpen = ref(false);
const editingTableId = ref(null);
const allElements = computed(() =>
pages.value.flatMap((page) => page.elements)
@ -54,7 +55,7 @@ const handleDrop = (dropData) => {
};
const createNewElement = (data) => ({
id: `element-${++elementCounter.value}`,
id: `el-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type: data.type,
pageIndex: data.pageIndex,
x: data.x,
@ -66,13 +67,29 @@ const createNewElement = (data) => ({
const getDefaultContent = (type) => {
if (type === "text") return "<p>Escribe algo...</p>";
if (type === "table")
return {
data: [
["Encabezado 1", "Encabezado 2"],
["Celda 1", "Celda 2"],
],
};
if (type === "table") {
return `<table class="tiptap-table">
<thead>
<tr>
<th>Encabezado 1</th>
<th>Encabezado 2</th>
<th>Encabezado 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Celda 1</td>
<td>Celda 2</td>
<td>Celda 3</td>
</tr>
<tr>
<td>Celda 4</td>
<td>Celda 5</td>
<td>Celda 6</td>
</tr>
</tbody>
</table>`;
}
return null;
};
@ -101,10 +118,9 @@ const deleteElement = (elementId) => {
if (selectedElementId.value === elementId) selectedElementId.value = null;
};
const clearCanvas = () => {
if (confirm("¿Seguro?")) {
if (confirm("¿Deseas limpiar todo el documento?")) {
pages.value = [{ id: 1, elements: [] }];
selectedElementId.value = null;
elementCounter.value = 0;
pageIdCounter.value = 1;
currentPage.value = 1;
}
@ -114,6 +130,31 @@ const handleTableEditing = (editing) => {
isTableEditing.value = editing;
};
const openTableEditor = (tableId) => {
editingTableId.value = tableId;
isTableModalOpen.value = true;
};
const saveTableContent = (newContent) => {
if (editingTableId.value) {
const el = allElements.value.find((e) => e.id === editingTableId.value);
if (el) {
el.content = newContent;
}
}
closeTableEditor();
};
const closeTableEditor = () => {
isTableModalOpen.value = false;
editingTableId.value = null;
};
const editingTable = computed(() => {
if (!editingTableId.value) return null;
return allElements.value.find((e) => e.id === editingTableId.value);
});
const handlePageSizeChange = (sizeData) => {
currentPageSize.value = sizeData.size;
};
@ -126,101 +167,323 @@ const handleKeydown = (event) => {
onMounted(() => document.addEventListener("keydown", handleKeydown));
onBeforeUnmount(() => document.removeEventListener("keydown", handleKeydown));
const PDF_CANVAS_SIZES = {
A4: { width: 794, height: 1123 },
A3: { width: 1123, height: 1587 },
Tabloid: { width: 1056, height: 1632 },
// Tamaños de página en mm
const PDF_SIZES = {
A4: { width: 210, height: 297, canvasWidth: 794, canvasHeight: 1123 },
A3: { width: 297, height: 420, canvasWidth: 1123, canvasHeight: 1587 },
Tabloid: { width: 279.4, height: 431.8, canvasWidth: 1056, canvasHeight: 1632 },
};
/* Tamaños en puntos */
const PDF_POINTS = {
A4: { width: 580.00, height: 841.89 },
A3: { width: 841.89, height: 1190.55 },
Tabloid: { width: 792, height: 1224 },
// Función para convertir color hex/rgb a array RGB
const parseColor = (colorStr) => {
if (!colorStr) return null;
// Si es hex (#RRGGBB)
if (colorStr.startsWith('#')) {
const hex = colorStr.replace('#', '');
return [
parseInt(hex.substr(0, 2), 16),
parseInt(hex.substr(2, 2), 16),
parseInt(hex.substr(4, 2), 16)
];
}
// Si es rgb(r, g, b)
const match = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (match) {
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
}
return null;
};
// Función auxiliar para procesar texto HTML y extraer líneas con estilos
const parseHTMLContent = (html) => {
const temp = document.createElement('div');
temp.innerHTML = html;
const lines = [];
// Procesar cada párrafo
const paragraphs = temp.querySelectorAll('p');
if (paragraphs.length === 0) {
const text = temp.textContent.trim();
if (text) {
return [{
segments: [{ text, isBold: false, isItalic: false, color: null, fontSize: null }],
align: 'left',
isNewParagraph: false
}];
}
return [];
}
paragraphs.forEach((p, pIndex) => {
// Detectar alineación del párrafo
let align = 'left';
if (p.style.textAlign) {
align = p.style.textAlign;
} else if (p.getAttribute('style')) {
const styleMatch = p.getAttribute('style').match(/text-align:\s*(left|center|right)/);
if (styleMatch) {
align = styleMatch[1];
}
}
// Recolectar segmentos de texto con estilos (inline, sin dividir por saltos)
const segments = [];
const processNode = (node, parentStyles = {}) => {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
// Dividir por saltos de línea explícitos
const textLines = text.split('\n');
textLines.forEach((lineText, idx) => {
if (lineText.trim()) {
segments.push({
text: lineText,
isBold: parentStyles.isBold || false,
isItalic: parentStyles.isItalic || false,
color: parentStyles.color || null,
fontSize: parentStyles.fontSize || null,
forceBreak: idx > 0 // Si hay múltiples líneas, forzar salto después de cada una
});
}
});
} else if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
// Si es un <br>, forzar salto de línea
if (element.tagName === 'BR') {
if (segments.length > 0) {
segments[segments.length - 1].forceBreak = true;
}
return;
}
const currentStyles = {
isBold: parentStyles.isBold || element.tagName === 'STRONG' || element.tagName === 'B',
isItalic: parentStyles.isItalic || element.tagName === 'EM' || element.tagName === 'I',
color: parseColor(element.style.color) || parentStyles.color,
fontSize: (element.style.fontSize ? parseInt(element.style.fontSize) : null) || parentStyles.fontSize
};
Array.from(element.childNodes).forEach(child => processNode(child, currentStyles));
}
};
Array.from(p.childNodes).forEach(node => processNode(node));
// Agrupar segmentos en líneas solo si hay salto forzado
let currentLine = { segments: [], align, isNewParagraph: false };
segments.forEach((segment, idx) => {
currentLine.segments.push({
text: segment.text,
isBold: segment.isBold,
isItalic: segment.isItalic,
color: segment.color,
fontSize: segment.fontSize
});
// Si hay salto forzado o es el último segmento, cerrar línea
if (segment.forceBreak || idx === segments.length - 1) {
if (currentLine.segments.length > 0) {
lines.push(currentLine);
currentLine = { segments: [], align, isNewParagraph: false };
}
}
});
// Marcar nuevo párrafo
if (pIndex < paragraphs.length - 1 && lines.length > 0) {
lines[lines.length - 1].isNewParagraph = true;
}
});
return lines;
};
const exportPDF = () => {
isExporting.value = true;
try {
const content = [];
const pageConfig = PDF_SIZES[currentPageSize.value] || PDF_SIZES.A4;
// calcular scale entre canvas-units y pdf-points
const canvasSize = PDF_CANVAS_SIZES[currentPageSize.value] || PDF_CANVAS_SIZES.A4;
const pdfSize = PDF_POINTS[currentPageSize.value] || PDF_POINTS.A4;
const scale = pdfSize.width / canvasSize.width;
pages.value.forEach((page, pageIndex) => {
const pageContent = page.elements
.map((element) => {
// convertir posición y dimensiones usando 'scale'
const position = { x: element.x * scale, y: element.y * scale };
if (element.type === "text" && element.content) {
return {
stack: htmlToPdfMake(element.content),
width: (element.width || 250) * scale,
absolutePosition: position,
};
}
if (element.type === "image" && element.content) {
return {
image: element.content,
width: (element.width || 150) * scale,
absolutePosition: position,
};
}
if (element.type === "table" && element.content) {
const body = element.content.data.map((row) =>
row.map((cellContent) => {
return {
stack: htmlToPdfMake(cellContent || "<p></p>"),
border: [true, true, true, true],
};
})
);
const numCols = element.content.data[0]?.length || 1;
const colWidth = ((element.width || 400) * scale) / numCols;
const widths = Array(numCols).fill(colWidth);
return {
table: {
body: body,
widths: widths,
},
width: (element.width || 400) * scale,
absolutePosition: position,
layout: {
hLineWidth: () => 0.5,
vLineWidth: () => 0.5,
hLineColor: () => "#000000",
vLineColor: () => "#000000",
paddingLeft: () => 4,
paddingRight: () => 4,
paddingTop: () => 2,
paddingBottom: () => 2,
},
};
}
return null;
})
.filter(Boolean);
content.push(...pageContent);
if (pageIndex < pages.value.length - 1)
content.push({ text: "", pageBreak: "after" });
// Crear documento PDF
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: [pageConfig.width, pageConfig.height]
});
const docDefinition = {
content,
pageSize: currentPageSize.value.toUpperCase(),
pageMargins: [40, 40, 40, 40],
styles: {
p: {
margin: [0, 0, 0, 0],
},
},
};
// Factor de escala de canvas a PDF (mm)
const scaleX = pageConfig.width / pageConfig.canvasWidth;
const scaleY = pageConfig.height / pageConfig.canvasHeight;
window.pdfMake.createPdf(docDefinition).download(`${documentTitle.value || "documento"}.pdf`);
pages.value.forEach((page, pageIndex) => {
if (pageIndex > 0) {
pdf.addPage([pageConfig.width, pageConfig.height]);
}
page.elements.forEach((element) => {
const x = element.x * scaleX;
const y = element.y * scaleY;
const width = element.width * scaleX;
const height = element.height * scaleY;
if (element.type === 'text' && element.content) {
const lines = parseHTMLContent(element.content);
let currentY = y + 5;
const lineHeight = 4.5;
lines.forEach((line) => {
// Construir el texto completo de la línea combinando segmentos
let fullLineText = '';
let currentX = x + 1;
// Calcular posición inicial según alineación
if (line.align === 'center') {
currentX = x + (width / 2);
} else if (line.align === 'right') {
currentX = x + width - 1;
}
// Si hay múltiples segmentos con diferentes estilos, dibujar cada uno
if (line.segments.length === 1 && !line.segments[0].color && !line.segments[0].isBold && !line.segments[0].isItalic) {
// Caso simple: un solo segmento sin estilos especiales
const segment = line.segments[0];
pdf.setFontSize(segment.fontSize || 12);
pdf.setFont('helvetica', 'normal');
pdf.setTextColor(0, 0, 0);
const textLines = pdf.splitTextToSize(segment.text, width - 2);
textLines.forEach((txtLine) => {
pdf.text(txtLine, currentX, currentY, { align: line.align });
currentY += lineHeight;
});
} else {
// Caso complejo: múltiples segmentos o con estilos
// Primero, calcular el ancho total de la línea
let totalWidth = 0;
const segmentWidths = [];
line.segments.forEach((segment) => {
pdf.setFontSize(segment.fontSize || 12);
let fontStyle = 'normal';
if (segment.isBold && segment.isItalic) fontStyle = 'bolditalic';
else if (segment.isBold) fontStyle = 'bold';
else if (segment.isItalic) fontStyle = 'italic';
pdf.setFont('helvetica', fontStyle);
const segWidth = pdf.getTextWidth(segment.text);
segmentWidths.push(segWidth);
totalWidth += segWidth;
});
// Ajustar posición inicial según alineación
let startX = x + 1;
if (line.align === 'center') {
startX = x + (width / 2) - (totalWidth / 2);
} else if (line.align === 'right') {
startX = x + width - 1 - totalWidth;
}
let segmentX = startX;
// Dibujar cada segmento
line.segments.forEach((segment, segIdx) => {
pdf.setFontSize(segment.fontSize || 12);
let fontStyle = 'normal';
if (segment.isBold && segment.isItalic) fontStyle = 'bolditalic';
else if (segment.isBold) fontStyle = 'bold';
else if (segment.isItalic) fontStyle = 'italic';
pdf.setFont('helvetica', fontStyle);
if (segment.color) {
pdf.setTextColor(segment.color[0], segment.color[1], segment.color[2]);
} else {
pdf.setTextColor(0, 0, 0);
}
pdf.text(segment.text, segmentX, currentY, { align: 'left' });
segmentX += segmentWidths[segIdx];
});
currentY += lineHeight;
}
// Agregar espacio extra si es nuevo párrafo
if (line.isNewParagraph) {
currentY += 1.5;
}
});
// Resetear color y fuente
pdf.setTextColor(0, 0, 0);
pdf.setFont('helvetica', 'normal');
}
if (element.type === 'image' && element.content) {
try {
pdf.addImage(element.content, 'PNG', x, y, width, height);
} catch (e) {
console.warn('Error al agregar imagen:', e);
}
}
if (element.type === 'table' && element.content) {
// Parsear tabla HTML de Tiptap
const temp = document.createElement('div');
temp.innerHTML = element.content;
const table = temp.querySelector('table');
if (table) {
// Extraer headers
const headerCells = Array.from(table.querySelectorAll('thead th, thead td'));
const head = headerCells.length > 0
? [headerCells.map(th => th.textContent.trim())]
: null;
// Extraer filas del body
const bodyRows = Array.from(table.querySelectorAll('tbody tr'));
const body = bodyRows.map(row => {
const cells = Array.from(row.children);
return cells.map(cell => cell.textContent.trim());
});
// Usar autoTable para renderizar la tabla
autoTable(pdf, {
head: head,
body: body,
startY: y,
margin: { left: x },
tableWidth: width,
styles: {
fontSize: 10,
cellPadding: 2,
lineColor: [209, 213, 219],
lineWidth: 0.1,
},
headStyles: {
fillColor: [249, 250, 251],
textColor: [0, 0, 0],
fontStyle: 'bold',
},
bodyStyles: {
textColor: [0, 0, 0],
},
theme: 'grid',
tableLineColor: [209, 213, 219],
tableLineWidth: 0.1,
});
}
}
});
});
pdf.save(`${documentTitle.value || "documento"}.pdf`);
} catch (e) {
console.error("Error al exportar PDF:", e);
alert("Hubo un error al generar el PDF.");
@ -229,26 +492,6 @@ const exportPDF = () => {
}
};
const handleTableAction = (action) => {
if (!selectedElement.value || selectedElement.value.type !== "table") return;
const tableData = selectedElement.value.content.data;
const numCols = tableData[0]?.length || 1;
if (action === "addRow") tableData.push(Array(numCols).fill(""));
if (action === "addCol") tableData.forEach((row) => row.push(""));
if (action === "delRow" && tableData.length > 1) tableData.pop();
if (action === "delCol" && numCols > 1) tableData.forEach((row) => row.pop());
// Actualizar tamaño de la tabla (opcional)
const newWidth = Math.max(200, tableData[0].length * 100);
const newHeight = Math.max(60, tableData.length * 30);
updateElement({
id: selectedElement.value.id,
content: { data: tableData },
width: newWidth,
height: newHeight,
});
};
const availableElements = [
{ type: "text", icon: "text_fields", title: "Texto" },
{ type: "image", icon: "image", title: "Imagen" },
@ -308,7 +551,6 @@ const availableElements = [
:editor="activeEditor"
:selected-element="selectedElement"
:is-table-editing="isTableEditing"
@table-action="handleTableAction"
/>
<PDFViewport
:pages="pages"
@ -324,11 +566,6 @@ const availableElements = [
<CanvasElement
v-for="element in page.elements"
:key="element.id"
:ref="
(el) => {
if (el) elementRefs[element.id] = el;
}
"
:element="element"
:is-selected="selectedElementId === element.id"
:page-dimensions="dimensions"
@ -338,9 +575,18 @@ const availableElements = [
@move="moveElement"
@editor-active="(editor) => (activeEditor = editor)"
@table-editing="handleTableEditing"
@edit-table="openTableEditor"
/>
</template>
</PDFViewport>
</div>
<!-- Modal de edición de tabla -->
<TableEditorModal
v-if="isTableModalOpen && editingTable"
:model-value="editingTable.content"
@save="saveTableContent"
@cancel="closeTableEditor"
/>
</div>
</template>